├── .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 | 
4 |
5 |
6 |
7 | If you like what i make, it would really be nice to have someone buy me a coffee
8 |
9 |

10 |
11 |
12 | ### Simple on-screen keyboard and mouse keystrokes display
13 |
14 | |  |  |
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 |
154 |
--------------------------------------------------------------------------------
/data/icons/16.svg:
--------------------------------------------------------------------------------
1 |
93 |
--------------------------------------------------------------------------------
/data/icons/24.svg:
--------------------------------------------------------------------------------
1 |
156 |
--------------------------------------------------------------------------------
/data/icons/32.svg:
--------------------------------------------------------------------------------
1 |
154 |
--------------------------------------------------------------------------------
/data/icons/48.svg:
--------------------------------------------------------------------------------
1 |
154 |
--------------------------------------------------------------------------------
/data/icons/64.svg:
--------------------------------------------------------------------------------
1 |
154 |
--------------------------------------------------------------------------------
/data/icons/key-press.svg:
--------------------------------------------------------------------------------
1 |
67 |
--------------------------------------------------------------------------------
/data/icons/key-release.svg:
--------------------------------------------------------------------------------
1 |
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 |
8 |
--------------------------------------------------------------------------------
/data/icons/mouse-scrollleft-symbolic.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/data/icons/mouse-scrollright-symbolic.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/data/icons/mouse-scrollup-symbolic.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/data/icons/movements1.svg:
--------------------------------------------------------------------------------
1 |
60 |
--------------------------------------------------------------------------------
/data/icons/movements2.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/data/icons/movements3.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/data/icons/movements4.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/data/icons/movements5.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/data/icons/movements6.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/data/icons/settings-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
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))
--------------------------------------------------------------------------------