Skip to content

Commit 2919980

Browse files
committed
fix bug: listen for pause/unpause commands on DBus for wlroots & X11 backends.
1 parent 62ace2e commit 2919980

File tree

6 files changed

+161
-19
lines changed

6 files changed

+161
-19
lines changed

llm-docs/LLM-TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ The project daemon is located at `src/daemon/` (Rust).
4545
- 2026-01-23: Legacy kanata support - versions without `RequestFakeKeyNames` API are detected via error response, reconnect skips the request. VK validation is bypassed for legacy kanata (all VKs pass through).
4646
- 2026-01-23: Config Rule struct uses `#[serde(deny_unknown_fields)]` to reject typos like `native_terminal` (should be `on_native_terminal`).
4747
- 2026-01-23: Rules without class/title matchers require `fallthrough: true` (otherwise would match everything and stop further matching).
48+
- 2026-01-27: DBus control service starts on Wayland/X11 regardless of SNI status so Pause/Unpause/Restart commands still work if SNI fails.

qa/desktop-backends-checklist.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Desktop Backends Checklist
22

3-
Last tested: 2026-01-22
4-
Environment: NixOS, KDE, Wayland
3+
Last tested: 2026-01-27
4+
Environment: NixOS, Hyprland, Wayland
55

66
## KDE Plasma
77
- [x] KWin script loads automatically
@@ -11,14 +11,14 @@ Environment: NixOS, KDE, Wayland
1111
- [x] DBus backend stays connected
1212

1313
## wlroots (Sway/Hyprland/Niri)
14-
- [ ] wlr-foreign-toplevel events received
15-
- [ ] Focus changes trigger expected actions
16-
- [ ] Daemon start applies current focused window without extra focus change
17-
- [ ] Pause/unpause re-queries current focus (no cached focus)
14+
- [x] wlr-foreign-toplevel events received
15+
- [x] Focus changes trigger expected actions
16+
- [x] Daemon start applies current focused window without extra focus change
17+
- [x] Pause/unpause re-queries current focus (no cached focus)
1818

1919
## COSMIC
20-
- [ ] cosmic-toplevel-info events received
21-
- [ ] Focus changes trigger expected actions
20+
- [x] cosmic-toplevel-info events received
21+
- [x] Focus changes trigger expected actions
2222
- [ ] Daemon start applies current focused window without extra focus change
2323
- [ ] Pause/unpause re-queries current focus (no cached focus)
2424

qa/installation-checklist.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Installation Checklist
22

3-
Last tested: 2026-01-22
4-
Environment: NixOS, KDE, Wayland
3+
Last tested: 2026-01-27
4+
Environment: NixOS, Hyprland, Wayland
55

66
## Cargo install
7-
- [ ] `cargo install --path .` succeeds
8-
- [ ] Binary is on PATH (or invoked directly)
7+
- [x] `cargo install --path .` succeeds
8+
- [x] Binary is on PATH (or invoked directly)
99
- [x] `kanata-switcher --help` shows CLI options
1010

1111
## Nix install
@@ -23,5 +23,5 @@ Environment: NixOS, KDE, Wayland
2323

2424
## Config discovery
2525
- [x] Default config path `~/.config/kanata/kanata-switcher.json` is used
26-
- [ ] Missing config errors show example config
27-
- [ ] Explicit `--config` path is honored
26+
- [x] Missing config errors show example config
27+
- [x] Explicit `--config` path is honored

qa/sni-indicator-checklist.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
# SNI Indicator Checklist (Non-GNOME)
22

3-
Last tested: 2026-01-22
4-
Environment: KDE, KDE Panel, Wayland
3+
Last tested: 2026-01-29
4+
Environment: Hyprland, No Panel, Wayland
55

66
## Indicator lifecycle
77
- [x] SNI indicator appears by default on non-GNOME
88
- [x] `--no-indicator` suppresses it
99
- [x] Logs show SNI startup and watcher online/offline
10-
- [ ] Logs show SNI watcher offline
10+
- [x] Logs show SNI watcher offline
1111

1212
## Visual behavior
1313
- [x] Layer glyph updates on focus changes
1414
- [x] VK glyph updates (single key / count / overflow)
1515
- [x] Layer glyph is white and VK glyph is cyan (matches GNOME indicator)
16-
- [ ] Glyphs use Noto Sans Mono bitmap (size 32) and VK overflow shows "9+"
16+
- [x] Glyphs use Noto Sans Mono bitmap (size 32)
17+
- [ ] VK overflow shows "9+"
1718
- [x] Tooltip shows layer and virtual keys
1819

1920
## Menu actions
@@ -28,4 +29,4 @@ Environment: KDE, KDE Panel, Wayland
2829
- [x] `--indicator-focus-only true|false` overrides startup value without locking the toggle
2930

3031
## Failure behavior
31-
- [ ] If SNI cannot be started, daemon keeps running and logs error
32+
- [x] If SNI cannot be started, daemon keeps running and logs error

src/daemon/integration_tests.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,6 +2297,110 @@ async fn test_handle_focus_event_ignored_when_paused() {
22972297
.await;
22982298
}
22992299

2300+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2301+
async fn test_dbus_pause_wayland_env() {
2302+
with_test_timeout(async {
2303+
use zbus::connection::Builder;
2304+
2305+
let dbus = DbusSessionGuard::start()
2306+
.expect("Failed to start dbus-daemon. Run `nix run .#test` or install dbus.");
2307+
2308+
let mock_server = MockKanataServer::start();
2309+
2310+
let rules = vec![Rule {
2311+
class: Some("test-app".to_string()),
2312+
title: None,
2313+
on_native_terminal: None,
2314+
layer: Some("browser".to_string()),
2315+
virtual_key: None,
2316+
raw_vk_action: None,
2317+
fallthrough: false,
2318+
}];
2319+
2320+
let address: zbus::Address = dbus.address().parse().expect("Invalid bus address");
2321+
2322+
let status_broadcaster = StatusBroadcaster::new();
2323+
let pause_broadcaster = PauseBroadcaster::new();
2324+
let pause_state = pause_broadcaster.clone();
2325+
let handler = Arc::new(Mutex::new(FocusHandler::new(rules, None, true)));
2326+
let kanata = KanataClient::new(
2327+
"127.0.0.1",
2328+
mock_server.port(),
2329+
Some("default".to_string()),
2330+
true,
2331+
status_broadcaster.clone(),
2332+
);
2333+
kanata.connect_with_retry().await;
2334+
2335+
drain_kanata_messages(&mock_server, Duration::from_millis(100));
2336+
2337+
let service_connection = Builder::address(address.clone())
2338+
.expect("Failed to create connection builder")
2339+
.build()
2340+
.await
2341+
.expect("Failed to connect to private bus");
2342+
let focus_query_connection = Builder::address(address.clone())
2343+
.expect("Failed to create focus query builder")
2344+
.build()
2345+
.await
2346+
.expect("Failed to connect focus query bus");
2347+
2348+
let restart_handle = RestartHandle::new();
2349+
register_dbus_service(
2350+
&service_connection,
2351+
focus_query_connection,
2352+
Environment::Wayland,
2353+
false,
2354+
kanata,
2355+
handler,
2356+
status_broadcaster,
2357+
restart_handle,
2358+
pause_broadcaster,
2359+
)
2360+
.await
2361+
.expect("Failed to register service");
2362+
2363+
let client = Builder::address(address)
2364+
.expect("Failed to create client builder")
2365+
.build()
2366+
.await
2367+
.expect("Failed to connect client");
2368+
2369+
let dbus_proxy = zbus::fdo::DBusProxy::new(&client)
2370+
.await
2371+
.expect("Failed to create DBus proxy");
2372+
wait_for_async(|| {
2373+
let proxy = dbus_proxy.clone();
2374+
async move {
2375+
proxy
2376+
.name_has_owner("com.github.kanata.Switcher".try_into().unwrap())
2377+
.await
2378+
.ok()
2379+
.filter(|&has_owner| has_owner)
2380+
}
2381+
})
2382+
.await
2383+
.expect("Timeout waiting for service registration");
2384+
2385+
let pause_result = client
2386+
.call_method(
2387+
Some("com.github.kanata.Switcher"),
2388+
"/com/github/kanata/Switcher",
2389+
Some("com.github.kanata.Switcher"),
2390+
"Pause",
2391+
&(),
2392+
)
2393+
.await;
2394+
assert!(
2395+
pause_result.is_ok(),
2396+
"DBus Pause failed: {:?}",
2397+
pause_result.err()
2398+
);
2399+
assert!(pause_state.is_paused(), "Expected daemon to be paused");
2400+
})
2401+
.await;
2402+
}
2403+
23002404
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
23012405
async fn test_control_command_returns_error_without_service() {
23022406
with_test_timeout(async {

src/daemon/main.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3867,6 +3867,18 @@ impl Drop for SniGuard {
38673867
}
38683868
}
38693869

3870+
struct DbusControlGuard {
3871+
_connection: Connection,
3872+
}
3873+
3874+
impl DbusControlGuard {
3875+
fn new(connection: Connection) -> Self {
3876+
Self {
3877+
_connection: connection,
3878+
}
3879+
}
3880+
}
3881+
38703882
fn dconf_get_bool(key: &str) -> Result<bool, String> {
38713883
let output = Command::new("dconf")
38723884
.args(["read", key])
@@ -5008,6 +5020,30 @@ async fn run_once() -> Result<RunOutcome, Box<dyn std::error::Error + Send + Syn
50085020
.await;
50095021
}
50105022

5023+
let dbus_control_guard = if matches!(env, Environment::Wayland | Environment::X11) {
5024+
let handler = focus_handler
5025+
.clone()
5026+
.expect("Focus handler missing for DBus control service");
5027+
let connection = Connection::session().await?;
5028+
let focus_query_connection = Connection::session().await?;
5029+
register_dbus_service(
5030+
&connection,
5031+
focus_query_connection,
5032+
env,
5033+
false,
5034+
kanata.clone(),
5035+
handler,
5036+
status_broadcaster.clone(),
5037+
restart_handle.clone(),
5038+
pause_broadcaster.clone(),
5039+
)
5040+
.await?;
5041+
Some(DbusControlGuard::new(connection))
5042+
} else {
5043+
None
5044+
};
5045+
let _dbus_control_guard = dbus_control_guard;
5046+
50115047
// Create shutdown guard - will switch to default layer when dropped
50125048
let _shutdown_guard = ShutdownGuard::new(kanata.clone());
50135049

0 commit comments

Comments
 (0)