├── .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 |
3 | Wordbook
4 |
5 |
6 | Look up definitions of any English term
7 |
8 |
9 |
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 |
28 |
29 |
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 |
45 |
46 | ### Using Nix
47 |
48 | [](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 |
14 | Fully offline after initial data download
15 | Pronunciations
16 | Random Word
17 | Live Search
18 | Double click to search
19 | Dark mode
20 |
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 |
--------------------------------------------------------------------------------