├── .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 | Tests passing 8 | License: MIT 9 | Code style: black 10 | Coverage Status 11 |

12 | 13 | ![Keyhint Screenshot](https://raw.githubusercontent.com/dynobo/keyhint/main/keyhint/resources/keyhint.png) 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 | ![WOMM](https://raw.githubusercontent.com/dynobo/lmdiag/master/badge.png) 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 | 7 | popover_menu 8 | 250 9 | 10 | 11 | popover_menu_box 12 | 1 13 | 16 | 17 | 18 | Section Order 19 | 22 | 23 | 24 | 25 | 26 | 0 27 | true 28 | 3 29 | 4 30 | 14 31 | 34 | 35 | 36 | Sort sections by height 37 | true 38 | win.sort_by 39 | 'size' 40 | 41 | 42 | By Size 43 | 44 | 45 | 46 | 47 | 48 | 49 | Sort sections alphabetically 50 | sort_by_size_button 51 | win.sort_by 52 | 'title' 53 | 54 | 55 | By Title 56 | 57 | 58 | 59 | 60 | 61 | 62 | Use section order from config files 63 | sort_by_size_button 64 | win.sort_by 65 | 'native' 66 | 67 | 68 | Native 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Scroll Orientation 78 | 81 | 82 | 83 | 84 | 85 | 0 86 | true 87 | 3 88 | 4 89 | 14 90 | 93 | 94 | 95 | Scroll left ↔ right 96 | win.orientation 97 | 'vertical' 98 | true 99 | 100 | 101 | Vertical 102 | 103 | 104 | 105 | 106 | 107 | 108 | Scroll up ↔ down 109 | win.orientation 110 | 'horizontal' 111 | scroll_vertical_button 112 | 113 | 114 | Horizontal 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | Zoom % 124 | 127 | 128 | 129 | 130 | 131 | Adjust font size 132 | 0 133 | 14 134 | 0 135 | 0 136 | 137 | 138 | 100 139 | 75 140 | 150 141 | 142 | 143 | 144 | 145 | 146 | 147 | Fallback Cheatsheet 148 | 151 | 152 | 153 | 154 | 155 | 0 156 | 3 157 | 4 158 | 12 159 | 162 | 163 | 164 | keyhint 165 | false 166 | Shown if no matching cheatsheet is found 167 | 168 | 169 | 170 | 171 | pin 172 | Set to current cheatsheet 173 | 174 | 175 | 176 | 177 | 178 | 179 | 4 180 | 6 181 | False 182 | 183 | 184 | 185 | 186 | 0 187 | win.open_folder 188 | 191 | 192 | 193 | Open Cheatsheet Folder... 194 | 0 195 | 196 | 197 | 198 | 199 | 200 | 201 | 0 202 | win.debug_info 203 | 206 | 207 | 208 | Show Debug Info 209 | 0 210 | 211 | 212 | 213 | 214 | 215 | 216 | 0 217 | win.about 218 | 221 | 222 | 223 | About Keyhint 224 | 0 225 | 226 | 227 | 228 | 229 | 230 | 231 | 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 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | 30 | 31 | 32 | 33 | 35 | 56 | 61 | 64 | 70 | 76 | 80 | 84 | 85 | 86 | 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 | 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 | --------------------------------------------------------------------------------