├── .github └── workflows │ └── main.yml ├── .gitignore ├── COPYING ├── README.md ├── build-aux └── meson │ └── postinstall.py ├── com.github.hezral.keystrokes.yml ├── data ├── application.css ├── com.github.hezral.keystrokes.appdata.xml.in ├── com.github.hezral.keystrokes.desktop.in ├── com.github.hezral.keystrokes.gschema.xml ├── icons │ ├── 128.png │ ├── 128.svg │ ├── 16.svg │ ├── 24.svg │ ├── 32.svg │ ├── 48.svg │ ├── 64.svg │ ├── key-press.svg │ ├── key-release.svg │ ├── meson.build │ ├── mouse-left-symbolic.svg │ ├── mouse-right-symbolic.svg │ ├── mouse-scrolldown-symbolic.svg │ ├── mouse-scrollleft-symbolic.svg │ ├── mouse-scrollright-symbolic.svg │ ├── mouse-scrollup-symbolic.svg │ ├── movements1.svg │ ├── movements2.svg │ ├── movements3.svg │ ├── movements4.svg │ ├── movements5.svg │ ├── movements6.svg │ └── settings-symbolic.svg ├── meson.build ├── screenshot-01.png └── screenshot-02.png ├── flow.drawio ├── meson.build ├── po ├── LINGUAS ├── POTFILES └── meson.build └── src ├── __init__.py ├── active_window_manager.py ├── custom_widgets.py ├── keystrokes.in ├── keystrokes_backend.py ├── main.py ├── meson.build ├── pynput_testing.py ├── utils.py ├── window.py ├── xfixes.py └── xinput.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # This workflow will run for any pull request or pushed commit 4 | on: [push, pull_request] 5 | 6 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 7 | jobs: 8 | # This workflow contains a single job called "flatpak" 9 | flatpak: 10 | # The type of runner that the job will run on 11 | runs-on: ubuntu-latest 12 | 13 | # This job runs in a special container designed for building Flatpaks for AppCenter 14 | container: 15 | image: ghcr.io/elementary/flatpak-platform/runtime:6 16 | options: --privileged 17 | 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so the job can access it 21 | - uses: actions/checkout@v2 22 | 23 | # Builds your flatpak manifest using the Flatpak Builder action 24 | - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4 25 | with: 26 | # This is the name of the Bundle file we're building and can be anything you like 27 | bundle: keystrokes.flatpak 28 | # This uses your app's RDNN ID 29 | manifest-path: com.github.hezral.keystrokes.yml 30 | 31 | # You can automatically run any of the tests you've created as part of this workflow 32 | run-tests: false 33 | 34 | # These lines specify the location of the elementary Runtime and Sdk 35 | repository-name: appcenter 36 | repository-url: https://flatpak.elementary.io/repo.flatpakrepo 37 | cache-key: "flatpak-builder-${{ github.sha }}" 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Flatpak 132 | .flatpak/ 133 | .flatpak-builder/ 134 | build-dir/ 135 | 136 | # Meson 137 | build/ 138 | 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![icon](data/icons/128.png) 4 | 5 |
6 | 7 | If you like what i make, it would really be nice to have someone buy me a coffee 8 |
9 | Buy Me A Coffee 10 |
11 | 12 | ### Simple on-screen keyboard and mouse keystrokes display 13 | 14 | | ![Screenshot](data/screenshot-01.png?raw=true) | ![Screenshot](data/screenshot-02.png?raw=true) | 15 | |------------------------------------------|-----------------------------------------| 16 | 17 | # Installation 18 | 19 | ## Build using flatpak 20 | * requires that you have flatpak-builder installed 21 | * flatpak enabled 22 | * flathub remote enabled 23 | 24 | ``` 25 | flatpak-builder --user --force-clean --install build-dir com.github.hezral.keystrokes.yml 26 | ``` 27 | 28 | ### Build using meson 29 | Ensure you have these dependencies installed 30 | 31 | * python3 32 | * python3-gi 33 | * libgranite-dev 34 | * python-xlib 35 | * pynput 36 | 37 | Download the updated source [here](https://github.com/hezral/keystrokes/archive/master.zip), or use git: 38 | ```bash 39 | git clone https://github.com/hezral/keystrokes.git 40 | cd clips 41 | meson build --prefix=/usr 42 | cd build 43 | ninja build 44 | sudo ninja install 45 | ``` 46 | The desktop launcher should show up on the application launcher for your desktop environment 47 | if it doesn't, try running 48 | ``` 49 | com.github.hezral.keystrokes 50 | ``` 51 | 52 | # Thanks/Credits 53 | - [ElementaryPython](https://github.com/mirkobrombin/ElementaryPython) This started me off on coding with Python and GTK 54 | - [Lazka's PyGObject API Reference](https://https://lazka.github.io) The best documentation site for coding with Python and GTK -------------------------------------------------------------------------------- /build-aux/meson/postinstall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from os import environ, path 4 | from subprocess import call 5 | 6 | prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local') 7 | datadir = path.join(prefix, 'share') 8 | destdir = environ.get('DESTDIR', '') 9 | 10 | # Package managers set this so we don't need to run 11 | if not destdir: 12 | print('Updating icon cache...') 13 | call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')]) 14 | 15 | print('Updating desktop database...') 16 | call(['update-desktop-database', '-q', path.join(datadir, 'applications')]) 17 | 18 | print('Compiling GSettings schemas...') 19 | call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')]) 20 | 21 | 22 | -------------------------------------------------------------------------------- /com.github.hezral.keystrokes.yml: -------------------------------------------------------------------------------- 1 | app-id: com.github.hezral.keystrokes 2 | runtime: io.elementary.Platform 3 | runtime-version: '7' 4 | sdk: io.elementary.Sdk 5 | command: keystrokes 6 | finish-args: 7 | - --share=ipc 8 | - --socket=wayland 9 | - --socket=fallback-x11 10 | - --device=dri 11 | 12 | # Gio.AppInfo workaround to get all installed apps based on common .desktop locations 13 | # read-only access to get installed app and icons based on .desktop files 14 | - --filesystem=host:ro 15 | - --filesystem=xdg-data:ro 16 | # read-only access to get installed system flatpak apps based on .desktop files 17 | - --filesystem=/var/lib/flatpak/app:ro #required as .desktop files in share/applications are symlinked to this dir 18 | - --filesystem=/var/lib/flatpak/exports/share:ro 19 | - --filesystem=/var/lib/flatpak/exports/share/applications:ro 20 | - --filesystem=/var/lib/flatpak/exports/share/icons:ro 21 | # read-only access to get installed snap apps based on .desktop files 22 | - --filesystem=/var/lib/snapd/desktop:ro 23 | # read-only access to get installed user flatpak apps based on .desktop files 24 | - --filesystem=~/.local/share/flatpak/exports/share/applications:ro 25 | - --filesystem=~/.local/share/flatpak/exports/share/icons:ro 26 | - --filesystem=xdg-data/flatpak/app:ro 27 | - --filesystem=xdg-data/flatpak/exports/share/applications:ro 28 | - --filesystem=xdg-data/flatpak/exports/share/icons:ro 29 | # read-only access to get installed system legacy apps based on .desktop files 30 | - --filesystem=/usr/share/applications:ro 31 | - --filesystem=/usr/share/icons:ro 32 | - --filesystem=/usr/share/pixmaps:ro 33 | # read-only access to get installed user legacy apps based on .desktop files 34 | - --filesystem=~/.local/share/applications:ro 35 | - --filesystem=~/.local/share/icons:ro 36 | 37 | modules: 38 | - name: python-xlib 39 | buildsystem: simple 40 | build-options: 41 | build-args: 42 | - --share=network 43 | build-commands: 44 | - "pip3 install --prefix=${FLATPAK_DEST} python-xlib" 45 | 46 | - name: pynput 47 | buildsystem: simple 48 | build-options: 49 | build-args: 50 | - --share=network 51 | build-commands: 52 | - "pip3 install --prefix=${FLATPAK_DEST} pynput" 53 | 54 | - name: chardet 55 | buildsystem: simple 56 | build-options: 57 | build-args: 58 | - --share=network 59 | build-commands: 60 | - "pip3 install --prefix=${FLATPAK_DEST} chardet" 61 | 62 | - name: keystrokes 63 | buildsystem: meson 64 | sources: 65 | - type: dir 66 | path: . 67 | -------------------------------------------------------------------------------- /data/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | # SPDX-FileCopyrightText: 2021 Adi Hezral 4 | */ 5 | 6 | headerbar#main button { 7 | color: white; 8 | } 9 | 10 | headerbar#main { 11 | background-image: none; 12 | border-color: transparent; 13 | } 14 | 15 | window#main, headerbar#main { 16 | box-shadow: none; 17 | border-radius: 16px; 18 | } 19 | 20 | headerbar#main.titlebar { 21 | background-color: transparent; 22 | } 23 | 24 | decoration {box-shadow: 0 0 0 0px rgba(0,0,0,0), 0 0px 0px rgba(0,0,0,0), 0 0px 0px rgba(0,0,0,0), 0 0px 16px rgba(0,0,0,0);} 25 | decoration-overlay {box-shadow: 0 0 0 1px rgba(0,0,0,0), 0 13px 16px 4px rgba(0,0,0,0), 0 3px 4px rgba(0,0,0,0), 0 3px 3px -3px rgba(0,0,0,0);} 26 | 27 | .custom-decoration > decoration {box-shadow: 0 0 0 1px rgba(0,0,0,0.75), 0 13px 16px 4px rgba(0,0,0,0), 0 2px 4px 2px rgba(0,0,0,0.2), 0 15px 12px -10px rgba(0,0,0,0.5), 0 8px 14px 4px rgba(0,0,0,0.25);} 28 | .custom-decoration-overlay > decoration-overlay {box-shadow: 0 -1px rgba(255,255,255,0.04) inset, 0 1px rgba(255,255,255,0.06) inset, 1px 0 rgba(255,255,255,0.014) inset, -1px 0 rgba(255,255,255,0.014) inset;} 29 | .custom-decoration > decoration:backdrop {box-shadow: 0 0 0 1px rgba(0,0,0,0.75), 0 13px 16px 4px rgba(0,0,0,0), 0 3px 4px rgba(0,0,0,0.25), 0 3px 3px -3px rgba(0,0,0,0.45);} 30 | .custom-decoration-overlay > decoration-overlay:backdrop {box-shadow: 0 -1px rgba(255,255,255,0.04) inset, 0 1px rgba(255,255,255,0.06) inset, 1px 0 rgba(255,255,255,0.014) inset, -1px 0 rgba(255,255,255,0.14) inset;} 31 | 32 | @keyframes crossfader { 33 | 0% { opacity: 0; } 34 | 03.33% { opacity: 0; } 35 | 06.66% { opacity: 0; } 36 | 09.99% { opacity: 0; } 37 | 13.33% { opacity: 0; } 38 | 16.65% { opacity: 0.75; } 39 | 100% { opacity: 1; } 40 | } 41 | 42 | .pressed { 43 | font-weight: bolder; 44 | color: rgb(42,42,42); 45 | text-shadow: 0px 1px 1px rgba(255, 255, 255, 1); 46 | background: rgb(198,198,198); 47 | background: shade(@theme_bg_color,0.5); 48 | background: 49 | linear-gradient( 50 | 180deg, 51 | rgba(198,198,198,1) 0%, 52 | rgba(198,198,198,1) 35%, 53 | rgba(223,223,223,1) 100%); 54 | border-top-style: solid; 55 | border-bottom-style: solid; 56 | border-top-color: white; 57 | border-bottom-color: gray; 58 | border-bottom-width: 5px; 59 | border-top-width: 2px; 60 | border-radius: 6px; 61 | box-shadow: 62 | 0 0 0 1px rgba(0,0,0,0.1), 63 | 0 2px 3px rgba(0,0,0,0.2), 64 | 0 2px 5px rgba(0,0,0,0.3), 65 | 0 14px 28px rgba(0,0,0,0); 66 | } 67 | 68 | .released { 69 | font-weight: bolder; 70 | color: rgb(42,42,42); 71 | text-shadow: 0px 1px 1px rgba(255, 255, 255, 1); 72 | background: rgb(198,198,198); 73 | background: shade(@theme_bg_color,0.5); 74 | background: 75 | linear-gradient( 76 | 180deg, 77 | rgba(198,198,198,1) 0%, 78 | rgba(198,198,198,1) 35%, 79 | rgba(223,223,223,1) 100%); 80 | border-top-style: solid; 81 | border-bottom-style: solid; 82 | border-top-color: white; 83 | border-bottom-color: gray; 84 | border-bottom-width: 5px; 85 | border-top-width: 2px; 86 | border-radius: 6px; 87 | box-shadow: 88 | 0 0 0 1px rgba(0,0,0,0.1), 89 | 0 2px 3px rgba(0,0,0,0.2), 90 | 0 2px 5px rgba(0,0,0,0.3), 91 | 0 14px 28px rgba(0,0,0,0); 92 | } 93 | 94 | grid#key-container { 95 | font-weight: bolder; 96 | color: rgb(42,42,42); 97 | text-shadow: 0px 1px 1px rgba(255, 255, 255, 1); 98 | /* / background: rgb(198,198,198); */ 99 | /* background: 100 | linear-gradient( 101 | 180deg, 102 | rgba(198,198,198,1) 0%, 103 | rgba(198,198,198,1) 35%, 104 | rgba(223,223,223,1) 100%); */ 105 | /* background: shade(@theme_bg_color,0.5); 106 | background: 107 | linear-gradient( 108 | 180deg, 109 | alpha(@theme_bg_color, 0.06) 0%, 110 | alpha(@theme_bg_color, 0.07) 100%); */ 111 | border-top-style: solid; 112 | border-bottom-style: solid; 113 | border-top-color: white; 114 | border-bottom-color: gray; 115 | border-bottom-width: 5px; 116 | border-top-width: 2px; 117 | border-radius: 6px; 118 | box-shadow: 119 | 0 0 0 1px rgba(0,0,0,0.1), 120 | 0 2px 3px rgba(0,0,0,0.2), 121 | 0 2px 5px rgba(0,0,0,0.3), 122 | 0 14px 28px rgba(0,0,0,0); 123 | /* animation: crossfader 0.25s ease-in-out forwards; */ 124 | } 125 | 126 | grid#mouse-container { 127 | font-weight: bolder; 128 | color: rgb(42,42,42); 129 | text-shadow: 0px 1px 1px rgba(255, 255, 255, 1); 130 | background: rgb(224, 224, 224); 131 | background: 132 | linear-gradient( 133 | 180deg, 134 | rgba(223,223,223,1) 0%, 135 | rgba(224, 224, 224, 1) 35%, 136 | rgba(223,223,223,1) 100%); 137 | border-top-style: solid; 138 | border-bottom-style: solid; 139 | border-top-color: white; 140 | border-bottom-color: gray; 141 | border-bottom-width: 4px; 142 | border-top-width: 2px; 143 | border-top-left-radius: 56px; 144 | border-top-right-radius: 56px; 145 | border-bottom-left-radius: 56px; 146 | border-bottom-right-radius: 56px; 147 | box-shadow: 148 | 0 0 0 1px rgba(0,0,0,0.1), 149 | 0 2px 3px rgba(0,0,0,0.2), 150 | 0 2px 5px rgba(0,0,0,0.3), 151 | 0 14px 28px rgba(0,0,0,0); 152 | /* animation: crossfader 0.25s ease-in-out forwards; */ 153 | } 154 | 155 | label#single-key { 156 | font-size: 200%; 157 | } 158 | 159 | label#mod-key { 160 | font-size: 150%; 161 | } 162 | 163 | label#symbol { 164 | font-size: 150%; 165 | } 166 | 167 | 168 | label#symbol-large { 169 | font-size: 250%; 170 | } 171 | 172 | 173 | label#on-state { 174 | font-weight: bolder; 175 | text-shadow: 0px 0px 3px rgba(0,255,4,1); 176 | color: rgba(0,255,4,1); 177 | font-size: 300%; 178 | } 179 | 180 | label#default-state { 181 | /* text-shadow: 0px 0px 5px rgba(0,255,4,1); */ 182 | font-weight: normal; 183 | font-size: 300%; 184 | } 185 | 186 | label#movement { 187 | font-weight: bold; 188 | font-feature-settings: "tnum"; 189 | /* font-variant-numeric: tabular-nums; */ 190 | text-shadow: 0px 1px 3px rgba(0, 0, 0, 0.5); 191 | } 192 | 193 | label#standby { 194 | font-size: 300%; 195 | letter-spacing: 2px; 196 | animation: pulsating 2.25s ease-in-out infinite; 197 | text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5); 198 | } 199 | 200 | 201 | 202 | .screen-display { 203 | background-image: 204 | linear-gradient( 205 | to bottom, 206 | rgba(0,0,0,0.1), 207 | rgba(0,0,0,0.1) 208 | ); 209 | border-radius: 3px; 210 | border-color: rgba(0,0,0,0.01); 211 | box-shadow: 212 | 0 0 0 1px rgba(0,0,0,0.15), 213 | 0 2px 5px rgba(0,0,0,0.16), 214 | 0 2px 5px rgba(0,0,0,0.23), 215 | 0 14px 28px rgba(0,0,0,0); 216 | /* background-repeat: no-repeat; 217 | background-position: -32px -32px, -32px calc(100% + 32px), calc(100% + 32px) -32px, calc(100% + 32px) calc(100% + 32px), 0; 218 | background-size: 64px 64px, 64px 64px, 64px 64px, 64px 64px, cover; */ 219 | } 220 | 221 | .position-default { 222 | color: white; 223 | border-color: black; 224 | background-color: black; 225 | box-shadow: 226 | 0 0 0 1px rgba(0,0,0,0.5), 227 | 0 2px 5px rgba(0,0,0,0.2), 228 | 0 2px 5px rgba(0,0,0,0.4), 229 | 0 14px 28px rgba(0,0,0,0); 230 | opacity: 0; 231 | } 232 | 233 | .position-selected { 234 | opacity: 1; 235 | } 236 | 237 | .position-hover { 238 | opacity: 0.5; 239 | } 240 | 241 | @keyframes pulsating { 242 | 25% {opacity: 0.25;} 243 | 75% {opacity: 0.75;} 244 | 100% {opacity: 1.00;} 245 | } 246 | 247 | label#repeat-counter, grid#emblem { 248 | padding: 5px; 249 | padding-left: 9px; 250 | padding-right: 9px; 251 | border-radius: 14px; 252 | border-style: solid; 253 | border-width: 1px; 254 | border-color: alpha(@accent_color_700, 0.75); 255 | background-color: @accent_color_500; 256 | } 257 | 258 | grid#emblem { 259 | border-style: solid; 260 | border-width: 1px; 261 | border-color: alpha(@accent_color_700, 0.25); 262 | background-color: @accent_color_500; 263 | } 264 | 265 | 266 | label#settings-group-label { 267 | font-weight: bold; 268 | opacity: 0.75; 269 | } 270 | 271 | frame#settings-group-frame { 272 | border-radius: 4px; 273 | border-color: rgba(0, 0, 0, 0.3); 274 | background-color: @shaded_dark; 275 | } 276 | 277 | .settings-sub-label { 278 | /* font-style: italic; */ 279 | font-size: 0.9em; 280 | color: gray; 281 | } 282 | 283 | .animate-released { 284 | animation: pressed-effect 1.25s ease-in-out infinite; 285 | } 286 | 287 | @keyframes pressed-effect { 288 | 25% { 289 | margin: 4px; 290 | /* transform: scale(1.125); */ 291 | } 292 | 50% { 293 | margin: 0px; 294 | /* transform: scale(1.0); */ 295 | } 296 | 75% { 297 | margin: 2px; 298 | /* transform: scale(0.95); */ 299 | } 300 | } -------------------------------------------------------------------------------- /data/com.github.hezral.keystrokes.appdata.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.hezral.keystrokes 5 | CC0 6 | GPL-3.0+ 7 | Keystrokes 8 | Simple keystrokes on-screen display 9 | Adi Hezral 10 | 11 |

Simple on-screen keyboard and mouse keystrokes display

12 |
13 | 14 | 15 | com.github.hezral.keystrokes 16 | 17 | 18 | https://github.com/hezral/keystrokes 19 | https://github.com/hezral/keystrokes/issues 20 | https://github.com/hezral/keystrokes/issues/labels/bug 21 | 22 | 23 | 24 | Keystrokes Main Window 25 | https://github.com/hezral/keystrokes/blob/master/data/screenshot-01.png?raw=true 26 | 27 | 28 | Keystrokes In Action Window 29 | https://github.com/hezral/keystrokes/blob/master/data/screenshot-02.png?raw=true 30 | 31 | 32 | 33 | 34 | none 35 | none 36 | none 37 | none 38 | none 39 | none 40 | none 41 | none 42 | none 43 | none 44 | none 45 | none 46 | none 47 | none 48 | none 49 | none 50 | none 51 | none 52 | none 53 | none 54 | none 55 | none 56 | none 57 | none 58 | none 59 | none 60 | none 61 | 62 | 63 | 64 | 65 | 66 |

First release!

67 |

Fixed manifest and appdata

68 |
69 |
70 |
71 | 72 | 73 | #212121 74 | #FFFFFF 75 | 0.00 76 | pk_live_51J03F1AXT8Az6AYFEiPc5mgpv7vUSWDnw135TmPT4tUbB2CJLwG19e3NxEuC7Zbz3VkXg7BygUQOTf7x6UDhSAn8005gq9ywZy 77 | 78 | 79 |
80 | -------------------------------------------------------------------------------- /data/com.github.hezral.keystrokes.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Keystrokes 3 | Exec=keystrokes 4 | Icon=com.github.hezral.keystrokes 5 | Terminal=false 6 | Type=Application 7 | Categories=Accessibility; 8 | StartupNotify=true 9 | -------------------------------------------------------------------------------- /data/com.github.hezral.keystrokes.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | Display app window on all workspaces 7 | Display Clips app window on all workspaces 8 | 9 | 10 | 1 11 | Screen position 12 | Screen position based on Gdk.Gravity values 13 | 14 | 15 | true 16 | Revert to initial position 17 | Revert to initial position 18 | 19 | 20 | true 21 | Revert to initial fixed position 22 | Revert to initial fixed position 23 | 24 | 25 | false 26 | Revert to initial last position 27 | Revert to initial last position 28 | 29 | 30 | true 31 | Detects words and group it to display 32 | Detects words and group it to display 33 | 34 | 35 | false 36 | Monitor repeated key press (experimental) 37 | Monitor repeated key press (experimental) 38 | 39 | 40 | false 41 | Monitor key press 42 | Monitor key press 43 | 44 | 45 | true 46 | Monitor key press 47 | Monitor key press 48 | 49 | 50 | true 51 | Monitor mouse clicks 52 | Monitor mouse clicks 53 | 54 | 55 | false 56 | Monitor mouse scrolls 57 | Monitor mouse scrolls 58 | 59 | 60 | false 61 | Monitor mouse scrolls 62 | Monitor mouse scrolls 63 | 64 | 65 | false 66 | Only show one key at a time 67 | Only show one key at a time 68 | 69 | 70 | 1000 71 | How long to until key is removed 72 | How long to until key is removed 73 | 74 | 75 | 1000 76 | How long to until repeat stops 77 | How long to until repeat stops 78 | 79 | 80 | 75 81 | Customize window transparency 82 | Customize window transparency 83 | 84 | 85 | "000000" 86 | display window color 87 | Customize display window color 88 | 89 | 90 | "upper-left" 91 | current position 92 | current position of main window 93 | 94 | 95 | 0 96 | Horizontal position 97 | Saved horizontal position of main window 98 | 99 | 100 | 0 101 | Vertical position 102 | Saved vertical position of main window 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /data/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezral/keystrokes/0d7cbb3d19d8d8d869494c77c087e1da875323f0/data/icons/128.png -------------------------------------------------------------------------------- /data/icons/128.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /data/icons/16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /data/icons/24.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /data/icons/32.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /data/icons/48.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /data/icons/64.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /data/icons/key-press.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /data/icons/key-release.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | application_id = 'com.github.hezral.keystrokes' 2 | 3 | scalable_dir = join_paths('hicolor', 'scalable', 'apps') 4 | install_data( 5 | join_paths(scalable_dir, ('@0@.svg').format(application_id)), 6 | install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) 7 | ) 8 | 9 | symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') 10 | install_data( 11 | join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)), 12 | install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir) 13 | ) 14 | -------------------------------------------------------------------------------- /data/icons/mouse-scrolldown-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/icons/mouse-scrollleft-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/icons/mouse-scrollright-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/icons/mouse-scrollup-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/icons/movements1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /data/icons/movements2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/icons/movements3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/icons/movements4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/icons/movements5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/icons/movements6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/icons/settings-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 14 | 15 | 17 | image/svg+xml 18 | 20 | 21 | 22 | 23 | 24 | 26 | 30 | 31 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | desktop_file = i18n.merge_file( 2 | input: 'com.github.hezral.keystrokes.desktop.in', 3 | output: 'com.github.hezral.keystrokes.desktop', 4 | type: 'desktop', 5 | po_dir: '../po', 6 | install: true, 7 | install_dir: join_paths(get_option('datadir'), 'applications') 8 | ) 9 | 10 | desktop_utils = find_program('desktop-file-validate', required: false) 11 | if desktop_utils.found() 12 | test('Validate desktop file', desktop_utils, 13 | args: [desktop_file] 14 | ) 15 | endif 16 | 17 | appstream_file = i18n.merge_file( 18 | input: 'com.github.hezral.keystrokes.appdata.xml.in', 19 | output: 'com.github.hezral.keystrokes.appdata.xml', 20 | po_dir: '../po', 21 | install: true, 22 | install_dir: join_paths(get_option('datadir'), 'metainfo') 23 | ) 24 | 25 | appstream_util = find_program('appstream-util', required: false) 26 | if appstream_util.found() 27 | test('Validate appstream file', appstream_util, 28 | args: ['validate', appstream_file] 29 | ) 30 | endif 31 | 32 | install_data('com.github.hezral.keystrokes.gschema.xml', 33 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 34 | ) 35 | 36 | compile_schemas = find_program('glib-compile-schemas', required: false) 37 | if compile_schemas.found() 38 | test('Validate schema file', compile_schemas, 39 | args: ['--strict', '--dry-run', meson.current_source_dir()] 40 | ) 41 | endif 42 | 43 | 44 | install_data('application.css', 45 | install_dir: join_paths(pkgdatadir, project_short_name, 'data') 46 | ) 47 | 48 | other_icon_sources = [ 49 | join_paths('icons', 'settings-symbolic' + '.svg'), 50 | join_paths('icons', 'mouse-left-symbolic.svg'), 51 | join_paths('icons', 'mouse-right-symbolic.svg'), 52 | join_paths('icons', 'mouse-scrollup-symbolic.svg'), 53 | join_paths('icons', 'mouse-scrolldown-symbolic.svg'), 54 | join_paths('icons', 'mouse-scrollleft-symbolic.svg'), 55 | join_paths('icons', 'mouse-scrollright-symbolic.svg'), 56 | join_paths('icons', 'key-press.svg'), 57 | join_paths('icons', 'key-release.svg'), 58 | join_paths('icons', 'movements1.svg'), 59 | join_paths('icons', 'movements2.svg'), 60 | join_paths('icons', 'movements3.svg'), 61 | join_paths('icons', 'movements4.svg'), 62 | join_paths('icons', 'movements5.svg'), 63 | join_paths('icons', 'movements6.svg'), 64 | ] 65 | iconsdir = join_paths(pkgdatadir, project_short_name, 'data', 'icons') 66 | install_data(other_icon_sources, install_dir: iconsdir) 67 | 68 | icon_sizes = ['16', '24', '32', '48', '64', '128'] 69 | foreach i : icon_sizes 70 | install_data( 71 | join_paths('icons', i + '.svg'), 72 | rename: meson.project_name() + '.svg', 73 | install_dir: join_paths(get_option ('datadir'), 'icons', 'hicolor', i + 'x' + i, 'apps') 74 | ) 75 | install_data( 76 | join_paths('icons', i + '.svg'), 77 | rename: meson.project_name() + '.svg', 78 | install_dir: join_paths(get_option ('datadir'), 'icons', 'hicolor', i + 'x' + i + '@2', 'apps') 79 | ) 80 | endforeach -------------------------------------------------------------------------------- /data/screenshot-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezral/keystrokes/0d7cbb3d19d8d8d869494c77c087e1da875323f0/data/screenshot-01.png -------------------------------------------------------------------------------- /data/screenshot-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezral/keystrokes/0d7cbb3d19d8d8d869494c77c087e1da875323f0/data/screenshot-02.png -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('com.github.hezral.keystrokes', 2 | version: '1.0.2', 3 | meson_version: '>= 0.50.0', 4 | default_options: [ 'warning_level=2', 5 | ], 6 | ) 7 | 8 | i18n = import('i18n') 9 | 10 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 11 | rdnn = meson.project_name().split('.') 12 | project_domain = '.'.join([rdnn[0],rdnn[1],rdnn[2]]) 13 | project_short_name = rdnn[3] 14 | 15 | subdir('data') 16 | subdir('src') 17 | subdir('po') 18 | 19 | meson.add_install_script('build-aux/meson/postinstall.py') 20 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezral/keystrokes/0d7cbb3d19d8d8d869494c77c087e1da875323f0/po/LINGUAS -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | data/com.github.hezral.keystrokes.desktop.in 2 | data/com.github.hezral.keystrokes.appdata.xml.in 3 | data/com.github.hezral.keystrokes.gschema.xml 4 | src/main.py 5 | src/window.py 6 | src/custom_widgets.py 7 | 8 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('keystrokes', preset: 'glib') 2 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezral/keystrokes/0d7cbb3d19d8d8d869494c77c087e1da875323f0/src/__init__.py -------------------------------------------------------------------------------- /src/active_window_manager.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | # SPDX-FileCopyrightText: 2021 Adi Hezral 3 | 4 | 5 | # pylint: disable=unused-import 6 | from contextlib import contextmanager 7 | from typing import Any, Dict, Optional, Tuple, Union # noqa 8 | 9 | from Xlib import X 10 | from Xlib.display import Display 11 | from Xlib.error import XError 12 | from Xlib.xobject.drawable import Window 13 | from Xlib.protocol.rq import Event 14 | 15 | import threading 16 | 17 | import gi 18 | from gi.repository import GLib 19 | 20 | from datetime import datetime 21 | 22 | class ActiveWindowManager(): 23 | # Based on code by Stephan Sokolow 24 | # Source: https://gist.github.com/ssokolow/e7c9aae63fb7973e4d64cff969a78ae8 25 | # Modified by hezral to add _get_window_class_name function 26 | 27 | """python-xlib example which reacts to changing the active window/title. 28 | 29 | Requires: 30 | - Python 31 | - python-xlib 32 | 33 | Tested with Python 2.x because my Kubuntu 14.04 doesn't come with python-xlib 34 | for Python 3.x. 35 | 36 | Design: 37 | ------- 38 | 39 | Any modern window manager that isn't horrendously broken maintains an X11 40 | property on the root window named _NET_ACTIVE_WINDOW. 41 | 42 | Any modern application toolkit presents the window title via a property 43 | named _NET_WM_NAME. 44 | 45 | This listens for changes to both of them and then hides duplicate events 46 | so it only reacts to title changes once. 47 | 48 | Known Bugs: 49 | ----------- 50 | 51 | - Under some circumstances, I observed that the first window creation and last 52 | window deletion on on an empty desktop (ie. not even a taskbar/panel) would 53 | go ignored when using this test setup: 54 | 55 | Xephyr :3 & 56 | DISPLAY=:3 openbox & 57 | DISPLAY=:3 python3 x11_watch_active_window.py 58 | 59 | # ...and then launch one or more of these in other terminals 60 | DISPLAY=:3 leafpad 61 | """ 62 | 63 | stop_thread = False 64 | id_thread = None 65 | callback = None 66 | 67 | def __init__(self, gtk_application=None): 68 | super().__init__() 69 | 70 | self.app = gtk_application 71 | 72 | # x11 = ctypes.cdll.LoadLibrary('libX11.so') 73 | # x11.XInitThreads() 74 | 75 | # Connect to the X server and get the root window 76 | self.disp = Display() 77 | self.root = self.disp.screen().root 78 | 79 | # Prepare the property names we use so they can be fed into X11 APIs 80 | self.NET_ACTIVE_WINDOW = self.disp.intern_atom('_NET_ACTIVE_WINDOW') 81 | self.NET_WM_NAME = self.disp.intern_atom('_NET_WM_NAME') # UTF-8 82 | self.WM_NAME = self.disp.intern_atom('WM_NAME') # Legacy encoding 83 | self.WM_CLASS = self.disp.intern_atom('WM_CLASS') 84 | 85 | self.last_seen = {'xid': None, 'title': None} # type: Dict[str, Any] 86 | 87 | def _run(self, callback=None): 88 | 89 | self.callback = callback 90 | 91 | def init_manager(): 92 | # Listen for _NET_ACTIVE_WINDOW changes 93 | self.root.change_attributes(event_mask=X.PropertyChangeMask) 94 | 95 | # Prime last_seen with whatever window was active when we started this 96 | self.get_window_name(self.get_active_window()[0]) 97 | self.handle_change(self.last_seen) 98 | 99 | while True: # next_event() sleeps until we get an event 100 | self.handle_xevent(self.disp.next_event()) 101 | if self.stop_thread: 102 | break 103 | 104 | self.thread = threading.Thread(target=init_manager) 105 | self.thread.daemon = True 106 | self.thread.start() 107 | print(datetime.now(), "active_window_manager started") 108 | 109 | def _stop(self): 110 | print(datetime.now(), "active_window_manager stopped") 111 | self.stop_thread = True 112 | 113 | @contextmanager 114 | def window_obj(self, win_id: Optional[int]) -> Window: 115 | """Simplify dealing with BadWindow (make it either valid or None)""" 116 | window_obj = None 117 | if win_id: 118 | try: 119 | window_obj = self.disp.create_resource_object('window', win_id) 120 | except XError: 121 | pass 122 | yield window_obj 123 | 124 | def get_active_window(self) -> Tuple[Optional[int], bool]: 125 | """Return a (window_obj, focus_has_changed) tuple for the active window.""" 126 | response = self.root.get_full_property(self.NET_ACTIVE_WINDOW, X.AnyPropertyType) 127 | if not response: 128 | return None, False 129 | win_id = response.value[0] 130 | 131 | focus_changed = (win_id != self.last_seen['xid']) 132 | if focus_changed: 133 | with self.window_obj(self.last_seen['xid']) as old_win: 134 | if old_win: 135 | old_win.change_attributes(event_mask=X.NoEventMask) 136 | 137 | self.last_seen['xid'] = win_id 138 | with self.window_obj(win_id) as new_win: 139 | if new_win: 140 | new_win.change_attributes(event_mask=X.PropertyChangeMask) 141 | 142 | return win_id, focus_changed 143 | 144 | def _get_window_name_inner(self, win_obj: Window) -> str: 145 | """Simplify dealing with _NET_WM_NAME (UTF-8) vs. WM_NAME (legacy)""" 146 | for atom in (self.NET_WM_NAME, self.WM_NAME): 147 | try: 148 | window_name = win_obj.get_full_property(atom, 0) 149 | except UnicodeDecodeError: # Apparently a Debian distro package bug 150 | title = "" 151 | else: 152 | if window_name: 153 | win_name = window_name.value # type: Union[str, bytes] 154 | if isinstance(win_name, bytes): 155 | # Apparently COMPOUND_TEXT is so arcane that this is how 156 | # tools like xprop deal with receiving it these days 157 | win_name = win_name.decode('latin1', 'replace') 158 | return win_name 159 | else: 160 | title = "" 161 | 162 | return "{} (XID: {})".format(title, win_obj.id) 163 | 164 | def _get_window_class_name(self, win_obj: Window) -> str: 165 | """SReturn window class name""" 166 | try: 167 | window_name = win_obj.get_full_property(self.WM_CLASS, 0) 168 | except UnicodeDecodeError: # Apparently a Debian distro package bug 169 | title = "" 170 | else: 171 | if window_name: 172 | win_class_name = window_name.value # type: Union[str, bytes] 173 | if isinstance(win_class_name, bytes): 174 | # Apparently COMPOUND_TEXT is so arcane that this is how 175 | # tools like xprop deal with receiving it these days 176 | win_class_name = win_class_name.replace(b'\x00',b' ').decode("utf-8").lower() 177 | return win_class_name 178 | else: 179 | title = "" 180 | 181 | return "{} (XID: {})".format(title, win_obj.id) 182 | 183 | def get_window_name(self, win_id: Optional[int]) -> Tuple[Optional[str], bool]: 184 | """ 185 | Look up the window class name for a given X11 window ID 186 | retrofitted to provide window class name instead of window title 187 | """ 188 | if not win_id: 189 | self.last_seen['title'] = None 190 | return self.last_seen['title'], True 191 | 192 | title_changed = False 193 | with self.window_obj(win_id) as wobj: 194 | if wobj: 195 | try: 196 | win_title = self._get_window_class_name(wobj) 197 | except XError: 198 | pass 199 | else: 200 | title_changed = (win_title != self.last_seen['title']) 201 | self.last_seen['title'] = win_title 202 | 203 | return self.last_seen['title'], title_changed 204 | 205 | def handle_xevent(self, event: Event): 206 | """Handler for X events which ignores anything but focus/title change""" 207 | if event.type != X.PropertyNotify: 208 | return 209 | 210 | changed = False 211 | if event.atom == self.NET_ACTIVE_WINDOW: 212 | if self.get_active_window()[1]: 213 | self.get_window_name(self.last_seen['xid']) # Rely on the side-effects 214 | changed = True 215 | elif event.atom in (self.NET_WM_NAME, self.WM_NAME): 216 | changed = changed or self.get_window_name(self.last_seen['xid'])[1] 217 | 218 | if changed: 219 | self.handle_change(self.last_seen) 220 | 221 | def handle_change(self, new_state: dict): 222 | """Replace this with whatever you want to actually do""" 223 | # active_app = self.app.utils.get_app_by_window_id(new_state['xid']) 224 | GLib.idle_add(self.callback, new_state['xid']) 225 | -------------------------------------------------------------------------------- /src/keystrokes.in: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | # SPDX-FileCopyrightText: 2021 Adi Hezral 5 | 6 | import os 7 | import sys 8 | import signal 9 | import gettext 10 | 11 | VERSION = '@VERSION@' 12 | pkgdatadir = '@pkgdatadir@' 13 | localedir = '@localedir@' 14 | 15 | sys.path.insert(1, pkgdatadir) 16 | signal.signal(signal.SIGINT, signal.SIG_DFL) 17 | gettext.install('keystrokes', localedir) 18 | 19 | if __name__ == '__main__': 20 | import gi 21 | 22 | from keystrokes import main 23 | print("Keystrokes", VERSION) 24 | sys.exit(main.main(VERSION)) 25 | -------------------------------------------------------------------------------- /src/keystrokes_backend.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | # SPDX-FileCopyrightText: 2021 Adi Hezral 3 | 4 | from pynput import keyboard, mouse 5 | 6 | from time import sleep 7 | 8 | import threading 9 | 10 | import logging 11 | logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(asctime)s, %(funcName)s:%(lineno)d: %(message)s") 12 | 13 | 14 | class KeyListener(): 15 | def __init__(self, on_press_callback, on_release_callback, *args, **kwargs): 16 | 17 | self.keyboard = keyboard 18 | 19 | self.listener = keyboard.Listener( 20 | on_press=on_press_callback, 21 | on_release=on_release_callback 22 | ) 23 | self.listener.start() 24 | 25 | logging.info("key listener started") 26 | 27 | 28 | class MouseListener(): 29 | 30 | stop_thread = False 31 | id_thread = None 32 | callback = None 33 | 34 | def __init__(self, on_move_callback, on_click_callback, on_scroll_callback, *args, **kwargs): 35 | 36 | self.mouse = mouse 37 | 38 | self.listener = self.mouse.Listener( 39 | on_move=on_move_callback, 40 | on_click=on_click_callback, 41 | on_scroll=on_scroll_callback) 42 | self.listener.start() 43 | 44 | logging.info("mouse listener started") 45 | 46 | 47 | # def _run(self, callback=None): 48 | 49 | # self.callback = callback 50 | 51 | # def init_monitor(): 52 | # while True: # next_event() sleeps until we get an event 53 | # logging.info("movements monitoring") 54 | # sleep(5) 55 | # if self.stop_thread: 56 | # break 57 | 58 | # self.thread = threading.Thread(target=init_monitor) 59 | # self.thread.daemon = True 60 | # self.thread.start() 61 | # logging.info("movements monitor started") 62 | 63 | # def _stop(self): 64 | # logging.info("movements monitor stopped") 65 | # self.stop_thread = True 66 | 67 | # class MouseController(): 68 | 69 | # def __init__(self, *args, **kwargs): 70 | 71 | # self.mouse = mouse 72 | 73 | # self.controller = self.mouse.Controller() 74 | 75 | # logging.info("mouse controller started") 76 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | # SPDX-FileCopyrightText: 2021 Adi Hezral 3 | 4 | import sys 5 | import os 6 | import gi 7 | 8 | gi.require_version('Handy', '1') 9 | gi.require_version('Gtk', '3.0') 10 | gi.require_version('Granite', '1.0') 11 | from gi.repository import Gtk, Gdk, Gio, Granite, GLib 12 | 13 | from .window import KeystrokesWindow 14 | from .keystrokes_backend import MouseListener, KeyListener 15 | from .active_window_manager import ActiveWindowManager 16 | from . import utils 17 | 18 | from datetime import datetime 19 | 20 | import logging 21 | logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(asctime)s, %(funcName)s:%(lineno)d: %(message)s") 22 | 23 | 24 | class Application(Gtk.Application): 25 | 26 | app_id = "com.github.hezral.keystrokes" 27 | granite_settings = Granite.Settings.get_default() 28 | gtk_settings = Gtk.Settings.get_default() 29 | gio_settings = Gio.Settings(schema_id=app_id) 30 | 31 | utils = utils 32 | 33 | main_window = None 34 | key_listener = None 35 | mouse_listener = None 36 | 37 | def __init__(self): 38 | super().__init__(application_id=self.app_id, 39 | flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE) 40 | 41 | self.add_main_option("test", ord("t"), GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Command line test", None) 42 | 43 | self.window_manager = ActiveWindowManager(gtk_application=self) 44 | 45 | prefers_color_scheme = self.granite_settings.get_prefers_color_scheme() 46 | self.gtk_settings.set_property("gtk-application-prefer-dark-theme", prefers_color_scheme) 47 | self.granite_settings.connect("notify::prefers-color-scheme", self.on_prefers_color_scheme) 48 | 49 | self.css_provider = Gtk.CssProvider() 50 | self.css_provider.load_from_path(os.path.join(os.path.dirname(__file__), "data", "application.css")) 51 | Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 52 | 53 | if "io.elementary.stylesheet" not in self.gtk_settings.props.gtk_theme_name: 54 | self.gtk_settings.set_property("gtk-theme-name", "io.elementary.stylesheet.blueberry") 55 | 56 | # prepend custom path for icon theme 57 | self.icon_theme = Gtk.IconTheme.get_default() 58 | self.icon_theme.prepend_search_path("/run/host/usr/share/pixmaps") 59 | self.icon_theme.prepend_search_path("/run/host/usr/share/icons") 60 | self.icon_theme.prepend_search_path("/var/lib/flatpak/exports/share/icons") 61 | self.icon_theme.prepend_search_path(os.path.join(GLib.get_home_dir(), ".local/share/flatpak/exports/share/icons")) 62 | self.icon_theme.prepend_search_path(os.path.join(os.path.dirname(__file__), "data", "icons")) 63 | 64 | def do_command_line(self, command_line): 65 | options = command_line.get_options_dict() 66 | # convert GVariantDict -> GVariant -> dict 67 | options = options.end().unpack() 68 | 69 | if "test" in options: 70 | # This is printed on the main instance 71 | print("Test argument recieved: %s" % options["test"]) 72 | 73 | self.activate() 74 | return 0 75 | 76 | 77 | def do_activate(self): 78 | if not self.main_window: 79 | self.main_window = KeystrokesWindow(application=self) 80 | self.main_window.present() 81 | 82 | GLib.timeout_add(500, self.setup_keyboard_listener, None) 83 | GLib.timeout_add(750, self.setup_mouse_listener, None) 84 | 85 | def on_prefers_color_scheme(self, *args): 86 | prefers_color_scheme = self.granite_settings.get_prefers_color_scheme() 87 | self.gtk_settings.set_property("gtk-application-prefer-dark-theme", prefers_color_scheme) 88 | 89 | def setup_keyboard_listener(self, *args): 90 | if self.key_listener is not None: 91 | self.key_listener.listener.stop() 92 | self.key_listener = None 93 | logging.info("key listener stopped") 94 | 95 | self.key_listener = KeyListener( 96 | on_press_callback=self.main_window.on_key_press, 97 | on_release_callback=self.main_window.on_key_release 98 | ) 99 | 100 | def setup_mouse_listener(self, *args): 101 | if self.mouse_listener is not None: 102 | self.mouse_listener.listener.stop() 103 | self.mouse_listener = None 104 | logging.info("mouse listener stopped") 105 | 106 | self.mouse_listener = MouseListener(self.main_window.on_mouse_move, self.main_window.on_mouse_click, self.main_window.on_mouse_scroll) 107 | 108 | # if self.gio_settings.get_value("monitor-movements"): 109 | # self.mouse_controller = MouseController() 110 | 111 | 112 | def main(version): 113 | app = Application() 114 | return app.run(sys.argv) 115 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | moduledir = join_paths(pkgdatadir, 'keystrokes') 3 | gnome = import('gnome') 4 | 5 | python = import('python') 6 | 7 | conf = configuration_data() 8 | conf.set('PYTHON', python.find_installation('python3').path()) 9 | conf.set('VERSION', meson.project_version()) 10 | conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) 11 | conf.set('pkgdatadir', pkgdatadir) 12 | 13 | configure_file( 14 | input: 'keystrokes.in', 15 | output: 'keystrokes', 16 | configuration: conf, 17 | install: true, 18 | install_dir: get_option('bindir') 19 | ) 20 | 21 | keystrokes_sources = [ 22 | '__init__.py', 23 | 'main.py', 24 | 'window.py', 25 | 'custom_widgets.py', 26 | 'keystrokes_backend.py', 27 | 'active_window_manager.py', 28 | 'utils.py' 29 | ] 30 | 31 | install_data(keystrokes_sources, install_dir: moduledir) -------------------------------------------------------------------------------- /src/pynput_testing.py: -------------------------------------------------------------------------------- 1 | from pynput import keyboard 2 | 3 | mod_state = False 4 | 5 | mod_keys = [keyboard.Key.shift, keyboard.Key.alt, keyboard.Key.ctrl, keyboard.Key.cmd, "<65032>"] 6 | mod_key_detected = [] 7 | 8 | def on_press(key): 9 | global mod_state 10 | global mod_key_detected 11 | 12 | if key in mod_keys: 13 | mod_state = True 14 | mod_key_detected.append(key) 15 | else: 16 | try: 17 | if mod_state: 18 | print("pressed", mod_key_detected, key) 19 | mod_state = False 20 | mod_key_detected = [] 21 | else: 22 | print("presed", key) 23 | except Exception as e: 24 | print(e) 25 | 26 | def on_release(key): 27 | global mod_state 28 | global mod_key_detected 29 | 30 | if key in mod_keys or str(key) in mod_keys: 31 | if mod_state: 32 | if len(mod_key_detected) == 1: 33 | print("released", key) 34 | else: 35 | print("released", mod_key_detected, key) 36 | mod_state = False 37 | mod_key_detected = [] 38 | else: 39 | print("released", key) 40 | 41 | 42 | # Collect events until released 43 | with keyboard.Listener( 44 | on_press=on_press, 45 | on_release=None) as listener: 46 | listener.join() -------------------------------------------------------------------------------- /src/xfixes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # examples/xfixes.py -- demonstrate the XFIXES extension 4 | # 5 | # Copyright (C) 2011 Outpost Embedded, LLC 6 | # Forest Bond 7 | # 8 | # This library is free software; you can redistribute it and/or 9 | # modify it under the terms of the GNU Lesser General Public License 10 | # as published by the Free Software Foundation; either version 2.1 11 | # of the License, or (at your option) any later version. 12 | # 13 | # This library is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | # See the GNU Lesser General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Lesser General Public 19 | # License along with this library; if not, write to the 20 | # Free Software Foundation, Inc., 21 | # 59 Temple Place, 22 | # Suite 330, 23 | # Boston, MA 02111-1307 USA 24 | 25 | # Python 2/3 compatibility. 26 | from __future__ import print_function 27 | 28 | import sys 29 | import os 30 | import time 31 | 32 | # Change path so we find Xlib 33 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 34 | 35 | from Xlib.display import Display 36 | 37 | 38 | def main(argv): 39 | display = Display() 40 | 41 | if not display.has_extension('XFIXES'): 42 | if display.query_extension('XFIXES') is None: 43 | print('XFIXES extension not supported', file=sys.stderr) 44 | return 1 45 | 46 | xfixes_version = display.xfixes_query_version() 47 | print('Found XFIXES version %s.%s' % ( 48 | xfixes_version.major_version, 49 | xfixes_version.minor_version, 50 | ), file=sys.stderr) 51 | 52 | screen = display.screen() 53 | 54 | print('Hiding cursor ...', file=sys.stderr) 55 | screen.root.xfixes_hide_cursor() 56 | display.sync() 57 | 58 | time.sleep(5) 59 | 60 | print('Showing cursor ...', file=sys.stderr) 61 | screen.root.xfixes_show_cursor() 62 | display.sync() 63 | 64 | 65 | if __name__ == '__main__': 66 | sys.exit(main(sys.argv)) -------------------------------------------------------------------------------- /src/xinput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # examples/xinput.py -- demonstrate the XInput extension 4 | # 5 | # Copyright (C) 2012 Outpost Embedded, LLC 6 | # Forest Bond 7 | # 8 | # This library is free software; you can redistribute it and/or 9 | # modify it under the terms of the GNU Lesser General Public License 10 | # as published by the Free Software Foundation; either version 2.1 11 | # of the License, or (at your option) any later version. 12 | # 13 | # This library is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | # See the GNU Lesser General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Lesser General Public 19 | # License along with this library; if not, write to the 20 | # Free Software Foundation, Inc., 21 | # 59 Temple Place, 22 | # Suite 330, 23 | # Boston, MA 02111-1307 USA 24 | 25 | # Python 2/3 compatibility. 26 | from __future__ import print_function 27 | 28 | import sys 29 | import os 30 | 31 | # Change path so we find Xlib 32 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 33 | 34 | from Xlib.display import Display 35 | from Xlib.ext import xinput 36 | 37 | 38 | def print_hierarchy_changed_event(event): 39 | print('') 47 | 48 | 49 | def print_info(info): 50 | print(' ' % ( 51 | info.deviceid, 52 | info.attachment, 53 | info.type, 54 | info.enabled, 55 | info.flags, 56 | )) 57 | 58 | 59 | def main(argv): 60 | display = Display() 61 | try: 62 | extension_info = display.query_extension('XInputExtension') 63 | xinput_major = extension_info.major_opcode 64 | 65 | version_info = display.xinput_query_version() 66 | print('Found XInput version %u.%u' % ( 67 | version_info.major_version, 68 | version_info.minor_version, 69 | )) 70 | 71 | screen = display.screen() 72 | screen.root.xinput_select_events([ 73 | (xinput.AllDevices, xinput.HierarchyChangedMask), 74 | ]) 75 | 76 | while True: 77 | event = display.next_event() 78 | if ( 79 | event.type == display.extension_event.GenericEvent 80 | and event.extension == xinput_major 81 | and event.evtype == 11 82 | ): 83 | print_hierarchy_changed_event(event) 84 | 85 | finally: 86 | display.close() 87 | 88 | 89 | if __name__ == '__main__': 90 | sys.exit(main(sys.argv)) --------------------------------------------------------------------------------