tray4hermes 2.0.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. tray4hermes-2.0.17/LICENSE +21 -0
  2. tray4hermes-2.0.17/PKG-INFO +624 -0
  3. tray4hermes-2.0.17/README.md +592 -0
  4. tray4hermes-2.0.17/pyproject.toml +149 -0
  5. tray4hermes-2.0.17/setup.cfg +4 -0
  6. tray4hermes-2.0.17/src/tray4hermes/__init__.py +17 -0
  7. tray4hermes-2.0.17/src/tray4hermes/__main__.py +176 -0
  8. tray4hermes-2.0.17/src/tray4hermes/_locales/cs/LC_MESSAGES/tray4hermes.mo +0 -0
  9. tray4hermes-2.0.17/src/tray4hermes/app.py +506 -0
  10. tray4hermes-2.0.17/src/tray4hermes/data/tray4hermes.desktop +9 -0
  11. tray4hermes-2.0.17/src/tray4hermes/i18n.py +311 -0
  12. tray4hermes-2.0.17/src/tray4hermes/icons.py +97 -0
  13. tray4hermes-2.0.17/src/tray4hermes/lock.py +54 -0
  14. tray4hermes-2.0.17/src/tray4hermes/log_dialog.py +787 -0
  15. tray4hermes-2.0.17/src/tray4hermes/log_parse.py +106 -0
  16. tray4hermes-2.0.17/src/tray4hermes/log_settings.py +343 -0
  17. tray4hermes-2.0.17/src/tray4hermes/log_theme.py +100 -0
  18. tray4hermes-2.0.17/src/tray4hermes/paths.py +75 -0
  19. tray4hermes-2.0.17/src/tray4hermes/py.typed +0 -0
  20. tray4hermes-2.0.17/src/tray4hermes/state.py +264 -0
  21. tray4hermes-2.0.17/src/tray4hermes/tray_settings.py +291 -0
  22. tray4hermes-2.0.17/src/tray4hermes.egg-info/PKG-INFO +624 -0
  23. tray4hermes-2.0.17/src/tray4hermes.egg-info/SOURCES.txt +45 -0
  24. tray4hermes-2.0.17/src/tray4hermes.egg-info/dependency_links.txt +1 -0
  25. tray4hermes-2.0.17/src/tray4hermes.egg-info/entry_points.txt +2 -0
  26. tray4hermes-2.0.17/src/tray4hermes.egg-info/requires.txt +7 -0
  27. tray4hermes-2.0.17/src/tray4hermes.egg-info/top_level.txt +1 -0
  28. tray4hermes-2.0.17/tests/test_app.py +624 -0
  29. tray4hermes-2.0.17/tests/test_cli.py +74 -0
  30. tray4hermes-2.0.17/tests/test_gateway_control.py +154 -0
  31. tray4hermes-2.0.17/tests/test_i18n_build.py +196 -0
  32. tray4hermes-2.0.17/tests/test_i18n_parity.py +177 -0
  33. tray4hermes-2.0.17/tests/test_i18n_runtime.py +289 -0
  34. tray4hermes-2.0.17/tests/test_launchers.py +251 -0
  35. tray4hermes-2.0.17/tests/test_lock.py +57 -0
  36. tray4hermes-2.0.17/tests/test_log_parse.py +91 -0
  37. tray4hermes-2.0.17/tests/test_log_refresh.py +85 -0
  38. tray4hermes-2.0.17/tests/test_log_settings.py +155 -0
  39. tray4hermes-2.0.17/tests/test_log_shortcuts.py +189 -0
  40. tray4hermes-2.0.17/tests/test_log_toolbar.py +100 -0
  41. tray4hermes-2.0.17/tests/test_readme_freshness.py +104 -0
  42. tray4hermes-2.0.17/tests/test_readme_parity.py +143 -0
  43. tray4hermes-2.0.17/tests/test_settings_dialog.py +75 -0
  44. tray4hermes-2.0.17/tests/test_state.py +315 -0
  45. tray4hermes-2.0.17/tests/test_test_environment.py +24 -0
  46. tray4hermes-2.0.17/tests/test_tray_settings.py +198 -0
  47. tray4hermes-2.0.17/tests/test_versioning.py +232 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MoDDO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,624 @@
1
+ Metadata-Version: 2.4
2
+ Name: tray4hermes
3
+ Version: 2.0.17
4
+ Summary: KDE/Plasma tray monitor and controller for Hermes Gateway (Nous Research Hermes Agent)
5
+ Author: MoDDO
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/MoDD0/tray4hermes
8
+ Project-URL: Repository, https://github.com/MoDD0/tray4hermes.git
9
+ Project-URL: Issues, https://github.com/MoDD0/tray4hermes/issues
10
+ Project-URL: Changelog, https://github.com/MoDD0/tray4hermes/blob/main/README.md#roadmap-next-up-ideas
11
+ Project-URL: Mirror, https://forgejo.he1.co/HERMbuddy/tray4hermes
12
+ Keywords: hermes,hermes-agent,tray,kde,plasma,system-tray,qt5,pyqt5
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Environment :: X11 Applications
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: PyQt5==5.15.11
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest==9.0.0; extra == "dev"
28
+ Requires-Dist: pytest-qt==4.5.0; extra == "dev"
29
+ Requires-Dist: ruff==0.14.0; extra == "dev"
30
+ Requires-Dist: bandit==1.8.6; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # tray4hermes
34
+
35
+ <!-- tray4hermes:version -->
36
+ [![version: 2.0.17](https://img.shields.io/badge/version-2.0.17-blue.svg)](https://github.com/MoDD0/tray4hermes)
37
+ [![CI](https://github.com/MoDD0/tray4hermes/actions/workflows/ci.yml/badge.svg)](https://github.com/MoDD0/tray4hermes/actions/workflows/ci.yml)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
40
+ [![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
41
+
42
+ ![KDE Plasma tray with tray4hermes icon](docs/images/preview.png)
43
+
44
+ *A real KDE Plasma 5 tray — the tray4hermes icon slots in next to your
45
+ other SNI apps. Screenshot from a Manjaro KDE session where this tray
46
+ is live.*
47
+
48
+ <!-- i18n:available-languages:START -->
49
+ <!-- DO NOT EDIT — auto-generated by scripts/i18n_build.py from the files in docs/i18n/. -->
50
+ <!-- Available languages: en → English, cs → Čeština -->
51
+ <!-- i18n:available-languages:END -->
52
+
53
+ > **Canonical:** English (this file)
54
+ >
55
+ > **Other languages:** [Čeština](docs/README.cs.md)
56
+
57
+ A KDE/Plasma system-tray monitor and controller for **Hermes Gateway**, the
58
+ messaging service included with
59
+ [Hermes Agent](https://github.com/NousResearch/hermes-agent) by Nous Research.
60
+
61
+ > tray4hermes is **read-only with respect to Hermes Agent**. It controls
62
+ > the gateway via `systemctl --user`, persists one small JSON file of its
63
+ > own, and reads everything else. It does not store tokens, does not
64
+ > configure providers, does not edit `~/.hermes/config.yaml`. All of
65
+ > that lives in Hermes Agent itself.
66
+
67
+ ---
68
+
69
+ ## Overview
70
+
71
+ `hermes-gateway` runs as a `systemd --user` service. tray4hermes adds a KDE
72
+ tray interface for monitoring and controlling it. The application:
73
+
74
+ - **shows the gateway state** directly in the KDE panel (green/orange/red)
75
+ - **lets you Start / Stop / Restart** without opening a terminal
76
+ - **switches profiles** (`default`, `work`, `off`…) from a menu
77
+ - **displays logs** in a viewer with coloured levels, per-level filters,
78
+ search, traceback awareness and a time-window filter
79
+ - **speaks English and Czech**, switchable at runtime without a restart
80
+ - **reads Hermes runtime state** and uses the standard `systemctl` API.
81
+
82
+ > ⚠️ **Disclaimer:** tray4hermes is a *passive convenience addon*, not an
83
+ > official Hermes Agent component. Hermes Agent runs perfectly well
84
+ > without it. Use it if you like it; ignore it if you don't.
85
+
86
+ ---
87
+
88
+ ## Features
89
+
90
+ - 📊 **Live status icon** in the system tray (🟢 active, 🟠 warming,
91
+ 🔵 activating, ⚫ inactive, 🔴 failed, ⚪ unknown)
92
+ - ▶️ **Start / Stop / Restart** of `hermes-gateway.service` in a single click
93
+ - 🔄 **Profile switcher** submenu, driven by `~/.hermes/profiles/`
94
+ - 📋 **Log viewer** — *see below*, it's quite polished
95
+ - ⚙️ **Open Hermes config** in your default editor
96
+ - 💻 **Launch Hermes CLI** in a new terminal
97
+ - 🛠️ **Settings dialog** — UI language plus the defaults the log viewer
98
+ starts with, stored alongside the rest of the tray state
99
+ - 🌍 **English and Czech UI** — pick a language in the Settings dialog and
100
+ the tray re-labels itself immediately, no restart; `tray4hermes -L en`
101
+ (or `cs`) overrides the stored choice for a single run, and
102
+ `tray4hermes --language` lists what the installed build ships
103
+
104
+ ### Log viewer
105
+
106
+ The log viewer reads `~/.hermes/logs/gateway.log` and provides:
107
+
108
+ - **Coloured log levels**: `DEBUG` gray, `INFO` white, `WARNING` yellow,
109
+ `ERROR` red, `CRITICAL` red + full-row highlight
110
+ - **Line-number gutter** like Qt Creator / VS Code
111
+ - **Per-level filters** on a toolbar row of their own, so they and the
112
+ controls above them stay visible instead of collapsing into the `»`
113
+ overflow popup — show only what you want
114
+ - **TRACEBACK toggle** — a separate category for stack traces; turn
115
+ it off to see only messages, or on to keep both
116
+ - **Time-window filter** (All / 5m / 15m / 1h / 6h / 24h) — show only
117
+ logs from the last hour, etc.
118
+ - **Reverse order** — flip to `journalctl` style (newest at top); every new
119
+ entry appears first
120
+ - **Max lines** spinbox — rolling buffer (0 = unlimited). Steps by 1; type any value manually
121
+ - **Search** in a collapsible find bar under the log (`Ctrl+F` opens and
122
+ focuses it, `F3` next, `Shift+F3` prev, `Esc` closes). It deliberately sits
123
+ outside the toolbar, so it stays reachable at any window width
124
+ - **Auto-scroll toggle** (default ON; OFF = preserve position on refresh).
125
+ While investigating, the viewer preserves a manually selected scroll
126
+ position even when auto-scroll is ON; it only follows the live edge when
127
+ you are already viewing that edge
128
+ - **Word-wrap toggle**
129
+ - **Copy / Clear / Refresh** actions
130
+ - **Settings dialog** (font size, max lines, per-level visibility, …)
131
+ - **Remembers window size/position** between opens
132
+ - **Persisted** — all settings are saved to
133
+ `~/.config/tray4hermes/state.json`
134
+
135
+ ---
136
+
137
+ ## Architecture
138
+
139
+ ```
140
+ ┌──────────────────────────────────────────────────────┐
141
+ │ Hermes Agent (Nous Research) │
142
+ │ • hermes-gateway.service (systemd --user) │
143
+ │ • Hermes Desktop (Electron consumer) │
144
+ │ • CLI / TUI / MCP / Plugins │
145
+ └──────────────────────┬───────────────────────────────┘
146
+ │ shares
147
+
148
+ ~/.hermes/ ← single source of truth
149
+ ├── config.yaml
150
+ ├── auth.json
151
+ ├── gateway_state.json
152
+ ├── logs/gateway.log
153
+ └── profiles/<name>/
154
+
155
+ │ reads (read-only)
156
+ ┌──────────────────────┴───────────────────────────────┐
157
+ │ tray4hermes (this package) │
158
+ │ • systray icon • Start/Stop/Restart │
159
+ │ • profile switcher • log viewer │
160
+ │ • writes only: ~/.config/tray4hermes/state.json │
161
+ └──────────────────────────────────────────────────────┘
162
+ ```
163
+
164
+ The package has zero coupling to the Hermes Agent source code. It only
165
+ knows about files in `~/.hermes/`, the systemd unit name, and the path
166
+ to the `hermes` CLI. The tray can be uninstalled at any time without
167
+ affecting the gateway.
168
+
169
+ ## State machine
170
+
171
+ The tray combines two sources of truth into six discrete states:
172
+
173
+ | Code | Icon | Meaning |
174
+ |------|------|---------|
175
+ | `active` | 🟢 | Gateway running, at least one platform connected |
176
+ | `warming` | 🟠 | Gateway running, credentials/platforms still initialising |
177
+ | `activating` | 🔵 | systemd is starting the service |
178
+ | `inactive` | ⚫ | Gateway stopped |
179
+ | `failed` | 🔴 | systemd unit failed |
180
+ | `unknown` | ⚪ | Cannot determine state (both sources unavailable) |
181
+
182
+ `gateway_state.json` is the primary source when fresh (< 1 hour old);
183
+ `systemctl is-active` is the fallback. The two-source design avoids
184
+ the OAuth warm-up race where the systemd unit shows `active` for a few
185
+ seconds before the first model call actually succeeds.
186
+
187
+ ## Requirements
188
+
189
+ - **Linux** (developed primarily on **Manjaro KDE**; should work on
190
+ any distro with KDE Plasma 5, see [Platform support](#platform-support))
191
+ - **KDE Plasma 5** (Plasma 6 uses Qt6 — tray4hermes is Qt5; Qt6 port
192
+ is in the Roadmap)
193
+ - Python ≥ 3.11
194
+ - A running `hermes-gateway.service` under `systemd --user`
195
+ - `loginctl enable-linger $USER` for autostart after logout
196
+
197
+ ## Installation
198
+
199
+ ### End-user (system-wide)
200
+
201
+ ```bash
202
+ uv pip install --system tray4hermes
203
+ # or with pipx:
204
+ pipx install tray4hermes
205
+ ```
206
+
207
+ Then enable autostart:
208
+
209
+ ```bash
210
+ # Find where the .desktop file ended up after pip install
211
+ DESKTOP_FILE=$(python3 -c "import tray4hermes, os; \
212
+ print(os.path.join(os.path.dirname(tray4hermes.__file__), 'data', 'tray4hermes.desktop'))")
213
+ cp "$DESKTOP_FILE" ~/.config/autostart/tray4hermes.desktop
214
+ # Adjust the Exec= line to point at your installed tray4hermes script
215
+ # (the wheel ships it at <prefix>/bin/tray4hermes).
216
+ ```
217
+
218
+ ### Development (editable)
219
+
220
+ ```bash
221
+ git clone https://github.com/MoDD0/tray4hermes.git
222
+ cd tray4hermes
223
+ uv pip install --system -e ".[dev]"
224
+ ./scripts/dev.sh # installs deps + runs tests
225
+ ```
226
+
227
+ Launch the tray:
228
+
229
+ ```bash
230
+ # Either via the installed console script:
231
+ tray4hermes
232
+
233
+ # Or via the watchdog wrapper (auto-restart on crash):
234
+ ./run.sh
235
+
236
+ # Or as a module:
237
+ python -m tray4hermes
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Platform support
243
+
244
+ **Primarily developed and tested on Manjaro KDE** (rolling release,
245
+ Plasma 5, `xcb` X11 backend).
246
+
247
+ **Should work natively on any Linux that has:**
248
+
249
+ 1. **Qt5 with PyQt5** (`python -c "from PyQt5.QtWidgets import QSystemTrayIcon; print('OK')"`
250
+ should succeed). PySide2 is not supported — every module imports PyQt5
251
+ directly.
252
+ 2. **DBus session bus** (`echo $DBUS_SESSION_BUS_ADDRESS` should be
253
+ set — typically automatic in a desktop session)
254
+ 3. **A system tray implementation** respecting the freedesktop.org
255
+ SNI spec (KDE Plasma, Xfce, LXQt, Cinnamon, MATE, GNOME with
256
+ extension, Elementary OS with extension…)
257
+ 4. **systemd --user** (or something compatible — OpenRC / runit
258
+ alternatives have a slightly different `hermes-gateway` API;
259
+ an adapter in `paths.py` would be needed)
260
+
261
+ ### Distro status
262
+
263
+ Only one row here is actually tested. The rest is reasoning from how
264
+ close the distro is to that one — useful, but not a promise.
265
+
266
+ | Distro / DE | Status | Notes |
267
+ |-------------|--------|-------|
268
+ | **Manjaro KDE** | ✅ tested | the platform this is developed and tested on |
269
+ | Arch Linux + KDE | 🟡 expected to work | practically identical to Manjaro, just package from `extra` not AUR |
270
+ | CachyOS KDE | 🟡 expected to work | Arch-based, practically identical |
271
+ | **Ubuntu + KDE** | 🟡 expected to work | see below |
272
+ | Fedora + KDE | 🟡 expected to work | `dnf install python3-pyqt5` |
273
+ | openSUSE + KDE | 🟡 expected to work | `zypper install python3-PyQt5` |
274
+
275
+ ### Ubuntu with Unity / GNOME / others
276
+
277
+ This is a **tray application**, so the main difference is tray support:
278
+
279
+ - **Ubuntu + KDE Plasma** (`apt install kubuntu-desktop`) — should work
280
+ out-of-the-box, you may need to install `python3-pyqt5` via `apt`
281
+ or `pip install PyQt5` for yourself.
282
+ - **Ubuntu + Unity (22.04+)** — Unity uses its own indicator tray,
283
+ not SNI. We would need to adapt `icons.py` and `app.py` so they
284
+ register via DBus at `com.canonical.Unity.LauncherEntry`. Technically
285
+ possible, but not implemented today. If you run Unity and want this
286
+ — open an issue.
287
+ - **Ubuntu + GNOME 41+** — GNOME intentionally **does not support
288
+ tray** without an SNI extension. You have to install an extension
289
+ like [AppIndicator Support](https://extensions.gnome.org/extension/615/appindicator-support/),
290
+ then it should work.
291
+ - **Ubuntu + Cinnamon / MATE / XFCE** — all support SNI tray, should
292
+ work out-of-the-box.
293
+
294
+ ### Practical recommendation for Ubuntu users
295
+
296
+ Short version: **install KDE Plasma** and it will work with the
297
+ highest probability. Not the smallest package, but the cleanest path.
298
+
299
+ ```bash
300
+ # 1. Install KDE Plasma desktop (meta-package, ~600 MB)
301
+ sudo apt install kde-plasma-desktop
302
+
303
+ # 2. Log out. On the login screen, choose the "Plasma" session, log in.
304
+
305
+ # 3. In a terminal inside the KDE session:
306
+ sudo apt install python3-pyqt5 # system PyQt5
307
+ python3 -m pip install --user tray4hermes # or from GitHub
308
+ python3 -m tray4hermes # start the tray
309
+
310
+ # 4. For autostart after login:
311
+ # The .desktop file ships inside the installed wheel at:
312
+ # /usr/local/lib/python3.*/site-packages/tray4hermes/data/tray4hermes.desktop
313
+ # (resolve with `python3 -c "import tray4hermes; import os; \
314
+ # print(os.path.join(os.path.dirname(tray4hermes.__file__), 'data', 'tray4hermes.desktop'))"`)
315
+ # Copy it to ~/.config/autostart/ and edit the Exec= path to point at
316
+ # your installed tray4hermes.
317
+ ```
318
+
319
+ **Why we recommend this:**
320
+
321
+ 1. **Hermes Agent** runs on Ubuntu exactly the same as on Manjaro
322
+ (both are Linux, both have systemd --user, both have Python 3.11+).
323
+ *No* inherent incompatibility with Ubuntu.
324
+ 2. **KDE Plasma's tray** implements the SNI spec most thoroughly of
325
+ any DE — no workaround, no extension, no "maybe works".
326
+ 3. **Higher chance of first-try success.** Instead of 75–95% with GNOME
327
+ / Cinnamon + workaround, you get ~99%.
328
+
329
+ **Alternative for purists:** if you insist on GNOME and really want
330
+ this to work there, follow the instructions in the `Ubuntu + GNOME 41+`
331
+ section above. But it's not a trivial first experience.
332
+
333
+ ### Future testing
334
+
335
+ **A dedicated Ubuntu VM**: if the community brings real demand for
336
+ Ubuntu support, we'd happily spin up a simple **Ubuntu LTS VM**
337
+ (VirtualBox or LXC) with KDE Plasma in CI and run a smoke test —
338
+ `tray4hermes` starts → menu icon appears → click Start/Stop → verify
339
+ that `systemctl --user` responds. Cost would be ~1–2 days of setup +
340
+ scripts. We don't have it right now, because demand is currently
341
+ **zero** (Manjaro/KDE covers ~95% of users who have written to us).
342
+ If you'd like it, add it to the Roadmap. Open an issue with a `+1`
343
+ and a vote, or just show up to a contributing call. 🙂
344
+
345
+ ### Troubleshooting
346
+
347
+ If the tray **doesn't show** on another distro:
348
+
349
+ ```bash
350
+ # 1. Verify Qt5 sees a system tray
351
+ python -c "from PyQt5.QtWidgets import QSystemTrayIcon, QApplication; \
352
+ app = QApplication([]); print('available:', QSystemTrayIcon.isSystemTrayAvailable())"
353
+
354
+ # 2. Check whether DBus is running
355
+ echo "DBus session bus: $DBUS_SESSION_BUS_ADDRESS"
356
+
357
+ # 3. Try explicit XDG, and run in the foreground so errors reach the terminal
358
+ export XDG_CURRENT_DESKTOP=KDE
359
+ export XDG_SESSION_TYPE=x11
360
+ python -m tray4hermes
361
+ ```
362
+
363
+ There is no `--debug` flag: the tray writes its failures to stderr, so
364
+ running it from a terminal as above is the whole debug story. `tray4hermes
365
+ --version` and `tray4hermes --language` are the only other arguments.
366
+
367
+ If `QSystemTrayIcon.isSystemTrayAvailable()` returns `False`, it's a
368
+ **distro/desktop combo problem**, not a tray4hermes bug. You can:
369
+ - open an issue with the output of `python -c "import sys; print(sys.platform, …)"`
370
+ plus your Qt version (`PyQt5.QtCore.PYQT_VERSION_STR`)
371
+ - ask in the Hermes community (or directly with us in issues, we'll
372
+ help extend `paths.py` for your setup)
373
+
374
+ We want it to work **everywhere** Hermes Agent has a chance of running.
375
+ So distro-specific PRs are very welcome.
376
+
377
+ ---
378
+
379
+ ## Security
380
+
381
+ This package **does not handle any credentials, tokens, or secrets**.
382
+ The only file it writes is `~/.config/tray4hermes/state.json`, which
383
+ contains the currently-selected profile name, log-viewer preferences,
384
+ and a schema version:
385
+
386
+ ```json
387
+ {
388
+ "version": 1,
389
+ "selected_profile": "default",
390
+ "log_settings": {
391
+ "max_lines": 2000,
392
+ "auto_scroll": true,
393
+ "word_wrap": false,
394
+ "font_size": 9,
395
+ "show_levels": ["ERROR", "WARNING", "INFO", "DEBUG", "CRITICAL", "TRACE"],
396
+ "show_tracebacks": true,
397
+ "time_window_minutes": 0,
398
+ "reverse_order": false
399
+ }
400
+ }
401
+ ```
402
+
403
+ If you find a security issue, please open a **private** issue via
404
+ GitHub's "Report a vulnerability" feature (Settings → Security →
405
+ Report a vulnerability). Do **not** disclose vulnerabilities in
406
+ public issues or commits.
407
+
408
+ ### Threat model
409
+
410
+ | Vector | Mitigation |
411
+ |--------|------------|
412
+ | RCE via malicious `gateway_state.json` | Loaded as JSON only; no `eval`/`exec`/shell. Strict shape. |
413
+ | Log injection | Log viewer is read-only; uses `QPlainTextEdit` (escapes HTML). |
414
+ | Profile path injection | Profile name validated by Hermes Agent itself (`hermes profile use` returns non-zero on missing). |
415
+ | Lock file race | `O_CREAT\|O_EXCL` + PID liveness probe + recursive single retry. |
416
+ | Filesystem exhaustion on `state.json` write | Atomic `tmp` + `os.replace`; directory created with `parents=True`. |
417
+
418
+ The tray is sandboxed against the rest of Hermes Agent: even a
419
+ zero-day in tray4hermes cannot read `auth.json`, the `.env` file, or
420
+ trigger a model call. The worst it can do is crash and be restarted
421
+ by the watchdog — at which point it just reads the (still intact)
422
+ state and re-renders the tray icon.
423
+
424
+ ## Development
425
+
426
+ ### Run tests
427
+
428
+ ```bash
429
+ ./scripts/dev.sh # install + pytest
430
+ ./scripts/dev.sh tests/test_state.py -v # specific file
431
+ ```
432
+
433
+ Tests use `QT_QPA_PLATFORM=offscreen` (set in `tests/conftest.py`) so
434
+ they run headless, without a display server.
435
+
436
+ ### Continuous integration
437
+
438
+ [GitHub Actions](.github/workflows/ci.yml) runs on every push to `main`
439
+ and on every pull request: lint (`ruff check` + `ruff format --check`)
440
+ and the full test suite on Python 3.11, 3.12 and 3.13 — the versions
441
+ this package claims to support. A separate job refuses any commit that
442
+ lowers the package version.
443
+
444
+ ### Lint & format
445
+
446
+ ```bash
447
+ uv run ruff check src tests
448
+ uv run ruff format src tests
449
+ ```
450
+
451
+ ### Security scan
452
+
453
+ ```bash
454
+ uv run bandit -c pyproject.toml -r src
455
+ ```
456
+
457
+ ### Pre-commit hooks (optional but recommended)
458
+
459
+ ```bash
460
+ uv pip install --system pre-commit
461
+ pre-commit install
462
+ pre-commit run --all-files
463
+ ```
464
+
465
+ ## Project layout
466
+
467
+ ```
468
+ tray4hermes/
469
+ ├── pyproject.toml # PEP 621, uv-friendly, exact-pinned deps
470
+ ├── LICENSE # MIT
471
+ ├── README.md # generated from docs/i18n/en.md — don't edit
472
+ ├── CONTRIBUTING.md
473
+ ├── CLAUDE.md # briefing for AI assistants working here
474
+ ├── .claude/ # architecture, conventions, session log, specs
475
+ ├── .gitignore # incl. secret patterns
476
+ ├── .pre-commit-config.yaml
477
+ ├── run.sh # watchdog wrapper
478
+ ├── docs/
479
+ │ ├── README.cs.md # generated from docs/i18n/cs.md — don't edit
480
+ │ ├── i18n/
481
+ │ │ ├── en.md # canonical README source (source of truth)
482
+ │ │ └── cs.md # Czech translation of en.md
483
+ │ └── images/
484
+ │ ├── preview.png # screenshot for README
485
+ │ └── tray4hermes.png # package logo
486
+ ├── scripts/
487
+ │ ├── dev.sh # install + test convenience
488
+ │ ├── i18n_build.py # docs/i18n/*.md → README.md + docs/README.cs.md
489
+ │ ├── i18n_compile.sh # .pot extraction + .po → .mo catalogs
490
+ │ ├── i18n_lint.py # heading parity across README translations
491
+ │ └── versioning.py # SemVer bump helper
492
+ ├── src/
493
+ │ └── tray4hermes/
494
+ │ ├── __init__.py # __version__ (single source of truth)
495
+ │ ├── __main__.py # python -m tray4hermes (argparse entry)
496
+ │ ├── app.py # HermesTray — icon, menu, poll timer
497
+ │ ├── state.py # @dataclass + aggregation logic
498
+ │ ├── paths.py # all filesystem paths and tunables
499
+ │ ├── icons.py # QPainter icon factory
500
+ │ ├── lock.py # single-instance lock
501
+ │ ├── log_dialog.py # LogDialog — editor, gutter, toolbars, find bar
502
+ │ ├── log_settings.py # viewer preferences: dataclass, persistence, dialog
503
+ │ ├── log_parse.py # level / timestamp / traceback parsing (no Qt widgets)
504
+ │ ├── log_theme.py # colours, fonts and filter constants shared by both dialogs
505
+ │ ├── tray_settings.py # global settings dialog (language, defaults)
506
+ │ ├── i18n.py # gettext wrapper + runtime language switch
507
+ │ ├── py.typed # PEP 561 marker
508
+ │ ├── _locales/ # UI catalogs, shipped inside the wheel
509
+ │ │ ├── tray4hermes.pot
510
+ │ │ └── cs/LC_MESSAGES/tray4hermes.{po,mo}
511
+ │ └── data/
512
+ │ └── tray4hermes.desktop # ships inside the wheel
513
+ └── tests/
514
+ ├── conftest.py
515
+ ├── test_app.py # Qt offscreen — tray menu and actions
516
+ ├── test_cli.py # `python -m tray4hermes` argument handling
517
+ ├── test_gateway_control.py # systemctl actions, profile switching
518
+ ├── test_i18n_build.py # README build script
519
+ ├── test_i18n_parity.py # build + lint scripts wired into pytest
520
+ ├── test_i18n_runtime.py # gettext catalogs, msgid language
521
+ ├── test_launchers.py # editor / terminal resolution and spawning
522
+ ├── test_lock.py # single-instance lock
523
+ ├── test_log_parse.py # level / timestamp parsing, both regex engines
524
+ ├── test_log_refresh.py # what the viewer shows when the log is unreadable
525
+ ├── test_log_settings.py # viewer settings persistence
526
+ ├── test_log_shortcuts.py # Ctrl+F / F3 / Shift+F3 / Esc, real key path
527
+ ├── test_log_toolbar.py # toolbar rows stay on screen at realistic widths
528
+ ├── test_readme_freshness.py # compiled READMEs match their sources
529
+ ├── test_readme_parity.py # image parity between en.md and cs.md
530
+ ├── test_settings_dialog.py # log-viewer settings dialog
531
+ ├── test_state.py # state aggregation and persistence
532
+ ├── test_tray_settings.py # global tray settings
533
+ └── test_versioning.py # SemVer bump helper
534
+ ```
535
+
536
+ ## License
537
+
538
+ MIT — see [LICENSE](LICENSE).
539
+
540
+ ---
541
+
542
+ ## Hosting
543
+
544
+ **Primary host: GitHub** → https://github.com/MoDD0/tray4hermes
545
+
546
+ - All development, issues, pull requests, releases, and CI happen there.
547
+ - This is where contributors fork from and push branches to.
548
+ - Releases are tagged here first; everything else cascades from here.
549
+
550
+ ---
551
+
552
+ ## Contributing
553
+
554
+ Issues, comments, suggestions — all welcome. Whether it's:
555
+
556
+ - 🐛 **Bug report** — ideally with the relevant part of
557
+ `~/.hermes/logs/gateway.log` and whatever `python -m tray4hermes`
558
+ prints when run from a terminal
559
+ - 💡 **Feature request** — short description of what it would be
560
+ for. We don't mind "wild" ideas (different backend, Wayland
561
+ support, custom icons…). Worth a discussion.
562
+ - 🎨 **UI tweak** — colours, layout, fonts, tooltips. This is
563
+ the area where contributor taste matters most.
564
+ - 📖 **Documentation** — screenshots of the log viewer are missing;
565
+ feel free to add them
566
+ - 🌍 **Localization** — the UI runs through `gettext` and ships English
567
+ and Czech. Another language means one `.po` file under
568
+ `src/tray4hermes/_locales/`; the README translations live separately in
569
+ `docs/i18n/`. Both routes are described in
570
+ [CONTRIBUTING.md](CONTRIBUTING.md)
571
+ - 🐧 **Distro-specific fixes** — see [Platform support](#platform-support).
572
+ Every distro we test on is a contribution away
573
+
574
+ ### How to send a PR / patch
575
+
576
+ ```bash
577
+ git clone https://github.com/MoDD0/tray4hermes.git
578
+ cd tray4hermes
579
+ ./scripts/dev.sh # installs deps, runs tests
580
+ # make your change
581
+ ./scripts/dev.sh -v # re-run tests with verbose
582
+ uv run ruff check src tests
583
+ uv run ruff format src tests
584
+ git commit -m "description"
585
+ git push
586
+ ```
587
+
588
+ Rules (flexible):
589
+
590
+ 1. **Don't break the tests.** The whole suite must stay green —
591
+ `./scripts/dev.sh` runs it.
592
+ 2. **Don't add new runtime dependencies** without discussion — the
593
+ package has one runtime dependency (`PyQt5`), and we want to keep it
594
+ that way.
595
+ 3. **No secrets in the diff**
596
+ (`grep -rE "sk-[a-z0-9]{16,}|api_key.*[a-z0-9]{20,}"`).
597
+ 4. **No writes to `~/.hermes/*`** from inside the tray — that area is
598
+ owned by Hermes Agent.
599
+ 5. **MIT-compatible contributions.** If you're adding code, stick to
600
+ the MIT license.
601
+
602
+ If you like this project and have an idea, **just open an issue** —
603
+ we'll discuss it. Even "this word doesn't sound right in the
604
+ translation" is welcome feedback.
605
+
606
+ ---
607
+
608
+ ## Roadmap (next-up ideas)
609
+
610
+ A few things on my mind that aren't done yet. If any of these
611
+ interests you more than the rest, speak up:
612
+
613
+ - **Wayland support** — currently `xcb` (X11) only because of the
614
+ KDE Plasma tray API; Plasma 6 + Qt6 would open Wayland. PRs welcome.
615
+ - **Custom icons per status** — SVG icons instead of `QPainter` raster
616
+ - **Log search across sessions** (FTS5 over the sessions DB)
617
+ - **Notifications on ERROR** — toast over D-Bus when the gateway
618
+ writes a traceback (I like it, but it's a matter of taste)
619
+ - **Settings export/import** — sharing log-viewer presets between
620
+ profiles
621
+ - **Ubuntu LTS CI VM** — see [Platform support → Future testing](#future-testing)
622
+ - **Plasma 6 / Qt6 port** — for the day Manjaro ships Plasma 6 by
623
+ default
624
+