Skip to content

Persistent daemon#3

Open
neko-kai wants to merge 94 commits into
mainfrom
persistent-daemon
Open

Persistent daemon#3
neko-kai wants to merge 94 commits into
mainfrom
persistent-daemon

Conversation

@neko-kai

@neko-kai neko-kai commented Apr 9, 2026

Copy link
Copy Markdown
Member

Goal: daemon should work in the Linux console and in the display manager before login. It should support switching between DEs, logging out to DM, destroying display server and starting it back, all without having to restart the daemon and while switching layers to expected state.

Non-goal: support parallel desktop sessions. The daemon works via dbus - if each session uses a separate dbus, that would work naturally, otherwise not.

Nice to have: support connections to multiple Kanatas within one daemon

Current state:

  • the diff is overlarge, lifecycle handling almost doubles original code size
  • tested: GNOME<->gdm<->console, KDE

neko-kai added 30 commits March 10, 2026 15:26
neko-kai added 30 commits May 13, 2026 13:08
Behaviour-preserving relocation:
- args.rs (new): TrayFocusOnly, Args (clap derive), parse_dbus_suffix_arg,
  resolve_install_gnome_extension, resolve_control_command.
- autostart.rs (new): the 8 desktop-entry helpers plus the 3 AUTOSTART_*
  constants.

main.rs: 7092 -> 6788 LOC.

Verification: cargo build clean; cargo test --bin kanata-switcher
261 passed / 0 failed.
Behaviour-preserving relocation:
- broadcasters.rs (new): StatusSnapshot, LayerSource, StatusBroadcaster,
  RestartHandle, PauseBroadcaster, RuntimeEnvironmentBroadcaster,
  ShutdownHandle, wait_for_restart_or_shutdown.

main.rs: 6788 -> 6588 LOC.

Verification: cargo build clean; cargo test --bin kanata-switcher
261 passed / 0 failed.
Behaviour-preserving relocation of the kanata TCP client. Highest-
coupling extraction so far — every backend consumes KanataClient.

kanata.rs (new): all *Msg/*Payload wire types, KanataClientInner,
KanataClient + full impl, ShutdownGuard + Drop. KanataClient stays
`pub` per plan lock; everything else `pub(crate)`. Whitebox tests
required widening the inner field and three helper fns to pub(crate).

main.rs: 6588 -> 5945 LOC.

Verification: cargo build clean; cargo test --bin kanata-switcher
261 passed / 0 failed.
First nested-directory module. Behaviour-preserving relocation:
- control/mod.rs: ControlCommand + impl, ControlDispatch.
- control/client.rs: send_control_command*, BroadcastEntryReport,
  BroadcastReport, enumerate_daemon_names,
  send_control_command_broadcast, plus BROADCAST_PER_CALL_TIMEOUT.

main.rs: 5945 -> 5791 LOC. args.rs updated to use
crate::control::ControlCommand (was super::ControlCommand from PR-04a).

Verification: cargo build clean; cargo test --bin kanata-switcher
261 passed / 0 failed.
Behaviour-preserving relocation:
- pause.rs (new): UnpauseContext, local_sni_unpause_context,
  pause_daemon, unpause_daemon, plus test-only TEST_LAST_UNPAUSE_REQUEST_ENV
  and its helpers.
- focus_pipeline.rs (new): execute_focus_actions, extract_focus_layer,
  update_status_for_focus, handle_focus_event, native_terminal_window,
  resolve_sni_focus_only.

main.rs: 5791 -> 5579 LOC. Visibility widenings (D19 + extras):
query_focus_for_env, apply_focus_for_env, SniSettingsStore,
SNI_DEFAULT_SHOW_FOCUS_ONLY are now pub(crate) so the new modules
can reach still-in-main.rs consumers. apply/query_focus_for_env
revert when they move to backends/mod.rs in PR-11; SNI items
move in PR-14.

Verification: cargo build clean; cargo test --bin kanata-switcher
261 passed / 0 failed.
Behaviour-preserving relocation of the lifecycle-provider cluster
into a directory module. ~640 LOC moved.

- lifecycle/mod.rs: LifecycleProvider enum + impl, plus pub(crate)
  re-exports of the three submodules so main.rs's lifecycle::*
  glob reaches submodule items (per plan D13).
- lifecycle/logind.rs: all logind parse/decode helpers,
  LogindLifecycleProvider, monitor task helpers, snapshot decoders.
- lifecycle/startup.rs: StartupSnapshotProvider.
- lifecycle/snapshot.rs: snapshot_no_session helper.

main.rs: 5579 -> 4911 LOC. resolve_logind_session_path widened
to pub(crate) because resolve_display_override_from_logind (still
in main.rs until PR-08) consumes it.

Verification: cargo build clean (warning count 1 -> 7, all unused
imports in main.rs from incomplete cleanup — follow-up tracked);
cargo test --bin kanata-switcher 261 passed / 0 failed.
Largest single-PR extraction so far (~920 LOC). Per defect D01,
display_override.rs lands at crate root (not under supervisor/)
to break the cycle that backends would create in PR-11.

- display_override.rs (top-level, new): the display-override block
  including both test statics (X11 and Wayland) and the cfg-test/
  cfg-not(test) variants of the test setters.
- supervisor/capabilities.rs (new): detect_desktop_capabilities,
  resolve_runtime_target_for_snapshot.
- supervisor/mod.rs (new): BackendContext, BackendHandle, runtime_target
  helpers, the five run_*_backend_task adapters, start_backend,
  SupervisorState, transition_runtime_target variants,
  run_lifecycle_supervisor variants, wait/poll helpers,
  WAYLAND_CAPABILITY_RECHECK_INTERVAL.

main.rs: 4911 -> 4026 LOC. Visibility widenings per D06 + D23:
run_gnome/kde/wayland/x11, query_*_focus / query_*_active_window,
BackendExit, map_run_outcome_to_backend_exit all widened to pub(crate);
plus the unplanned session_bus_name_has_owner widening. BackendExit
and the mapping helper stay in main.rs until PR-12 moves them to
backends/mod.rs.

Verification: cargo build clean; cargo test --bin kanata-switcher
261 passed / 0 failed.
Highest-macro-risk PR — wayland_scanner generators relocated into a
nested module. PR-00 confirmed CARGO_MANIFEST_DIR-relative path
resolution; the literal "src/protocols/..." path strings ride
along unchanged.

- backends/mod.rs: RawFdWatcher (shared by wayland and x11 per D05);
  pub(crate) mod wayland; pub(crate) use wayland::*.
- backends/wayland/mod.rs: ToplevelWindow, WaylandState + impl,
  WaylandProtocol, run_wayland and the four wayland helpers
  (resolve_wayland_socket_path, connect_wayland_with_display_override,
  query_wayland_active_window, wayland_query_count). Submodule
  re-exports per D24.
- backends/wayland/protocols.rs: mod cosmic_workspace + mod cosmic_toplevel
  with wayland_scanner generators. Intra-module crate::cosmic_workspace
  paths rewritten to super::/super::super:: per D03.
- backends/wayland/dispatch_{common,wlr,cosmic}.rs: split-out Dispatch
  impls (8 total) for cleaner per-protocol diffs.

main.rs: 4026 -> 3526 LOC. supervisor/mod.rs's run_wayland call site
updated to crate::backends::wayland::run_wayland.

Verification: cargo build clean (warnings 8 -> 18, all unused-import
drift; cleanup deferred); cargo test --bin kanata-switcher 261/0.
Behaviour-preserving relocation:
- backends/x11.rs (new): x11rb::atom_manager! block (per D11),
  X11State + impl, run_x11, query_x11_active_window.
- backends/linux_console.rs: placeholder only. run_linux_console_backend_task
  stays in supervisor/mod.rs because it takes BackendContext, which would
  create a backends -> supervisor cycle if moved. PR-12 will absorb it
  into a FocusBackend impl together with the other backend_task adapters.

main.rs: 3526 -> 3300 LOC. supervisor/mod.rs's run_x11 import updated
to crate::backends::x11::run_x11.

Verification: cargo build clean; cargo test --bin kanata-switcher 261/0.
Behaviour-preserving relocation. ~800 LOC across 4 files.
- backends/gnome.rs: run_gnome + query_gnome_focus + GNOME focus
  signal subscription helpers.
- backends/kde/mod.rs: KwinScriptGuard + run_kde, plus pub(crate)
  re-exports of script and probe submodules (per D24).
- backends/kde/script.rs: KWin script-generation helpers
  (kwin_*_script_path, KdeFocusQueryService with zbus interface,
  build_kde_query_script, build_kde_focus_push_script).
- backends/kde/probe.rs: KWin runtime-mode probe + script unload
  helpers + query_kde_focus.
- backends/mod.rs: added query_focus_for_env and apply_focus_for_env
  dispatch helpers (the natural home per plan §3 line 113).

main.rs: 3300 -> 2514 LOC. supervisor/mod.rs's run_* imports
updated to crate::backends::{gnome,kde}::run_{gnome,kde}.
pause.rs's apply_focus_for_env import updated to
crate::backends::apply_focus_for_env. Visibility widenings
on these items from PR-06/PR-08 are now redundant (functions
moved); no main.rs widening to revert because the items left
main.rs entirely.

Verification: cargo build clean; cargo test --bin kanata-switcher 261/0.
First semantic-shaped change. Behaviour identical; dispatch via
Box<dyn FocusBackend> instead of a 5-arm match calling free
adapter functions. Trait shape per PR-00 finding:

    fn run(
        self: Box<Self>,
        ctx: BackendRunContext,
    ) -> Pin<Box<dyn Future<Output = Result<BackendExit, DynError>>
                + Send + 'static>>;

(RPIT-in-traits remains dyn-incompatible on rustc 1.92.0; boxed
future is the locked shape.)

- backends/mod.rs: BackendExit + map_run_outcome_to_backend_exit
  relocated from main.rs (D23 contract honored — they lived in
  main.rs widened to pub(crate) from PR-08 to here). Added
  BackendRunContext struct and FocusBackend trait.
- backends/{gnome,kde,wayland,x11,linux_console}.rs: one
  XxxBackend zero-sized struct + impl FocusBackend per backend.
  Each run body Box::pin-s an async move that delegates to the
  existing free run_xxx function and maps the result through
  map_run_outcome_to_backend_exit. linux_console got its body
  inlined from the former supervisor adapter (the only one with
  substantive content).
- supervisor/mod.rs: start_backend now dispatches via the trait;
  the five run_*_backend_task adapter functions deleted (-188 LOC).

main.rs: 2514 -> 2501 LOC.

Verification: cargo build clean; cargo test --bin kanata-switcher
261/0. No Send-propagation issues.
Behaviour-preserving relocation. ~545 LOC moved out of main.rs.
- control/server.rs: DbusWindowFocusService + its zbus #[interface]
  impl (kept colocated — macro requires same file),
  resolve_runtime_unpause_context, DbusServiceRegistration + Drop,
  register_dbus_service variants.
- control/persistent.rs: DBUS_RECONNECT_DELAYS_MS,
  dbus_reconnect_delay, wait_for_dbus_reconnect_retry,
  PersistentDbusServiceGuard + Drop, start/run persistent
  service variants.
- control/mod.rs: pub(crate) mod server / persistent and
  pub(crate) use {server::*, persistent::*} for test reach.

main.rs: 2501 -> 1979 LOC. All consumers of the moved items lived
in main.rs already; no cross-call-site updates needed.

Verification: cargo build clean; cargo test --bin kanata-switcher 261/0.
Largest single-PR extraction so far (~1015 LOC). Behaviour-preserving.

- sni/mod.rs: SniControl variant enum, SniControlMode,
  SniRuntimeTransitionPlan, SniRuntimeWakeReason, mode-resolution
  and transition-planning helpers. pub(crate) use re-exports all
  submodules per D24.
- sni/settings.rs: DconfBackend trait + ShellDconfBackend,
  SniSettingsStore, dconf helpers.
- sni/state.rs: MenuRefresh + SniIndicatorState.
- sni/indicator.rs: SniIndicator + impl Tray, start_sni_indicator,
  SniIndicatorRuntimeHandle + Drop. Test-only ACTIVE_SNI_WATCHER_TASKS
  + SniWatcherTaskGuard + sni_watcher_task_count under cfg(test).
- sni/control_{local,dbus}.rs: SniLocalControl and SniDbusControl
  structs (fields pub(crate) so control_ops can pattern-match).
- sni/control_ops.rs: SniControlOps trait + impl for SniControl.
- sni/guard.rs: SniGuard + build_sni_control_for_mode +
  SNI_RUNTIME_RETRY_INTERVAL.

main.rs: 1979 -> 958 LOC. (Broke below 1000 LOC.) Removed ksni and
noto_sans_mono_bitmap top-level imports.

Verification: cargo build clean; cargo test --bin kanata-switcher
261 passed / 0 failed.
Behaviour-preserving relocation. Closes the M1 milestone.

- gnome_ext/embed.rs (cfg-gated): gnome_ext_file! macro with the
  critical relative-path bump from concat!("../../", ...) to
  concat!("../../../", ...) — one level deeper because the file
  moved one module level. All 9 EMBEDDED_* include_str! consts.
  compile_gnome_schemas + write_embedded_extension_to_dir.
- gnome_ext/detection.rs: GnomeExtensionStatus, GnomeDbusProbeResult,
  state parsers, dbus probe variants, gnome_extension_status,
  session_bus_name_has_owner (PR-08 widening's final home).
- gnome_ext/install.rs: pack/install/enable + path helpers.
- gnome_ext/mod.rs: submodule declarations + pub(crate) use re-exports
  + orchestration fns (setup_gnome_extension, ensure_gnome_extension,
  print_*).

main.rs: 958 -> 250 LOC. Pre-refactor was 7983 LOC — 96.9% reduction.
Well below the plan's <=400 LOC target.

supervisor/capabilities.rs: two call sites updated to
crate::gnome_ext::detection::session_bus_name_has_owner.

Verification: cargo build (default features) ✓; cargo build
--no-default-features ✓; cargo test --bin kanata-switcher 261/0.
M1 milestone complete.
cargo fix cleanup: 16 unused imports removed across backends/mod.rs,
backends/wayland/mod.rs, control/mod.rs, lifecycle/mod.rs,
gnome_ext/mod.rs, sni/mod.rs.

main.rs and sni/indicator.rs were reverted from cargo fix because
their "unused" imports are used by test code via the
#[cfg(test)] pub(crate) use super::*; re-export chain — removing
them broke cargo test. Remaining 26 warnings on those two files
are intentional test-surface re-exports; leave them.

Verification: cargo build clean (default + no-default-features);
cargo test --bin kanata-switcher 261/0.
Captures the full PR-00 through PR-15 sequence: per-PR LOC moved,
surprises (e.g. environ vs env, dropping BackendRunContext.environment,
linux_console staying in supervisor), trait seams (FocusBackend new,
DconfBackend and SniControlOps formalized), V0 reality (cargo build
+ test only — pre-existing clippy/fmt debt out of scope), and
follow-ups for the user (test-surface unused-import warnings,
pre-existing clippy debt).
Behaviour-preserving: 4864 LOC flat file becomes:
- tests/common/helpers.rs (101 LOC): TEST_TIMEOUT const,
  SNI_WATCHER_TEST_LOCK static, with_test_timeout, win, rule, rule_vk,
  rule_raw_vk, rule_with_fallthrough, has_action, get_layers,
  get_raw_vk_actions.
- tests/common/mod.rs (3 LOC): use super::*; mod helpers;
  pub(super) use helpers::*;
- tests/mod.rs (4768 LOC): all 189 tests, unchanged, plus
  `mod common; pub(super) use common::*;` at the top.

src/daemon/tests.rs deleted. Rust's mod resolver picks tests/mod.rs
automatically — main.rs unchanged.

Helper visibility is pub(crate) rather than pub(super) (deviation
from plan §4); fine because the whole tests module is cfg(test).

Verification: 189 test attrs preserved; cargo build clean;
cargo test 261 passed / 0 failed.
Behaviour-preserving. 189 test attributes preserved (105 in new leaf
files + 86 still in mod.rs awaiting M2-PR-03 lifecycle split).

- focus_flow.rs (711 LOC, 32 tests)
- focus_pipeline.rs (193, 6 — including 2 misfiled from the
  "GNOME Extension State Parsing Tests" banner)
- focus_property.rs (279, 1 proptest block / 5 prop tests + 8 strategy fns)
- autostart.rs (58, 3)
- dbus_naming.rs (303, 28 + 1 proptest block)
- control_commands.rs (31, 4)
- kde_script_paths.rs (69, 4 path tests + build_kde_focus_push)
- sni_presentation.rs (484, 17 — plan estimated 14 but actual audit
  shows 17 legitimate SNI tests)
- gnome_ext_state.rs (46, 3 — the rest of the misnamed banner
  relocated to focus_pipeline.rs and stay in mod.rs)
- config_parsing.rs (119, 8)

Residual tests/mod.rs: 2499 LOC, 86 test attrs — all relocate in
M2-PR-03 (lifecycle subtree).

No production source files modified. cargo build clean;
cargo test --bin kanata-switcher 261/0.
Behaviour-preserving. 86 tests moved from tests/mod.rs to 10 leaf
files under tests/lifecycle/. After this PR, tests/mod.rs is 26 LOC —
just module declarations + re-exports; zero test bodies remain at
the directory root.

- lifecycle/fixtures.rs (67, 0): 4 backend-context helpers (pub(crate))
- lifecycle/restart_or_shutdown.rs (47, 3): rescued from
  "GNOME Extension State Parsing Tests" misnomer banner
- lifecycle/logind_decode.rs (388, 29): 9 rescued + 20 logind decode/
  path/error tests
- lifecycle/display_apply.rs (81, 5)
- lifecycle/runtime_target.rs (273, 10)
- lifecycle/persistent_dbus.rs (74, 2)
- lifecycle/sni_runtime.rs (311, 7)
- lifecycle/transition.rs (370, 9)
- lifecycle/provider.rs (98, 5)
- lifecycle/supervisor.rs (770, 16)

No production source files modified. cargo build clean; cargo test
--bin kanata-switcher 261/0. tests.rs split complete: 4864 LOC mono
file -> 22 leaf files + common/helpers.rs, all <= 770 LOC.
Behaviour-preserving. 7176 LOC flat file becomes:
- integration_tests/common/polling.rs (90): POLL_*/LONG_TEST_TIMEOUT
  constants, 4 env locks, EnvVarGuard, wait_for, wait_for_async,
  with_*_test_timeout.
- integration_tests/common/mock_kanata.rs (191): KanataMessage,
  wait_for/drain_kanata_messages, MockKanataConfig, MockKanataServer.
- integration_tests/common/focus_service.rs (149):
  start_wayland_test_server, pause/unpause_daemon_direct,
  FocusService zbus interface, TEST_DAEMON_DBUS_NAME,
  start_gnome_focus_service.
- integration_tests/common/dbus_session.rs (119):
  dbus_daemon_available, DBUS_TEST_COUNTER, DbusSessionGuard.
- integration_tests/common/mod.rs (11): mod decls + pub(crate) use.
- integration_tests/mod.rs (6638): all 65 tests + mod common.

src/daemon/integration_tests.rs deleted.

One zbus visibility tweak: FocusService::focus_changed signal made
pub (was implicit-private when the interface lived next to callers).
No other production source changes.

start_wayland_test_server stays in common/focus_service.rs;
references super::super::wayland_mock::WaylandMockServer
(inline child still in mod.rs). No cycle.

Verification: 65 test attrs preserved; cargo build clean;
cargo test --bin kanata-switcher 261 passed / 0 failed.
Behaviour-preserving. 65 tests moved from integration_tests/mod.rs
into 10 backend-grouped leaf files. After this PR, mod.rs is 32 LOC
of module declarations; zero test bodies remain at the directory root.

- gnome/mod.rs (4) + gnome/focus_query.rs (100, 1) + gnome/
  extension_detection.rs (309, 2 + MockGnomeShellExtensions)
- kde/mod.rs (3) + kde/focus_query.rs (742, 4 + MockKwinScripting/
  MockKwinScript/extract_call_dbus_parts)
- wayland.rs (444, 4 + inline pub(super) mod wayland_mock)
- x11.rs (753, 6 + xvfb_available, XvfbGuard)
- vk_validation.rs (694, 12)
- dbus_control.rs (288, 3)
- dbus_session_tests.rs (1631, 14) — slightly over the 1500-LOC
  soft cap; tests share DbusSessionGuard lifecycle so kept together.
  Optional M2-PR-06 can split if the byte count is unacceptable.
- dbus_multiplex.rs (1463, 15 + TEST_DAEMON_DBUS_NAME_A/_B consts +
  register_test_daemon_with_name)
- dconf.rs (201, 3 + IsolatedDconfEnv, is_dconf_available)

One visibility fix during integration: mod wayland_mock (inline
child of wayland.rs) needed pub(super) modifier — fixed inline.
common/focus_service.rs path updated to
super::super::wayland::wayland_mock::WaylandMockServer.

Verification: 65 test attrs preserved (sum across leaves = 65;
mod.rs = 0). cargo build clean; cargo test --bin kanata-switcher
261 passed / 0 failed.

M2 milestone complete: tests.rs (4864 LOC) -> 22 leaf files;
integration_tests.rs (7176 LOC) -> 14 leaf files. No production
source changes; behaviour preserved.
Behaviour-preserving. 15 tests moved from dbus_session_tests.rs into
4 leaf files by what each test exercises:

- dbus_session_status.rs (674, 5): real-bus + initial-layer +
  focus-source + paused-changed signal + status-changed focus signal.
- dbus_session_restart.rs (238, 3): dbus-restart + control-command
  restart + control-command error-without-service.
- dbus_session_pause.rs (627, 6): pause/unpause + focus-ignored
  paused + pause-wayland-env + unfocus-ignored + release-VKs/reset
  + control-command pause/unpause.
- dbus_session_persistent.rs (95, 1): persistent-service handles
  restart in idle.

All four files under 1500 LOC. dbus_session_tests.rs deleted.
integration_tests/mod.rs declarations updated.

Plan inventory delta: M2-PR-05 reported 14 tests in this section;
actual count was 15 (plan §2 missed test_dbus_paused_changed_signal).
The aggregate 65 integration-test count is unchanged.

Verification: 15 test attrs preserved (sum across leaves); cargo
build clean; cargo test --bin kanata-switcher 261 passed / 0 failed.

M2 milestone complete:
- tests.rs (4864 LOC, 189 tests) -> 22 leaf files (max 770 LOC)
- integration_tests.rs (7176 LOC, 65 tests) -> 16 leaf files (max 1463)
- No production source changes; behaviour preserved.
M2 milestone complete: tests.rs (4864 LOC, 189 tests) split into
22 leaf files; integration_tests.rs (7176 LOC, 65 tests) split
into 17 leaf files. Max leaf-file LOC 1463 (under plan's 1500
strict ceiling). 261/0 tests pass throughout. Two trivial
visibility tweaks to production code (FocusService::focus_changed
pub, mod wayland_mock pub(super)); otherwise zero daemon changes.

Plan deviations: pub(super) -> pub(crate) on shared helpers (test
re-export chain needed broader visibility); a couple of plan
test counts were off by 1-3 (sni_presentation, dbus_session_tests).
M2-PR-06 was conditional in the plan; activated because
dbus_session_tests.rs landed 1631 LOC, over the 1500 ceiling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants