fix(snap): add hardware-observe so hotkeys can read input devices (#858)

Snap confinement denies /proc/bus/input/devices unless hardware-observe
is connected. Declare the plug, tell users to connect it next to
raw-input, and update the in-app hints.

Fixes #857
This commit is contained in:
Jatin K Malik
2026-09-23 00:09:54 -07:00
committed by GitHub
parent acd90fdd05
commit 5b4e2c3572
8 changed files with 69 additions and 6 deletions
+26
View File
@@ -394,6 +394,32 @@ class TestEvdevKeyboardBackendPermissionHint:
result = backend.get_permission_hint()
assert result is None
@patch.dict("os.environ", {"SNAP": "/snap/vocalinux/x1"}, clear=False)
@patch("vocalinux.ui.keyboard_backends.evdev_backend.EVDEV_AVAILABLE", True)
@patch("vocalinux.ui.keyboard_backends.evdev_backend.find_keyboard_devices")
def test_permission_hint_snap_no_devices(self, mock_find_devices):
"""Snap with no listed devices should ask for both input plugs."""
mock_find_devices.return_value = []
backend = EvdevKeyboardBackend()
result = backend.get_permission_hint()
assert result is not None
assert "raw-input" in result
assert "hardware-observe" in result
@patch.dict("os.environ", {"SNAP": "/snap/vocalinux/x1"}, clear=False)
@patch("vocalinux.ui.keyboard_backends.evdev_backend.EVDEV_AVAILABLE", True)
@patch("vocalinux.ui.keyboard_backends.evdev_backend.find_keyboard_devices")
@patch("vocalinux.ui.keyboard_backends.evdev_backend.InputDevice")
def test_permission_hint_snap_permission_denied(self, mock_input_device, mock_find_devices):
"""Snap that cannot open /dev/input should ask for both input plugs."""
mock_find_devices.return_value = ["/dev/input/event0"]
mock_input_device.side_effect = OSError(errno.EACCES, "Permission denied")
backend = EvdevKeyboardBackend()
result = backend.get_permission_hint()
assert result is not None
assert "raw-input" in result
assert "hardware-observe" in result
class TestEvdevKeyboardBackendStart:
"""Test start() method."""
+11
View File
@@ -354,6 +354,17 @@ class TestKeyboardShortcuts(unittest.TestCase):
warning_calls = [str(call) for call in mock_logger.warning.call_args_list]
self.assertTrue(any("Permission issue" in str(call) for call in warning_calls))
def test_unavailable_hints_snap_lists_both_connects(self):
"""Snap log banner should list both raw-input and hardware-observe."""
self.mock_create_backend.return_value = None
with patch.dict("os.environ", {"SNAP": "/snap/vocalinux/x1"}, clear=False):
with patch("vocalinux.ui.keyboard_shortcuts.logger") as mock_logger:
KeyboardShortcutManager()
messages = " ".join(str(call) for call in mock_logger.warning.call_args_list)
self.assertIn("raw-input", messages)
self.assertIn("hardware-observe", messages)
class TestPynputBackend(unittest.TestCase):
"""Test cases for the pynput backend specifically."""
+15
View File
@@ -24,11 +24,16 @@ def test_snapcraft_recipe_and_gui_assets() -> None:
plugs = set((doc.get("apps") or {}).get("vocalinux", {}).get("plugs") or [])
assert "raw-input" in plugs
assert "hardware-observe" in plugs
assert "audio-record" in plugs
assert "uinput" in plugs
stage = doc["parts"]["vocalinux"].get("stage-packages") or []
assert "ydotool" in stage
recipe_text = SNAPCRAFT_YAML.read_text(encoding="utf-8")
assert "sudo snap connect vocalinux:raw-input" in recipe_text
assert "sudo snap connect vocalinux:hardware-observe" in recipe_text
assert DESKTOP_FILE.is_file()
assert SNAP_PNG.is_file()
assert SNAP_PNG.stat().st_size > 0
@@ -50,6 +55,16 @@ def test_snap_puts_gnome_platform_first_on_ld_library_path() -> None:
assert ld_path.endswith(":$LD_LIBRARY_PATH")
def test_snap_docs_tell_users_to_connect_hardware_observe() -> None:
"""Hotkeys need hardware-observe for /proc/bus/input/devices (#857)."""
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
install = (REPO_ROOT / "docs" / "INSTALL.md").read_text(encoding="utf-8")
snapcraft = SNAPCRAFT_YAML.read_text(encoding="utf-8")
for text in (readme, install, snapcraft):
assert "sudo snap connect vocalinux:raw-input" in text
assert "sudo snap connect vocalinux:hardware-observe" in text
def test_snap_docs_warn_that_0162_has_no_uinput_plug() -> None:
"""v0.16.2 edge (rev 7) has no uinput plug; the connect command must not stand alone."""
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")