From 4ac29a3e5c49993d07e626d709bfa6ebc6fe73e9 Mon Sep 17 00:00:00 2001 From: cjjohn <72096478+Grovy311@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:30:50 +0200 Subject: [PATCH] Add custom icon and tray-minimize behavior (like VersaGUI's TrayApp) Minimizing or closing hides the window instead of leaving a taskbar entry; only "Beenden" in the tray right-click menu really exits. Tray callbacks route through a queue instead of touching Tk directly from pystray's thread (same pattern as the earlier serial-thread fix). Also fixes the build script: PyInstaller failed on the network share while copying Tcl/Tk tzdata (path-length issue), so building now happens in a local temp copy instead of directly on Z:. Adds an info button that explains the MCP server's available tools. --- AGENTS.md | 61 +++++++++++++++----- VersaPadViewer.spec | 3 +- build_and_deploy.ps1 | 36 +++++++++--- desktop_viewer.py | 133 ++++++++++++++++++++++++++++++++++++++++++- icon.ico | Bin 0 -> 14782 bytes icon.png | Bin 0 -> 1967 bytes 6 files changed, 208 insertions(+), 25 deletions(-) create mode 100644 icon.ico create mode 100644 icon.png diff --git a/AGENTS.md b/AGENTS.md index 2faadb5..99af9a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,30 +116,63 @@ zuverlässig zur echten Python313-Installation auf. ## Gepackte .exe (PyInstaller) — kein Terminal-Flash, kein Python-Setup nötig -`build_and_deploy.ps1` baut `desktop_viewer.py` mit PyInstaller -(`--windowed --onedir`) und kopiert das Ergebnis lokal nach -`%LOCALAPPDATA%\VersaPadViewer\VersaPadViewer.exe`. Desktop-Verknüpfung -"VersaPad Viewer (Fenster)" zeigt dorthin. +`build_and_deploy.ps1` kopiert den Quellcode zuerst nach `%TEMP%`, baut dort +mit PyInstaller (`--windowed --onedir --icon icon.ico`), und kopiert das +Ergebnis nach `%LOCALAPPDATA%\VersaPadViewer\VersaPadViewer.exe`. +Desktop-Verknüpfung "VersaPad Viewer (Fenster)" zeigt dorthin (inkl. eigenem +Icon, `IconLocation` zeigt auf die `.exe` selbst statt auf `pythonw.exe`). **`--onedir`, nicht `--onefile`** — Single-File-PyInstaller-Bundles lösen öfter AV-Fehlalarme aus (sehen strukturell wie ein Packer aus); `--onedir` (Ordner mit `.exe` + `_internal`) ist unauffälliger. Trotzdem unsigniert, also grundsätzlich nie über Smart App Control/AV in Stein gemeißelt sicher. -**Kritischer Stolperstein:** Die gebaute `.exe` läuft NICHT vom Netzlaufwerk -aus (`Z:\Git\...`, SMB-Share auf cj-ki) — Windows blockiert das Nachladen -der `_internal`-DLLs von einem Netzwerkpfad, der Prozess startet dann -einfach nie, ganz ohne Fehlermeldung/Crash-Log/AV-Benachrichtigung (sah -zuerst wie ein AV-Block aus, war aber keiner — sauber verifiziert indem -dieselbe `.exe` 1:1 lokal kopiert sofort lief). Bauen darf auf Z: passieren, -das fertige Bundle muss vor dem Start nach C: kopiert werden — genau das -macht `build_and_deploy.ps1`. +**Zwei Netzlaufwerk-Stolpersteine, deshalb baut UND deployt alles lokal:** +1. Laufzeit: die gebaute `.exe` läuft NICHT vom Netzlaufwerk aus (`Z:\Git\...`, + SMB-Share auf cj-ki) — Windows blockiert das Nachladen der + `_internal`-DLLs von einem Netzwerkpfad, der Prozess startet dann einfach + nie, ganz ohne Fehlermeldung/Crash-Log/AV-Benachrichtigung (sah zuerst wie + ein AV-Block aus, war aber keiner — verifiziert indem dieselbe `.exe` 1:1 + lokal kopiert sofort lief). +2. Buildzeit: PyInstaller scheitert beim Kopieren der Tcl/Tk-`tzdata` + (viele tief verschachtelte, lang benannte Zeitzonen-Ordner) auf dem + SMB-Share mit `FileNotFoundError: [WinError 3]` — Pfadlängen-Problem, + kombiniert mit dem eh schon langen Projektpfad (`Z:\Git\Versa Board\ + versapad-viewer\dist\...`). + +Deshalb kopiert `build_and_deploy.ps1` den Quellcode zuerst nach `%TEMP%` +und baut NUR dort — Z: wird nur zum Lesen der `.py`-Dateien angefasst. Bei erneutem Bauen: `.\build_and_deploy.ps1` in PowerShell ausführen (lokal, nicht über die sandboxed Bash-Tool-Umgebung — die hat eine eigene Dateisystem-Sicht, die nicht zuverlässig mit dem echten Windows-Dateisystem -übereinstimmt; Kopieroperationen für Dinge, die der User/andere Prozesse -sehen sollen, gehören in PowerShell, nicht in Bash). +übereinstimmt; siehe Sessionlog: ein `cp -r` über Bash landete an einem Ort, +den PowerShell/der reale Explorer nicht sah. Kopieroperationen für Dinge, +die der User/andere Prozesse sehen sollen, gehören in PowerShell, nicht in +Bash). + +## Icon + Tray (kein Taskleisten-Eintrag beim Minimieren, wie VersaGUI) + +- `icon.png`/`icon.ico` — generiert via Pillow (dunkles abgerundetes Quadrat, + blauer Block, 2x2-Punktraster; flaches Design, keine Farbverläufe/ + Anti-Aliasing-Details, siehe Speicher-Notiz zu Tkinter-Flat-Design). + `.ico` fürs Fenster-/Taskleisten-/Exe-Icon, `.png` fürs Tray-Icon (pystray + braucht ein PIL-Image, kein Multi-Res-`.ico`). +- `resource_path()` in `desktop_viewer.py` findet Assets sowohl im + Quellordner als auch im PyInstaller-Bundle (`sys._MEIPASS`). +- Minimieren (`` mit `state()=='iconic'`) ruft `withdraw()` statt + normal zu minimieren → kein Taskleisten-Eintrag, nur noch Tray-Icon. + Schließen (X) macht dasselbe (`_hide_to_tray`, nicht `_quit`) — echtes + Beenden nur über "Beenden" im Tray-Rechtsklickmenü (mirrort VersaGUIs + `TrayApp`-Menü: Öffnen/Beenden). Tray-Icon läuft über `pystray.Icon. + run_detached()` in eigenem Thread — Klicks im Tray-Menü legen nur ein + Ereignis in `self._tray_queue`, das der Main-Thread per `after()`-Polling + abholt (dieselbe Queue-statt-Direktzugriff-Regel wie beim Serial-Thread, + siehe oben — Tk-Aufrufe aus einem Fremdthread haben schon mal einen + stillen Absturz verursacht). +- Neue Abhängigkeiten: `pystray`, `pillow` (zusätzlich zu `pyserial`). +- Info-Button (ⓘ oben rechts im Header) öffnet einen Dialog mit der + MCP-Server-Doku (`MCP_INFO_TEXT`-Konstante) — rein informativ, keine Logik. ## Dateien/Pfade (hardcoded, hal9001-spezifisch) diff --git a/VersaPadViewer.spec b/VersaPadViewer.spec index fc2858d..c41bbc3 100644 --- a/VersaPadViewer.spec +++ b/VersaPadViewer.spec @@ -5,7 +5,7 @@ a = Analysis( ['desktop_viewer.py'], pathex=[], binaries=[], - datas=[], + datas=[('icon.png', '.'), ('icon.ico', '.')], hiddenimports=[], hookspath=[], hooksconfig={}, @@ -32,6 +32,7 @@ exe = EXE( target_arch=None, codesign_identity=None, entitlements_file=None, + icon=['icon.ico'], ) coll = COLLECT( exe, diff --git a/build_and_deploy.ps1 b/build_and_deploy.ps1 index 23318b4..8243798 100644 --- a/build_and_deploy.ps1 +++ b/build_and_deploy.ps1 @@ -1,19 +1,38 @@ # Baut VersaPadViewer.exe (PyInstaller --onedir) und kopiert das Ergebnis # nach lokal (C:\Users\\AppData\Local\VersaPadViewer). # -# WICHTIG: Muss lokal laufen, NICHT direkt vom Netzlaufwerk (Z:\Git\...) -- -# Windows blockiert das Nachladen der PyInstaller-_internal-DLLs von einem -# Netzwerkpfad (SMB-Share) ohne jede Fehlermeldung, der Prozess startet -# einfach nie. Deshalb bauen wir hier (Quelle darf auf Z: liegen) und -# kopieren das fertige Bundle danach nach C:. +# WICHTIG: Sowohl Bauen als auch Laufen muessen lokal passieren, NICHT auf +# dem Netzlaufwerk (Z:\Git\...): +# - Laufzeit: Windows blockiert das Nachladen der PyInstaller-_internal- +# DLLs von einem Netzwerkpfad (SMB-Share) ohne jede Fehlermeldung, der +# Prozess startet einfach nie. +# - Buildzeit: PyInstaller scheitert beim Kopieren von Tcl/Tk-tzdata (viele +# tief verschachtelte, lang benannte Zeitzonen-Ordner) mit +# "FileNotFoundError: [WinError 3]" -- Pfadlaengen-Problem auf dem +# SMB-Share, kombiniert mit dem eh schon langen Projektpfad. +# +# Deshalb: Quellcode zuerst nach lokal (%TEMP%) kopieren, dort bauen, danach +# das fertige Bundle nach %LOCALAPPDATA% kopieren. Z: wird nur zum Lesen der +# Quelldateien angefasst. $ErrorActionPreference = "Stop" $projectDir = $PSScriptRoot +$buildSrc = "$env:TEMP\versapad_build_src" $localDir = "$env:LOCALAPPDATA\VersaPadViewer" -Push-Location $projectDir +if (Test-Path $buildSrc) { + Remove-Item $buildSrc -Recurse -Force +} +New-Item -ItemType Directory -Path $buildSrc | Out-Null +Copy-Item "$projectDir\*.py" -Destination $buildSrc +Copy-Item "$projectDir\icon.ico" -Destination $buildSrc +Copy-Item "$projectDir\icon.png" -Destination $buildSrc + +Push-Location $buildSrc try { - py -m PyInstaller --windowed --onedir --name VersaPadViewer desktop_viewer.py --noconfirm + py -m PyInstaller --windowed --onedir --name VersaPadViewer ` + --icon icon.ico --add-data "icon.png;." --add-data "icon.ico;." ` + desktop_viewer.py --noconfirm } finally { Pop-Location } @@ -21,6 +40,7 @@ try { if (Test-Path $localDir) { Remove-Item $localDir -Recurse -Force } -Copy-Item "$projectDir\dist\VersaPadViewer" -Destination $localDir -Recurse +Copy-Item "$buildSrc\dist\VersaPadViewer" -Destination $localDir -Recurse +Remove-Item $buildSrc -Recurse -Force Write-Host "Fertig: $localDir\VersaPadViewer.exe" diff --git a/desktop_viewer.py b/desktop_viewer.py index 02e6fd5..2c913ed 100644 --- a/desktop_viewer.py +++ b/desktop_viewer.py @@ -21,17 +21,28 @@ Start: python desktop_viewer.py """ import os import queue +import sys import threading import time import tkinter as tk from tkinter import filedialog, messagebox, simpledialog +import pystray +from PIL import Image + import action_dialog import versapad_combined as vcomb import versapad_data as vp import versapad_protocol as vproto import versapad_serial as vs + +def resource_path(name): + """Findet Assets (z.B. icon.png) sowohl im Quellordner als auch im + PyInstaller-Bundle (sys._MEIPASS zur Laufzeit gesetzt).""" + base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) + return os.path.join(base, name) + BG = "#14161b" CARD_BG = "#1e2129" CARD_BORDER = "#2a2d37" @@ -57,6 +68,42 @@ SERIAL_STATUS_TEXT = { "too_large": "Datenblock zu groß", } +MCP_INFO_TEXT = """Registrierung (einmalig, schon erledigt): + claude mcp add -s user versapad -- versapad_mcp_server.py + landet in ~/.claude.json, gilt fuer alle Claude-Code-Sessions. + +Lesen: + list_profiles() Profile + lokale Namen + get_profile(profile) alle 20 Buttons + 4 Encoder, lesbar beschriftet + get_macro(slot) Tastenfolge eines Makro-Slots (0-31) + get_board_status() ist das Board erreichbar, welches Profil aktiv + +Buttons setzen (profile 0-2, index 0-19): + set_button_key(profile, index, key, modifiers) + set_button_consumer(profile, index, consumer) + set_button_macro(profile, index, slot) + set_button_profile_switch(profile, index, target) + set_button_none(profile, index) + set_button_led(profile, index, r, g, b, anim, period_ms) + +Encoder setzen (index 0-3, field 'sw'/'cw'/'ccw'): + set_encoder_key / _consumer / _macro / _profile_switch / _none(...) + +Makro: + set_macro(slot, steps) steps=[{"key":"Z","modifiers":["Strg"]}, ...] + +Profilname (nur lokal, nicht aufs Board): + rename_profile(profile, name) + +Speichern/Laden: + save_local() / load_local() <-> versapad_config_all.json + load_from_board() / write_to_board() <-> echtes Board per Serial + +Wichtig: set_*-Aufrufe aendern nur den In-Memory-State. Erst +save_local() oder write_to_board() macht die Aenderung dauerhaft. +Board-Zugriff braucht den COM-Port exklusiv (nicht gleichzeitig mit +VersaGUI oder Live-Sync/Programmiermodus hier in der GUI).""" + class VersaPadViewer(tk.Tk): def __init__(self): @@ -75,6 +122,21 @@ class VersaPadViewer(tk.Tk): self._serial_results = queue.Queue() self._serial_thread = threading.Thread(target=self._serial_loop, daemon=True) + try: + self.iconbitmap(resource_path("icon.ico")) + except tk.TclError: + pass # z.B. kein .ico verfuegbar -- kein Beinbruch, nur Fenster-Icon fehlt dann + + self._tray_queue = queue.Queue() + self._tray_icon = pystray.Icon( + "versapad", Image.open(resource_path("icon.png")), "VersaPad Steuermatrix", + menu=pystray.Menu( + pystray.MenuItem("Öffnen", lambda: self._tray_queue.put("show"), default=True), + pystray.MenuItem("Beenden", lambda: self._tray_queue.put("quit")), + ), + ) + self._tray_icon.run_detached() + header = tk.Frame(self, bg=BG) header.pack(fill="x", padx=20, pady=(18, 4)) tk.Label(header, text="VersaPad Steuermatrix", bg=BG, fg=TEXT, @@ -83,6 +145,13 @@ class VersaPadViewer(tk.Tk): fg=TEXT_DIM, font=("Segoe UI", 9)) self.header_sub.pack(anchor="w") + info_btn = tk.Label(header, text="ⓘ", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 14), + cursor="hand2") + info_btn.place(relx=1.0, x=0, y=-4, anchor="ne") + info_btn.bind("", lambda e: self._show_mcp_info()) + info_btn.bind("", lambda e: info_btn.configure(fg=ACCENT)) + info_btn.bind("", lambda e: info_btn.configure(fg=TEXT_DIM)) + self.tabs = tk.Frame(self, bg=BG) self.tabs.pack(fill="x", padx=20, pady=(12, 10)) self.tab_buttons = {} @@ -135,12 +204,14 @@ class VersaPadViewer(tk.Tk): self.footer = tk.Label(self, text="", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 8)) self.footer.pack(anchor="w", padx=20, pady=(20, 10)) - self.protocol("WM_DELETE_WINDOW", self._on_close) + self.protocol("WM_DELETE_WINDOW", self._hide_to_tray) + self.bind("", self._on_unmap) self._serial_thread.start() self.set_profile(0) self.after(POLL_MS, self._poll) self.after(200, self._drain_serial_queue) + self.after(200, self._drain_tray_queue) # ── Profil-Auswahl ───────────────────────────────────────────────────── @@ -168,6 +239,30 @@ class VersaPadViewer(tk.Tk): self.combined["profile_names"][profile] = name self._update_tab_labels() + def _show_mcp_info(self): + win = tk.Toplevel(self) + win.title("MCP-Server: versapad") + win.configure(bg=BG) + win.geometry("480x520") + win.transient(self) + + tk.Label(win, text="MCP-Server \"versapad\"", bg=BG, fg=TEXT, + font=("Segoe UI", 13, "bold")).pack(anchor="w", padx=16, pady=(16, 4)) + tk.Label(win, text="Lässt eine KI (z.B. Claude) die VersaPad-Config direkt per\n" + "Tool-Aufruf lesen/bearbeiten -- ohne JSON von Hand zu schreiben\n" + "oder Klicks in dieser GUI.", + bg=BG, fg=TEXT_DIM, font=("Segoe UI", 9), justify="left").pack(anchor="w", padx=16) + + text = tk.Text(win, bg=CARD_BG, fg=TEXT, font=("Consolas", 9), wrap="word", + relief="flat", padx=12, pady=10, bd=0, highlightthickness=0) + text.pack(fill="both", expand=True, padx=16, pady=12) + text.insert("1.0", MCP_INFO_TEXT) + text.configure(state="disabled") + + tk.Button(win, text="Schließen", command=win.destroy, bg=CARD_BG, fg=TEXT, + activebackground=ACCENT, activeforeground="#fff", relief="flat", + padx=14, pady=4).pack(pady=(0, 16)) + # ── Config-Datei-Polling (nur im Nicht-Edit-Modus relevant) ──────────── def _poll(self): @@ -224,9 +319,43 @@ class VersaPadViewer(tk.Tk): text = SERIAL_STATUS_TEXT.get(error, error or "Fehler") self.sync_status.configure(text=text, fg=WARN_RED) - def _on_close(self): + # ── Tray-Icon (kein Taskleisten-Eintrag beim Minimieren, wie VersaGUI) ── + + def _on_unmap(self, event): + """Minimieren faengt Windows normalerweise als Taskleisten-Icon ab -- + wir wollen stattdessen: Fenster komplett weg, nur noch Tray-Icon.""" + if event.widget is self and self.state() == "iconic": + self._hide_to_tray() + + def _hide_to_tray(self): + self.withdraw() + + def _show_from_tray(self): + self.deiconify() + self.state("normal") + self.lift() + self.focus_force() + + def _drain_tray_queue(self): + try: + while True: + action = self._tray_queue.get_nowait() + if action == "show": + self._show_from_tray() + elif action == "quit": + self._quit() + except queue.Empty: + pass + if not self._closing: + self.after(200, self._drain_tray_queue) + + def _quit(self): self._closing = True self._link.close() + try: + self._tray_icon.stop() + except Exception: + pass self.destroy() # ── Programmiermodus ────────────────────────────────────────── diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3055bcbf85d2ce9eda66b2aaa730393dba89f1fc GIT binary patch literal 14782 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 literal 0 HcmV?d00001 diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7b7b0459f35111f418b0716d8d0a6dc920b8661 GIT binary patch literal 1967 zcmeHIYgp4|6hHrMu#Le0t)LiVO6k)FVHP-%F^1zJUcd{bNyrE-Qb8016x{w;VzwSu z@jM91CPX3?gOOYkP=;8PB+QUof{ZM{0dvBbW7qzpo_hMyx4!j$IPW>d?r1)L?%ncR5uSNFXGJ`@*#~Eas$ubLhPbMQ0r_@%o7qo9trQ#XdaW zP@yrGy#SrAjgwOi;3=pMjlDv^jKtSAu%W=GCqtLF#VIXsBpwXidgra}dInx8g9{>CsouvWS=~@8+J$C?I9+f)PuFG=AbI zGMpv@AvYUnwu;)PJ(FGYeg~wJy~3IsgxUs+{%T{(%~k<9?5EK&d2i7(XDqi9nJdBH z(@5(}mQxf9&Vt(16cW`{aqD4w$2-SJwdc6|!I?u|F;qw5jtnB6f*G+AOJ%0Wi;lGT z*jzYYhzX`_I{v#$SlaqndoA=kmgue}+8>5Y=^e|*yR0n&L6j8G>5rVSZ1is|q47&J zAstX9d}B(sIS_&5g2B(SS=OX_86MtuEYCPzI(hAp;IJ2{1UTr}>sUU0R5UF0(sUOi zP`CozwgRIBxbiGy$`DBjl3twc&B6fRC19wl-{a$7pZW{i7vE_&UwFE6k9c7&E1_bW zbztlw4L`l3dqgs@zfw3o7UPt^Q2j$Le7nx{ysh`(K&2nDe&{ameh6-$avhSwtV!_Y zhp3mMt9U=J!Bj*fC7>)}B|pUI2BcMRM{~PB78$7YKu|swB%yr#Xq!42U6_hm3Xb`w zdZ{_f(cl_mzV~aw?H~M&r)15@PMX{0^y?%bEJq`=udB0dl>ge z(>%Uce2}2ZU-e-4aZ<>@JRLj09`m1t`)6$b0q}nh{-p;s3D7hq*msJ|d|TK=fppqm zH>J^;+sxYgy*zN_{2Dyac)-|mA2Qi#YX=81Lz`T{jgHp#A1IbR;T-k`1{);PTZHlR zmO&11bD(8+iL?nZrG-OPo2RbcW%x)kj`=8}qA69$@p=|iDwq*k7RSH%B>H%jR5oli zJ}?)uk<8prrjoqAsf%>vt3}^#xUaMXtb?M&0Qw=XHgP18ZxnScHjSGm)hq+YV(1&Z zG9eAM)f<pui9M|C?;&Bn-9^E2MZCL z<-S_~72h|y(9BQskW0|UAOuxWt!H2)+Igasqq{=n&m}7Y!z;v}C$&$SvI%e+uhqoj zvrHQ9rmx#GI1g`7!63$7{6(BHzlwQG-S zB%!OZA-M_4aH=PTC9R!y6LH49Hg6+|flI&PJNaWSxXox`y@TRJp|oz79S@mMH$;Iu zYMjqh=VCH8#C0qr0@OXdJPw6{+n$j|*T2hkM^DbrDeLfVNOj<9SKCI0JGx=MUKf?O z+V-h?b$arf#>ft}TZ(?(~g{cnbKFk-U{z#l#^O-`X59_4<@lmp0G7u|DzdUkD9