Repository navigation
feat: program categories, media/Discord LED sources, output resync, tray fix - #10
Merged
Merged
Conversation
…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
force-pushed
the
feature/app-categories-led-sources
branch
from
September 12, 2026 14:37
433f0af to
afb826d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
assets/app_groups.json(executable-name stems),game, whatever it is called. This is the part that keeps the category useful
without an impossible list of every game ever made,
StartMenuInternet, i.e. the samelist the Windows "Default apps" page shows.
Users can extend any group (or add their own) with an
app_groups.jsonin theconfig directory; it is merged on top of the bundled file. Targets are stored as
@group:<id>; WASAPI resolves membership per audio session, using the processimage path as well as its name. Launchers and crash handlers are vetoed, so
steam.exesitting 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 replaystwice 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
src/media.rs) — the OS media session (WinRT SMTC), so itcovers Spotify, browser tabs, and any player that publishes transport
controls, with no login and no API key. Optional source filter; empty = any
player.
src/discord.rs) — self-mute, deafen, or in-a-voice-channel, readfrom 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_SECRETcan supply it insteadof 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
--hiddenlaunch the tray menu read "Minimize Slidr" whileclicking 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
cargo check --all-targetsclean, 25 tests pass.cargo checkpasses — this is the first Linux compile ofdiscord.rs's Unix-socket path,app_groups.rs's/procwalk andmedia.rs's non-Windows branch.Discord client (that needs a personal OAuth application).
cargo testfails at the link step withunable to find library -lxdo.That is main's pre-existing enigo configuration (the default
xdobackendneeds
libxdo.soat link time), which #9 fixes by switching to the pure-Rustx11rbbackend. Every branch off current main fails the same way.Deliberately not worked around here: adding
libxdo-devto CI would paper overthe 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 branchesinsert a block at the same spot after the settings load; the resolution is
keep-both.
Follow-up
default_output_id()has aNonedefault, so change 2 is Windows-only for nowand Linux simply no-ops. The PulseAudio implementation is five lines on top of
#9's rewritten
pulse.rsand is tracked there.🤖 Generated with Claude Code