├── .github
└── workflows
│ ├── coverage.yml
│ └── python.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── keyhint
├── __init__.py
├── __main__.py
├── app.py
├── binding.py
├── config.py
├── config
│ ├── alacritty.toml
│ ├── cli.toml
│ ├── firefox-tridactyl.toml
│ ├── firefox.toml
│ ├── foot.toml
│ ├── forge.toml
│ ├── github.toml
│ ├── gnome-terminal.toml
│ ├── keyhint.toml
│ ├── kitty.toml
│ ├── pop-shell.toml
│ ├── tilix.toml
│ ├── tmux.toml
│ ├── vim.toml
│ ├── vscode-copilot.toml
│ ├── vscode-neovim.toml
│ └── vscode.toml
├── context.py
├── css.py
├── headerbar.py
├── resources
│ ├── headerbar.ui
│ ├── keyhint.png
│ ├── keyhint_128.png
│ ├── keyhint_32.png
│ ├── keyhint_48.png
│ ├── keyhint_64.png
│ ├── keyhint_icon.svg
│ ├── style.css
│ └── window.ui
├── sheets.py
└── window.py
├── pyproject.toml
├── tests
├── __init__.py
└── test_sheets.py
└── uv.lock
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
1 | name: "coverage.io"
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | build:
11 | name: Coverage
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Install uv
17 | uses: astral-sh/setup-uv@v5
18 | with:
19 | enable-cache: true
20 |
21 | - uses: actions/setup-python@v5
22 | with:
23 | python-version-file: "pyproject.toml"
24 |
25 | - name: Install system deps
26 | run: |
27 | sudo apt-get update
28 | sudo apt-get install \
29 | girepository-2.0 \
30 | libcairo2-dev \
31 | python3-gi \
32 | gobject-introspection \
33 | libgtk-3-dev
34 |
35 | - name: Install the project
36 | run: uv sync --all-extras --dev
37 |
38 | - name: Run pytest
39 | run: uv run pytest
40 |
41 | - name: Coveralls
42 | run: uv run coveralls
43 | env:
44 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
45 |
--------------------------------------------------------------------------------
/.github/workflows/python.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on: [push, pull_request]
3 |
4 | concurrency:
5 | group: cicd-${{ github.ref }}
6 | cancel-in-progress: true
7 |
8 | jobs:
9 | test:
10 | name: Test on Linux64
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Install uv
16 | uses: astral-sh/setup-uv@v5
17 | with:
18 | enable-cache: true
19 |
20 | - uses: actions/setup-python@v5
21 | with:
22 | python-version-file: "pyproject.toml"
23 |
24 | - name: Install system deps
25 | run: |
26 | sudo apt-get update
27 | sudo apt-get install \
28 | girepository-2.0-dev \
29 | libcairo2-dev \
30 | python3-gi \
31 | gobject-introspection \
32 | libgtk-4-dev
33 |
34 | - name: Install the project
35 | run: uv sync --all-extras --dev
36 |
37 | - name: Run project checks
38 | run: uv run pre-commit run --all-files
39 |
40 | publish:
41 | name: Build & Publish
42 | needs: test
43 | if: startsWith(github.ref, 'refs/tags/v')
44 | runs-on: ubuntu-latest
45 | permissions:
46 | # Used to authenticate to PyPI via OIDC.
47 | # Used to sign the release's artifacts with sigstore-python.
48 | id-token: write
49 | # Used to attach signing artifacts to the published release.
50 | contents: write
51 |
52 | steps:
53 | - uses: actions/checkout@v4
54 |
55 | - name: Install uv
56 | uses: astral-sh/setup-uv@v5
57 | with:
58 | enable-cache: true
59 |
60 | - uses: actions/setup-python@v5
61 | with:
62 | python-version-file: "pyproject.toml"
63 |
64 | - name: Install system deps
65 | run: |
66 | sudo apt-get update
67 | sudo apt-get install \
68 | libgirepository1.0-dev \
69 | libcairo2-dev \
70 | python3-gi \
71 | gobject-introspection \
72 | libgtk-4-dev
73 |
74 | - name: Build Python package
75 | run: uv build
76 |
77 | - name: Publish to PyPi
78 | run: uv publish
79 |
80 | - uses: ncipollo/release-action@v1
81 | with:
82 | body: See [CHANGELOG.md](https://github.com/dynobo/keyhint/blob/main/CHANGELOG.md) for details.
83 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | cheatsheet.jpeg
2 | .virtualenvs
3 | .vscode
4 | run.sh
5 | *.*~
6 | .envrc
7 | .ruff_cache
8 | coverage.lcov
9 |
10 |
11 | # Byte-compiled / optimized / DLL files
12 | __pycache__/
13 | *.py[cod]
14 | *$py.class
15 |
16 | # OSX useful to ignore
17 | *.DS_Store
18 | .AppleDouble
19 | .LSOverride
20 |
21 | # Thumbnails
22 | ._*
23 |
24 | # Files that might appear in the root of a volume
25 | .DocumentRevisions-V100
26 | .fseventsd
27 | .Spotlight-V100
28 | .TemporaryItems
29 | .Trashes
30 | .VolumeIcon.icns
31 | .com.apple.timemachine.donotpresent
32 |
33 | # Directories potentially created on remote AFP share
34 | .AppleDB
35 | .AppleDesktop
36 | Network Trash Folder
37 | Temporary Items
38 | .apdisk
39 |
40 | # C extensions
41 | *.so
42 |
43 | # Distribution / packaging
44 | .Python
45 | env/
46 | build/
47 | develop-eggs/
48 | dist/
49 | downloads/
50 | eggs/
51 | .eggs/
52 | lib/
53 | lib64/
54 | parts/
55 | sdist/
56 | var/
57 | *.egg-info/
58 | .installed.cfg
59 | *.egg
60 |
61 | # IntelliJ Idea family of suites
62 | .idea
63 | *.iml
64 | ## File-based project format:
65 | *.ipr
66 | *.iws
67 | ## mpeltonen/sbt-idea plugin
68 | .idea_modules/
69 |
70 | # Briefcase build directories
71 | iOS/
72 | macOS/
73 | windows/
74 | android/
75 | linux/
76 | django/
77 |
78 | # Byte-compiled / optimized / DLL files
79 | __pycache__/
80 | *.py[cod]
81 | *$py.class
82 |
83 | # C extensions
84 | *.so
85 |
86 | # Distribution / packaging
87 | .Python
88 | build/
89 | develop-eggs/
90 | dist/
91 | downloads/
92 | eggs/
93 | .eggs/
94 | lib/
95 | lib64/
96 | parts/
97 | sdist/
98 | var/
99 | wheels/
100 | pip-wheel-metadata/
101 | share/python-wheels/
102 | *.egg-info/
103 | .installed.cfg
104 | *.egg
105 | MANIFEST
106 |
107 | # PyInstaller
108 | # Usually these files are written by a python script from a template
109 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
110 | *.manifest
111 | *.spec
112 |
113 | # Installer logs
114 | pip-log.txt
115 | pip-delete-this-directory.txt
116 |
117 | # Unit test / coverage reports
118 | htmlcov/
119 | .tox/
120 | .nox/
121 | .coverage
122 | .coverage.*
123 | .cache
124 | nosetests.xml
125 | coverage.xml
126 | *.cover
127 | *.py,cover
128 | .hypothesis/
129 | .pytest_cache/
130 |
131 | # Translations
132 | *.mo
133 | *.pot
134 |
135 | # Django stuff:
136 | *.log
137 | local_settings.py
138 | db.sqlite3
139 | db.sqlite3-journal
140 |
141 | # Flask stuff:
142 | instance/
143 | .webassets-cache
144 |
145 | # Scrapy stuff:
146 | .scrapy
147 |
148 | # Sphinx documentation
149 | docs/_build/
150 |
151 | # PyBuilder
152 | target/
153 |
154 | # Jupyter Notebook
155 | .ipynb_checkpoints
156 |
157 | # IPython
158 | profile_default/
159 | ipython_config.py
160 |
161 | # pyenv
162 | .python-version
163 |
164 | # pipenv
165 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
166 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
167 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
168 | # install all needed dependencies.
169 | #Pipfile.lock
170 |
171 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
172 | __pypackages__/
173 |
174 | # Celery stuff
175 | celerybeat-schedule
176 | celerybeat.pid
177 |
178 | # SageMath parsed files
179 | *.sage.py
180 |
181 | # Environments
182 | .env
183 | .venv
184 | env/
185 | venv/
186 | ENV/
187 | env.bak/
188 | venv.bak/
189 |
190 | # Spyder project settings
191 | .spyderproject
192 | .spyproject
193 |
194 | # Rope project settings
195 | .ropeproject
196 |
197 | # mkdocs documentation
198 | /site
199 |
200 | # mypy
201 | .mypy_cache/
202 | .dmypy.json
203 | dmypy.json
204 |
205 | # Pyre type checker
206 | .pyre/
207 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com/ for usage and config
2 | fail_fast: true
3 |
4 | repos:
5 | - repo: https://github.com/compilerla/conventional-pre-commit
6 | rev: v4.0.0
7 | hooks:
8 | - id: conventional-pre-commit
9 | stages: [commit-msg]
10 | - repo: https://github.com/pre-commit/pre-commit-hooks
11 | rev: v5.0.0
12 | hooks:
13 | - id: check-ast
14 | - id: check-toml
15 | - id: end-of-file-fixer
16 | exclude: ".srt$"
17 | - id: trailing-whitespace
18 | exclude: ".srt$"
19 | - id: mixed-line-ending
20 | - repo: local
21 | hooks:
22 | - id: ruff-check
23 | name: ruff check --fix .
24 | stages: [pre-commit]
25 | language: system
26 | entry: ruff check .
27 | pass_filenames: false
28 | - id: ruff-format .
29 | name: ruff format
30 | stages: [pre-commit]
31 | language: system
32 | entry: ruff check .
33 | pass_filenames: false
34 | - id: md-format .
35 | name: md format
36 | stages: [pre-commit]
37 | language: system
38 | entry: mdformat --end-of-line keep .
39 | pass_filenames: false
40 | - id: mypy
41 | name: mypy
42 | stages: [pre-commit]
43 | language: system
44 | entry: mypy
45 | pass_filenames: false
46 | - id: pytest
47 | name: pytest
48 | stages: [pre-commit]
49 | language: system
50 | entry: pytest
51 | pass_filenames: false
52 | - id: coverage
53 | name: coverage
54 | stages: [pre-commit]
55 | language: system
56 | entry: coverage lcov
57 | pass_filenames: false
58 | - id: pip-audit
59 | name: pip-audit
60 | stages: [pre-commit]
61 | language: system
62 | entry: pip-audit
63 | pass_filenames: false
64 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.5.6 (2025-04-12)
4 |
5 | - Revert pygobject version bump to stay compatible with Ubuntu 22.04 LTS (last try wasn't enough)
6 |
7 | ## v0.5.5 (2025-03-27)
8 |
9 | - Revert pygobject version bump to stay compatible with Ubuntu 22.04 LTS
10 |
11 | ## v0.5.4 (2025-03-23)
12 |
13 | - Slightly speed up startup
14 | - Fix potential error when window titles contain single quotes
15 | - Extend vscode cheatsheet
16 | - View bindings a bit more compact
17 |
18 | ## v0.5.3 (2025-03-21)
19 |
20 | - Add shortcuts `Ctrl + Up`/`Down` to switch to next/previous cheatsheet:
21 |
22 | ## v0.5.2 (2024-10-04)
23 |
24 | - Add cheatsheet for Alacritty.
25 | - Changed `cli` cheatsheet to hidden by default.
26 | - Renamed browser- and vscode-extension cheatsheets.
27 |
28 | ## v0.5.1 (2024-07-04)
29 |
30 | - Fix support for older version of `libadwaita`.
31 | - Add cheatsheet for GH copilot.
32 |
33 | ## v0.5.0 (2024-04-23)
34 |
35 | - Breaking changes:
36 | - Renamed the attribute `regex_process` in `toml`-files to `regex_wmclass`.
37 | - Renamed the attribute `source` in `toml`-files to `url`.
38 | - Removed cli-args which are now covered by settings menu.
39 | - Fix duplicate IDs in included sections.
40 | - Add settings menu to ui.
41 | - Add Support KDE + Wayland.
42 | - Focus search field on start.
43 | - Cheatsheets:
44 | - Moved some CLI commands into separate cheatsheet and include it in terminal apps.
45 | - Added sheet for Keyhint itself.
46 |
47 | ## v0.4.3 (2024-02-13)
48 |
49 | - Fix background color in GTK 4.6.
50 |
51 | ## v0.4.2 (2024-02-13)
52 |
53 | - Fix missing method in GTK 4.6.
54 |
55 | ## v0.4.1 (2024-02-04)
56 |
57 | - Fix `No module named 'toml'`.
58 |
59 | ## v0.4.0 (2024-02-04)
60 |
61 | - Breaking changes in config files:
62 | - Switch from yaml to toml format for shortcuts (you can use
63 | [`yq`](https://mikefarah.gitbook.io/yq/) to convert yaml to toml)
64 | - The key `hints` is renamed to `section`
65 | - Dropping binary builds! Please install via `pipx install keyhint` instead.
66 | - Add filter for shortcuts or section.
67 | - Add possibility to hide whole cheatsheets via `hidden = true` in config files.
68 | - Add fullscreen mode as default (toggle via `F11`)
69 |
70 | ## v0.3.0 (2023-02-12)
71 |
72 | - Update app to Gtk4
73 | - Adjust hint files
74 |
75 | ## v0.2.4 (2022-03-09)
76 |
77 | - Switch to Nuitka for building binary release
78 |
79 | ## v0.2.3 (2022-03-06)
80 |
81 | - Add accent color for section titles
82 |
83 | ## v0.2.2 (2022-03-05)
84 |
85 | - Add hints for kitty
86 | - Add hints for pop-shell
87 | - Update dependencies
88 |
89 | ## v0.2.1 (2021-05-18)
90 |
91 | - Slightly improve shortcuts for vscode
92 |
93 | ## v0.2.0 (2021-04-03)
94 |
95 | - Complete rewrite
96 | - Drop support for Windows (for now)
97 | - Use GTK+ framework
98 |
99 | ## v0.1.3 (2020-10-18)
100 |
101 | - Switch to TKinter
102 | - Speed improvements
103 |
104 | ## v0.1.0 (2020-05-15)
105 |
106 | - Initial version
107 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 dynobo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KeyHint
2 |
3 | **_Utility to display keyboard shortcuts or other hints based on the active window on
4 | Linux._**
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 
14 |
15 | ## Prerequisites
16 |
17 | - Python 3.11+
18 | - GTK 4.6+ (shipped since Ubuntu 22.04) + related dev packages:
19 | ```sh
20 | sudo apt-get install \
21 | libgirepository1.0-dev \
22 | libcairo2-dev \
23 | python3-gi \
24 | gobject-introspection \
25 | libgtk-4-dev
26 | ```
27 | - Wayland & Gnome: The
28 | [Gnome Extension "Window-Calls"](https://extensions.gnome.org/extension/4724/window-calls/)
29 | is required to auto-select the cheatsheet based on the current active application.
30 |
31 | ## Installation
32 |
33 | - `uv tool install keyhint` (recommended, requires [uv](https://docs.astral.sh/uv/))
34 | - `pipx install keyhint` (requires [pipx](https://pipx.pypa.io/))
35 | - _or_ `pip install keyhint`
36 |
37 | ## Usage
38 |
39 | - Configure a **global hotkey** (e.g. `Ctrl + F1`) **via your system settings** to
40 | launch `keyhint`.
41 | - If KeyHint is launched via hotkey, it detects the current active application and shows
42 | the appropriate hints. (This feature won't work reliably when KeyHint ist started via
43 | Menu or Launcher.)
44 |
45 | ## CLI Options
46 |
47 | ```
48 | Application Options:
49 | -c, --cheatsheet=SHEET-ID Show cheatsheet with this ID on startup
50 | -v, --verbose Verbose log output for debugging
51 | ```
52 |
53 | ## Cheatsheet Configuration
54 |
55 | The content which KeyHint displays is configured using [`toml`](https://toml.io/en/)
56 | configuration files.
57 |
58 | KeyHint reads those files from two locations:
59 |
60 | 1. The [built-in directory](https://github.com/dynobo/keyhint/tree/main/keyhint/config)
61 | 1. The user directory, usually located in `~/.config/keyhint`
62 |
63 | ### How Keyhint selects the cheatsheet to show
64 |
65 | - The cheatsheet to be displayed on startup are selected by comparing the value of
66 | `regex_wmclass` with the wm_class of the active window and the value of `regex_title`
67 | with the title of the active window.
68 | - The potential cheatsheets are processed alphabetically by filename, the first file
69 | that matches both wm_class and title are getting displayed.
70 | - Both of `regex_` values are interpreted as **case in-sensitive regular expressions**.
71 | - Check "Debug Info" in the application menu to get insights about the active window and
72 | the selected cheatsheet file.
73 |
74 | ### Customize or add cheatsheets
75 |
76 | - To **change built-in** cheatsheets, copy
77 | [the corresponding .toml-file](https://github.com/dynobo/keyhint/tree/main/keyhint/config)
78 | into the config directory. Make your changes in a text editor. As long as you don't
79 | change the `id` it will overwrite the defaults.
80 | - To **create new** cheatsheets, I suggest you start with
81 | [one of the existing .toml-file](https://github.com/dynobo/keyhint/tree/main/keyhint/config):
82 | - Place it in the config directory and give it a good file name.
83 | - Change the value `id` to something unique.
84 | - Adjust `regex_wmclass` and `regex_title` so it will be selected based on the active
85 | window. (See [Tips](#tips))
86 | - Add the `shortcuts` & `label` to a `section`.
87 | - If you think your cheatsheet might be useful for others, please consider opening a
88 | pull request or an issue!
89 | - You can always **reset cheatsheets** to the shipped version by deleting the
90 | corresponding `.toml` files from the config folder.
91 | - You can **include shortcuts from other cheatsheets** by adding
92 | `include = [""]`
93 |
94 | ### Examples
95 |
96 | #### Hide existing cheatsheets
97 |
98 | To hide a cheatsheet, e.g. the
99 | [built-in](https://github.com/dynobo/keyhint/blob/main/keyhint/config/tilix.toml) one
100 | with the ID `tilix`, create a new file `~/.config/keyhint/tilix.toml` with the content:
101 |
102 | ```toml
103 | id = "tilix"
104 | hidden = true
105 | ```
106 |
107 | #### Extend existing cheatsheets
108 |
109 | To add keybindings to an existing cheatsheet, e.g. the
110 | [built-in](https://github.com/dynobo/keyhint/blob/main/keyhint/config/firefox.toml) one
111 | with the ID `firefox`, create a new file `~/.config/keyhint/firefox.toml` which only
112 | contains the ID and the additional bindings:
113 |
114 | ```toml
115 | id = "firefox"
116 |
117 | [section]
118 | [section."My Personal Favorites"] # New section
119 | "Ctrl + Shift + Tab" = "Show all Tabs"
120 | # ...
121 | ```
122 |
123 | #### Add new cheatsheet which never gets auto-selected
124 |
125 | To add a new cheatsheet, which never gets automatically selected and displayed by
126 | KeyHint, but remains accessible through KeyHint's cheatsheet dropdown, create a file
127 | `~/.config/keyhint/my-app.toml`:
128 |
129 | ```toml
130 | id = "my-app"
131 | url = "url-to-my-apps-keybindings"
132 |
133 | [match]
134 | regex_wmclass = "a^" # Patter which never matches
135 | regex_title = "a^"
136 |
137 | [section]
138 | [section.General]
139 | "Ctrl + C" = "Copy"
140 | # ...
141 |
142 | ```
143 |
144 | #### Different cheatsheets for different Websites
145 |
146 | For showing different browser-cheatsheets depending on the current website, you might
147 | want to use a browser extension like
148 | "[Add URL To Window Title](https://addons.mozilla.org/en-US/firefox/addon/add-url-to-window-title/)"
149 | and configure the `[match]` section to look for the url in the title. E.g.
150 | `~/.config/keyhint/github.toml`
151 |
152 | ```toml
153 | id = "github.com"
154 |
155 | [match]
156 | regex_wmclass = "Firefox"
157 | regex_title = ".*github\\.com.*" # URL added by browser extensions to window title
158 |
159 | [section]
160 | [section.Repositories]
161 | gc = "Goto code tab"
162 | # ...
163 | ```
164 |
165 | ## Contribute
166 |
167 | I'm happy about any contribution! Especially I would appreciate submissions to improve
168 | the
169 | [shipped cheatsheets](https://github.com/dynobo/keyhint/tree/main/keyhint/config).
170 | (The current set are the cheatsheets I personally use).
171 |
172 | ## Design Principles
173 |
174 | - **Don't run as service**
It shouldn't consume resources in the background, even if
175 | this leads to slightly slower start-up time.
176 | - **No network connection**
Everything should run locally without any network
177 | communication.
178 | - **Dependencies**
The fewer dependencies, the better.
179 |
180 | ## Certification
181 |
182 | 
183 |
--------------------------------------------------------------------------------
/keyhint/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.5.6"
2 |
--------------------------------------------------------------------------------
/keyhint/__main__.py:
--------------------------------------------------------------------------------
1 | """Package entry for keyhint."""
2 |
3 | from keyhint import app
4 |
5 | if __name__ == "__main__":
6 | app.main()
7 |
--------------------------------------------------------------------------------
/keyhint/app.py:
--------------------------------------------------------------------------------
1 | """Cheatsheet for keyboard shortcuts & commands.
2 |
3 | Main entry point that get's executed on start.
4 | """
5 |
6 | import logging
7 | import sys
8 |
9 | import gi
10 |
11 | gi.require_version("Gtk", "4.0")
12 | gi.require_version("Adw", "1")
13 |
14 | from gi.repository import Adw, Gio, GLib # noqa: E402
15 |
16 | from keyhint.window import KeyhintWindow # noqa: E402
17 |
18 | logging.basicConfig(
19 | format="%(asctime)s - %(levelname)-7s - %(module)s.py:%(lineno)d - %(message)s",
20 | datefmt="%H:%M:%S",
21 | level="WARNING",
22 | )
23 | logger = logging.getLogger("keyhint")
24 |
25 |
26 | class Application(Adw.Application):
27 | """Main application class.
28 |
29 | Handle command line options and display the window.
30 |
31 | Args:
32 | Gtk (Gtk.Application): Application Class
33 | """
34 |
35 | def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
36 | """Initialize application with command line options."""
37 | kwargs.update(
38 | application_id="com.github.dynobo.keyhint",
39 | flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
40 | )
41 | super().__init__(
42 | *args,
43 | **kwargs,
44 | )
45 | self.options: dict = {}
46 |
47 | self.add_main_option(
48 | "cheatsheet",
49 | ord("c"),
50 | GLib.OptionFlags.NONE,
51 | GLib.OptionArg.STRING,
52 | "Show cheatsheet with this ID on startup",
53 | "SHEET-ID",
54 | )
55 | self.add_main_option(
56 | "verbose",
57 | ord("v"),
58 | GLib.OptionFlags.NONE,
59 | GLib.OptionArg.NONE,
60 | "Verbose log output for debugging",
61 | None,
62 | )
63 |
64 | def do_activate(self, *_, **__) -> None: # noqa: ANN002, ANN003
65 | """Create and activate a window."""
66 | window = KeyhintWindow(self.options)
67 | window.set_application(self)
68 | window.present()
69 |
70 | def do_command_line(self, cli: Gio.ApplicationCommandLine) -> int:
71 | """Store command line options in class attribute for later usage."""
72 | self.options = cli.get_options_dict().end().unpack()
73 |
74 | if "verbose" in self.options:
75 | logger.setLevel("DEBUG")
76 | logger.debug("CLI Options: %s", self.options)
77 |
78 | self.activate()
79 | return 0
80 |
81 |
82 | def main() -> None:
83 | """Start application on script call."""
84 | app = Application()
85 | app.run(sys.argv)
86 |
87 |
88 | if __name__ == "__main__":
89 | main()
90 |
--------------------------------------------------------------------------------
/keyhint/binding.py:
--------------------------------------------------------------------------------
1 | """Utility functions to format and view bindings (shortcut + label)."""
2 |
3 | import logging
4 |
5 | from gi.repository import GLib, GObject, Gtk
6 |
7 | logger = logging.getLogger("keyhint")
8 |
9 |
10 | def replace_keys(text: str) -> str:
11 | """Replace certain key names by corresponding unicode symbol.
12 |
13 | Args:
14 | text (str): Text with key names.
15 |
16 | Returns:
17 | str: Text where some key names have been replaced by unicode symbol.
18 | """
19 | if text in {"PageUp", "PageDown"}:
20 | text = text.replace("Page", "Page ")
21 |
22 | text = text.replace("Down", "↓")
23 | text = text.replace("Up", "↑")
24 | text = text.replace("Left", "←")
25 | text = text.replace("Right", "→")
26 | text = text.replace("Direction", "←↓↑→")
27 | text = text.replace("PlusMinus", "±")
28 | text = text.replace("Plus", "+") # noqa: RUF001
29 | text = text.replace("Minus", "−") # noqa: RUF001
30 | text = text.replace("Slash", "/")
31 |
32 | return text # noqa: RET504
33 |
34 |
35 | def style_key(text: str) -> tuple[str, list[str]]:
36 | """Style the key as keycap or as divider (between two keycaps).
37 |
38 | Args:
39 | text: A single partition of a shortcut.
40 |
41 | Returns:
42 | (Unescaped) key of the shortcut, css classes to use.
43 | """
44 | key_dividers = ["+", "/", "&", "or"]
45 | if text in key_dividers:
46 | css_classes = ["dim-label"]
47 | else:
48 | text = text.replace("\\/", "/")
49 | text = text.replace("\\+", "+")
50 | text = text.replace("\\&", "&")
51 | css_classes = ["keycap"]
52 | return text, css_classes
53 |
54 |
55 | class Row(GObject.Object):
56 | shortcut: str
57 | label: str
58 | filter_text: str
59 |
60 | def __init__(self, shortcut: str, label: str, section: str) -> None:
61 | super().__init__()
62 | self.shortcut = shortcut
63 | self.label = label
64 | self.filter_text = f"{shortcut} {label} {section}"
65 |
66 |
67 | def create_shortcut(text: str) -> Gtk.Box:
68 | box = Gtk.Box(
69 | orientation=Gtk.Orientation.HORIZONTAL, spacing=6, halign=Gtk.Align.END
70 | )
71 | keys = [text.replace("`", "")] if text.startswith("`") else text.split()
72 | for k in keys:
73 | key = replace_keys(text=k.strip())
74 | key, css_classes = style_key(text=key)
75 | label = Gtk.Label()
76 | label.set_css_classes(css_classes)
77 | label.set_markup(f"{GLib.markup_escape_text(key)}")
78 | box.append(label)
79 | return box
80 |
81 |
82 | def create_column_view_column(
83 | title: str,
84 | factory: Gtk.SignalListItemFactory,
85 | fixed_width: float | None = None,
86 | ) -> Gtk.ColumnViewColumn:
87 | column = Gtk.ColumnViewColumn(title=title, factory=factory)
88 | if fixed_width:
89 | column.set_fixed_width(int(fixed_width))
90 | return column
91 |
92 |
93 | def create_column_view(
94 | selection: Gtk.SelectionModel,
95 | shortcut_column: Gtk.ColumnViewColumn,
96 | label_column: Gtk.ColumnViewColumn,
97 | ) -> Gtk.ColumnView:
98 | column_view = Gtk.ColumnView()
99 | column_view.get_style_context().add_class("bindings-section")
100 | column_view.set_hexpand(True)
101 | column_view.set_model(selection)
102 | column_view.append_column(shortcut_column)
103 | column_view.append_column(label_column)
104 | return column_view
105 |
--------------------------------------------------------------------------------
/keyhint/config.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from configparser import ConfigParser
4 | from pathlib import Path
5 |
6 | if xdg_conf := os.getenv("XDG_CONFIG_HOME", None):
7 | CONFIG_PATH = Path(xdg_conf) / "keyhint"
8 | else:
9 | CONFIG_PATH = Path.home() / ".config" / "keyhint"
10 | CONFIG_FILE = CONFIG_PATH / "keyhint.ini"
11 |
12 | logger = logging.getLogger("keyhint")
13 |
14 |
15 | class WritingConfigParser(ConfigParser):
16 | def set_persistent(
17 | self, section: str, option: str, value: str | bool | int
18 | ) -> None:
19 | """Updates the config file on disk, in case the value has changed.
20 |
21 | Args:
22 | section: Config section in the toml file.
23 | option: Setting name in the section.
24 | value: Setting value to be updated.
25 | """
26 | if self.get(section, option) == str(value):
27 | return
28 | self.set(section, option, str(value))
29 | if not CONFIG_FILE.parent.exists():
30 | CONFIG_FILE.parent.mkdir(exist_ok=True, parents=True)
31 |
32 | self.write(CONFIG_FILE.open("w"))
33 |
34 |
35 | def load() -> WritingConfigParser:
36 | """Load the settings file or create a default settings file if it doesn't exist."""
37 | config = WritingConfigParser(
38 | defaults={
39 | "fullscreen": "True",
40 | "sort_by": "size",
41 | "orientation": "vertical",
42 | "fallback_cheatsheet": "keyhint",
43 | "zoom": "100",
44 | },
45 | )
46 | if CONFIG_FILE.exists():
47 | config.read(CONFIG_FILE)
48 | logger.debug("Loaded config from %s.", CONFIG_FILE)
49 | if not config.has_section("main"):
50 | config.add_section("main")
51 | logger.debug("Created missing 'main' section.")
52 | return config
53 |
54 |
55 | if __name__ == "__main__":
56 | load()
57 |
--------------------------------------------------------------------------------
/keyhint/config/alacritty.toml:
--------------------------------------------------------------------------------
1 | id = "alacritty"
2 | url = "https://github.com/alacritty/alacritty/blob/master/docs/features.md"
3 | include = ["cli"]
4 |
5 | [match]
6 | regex_wmclass = "alacritty"
7 | regex_title = ".*"
8 |
9 | [section]
10 |
11 | [section.Search]
12 | "Ctrl + Shift + F" = "Search forward"
13 | "Ctrl + Shift + B" = "Search backwards"
14 | "Ctrl + Shift + S" = "Paste from selection"
15 | "Enter / Shift + Enter" = "Next / previous match"
16 | "Esc" = "Leave search"
17 |
18 | [section."Vi Mode"]
19 | "Ctrl + Shift + Space" = "Launch Vi mode"
20 | "/" = "Search forward"
21 | "?" = "Search backwards"
22 | "Enter" = "Activate hint (e.g. open URL)"
23 |
24 | [section.Mouse]
25 | "`Double click`" = "Select word"
26 | "`Triple click`" = "Select line"
27 | "Ctrl + select" = "Select block"
28 |
--------------------------------------------------------------------------------
/keyhint/config/cli.toml:
--------------------------------------------------------------------------------
1 | id = "cli"
2 | url = ""
3 | hidden = true
4 |
5 | [match]
6 | regex_wmclass = "^$"
7 | regex_title = "^$"
8 |
9 | [section]
10 |
11 | [section."Shell History"]
12 | "Ctrl + r" = "Reverse cmd search / find next"
13 | "`history | grep `" = "Search for \"\""
14 | "`history 10`" = "Print last 10 cmds"
15 | "!3" = "Execute cmd 3 from history"
16 |
17 | [section."Data Wrangling"]
18 | "`awk '{print $1}'`" = "Column base manipulations"
19 | "`sed -E 's/.*(d+)/\\1/'`" = "Line base manipulations"
20 | "`paste -sd,`" = "Concatenate lines"
21 | "`tail -n10`" = "Last lines"
22 | "`head -n10`" = "First lines"
23 | "`uniq -c`" = "Count unique lines"
24 | "`sort -nk1,1" = "Sort by column 1"
25 |
26 | [section.Tools]
27 | bc = "Calculator"
28 | "gtop / htop" = "System Monitors"
29 | xplr = "File manager"
30 | moc = "Music Player in Console"
31 | nvtop = "NVidia Card usage"
32 | fd = "Easy to use find clone"
33 | hyperfine = "Benchmarking"
34 | tl = "TooLong log file viewer"
35 | bat = "cat clone to view files with style"
36 | eza = "ls clone with coloring"
37 | duf = "Basic disk usage"
38 | ncdu = "Recursive disk usage analyzer"
39 | s-tui = "Stress test and monitor"
40 | btm = "System resources dashboard"
41 |
42 | [section.Navigating]
43 | pwd = "Print working dir"
44 | cd = "Change to home dir"
45 | "`cd -`" = "Change to previous dir"
46 |
47 | [section.Debugging]
48 | "journalctl --since \"1m ago\"" = "System logs since time"
49 | "journalctl -k" = "System logs since boot"
50 |
--------------------------------------------------------------------------------
/keyhint/config/firefox-tridactyl.toml:
--------------------------------------------------------------------------------
1 | id = "firefox-tridactyl"
2 | url = "https://github.com/tridactyl/tridactyl"
3 | hidden = true
4 |
5 | [match]
6 | regex_wmclass = "$^"
7 | regex_title = "$^"
8 |
9 | [section]
10 | [section.Modes]
11 | Esc = "Normal"
12 | f = "Hint"
13 | v = "Visual"
14 | ":" = "Command"
15 | "Shift + Esc" = "Ignore"
16 |
17 | [section.Tabs]
18 | "J / K" = "Next / previous"
19 | "d / x" = "Close"
20 | "u / X" = "Reopen"
21 | t = "Create new"
22 | b = "Switch between"
23 | "gt / GT" = "Go to next / previous"
24 | yt = "Duplicate"
25 | W = "Move tab new window"
26 |
27 | [section.Normal]
28 | "H / L" = "History back / forward"
29 | "o / O" = "Open URL in current tab"
30 | "t / T" = "Open URL in new tab"
31 | "w / W" = "Open URL in new Window"
32 | "p / P" = "Open clipboard in current/new tab"
33 | "s / S" = "Search with engine / in new tab"
34 | "gg / G" = "Scroll to start / end"
35 | yy = "Copy URL to clipboard"
36 | "zi / zo / zz" = "Zoom in / out / reset"
37 | "\\/" = "Search"
38 |
39 | [section.Hint]
40 | f = "Open in current tab"
41 | F = "Open in new background tab"
42 | "; + h" = "Select element"
43 | "; + y" = "Copy link to clipboard"
44 | "; + p" = "Copy element to clipboard"
45 | "; + k" = "Kill element"
46 |
47 | [section.Visual]
48 | hjklewb = "Adjust selection"
49 | y = "yank to clipboard"
50 | "s / S" = "Search selected text in current /new tab"
51 | f = "Open link in current tab"
52 | F = "Open link new current tab"
53 | H = "Back in History"
54 | L = "Forward in History"
55 |
--------------------------------------------------------------------------------
/keyhint/config/firefox.toml:
--------------------------------------------------------------------------------
1 | id = "firefox"
2 | url = "https://support.mozilla.org/en-US/kb/keyboard-shortcuts-perform-firefox-tasks-quickly"
3 |
4 | [match]
5 | regex_wmclass = "Firefox"
6 | regex_title = ".*"
7 |
8 | [section]
9 | [section."URL Navigation"]
10 | "Ctrl + L" = "Location bar"
11 | "Alt + Left" = "Back in History"
12 | "Alt + Right" = "Forward in History"
13 |
14 | [section."Tab Management"]
15 | "Ctrl + T" = "New tab"
16 | "Ctrl + Tab" = "Next tab"
17 | "Ctrl + 1..8" = "Select tab 1..8"
18 | "Ctrl + W" = "Close tab"
19 | "Ctrl + Shift + T" = "Undo close tab"
20 | "\\/" = "Quick Find"
21 |
22 | [section.Various]
23 | "Ctrl + Shift + Y" = "Show downloads"
24 | "Ctrl + g" = "Search in site"
25 | "Ctrl + G" = "Search next match"
26 |
--------------------------------------------------------------------------------
/keyhint/config/foot.toml:
--------------------------------------------------------------------------------
1 | id = "foot-terminal"
2 | url = "https://codeberg.org/dnkl/foot#user-content-shortcuts"
3 | include = ["cli"]
4 |
5 | [match]
6 | regex_wmclass = "foot"
7 | regex_title = ".*"
8 |
9 | [section]
10 | [section.Foot]
11 | "Shift + PageUp / PageDown" = "Scroll up/down"
12 | "Ctrl + Shift + c / v" = "Copy/paste from clipboard"
13 | "Ctrl + Shift + r" = "Search mode"
14 | "Ctrl + r / s" = "Search for previous / next match"
15 | "Ctrl + Shift + u" = "URL mode"
16 | t = "Toggle URL in label"
17 |
--------------------------------------------------------------------------------
/keyhint/config/forge.toml:
--------------------------------------------------------------------------------
1 | id = "forge"
2 | url = "https://github.com/forge-ext/forge"
3 | hidden = true
4 |
5 | [match]
6 | regex_wmclass = "^$"
7 | regex_title = "^$"
8 |
9 | [section]
10 |
11 | [section."Move window"]
12 | "Super + Shift + hjkl" = "Move window"
13 | "Super + Ctrl + hjkl" = "Swap window"
14 | "Super + Enter" = "Swap with last active"
15 | "Super + g" = "Toggle vertical/horizontal"
16 | "Super + c" = "Toggle floating"
17 |
18 | [section.Navigation]
19 | "Super + Direction" = "Move focus"
20 | "Super + Ctrl + UpDown" = "Navigate between workspaces"
21 |
22 | [section."Manipulate window"]
23 | "Super + q" = "Quit"
24 | "Super + Ctrl + yuio" = "Increase size left / bottom / top / right /"
25 | "Super + Ctrl + Shift + yuio" = "Decrease size left / bottom / top / right /"
26 |
27 | [section."Manage workspaces"]
28 |
29 | [section.Miscellaneous]
30 | "Super + Tab" = "Switch apps"
31 | "Alt + F2" = "Run command"
32 | "Ctrl + Alt + Del" = "Logout"
33 |
--------------------------------------------------------------------------------
/keyhint/config/github.toml:
--------------------------------------------------------------------------------
1 | id = "github.com"
2 | url = "https://help.github.com/en/github/getting-started-with-github/keyboard-shortcuts"
3 | hidden = true
4 |
5 | [match]
6 | regex_wmclass = ".*"
7 | regex_title = ".*github\\.com.*"
8 |
9 | [section]
10 | [section."Site Wide"]
11 | "S / \\/" = "Focus search bar"
12 | gn = "Goto notifications"
13 | esc = "Close hovercard, if open"
14 |
15 | [section.Repositories]
16 | gc = "Goto code tab"
17 | gi = "Goto issues tab"
18 | gp = "Goto pull request tab"
19 | gb = "Goto projects tab"
20 | gw = "Goto wiki tab"
21 |
22 | [section."Browse Code"]
23 | t = "Activate file finder"
24 | l = "Jump to line"
25 | w = "Switch to new branch/tag"
26 | y = "Expand URL to canonical"
27 | i = "Toggle comments on diffs"
28 | b = "Open blame view"
29 |
--------------------------------------------------------------------------------
/keyhint/config/gnome-terminal.toml:
--------------------------------------------------------------------------------
1 | id = "gnome-terminal"
2 | url = "https://help.gnome.org/users/gnome-terminal/stable/adv-keyboard-shortcuts.html.en"
3 | include = ["cli"]
4 |
5 | [match]
6 | regex_wmclass = "gnome-terminal"
7 | regex_title = ".*"
8 |
9 | [section]
10 |
11 | [section.Tabs]
12 | "Shift + Ctrl + T" = "New Tab"
13 | "Shift + Ctrl + N" = "New Window"
14 | "Shift + Ctrl + W" = "Close Tab"
15 | "Shift + Ctrl + Q" = "Close Window"
16 |
--------------------------------------------------------------------------------
/keyhint/config/keyhint.toml:
--------------------------------------------------------------------------------
1 | id = "keyhint"
2 | url = "https://github.com/dynobo/keyhint/README.md"
3 |
4 | [match]
5 | regex_wmclass = "keyhint"
6 | regex_title = ".*"
7 |
8 | [section]
9 | [section.General]
10 | F11 = "Toggle Fullscreen"
11 | "Ctrl + f" = "Focus search field"
12 | "Ctrl + Backspace / u" = "Clear search field"
13 | Esc = "Exit"
14 |
15 | [section."Switch Cheatsheet"]
16 | "Ctrl + Down" = "Next cheatsheet"
17 | "Ctrl + Up" = "Previous cheatsheet"
18 | "Ctrl + s & Enter" = "Focus & open sheet selection dropdown"
19 |
20 | [section.Scroll]
21 | "Down or Ctrl + j" = "Forward"
22 | "Up or Ctrl + k" = "Backward"
23 | PageDown = "Page forward"
24 | PageUp = "Page backward"
25 |
--------------------------------------------------------------------------------
/keyhint/config/kitty.toml:
--------------------------------------------------------------------------------
1 | id = "kitty"
2 | url = "https://sw.kovidgoyal.net/kitty/overview/"
3 | include = ["cli"]
4 |
5 | [match]
6 | regex_wmclass = "kitty"
7 | regex_title = ".*"
8 |
9 | [section]
10 | [section.Scrolling]
11 | "Ctrl + Shift + UpDown" = "Line up/down"
12 | "Ctrl + Shift + PageUpDown" = "Page up/down"
13 | "Ctrl + Shift + Home" = "Top"
14 | "Ctrl + Shift + End" = "Bottom"
15 | "Ctrl + Shift + z" = "Previous shell prompt"
16 | "Ctrl + Shift + x" = "Next shell prompt"
17 |
18 | [section.Clipboard]
19 | "Ctrl + Shift + C" = "Copy to clipboard"
20 | "Ctrl + Shift + V" = "Paste from clipboard"
21 | "Ctrl + Shift + S" = "Paste from selection"
22 |
23 | [section.Windows]
24 | "Ctrl + Shift + Enter" = "New window"
25 | "Ctrl + Shift + w" = "Close window"
26 | "Ctrl + Shift + ]" = "Next window"
27 | "Ctrl + Shift + [" = "Previous window"
28 | "Ctrl + Shift + f" = "Move window forward"
29 | "Ctrl + Shift + b" = "Move window backward"
30 | "Ctrl + Shift + l" = "Switch between 7 layouts"
31 |
32 | [section.Miscellaneous]
33 | "Ctrl + Shift + PlusMinus" = "Increase/decrease font size"
34 | "Ctrl + Shift + e" = "Open URL in browser"
35 |
--------------------------------------------------------------------------------
/keyhint/config/pop-shell.toml:
--------------------------------------------------------------------------------
1 | id = "pop-shell"
2 | url = "https://support.system76.com/articles/pop-keyboard-shortcuts/"
3 |
4 | [match]
5 | regex_wmclass = "pop-shell"
6 | regex_title = ".*"
7 |
8 | [section]
9 | [section."Manipulate windows"]
10 | "Super + Direction" = "Move focus"
11 | "Super + O" = "Switch tiling orientation"
12 | "Super + M" = "Toggle maximized"
13 | "Super + Y" = "Toggle tiling"
14 | "Super + G" = "Toggle floating"
15 | "Super + S" = "Toggle stacking"
16 | "Super + Q" = "Quit window"
17 |
18 | [section."Adjustment Mode"]
19 | "Super + Enter" = "Enter window adjustment mode"
20 | Direction = "Move window"
21 | "Shift + Direction" = "Resize window"
22 | "Ctrl + Direction" = "Swap windows"
23 | Enter = "Apply changes"
24 | Esc = "Exit window adjustment mode"
25 |
26 | [section."Manage workspaces"]
27 | "Super + Ctrl + UpDown" = "Navigate between workspaces"
28 | "Super + Shift + Direction" = "Move window between workspaces"
29 |
30 | [section.Miscellaneous]
31 | "Super + Tab" = "Switch apps"
32 | "Super + ^" = "Switch windows of current app"
33 | "Alt + F2" = "Run command"
34 | "Ctrl + Alt + Del" = "Logout"
35 |
--------------------------------------------------------------------------------
/keyhint/config/tilix.toml:
--------------------------------------------------------------------------------
1 | id = "tilix"
2 | url = "https://github.com/gnunn1/tilix/blob/master/data/resources/ui/shortcuts.ui"
3 | include = ["cli"]
4 |
5 | [match]
6 | regex_wmclass = "tilix"
7 | regex_title = ".*"
8 |
9 | [section]
10 | [section.Window]
11 | F11 = "Toggle fullscreen"
12 | F12 = "View session sidebar"
13 |
14 | [section.Sessions]
15 | "Ctrl + Shift + T" = "Open new session"
16 | "Ctrl + PageUp / PageDown" = "Switch to next/preview session"
17 | "Ctrl + Shift + Q" = "Close current session"
18 |
19 | [section.Tiling]
20 | "Ctrl + Alt + A" = "Add terminal automatically"
21 | "Ctrl + Alt + R" = "Add terminal right"
22 | "Ctrl + Alt + D" = "Add terminal down"
23 | "Shift + Alt + Up Down Left Right" = "Resize terminal"
24 | "Ctrl + Shift + W" = "Close"
25 |
26 | [section.Terminal]
27 | "Shift + Ctrl + F" = "Find"
28 | "Shift + Ctrl + G / H" = "Find next / previous"
29 | "Shift + Ctrl + Up Down" = "Scroll up / down"
30 | "Shift + PageUp PageDown" = "Page up / down"
31 |
--------------------------------------------------------------------------------
/keyhint/config/tmux.toml:
--------------------------------------------------------------------------------
1 | id = "tmux"
2 | url = "https://github.com/gnunn1/tilix/blob/master/data/resources/ui/shortcuts.ui"
3 | include = ["cli"]
4 |
5 | [match]
6 | regex_wmclass = "foot"
7 | regex_title = "tmux"
8 |
9 | [section]
10 | [section.General]
11 | "Ctrl + a" = "Prefix key for all shortcuts!"
12 | "?" = "Show shortcuts"
13 |
14 | [section.Panes]
15 | "\"" = "Split vertically"
16 | "%" = "Split horizontally"
17 | Space = "Switch layouts"
18 | z = "Zoom current"
19 | x = "Kill current"
20 | "`Up Down Left Right`" = "Select"
21 | "Alt + Up Down Left Right" = "Resize"
22 | o = "Switch to next"
23 | "Ctrl + o" = "Rotate"
24 | e = "Spread equally"
25 |
26 | [section.Window]
27 | "&" = "Kill current"
28 |
29 | [section.Session]
30 | d = "Detach"
31 | "$" = "Rename"
32 |
--------------------------------------------------------------------------------
/keyhint/config/vim.toml:
--------------------------------------------------------------------------------
1 | id = "vim"
2 | url = ""
3 |
4 | [match]
5 | regex_wmclass = "(foot|kitty|gnome-terminal)"
6 | regex_title = "^(n)?vim( .*)?$"
7 |
8 | [section]
9 |
10 | [section."Verbs (Operators)"]
11 | "y / Y" = "Yank (copy) / until end"
12 | "p / ]p" = "Paste / and adjust indent"
13 | "d / D" = "Delete / until end"
14 | "c / C" = "Change / until end"
15 | "v / V" = "Visually select / whole line"
16 | "> / <" = "Add / remove indentation"
17 |
18 | [section.Modifiers]
19 | i = "Inner"
20 | a = "Around"
21 | "{int}" = "x times"
22 | "t / T + {c}" = "Toward next / previous char"
23 | "f / F + {c}" = "Find next / previous char"
24 | "/" = "Search"
25 |
26 | [section."Nouns (Motions)"]
27 | "Ctrl + d / u" = "Half page down / up"
28 | "H / M / L" = "Top / middle / bottom of screen"
29 | "0 / ^ / $" = "Line start / first non-blank / end"
30 | "* / #" = "Next / previous token under cursor"
31 | "b / B" = "Beginning of word / token"
32 | "w / W" = "Next word / token"
33 | "e / E" = "End of word / token"
34 | "} / {" = "Next paragraph / previous paragraph"
35 | "%" = "Jump to matching bracket"
36 | "{mod} + p" = "Paragraph"
37 | "{mod} + s" = "Sentence"
38 | "{mod} + b" = "Block"
39 | "{mod} + t" = "XML Tag"
40 | "{mod} + \"" = "Quoted string"
41 | "{mod} + ( / )" = "Paired brackets"
42 |
43 | [section.Search]
44 | "\\/ / ?" = "Search forward / backwards"
45 | "n / N" = "Next / previous occurance"
46 | "* / #" = "Search next / previous word under cursor"
47 |
48 | [section.Navigating]
49 | gd = "Go to definition"
50 | gf = "Go to file in import"
51 | "gg / G" = "Go to top/bottom of file"
52 | 5G = "Go to line 5"
53 | "+" = "Go to first char of next line"
54 | "ma / `a" = "Mark a / jump to a"
55 |
56 | [section.Editing]
57 | "i / I" = "Insert before cursor / line"
58 | "a / A" = "Append after cursor / line"
59 | "o / O" = "Open new line below / above"
60 | "r / R" = "Replace char / and insert"
61 | "s / S" = "Substitute char / line"
62 | J = "Join lines"
63 | "x / X" = "Exterminate char under / before cursor"
64 | "u / Ctrl + r" = "Undo / redo"
65 |
66 | [section."Useful commands"]
67 | d5j = "Delete 5 lines downwards"
68 | "df\"" = "Delete in line until \""
69 | "dt\"" = "Delete in line until before \""
70 | ea = "Append to end of word"
71 | "d/foo" = "Delete from cursor until foo"
72 | ciw = "Change inner word"
73 | "gu / gU" = "Lowercase / uppercase"
74 | "Ctrl + v & j / k" = "Visual block select"
75 | "I & # & Esc" = "Comment selected visual block"
76 | "\"ayi\"" = "Yank inner \" to register a"
77 | "vi\"\"ap" = "Replace inner \" from register a"
78 | "V\"0p" = "Replace line with register 0 (w/o replacing R0)"
79 |
80 | [section.Registers]
81 | "`\"ay`" = "yank to regesiter a"
82 | "`\"ap`" = "paste from register a"
83 | "`\"+y`" = "yank to system clipboard"
84 | "`\"+p`" = "paste from system clipboard"
85 |
--------------------------------------------------------------------------------
/keyhint/config/vscode-copilot.toml:
--------------------------------------------------------------------------------
1 | id = "vscode-copilot"
2 | url = "https://docs.github.com/en/copilot/configuring-github-copilot/configuring-github-copilot-in-your-environment"
3 | hidden = true
4 |
5 | [match]
6 | regex_wmclass = "^$"
7 | regex_title = "^$"
8 |
9 | [section]
10 |
11 | [section.Suggestions]
12 | "Tab" = "Accept all"
13 | "Ctrl + r Right" = "Accept next word"
14 | "Alt + ] / [ " = "Show next / previous"
15 | "Alt + \\" = "Trigger suggestion"
16 | "Ctrl + Enter" = "Open 10 suggestions"
17 | "Esc" = "Dismiss"
18 |
19 | [section.Chat]
20 | "Ctrl + Alt + i" = "Open chat in sidebar"
21 | "Ctrl + i" = "Open inline chat"
22 | "@" = "Add experts a.k.a. 'participants'"
23 | "#" = "Add context"
24 | "/new" = "New conversation"
25 | "/fix" = "Propose fix for problem in selected code"
26 | "/clear" = "Clear conversation"
27 | "/delete" = "Delete conversation"
28 | "/help" = "GitHub Copilot quick reference"
29 |
--------------------------------------------------------------------------------
/keyhint/config/vscode-neovim.toml:
--------------------------------------------------------------------------------
1 | id = "vscode-neovim"
2 | url = ""
3 | hidden = true
4 |
5 | [match]
6 | regex_wmclass = "^$"
7 | regex_title = "^$"
8 |
9 | [section]
10 | [section.General]
11 | "ma / mA" = "Multicursor after cursor / line"
12 | "mi / mI" = "Multicursor before cursor / line"
13 | "K" = "Show hover, repeat to focus hover"
14 | "gd" = "Goto definition"
15 | "gf" = "Goto declaration"
16 | "gH" = "Reference Search"
17 | "Ctrl + n / p" = "Next / previous list item"
18 |
--------------------------------------------------------------------------------
/keyhint/config/vscode.toml:
--------------------------------------------------------------------------------
1 | id = "vscode"
2 | url = "https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf"
3 |
4 | [match]
5 | regex_wmclass = "code"
6 | regex_title = ".*"
7 |
8 | [section]
9 | [section."Navigating UI"]
10 | "Ctrl + 0" = "Focus sidebar"
11 | "Ctrl + 1" = "Focus editor 1"
12 | "Ctrl + 2" = "Focus editor 2"
13 |
14 | [section.Sidebar]
15 | "Ctrl + B" = "Toggle sidebar"
16 | "Ctrl + Shift + E" = "Explorer in sidebar"
17 | "Ctrl + Shift + G" = "Git in sidebar"
18 | "Ctrl + Shift + D" = "Debug in sidebar"
19 |
20 | [section.Panel]
21 | "Ctrl + J" = "Toggle panel"
22 | "Ctrl + `" = "Toggle terminal"
23 | "Ctrl + Shift + M" = "Toggle problems"
24 | "Ctrl + Shift + Y" = "Toggle debug console"
25 |
26 | [section.View]
27 | "Ctrl + K + Z" = "Zen mode"
28 |
29 | [section."Basic Editing"]
30 | "Ctrl + L" = "Select current line"
31 | "Ctrl + Backspace" = "Delete last word"
32 | "Alt + Up Down" = "Move line up/down"
33 | "Alt + Shift + Up Down" = "Duplicate cursor"
34 | "Ctrl+ Alt + Shift + Up Down" = "Duplicate line"
35 | "Shift + Alt + Left Right" = "Expand/shrink selection"
36 | "Ctrl + H" = "Search and replace"
37 | D = "Delete from cursor till end of line"
38 |
39 | [section."Language Editing"]
40 | "Ctrl + Space" = "Trigger suggestion"
41 | "Ctrl + Shift + Space" = "Trigger parameter suggestion"
42 | "Ctrl + K & I" = "Show hover docs"
43 | F12 = "Go to definition"
44 | "Ctrl + K & F12" = "Open definition to the side"
45 | "Ctrl + Shift + F10" = "Peek definition"
46 | F2 = "Rename symbol"
47 | "Ctrl + K & C" = "Add line comment(s)"
48 | "Ctrl + K & U" = "Remove line comment(s)"
49 |
50 | [section.Navigation]
51 | "Ctrl + G" = "Go to line..."
52 | "Ctrl + P" = "Go to file..."
53 | "Ctrl + T" = "Go to any symbol..."
54 | "Ctrl + Shift + O" = "Go to symbol in current file..."
55 | "Ctrl + Alt + -" = "Go back"
56 | "Ctrl + Shift + -" = "Go forward"
57 | F8 = "Go to next error"
58 | "Shift + F8" = "Go to previous error"
59 |
60 | [section.Debugging]
61 | F9 = "Toogle breakpoint"
62 | F5 = "Start/continue"
63 | "Shift + F5" = "Stop"
64 | F11 = "Step into"
65 | "Shift + F11" = "Step out"
66 | F10 = "Step over"
67 |
68 | [section."File/Tab Management"]
69 | "Ctrl + Tab" = "Switch between tabs"
70 | "Ctrl + P" = "Quick open file"
71 |
72 | [section.Commands]
73 | "Ctrl + Shift + P" = "Open command palette"
74 | "Ctrl + ; & A" = "Run all tests"
75 | "Ctrl + ; & F" = "Run tests in current file"
76 | "Ctrl + ; & C" = "Run test at cursor"
77 | "Ctrl + ; & Ctrl + C" = "Debug test at cursor"
78 |
--------------------------------------------------------------------------------
/keyhint/context.py:
--------------------------------------------------------------------------------
1 | """Functions to provide info about the context in which keyhint was started."""
2 |
3 | import json
4 | import logging
5 | import os
6 | import re
7 | import shutil
8 | import subprocess
9 | import tempfile
10 | import textwrap
11 | from datetime import datetime
12 | from functools import cache
13 |
14 | logger = logging.getLogger("keyhint")
15 |
16 |
17 | def is_using_wayland() -> bool:
18 | """Check if we are running on Wayland DE.
19 |
20 | Returns:
21 | [bool] -- {True} if probably Wayland
22 | """
23 | return "WAYLAND_DISPLAY" in os.environ
24 |
25 |
26 | def has_xprop() -> bool:
27 | """Check if xprop is installed.
28 |
29 | Returns:
30 | [bool] -- {True} if xprop is installed
31 | """
32 | return shutil.which("xprop") is not None
33 |
34 |
35 | def get_gnome_version() -> str:
36 | """Detect Gnome version of current session.
37 |
38 | Returns:
39 | Version string or '(n/a)'.
40 | """
41 | if not shutil.which("gnome-shell"):
42 | return "(n/a)"
43 |
44 | try:
45 | output = subprocess.check_output( # noqa: S603
46 | ["gnome-shell", "--version"], # noqa: S607
47 | shell=False,
48 | text=True,
49 | )
50 | if result := re.search(r"\s+([\d\.]+)", output.strip()):
51 | gnome_version = result.groups()[0]
52 | except Exception as e:
53 | logger.warning("Exception when trying to get gnome version from cli %s", e)
54 | return "(n/a)"
55 | else:
56 | return gnome_version
57 |
58 |
59 | def is_flatpak_package() -> bool:
60 | """Check if the application is running inside a flatpak package."""
61 | return os.getenv("FLATPAK_ID") is not None
62 |
63 |
64 | def get_kde_version() -> str:
65 | """Detect KDE platform version of current session.
66 |
67 | Returns:
68 | Version string or '(n/a)'.
69 | """
70 | if not shutil.which("plasmashell"):
71 | return "(n/a)"
72 |
73 | try:
74 | output = subprocess.check_output( # noqa: S603
75 | ["plasmashell", "--version"], # noqa: S607
76 | shell=False,
77 | text=True,
78 | )
79 | if result := re.search(r"([\d+\.]+)", output.strip()):
80 | kde_version = result.groups()[0]
81 | except Exception as e:
82 | logger.warning("Exception when trying to get kde version from cli %s", e)
83 | return "(n/a)"
84 | else:
85 | return kde_version
86 |
87 |
88 | @cache
89 | def get_desktop_environment() -> str:
90 | """Detect used desktop environment."""
91 | kde_full_session = os.environ.get("KDE_FULL_SESSION", "").lower()
92 | xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
93 | desktop_session = os.environ.get("DESKTOP_SESSION", "").lower()
94 | gnome_desktop_session_id = os.environ.get("GNOME_DESKTOP_SESSION_ID", "")
95 | hyprland_instance_signature = os.environ.get("HYPRLAND_INSTANCE_SIGNATURE", "")
96 |
97 | if gnome_desktop_session_id == "this-is-deprecated":
98 | gnome_desktop_session_id = ""
99 |
100 | de = "(DE not detected)"
101 |
102 | if gnome_desktop_session_id or "gnome" in xdg_current_desktop:
103 | de = "Gnome"
104 | if kde_full_session or "kde-plasma" in desktop_session:
105 | de = "KDE"
106 | if "sway" in xdg_current_desktop or "sway" in desktop_session:
107 | de = "Sway"
108 | if "unity" in xdg_current_desktop:
109 | de = "Unity"
110 | if hyprland_instance_signature:
111 | de = "Hyprland"
112 | if "awesome" in xdg_current_desktop:
113 | de = "Awesome"
114 |
115 | return de
116 |
117 |
118 | def has_window_calls_extension() -> bool:
119 | cmd_introspect = (
120 | "gdbus introspect --session --dest org.gnome.Shell "
121 | "--object-path /org/gnome/Shell/Extensions/Windows "
122 | )
123 | stdout_bytes = subprocess.check_output(cmd_introspect, shell=True) # noqa: S602
124 | stdout = stdout_bytes.decode("utf-8")
125 | return all(["List" in stdout, "GetTitle" in stdout])
126 |
127 |
128 | def get_active_window_via_window_calls() -> tuple[str, str]:
129 | """Retrieve active window class and active window title on Gnome + Wayland.
130 |
131 | Inspired by https://gist.github.com/rbreaves/257c3edfa301786e66e964d7ac036269
132 |
133 | Returns:
134 | Tuple(str, str): window class, window title
135 | """
136 |
137 | def _get_cmd_result(cmd: str) -> str:
138 | stdout_bytes: bytes = subprocess.check_output(cmd, shell=True) # noqa: S602
139 | return stdout_bytes.decode("utf-8").lstrip("('\n ").rstrip("),' \n")
140 |
141 | cmd_windows_list = (
142 | "gdbus call --session --dest org.gnome.Shell "
143 | "--object-path /org/gnome/Shell/Extensions/Windows "
144 | "--method org.gnome.Shell.Extensions.Windows.List"
145 | )
146 | stdout = _get_cmd_result(cmd_windows_list)
147 | windows = json.loads(stdout)
148 |
149 | focused_windows = list(filter(lambda x: x["focus"], windows))
150 | if not focused_windows:
151 | return "", ""
152 |
153 | focused_window = focused_windows[0]
154 | wm_class = focused_window["wm_class"]
155 |
156 | if "title" in focused_window:
157 | title = focused_window["title"]
158 | else:
159 | # Older versions of window calls doesn't expose the title in the List call,
160 | # therefor we need to do a second:
161 | cmd_windows_get_title = (
162 | "gdbus call --session --dest org.gnome.Shell "
163 | "--object-path /org/gnome/Shell/Extensions/Windows "
164 | "--method org.gnome.Shell.Extensions.Windows.GetTitle "
165 | f"{focused_window['id']}"
166 | )
167 | title = _get_cmd_result(cmd_windows_get_title)
168 |
169 | return wm_class, title
170 |
171 |
172 | def get_active_window_via_kwin() -> tuple[str, str]:
173 | """Retrieve active window class and active window title on KDE + Wayland.
174 |
175 | Returns:
176 | Tuple(str, str): window class, window title
177 | """
178 | kwin_script = textwrap.dedent("""
179 | console.info("keyhint test");
180 | client = workspace.activeClient;
181 | title = client.caption;
182 | wm_class = client.resourceClass;
183 | console.info(`keyhint_out: wm_class=${wm_class}, window_title=${title}`);
184 | """)
185 |
186 | with tempfile.NamedTemporaryFile(suffix=".js", delete=False) as fh:
187 | fh.write(kwin_script.encode())
188 | cmd_load = (
189 | "gdbus call --session --dest org.kde.KWin "
190 | "--object-path /Scripting "
191 | f"--method org.kde.kwin.Scripting.loadScript '{fh.name}'"
192 | )
193 | logger.debug("cmd_load: %s", cmd_load)
194 | stdout = subprocess.check_output(cmd_load, shell=True).decode() # noqa: S602
195 |
196 | logger.debug("loadScript output: %s", stdout)
197 | script_id = stdout.strip().strip("()").split(",")[0]
198 |
199 | since = str(datetime.now())
200 |
201 | cmd_run = (
202 | "gdbus call --session --dest org.kde.KWin "
203 | f"--object-path /{script_id} "
204 | "--method org.kde.kwin.Script.run"
205 | )
206 | subprocess.check_output(cmd_run, shell=True) # noqa: S602
207 |
208 | cmd_unload = (
209 | "gdbus call --session --dest org.kde.KWin "
210 | "--object-path /Scripting "
211 | f"--method org.kde.kwin.Scripting.unloadScript {script_id}"
212 | )
213 | subprocess.check_output(cmd_unload, shell=True) # noqa: S602
214 |
215 | # Unfortunately, we can read script output from stdout, because of a KDE bug:
216 | # https://bugs.kde.org/show_bug.cgi?id=445058
217 | # The output has to be read through journalctl instead. A timestamp for
218 | # filtering speeds up the process.
219 | log_lines = (
220 | subprocess.check_output( # noqa: S602
221 | f'journalctl --user -o cat --since "{since}"',
222 | shell=True,
223 | )
224 | .decode()
225 | .split("\n")
226 | )
227 | logger.debug("Journal message: %s", log_lines)
228 | result_line = [m for m in log_lines if "keyhint_out" in m][-1]
229 | match = re.search(r"keyhint_out: wm_class=(.+), window_title=(.+)", result_line)
230 | if match:
231 | wm_class = match.group(1)
232 | title = match.group(2)
233 | else:
234 | logger.warning("Could not extract window info from KWin log!")
235 | wm_class = title = ""
236 |
237 | return wm_class, title
238 |
239 |
240 | def get_active_window_via_xprop() -> tuple[str, str]:
241 | """Retrieve active window class and active window title on Xorg desktops.
242 |
243 | Returns:
244 | Tuple(str, str): window class, window title
245 | """
246 | # Query id of active window
247 | stdout_bytes: bytes = subprocess.check_output( # noqa: S602
248 | "xprop -root _NET_ACTIVE_WINDOW", # noqa: S607
249 | shell=True,
250 | )
251 | stdout = stdout_bytes.decode()
252 |
253 | # Identify id of active window in output
254 | match = re.search(r"^_NET_ACTIVE_WINDOW.* ([\w]+)$", stdout)
255 | if match is None:
256 | # Stop, if there is not active window detected
257 | return "", ""
258 | window_id: str = match.group(1)
259 |
260 | # Query app_title and app_process
261 | stdout_bytes = subprocess.check_output( # noqa: S602
262 | f"xprop -id {window_id} WM_NAME WM_CLASS",
263 | shell=True,
264 | )
265 | stdout = stdout_bytes.decode()
266 |
267 | # Extract app_title and app_process from output
268 | title = wm_class = ""
269 |
270 | match = re.search(r'WM_NAME\(\w+\) = "(?P.+)"', stdout)
271 | if match is not None:
272 | title = match.group("name")
273 |
274 | match = re.search(r'WM_CLASS\(\w+\) =.*"(?P.+?)"$', stdout)
275 | if match is not None:
276 | wm_class = match.group("class")
277 |
278 | return wm_class, title
279 |
--------------------------------------------------------------------------------
/keyhint/css.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from gi.repository import Gdk, Gtk
4 |
5 |
6 | def new_provider(display: Gdk.Display, css_file: Path | None = None) -> Gtk.CssProvider:
7 | """Create a new css provider which applies globally.
8 |
9 | Args:
10 | display: Target Gtk.Display.
11 | css_file: Path to file with css rules to be loaded. If None, an empty css
12 | provider is created.
13 |
14 | Returns:
15 | css provider which rules apply to the whole application.
16 | """
17 | provider = Gtk.CssProvider()
18 | Gtk.StyleContext().add_provider_for_display(
19 | display, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER
20 | )
21 | if css_file and css_file.exists():
22 | provider.load_from_path(str(css_file.resolve()))
23 | return provider
24 |
--------------------------------------------------------------------------------
/keyhint/headerbar.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterator
2 | from pathlib import Path
3 | from typing import cast
4 |
5 | from gi.repository import Gtk
6 |
7 | RESOURCE_PATH = Path(__file__).parent / "resources"
8 |
9 |
10 | @Gtk.Template(filename=f"{RESOURCE_PATH}/headerbar.ui")
11 | class HeaderBarBox(Gtk.HeaderBar):
12 | __gtype_name__ = "headerbar"
13 |
14 | sheet_dropdown = cast(Gtk.DropDown, Gtk.Template.Child())
15 | search_entry = cast(Gtk.SearchEntry, Gtk.Template.Child())
16 | fullscreen_button = cast(Gtk.ToggleButton, Gtk.Template.Child())
17 | zoom_scale = cast(Gtk.Scale, Gtk.Template.Child())
18 | fallback_sheet_entry = cast(Gtk.Entry, Gtk.Template.Child())
19 | fallback_sheet_button = cast(Gtk.Button, Gtk.Template.Child())
20 |
21 | def __init__(self, for_fullscreen: bool = False) -> None:
22 | super().__init__()
23 | if for_fullscreen:
24 | self.set_decoration_layout(":minimize,close")
25 | self.fullscreen_button.set_icon_name("view-restore-symbolic")
26 | self.set_visible(False)
27 | else:
28 | self.set_visible(True)
29 |
30 |
31 | class HeaderBars:
32 | """Utility class for easier accessing the two header bars.
33 |
34 | The 'normal' header bar is used as application title bar. It is shown in the normal
35 | window mode, and automatically hidden in fullscreen mode (by design of GTK).
36 |
37 | The 'fullscreen' header bar is added as a widget to the window content. Its
38 | visibility needs to be toggled depending on window state.
39 | """
40 |
41 | normal = HeaderBarBox()
42 | fullscreen = HeaderBarBox(for_fullscreen=True)
43 |
44 | def __iter__(self) -> Iterator[HeaderBarBox]:
45 | yield from (self.normal, self.fullscreen)
46 |
--------------------------------------------------------------------------------
/keyhint/resources/headerbar.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
232 |
233 |
267 |
268 |
--------------------------------------------------------------------------------
/keyhint/resources/keyhint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dynobo/keyhint/5ac38c3195b197a182d25ccdd7066baa1c293321/keyhint/resources/keyhint.png
--------------------------------------------------------------------------------
/keyhint/resources/keyhint_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dynobo/keyhint/5ac38c3195b197a182d25ccdd7066baa1c293321/keyhint/resources/keyhint_128.png
--------------------------------------------------------------------------------
/keyhint/resources/keyhint_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dynobo/keyhint/5ac38c3195b197a182d25ccdd7066baa1c293321/keyhint/resources/keyhint_32.png
--------------------------------------------------------------------------------
/keyhint/resources/keyhint_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dynobo/keyhint/5ac38c3195b197a182d25ccdd7066baa1c293321/keyhint/resources/keyhint_48.png
--------------------------------------------------------------------------------
/keyhint/resources/keyhint_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dynobo/keyhint/5ac38c3195b197a182d25ccdd7066baa1c293321/keyhint/resources/keyhint_64.png
--------------------------------------------------------------------------------
/keyhint/resources/keyhint_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
87 |
--------------------------------------------------------------------------------
/keyhint/resources/style.css:
--------------------------------------------------------------------------------
1 | @define-color bg_color rgba(0,0,0,0.2);
2 | @define-color borders_color rgba(0,0,0,0.5);
3 | @define-color shadow_color rgba(0,0,0,0.3);
4 | @define-color accent_color #FF2E88;
5 | @define-color banner_bg_color rgba(42, 123, 222, 0.3);
6 |
7 | .keycap {
8 | min-width: 15px;
9 | min-height: 25px;
10 | margin-top: 2px;
11 | padding-bottom: 3px;
12 | padding-left: 5px;
13 | padding-right: 5px;
14 | background-color: @bg_color;
15 | border: 1px solid;
16 | border-radius: 5px;
17 | border-color: @borders_color;
18 | box-shadow: inset 0 -3px @shadow_color;
19 | }
20 |
21 | .bindings-section header label {
22 | color: @accent_color;
23 | margin-top: 26px;
24 | filter: unset;
25 | }
26 |
27 | .popover_menu_box > list > row {
28 | border-radius: 10px;
29 | }
30 |
31 | .popover_menu_box .setting_label {
32 | font-size: 85%;
33 | opacity: 0.8;
34 | }
35 |
36 | .popover_menu_box .menu_entry {
37 | font-weight: normal;
38 | }
39 |
40 | .transparent,
41 | .bindings-section,
42 | .bindings-section .view {
43 | background-color: transparent;
44 | }
45 |
46 | .banner {
47 | background-color: @banner_bg_color;
48 | font-weight: bold;
49 | }
50 |
51 | .dim-label {
52 | margin: 0px -2px;
53 | }
54 |
--------------------------------------------------------------------------------
/keyhint/resources/window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 300
8 | 800
9 | KeyHint
11 |
12 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/keyhint/sheets.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import re
4 | import tomllib
5 | from copy import deepcopy
6 | from pathlib import Path
7 | from typing import Any
8 |
9 | from keyhint import config
10 |
11 | logger = logging.getLogger("keyhint")
12 |
13 |
14 | def _load_toml(file_path: str | os.PathLike) -> dict:
15 | """Load a toml file from resource path or other path.
16 |
17 | Args:
18 | file_path: Filename in resources, or complete path to file.
19 | from_resources: Set to true to load from resource. Defaults
20 | to False.
21 |
22 | Returns:
23 | [description]
24 | """
25 | try:
26 | with Path(file_path).open("rb") as fh:
27 | result = tomllib.load(fh)
28 | except Exception as exc:
29 | logger.warning("Could not loading toml file %s: %s", file_path, exc)
30 | result = {}
31 |
32 | return result
33 |
34 |
35 | def load_default_sheets() -> list[dict]:
36 | """Load default keyhints from toml files shipped with the package.
37 |
38 | Returns:
39 | List[dict]: List of application keyhints and meta info.
40 | """
41 | default_sheet_path = Path(__file__).parent / "config"
42 | sheets = [_load_toml(f) for f in default_sheet_path.glob("*.toml")]
43 | logger.debug("Found %s default sheets.", len(sheets))
44 | return sorted(sheets, key=lambda k: k["id"])
45 |
46 |
47 | def load_user_sheets() -> list[dict]:
48 | """Load cheatsheets from toml files in the users .config/keyhint/ directory.
49 |
50 | Returns:
51 | List[dict]: List of application keyhints and meta info.
52 | """
53 | files = config.CONFIG_PATH.glob("*.toml")
54 | sheets = [_load_toml(f) for f in files]
55 | logger.debug("Found %s user sheets in %s/.", len(sheets), config.CONFIG_PATH)
56 | return sorted(sheets, key=lambda k: k["id"])
57 |
58 |
59 | def _expand_includes(sheets: list[dict]) -> list[dict]:
60 | new_sheets = []
61 | for s in sheets:
62 | for include in s.get("include", []):
63 | included_sheets = [c for c in sheets if c["id"] == include]
64 | if not included_sheets:
65 | message = f"Sheet '{include}' included by '{s['id']}' not found!"
66 | raise ValueError(message)
67 | included_sheet = deepcopy(included_sheets[0])
68 | included_sheet["section"] = {
69 | f"[{included_sheet['id']}] {k}": v
70 | for k, v in included_sheet["section"].items()
71 | }
72 | s["section"].update(included_sheet["section"])
73 | new_sheets.append(s)
74 | return new_sheets
75 |
76 |
77 | def _remove_empty_sections(sheets: list[dict]) -> list[dict]:
78 | for sheet in sheets:
79 | sheet["section"] = {k: v for k, v in sheet["section"].items() if v}
80 | return sheets
81 |
82 |
83 | def _remove_hidden(sheets: list[dict]) -> list[dict]:
84 | return [s for s in sheets if not s.get("hidden", False)]
85 |
86 |
87 | def _update_or_append(sheets: list[dict], new_sheet: dict) -> list[dict]:
88 | for sheet in sheets:
89 | if sheet["id"] == new_sheet["id"]:
90 | # Update existing default sheet by user sheet
91 | sheet["section"].update(new_sheet.pop("section", {}))
92 | sheet["match"].update(new_sheet.pop("match", {}))
93 | sheet.update(new_sheet)
94 | break
95 | else:
96 | # If default sheet didn't exist, append as new
97 | sheets.append(new_sheet)
98 | return sheets
99 |
100 |
101 | def load_sheets() -> list[dict]:
102 | """Load unified default keyhints and keyhints from user config.
103 |
104 | First the default keyhints are loaded, then they are update (added/overwritten)
105 | by the keyhints loaded from user config.
106 |
107 | Returns:
108 | List[dict]: List of application keyhints and meta info.
109 | """
110 | sheets = load_default_sheets()
111 | user_sheets = load_user_sheets()
112 |
113 | for user_sheet in user_sheets:
114 | sheets = _update_or_append(sheets, user_sheet)
115 |
116 | sheets = _expand_includes(sheets)
117 | sheets = _remove_hidden(sheets)
118 | sheets = _remove_empty_sections(sheets)
119 | logger.debug("Loaded %s sheets.", len(sheets))
120 | return sheets
121 |
122 |
123 | def get_sheet_by_id(sheets: list[dict], sheet_id: str) -> dict[str, Any]:
124 | return next(sheet for sheet in sheets if sheet["id"] == sheet_id)
125 |
126 |
127 | def get_sheet_id_by_active_window(
128 | sheets: list[dict], wm_class: str, window_title: str
129 | ) -> str | None:
130 | matching_sheets = [
131 | h
132 | for h in sheets
133 | if re.search(h["match"]["regex_wmclass"], wm_class, re.IGNORECASE)
134 | and re.search(h["match"]["regex_title"], window_title, re.IGNORECASE)
135 | ]
136 |
137 | if not matching_sheets:
138 | return None
139 |
140 | # First sort by secondary criterion
141 | matching_sheets.sort(key=lambda h: len(h["match"]["regex_title"]), reverse=True)
142 |
143 | # Then sort by primary criterion
144 | matching_sheets.sort(key=lambda h: len(h["match"]["regex_wmclass"]), reverse=True)
145 |
146 | # First element is (hopefully) the best fitting sheet id
147 | return matching_sheets[0]["id"]
148 |
149 |
150 | if __name__ == "__main__":
151 | load_sheets()
152 |
--------------------------------------------------------------------------------
/keyhint/window.py:
--------------------------------------------------------------------------------
1 | """Logic handler used by the application window.
2 |
3 | Does the rendering of the cheatsheets as well interface actions.
4 |
5 | Naming Hierarchy:
6 | 1. Keyhint works with multiple cheatsheets, in short "Sheets".
7 | 2. A single "Sheet" corresponds to one application and is rendered as one page in UI.
8 | The sheet to render can be selected in the dropdown field. Each sheet has a
9 | "Sheet ID" which must be unique.
10 | 3. A "Sheet" consists of multiple "Sections", which group together shortcuts or commands
11 | into a blocks. Each section has a section title.
12 | 4. A "Section" consists of multiple "Bindings"
13 | 5. A "Binding" consists of a "Shortcut", which contains the key combination or command,
14 | and a "Label" which describes the combination/command.
15 |
16 | TODO: Create Flatpak
17 | """
18 |
19 | import logging
20 | import platform
21 | import subprocess
22 | import textwrap
23 | from collections.abc import Callable
24 | from pathlib import Path
25 | from typing import Literal, TypeVar, cast
26 |
27 | from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk, Pango
28 |
29 | from keyhint import __version__, binding, config, context, css, headerbar, sheets
30 |
31 | logger = logging.getLogger("keyhint")
32 |
33 | RESOURCE_PATH = Path(__file__).parent / "resources"
34 |
35 | TKeyhintWindow = TypeVar("TKeyhintWindow", bound="KeyhintWindow")
36 | TActionCallback = Callable[[TKeyhintWindow, Gio.SimpleAction, GLib.Variant], None]
37 |
38 |
39 | def check_state(func: TActionCallback) -> TActionCallback:
40 | """Decorator to only execute a function if the action state really changed."""
41 |
42 | def wrapper(cls: Gtk.Widget, action: Gio.SimpleAction, state: GLib.Variant) -> None:
43 | if action.get_state_type() and action.get_state() == state:
44 | return None
45 |
46 | if state:
47 | action.set_state(state)
48 |
49 | return func(cls, action, state)
50 |
51 | return wrapper
52 |
53 |
54 | @Gtk.Template(filename=f"{RESOURCE_PATH}/window.ui")
55 | class KeyhintWindow(Gtk.ApplicationWindow):
56 | """The main UI for Keyhint."""
57 |
58 | __gtype_name__ = "main_window"
59 |
60 | overlay = cast(Adw.ToastOverlay, Gtk.Template.Child())
61 | banner_window_calls = cast(Gtk.Revealer, Gtk.Template.Child())
62 | banner_xprop = cast(Gtk.Revealer, Gtk.Template.Child())
63 | scrolled_window = cast(Gtk.ScrolledWindow, Gtk.Template.Child())
64 | container = cast(Gtk.Box, Gtk.Template.Child())
65 | sheet_container_box = cast(Gtk.FlowBox, Gtk.Template.Child())
66 |
67 | shortcut_column_factory = Gtk.SignalListItemFactory()
68 | label_column_factory = Gtk.SignalListItemFactory()
69 |
70 | headerbars = headerbar.HeaderBars()
71 |
72 | max_shortcut_width = 0
73 |
74 | def __init__(self, cli_args: dict) -> None:
75 | super().__init__()
76 |
77 | self.cli_args = cli_args
78 | self.config = config.load()
79 | self.sheets = sheets.load_sheets()
80 | self.wm_class, self.window_title = self.init_last_active_window_info()
81 |
82 | self.skip_search_changed: bool = False
83 | self.search_text: str = ""
84 |
85 | self.css_provider = css.new_provider(
86 | display=self.get_display(), css_file=RESOURCE_PATH / "style.css"
87 | )
88 | self.zoom_css_provider = css.new_provider(display=self.get_display())
89 |
90 | self.set_titlebar(self.headerbars.normal)
91 | self.container.prepend(self.headerbars.fullscreen)
92 |
93 | self.bindings_filter = Gtk.CustomFilter.new(match_func=self.bindings_match_func)
94 | self.sheet_container_box.set_filter_func(filter_func=self.sections_filter_func)
95 | self.sheet_container_box.set_sort_func(sort_func=self.sections_sort_func)
96 |
97 | self.shortcut_column_factory.connect("bind", self.bind_shortcuts_callback)
98 | self.label_column_factory.connect("bind", self.bind_labels_callback)
99 |
100 | self.init_sheet_dropdown()
101 | self.init_action_sheet()
102 | self.init_action_fullscreen()
103 | self.init_action_sort_by()
104 | self.init_action_zoom()
105 | self.init_action_orientation()
106 | self.init_action_fallback_sheet()
107 | self.init_actions_for_menu_entries()
108 | self.init_actions_for_toasts()
109 | self.init_actions_for_banners()
110 | self.init_search_entry()
111 | self.init_key_event_controllers()
112 |
113 | self.focus_search_entry()
114 |
115 | def init_last_active_window_info(self) -> tuple[str, str]:
116 | """Get class and title of active window.
117 |
118 | Identify the OS and display server and pick the method accordingly.
119 |
120 | Returns:
121 | Tuple[str, str]: wm_class, window title
122 | """
123 | wm_class = wm_title = ""
124 |
125 | on_wayland = context.is_using_wayland()
126 | desktop_environment = context.get_desktop_environment().lower()
127 |
128 | match (on_wayland, desktop_environment):
129 | case True, "gnome":
130 | if context.has_window_calls_extension():
131 | wm_class, wm_title = context.get_active_window_via_window_calls()
132 | else:
133 | self.banner_window_calls.set_reveal_child(True)
134 | logger.error("Window Calls extension not found!")
135 | case True, "kde":
136 | wm_class, wm_title = context.get_active_window_via_kwin()
137 | case False, _:
138 | if context.has_xprop():
139 | wm_class, wm_title = context.get_active_window_via_xprop()
140 | else:
141 | self.banner_xprop.set_reveal_child(True)
142 | logger.error("xprop not found!")
143 |
144 | logger.debug("Detected wm_class: '%s'.", wm_class)
145 | logger.debug("Detected window_title: '%s'.", wm_title)
146 |
147 | if "" in [wm_class, wm_title]:
148 | logger.warning("Couldn't detect active window!")
149 |
150 | return wm_class, wm_title
151 |
152 | def init_action_sort_by(self) -> None:
153 | action = Gio.SimpleAction.new_stateful(
154 | name="sort_by",
155 | state=GLib.Variant("s", ""),
156 | parameter_type=GLib.VariantType.new("s"),
157 | )
158 | action.connect("activate", self.on_change_sort)
159 | action.connect("change-state", self.on_change_sort)
160 | self.add_action(action)
161 |
162 | # Connect to button happens via headerbar.ui
163 |
164 | self.change_action_state(
165 | "sort_by", GLib.Variant("s", self.config["main"].get("sort_by", "size"))
166 | )
167 |
168 | def init_action_zoom(self) -> None:
169 | action = Gio.SimpleAction.new_stateful(
170 | name="zoom",
171 | state=GLib.Variant("i", 0),
172 | parameter_type=GLib.VariantType.new("i"),
173 | )
174 | action.connect("change-state", self.on_change_zoom)
175 | self.add_action(action)
176 |
177 | for bar in self.headerbars:
178 | bar.zoom_scale.connect(
179 | "value-changed",
180 | lambda btn: self.change_action_state(
181 | "zoom", GLib.Variant("i", btn.get_value())
182 | ),
183 | )
184 | slider_range = bar.zoom_scale.get_adjustment()
185 | lower_bound = int(slider_range.get_lower())
186 | upper_bound = int(slider_range.get_upper())
187 | for i in range(lower_bound, upper_bound + 1, 25):
188 | bar.zoom_scale.add_mark(
189 | i, Gtk.PositionType.BOTTOM, f"{i}"
190 | )
191 |
192 | self.change_action_state(
193 | "zoom", GLib.Variant("i", self.config["main"].getint("zoom", 100))
194 | )
195 |
196 | def init_action_fullscreen(self) -> None:
197 | action = Gio.SimpleAction.new_stateful(
198 | name="fullscreen",
199 | state=GLib.Variant("b", False),
200 | parameter_type=None,
201 | )
202 | action.connect("activate", self.on_change_fullscreen)
203 | action.connect("change-state", self.on_change_fullscreen)
204 | self.add_action(action)
205 |
206 | # Connect to button happens via headerbar.ui
207 |
208 | self.connect("notify::fullscreened", self.on_fullscreen_state_changed)
209 |
210 | self.change_action_state(
211 | "fullscreen",
212 | GLib.Variant("b", self.config["main"].getboolean("fullscreen", False)),
213 | )
214 |
215 | def init_action_orientation(self) -> None:
216 | action = Gio.SimpleAction.new_stateful(
217 | name="orientation",
218 | state=GLib.Variant("s", ""),
219 | parameter_type=GLib.VariantType.new("s"),
220 | )
221 | action.connect("activate", self.on_change_orientation)
222 | action.connect("change-state", self.on_change_orientation)
223 | self.add_action(action)
224 |
225 | # Connect to button happens via headerbar.ui
226 |
227 | self.change_action_state(
228 | "orientation",
229 | GLib.Variant("s", self.config["main"].get("orientation", "vertical")),
230 | )
231 |
232 | def init_action_fallback_sheet(self) -> None:
233 | action = Gio.SimpleAction.new_stateful(
234 | name="fallback_sheet",
235 | state=GLib.Variant("s", ""),
236 | parameter_type=GLib.VariantType.new("s"),
237 | )
238 | action.connect("change-state", self.on_set_fallback_sheet)
239 | self.add_action(action)
240 |
241 | for bar in self.headerbars:
242 | bar.fallback_sheet_button.connect(
243 | "clicked",
244 | lambda *args: self.change_action_state(
245 | "fallback_sheet",
246 | GLib.Variant("s", self.get_current_sheet_id() or "keyhint"),
247 | ),
248 | )
249 |
250 | self.change_action_state(
251 | "fallback_sheet",
252 | GLib.Variant(
253 | "s", self.config["main"].get("fallback_cheatsheet", "keyhint")
254 | ),
255 | )
256 |
257 | def init_action_sheet(self) -> None:
258 | action = Gio.SimpleAction.new_stateful(
259 | name="sheet",
260 | state=GLib.Variant("s", ""),
261 | parameter_type=GLib.VariantType.new("s"),
262 | )
263 | action.connect("change-state", self.on_change_sheet)
264 | self.add_action(action)
265 |
266 | for bar in self.headerbars:
267 | bar.sheet_dropdown.connect(
268 | "notify::selected-item",
269 | lambda btn, param: self.change_action_state(
270 | "sheet", GLib.Variant("s", btn.get_selected_item().get_string())
271 | ),
272 | )
273 |
274 | self.change_action_state(
275 | "sheet", GLib.Variant("s", self.get_appropriate_sheet_id())
276 | )
277 |
278 | def init_search_entry(self) -> None:
279 | for bar in self.headerbars:
280 | self.search_changed_handler = bar.search_entry.connect(
281 | "search-changed", self.on_search_entry_changed
282 | )
283 |
284 | def init_actions_for_menu_entries(self) -> None:
285 | action = Gio.SimpleAction.new("about", None)
286 | action.connect("activate", self.on_about_action)
287 | self.add_action(action)
288 |
289 | action = Gio.SimpleAction.new("debug_info", None)
290 | action.connect("activate", self.on_debug_action)
291 | self.add_action(action)
292 |
293 | action = Gio.SimpleAction.new("open_folder", None)
294 | action.connect("activate", self.on_open_folder_action)
295 | self.add_action(action)
296 |
297 | def init_actions_for_toasts(self) -> None:
298 | """Register actions which are triggered from toast notifications."""
299 | action = Gio.SimpleAction.new("create_new_sheet", None)
300 | action.connect("activate", self.on_create_new_sheet)
301 | self.add_action(action)
302 |
303 | def init_actions_for_banners(self) -> None:
304 | """Register actions which are triggered from banners."""
305 | action = Gio.SimpleAction.new("visit_window_calls", None)
306 | action.connect(
307 | "activate",
308 | lambda *args: Gio.AppInfo.launch_default_for_uri(
309 | "https://extensions.gnome.org/extension/4724/window-calls/"
310 | ),
311 | )
312 | self.add_action(action)
313 |
314 | def init_key_event_controllers(self) -> None:
315 | """Register key press handlers."""
316 | evk = Gtk.EventControllerKey()
317 | evk.connect("key-pressed", self.on_key_pressed)
318 | self.add_controller(evk)
319 |
320 | evk = Gtk.EventControllerKey()
321 | evk.connect("key-pressed", self.on_search_entry_key_pressed)
322 | self.headerbars.normal.search_entry.add_controller(evk)
323 |
324 | # NOTE: Reusing the same evk would lead to critical assertion error!
325 | evk = Gtk.EventControllerKey()
326 | evk.connect("key-pressed", self.on_search_entry_key_pressed)
327 | self.headerbars.fullscreen.search_entry.add_controller(evk)
328 |
329 | def init_sheet_dropdown(self) -> None:
330 | """Populate sheet dropdown with available sheet IDs."""
331 | # Use the model from normal dropdown also for the fullscreen dropdown
332 | model = self.headerbars.normal.sheet_dropdown.get_model()
333 | self.headerbars.fullscreen.sheet_dropdown.set_model(model)
334 |
335 | # Satisfy type checker
336 | if not isinstance(model, Gtk.StringList):
337 | raise TypeError("Sheet dropdown model is not a Gtk.StringList.")
338 |
339 | for sheet_id in sorted([s["id"] for s in self.sheets]):
340 | model.append(sheet_id)
341 |
342 | @property
343 | def active_headerbar(self) -> headerbar.HeaderBarBox:
344 | """Return the currently active headerbar depending on window state."""
345 | return (
346 | self.headerbars.fullscreen
347 | if self.is_fullscreen()
348 | else self.headerbars.normal
349 | )
350 |
351 | @check_state
352 | def on_set_fallback_sheet(
353 | self, action: Gio.SimpleAction, state: GLib.Variant
354 | ) -> None:
355 | """Set the sheet to use in case no matching sheet is found."""
356 | sheet_id = state.get_string()
357 | self.config.set_persistent("main", "fallback_cheatsheet", sheet_id)
358 | for bar in self.headerbars:
359 | bar.fallback_sheet_entry.set_text(sheet_id)
360 |
361 | @check_state
362 | def on_change_sheet(self, action: Gio.SimpleAction, state: GLib.Variant) -> None:
363 | """Get selected sheet and render it in the UI."""
364 | dropdown_model = self.headerbars.normal.sheet_dropdown.get_model()
365 |
366 | # Satisfy type checker
367 | if not dropdown_model:
368 | raise TypeError("Sheet dropdown model is not a Gtk.StringList.")
369 |
370 | dropdown_strings = [
371 | s.get_string() for s in dropdown_model if isinstance(s, Gtk.StringObject)
372 | ]
373 |
374 | sheet_id = state.get_string()
375 | select_idx = dropdown_strings.index(sheet_id)
376 |
377 | for bar in self.headerbars:
378 | bar.sheet_dropdown.set_selected(select_idx)
379 |
380 | self.show_sheet(sheet_id=sheet_id)
381 |
382 | @check_state
383 | def on_change_zoom(self, action: Gio.SimpleAction, state: GLib.Variant) -> None:
384 | """Set the zoom level of the sheet container via css."""
385 | value = state.get_int32()
386 |
387 | for bar in self.headerbars:
388 | bar.zoom_scale.set_value(value)
389 |
390 | css_style = f"""
391 | .sheet_container_box,
392 | .sheet_container_box .bindings-section header label {{
393 | font-size: {value}%;
394 | }}
395 | """
396 |
397 | if hasattr(self.zoom_css_provider, "load_from_string"):
398 | # GTK 4.12+
399 | self.zoom_css_provider.load_from_string(css_style)
400 | else:
401 | # ONHOLD: Remove once GTK 4.12+ is required
402 | self.zoom_css_provider.load_from_data(css_style, len(css_style))
403 |
404 | self.config.set_persistent("main", "zoom", str(int(value)))
405 |
406 | @check_state
407 | def on_change_orientation(
408 | self, action: Gio.SimpleAction, state: GLib.Variant
409 | ) -> None:
410 | """Set the orientation or scroll direction of the sheet container."""
411 | # The used GTK orientation is the opposite of the config value, which follows
412 | # the naming from user perspective!
413 | value = state.get_string()
414 | gtk_orientation = (
415 | Gtk.Orientation.VERTICAL
416 | if value == "horizontal"
417 | else Gtk.Orientation.HORIZONTAL
418 | )
419 |
420 | self.sheet_container_box.set_orientation(gtk_orientation)
421 | self.config.set_persistent("main", "orientation", value)
422 |
423 | @check_state
424 | def on_change_sort(self, action: Gio.SimpleAction, state: GLib.Variant) -> None:
425 | """Set the order of the sections."""
426 | self.config.set_persistent("main", "sort_by", state.get_string())
427 | self.sheet_container_box.invalidate_sort()
428 |
429 | @check_state
430 | def on_change_fullscreen(
431 | self, action: Gio.SimpleAction, state: GLib.Variant
432 | ) -> None:
433 | """Set the fullscreen state."""
434 | if state is not None:
435 | to_fullscreen = bool(state)
436 | else:
437 | # If state is not provided, just toggle the action's state
438 | to_fullscreen = not bool(action.get_state())
439 | action.set_state(GLib.Variant("b", to_fullscreen))
440 |
441 | # Set flag to temporarily ignore search entry changes (to avoid recursion)
442 | self.skip_search_changed = True
443 |
444 | for bar in self.headerbars:
445 | bar.search_entry.set_text(self.search_text)
446 |
447 | if to_fullscreen:
448 | self.fullscreen()
449 | else:
450 | self.unfullscreen()
451 |
452 | self.config.set_persistent("main", "fullscreen", to_fullscreen)
453 |
454 | def scroll(self, to_start: bool, by_page: bool) -> None:
455 | """Scroll the sheet container by a certain distance."""
456 | if self.sheet_container_box.get_orientation() == 1:
457 | adj = self.scrolled_window.get_hadjustment()
458 | else:
459 | adj = self.scrolled_window.get_vadjustment()
460 |
461 | default_distance = 25
462 | distance = adj.get_page_size() if by_page else default_distance
463 | if to_start:
464 | distance *= -1
465 |
466 | adj.set_value(adj.get_value() + distance)
467 |
468 | def cycle_sheets(self, direction: Literal["next", "previous"]) -> None:
469 | dropdown = self.headerbars.normal.sheet_dropdown
470 | dropdown_model = dropdown.get_model()
471 |
472 | # Satisfy type checker
473 | if not dropdown_model:
474 | raise TypeError("Sheet dropdown model is not a Gtk.StringList.")
475 |
476 | relative_change = 1 if direction == "next" else -1
477 |
478 | new_position = dropdown.get_selected() + relative_change
479 | new_position = max(0, new_position)
480 | new_position = min(new_position, dropdown_model.get_n_items())
481 | dropdown.set_selected(position=new_position)
482 |
483 | def focus_search_entry(self) -> None:
484 | """Focus search entry of the active headerbar."""
485 | self.active_headerbar.search_entry.grab_focus()
486 | self.active_headerbar.search_entry.set_position(-1)
487 |
488 | def show_create_new_sheet_toast(self) -> None:
489 | """Display a toast notification to offer the creation of a new cheatsheet."""
490 | toast = Adw.Toast.new(f"No cheatsheet found for '{self.wm_class}'.")
491 | toast.set_button_label("Create new")
492 | toast.set_action_name("win.create_new_sheet")
493 | toast.set_timeout(5)
494 | self.overlay.add_toast(toast)
495 |
496 | def on_fullscreen_state_changed(self, _: Gtk.Widget, __: GObject.Parameter) -> None:
497 | """Toggle fullscreen header bar according to current window state."""
498 | if self.is_fullscreen():
499 | self.headerbars.fullscreen.set_visible(True)
500 | else:
501 | self.headerbars.fullscreen.set_visible(False)
502 | self.focus_search_entry()
503 |
504 | def on_search_entry_changed(self, search_entry: Gtk.SearchEntry) -> None:
505 | """Execute on change of the sheet selection dropdown."""
506 | if self.skip_search_changed:
507 | self.skip_search_changed = False
508 | return
509 |
510 | if search_entry.get_text() == self.search_text:
511 | return
512 |
513 | self.search_text = search_entry.get_text()
514 | self.bindings_filter.changed(Gtk.FilterChange.DIFFERENT)
515 | self.sheet_container_box.invalidate_filter()
516 |
517 | def on_search_entry_key_pressed(
518 | self,
519 | evk: Gtk.EventControllerKey,
520 | keycode: int,
521 | keyval: int,
522 | modifier: Gdk.ModifierType,
523 | ) -> None:
524 | """Handle key press events in the search entry field.
525 |
526 | Note: The search itself is triggered by the 'change' event, not by 'key-pressed'
527 | """
528 | if keycode == Gdk.KEY_Escape:
529 | self.close()
530 |
531 | def on_key_pressed( # noqa: C901
532 | self,
533 | evk: Gtk.EventControllerKey,
534 | keycode: int,
535 | keyval: int,
536 | modifier: Gdk.ModifierType,
537 | ) -> None:
538 | """Handle key press events in the main window."""
539 | ctrl_pressed = modifier == Gdk.ModifierType.CONTROL_MASK
540 | match keycode, ctrl_pressed:
541 | case Gdk.KEY_Escape, _:
542 | self.close()
543 |
544 | case Gdk.KEY_F11, _:
545 | self.activate_action("win.fullscreen")
546 |
547 | case Gdk.KEY_f, True:
548 | self.active_headerbar.grab_focus()
549 | case Gdk.KEY_s, True:
550 | self.active_headerbar.sheet_dropdown.grab_focus()
551 |
552 | case (Gdk.KEY_Up, False) | (Gdk.KEY_k, True):
553 | self.scroll(to_start=True, by_page=False)
554 | case (Gdk.KEY_Down, False) | (Gdk.KEY_j, True):
555 | self.scroll(to_start=False, by_page=False)
556 | case Gdk.KEY_Page_Up, False:
557 | self.scroll(to_start=True, by_page=True)
558 | case Gdk.KEY_Page_Down, False:
559 | self.scroll(to_start=False, by_page=True)
560 |
561 | case (Gdk.KEY_Up, True):
562 | self.cycle_sheets(direction="previous")
563 | case (Gdk.KEY_Down, True):
564 | self.cycle_sheets(direction="next")
565 |
566 | def on_about_action(self, _: Gio.SimpleAction, __: None) -> None:
567 | """Show modal 'About' dialog."""
568 | logo = Gtk.Image.new_from_file(f"{RESOURCE_PATH}/keyhint_icon.svg")
569 | Gtk.AboutDialog(
570 | program_name="KeyHint",
571 | comments="Cheatsheet for keyboard shortcuts & commands",
572 | version=__version__,
573 | website_label="Github",
574 | website="https://github.com/dynobo/keyhint",
575 | logo=logo.get_paintable(),
576 | license_type=Gtk.License.MIT_X11,
577 | modal=True,
578 | resizable=True,
579 | transient_for=self,
580 | ).show()
581 |
582 | def on_debug_action(self, _: Gio.SimpleAction, __: None) -> None:
583 | """Show modal dialog with information useful for error reporting."""
584 | label = Gtk.Label()
585 | label.set_use_markup(True)
586 | label.set_wrap(True)
587 | label.set_selectable(True)
588 | label.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
589 | label.set_markup(self.get_debug_info_text())
590 | label.set_margin_start(24)
591 | label.set_margin_end(24)
592 |
593 | def _on_copy_clicked(button: Gtk.Button) -> None:
594 | if display := Gdk.Display.get_default():
595 | clipboard = display.get_clipboard()
596 | clipboard.set(f"### Debug Info\n\n```\n{label.get_text().strip()}\n```")
597 | button.set_icon_name("object-select-symbolic")
598 | button.set_tooltip_text("Copied!")
599 |
600 | copy_button = Gtk.Button()
601 | copy_button.set_icon_name("edit-copy")
602 | copy_button.set_tooltip_text("Copy to clipboard")
603 | copy_button.connect("clicked", _on_copy_clicked)
604 |
605 | dialog = Gtk.Dialog(title="Debug Info", transient_for=self, modal=True)
606 | dialog.get_content_area().append(label)
607 | dialog.add_action_widget(copy_button, Gtk.ResponseType.NONE)
608 | dialog.show()
609 |
610 | def on_create_new_sheet(self, _: Gio.SimpleAction, __: None) -> None:
611 | """Create a new text file with a template for a new cheatsheet."""
612 | title = self.wm_class.lower().replace(" ", "")
613 | pad = 26 - len(title)
614 | template = f"""\
615 | id = "{title}"{" " * pad} # Unique ID, used e.g. in cheatsheet dropdown
616 | url = "" # (Optional) URL to keybinding docs
617 |
618 | [match]
619 | regex_wmclass = "{self.wm_class}"
620 | regex_title = ".*" # Narrow down by window title if needed
621 |
622 | [section]
623 |
624 | [section."My Section Title"] # Add as many sections you like ...
625 | "Ctrl + c" = "Copy to clipboard" # ... with keybinding + description
626 | "Ctrl + v" = "Paste from clipboard"
627 | """
628 | template = textwrap.dedent(template)
629 |
630 | new_file = config.CONFIG_PATH / f"{title}.toml"
631 |
632 | # Make sure the file name is unique
633 | idx = 1
634 | while new_file.exists():
635 | new_file = new_file.with_name(f"{title}_{idx}.toml")
636 | idx += 1
637 |
638 | new_file.write_text(template)
639 | subprocess.Popen(["xdg-open", str(new_file.resolve())]) # noqa: S603, S607
640 |
641 | def on_open_folder_action(self, _: Gio.SimpleAction, __: None) -> None:
642 | """Open config folder in default file manager."""
643 | subprocess.Popen(["xdg-open", str(config.CONFIG_PATH.resolve())]) # noqa: S603, S607
644 |
645 | def sections_filter_func(self, child: Gtk.FlowBoxChild) -> bool:
646 | """Filter binding sections based on the search entry text."""
647 | # If no text, show all sections
648 | if not self.search_text:
649 | return True
650 |
651 | # If text, show only sections with 1 or more visible bindings
652 | column_view = child.get_child()
653 | if not isinstance(column_view, Gtk.ColumnView):
654 | raise TypeError("Child is not a ColumnView.")
655 |
656 | selection = column_view.get_model()
657 | if not isinstance(selection, Gtk.NoSelection):
658 | raise TypeError("ColumnView model is not a NoSelection.")
659 |
660 | filter_model = selection.get_model()
661 | if not isinstance(filter_model, Gtk.FilterListModel):
662 | raise TypeError("ColumnView model is not a FilterListModel.")
663 |
664 | return filter_model.get_n_items() > 0
665 |
666 | def sections_sort_func(
667 | self, child_a: Gtk.FlowBoxChild, child_b: Gtk.FlowBoxChild
668 | ) -> bool:
669 | """Sort function for the sections of the cheatsheet."""
670 | sort_by = self.config["main"].get("sort_by", "size")
671 |
672 | if sort_by == "native":
673 | # The names use the format 'section-{INDEX}', so just sort by that
674 | return child_a.get_name() > child_b.get_name()
675 |
676 | sub_child_a = child_a.get_child()
677 | sub_child_b = child_b.get_child()
678 |
679 | # Satisfy type checker
680 | if not isinstance(sub_child_a, Gtk.ColumnView) or not isinstance(
681 | sub_child_b, Gtk.ColumnView
682 | ):
683 | raise TypeError("Child is not a ColumnView.")
684 |
685 | if sort_by == "size":
686 | # Sorts by number of bindings in the section
687 | model_a = sub_child_a.get_model()
688 | model_b = sub_child_b.get_model()
689 | if not isinstance(model_a, Gtk.NoSelection) or not isinstance(
690 | model_b, Gtk.NoSelection
691 | ):
692 | raise TypeError("ColumnView model is not a NoSelection.")
693 | return model_a.get_n_items() < model_b.get_n_items()
694 |
695 | if sort_by == "title":
696 | column_a = sub_child_a.get_columns().get_item(1)
697 | column_b = sub_child_b.get_columns().get_item(1)
698 | if not isinstance(column_a, Gtk.ColumnViewColumn) or not isinstance(
699 | column_b, Gtk.ColumnViewColumn
700 | ):
701 | raise TypeError("Column is not a ColumnViewColumn.")
702 | return (column_a.get_title() or "") > (column_b.get_title() or "")
703 |
704 | raise ValueError(f"Invalid sort_by value: {sort_by}")
705 |
706 | def get_appropriate_sheet_id(self) -> str:
707 | """Determine the sheet ID based on context or configuration."""
708 | sheet_id = None
709 |
710 | # If sheet-id was provided via cli option, use that one
711 | if sheet_id := self.cli_args.get("cheatsheet", None):
712 | logger.debug("Using provided sheet-id: %s.", sheet_id)
713 | return sheet_id
714 |
715 | # Else try to find cheatsheet for active window
716 | if sheet_id := sheets.get_sheet_id_by_active_window(
717 | sheets=self.sheets, wm_class=self.wm_class, window_title=self.window_title
718 | ):
719 | logger.debug("Found matching sheet: %s.", sheet_id)
720 | return sheet_id
721 |
722 | # If no sheet found, show toast to create new one...
723 | self.show_create_new_sheet_toast()
724 |
725 | # ...and use the configured fallback sheet
726 | if sheet_id := self.config["main"].get("fallback_cheatsheet", ""):
727 | logger.debug("Using provided fallback sheet-id: %s.", sheet_id)
728 | return sheet_id
729 |
730 | # If that fallback sheet also does not exist, just use the first in dropdown
731 | model = self.headerbars.normal.sheet_dropdown.get_model()
732 | item = model.get_item(0) if model else None
733 | sheet_id = (
734 | item.get_string() if isinstance(item, Gtk.StringObject) else "keyhint"
735 | )
736 | logger.debug("No matching or fallback sheet found. Using first sheet.")
737 | return sheet_id
738 |
739 | def bind_shortcuts_callback(
740 | self,
741 | _: Gtk.SignalListItemFactory,
742 | item, # noqa: ANN001 # ONHOLD: Gtk.ColumnViewCell for GTK 4.12+
743 | ) -> None:
744 | row = cast(binding.Row, item.get_item())
745 | shortcut = binding.create_shortcut(row.shortcut)
746 | self.max_shortcut_width = max(
747 | self.max_shortcut_width,
748 | shortcut.get_preferred_size().natural_size.width, # type: ignore # False Positive
749 | )
750 | item.set_child(shortcut)
751 |
752 | def bind_labels_callback(
753 | self,
754 | _: Gtk.SignalListItemFactory,
755 | item, # noqa: ANN001 # ONHOLD: Gtk.ColumnViewCell for GTK 4.12+
756 | ) -> None:
757 | row = cast(binding.Row, item.get_item())
758 | if row.shortcut:
759 | child = Gtk.Label(label=row.label, xalign=0.0)
760 | else:
761 | # Section title
762 | child = Gtk.Label(xalign=0.0)
763 | child.set_markup(f"{row.label}")
764 | item.set_child(child)
765 |
766 | def bindings_match_func(self, bindings_row: binding.Row) -> bool:
767 | if self.search_text:
768 | return self.search_text.lower() in bindings_row.filter_text.lower()
769 | return True
770 |
771 | def show_sheet(self, sheet_id: str) -> None:
772 | """Clear sheet container and populate it with the selected sheet."""
773 | if hasattr(self.sheet_container_box, "remove_all"):
774 | # Only available in GTK 4.12+
775 | self.sheet_container_box.remove_all()
776 | else:
777 | # ONHOLD: Remove once GTK 4.12+ is required
778 | while child := self.sheet_container_box.get_first_child():
779 | self.sheet_container_box.remove(child)
780 |
781 | self.max_shortcut_width = 0
782 |
783 | sheet = sheets.get_sheet_by_id(sheets=self.sheets, sheet_id=sheet_id)
784 | sections = sheet["section"]
785 | for index, (section, bindings) in enumerate(sections.items()):
786 | section_child = self.create_section(section, bindings)
787 | section_child.set_name(f"section-{index:03}")
788 | self.sheet_container_box.append(section_child)
789 |
790 | def create_section(
791 | self, section: str, bindings: dict[str, str]
792 | ) -> Gtk.FlowBoxChild:
793 | ls = Gio.ListStore()
794 | for shortcut, label in bindings.items():
795 | ls.append(binding.Row(shortcut=shortcut, label=label, section=section))
796 |
797 | filter_list = Gtk.FilterListModel(model=ls)
798 | filter_list.set_filter(self.bindings_filter)
799 |
800 | selection = Gtk.NoSelection.new(filter_list)
801 |
802 | # TODO: Dynamic width based on content
803 | shortcut_column_width = self.config["main"].getint("zoom", 100) * 1.1 + 135
804 | shortcut_column = binding.create_column_view_column(
805 | "", self.shortcut_column_factory, shortcut_column_width
806 | )
807 | label_column = binding.create_column_view_column(
808 | section, self.label_column_factory
809 | )
810 | column_view = binding.create_column_view(
811 | selection, shortcut_column, label_column
812 | )
813 |
814 | section_child = Gtk.FlowBoxChild()
815 | section_child.set_vexpand(False)
816 | section_child.set_child(column_view)
817 | return section_child
818 |
819 | def get_current_sheet_id(self) -> str:
820 | action = self.lookup_action("sheet")
821 | state = action.get_state() if action else None
822 | return state.get_string() if state else ""
823 |
824 | def get_debug_info_text(self) -> str:
825 | """Compile information which is useful for error analysis."""
826 | sheet_id = self.get_current_sheet_id()
827 | sheet = (
828 | sheets.get_sheet_by_id(sheets=self.sheets, sheet_id=sheet_id)
829 | if sheet_id
830 | else {}
831 | )
832 | regex_wm_class = sheet.get("match", {}).get("regex_wmclass", "n/a")
833 | regex_title = sheet.get("match", {}).get("regex_title", "n/a")
834 | link = sheet.get("url", "")
835 | link_text = f"{link or 'n/a'}"
836 | desktop_environment = context.get_desktop_environment()
837 | if desktop_environment.lower() == "gnome":
838 | desktop_environment += " " + context.get_gnome_version()
839 | elif desktop_environment.lower() == "kde":
840 | desktop_environment += " " + context.get_kde_version()
841 |
842 | return textwrap.dedent(
843 | f"""
844 | Last Active Application
845 |
846 | title: {self.window_title}
847 | wmclass: {self.wm_class}
848 |
849 | Selected Cheatsheet
850 |
851 | ID: {sheet_id}
852 | regex_wmclass: {regex_wm_class}
853 | regex_title: {regex_title}
854 | source: {link_text}
855 |
856 | System Information
857 |
858 | Platform: {platform.platform()}
859 | Desktop Environment: {desktop_environment}
860 | Wayland: {context.is_using_wayland()}
861 | Python: {platform.python_version()}
862 | Keyhint: v{__version__}
863 | Flatpak: {context.is_flatpak_package()}"""
864 | )
865 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "keyhint"
7 | version = "0.5.6"
8 | description = "Cheat-sheets for shortcuts & commands at your fingertips."
9 | keywords = ["shortcuts", "keybindings", "hints", "helper", "cheatsheet"]
10 | readme = "README.md"
11 | requires-python = ">=3.11"
12 | authors = [{ name = "dynobo", email = "dynobo@mailbox.org" }]
13 | classifiers = [
14 | "Development Status :: 4 - Beta",
15 | "License :: OSI Approved :: MIT License",
16 | "Programming Language :: Python",
17 | "Programming Language :: Python :: 3.11",
18 | "Programming Language :: Python :: 3.12",
19 | "Programming Language :: Python :: Implementation :: CPython",
20 | "Programming Language :: Python :: Implementation :: PyPy",
21 | "Topic :: Utilities",
22 | "Intended Audience :: End Users/Desktop",
23 | "Operating System :: POSIX :: Linux",
24 | ]
25 | dependencies = ["PyGObject>=3.42.2"]
26 |
27 | [project.urls]
28 | Documentation = "https://github.com/dynobo/keyhint#readme"
29 | Issues = "https://github.com/dynobo/keyhint/issues"
30 | Source = "https://github.com/dynobo/keyhint"
31 |
32 | [project.scripts]
33 | keyhint = "keyhint.app:main"
34 |
35 | [dependency-groups]
36 | dev = [
37 | "coverage[toml]>=6.5",
38 | "pytest",
39 | "pytest-cov",
40 | "pre-commit",
41 | "coveralls",
42 | "types-toml",
43 | "tbump",
44 | "ruff",
45 | "pip-audit",
46 | "mypy",
47 | "mdformat",
48 | "pygobject-stubs",
49 | ]
50 |
51 | [tool.ruff]
52 | target-version = "py311"
53 | line-length = 88
54 | exclude = [".venv"]
55 |
56 | [tool.ruff.lint]
57 | select = [
58 | "F", # Pyflakes
59 | "E", # pycodestyle
60 | "I", # Isort
61 | "D", # pydocstyle
62 | "W", # warning
63 | "UP", # pyupgrad
64 | "N", # pep8-naming
65 | "C90", # mccabe
66 | "TRY", # tryceratops (exception handling)
67 | "ANN", # flake8-annotations
68 | "S", # flake8-bandits
69 | "C4", # flake8-comprehensions
70 | "B", # flake8-bugbear
71 | "A", # flake8-builtins
72 | "ISC", # flake8-implicit-str-concat
73 | "ICN", # flake8-import-conventions
74 | "T20", # flake8-print
75 | "PYI", # flake8-pyi
76 | "PT", # flake8-pytest-style
77 | "Q", # flake8-quotes
78 | "RET", # flake8-return
79 | "SIM", # flake8-simplify
80 | "PTH", # flake8-use-pathlib
81 | "G", # flake8-logging-format
82 | "PL", # pylint
83 | "RUF", # meta rules (unused noqa)
84 | "PL", # meta rules (unused noqa)
85 | "PERF", # perflint
86 | ]
87 | ignore = [
88 | "D100", # Missing docstring in public module
89 | "D101", # Missing docstring in public class
90 | "D102", # Missing docstring in public method
91 | "D103", # Missing docstring in public function
92 | "D104", # Missing docstring in public package
93 | "D105", # Missing docstring in magic method
94 | "D107", # Missing docstring in __init__
95 | "ANN101", # Missing type annotation for `self` in method
96 | "TRY003", # Avoid specifying long messages outside the exception class
97 | "ISC001", # Rule conflicts with ruff's formaatter
98 | ]
99 |
100 | [tool.ruff.lint.flake8-tidy-imports]
101 | ban-relative-imports = "all"
102 |
103 | [tool.ruff.lint.per-file-ignores]
104 | "tests/**/*" = ["PLR2004", "PLR0913", "S101", "TID252", "ANN", "D"]
105 |
106 | [tool.ruff.lint.pydocstyle]
107 | convention = "google"
108 |
109 | [tool.ruff.lint.isort]
110 | known-first-party = ["keyhint"]
111 |
112 | [tool.mypy]
113 | files = ["keyhint/**/*.py", "tests/**/*.py"]
114 | follow_imports = "skip"
115 | ignore_missing_imports = true
116 |
117 | [tool.pytest.ini_options]
118 | testpaths = ["tests"]
119 | addopts = [
120 | "--durations=5",
121 | "--showlocals",
122 | "--cov",
123 | "--cov-report=xml",
124 | "--cov-report=html",
125 | ]
126 |
127 | [tool.coverage.run]
128 | source_pkgs = ["keyhint"]
129 | branch = true
130 | parallel = true
131 | omit = []
132 |
133 | [tool.mdformat]
134 | wrap = 88
135 | number = true
136 | end_of_line = "keep"
137 |
138 | [tool.tbump]
139 |
140 | [tool.tbump.version]
141 | current = "0.5.6"
142 | regex = '''
143 | (?P\d+)
144 | \.
145 | (?P\d+)
146 | \.
147 | (?P\d+)
148 | ((?P.+))?
149 | '''
150 |
151 | [tool.tbump.git]
152 | message_template = "Bump to {new_version}"
153 | tag_template = "v{new_version}"
154 |
155 | [[tool.tbump.file]]
156 | src = "pyproject.toml"
157 | search = 'version = "{current_version}"'
158 |
159 | [[tool.tbump.file]]
160 | src = "keyhint/__init__.py"
161 |
162 | [[tool.tbump.before_commit]]
163 | name = "check changelog"
164 | cmd = "grep -q {new_version} CHANGELOG.md"
165 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dynobo/keyhint/5ac38c3195b197a182d25ccdd7066baa1c293321/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_sheets.py:
--------------------------------------------------------------------------------
1 | """Test the utility functions."""
2 |
3 | from pathlib import Path
4 |
5 | from keyhint import sheets
6 |
7 |
8 | def test_load_default_sheets():
9 | """Test loading of toml files shipped with package."""
10 | default_sheets = sheets.load_default_sheets()
11 | toml_files = (Path(__file__).parent.parent / "keyhint" / "config").glob("*.toml")
12 |
13 | assert len(default_sheets) == len(list(toml_files))
14 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | revision = 1
3 | requires-python = ">=3.11"
4 |
5 | [[package]]
6 | name = "boolean-py"
7 | version = "4.0"
8 | source = { registry = "https://pypi.org/simple" }
9 | sdist = { url = "https://files.pythonhosted.org/packages/a2/d9/b6e56a303d221fc0bdff2c775e4eef7fedd58194aa5a96fa89fb71634cc9/boolean.py-4.0.tar.gz", hash = "sha256:17b9a181630e43dde1851d42bef546d616d5d9b4480357514597e78b203d06e4", size = 34504 }
10 | wheels = [
11 | { url = "https://files.pythonhosted.org/packages/3f/02/6389ef0529af6da0b913374dedb9bbde8eabfe45767ceec38cc37801b0bd/boolean.py-4.0-py3-none-any.whl", hash = "sha256:2876f2051d7d6394a531d82dc6eb407faa0b01a0a0b3083817ccd7323b8d96bd", size = 25909 },
12 | ]
13 |
14 | [[package]]
15 | name = "cachecontrol"
16 | version = "0.14.2"
17 | source = { registry = "https://pypi.org/simple" }
18 | dependencies = [
19 | { name = "msgpack" },
20 | { name = "requests" },
21 | ]
22 | sdist = { url = "https://files.pythonhosted.org/packages/b7/a4/3390ac4dfa1773f661c8780368018230e8207ec4fd3800d2c0c3adee4456/cachecontrol-0.14.2.tar.gz", hash = "sha256:7d47d19f866409b98ff6025b6a0fca8e4c791fb31abbd95f622093894ce903a2", size = 28832 }
23 | wheels = [
24 | { url = "https://files.pythonhosted.org/packages/c8/63/baffb44ca6876e7b5fc8fe17b24a7c07bf479d604a592182db9af26ea366/cachecontrol-0.14.2-py3-none-any.whl", hash = "sha256:ebad2091bf12d0d200dfc2464330db638c5deb41d546f6d7aca079e87290f3b0", size = 21780 },
25 | ]
26 |
27 | [package.optional-dependencies]
28 | filecache = [
29 | { name = "filelock" },
30 | ]
31 |
32 | [[package]]
33 | name = "certifi"
34 | version = "2025.1.31"
35 | source = { registry = "https://pypi.org/simple" }
36 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
37 | wheels = [
38 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
39 | ]
40 |
41 | [[package]]
42 | name = "cfgv"
43 | version = "3.4.0"
44 | source = { registry = "https://pypi.org/simple" }
45 | sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
46 | wheels = [
47 | { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
48 | ]
49 |
50 | [[package]]
51 | name = "charset-normalizer"
52 | version = "3.4.1"
53 | source = { registry = "https://pypi.org/simple" }
54 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
55 | wheels = [
56 | { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
57 | { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
58 | { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
59 | { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
60 | { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
61 | { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
62 | { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
63 | { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
64 | { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
65 | { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
66 | { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
67 | { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
68 | { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
69 | { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
70 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
71 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
72 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
73 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
74 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
75 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
76 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
77 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
78 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
79 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
80 | { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
81 | { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
82 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
83 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
84 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
85 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
86 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
87 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
88 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
89 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
90 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
91 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
92 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
93 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
94 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
95 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
96 | ]
97 |
98 | [[package]]
99 | name = "cli-ui"
100 | version = "0.18.0"
101 | source = { registry = "https://pypi.org/simple" }
102 | dependencies = [
103 | { name = "colorama" },
104 | { name = "tabulate" },
105 | { name = "unidecode" },
106 | ]
107 | sdist = { url = "https://files.pythonhosted.org/packages/26/a9/b44b1048064206e9ceceffb7ce38aa2432dbf79bd13d45da8a1452a2e3db/cli_ui-0.18.0.tar.gz", hash = "sha256:3e6c80ada5b4b09c6701ca93daf31df8b70486c64348d1fc7f3288ef3bd0479c", size = 13012 }
108 | wheels = [
109 | { url = "https://files.pythonhosted.org/packages/24/07/167c0ccdcf220613872ca25c50d6006b841c2aac21f0274d4f9e4b80769a/cli_ui-0.18.0-py3-none-any.whl", hash = "sha256:8d9484586d8eaba9f94aebaa12aa876fabdf1a3a50bdca113b2cb739eeaf78fa", size = 13401 },
110 | ]
111 |
112 | [[package]]
113 | name = "colorama"
114 | version = "0.4.6"
115 | source = { registry = "https://pypi.org/simple" }
116 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
117 | wheels = [
118 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
119 | ]
120 |
121 | [[package]]
122 | name = "coverage"
123 | version = "7.7.1"
124 | source = { registry = "https://pypi.org/simple" }
125 | sdist = { url = "https://files.pythonhosted.org/packages/6b/bf/3effb7453498de9c14a81ca21e1f92e6723ce7ebdc5402ae30e4dcc490ac/coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393", size = 810332 }
126 | wheels = [
127 | { url = "https://files.pythonhosted.org/packages/c2/4c/5118ca60ed4141ec940c8cbaf1b2ebe8911be0f03bfc028c99f63de82c44/coverage-7.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1165490be0069e34e4f99d08e9c5209c463de11b471709dfae31e2a98cbd49fd", size = 211064 },
128 | { url = "https://files.pythonhosted.org/packages/e8/6c/0e9aac4cf5dba49feede79109fdfd2fafca3bdbc02992bcf9b25d58351dd/coverage-7.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:44af11c00fd3b19b8809487630f8a0039130d32363239dfd15238e6d37e41a48", size = 211501 },
129 | { url = "https://files.pythonhosted.org/packages/23/1a/570666f276815722f0a94f92b61e7123d66b166238e0f9f224f1a38f17cf/coverage-7.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbba59022e7c20124d2f520842b75904c7b9f16c854233fa46575c69949fb5b9", size = 244128 },
130 | { url = "https://files.pythonhosted.org/packages/e8/0d/cb23f89eb8c7018429c6cf8cc436b4eb917f43e81354d99c86c435ab1813/coverage-7.7.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af94fb80e4f159f4d93fb411800448ad87b6039b0500849a403b73a0d36bb5ae", size = 241818 },
131 | { url = "https://files.pythonhosted.org/packages/54/fd/584a5d099bba4e79ac3893d57e0bd53034f7187c30f940e6a581bfd38c8f/coverage-7.7.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae79f8e3501133aa0e220bbc29573910d096795882a70e6f6e6637b09522133", size = 243602 },
132 | { url = "https://files.pythonhosted.org/packages/78/d7/a28b6a5ee64ff1e4a66fbd8cd7b9372471c951c3a0c4ec9d1d0f47819f53/coverage-7.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e33426a5e1dc7743dd54dfd11d3a6c02c5d127abfaa2edd80a6e352b58347d1a", size = 243247 },
133 | { url = "https://files.pythonhosted.org/packages/b2/9e/210814fae81ea7796f166529a32b443dead622a8c1ad315d0779520635c6/coverage-7.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b559adc22486937786731dac69e57296cb9aede7e2687dfc0d2696dbd3b1eb6b", size = 241422 },
134 | { url = "https://files.pythonhosted.org/packages/99/5e/80ed1955fa8529bdb72dc11c0a3f02a838285250c0e14952e39844993102/coverage-7.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b838a91e84e1773c3436f6cc6996e000ed3ca5721799e7789be18830fad009a2", size = 241958 },
135 | { url = "https://files.pythonhosted.org/packages/7e/26/f0bafc8103284febc4e3a3cd947b49ff36c50711daf3d03b3e11b23bc73a/coverage-7.7.1-cp311-cp311-win32.whl", hash = "sha256:2c492401bdb3a85824669d6a03f57b3dfadef0941b8541f035f83bbfc39d4282", size = 213571 },
136 | { url = "https://files.pythonhosted.org/packages/c1/fe/fef0a0201af72422fb9634b5c6079786bb405ac09cce5661fdd54a66e773/coverage-7.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e6f867379fd033a0eeabb1be0cffa2bd660582b8b0c9478895c509d875a9d9e", size = 214488 },
137 | { url = "https://files.pythonhosted.org/packages/cf/b0/4eaba302a86ec3528231d7cfc954ae1929ec5d42b032eb6f5b5f5a9155d2/coverage-7.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eff187177d8016ff6addf789dcc421c3db0d014e4946c1cc3fbf697f7852459d", size = 211253 },
138 | { url = "https://files.pythonhosted.org/packages/fd/68/21b973e6780a3f2457e31ede1aca6c2f84bda4359457b40da3ae805dcf30/coverage-7.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2444fbe1ba1889e0b29eb4d11931afa88f92dc507b7248f45be372775b3cef4f", size = 211504 },
139 | { url = "https://files.pythonhosted.org/packages/d1/b4/c19e9c565407664390254252496292f1e3076c31c5c01701ffacc060e745/coverage-7.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:177d837339883c541f8524683e227adcaea581eca6bb33823a2a1fdae4c988e1", size = 245566 },
140 | { url = "https://files.pythonhosted.org/packages/7b/0e/f9829cdd25e5083638559c8c267ff0577c6bab19dacb1a4fcfc1e70e41c0/coverage-7.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d54ecef1582b1d3ec6049b20d3c1a07d5e7f85335d8a3b617c9960b4f807e0", size = 242455 },
141 | { url = "https://files.pythonhosted.org/packages/29/57/a3ada2e50a665bf6d9851b5eb3a9a07d7e38f970bdd4d39895f311331d56/coverage-7.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c82b27c56478d5e1391f2e7b2e7f588d093157fa40d53fd9453a471b1191f2", size = 244713 },
142 | { url = "https://files.pythonhosted.org/packages/0f/d3/f15c7d45682a73eca0611427896016bad4c8f635b0fc13aae13a01f8ed9d/coverage-7.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:315ff74b585110ac3b7ab631e89e769d294f303c6d21302a816b3554ed4c81af", size = 244476 },
143 | { url = "https://files.pythonhosted.org/packages/19/3b/64540074e256082b220e8810fd72543eff03286c59dc91976281dc0a559c/coverage-7.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4dd532dac197d68c478480edde74fd4476c6823355987fd31d01ad9aa1e5fb59", size = 242695 },
144 | { url = "https://files.pythonhosted.org/packages/8a/c1/9cad25372ead7f9395a91bb42d8ae63e6cefe7408eb79fd38797e2b763eb/coverage-7.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:385618003e3d608001676bb35dc67ae3ad44c75c0395d8de5780af7bb35be6b2", size = 243888 },
145 | { url = "https://files.pythonhosted.org/packages/66/c6/c3e6c895bc5b95ccfe4cb5838669dbe5226ee4ad10604c46b778c304d6f9/coverage-7.7.1-cp312-cp312-win32.whl", hash = "sha256:63306486fcb5a827449464f6211d2991f01dfa2965976018c9bab9d5e45a35c8", size = 213744 },
146 | { url = "https://files.pythonhosted.org/packages/cc/8a/6df2fcb4c3e38ec6cd7e211ca8391405ada4e3b1295695d00aa07c6ee736/coverage-7.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:37351dc8123c154fa05b7579fdb126b9f8b1cf42fd6f79ddf19121b7bdd4aa04", size = 214546 },
147 | { url = "https://files.pythonhosted.org/packages/ec/2a/1a254eaadb01c163b29d6ce742aa380fc5cfe74a82138ce6eb944c42effa/coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585", size = 211277 },
148 | { url = "https://files.pythonhosted.org/packages/cf/00/9636028365efd4eb6db71cdd01d99e59f25cf0d47a59943dbee32dd1573b/coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c", size = 211551 },
149 | { url = "https://files.pythonhosted.org/packages/6f/c8/14aed97f80363f055b6cd91e62986492d9fe3b55e06b4b5c82627ae18744/coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019", size = 245068 },
150 | { url = "https://files.pythonhosted.org/packages/d6/76/9c5fe3f900e01d7995b0cda08fc8bf9773b4b1be58bdd626f319c7d4ec11/coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf", size = 242109 },
151 | { url = "https://files.pythonhosted.org/packages/c0/81/760993bb536fb674d3a059f718145dcd409ed6d00ae4e3cbf380019fdfd0/coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b", size = 244129 },
152 | { url = "https://files.pythonhosted.org/packages/00/be/1114a19f93eae0b6cd955dabb5bee80397bd420d846e63cd0ebffc134e3d/coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f", size = 244201 },
153 | { url = "https://files.pythonhosted.org/packages/06/8d/9128fd283c660474c7dc2b1ea5c66761bc776b970c1724989ed70e9d6eee/coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777", size = 242282 },
154 | { url = "https://files.pythonhosted.org/packages/d4/2a/6d7dbfe9c1f82e2cdc28d48f4a0c93190cf58f057fa91ba2391b92437fe6/coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a", size = 243570 },
155 | { url = "https://files.pythonhosted.org/packages/cf/3e/29f1e4ce3bb951bcf74b2037a82d94c5064b3334304a3809a95805628838/coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a", size = 213772 },
156 | { url = "https://files.pythonhosted.org/packages/bc/3a/cf029bf34aefd22ad34f0e808eba8d5830f297a1acb483a2124f097ff769/coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1", size = 214575 },
157 | { url = "https://files.pythonhosted.org/packages/92/4c/fb8b35f186a2519126209dce91ab8644c9a901cf04f8dfa65576ca2dd9e8/coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511", size = 212113 },
158 | { url = "https://files.pythonhosted.org/packages/59/90/e834ffc86fd811c5b570a64ee1895b20404a247ec18a896b9ba543b12097/coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24", size = 212333 },
159 | { url = "https://files.pythonhosted.org/packages/a5/a1/27f0ad39569b3b02410b881c42e58ab403df13fcd465b475db514b83d3d3/coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950", size = 256566 },
160 | { url = "https://files.pythonhosted.org/packages/9f/3b/21fa66a1db1b90a0633e771a32754f7c02d60236a251afb1b86d7e15d83a/coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d", size = 252276 },
161 | { url = "https://files.pythonhosted.org/packages/d6/e5/4ab83a59b0f8ac4f0029018559fc4c7d042e1b4552a722e2bfb04f652296/coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e", size = 254616 },
162 | { url = "https://files.pythonhosted.org/packages/db/7a/4224417c0ccdb16a5ba4d8d1fcfaa18439be1624c29435bb9bc88ccabdfb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862", size = 255707 },
163 | { url = "https://files.pythonhosted.org/packages/51/20/ff18a329ccaa3d035e2134ecf3a2e92a52d3be6704c76e74ca5589ece260/coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271", size = 253876 },
164 | { url = "https://files.pythonhosted.org/packages/e4/e8/1d6f1a6651672c64f45ffad05306dad9c4c189bec694270822508049b2cb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de", size = 254687 },
165 | { url = "https://files.pythonhosted.org/packages/6b/ea/1b9a14cf3e2bc3fd9de23a336a8082091711c5f480b500782d59e84a8fe5/coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c", size = 214486 },
166 | { url = "https://files.pythonhosted.org/packages/cc/bb/faa6bcf769cb7b3b660532a30d77c440289b40636c7f80e498b961295d07/coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c", size = 215647 },
167 | { url = "https://files.pythonhosted.org/packages/f9/4e/a501ec475ed455c1ee1570063527afe2c06ab1039f8ff18eefecfbdac8fd/coverage-7.7.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:5b7b02e50d54be6114cc4f6a3222fec83164f7c42772ba03b520138859b5fde1", size = 203014 },
168 | { url = "https://files.pythonhosted.org/packages/52/26/9f53293ff4cc1d47d98367ce045ca2e62746d6be74a5c6851a474eabf59b/coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31", size = 203006 },
169 | ]
170 |
171 | [package.optional-dependencies]
172 | toml = [
173 | { name = "tomli", marker = "python_full_version <= '3.11'" },
174 | ]
175 |
176 | [[package]]
177 | name = "coveralls"
178 | version = "4.0.1"
179 | source = { registry = "https://pypi.org/simple" }
180 | dependencies = [
181 | { name = "coverage", extra = ["toml"] },
182 | { name = "docopt" },
183 | { name = "requests" },
184 | ]
185 | sdist = { url = "https://files.pythonhosted.org/packages/61/75/a454fb443eb6a053833f61603a432ffbd7dd6ae53a11159bacfadb9d6219/coveralls-4.0.1.tar.gz", hash = "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69", size = 12419 }
186 | wheels = [
187 | { url = "https://files.pythonhosted.org/packages/63/e5/6708c75e2a4cfca929302d4d9b53b862c6dc65bd75e6933ea3d20016d41d/coveralls-4.0.1-py3-none-any.whl", hash = "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809", size = 13599 },
188 | ]
189 |
190 | [[package]]
191 | name = "cyclonedx-python-lib"
192 | version = "8.9.0"
193 | source = { registry = "https://pypi.org/simple" }
194 | dependencies = [
195 | { name = "license-expression" },
196 | { name = "packageurl-python" },
197 | { name = "py-serializable" },
198 | { name = "sortedcontainers" },
199 | ]
200 | sdist = { url = "https://files.pythonhosted.org/packages/96/0c/af4b06d555c5e9b8b5bd48db7593c4f29ce21f91b139d30e29d76c02b55d/cyclonedx_python_lib-8.9.0.tar.gz", hash = "sha256:112c6e6e5290420e32026c49b8391645bf3e646c7602f7bdb5d02c6febbaa073", size = 1046896 }
201 | wheels = [
202 | { url = "https://files.pythonhosted.org/packages/41/42/e6680a2c452d171b43b23a39eeffe7cc95738a1ec22db4e36dd3fc2ea533/cyclonedx_python_lib-8.9.0-py3-none-any.whl", hash = "sha256:017b95b334aa83b2d0db8af9764e13a46f0e903bd30a57d93d08dcd302c84032", size = 375022 },
203 | ]
204 |
205 | [[package]]
206 | name = "defusedxml"
207 | version = "0.7.1"
208 | source = { registry = "https://pypi.org/simple" }
209 | sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 }
210 | wheels = [
211 | { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 },
212 | ]
213 |
214 | [[package]]
215 | name = "distlib"
216 | version = "0.3.9"
217 | source = { registry = "https://pypi.org/simple" }
218 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
219 | wheels = [
220 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
221 | ]
222 |
223 | [[package]]
224 | name = "docopt"
225 | version = "0.6.2"
226 | source = { registry = "https://pypi.org/simple" }
227 | sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901 }
228 |
229 | [[package]]
230 | name = "filelock"
231 | version = "3.18.0"
232 | source = { registry = "https://pypi.org/simple" }
233 | sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 }
234 | wheels = [
235 | { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 },
236 | ]
237 |
238 | [[package]]
239 | name = "identify"
240 | version = "2.6.9"
241 | source = { registry = "https://pypi.org/simple" }
242 | sdist = { url = "https://files.pythonhosted.org/packages/9b/98/a71ab060daec766acc30fb47dfca219d03de34a70d616a79a38c6066c5bf/identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf", size = 99249 }
243 | wheels = [
244 | { url = "https://files.pythonhosted.org/packages/07/ce/0845144ed1f0e25db5e7a79c2354c1da4b5ce392b8966449d5db8dca18f1/identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", size = 99101 },
245 | ]
246 |
247 | [[package]]
248 | name = "idna"
249 | version = "3.10"
250 | source = { registry = "https://pypi.org/simple" }
251 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
252 | wheels = [
253 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
254 | ]
255 |
256 | [[package]]
257 | name = "iniconfig"
258 | version = "2.1.0"
259 | source = { registry = "https://pypi.org/simple" }
260 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
261 | wheels = [
262 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
263 | ]
264 |
265 | [[package]]
266 | name = "keyhint"
267 | version = "0.5.5"
268 | source = { editable = "." }
269 | dependencies = [
270 | { name = "pygobject" },
271 | ]
272 |
273 | [package.dev-dependencies]
274 | dev = [
275 | { name = "coverage", extra = ["toml"] },
276 | { name = "coveralls" },
277 | { name = "mdformat" },
278 | { name = "mypy" },
279 | { name = "pip-audit" },
280 | { name = "pre-commit" },
281 | { name = "pygobject-stubs" },
282 | { name = "pytest" },
283 | { name = "pytest-cov" },
284 | { name = "ruff" },
285 | { name = "tbump" },
286 | { name = "types-toml" },
287 | ]
288 |
289 | [package.metadata]
290 | requires-dist = [{ name = "pygobject", specifier = ">=3.42.2" }]
291 |
292 | [package.metadata.requires-dev]
293 | dev = [
294 | { name = "coverage", extras = ["toml"], specifier = ">=6.5" },
295 | { name = "coveralls" },
296 | { name = "mdformat" },
297 | { name = "mypy" },
298 | { name = "pip-audit" },
299 | { name = "pre-commit" },
300 | { name = "pygobject-stubs" },
301 | { name = "pytest" },
302 | { name = "pytest-cov" },
303 | { name = "ruff" },
304 | { name = "tbump" },
305 | { name = "types-toml" },
306 | ]
307 |
308 | [[package]]
309 | name = "license-expression"
310 | version = "30.4.1"
311 | source = { registry = "https://pypi.org/simple" }
312 | dependencies = [
313 | { name = "boolean-py" },
314 | ]
315 | sdist = { url = "https://files.pythonhosted.org/packages/74/6f/8709031ea6e0573e6075d24ea34507b0eb32f83f10e1420f2e34606bf0da/license_expression-30.4.1.tar.gz", hash = "sha256:9f02105f9e0fcecba6a85dfbbed7d94ea1c3a70cf23ddbfb5adf3438a6f6fce0", size = 177184 }
316 | wheels = [
317 | { url = "https://files.pythonhosted.org/packages/53/84/8a89614b2e7eeeaf0a68a4046d6cfaea4544c8619ea02595ebeec9b2bae3/license_expression-30.4.1-py3-none-any.whl", hash = "sha256:679646bc3261a17690494a3e1cada446e5ee342dbd87dcfa4a0c24cc5dce13ee", size = 111457 },
318 | ]
319 |
320 | [[package]]
321 | name = "markdown-it-py"
322 | version = "3.0.0"
323 | source = { registry = "https://pypi.org/simple" }
324 | dependencies = [
325 | { name = "mdurl" },
326 | ]
327 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
328 | wheels = [
329 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
330 | ]
331 |
332 | [[package]]
333 | name = "mdformat"
334 | version = "0.7.22"
335 | source = { registry = "https://pypi.org/simple" }
336 | dependencies = [
337 | { name = "markdown-it-py" },
338 | ]
339 | sdist = { url = "https://files.pythonhosted.org/packages/fc/eb/b5cbf2484411af039a3d4aeb53a5160fae25dd8c84af6a4243bc2f3fedb3/mdformat-0.7.22.tar.gz", hash = "sha256:eef84fa8f233d3162734683c2a8a6222227a229b9206872e6139658d99acb1ea", size = 34610 }
340 | wheels = [
341 | { url = "https://files.pythonhosted.org/packages/f2/6f/94a7344f6d634fe3563bea8b33bccedee37f2726f7807e9a58440dc91627/mdformat-0.7.22-py3-none-any.whl", hash = "sha256:61122637c9e1d9be1329054f3fa216559f0d1f722b7919b060a8c2a4ae1850e5", size = 34447 },
342 | ]
343 |
344 | [[package]]
345 | name = "mdurl"
346 | version = "0.1.2"
347 | source = { registry = "https://pypi.org/simple" }
348 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
349 | wheels = [
350 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
351 | ]
352 |
353 | [[package]]
354 | name = "msgpack"
355 | version = "1.1.0"
356 | source = { registry = "https://pypi.org/simple" }
357 | sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 }
358 | wheels = [
359 | { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 },
360 | { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 },
361 | { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 },
362 | { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 },
363 | { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 },
364 | { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 },
365 | { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 },
366 | { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 },
367 | { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 },
368 | { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 },
369 | { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 },
370 | { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 },
371 | { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 },
372 | { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 },
373 | { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 },
374 | { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 },
375 | { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 },
376 | { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 },
377 | { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 },
378 | { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 },
379 | { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 },
380 | { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 },
381 | { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 },
382 | { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 },
383 | { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 },
384 | { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 },
385 | { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 },
386 | { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 },
387 | { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 },
388 | { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 },
389 | { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 },
390 | { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 },
391 | { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 },
392 | ]
393 |
394 | [[package]]
395 | name = "mypy"
396 | version = "1.15.0"
397 | source = { registry = "https://pypi.org/simple" }
398 | dependencies = [
399 | { name = "mypy-extensions" },
400 | { name = "typing-extensions" },
401 | ]
402 | sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
403 | wheels = [
404 | { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 },
405 | { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 },
406 | { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 },
407 | { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 },
408 | { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 },
409 | { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 },
410 | { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 },
411 | { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 },
412 | { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 },
413 | { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 },
414 | { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 },
415 | { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 },
416 | { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
417 | { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
418 | { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
419 | { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
420 | { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
421 | { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
422 | { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
423 | ]
424 |
425 | [[package]]
426 | name = "mypy-extensions"
427 | version = "1.0.0"
428 | source = { registry = "https://pypi.org/simple" }
429 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
430 | wheels = [
431 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
432 | ]
433 |
434 | [[package]]
435 | name = "nodeenv"
436 | version = "1.9.1"
437 | source = { registry = "https://pypi.org/simple" }
438 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
439 | wheels = [
440 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
441 | ]
442 |
443 | [[package]]
444 | name = "packageurl-python"
445 | version = "0.16.0"
446 | source = { registry = "https://pypi.org/simple" }
447 | sdist = { url = "https://files.pythonhosted.org/packages/68/7d/0bd319dc94c7956b4d864e87d3dc03739f125ce174671e3128edd566a63e/packageurl_python-0.16.0.tar.gz", hash = "sha256:69e3bf8a3932fe9c2400f56aaeb9f86911ecee2f9398dbe1b58ec34340be365d", size = 40492 }
448 | wheels = [
449 | { url = "https://files.pythonhosted.org/packages/c4/47/3c197fb7596a813afef2e4198d507b761aed2c7150d90be64dff9fe0ea71/packageurl_python-0.16.0-py3-none-any.whl", hash = "sha256:5c3872638b177b0f1cf01c3673017b7b27ebee485693ae12a8bed70fa7fa7c35", size = 28544 },
450 | ]
451 |
452 | [[package]]
453 | name = "packaging"
454 | version = "24.2"
455 | source = { registry = "https://pypi.org/simple" }
456 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
457 | wheels = [
458 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
459 | ]
460 |
461 | [[package]]
462 | name = "pip"
463 | version = "25.0.1"
464 | source = { registry = "https://pypi.org/simple" }
465 | sdist = { url = "https://files.pythonhosted.org/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850 }
466 | wheels = [
467 | { url = "https://files.pythonhosted.org/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f", size = 1841526 },
468 | ]
469 |
470 | [[package]]
471 | name = "pip-api"
472 | version = "0.0.34"
473 | source = { registry = "https://pypi.org/simple" }
474 | dependencies = [
475 | { name = "pip" },
476 | ]
477 | sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017 }
478 | wheels = [
479 | { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369 },
480 | ]
481 |
482 | [[package]]
483 | name = "pip-audit"
484 | version = "2.8.0"
485 | source = { registry = "https://pypi.org/simple" }
486 | dependencies = [
487 | { name = "cachecontrol", extra = ["filecache"] },
488 | { name = "cyclonedx-python-lib" },
489 | { name = "packaging" },
490 | { name = "pip-api" },
491 | { name = "pip-requirements-parser" },
492 | { name = "platformdirs" },
493 | { name = "requests" },
494 | { name = "rich" },
495 | { name = "toml" },
496 | ]
497 | sdist = { url = "https://files.pythonhosted.org/packages/e8/c8/44ccea85bd2024f1ebe55eb6cdaf1f2183359176689eed3c0b01926c24ad/pip_audit-2.8.0.tar.gz", hash = "sha256:9816cbd94de6f618a8965c117433006b3d565a708dc05d5a7be47ab65b66fa05", size = 51073 }
498 | wheels = [
499 | { url = "https://files.pythonhosted.org/packages/11/0c/be5c42643284b2cfc5d9d36b576b7465268a163bd7df481a3979a3d87a0b/pip_audit-2.8.0-py3-none-any.whl", hash = "sha256:200f50d56cb6fba3a9189c20d53250354f72f161d63b6ef77ae5de2b53044566", size = 57002 },
500 | ]
501 |
502 | [[package]]
503 | name = "pip-requirements-parser"
504 | version = "32.0.1"
505 | source = { registry = "https://pypi.org/simple" }
506 | dependencies = [
507 | { name = "packaging" },
508 | { name = "pyparsing" },
509 | ]
510 | sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359 }
511 | wheels = [
512 | { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648 },
513 | ]
514 |
515 | [[package]]
516 | name = "platformdirs"
517 | version = "4.3.7"
518 | source = { registry = "https://pypi.org/simple" }
519 | sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 }
520 | wheels = [
521 | { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 },
522 | ]
523 |
524 | [[package]]
525 | name = "pluggy"
526 | version = "1.5.0"
527 | source = { registry = "https://pypi.org/simple" }
528 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
529 | wheels = [
530 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
531 | ]
532 |
533 | [[package]]
534 | name = "pre-commit"
535 | version = "4.2.0"
536 | source = { registry = "https://pypi.org/simple" }
537 | dependencies = [
538 | { name = "cfgv" },
539 | { name = "identify" },
540 | { name = "nodeenv" },
541 | { name = "pyyaml" },
542 | { name = "virtualenv" },
543 | ]
544 | sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 }
545 | wheels = [
546 | { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 },
547 | ]
548 |
549 | [[package]]
550 | name = "py-serializable"
551 | version = "1.1.2"
552 | source = { registry = "https://pypi.org/simple" }
553 | dependencies = [
554 | { name = "defusedxml" },
555 | ]
556 | sdist = { url = "https://files.pythonhosted.org/packages/16/cf/6e482507764034d6c41423a19f33fdd59655052fdb2ca4358faa3b0bcfd1/py_serializable-1.1.2.tar.gz", hash = "sha256:89af30bc319047d4aa0d8708af412f6ce73835e18bacf1a080028bb9e2f42bdb", size = 55844 }
557 | wheels = [
558 | { url = "https://files.pythonhosted.org/packages/30/f2/3483060562245668bb07193b65277f0ea619cabf530deb351911eb0453eb/py_serializable-1.1.2-py3-none-any.whl", hash = "sha256:801be61b0a1ba64c3861f7c624f1de5cfbbabf8b458acc9cdda91e8f7e5effa1", size = 22786 },
559 | ]
560 |
561 | [[package]]
562 | name = "pycairo"
563 | version = "1.27.0"
564 | source = { registry = "https://pypi.org/simple" }
565 | sdist = { url = "https://files.pythonhosted.org/packages/07/4a/42b26390181a7517718600fa7d98b951da20be982a50cd4afb3d46c2e603/pycairo-1.27.0.tar.gz", hash = "sha256:5cb21e7a00a2afcafea7f14390235be33497a2cce53a98a19389492a60628430", size = 661450 }
566 | wheels = [
567 | { url = "https://files.pythonhosted.org/packages/3b/4b/d7887904e10673ff8d99aaa2e63f82b245441261ea29136254576ac1c730/pycairo-1.27.0-cp311-cp311-win32.whl", hash = "sha256:9a9b79f92a434dae65c34c830bb9abdbd92654195e73d52663cbe45af1ad14b2", size = 749772 },
568 | { url = "https://files.pythonhosted.org/packages/90/d2/ae7c781ceaac315e7c80381f83dc779a591bde6892e3498c7b5f42ec6cb8/pycairo-1.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:d40a6d80b15dacb3672dc454df4bc4ab3988c6b3f36353b24a255dc59a1c8aea", size = 844101 },
569 | { url = "https://files.pythonhosted.org/packages/1c/65/7664fe3d9928572b7804c651a99cfd6113338eee7436d5b25401c9382619/pycairo-1.27.0-cp312-cp312-win32.whl", hash = "sha256:e2239b9bb6c05edae5f3be97128e85147a155465e644f4d98ea0ceac7afc04ee", size = 750005 },
570 | { url = "https://files.pythonhosted.org/packages/f4/bd/114597b9f79fbdb4eb0a4bf4aa54e70246d87f512a880e66a85c1e2ff407/pycairo-1.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:27cb4d3a80e3b9990af552818515a8e466e0317063a6e61585533f1a86f1b7d5", size = 844100 },
571 | { url = "https://files.pythonhosted.org/packages/93/76/35d2feef50584cb00d2b4d2215337b0bc765508f8856735a41bfedcb4699/pycairo-1.27.0-cp313-cp313-win32.whl", hash = "sha256:01505c138a313df2469f812405963532fc2511fb9bca9bdc8e0ab94c55d1ced8", size = 750001 },
572 | { url = "https://files.pythonhosted.org/packages/9c/e7/92d6e57deee53229bb8b3f7df6d02c503585be7bdd69cb9e54f34aab089b/pycairo-1.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:b0349d744c068b6644ae23da6ada111c8a8a7e323b56cbce3707cba5bdb474cc", size = 844102 },
573 | ]
574 |
575 | [[package]]
576 | name = "pygments"
577 | version = "2.19.1"
578 | source = { registry = "https://pypi.org/simple" }
579 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
580 | wheels = [
581 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
582 | ]
583 |
584 | [[package]]
585 | name = "pygobject"
586 | version = "3.52.3"
587 | source = { registry = "https://pypi.org/simple" }
588 | dependencies = [
589 | { name = "pycairo" },
590 | ]
591 | sdist = { url = "https://files.pythonhosted.org/packages/4a/36/fec530a313d3d48f12e112ac0a65ee3ccc87f385123a0493715609e8e99c/pygobject-3.52.3.tar.gz", hash = "sha256:00e427d291e957462a8fad659a9f9c8be776ff82a8b76bdf402f1eaeec086d82", size = 1235825 }
592 |
593 | [[package]]
594 | name = "pygobject-stubs"
595 | version = "2.13.0"
596 | source = { registry = "https://pypi.org/simple" }
597 | sdist = { url = "https://files.pythonhosted.org/packages/d1/3f/d9a43ab76ad7a2d6d3a2968513b76760100c33128c6a0d3ac996dfb37c77/pygobject_stubs-2.13.0.tar.gz", hash = "sha256:4f608f5dfe10c3173f0a082416e22e27b693743c2a635de245c78a51458e2ab6", size = 870193 }
598 |
599 | [[package]]
600 | name = "pyparsing"
601 | version = "3.2.1"
602 | source = { registry = "https://pypi.org/simple" }
603 | sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 }
604 | wheels = [
605 | { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 },
606 | ]
607 |
608 | [[package]]
609 | name = "pytest"
610 | version = "8.3.5"
611 | source = { registry = "https://pypi.org/simple" }
612 | dependencies = [
613 | { name = "colorama", marker = "sys_platform == 'win32'" },
614 | { name = "iniconfig" },
615 | { name = "packaging" },
616 | { name = "pluggy" },
617 | ]
618 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
619 | wheels = [
620 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
621 | ]
622 |
623 | [[package]]
624 | name = "pytest-cov"
625 | version = "6.0.0"
626 | source = { registry = "https://pypi.org/simple" }
627 | dependencies = [
628 | { name = "coverage", extra = ["toml"] },
629 | { name = "pytest" },
630 | ]
631 | sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 }
632 | wheels = [
633 | { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
634 | ]
635 |
636 | [[package]]
637 | name = "pyyaml"
638 | version = "6.0.2"
639 | source = { registry = "https://pypi.org/simple" }
640 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
641 | wheels = [
642 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
643 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
644 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
645 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
646 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
647 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
648 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
649 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
650 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
651 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
652 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
653 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
654 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
655 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
656 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
657 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
658 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
659 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
660 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
661 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
662 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
663 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
664 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
665 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
666 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
667 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
668 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
669 | ]
670 |
671 | [[package]]
672 | name = "requests"
673 | version = "2.32.3"
674 | source = { registry = "https://pypi.org/simple" }
675 | dependencies = [
676 | { name = "certifi" },
677 | { name = "charset-normalizer" },
678 | { name = "idna" },
679 | { name = "urllib3" },
680 | ]
681 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
682 | wheels = [
683 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
684 | ]
685 |
686 | [[package]]
687 | name = "rich"
688 | version = "13.9.4"
689 | source = { registry = "https://pypi.org/simple" }
690 | dependencies = [
691 | { name = "markdown-it-py" },
692 | { name = "pygments" },
693 | ]
694 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
695 | wheels = [
696 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
697 | ]
698 |
699 | [[package]]
700 | name = "ruff"
701 | version = "0.11.2"
702 | source = { registry = "https://pypi.org/simple" }
703 | sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 }
704 | wheels = [
705 | { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 },
706 | { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 },
707 | { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 },
708 | { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 },
709 | { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 },
710 | { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 },
711 | { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 },
712 | { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 },
713 | { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 },
714 | { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 },
715 | { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 },
716 | { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 },
717 | { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 },
718 | { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 },
719 | { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 },
720 | { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 },
721 | { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 },
722 | ]
723 |
724 | [[package]]
725 | name = "schema"
726 | version = "0.7.7"
727 | source = { registry = "https://pypi.org/simple" }
728 | sdist = { url = "https://files.pythonhosted.org/packages/d4/01/0ea2e66bad2f13271e93b729c653747614784d3ebde219679e41ccdceecd/schema-0.7.7.tar.gz", hash = "sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807", size = 44245 }
729 | wheels = [
730 | { url = "https://files.pythonhosted.org/packages/ad/1b/81855a88c6db2b114d5b2e9f96339190d5ee4d1b981d217fa32127bb00e0/schema-0.7.7-py2.py3-none-any.whl", hash = "sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde", size = 18632 },
731 | ]
732 |
733 | [[package]]
734 | name = "sortedcontainers"
735 | version = "2.4.0"
736 | source = { registry = "https://pypi.org/simple" }
737 | sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
738 | wheels = [
739 | { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
740 | ]
741 |
742 | [[package]]
743 | name = "tabulate"
744 | version = "0.9.0"
745 | source = { registry = "https://pypi.org/simple" }
746 | sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 }
747 | wheels = [
748 | { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 },
749 | ]
750 |
751 | [[package]]
752 | name = "tbump"
753 | version = "6.11.0"
754 | source = { registry = "https://pypi.org/simple" }
755 | dependencies = [
756 | { name = "cli-ui" },
757 | { name = "docopt" },
758 | { name = "schema" },
759 | { name = "tomlkit" },
760 | ]
761 | sdist = { url = "https://files.pythonhosted.org/packages/ab/1f/d02379532311192521a20b3597dc0f01bd37596e950a6cb40795ae9acb94/tbump-6.11.0.tar.gz", hash = "sha256:385e710eedf0a8a6ff959cf1e9f3cfd17c873617132fc0ec5f629af0c355c870", size = 28642 }
762 | wheels = [
763 | { url = "https://files.pythonhosted.org/packages/48/41/c21994a64efe86ed81c1a0935aeec840548839a37a7f3716f74a75e54fc2/tbump-6.11.0-py3-none-any.whl", hash = "sha256:6b181fe6f3ae84ce0b9af8cc2009a8bca41ded34e73f623a7413b9684f1b4526", size = 35607 },
764 | ]
765 |
766 | [[package]]
767 | name = "toml"
768 | version = "0.10.2"
769 | source = { registry = "https://pypi.org/simple" }
770 | sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 }
771 | wheels = [
772 | { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 },
773 | ]
774 |
775 | [[package]]
776 | name = "tomli"
777 | version = "2.2.1"
778 | source = { registry = "https://pypi.org/simple" }
779 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
780 | wheels = [
781 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
782 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
783 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
784 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
785 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
786 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
787 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
788 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
789 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
790 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
791 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
792 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
793 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
794 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
795 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
796 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
797 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
798 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
799 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
800 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
801 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
802 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
803 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
804 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
805 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
806 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
807 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
808 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
809 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
810 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
811 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
812 | ]
813 |
814 | [[package]]
815 | name = "tomlkit"
816 | version = "0.11.8"
817 | source = { registry = "https://pypi.org/simple" }
818 | sdist = { url = "https://files.pythonhosted.org/packages/10/37/dd53019ccb72ef7d73fff0bee9e20b16faff9658b47913a35d79e89978af/tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3", size = 188825 }
819 | wheels = [
820 | { url = "https://files.pythonhosted.org/packages/ef/a8/b1c193be753c02e2a94af6e37ee45d3378a74d44fe778c2434a63af92731/tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171", size = 35807 },
821 | ]
822 |
823 | [[package]]
824 | name = "types-toml"
825 | version = "0.10.8.20240310"
826 | source = { registry = "https://pypi.org/simple" }
827 | sdist = { url = "https://files.pythonhosted.org/packages/86/47/3e4c75042792bff8e90d7991aa5c51812cc668828cc6cce711e97f63a607/types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331", size = 4392 }
828 | wheels = [
829 | { url = "https://files.pythonhosted.org/packages/da/a2/d32ab58c0b216912638b140ab2170ee4b8644067c293b170e19fba340ccc/types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d", size = 4777 },
830 | ]
831 |
832 | [[package]]
833 | name = "typing-extensions"
834 | version = "4.12.2"
835 | source = { registry = "https://pypi.org/simple" }
836 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
837 | wheels = [
838 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
839 | ]
840 |
841 | [[package]]
842 | name = "unidecode"
843 | version = "1.3.8"
844 | source = { registry = "https://pypi.org/simple" }
845 | sdist = { url = "https://files.pythonhosted.org/packages/f7/89/19151076a006b9ac0dd37b1354e031f5297891ee507eb624755e58e10d3e/Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4", size = 192701 }
846 | wheels = [
847 | { url = "https://files.pythonhosted.org/packages/84/b7/6ec57841fb67c98f52fc8e4a2d96df60059637cba077edc569a302a8ffc7/Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39", size = 235494 },
848 | ]
849 |
850 | [[package]]
851 | name = "urllib3"
852 | version = "2.3.0"
853 | source = { registry = "https://pypi.org/simple" }
854 | sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
855 | wheels = [
856 | { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
857 | ]
858 |
859 | [[package]]
860 | name = "virtualenv"
861 | version = "20.29.3"
862 | source = { registry = "https://pypi.org/simple" }
863 | dependencies = [
864 | { name = "distlib" },
865 | { name = "filelock" },
866 | { name = "platformdirs" },
867 | ]
868 | sdist = { url = "https://files.pythonhosted.org/packages/c7/9c/57d19fa093bcf5ac61a48087dd44d00655f85421d1aa9722f8befbf3f40a/virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac", size = 4320280 }
869 | wheels = [
870 | { url = "https://files.pythonhosted.org/packages/c2/eb/c6db6e3001d58c6a9e67c74bb7b4206767caa3ccc28c6b9eaf4c23fb4e34/virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170", size = 4301458 },
871 | ]
872 |
--------------------------------------------------------------------------------