The niri example binds move to Mod+Shift+M (toggle), Mod+Shift+S (start) and Mod+Shift+X (stop), free in stock niri and in Dank Material Shell's binds. inject-probe reuses the injector's niri guard, so it no longer reports the portal on niri. Fixes #45 Fixes #46
106 lines
3.9 KiB
Python
106 lines
3.9 KiB
Python
"""Ticket #22: niri + Dank Material Shell integration pack (docs/keybind seam).
|
|
|
|
A niri user following only the new documentation gets a working toggle bind
|
|
(fires once per press) with no compositor-internal APIs. The tray-watcher
|
|
question stays code-untouched until hardware observation (ticket #32);
|
|
the probe assertion below pins the current watcher name so a silent
|
|
widening fails loudly. When #32 records DMS evidence, update the doc and
|
|
this assertion together.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
NIRI_DOC = REPO_ROOT / "docs" / "NIRI.md"
|
|
NIRI_INCLUDE = REPO_ROOT / "resources" / "niri" / "voculinux.kdl"
|
|
DOCS_INDEX = REPO_ROOT / "docs" / "README.md"
|
|
USER_GUIDE = REPO_ROOT / "docs" / "USER_GUIDE.md"
|
|
TRAY_INDICATOR = REPO_ROOT / "src" / "vocalinux" / "ui" / "tray_indicator.py"
|
|
|
|
TOGGLE_SPAWN = 'spawn "voculinux" "--toggle"'
|
|
TOGGLE_HEADER = re.compile(r"^(\S+) repeat=false")
|
|
#: Move Column Right in Dank Material Shell's binds; the shipped key must not reuse it (#45).
|
|
DMS_MOVE_COLUMN_RIGHT_KEY = "Mod+Shift+D"
|
|
CURRENT_WATCHER = "org.kde.StatusNotifierWatcher"
|
|
|
|
|
|
def _read(path: Path) -> str:
|
|
assert path.is_file(), f"{path.relative_to(REPO_ROOT)} is missing"
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def _active_lines(text: str) -> list[str]:
|
|
"""Non-blank lines that are not niri // comments."""
|
|
return [
|
|
line.strip()
|
|
for line in text.splitlines()
|
|
if line.strip() and not line.strip().startswith("//")
|
|
]
|
|
|
|
|
|
def test_include_file_spawns_renamed_toggle_trigger() -> None:
|
|
"""The shipped bind drives the renamed CLI trigger with split argv."""
|
|
assert any(TOGGLE_SPAWN in line for line in _active_lines(_read(NIRI_INCLUDE)))
|
|
|
|
|
|
def test_include_bind_is_non_repeating() -> None:
|
|
"""Key repeat must not re-fire the toggle while held."""
|
|
assert "repeat=false" in _read(NIRI_INCLUDE)
|
|
|
|
|
|
def test_doc_and_include_share_a_toggle_key_not_taken_by_dms() -> None:
|
|
"""The doc's example key is the include file's toggle key, and it is not in use (#45)."""
|
|
keys = [
|
|
match.group(1)
|
|
for match in (TOGGLE_HEADER.match(line) for line in _active_lines(_read(NIRI_INCLUDE)))
|
|
if match
|
|
]
|
|
assert len(keys) == 1, "the include file should ship exactly one toggle bind"
|
|
key = keys[0]
|
|
assert key != DMS_MOVE_COLUMN_RIGHT_KEY
|
|
assert f"{key} repeat=false" in _read(NIRI_DOC)
|
|
|
|
|
|
def test_include_file_uses_no_compositor_internals() -> None:
|
|
"""Only plain spawn actions; no compositor message APIs."""
|
|
assert "niri msg" not in _read(NIRI_INCLUDE).lower()
|
|
|
|
|
|
def test_doc_shows_include_and_once_per_press() -> None:
|
|
"""Doc presents the optional include file and the once-per-press promise."""
|
|
doc = _read(NIRI_DOC).lower()
|
|
assert "include" in doc
|
|
assert "repeat=false" in doc
|
|
assert "once per press" in doc
|
|
|
|
|
|
def test_doc_warns_compositor_bind_xor_in_app_shortcut() -> None:
|
|
"""One key is a compositor bind or an in-app shortcut, never both."""
|
|
doc = _read(NIRI_DOC).lower()
|
|
assert "never both" in doc
|
|
assert "external activation" in doc
|
|
|
|
|
|
def test_doc_keeps_hold_to_talk_in_app() -> None:
|
|
"""A press-only bind cannot do push-to-talk; the release never arrives."""
|
|
doc = _read(NIRI_DOC).lower()
|
|
assert "push-to-talk" in doc or "hold-to-talk" in doc
|
|
assert "release" in doc
|
|
|
|
|
|
def test_doc_records_tray_watcher_gap_without_probe_change() -> None:
|
|
"""DMS observation is deferred to #32; the probe stays as-is meanwhile."""
|
|
doc = _read(NIRI_DOC)
|
|
assert CURRENT_WATCHER in doc
|
|
assert "#32" in doc
|
|
probe = _read(TRAY_INDICATOR)
|
|
assert CURRENT_WATCHER in probe
|
|
assert "org.freedesktop.StatusNotifierWatcher" not in probe
|
|
|
|
|
|
def test_doc_linked_from_index_and_user_guide() -> None:
|
|
"""The niri page is reachable from the docs index and the user guide."""
|
|
assert "NIRI.md" in _read(DOCS_INDEX)
|
|
assert "NIRI.md" in _read(USER_GUIDE)
|