Skip to content

Commit c6d6a75

Browse files
authored
chore(tray): replace GTK3 tray backend with ksni (#163)
* chore(tray): replace gtk tray backend with ksni * chore(packaging): refresh aur metadata * fix(packaging): validate aur packages against correct sources * fix(packaging): address tray migration review
1 parent bdeea91 commit c6d6a75

24 files changed

Lines changed: 321 additions & 1433 deletions

.github/workflows/appimage.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ jobs:
3636
run: |
3737
sudo apt-get -o Acquire::Retries=3 update
3838
sudo apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
39-
libgtk-3-dev libglib2.0-dev libxdo-dev libpipewire-0.3-dev \
40-
pkg-config imagemagick file
39+
libpipewire-0.3-dev libwayland-dev pkg-config imagemagick file
4140
4241
- name: Cache cargo registry and build
4342
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

.github/workflows/aur.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ jobs:
3434
fail-fast: false
3535
matrix:
3636
# honkhonk: source build. honkhonk-bin: .deb re-extract.
37-
# honkhonk-git: VCS build from main.
37+
# honkhonk-git: VCS build from main. PR validation rewrites it to the
38+
# candidate head commit below so dependency removals are tested against
39+
# the code that will become main.
3840
pkg: [honkhonk, honkhonk-bin, honkhonk-git]
3941
steps:
4042
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@@ -59,6 +61,17 @@ jobs:
5961
sudo -u builder makepkg --printsrcinfo > /tmp/.SRCINFO.fresh
6062
diff -u .SRCINFO /tmp/.SRCINFO.fresh
6163
64+
- name: Point VCS package at PR head
65+
if: github.event_name == 'pull_request' && matrix.pkg == 'honkhonk-git'
66+
working-directory: packaging/aur/${{ matrix.pkg }}
67+
env:
68+
PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
69+
PR_HEAD: ${{ github.event.pull_request.head.sha }}
70+
run: |
71+
sed -i \
72+
"s|^source=.*|source=(\"git+https://github.com/${PR_REPO}.git#commit=${PR_HEAD}\")|" \
73+
PKGBUILD
74+
6275
- name: Wait for release artifacts (tag push only)
6376
if: startsWith(github.ref, 'refs/tags/') && matrix.pkg != 'honkhonk-git'
6477
working-directory: packaging/aur/${{ matrix.pkg }}

.github/workflows/codeql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
run: |
3030
sudo apt-get -o Acquire::Retries=3 update
3131
sudo apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
32-
libgtk-3-dev libglib2.0-dev libxdo-dev libpipewire-0.3-dev pkg-config
32+
libpipewire-0.3-dev libwayland-dev pkg-config
3333
3434
- name: Autobuild
3535
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5

.github/workflows/deb.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ jobs:
2727
run: |
2828
sudo apt-get -o Acquire::Retries=3 update
2929
sudo apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
30-
libgtk-3-dev libglib2.0-dev libxdo-dev libpipewire-0.3-dev \
31-
pkg-config lintian imagemagick
30+
libpipewire-0.3-dev libwayland-dev pkg-config lintian imagemagick
3231
3332
- name: Cache cargo registry and build
3433
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

.github/workflows/rust.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
run: |
3131
sudo apt-get -o Acquire::Retries=3 update
3232
sudo apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
33-
libgtk-3-dev libglib2.0-dev libxdo-dev libpipewire-0.3-dev pkg-config
33+
libpipewire-0.3-dev libwayland-dev pkg-config
3434
3535
- name: Cache cargo registry and build
3636
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
@@ -87,7 +87,7 @@ jobs:
8787
run: |
8888
sudo apt-get -o Acquire::Retries=3 update
8989
sudo apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
90-
libgtk-3-dev libglib2.0-dev libxdo-dev libpipewire-0.3-dev pkg-config
90+
libpipewire-0.3-dev libwayland-dev pkg-config
9191
9292
- name: Cache cargo registry and build
9393
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
@@ -112,7 +112,7 @@ jobs:
112112
run: |
113113
sudo apt-get -o Acquire::Retries=3 update
114114
sudo apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
115-
libgtk-3-dev libglib2.0-dev libxdo-dev libpipewire-0.3-dev pkg-config
115+
libpipewire-0.3-dev libwayland-dev pkg-config
116116
117117
- name: Cache cargo registry and build
118118
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

AGENTS.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Wayland-native Linux soundboard. Pure Rust — Iced 0.14 GUI (Elm/MVU) + PipeWir
1919
- Iced 0.14 GUI (Elm/MVU); no other frontend framework — UI is `Element<Message>` functions.
2020
- PipeWire via pipewire-rs: one persistent virtual sink, no per-sound node churn.
2121
- Global shortcuts via ashpd (xdg-desktop-portal GlobalShortcuts v2) — Phase 2.
22-
- Tray via tray-icon (StatusNotifierItem; cross-DE). Audio decode via symphonia (pure Rust).
22+
- Tray via ksni (StatusNotifierItem over zbus; cross-DE). Audio decode via symphonia (pure Rust).
2323
- Renderer: wgpu default, tiny-skia software fallback (`HONKHONK_RENDERER=software`).
2424
- **Wayland-native only — no X11. PipeWire only — no direct PulseAudio.**
2525

@@ -63,8 +63,7 @@ specific APIs. One binary must work on KDE, GNOME, Hyprland, Sway:
6363
`org.freedesktop.portal.FileChooser` · Notifications `org.freedesktop.portal.Notification` ·
6464
Autostart `org.freedesktop.portal.Background`.
6565

66-
Tray uses StatusNotifierItem (SNI) via tray-icon — no XEmbed. The `libayatana-appindicator`
67-
deprecation warning on stderr is upstream and harmless — ignore it.
66+
Tray uses StatusNotifierItem (SNI) via ksni — no XEmbed, no GTK/appindicator tray stack.
6867

6968
## Packaging / XDG
7069

ARCHITECTURE.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ The complete HTML/JSX mockup lives in the design handoff bundle (exported from c
168168
| Renderer | **wgpu** (default) / **tiny-skia** (fallback) | GPU-accelerated by default, software renderer via env var for edge cases |
169169
| PipeWire | **pipewire-rs 0.8** | Official Rust bindings from PipeWire project. Production-proven |
170170
| Global Shortcuts | **ashpd 0.13** (`global_shortcuts` feature) | Full xdg-desktop-portal GlobalShortcuts API. Async/tokio |
171-
| System Tray | **tray-icon 0.19** + **muda 0.15** | Actively maintained (Tauri team), standalone SNI, cross-DE |
171+
| System Tray | **ksni 0.3** | Pure-Rust StatusNotifierItem over zbus, cross-DE |
172172
| Audio Decode | **symphonia 0.5** | Pure Rust. MP3, WAV, OGG, FLAC, AAC. No C dependencies |
173173
| Audio Playback | **pipewire-rs** streams | Direct PipeWire playback — no rodio/ALSA intermediary |
174174

@@ -225,8 +225,8 @@ Wayland sessions require GPU drivers (compositor needs them), so wgpu works on a
225225
│ Channels (no IPC serialization)
226226
227227
┌─────────────────────────────┐ ┌────────────────────┐
228-
│ AudioEngine (PipeWire │ │ tray-icon
229-
│ thread) │ │ (main thread,
228+
│ AudioEngine (PipeWire │ │ ksni tray service
229+
│ thread) │ │ (D-Bus thread, │
230230
│ - virtual sink │ │ channel → sub) │
231231
│ - mic passthrough │ └────────────────────┘
232232
│ - playback streams │
@@ -245,11 +245,12 @@ No IPC serialization. Direct Rust channel communication:
245245

246246
1. **UI → Audio (Commands):** Iced `Command::perform` sends `AudioCommand` via `tokio::sync::mpsc` to PipeWire thread.
247247
2. **Audio → UI (Events):** PipeWire thread sends `AudioEvent` via channel. Iced `Subscription` polls receiver each frame.
248-
3. **Tray → App:** `tray-icon` sends events via channel → Iced `Subscription`.
248+
3. **Tray → App:** `ksni` sends SNI events via channel → Iced `Subscription`.
249249

250250
### Threading Model
251251

252-
- **Main thread:** Iced event loop + tray-icon (both need main thread on Linux)
252+
- **Main thread:** Iced event loop.
253+
- **Tray thread:** Blocking ksni D-Bus StatusNotifierItem service.
253254
- **PipeWire thread:** Dedicated thread running PipeWire's own event loop. Owns `AudioEngine`.
254255
- **Communication:** Bounded `mpsc` channels. Non-blocking sends from main thread.
255256

@@ -363,7 +364,7 @@ honkhonk/
363364
│ │ └── playback.rs # Play sound to sink + monitor output
364365
│ ├── tray/
365366
│ │ ├── mod.rs
366-
│ │ └── icon.rs # tray-icon setup, menu, quit handler
367+
│ │ └── icon.rs # ksni SNI setup, menu, quit handler
367368
│ ├── shortcuts/ # Phase 2
368369
│ │ ├── mod.rs
369370
│ │ ├── error.rs # PortalError enum
@@ -400,7 +401,10 @@ honkhonk/
400401
│ ├── 002-pipewire-only-no-pulseaudio.md
401402
│ ├── 003-fixed-slot-hotkey-model.md
402403
│ ├── 004-persistent-sink-no-per-sound-nodes.md
403-
│ └── 005-tray-icon-over-ksni.md
404+
│ ├── 006-fundsp-over-rubberband-soundtouch.md
405+
│ ├── 007-links-only-routing-over-pw-filter.md
406+
│ ├── 008-subprocess-fallback-for-shortcut-config-ui.md
407+
│ └── 009-canvas-sticker-tiles-rejected.md
404408
├── .github/
405409
│ └── workflows/
406410
│ ├── ci.yml # Lint, test, build on PR
@@ -422,8 +426,7 @@ iced = { version = "0.14", features = ["tokio", "tiny-skia"] }
422426
pipewire = "0.8" # pipewire-rs — official PipeWire Rust bindings
423427
ashpd = { version = "0.13", features = ["global_shortcuts", "tokio"] } # Phase 2
424428
symphonia = { version = "0.5", features = ["mp3", "ogg", "flac", "wav", "pcm", "aac"] }
425-
tray-icon = "0.19" # System tray (StatusNotifierItem)
426-
muda = "0.15" # Menu for tray-icon
429+
ksni = { version = "0.3", features = ["blocking"] } # System tray SNI over zbus
427430
thiserror = "2" # Typed error enums at module boundaries
428431
anyhow = "1" # Error context chains in glue/app layer
429432
serde = { version = "1", features = ["derive"] }
@@ -530,14 +533,12 @@ This ensures cross-DE compatibility. KDE, GNOME, and Hyprland each implement the
530533

531534
### System Tray
532535

533-
StatusNotifierItem (SNI) is the standard. Implementation via `tray-icon` crate:
534-
1. Initialize on main thread before Iced event loop
535-
2. Menu via `muda` crate: "Show/Hide", separator, "Quit"
536+
StatusNotifierItem (SNI) is the standard. Implementation via `ksni`:
537+
1. Start a blocking SNI D-Bus service from the tray module
538+
2. Provide icon pixmap plus DBusMenu entries: "Show/Hide", separator, "Quit"
536539
3. Events communicated to Iced via channel → Subscription
537540
4. No legacy XEmbed tray support
538541

539-
**Known warning:** `tray-icon` depends on `libappindicator` which loads `libayatana-appindicator` at runtime. This produces a harmless deprecation warning on stderr: `libayatana-appindicator is deprecated. Please use libayatana-appindicator-glib in newly written code.` This is an upstream issue in the `libappindicator` crate — not actionable from our code. Do not suppress it.
540-
541542
### .desktop File
542543

543544
```ini

0 commit comments

Comments
 (0)