├── .github └── workflows │ ├── build-deb.yml │ └── build-rpm.yml ├── .gitignore ├── .pybuild ├── cpython3_3.12_jottr │ └── .pydistutils.cfg └── cpython3_3.13_jottr │ └── .pydistutils.cfg ├── LICENSE ├── README.md ├── deb_dist ├── jottr-1.0 │ ├── .pc │ │ ├── .quilt_patches │ │ ├── .quilt_series │ │ ├── .version │ │ └── applied-patches │ ├── .pybuild │ │ ├── cpython3_3.12_jottr │ │ │ └── .pydistutils.cfg │ │ └── cpython3_3.13_jottr │ │ │ └── .pydistutils.cfg │ ├── PKG-INFO │ ├── README.md │ ├── debian │ │ ├── .debhelper │ │ │ └── generated │ │ │ │ └── python3-jottr │ │ │ │ ├── dh_installchangelogs.dch.trimmed │ │ │ │ └── installed-by-dh_installdocs │ │ ├── changelog │ │ ├── compat │ │ ├── control │ │ ├── files │ │ ├── python3-jottr.debhelper.log │ │ ├── python3-jottr.substvars │ │ ├── python3-jottr │ │ │ ├── DEBIAN │ │ │ │ ├── control │ │ │ │ └── md5sums │ │ │ └── usr │ │ │ │ ├── bin │ │ │ │ └── jottr │ │ │ │ └── share │ │ │ │ ├── applications │ │ │ │ └── jottr.desktop │ │ │ │ └── doc │ │ │ │ └── python3-jottr │ │ │ │ └── changelog.Debian.gz │ │ ├── rules │ │ ├── source │ │ │ └── format │ │ └── watch │ ├── packaging │ │ └── debian │ │ │ └── jottr.desktop │ ├── setup.cfg │ └── setup.py ├── jottr_1.0-1.debian.tar.xz ├── jottr_1.0-1.dsc ├── jottr_1.0-1_amd64.buildinfo ├── jottr_1.0-1_amd64.changes ├── jottr_1.0-1_source.buildinfo ├── jottr_1.0-1_source.changes └── jottr_1.0.orig.tar.gz ├── icons ├── about.svg ├── applications-system.svg ├── browser.svg ├── color-mode-invert-text.svg ├── document-open.svg ├── find.svg ├── focus-mode.svg ├── font.svg ├── format-text-color.svg ├── globe.svg ├── gtk-select-font.svg ├── help.svg ├── im-google.svg ├── insert-text-frame.svg ├── insert-text.svg ├── jottr.png ├── jottr_icon_128x128.png ├── jottr_icon_16x16.png ├── jottr_icon_256x256.png ├── jottr_icon_32x32.png ├── jottr_icon_48x48.png ├── jottr_icon_512x512.png ├── jottr_icon_64x64.png ├── larger.svg ├── menu.svg ├── new.svg ├── open.svg ├── preferences-desktop-display-randr.svg ├── preferences-desktop-font.svg ├── preferences-desktop-theme-applications.svg ├── redo.svg ├── save-as.svg ├── save.svg ├── smaller.svg ├── snippets.svg ├── theme.svg ├── undo.svg ├── view-fullscreen-symbolic.svg ├── zoom-in.svg ├── zoom-out.svg └── zoom-reset.svg ├── io.github.mfat.jottr.desktop ├── io.github.mfat.jottr.metainfo.xml ├── io.github.mfat.jottr.yml ├── jottr-1.0.tar.gz ├── jottr.sh ├── packaging ├── debian │ ├── build.sh │ ├── changelog │ ├── control │ ├── copyright │ ├── debhelper-build-stamp │ ├── files │ ├── install │ ├── jottr.desktop │ ├── jottr.substvars │ ├── jottr │ │ ├── DEBIAN │ │ │ ├── control │ │ │ └── md5sums │ │ └── usr │ │ │ ├── bin │ │ │ └── jottr │ │ │ └── share │ │ │ ├── applications │ │ │ └── jottr.desktop │ │ │ ├── icons │ │ │ └── hicolor │ │ │ │ └── 128x128 │ │ │ │ └── apps │ │ │ │ └── jottr.png │ │ │ └── jottr │ │ │ ├── __init__.py │ │ │ ├── editor_tab.py │ │ │ ├── feed_manager_dialog.py │ │ │ ├── icons │ │ │ └── jottr.png │ │ │ ├── main.py │ │ │ ├── rss_reader.py │ │ │ ├── rss_tab.py │ │ │ ├── settings_dialog.py │ │ │ ├── settings_manager.py │ │ │ ├── snippet_editor_dialog.py │ │ │ ├── snippet_manager.py │ │ │ └── theme_manager.py │ ├── rules │ └── test-package.sh └── rpm │ └── build.sh ├── pypi-dependencies.yaml ├── requirements.txt ├── rpm.spec ├── rss_feeds.json ├── runtime-requirements.txt ├── screenshots └── main.png ├── setup.py ├── snippets.json └── src ├── jottr.egg-info ├── PKG-INFO ├── SOURCES.txt ├── dependency_links.txt └── top_level.txt └── jottr ├── __init__.py ├── editor_tab.py ├── feed_manager_dialog.py ├── help └── help.md ├── icons └── jottr.png ├── jottr-mac.spec ├── jottr-windows.spec ├── jottr_icon.icns ├── main.py ├── rss_reader.py ├── rss_tab.py ├── settings_dialog.py ├── settings_manager.py ├── snippet_editor_dialog.py ├── snippet_manager.py └── theme_manager.py /.github/workflows/build-deb.yml: -------------------------------------------------------------------------------- 1 | name: Build Debian Package 2 | 3 | on: 4 | push: 5 | tags: [ 'v*' ] 6 | workflow_dispatch: 7 | 8 | # Add these permission settings 9 | permissions: 10 | contents: write 11 | packages: write 12 | 13 | jobs: 14 | build-deb: 15 | runs-on: ubuntu-latest 16 | container: ubuntu:20.04 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Install build dependencies 22 | run: | 23 | apt-get update 24 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 25 | devscripts \ 26 | debhelper \ 27 | dh-python \ 28 | python3-all \ 29 | python3-setuptools \ 30 | python3-pyqt5 \ 31 | python3-pyqt5.qtwebengine \ 32 | python3-feedparser \ 33 | python3-enchant \ 34 | python3-pyqt5.qtsvg \ 35 | python3-pip \ 36 | unzip \ 37 | git \ 38 | build-essential 39 | 40 | # Add step to download pyspellchecker 41 | - name: Download pyspellchecker 42 | run: | 43 | mkdir -p src/jottr/vendor/ 44 | pip3 download pyspellchecker --no-deps -d src/jottr/vendor/ 45 | cd src/jottr/vendor/ 46 | unzip pyspellchecker*.whl 47 | rm -rf pyspellchecker*.whl *.dist-info/ 48 | 49 | - name: Create debian directory link 50 | run: | 51 | rm -rf debian 52 | ln -s packaging/debian debian 53 | 54 | - name: Update changelog date 55 | run: | 56 | sed -i "s/\$(LC_ALL=C date -R)/$(LC_ALL=C date -R)/" debian/changelog 57 | 58 | - name: Build package 59 | run: | 60 | dpkg-buildpackage -us -uc -b -d 61 | mkdir -p artifacts 62 | mv ../*.deb artifacts/ 63 | 64 | - name: List built packages 65 | run: ls -l artifacts/*.deb 66 | 67 | - name: Upload artifacts 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: debian-packages 71 | path: artifacts/*.deb 72 | 73 | - name: Create Release 74 | if: startsWith(github.ref, 'refs/tags/') 75 | uses: softprops/action-gh-release@v1 76 | with: 77 | files: artifacts/*.deb 78 | name: Release ${{ github.ref_name }} 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/build-rpm.yml: -------------------------------------------------------------------------------- 1 | name: Build RPM Package 2 | 3 | on: 4 | push: 5 | tags: [ 'v*' ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-rpm: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: fedora:latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Install build dependencies 18 | run: | 19 | dnf install -y \ 20 | rpm-build \ 21 | rpm-devel \ 22 | rpmlint \ 23 | python3-devel \ 24 | python3-setuptools \ 25 | python3-qt5 \ 26 | python3-qt5-webengine \ 27 | python3-feedparser \ 28 | python3-pyspellchecker \ 29 | python3-enchant \ 30 | rpmdevtools \ 31 | gcc \ 32 | make 33 | 34 | - name: Set up RPM build environment 35 | run: | 36 | mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} 37 | cp rpm.spec ~/rpmbuild/SPECS/ 38 | 39 | - name: Get version from spec 40 | id: get_version 41 | run: | 42 | VERSION=$(grep "Version:" rpm.spec | awk '{print $2}') 43 | echo "version=$VERSION" >> $GITHUB_OUTPUT 44 | 45 | - name: Create source tarball 46 | run: | 47 | mkdir -p /tmp/jottr-${{ steps.get_version.outputs.version }} 48 | cp -r src LICENSE README.md icons /tmp/jottr-${{ steps.get_version.outputs.version }}/ 49 | cd /tmp 50 | tar czf ~/rpmbuild/SOURCES/jottr-${{ steps.get_version.outputs.version }}.tar.gz jottr-${{ steps.get_version.outputs.version }}/ 51 | 52 | - name: Build RPM package 53 | run: | 54 | cd ~/rpmbuild/SPECS 55 | rpmbuild -ba rpm.spec 56 | 57 | - name: Run rpmlint 58 | run: | 59 | rpmlint ~/rpmbuild/RPMS/noarch/jottr-*.rpm 60 | 61 | - name: Test RPM installation 62 | run: | 63 | dnf install -y ~/rpmbuild/RPMS/noarch/jottr-*.rpm 64 | 65 | - name: Upload RPM artifacts 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: rpm-packages 69 | path: | 70 | ~/rpmbuild/RPMS/noarch/jottr-*.rpm 71 | ~/rpmbuild/SRPMS/jottr-*.rpm 72 | 73 | - name: Create Release 74 | if: startsWith(github.ref, 'refs/tags/') 75 | uses: softprops/action-gh-release@v1 76 | with: 77 | files: | 78 | ~/rpmbuild/RPMS/noarch/jottr-*.rpm 79 | ~/rpmbuild/SRPMS/jottr-*.rpm 80 | env: 81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build directories 2 | dist/ 3 | build/ 4 | __pycache__/ 5 | *.pyc 6 | 7 | # Package build artifacts 8 | packaging/debian/jottr_*/ 9 | packaging/rpm/BUILD/ 10 | packaging/rpm/BUILDROOT/ 11 | packaging/rpm/RPMS/ 12 | packaging/rpm/SOURCES/ 13 | packaging/rpm/SRPMS/ 14 | 15 | .history/ 16 | 17 | src/jottr/Jottr.spec 18 | src/.DS_Store 19 | .DS_Store 20 | /.flatpak-builder 21 | /repo 22 | io.github.mfat.jottr-1.4.1.flatpak 23 | /deb_dist 24 | /deb_dist 25 | -------------------------------------------------------------------------------- /.pybuild/cpython3_3.12_jottr/.pydistutils.cfg: -------------------------------------------------------------------------------- 1 | [clean] 2 | all=1 3 | [build] 4 | build_lib=/home/mahdi/GitHub/jottr/.pybuild/cpython3_3.12_jottr/build 5 | [install] 6 | force=1 7 | install_layout=deb 8 | install_scripts=$base/bin 9 | install_lib=/usr/lib/python3.12/dist-packages 10 | prefix=/usr 11 | -------------------------------------------------------------------------------- /.pybuild/cpython3_3.13_jottr/.pydistutils.cfg: -------------------------------------------------------------------------------- 1 | [clean] 2 | all=1 3 | [build] 4 | build_lib=/home/mahdi/GitHub/jottr/.pybuild/cpython3_3.13_jottr/build 5 | [install] 6 | force=1 7 | install_layout=deb 8 | install_scripts=$base/bin 9 | install_lib=/usr/lib/python3.13/dist-packages 10 | prefix=/usr 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | [... Rest of GPLv3 license text omitted for brevity ...] 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Jottr 3 | 4 | 5 | Get it on Flathub 6 | 7 | 8 | 9 | https://github.com/user-attachments/assets/d478920b-1d99-41e9-b37a-7c63d7384781 10 | 11 | 12 | 13 | 14 | 15 | ![image](https://github.com/user-attachments/assets/ee7b18fc-73cc-4f0b-a8bf-6508dd67defa) 16 | 17 | 18 | Jottr is a free, cross-platform, small and fast text editor released under GPL v3 license. 19 | 20 | It has cool features including: 21 | 22 | * An integrated web browser for quickly looking up selected text in various sources, including user-defined websites 23 | * A "Focus Mode", for distraction-free writing 24 | * Two methods to quickly inserting frequently used text blocks: add words to your dictionary or create text snippets and insert with custom keywords 25 | * Smart, intuitive autocompletion with tab key 26 | * Themes: choose between, light, dark and Sepia (paper-like) themes for the editor 27 | * Familiar, intuitive keyboard shortcuts (ctrl+= to make text bigger, for example) 28 | 29 | [Video overview](https://www.youtube.com/watch?v=P2nyr5V01SU) 30 | 31 | ### Download: 32 | Downloads for Linux, Mac and Windows are available from the [Releases](https://github.com/mfat/jottr/releases) section. 33 | 34 | If you use linux, you gan get jottr from [Flathub](https://flathub.org/apps/io.github.mfat.jottr) using the button below, or install the DEB or RPM packages. 35 | 36 | 37 | Get it on Flathub 38 | 39 | 40 | 41 | **Note for macOS users:** 42 | To run Jottr on Mac you need to install echant through [homebrew](https://brew.sh/). 43 | 44 | `brew install enchant` 45 | 46 | 47 | ### How to run Jottr from source: 48 | 49 | `git clone https://github.com/mfat/jottr` 50 | 51 | `cd jottr` 52 | 53 | `pip3 install -r requirements.txt` 54 | 55 | `cd src/jottr` 56 | 57 | `python3 main.py` 58 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/.pc/.quilt_patches: -------------------------------------------------------------------------------- 1 | debian/patches 2 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/.pc/.quilt_series: -------------------------------------------------------------------------------- 1 | series 2 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/.pc/.version: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/.pc/applied-patches: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/deb_dist/jottr-1.0/.pc/applied-patches -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/.pybuild/cpython3_3.12_jottr/.pydistutils.cfg: -------------------------------------------------------------------------------- 1 | [clean] 2 | all=1 3 | [build] 4 | build_lib=/home/mahdi/GitHub/jottr/deb_dist/jottr-1.0/.pybuild/cpython3_3.12_jottr/build 5 | [install] 6 | force=1 7 | install_layout=deb 8 | install_scripts=$base/bin 9 | install_lib=/usr/lib/python3.12/dist-packages 10 | prefix=/usr 11 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/.pybuild/cpython3_3.13_jottr/.pydistutils.cfg: -------------------------------------------------------------------------------- 1 | [clean] 2 | all=1 3 | [build] 4 | build_lib=/home/mahdi/GitHub/jottr/deb_dist/jottr-1.0/.pybuild/cpython3_3.13_jottr/build 5 | [install] 6 | force=1 7 | install_layout=deb 8 | install_scripts=$base/bin 9 | install_lib=/usr/lib/python3.13/dist-packages 10 | prefix=/usr 11 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: jottr 3 | Version: 1.0 4 | Summary: Modern text editor for writers and journalists 5 | Home-page: https://github.com/mfat/jottr 6 | Author: mFat 7 | Author-email: newmfat@gmail.com 8 | Classifier: Development Status :: 4 - Beta 9 | Classifier: Environment :: X11 Applications :: Qt 10 | Classifier: Intended Audience :: End Users/Desktop 11 | Classifier: License :: OSI Approved :: GPL-3.0+ 12 | Classifier: Operating System :: POSIX :: Linux 13 | Classifier: Programming Language :: Python :: 3 14 | Classifier: Topic :: Text Editors 15 | Requires-Dist: PyQt5>=5.15.0 16 | Requires-Dist: PyQtWebEngine>=5.15.0 17 | Requires-Dist: pyspellchecker>=0.7.2 18 | Requires-Dist: feedparser>=6.0.0 19 | Requires-Dist: requests==2.31.0 20 | Requires-Dist: pyinstaller>=4.5.1 21 | Requires-Dist: dmgbuild>=1.4.2 22 | 23 | Jottr is a feature-rich text editor designed specifically 24 | for writers and journalists, with features like smart 25 | completion, snippets, and integrated web browsing. 26 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/README.md: -------------------------------------------------------------------------------- 1 | # Jottr 2 | 3 | A modern, intelligent text editor designed for writers and journalists. Jottr combines powerful writing tools with smart features to enhance your writing workflow. 4 | 5 | 6 | ## Key Features 7 | 8 | ### Smart Writing Tools 9 | - **Intelligent Autocompletion**: Context-aware suggestions based on your personal dictionary 10 | - **Custom Dictionary**: Build your own vocabulary for better suggestions 11 | - **Text Snippets**: Save and reuse frequently used text blocks 12 | - **Word Count**: Real-time statistics for words, characters, and cursor position 13 | 14 | ### Research & Fact-Checking 15 | - **Integrated Browser**: Browse reference materials without leaving the editor 16 | - **Custom Search Contexts**: Right-click any text to Google-search across: 17 | - AP Newsroom 18 | - AP Pronto 19 | - Google News 20 | - Custom websites 21 | 22 | ### Focus on Writing 23 | - **Distraction-Free Mode**: Clean, minimal interface for focused writing 24 | - **Customizable Themes**: Light and dark modes plus a sepia theme 25 | - **Font Customization**: Choose your perfect writing font and size 26 | - **Zoom Controls**: Easily adjust text size with keyboard shortcuts 27 | 28 | ### Cross-Platform 29 | - Works on Windows, macOS, and Linux 30 | - Native look and feel on each platform 31 | - Consistent feature set across all systems 32 | 33 | 34 | ### How to run Jottr: 35 | `git clone https://github.com/mfat/jottr` 36 | 37 | `cd jottr` 38 | 39 | `pip3 install -r requirements.txt` 40 | 41 | `python3 main.py` 42 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/.debhelper/generated/python3-jottr/dh_installchangelogs.dch.trimmed: -------------------------------------------------------------------------------- 1 | jottr (1.0-1) unstable; urgency=low 2 | 3 | * source package automatically created by stdeb 0.10.0 4 | 5 | -- None Tue, 11 Feb 2025 03:47:36 +0330 6 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/.debhelper/generated/python3-jottr/installed-by-dh_installdocs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/deb_dist/jottr-1.0/debian/.debhelper/generated/python3-jottr/installed-by-dh_installdocs -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/changelog: -------------------------------------------------------------------------------- 1 | jottr (1.0-1) unstable; urgency=low 2 | 3 | * source package automatically created by stdeb 0.10.0 4 | 5 | -- None Tue, 11 Feb 2025 03:47:36 +0330 6 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/control: -------------------------------------------------------------------------------- 1 | Source: jottr 2 | Maintainer: None 3 | Section: python 4 | Priority: optional 5 | Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9) 6 | Standards-Version: 3.9.6 7 | Homepage: https://github.com/mfat/jottr 8 | 9 | Package: python3-jottr 10 | Architecture: all 11 | Depends: ${misc:Depends}, ${python3:Depends} 12 | Description: Modern text editor for writers and journalists 13 | Jottr is a feature-rich text editor designed specifically 14 | for writers and journalists, with features like smart 15 | completion, snippets, and integrated web browsing. 16 | 17 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/files: -------------------------------------------------------------------------------- 1 | jottr_1.0-1_amd64.buildinfo python optional 2 | python3-jottr_1.0-1_all.deb python optional 3 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/python3-jottr.debhelper.log: -------------------------------------------------------------------------------- 1 | dh_update_autotools_config 2 | dh_auto_configure 3 | dh_auto_build 4 | dh_auto_test 5 | dh_prep 6 | dh_auto_install 7 | dh_installdocs 8 | dh_installchangelogs 9 | dh_installinit 10 | dh_perl 11 | dh_link 12 | dh_strip_nondeterminism 13 | dh_compress 14 | dh_fixperms 15 | dh_missing 16 | dh_installdeb 17 | dh_gencontrol 18 | dh_md5sums 19 | dh_builddeb 20 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/python3-jottr.substvars: -------------------------------------------------------------------------------- 1 | python3:Depends=python3-feedparser, python3-pyqt5, python3-pyqt5.qtwebengine, python3-requests, python3:any 2 | misc:Depends= 3 | misc:Pre-Depends= 4 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/python3-jottr/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: python3-jottr 2 | Source: jottr 3 | Version: 1.0-1 4 | Architecture: all 5 | Maintainer: None 6 | Installed-Size: 20 7 | Depends: python3-feedparser, python3-pyqt5, python3-pyqt5.qtwebengine, python3-requests, python3:any 8 | Section: python 9 | Priority: optional 10 | Homepage: https://github.com/mfat/jottr 11 | Description: Modern text editor for writers and journalists 12 | Jottr is a feature-rich text editor designed specifically 13 | for writers and journalists, with features like smart 14 | completion, snippets, and integrated web browsing. 15 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/python3-jottr/DEBIAN/md5sums: -------------------------------------------------------------------------------- 1 | b2a3105eaf2089b3ace3e35795a8009f usr/bin/jottr 2 | fca5cf8df71eaca69cc8aa8d0106c951 usr/lib/python3/dist-packages/jottr-1.0.egg-info/PKG-INFO 3 | 68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/jottr-1.0.egg-info/dependency_links.txt 4 | 941fe216c4c83556b07d5f0380eca3f5 usr/lib/python3/dist-packages/jottr-1.0.egg-info/entry_points.txt 5 | 1fd97c8db252a6e722d4f13fd082c82d usr/lib/python3/dist-packages/jottr-1.0.egg-info/requires.txt 6 | 68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/jottr-1.0.egg-info/top_level.txt 7 | e0804e80bd59c9159d86b901ea1f1b98 usr/share/applications/jottr.desktop 8 | 2581a2f750c06efbe6021b26b01bc862 usr/share/doc/python3-jottr/changelog.Debian.gz 9 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/python3-jottr/usr/bin/jottr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # EASY-INSTALL-ENTRY-SCRIPT: 'jottr==1.0','console_scripts','jottr' 3 | import re 4 | import sys 5 | 6 | # for compatibility with easy_install; see #2198 7 | __requires__ = 'jottr==1.0' 8 | 9 | try: 10 | from importlib.metadata import distribution 11 | except ImportError: 12 | try: 13 | from importlib_metadata import distribution 14 | except ImportError: 15 | from pkg_resources import load_entry_point 16 | 17 | 18 | def importlib_load_entry_point(spec, group, name): 19 | dist_name, _, _ = spec.partition('==') 20 | matches = ( 21 | entry_point 22 | for entry_point in distribution(dist_name).entry_points 23 | if entry_point.group == group and entry_point.name == name 24 | ) 25 | return next(matches).load() 26 | 27 | 28 | globals().setdefault('load_entry_point', importlib_load_entry_point) 29 | 30 | 31 | if __name__ == '__main__': 32 | sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) 33 | sys.exit(load_entry_point('jottr==1.0', 'console_scripts', 'jottr')()) 34 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/python3-jottr/usr/share/applications/jottr.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Jottr 3 | Comment=Modern text editor for writers and journalists 4 | Exec=jottr 5 | Icon=jottr 6 | Terminal=false 7 | Type=Application 8 | Categories=Office;TextEditor; 9 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/python3-jottr/usr/share/doc/python3-jottr/changelog.Debian.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/deb_dist/jottr-1.0/debian/python3-jottr/usr/share/doc/python3-jottr/changelog.Debian.gz -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # This file was automatically generated by stdeb 0.10.0 at 4 | # Tue, 11 Feb 2025 03:47:36 +0330 5 | export PYBUILD_NAME=jottr 6 | %: 7 | dh $@ --with python3 --buildsystem=pybuild 8 | 9 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/debian/watch: -------------------------------------------------------------------------------- 1 | # please also check http://pypi.debian.net/jottr/watch 2 | version=3 3 | opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ 4 | http://pypi.debian.net/jottr/jottr-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/packaging/debian/jottr.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Jottr 3 | Comment=Modern text editor for writers and journalists 4 | Exec=jottr 5 | Icon=jottr 6 | Terminal=false 7 | Type=Application 8 | Categories=Office;TextEditor; 9 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | tag_date = 0 4 | 5 | -------------------------------------------------------------------------------- /deb_dist/jottr-1.0/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | # Default requirements if requirements.txt is not found 5 | default_requires = [ 6 | "PyQt5>=5.15.0", 7 | "PyQtWebEngine>=5.15.0", 8 | "pyspellchecker>=0.7.2", 9 | "feedparser>=6.0.0", 10 | "requests>=2.31.0", 11 | ] 12 | 13 | # Try to read requirements.txt, fall back to defaults if not found 14 | try: 15 | with open('requirements.txt') as f: 16 | requirements = f.read().splitlines() 17 | except FileNotFoundError: 18 | requirements = default_requires 19 | 20 | # Data files to install 21 | data_files = [ 22 | ('share/applications', ['packaging/debian/jottr.desktop']), 23 | ] 24 | 25 | # Add icon if it exists 26 | if os.path.exists('jottr/icons/jottr.png'): 27 | data_files.append(('share/icons/hicolor/128x128/apps', ['jottr/icons/jottr.png'])) 28 | 29 | setup( 30 | name="jottr", 31 | version="1.0", 32 | description="Modern text editor for writers and journalists", 33 | long_description="""Jottr is a feature-rich text editor designed specifically 34 | for writers and journalists, with features like smart 35 | completion, snippets, and integrated web browsing.""", 36 | author="mFat", 37 | author_email="newmfat@gmail.com", 38 | url="https://github.com/mfat/jottr", 39 | packages=find_packages(), 40 | include_package_data=True, 41 | install_requires=requirements, 42 | entry_points={ 43 | 'console_scripts': [ 44 | 'jottr=jottr.main:main', 45 | ], 46 | }, 47 | data_files=data_files, 48 | classifiers=[ 49 | 'Development Status :: 4 - Beta', 50 | 'Environment :: X11 Applications :: Qt', 51 | 'Intended Audience :: End Users/Desktop', 52 | 'License :: OSI Approved :: GPL-3.0+', 53 | 'Operating System :: POSIX :: Linux', 54 | 'Programming Language :: Python :: 3', 55 | 'Topic :: Text Editors', 56 | ], 57 | ) -------------------------------------------------------------------------------- /deb_dist/jottr_1.0-1.debian.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/deb_dist/jottr_1.0-1.debian.tar.xz -------------------------------------------------------------------------------- /deb_dist/jottr_1.0-1.dsc: -------------------------------------------------------------------------------- 1 | Format: 3.0 (quilt) 2 | Source: jottr 3 | Binary: python3-jottr 4 | Architecture: all 5 | Version: 1.0-1 6 | Maintainer: None 7 | Homepage: https://github.com/mfat/jottr 8 | Standards-Version: 3.9.6 9 | Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9) 10 | Package-List: 11 | python3-jottr deb python optional arch=all 12 | Checksums-Sha1: 13 | 09c6f0d87ae3ab29e8190e4017bed2b904625804 2738 jottr_1.0.orig.tar.gz 14 | a7ed0492f8041468ae3fa29f6e957c11c0199d51 960 jottr_1.0-1.debian.tar.xz 15 | Checksums-Sha256: 16 | f5d55e94694f5f2ed3f07a7d1bc483753aac74a7f92921fa73a9a277bdbfee45 2738 jottr_1.0.orig.tar.gz 17 | b6da4785d6feb0dc9b875dc4ef21822380d87687fc34e377bd918d471bab536f 960 jottr_1.0-1.debian.tar.xz 18 | Files: 19 | 9bfdab7c2a526138c56c70557751f98f 2738 jottr_1.0.orig.tar.gz 20 | ee88bebb45bffecec9b50b0bc6611923 960 jottr_1.0-1.debian.tar.xz 21 | -------------------------------------------------------------------------------- /deb_dist/jottr_1.0-1_amd64.buildinfo: -------------------------------------------------------------------------------- 1 | Format: 1.0 2 | Source: jottr 3 | Binary: python3-jottr 4 | Architecture: all 5 | Version: 1.0-1 6 | Checksums-Md5: 7 | 8f4fa60181e4e0c944d84ec82a44e458 2780 python3-jottr_1.0-1_all.deb 8 | Checksums-Sha1: 9 | 88b8cc4a6f4bfe11c4169f59050bdbb6313810c9 2780 python3-jottr_1.0-1_all.deb 10 | Checksums-Sha256: 11 | 5e58f65448872050254bfbeaaeeda0d5089acafcaacd6761d64f3f3c9b2c1be2 2780 python3-jottr_1.0-1_all.deb 12 | Build-Origin: Debian 13 | Build-Architecture: amd64 14 | Build-Date: Tue, 11 Feb 2025 03:47:55 +0330 15 | Build-Tainted-By: 16 | usr-local-has-libraries 17 | Installed-Build-Depends: 18 | autoconf (= 2.72-3), 19 | automake (= 1:1.16.5-1.3), 20 | autopoint (= 0.23.1-1), 21 | autotools-dev (= 20220109.1), 22 | base-files (= 13.6), 23 | base-passwd (= 3.6.6), 24 | bash (= 5.2.37-1), 25 | binutils (= 2.43.90.20250127-1), 26 | binutils-common (= 2.43.90.20250127-1), 27 | binutils-gold (= 2.44-1), 28 | binutils-gold-x86-64-linux-gnu (= 2.44-1), 29 | binutils-x86-64-linux-gnu (= 2.43.90.20250127-1), 30 | bsdextrautils (= 2.40.4-2), 31 | bsdutils (= 1:2.40.4-2), 32 | build-essential (= 12.12), 33 | bzip2 (= 1.0.8-6), 34 | coreutils (= 9.5-1+b1), 35 | cpp (= 4:14.2.0-1), 36 | cpp-14 (= 14.2.0-12), 37 | cpp-14-x86-64-linux-gnu (= 14.2.0-12), 38 | cpp-x86-64-linux-gnu (= 4:14.2.0-1), 39 | dash (= 0.5.12-11), 40 | debconf (= 1.5.89), 41 | debhelper (= 13.24.1), 42 | debianutils (= 5.21), 43 | dh-autoreconf (= 20), 44 | dh-python (= 6.20250108), 45 | dh-strip-nondeterminism (= 1.14.1-1), 46 | diffutils (= 1:3.10-2), 47 | dpkg (= 1.22.14), 48 | dpkg-dev (= 1.22.14), 49 | dwz (= 0.15-1+b1), 50 | file (= 1:5.45-3+b1), 51 | findutils (= 4.10.0-3), 52 | g++ (= 4:14.2.0-1), 53 | g++-14 (= 14.2.0-12), 54 | g++-14-x86-64-linux-gnu (= 14.2.0-12), 55 | g++-x86-64-linux-gnu (= 4:14.2.0-1), 56 | gcc (= 4:14.2.0-1), 57 | gcc-14 (= 14.2.0-12), 58 | gcc-14-base (= 14.2.0-12), 59 | gcc-14-x86-64-linux-gnu (= 14.2.0-12), 60 | gcc-x86-64-linux-gnu (= 4:14.2.0-1), 61 | gettext (= 0.23.1-1), 62 | gettext-base (= 0.23.1-1), 63 | grep (= 3.11-4), 64 | groff-base (= 1.23.0-7), 65 | gzip (= 1.13-1), 66 | hostname (= 3.25), 67 | init-system-helpers (= 1.68), 68 | intltool-debian (= 0.35.0+20060710.6), 69 | libacl1 (= 2.3.2-2+b1), 70 | libarchive-zip-perl (= 1.68-1), 71 | libasan8 (= 14.2.0-12), 72 | libatomic1 (= 14.2.0-12), 73 | libattr1 (= 1:2.5.2-2), 74 | libaudit-common (= 1:4.0.2-2), 75 | libaudit1 (= 1:4.0.2-2+b1), 76 | libbinutils (= 2.43.90.20250127-1), 77 | libblkid1 (= 2.40.4-2), 78 | libbz2-1.0 (= 1.0.8-6), 79 | libc-bin (= 2.40-6), 80 | libc-dev-bin (= 2.40-6), 81 | libc6 (= 2.40-6), 82 | libc6-dev (= 2.40-6), 83 | libcap-ng0 (= 0.8.5-4), 84 | libcap2 (= 1:2.66-5+b1), 85 | libcc1-0 (= 14.2.0-12), 86 | libcom-err2 (= 1.47.2-1), 87 | libcrypt-dev (= 1:4.4.38-1), 88 | libcrypt1 (= 1:4.4.38-1), 89 | libctf-nobfd0 (= 2.43.90.20250127-1), 90 | libctf0 (= 2.43.90.20250127-1), 91 | libdb5.3t64 (= 5.3.28+dfsg2-9), 92 | libdebconfclient0 (= 0.277), 93 | libdebhelper-perl (= 13.24.1), 94 | libdpkg-perl (= 1.22.14), 95 | libelf1t64 (= 0.192-4), 96 | libexpat1 (= 2.6.4-1), 97 | libffi8 (= 3.4.6-1), 98 | libfile-stripnondeterminism-perl (= 1.14.1-1), 99 | libgcc-14-dev (= 14.2.0-12), 100 | libgcc-s1 (= 14.2.0-12), 101 | libgdbm-compat4t64 (= 1.24-2), 102 | libgdbm6t64 (= 1.24-2), 103 | libgmp10 (= 2:6.3.0+dfsg-3), 104 | libgomp1 (= 14.2.0-12), 105 | libgprofng0 (= 2.43.90.20250127-1), 106 | libgssapi-krb5-2 (= 1.21.3-4), 107 | libhwasan0 (= 14.2.0-12), 108 | libicu72 (= 72.1-6), 109 | libisl23 (= 0.27-1), 110 | libitm1 (= 14.2.0-12), 111 | libjansson4 (= 2.14-2+b3), 112 | libk5crypto3 (= 1.21.3-4), 113 | libkeyutils1 (= 1.6.3-4), 114 | libkrb5-3 (= 1.21.3-4), 115 | libkrb5support0 (= 1.21.3-4), 116 | liblsan0 (= 14.2.0-12), 117 | liblzma5 (= 5.6.3-1+b1), 118 | libmagic-mgc (= 1:5.45-3+b1), 119 | libmagic1t64 (= 1:5.45-3+b1), 120 | libmd0 (= 1.1.0-2+b1), 121 | libmount1 (= 2.40.4-2), 122 | libmpc3 (= 1.3.1-1+b3), 123 | libmpfr6 (= 4.2.1-1+b2), 124 | libncursesw6 (= 6.5-2+b1), 125 | libnsl2 (= 1.3.0-3+b3), 126 | libpam-modules (= 1.7.0-2), 127 | libpam-modules-bin (= 1.7.0-2), 128 | libpam-runtime (= 1.7.0-2), 129 | libpam0g (= 1.7.0-2), 130 | libpcre2-8-0 (= 10.44-5), 131 | libperl5.40 (= 5.40.0-8), 132 | libpipeline1 (= 1.5.8-1), 133 | libpython3-stdlib (= 3.13.1-2), 134 | libpython3.12-minimal (= 3.12.8-5+b1), 135 | libpython3.12-stdlib (= 3.12.8-5+b1), 136 | libpython3.13-minimal (= 3.13.1-3+b1), 137 | libpython3.13-stdlib (= 3.13.1-3+b1), 138 | libquadmath0 (= 14.2.0-12), 139 | libreadline8t64 (= 8.2-6), 140 | libseccomp2 (= 2.5.5-2), 141 | libselinux1 (= 3.7-3.1), 142 | libsframe1 (= 2.43.90.20250127-1), 143 | libsmartcols1 (= 2.40.4-2), 144 | libsqlite3-0 (= 3.46.1-1), 145 | libssl3t64 (= 3.4.0-2), 146 | libstdc++-14-dev (= 14.2.0-12), 147 | libstdc++6 (= 14.2.0-12), 148 | libsystemd0 (= 257.2-3), 149 | libtinfo6 (= 6.5-2+b1), 150 | libtirpc-common (= 1.3.4+ds-1.3), 151 | libtirpc3t64 (= 1.3.4+ds-1.3+b1), 152 | libtool (= 2.5.4-2), 153 | libtsan2 (= 14.2.0-12), 154 | libubsan1 (= 14.2.0-12), 155 | libuchardet0 (= 0.0.8-1+b2), 156 | libudev1 (= 257.2-3), 157 | libunistring5 (= 1.3-1), 158 | libuuid1 (= 2.40.4-2), 159 | libxml2 (= 2.12.7+dfsg+really2.9.14-0.2+b1), 160 | libzstd1 (= 1.5.6+dfsg-2), 161 | linux-libc-dev (= 6.12.11-1), 162 | m4 (= 1.4.19-5), 163 | make (= 4.4.1-1), 164 | man-db (= 2.13.0-1), 165 | mawk (= 1.3.4.20240905-1), 166 | media-types (= 10.1.0), 167 | ncurses-base (= 6.5-2), 168 | ncurses-bin (= 6.5-2+b1), 169 | netbase (= 6.4), 170 | openssl-provider-legacy (= 3.4.0-2), 171 | patch (= 2.7.6-7), 172 | perl (= 5.40.0-8), 173 | perl-base (= 5.40.0-8), 174 | perl-modules-5.40 (= 5.40.0-8), 175 | po-debconf (= 1.0.21+nmu1), 176 | python3 (= 3.13.1-2), 177 | python3-all (= 3.13.1-2), 178 | python3-autocommand (= 2.2.2-3), 179 | python3-importlib-metadata (= 8.6.1-1), 180 | python3-inflect (= 7.3.1-2), 181 | python3-jaraco.context (= 6.0.0-1), 182 | python3-jaraco.functools (= 4.1.0-1), 183 | python3-jaraco.text (= 4.0.0-1), 184 | python3-minimal (= 3.13.1-2), 185 | python3-more-itertools (= 10.6.0-1), 186 | python3-pkg-resources (= 75.6.0-1), 187 | python3-setuptools (= 75.6.0-1), 188 | python3-typeguard (= 4.4.1-1), 189 | python3-typing-extensions (= 4.12.2-2), 190 | python3-zipp (= 3.21.0-1), 191 | python3.12 (= 3.12.8-5+b1), 192 | python3.12-minimal (= 3.12.8-5+b1), 193 | python3.13 (= 3.13.1-3+b1), 194 | python3.13-minimal (= 3.13.1-3+b1), 195 | readline-common (= 8.2-6), 196 | rpcsvc-proto (= 1.4.3-1), 197 | sed (= 4.9-2), 198 | sensible-utils (= 0.0.24), 199 | sysvinit-utils (= 3.13-1), 200 | tar (= 1.35+dfsg-3.1), 201 | tzdata (= 2024b-6), 202 | util-linux (= 2.40.4-2), 203 | xz-utils (= 5.6.3-1+b1), 204 | zlib1g (= 1:1.3.dfsg+really1.3.1-1+b1) 205 | Environment: 206 | DEB_BUILD_OPTIONS="parallel=12" 207 | LANG="en_US.UTF-8" 208 | LC_ADDRESS="en_US.UTF-8" 209 | LC_IDENTIFICATION="en_US.UTF-8" 210 | LC_MEASUREMENT="en_US.UTF-8" 211 | LC_MONETARY="en_US.UTF-8" 212 | LC_NAME="en_US.UTF-8" 213 | LC_NUMERIC="en_US.UTF-8" 214 | LC_PAPER="en_US.UTF-8" 215 | LC_TELEPHONE="en_US.UTF-8" 216 | LC_TIME="en_US.UTF-8" 217 | LD_LIBRARY_PATH="/tmp/.mount_cursor1KuXmY/usr/lib:" 218 | SOURCE_DATE_EPOCH="1739233056" 219 | -------------------------------------------------------------------------------- /deb_dist/jottr_1.0-1_amd64.changes: -------------------------------------------------------------------------------- 1 | Format: 1.8 2 | Date: Tue, 11 Feb 2025 03:47:36 +0330 3 | Source: jottr 4 | Binary: python3-jottr 5 | Architecture: all 6 | Version: 1.0-1 7 | Distribution: unstable 8 | Urgency: low 9 | Maintainer: None 10 | Changed-By: None 11 | Description: 12 | python3-jottr - Modern text editor for writers and journalists 13 | Changes: 14 | jottr (1.0-1) unstable; urgency=low 15 | . 16 | * source package automatically created by stdeb 0.10.0 17 | Checksums-Sha1: 18 | cf3d94ffc73a43da3ab65dee6ad975585f619478 6236 jottr_1.0-1_amd64.buildinfo 19 | 88b8cc4a6f4bfe11c4169f59050bdbb6313810c9 2780 python3-jottr_1.0-1_all.deb 20 | Checksums-Sha256: 21 | 1e643a31998a2991039d37b4a2b91a48155ef274e15c1d1de4c5d2b062fe9e47 6236 jottr_1.0-1_amd64.buildinfo 22 | 5e58f65448872050254bfbeaaeeda0d5089acafcaacd6761d64f3f3c9b2c1be2 2780 python3-jottr_1.0-1_all.deb 23 | Files: 24 | af42848c3a3bde028e83894d755e7b84 6236 python optional jottr_1.0-1_amd64.buildinfo 25 | 8f4fa60181e4e0c944d84ec82a44e458 2780 python optional python3-jottr_1.0-1_all.deb 26 | -------------------------------------------------------------------------------- /deb_dist/jottr_1.0-1_source.buildinfo: -------------------------------------------------------------------------------- 1 | Format: 1.0 2 | Source: jottr 3 | Architecture: source 4 | Version: 1.0-1 5 | Checksums-Md5: 6 | f28543451581bbaecff085bf07e042d4 808 jottr_1.0-1.dsc 7 | Checksums-Sha1: 8 | b2b06a4fd2524c99001d1557c284834ca92463e5 808 jottr_1.0-1.dsc 9 | Checksums-Sha256: 10 | b2c606663a711c2858fca351dad028899efb54cf311fb7ac73d8b8414bbc23eb 808 jottr_1.0-1.dsc 11 | Build-Origin: Debian 12 | Build-Architecture: amd64 13 | Build-Date: Tue, 11 Feb 2025 03:47:40 +0330 14 | Build-Tainted-By: 15 | usr-local-has-libraries 16 | Installed-Build-Depends: 17 | autoconf (= 2.72-3), 18 | automake (= 1:1.16.5-1.3), 19 | autopoint (= 0.23.1-1), 20 | autotools-dev (= 20220109.1), 21 | base-files (= 13.6), 22 | base-passwd (= 3.6.6), 23 | bash (= 5.2.37-1), 24 | binutils (= 2.43.90.20250127-1), 25 | binutils-common (= 2.43.90.20250127-1), 26 | binutils-gold (= 2.44-1), 27 | binutils-gold-x86-64-linux-gnu (= 2.44-1), 28 | binutils-x86-64-linux-gnu (= 2.43.90.20250127-1), 29 | bsdextrautils (= 2.40.4-2), 30 | bsdutils (= 1:2.40.4-2), 31 | build-essential (= 12.12), 32 | bzip2 (= 1.0.8-6), 33 | coreutils (= 9.5-1+b1), 34 | cpp (= 4:14.2.0-1), 35 | cpp-14 (= 14.2.0-12), 36 | cpp-14-x86-64-linux-gnu (= 14.2.0-12), 37 | cpp-x86-64-linux-gnu (= 4:14.2.0-1), 38 | dash (= 0.5.12-11), 39 | debconf (= 1.5.89), 40 | debhelper (= 13.24.1), 41 | debianutils (= 5.21), 42 | dh-autoreconf (= 20), 43 | dh-python (= 6.20250108), 44 | dh-strip-nondeterminism (= 1.14.1-1), 45 | diffutils (= 1:3.10-2), 46 | dpkg (= 1.22.14), 47 | dpkg-dev (= 1.22.14), 48 | dwz (= 0.15-1+b1), 49 | file (= 1:5.45-3+b1), 50 | findutils (= 4.10.0-3), 51 | g++ (= 4:14.2.0-1), 52 | g++-14 (= 14.2.0-12), 53 | g++-14-x86-64-linux-gnu (= 14.2.0-12), 54 | g++-x86-64-linux-gnu (= 4:14.2.0-1), 55 | gcc (= 4:14.2.0-1), 56 | gcc-14 (= 14.2.0-12), 57 | gcc-14-base (= 14.2.0-12), 58 | gcc-14-x86-64-linux-gnu (= 14.2.0-12), 59 | gcc-x86-64-linux-gnu (= 4:14.2.0-1), 60 | gettext (= 0.23.1-1), 61 | gettext-base (= 0.23.1-1), 62 | grep (= 3.11-4), 63 | groff-base (= 1.23.0-7), 64 | gzip (= 1.13-1), 65 | hostname (= 3.25), 66 | init-system-helpers (= 1.68), 67 | intltool-debian (= 0.35.0+20060710.6), 68 | libacl1 (= 2.3.2-2+b1), 69 | libarchive-zip-perl (= 1.68-1), 70 | libasan8 (= 14.2.0-12), 71 | libatomic1 (= 14.2.0-12), 72 | libattr1 (= 1:2.5.2-2), 73 | libaudit-common (= 1:4.0.2-2), 74 | libaudit1 (= 1:4.0.2-2+b1), 75 | libbinutils (= 2.43.90.20250127-1), 76 | libblkid1 (= 2.40.4-2), 77 | libbz2-1.0 (= 1.0.8-6), 78 | libc-bin (= 2.40-6), 79 | libc-dev-bin (= 2.40-6), 80 | libc6 (= 2.40-6), 81 | libc6-dev (= 2.40-6), 82 | libcap-ng0 (= 0.8.5-4), 83 | libcap2 (= 1:2.66-5+b1), 84 | libcc1-0 (= 14.2.0-12), 85 | libcom-err2 (= 1.47.2-1), 86 | libcrypt-dev (= 1:4.4.38-1), 87 | libcrypt1 (= 1:4.4.38-1), 88 | libctf-nobfd0 (= 2.43.90.20250127-1), 89 | libctf0 (= 2.43.90.20250127-1), 90 | libdb5.3t64 (= 5.3.28+dfsg2-9), 91 | libdebconfclient0 (= 0.277), 92 | libdebhelper-perl (= 13.24.1), 93 | libdpkg-perl (= 1.22.14), 94 | libelf1t64 (= 0.192-4), 95 | libexpat1 (= 2.6.4-1), 96 | libffi8 (= 3.4.6-1), 97 | libfile-stripnondeterminism-perl (= 1.14.1-1), 98 | libgcc-14-dev (= 14.2.0-12), 99 | libgcc-s1 (= 14.2.0-12), 100 | libgdbm-compat4t64 (= 1.24-2), 101 | libgdbm6t64 (= 1.24-2), 102 | libgmp10 (= 2:6.3.0+dfsg-3), 103 | libgomp1 (= 14.2.0-12), 104 | libgprofng0 (= 2.43.90.20250127-1), 105 | libgssapi-krb5-2 (= 1.21.3-4), 106 | libhwasan0 (= 14.2.0-12), 107 | libicu72 (= 72.1-6), 108 | libisl23 (= 0.27-1), 109 | libitm1 (= 14.2.0-12), 110 | libjansson4 (= 2.14-2+b3), 111 | libk5crypto3 (= 1.21.3-4), 112 | libkeyutils1 (= 1.6.3-4), 113 | libkrb5-3 (= 1.21.3-4), 114 | libkrb5support0 (= 1.21.3-4), 115 | liblsan0 (= 14.2.0-12), 116 | liblzma5 (= 5.6.3-1+b1), 117 | libmagic-mgc (= 1:5.45-3+b1), 118 | libmagic1t64 (= 1:5.45-3+b1), 119 | libmd0 (= 1.1.0-2+b1), 120 | libmount1 (= 2.40.4-2), 121 | libmpc3 (= 1.3.1-1+b3), 122 | libmpfr6 (= 4.2.1-1+b2), 123 | libncursesw6 (= 6.5-2+b1), 124 | libnsl2 (= 1.3.0-3+b3), 125 | libpam-modules (= 1.7.0-2), 126 | libpam-modules-bin (= 1.7.0-2), 127 | libpam-runtime (= 1.7.0-2), 128 | libpam0g (= 1.7.0-2), 129 | libpcre2-8-0 (= 10.44-5), 130 | libperl5.40 (= 5.40.0-8), 131 | libpipeline1 (= 1.5.8-1), 132 | libpython3-stdlib (= 3.13.1-2), 133 | libpython3.12-minimal (= 3.12.8-5+b1), 134 | libpython3.12-stdlib (= 3.12.8-5+b1), 135 | libpython3.13-minimal (= 3.13.1-3+b1), 136 | libpython3.13-stdlib (= 3.13.1-3+b1), 137 | libquadmath0 (= 14.2.0-12), 138 | libreadline8t64 (= 8.2-6), 139 | libseccomp2 (= 2.5.5-2), 140 | libselinux1 (= 3.7-3.1), 141 | libsframe1 (= 2.43.90.20250127-1), 142 | libsmartcols1 (= 2.40.4-2), 143 | libsqlite3-0 (= 3.46.1-1), 144 | libssl3t64 (= 3.4.0-2), 145 | libstdc++-14-dev (= 14.2.0-12), 146 | libstdc++6 (= 14.2.0-12), 147 | libsystemd0 (= 257.2-3), 148 | libtinfo6 (= 6.5-2+b1), 149 | libtirpc-common (= 1.3.4+ds-1.3), 150 | libtirpc3t64 (= 1.3.4+ds-1.3+b1), 151 | libtool (= 2.5.4-2), 152 | libtsan2 (= 14.2.0-12), 153 | libubsan1 (= 14.2.0-12), 154 | libuchardet0 (= 0.0.8-1+b2), 155 | libudev1 (= 257.2-3), 156 | libunistring5 (= 1.3-1), 157 | libuuid1 (= 2.40.4-2), 158 | libxml2 (= 2.12.7+dfsg+really2.9.14-0.2+b1), 159 | libzstd1 (= 1.5.6+dfsg-2), 160 | linux-libc-dev (= 6.12.11-1), 161 | m4 (= 1.4.19-5), 162 | make (= 4.4.1-1), 163 | man-db (= 2.13.0-1), 164 | mawk (= 1.3.4.20240905-1), 165 | media-types (= 10.1.0), 166 | ncurses-base (= 6.5-2), 167 | ncurses-bin (= 6.5-2+b1), 168 | netbase (= 6.4), 169 | openssl-provider-legacy (= 3.4.0-2), 170 | patch (= 2.7.6-7), 171 | perl (= 5.40.0-8), 172 | perl-base (= 5.40.0-8), 173 | perl-modules-5.40 (= 5.40.0-8), 174 | po-debconf (= 1.0.21+nmu1), 175 | python3 (= 3.13.1-2), 176 | python3-all (= 3.13.1-2), 177 | python3-autocommand (= 2.2.2-3), 178 | python3-importlib-metadata (= 8.6.1-1), 179 | python3-inflect (= 7.3.1-2), 180 | python3-jaraco.context (= 6.0.0-1), 181 | python3-jaraco.functools (= 4.1.0-1), 182 | python3-jaraco.text (= 4.0.0-1), 183 | python3-minimal (= 3.13.1-2), 184 | python3-more-itertools (= 10.6.0-1), 185 | python3-pkg-resources (= 75.6.0-1), 186 | python3-setuptools (= 75.6.0-1), 187 | python3-typeguard (= 4.4.1-1), 188 | python3-typing-extensions (= 4.12.2-2), 189 | python3-zipp (= 3.21.0-1), 190 | python3.12 (= 3.12.8-5+b1), 191 | python3.12-minimal (= 3.12.8-5+b1), 192 | python3.13 (= 3.13.1-3+b1), 193 | python3.13-minimal (= 3.13.1-3+b1), 194 | readline-common (= 8.2-6), 195 | rpcsvc-proto (= 1.4.3-1), 196 | sed (= 4.9-2), 197 | sensible-utils (= 0.0.24), 198 | sysvinit-utils (= 3.13-1), 199 | tar (= 1.35+dfsg-3.1), 200 | tzdata (= 2024b-6), 201 | util-linux (= 2.40.4-2), 202 | xz-utils (= 5.6.3-1+b1), 203 | zlib1g (= 1:1.3.dfsg+really1.3.1-1+b1) 204 | Environment: 205 | DEB_BUILD_OPTIONS="parallel=12" 206 | LANG="en_US.UTF-8" 207 | LC_ADDRESS="en_US.UTF-8" 208 | LC_IDENTIFICATION="en_US.UTF-8" 209 | LC_MEASUREMENT="en_US.UTF-8" 210 | LC_MONETARY="en_US.UTF-8" 211 | LC_NAME="en_US.UTF-8" 212 | LC_NUMERIC="en_US.UTF-8" 213 | LC_PAPER="en_US.UTF-8" 214 | LC_TELEPHONE="en_US.UTF-8" 215 | LC_TIME="en_US.UTF-8" 216 | LD_LIBRARY_PATH="/tmp/.mount_cursor1KuXmY/usr/lib:" 217 | SOURCE_DATE_EPOCH="1739233056" 218 | -------------------------------------------------------------------------------- /deb_dist/jottr_1.0-1_source.changes: -------------------------------------------------------------------------------- 1 | Format: 1.8 2 | Date: Tue, 11 Feb 2025 03:47:36 +0330 3 | Source: jottr 4 | Architecture: source 5 | Version: 1.0-1 6 | Distribution: unstable 7 | Urgency: low 8 | Maintainer: None 9 | Changed-By: None 10 | Changes: 11 | jottr (1.0-1) unstable; urgency=low 12 | . 13 | * source package automatically created by stdeb 0.10.0 14 | Checksums-Sha1: 15 | b2b06a4fd2524c99001d1557c284834ca92463e5 808 jottr_1.0-1.dsc 16 | 09c6f0d87ae3ab29e8190e4017bed2b904625804 2738 jottr_1.0.orig.tar.gz 17 | a7ed0492f8041468ae3fa29f6e957c11c0199d51 960 jottr_1.0-1.debian.tar.xz 18 | 74ce0f874e9483ee43e1e6da3d69e88493756680 6178 jottr_1.0-1_source.buildinfo 19 | Checksums-Sha256: 20 | b2c606663a711c2858fca351dad028899efb54cf311fb7ac73d8b8414bbc23eb 808 jottr_1.0-1.dsc 21 | f5d55e94694f5f2ed3f07a7d1bc483753aac74a7f92921fa73a9a277bdbfee45 2738 jottr_1.0.orig.tar.gz 22 | b6da4785d6feb0dc9b875dc4ef21822380d87687fc34e377bd918d471bab536f 960 jottr_1.0-1.debian.tar.xz 23 | 3575be1ede676ead92df31d1140ace934d7f612e24dd4ce38ed00a8a6b35578f 6178 jottr_1.0-1_source.buildinfo 24 | Files: 25 | f28543451581bbaecff085bf07e042d4 808 python optional jottr_1.0-1.dsc 26 | 9bfdab7c2a526138c56c70557751f98f 2738 python optional jottr_1.0.orig.tar.gz 27 | ee88bebb45bffecec9b50b0bc6611923 960 python optional jottr_1.0-1.debian.tar.xz 28 | cc326c4ef1f5f58d571e556123930094 6178 python optional jottr_1.0-1_source.buildinfo 29 | -------------------------------------------------------------------------------- /deb_dist/jottr_1.0.orig.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/deb_dist/jottr_1.0.orig.tar.gz -------------------------------------------------------------------------------- /icons/about.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/applications-system.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /icons/browser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/color-mode-invert-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /icons/document-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/find.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/focus-mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/font.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/format-text-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/gtk-select-font.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /icons/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/im-google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/insert-text-frame.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/insert-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/jottr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/icons/jottr.png -------------------------------------------------------------------------------- /icons/jottr_icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/icons/jottr_icon_128x128.png -------------------------------------------------------------------------------- /icons/jottr_icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/icons/jottr_icon_16x16.png -------------------------------------------------------------------------------- /icons/jottr_icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/icons/jottr_icon_256x256.png -------------------------------------------------------------------------------- /icons/jottr_icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/icons/jottr_icon_32x32.png -------------------------------------------------------------------------------- /icons/jottr_icon_48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/icons/jottr_icon_48x48.png -------------------------------------------------------------------------------- /icons/jottr_icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/icons/jottr_icon_512x512.png -------------------------------------------------------------------------------- /icons/jottr_icon_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/icons/jottr_icon_64x64.png -------------------------------------------------------------------------------- /icons/larger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/preferences-desktop-display-randr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /icons/preferences-desktop-font.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /icons/preferences-desktop-theme-applications.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /icons/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/save-as.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/smaller.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/snippets.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /icons/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/view-fullscreen-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /icons/zoom-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/zoom-reset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /io.github.mfat.jottr.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Jottr 3 | Comment=A simple text editor for writers, journalists and researchers 4 | Exec=jottr 5 | Icon=io.github.mfat.jottr 6 | Terminal=false 7 | Type=Application 8 | Categories=TextEditor;Utility; 9 | StartupNotify=true -------------------------------------------------------------------------------- /io.github.mfat.jottr.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.mfat.jottr 4 | 5 | Jottr 6 | A simple text editor mainly intended for writers, journalists and researchers 7 | 8 | FSFAP 9 | GPL-3.0-or-later 10 | 11 | 12 |

13 | A simple, opinionated plain text editor designed for writers, journalists and researchers. Jottr has features like distraction-free writing, an integrated web-browser, customizable search in specific sites from the context menu and smart auto-completion. 14 |

15 |
16 | 17 | io.github.mfat.jottr.desktop 18 | 19 | 20 | https://github.com/mfat/jottr 21 | mFat 22 | 23 | 24 | 25 | 26 | 27 | 28 |

Bug fixes and improvements

29 |

Context menu reordered

30 |
31 |
32 |
33 | 34 | 35 | 36 | https://raw.githubusercontent.com/mfat/jottr/266df5566ca0834bd1f29aeb84a9def9070cc5e3/screenshots/main.png 37 | Main window of Jottr 38 | 39 | 40 |
-------------------------------------------------------------------------------- /io.github.mfat.jottr.yml: -------------------------------------------------------------------------------- 1 | app-id: io.github.mfat.jottr 2 | runtime: org.kde.Platform 3 | runtime-version: '5.15-24.08' 4 | sdk: org.kde.Sdk 5 | base: com.riverbankcomputing.PyQt.BaseApp 6 | base-version: '5.15-24.08' 7 | command: jottr 8 | separate-locales: false 9 | cleanup-commands: 10 | - /app/cleanup-BaseApp.sh 11 | finish-args: 12 | - --share=network 13 | - --share=ipc 14 | - --socket=wayland 15 | - --socket=fallback-x11 16 | - --device=dri 17 | - --env=QTWEBENGINEPROCESS_PATH=/app/bin/QtWebEngineProcess 18 | - --env=QTWEBENGINE_DISABLE_SANDBOX=1 19 | modules: 20 | - name: enchant 21 | buildsystem: autotools 22 | config-opts: 23 | - --disable-static 24 | - --enable-relocatable 25 | sources: 26 | - type: archive 27 | url: https://github.com/rrthomas/enchant/releases/download/v2.2.15/enchant-2.2.15.tar.gz 28 | sha256: 3b0f2215578115f28e2a6aa549b35128600394304bd79d6f28b0d3b3d6f46c03 29 | cleanup: 30 | - /share/doc 31 | - /share/man 32 | 33 | - pypi-dependencies.yaml 34 | 35 | - name: jottr 36 | buildsystem: simple 37 | build-commands: 38 | - mkdir -p /app/share/jottr 39 | - cp -r src/jottr/* /app/share/jottr/ 40 | - install -D -m755 jottr.sh /app/bin/jottr 41 | - install -D -m644 io.github.mfat.jottr.desktop -t /app/share/applications/ 42 | - install -D icons/jottr_icon_16x16.png /app/share/icons/hicolor/16x16/apps/io.github.mfat.jottr.png 43 | - install -D icons/jottr_icon_32x32.png /app/share/icons/hicolor/32x32/apps/io.github.mfat.jottr.png 44 | - install -D icons/jottr_icon_48x48.png /app/share/icons/hicolor/48x48/apps/io.github.mfat.jottr.png 45 | - install -D icons/jottr_icon_64x64.png /app/share/icons/hicolor/64x64/apps/io.github.mfat.jottr.png 46 | - install -D icons/jottr_icon_128x128.png /app/share/icons/hicolor/128x128/apps/io.github.mfat.jottr.png 47 | - install -D icons/jottr_icon_256x256.png /app/share/icons/hicolor/256x256/apps/io.github.mfat.jottr.png 48 | - install -D icons/jottr_icon_512x512.png /app/share/icons/hicolor/512x512/apps/io.github.mfat.jottr.png 49 | - install -D -m644 io.github.mfat.jottr.metainfo.xml -t /app/share/metainfo/ 50 | sources: 51 | - type: git 52 | url: "https://github.com/mfat/jottr.git" 53 | tag: v1.4.3 54 | commit: ae78bb0d7f35e8ed49ad3e3d510cb569d86c16e2 55 | - type: file 56 | path: jottr.sh 57 | -------------------------------------------------------------------------------- /jottr-1.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/jottr-1.0.tar.gz -------------------------------------------------------------------------------- /jottr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python3 /app/share/jottr/main.py "$@" -------------------------------------------------------------------------------- /packaging/debian/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Change to project root directory 5 | cd "$(dirname "$0")/../.." 6 | 7 | # Clean up any previous builds or installations 8 | rm -rf build/ dist/ *.egg-info/ 9 | 10 | # Install build dependencies 11 | sudo apt-get update 12 | sudo apt-get install -y devscripts debhelper python3-all python3-setuptools python3-enchant python3-pyqt5.qtsvg 13 | 14 | # Create debian directory link 15 | rm -rf debian 16 | ln -s packaging/debian debian 17 | 18 | # Update changelog with current date 19 | sed -i "s/\$(LC_ALL=C date -R)/$(LC_ALL=C date -R)/" debian/changelog 20 | 21 | # Build the package 22 | dpkg-buildpackage -us -uc -b 23 | 24 | # Clean up the symlink 25 | rm debian 26 | 27 | # Show the built package 28 | ls -l ../jottr_*.deb 29 | -------------------------------------------------------------------------------- /packaging/debian/changelog: -------------------------------------------------------------------------------- 1 | jottr (1.4.3-1) unstable; urgency=medium 2 | 3 | * Bug fixes 4 | * Updated icon 5 | 6 | -- mFat Sat, 01 Mar 2025 12:00:00 +0000 7 | -------------------------------------------------------------------------------- /packaging/debian/control: -------------------------------------------------------------------------------- 1 | Source: jottr 2 | Section: editors 3 | Priority: optional 4 | Maintainer: mFat 5 | Build-Depends: debhelper-compat (= 13) 6 | 7 | Package: jottr 8 | Architecture: all 9 | Depends: ${misc:Depends}, 10 | python3, 11 | python3-pyqt5, 12 | python3-pyqt5.qtwebengine, 13 | python3-feedparser, 14 | python3-enchant, 15 | python3-xdg, 16 | python3-pyqt5.qtsvg 17 | Description: Modern text editor for writers and journalists 18 | Jottr is a feature-rich text editor designed specifically 19 | for writers and journalists, with features like smart 20 | completion, snippets, and integrated web browsing. 21 | -------------------------------------------------------------------------------- /packaging/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://github.com/mfat/jottr 3 | 4 | Files: * 5 | Copyright: 2024 mFat 6 | License: GPL-3.0+ 7 | -------------------------------------------------------------------------------- /packaging/debian/debhelper-build-stamp: -------------------------------------------------------------------------------- 1 | jottr 2 | -------------------------------------------------------------------------------- /packaging/debian/files: -------------------------------------------------------------------------------- 1 | jottr_1.0-1_all.deb editors optional 2 | jottr_1.0-1_amd64.buildinfo editors optional 3 | -------------------------------------------------------------------------------- /packaging/debian/install: -------------------------------------------------------------------------------- 1 | src/jottr/* usr/share/jottr/ -------------------------------------------------------------------------------- /packaging/debian/jottr.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Jottr 3 | Comment=Modern text editor for writers and journalists 4 | Exec=jottr 5 | Icon=jottr 6 | Terminal=false 7 | Type=Application 8 | Categories=Office;TextEditor; 9 | -------------------------------------------------------------------------------- /packaging/debian/jottr.substvars: -------------------------------------------------------------------------------- 1 | misc:Depends= 2 | misc:Pre-Depends= 3 | -------------------------------------------------------------------------------- /packaging/debian/jottr/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: jottr 2 | Version: 1.0-1 3 | Architecture: all 4 | Maintainer: mFat 5 | Installed-Size: 275 6 | Depends: python3, python3-pyqt5, python3-pyqt5.qtwebengine, python3-feedparser, python3-enchant 7 | Section: editors 8 | Priority: optional 9 | Description: Modern text editor for writers and journalists 10 | Jottr is a feature-rich text editor designed specifically 11 | for writers and journalists, with features like smart 12 | completion, snippets, and integrated web browsing. 13 | -------------------------------------------------------------------------------- /packaging/debian/jottr/DEBIAN/md5sums: -------------------------------------------------------------------------------- 1 | 8c5d8ef4f0ecd2af550a226fc12eaf21 usr/bin/jottr 2 | e0804e80bd59c9159d86b901ea1f1b98 usr/share/applications/jottr.desktop 3 | 314f61998f795e5bffbc00653ba1c9ea usr/share/icons/hicolor/128x128/apps/jottr.png 4 | d41d8cd98f00b204e9800998ecf8427e usr/share/jottr/__init__.py 5 | 56bf9f4da0676ced7d261e573547dad8 usr/share/jottr/editor_tab.py 6 | 08b4bcbb60c51d4f60b2b445143b5f2b usr/share/jottr/feed_manager_dialog.py 7 | 314f61998f795e5bffbc00653ba1c9ea usr/share/jottr/icons/jottr.png 8 | 230d0653e969116770c737c27d36ed22 usr/share/jottr/main.py 9 | c0f2af2419012981d4b8f27639d7f55e usr/share/jottr/rss_reader.py 10 | b2a2e22d0bc55a4977c38990cb92c306 usr/share/jottr/rss_tab.py 11 | 4afa260412ec06803bdcb510d42d263b usr/share/jottr/settings_dialog.py 12 | 6fa03ddeb7d9278a9ff1d1e5110413ce usr/share/jottr/settings_manager.py 13 | 2e1eafe534f4982cd6faa68bc2dbd424 usr/share/jottr/snippet_editor_dialog.py 14 | dbd5ccf8f130119b9643088736165c3b usr/share/jottr/snippet_manager.py 15 | c40303f9cbd3ed63f38af9c66e76cf87 usr/share/jottr/theme_manager.py 16 | -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/bin/jottr: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PYTHONPATH=/usr/share/jottr exec python3 /usr/share/jottr/main.py "$@" 3 | -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/applications/jottr.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Jottr 3 | Comment=Modern text editor for writers and journalists 4 | Exec=jottr 5 | Icon=jottr 6 | Terminal=false 7 | Type=Application 8 | Categories=Office;TextEditor; 9 | -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/icons/hicolor/128x128/apps/jottr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/packaging/debian/jottr/usr/share/icons/hicolor/128x128/apps/jottr.png -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/packaging/debian/jottr/usr/share/jottr/__init__.py -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/feed_manager_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, 2 | QTableWidget, QTableWidgetItem, QInputDialog, 3 | QMessageBox, QHeaderView) 4 | import requests 5 | import feedparser 6 | 7 | class FeedManagerDialog(QDialog): 8 | def __init__(self, feeds, parent=None): 9 | super().__init__(parent) 10 | self.feeds = feeds.copy() # Work with a copy of the feeds 11 | self.setWindowTitle("Feed Manager") 12 | self.setMinimumWidth(600) 13 | self.setMinimumHeight(400) 14 | self.setup_ui() 15 | 16 | def setup_ui(self): 17 | layout = QVBoxLayout(self) 18 | 19 | # Create table 20 | self.table = QTableWidget(0, 2) 21 | self.table.setHorizontalHeaderLabels(["Feed Title", "URL"]) 22 | self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) 23 | self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) 24 | layout.addWidget(self.table) 25 | 26 | # Buttons 27 | button_layout = QHBoxLayout() 28 | 29 | add_button = QPushButton("Add Feed") 30 | edit_button = QPushButton("Edit Feed") 31 | remove_button = QPushButton("Remove Feed") 32 | test_button = QPushButton("Test Feed") 33 | 34 | add_button.clicked.connect(self.add_feed) 35 | edit_button.clicked.connect(self.edit_feed) 36 | remove_button.clicked.connect(self.remove_feed) 37 | test_button.clicked.connect(self.test_feed) 38 | 39 | button_layout.addWidget(add_button) 40 | button_layout.addWidget(edit_button) 41 | button_layout.addWidget(remove_button) 42 | button_layout.addWidget(test_button) 43 | button_layout.addStretch() 44 | 45 | ok_button = QPushButton("OK") 46 | cancel_button = QPushButton("Cancel") 47 | 48 | ok_button.clicked.connect(self.accept) 49 | cancel_button.clicked.connect(self.reject) 50 | 51 | button_layout.addWidget(ok_button) 52 | button_layout.addWidget(cancel_button) 53 | 54 | layout.addLayout(button_layout) 55 | 56 | # Populate table 57 | self.refresh_table() 58 | 59 | def refresh_table(self): 60 | self.table.setRowCount(0) 61 | for title, url in sorted(self.feeds.items()): 62 | row = self.table.rowCount() 63 | self.table.insertRow(row) 64 | self.table.setItem(row, 0, QTableWidgetItem(title)) 65 | self.table.setItem(row, 1, QTableWidgetItem(url)) 66 | 67 | def add_feed(self): 68 | title, ok = QInputDialog.getText(self, 'Add Feed', 'Feed Title:') 69 | if ok and title: 70 | if title in self.feeds: 71 | QMessageBox.warning(self, "Error", "A feed with this title already exists") 72 | return 73 | 74 | url, ok = QInputDialog.getText(self, 'Add Feed', 'Feed URL:') 75 | if ok and url: 76 | # Don't test by default, just add 77 | self.feeds[title] = url 78 | self.refresh_table() 79 | 80 | def edit_feed(self): 81 | current_row = self.table.currentRow() 82 | if current_row < 0: 83 | QMessageBox.warning(self, "Error", "Please select a feed to edit") 84 | return 85 | 86 | old_title = self.table.item(current_row, 0).text() 87 | old_url = self.table.item(current_row, 1).text() 88 | 89 | title, ok = QInputDialog.getText(self, 'Edit Feed', 'Feed Title:', 90 | text=old_title) 91 | if ok and title: 92 | if title != old_title and title in self.feeds: 93 | QMessageBox.warning(self, "Error", "A feed with this title already exists") 94 | return 95 | 96 | url, ok = QInputDialog.getText(self, 'Edit Feed', 'Feed URL:', 97 | text=old_url) 98 | if ok and url: 99 | # Don't test by default, just update 100 | if old_title in self.feeds: 101 | del self.feeds[old_title] 102 | self.feeds[title] = url 103 | self.refresh_table() 104 | 105 | def remove_feed(self): 106 | current_row = self.table.currentRow() 107 | if current_row < 0: 108 | QMessageBox.warning(self, "Error", "Please select a feed to remove") 109 | return 110 | 111 | title = self.table.item(current_row, 0).text() 112 | reply = QMessageBox.question(self, 'Remove Feed', 113 | f'Remove feed "{title}"?', 114 | QMessageBox.Yes | QMessageBox.No) 115 | if reply == QMessageBox.Yes: 116 | del self.feeds[title] 117 | self.refresh_table() 118 | 119 | def test_feed(self): 120 | current_row = self.table.currentRow() 121 | if current_row < 0: 122 | QMessageBox.warning(self, "Error", "Please select a feed to test") 123 | return 124 | 125 | url = self.table.item(current_row, 1).text() 126 | if self.test_feed_url(url): 127 | QMessageBox.information(self, "Success", "Feed is valid and accessible") 128 | 129 | def test_feed_url(self, url): 130 | try: 131 | response = requests.get(url, timeout=10) 132 | response.raise_for_status() 133 | 134 | feed = feedparser.parse(response.text) 135 | if hasattr(feed, 'entries') and feed.entries: 136 | return True 137 | else: 138 | QMessageBox.warning(self, "Error", "Invalid RSS feed (no entries found)") 139 | return False 140 | except Exception as e: 141 | QMessageBox.warning(self, "Error", f"Could not parse RSS feed: {str(e)}") 142 | return False 143 | 144 | def get_feeds(self): 145 | return self.feeds -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/icons/jottr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/packaging/debian/jottr/usr/share/jottr/icons/jottr.png -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/rss_reader.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QListWidget, 2 | QTextBrowser, QPushButton, QInputDialog, QMessageBox, 3 | QComboBox, QListWidgetItem, QDialog) 4 | from PyQt5.QtCore import Qt, QUrl 5 | import feedparser 6 | import json 7 | import os 8 | import requests 9 | from feed_manager_dialog import FeedManagerDialog 10 | 11 | class RSSReader(QWidget): 12 | def __init__(self, parent=None): 13 | super().__init__(parent) 14 | self.feeds = { 15 | "BBC World": "https://feeds.bbci.co.uk/news/world/rss.xml", 16 | "Reuters Top News": "https://feeds.reuters.com/reuters/topNews", 17 | "Al Jazeera": "https://www.aljazeera.com/xml/rss/all.xml", 18 | "CNN Top Stories": "http://rss.cnn.com/rss/edition.rss", 19 | # AP News feeds 20 | "AP Top News": "https://apnews.com/feed", 21 | "AP World News": "https://apnews.com/hub/world-news/feed", 22 | "AP Middle East": "https://apnews.com/hub/middle-east/feed" 23 | } 24 | self.feed_file = "rss_feeds.json" 25 | self.setup_ui() 26 | self.load_feeds() 27 | 28 | def setup_ui(self): 29 | layout = QVBoxLayout(self) 30 | 31 | # Top controls layout 32 | controls_layout = QHBoxLayout() 33 | 34 | # Feed selector dropdown 35 | self.feed_selector = QComboBox() 36 | self.feed_selector.currentTextChanged.connect(self.on_feed_selected) 37 | controls_layout.addWidget(self.feed_selector) 38 | 39 | # Buttons 40 | add_button = QPushButton("Add Feed") 41 | remove_button = QPushButton("Remove Feed") 42 | refresh_button = QPushButton("Refresh") 43 | manage_button = QPushButton("Manage Feeds") 44 | 45 | add_button.clicked.connect(self.add_feed) 46 | remove_button.clicked.connect(self.remove_feed) 47 | refresh_button.clicked.connect(self.refresh_feeds) 48 | manage_button.clicked.connect(self.manage_feeds) 49 | 50 | controls_layout.addWidget(manage_button) 51 | controls_layout.addWidget(add_button) 52 | controls_layout.addWidget(remove_button) 53 | controls_layout.addWidget(refresh_button) 54 | controls_layout.addStretch() 55 | 56 | layout.addLayout(controls_layout) 57 | 58 | # Feed entries list 59 | self.entries_list = QListWidget() 60 | self.entries_list.currentItemChanged.connect(self.show_entry) 61 | layout.addWidget(self.entries_list) 62 | 63 | # Content viewer 64 | self.content_viewer = QTextBrowser() 65 | self.content_viewer.setOpenExternalLinks(True) 66 | layout.addWidget(self.content_viewer) 67 | 68 | # Set size ratio between list and content 69 | layout.setStretch(1, 1) 70 | layout.setStretch(2, 2) 71 | 72 | def load_feeds(self): 73 | if os.path.exists(self.feed_file): 74 | try: 75 | with open(self.feed_file, 'r') as f: 76 | loaded_feeds = json.load(f) 77 | self.feeds.update(loaded_feeds) # Merge with default feeds 78 | except: 79 | pass # Keep default feeds if file load fails 80 | 81 | self.save_feeds() # Save combined feeds 82 | self.update_feed_selector() 83 | 84 | def save_feeds(self): 85 | with open(self.feed_file, 'w') as f: 86 | json.dump(self.feeds, f) 87 | 88 | def update_feed_selector(self): 89 | self.feed_selector.clear() 90 | self.feed_selector.addItems(sorted(self.feeds.keys())) 91 | 92 | def on_feed_selected(self, feed_title): 93 | # Don't automatically refresh when feed is selected 94 | pass 95 | 96 | def refresh_current_feed(self): 97 | self.entries_list.clear() 98 | self.content_viewer.clear() 99 | 100 | feed_title = self.feed_selector.currentText() 101 | if not feed_title or feed_title not in self.feeds: 102 | return 103 | 104 | url = self.feeds[feed_title] 105 | try: 106 | # Enhanced headers especially for RSSHub 107 | headers = { 108 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 109 | 'Accept': 'application/rss+xml, application/xml, application/json, */*', 110 | 'Accept-Language': 'en-US,en;q=0.9', 111 | 'Cache-Control': 'no-cache', 112 | 'Pragma': 'no-cache', 113 | 'Connection': 'keep-alive' 114 | } 115 | 116 | # Special handling for RSSHub 117 | if 'rsshub.app' in url: 118 | # Try direct feedparser first 119 | feed = feedparser.parse(url) 120 | if not hasattr(feed, 'entries') or not feed.entries: 121 | # If direct parsing fails, try with requests 122 | response = requests.get(url, timeout=10, headers=headers) 123 | response.raise_for_status() 124 | feed = feedparser.parse(response.text) 125 | else: 126 | # Normal handling for other feeds 127 | response = requests.get(url, timeout=10, headers=headers) 128 | response.raise_for_status() 129 | feed = feedparser.parse(response.text) 130 | 131 | if hasattr(feed, 'entries') and feed.entries: 132 | for entry in feed.entries: 133 | item_text = entry.title if hasattr(entry, 'title') else 'No Title' 134 | list_item = QListWidgetItem(item_text) 135 | list_item.setData(Qt.UserRole, entry) 136 | self.entries_list.addItem(list_item) 137 | else: 138 | print(f"Feed {feed_title} has no entries. Feed status: {feed.get('status', 'unknown')}") 139 | print(f"Feed bozo: {feed.get('bozo', 'unknown')}") 140 | if hasattr(feed, 'debug_message'): 141 | print(f"Feed debug: {feed.debug_message}") 142 | QMessageBox.warning(self, "Error", f"No entries found in feed: {feed_title}") 143 | 144 | except requests.exceptions.HTTPError as e: 145 | if e.response.status_code == 429: 146 | print(f"Rate limit headers: {e.response.headers}") # Debug rate limit info 147 | QMessageBox.warning(self, "Error", 148 | f"Rate limit exceeded for {feed_title}. Please try again later.") 149 | else: 150 | QMessageBox.warning(self, "Error", 151 | f"Could not fetch feed {feed_title}: {str(e)}") 152 | except Exception as e: 153 | print(f"Error fetching feed {feed_title}: {str(e)}") 154 | QMessageBox.warning(self, "Error", 155 | f"Could not fetch feed {feed_title}: {str(e)}") 156 | 157 | def refresh_feeds(self): 158 | self.refresh_current_feed() 159 | 160 | def add_feed(self): 161 | title, ok = QInputDialog.getText(self, 'Add RSS Feed', 'Feed Title:') 162 | if ok and title: 163 | url, ok = QInputDialog.getText(self, 'Add RSS Feed', 'Feed URL:') 164 | if ok and url: 165 | try: 166 | response = requests.get(url, timeout=10) 167 | response.raise_for_status() 168 | 169 | feed = feedparser.parse(response.text) 170 | if hasattr(feed, 'entries') and feed.entries: 171 | self.feeds[title] = url 172 | self.save_feeds() 173 | self.update_feed_selector() 174 | self.feed_selector.setCurrentText(title) 175 | else: 176 | QMessageBox.warning(self, "Error", "Invalid RSS feed") 177 | except Exception as e: 178 | QMessageBox.warning(self, "Error", f"Could not parse RSS feed: {str(e)}") 179 | 180 | def remove_feed(self): 181 | current_feed = self.feed_selector.currentText() 182 | if current_feed: 183 | reply = QMessageBox.question(self, 'Remove Feed', 184 | f'Remove feed "{current_feed}"?', 185 | QMessageBox.Yes | QMessageBox.No) 186 | if reply == QMessageBox.Yes: 187 | del self.feeds[current_feed] 188 | self.save_feeds() 189 | self.update_feed_selector() 190 | self.refresh_current_feed() 191 | 192 | def show_entry(self, current, previous): 193 | if current: 194 | entry = current.data(Qt.UserRole) 195 | content = f"

{entry.title}

" 196 | if hasattr(entry, 'published'): 197 | content += f"

Published: {entry.published}

" 198 | if hasattr(entry, 'description'): 199 | content += f"

{entry.description}

" 200 | if hasattr(entry, 'link'): 201 | content += f'

Read more...

' 202 | self.content_viewer.setHtml(content) 203 | 204 | def manage_feeds(self): 205 | dialog = FeedManagerDialog(self.feeds, self) 206 | if dialog.exec_() == QDialog.Accepted: 207 | self.feeds = dialog.get_feeds() 208 | self.save_feeds() 209 | self.update_feed_selector() 210 | self.refresh_current_feed() -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/rss_tab.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QWidget, QVBoxLayout 2 | from rss_reader import RSSReader 3 | 4 | class RSSTab(QWidget): 5 | def __init__(self, parent=None): 6 | super().__init__(parent) 7 | layout = QVBoxLayout(self) 8 | self.rss_reader = RSSReader() 9 | layout.addWidget(self.rss_reader) -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/settings_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, 2 | QLineEdit, QPushButton, QListWidget, QTabWidget, 3 | QWidget, QCheckBox, QMessageBox, QInputDialog) 4 | from PyQt5.QtCore import Qt 5 | import json 6 | import os 7 | 8 | class SettingsDialog(QDialog): 9 | def __init__(self, settings_manager, parent=None): 10 | super().__init__(parent) 11 | self.settings_manager = settings_manager 12 | self.setWindowTitle("Settings") 13 | self.setMinimumWidth(500) 14 | 15 | self.setup_ui() 16 | 17 | def setup_ui(self): 18 | """Setup the UI components""" 19 | # Create layout 20 | layout = QVBoxLayout(self) 21 | 22 | # Create tab widget 23 | tabs = QTabWidget() 24 | 25 | # Browser tab 26 | browser_tab = QWidget() 27 | browser_layout = QVBoxLayout(browser_tab) 28 | 29 | # Homepage setting 30 | homepage_layout = QHBoxLayout() 31 | homepage_label = QLabel("Homepage:") 32 | self.homepage_edit = QLineEdit() 33 | self.homepage_edit.setText(self.settings_manager.get_setting('homepage', 'https://www.apnews.com/')) 34 | homepage_layout.addWidget(homepage_label) 35 | homepage_layout.addWidget(self.homepage_edit) 36 | browser_layout.addLayout(homepage_layout) 37 | 38 | # Search sites 39 | search_label = QLabel("Site-specific searches:") 40 | browser_layout.addWidget(search_label) 41 | 42 | self.search_list = QListWidget() 43 | self.load_search_sites() 44 | browser_layout.addWidget(self.search_list) 45 | 46 | # Search site buttons 47 | search_buttons = QHBoxLayout() 48 | add_search = QPushButton("Add") 49 | edit_search = QPushButton("Edit") 50 | delete_search = QPushButton("Delete") 51 | add_search.clicked.connect(self.add_search_site) 52 | edit_search.clicked.connect(self.edit_search_site) 53 | delete_search.clicked.connect(self.delete_search_site) 54 | search_buttons.addWidget(add_search) 55 | search_buttons.addWidget(edit_search) 56 | search_buttons.addWidget(delete_search) 57 | browser_layout.addLayout(search_buttons) 58 | 59 | # Dictionary tab 60 | dict_tab = QWidget() 61 | dict_layout = QVBoxLayout(dict_tab) 62 | 63 | dict_label = QLabel("User Dictionary:") 64 | dict_layout.addWidget(dict_label) 65 | 66 | self.dict_list = QListWidget() 67 | self.load_user_dict() 68 | dict_layout.addWidget(self.dict_list) 69 | 70 | # Dictionary buttons 71 | dict_buttons = QHBoxLayout() 72 | add_word = QPushButton("Add Word") 73 | delete_word = QPushButton("Delete Word") 74 | add_word.clicked.connect(self.add_dict_word) 75 | delete_word.clicked.connect(self.delete_dict_word) 76 | dict_buttons.addWidget(add_word) 77 | dict_buttons.addWidget(delete_word) 78 | dict_layout.addLayout(dict_buttons) 79 | 80 | # Add tabs 81 | tabs.addTab(browser_tab, "Browser") 82 | tabs.addTab(dict_tab, "Dictionary") 83 | 84 | layout.addWidget(tabs) 85 | 86 | # Dialog buttons 87 | buttons = QHBoxLayout() 88 | ok_button = QPushButton("OK") 89 | cancel_button = QPushButton("Cancel") 90 | ok_button.clicked.connect(self.accept) 91 | cancel_button.clicked.connect(self.reject) 92 | buttons.addWidget(ok_button) 93 | buttons.addWidget(cancel_button) 94 | layout.addLayout(buttons) 95 | 96 | def load_search_sites(self): 97 | """Load search sites from settings""" 98 | sites = self.settings_manager.get_setting('search_sites', { 99 | 'AP News': 'site:apnews.com', 100 | 'Reuters': 'site:reuters.com', 101 | 'BBC News': 'site:bbc.com/news' 102 | }) 103 | for name, site in sites.items(): 104 | self.search_list.addItem(f"{name}: {site}") 105 | 106 | def load_user_dict(self): 107 | """Load user dictionary words""" 108 | words = self.settings_manager.get_setting('user_dictionary', []) 109 | self.dict_list.addItems(words) 110 | 111 | def add_search_site(self): 112 | """Add new search site""" 113 | dialog = SearchSiteDialog(self) 114 | if dialog.exec_(): 115 | name, site = dialog.get_data() 116 | self.search_list.addItem(f"{name}: {site}") 117 | 118 | def edit_search_site(self): 119 | """Edit selected search site""" 120 | current = self.search_list.currentItem() 121 | if current: 122 | name, site = current.text().split(': ', 1) 123 | dialog = SearchSiteDialog(self, name, site) 124 | if dialog.exec_(): 125 | new_name, new_site = dialog.get_data() 126 | current.setText(f"{new_name}: {new_site}") 127 | 128 | def delete_search_site(self): 129 | """Delete selected search site""" 130 | current = self.search_list.currentRow() 131 | if current >= 0: 132 | self.search_list.takeItem(current) 133 | 134 | def add_dict_word(self): 135 | """Add word to user dictionary""" 136 | word, ok = QInputDialog.getText(self, "Add Word", "Enter word:") 137 | if ok and word: 138 | self.dict_list.addItem(word) 139 | 140 | def delete_dict_word(self): 141 | """Delete word from user dictionary""" 142 | current = self.dict_list.currentRow() 143 | if current >= 0: 144 | self.dict_list.takeItem(current) 145 | 146 | def get_data(self): 147 | """Get dialog data""" 148 | return { 149 | 'homepage': self.homepage_edit.text(), 150 | 'search_sites': self.get_search_sites(), 151 | 'user_dictionary': self.get_user_dictionary() 152 | } 153 | 154 | def get_search_sites(self): 155 | """Get search sites from list widget""" 156 | sites = {} 157 | for i in range(self.search_list.count()): 158 | name, site = self.search_list.item(i).text().split(': ', 1) 159 | sites[name] = site 160 | return sites 161 | 162 | def get_user_dictionary(self): 163 | """Get words from dictionary list widget""" 164 | words = [] 165 | for i in range(self.dict_list.count()): 166 | words.append(self.dict_list.item(i).text()) 167 | return words 168 | 169 | class SearchSiteDialog(QDialog): 170 | def __init__(self, parent=None, name='', site=''): 171 | super().__init__(parent) 172 | self.setWindowTitle("Search Site") 173 | 174 | layout = QVBoxLayout(self) 175 | 176 | # Name field 177 | name_layout = QHBoxLayout() 178 | name_label = QLabel("Name:") 179 | self.name_edit = QLineEdit(name) 180 | name_layout.addWidget(name_label) 181 | name_layout.addWidget(self.name_edit) 182 | layout.addLayout(name_layout) 183 | 184 | # Site field 185 | site_layout = QHBoxLayout() 186 | site_label = QLabel("Site:") 187 | self.site_edit = QLineEdit(site) 188 | site_layout.addWidget(site_label) 189 | site_layout.addWidget(self.site_edit) 190 | layout.addLayout(site_layout) 191 | 192 | # Buttons 193 | buttons = QHBoxLayout() 194 | ok_button = QPushButton("OK") 195 | cancel_button = QPushButton("Cancel") 196 | ok_button.clicked.connect(self.accept) 197 | cancel_button.clicked.connect(self.reject) 198 | buttons.addWidget(ok_button) 199 | buttons.addWidget(cancel_button) 200 | layout.addLayout(buttons) 201 | 202 | def get_data(self): 203 | """Get dialog data""" 204 | return self.name_edit.text(), self.site_edit.text() -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/settings_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from PyQt5.QtGui import QFont 4 | import time 5 | 6 | class SettingsManager: 7 | def __init__(self): 8 | self.settings_file = os.path.join(os.path.expanduser("~"), ".editor_settings.json") 9 | self.settings = self.load_settings() 10 | 11 | # Create autosave directory 12 | self.autosave_dir = os.path.join(os.path.expanduser("~"), ".ap_editor_autosave") 13 | os.makedirs(self.autosave_dir, exist_ok=True) 14 | 15 | # Create running flag file 16 | self.running_flag = os.path.join(self.autosave_dir, "editor_running") 17 | # Set running flag 18 | with open(self.running_flag, 'w') as f: 19 | f.write(str(os.getpid())) 20 | 21 | # Setup backup and recovery directories 22 | self.backup_dir = os.path.join(os.path.expanduser("~"), ".ap_editor", "backups") 23 | self.recovery_dir = os.path.join(os.path.expanduser("~"), ".ap_editor", "recovery") 24 | os.makedirs(self.backup_dir, exist_ok=True) 25 | os.makedirs(self.recovery_dir, exist_ok=True) 26 | 27 | # Create session file 28 | self.session_file = os.path.join(self.recovery_dir, "session.json") 29 | self.create_session_file() 30 | 31 | # Create session state file 32 | self.session_state_file = os.path.join(self.recovery_dir, "session_state.json") 33 | 34 | # Initialize with unclean state 35 | self.initialize_session_state() 36 | 37 | # Clean up old session files if last exit was clean 38 | if os.path.exists(self.session_state_file): 39 | try: 40 | with open(self.session_state_file, 'r') as f: 41 | state = json.load(f) 42 | if state.get('clean_exit', False): 43 | self.cleanup_old_sessions() 44 | except: 45 | pass 46 | 47 | def load_settings(self): 48 | """Load all settings with defaults""" 49 | default_settings = { 50 | "theme": "Light", 51 | "font_family": "Consolas" if os.name == 'nt' else "DejaVu Sans Mono", 52 | "font_size": 10, 53 | "font_weight": 50, 54 | "font_italic": False, 55 | "show_snippets": True, 56 | "show_browser": True, 57 | "last_files": [], 58 | "homepage": "https://www.apnews.com/", 59 | "search_sites": { 60 | "AP News": "site:apnews.com", 61 | "Reuters": "site:reuters.com", 62 | "BBC News": "site:bbc.com/news" 63 | }, 64 | "user_dictionary": [], 65 | "start_focus_mode": False, 66 | "pane_states": { 67 | "snippets_visible": False, 68 | "browser_visible": False, 69 | "sizes": [700, 300, 300] 70 | } 71 | } 72 | 73 | try: 74 | if os.path.exists(self.settings_file): 75 | with open(self.settings_file, 'r') as f: 76 | saved_settings = json.load(f) 77 | # Merge saved settings with defaults 78 | return {**default_settings, **saved_settings} 79 | except Exception as e: 80 | print(f"Failed to load settings: {str(e)}") 81 | 82 | return default_settings 83 | 84 | def save_settings(self): 85 | with open(self.settings_file, 'w') as f: 86 | json.dump(self.settings, f) 87 | 88 | def get_font(self): 89 | font = QFont( 90 | self.settings["font_family"], 91 | self.settings["font_size"], 92 | self.settings["font_weight"] 93 | ) 94 | font.setItalic(self.settings["font_italic"]) 95 | return font 96 | 97 | def save_font(self, font): 98 | self.settings.update({ 99 | "font_family": font.family(), 100 | "font_size": font.pointSize(), 101 | "font_weight": font.weight(), 102 | "font_italic": font.italic() 103 | }) 104 | self.save_settings() 105 | 106 | def get_theme(self): 107 | return self.settings["theme"] 108 | 109 | def save_theme(self, theme): 110 | self.settings["theme"] = theme 111 | self.save_settings() 112 | 113 | def get_pane_visibility(self): 114 | return (self.settings["show_snippets"], self.settings["show_browser"]) 115 | 116 | def save_pane_visibility(self, show_snippets, show_browser): 117 | self.settings.update({ 118 | "show_snippets": show_snippets, 119 | "show_browser": show_browser 120 | }) 121 | self.save_settings() 122 | 123 | def save_last_files(self, files): 124 | """Save list of last opened files""" 125 | self.settings["last_files"] = files 126 | self.save_settings() 127 | 128 | def get_last_files(self): 129 | """Get list of last opened files""" 130 | return self.settings.get("last_files", []) 131 | 132 | def get_autosave_dir(self): 133 | """Get the directory for autosave files""" 134 | return self.autosave_dir 135 | 136 | def cleanup_autosave_dir(self): 137 | """Clean up old autosave files""" 138 | if os.path.exists(self.autosave_dir): 139 | try: 140 | # Remove files older than 7 days 141 | for filename in os.listdir(self.autosave_dir): 142 | filepath = os.path.join(self.autosave_dir, filename) 143 | if os.path.getmtime(filepath) < time.time() - 7 * 86400: 144 | os.remove(filepath) 145 | except: 146 | pass 147 | 148 | def clear_running_flag(self): 149 | """Clear the running flag on clean exit""" 150 | try: 151 | if os.path.exists(self.running_flag): 152 | os.remove(self.running_flag) 153 | except: 154 | pass 155 | 156 | def was_previous_crash(self): 157 | """Check if previous session crashed""" 158 | if os.path.exists(self.running_flag): 159 | try: 160 | with open(self.running_flag, 'r') as f: 161 | old_pid = int(f.read().strip()) 162 | # Check if the process is still running 163 | try: 164 | os.kill(old_pid, 0) 165 | # If we get here, the process is still running 166 | return False 167 | except OSError: 168 | # Process is not running, was a crash 169 | return True 170 | except: 171 | return True 172 | return False 173 | 174 | def create_session_file(self): 175 | """Create a session file to track clean/dirty exits""" 176 | session_data = { 177 | 'pid': os.getpid(), 178 | 'timestamp': time.time(), 179 | 'clean_exit': False 180 | } 181 | try: 182 | with open(self.session_file, 'w') as f: 183 | json.dump(session_data, f) 184 | except Exception as e: 185 | print(f"Failed to create session file: {str(e)}") 186 | 187 | def mark_clean_exit(self): 188 | """Mark that the editor exited cleanly""" 189 | try: 190 | if os.path.exists(self.session_file): 191 | with open(self.session_file, 'r') as f: 192 | session_data = json.load(f) 193 | session_data['clean_exit'] = True 194 | with open(self.session_file, 'w') as f: 195 | json.dump(session_data, f) 196 | except Exception as e: 197 | print(f"Failed to mark clean exit: {str(e)}") 198 | 199 | def needs_recovery(self): 200 | """Check if we need to recover from a crash""" 201 | try: 202 | if os.path.exists(self.session_file): 203 | with open(self.session_file, 'r') as f: 204 | session_data = json.load(f) 205 | return not session_data.get('clean_exit', True) 206 | return True # If no session file, assume we need recovery 207 | except Exception as e: 208 | print(f"Error checking recovery status: {str(e)}") 209 | return True # If we can't read the session file, assume we need recovery 210 | 211 | def get_backup_dir(self): 212 | return self.backup_dir 213 | 214 | def get_recovery_dir(self): 215 | return self.recovery_dir 216 | 217 | def initialize_session_state(self): 218 | """Initialize or update session state""" 219 | try: 220 | state = { 221 | 'clean_exit': False, 222 | 'timestamp': time.time(), 223 | 'open_tabs': [] 224 | } 225 | with open(self.session_state_file, 'w') as f: 226 | json.dump(state, f) 227 | except Exception as e: 228 | print(f"Failed to initialize session state: {str(e)}") 229 | 230 | def save_session_state(self, tab_ids, clean_exit=False): 231 | """Save the list of currently open tab IDs""" 232 | try: 233 | state = { 234 | 'open_tabs': tab_ids, 235 | 'clean_exit': clean_exit, 236 | 'timestamp': time.time() 237 | } 238 | with open(self.session_state_file, 'w') as f: 239 | json.dump(state, f) 240 | except Exception as e: 241 | print(f"Failed to save session state: {str(e)}") 242 | 243 | def get_session_state(self): 244 | """Get list of tab IDs that were open in last session""" 245 | try: 246 | if os.path.exists(self.session_state_file): 247 | with open(self.session_state_file, 'r') as f: 248 | state = json.load(f) 249 | return state.get('open_tabs', []) 250 | except Exception as e: 251 | print(f"Failed to load session state: {str(e)}") 252 | return [] 253 | 254 | def cleanup_old_sessions(self): 255 | """Clean up session files from previous clean exits""" 256 | # Don't clean up by default - let the session restore handle it 257 | pass 258 | 259 | def get_setting(self, key, default=None): 260 | """Get a setting value with a default fallback""" 261 | try: 262 | settings = self.load_settings() 263 | return settings.get(key, default) 264 | except Exception as e: 265 | print(f"Failed to get setting {key}: {str(e)}") 266 | return default 267 | 268 | def save_setting(self, key, value): 269 | """Save a single setting""" 270 | try: 271 | settings = self.load_settings() 272 | settings[key] = value 273 | with open(self.settings_file, 'w') as f: 274 | json.dump(settings, f) 275 | except Exception as e: 276 | print(f"Failed to save setting {key}: {str(e)}") -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/snippet_editor_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, 2 | QTextEdit, QPushButton, QLabel) 3 | 4 | class SnippetEditorDialog(QDialog): 5 | def __init__(self, title="", content="", parent=None): 6 | super().__init__(parent) 7 | self.setWindowTitle("Edit Snippet") 8 | self.setMinimumWidth(500) 9 | self.setMinimumHeight(400) 10 | 11 | layout = QVBoxLayout(self) 12 | 13 | # Title input 14 | title_layout = QHBoxLayout() 15 | title_label = QLabel("Title:") 16 | self.title_edit = QLineEdit(title) 17 | title_layout.addWidget(title_label) 18 | title_layout.addWidget(self.title_edit) 19 | layout.addLayout(title_layout) 20 | 21 | # Content input 22 | content_label = QLabel("Content:") 23 | layout.addWidget(content_label) 24 | self.content_edit = QTextEdit() 25 | self.content_edit.setPlainText(content) 26 | layout.addWidget(self.content_edit) 27 | 28 | # Buttons 29 | button_layout = QHBoxLayout() 30 | save_button = QPushButton("Save") 31 | cancel_button = QPushButton("Cancel") 32 | button_layout.addWidget(save_button) 33 | button_layout.addWidget(cancel_button) 34 | layout.addLayout(button_layout) 35 | 36 | save_button.clicked.connect(self.accept) 37 | cancel_button.clicked.connect(self.reject) 38 | 39 | def get_data(self): 40 | return { 41 | 'title': self.title_edit.text(), 42 | 'content': self.content_edit.toPlainText() 43 | } -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/snippet_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | class SnippetManager: 5 | def __init__(self): 6 | self.snippets = {} 7 | self.file_path = "snippets.json" 8 | self.load_snippets() 9 | 10 | def load_snippets(self): 11 | if os.path.exists(self.file_path): 12 | try: 13 | with open(self.file_path, 'r') as file: 14 | self.snippets = json.load(file) 15 | except: 16 | self.snippets = {} 17 | 18 | def save_snippets(self): 19 | with open(self.file_path, 'w') as file: 20 | json.dump(self.snippets, file) 21 | 22 | def add_snippet(self, title, text): 23 | self.snippets[title] = text 24 | self.save_snippets() 25 | 26 | def get_snippet(self, title): 27 | return self.snippets.get(title) 28 | 29 | def get_snippets(self): 30 | return list(self.snippets.keys()) 31 | 32 | def delete_snippet(self, title): 33 | if title in self.snippets: 34 | del self.snippets[title] 35 | self.save_snippets() 36 | 37 | def get_all_snippet_contents(self): 38 | """Return a list of all snippet contents""" 39 | return list(self.snippets.values()) -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/theme_manager.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtGui import QColor, QPalette 2 | from PyQt5.QtWidgets import QStyleFactory 3 | 4 | class ThemeManager: 5 | @staticmethod 6 | def get_themes(): 7 | return { 8 | "Light": { 9 | "bg": "#ffffff", 10 | "text": "#000000", 11 | "selection": "#b3d4fc" 12 | }, 13 | "Dark": { 14 | "bg": "#1e1e1e", 15 | "text": "#d4d4d4", 16 | "selection": "#264f78" 17 | }, 18 | "Sepia": { 19 | "bg": "#f4ecd8", 20 | "text": "#5b4636", 21 | "selection": "#c4b5a0" 22 | } 23 | } 24 | 25 | @staticmethod 26 | def apply_theme(editor, theme_name): 27 | themes = ThemeManager.get_themes() 28 | if theme_name in themes: 29 | theme = themes[theme_name] 30 | editor.setStyleSheet(f""" 31 | QTextEdit {{ 32 | background-color: {theme['bg']}; 33 | color: {theme['text']}; 34 | selection-background-color: {theme['selection']}; 35 | font-family: {editor.font().family()}; 36 | font-size: {editor.font().pointSize()}pt; 37 | }} 38 | """) -------------------------------------------------------------------------------- /packaging/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | 6 | # Override all Python-related build steps 7 | override_dh_auto_clean: 8 | # Skip Python build cleaning 9 | 10 | override_dh_auto_configure: 11 | # Skip Python configure 12 | 13 | override_dh_auto_build: 14 | # Skip Python build 15 | 16 | override_dh_auto_test: 17 | # Skip Python tests 18 | 19 | override_dh_installdocs: 20 | # Skip docs 21 | 22 | override_dh_installchangelogs: 23 | # Skip changelogs 24 | 25 | override_dh_auto_install: 26 | # Create directories 27 | mkdir -p debian/jottr/usr/bin 28 | mkdir -p debian/jottr/usr/share/applications 29 | mkdir -p debian/jottr/usr/share/jottr 30 | mkdir -p debian/jottr/usr/share/icons/hicolor/128x128/apps 31 | 32 | # Install program files 33 | cp -r src/jottr/* debian/jottr/usr/share/jottr/ 34 | 35 | # Install desktop file 36 | cp packaging/debian/jottr.desktop debian/jottr/usr/share/applications/ 37 | 38 | # Install icon (if it exists) 39 | if [ -f src/jottr/icons/jottr.png ]; then \ 40 | cp src/jottr/icons/jottr.png debian/jottr/usr/share/icons/hicolor/128x128/apps/; \ 41 | fi 42 | 43 | # Create launcher script 44 | echo '#!/bin/sh' > debian/jottr/usr/bin/jottr 45 | echo 'PYTHONPATH=/usr/share/jottr exec python3 /usr/share/jottr/main.py "$$@"' >> debian/jottr/usr/bin/jottr 46 | chmod +x debian/jottr/usr/bin/jottr 47 | -------------------------------------------------------------------------------- /packaging/debian/test-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Colors for output 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | NC='\033[0m' 8 | 9 | echo "Testing Jottr package..." 10 | 11 | # Find the latest built package 12 | DEB_PACKAGE=$(ls -t deb_dist/python3-jottr*.deb | head -n1) 13 | if [ ! -f "$DEB_PACKAGE" ]; then 14 | echo -e "${RED}Error: No .deb package found in deb_dist/${NC}" 15 | exit 1 16 | fi 17 | 18 | # Install the package 19 | echo "Installing package: $DEB_PACKAGE" 20 | sudo dpkg -i "$DEB_PACKAGE" || sudo apt-get install -f -y 21 | 22 | # Check if binary is installed 23 | if ! which jottr > /dev/null; then 24 | echo -e "${RED}Error: jottr binary not found in PATH${NC}" 25 | exit 1 26 | fi 27 | 28 | # Check if desktop file is installed 29 | if [ ! -f "/usr/share/applications/jottr.desktop" ]; then 30 | echo -e "${RED}Error: Desktop file not installed${NC}" 31 | exit 1 32 | fi 33 | 34 | # Check if icon is installed 35 | if [ ! -f "/usr/share/icons/hicolor/128x128/apps/jottr.png" ]; then 36 | echo -e "${RED}Error: Application icon not installed${NC}" 37 | exit 1 38 | fi 39 | 40 | # Try to import the module 41 | if ! python3 -c "import jottr" 2>/dev/null; then 42 | echo -e "${RED}Error: Cannot import jottr module${NC}" 43 | exit 1 44 | fi 45 | 46 | echo -e "${GREEN}All tests passed successfully!${NC}" 47 | 48 | # Optional: Remove the package 49 | read -p "Do you want to remove the test package? [y/N] " -n 1 -r 50 | echo 51 | if [[ $REPLY =~ ^[Yy]$ ]]; then 52 | sudo apt-get remove -y python3-jottr 53 | fi 54 | -------------------------------------------------------------------------------- /packaging/rpm/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$1 4 | PACKAGE_NAME="jottr" 5 | PACKAGE_VERSION="${VERSION:-1.0.0}" 6 | 7 | # Create RPM build structure 8 | mkdir -p {BUILD,RPMS,SOURCES,SPECS,SRPMS} 9 | 10 | # Create source distribution 11 | cd ../.. 12 | python -m build --sdist 13 | cd packaging/rpm 14 | cp ../../dist/${PACKAGE_NAME}-${PACKAGE_VERSION}.tar.gz SOURCES/ 15 | 16 | # Create spec file 17 | cat > SPECS/jottr.spec << EOF 18 | %global __python %{__python3} 19 | 20 | Name: python3-jottr 21 | Version: ${PACKAGE_VERSION} 22 | Release: 1%{?dist} 23 | Summary: Modern text editor for writers 24 | 25 | License: MIT 26 | URL: https://github.com/yourusername/jottr 27 | Source0: %{name}-%{version}.tar.gz 28 | BuildArch: noarch 29 | 30 | BuildRequires: python3-devel 31 | BuildRequires: python3-setuptools 32 | Requires: python3-qt5 33 | Requires: python3-qt5-webengine 34 | Requires: python3-enchant 35 | %description 36 | Jottr is a feature-rich text editor designed specifically 37 | for writers and journalists, with features like smart 38 | completion, snippets, and integrated web browsing. 39 | 40 | %prep 41 | %autosetup -n jottr-%{version} 42 | 43 | %build 44 | %py3_build 45 | 46 | %install 47 | %py3_install 48 | 49 | # Install desktop file 50 | mkdir -p %{buildroot}%{_datadir}/applications/ 51 | cat > %{buildroot}%{_datadir}/applications/jottr.desktop << EOL 52 | [Desktop Entry] 53 | Name=Jottr 54 | Comment=Modern text editor for writers 55 | Exec=jottr 56 | Icon=jottr 57 | Terminal=false 58 | Type=Application 59 | Categories=Office;TextEditor; 60 | EOL 61 | 62 | %files 63 | %{python3_sitelib}/jottr/ 64 | %{python3_sitelib}/jottr-%{version}* 65 | %{_bindir}/jottr 66 | %{_datadir}/applications/jottr.desktop 67 | 68 | %changelog 69 | * $(date '+%a %b %d %Y') Package Builder - ${PACKAGE_VERSION}-1 70 | - Initial package release 71 | EOF 72 | 73 | # Build RPM 74 | rpmbuild --define "_topdir $(pwd)" -bb SPECS/jottr.spec -------------------------------------------------------------------------------- /pypi-dependencies.yaml: -------------------------------------------------------------------------------- 1 | # Generated with flatpak-pip-generator --runtime=org.kde.Sdk//5.15 --yaml --requirements-file=runtime-requirements.txt --output=pypi-dependencies 2 | build-commands: [] 3 | buildsystem: simple 4 | modules: 5 | - name: python3-pyenchant 6 | buildsystem: simple 7 | build-commands: 8 | - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" 9 | --prefix=${FLATPAK_DEST} "pyenchant>=3.2.0" --no-build-isolation 10 | sources: 11 | - type: file 12 | url: https://files.pythonhosted.org/packages/54/4c/a741dddab6ad96f257d90cb4d23067ffadac526c9cab3a99ca6ce3c05477/pyenchant-3.2.2-py3-none-any.whl 13 | sha256: 5facc821ece957208a81423af7d6ec7810dad29697cb0d77aae81e4e11c8e5a6 14 | - name: python3-feedparser 15 | buildsystem: simple 16 | build-commands: 17 | - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" 18 | --prefix=${FLATPAK_DEST} "feedparser>=6.0.0" --no-build-isolation 19 | sources: 20 | - type: file 21 | url: https://files.pythonhosted.org/packages/7c/d4/8c31aad9cc18f451c49f7f9cfb5799dadffc88177f7917bc90a66459b1d7/feedparser-6.0.11-py3-none-any.whl 22 | sha256: 0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45 23 | - type: file 24 | url: https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz 25 | sha256: 7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9 26 | - name: python3-requests 27 | buildsystem: simple 28 | build-commands: 29 | - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" 30 | --prefix=${FLATPAK_DEST} "requests==2.31.0" --no-build-isolation 31 | sources: 32 | - type: file 33 | url: https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl 34 | sha256: ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe 35 | - type: file 36 | url: https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl 37 | sha256: d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 38 | - type: file 39 | url: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl 40 | sha256: 946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 41 | - type: file 42 | url: https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl 43 | sha256: 58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f 44 | - type: file 45 | url: https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl 46 | sha256: ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac 47 | name: pypi-dependencies 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # System dependencies: 2 | # macOS: 3 | # brew install hunspell 4 | # brew install hunspell-en 5 | # Ubuntu/Debian: 6 | # sudo apt-get install hunspell hunspell-en-us hunspell-en-gb 7 | # Fedora: 8 | # sudo dnf install hunspell hunspell-en 9 | 10 | PyQt5>=5.15.0 11 | PyQtWebEngine>=5.15.0 12 | pyenchant>=3.2.0 13 | feedparser>=6.0.0 14 | requests==2.31.0 15 | pyinstaller>=4.5.1 16 | dmgbuild>=1.4.2 17 | pyspellchecker>=0.7.2 -------------------------------------------------------------------------------- /rpm.spec: -------------------------------------------------------------------------------- 1 | Name: jottr 2 | Version: 1.4.3 3 | Release: 1%{?dist} 4 | Summary: A simple text editor for writers, journalists and researchers 5 | 6 | License: GPLv3 7 | URL: https://github.com/mfat/jottr 8 | Source0: %{name}-%{version}.tar.gz 9 | 10 | BuildArch: noarch 11 | BuildRequires: python3-devel 12 | 13 | Requires: python3 14 | Requires: python3-qt5 15 | Requires: python3-qt5-webengine 16 | Requires: python3-feedparser 17 | Requires: python3-enchant 18 | Requires: python3-pyqt5-sip 19 | Requires: qt5-qtsvg 20 | Requires: python3-pyxdg 21 | 22 | %description 23 | Jottr is a simple text editor designed specifically for writers, journalists, 24 | and researchers. 25 | 26 | %prep 27 | %autosetup 28 | 29 | %install 30 | # Install application files 31 | mkdir -p %{buildroot}%{_datadir}/%{name} 32 | cp -r src/jottr/* %{buildroot}%{_datadir}/%{name}/ 33 | 34 | # Create executable script 35 | mkdir -p %{buildroot}%{_bindir} 36 | cat > %{buildroot}%{_bindir}/%{name} << EOF 37 | #!/bin/bash 38 | # Get the absolute path of any file arguments 39 | args=() 40 | for arg in "\$@"; do 41 | if [ -f "\$arg" ]; then 42 | args+=("\$(readlink -f "\$arg")") 43 | else 44 | args+=("\$arg") 45 | fi 46 | done 47 | 48 | # Pass command-line arguments to the application 49 | exec python3 %{_datadir}/%{name}/main.py "\${args[@]}" 50 | EOF 51 | chmod 755 %{buildroot}%{_bindir}/%{name} 52 | 53 | # Create desktop entry 54 | mkdir -p %{buildroot}%{_datadir}/applications 55 | cat > %{buildroot}%{_datadir}/applications/%{name}.desktop << EOF 56 | [Desktop Entry] 57 | Name=Jottr 58 | Comment=Text editor for writers 59 | Exec=jottr %F 60 | Icon=jottr 61 | Terminal=false 62 | Type=Application 63 | Categories=Utility;TextEditor; 64 | MimeType=text/plain;text/markdown;text/x-markdown; 65 | StartupNotify=true 66 | EOF 67 | 68 | # Add icons 69 | mkdir -p %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/ 70 | install -p -m 644 icons/jottr.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/%{name}.png 71 | 72 | %files 73 | %license LICENSE 74 | %doc README.md 75 | %{_datadir}/%{name} 76 | %{_bindir}/%{name} 77 | %{_datadir}/applications/%{name}.desktop 78 | %{_datadir}/icons/hicolor/256x256/apps/%{name}.png 79 | 80 | %changelog 81 | * Sat Mar 01 2025 mFat - 1.4.3-1 82 | - Bug fixes and improvements 83 | 84 | -------------------------------------------------------------------------------- /rss_feeds.json: -------------------------------------------------------------------------------- 1 | {"BBC World": "https://feeds.bbci.co.uk/news/world/rss.xml", "Reuters Top News": "https://feeds.reuters.com/reuters/topNews", "Al Jazeera": "https://www.aljazeera.com/xml/rss/all.xml", "CNN Top Stories": "http://rss.cnn.com/rss/edition.rss", "AP Top News": "https://apnews.com/feed", "AP World News": "https://apnews.com/hub/world-news/feed", "AP Middle East": "https://rsshub.app/apnews/topics/middle-east"} -------------------------------------------------------------------------------- /runtime-requirements.txt: -------------------------------------------------------------------------------- 1 | # System dependencies: 2 | # macOS: 3 | # brew install hunspell 4 | # brew install hunspell-en 5 | # Ubuntu/Debian: 6 | # sudo apt-get install hunspell hunspell-en-us hunspell-en-gb 7 | # Fedora: 8 | # sudo dnf install hunspell hunspell-en 9 | 10 | pyenchant>=3.2.0 11 | feedparser>=6.0.0 12 | requests==2.31.0 13 | -------------------------------------------------------------------------------- /screenshots/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/screenshots/main.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="jottr", 5 | version="1.0", 6 | description="Modern text editor for writers and journalists", 7 | author="mFat", 8 | author_email="newmfat@gmail.com", 9 | url="https://github.com/mfat/jottr", 10 | ) -------------------------------------------------------------------------------- /snippets.json: -------------------------------------------------------------------------------- 1 | {"sb": "SOUNDBITE (Farsi) "} -------------------------------------------------------------------------------- /src/jottr.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: jottr 3 | Version: 1.0 4 | Summary: Modern text editor for writers and journalists 5 | Home-page: https://github.com/mfat/jottr 6 | Author: mFat 7 | Author-email: newmfat@gmail.com 8 | -------------------------------------------------------------------------------- /src/jottr.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | README.md 2 | setup.py 3 | src/jottr/__init__.py 4 | src/jottr/editor_tab.py 5 | src/jottr/feed_manager_dialog.py 6 | src/jottr/main.py 7 | src/jottr/rss_reader.py 8 | src/jottr/rss_tab.py 9 | src/jottr/settings_dialog.py 10 | src/jottr/settings_manager.py 11 | src/jottr/snippet_editor_dialog.py 12 | src/jottr/snippet_manager.py 13 | src/jottr/theme_manager.py 14 | src/jottr.egg-info/PKG-INFO 15 | src/jottr.egg-info/SOURCES.txt 16 | src/jottr.egg-info/dependency_links.txt 17 | src/jottr.egg-info/top_level.txt -------------------------------------------------------------------------------- /src/jottr.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/jottr.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | jottr 2 | -------------------------------------------------------------------------------- /src/jottr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/src/jottr/__init__.py -------------------------------------------------------------------------------- /src/jottr/feed_manager_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, 2 | QTableWidget, QTableWidgetItem, QInputDialog, 3 | QMessageBox, QHeaderView) 4 | import requests 5 | import feedparser 6 | 7 | class FeedManagerDialog(QDialog): 8 | def __init__(self, feeds, parent=None): 9 | super().__init__(parent) 10 | self.feeds = feeds.copy() # Work with a copy of the feeds 11 | self.setWindowTitle("Feed Manager") 12 | self.setMinimumWidth(600) 13 | self.setMinimumHeight(400) 14 | self.setup_ui() 15 | 16 | def setup_ui(self): 17 | layout = QVBoxLayout(self) 18 | 19 | # Create table 20 | self.table = QTableWidget(0, 2) 21 | self.table.setHorizontalHeaderLabels(["Feed Title", "URL"]) 22 | self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) 23 | self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) 24 | layout.addWidget(self.table) 25 | 26 | # Buttons 27 | button_layout = QHBoxLayout() 28 | 29 | add_button = QPushButton("Add Feed") 30 | edit_button = QPushButton("Edit Feed") 31 | remove_button = QPushButton("Remove Feed") 32 | test_button = QPushButton("Test Feed") 33 | 34 | add_button.clicked.connect(self.add_feed) 35 | edit_button.clicked.connect(self.edit_feed) 36 | remove_button.clicked.connect(self.remove_feed) 37 | test_button.clicked.connect(self.test_feed) 38 | 39 | button_layout.addWidget(add_button) 40 | button_layout.addWidget(edit_button) 41 | button_layout.addWidget(remove_button) 42 | button_layout.addWidget(test_button) 43 | button_layout.addStretch() 44 | 45 | ok_button = QPushButton("OK") 46 | cancel_button = QPushButton("Cancel") 47 | 48 | ok_button.clicked.connect(self.accept) 49 | cancel_button.clicked.connect(self.reject) 50 | 51 | button_layout.addWidget(ok_button) 52 | button_layout.addWidget(cancel_button) 53 | 54 | layout.addLayout(button_layout) 55 | 56 | # Populate table 57 | self.refresh_table() 58 | 59 | def refresh_table(self): 60 | self.table.setRowCount(0) 61 | for title, url in sorted(self.feeds.items()): 62 | row = self.table.rowCount() 63 | self.table.insertRow(row) 64 | self.table.setItem(row, 0, QTableWidgetItem(title)) 65 | self.table.setItem(row, 1, QTableWidgetItem(url)) 66 | 67 | def add_feed(self): 68 | title, ok = QInputDialog.getText(self, 'Add Feed', 'Feed Title:') 69 | if ok and title: 70 | if title in self.feeds: 71 | QMessageBox.warning(self, "Error", "A feed with this title already exists") 72 | return 73 | 74 | url, ok = QInputDialog.getText(self, 'Add Feed', 'Feed URL:') 75 | if ok and url: 76 | # Don't test by default, just add 77 | self.feeds[title] = url 78 | self.refresh_table() 79 | 80 | def edit_feed(self): 81 | current_row = self.table.currentRow() 82 | if current_row < 0: 83 | QMessageBox.warning(self, "Error", "Please select a feed to edit") 84 | return 85 | 86 | old_title = self.table.item(current_row, 0).text() 87 | old_url = self.table.item(current_row, 1).text() 88 | 89 | title, ok = QInputDialog.getText(self, 'Edit Feed', 'Feed Title:', 90 | text=old_title) 91 | if ok and title: 92 | if title != old_title and title in self.feeds: 93 | QMessageBox.warning(self, "Error", "A feed with this title already exists") 94 | return 95 | 96 | url, ok = QInputDialog.getText(self, 'Edit Feed', 'Feed URL:', 97 | text=old_url) 98 | if ok and url: 99 | # Don't test by default, just update 100 | if old_title in self.feeds: 101 | del self.feeds[old_title] 102 | self.feeds[title] = url 103 | self.refresh_table() 104 | 105 | def remove_feed(self): 106 | current_row = self.table.currentRow() 107 | if current_row < 0: 108 | QMessageBox.warning(self, "Error", "Please select a feed to remove") 109 | return 110 | 111 | title = self.table.item(current_row, 0).text() 112 | reply = QMessageBox.question(self, 'Remove Feed', 113 | f'Remove feed "{title}"?', 114 | QMessageBox.Yes | QMessageBox.No) 115 | if reply == QMessageBox.Yes: 116 | del self.feeds[title] 117 | self.refresh_table() 118 | 119 | def test_feed(self): 120 | current_row = self.table.currentRow() 121 | if current_row < 0: 122 | QMessageBox.warning(self, "Error", "Please select a feed to test") 123 | return 124 | 125 | url = self.table.item(current_row, 1).text() 126 | if self.test_feed_url(url): 127 | QMessageBox.information(self, "Success", "Feed is valid and accessible") 128 | 129 | def test_feed_url(self, url): 130 | try: 131 | response = requests.get(url, timeout=10) 132 | response.raise_for_status() 133 | 134 | feed = feedparser.parse(response.text) 135 | if hasattr(feed, 'entries') and feed.entries: 136 | return True 137 | else: 138 | QMessageBox.warning(self, "Error", "Invalid RSS feed (no entries found)") 139 | return False 140 | except Exception as e: 141 | QMessageBox.warning(self, "Error", f"Could not parse RSS feed: {str(e)}") 142 | return False 143 | 144 | def get_feeds(self): 145 | return self.feeds -------------------------------------------------------------------------------- /src/jottr/help/help.md: -------------------------------------------------------------------------------- 1 | # Jottr Help 2 | 3 | ## Themes 4 | The editor supports multiple color themes that can be changed in Settings: 5 | - Light theme: Default light color scheme 6 | - Dark theme: Dark color scheme for low-light environments 7 | - Sepia theme: Warm, paper-like theme for comfortable reading 8 | 9 | To change the theme: 10 | 1. Click the Theme button in the toolbar 11 | 2. Select your preferred theme 12 | 3. Changes are applied immediately 13 | 14 | ## Font Customization 15 | Customize the editor font to your preference: 16 | 17 | 1. Click the Font button in the toolbar 18 | 2. Select your preferred: 19 | - Font family 20 | - Font size 21 | - Font style 22 | 3. Changes are applied immediately 23 | 24 | ## Focus Mode 25 | Enter distraction-free writing mode: 26 | 27 | - Click the Focus Mode button in the toolbar or press Ctrl+Shift+D (or Cmd+Shift+D on Mac) 28 | - Hides side panels for distraction-free writing 29 | - Click exit button, Escape key or Ctrl+Shift+D (or Cmd+Shift+D on Mac) to exit focus mode 30 | 31 | ## User Dictionary 32 | The editor maintains a custom dictionary for your frequently used words: 33 | 34 | - Words you add to the dictionary won't be marked as misspelled 35 | - These words will also appear as autocomplete suggestions 36 | - Manage your dictionary in Settings > User Dictionary 37 | 38 | To add words: 39 | 1. Right-click on a word 40 | 2. Select "Add to Dictionary" 41 | 42 | ## Word Completion 43 | The editor suggests completions from your user dictionary as you type: 44 | 45 | - Start typing a word (at least 2 characters) 46 | - If a matching word exists in your dictionary, it appears in grey 47 | - Press Tab or Enter to accept the suggestion 48 | - The suggestion disappears if you continue typing 49 | 50 | ## Snippets 51 | Snippets are reusable text blocks that can be quickly inserted with mouse or keyboard: 52 | 53 | - Create snippets for frequently used text 54 | - Access snippets from the side panel 55 | - Insert a snippet by double-clicking it or using typing the snippet name. 56 | 57 | 58 | To create a snippet: 59 | 1. Select text you want to save 60 | 2. Right-click and choose "Save as Snippet" 61 | 3. Enter a name for your snippet 62 | 63 | To use snippets: 64 | 1. Click the Snippets button to show the panel 65 | 2. Double-click a snippet to insert it 66 | 3. Or start typing the snippet name for auto-completion 67 | 68 | ## Browser Panel 69 | The integrated browser panel allows quick web access: 70 | 71 | - Toggle the browser with the Browser button 72 | - Enter URLs directly in the address bar 73 | - Use for quick reference while writing 74 | - Default homepage can be set in Settings 75 | 76 | ## Site-Specific Searches 77 | Quickly search selected text on specific news sites. You can add any website frm Settings, and Google-search inside that site from the context menu. 78 | 79 | 1. Select text in the editor 80 | 2. Right-click and choose "Search in..." 81 | 3. Select a news site: 82 | - AP News 83 | - Reuters 84 | - BBC News 85 | - Or use regular Google search 86 | 87 | Configure search sites: 88 | 1. Open Settings 89 | 2. Add or modify sites in the Search Sites section 90 | 91 | ## Keyboard Shortcuts 92 | Common operations: 93 | - Ctrl+N: New document 94 | - Ctrl+O: Open file 95 | - Ctrl+S: Save file 96 | - Ctrl+Z: Undo 97 | - Ctrl+Shift+Z: Redo 98 | - Ctrl++: Zoom in 99 | - Ctrl+-: Zoom out 100 | - Ctrl+0: Reset zoom 101 | - Ctrl+Shift+D: Toggle focus mode (cmd+shift+d on mac) 102 | - Escape: Exit focus mode 103 | - Ctrl+F: Find -------------------------------------------------------------------------------- /src/jottr/icons/jottr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/src/jottr/icons/jottr.png -------------------------------------------------------------------------------- /src/jottr/jottr-mac.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | import os 6 | import glob 7 | 8 | # Enchant and Aspell paths 9 | #enchant_cellar = '/usr/local/Cellar/enchant/2.8.2' 10 | aspell_cellar = '/usr/local/Cellar/aspell/0.60.8.1_1' 11 | 12 | # Binaries (libraries) 13 | # enchant_lib_dir = os.path.join(enchant_cellar, 'lib') 14 | # aspell_lib_dir = os.path.join(aspell_cellar, 'lib', 'aspell-0.60') 15 | # binaries = [ 16 | # (os.path.join(enchant_lib_dir, 'libenchant-2.dylib'), '.'), 17 | # (os.path.join(enchant_lib_dir, 'enchant-2', 'enchant_aspell.so'), 'enchant-2'), 18 | # (os.path.join(enchant_lib_dir, 'enchant-2', 'enchant_applespell.so'), 'enchant-2'), 19 | # ('/usr/local/opt/glib/lib/libglib-2.0.0.dylib', '.'), 20 | # ('/usr/local/opt/glib/lib/libgobject-2.0.0.dylib', '.'), 21 | # ('/usr/local/opt/glib/lib/libgmodule-2.0.0.dylib', '.'), 22 | # ('/usr/local/opt/gettext/lib/libintl.8.dylib', '.'), 23 | # ] 24 | 25 | # Datas (app files and enchant module) 26 | datas = [ 27 | ('editor_tab.py', '.'), 28 | ('snippet_manager.py', '.'), 29 | ('rss_tab.py', '.'), 30 | ('theme_manager.py', '.'), 31 | ('settings_manager.py', '.'), 32 | ('settings_dialog.py', '.'), 33 | ('snippet_editor_dialog.py', '.'), 34 | ('rss_reader.py', '.'), 35 | ('icons', 'icons'), 36 | ('help', 'help'), 37 | ('/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/enchant', 'enchant'), 38 | ('/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/spellchecker/resources', 'spellchecker/resources'), 39 | ] 40 | 41 | # Add Aspell dictionaries 42 | # aspell_files = glob.glob(os.path.join(aspell_lib_dir, '*.multi')) + \ 43 | # glob.glob(os.path.join(aspell_lib_dir, '*.rws')) + \ 44 | # glob.glob(os.path.join(aspell_lib_dir, '*.dat')) 45 | # for dict_file in aspell_files: 46 | # datas.append((dict_file, 'aspell')) 47 | 48 | a = Analysis(['main.py'], 49 | pathex=['/Users/mahdi/GitHub/jottr/src/jottr'], 50 | #binaries=binaries, 51 | datas=datas, 52 | hiddenimports=['ctypes', 'ctypes.util', 'pyenchant', 'pyspellchecker'], 53 | hookspath=[], 54 | runtime_hooks=[], 55 | excludes=[], 56 | cipher=block_cipher, 57 | noarchive=False) 58 | 59 | pyz = PYZ(a.pure, a.zipped_data, 60 | cipher=block_cipher) 61 | 62 | exe = EXE(pyz, 63 | a.scripts, 64 | [], 65 | exclude_binaries=True, 66 | name='jottr', 67 | debug=False, 68 | bootloader_ignore_signals=False, 69 | strip=False, 70 | upx=True, 71 | console=False, 72 | icon='app.ico') 73 | 74 | coll = COLLECT(exe, 75 | a.binaries, 76 | a.zipfiles, 77 | a.datas, 78 | strip=False, 79 | upx=True, 80 | upx_exclude=[], 81 | name='jottr') 82 | 83 | app = BUNDLE(coll, 84 | name='Jottr.app', 85 | icon='jottr_icon.icns', 86 | bundle_identifier='io.github.mfat.jottr') -------------------------------------------------------------------------------- /src/jottr/jottr-windows.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | a = Analysis(['main.py'], 6 | pathex=['C:\\Users\\Mahdi\\Github\\jottr\\src\\jottr'], 7 | binaries=[('C:\\Python\\Python313\\Lib\\site-packages\\enchant\\data\\mingw64\\bin\\libenchant-2.dll', 'enchant'), 8 | ('C:\\Python\\Python313\\Lib\\site-packages\\enchant\\data\\mingw64\\bin\\libglib-2.0-0.dll', '.'), 9 | ('C:\\Python\\Python313\\Lib\\site-packages\\enchant\\data\\mingw64\\bin\\libhunspell-1.6-0.dll', '.')], 10 | datas=[('editor_tab.py', '.'), 11 | ('snippet_manager.py', '.'), 12 | ('rss_tab.py', '.'), 13 | ('theme_manager.py', '.'), 14 | ('settings_manager.py', '.'), 15 | ('settings_dialog.py', '.'), 16 | ('snippet_editor_dialog.py', '.'), 17 | ('rss_reader.py', '.'), 18 | ('icons', 'icons'), 19 | ('help', 'help'), 20 | ('C:\\Python\\Python313\\Lib\\site-packages\\enchant', 'enchant')], 21 | hiddenimports=['ctypes', 'ctypes.util', 'pyenchant'], 22 | hookspath=[], 23 | runtime_hooks=[], 24 | excludes=[], 25 | win_no_prefer_redirects=False, 26 | win_private_assemblies=False, 27 | cipher=block_cipher, 28 | noarchive=False) 29 | pyz = PYZ(a.pure, a.zipped_data, 30 | cipher=block_cipher) 31 | exe = EXE(pyz, 32 | a.scripts, 33 | [], 34 | exclude_binaries=True, 35 | name='jottr', 36 | debug=False, 37 | bootloader_ignore_signals=False, 38 | strip=False, 39 | upx=True, 40 | console=False, 41 | icon='app.ico') 42 | coll = COLLECT(exe, 43 | a.binaries, 44 | a.zipfiles, 45 | a.datas, 46 | strip=False, 47 | upx=True, 48 | upx_exclude=[], 49 | name='jottr') -------------------------------------------------------------------------------- /src/jottr/jottr_icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfat/jottr/cd85b9016b95afc834ed05248427c72dc297a99d/src/jottr/jottr_icon.icns -------------------------------------------------------------------------------- /src/jottr/rss_reader.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QListWidget, 2 | QTextBrowser, QPushButton, QInputDialog, QMessageBox, 3 | QComboBox, QListWidgetItem, QDialog) 4 | from PyQt5.QtCore import Qt, QUrl 5 | import feedparser 6 | import json 7 | import os 8 | import requests 9 | from feed_manager_dialog import FeedManagerDialog 10 | 11 | class RSSReader(QWidget): 12 | def __init__(self, parent=None): 13 | super().__init__(parent) 14 | self.feeds = { 15 | "BBC World": "https://feeds.bbci.co.uk/news/world/rss.xml", 16 | "Reuters Top News": "https://feeds.reuters.com/reuters/topNews", 17 | "Al Jazeera": "https://www.aljazeera.com/xml/rss/all.xml", 18 | "CNN Top Stories": "http://rss.cnn.com/rss/edition.rss", 19 | # AP News feeds 20 | "AP Top News": "https://apnews.com/feed", 21 | "AP World News": "https://apnews.com/hub/world-news/feed", 22 | "AP Middle East": "https://apnews.com/hub/middle-east/feed" 23 | } 24 | self.feed_file = "rss_feeds.json" 25 | self.setup_ui() 26 | self.load_feeds() 27 | 28 | def setup_ui(self): 29 | layout = QVBoxLayout(self) 30 | 31 | # Top controls layout 32 | controls_layout = QHBoxLayout() 33 | 34 | # Feed selector dropdown 35 | self.feed_selector = QComboBox() 36 | self.feed_selector.currentTextChanged.connect(self.on_feed_selected) 37 | controls_layout.addWidget(self.feed_selector) 38 | 39 | # Buttons 40 | add_button = QPushButton("Add Feed") 41 | remove_button = QPushButton("Remove Feed") 42 | refresh_button = QPushButton("Refresh") 43 | manage_button = QPushButton("Manage Feeds") 44 | 45 | add_button.clicked.connect(self.add_feed) 46 | remove_button.clicked.connect(self.remove_feed) 47 | refresh_button.clicked.connect(self.refresh_feeds) 48 | manage_button.clicked.connect(self.manage_feeds) 49 | 50 | controls_layout.addWidget(manage_button) 51 | controls_layout.addWidget(add_button) 52 | controls_layout.addWidget(remove_button) 53 | controls_layout.addWidget(refresh_button) 54 | controls_layout.addStretch() 55 | 56 | layout.addLayout(controls_layout) 57 | 58 | # Feed entries list 59 | self.entries_list = QListWidget() 60 | self.entries_list.currentItemChanged.connect(self.show_entry) 61 | layout.addWidget(self.entries_list) 62 | 63 | # Content viewer 64 | self.content_viewer = QTextBrowser() 65 | self.content_viewer.setOpenExternalLinks(True) 66 | layout.addWidget(self.content_viewer) 67 | 68 | # Set size ratio between list and content 69 | layout.setStretch(1, 1) 70 | layout.setStretch(2, 2) 71 | 72 | def load_feeds(self): 73 | if os.path.exists(self.feed_file): 74 | try: 75 | with open(self.feed_file, 'r') as f: 76 | loaded_feeds = json.load(f) 77 | self.feeds.update(loaded_feeds) # Merge with default feeds 78 | except: 79 | pass # Keep default feeds if file load fails 80 | 81 | self.save_feeds() # Save combined feeds 82 | self.update_feed_selector() 83 | 84 | def save_feeds(self): 85 | with open(self.feed_file, 'w') as f: 86 | json.dump(self.feeds, f) 87 | 88 | def update_feed_selector(self): 89 | self.feed_selector.clear() 90 | self.feed_selector.addItems(sorted(self.feeds.keys())) 91 | 92 | def on_feed_selected(self, feed_title): 93 | # Don't automatically refresh when feed is selected 94 | pass 95 | 96 | def refresh_current_feed(self): 97 | self.entries_list.clear() 98 | self.content_viewer.clear() 99 | 100 | feed_title = self.feed_selector.currentText() 101 | if not feed_title or feed_title not in self.feeds: 102 | return 103 | 104 | url = self.feeds[feed_title] 105 | try: 106 | # Enhanced headers especially for RSSHub 107 | headers = { 108 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 109 | 'Accept': 'application/rss+xml, application/xml, application/json, */*', 110 | 'Accept-Language': 'en-US,en;q=0.9', 111 | 'Cache-Control': 'no-cache', 112 | 'Pragma': 'no-cache', 113 | 'Connection': 'keep-alive' 114 | } 115 | 116 | # Special handling for RSSHub 117 | if 'rsshub.app' in url: 118 | # Try direct feedparser first 119 | feed = feedparser.parse(url) 120 | if not hasattr(feed, 'entries') or not feed.entries: 121 | # If direct parsing fails, try with requests 122 | response = requests.get(url, timeout=10, headers=headers) 123 | response.raise_for_status() 124 | feed = feedparser.parse(response.text) 125 | else: 126 | # Normal handling for other feeds 127 | response = requests.get(url, timeout=10, headers=headers) 128 | response.raise_for_status() 129 | feed = feedparser.parse(response.text) 130 | 131 | if hasattr(feed, 'entries') and feed.entries: 132 | for entry in feed.entries: 133 | item_text = entry.title if hasattr(entry, 'title') else 'No Title' 134 | list_item = QListWidgetItem(item_text) 135 | list_item.setData(Qt.UserRole, entry) 136 | self.entries_list.addItem(list_item) 137 | else: 138 | print(f"Feed {feed_title} has no entries. Feed status: {feed.get('status', 'unknown')}") 139 | print(f"Feed bozo: {feed.get('bozo', 'unknown')}") 140 | if hasattr(feed, 'debug_message'): 141 | print(f"Feed debug: {feed.debug_message}") 142 | QMessageBox.warning(self, "Error", f"No entries found in feed: {feed_title}") 143 | 144 | except requests.exceptions.HTTPError as e: 145 | if e.response.status_code == 429: 146 | print(f"Rate limit headers: {e.response.headers}") # Debug rate limit info 147 | QMessageBox.warning(self, "Error", 148 | f"Rate limit exceeded for {feed_title}. Please try again later.") 149 | else: 150 | QMessageBox.warning(self, "Error", 151 | f"Could not fetch feed {feed_title}: {str(e)}") 152 | except Exception as e: 153 | print(f"Error fetching feed {feed_title}: {str(e)}") 154 | QMessageBox.warning(self, "Error", 155 | f"Could not fetch feed {feed_title}: {str(e)}") 156 | 157 | def refresh_feeds(self): 158 | self.refresh_current_feed() 159 | 160 | def add_feed(self): 161 | title, ok = QInputDialog.getText(self, 'Add RSS Feed', 'Feed Title:') 162 | if ok and title: 163 | url, ok = QInputDialog.getText(self, 'Add RSS Feed', 'Feed URL:') 164 | if ok and url: 165 | try: 166 | response = requests.get(url, timeout=10) 167 | response.raise_for_status() 168 | 169 | feed = feedparser.parse(response.text) 170 | if hasattr(feed, 'entries') and feed.entries: 171 | self.feeds[title] = url 172 | self.save_feeds() 173 | self.update_feed_selector() 174 | self.feed_selector.setCurrentText(title) 175 | else: 176 | QMessageBox.warning(self, "Error", "Invalid RSS feed") 177 | except Exception as e: 178 | QMessageBox.warning(self, "Error", f"Could not parse RSS feed: {str(e)}") 179 | 180 | def remove_feed(self): 181 | current_feed = self.feed_selector.currentText() 182 | if current_feed: 183 | reply = QMessageBox.question(self, 'Remove Feed', 184 | f'Remove feed "{current_feed}"?', 185 | QMessageBox.Yes | QMessageBox.No) 186 | if reply == QMessageBox.Yes: 187 | del self.feeds[current_feed] 188 | self.save_feeds() 189 | self.update_feed_selector() 190 | self.refresh_current_feed() 191 | 192 | def show_entry(self, current, previous): 193 | if current: 194 | entry = current.data(Qt.UserRole) 195 | content = f"

{entry.title}

" 196 | if hasattr(entry, 'published'): 197 | content += f"

Published: {entry.published}

" 198 | if hasattr(entry, 'description'): 199 | content += f"

{entry.description}

" 200 | if hasattr(entry, 'link'): 201 | content += f'

Read more...

' 202 | self.content_viewer.setHtml(content) 203 | 204 | def manage_feeds(self): 205 | dialog = FeedManagerDialog(self.feeds, self) 206 | if dialog.exec_() == QDialog.Accepted: 207 | self.feeds = dialog.get_feeds() 208 | self.save_feeds() 209 | self.update_feed_selector() 210 | self.refresh_current_feed() -------------------------------------------------------------------------------- /src/jottr/rss_tab.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QWidget, QVBoxLayout 2 | from rss_reader import RSSReader 3 | 4 | class RSSTab(QWidget): 5 | def __init__(self, parent=None): 6 | super().__init__(parent) 7 | layout = QVBoxLayout(self) 8 | self.rss_reader = RSSReader() 9 | layout.addWidget(self.rss_reader) -------------------------------------------------------------------------------- /src/jottr/settings_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, 2 | QLineEdit, QPushButton, QListWidget, QTabWidget, 3 | QWidget, QCheckBox, QMessageBox, QInputDialog, QComboBox) 4 | from PyQt5.QtCore import Qt 5 | import json 6 | import os 7 | 8 | class SettingsDialog(QDialog): 9 | def __init__(self, settings_manager, parent=None): 10 | super().__init__(parent) 11 | self.settings_manager = settings_manager 12 | self.setWindowTitle("Settings") 13 | self.setMinimumWidth(500) 14 | 15 | self.setup_ui() 16 | 17 | def setup_ui(self): 18 | """Setup the UI components""" 19 | # Create layout 20 | layout = QVBoxLayout(self) 21 | 22 | # Create tab widget 23 | tabs = QTabWidget() 24 | 25 | # Appearance tab 26 | appearance_tab = QWidget() 27 | appearance_layout = QVBoxLayout(appearance_tab) 28 | 29 | # UI Theme 30 | theme_layout = QHBoxLayout() 31 | theme_label = QLabel("UI Theme:") 32 | self.theme_combo = QComboBox() 33 | self.theme_combo.addItems(['system', 'Fusion', 'Windows', 'Macintosh']) 34 | self.theme_combo.setCurrentText(self.settings_manager.get_ui_theme()) 35 | theme_layout.addWidget(theme_label) 36 | theme_layout.addWidget(self.theme_combo) 37 | appearance_layout.addLayout(theme_layout) 38 | 39 | # Add appearance tab 40 | tabs.addTab(appearance_tab, "Appearance") 41 | 42 | # Browser tab 43 | browser_tab = QWidget() 44 | browser_layout = QVBoxLayout(browser_tab) 45 | 46 | # Homepage setting 47 | homepage_layout = QHBoxLayout() 48 | homepage_label = QLabel("Homepage:") 49 | self.homepage_edit = QLineEdit() 50 | self.homepage_edit.setText(self.settings_manager.get_setting('homepage', 'https://www.apnews.com/')) 51 | homepage_layout.addWidget(homepage_label) 52 | homepage_layout.addWidget(self.homepage_edit) 53 | browser_layout.addLayout(homepage_layout) 54 | 55 | # Search sites 56 | search_label = QLabel("Site-specific searches:") 57 | browser_layout.addWidget(search_label) 58 | 59 | self.search_list = QListWidget() 60 | self.load_search_sites() 61 | browser_layout.addWidget(self.search_list) 62 | 63 | # Search site buttons 64 | search_buttons = QHBoxLayout() 65 | add_search = QPushButton("Add") 66 | edit_search = QPushButton("Edit") 67 | delete_search = QPushButton("Delete") 68 | add_search.clicked.connect(self.add_search_site) 69 | edit_search.clicked.connect(self.edit_search_site) 70 | delete_search.clicked.connect(self.delete_search_site) 71 | search_buttons.addWidget(add_search) 72 | search_buttons.addWidget(edit_search) 73 | search_buttons.addWidget(delete_search) 74 | browser_layout.addLayout(search_buttons) 75 | 76 | # Dictionary tab 77 | dict_tab = QWidget() 78 | dict_layout = QVBoxLayout(dict_tab) 79 | 80 | dict_label = QLabel("User Dictionary:") 81 | dict_layout.addWidget(dict_label) 82 | 83 | self.dict_list = QListWidget() 84 | self.load_user_dict() 85 | dict_layout.addWidget(self.dict_list) 86 | 87 | # Dictionary buttons 88 | dict_buttons = QHBoxLayout() 89 | add_word = QPushButton("Add Word") 90 | delete_word = QPushButton("Delete Word") 91 | add_word.clicked.connect(self.add_dict_word) 92 | delete_word.clicked.connect(self.delete_dict_word) 93 | dict_buttons.addWidget(add_word) 94 | dict_buttons.addWidget(delete_word) 95 | dict_layout.addLayout(dict_buttons) 96 | 97 | # Add tabs 98 | tabs.addTab(browser_tab, "Browser") 99 | tabs.addTab(dict_tab, "Dictionary") 100 | 101 | layout.addWidget(tabs) 102 | 103 | # Dialog buttons 104 | buttons = QHBoxLayout() 105 | ok_button = QPushButton("OK") 106 | cancel_button = QPushButton("Cancel") 107 | ok_button.clicked.connect(self.accept) 108 | cancel_button.clicked.connect(self.reject) 109 | buttons.addWidget(ok_button) 110 | buttons.addWidget(cancel_button) 111 | layout.addLayout(buttons) 112 | 113 | def load_search_sites(self): 114 | """Load search sites from settings""" 115 | sites = self.settings_manager.get_setting('search_sites', { 116 | 'AP News': 'site:apnews.com', 117 | 'Reuters': 'site:reuters.com', 118 | 'BBC News': 'site:bbc.com/news' 119 | }) 120 | for name, site in sites.items(): 121 | self.search_list.addItem(f"{name}: {site}") 122 | 123 | def load_user_dict(self): 124 | """Load user dictionary words""" 125 | words = self.settings_manager.get_setting('user_dictionary', []) 126 | self.dict_list.addItems(words) 127 | 128 | def add_search_site(self): 129 | """Add new search site""" 130 | dialog = SearchSiteDialog(self) 131 | if dialog.exec_(): 132 | name, site = dialog.get_data() 133 | self.search_list.addItem(f"{name}: {site}") 134 | 135 | def edit_search_site(self): 136 | """Edit selected search site""" 137 | current = self.search_list.currentItem() 138 | if current: 139 | name, site = current.text().split(': ', 1) 140 | dialog = SearchSiteDialog(self, name, site) 141 | if dialog.exec_(): 142 | new_name, new_site = dialog.get_data() 143 | current.setText(f"{new_name}: {new_site}") 144 | 145 | def delete_search_site(self): 146 | """Delete selected search site""" 147 | current = self.search_list.currentRow() 148 | if current >= 0: 149 | self.search_list.takeItem(current) 150 | 151 | def add_dict_word(self): 152 | """Add word to user dictionary""" 153 | word, ok = QInputDialog.getText(self, "Add Word", "Enter word:") 154 | if ok and word: 155 | self.dict_list.addItem(word) 156 | 157 | def delete_dict_word(self): 158 | """Delete word from user dictionary""" 159 | current = self.dict_list.currentRow() 160 | if current >= 0: 161 | self.dict_list.takeItem(current) 162 | 163 | def get_data(self): 164 | """Get dialog data""" 165 | return { 166 | 'homepage': self.homepage_edit.text(), 167 | 'search_sites': self.get_search_sites(), 168 | 'user_dictionary': self.get_user_dictionary(), 169 | 'ui_theme': self.theme_combo.currentText() 170 | } 171 | 172 | def get_search_sites(self): 173 | """Get search sites from list widget""" 174 | sites = {} 175 | for i in range(self.search_list.count()): 176 | name, site = self.search_list.item(i).text().split(': ', 1) 177 | sites[name] = site 178 | return sites 179 | 180 | def get_user_dictionary(self): 181 | """Get words from dictionary list widget""" 182 | words = [] 183 | for i in range(self.dict_list.count()): 184 | words.append(self.dict_list.item(i).text()) 185 | return words 186 | 187 | class SearchSiteDialog(QDialog): 188 | def __init__(self, parent=None, name='', site=''): 189 | super().__init__(parent) 190 | self.setWindowTitle("Search Site") 191 | 192 | # Remove 'site:' prefix if it exists for display 193 | if site.startswith('site:'): 194 | site = site[5:] 195 | 196 | layout = QVBoxLayout(self) 197 | 198 | # Name field 199 | name_layout = QHBoxLayout() 200 | name_label = QLabel("Name:") 201 | self.name_edit = QLineEdit(name) 202 | name_layout.addWidget(name_label) 203 | name_layout.addWidget(self.name_edit) 204 | layout.addLayout(name_layout) 205 | 206 | # Site field 207 | site_layout = QHBoxLayout() 208 | site_label = QLabel("Website:") 209 | self.site_edit = QLineEdit(site) 210 | self.site_edit.setPlaceholderText("example.com") 211 | site_layout.addWidget(site_label) 212 | site_layout.addWidget(self.site_edit) 213 | layout.addLayout(site_layout) 214 | 215 | # Add help text 216 | help_label = QLabel("Enter the website domain without 'http://' or 'www.'") 217 | help_label.setStyleSheet("color: gray; font-size: 10px;") 218 | layout.addWidget(help_label) 219 | 220 | # Buttons 221 | buttons = QHBoxLayout() 222 | ok_button = QPushButton("OK") 223 | cancel_button = QPushButton("Cancel") 224 | ok_button.clicked.connect(self.accept) 225 | cancel_button.clicked.connect(self.reject) 226 | buttons.addWidget(ok_button) 227 | buttons.addWidget(cancel_button) 228 | layout.addLayout(buttons) 229 | 230 | def get_data(self): 231 | """Get dialog data with 'site:' prefix automatically added""" 232 | name = self.name_edit.text() 233 | site = self.site_edit.text().strip() 234 | 235 | # Remove any existing 'site:' prefix 236 | if site.startswith('site:'): 237 | site = site[5:] 238 | 239 | # Remove http://, https://, and www. if present 240 | site = site.replace('http://', '').replace('https://', '').replace('www.', '') 241 | 242 | # Add 'site:' prefix 243 | site = f'site:{site}' 244 | 245 | return name, site -------------------------------------------------------------------------------- /src/jottr/settings_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from PyQt5.QtGui import QFont 4 | import time 5 | from PyQt5.QtWidgets import QApplication, QStyleFactory 6 | import sys 7 | 8 | class SettingsManager: 9 | def __init__(self): 10 | # Initialize default settings 11 | self.settings = { 12 | "font_family": "DejaVu Sans Mono", 13 | "font_size": 12, 14 | "font_weight": 50, 15 | "font_italic": False, 16 | "theme": "default", 17 | "ui_theme": "light", 18 | "spell_check": True, 19 | "search_sites": { 20 | "AP News": "site:apnews.com", 21 | "Reuters": "site:reuters.com", 22 | "BBC News": "site:bbc.com/news" 23 | }, 24 | "show_snippets": False, 25 | "show_browser": False, 26 | "pane_states": { 27 | "snippets_visible": False, 28 | "browser_visible": False, 29 | "sizes": [700, 300, 300] 30 | } 31 | } 32 | 33 | # Set up config directory based on platform 34 | if sys.platform == 'darwin': 35 | self.config_dir = os.path.join(os.path.expanduser('~/Library/Application Support'), 'Jottr') 36 | elif sys.platform == 'win32': 37 | self.config_dir = os.path.join(os.getenv('APPDATA'), 'Jottr') 38 | else: # Linux/Unix 39 | CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) 40 | self.config_dir = os.path.join(CONFIG_HOME, "Jottr") 41 | 42 | # Create config directory if it doesn't exist 43 | os.makedirs(self.config_dir, exist_ok=True) 44 | 45 | # Define paths for different data types 46 | self.settings_file = os.path.join(self.config_dir, 'settings.json') 47 | self.snippets_dir = os.path.join(self.config_dir, 'snippets') 48 | self.dict_file = os.path.join(self.config_dir, 'user_dictionary.txt') 49 | 50 | # Create snippets directory 51 | os.makedirs(self.snippets_dir, exist_ok=True) 52 | 53 | # Initialize settings 54 | self.load_settings() 55 | 56 | # # Create autosave directory 57 | # self.autosave_dir = os.path.join(os.path.expanduser("~"), ".ap_editor_autosave") 58 | # os.makedirs(self.autosave_dir, exist_ok=True) 59 | 60 | # # Create running flag file 61 | # self.running_flag = os.path.join(self.autosave_dir, "editor_running") 62 | # # Set running flag 63 | # with open(self.running_flag, 'w') as f: 64 | # f.write(str(os.getpid())) 65 | 66 | # # Setup backup and recovery directories 67 | # self.backup_dir = os.path.join(os.path.expanduser("~"), ".ap_editor", "backups") 68 | # self.recovery_dir = os.path.join(os.path.expanduser("~"), ".ap_editor", "recovery") 69 | # os.makedirs(self.backup_dir, exist_ok=True) 70 | # os.makedirs(self.recovery_dir, exist_ok=True) 71 | 72 | # # Create session file 73 | # self.session_file = os.path.join(self.recovery_dir, "session.json") 74 | # self.create_session_file() 75 | 76 | # # Create session state file 77 | # self.session_state_file = os.path.join(self.recovery_dir, "session_state.json") 78 | 79 | # # Initialize with unclean state 80 | # self.initialize_session_state() 81 | 82 | # Clean up old session files if last exit was clean 83 | # if os.path.exists(self.session_state_file): 84 | # try: 85 | # with open(self.session_state_file, 'r') as f: 86 | # state = json.load(f) 87 | # if state.get('clean_exit', False): 88 | # self.cleanup_old_sessions() 89 | # except: 90 | # pass 91 | 92 | def load_settings(self): 93 | """Load settings from file""" 94 | settings_path = os.path.join(self.config_dir, 'settings.json') 95 | if os.path.exists(settings_path): 96 | try: 97 | with open(settings_path, 'r', encoding='utf-8') as f: 98 | saved_settings = json.load(f) 99 | self.settings.update(saved_settings) 100 | except Exception as e: 101 | print(f"Error loading settings: {str(e)}") 102 | 103 | def save_settings(self): 104 | with open(self.settings_file, 'w') as f: 105 | json.dump(self.settings, f) 106 | 107 | def get_font(self): 108 | font = QFont( 109 | self.settings["font_family"], 110 | self.settings["font_size"], 111 | self.settings["font_weight"] 112 | ) 113 | font.setItalic(self.settings["font_italic"]) 114 | return font 115 | 116 | def save_font(self, font): 117 | self.settings.update({ 118 | "font_family": font.family(), 119 | "font_size": font.pointSize(), 120 | "font_weight": font.weight(), 121 | "font_italic": font.italic() 122 | }) 123 | self.save_settings() 124 | 125 | def get_theme(self): 126 | return self.settings["theme"] 127 | 128 | def save_theme(self, theme): 129 | self.settings["theme"] = theme 130 | self.save_settings() 131 | 132 | def get_pane_visibility(self): 133 | return (self.settings["show_snippets"], self.settings["show_browser"]) 134 | 135 | def save_pane_visibility(self, show_snippets, show_browser): 136 | self.settings.update({ 137 | "show_snippets": show_snippets, 138 | "show_browser": show_browser 139 | }) 140 | self.save_settings() 141 | 142 | # def save_last_files(self, files): 143 | # """Save list of last opened files""" 144 | # self.settings["last_files"] = files 145 | # self.save_settings() 146 | 147 | # def get_last_files(self): 148 | # """Get list of last opened files""" 149 | # return self.settings.get("last_files", []) 150 | 151 | # def get_autosave_dir(self): 152 | # """Get the directory for autosave files""" 153 | # return self.autosave_dir 154 | 155 | # def cleanup_autosave_dir(self): 156 | # """Clean up old autosave files""" 157 | # if os.path.exists(self.autosave_dir): 158 | # try: 159 | # # Remove files older than 7 days 160 | # for filename in os.listdir(self.autosave_dir): 161 | # filepath = os.path.join(self.autosave_dir, filename) 162 | # if os.path.getmtime(filepath) < time.time() - 7 * 86400: 163 | # os.remove(filepath) 164 | # except: 165 | # pass 166 | 167 | # def clear_running_flag(self): 168 | # """Clear the running flag on clean exit""" 169 | # try: 170 | # if os.path.exists(self.running_flag): 171 | # os.remove(self.running_flag) 172 | # except: 173 | # pass 174 | 175 | # def was_previous_crash(self): 176 | # """Check if previous session crashed""" 177 | # if os.path.exists(self.running_flag): 178 | # try: 179 | # with open(self.running_flag, 'r') as f: 180 | # old_pid = int(f.read().strip()) 181 | # # Check if the process is still running 182 | # try: 183 | # os.kill(old_pid, 0) 184 | # # If we get here, the process is still running 185 | # return False 186 | # except OSError: 187 | # # Process is not running, was a crash 188 | # return True 189 | # except: 190 | # return True 191 | # return False 192 | 193 | # def create_session_file(self): 194 | # """Create a session file to track clean/dirty exits""" 195 | # session_data = { 196 | # 'pid': os.getpid(), 197 | # 'timestamp': time.time(), 198 | # 'clean_exit': False 199 | # } 200 | # try: 201 | # with open(self.session_file, 'w') as f: 202 | # json.dump(session_data, f) 203 | # except Exception as e: 204 | # print(f"Failed to create session file: {str(e)}") 205 | 206 | # def mark_clean_exit(self): 207 | # """Mark that the editor exited cleanly""" 208 | # try: 209 | # if os.path.exists(self.session_file): 210 | # with open(self.session_file, 'r') as f: 211 | # session_data = json.load(f) 212 | # session_data['clean_exit'] = True 213 | # with open(self.session_file, 'w') as f: 214 | # json.dump(session_data, f) 215 | # except Exception as e: 216 | # print(f"Failed to mark clean exit: {str(e)}") 217 | 218 | # def needs_recovery(self): 219 | # """Check if we need to recover from a crash""" 220 | # try: 221 | # if os.path.exists(self.session_file): 222 | # with open(self.session_file, 'r') as f: 223 | # session_data = json.load(f) 224 | # return not session_data.get('clean_exit', True) 225 | # return True # If no session file, assume we need recovery 226 | # except Exception as e: 227 | # print(f"Error checking recovery status: {str(e)}") 228 | # return True # If we can't read the session file, assume we need recovery 229 | 230 | # def get_backup_dir(self): 231 | # return self.backup_dir 232 | 233 | # def get_recovery_dir(self): 234 | # return self.recovery_dir 235 | 236 | # def initialize_session_state(self): 237 | # """Initialize or update session state""" 238 | # try: 239 | # state = { 240 | # 'clean_exit': False, 241 | # 'timestamp': time.time(), 242 | # 'open_tabs': [] 243 | # } 244 | # with open(self.session_state_file, 'w') as f: 245 | # json.dump(state, f) 246 | # except Exception as e: 247 | # print(f"Failed to initialize session state: {str(e)}") 248 | 249 | # def save_session_state(self, tab_ids, clean_exit=False): 250 | # """Save the list of currently open tab IDs""" 251 | # try: 252 | # state = { 253 | # 'open_tabs': tab_ids, 254 | # 'clean_exit': clean_exit, 255 | # 'timestamp': time.time() 256 | # } 257 | # with open(self.session_state_file, 'w') as f: 258 | # json.dump(state, f) 259 | # except Exception as e: 260 | # print(f"Failed to save session state: {str(e)}") 261 | 262 | # def get_session_state(self): 263 | # """Get list of tab IDs that were open in last session""" 264 | # try: 265 | # if os.path.exists(self.session_state_file): 266 | # with open(self.session_state_file, 'r') as f: 267 | # state = json.load(f) 268 | # return state.get('open_tabs', []) 269 | # except Exception as e: 270 | # print(f"Failed to load session state: {str(e)}") 271 | # return [] 272 | 273 | # def cleanup_old_sessions(self): 274 | # """Clean up session files from previous clean exits""" 275 | # # Don't clean up by default - let the session restore handle it 276 | # pass 277 | 278 | def get_setting(self, key, default=None): 279 | """Get a setting value with a default fallback""" 280 | try: 281 | return self.settings.get(key, default) 282 | except Exception as e: 283 | print(f"Failed to get setting {key}: {str(e)}") 284 | return default 285 | 286 | def save_setting(self, key, value): 287 | """Save a single setting""" 288 | try: 289 | self.settings[key] = value 290 | with open(self.settings_file, 'w') as f: 291 | json.dump(self.settings, f) 292 | except Exception as e: 293 | print(f"Failed to save setting {key}: {str(e)}") 294 | 295 | def get_ui_theme(self): 296 | """Get current UI theme setting""" 297 | return self.settings.get('ui_theme', 'system') 298 | 299 | def save_ui_theme(self, theme): 300 | """Save UI theme setting""" 301 | self.settings['ui_theme'] = theme 302 | self.save_settings() 303 | 304 | def apply_ui_theme(self, theme): 305 | """Apply UI theme to application""" 306 | if theme == 'system': 307 | # Use system default theme 308 | QApplication.setStyle(QStyleFactory.create('Fusion')) 309 | else: 310 | # Apply custom theme 311 | QApplication.setStyle(QStyleFactory.create(theme)) 312 | 313 | # Save the theme 314 | self.save_ui_theme(theme) -------------------------------------------------------------------------------- /src/jottr/snippet_editor_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, 2 | QTextEdit, QPushButton, QLabel) 3 | 4 | class SnippetEditorDialog(QDialog): 5 | def __init__(self, title="", content="", parent=None): 6 | super().__init__(parent) 7 | self.setWindowTitle("Edit Snippet") 8 | self.setMinimumWidth(500) 9 | self.setMinimumHeight(400) 10 | 11 | layout = QVBoxLayout(self) 12 | 13 | # Title input 14 | title_layout = QHBoxLayout() 15 | title_label = QLabel("Title:") 16 | self.title_edit = QLineEdit(title) 17 | title_layout.addWidget(title_label) 18 | title_layout.addWidget(self.title_edit) 19 | layout.addLayout(title_layout) 20 | 21 | # Content input 22 | content_label = QLabel("Content:") 23 | layout.addWidget(content_label) 24 | self.content_edit = QTextEdit() 25 | self.content_edit.setPlainText(content) 26 | layout.addWidget(self.content_edit) 27 | 28 | # Buttons 29 | button_layout = QHBoxLayout() 30 | save_button = QPushButton("Save") 31 | cancel_button = QPushButton("Cancel") 32 | button_layout.addWidget(save_button) 33 | button_layout.addWidget(cancel_button) 34 | layout.addLayout(button_layout) 35 | 36 | save_button.clicked.connect(self.accept) 37 | cancel_button.clicked.connect(self.reject) 38 | 39 | def get_data(self): 40 | return { 41 | 'title': self.title_edit.text(), 42 | 'content': self.content_edit.toPlainText() 43 | } -------------------------------------------------------------------------------- /src/jottr/snippet_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | class SnippetManager: 5 | def __init__(self, settings_manager): 6 | self.settings_manager = settings_manager 7 | self.file_path = os.path.join( 8 | self.settings_manager.config_dir, 9 | 'snippets.json' 10 | ) 11 | self.snippets = {} 12 | self.load_snippets() 13 | 14 | def load_snippets(self): 15 | """Load snippets from file""" 16 | if os.path.exists(self.file_path): 17 | try: 18 | with open(self.file_path, 'r', encoding='utf-8') as f: 19 | self.snippets = json.load(f) 20 | except Exception as e: 21 | print(f"Error loading snippets: {str(e)}") 22 | self.snippets = {} 23 | 24 | def save_snippets(self): 25 | with open(self.file_path, 'w') as file: 26 | json.dump(self.snippets, file) 27 | 28 | def add_snippet(self, title, text): 29 | self.snippets[title] = text 30 | self.save_snippets() 31 | 32 | def get_snippet(self, title): 33 | return self.snippets.get(title) 34 | 35 | def get_snippets(self): 36 | return list(self.snippets.keys()) 37 | 38 | def delete_snippet(self, title): 39 | if title in self.snippets: 40 | del self.snippets[title] 41 | self.save_snippets() 42 | 43 | def get_all_snippet_contents(self): 44 | """Return a list of all snippet contents""" 45 | return list(self.snippets.values()) -------------------------------------------------------------------------------- /src/jottr/theme_manager.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtGui import QColor, QPalette 2 | from PyQt5.QtWidgets import QStyleFactory 3 | 4 | class ThemeManager: 5 | @staticmethod 6 | def get_themes(): 7 | return { 8 | "Light": { 9 | "bg": "#ffffff", 10 | "text": "#000000", 11 | "selection": "#b3d4fc" 12 | }, 13 | "Dark": { 14 | "bg": "#1e1e1e", 15 | "text": "#d4d4d4", 16 | "selection": "#264f78" 17 | }, 18 | "Sepia": { 19 | "bg": "#f4ecd8", 20 | "text": "#5b4636", 21 | "selection": "#c4b5a0" 22 | } 23 | } 24 | 25 | @staticmethod 26 | def apply_theme(editor, theme_name): 27 | themes = ThemeManager.get_themes() 28 | if theme_name in themes: 29 | theme = themes[theme_name] 30 | editor.setStyleSheet(f""" 31 | QTextEdit {{ 32 | background-color: {theme['bg']}; 33 | color: {theme['text']}; 34 | selection-background-color: {theme['selection']}; 35 | font-family: {editor.font().family()}; 36 | font-size: {editor.font().pointSize()}pt; 37 | }} 38 | """) --------------------------------------------------------------------------------