Skip to content

refactor: focus model, bar update/render split, flicker & title fixes; +logging rotation, Lua REPL - #16

Merged
TheB1t merged 5 commits into
masterfrom
feat/logging-rotation
Apr 16, 2026
Merged

refactor: focus model, bar update/render split, flicker & title fixes; +logging rotation, Lua REPL#16
TheB1t merged 5 commits into
masterfrom
feat/logging-rotation

Conversation

@TheB1t

@TheB1t TheB1t commented Apr 16, 2026

Copy link
Copy Markdown
Owner

Summary

Five commits on this branch, listed in git order (oldest → newest).

1. feat(log) — XDG state dir + rotation + per-call flush

  • Log path via $XDG_STATE_HOME/sirenwm (fallback $HOME/.local/state/sirenwm), auto-created.
  • Rotating file sink (5 MB × 5) plus stderr color sink.
  • Split logs by app: sirenwm.log vs sirenwm-display.log (argv pre-scanned for --display-server).
  • _LOG_EMIT flushes after every LOG_* so no lines are lost on crash.
  • log_init_null() replaces /dev/null in tests.
  • Wayland integration harness (run_tests_wayland.sh) now points at the new log location via a RUNTIME_LOG variable.

2. refactor(focus) — Core is sole producer of FocusChanged events

Earlier boundary fix: Core::dispatch(FocusWindow) emits FocusChanged only on actual state transition; the X11 arbiter becomes a mute applier. Removes 3–6× FocusChanged spam per real transition caused by three independent emit paths.

3. feat(debug_ui) — Lua REPL tab

  • New REPL tab inside the existing debug window (no new window).
  • Unrestricted eval/exec against the live Lua state via LuaHost::repl_eval.
  • Expr-first fallback: tries return <code> first; falls back to statement block on load failure.
  • Captures print() via thread-local buffer + repl_print; restores original print after the call.
  • Debug UI only — no sandboxing.

4. refactor(focus) — single source of truth via Core::focus

Follow-up that collapses the focus subsystem onto a dwm-style single-writer model. Core::focus(WindowId) is now the only path that writes "who is focused". Removes FocusState cache, FocusPriority arbiter, ensure_focused, restore_visible_focus, request_focus, and the 60-line parent-walk in handle_focus_event. Intent chain: focused_monitor_Monitor.active_wsWorkspace::current.

Fixes:

  • Borders not repainted after reload/restart.
  • Double FocusChanged on cross-monitor EnterNotify.
  • _NET_WM_STATE_FOCUSED sticking on the previously focused window when switching to an empty workspace — the else-branch in Core::focus() now emits FocusChanged(NO_WINDOW) unconditionally (symmetric with the focused branch), since prev read from wsman after a switch_to reflects the new workspace, not the window we are defocusing.

5. refactor(bar) — split widget update / render; restart/reload flicker & title fixes

  • Widgets now separate expensive state mutation (update) from cheap rendering (render). update() runs on the interval schedule or on first load via prime_widgets(); render() runs on every repaint. Reactive widgets (interval=0) update+render on every redraw.
  • Restart flicker: Core::dispatch(ApplyMonitorTopology) no longer emits DisplayTopologyChanged. Emission is the caller's responsibility — Runtime::dispatch_display_change() posts on hot-plug / reload; Runtime::start() does not (initial apply isn't a "change" and must not trigger reactive rebuilds that duplicate on_start).
  • Reload flicker: BarModule::on_reload() no longer rebuilds physical windows — it only absorbs new config/theme. DisplayTopologyChanged (posted by dispatch_display_change() after reload) is now the single rebuild trigger.
  • Missing focused-window title after restart: adopt path dropped title because ExistingWindowSnapshot didn't carry it. Title is now read directly from X at adopt time (not persisted across restarts) and threaded through snapshot → SetWindowMetadata. Single path through read_window_metadata; redundant read_window_title and local _NET_WM_NAME/UTF8_STRING interning removed.
  • New LuaHost::call_ref_method_void for widget:update() (no return).

Test plan

  • cmake --build build -j$(nproc) — clean.
  • ctest --test-dir build --output-on-failure — 4/4 green (updated Hotplug.ApplyTopologyDoesNotEmitDomainEvent reflects the ApplyMonitorTopology contract change).
  • CI Format (uncrustify) — green.
  • X11 integration (run_tests.sh) — 05_focus now passes including the FOCUSED cleared after ws switch case.
  • Wayland integration (run_tests_wayland.sh) — structured-log checks now resolve against the XDG runtime log path.
  • Manual: bar no longer flickers on siren.restart() / siren.reload().
  • Manual: focused-window title renders correctly on all monitors after restart.
  • Manual: focus transitions produce exactly one FocusChanged per real change.
  • Manual: logs rotate at 5 MB, split between sirenwm.log and sirenwm-display.log.
  • Manual: REPL tab evaluates expressions, runs statements, captures print.

🤖 Generated with Claude Code

@TheB1t TheB1t changed the title feat: logging rotation, focus boundary fix, Lua REPL in debug_ui refactor: focus model, bar update/render split, flicker & title fixes; +logging rotation, Lua REPL Apr 16, 2026
@TheB1t
TheB1t force-pushed the feat/logging-rotation branch 2 times, most recently from 9dfb1b1 to f7f38f3 Compare April 16, 2026 19:13
TheB1t and others added 5 commits April 17, 2026 00:36
Writes to $XDG_STATE_HOME/sirenwm/ (fallback ~/.local/state/sirenwm/),
rotates at 5 MiB × 5 files. WM process logs to sirenwm.log, embedded
display-server child logs to sirenwm-display.log. Every LOG_* call
flushes immediately so nothing is lost on crash/kill. Test harnesses
switched to log_init_null() to avoid touching the user's state dir.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
X11 backend paths were emitting FocusChanged directly, leading to
3–6 duplicate events per real focus change (EnterNotify → dispatch
emits, arbiter emits, FocusIn → dispatch emits again). Listeners
saw the spam in the debug UI event log.

Realign the boundary: only Core emits domain events. Introduce
Core::ensure_focused(WindowId) — idempotent, no BackendEffect — for
paths that merely reconcile core state with an authoritative backend
signal (X FocusIn, reload, root fallback). dispatch(FocusWindow) is
now idempotent on the event side too, always emitting the X effect
but skipping FocusChanged when core already tracks the target.

X11 backend changes:
- Arbiter applies xcb_set_input_focus only; emits nothing.
- handle_focus_event → core.ensure_focused() instead of dispatch.
- handle_enter_notify drops its synchronous dispatch — the follow-up
  X FocusIn reconciles core via ensure_focused.
- restore_visible_focus/none and on_reload_applied route through
  ensure_focused so the one-emit-per-transition invariant holds.

Result: exactly one FocusChanged per real focus transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a live Lua REPL to the debug UI as a new tab alongside
Monitors/Workspaces/Windows/Focus/Events. Input is evaluated via
LuaHost::repl_eval(), which first attempts "return <code>" so bare
expressions print their value, and falls back to loading the raw
code as a statement block. print() output is captured for the
duration of the call and appended to the REPL output pane.

No sandboxing — this is a debug-only surface with full access to
the live Lua state, same as the rest of the debug UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collapse the focus subsystem onto a dwm-style single-writer model.
Motivation: accumulated ad-hoc fixes (FocusPriority arbiter,
ensure_focused, restore_visible_focus, FocusState cache) meant five
different code paths could write "who is focused" — causing desync
after siren.restart() and double FocusChanged emissions across
monitors.

Intent chain, enforced by code and comments:
  focused_monitor_  ->  Monitor.active_ws  ->  Workspace::current

Core::focus(WindowId) is now the only writer. It mirrors dwm's
focus(Client*) in dwm.c:789: picks a visible candidate when window is
NO_WINDOW, validates the target, drives X via BackendEffect, and emits
FocusChanged exactly once per call. All intent entry points
(FocusWindow atom, FocusMonitor, focus_monitor_at_point,
SwitchWorkspace, FocusNext/Prev, MoveWindowToWorkspace,
SetWindowFullscreen, reconcile) route through it.

Removed:
- FocusState cache + 15 sync_focus_state() call sites — now derived
- Core::ensure_focused / Core::sync_current_focus (X-driven inversions)
- X11Backend::FocusPriority arbiter + pending_focus_*_ fields
- X11Backend::restore_visible_focus (redundant after ReconcileNow)
- X11Backend::request_focus
- Workspace::advance_focus (unused)
- handle_focus_event's 60-line parent-walk — collapsed to 12-line
  pure theft-protection (dwm.c:814)

Fixed:
- Borders not repainted after reload/restart (Core::focus now always
  emits FocusChanged on a valid window; consumers are idempotent)
- Double FocusChanged on cross-monitor EnterNotify — wsman.focus_window
  now owns the focused_monitor_ update as part of focus intent

Secondary (restart hardening, picked up from the same branch):
- Re-assert managed event mask on every MapWindow backend effect so
  clients that replace their event mask on remap don't silently drop
  Enter/Focus/Structure subscriptions
- libxcb: add XConnection::query_parent
- Drop unused display_server input port plumbing and dead keybinding
  wiring

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Widgets now separate expensive state mutation (update) from cheap
rendering (render). update() runs on the interval schedule or on first
load via prime_widgets(); render() runs on every repaint. Reactive
widgets (interval=0) update+render on every redraw.

Also fix a cluster of issues that showed up together:

- Bar flicker on siren.restart(): Core::dispatch(ApplyMonitorTopology)
  no longer emits DisplayTopologyChanged. Emission is the caller's
  responsibility — Runtime::dispatch_display_change() posts it on true
  hot-plug / reload, Runtime::start() does not, since the initial
  topology apply is not a "change" and must not trigger reactive
  rebuilds that duplicate what on_start() already does.

- Bar flicker on siren.reload(): BarModule::on_reload() no longer
  rebuilds physical windows — it only absorbs new config/theme.
  dispatch_display_change() posts DisplayTopologyChanged after reload,
  which is now the single rebuild trigger.

- Focused-window title missing on bar after restart: adopt path dropped
  title because ExistingWindowSnapshot didn't carry it. title is now
  read directly from X at adopt time (not persisted across restarts)
  and threaded through snapshot → SetWindowMetadata. Single path
  through read_window_metadata; redundant read_window_title and local
  _NET_WM_NAME/UTF8_STRING interning removed.

- New LuaHost::call_ref_method_void for widget:update() (no return).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@TheB1t
TheB1t force-pushed the feat/logging-rotation branch from f7f38f3 to 319d14d Compare April 16, 2026 19:37
@TheB1t
TheB1t merged commit 24f2a09 into master Apr 16, 2026
14 checks passed
@TheB1t
TheB1t deleted the feat/logging-rotation branch April 16, 2026 20:43
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.

1 participant