Skip to content

feat: program categories, media/Discord LED sources, output resync, tray fix - #10

Merged
PalmarHealer merged 5 commits into
mainfrom
feature/app-categories-led-sources
Sep 12, 2026
Merged

PalmarHealer merged 5 commits into
mainfrom
feature/app-categories-led-sources

Conversation

@PalmarHealer

Copy link
Copy Markdown
Contributor

Four independent changes, each usable on its own.

1. Program categories for process actions

A slider or button can target Browsers or Games instead of one named
executable, so a newly installed game or browser needs no config change.

Membership comes from three places:

  • the bundled assets/app_groups.json (executable-name stems),
  • path heuristics — anything under a Steam/Epic/GOG/Riot library counts as a
    game, whatever it is called. This is the part that keeps the category useful
    without an impossible list of every game ever made,
  • the OS itself — browsers registered under StartMenuInternet, i.e. the same
    list the Windows "Default apps" page shows.

Users can extend any group (or add their own) with an app_groups.json in the
config directory; it is merged on top of the bundled file. Targets are stored as
@group:<id>; WASAPI resolves membership per audio session, using the process
image path as well as its name. Launchers and crash handlers are vetoed, so
steam.exe sitting inside the game library is not treated as a game.

2. Re-apply volumes after the default output device changes

Per-app volumes live on the audio session, and a session that follows the
default device to a new endpoint arrives carrying that endpoint's remembered
level — so the physical slider and the actual volume drift apart until the next
nudge. That jump is what this fixes.

The actuator remembers the last volume it set per target, polls the default
endpoint id (new AudioBackend::default_output_id) every 400 ms, and replays
twice after a switch (350 ms and 1500 ms) for streams that migrate late. Mute
state is deliberately untouched: switching outputs should not unmute anything.

Works for switches Slidr makes itself and for ones made in Windows.

3. Two new LED condition sources

  • Music playing (src/media.rs) — the OS media session (WinRT SMTC), so it
    covers Spotify, browser tabs, and any player that publishes transport
    controls, with no login and no API key. Optional source filter; empty = any
    player.
  • Discord (src/discord.rs) — self-mute, deafen, or in-a-voice-channel, read
    from Discord's local RPC socket. Nothing else reports Discord's self-mute:
    the OS mic endpoint stays unmuted while Discord mutes you internally.

Discord needs a one-off OAuth application (Client ID + Secret, entered under
Settings → LEDs; setup steps are in the module header). The secret is only read
until a token is cached, and SLIDR_DISCORD_CLIENT_SECRET can supply it instead
of the settings file. A half-typed Client ID never triggers a connection.

Both sources are served from a cache filled by a background worker, so the LED
tick never blocks on WinRT or a socket.

4. Tray bugfix

After an autostart --hidden launch the tray menu read "Minimize Slidr" while
clicking it opened the window. The label was hard-coded at construction time.

It is now seeded from the startup state and re-derived from the real window
state on every poll, so it cannot drift — including when the window is minimized
from the taskbar or restored outside our own code paths. Double-click (and the
menu entry) now also restore a window minimized to the taskbar, raising it to
the foreground.

Verification

  • Windows: cargo check --all-targets clean, 25 tests pass.
  • Linux (CI): cargo check passes — this is the first Linux compile of
    discord.rs's Unix-socket path, app_groups.rs's /proc walk and
    media.rs's non-Windows branch.
  • Not verified: the interactive UI paths and the Discord flow against a real
    Discord client (that needs a personal OAuth application).

⚠️ CI is red, and not because of this branch

cargo test fails at the link step with unable to find library -lxdo.
That is main's pre-existing enigo configuration (the default xdo backend
needs libxdo.so at link time), which #9 fixes by switching to the pure-Rust
x11rb backend. Every branch off current main fails the same way.

Deliberately not worked around here: adding libxdo-dev to CI would paper over
the bug #9 fixes properly, and copying #9's one-line enigo change would conflict
with it.

Merge order: #9 first, then rebase this branch — it goes green on its own.
The only conflict between the two is in src/main.rs, where both branches
insert a block at the same spot after the settings load; the resolution is
keep-both.

Follow-up

default_output_id() has a None default, so change 2 is Windows-only for now
and Linux simply no-ops. The PulseAudio implementation is five lines on top of
#9's rewritten pulse.rs and is tracked there.

🤖 Generated with Claude Code

PalmarHealer and others added 5 commits September 12, 2026 16:36
…ray fix

Program categories for process actions (src/app_groups.rs). A slider or button
can target "Browsers" or "Games" instead of one named executable, so a newly
installed game or browser needs no config change. Membership comes from the
bundled assets/app_groups.json, from path heuristics (anything under a
Steam/Epic/GOG library is a game — the part that keeps the list from going
stale), and from the OS itself (browsers registered under StartMenuInternet).
Users can extend any group with an app_groups.json in the config directory.
Stored as "@group:<id>"; WASAPI resolves membership per audio session, using
the process image path as well as its name.

Re-apply volumes after the default output device changes (src/actuator.rs).
Per-app volumes live on the audio session, and a session that follows the
default device to a new endpoint arrives carrying that endpoint's remembered
level — so the slider and the actual volume drift apart until the next nudge,
which is the jump this fixes. The actuator now remembers the last volume set
per target, polls the default endpoint id (new AudioBackend::default_output_id)
and replays twice after a switch, for the streams that move late. Mute state is
deliberately untouched.

Two new LED condition sources: the OS media session (src/media.rs, WinRT SMTC —
covers Spotify, browser tabs, any player publishing transport controls) and
Discord voice state (src/discord.rs, local RPC socket: self-mute, deafen, in a
voice channel). Discord needs a one-off OAuth application; its client secret is
only read until a token is cached, and SLIDR_DISCORD_CLIENT_SECRET can supply
it instead of the settings file.

Tray: after an autostart --hidden launch the menu read "Minimize Slidr" while
clicking it opened the window. The label is now seeded from the startup state
and re-derived from the real window state on every poll, so it cannot drift.
Double-click (and the menu entry) also restore a window minimized to the
taskbar now, raising it to the foreground.

Also adds .github/workflows/ci.yml, authored on the Linux-fix branch and
included here so this branch gets the same Linux verification.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fails

The media condition asked one question — "is something playing?" — which turns
out to be the least useful of the four states a player can be in. It is now a
"Media" condition with an explicit state: Playing, Paused, Stopped, or Nothing
detected (no media session at all). The optional source filter stays.

With several players open the liveliest state wins: a playing Spotify outranks
a paused browser tab. Without that, "Playing" would stop meaning "music is on"
as soon as any paused tab existed, and the four states would not be mutually
exclusive.

Discord, after watching someone try to use it:

* The fields were findable only by knowing where to look, and every failure
  produced the same generic line. The status now names the actual cause —
  Discord not running, Client ID/Secret rejected (HTTP 401), or the missing
  http://localhost redirect (HTTP 400) — instead of "waiting for Discord".
* That status is refreshed while the settings page or the LED popup is open.
  The worker connects seconds after the settings are saved, so the old
  push-once behaviour meant the line was stale exactly when it mattered.
* Added an "Open developer portal" button next to the credentials, since the
  setup is a detour through a web page the app can at least open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…klist

A setting row's label and description did not wrap. A Text without `wrap`
reports its full single-line width as preferred, and inside a ScrollView that
wins — so the long Discord description added in the previous commit dragged
every card to the right and pushed the controls out of view entirely. Two
changes, because either alone leaves the failure mode reachable:

* the row's texts now wrap, and
* the settings content is pinned to the viewport width, so this page can only
  ever scroll vertically no matter what text ends up in it.

The Discord instructions move out of that description into a checklist under
the credentials: the three steps, a button to the developer portal, and the
redirect URL with a Copy button beside it. Forgetting the redirect is the usual
reason the connection fails, and asking someone to retype a URL character for
character was never going to work.

Copying needs a clipboard, which Slint does not expose, so src/clipboard.rs
talks to the platform: Win32 on Windows, and wl-copy/xclip/xsel on Linux. No
new crate — just two more features on the `windows` dependency. The Win32 path
is covered by a round-trip test (including non-ASCII, to catch a byte-truncating
UTF-16 conversion); it restores whatever was on the clipboard.

Verified by screenshotting the running window: no horizontal scrollbar, cards
inside the viewport, checklist and controls visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tray timer took a single event per 120ms tick. `tray-icon` posts a Move
event for every mouse movement across the icon, so a click sat behind a flood
of them: it was handled seconds later, and further queued clicks then arrived
one per tick — which is why the window appeared to open itself over and over
and flashed in the taskbar. Both queues are now drained each tick and a burst
collapses into one action.

Left click opens the window (`with_menu_on_left_click(false)`), right click
keeps the existing menu. Double click still works, but nothing depends on it
any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the output-switch volume re-apply on Linux. Until now the trait
method fell back to its `None` default there, so the actuator never noticed a
default-sink change and the feature was Windows-only.

Reads the default sink from the cache `refresh()` already fills from
`pactl info`, so the actuator's 400ms poll costs no extra process.

Written by the session that did the Linux fixes in #9 and handed over for this
branch, since it needs both halves: the trait method from here and the
rewritten pulse.rs from there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PalmarHealer
PalmarHealer force-pushed the feature/app-categories-led-sources branch from 433f0af to afb826d Compare September 12, 2026 14:37
@PalmarHealer
PalmarHealer merged commit 3faeed8 into main Sep 12, 2026
2 checks passed
@PalmarHealer
PalmarHealer deleted the feature/app-categories-led-sources branch October 31, 2026 00:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant