├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── codeql │ └── codeql-config.yml └── workflows │ ├── CI.yml │ └── codeql.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── build-aux └── flatpak │ ├── .prettierrc │ └── dev.mufeed.Wordbook.Devel.json ├── data ├── dev.mufeed.Wordbook.SearchProvider.ini ├── dev.mufeed.Wordbook.SearchProvider.service.in ├── dev.mufeed.Wordbook.desktop.in.in ├── dev.mufeed.Wordbook.gschema.xml.in ├── dev.mufeed.Wordbook.metainfo.xml.in.in ├── icons │ ├── dev.mufeed.Wordbook-symbolic.svg │ ├── dev.mufeed.Wordbook.Devel.svg │ ├── dev.mufeed.Wordbook.Source.svg │ ├── dev.mufeed.Wordbook.svg │ └── meson.build ├── meson.build ├── resources │ ├── meson.build │ ├── resources.gresource.xml │ ├── style.css │ └── ui │ │ ├── settings_window.blp │ │ ├── shortcuts_window.blp │ │ └── window.blp └── search_provider.in ├── images ├── ss.png ├── ss1.png ├── ss2.png └── ss3.png ├── justfile ├── meson.build ├── meson_options.txt ├── po ├── LINGUAS ├── POTFILES ├── de.po ├── fr.po ├── hi.po ├── it.po ├── meson.build ├── nl.po ├── ru.po ├── tr.po └── wordbook.pot ├── pyproject.toml ├── setup.cfg ├── subprojects └── blueprint-compiler.wrap ├── uv.lock └── wordbook ├── __init__.py ├── base.py ├── main.py ├── meson.build ├── settings.py ├── settings_window.py ├── utils.py ├── window.py └── wordbook.in /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: 'mufeedali' 2 | github: 'mufeedali' 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Wordbook version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | ``` 15 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "Wordbook CodeQL Config" 2 | 3 | paths-ignore: 4 | # File from flatpak/flatpak-builder-tools for convenience 5 | - build-aux/flatpak/flatpak-pip-generator -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | name: CI 6 | jobs: 7 | codespell: 8 | name: Check for spelling errors 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: codespell-project/actions-codespell@master 12 | flatpak-builder: 13 | name: "Flatpak" 14 | runs-on: ubuntu-latest 15 | container: 16 | image: bilelmoussaoui/flatpak-github-actions:gnome-nightly 17 | options: --privileged 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 21 | with: 22 | bundle: "wordbook-devel.flatpak" 23 | manifest-path: "build-aux/flatpak/dev.mufeed.Wordbook.Devel.json" 24 | run-tests: "true" 25 | cache-key: flatpak-builder-${{ github.sha }} 26 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | schedule: 9 | - cron: "51 4 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [python] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | config-file: ./.github/codeql/codeql-config.yml 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | with: 42 | category: "/language:${{ matrix.language }}" 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Self-defined 2 | .vscode/ 3 | .buildconfig 4 | *.ui~ 5 | .pylintrc 6 | *.pyc 7 | *.backup 8 | .idea/ 9 | .flatpak/ 10 | .flatpak-builder/ 11 | deprecated_stuff/ 12 | _build/ 13 | 14 | 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | pip-wheel-metadata/ 38 | share/python-wheels/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | MANIFEST 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .nox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *.cover 64 | *.py,cover 65 | .hypothesis/ 66 | .pytest_cache/ 67 | 68 | # Translations 69 | *.mo 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | env/ 121 | venv/ 122 | ENV/ 123 | env.bak/ 124 | venv.bak/ 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | /subprojects/blueprint-compiler 145 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "build-aux/flatpak/flatpak-builder-tools"] 2 | path = build-aux/flatpak/flatpak-builder-tools 3 | url = git@github.com:flatpak/flatpak-builder-tools.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Wordbook
3 | Wordbook 4 |

5 | 6 |

Look up definitions of any English term

7 | 8 |

9 | Searching (Light mode) 10 |

11 | 12 |

13 | Wordbook is an offline English-English dictionary application built for GNOME using the Open English WordNet database for definitions and the reliable eSpeak for pronunciations (both audio and phoneme). 14 |

15 | 16 | ## Features 17 | 18 | * Fully offline after initial data download 19 | * Random Word 20 | * Live Search 21 | * Double click to search 22 | * Custom Definitions feature using Pango Markup or an HTML subset for formatting 23 | * Support for GNOME Dark Mode and launching app in dark mode. 24 | 25 | ## Screenshots 26 | 27 | Welcome screen (Light mode) Searching (Light mode) 28 | 29 | Welcome screen (Dark mode) Searching (Dark mode) 30 | 31 | ## Requirements 32 | 33 | * GTK 4.6+ [Arch: `gtk4`] 34 | * libadwaita 1.1.0+ [Arch: `libadwaita`] 35 | * Python 3 [Arch: `python`] 36 | * Standalone WordNet Python module [Arch AUR: `python-wn`] 37 | * Python GObject [Arch: `python-gobject`] 38 | * eSpeak-ng (For pronunciations and audio) [Arch: `espeak-ng`] 39 | 40 | ## Installation 41 | 42 | ### Using Flatpak 43 | 44 | Download on Flathub 45 | 46 | ### Using Nix 47 | 48 | [![](https://raw.githubusercontent.com/dch82/Nixpkgs-Badges/main/nixpkgs-badge-light.svg)](https://search.nixos.org/packages?size=1&show=wordbook) 49 | 50 | This method can be used anywhere the Nix package manager is installed. 51 | 52 | ### Using distro-specific packages 53 | 54 | Right now, Wordbook is only packaged for Arch through the AUR as [`wordbook`](https://aur.archlinux.org/packages/wordbook). 55 | 56 | On NixOS, Wordbook can be installed using the Nix package manager as shown above. Additionally, the following code can be added to your NixOS configuration file, usually located in `/etc/nixos/configuration.nix`. 57 | 58 | ``` 59 | environment.systemPackages = [ 60 | pkgs.wordbook 61 | ]; 62 | ``` 63 | 64 | ### From Source 65 | 66 | To install, first make sure of the dependencies as listed above. You can use `just` to make the process easy. 67 | 68 | ```bash 69 | just setup 70 | just install 71 | ``` 72 | 73 | Without `just`: 74 | ```bash 75 | mkdir -p _build 76 | meson setup . _build 77 | ninja -C _build install 78 | ``` 79 | 80 | For a local build with debugging enabled: 81 | 82 | ```bash 83 | just run 84 | # OR 85 | just setup 86 | just develop-configure 87 | just local-run 88 | ``` 89 | -------------------------------------------------------------------------------- /build-aux/flatpak/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /build-aux/flatpak/dev.mufeed.Wordbook.Devel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dev.mufeed.Wordbook", 3 | "runtime": "org.gnome.Platform", 4 | "runtime-version": "master", 5 | "sdk": "org.gnome.Sdk", 6 | "command": "wordbook", 7 | "finish-args": [ 8 | "--share=network", 9 | "--socket=pulseaudio", 10 | "--share=ipc", 11 | "--device=dri", 12 | "--socket=fallback-x11", 13 | "--socket=wayland" 14 | ], 15 | "cleanup": ["*blueprint*", "*.a", "*.la", "/lib/pkgconfig", "/include"], 16 | "modules": [ 17 | { 18 | "name": "blueprint-compiler", 19 | "buildsystem": "meson", 20 | "cleanup": ["*"], 21 | "sources": [ 22 | { 23 | "type": "git", 24 | "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", 25 | "branch": "main" 26 | } 27 | ] 28 | }, 29 | { 30 | "name": "pcaudiolib", 31 | "sources": [ 32 | { 33 | "type": "archive", 34 | "url": "https://github.com/espeak-ng/pcaudiolib/archive/1.2.tar.gz", 35 | "sha256": "44b9d509b9eac40a0c61585f756d76a7b555f732e8b8ae4a501c8819c59c6619" 36 | } 37 | ] 38 | }, 39 | { 40 | "name": "espeak-ng", 41 | "no-parallel-make": true, 42 | "sources": [ 43 | { 44 | "type": "git", 45 | "url": "https://github.com/espeak-ng/espeak-ng.git", 46 | "tag": "1.52.0", 47 | "commit": "4870adfa25b1a32b4361592f1be8a40337c58d6c" 48 | } 49 | ] 50 | }, 51 | { 52 | "name": "python3-modules", 53 | "buildsystem": "simple", 54 | "build-commands": [ 55 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"wn\" \"pydantic\" --no-build-isolation" 56 | ], 57 | "sources": [ 58 | { 59 | "type": "file", 60 | "url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", 61 | "sha256": "1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" 62 | }, 63 | { 64 | "type": "file", 65 | "url": "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", 66 | "sha256": "9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c" 67 | }, 68 | { 69 | "type": "file", 70 | "url": "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", 71 | "sha256": "ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" 72 | }, 73 | { 74 | "type": "file", 75 | "url": "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", 76 | "sha256": "e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" 77 | }, 78 | { 79 | "type": "file", 80 | "url": "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", 81 | "sha256": "a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd" 82 | }, 83 | { 84 | "type": "file", 85 | "url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", 86 | "sha256": "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" 87 | }, 88 | { 89 | "type": "file", 90 | "url": "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", 91 | "sha256": "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 92 | }, 93 | { 94 | "type": "file", 95 | "url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", 96 | "sha256": "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" 97 | }, 98 | { 99 | "type": "file", 100 | "url": "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", 101 | "sha256": "d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb" 102 | }, 103 | { 104 | "type": "file", 105 | "url": "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", 106 | "sha256": "4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", 107 | "only-arches": ["aarch64"] 108 | }, 109 | { 110 | "type": "file", 111 | "url": "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 112 | "sha256": "8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", 113 | "only-arches": ["x86_64"] 114 | }, 115 | { 116 | "type": "file", 117 | "url": "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", 118 | "sha256": "cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc" 119 | }, 120 | { 121 | "type": "file", 122 | "url": "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", 123 | "sha256": "50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f" 124 | }, 125 | { 126 | "type": "file", 127 | "url": "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", 128 | "sha256": "4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69" 129 | }, 130 | { 131 | "type": "file", 132 | "url": "https://files.pythonhosted.org/packages/c0/5b/0b3d27d810272deef9a2a16e9087b1cf526148ba4815f58cc1f275aa227f/wn-0.11.0-py3-none-any.whl", 133 | "sha256": "39102e6283d7517ba5effcc56efa6763394db3a0511a09ba74db163e83c9663d" 134 | } 135 | ] 136 | }, 137 | { 138 | "name": "wordbook", 139 | "buildsystem": "meson", 140 | "config-opts": ["-Dprofile=development"], 141 | "sources": [ 142 | { 143 | "type": "dir", 144 | "path": "../../." 145 | } 146 | ] 147 | } 148 | ] 149 | } 150 | -------------------------------------------------------------------------------- /data/dev.mufeed.Wordbook.SearchProvider.ini: -------------------------------------------------------------------------------- 1 | [Shell Search Provider] 2 | DesktopId=@APP_ID@.desktop 3 | BusName=@APP_ID@.SearchProvider 4 | ObjectPath=@object_path@ 5 | Version=2 6 | -------------------------------------------------------------------------------- /data/dev.mufeed.Wordbook.SearchProvider.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=@APP_ID@.SearchProvider 3 | Exec=@pkgdatadir@/search_provider 4 | -------------------------------------------------------------------------------- /data/dev.mufeed.Wordbook.desktop.in.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Name=Wordbook 5 | Exec=wordbook 6 | Comment=Look up definitions for any English term 7 | Terminal=false 8 | Type=Application 9 | Icon=@app-id@ 10 | Categories=Dictionary;Education;GTK; 11 | # Translators: These are search terms to find this application. Do NOT translate or localize the semicolons. The list MUST also end with a semicolon. 12 | Keywords=dictionary; 13 | # Translators: Do NOT translate or transliterate this text (these are enum types)! 14 | X-Purism-FormFactor=Workstation;Mobile; 15 | -------------------------------------------------------------------------------- /data/dev.mufeed.Wordbook.gschema.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/dev.mufeed.Wordbook.metainfo.xml.in.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | @app-id@ 4 | CC0-1.0 5 | GPL-3.0-or-later 6 | 7 | Wordbook 8 | Look up definitions for any English term 9 | 10 |

Wordbook is an offline English-English dictionary application powered by WordNet and 11 | eSpeak.

12 |

Features:

13 | 21 |
22 | 23 | Mufeed Ali 24 | 25 | Mufeed Ali 26 | 27 | me@mufeed.dev 28 | 29 | https://github.com/mufeedali/Wordbook 30 | https://github.com/mufeedali/Wordbook/issues 31 | https://github.com/mufeedali/Wordbook 32 | https://github.com/mufeedali/Wordbook/tree/main/po 33 | https://liberapay.com/mufeedali/donate 34 | 35 | @app-id@.desktop 36 | wordbook 37 | 38 | 39 | 40 | https://raw.githubusercontent.com/mufeedali/Wordbook/main/images/ss.png 41 | 42 | 43 | https://raw.githubusercontent.com/mufeedali/Wordbook/main/images/ss1.png 44 | 45 | 46 | https://raw.githubusercontent.com/mufeedali/Wordbook/main/images/ss2.png 47 | 48 | 49 | https://raw.githubusercontent.com/mufeedali/Wordbook/main/images/ss3.png 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
  • GTK4 + libadwaita Port
  • 58 |
  • Persistent history storage
  • 59 |
  • Default to live search
  • 60 |
  • Update WordNet library and database
  • 61 |
62 |
63 |
64 | 65 | 66 |
    67 |
  • Made some minor tweaks to the UI
  • 68 |
  • Fixed search entry focus on launch
  • 69 |
70 |
71 |
72 | 73 | 74 |
    75 |
  • Switched to a brand new icon
  • 76 |
  • Moved history from completions to a new sidebar
  • 77 |
  • Added "--look-up" CLI argument
  • 78 |
  • Made some UI improvements to adapt better to different display sizes
  • 79 |
  • Made some other minor improvements
  • 80 |
81 |
82 |
83 | 84 | 85 |
    86 |
  • Updated WordNet dependency, will need a redownload of the database
  • 87 |
  • Fixed symbolic icon issues
  • 88 |
  • Improved responsiveness
  • 89 |
  • Removed a feature called "Hide window buttons when maximized"
  • 90 |
  • Fixed some issues with the Flatpak
  • 91 |
  • Fixed several minor bugs
  • 92 |
93 |
94 |
95 | 96 | 97 |

Initial release

98 |
99 |
100 |
101 | 102 | keyboard 103 | pointing 104 | touch 105 | 106 | 107 | ModernToolkit 108 | HiDpiIcon 109 | 110 | 111 | mobile 112 | 113 |
114 | -------------------------------------------------------------------------------- /data/icons/dev.mufeed.Wordbook-symbolic.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 | -------------------------------------------------------------------------------- /data/icons/dev.mufeed.Wordbook.Devel.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 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /data/icons/dev.mufeed.Wordbook.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 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | message('Installing icons') 2 | 3 | # Scalable 4 | icondir = join_paths(get_option('datadir'), 'icons/hicolor/scalable/apps') 5 | install_data ( 6 | application_id + '.svg', 7 | install_dir: icondir 8 | ) 9 | 10 | # Symbolic 11 | icondir = join_paths(get_option('datadir'), 'icons/hicolor/symbolic/apps') 12 | install_data ( 13 | base_id + '-symbolic.svg', 14 | install_dir: icondir 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | subdir('icons') 2 | subdir('resources') 3 | 4 | # Desktop file 5 | desktop_conf = configuration_data() 6 | desktop_conf.set('app-id', application_id) 7 | desktop_file = i18n.merge_file( 8 | input: configure_file( 9 | input: '@0@.desktop.in.in'.format(base_id), 10 | output: '@BASENAME@', 11 | configuration: desktop_conf 12 | ), 13 | output: '@0@.desktop'.format(application_id), 14 | type: 'desktop', 15 | po_dir: '../po', 16 | install: true, 17 | install_dir: join_paths(get_option('datadir'), 'applications') 18 | ) 19 | 20 | desktop_utils = find_program('desktop-file-validate', required: false) 21 | if desktop_utils.found() 22 | test('Validate desktop file', desktop_utils, 23 | args: [desktop_file.full_path()] 24 | ) 25 | endif 26 | 27 | # Metainfo file 28 | appdata_conf = configuration_data() 29 | appdata_conf.set('app-id', application_id) 30 | appstream_file = i18n.merge_file( 31 | input: configure_file( 32 | input: '@0@.metainfo.xml.in.in'.format(base_id), 33 | output: '@BASENAME@', 34 | configuration: appdata_conf 35 | ), 36 | output: '@0@.metainfo.xml'.format(application_id), 37 | po_dir: '../po', 38 | install: true, 39 | install_dir: join_paths(get_option('datadir'), 'metainfo') 40 | ) 41 | 42 | appstreamcli = find_program('appstreamcli', required: false) 43 | if appstreamcli.found() 44 | test('Validate appstream file', appstreamcli, 45 | args: ['validate', '--no-net', appstream_file.full_path()] 46 | ) 47 | endif 48 | 49 | # GSettings schema 50 | gschema_conf = configuration_data() 51 | gschema_conf.set('app-id', application_id) 52 | configure_file( 53 | input: '@0@.gschema.xml.in'.format(base_id), 54 | output: '@0@.gschema.xml'.format(application_id), 55 | configuration: gschema_conf, 56 | install: true, 57 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 58 | ) 59 | 60 | compile_schemas = find_program('glib-compile-schemas', required: false) 61 | if compile_schemas.found() 62 | test('Validate schema file', compile_schemas, 63 | args: ['--strict', '--dry-run', meson.current_source_dir()] 64 | ) 65 | endif 66 | 67 | conf = configuration_data() 68 | 69 | conf.set('APP_ID', application_id) 70 | conf.set('PYTHON', python.find_installation('python3').full_path()) 71 | conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) 72 | conf.set('pkgdatadir', pkgdatadir) 73 | conf.set('BIN', join_paths(get_option('prefix'), get_option('bindir'), meson.project_name())) 74 | 75 | if get_option('profile') == 'development' 76 | object_path = '/dev/mufeed/Wordbook/Devel/SearchProvider' 77 | else 78 | object_path = '/dev/mufeed/Wordbook/SearchProvider' 79 | endif 80 | object_path 81 | conf.set('object_path', object_path) 82 | 83 | search_provider_dir = join_paths(get_option('prefix'), get_option('datadir'), 'gnome-shell', 'search-providers') 84 | service_dir = join_paths(get_option('prefix'), get_option('datadir'), 'dbus-1', 'services') 85 | 86 | configure_file( 87 | input: '@0@.SearchProvider.ini'.format(base_id), 88 | output: '@0@.SearchProvider.ini'.format(application_id), 89 | configuration: conf, 90 | install_dir: search_provider_dir, 91 | ) 92 | 93 | configure_file( 94 | input: '@0@.SearchProvider.service.in'.format(base_id), 95 | output: '@0@.SearchProvider.service'.format(application_id), 96 | configuration: conf, 97 | install_dir: service_dir, 98 | ) 99 | 100 | configure_file( 101 | input: 'search_provider.in', 102 | output: 'search_provider', 103 | configuration: conf, 104 | install_dir: pkgdatadir, 105 | ) 106 | -------------------------------------------------------------------------------- /data/resources/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | gnome = import('gnome') 3 | 4 | blueprints = custom_target('blueprints', 5 | input: files( 6 | 'ui/shortcuts_window.blp', 7 | 'ui/settings_window.blp', 8 | 'ui/window.blp', 9 | ), 10 | output: '.', 11 | command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], 12 | ) 13 | 14 | gnome.compile_resources( 15 | 'resources', 16 | 'resources.gresource.xml', 17 | dependencies: blueprints, 18 | gresource_bundle: true, 19 | install: true, 20 | install_dir: pkgdatadir, 21 | ) 22 | -------------------------------------------------------------------------------- /data/resources/resources.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | style.css 5 | ui/shortcuts_window.ui 6 | 7 | ui/settings_window.ui 8 | ui/window.ui 9 | 10 | 11 | -------------------------------------------------------------------------------- /data/resources/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufeedali/Wordbook/1c4f803e1ebf41aa0a98e82ece62374dc3717084/data/resources/style.css -------------------------------------------------------------------------------- /data/resources/ui/settings_window.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $SettingsDialog: Adw.PreferencesDialog { 5 | content-width: 450; 6 | content-height: 350; 7 | 8 | Adw.PreferencesPage { 9 | Adw.PreferencesGroup appearance { 10 | title: _("Appearance"); 11 | 12 | Adw.SwitchRow dark_ui_switch { 13 | title: _("Force Dark Mode"); 14 | } 15 | } 16 | 17 | Adw.PreferencesGroup { 18 | title: _("Behavior"); 19 | 20 | Adw.SwitchRow live_search_switch { 21 | title: _("Live Search"); 22 | subtitle: _("Show definition as the terms are typed in"); 23 | } 24 | 25 | Adw.SwitchRow double_click_switch { 26 | title: _("Double Click Search"); 27 | subtitle: _("Search any word by double clicking on it"); 28 | } 29 | 30 | Adw.SwitchRow auto_paste_switch { 31 | title: _("Auto Paste on Launch"); 32 | subtitle: _("Automatically paste and search clipboard content on launch"); 33 | } 34 | 35 | Adw.ComboRow pronunciations_accent_row { 36 | title: _("Pronunciations Accent"); 37 | 38 | model: StringList { 39 | strings [ 40 | "American English", 41 | "British English", 42 | ] 43 | }; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /data/resources/ui/shortcuts_window.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | ShortcutsWindow help_overlay { 4 | modal: true; 5 | 6 | ShortcutsSection { 7 | section-name: "shortcuts"; 8 | max-height: 10; 9 | 10 | ShortcutsGroup { 11 | title: C_("shortcut window", "General"); 12 | 13 | ShortcutsShortcut { 14 | title: C_("shortcut window", "Show Shortcuts"); 15 | action-name: "win.show-help-overlay"; 16 | } 17 | 18 | ShortcutsShortcut { 19 | title: C_("shortcut window", "Preferences"); 20 | action-name: "win.preferences"; 21 | } 22 | 23 | ShortcutsShortcut { 24 | title: C_("shortcut window", "Paste and Search"); 25 | action-name: "win.paste-search"; 26 | } 27 | 28 | ShortcutsShortcut { 29 | title: C_("shortcut window", "Random Word"); 30 | action-name: "win.random-word"; 31 | } 32 | 33 | ShortcutsShortcut { 34 | title: C_("shortcut window", "Search Selected Text"); 35 | action-name: "win.search-selected"; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /data/resources/ui/window.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | menu app-menu { 5 | section { 6 | id: "search-section"; 7 | 8 | item { 9 | label: _("Paste & Search"); 10 | action: "win.paste-search"; 11 | } 12 | 13 | item { 14 | label: _("Search Selected Text"); 15 | action: "win.search-selected"; 16 | } 17 | 18 | item { 19 | label: _("_Random Word"); 20 | action: "win.random-word"; 21 | } 22 | } 23 | 24 | section { 25 | id: "help-section"; 26 | 27 | item { 28 | label: _("_Preferences"); 29 | action: "win.preferences"; 30 | } 31 | 32 | item { 33 | label: _("_Keyboard Shortcuts"); 34 | action: "win.show-help-overlay"; 35 | } 36 | 37 | item { 38 | label: _("_About Wordbook"); 39 | action: "app.about"; 40 | } 41 | } 42 | } 43 | 44 | template $WordbookWindow: Adw.ApplicationWindow { 45 | default-width: 390; 46 | default-height: 610; 47 | focus-widget: search_entry; 48 | default-widget: search_button; 49 | icon-name: "accesories-dictionary"; 50 | 51 | Adw.Breakpoint { 52 | condition ("max-width: 400sp") 53 | 54 | setters { 55 | main_split_view.collapsed: true; 56 | } 57 | } 58 | 59 | content: Adw.OverlaySplitView main_split_view { 60 | show-sidebar: false; 61 | 62 | sidebar: Adw.ToolbarView { 63 | [top] 64 | Adw.HeaderBar { 65 | [title] 66 | Adw.WindowTitle { 67 | title: _("History"); 68 | } 69 | 70 | [end] 71 | Button clear_history_button { 72 | icon-name: "user-trash-symbolic"; 73 | tooltip-text: _("Clear History"); 74 | sensitive: false; 75 | 76 | styles [ 77 | "flat", 78 | ] 79 | } 80 | } 81 | 82 | content: Adw.ViewStack history_stack { 83 | vexpand: true; 84 | 85 | Adw.ViewStackPage { 86 | name: "list"; 87 | 88 | child: ScrolledWindow { 89 | hscrollbar-policy: never; 90 | has-frame: false; 91 | 92 | child: Viewport { 93 | child: ListBox history_listbox { 94 | selection-mode: none; 95 | 96 | styles [ 97 | "navigation-sidebar", 98 | ] 99 | }; 100 | }; 101 | }; 102 | } 103 | 104 | Adw.ViewStackPage { 105 | name: "empty"; 106 | 107 | child: Adw.StatusPage { 108 | icon-name: "document-open-recent-symbolic"; 109 | title: "No recent searches"; 110 | }; 111 | } 112 | }; 113 | }; 114 | 115 | content: Adw.ToolbarView { 116 | [top] 117 | Adw.HeaderBar header_bar { 118 | centering-policy: strict; 119 | 120 | [title] 121 | Adw.Clamp title_clamp { 122 | tightening-threshold: 300; 123 | 124 | Box { 125 | hexpand: true; 126 | spacing: 4; 127 | 128 | Entry search_entry { 129 | hexpand: true; 130 | primary-icon-name: "edit-find-symbolic"; 131 | activates-default: true; 132 | } 133 | 134 | Button search_button { 135 | icon-name: "edit-find-symbolic"; 136 | tooltip-text: _("Search"); 137 | 138 | styles [ 139 | "suggested-action", 140 | ] 141 | } 142 | } 143 | } 144 | 145 | ToggleButton split_view_toggle_button { 146 | icon-name: "sidebar-show-symbolic"; 147 | tooltip-text: _("Show History"); 148 | } 149 | 150 | [end] 151 | MenuButton wordbook_menu_button { 152 | menu-model: app-menu; 153 | receives-default: true; 154 | direction: none; 155 | } 156 | } 157 | 158 | content: ScrolledWindow main_scroll { 159 | hexpand: true; 160 | vexpand: true; 161 | hscrollbar-policy: never; 162 | 163 | child: Viewport { 164 | Adw.Clamp main_clamp { 165 | tightening-threshold: 500; 166 | 167 | Box clamped_box { 168 | orientation: vertical; 169 | 170 | Adw.ViewStack main_stack { 171 | Adw.ViewStackPage { 172 | name: "download_page"; 173 | 174 | child: Adw.StatusPage download_status_page { 175 | title: _("Setting things up…"); 176 | description: _("Downloading WordNet…"); 177 | 178 | child: Adw.Clamp { 179 | tightening-threshold: 200; 180 | 181 | ProgressBar loading_progress { 182 | ellipsize: end; 183 | } 184 | }; 185 | }; 186 | } 187 | 188 | Adw.ViewStackPage { 189 | name: "welcome_page"; 190 | 191 | child: Adw.StatusPage before_search_page { 192 | icon-name: "dev.mufeed.Wordbook-symbolic"; 193 | title: _("Wordbook"); 194 | description: _("Look up definitions of any English term"); 195 | }; 196 | } 197 | 198 | Adw.ViewStackPage { 199 | name: "content_page"; 200 | 201 | child: Box content_box { 202 | orientation: vertical; 203 | 204 | Box { 205 | hexpand: false; 206 | 207 | Box { 208 | margin-start: 18; 209 | margin-end: 12; 210 | margin-top: 12; 211 | margin-bottom: 12; 212 | orientation: vertical; 213 | hexpand: false; 214 | 215 | Label term_view { 216 | label: "Term>"; 217 | use-markup: true; 218 | single-line-mode: true; 219 | ellipsize: end; 220 | xalign: 0; 221 | hexpand: false; 222 | } 223 | 224 | Label pronunciation_view { 225 | label: _("/Pronunciation/"); 226 | use-markup: true; 227 | selectable: true; 228 | ellipsize: end; 229 | single-line-mode: true; 230 | xalign: 0; 231 | hexpand: false; 232 | } 233 | } 234 | 235 | Button speak_button { 236 | margin-start: 4; 237 | margin-end: 12; 238 | margin-top: 12; 239 | margin-bottom: 12; 240 | receives-default: true; 241 | halign: center; 242 | valign: center; 243 | icon-name: "audio-volume-high-symbolic"; 244 | has-frame: false; 245 | hexpand: false; 246 | tooltip-text: _("Listen to Pronunciation"); 247 | 248 | styles [ 249 | "circular", 250 | ] 251 | } 252 | } 253 | 254 | Label def_view { 255 | margin-start: 18; 256 | margin-end: 18; 257 | margin-top: 12; 258 | margin-bottom: 12; 259 | wrap: true; 260 | selectable: true; 261 | xalign: 0; 262 | yalign: 0; 263 | 264 | GestureClick def_ctrlr {} 265 | } 266 | }; 267 | } 268 | 269 | Adw.ViewStackPage { 270 | name: "search_fail_page"; 271 | 272 | child: Adw.StatusPage search_fail_status_page { 273 | vexpand: true; 274 | icon-name: "edit-find-symbolic"; 275 | title: _("No definition found"); 276 | }; 277 | } 278 | 279 | Adw.ViewStackPage { 280 | name: "network_fail_page"; 281 | 282 | child: Adw.StatusPage network_fail_status_page { 283 | icon-name: "network-error-symbolic"; 284 | title: _("Download failed"); 285 | 286 | child: Box { 287 | spacing: 12; 288 | halign: center; 289 | 290 | Button retry_button { 291 | label: _("Retry"); 292 | 293 | styles [ 294 | "pill", 295 | "suggested-action", 296 | ] 297 | } 298 | 299 | Button exit_button { 300 | label: _("Exit"); 301 | 302 | styles [ 303 | "pill", 304 | ] 305 | } 306 | }; 307 | }; 308 | } 309 | 310 | Adw.ViewStackPage { 311 | name: "spinner_page"; 312 | 313 | child: Adw.Spinner {}; 314 | } 315 | } 316 | } 317 | } 318 | }; 319 | }; 320 | }; 321 | }; 322 | 323 | EventControllerKey key_ctrlr {} 324 | } 325 | -------------------------------------------------------------------------------- /data/search_provider.in: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | # -*- coding: utf-8 -*- 4 | # SPDX-FileCopyrightText: 2016-2025 Mufeed Ali 5 | # SPDX-License-Identifier: GPL-3.0-or-later 6 | 7 | 8 | import os 9 | import sys 10 | import uuid 11 | from gi.repository import GLib, Gio 12 | 13 | pkgdatadir = "@pkgdatadir@" 14 | localedir = "@localedir@" 15 | 16 | from wordbook import base, utils 17 | import wn 18 | 19 | wn.config.data_directory = os.path.join(utils.WN_DIR) 20 | wn.config.allow_multithreading = True 21 | 22 | dbus_interface_description = """ 23 | 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 | # Search provider service for integration with GNOME Shell search 56 | class WordbookSearchService: 57 | _results = {} 58 | 59 | # Get results for first search 60 | def GetInitialResultSet(self, terms): 61 | self._results = {} 62 | for term in terms: 63 | try: 64 | definitionResult = base.get_definition(term, "", "", wn.Wordnet(base.WN_DB_VERSION))["result"] 65 | if definitionResult: 66 | for resultArray in definitionResult.values(): 67 | if resultArray: 68 | result = resultArray[0] 69 | self._results[str(uuid.uuid4())] = { 70 | "name": result["name"], 71 | "definition": result["definition"], 72 | } 73 | except: 74 | print("Error while searching, WordNet is probably not downloaded yet.") 75 | 76 | return self._results.keys() 77 | 78 | # Get results for next searches 79 | def GetSubsearchResultSet(self, previous_results, new_terms): 80 | return self.GetInitialResultSet(new_terms) 81 | 82 | # Get detailed information for results 83 | def GetResultMetas(self, ids): 84 | metas = [] 85 | for item in ids: 86 | if item in self._results: 87 | meta = dict( 88 | id=GLib.Variant("s", self._results[item]["name"]), 89 | name=GLib.Variant("s", self._results[item]["name"]), 90 | description=GLib.Variant("s", self._results[item]["definition"]), 91 | ) 92 | metas.append(meta) 93 | 94 | return metas 95 | 96 | # Open clicked result in app 97 | def ActivateResult(self, id, terms, timestamp): 98 | GLib.spawn_async_with_pipes(None, ["@BIN@", "--look-up", id], None, GLib.SpawnFlags.SEARCH_PATH, None) 99 | 100 | # Open app on its current page 101 | def LaunchSearch(self, terms, timestamp): 102 | GLib.spawn_async_with_pipes(None, ["@BIN@"], None, GLib.SpawnFlags.SEARCH_PATH, None) 103 | 104 | 105 | # GIO application for search provider 106 | class WordbookSearchServiceApplication(Gio.Application): 107 | def __init__(self): 108 | Gio.Application.__init__( 109 | self, 110 | application_id="@APP_ID@.SearchProvider", 111 | flags=Gio.ApplicationFlags.IS_SERVICE, 112 | inactivity_timeout=10000, 113 | ) 114 | self.service_object = WordbookSearchService() 115 | self.search_interface = Gio.DBusNodeInfo.new_for_xml(dbus_interface_description).interfaces[0] 116 | 117 | # Register DBUS search provider object 118 | def do_dbus_register(self, connection, object_path): 119 | try: 120 | connection.register_object( 121 | object_path=object_path, 122 | interface_info=self.search_interface, 123 | method_call_closure=self.on_dbus_method_call, 124 | ) 125 | except: 126 | self.quit() 127 | return False 128 | finally: 129 | return True 130 | 131 | # Handle incoming method calls 132 | def on_dbus_method_call(self, connection, sender, object_path, interface_name, method_name, parameters, invocation): 133 | self.hold() 134 | 135 | method = getattr(self.service_object, method_name) 136 | arguments = list(parameters.unpack()) 137 | 138 | results = (method(*arguments),) 139 | if results == (None,): 140 | results = () 141 | results_type = ( 142 | "(" 143 | + "".join( 144 | map( 145 | lambda argument_info: argument_info.signature, 146 | self.search_interface.lookup_method(method_name).out_args, 147 | ) 148 | ) 149 | + ")" 150 | ) 151 | wrapped_results = GLib.Variant(results_type, results) 152 | 153 | invocation.return_value(wrapped_results) 154 | 155 | self.release() 156 | 157 | 158 | # Run search provider application 159 | if __name__ == "__main__": 160 | app = WordbookSearchServiceApplication() 161 | sys.exit(app.run()) 162 | -------------------------------------------------------------------------------- /images/ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufeedali/Wordbook/1c4f803e1ebf41aa0a98e82ece62374dc3717084/images/ss.png -------------------------------------------------------------------------------- /images/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufeedali/Wordbook/1c4f803e1ebf41aa0a98e82ece62374dc3717084/images/ss1.png -------------------------------------------------------------------------------- /images/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufeedali/Wordbook/1c4f803e1ebf41aa0a98e82ece62374dc3717084/images/ss2.png -------------------------------------------------------------------------------- /images/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufeedali/Wordbook/1c4f803e1ebf41aa0a98e82ece62374dc3717084/images/ss3.png -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | BUILD := "_build" 2 | 3 | default: 4 | @just --choose 5 | 6 | # Setup build folder. 7 | setup: 8 | mkdir -p {{BUILD}} 9 | meson setup . {{BUILD}} 10 | 11 | # Configure a local build. 12 | local-configure: 13 | meson configure {{BUILD}} -Dprefix=$(pwd)/{{BUILD}}/testdir 14 | ninja -C {{BUILD}} install 15 | 16 | # Configure a local build with debugging. 17 | develop-configure: 18 | meson configure {{BUILD}} -Dprefix=$(pwd)/{{BUILD}}/testdir -Dprofile=development 19 | ninja -C {{BUILD}} install 20 | 21 | # Run the local build. 22 | local-run: 23 | ninja -C {{BUILD}} run 24 | 25 | # Install system-wide. 26 | install: 27 | ninja -C {{BUILD}} install 28 | 29 | # Clean build files. 30 | clean: 31 | rm -r {{BUILD}} 32 | 33 | # Do everything needed and then run Wordbook for develpment in one command. 34 | run: setup develop-configure local-run clean -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'wordbook', 3 | version: '0.4.0', 4 | meson_version: '>= 0.59.0', 5 | default_options: [ 'warning_level=2', 6 | ], 7 | ) 8 | 9 | i18n = import('i18n') 10 | gnome = import('gnome') 11 | python = import('python') 12 | 13 | base_id = 'dev.mufeed.Wordbook' 14 | 15 | message('Looking for dependencies') 16 | py_installation = python.find_installation('python3') 17 | if not py_installation.found() 18 | error('No valid python3 binary found') 19 | else 20 | message('Found python3 binary') 21 | endif 22 | 23 | dependency('gobject-introspection-1.0', version: '>= 1.35.0') 24 | dependency('gtk4', version: '>= 4.6') 25 | dependency('libadwaita-1', version: '>=1.0') 26 | dependency('glib-2.0') 27 | dependency('pygobject-3.0', version: '>= 3.29.1') 28 | 29 | glib_compile_resources = find_program('glib-compile-resources', required: true) 30 | glib_compile_schemas = find_program('glib-compile-schemas', required: true) 31 | desktop_file_validate = find_program('desktop-file-validate', required: false) 32 | appstreamcli = find_program('appstreamcli', required: false) 33 | 34 | version = meson.project_version() 35 | version_array = version.split('.') 36 | major_version = version_array[0].to_int() 37 | minor_version = version_array[1].to_int() 38 | version_micro = version_array[2].to_int() 39 | 40 | prefix = get_option('prefix') 41 | bindir = prefix / get_option('bindir') 42 | localedir = prefix / get_option('localedir') 43 | 44 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 45 | profile = get_option('profile') 46 | moduledir = join_paths(pkgdatadir, meson.project_name()) 47 | 48 | if get_option('profile') == 'development' 49 | profile = 'Devel' 50 | vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip() 51 | application_id = '@0@.@1@'.format(base_id, profile) 52 | else 53 | profile = '' 54 | application_id = base_id 55 | endif 56 | 57 | subdir('data') 58 | subdir('wordbook') 59 | subdir('po') 60 | 61 | gnome.post_install( 62 | gtk_update_icon_cache: true, 63 | glib_compile_schemas: true, 64 | update_desktop_database: true, 65 | ) 66 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option( 2 | 'profile', 3 | type: 'combo', 4 | choices: [ 5 | 'default', 6 | 'development' 7 | ], 8 | value: 'default', 9 | description: 'The build profile for Wordbook. One of "default" or "development".' 10 | ) 11 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | de 2 | fr 3 | hi 4 | it 5 | nl 6 | ru 7 | tr 8 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | data/resources/ui/settings_window.blp 2 | data/resources/ui/shortcuts_window.blp 3 | data/resources/ui/window.blp 4 | wordbook/main.py 5 | wordbook/settings_window.py 6 | wordbook/window.py 7 | -------------------------------------------------------------------------------- /po/de.po: -------------------------------------------------------------------------------- 1 | # German translation for the Wordbook's package. 2 | # Copyright (C) 2024 THE Wordbook'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Wordbook's package. 4 | # gregorni , 2023. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: wordbook\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-10-10 18:31+0530\n" 11 | "PO-Revision-Date: 2023-02-18 22:39+0100\n" 12 | "Last-Translator: gregorni \n" 13 | "Language-Team: German \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: data/resources/ui/settings_window.blp:10 20 | msgid "Appearance" 21 | msgstr "Erscheinungsbild" 22 | 23 | #: data/resources/ui/settings_window.blp:13 24 | msgid "Force Dark Mode" 25 | msgstr "Dunkelmodus benutzen" 26 | 27 | #: data/resources/ui/settings_window.blp:18 28 | msgid "Behavior" 29 | msgstr "Verhalten" 30 | 31 | #: data/resources/ui/settings_window.blp:21 32 | msgid "Live Search" 33 | msgstr "Live-Suche" 34 | 35 | #: data/resources/ui/settings_window.blp:22 36 | msgid "Show definition as the terms are typed in" 37 | msgstr "Zeigt Ergebnisse an, während der Suchbegriff eingegeben wird" 38 | 39 | #: data/resources/ui/settings_window.blp:26 40 | msgid "Double Click Search" 41 | msgstr "Doppelklick zum Suchen" 42 | 43 | #: data/resources/ui/settings_window.blp:27 44 | msgid "Search any word by double clicking on it" 45 | msgstr "Doppelklicken Sie Wörter, um sie zu suchen" 46 | 47 | #: data/resources/ui/settings_window.blp:31 48 | msgid "Pronunciations Accent" 49 | msgstr "Akzent der Aussprache" 50 | 51 | #: data/resources/ui/shortcuts_window.blp:11 52 | msgctxt "shortcut window" 53 | msgid "General" 54 | msgstr "Allgemein" 55 | 56 | #: data/resources/ui/shortcuts_window.blp:14 57 | msgctxt "shortcut window" 58 | msgid "Show Shortcuts" 59 | msgstr "Tastenkürzel anzeigen" 60 | 61 | #: data/resources/ui/shortcuts_window.blp:19 62 | msgctxt "shortcut window" 63 | msgid "Preferences" 64 | msgstr "Einstellungen" 65 | 66 | #: data/resources/ui/shortcuts_window.blp:24 67 | msgctxt "shortcut window" 68 | msgid "Paste and Search" 69 | msgstr "Einfügen und Suchen" 70 | 71 | #: data/resources/ui/shortcuts_window.blp:29 72 | msgctxt "shortcut window" 73 | msgid "Random Word" 74 | msgstr "Zufälliges Wort" 75 | 76 | #: data/resources/ui/shortcuts_window.blp:34 77 | msgctxt "shortcut window" 78 | msgid "Search Selected Text" 79 | msgstr "Ausgewählten Text Suchen" 80 | 81 | #: data/resources/ui/window.blp:9 82 | msgid "Paste & Search" 83 | msgstr "Einfügen & Suchen" 84 | 85 | #: data/resources/ui/window.blp:14 86 | msgid "Search Selected Text" 87 | msgstr "Ausgewählten Text Suchen" 88 | 89 | #: data/resources/ui/window.blp:19 90 | msgid "_Random Word" 91 | msgstr "_Zufälliges Wort" 92 | 93 | #: data/resources/ui/window.blp:28 94 | msgid "_Preferences" 95 | msgstr "_Einstellungen" 96 | 97 | #: data/resources/ui/window.blp:33 98 | msgid "_Keyboard Shortcuts" 99 | msgstr "_Tastenkürzel" 100 | 101 | #: data/resources/ui/window.blp:38 102 | msgid "_About Wordbook" 103 | msgstr "_Über Wordbook" 104 | 105 | #: data/resources/ui/window.blp:67 106 | msgid "History" 107 | msgstr "Verlauf" 108 | 109 | #: data/resources/ui/window.blp:125 110 | msgid "Search" 111 | msgstr "Suchen" 112 | 113 | #: data/resources/ui/window.blp:136 114 | msgid "Show History" 115 | msgstr "Verlauf anzeigen" 116 | 117 | #: data/resources/ui/window.blp:164 118 | msgid "Setting things up…" 119 | msgstr "Die App wird vorbereitet…" 120 | 121 | #: data/resources/ui/window.blp:165 wordbook/window.py:576 122 | msgid "Downloading WordNet…" 123 | msgstr "WordNet wird heruntergeladen…" 124 | 125 | #: data/resources/ui/window.blp:182 wordbook/main.py:35 wordbook/main.py:84 126 | #: wordbook/main.py:117 127 | msgid "Wordbook" 128 | msgstr "Wordbook" 129 | 130 | #: data/resources/ui/window.blp:183 131 | msgid "Look up definitions of any English term" 132 | msgstr "Schlagen Sie die Definitionen englischer Wörter nach" 133 | 134 | #: data/resources/ui/window.blp:214 135 | msgid "/Pronunciation/" 136 | msgstr "/Aussprache/" 137 | 138 | #: data/resources/ui/window.blp:235 139 | msgid "Listen to Pronunciation" 140 | msgstr "Aussprache anhören" 141 | 142 | #: data/resources/ui/window.blp:264 143 | msgid "No definition found" 144 | msgstr "Keine Definition gefunden" 145 | 146 | #: data/resources/ui/window.blp:273 147 | msgid "Download failed" 148 | msgstr "Download fehlgeschlagen" 149 | 150 | #: data/resources/ui/window.blp:280 151 | msgid "Retry" 152 | msgstr "Erneut versuchen" 153 | 154 | #: data/resources/ui/window.blp:289 155 | msgid "Exit" 156 | msgstr "Beenden" 157 | 158 | #: wordbook/main.py:119 159 | msgid "Look up definitions of any English term." 160 | msgstr "Schlagen Sie die Definitionen englischer Wörter nach." 161 | 162 | #: wordbook/main.py:121 163 | msgid "translator-credits" 164 | msgstr "gregorni https://gitlab.com/gregorni" 165 | 166 | #: wordbook/main.py:125 167 | msgid "Copyright © 2016-2025 Mufeed Ali" 168 | msgstr "Copyright © 2016-2025 Mufeed Ali" 169 | 170 | #: wordbook/window.py:420 171 | msgid "Ready." 172 | msgstr "Bereit." 173 | 174 | #: wordbook/window.py:445 175 | msgid "Dismiss" 176 | msgstr "" 177 | 178 | #: wordbook/window.py:529 179 | msgid "Invalid input" 180 | msgstr "Ungültige Eingabe" 181 | 182 | #: wordbook/window.py:530 183 | msgid "Nothing definable was found in your search input" 184 | msgstr "Es wurde keine Definition für Ihren Suchbegriff gefunden" 185 | 186 | #: wordbook/window.py:594 187 | msgid "Re-downloading WordNet database" 188 | msgstr "Lade die WordNet-Datenbank erneut herunter" 189 | 190 | #: wordbook/window.py:596 191 | msgid "Just a database upgrade." 192 | msgstr "Die Datenbank wird aktualisiert." 193 | 194 | #: wordbook/window.py:598 195 | msgid "This shouldn't happen too often." 196 | msgstr "Dies sollte nicht zu häufig passieren." 197 | 198 | #: wordbook/window.py:643 199 | msgid "Building Database…" 200 | msgstr "Datenbank wird erstellt…" 201 | -------------------------------------------------------------------------------- /po/fr.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the wordbook package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: wordbook\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-10-10 18:31+0530\n" 11 | "PO-Revision-Date: 2022-07-03 00:53+0200\n" 12 | "Last-Translator: Irénée Thirion \n" 13 | "Language-Team: \n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | "X-Generator: Poedit 3.1\n" 20 | 21 | #: data/resources/ui/settings_window.blp:10 22 | msgid "Appearance" 23 | msgstr "Apparence" 24 | 25 | #: data/resources/ui/settings_window.blp:13 26 | msgid "Force Dark Mode" 27 | msgstr "Forcer le mode sombre" 28 | 29 | #: data/resources/ui/settings_window.blp:18 30 | msgid "Behavior" 31 | msgstr "Comportement" 32 | 33 | #: data/resources/ui/settings_window.blp:21 34 | msgid "Live Search" 35 | msgstr "Recherche en direct" 36 | 37 | #: data/resources/ui/settings_window.blp:22 38 | msgid "Show definition as the terms are typed in" 39 | msgstr "Afficher les définitions pendant que les termes sont tapés" 40 | 41 | #: data/resources/ui/settings_window.blp:26 42 | msgid "Double Click Search" 43 | msgstr "Recherche par double-clic" 44 | 45 | #: data/resources/ui/settings_window.blp:27 46 | msgid "Search any word by double clicking on it" 47 | msgstr "Recherchez n’importe quel mot en double-cliquant dessus" 48 | 49 | #: data/resources/ui/settings_window.blp:31 50 | msgid "Pronunciations Accent" 51 | msgstr "Accent de prononciation" 52 | 53 | #: data/resources/ui/shortcuts_window.blp:11 54 | msgctxt "shortcut window" 55 | msgid "General" 56 | msgstr "Général" 57 | 58 | #: data/resources/ui/shortcuts_window.blp:14 59 | msgctxt "shortcut window" 60 | msgid "Show Shortcuts" 61 | msgstr "Afficher les raccourcis" 62 | 63 | #: data/resources/ui/shortcuts_window.blp:19 64 | msgctxt "shortcut window" 65 | msgid "Preferences" 66 | msgstr "Préférences" 67 | 68 | #: data/resources/ui/shortcuts_window.blp:24 69 | msgctxt "shortcut window" 70 | msgid "Paste and Search" 71 | msgstr "Copier et rechercher" 72 | 73 | #: data/resources/ui/shortcuts_window.blp:29 74 | msgctxt "shortcut window" 75 | msgid "Random Word" 76 | msgstr "Mot aléatoire" 77 | 78 | #: data/resources/ui/shortcuts_window.blp:34 79 | msgctxt "shortcut window" 80 | msgid "Search Selected Text" 81 | msgstr "Rechercher le texte sélectionné" 82 | 83 | #: data/resources/ui/window.blp:9 84 | msgid "Paste & Search" 85 | msgstr "Copier et rechercher" 86 | 87 | #: data/resources/ui/window.blp:14 88 | msgid "Search Selected Text" 89 | msgstr "Rechercher le texte sélectionné" 90 | 91 | #: data/resources/ui/window.blp:19 92 | msgid "_Random Word" 93 | msgstr "_Mot aléatoire" 94 | 95 | #: data/resources/ui/window.blp:28 96 | msgid "_Preferences" 97 | msgstr "_Préférences" 98 | 99 | #: data/resources/ui/window.blp:33 100 | msgid "_Keyboard Shortcuts" 101 | msgstr "_Raccourcis clavier" 102 | 103 | #: data/resources/ui/window.blp:38 104 | msgid "_About Wordbook" 105 | msgstr "À _propos de Wordbook" 106 | 107 | #: data/resources/ui/window.blp:67 108 | msgid "History" 109 | msgstr "Historique" 110 | 111 | #: data/resources/ui/window.blp:125 112 | msgid "Search" 113 | msgstr "Rechercher" 114 | 115 | #: data/resources/ui/window.blp:136 116 | msgid "Show History" 117 | msgstr "Afficher l’historique" 118 | 119 | #: data/resources/ui/window.blp:164 120 | msgid "Setting things up…" 121 | msgstr "Préparation…" 122 | 123 | #: data/resources/ui/window.blp:165 wordbook/window.py:576 124 | msgid "Downloading WordNet…" 125 | msgstr "Téléchargement de WordNet…" 126 | 127 | #: data/resources/ui/window.blp:182 wordbook/main.py:35 wordbook/main.py:84 128 | #: wordbook/main.py:117 129 | msgid "Wordbook" 130 | msgstr "Wordbook" 131 | 132 | #: data/resources/ui/window.blp:183 133 | msgid "Look up definitions of any English term" 134 | msgstr "Recherchez les définitions d’un terme anglais" 135 | 136 | #: data/resources/ui/window.blp:214 137 | msgid "/Pronunciation/" 138 | msgstr "/Prononciation/" 139 | 140 | #: data/resources/ui/window.blp:235 141 | msgid "Listen to Pronunciation" 142 | msgstr "Écouter la prononciation" 143 | 144 | #: data/resources/ui/window.blp:264 145 | msgid "No definition found" 146 | msgstr "Aucune définition trouvée" 147 | 148 | #: data/resources/ui/window.blp:273 149 | msgid "Download failed" 150 | msgstr "Téléchargement échoué" 151 | 152 | #: data/resources/ui/window.blp:280 153 | msgid "Retry" 154 | msgstr "Réessayer" 155 | 156 | #: data/resources/ui/window.blp:289 157 | msgid "Exit" 158 | msgstr "Quitter" 159 | 160 | #: wordbook/main.py:119 161 | msgid "Look up definitions of any English term." 162 | msgstr "Recherchez les définitions d’un terme anglais" 163 | 164 | #: wordbook/main.py:121 165 | msgid "translator-credits" 166 | msgstr "" 167 | 168 | #: wordbook/main.py:125 169 | msgid "Copyright © 2016-2025 Mufeed Ali" 170 | msgstr "Copyright © 2016-2025 Mufeed Ali" 171 | 172 | #: wordbook/window.py:420 173 | msgid "Ready." 174 | msgstr "Prêt." 175 | 176 | #: wordbook/window.py:445 177 | msgid "Dismiss" 178 | msgstr "" 179 | 180 | #: wordbook/window.py:529 181 | msgid "Invalid input" 182 | msgstr "Entrée invalide" 183 | 184 | #: wordbook/window.py:530 185 | msgid "Nothing definable was found in your search input" 186 | msgstr "Rien de définissable n’a été trouvé dans votre entrée de recherche" 187 | 188 | #: wordbook/window.py:594 189 | msgid "Re-downloading WordNet database" 190 | msgstr "Re-téléchargement de la base de données WordNet" 191 | 192 | #: wordbook/window.py:596 193 | msgid "Just a database upgrade." 194 | msgstr "Simplement une mise à jour de la base de données" 195 | 196 | #: wordbook/window.py:598 197 | msgid "This shouldn't happen too often." 198 | msgstr "Cela ne devrait pas se produire trop souvent." 199 | 200 | #: wordbook/window.py:643 201 | msgid "Building Database…" 202 | msgstr "Construction de la base de données…" 203 | -------------------------------------------------------------------------------- /po/hi.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the wordbook package. 4 | # FIRST AUTHOR , YEAR. 5 | # Scrambled777 , 2024. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: wordbook\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-10-10 18:31+0530\n" 12 | "PO-Revision-Date: 2024-05-11 15:18+0530\n" 13 | "Last-Translator: Scrambled777 \n" 14 | "Language-Team: Hindi \n" 15 | "Language: hi\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | "X-Generator: Gtranslator 46.1\n" 21 | 22 | #: data/resources/ui/settings_window.blp:10 23 | msgid "Appearance" 24 | msgstr "दिखावट" 25 | 26 | #: data/resources/ui/settings_window.blp:13 27 | msgid "Force Dark Mode" 28 | msgstr "बलपूर्वक गहरा मोड" 29 | 30 | #: data/resources/ui/settings_window.blp:18 31 | msgid "Behavior" 32 | msgstr "व्यवहार" 33 | 34 | #: data/resources/ui/settings_window.blp:21 35 | msgid "Live Search" 36 | msgstr "लाइव खोज" 37 | 38 | #: data/resources/ui/settings_window.blp:22 39 | msgid "Show definition as the terms are typed in" 40 | msgstr "जैसे ही शब्द टाइप किए जाएं, परिभाषा दिखाएं" 41 | 42 | #: data/resources/ui/settings_window.blp:26 43 | msgid "Double Click Search" 44 | msgstr "डबल क्लिक पर खोजें" 45 | 46 | #: data/resources/ui/settings_window.blp:27 47 | msgid "Search any word by double clicking on it" 48 | msgstr "किसी भी शब्द पर डबल क्लिक करके उसे खोजें" 49 | 50 | #: data/resources/ui/settings_window.blp:31 51 | msgid "Pronunciations Accent" 52 | msgstr "उच्चारण लहजा" 53 | 54 | #: data/resources/ui/shortcuts_window.blp:11 55 | msgctxt "shortcut window" 56 | msgid "General" 57 | msgstr "सामान्य" 58 | 59 | #: data/resources/ui/shortcuts_window.blp:14 60 | msgctxt "shortcut window" 61 | msgid "Show Shortcuts" 62 | msgstr "शॉर्टकट दिखाएं" 63 | 64 | #: data/resources/ui/shortcuts_window.blp:19 65 | msgctxt "shortcut window" 66 | msgid "Preferences" 67 | msgstr "प्राथमिकताएं" 68 | 69 | #: data/resources/ui/shortcuts_window.blp:24 70 | msgctxt "shortcut window" 71 | msgid "Paste and Search" 72 | msgstr "पेस्ट करें और खोजें" 73 | 74 | #: data/resources/ui/shortcuts_window.blp:29 75 | msgctxt "shortcut window" 76 | msgid "Random Word" 77 | msgstr "यादृच्छिक शब्द" 78 | 79 | #: data/resources/ui/shortcuts_window.blp:34 80 | msgctxt "shortcut window" 81 | msgid "Search Selected Text" 82 | msgstr "चयनित पाठ खोजें" 83 | 84 | #: data/resources/ui/window.blp:9 85 | msgid "Paste & Search" 86 | msgstr "पेस्ट करें और खोजें" 87 | 88 | #: data/resources/ui/window.blp:14 89 | msgid "Search Selected Text" 90 | msgstr "चयनित पाठ खोजें" 91 | 92 | #: data/resources/ui/window.blp:19 93 | msgid "_Random Word" 94 | msgstr "यादृच्छिक शब्द (_R)" 95 | 96 | #: data/resources/ui/window.blp:28 97 | msgid "_Preferences" 98 | msgstr "प्राथमिकताएं (_P)" 99 | 100 | #: data/resources/ui/window.blp:33 101 | msgid "_Keyboard Shortcuts" 102 | msgstr "कीबोर्ड शॉर्टकट (_K)" 103 | 104 | #: data/resources/ui/window.blp:38 105 | msgid "_About Wordbook" 106 | msgstr "Wordbook के बारे में (_A)" 107 | 108 | #: data/resources/ui/window.blp:67 109 | msgid "History" 110 | msgstr "इतिहास" 111 | 112 | #: data/resources/ui/window.blp:125 113 | msgid "Search" 114 | msgstr "खोजें" 115 | 116 | #: data/resources/ui/window.blp:136 117 | msgid "Show History" 118 | msgstr "इतिहास दिखाएं" 119 | 120 | #: data/resources/ui/window.blp:164 121 | msgid "Setting things up…" 122 | msgstr "चीज़ें निर्धारित की जा रही हैं…" 123 | 124 | #: data/resources/ui/window.blp:165 wordbook/window.py:576 125 | msgid "Downloading WordNet…" 126 | msgstr "WordNet डाउनलोड हो रहा है…" 127 | 128 | #: data/resources/ui/window.blp:182 wordbook/main.py:35 wordbook/main.py:84 129 | #: wordbook/main.py:117 130 | msgid "Wordbook" 131 | msgstr "Wordbook" 132 | 133 | #: data/resources/ui/window.blp:183 134 | msgid "Look up definitions of any English term" 135 | msgstr "किसी भी अंग्रेजी शब्द की परिभाषा देखें" 136 | 137 | #: data/resources/ui/window.blp:214 138 | msgid "/Pronunciation/" 139 | msgstr "/उच्चारण/" 140 | 141 | #: data/resources/ui/window.blp:235 142 | msgid "Listen to Pronunciation" 143 | msgstr "उच्चारण सुनें" 144 | 145 | #: data/resources/ui/window.blp:264 146 | msgid "No definition found" 147 | msgstr "कोई परिभाषा नहीं मिली" 148 | 149 | #: data/resources/ui/window.blp:273 150 | msgid "Download failed" 151 | msgstr "डाउनलोड विफल" 152 | 153 | #: data/resources/ui/window.blp:280 154 | msgid "Retry" 155 | msgstr "पुन: प्रयास करें" 156 | 157 | #: data/resources/ui/window.blp:289 158 | msgid "Exit" 159 | msgstr "बाहर निकलें" 160 | 161 | #: wordbook/main.py:119 162 | msgid "Look up definitions of any English term." 163 | msgstr "किसी भी अंग्रेजी शब्द की परिभाषा देखें।" 164 | 165 | #: wordbook/main.py:121 166 | msgid "translator-credits" 167 | msgstr "Scrambled777 " 168 | 169 | #: wordbook/main.py:125 170 | msgid "Copyright © 2016-2025 Mufeed Ali" 171 | msgstr "कॉपीराइट © 2016-2025 मुफ़ीद अली" 172 | 173 | #: wordbook/window.py:420 174 | msgid "Ready." 175 | msgstr "तैयार।" 176 | 177 | #: wordbook/window.py:445 178 | msgid "Dismiss" 179 | msgstr "" 180 | 181 | #: wordbook/window.py:529 182 | msgid "Invalid input" 183 | msgstr "अमान्य आगत" 184 | 185 | #: wordbook/window.py:530 186 | msgid "Nothing definable was found in your search input" 187 | msgstr "आपके खोज आगत में कुछ भी निश्चित नहीं मिला" 188 | 189 | #: wordbook/window.py:594 190 | msgid "Re-downloading WordNet database" 191 | msgstr "WordNet डेटाबेस को पुनः डाउनलोड किया जा रहा है" 192 | 193 | #: wordbook/window.py:596 194 | msgid "Just a database upgrade." 195 | msgstr "बस एक डेटाबेस उन्नयन।" 196 | 197 | #: wordbook/window.py:598 198 | msgid "This shouldn't happen too often." 199 | msgstr "ऐसा अक्सर नहीं होना चाहिए।" 200 | 201 | #: wordbook/window.py:643 202 | msgid "Building Database…" 203 | msgstr "डेटाबेस का निर्माण…" 204 | -------------------------------------------------------------------------------- /po/it.po: -------------------------------------------------------------------------------- 1 | # ITALIAN TRANSLATION. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the wordbook package. 4 | # ALBANO BATTISTELLA , 2023. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: wordbook\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-10-10 18:31+0530\n" 11 | "PO-Revision-Date: 2023-10-01 08:26+0100\n" 12 | "Last-Translator: Albano Battistella \n" 13 | "Language-Team: italian \n" 14 | "Language: it\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: data/resources/ui/settings_window.blp:10 20 | msgid "Appearance" 21 | msgstr "Aspetto" 22 | 23 | #: data/resources/ui/settings_window.blp:13 24 | msgid "Force Dark Mode" 25 | msgstr "Forza la modalità scura" 26 | 27 | #: data/resources/ui/settings_window.blp:18 28 | msgid "Behavior" 29 | msgstr "Comportamento" 30 | 31 | #: data/resources/ui/settings_window.blp:21 32 | msgid "Live Search" 33 | msgstr "Ricerca in tempo reale" 34 | 35 | #: data/resources/ui/settings_window.blp:22 36 | msgid "Show definition as the terms are typed in" 37 | msgstr "Mostra la definizione mentre i termini vengono digitati" 38 | 39 | #: data/resources/ui/settings_window.blp:26 40 | msgid "Double Click Search" 41 | msgstr "Ricerca con doppio clic" 42 | 43 | #: data/resources/ui/settings_window.blp:27 44 | msgid "Search any word by double clicking on it" 45 | msgstr "Cerca qualsiasi parola facendo doppio clic su di essa" 46 | 47 | #: data/resources/ui/settings_window.blp:31 48 | msgid "Pronunciations Accent" 49 | msgstr "Accento delle pronunce" 50 | 51 | #: data/resources/ui/shortcuts_window.blp:11 52 | msgctxt "shortcut window" 53 | msgid "General" 54 | msgstr "Generale" 55 | 56 | #: data/resources/ui/shortcuts_window.blp:14 57 | msgctxt "shortcut window" 58 | msgid "Show Shortcuts" 59 | msgstr "Mostra scorciatoie" 60 | 61 | #: data/resources/ui/shortcuts_window.blp:19 62 | msgctxt "shortcut window" 63 | msgid "Preferences" 64 | msgstr "Preferenze" 65 | 66 | #: data/resources/ui/shortcuts_window.blp:24 67 | msgctxt "shortcut window" 68 | msgid "Paste and Search" 69 | msgstr "Incolla e cerca" 70 | 71 | #: data/resources/ui/shortcuts_window.blp:29 72 | msgctxt "shortcut window" 73 | msgid "Random Word" 74 | msgstr "Parola Casuale" 75 | 76 | #: data/resources/ui/shortcuts_window.blp:34 77 | msgctxt "shortcut window" 78 | msgid "Search Selected Text" 79 | msgstr "Cerca testo selezionato" 80 | 81 | #: data/resources/ui/window.blp:9 82 | msgid "Paste & Search" 83 | msgstr "Incolla & Cerca" 84 | 85 | #: data/resources/ui/window.blp:14 86 | msgid "Search Selected Text" 87 | msgstr "Cerca testo selezionato" 88 | 89 | #: data/resources/ui/window.blp:19 90 | msgid "_Random Word" 91 | msgstr "_Parola casuale" 92 | 93 | #: data/resources/ui/window.blp:28 94 | msgid "_Preferences" 95 | msgstr "_Preferenze" 96 | 97 | #: data/resources/ui/window.blp:33 98 | msgid "_Keyboard Shortcuts" 99 | msgstr "_Scorciatoie da tastiera" 100 | 101 | #: data/resources/ui/window.blp:38 102 | msgid "_About Wordbook" 103 | msgstr "_Informazioni su Wordbook" 104 | 105 | #: data/resources/ui/window.blp:67 106 | msgid "History" 107 | msgstr "Cronologia" 108 | 109 | #: data/resources/ui/window.blp:125 110 | msgid "Search" 111 | msgstr "Cerca" 112 | 113 | #: data/resources/ui/window.blp:136 114 | msgid "Show History" 115 | msgstr "Mostra Cronologia" 116 | 117 | #: data/resources/ui/window.blp:164 118 | msgid "Setting things up…" 119 | msgstr "Impostazione delle cose…" 120 | 121 | #: data/resources/ui/window.blp:165 wordbook/window.py:576 122 | msgid "Downloading WordNet…" 123 | msgstr "Download di WordNet…" 124 | 125 | #: data/resources/ui/window.blp:182 wordbook/main.py:35 wordbook/main.py:84 126 | #: wordbook/main.py:117 127 | msgid "Wordbook" 128 | msgstr "Wordbook" 129 | 130 | #: data/resources/ui/window.blp:183 131 | msgid "Look up definitions of any English term" 132 | msgstr "Cerca le definizioni di qualsiasi termine inglese" 133 | 134 | #: data/resources/ui/window.blp:214 135 | msgid "/Pronunciation/" 136 | msgstr "/Pronuncia/" 137 | 138 | #: data/resources/ui/window.blp:235 139 | msgid "Listen to Pronunciation" 140 | msgstr "Ascolta la pronuncia" 141 | 142 | #: data/resources/ui/window.blp:264 143 | msgid "No definition found" 144 | msgstr "Nessuna definizione trovata" 145 | 146 | #: data/resources/ui/window.blp:273 147 | msgid "Download failed" 148 | msgstr "Download non riuscito" 149 | 150 | #: data/resources/ui/window.blp:280 151 | msgid "Retry" 152 | msgstr "Riprova" 153 | 154 | #: data/resources/ui/window.blp:289 155 | msgid "Exit" 156 | msgstr "Uscita" 157 | 158 | #: wordbook/main.py:119 159 | msgid "Look up definitions of any English term." 160 | msgstr "Cerca le definizioni di qualsiasi termine inglese." 161 | 162 | #: wordbook/main.py:121 163 | msgid "translator-credits" 164 | msgstr "Albano Battistella" 165 | 166 | #: wordbook/main.py:125 167 | msgid "Copyright © 2016-2025 Mufeed Ali" 168 | msgstr "Copyright © 2016-2025 Mufeed Ali" 169 | 170 | #: wordbook/window.py:420 171 | msgid "Ready." 172 | msgstr "Pronto." 173 | 174 | #: wordbook/window.py:445 175 | msgid "Dismiss" 176 | msgstr "" 177 | 178 | #: wordbook/window.py:529 179 | msgid "Invalid input" 180 | msgstr "Inserimento non valido" 181 | 182 | #: wordbook/window.py:530 183 | msgid "Nothing definable was found in your search input" 184 | msgstr "Non è stato trovato nulla di definibile nel tuo input di ricerca" 185 | 186 | #: wordbook/window.py:594 187 | msgid "Re-downloading WordNet database" 188 | msgstr "Nuovo download del database WordNet" 189 | 190 | #: wordbook/window.py:596 191 | msgid "Just a database upgrade." 192 | msgstr "Solo un aggiornamento del database." 193 | 194 | #: wordbook/window.py:598 195 | msgid "This shouldn't happen too often." 196 | msgstr "Questo non dovrebbe accadere troppo spesso." 197 | 198 | #: wordbook/window.py:643 199 | msgid "Building Database…" 200 | msgstr "Creazione del database…" 201 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('wordbook', preset: 'glib') 2 | -------------------------------------------------------------------------------- /po/nl.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 2 | # This file is distributed under the same license as the PACKAGE package. 3 | # 4 | # Heimen Stoffels , 2022. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: \n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2024-10-10 18:31+0530\n" 10 | "PO-Revision-Date: 2022-09-19 20:14+0200\n" 11 | "Last-Translator: Heimen Stoffels \n" 12 | "Language-Team: Dutch\n" 13 | "Language: nl\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | "X-Generator: Lokalize 22.08.1\n" 19 | 20 | #: data/resources/ui/settings_window.blp:10 21 | msgid "Appearance" 22 | msgstr "Vormgeving" 23 | 24 | #: data/resources/ui/settings_window.blp:13 25 | msgid "Force Dark Mode" 26 | msgstr "Donker thema gebruiken" 27 | 28 | #: data/resources/ui/settings_window.blp:18 29 | msgid "Behavior" 30 | msgstr "Gedrag" 31 | 32 | #: data/resources/ui/settings_window.blp:21 33 | msgid "Live Search" 34 | msgstr "Typen om te zoeken" 35 | 36 | #: data/resources/ui/settings_window.blp:22 37 | msgid "Show definition as the terms are typed in" 38 | msgstr "Toon resultaten tijdens het typen" 39 | 40 | #: data/resources/ui/settings_window.blp:26 41 | msgid "Double Click Search" 42 | msgstr "Dubbelklikken om te zoeken" 43 | 44 | #: data/resources/ui/settings_window.blp:27 45 | msgid "Search any word by double clicking on it" 46 | msgstr "Zoek de betekenis van een woord door er op te dubbelklikken" 47 | 48 | #: data/resources/ui/settings_window.blp:31 49 | msgid "Pronunciations Accent" 50 | msgstr "Accent" 51 | 52 | #: data/resources/ui/shortcuts_window.blp:11 53 | msgctxt "shortcut window" 54 | msgid "General" 55 | msgstr "Algemeen" 56 | 57 | #: data/resources/ui/shortcuts_window.blp:14 58 | msgctxt "shortcut window" 59 | msgid "Show Shortcuts" 60 | msgstr "Sneltoetsen tonen" 61 | 62 | #: data/resources/ui/shortcuts_window.blp:19 63 | msgctxt "shortcut window" 64 | msgid "Preferences" 65 | msgstr "Voorkeuren" 66 | 67 | #: data/resources/ui/shortcuts_window.blp:24 68 | msgctxt "shortcut window" 69 | msgid "Paste and Search" 70 | msgstr "Plakken en zoeken" 71 | 72 | #: data/resources/ui/shortcuts_window.blp:29 73 | msgctxt "shortcut window" 74 | msgid "Random Word" 75 | msgstr "Willekeurig woord tonen" 76 | 77 | #: data/resources/ui/shortcuts_window.blp:34 78 | msgctxt "shortcut window" 79 | msgid "Search Selected Text" 80 | msgstr "Selectie zoeken" 81 | 82 | #: data/resources/ui/window.blp:9 83 | msgid "Paste & Search" 84 | msgstr "Plakken en zoeken" 85 | 86 | #: data/resources/ui/window.blp:14 87 | msgid "Search Selected Text" 88 | msgstr "Selectie zoeken" 89 | 90 | #: data/resources/ui/window.blp:19 91 | msgid "_Random Word" 92 | msgstr "Willekeu_rig woord tonen" 93 | 94 | #: data/resources/ui/window.blp:28 95 | msgid "_Preferences" 96 | msgstr "_Voorkeuren" 97 | 98 | #: data/resources/ui/window.blp:33 99 | msgid "_Keyboard Shortcuts" 100 | msgstr "_Sneltoetsen" 101 | 102 | #: data/resources/ui/window.blp:38 103 | msgid "_About Wordbook" 104 | msgstr "_Over Woordenboek" 105 | 106 | #: data/resources/ui/window.blp:67 107 | msgid "History" 108 | msgstr "Geschiedenis" 109 | 110 | #: data/resources/ui/window.blp:125 111 | msgid "Search" 112 | msgstr "Zoeken" 113 | 114 | #: data/resources/ui/window.blp:136 115 | msgid "Show History" 116 | msgstr "Geschiedenis tonen" 117 | 118 | #: data/resources/ui/window.blp:164 119 | msgid "Setting things up…" 120 | msgstr "Bezig met voorbereiden…" 121 | 122 | #: data/resources/ui/window.blp:165 wordbook/window.py:576 123 | msgid "Downloading WordNet…" 124 | msgstr "Bezig met ophalen van WordNet…" 125 | 126 | #: data/resources/ui/window.blp:182 wordbook/main.py:35 wordbook/main.py:84 127 | #: wordbook/main.py:117 128 | msgid "Wordbook" 129 | msgstr "Woordenboek" 130 | 131 | #: data/resources/ui/window.blp:183 132 | msgid "Look up definitions of any English term" 133 | msgstr "Bekijk de betekenis van Engelstalige termen" 134 | 135 | #: data/resources/ui/window.blp:214 136 | msgid "/Pronunciation/" 137 | msgstr "/Uitspraak/" 138 | 139 | #: data/resources/ui/window.blp:235 140 | msgid "Listen to Pronunciation" 141 | msgstr "Voorlezen" 142 | 143 | #: data/resources/ui/window.blp:264 144 | msgid "No definition found" 145 | msgstr "Er is geen betekenis gevonden" 146 | 147 | #: data/resources/ui/window.blp:273 148 | msgid "Download failed" 149 | msgstr "Het downloaden is mislukt" 150 | 151 | #: data/resources/ui/window.blp:280 152 | msgid "Retry" 153 | msgstr "Opnieuw" 154 | 155 | #: data/resources/ui/window.blp:289 156 | msgid "Exit" 157 | msgstr "Afsluiten" 158 | 159 | #: wordbook/main.py:119 160 | msgid "Look up definitions of any English term." 161 | msgstr "Bekijk de betekenis van Engelstalige termen." 162 | 163 | #: wordbook/main.py:121 164 | msgid "translator-credits" 165 | msgstr "Heimen Stoffels " 166 | 167 | #: wordbook/main.py:125 168 | msgid "Copyright © 2016-2025 Mufeed Ali" 169 | msgstr "Copyright © 2016-2025 Mufeed Ali" 170 | 171 | #: wordbook/window.py:420 172 | msgid "Ready." 173 | msgstr "Klaar voor gebruik." 174 | 175 | #: wordbook/window.py:445 176 | msgid "Dismiss" 177 | msgstr "" 178 | 179 | #: wordbook/window.py:529 180 | msgid "Invalid input" 181 | msgstr "Ongeldige invoer" 182 | 183 | #: wordbook/window.py:530 184 | msgid "Nothing definable was found in your search input" 185 | msgstr "Er is geen betekenis aangetroffen van de door u gezochte term" 186 | 187 | #: wordbook/window.py:594 188 | msgid "Re-downloading WordNet database" 189 | msgstr "Bezig met opnieuw ophalen van WordNet…" 190 | 191 | #: wordbook/window.py:596 192 | msgid "Just a database upgrade." 193 | msgstr "Gewoon een databankupdate." 194 | 195 | #: wordbook/window.py:598 196 | msgid "This shouldn't happen too often." 197 | msgstr "Dit zou niet al te vaak moeten gebeuren." 198 | 199 | #: wordbook/window.py:643 200 | msgid "Building Database…" 201 | msgstr "Bezig met samenstellen van databank…" 202 | -------------------------------------------------------------------------------- /po/ru.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the wordbook package. 4 | # FIRST AUTHOR , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: wordbook\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-10-10 18:31+0530\n" 11 | "PO-Revision-Date: 2022-08-27 14:57+0300\n" 12 | "Last-Translator: Danila Daniilov \n" 13 | "Language-Team: \n" 14 | "Language: ru\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 3.1\n" 19 | 20 | #: data/resources/ui/settings_window.blp:10 21 | msgid "Appearance" 22 | msgstr "Внешний вид" 23 | 24 | #: data/resources/ui/settings_window.blp:13 25 | msgid "Force Dark Mode" 26 | msgstr "Всегда использовать тёмный режим" 27 | 28 | #: data/resources/ui/settings_window.blp:18 29 | msgid "Behavior" 30 | msgstr "Поведение" 31 | 32 | #: data/resources/ui/settings_window.blp:21 33 | msgid "Live Search" 34 | msgstr "Поиск в реальном времени" 35 | 36 | #: data/resources/ui/settings_window.blp:22 37 | msgid "Show definition as the terms are typed in" 38 | msgstr "Показывать определение при вводе терминов" 39 | 40 | #: data/resources/ui/settings_window.blp:26 41 | msgid "Double Click Search" 42 | msgstr "Поиск по двойному клику" 43 | 44 | #: data/resources/ui/settings_window.blp:27 45 | msgid "Search any word by double clicking on it" 46 | msgstr "Поиск любого слова при двойном нажатии на него" 47 | 48 | #: data/resources/ui/settings_window.blp:31 49 | msgid "Pronunciations Accent" 50 | msgstr "Акцент произношения" 51 | 52 | #: data/resources/ui/shortcuts_window.blp:11 53 | msgctxt "shortcut window" 54 | msgid "General" 55 | msgstr "Общее" 56 | 57 | #: data/resources/ui/shortcuts_window.blp:14 58 | msgctxt "shortcut window" 59 | msgid "Show Shortcuts" 60 | msgstr "Показать комбинации клавиш" 61 | 62 | #: data/resources/ui/shortcuts_window.blp:19 63 | msgctxt "shortcut window" 64 | msgid "Preferences" 65 | msgstr "Настройки" 66 | 67 | #: data/resources/ui/shortcuts_window.blp:24 68 | msgctxt "shortcut window" 69 | msgid "Paste and Search" 70 | msgstr "Вставить и искать" 71 | 72 | #: data/resources/ui/shortcuts_window.blp:29 73 | msgctxt "shortcut window" 74 | msgid "Random Word" 75 | msgstr "Случайное слово" 76 | 77 | #: data/resources/ui/shortcuts_window.blp:34 78 | msgctxt "shortcut window" 79 | msgid "Search Selected Text" 80 | msgstr "Поиск выделенного текста" 81 | 82 | #: data/resources/ui/window.blp:9 83 | msgid "Paste & Search" 84 | msgstr "Вставить и искать" 85 | 86 | #: data/resources/ui/window.blp:14 87 | msgid "Search Selected Text" 88 | msgstr "Поиск выделенного текста" 89 | 90 | #: data/resources/ui/window.blp:19 91 | msgid "_Random Word" 92 | msgstr "Случайное слово" 93 | 94 | #: data/resources/ui/window.blp:28 95 | msgid "_Preferences" 96 | msgstr "Настройки" 97 | 98 | #: data/resources/ui/window.blp:33 99 | msgid "_Keyboard Shortcuts" 100 | msgstr "Комбинации клавиш" 101 | 102 | #: data/resources/ui/window.blp:38 103 | msgid "_About Wordbook" 104 | msgstr "О Wordbook" 105 | 106 | #: data/resources/ui/window.blp:67 107 | msgid "History" 108 | msgstr "История" 109 | 110 | #: data/resources/ui/window.blp:125 111 | msgid "Search" 112 | msgstr "Поиск" 113 | 114 | #: data/resources/ui/window.blp:136 115 | msgid "Show History" 116 | msgstr "Показать историю" 117 | 118 | #: data/resources/ui/window.blp:164 119 | msgid "Setting things up…" 120 | msgstr "Настройка..." 121 | 122 | #: data/resources/ui/window.blp:165 wordbook/window.py:576 123 | msgid "Downloading WordNet…" 124 | msgstr "Загрузка WordNet..." 125 | 126 | #: data/resources/ui/window.blp:182 wordbook/main.py:35 wordbook/main.py:84 127 | #: wordbook/main.py:117 128 | msgid "Wordbook" 129 | msgstr "" 130 | 131 | #: data/resources/ui/window.blp:183 132 | msgid "Look up definitions of any English term" 133 | msgstr "Поиск определений любых английских терминов" 134 | 135 | #: data/resources/ui/window.blp:214 136 | msgid "/Pronunciation/" 137 | msgstr "Произношение" 138 | 139 | #: data/resources/ui/window.blp:235 140 | msgid "Listen to Pronunciation" 141 | msgstr "Прослушать Произношение" 142 | 143 | #: data/resources/ui/window.blp:264 144 | msgid "No definition found" 145 | msgstr "Определение не найдено" 146 | 147 | #: data/resources/ui/window.blp:273 148 | msgid "Download failed" 149 | msgstr "Ошибка загрузки" 150 | 151 | #: data/resources/ui/window.blp:280 152 | msgid "Retry" 153 | msgstr "Попробовать снова" 154 | 155 | #: data/resources/ui/window.blp:289 156 | msgid "Exit" 157 | msgstr "Выйти" 158 | 159 | #: wordbook/main.py:119 160 | msgid "Look up definitions of any English term." 161 | msgstr "Поиск определений любых английских терминов." 162 | 163 | #: wordbook/main.py:121 164 | msgid "translator-credits" 165 | msgstr "hdsujnb" 166 | 167 | #: wordbook/main.py:125 168 | msgid "Copyright © 2016-2025 Mufeed Ali" 169 | msgstr "" 170 | 171 | #: wordbook/window.py:420 172 | msgid "Ready." 173 | msgstr "Готово." 174 | 175 | #: wordbook/window.py:445 176 | msgid "Dismiss" 177 | msgstr "" 178 | 179 | #: wordbook/window.py:529 180 | msgid "Invalid input" 181 | msgstr "Неправильный ввод" 182 | 183 | #: wordbook/window.py:530 184 | msgid "Nothing definable was found in your search input" 185 | msgstr "В результате поиска ничего не было найдено" 186 | 187 | #: wordbook/window.py:594 188 | msgid "Re-downloading WordNet database" 189 | msgstr "Повторная загрузка базы данных WordNet" 190 | 191 | #: wordbook/window.py:596 192 | msgid "Just a database upgrade." 193 | msgstr "Только обновление базы данных." 194 | 195 | #: wordbook/window.py:598 196 | msgid "This shouldn't happen too often." 197 | msgstr "Это не должно происходить слишком часто." 198 | 199 | #: wordbook/window.py:643 200 | msgid "Building Database…" 201 | msgstr "Создание базы данных..." 202 | -------------------------------------------------------------------------------- /po/tr.po: -------------------------------------------------------------------------------- 1 | # Turkish translation of dev.mufeed.Wordbook. 2 | # Copyright (C) 2024 dev.mufeed.Wordbook's COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the dev.mufeed.Wordbook package. 4 | # 5 | # Sabri Ünal , 2023. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: dev.mufeed.Wordbook\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-10-10 18:31+0530\n" 12 | "PO-Revision-Date: 2023-05-25 17:42+0300\n" 13 | "Last-Translator: Sabri Ünal \n" 14 | "Language-Team: Turkish \n" 15 | "Language: tr\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | "X-Generator: Poedit 3.2.2\n" 21 | 22 | #: data/resources/ui/settings_window.blp:10 23 | msgid "Appearance" 24 | msgstr "Görünüm" 25 | 26 | #: data/resources/ui/settings_window.blp:13 27 | msgid "Force Dark Mode" 28 | msgstr "Karanlık Kipi Zorla" 29 | 30 | #: data/resources/ui/settings_window.blp:18 31 | msgid "Behavior" 32 | msgstr "Davranış" 33 | 34 | #: data/resources/ui/settings_window.blp:21 35 | msgid "Live Search" 36 | msgstr "Canlı Arama" 37 | 38 | #: data/resources/ui/settings_window.blp:22 39 | msgid "Show definition as the terms are typed in" 40 | msgstr "Terimler yazılırken tanımı göster" 41 | 42 | #: data/resources/ui/settings_window.blp:26 43 | msgid "Double Click Search" 44 | msgstr "Çift Tık Arama" 45 | 46 | #: data/resources/ui/settings_window.blp:27 47 | msgid "Search any word by double clicking on it" 48 | msgstr "Üzerine çift tıklayarak herhangi bir kelimeyi ara" 49 | 50 | #: data/resources/ui/settings_window.blp:31 51 | msgid "Pronunciations Accent" 52 | msgstr "Telaffuz Aksanı" 53 | 54 | #: data/resources/ui/shortcuts_window.blp:11 55 | msgctxt "shortcut window" 56 | msgid "General" 57 | msgstr "Genel" 58 | 59 | #: data/resources/ui/shortcuts_window.blp:14 60 | msgctxt "shortcut window" 61 | msgid "Show Shortcuts" 62 | msgstr "Kısayolları Göster" 63 | 64 | #: data/resources/ui/shortcuts_window.blp:19 65 | msgctxt "shortcut window" 66 | msgid "Preferences" 67 | msgstr "Tercihler" 68 | 69 | #: data/resources/ui/shortcuts_window.blp:24 70 | msgctxt "shortcut window" 71 | msgid "Paste and Search" 72 | msgstr "Yapıştır ve Ara" 73 | 74 | #: data/resources/ui/shortcuts_window.blp:29 75 | msgctxt "shortcut window" 76 | msgid "Random Word" 77 | msgstr "Rastgele Sözcük" 78 | 79 | #: data/resources/ui/shortcuts_window.blp:34 80 | msgctxt "shortcut window" 81 | msgid "Search Selected Text" 82 | msgstr "Seçilen Metni Ara" 83 | 84 | #: data/resources/ui/window.blp:9 85 | msgid "Paste & Search" 86 | msgstr "Yapıştır ve Ara" 87 | 88 | #: data/resources/ui/window.blp:14 89 | msgid "Search Selected Text" 90 | msgstr "Seçilen Metni Ara" 91 | 92 | #: data/resources/ui/window.blp:19 93 | msgid "_Random Word" 94 | msgstr "_Rastgele Sözcük" 95 | 96 | #: data/resources/ui/window.blp:28 97 | msgid "_Preferences" 98 | msgstr "Te_rcihler" 99 | 100 | #: data/resources/ui/window.blp:33 101 | msgid "_Keyboard Shortcuts" 102 | msgstr "_Klavye Kısayolları" 103 | 104 | #: data/resources/ui/window.blp:38 105 | msgid "_About Wordbook" 106 | msgstr "Wordbook _Hakkında" 107 | 108 | #: data/resources/ui/window.blp:67 109 | msgid "History" 110 | msgstr "Geçmiş" 111 | 112 | #: data/resources/ui/window.blp:125 113 | msgid "Search" 114 | msgstr "Ara" 115 | 116 | #: data/resources/ui/window.blp:136 117 | msgid "Show History" 118 | msgstr "Geçmişi Göster" 119 | 120 | #: data/resources/ui/window.blp:164 121 | msgid "Setting things up…" 122 | msgstr "Wordbook ayarlanıyor…" 123 | 124 | #: data/resources/ui/window.blp:165 wordbook/window.py:576 125 | msgid "Downloading WordNet…" 126 | msgstr "WordNet İndiriliyor…" 127 | 128 | #: data/resources/ui/window.blp:182 wordbook/main.py:35 wordbook/main.py:84 129 | #: wordbook/main.py:117 130 | msgid "Wordbook" 131 | msgstr "Wordbook" 132 | 133 | #: data/resources/ui/window.blp:183 134 | msgid "Look up definitions of any English term" 135 | msgstr "Herhangi bir İngilizce terimin tanımını ara" 136 | 137 | #: data/resources/ui/window.blp:214 138 | msgid "/Pronunciation/" 139 | msgstr "/Telaffuz/" 140 | 141 | #: data/resources/ui/window.blp:235 142 | msgid "Listen to Pronunciation" 143 | msgstr "Telaffuzu Dinle" 144 | 145 | #: data/resources/ui/window.blp:264 146 | msgid "No definition found" 147 | msgstr "Tanım bulunamadı" 148 | 149 | #: data/resources/ui/window.blp:273 150 | msgid "Download failed" 151 | msgstr "İndirilemedi" 152 | 153 | #: data/resources/ui/window.blp:280 154 | msgid "Retry" 155 | msgstr "Yeniden Dene" 156 | 157 | #: data/resources/ui/window.blp:289 158 | msgid "Exit" 159 | msgstr "Çıkış" 160 | 161 | #: wordbook/main.py:119 162 | msgid "Look up definitions of any English term." 163 | msgstr "Herhangi bir İngilizce terimin tanımını ara." 164 | 165 | #: wordbook/main.py:121 166 | msgid "translator-credits" 167 | msgstr "Sabri Ünal " 168 | 169 | #: wordbook/main.py:125 170 | msgid "Copyright © 2016-2025 Mufeed Ali" 171 | msgstr "Telif Hakkı © 2016-2025 Mufeed Ali" 172 | 173 | #: wordbook/window.py:420 174 | msgid "Ready." 175 | msgstr "Hazır." 176 | 177 | #: wordbook/window.py:445 178 | msgid "Dismiss" 179 | msgstr "" 180 | 181 | #: wordbook/window.py:529 182 | msgid "Invalid input" 183 | msgstr "Geçersiz girdi" 184 | 185 | #: wordbook/window.py:530 186 | msgid "Nothing definable was found in your search input" 187 | msgstr "Arama girdinizde tanımlanabilir hiçbir şey bulunamadı" 188 | 189 | #: wordbook/window.py:594 190 | msgid "Re-downloading WordNet database" 191 | msgstr "WordNet veri tabanı yeniden indiriliyor" 192 | 193 | #: wordbook/window.py:596 194 | msgid "Just a database upgrade." 195 | msgstr "Sadece veri tabanı güncellemesi." 196 | 197 | #: wordbook/window.py:598 198 | msgid "This shouldn't happen too often." 199 | msgstr "Bu çok sık olmamalı." 200 | 201 | #: wordbook/window.py:643 202 | msgid "Building Database…" 203 | msgstr "Veritabanı inşa ediliyor…" 204 | -------------------------------------------------------------------------------- /po/wordbook.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the wordbook package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: wordbook\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-10-10 18:31+0530\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: data/resources/ui/settings_window.blp:10 21 | msgid "Appearance" 22 | msgstr "" 23 | 24 | #: data/resources/ui/settings_window.blp:13 25 | msgid "Force Dark Mode" 26 | msgstr "" 27 | 28 | #: data/resources/ui/settings_window.blp:18 29 | msgid "Behavior" 30 | msgstr "" 31 | 32 | #: data/resources/ui/settings_window.blp:21 33 | msgid "Live Search" 34 | msgstr "" 35 | 36 | #: data/resources/ui/settings_window.blp:22 37 | msgid "Show definition as the terms are typed in" 38 | msgstr "" 39 | 40 | #: data/resources/ui/settings_window.blp:26 41 | msgid "Double Click Search" 42 | msgstr "" 43 | 44 | #: data/resources/ui/settings_window.blp:27 45 | msgid "Search any word by double clicking on it" 46 | msgstr "" 47 | 48 | #: data/resources/ui/settings_window.blp:31 49 | msgid "Pronunciations Accent" 50 | msgstr "" 51 | 52 | #: data/resources/ui/shortcuts_window.blp:11 53 | msgctxt "shortcut window" 54 | msgid "General" 55 | msgstr "" 56 | 57 | #: data/resources/ui/shortcuts_window.blp:14 58 | msgctxt "shortcut window" 59 | msgid "Show Shortcuts" 60 | msgstr "" 61 | 62 | #: data/resources/ui/shortcuts_window.blp:19 63 | msgctxt "shortcut window" 64 | msgid "Preferences" 65 | msgstr "" 66 | 67 | #: data/resources/ui/shortcuts_window.blp:24 68 | msgctxt "shortcut window" 69 | msgid "Paste and Search" 70 | msgstr "" 71 | 72 | #: data/resources/ui/shortcuts_window.blp:29 73 | msgctxt "shortcut window" 74 | msgid "Random Word" 75 | msgstr "" 76 | 77 | #: data/resources/ui/shortcuts_window.blp:34 78 | msgctxt "shortcut window" 79 | msgid "Search Selected Text" 80 | msgstr "" 81 | 82 | #: data/resources/ui/window.blp:9 83 | msgid "Paste & Search" 84 | msgstr "" 85 | 86 | #: data/resources/ui/window.blp:14 87 | msgid "Search Selected Text" 88 | msgstr "" 89 | 90 | #: data/resources/ui/window.blp:19 91 | msgid "_Random Word" 92 | msgstr "" 93 | 94 | #: data/resources/ui/window.blp:28 95 | msgid "_Preferences" 96 | msgstr "" 97 | 98 | #: data/resources/ui/window.blp:33 99 | msgid "_Keyboard Shortcuts" 100 | msgstr "" 101 | 102 | #: data/resources/ui/window.blp:38 103 | msgid "_About Wordbook" 104 | msgstr "" 105 | 106 | #: data/resources/ui/window.blp:67 107 | msgid "History" 108 | msgstr "" 109 | 110 | #: data/resources/ui/window.blp:125 111 | msgid "Search" 112 | msgstr "" 113 | 114 | #: data/resources/ui/window.blp:136 115 | msgid "Show History" 116 | msgstr "" 117 | 118 | #: data/resources/ui/window.blp:164 119 | msgid "Setting things up…" 120 | msgstr "" 121 | 122 | #: data/resources/ui/window.blp:165 wordbook/window.py:576 123 | msgid "Downloading WordNet…" 124 | msgstr "" 125 | 126 | #: data/resources/ui/window.blp:182 wordbook/main.py:35 wordbook/main.py:84 127 | #: wordbook/main.py:117 128 | msgid "Wordbook" 129 | msgstr "" 130 | 131 | #: data/resources/ui/window.blp:183 132 | msgid "Look up definitions of any English term" 133 | msgstr "" 134 | 135 | #: data/resources/ui/window.blp:214 136 | msgid "/Pronunciation/" 137 | msgstr "" 138 | 139 | #: data/resources/ui/window.blp:235 140 | msgid "Listen to Pronunciation" 141 | msgstr "" 142 | 143 | #: data/resources/ui/window.blp:264 144 | msgid "No definition found" 145 | msgstr "" 146 | 147 | #: data/resources/ui/window.blp:273 148 | msgid "Download failed" 149 | msgstr "" 150 | 151 | #: data/resources/ui/window.blp:280 152 | msgid "Retry" 153 | msgstr "" 154 | 155 | #: data/resources/ui/window.blp:289 156 | msgid "Exit" 157 | msgstr "" 158 | 159 | #: wordbook/main.py:119 160 | msgid "Look up definitions of any English term." 161 | msgstr "" 162 | 163 | #: wordbook/main.py:121 164 | msgid "translator-credits" 165 | msgstr "" 166 | 167 | #: wordbook/main.py:125 168 | msgid "Copyright © 2016-2025 Mufeed Ali" 169 | msgstr "" 170 | 171 | #: wordbook/window.py:420 172 | msgid "Ready." 173 | msgstr "" 174 | 175 | #: wordbook/window.py:445 176 | msgid "Dismiss" 177 | msgstr "" 178 | 179 | #: wordbook/window.py:529 180 | msgid "Invalid input" 181 | msgstr "" 182 | 183 | #: wordbook/window.py:530 184 | msgid "Nothing definable was found in your search input" 185 | msgstr "" 186 | 187 | #: wordbook/window.py:594 188 | msgid "Re-downloading WordNet database" 189 | msgstr "" 190 | 191 | #: wordbook/window.py:596 192 | msgid "Just a database upgrade." 193 | msgstr "" 194 | 195 | #: wordbook/window.py:598 196 | msgid "This shouldn't happen too often." 197 | msgstr "" 198 | 199 | #: wordbook/window.py:643 200 | msgid "Building Database…" 201 | msgstr "" 202 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "Wordbook" 3 | dynamic = ["version"] 4 | requires-python = ">=3.12" 5 | dependencies = [ 6 | "pydantic>=2.11.5", 7 | "wn>=0.11.0", 8 | ] 9 | 10 | [tool.uv] 11 | package = false 12 | 13 | [tool.setuptools] 14 | packages = ["wordbook"] 15 | 16 | [tool.isort] 17 | profile = "black" 18 | line_length = 120 19 | 20 | [tool.pylint.format] 21 | max-line-length = 120 22 | 23 | [tool.black] 24 | line-length = 120 25 | 26 | [tool.ruff] 27 | line-length = 120 28 | 29 | [tool.pyright] 30 | reportMissingModuleSource = "none" 31 | 32 | [dependency-groups] 33 | dev = [ 34 | "basedpyright>=1.29.2", 35 | "pip>=25.0.1", 36 | "pygobject-stubs>=2.12.0", 37 | "requirements-parser>=0.11.0", 38 | ] 39 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | extend-ignore = E203 4 | exclude = build-aux/flatpak 5 | max-complexity = 15 6 | 7 | [pycodestyle] 8 | max-line-length = 120 9 | 10 | [pylama:pycodestyle] 11 | max_line_length = 120 12 | 13 | [pydocstyle] 14 | match = '(?!test_).*\.py' 15 | 16 | [aliases] 17 | -------------------------------------------------------------------------------- /subprojects/blueprint-compiler.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | directory = blueprint-compiler 3 | url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git 4 | revision = main 5 | depth = 1 6 | 7 | [provide] 8 | program_names = blueprint-compiler -------------------------------------------------------------------------------- /wordbook/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016-2025 Mufeed Ali 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | """Top-level package for wordbook. Empty because everything is in sub-modules.""" 5 | -------------------------------------------------------------------------------- /wordbook/base.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016-2025 Mufeed Ali 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | """ 5 | Base module for Wordbook, containing UI-independent logic. 6 | """ 7 | 8 | import difflib 9 | import html 10 | import json 11 | import os 12 | import subprocess 13 | import sys 14 | from concurrent.futures import ThreadPoolExecutor 15 | from functools import lru_cache 16 | from shutil import rmtree 17 | from typing import Any 18 | from collections.abc import Callable 19 | 20 | import wn 21 | 22 | from wordbook import utils 23 | 24 | POOL = ThreadPoolExecutor() 25 | WN_DB_VERSION: str = "oewn:2024" 26 | 27 | # Configure wn library 28 | wn.config.data_directory = os.path.join(utils.WN_DIR) 29 | wn.config.allow_multithreading = True 30 | 31 | # Parts of Speech Mapping (from wn tagset to human-readable) 32 | POS_MAP: dict[str, str] = { 33 | "s": "adjective", 34 | "n": "noun", 35 | "v": "verb", 36 | "r": "adverb", 37 | "a": "adjective", # Note: 'a' and 's' both map to adjective 38 | "t": "phrase", 39 | "c": "conjunction", 40 | "p": "adposition", 41 | "x": "other", 42 | "u": "unknown", 43 | } 44 | 45 | # Color Constants for Output Formatting 46 | DARK_MODE_SENTENCE_COLOR = "cyan" 47 | DARK_MODE_WORD_COLOR = "lightgreen" 48 | LIGHT_MODE_SENTENCE_COLOR = "blue" 49 | LIGHT_MODE_WORD_COLOR = "green" 50 | 51 | # Characters to remove during search term cleaning 52 | SEARCH_TERM_CLEANUP_CHARS = '<>"-?`![](){}/:;,' 53 | SEARCH_TERM_REPLACE_CHARS = ["(", ")", "<", ">", "[", "]", "&", "\\", "\n"] 54 | 55 | 56 | def _threadpool(func: Callable) -> Callable: 57 | """ 58 | Wraps around a function allowing it to run in a separate thread and 59 | return a future object. 60 | """ 61 | 62 | def wrap(*args: Any, **kwargs: Any) -> Any: 63 | return (POOL).submit(func, *args, **kwargs) 64 | 65 | return wrap 66 | 67 | 68 | def clean_search_terms(search_term: str) -> str: 69 | """ 70 | Cleans up search terms by removing leading/trailing whitespace, 71 | specific punctuation, and unwanted characters. 72 | """ 73 | text = search_term.strip().strip(SEARCH_TERM_CLEANUP_CHARS) 74 | for char in SEARCH_TERM_REPLACE_CHARS: 75 | text = text.replace(char, "") 76 | return text 77 | 78 | 79 | def create_required_dirs() -> None: 80 | """Make required directories if they don't already exist.""" 81 | os.makedirs(utils.CONFIG_DIR, exist_ok=True) # create Wordbook folder 82 | os.makedirs(utils.CDEF_DIR, exist_ok=True) # create Custom Definitions folder. 83 | 84 | 85 | def fetch_definition( 86 | text: str, 87 | wn_instance: wn.Wordnet, 88 | use_custom_def: bool = True, 89 | accent: str = "us", 90 | ) -> dict[str, Any]: 91 | """ 92 | Fetches the definition for a term, checking for a custom definition first if requested. 93 | 94 | Args: 95 | text: The term to define. 96 | wn_instance: The initialized Wordnet instance. 97 | use_custom_def: Whether to check for a custom definition first. 98 | accent: The espeak-ng accent code (e.g., "us", "gb"). 99 | 100 | Returns: 101 | A dictionary with the definition data. 102 | """ 103 | custom_def_path = os.path.join(utils.CDEF_DIR, text.lower()) 104 | if use_custom_def and os.path.isfile(custom_def_path): 105 | return get_custom_def(text, wn_instance, accent) 106 | return get_data(text, wn_instance, accent) 107 | 108 | 109 | def get_cowfortune() -> str: 110 | """ 111 | Presents a cowsay version of a fortune easter egg. 112 | 113 | Requires 'cowsay' and 'fortune'/'fortune-mod' to be installed. 114 | 115 | Returns: 116 | An HTML formatted string with the cowsay output, or an error message. 117 | """ 118 | try: 119 | # Ensure get_fortune runs first and potentially raises its own error if fortune isn't found 120 | fortune_text = get_fortune(mono=False) 121 | if "Easter egg fail!" in fortune_text: # Check if get_fortune failed 122 | return f"{fortune_text}" # Return fortune's error message 123 | 124 | process = subprocess.Popen( 125 | ["cowsay", fortune_text], 126 | stdout=subprocess.PIPE, 127 | stderr=subprocess.PIPE, # Capture stderr separately 128 | text=True, # Decode output automatically 129 | ) 130 | stdout, stderr = process.communicate() 131 | 132 | if process.returncode == 0 and stdout: 133 | return f"{html.escape(stdout)}" 134 | else: 135 | error_msg = f"Cowsay command failed. Return code: {process.returncode}. Stderr: {stderr.strip()}" 136 | utils.log_error(error_msg) 137 | return "Cowsay fail… Too bad…" 138 | except FileNotFoundError: 139 | error_msg = "Easter Egg Fail! 'cowsay' command not found. Please install it." 140 | utils.log_error(error_msg) 141 | return f"{error_msg}" 142 | except OSError as ex: 143 | error_msg = f"Easter Egg Fail! OS error during cowsay execution: {ex}" 144 | utils.log_error(error_msg) 145 | return f"{error_msg}" 146 | 147 | 148 | def get_custom_def(text: str, wn_instance: wn.Wordnet, accent: str = "us") -> dict[str, str]: 149 | """ 150 | Loads and presents a custom definition from a local file. 151 | 152 | Args: 153 | text: The term whose custom definition file should be read. 154 | wn_instance: The initialized Wordnet instance. 155 | accent: The espeak-ng accent code. 156 | 157 | Returns: 158 | A dictionary with the custom definition and related data. 159 | """ 160 | custom_def_path = os.path.join(utils.CDEF_DIR, text.lower()) 161 | try: 162 | with open(custom_def_path, encoding="utf-8") as def_file: 163 | custom_def_dict: dict[str, str] = json.load(def_file) 164 | except FileNotFoundError: 165 | # This is not an error, just means no custom definition exists. Fallback silently. 166 | return get_data(text, wn_instance, accent) 167 | except json.JSONDecodeError as e: 168 | utils.log_error(f"Error decoding custom definition file '{custom_def_path}': {e}") 169 | # Fallback to standard definition if custom file is corrupt 170 | return get_data(text, wn_instance, accent) 171 | except OSError as e: 172 | utils.log_error(f"OS error reading custom definition file '{custom_def_path}': {e}") 173 | # Fallback on other OS errors during file read 174 | return get_data(text, wn_instance, accent) 175 | 176 | # Handle 'linkto' redirection 177 | linked_term = custom_def_dict.get("linkto") 178 | if linked_term: 179 | return get_data(linked_term, wn_instance, accent) 180 | 181 | # Get definition string, falling back to WordNet if not provided 182 | definition = custom_def_dict.get("out_string", "") 183 | 184 | formatted_definition = definition or "" 185 | 186 | term = custom_def_dict.get("term", text) 187 | # Get pronunciation, falling back to espeak-ng 188 | pronunciation = custom_def_dict.get("pronunciation") 189 | if not pronunciation: 190 | pronunciation = get_pronunciation(term, accent) 191 | pronunciation = ( 192 | pronunciation 193 | if pronunciation and not pronunciation.isspace() 194 | else "Pronunciation unavailable (is espeak-ng installed?)" 195 | ) 196 | 197 | final_data: dict[str, str] = { 198 | "term": term, 199 | "pronunciation": pronunciation, 200 | "out_string": formatted_definition, 201 | } 202 | return final_data 203 | 204 | 205 | def get_data(term: str, wn_instance: wn.Wordnet, accent: str = "us") -> dict[str, Any]: 206 | """ 207 | Obtains the definition and pronunciation data for a term from WordNet. 208 | 209 | Args: 210 | term: The term to define. 211 | wn_instance: The initialized Wordnet instance. 212 | accent: The espeak-ng accent code. 213 | 214 | Returns: 215 | A dictionary containing the definition data with pronunciation information. 216 | """ 217 | # Obtain definition from WordNet 218 | definition_data = get_definition(term, wn_instance) 219 | 220 | # Determine the term to use for pronunciation (found lemma or original) 221 | pronunciation_term = definition_data.get("term") or term 222 | 223 | # Get pronunciation 224 | pron = get_pronunciation(pronunciation_term, accent) 225 | final_pron = pron if pron and not pron.isspace() else "Pronunciation unavailable (is espeak-ng installed?)" 226 | # Create the dictionary to be returned. 227 | final_data: dict[str, Any] = { 228 | "term": definition_data.get("term", term), # Use original term if lookup failed 229 | "pronunciation": final_pron, 230 | "result": definition_data.get("result"), # This holds the structured data 231 | "out_string": definition_data.get("out_string"), 232 | } 233 | 234 | return final_data 235 | 236 | 237 | def _find_best_lemma_match(term: str, lemmas: list[str]) -> str: 238 | """Finds the best matching lemma for the search term.""" 239 | diff_match = difflib.get_close_matches(term, lemmas, n=1, cutoff=0.8) 240 | return diff_match[0].strip() if diff_match else lemmas[0].strip() 241 | 242 | 243 | def _extract_related_lemmas(synset: wn.Synset) -> dict[str, list[str]]: 244 | """Extracts synonyms, antonyms, similar terms, and 'also sees'.""" 245 | related: dict[str, list[str]] = {"syn": [], "ant": [], "sim": [], "also_sees": []} 246 | base_lemma = synset.lemmas()[0] # Use first lemma as reference if needed 247 | 248 | # Synonyms (other lemmas in the same synset) 249 | related["syn"] = [lemma.replace("_", " ").strip() for lemma in synset.lemmas() if lemma != base_lemma] 250 | 251 | # Antonyms 252 | for sense in synset.senses(): 253 | for ant_sense in sense.get_related("antonym"): 254 | ant_name = ant_sense.word().lemma().replace("_", " ").strip() 255 | if ant_name not in related["ant"]: # Avoid duplicates 256 | related["ant"].append(ant_name) 257 | 258 | # Similar To 259 | for sim_synset in synset.get_related("similar"): 260 | related["sim"].extend(lemma.replace("_", " ").strip() for lemma in sim_synset.lemmas()) 261 | 262 | # Also See 263 | for also_synset in synset.get_related("also"): 264 | related["also_sees"].extend(lemma.replace("_", " ").strip() for lemma in also_synset.lemmas()) 265 | 266 | return related 267 | 268 | 269 | def get_definition(term: str, wn_instance: wn.Wordnet) -> dict[str, Any]: 270 | """ 271 | Gets the definition from WordNet, processes it, and prepares data structure. 272 | 273 | Args: 274 | term: The term to define. 275 | wn_instance: The initialized Wordnet instance. 276 | 277 | Returns: 278 | A dictionary with the processed definition data ('term', 'result', 'out_string'). 279 | """ 280 | first_match: str | None = None 281 | # Initialize result_dict with all possible POS keys from POS_MAP 282 | result_dict: dict[str, Any] = {pos: [] for pos in POS_MAP.values()} 283 | 284 | synsets = wn_instance.synsets(term) 285 | 286 | if not synsets: 287 | # Term not found in WordNet 288 | clean_def = {"term": term, "result": None, "out_string": None} 289 | return clean_def 290 | 291 | for synset in synsets: 292 | pos_tag = synset.pos 293 | pos_name = POS_MAP.get(pos_tag) 294 | if not pos_name: 295 | utils.log_warning(f"Unknown POS tag encountered: {pos_tag} for term '{term}'") 296 | pos_name = POS_MAP["u"] # Default to 'unknown' 297 | 298 | lemmas = synset.lemmas() 299 | if not lemmas: 300 | continue # Skip synsets with no lemmas 301 | 302 | # Find the best lemma match and store the first good one found 303 | matched_lemma = _find_best_lemma_match(term, lemmas) 304 | if first_match is None: 305 | first_match = matched_lemma 306 | 307 | # Extract related lemmas (synonyms, antonyms, etc.) 308 | related_lemmas = _extract_related_lemmas(synset) 309 | 310 | synset_data: dict[str, Any] = { 311 | "name": matched_lemma, 312 | "definition": synset.definition() or "No definition available.", 313 | "examples": synset.examples() or [], 314 | **related_lemmas, # Merge related lemmas dict 315 | } 316 | 317 | result_dict[pos_name].append(synset_data) 318 | 319 | # Prepare the final output structure 320 | # Note: 'out_string' is usually generated later by the UI/formatter based on 'result' 321 | clean_def = { 322 | "term": first_match or term, # Fallback to original term if no match found 323 | "result": result_dict, 324 | "out_string": None, # Formatted string is generated elsewhere 325 | } 326 | return clean_def 327 | 328 | 329 | def get_fortune(mono: bool = True) -> str: 330 | """ 331 | Presents a fortune easter egg. Requires 'fortune' or 'fortune-mod'. 332 | 333 | Args: 334 | mono: If True, wraps the output in tags for monospace display. 335 | 336 | Returns: 337 | The fortune text, HTML escaped, optionally in tags, or an error message. 338 | """ 339 | try: 340 | process = subprocess.Popen( 341 | ["fortune", "-a"], 342 | stdout=subprocess.PIPE, 343 | stderr=subprocess.PIPE, 344 | text=True, 345 | ) 346 | stdout, stderr = process.communicate() 347 | 348 | if process.returncode == 0 and stdout: 349 | fortune_output = html.escape(stdout.strip(), False) 350 | else: 351 | error_msg = f"Fortune command failed. Return code: {process.returncode}. Stderr: {stderr.strip()}" 352 | utils.log_error(error_msg) 353 | fortune_output = "Easter egg fail! Could not get fortune." 354 | 355 | except FileNotFoundError: 356 | fortune_output = "Easter egg fail! 'fortune' command not found. Install 'fortune' or 'fortune-mod'." 357 | utils.log_error(fortune_output) 358 | except OSError as ex: 359 | fortune_output = f"Easter egg fail! OS error during fortune execution: {ex}" 360 | utils.log_error(fortune_output) 361 | 362 | return f"{fortune_output}" if mono else fortune_output 363 | 364 | 365 | @lru_cache(maxsize=128) 366 | def get_pronunciation(term: str, accent: str = "us") -> str | None: 367 | """ 368 | Gets the pronunciation of a term using espeak-ng. 369 | 370 | Args: 371 | term: The word or phrase to pronounce. 372 | accent: The espeak-ng accent code (e.g., "us", "gb"). 373 | 374 | Returns: 375 | The pronunciation in IPA format (e.g., "/tˈɛst/"), or None if espeak-ng fails. 376 | """ 377 | try: 378 | process = subprocess.Popen( 379 | ["espeak-ng", "-v", f"en-{accent}", "--ipa=3", "-q", term], # Use IPA level 3 for more detail 380 | stdout=subprocess.PIPE, 381 | stderr=subprocess.PIPE, 382 | text=True, 383 | ) 384 | stdout, stderr = process.communicate(timeout=5) # Add timeout 385 | 386 | if process.returncode == 0 and stdout: 387 | # Clean up potential extra whitespace and format 388 | ipa_pronunciation = stdout.strip().replace("\n", " ").replace(" ", " ") 389 | return f"/{ipa_pronunciation.strip('/')}/" 390 | 391 | utils.log_warning(f"espeak-ng failed for term '{term}'. RC: {process.returncode}. Stderr: {stderr.strip()}") 392 | return None 393 | except FileNotFoundError: 394 | utils.log_error("'espeak-ng' command not found. Please install espeak-ng.") 395 | return None 396 | except subprocess.TimeoutExpired: 397 | utils.log_error(f"espeak-ng timed out for term '{term}'.") 398 | return None 399 | except OSError as ex: 400 | utils.log_error(f"OS error executing espeak-ng for term '{term}': {ex}") 401 | return None 402 | 403 | 404 | def get_version_info(app_version: str) -> None: 405 | """Prints application and dependency version info to the console.""" 406 | print(f"Wordbook - {app_version}") 407 | print("Copyright 2016-2025 Mufeed Ali") 408 | print() 409 | try: 410 | process = subprocess.Popen( 411 | ["espeak-ng", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True 412 | ) 413 | stdout, stderr = process.communicate(timeout=5) 414 | 415 | if process.returncode == 0 and stdout: 416 | print(stdout.strip()) 417 | else: 418 | utils.log_error(f"Failed to get espeak-ng version. RC: {process.returncode}. Stderr: {stderr.strip()}") 419 | print("Could not retrieve espeak-ng version information.") 420 | 421 | except FileNotFoundError: 422 | utils.log_error("'espeak-ng' command not found during version check.") 423 | print("Dependency missing: espeak-ng is not installed or not in PATH.") 424 | except subprocess.TimeoutExpired: 425 | utils.log_error("espeak-ng --version command timed out.") 426 | print("Could not retrieve espeak-ng version information (timeout).") 427 | except OSError as ex: 428 | utils.log_error(f"OS error executing espeak-ng --version: {ex}") 429 | print(f"Could not retrieve espeak-ng version information (OS error: {ex})") 430 | 431 | 432 | @_threadpool 433 | def get_wn_file(reloader: Callable[[], None]) -> dict[str, Any]: 434 | """ 435 | Initializes the WordNet instance and fetches the word list in a thread. 436 | 437 | Handles potential WordNet database errors and triggers the reloader function. 438 | 439 | Args: 440 | reloader: A function to call if WordNet initialization fails (e.g., to trigger download). 441 | 442 | Returns: 443 | A dictionary containing the WordNet instance ('instance') and the word list ('list'), 444 | or the result of the reloader function if initialization fails. 445 | """ 446 | utils.log_info("Initializing WordNet...") 447 | try: 448 | wn_instance: wn.Wordnet = wn.Wordnet(lexicon=WN_DB_VERSION) 449 | utils.log_info(f"WordNet instance ({WN_DB_VERSION}) created.") 450 | 451 | utils.log_info("Fetching WordNet wordlist...") 452 | 453 | wn_lemmas = [w.lemma() for w in wn_instance.words()] 454 | utils.log_info(f"WordNet wordlist fetched ({len(wn_lemmas)} lemmas). WordNet is ready.") 455 | return {"instance": wn_instance, "list": wn_lemmas} 456 | 457 | except (wn.Error, wn.DatabaseError) as e: 458 | utils.log_error(f"WordNet initialization failed: {e}. Triggering reloader.") 459 | return reloader() 460 | except Exception as e: 461 | # Catch unexpected errors during initialization 462 | utils.log_error(f"Unexpected error during WordNet initialization: {e}. Retrying.") 463 | return reloader() 464 | 465 | 466 | def format_output( 467 | text: str, wn_instance: wn.Wordnet, use_custom_def: bool, accent: str = "us" 468 | ) -> dict[str, Any] | None: 469 | """ 470 | Determines colors, handles special commands (fortune, exit), and fetches definitions. 471 | 472 | Args: 473 | text: The input text (search term or command). 474 | wn_instance: The initialized Wordnet instance. 475 | use_custom_def: Whether to check for custom definitions. 476 | accent: The espeak-ng accent code. 477 | 478 | Returns: 479 | A dictionary containing definition data, or None if input is invalid/empty. 480 | Exits the program for specific commands. 481 | """ 482 | # Easter Eggs and Special Commands 483 | if text == "fortune -a": 484 | return { 485 | "term": "Some random adage", 486 | "pronunciation": "Courtesy of fortune", 487 | "out_string": get_fortune(), 488 | } 489 | if text == "cowfortune": 490 | return { 491 | "term": "Some random adage from a cow", 492 | "pronunciation": "Courtesy of fortune and cowsay", 493 | "out_string": get_cowfortune(), 494 | } 495 | if text in ("crash now", "close now"): 496 | utils.log_info(f"Exiting due to command: '{text}'") 497 | sys.exit(0) # Intentional exit 498 | 499 | # Fetch definition for valid, non-empty text 500 | if text and not text.isspace(): 501 | cleaned_text = clean_search_terms(text) 502 | if cleaned_text: # Ensure text isn't empty after cleaning 503 | definition_data = fetch_definition(cleaned_text, wn_instance, use_custom_def=use_custom_def, accent=accent) 504 | return definition_data 505 | else: 506 | utils.log_info(f"Input '{text}' became empty after cleaning.") 507 | return None # Return None if text becomes empty after cleaning 508 | else: 509 | utils.log_info(f"Input text is empty or whitespace: '{text}'") 510 | return None # Return None for empty or whitespace input 511 | 512 | 513 | def read_term(text: str, speed: int = 120, accent: str = "us") -> None: 514 | """ 515 | Uses espeak-ng to speak the given text aloud. 516 | 517 | Args: 518 | text: The text to speak. 519 | speed: Speaking speed (words per minute). 520 | accent: The espeak-ng accent code. 521 | """ 522 | try: 523 | subprocess.run( 524 | ["espeak-ng", "-s", str(speed), "-v", f"en-{accent}", text], 525 | stdout=subprocess.DEVNULL, # Discard standard output 526 | stderr=subprocess.PIPE, # Capture standard error 527 | check=False, # Don't raise exception on non-zero exit, handle manually 528 | timeout=10, # Add timeout 529 | text=True, 530 | ) 531 | # Note: We don't check result.check_returncode() here as espeak might return non-zero 532 | # even on successful speech in some cases. Logging stderr might be useful if debugging. 533 | # if result.stderr: 534 | # utils.log_warning(f"espeak-ng stderr while reading term '{text}': {result.stderr.strip()}") 535 | 536 | except FileNotFoundError: 537 | utils.log_error("'espeak-ng' command not found. Cannot read term aloud.") 538 | except subprocess.TimeoutExpired: 539 | utils.log_error(f"espeak-ng timed out while trying to read term: '{text}'") 540 | except OSError as ex: 541 | utils.log_error(f"OS error executing espeak-ng to read term '{text}': {ex}") 542 | 543 | 544 | class WordnetDownloader: 545 | @staticmethod 546 | def check_status() -> bool: 547 | """ 548 | Checks if the primary WordNet database file exists. 549 | """ 550 | db_path = os.path.join(utils.WN_DIR, "wn.db") 551 | utils.log_info(f"Checking for WordNet DB at: {db_path}") 552 | return os.path.isfile(db_path) 553 | 554 | @staticmethod 555 | def download(progress_handler: Callable[[int, int], None] | None = None) -> None: 556 | """ 557 | Downloads the specified WordNet database version using wn.download. 558 | 559 | Removes the temporary 'downloads' directory first if it exists. 560 | 561 | Args: 562 | progress_handler: An optional callback function for progress updates. 563 | """ 564 | download_dir = os.path.join(utils.WN_DIR, "downloads") 565 | if os.path.isdir(download_dir): 566 | utils.log_info(f"Removing existing temporary download directory: {download_dir}") 567 | rmtree(download_dir) 568 | 569 | utils.log_info(f"Starting download of WordNet version: {WN_DB_VERSION}") 570 | try: 571 | # Let wn handle the download, but pass a callback to track progress 572 | _ = wn.download(WN_DB_VERSION, progress_handler=progress_handler) 573 | utils.log_info(f"WordNet download completed for {WN_DB_VERSION}.") 574 | except Exception as e: 575 | # Catch potential errors during download (network issues, wn errors) 576 | utils.log_error(f"WordNet download failed for {WN_DB_VERSION}: {e}") 577 | raise 578 | 579 | @staticmethod 580 | def delete_db() -> None: 581 | """ 582 | Deletes the primary WordNet database file. 583 | """ 584 | db_path = os.path.join(utils.WN_DIR, "wn.db") 585 | if os.path.isfile(db_path): 586 | try: 587 | utils.log_info(f"Deleting WordNet database file: {db_path}") 588 | os.remove(db_path) 589 | except OSError as e: 590 | utils.log_error(f"Failed to delete WordNet database file '{db_path}': {e}") 591 | else: 592 | utils.log_warning(f"Attempted to delete WordNet database, but file not found: {db_path}") 593 | -------------------------------------------------------------------------------- /wordbook/main.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016-2025 Mufeed Ali 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | from gettext import gettext as _ 5 | 6 | import gi 7 | 8 | gi.require_version("Gdk", "4.0") 9 | gi.require_version("Gtk", "4.0") 10 | gi.require_version("Adw", "1") 11 | from gi.repository import Adw, Gio, GLib, Gtk # noqa 12 | 13 | from wordbook import base, utils # noqa 14 | from wordbook.window import WordbookWindow # noqa 15 | from wordbook.settings import Settings # noqa 16 | 17 | 18 | class Application(Adw.Application): 19 | """Manages the windows, properties, etc of Wordbook.""" 20 | 21 | app_id: str = "" 22 | development_mode: bool = False 23 | version: str = "0.0.0" 24 | 25 | lookup_term: str | None = None 26 | auto_paste_requested: bool = False 27 | win: WordbookWindow | None = None 28 | 29 | def __init__(self, app_id: str, version: str): 30 | """Initialize the application.""" 31 | super().__init__( 32 | application_id=app_id, 33 | flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, 34 | ) 35 | GLib.set_application_name(_("Wordbook")) 36 | GLib.set_prgname(self.app_id) 37 | 38 | self.app_id = app_id 39 | self.version = version 40 | 41 | # Add command line options 42 | self.add_main_option( 43 | "look-up", 44 | b"l", 45 | GLib.OptionFlags.NONE, 46 | GLib.OptionArg.STRING, 47 | "Term to look up", 48 | None, 49 | ) 50 | self.add_main_option( 51 | "info", 52 | ord("i"), 53 | GLib.OptionFlags.NONE, 54 | GLib.OptionArg.NONE, 55 | "Print version info", 56 | None, 57 | ) 58 | self.add_main_option( 59 | "verbose", 60 | ord("v"), 61 | GLib.OptionFlags.NONE, 62 | GLib.OptionArg.NONE, 63 | "Make it scream louder", 64 | None, 65 | ) 66 | self.add_main_option( 67 | "auto-paste", 68 | b"p", 69 | GLib.OptionFlags.NONE, 70 | GLib.OptionArg.NONE, 71 | "Automatically paste and search clipboard content", 72 | None, 73 | ) 74 | 75 | Adw.StyleManager.get_default().set_color_scheme( 76 | Adw.ColorScheme.FORCE_DARK if Settings.get().gtk_dark_ui else Adw.ColorScheme.PREFER_LIGHT 77 | ) 78 | 79 | base.create_required_dirs() 80 | 81 | def do_startup(self): 82 | """Manage startup of the application.""" 83 | self.set_resource_base_path(utils.RES_PATH) 84 | Adw.Application.do_startup(self) 85 | 86 | def do_activate(self): 87 | """Activate the application.""" 88 | self.win = self.get_active_window() 89 | if not self.win: 90 | self.win = WordbookWindow( 91 | application=self, 92 | title=_("Wordbook"), 93 | term=self.lookup_term, 94 | auto_paste_requested=self.auto_paste_requested, 95 | ) 96 | self.setup_actions() 97 | 98 | self.win.present() 99 | 100 | def do_command_line(self, command_line): 101 | """Parse commandline arguments.""" 102 | options = command_line.get_options_dict().end().unpack() 103 | term = "" 104 | 105 | if "verinfo" in options: 106 | base.get_version_info(self.version) 107 | return 0 108 | 109 | if "look-up" in options: 110 | term = options["look-up"] 111 | 112 | if "auto-paste" in options: 113 | self.auto_paste_requested = True 114 | 115 | utils.log_init(self.development_mode or "verbose" in options or False) 116 | 117 | if self.win is not None: 118 | if term: 119 | self.win.trigger_search(term) 120 | elif self.auto_paste_requested: 121 | self.win.queue_auto_paste() 122 | else: 123 | self.lookup_term = term 124 | 125 | self.activate() 126 | return 0 127 | 128 | def on_about(self, _action, _param): 129 | """Show the about window.""" 130 | about_window = Adw.AboutWindow() 131 | about_window.set_application_icon(Gio.Application.get_default().app_id) 132 | about_window.set_application_name(_("Wordbook")) 133 | about_window.set_version(Gio.Application.get_default().version) 134 | about_window.set_comments(_("Look up definitions of any English term.")) 135 | about_window.set_developer_name("Mufeed Ali") 136 | about_window.set_translator_credits(_("translator-credits")) 137 | about_window.set_license_type(Gtk.License.GPL_3_0) 138 | about_window.set_website("https://github.com/mufeedali/Wordbook") 139 | about_window.set_issue_url("https://github.com/mufeedali/Wordbook/issues") 140 | about_window.set_copyright(_("Copyright © 2016-2025 Mufeed Ali")) 141 | about_window.set_transient_for(self.win) 142 | about_window.present() 143 | 144 | def setup_actions(self): 145 | """Setup the Gio actions for the application.""" 146 | about_action = Gio.SimpleAction.new("about", None) 147 | about_action.connect("activate", self.on_about) 148 | self.add_action(about_action) 149 | 150 | self.set_accels_for_action("win.search-selected", ["s"]) 151 | self.set_accels_for_action("win.random-word", ["r"]) 152 | self.set_accels_for_action("win.paste-search", ["v"]) 153 | self.set_accels_for_action("win.preferences", ["comma"]) 154 | -------------------------------------------------------------------------------- /wordbook/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | moduledir = join_paths(pkgdatadir, 'wordbook') 3 | 4 | python = import('python') 5 | 6 | conf = configuration_data() 7 | 8 | conf.set_quoted('APP_ID', application_id) 9 | conf.set_quoted('VERSION', meson.project_version()) 10 | conf.set_quoted('PROFILE', profile) 11 | 12 | conf.set('PYTHON', python.find_installation('python3').full_path()) 13 | conf.set_quoted('localedir', join_paths(get_option('prefix'), get_option('localedir'))) 14 | conf.set_quoted('pkgdatadir', pkgdatadir) 15 | conf.set('datadir', get_option('datadir')) 16 | conf.set('prefix', get_option('prefix')) 17 | 18 | configure_file( 19 | input: 'wordbook.in', 20 | output: 'wordbook', 21 | configuration: conf, 22 | install: true, 23 | install_dir: get_option('bindir') 24 | ) 25 | 26 | program_executable = join_paths( 27 | meson.project_build_root(), 28 | meson.project_name(), 29 | meson.project_name() 30 | ) 31 | run_target('run', 32 | command: [program_executable] 33 | ) 34 | 35 | wordbook_sources = [ 36 | '__init__.py', 37 | 'base.py', 38 | 'main.py', 39 | 'settings.py', 40 | 'settings_window.py', 41 | 'utils.py', 42 | 'window.py', 43 | ] 44 | 45 | install_data(wordbook_sources, install_dir: moduledir) 46 | -------------------------------------------------------------------------------- /wordbook/settings.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016-2025 Mufeed Ali 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | from __future__ import annotations 5 | 6 | import json 7 | import os 8 | from enum import Enum 9 | from pathlib import Path 10 | from typing import Any 11 | 12 | from pydantic import BaseModel, Field, field_validator 13 | 14 | from wordbook import utils 15 | 16 | 17 | class PronunciationAccent(Enum): 18 | """Enumeration of supported pronunciation accents.""" 19 | 20 | US = ("us", "American English") 21 | GB = ("gb", "British English") 22 | 23 | def __init__(self, code: str, display_name: str): 24 | self.code = code 25 | self.display_name = display_name 26 | 27 | @classmethod 28 | def from_code(cls, code: str) -> "PronunciationAccent": 29 | """Get accent enum from code string.""" 30 | for accent in cls: 31 | if accent.code == code: 32 | return accent 33 | return cls.US # Default fallback 34 | 35 | @classmethod 36 | def from_index(cls, index: int) -> "PronunciationAccent": 37 | """Get accent enum from index.""" 38 | accents = list(cls) 39 | if 0 <= index < len(accents): 40 | return accents[index] 41 | return cls.US # Default fallback 42 | 43 | @property 44 | def index(self) -> int: 45 | """Get the index of this accent in the enum.""" 46 | return list(PronunciationAccent).index(self) 47 | 48 | 49 | class BehaviorSettings(BaseModel): 50 | """Settings related to application behavior.""" 51 | 52 | custom_definitions: bool = Field(default=True, description="Enable custom definitions") 53 | live_search: bool = Field(default=True, description="Enable live search") 54 | double_click: bool = Field(default=False, description="Search on double click") 55 | pronunciations_accent: str = Field(default="us", description="Pronunciation accent") 56 | auto_paste_on_launch: bool = Field(default=False, description="Auto paste from clipboard on launch") 57 | 58 | @field_validator("pronunciations_accent") 59 | @classmethod 60 | def validate_accent(cls, accent: str) -> str: 61 | """Validate pronunciation accent.""" 62 | if accent not in [a.code for a in PronunciationAccent]: 63 | return PronunciationAccent.US.code # Default fallback 64 | return accent 65 | 66 | 67 | class AppearanceSettings(BaseModel): 68 | """Settings related to application appearance.""" 69 | 70 | force_dark_mode: bool = Field(default=False, description="Force dark mode") 71 | 72 | 73 | class StateSettings(BaseModel): 74 | """State settings.""" 75 | 76 | history: list[str] = Field(default_factory=list, description="Search history") 77 | 78 | @field_validator("history") 79 | @classmethod 80 | def validate_history(cls, v: list[str]) -> list[str]: 81 | """Validate and limit history size.""" 82 | # Keep only last 10 items 83 | return v[-10:] if len(v) > 10 else v 84 | 85 | 86 | class WordbookSettings(BaseModel): 87 | """Main settings model for Wordbook application.""" 88 | 89 | behavior: BehaviorSettings = Field(default_factory=BehaviorSettings) 90 | appearance: AppearanceSettings = Field(default_factory=AppearanceSettings) 91 | state: StateSettings = Field(default_factory=StateSettings) 92 | 93 | 94 | class Settings: 95 | """Manages all the settings of the application using Pydantic models.""" 96 | 97 | _initializing: bool = True 98 | _instance: Settings | None = None 99 | _settings: WordbookSettings 100 | 101 | def __init__(self): 102 | """Initialize settings.""" 103 | self._config_file: Path = Path(utils.CONFIG_DIR) / "wordbook.json" 104 | 105 | # Ensure config directory exists 106 | os.makedirs(utils.CONFIG_DIR, exist_ok=True) 107 | 108 | self._load_settings() 109 | self._initializing = False 110 | 111 | def __setattr__(self, name: str, value: Any) -> None: 112 | """Override setattr to automatically save settings when properties are changed.""" 113 | super().__setattr__(name, value) 114 | # Auto-save after setting any property 115 | # Avoid during initialization or for private attributes 116 | if not self._initializing and not name.startswith("_"): 117 | self._save_settings() 118 | 119 | @classmethod 120 | def get(cls) -> Settings: 121 | """Get singleton instance of Settings.""" 122 | if cls._instance is None: 123 | cls._instance = cls() 124 | return cls._instance 125 | 126 | def _load_settings(self) -> None: 127 | """Load settings from file.""" 128 | if self._config_file.exists(): 129 | self._load_from_json() 130 | else: 131 | # Create default settings 132 | self._settings = WordbookSettings() 133 | self._save_settings() 134 | 135 | def _load_from_json(self) -> None: 136 | """Load settings from JSON file.""" 137 | try: 138 | with open(self._config_file, "r") as f: 139 | data = json.load(f) 140 | 141 | self._settings = WordbookSettings.model_validate(data) 142 | utils.log_info("Loaded settings from JSON configuration") 143 | 144 | except Exception as e: 145 | utils.log_error(f"Failed to load JSON settings: {e}") 146 | utils.log_info("Creating default settings") 147 | self._settings = WordbookSettings() 148 | self._save_settings() 149 | 150 | def _save_settings(self) -> None: 151 | """Save settings to JSON file.""" 152 | try: 153 | with open(self._config_file, "w") as f: 154 | json.dump(self._settings.model_dump(), f, indent=4) 155 | utils.log_debug("Settings saved successfully") 156 | except Exception as e: 157 | utils.log_error(f"Failed to save settings: {e}") 158 | 159 | # Behavior settings properties 160 | @property 161 | def cdef(self) -> bool: 162 | """Get custom definition status.""" 163 | return self._settings.behavior.custom_definitions 164 | 165 | @cdef.setter 166 | def cdef(self, value: bool) -> None: 167 | """Set custom definition status.""" 168 | self._settings.behavior.custom_definitions = value 169 | 170 | @property 171 | def live_search(self) -> bool: 172 | """Get live search status.""" 173 | return self._settings.behavior.live_search 174 | 175 | @live_search.setter 176 | def live_search(self, value: bool) -> None: 177 | """Set live search status.""" 178 | self._settings.behavior.live_search = value 179 | 180 | @property 181 | def double_click(self) -> bool: 182 | """Get double click search status.""" 183 | return self._settings.behavior.double_click 184 | 185 | @double_click.setter 186 | def double_click(self, value: bool) -> None: 187 | """Set double click search status.""" 188 | self._settings.behavior.double_click = value 189 | 190 | @property 191 | def auto_paste_on_launch(self) -> bool: 192 | """Get auto paste on launch status.""" 193 | return self._settings.behavior.auto_paste_on_launch 194 | 195 | @auto_paste_on_launch.setter 196 | def auto_paste_on_launch(self, value: bool) -> None: 197 | """Set auto paste on launch status.""" 198 | self._settings.behavior.auto_paste_on_launch = value 199 | 200 | @property 201 | def pronunciations_accent(self) -> PronunciationAccent: 202 | """Get pronunciations accent as enum.""" 203 | return PronunciationAccent.from_code(self._settings.behavior.pronunciations_accent) 204 | 205 | @pronunciations_accent.setter 206 | def pronunciations_accent(self, value: PronunciationAccent) -> None: 207 | """Set pronunciations accent by enum value.""" 208 | self._settings.behavior.pronunciations_accent = value.code 209 | 210 | @property 211 | def pronunciations_accent_enum(self) -> PronunciationAccent: 212 | """Get pronunciations accent as enum.""" 213 | return PronunciationAccent.from_code(self._settings.behavior.pronunciations_accent) 214 | 215 | # Appearance settings properties 216 | @property 217 | def gtk_dark_ui(self) -> bool: 218 | """Get GTK dark theme setting.""" 219 | return self._settings.appearance.force_dark_mode 220 | 221 | @gtk_dark_ui.setter 222 | def gtk_dark_ui(self, value: bool) -> None: 223 | """Set GTK dark theme setting.""" 224 | self._settings.appearance.force_dark_mode = value 225 | 226 | # State settings properties 227 | @property 228 | def history(self) -> list[str]: 229 | """Get search history.""" 230 | return self._settings.state.history.copy() 231 | 232 | @history.setter 233 | def history(self, value: list[str]) -> None: 234 | """Set search history.""" 235 | # Validate and limit history 236 | self._settings.state.history = value[-10:] if len(value) > 10 else value 237 | 238 | def clear_history(self) -> None: 239 | """Clear search history.""" 240 | self._settings.state.history = [] 241 | 242 | # Utility methods 243 | def reset_to_defaults(self) -> None: 244 | """Reset all settings to defaults.""" 245 | utils.log_info("Resetting settings to defaults") 246 | self._settings = WordbookSettings() 247 | self._save_settings() 248 | -------------------------------------------------------------------------------- /wordbook/settings_window.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016-2025 Mufeed Ali 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import os 5 | 6 | from gi.repository import Adw, Gtk 7 | 8 | from wordbook import utils 9 | from wordbook.settings import PronunciationAccent, Settings 10 | 11 | PATH: str = os.path.dirname(__file__) 12 | 13 | 14 | @Gtk.Template(resource_path=f"{utils.RES_PATH}/ui/settings_window.ui") 15 | class SettingsDialog(Adw.PreferencesDialog): 16 | """Allows the user to customize Wordbook to some extent.""" 17 | 18 | __gtype_name__ = "SettingsDialog" 19 | 20 | _dark_ui_switch: Adw.SwitchRow = Gtk.Template.Child("dark_ui_switch") 21 | 22 | _double_click_switch: Adw.SwitchRow = Gtk.Template.Child("double_click_switch") 23 | _live_search_switch: Adw.SwitchRow = Gtk.Template.Child("live_search_switch") 24 | _auto_paste_switch: Adw.SwitchRow = Gtk.Template.Child("auto_paste_switch") 25 | _pronunciations_accent_row: Adw.ComboRow = Gtk.Template.Child("pronunciations_accent_row") 26 | 27 | def __init__(self, parent: Adw.ApplicationWindow, **kwargs): 28 | """Initialize the Settings window.""" 29 | super().__init__(**kwargs) 30 | 31 | self.parent = parent 32 | 33 | self.load_settings() 34 | 35 | self._double_click_switch.connect("notify::active", self._double_click_switch_activate) 36 | self._live_search_switch.connect("notify::active", self._on_live_search_activate) 37 | self._auto_paste_switch.connect("notify::active", self._on_auto_paste_switch_activate) 38 | self._dark_ui_switch.connect("notify::active", self._on_dark_ui_switch_activate) 39 | self._pronunciations_accent_row.connect("notify::selected", self._on_pronunciations_accent_activate) 40 | 41 | def load_settings(self): 42 | """Load settings from the Settings instance.""" 43 | self._double_click_switch.set_active(Settings.get().double_click) 44 | self._live_search_switch.set_active(Settings.get().live_search) 45 | self._auto_paste_switch.set_active(Settings.get().auto_paste_on_launch) 46 | self._pronunciations_accent_row.set_selected(Settings.get().pronunciations_accent.index) 47 | 48 | self._dark_ui_switch.set_active(Settings.get().gtk_dark_ui) 49 | 50 | @staticmethod 51 | def _double_click_switch_activate(switch, _gparam): 52 | """Change 'double click to search' state.""" 53 | Settings.get().double_click = switch.get_active() 54 | 55 | def _on_live_search_activate(self, switch, _gparam): 56 | """Change live search state.""" 57 | self.parent.completer.set_popup_completion(not switch.get_active()) 58 | self.parent.search_button.set_visible(not switch.get_active()) 59 | if not switch.get_active(): 60 | self.parent.set_default_widget(self.parent.search_button) 61 | Settings.get().live_search = switch.get_active() 62 | 63 | @staticmethod 64 | def _on_auto_paste_switch_activate(switch, _gparam): 65 | """Change auto paste on launch state.""" 66 | Settings.get().auto_paste_on_launch = switch.get_active() 67 | 68 | @staticmethod 69 | def _on_pronunciations_accent_activate(row, _gparam): 70 | """Change pronunciations' accent.""" 71 | Settings.get().pronunciations_accent = PronunciationAccent.from_index(row.get_selected()) 72 | 73 | @staticmethod 74 | def _on_dark_ui_switch_activate(switch, _gparam): 75 | """Change UI theme.""" 76 | Settings.get().gtk_dark_ui = switch.get_active() 77 | Adw.StyleManager.get_default().set_color_scheme( 78 | Adw.ColorScheme.FORCE_DARK if switch.get_active() else Adw.ColorScheme.PREFER_LIGHT 79 | ) 80 | -------------------------------------------------------------------------------- /wordbook/utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016-2025 Mufeed Ali 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | """utils contains a few global variables and essential functions.""" 5 | 6 | from __future__ import annotations 7 | 8 | import logging 9 | import os 10 | import traceback 11 | from typing import TYPE_CHECKING 12 | 13 | from gi.repository import GLib 14 | 15 | if TYPE_CHECKING: 16 | from logging import Logger 17 | from typing import Literal 18 | 19 | RES_PATH = "/dev/mufeed/Wordbook" 20 | 21 | CONFIG_DIR: str = os.path.join(GLib.get_user_config_dir(), "wordbook") 22 | CONFIG_FILE: str = os.path.join(CONFIG_DIR, "wordbook.conf") 23 | DATA_DIR: str = os.path.join(GLib.get_user_data_dir(), "wordbook") 24 | CDEF_DIR: str = os.path.join(DATA_DIR, "cdef") 25 | WN_DIR: str = os.path.join(DATA_DIR, "wn") 26 | 27 | logging.basicConfig(format="%(asctime)s - [%(levelname)s] [%(threadName)s] (%(module)s:%(lineno)d) %(message)s") 28 | LOGGER: Logger = logging.getLogger() 29 | 30 | 31 | def bool_to_str(boolean: bool) -> Literal["yes", "no"]: 32 | """Convert boolean to string for configuration parser.""" 33 | if boolean is True: 34 | return "yes" 35 | return "no" 36 | 37 | 38 | def log_init(debug: bool) -> None: 39 | """Initialize logging.""" 40 | if debug is True: 41 | level = logging.DEBUG 42 | else: 43 | level = logging.WARNING 44 | LOGGER.setLevel(level) 45 | 46 | 47 | def log_critical(message: str) -> None: 48 | """Log a critical error and if possible, its traceback.""" 49 | LOGGER.critical(message) 50 | trace = traceback.format_exc() 51 | if trace.strip() != "NoneType: None": 52 | LOGGER.critical(traceback.format_exc()) 53 | 54 | 55 | def log_debug(message: str) -> None: 56 | """Log a debug message and if possible, its traceback.""" 57 | LOGGER.debug(message) 58 | trace = traceback.format_exc() 59 | if trace.strip() != "NoneType: None": 60 | LOGGER.debug(traceback.format_exc()) 61 | 62 | 63 | def log_error(message: str) -> None: 64 | """Log an error and if possible, its traceback.""" 65 | LOGGER.error(message) 66 | trace = traceback.format_exc() 67 | if trace.strip() != "NoneType: None": 68 | LOGGER.error(traceback.format_exc()) 69 | 70 | 71 | def log_info(message: str) -> None: 72 | """Log a message and if possible, its traceback.""" 73 | LOGGER.info(message) 74 | trace = traceback.format_exc() 75 | if trace.strip() != "NoneType: None": 76 | LOGGER.info(trace) 77 | 78 | 79 | def log_warning(message: str) -> None: 80 | """Log a warning and if possible, its traceback.""" 81 | LOGGER.warning(message) 82 | trace = traceback.format_exc() 83 | if trace.strip() != "NoneType: None": 84 | LOGGER.warning(traceback.format_exc()) 85 | -------------------------------------------------------------------------------- /wordbook/window.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016-2025 Mufeed Ali 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | from __future__ import annotations 5 | 6 | import os 7 | import random 8 | import sys 9 | import threading 10 | from enum import auto, Enum 11 | from gettext import gettext as _ 12 | from html import escape 13 | from typing import TYPE_CHECKING 14 | 15 | from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk 16 | from wn import Error 17 | from wn.util import ProgressHandler 18 | 19 | from wordbook import base, utils 20 | from wordbook.settings import Settings 21 | from wordbook.settings_window import SettingsDialog 22 | 23 | if TYPE_CHECKING: 24 | from typing import Any, Literal 25 | 26 | 27 | @Gtk.Template(resource_path=f"{utils.RES_PATH}/ui/window.ui") 28 | class WordbookWindow(Adw.ApplicationWindow): 29 | __gtype_name__ = "WordbookWindow" 30 | 31 | search_button: Gtk.Button = Gtk.Template.Child("search_button") # type: ignore 32 | download_status_page: Adw.StatusPage = Gtk.Template.Child("download_status_page") # type: ignore 33 | loading_progress: Gtk.ProgressBar = Gtk.Template.Child("loading_progress") # type: ignore 34 | 35 | _key_ctrlr: Gtk.EventControllerKey = Gtk.Template.Child("key_ctrlr") # type: ignore 36 | _title_clamp: Adw.Clamp = Gtk.Template.Child("title_clamp") # type: ignore 37 | _split_view_toggle_button: Gtk.ToggleButton = Gtk.Template.Child("split_view_toggle_button") # type: ignore 38 | _search_entry: Gtk.Entry = Gtk.Template.Child("search_entry") # type: ignore 39 | _speak_button: Gtk.Button = Gtk.Template.Child("speak_button") # type: ignore 40 | _menu_button: Gtk.MenuButton = Gtk.Template.Child("wordbook_menu_button") # type: ignore 41 | _main_split_view: Adw.OverlaySplitView = Gtk.Template.Child("main_split_view") # type: ignore 42 | _history_listbox: Gtk.ListBox = Gtk.Template.Child("history_listbox") # type: ignore 43 | _main_stack: Adw.ViewStack = Gtk.Template.Child("main_stack") # type: ignore 44 | _main_scroll: Gtk.ScrolledWindow = Gtk.Template.Child("main_scroll") # type: ignore 45 | _def_view: Gtk.Label = Gtk.Template.Child("def_view") # type: ignore 46 | _def_ctrlr: Gtk.GestureClick = Gtk.Template.Child("def_ctrlr") # type: ignore 47 | _pronunciation_view: Gtk.Label = Gtk.Template.Child("pronunciation_view") # type: ignore 48 | _term_view: Gtk.Label = Gtk.Template.Child("term_view") # type: ignore 49 | _network_fail_status_page: Adw.StatusPage = Gtk.Template.Child("network_fail_status_page") # type: ignore 50 | _retry_button: Gtk.Button = Gtk.Template.Child("retry_button") # type: ignore 51 | _exit_button: Gtk.Button = Gtk.Template.Child("exit_button") # type: ignore 52 | _clear_history_button: Gtk.Button = Gtk.Template.Child("clear_history_button") # type: ignore 53 | 54 | _style_manager: Adw.StyleManager | None = None 55 | 56 | _wn_downloader: base.WordnetDownloader = base.WordnetDownloader() 57 | _wn_future = None 58 | 59 | _doubled: bool = False 60 | _completion_request_count: int = 0 61 | _searched_term: str | None = None 62 | _search_history: Gio.ListStore | None = None 63 | _search_history_list: list[str] = [] 64 | _search_queue: list[str] = [] 65 | _last_search_fail: bool = False 66 | _active_thread: threading.Thread | None = None 67 | _primary_clipboard_text: str | None = None 68 | 69 | # Initialize history delay timer for live search 70 | _history_delay_timer = None 71 | _pending_history_text = None 72 | 73 | # Auto-paste queuing 74 | _auto_paste_queued: bool = False 75 | 76 | def __init__(self, term="", auto_paste_requested=False, **kwargs): 77 | """Initialize the window.""" 78 | super().__init__(**kwargs) 79 | 80 | self.lookup_term = term 81 | self.auto_paste_requested = auto_paste_requested 82 | 83 | if Gio.Application.get_default().development_mode is True: 84 | self.get_style_context().add_class("devel") 85 | self.set_default_icon_name(Gio.Application.get_default().app_id) 86 | 87 | self.setup_widgets() 88 | self.setup_actions() 89 | 90 | def setup_widgets(self): 91 | """Setup the widgets in the window.""" 92 | self._search_history = Gio.ListStore.new(HistoryObject) 93 | self._history_listbox.bind_model(self._search_history, self._create_label) 94 | 95 | self.connect("notify::is-active", self._on_is_active_changed) 96 | self.connect("unrealize", self._on_destroy) 97 | self._key_ctrlr.connect("key-pressed", self._on_key_pressed) 98 | self._history_listbox.connect("row-activated", self._on_history_item_activated) 99 | 100 | self._def_ctrlr.connect("pressed", self._on_def_press_event) 101 | self._def_ctrlr.connect("stopped", self._on_def_stop_event) 102 | self._def_view.connect("activate-link", self._on_link_activated) 103 | 104 | self.search_button.connect("clicked", self.on_search_clicked) 105 | self._search_entry.connect("changed", self._on_entry_changed) 106 | self._speak_button.connect("clicked", self._on_speak_clicked) 107 | self._retry_button.connect("clicked", self._on_retry_clicked) 108 | self._exit_button.connect("clicked", self._on_exit_clicked) 109 | self._clear_history_button.connect("clicked", self._on_clear_history) 110 | 111 | self._main_split_view.bind_property( 112 | "show-sidebar", 113 | self._split_view_toggle_button, 114 | "active", 115 | GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE, 116 | ) 117 | 118 | self._style_manager = self.get_application().get_style_manager() 119 | self._style_manager.connect("notify::dark", self._on_dark_style) 120 | 121 | # Loading and setup. 122 | self._dl_wn() 123 | if self._wn_downloader.check_status(): 124 | self._wn_future = base.get_wn_file(self._retry_dl_wn) 125 | self._set_header_sensitive(True) 126 | self._page_switch(Page.WELCOME) 127 | if self.lookup_term: 128 | self.trigger_search(self.lookup_term) 129 | elif Settings.get().auto_paste_on_launch or self.auto_paste_requested: 130 | self.queue_auto_paste() 131 | self._search_entry.grab_focus_without_selecting() 132 | 133 | # Completions 134 | # FIXME: Remove use of EntryCompletion 135 | self.completer = Gtk.EntryCompletion() 136 | self.completer.set_popup_single_match(False) 137 | self.completer.set_text_column(0) 138 | self.completer.set_popup_completion(not Settings.get().live_search) 139 | self.completer.set_popup_set_width(True) 140 | self._search_entry.set_completion(self.completer) 141 | 142 | # Load History. 143 | self._search_history_list = Settings.get().history 144 | for text in self._search_history_list: 145 | history_object = HistoryObject(text) 146 | self._search_history.insert(0, history_object) 147 | 148 | # Update clear button sensitivity 149 | self._update_clear_button_sensitivity() 150 | 151 | # Set search button visibility. 152 | self.search_button.set_visible(not Settings.get().live_search) 153 | if not Settings.get().live_search: 154 | self.set_default_widget(self.search_button) 155 | 156 | def_extra_menu_model = Gio.Menu.new() 157 | item = Gio.MenuItem.new("Search Selected Text", "win.search-selected") 158 | def_extra_menu_model.append_item(item) 159 | 160 | # Set the extra menu model for the label 161 | self._def_view.set_extra_menu(def_extra_menu_model) 162 | 163 | def setup_actions(self): 164 | """Setup the Gio actions for the application window.""" 165 | paste_search_action: Gio.SimpleAction = Gio.SimpleAction.new("paste-search", None) 166 | paste_search_action.connect("activate", self.on_paste_search) 167 | self.add_action(paste_search_action) 168 | 169 | preferences_action: Gio.SimpleAction = Gio.SimpleAction.new("preferences", None) 170 | preferences_action.connect("activate", self.on_preferences) 171 | self.add_action(preferences_action) 172 | 173 | random_word_action: Gio.SimpleAction = Gio.SimpleAction.new("random-word", None) 174 | random_word_action.connect("activate", self.on_random_word) 175 | self.add_action(random_word_action) 176 | 177 | search_selected_action: Gio.SimpleAction = Gio.SimpleAction.new("search-selected", None) 178 | search_selected_action.connect("activate", self.on_search_selected) 179 | search_selected_action.set_enabled(False) 180 | self.add_action(search_selected_action) 181 | 182 | clipboard: Gdk.Clipboard = self.get_primary_clipboard() 183 | clipboard.connect("changed", self.on_clipboard_changed) 184 | 185 | def _get_theme_colors(self) -> tuple[str, str]: 186 | """Get word and sentence colors based on current theme.""" 187 | if self._style_manager.get_dark(): 188 | return base.DARK_MODE_WORD_COLOR, base.DARK_MODE_SENTENCE_COLOR 189 | else: 190 | return base.LIGHT_MODE_WORD_COLOR, base.LIGHT_MODE_SENTENCE_COLOR 191 | 192 | def on_clipboard_changed(self, clipboard: Gdk.Clipboard | None): 193 | clipboard = Gdk.Display.get_default().get_primary_clipboard() 194 | 195 | def on_selection(_clipboard, result): 196 | """Callback for the text selection.""" 197 | try: 198 | text = clipboard.read_text_finish(result) 199 | if text is not None and not text.strip() == "" and not text.isspace(): 200 | text = text.replace(" ", "").replace("\n", "") 201 | self._primary_clipboard_text = text 202 | self.lookup_action("search-selected").props.enabled = True 203 | else: 204 | self._primary_clipboard_text = None 205 | self.lookup_action("search-selected").props.enabled = False 206 | except GLib.GError: 207 | # Usually happens when clipboard is empty or unsupported data type 208 | self._primary_clipboard_text = None 209 | self.lookup_action("search-selected").props.enabled = False 210 | 211 | cancellable = Gio.Cancellable() 212 | clipboard.read_text_async(cancellable, on_selection) 213 | 214 | def on_paste_search(self, _action=None, _param=None): 215 | """Search text in clipboard.""" 216 | clipboard = Gdk.Display.get_default().get_clipboard() 217 | 218 | def on_paste(_clipboard: Gdk.Clipboard, result: Gio.AsyncResult): 219 | """Callback for the clipboard paste.""" 220 | try: 221 | text = clipboard.read_text_finish(result) 222 | if text is not None: 223 | text = base.clean_search_terms(text) 224 | if text and text.strip() and not text.isspace(): 225 | self.trigger_search(text) 226 | except GLib.GError: 227 | # Usually happens when clipboard is empty or unsupported data type 228 | pass 229 | 230 | cancellable = Gio.Cancellable() 231 | clipboard.read_text_async(cancellable, on_paste) 232 | 233 | def on_preferences(self, _action, _param): 234 | """Show settings window.""" 235 | window = SettingsDialog(self) 236 | window.present(self) 237 | 238 | def on_random_word(self, _action, _param): 239 | """Search a random word from the wordlist.""" 240 | random_word = random.choice(self._wn_future.result()["list"]) 241 | random_word = random_word.replace("_", " ") 242 | self.trigger_search(random_word) 243 | 244 | def on_search_selected(self, _action, _param): 245 | """Search selected text from inside or outside the window.""" 246 | self.trigger_search(self._primary_clipboard_text) 247 | 248 | def on_search_clicked(self, _button=None, pass_check=False, text=None): 249 | """Pass data to search function and set TextView data.""" 250 | if text is None: 251 | text = self._search_entry.get_text().strip() 252 | self._page_switch(Page.SPINNER) 253 | self._add_to_queue(text, pass_check) 254 | 255 | def threaded_search(self, pass_check=False): 256 | """Manage a single thread to search for each term.""" 257 | except_list = ("fortune -a", "cowfortune") 258 | status = SearchStatus.NONE 259 | while self._search_queue: 260 | text = self._search_queue.pop(0) 261 | orig_term = self._searched_term 262 | self._searched_term = text 263 | if text and (pass_check or not text == orig_term or text in except_list): 264 | if not text.strip() == "": 265 | GLib.idle_add(self._def_view.set_markup, "") 266 | 267 | out = self._search(text) 268 | 269 | if out is None: 270 | status = SearchStatus.RESET 271 | continue 272 | 273 | def validate_result(text, out_string) -> Literal[SearchStatus.SUCCESS]: 274 | # Add to history (with delay for live search) 275 | if Settings.get().live_search: 276 | self._add_to_history_delayed(text) 277 | else: 278 | self._add_to_history(text) 279 | 280 | GLib.idle_add(self._def_view.set_markup, out_string) 281 | return SearchStatus.SUCCESS 282 | 283 | if out["out_string"] is not None: 284 | status = validate_result(text, out["out_string"]) 285 | elif out["result"] is not None: 286 | status = validate_result(text, self._process_result(out["result"])) 287 | else: 288 | status = SearchStatus.FAILURE 289 | self._last_search_fail = True 290 | continue 291 | 292 | term_view_text = f'{out["term"].strip()}' 293 | GLib.idle_add( 294 | self._term_view.set_markup, 295 | term_view_text, 296 | ) 297 | GLib.idle_add( 298 | self._term_view.set_tooltip_markup, 299 | term_view_text, 300 | ) 301 | 302 | pron = "" + out["pronunciation"].strip().replace("\n", "") + "" 303 | GLib.idle_add( 304 | self._pronunciation_view.set_markup, 305 | pron, 306 | ) 307 | GLib.idle_add( 308 | self._pronunciation_view.set_tooltip_markup, 309 | pron, 310 | ) 311 | 312 | if text not in except_list: 313 | GLib.idle_add(self._speak_button.set_visible, True) 314 | 315 | self._last_search_fail = False 316 | continue 317 | 318 | status = SearchStatus.RESET 319 | continue 320 | 321 | if text and text == orig_term and not self._last_search_fail: 322 | status = SearchStatus.SUCCESS 323 | continue 324 | 325 | if text and text == orig_term and self._last_search_fail: 326 | status = SearchStatus.FAILURE 327 | continue 328 | 329 | status = SearchStatus.RESET 330 | 331 | if status == SearchStatus.SUCCESS: 332 | self._page_switch(Page.CONTENT) 333 | elif status == SearchStatus.FAILURE: 334 | self._page_switch(Page.SEARCH_FAIL) 335 | elif status == SearchStatus.RESET: 336 | self._page_switch(Page.WELCOME) 337 | 338 | self._active_thread = None 339 | 340 | def trigger_search(self, text): 341 | """Trigger search action.""" 342 | GLib.idle_add(self._search_entry.set_text, text) 343 | GLib.idle_add(self.on_search_clicked, None, False, text) 344 | 345 | def _on_dark_style(self, _object, _param): 346 | """Refresh definition view when switching dark theme.""" 347 | if self._searched_term is not None: 348 | self.on_search_clicked(pass_check=True, text=self._searched_term) 349 | 350 | def _on_def_press_event(self, _click, n_press, _x, _y): 351 | """Handle double click on definition view.""" 352 | if Settings.get().double_click: 353 | self._doubled = n_press == 2 354 | else: 355 | self._doubled = False 356 | 357 | def _on_def_stop_event(self, _click): 358 | """Search on double click.""" 359 | if self._doubled: 360 | clipboard = Gdk.Display.get_default().get_primary_clipboard() 361 | 362 | def on_paste(_clipboard, result): 363 | text = clipboard.read_text_finish(result) 364 | text = base.clean_search_terms(text) 365 | if text is not None and not text.strip() == "" and not text.isspace(): 366 | self.trigger_search(text) 367 | 368 | cancellable = Gio.Cancellable() 369 | clipboard.read_text_async(cancellable, on_paste) 370 | 371 | def queue_auto_paste(self): 372 | """Queue auto-paste or execute it immediately if window is active.""" 373 | if self.props.is_active: 374 | GLib.idle_add(self.on_paste_search) 375 | else: 376 | self._auto_paste_queued = True 377 | 378 | def _on_is_active_changed(self, *_args): 379 | """Handle window becoming active and execute queued auto-paste if needed.""" 380 | if self._auto_paste_queued and self.props.is_active: 381 | self._auto_paste_queued = False 382 | GLib.idle_add(self.on_paste_search) 383 | 384 | def _on_destroy(self, _window: Gtk.Window): 385 | """Detect closing of the window.""" 386 | # Cancel any pending history delay timer 387 | if self._history_delay_timer is not None: 388 | GLib.source_remove(self._history_delay_timer) 389 | self._history_delay_timer = None 390 | 391 | Settings.get().history = self._search_history_list[-10:] 392 | 393 | def _on_entry_changed(self, _entry): 394 | """Detect changes to text and do live search if enabled.""" 395 | 396 | self._completion_request_count += 1 397 | if self._completion_request_count == 1: 398 | threading.Thread( 399 | target=self._update_completions, 400 | args=[self._search_entry.get_text()], 401 | daemon=True, 402 | ).start() 403 | 404 | if Settings.get().live_search: 405 | GLib.idle_add(self.on_search_clicked) 406 | 407 | def _on_clear_history(self, _widget): 408 | """Clear the search history.""" 409 | self._search_history.remove_all() 410 | self._search_history_list = [] 411 | Settings.get().history = [] 412 | self._update_clear_button_sensitivity() 413 | 414 | def _update_clear_button_sensitivity(self): 415 | """Update the sensitivity of the clear history button.""" 416 | has_history = len(self._search_history_list) > 0 417 | self._clear_history_button.set_sensitive(has_history) 418 | 419 | @staticmethod 420 | def _on_exit_clicked(_widget): 421 | """Handle exit button click in network failure page.""" 422 | sys.exit() 423 | 424 | def _on_link_activated(self, _widget, data): 425 | """Search for terms that are marked as hyperlinks.""" 426 | if data.startswith("search;"): 427 | GLib.idle_add(self._search_entry.set_text, data[7:]) 428 | self.on_search_clicked(text=data[7:]) 429 | return Gdk.EVENT_STOP 430 | 431 | def _on_key_pressed(self, _button, keyval, _keycode, state): 432 | """Handle key press events for quick search.""" 433 | modifiers = state & Gtk.accelerator_get_default_mod_mask() 434 | shift_mask = Gdk.ModifierType.SHIFT_MASK 435 | unicode_key_val = Gdk.keyval_to_unicode(keyval) 436 | if ( 437 | GLib.unichar_isgraph(chr(unicode_key_val)) 438 | and modifiers in (shift_mask, 0) 439 | and not self._search_entry.is_focus() 440 | ): 441 | self._search_entry.grab_focus_without_selecting() 442 | text = self._search_entry.get_text() + chr(unicode_key_val) 443 | self._search_entry.set_text(text) 444 | self._search_entry.set_position(len(text)) 445 | return Gdk.EVENT_STOP 446 | return Gdk.EVENT_PROPAGATE 447 | 448 | def _on_history_item_activated(self, _widget, row): 449 | """Handle history item clicks.""" 450 | term = row.get_first_child().get_first_child().get_label() 451 | self.trigger_search(term) 452 | 453 | def _on_retry_clicked(self, _widget): 454 | """Handle retry button click in network failure page.""" 455 | self._page_switch(Page.DOWNLOAD) 456 | self._dl_wn() 457 | 458 | def _add_to_queue(self, text: str, pass_check: bool = False): 459 | """Add search term to queue.""" 460 | if self._search_queue: 461 | self._search_queue.pop(0) 462 | self._search_queue.append(text) 463 | 464 | if self._active_thread is None: 465 | # If there is no active thread, create one and start it. 466 | self._active_thread = threading.Thread(target=self.threaded_search, args=[pass_check], daemon=True) 467 | self._active_thread.start() 468 | 469 | def _add_to_history(self, text): 470 | """Add text to history immediately.""" 471 | history_object = HistoryObject(text) 472 | if text not in self._search_history_list: 473 | self._search_history_list.append(text) 474 | self._search_history.insert(0, history_object) 475 | self._update_clear_button_sensitivity() 476 | 477 | def _add_to_history_delayed(self, text): 478 | """Add text to history after a 1-second delay, cancelling any previous delay.""" 479 | # Cancel any existing timer 480 | if self._history_delay_timer is not None: 481 | GLib.source_remove(self._history_delay_timer) 482 | self._history_delay_timer = None 483 | 484 | # Store the text to be added 485 | self._pending_history_text = text 486 | 487 | # Set up a new timer to add to history after 1 second (1000ms) 488 | self._history_delay_timer = GLib.timeout_add(1000, self._execute_delayed_history_add) 489 | 490 | def _execute_delayed_history_add(self): 491 | """Execute the delayed history addition.""" 492 | if self._pending_history_text is not None: 493 | self._add_to_history(self._pending_history_text) 494 | self._pending_history_text = None 495 | 496 | self._history_delay_timer = None 497 | return False # Don't repeat the timer 498 | 499 | def _on_speak_clicked(self, _button): 500 | """Say the search entry out loud with espeak speech synthesis.""" 501 | base.read_term( 502 | self._searched_term, 503 | speed="120", 504 | accent=Settings.get().pronunciations_accent.code, 505 | ) 506 | 507 | def progress_complete(self): 508 | """Run upon completion of loading.""" 509 | GLib.idle_add(self.download_status_page.set_title, _("Ready.")) 510 | self._wn_future = base.get_wn_file(self._retry_dl_wn) 511 | GLib.idle_add(self._set_header_sensitive, True) 512 | self._page_switch(Page.WELCOME) 513 | if self.lookup_term: 514 | self.trigger_search(self.lookup_term) 515 | self._search_entry.grab_focus_without_selecting() 516 | 517 | @staticmethod 518 | def _create_label(element): 519 | """Create labels for history list.""" 520 | box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, visible=True) 521 | label = Gtk.Label( 522 | label=element.term, 523 | margin_top=8, 524 | margin_bottom=8, 525 | margin_start=8, 526 | margin_end=8, 527 | ) 528 | box.append(label) 529 | return box 530 | 531 | def _new_error(self, primary_text, secondary_text) -> None: 532 | """Show an error dialog.""" 533 | dialog = Adw.AlertDialog.new(primary_text, secondary_text) 534 | dialog.add_response("dismiss", _("Dismiss")) 535 | dialog.choose(self) 536 | 537 | def _page_switch(self, page: str) -> bool: 538 | """Switch main stack pages.""" 539 | if page == "content_page": 540 | GLib.idle_add(self._main_scroll.get_vadjustment().set_value, 0) 541 | if self._main_stack.get_visible_child_name() == page: 542 | return True 543 | GLib.idle_add(self._main_stack.set_visible_child_name, page) 544 | return False 545 | 546 | def _process_result(self, result: dict[str, Any]): 547 | """Process results from wn.""" 548 | out_string = "" 549 | word_color, sentence_color = self._get_theme_colors() 550 | first = True 551 | for pos in result.keys(): 552 | i = 0 553 | orig_synset = None 554 | if not result[pos]: 555 | continue 556 | for synset in sorted(result[pos], key=lambda k: k["name"]): 557 | synset_name = synset["name"] 558 | if orig_synset is None: 559 | i = 1 560 | if not first: 561 | out_string += "\n\n" 562 | out_string += f"{synset_name} ~ {pos}" 563 | orig_synset = synset_name 564 | first = False 565 | elif synset_name != orig_synset: 566 | i = 1 567 | out_string += f"\n\n{synset_name} ~ {pos}" 568 | orig_synset = synset_name 569 | else: 570 | i += 1 571 | out_string += f"\n {i}: {synset['definition']}" 572 | 573 | for example in synset["examples"]: 574 | out_string += f'\n {example}' 575 | 576 | pretty_syn = self._process_word_links(synset["syn"], word_color) 577 | if pretty_syn: 578 | out_string += f"\n Synonyms: {pretty_syn}" 579 | 580 | pretty_ant = self._process_word_links(synset["ant"], word_color) 581 | if pretty_ant: 582 | out_string += f"\n Antonyms: {pretty_ant}" 583 | 584 | pretty_sims = self._process_word_links(synset["sim"], word_color) 585 | if pretty_sims: 586 | out_string += f"\n Similar to: {pretty_sims}" 587 | 588 | pretty_alsos = self._process_word_links(synset["also_sees"], word_color) 589 | if pretty_alsos: 590 | out_string += f"\n Also see: {pretty_alsos}" 591 | return out_string 592 | 593 | @staticmethod 594 | def _process_word_links(word_list, word_color): 595 | """Process word links like synonyms, antonyms, etc.""" 596 | pretty_list = [] 597 | for word in word_list: 598 | pretty_list.append(f'{word}') 599 | if pretty_list: 600 | pretty_list = ", ".join(pretty_list) 601 | return pretty_list 602 | return "" 603 | 604 | def _search(self, search_text: str) -> dict[str, Any] | None: 605 | """Clean input text, give errors and pass data to formatter.""" 606 | text = base.clean_search_terms(search_text) 607 | if not text == "" and not text.isspace(): 608 | return base.format_output( 609 | text, 610 | self._wn_future.result()["instance"], 611 | Settings.get().cdef, 612 | accent=Settings.get().pronunciations_accent.code, 613 | ) 614 | if not Settings.get().live_search: 615 | GLib.idle_add( 616 | self._new_error, 617 | _("Invalid input"), 618 | _("Nothing definable was found in your search input"), 619 | ) 620 | self._searched_term = None 621 | return None 622 | 623 | def _update_completions(self, text): 624 | """Update completions from wordlist and cdef folder.""" 625 | while self._completion_request_count > 0: 626 | completer_liststore = Gtk.ListStore(str) 627 | _complete_list = [] 628 | 629 | for item in self._wn_future.result()["list"]: 630 | if len(_complete_list) >= 10: 631 | break 632 | item = item.replace("_", " ") 633 | if item.lower().startswith(text.lower()) and item not in _complete_list: 634 | _complete_list.append(item) 635 | 636 | if Settings.get().cdef: 637 | for item in os.listdir(utils.CDEF_DIR): 638 | # FIXME: There is no indicator that this is a custom definition 639 | # Not a priority but a nice-to-have. 640 | if len(_complete_list) >= 10: 641 | break 642 | item = escape(item).replace("_", " ") 643 | if item.lower().startswith(text.lower()) and item not in _complete_list: 644 | _complete_list.append(item) 645 | 646 | _complete_list = sorted(_complete_list, key=str.casefold) 647 | for item in _complete_list: 648 | completer_liststore.append((item,)) 649 | 650 | self._completion_request_count -= 1 651 | GLib.idle_add(self.completer.set_model, completer_liststore) 652 | GLib.idle_add(self.completer.complete) 653 | 654 | def _set_header_sensitive(self, status): 655 | """Disable/enable header buttons.""" 656 | self._title_clamp.set_sensitive(status) 657 | self._split_view_toggle_button.set_sensitive(status) 658 | self._menu_button.set_sensitive(status) 659 | 660 | def _dl_wn(self): 661 | """Download WordNet data.""" 662 | self._set_header_sensitive(False) 663 | if not self._wn_downloader.check_status(): 664 | self.download_status_page.set_description(_("Downloading WordNet…")) 665 | threading.Thread(target=self._try_dl_wn).start() 666 | 667 | def _try_dl_wn(self): 668 | """Attempt to download WordNet data.""" 669 | try: 670 | self._wn_downloader.download(ProgressUpdater) 671 | except Error as err: 672 | self._network_fail_status_page.set_description(f"Error: {err}") 673 | utils.log_warning(err) 674 | self._page_switch(Page.NETWORK_FAIL) 675 | 676 | def _retry_dl_wn(self): 677 | """Re-download WordNet data in event of failure.""" 678 | self._page_switch(Page.DOWNLOAD) 679 | self._wn_downloader.delete_db() 680 | self._dl_wn() 681 | self.download_status_page.set_description( 682 | _("Re-downloading WordNet database") 683 | + "\n" 684 | + _("Just a database upgrade.") 685 | + "\n" 686 | + _("This shouldn't happen too often.") 687 | ) 688 | 689 | 690 | class SearchStatus(Enum): 691 | NONE = auto() 692 | SUCCESS = auto() 693 | FAILURE = auto() 694 | RESET = auto() 695 | 696 | 697 | class Page(str, Enum): 698 | CONTENT = "content_page" 699 | DOWNLOAD = "download_page" 700 | NETWORK_FAIL = "network_fail_page" 701 | SEARCH_FAIL = "search_fail_page" 702 | SPINNER = "spinner_page" 703 | WELCOME = "welcome_page" 704 | 705 | 706 | class HistoryObject(GObject.Object): 707 | term = "" 708 | 709 | def __init__(self, term): 710 | super().__init__() 711 | self.term = term 712 | 713 | 714 | class ProgressUpdater(ProgressHandler): 715 | def update(self, n: int = 1, force: bool = False): 716 | """Update the progress bar.""" 717 | self.kwargs["count"] += n 718 | if self.kwargs["total"] > 0: 719 | progress_fraction = self.kwargs["count"] / self.kwargs["total"] 720 | GLib.idle_add( 721 | Gio.Application.get_default().win.loading_progress.set_fraction, 722 | progress_fraction, 723 | ) 724 | 725 | @staticmethod 726 | def flash(message): 727 | """Update the progress label.""" 728 | if message == "Database": 729 | GLib.idle_add( 730 | Gio.Application.get_default().win.download_status_page.set_description, 731 | _("Building Database…"), 732 | ) 733 | else: 734 | GLib.idle_add( 735 | Gio.Application.get_default().win.download_status_page.set_description, 736 | message, 737 | ) 738 | 739 | def close(self): 740 | """Signal the completion of building the WordNet database.""" 741 | if self.kwargs["message"] not in ("Download", "Read"): 742 | Gio.Application.get_default().win.progress_complete() 743 | -------------------------------------------------------------------------------- /wordbook/wordbook.in: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | # -*- coding: utf-8 -*- 4 | # SPDX-FileCopyrightText: 2016-2025 Mufeed Ali 5 | # SPDX-License-Identifier: GPL-3.0-or-later 6 | 7 | import gettext 8 | import locale 9 | import os 10 | import sys 11 | import signal 12 | 13 | APP_ID = @APP_ID@ 14 | VERSION = @VERSION@ 15 | pkgdatadir = @pkgdatadir@ 16 | localedir = @localedir@ 17 | 18 | builddir = os.environ.get("MESON_BUILD_ROOT") 19 | if builddir: 20 | sys.dont_write_bytecode = True 21 | sys.path.insert(1, os.environ["MESON_SOURCE_ROOT"]) 22 | data_dir = os.path.join(builddir, "@prefix@", "@datadir@") 23 | os.putenv( 24 | "XDG_DATA_DIRS", 25 | "%s:%s" 26 | % (data_dir, os.getenv("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")), 27 | ) 28 | 29 | sys.path.insert(1, pkgdatadir) 30 | signal.signal(signal.SIGINT, signal.SIG_DFL) 31 | 32 | locale.bindtextdomain("wordbook", localedir) 33 | locale.textdomain("wordbook") 34 | gettext.bindtextdomain("wordbook", localedir) 35 | gettext.textdomain("wordbook") 36 | 37 | if __name__ == "__main__": 38 | from gi.repository import Gio 39 | 40 | resource = Gio.Resource.load(os.path.join(pkgdatadir, "resources.gresource")) 41 | resource._register() 42 | 43 | from wordbook.main import Application 44 | 45 | Application.development_mode = @PROFILE@ == "Devel" 46 | app = Application(APP_ID, VERSION) 47 | 48 | try: 49 | status = app.run(sys.argv) 50 | except SystemExit as e: 51 | status = e.code 52 | 53 | sys.exit(status) 54 | --------------------------------------------------------------------------------