Compare commits

...
2 Commits
Author SHA1 Message Date
SuperTuxii f18918f781 input-relay: Add mute key as volume key
Add KEY_MUTE as volume key. It will be mapped to KEY_MACRO28 and
use wpctl to toggle the mute of the default audio sink. The toggle of
the mute state will be done once when the key is pressed instead of
continuously toggling it when it is held down.
This allows the mute button on keyboards to work properly.

Signed-off-by: SuperTuxii <123881249+SuperTuxii@users.noreply.github.com>
2026-10-04 16:06:37 +02:00
Patrick McDavidandClaude Opus 5.5 1ebf3692fb ft-floatd: a launch for a missing app no longer kills the control socket (#16)
Gio.DesktopAppInfo.new() returns NULL for a desktop file that doesn't
exist, and PyGObject raises TypeError ("constructor returned NULL")
rather than returning None, so launch()'s `if info is None` never ran.
The exception escaped the control socket's GLib callback, GLib dropped
the watch, and ft-floatd stopped answering everything: the float key,
dock, Launch as Standalone, and profiles, until the desktop restarted.

Found on the Frame (2026-10-02): a profile saved with RustDesk's
Flatpak open records its window's app id, com.carriez.flutter_hbb,
which has no desktop file (the Flatpak's is com.rustdesk.RustDesk).
`ft-layout use` on that profile asked ft-floatd to launch it, and
ft-floatd went silent. With this, that launch replies "error no app
com.carriez.flutter_hbb" and the profile's other apps open.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:12:49 -06:00
2 changed files with 15 additions and 8 deletions

No files matched your search

+4 -1
View File
@@ -745,7 +745,10 @@ class Daemon:
Returns (reply, pid or None).""" Returns (reply, pid or None)."""
if app: if app:
app = app.removesuffix(".desktop") app = app.removesuffix(".desktop")
info = Gio.DesktopAppInfo.new(app + ".desktop") try:
info = Gio.DesktopAppInfo.new(app + ".desktop")
except TypeError: # PyGObject raises for the NULL a missing desktop file returns
info = None
if info is None: if info is None:
return f"error no app {app}", None return f"error no app {app}", None
pids = [] pids = []
+11 -7
View File
@@ -132,10 +132,10 @@ KEY_A = 30
REL_X, REL_Y, REL_WHEEL, REL_MAX = 0x00, 0x01, 0x08, 0x0F REL_X, REL_Y, REL_WHEEL, REL_MAX = 0x00, 0x01, 0x08, 0x0F
BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_SIDE, BTN_EXTRA = 0x110, 0x111, 0x112, 0x113, 0x114 BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_SIDE, BTN_EXTRA = 0x110, 0x111, 0x112, 0x113, 0x114
KEY_LEFTMETA, KEY_RIGHTMETA = 125, 126 KEY_LEFTMETA, KEY_RIGHTMETA = 125, 126
KEY_VOLUMEDOWN, KEY_VOLUMEUP = 114, 115 KEY_MUTE, KEY_VOLUMEDOWN, KEY_VOLUMEUP = 113, 114, 115
# Volume keys are remapped to KEY_MACRO29 and KEY_MACRO30: above 255, so X11 can't # Volume keys are remapped to KEY_MACRO28, KEY_MACRO29 and KEY_MACRO30: above 255, so X11
# carry them, and bound to nothing in the default keymap. # can't carry them, and bound to nothing in the default keymap.
VOLUME_STANDIN = {KEY_VOLUMEUP: 0x2AC, KEY_VOLUMEDOWN: 0x2AD} VOLUME_STANDIN = {KEY_MUTE: 0x2AB, KEY_VOLUMEUP: 0x2AC, KEY_VOLUMEDOWN: 0x2AD}
VOLUME_ORIGINAL = {v: k for k, v in VOLUME_STANDIN.items()} VOLUME_ORIGINAL = {v: k for k, v in VOLUME_STANDIN.items()}
VOLUME_CODES = set(VOLUME_STANDIN) | set(VOLUME_ORIGINAL) VOLUME_CODES = set(VOLUME_STANDIN) | set(VOLUME_ORIGINAL)
BUS_USB, BUS_BLUETOOTH, BUS_VIRTUAL = 0x03, 0x05, 0x06 BUS_USB, BUS_BLUETOOTH, BUS_VIRTUAL = 0x03, 0x05, 0x06
@@ -383,9 +383,13 @@ class Volume:
def key(self, fd, code, value, now): def key(self, fd, code, value, now):
if value == 1: if value == 1:
self.held = (fd, code) if VOLUME_ORIGINAL.get(code, code) == KEY_MUTE:
self.step(code) subprocess.Popen(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"],
self.next_at = now + self.DELAY stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
self.held = (fd, code)
self.step(code)
self.next_at = now + self.DELAY
elif value == 0 and self.held == (fd, code): elif value == 0 and self.held == (fd, code):
self.release() self.release()