cofi is a single-binary C99/GTK3 X11 window switcher with daemon-mode IPC. This document describes the system shape — what the pieces are and how they wire together. For product behavior see SPEC.md. For contributor workflow see CLAUDE.md. For domain terms see glossary.md.
cofi runs as a long-lived daemon plus an invocation-time delegating client, all from the same binary.
┌────────────────────────────────────────────────────────────┐
│ user typed `cofi --windows` (or pressed a global hotkey) │
└────────────────────────────────────────────────────────────┘
│
┌───────────┴───────────┐
│ Is daemon already up? │
│ (Unix socket bound?) │
└───┬───────────────┬───┘
│ no │ yes
▼ ▼
┌──────────────────┐ ┌──────────────────────────────┐
│ Become daemon: │ │ Send argv+opcode over socket │
│ - bind socket │ │ Exit immediately │
│ - grab hotkeys │ └──────────────┬───────────────┘
│ - hide window │ │
│ - event loop │ ◄────────────────┘
└──────────────────┘ daemon receives opcode, shows
the requested surface
- Single-instance guard. Unix-socket listener at
$XDG_RUNTIME_DIR/cofi.sock(fallback/tmp/cofi.sock). A second invocation that finds the socket bound becomes a delegator: it sends a one-byte opcode + argv tail, then exits. The daemon dispatches the opcode to the appropriate UI surface. - Daemon bootstrap. First invocation registers global X11 hotkeys, opens a GIOChannel on the X11 connection for PropertyNotify events, hides the GTK toplevel, and waits.
- No polling. Window list updates come from
_NET_CLIENT_LISTand_NET_ACTIVE_WINDOWPropertyNotify events, not from periodic re-enumeration. MRU history is maintained from focus changes. This rule is specific to the X11 window-list path; provideron_tickpolling is the sanctioned pattern for external data sources such as Bluetooth. - systemd integration.
mise run installinstalls a user service (scripts/cofi.service) that auto-restarts on crash.
┌───────────────────────────────┐
│ main.c │
│ argv → daemon or delegate │
└───────────────┬───────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────────────┐ ┌─────────────────────┐
│ daemon_socket │ │ GTK UI │ │ X11 backend │
│ - bind/listen │ │ - Windows core tab │ │ - x11_utils │
│ - opcode │ │ - provider tabs │ │ - x11_events │
│ dispatch │ │ - command mode (`:`) │ │ - window_list │
│ │ │ - modal prefixes │ │ - hotkeys │
│ │ │ - slot overlays │ │ (XGrabKey) │
│ │ │ - key dispatch │ │ - monitor_move │
└─────────────────┘ └─────────┬───────────────┘ └─────────────────────┘
│
▼
┌────────────────────────────────────┐
│ Provider registry + data modules │
│ - cofi_tab_provider registry │
│ - workspaces / harpoon / matching │
│ - config / hotkeys / rules / apps │
│ - path / calc / sinks / run / proc│
│ - projects / profiles / bookmarks │
│ - history + filter + display │
│ - config/state persistence │
└────────────────────────────────────┘
src/core/app/main.c— argv parse, decide daemon vs delegate, GTK setup, daemon bootstrap, handoff to UI surface.src/cli/cli_args.cpp— popl-based CLI option parsing (--windows,--workspaces,--harpoon,--matching,--names,--command,--run,--applications,--assign-slots, etc.).src/commands/command_registry.c— command storage and lookup registry: primary names, aliases, compact suffixes, handlers, help text, activation policy, keep-open policy, and explicit owner.src/commands/core_commands.c— built-in core command registration. Provider-owned commands register from their provider modules.src/providers/builtin_plugins.c— compiled-in plugin/provider registration list used during startup.src/commands/command_parser.c— compact-syntax splitter (tw3,jw1) and alias resolution using the command registry.src/commands/command_availability.c— owner-aware availability gate for command candidates, help, and dispatch.src/commands/command_mode.c,src/commands/command_handlers*.c, and provider-ownedCommandSpechandlers — execution of:commands.
src/daemon/daemon_socket.c— protocol primitives (path resolution, bind, connect, send, accept).src/daemon/daemon_socket_runtime.c— GIOChannel integration, opcode-to-UI dispatch for the fixed delegate opcodes (COFI_OPCODE_WINDOWS,_WORKSPACES,_HARPOON,_MATCHING,_NAMES,_COMMAND,_RUN,_APPLICATIONS) plusCOFI_OPCODE_SHOW_TABfor dynamic provider tabs such as Bookmarks. Dynamic providers do not get per-provider opcodes.
src/x11/x11_utils.c— EWMH property extraction (_NET_WM_NAME,_NET_WM_PID, etc.), window activation via_NET_ACTIVE_WINDOWClientMessage.src/x11/x11_events.c— event-driven window list updates from PropertyNotify on root.src/x11/window_list.c—_NET_CLIENT_LISTenumeration + filtering (skip-taskbar, types).src/daemon/hotkeys.c—XGrabKeyregistration and dispatch from KeyPress events.src/x11/monitor_move.c— XRandR geometry + work-area calculation for tiling and multi-monitor.src/x11/frame_extents_restore.c— deferred geometry restore via a 150msg_timeout_addafter_NET_FRAME_EXTENTSchanges so cofi wins marco's first-map placement race; it stays PropertyNotify-driven and tracks one pending timeout per window.src/x11/xrandr_helpers.c— shared XRandR helper layer forget_monitors_xrandr()andget_window_monitor_xrandr(), reused by tiling and monitor-move flows.
src/ui/window_lifecycle.c— show/hide of the cofi toplevel. Recomputes Pango font metrics + window size + monitor placement on every show (handles XSettings/DPI changes mid-session).src/providers/cofi_tab_provider.c— provider registry. Providers register tabs, modal prefixes, delegate opcodes, hotkey mode claims, slots, tick callbacks, dynamic tab handles, and enablement metadata.src/ui/display.c— top-level display assembly. Windows remains core-special; provider tabs render throughCofiTabProviderrow callbacks.src/ui/display_pipeline.c— assembly of the display strings from filter results.src/ui/tab_header.c+src/ui/tab_switching.c— tab header formatting, overflow, visibility, and cycling. Header/cycling enumerate Windows plus registered provider tabs, not a fixedTAB_*loop.src/ui/key_handler.c+src/ui/key_handler_*.c— core key dispatch and mode precedence. Provider-specific key handling lives on providerhandle_keycallbacks.src/ui/slot_overlay.c— transient[N]indicators drawn on each window.src/ui/window_highlight.c— circle ripple effect on activation.
- Provider tabs — list-with-action surfaces registered through
CofiTabProvider. Current provider tabs are Bluetooth, Sessions, Workspaces, Harpoon, Names, Config, Hotkeys, Rules, Apps, Path, Calc, Sinks, Run, Proc, Projects, Profiles, and Bookmarks. - Dynamic tab handles — provider tabs request
COFI_PROVIDER_DYNAMIC_TABand receive a runtime tab handle.TabModeis now core-only (TAB_WINDOWSplus theTAB_COUNTsentinel); provider tabs are enumerated through the registry. - Enablement — providers stay registered but can be disabled through config. Registry lookups for tab, command, and prefix surfaces fail closed for disabled providers. Required providers, currently Config, cannot be disabled.
- Commands — commands register
CommandSpecentries withcommand_registry. Provider command specs live in their provider modules; core commands live in the built-in core registration list. Provider-owned commands are hidden when that provider is disabled. - Delegates — provider-backed daemon opcodes and hotkey modes resolve through provider metadata, keeping compatibility constants out of provider-specific dispatch code.
- Built-ins —
builtin_plugins.cis the only startup module that should know the compiled-in plugin/provider list. - Plan — docs/decisions/0004-plugin-architecture-plan.md is the current TFD-675 roadmap for reducing remaining central tables and hardcoded surfaces.
src/core/history/history.c— MRU list;partition_and_reordersplits by type/desktop.src/ui/window_filter.c— Windows-tab filtering, search/MRU/native ordering modes, workspace bias, and display-title search strings.src/matching/fzf_algo.c— the scoring algorithm.src/matching/tier_score.c— extracted shared two-tier scorer (TIER_DIRECTvsTIER_INDIRECT) used by the Windows and Bookmarks tabs; window-only Signal A remains wrapped inmatch_window.src/matching/match.c— match-stage classification (exact / prefix / initials / fuzzy).src/harpoon/harpoon.c— 36 persistent slot assignments (~/.config/cofi/harpoon.json).src/harpoon/workspace_slots.c— per-workspace auto-numbered slots (column-major).src/matching/match_entry.c+src/matching/window_matcher.c— pure matching identities and match-entry pattern/anchor evaluation.src/names/names_store.c+src/names/names_provider.c— user-assigned custom names keyed one-to-one by match id.src/rules/rules.c+src/rules/rules_config.c— rule-based window classification.src/rules/rules_dispatch.c— automatic rule-dispatch loop owner. X11 event code supplies the trigger, but rules owns the policy: window iteration, trigger gating, fire-once handling, circuit-breaker behavior, and re-entry guards.src/sessions/sessions.c— live cancellablergsearch over Claude/Codex JSONL session files; groups raw matches into session rows and appliesterms | refinefiltering without a persistent index.
src/config/config.c— load/save/apply config. Built-in keys and provider-registeredCofiConfigSpecentries share one registry path consumed by:set, save/load, and the Config tab.src/daemon/hotkey_config.c—hotkeys.jsonparsing and grab registration.
src/run/run_mode.c—!prefix; session-only history; detached shell launch.src/apps/apps.c— desktop-app loader, Apps-local matching/ranking, detached launch.src/path/— PATH executable discovery, async scan + monitor updates, ranking, and the hidden Path provider tab.PathEntryrows own thepath:<exec_path>identity surface, and command aliasespath/binaries/bin/exesurface the tab.src/bluetooth/— provider / BlueZ glue / model split for the Bluetooth tab. Uses async-only GDBus againstorg.bluez, acquires the system bus lazily via aBUS_UNINITIALIZED→BUS_ACQUIRING→BUS_READY/BUS_FAILEDstate machine, and refreshes paired-device state on a 3-second provideron_tickcadence.src/system_actions/system_actions.c— logind D-Bus calls (Lock, Suspend, Hibernate, Logout, Reboot, Shutdown) with shell fallback (blocking/sync D-Bus; not the pattern for new D-Bus work — seesrc/bluetooth/for the async pattern).src/daemon/detach_launch.c— shared detached-launch helpers (systemd-runprimary,fork+setsidfallback).src/geom/tiling.c— half/quarter/third/grid geometry calculation.src/bookmarks/—:bookmarkstab, Chrome bookmark parsing across all discovered profiles, combined match-string ranking, and bookmark slot payloads in the formbookmark:chrome:<profile_dir>:<url>.
src/core/log/log.c— rxi/log.c bundled logging library.src/core/utils/utils.c— string and path helpers.src/core/json/cofi_json_io.c— tolerant JSON I/O wrapper overjson-glib. Convergence point for persistence stores; codifies the unknown-field / missing-key / wrong-type / corrupt-file policy in one place. See ADR-0009. Migration to it is staged per-store; some legacy stores still use hand-rolledfprintf/sscanf.
These are the rules that don't live in any one file but must hold across the system. The full list of regressions to avoid is in docs/gotchas.md.
- Inverted-list render. Index 0 is the visible bottom of the list;
render_display_pipelineiteratesend−1 → start. Any code that maps filter rank to display position must account for this reversal (see ADR-0007). - MRU before display. Filter/scoring runs against MRU-ordered candidates, not the native EWMH order. Reordering for display happens once, after scoring.
- Display order = search order. Never reorder the match-target string vs the display columns — the search string is what scoring sees.
- Cache invalidation on show. Pango/monitor/DPI state is sampled per show, not at startup, to survive
xrandrand XSettings (Xft/DPI) changes mid-session. - Single source of truth for config keys. Config descriptors in
src/config/config.cdrive save/load,:set, and Config tab rows. Provider-owned keys use aprovider_id.key_namenamespace and must be registered during provider bootstrap. - Detached launch. Anything cofi launches (run mode, apps tab,
:run) must outlive cofi itself.systemd-run --scope --useris the primary path;fork+setsidis the fallback. - Rules-as-policy boundary. Auto-apply behaviors on window-appear/title-change (geom restore, sticky, always-above, workspace pin) attach via the rules engine, not bespoke triggers. A single execution path keeps the fire-once + circuit-breaker + idempotency invariants in one place — fixes there pay off everywhere. Geom auto-restore is the canonical example: layout payload in
layouts.jsonkeyed by match_id; trigger viapattern → rlrule. - Tolerant persistence I/O. All JSON reads through
cofi_json_io(migration in progress, see ADR-0009) treat unknown fields as forward-compat, missing/wrong-type aspresent == FALSE+ fallback, corrupt files aslog_error+ empty-load. Stores never abort on bad input. Atomic save via tmp+rename in the same directory. - Selection-by-identity on non-query refresh. Any list refresh that does not change the query/filter membership — data updates, flag toggles, tick callbacks, row mutations — must preserve the selected row by calling
preserve_selection()before the refresh andrestore_selection()after; only a genuineon_query_changedpath may move selection to the best/top match. When the selected row is gone after refresh, the fallback is the nearest surviving row (same index if in range, else previous), not the top. Seesrc/core/selection/CONTEXT.mdcriteria 6–8 and TFD-835 for known non-compliant paths. - Single owner per match id. Each persisted
match_idhas exactly one owning subsystem record: Harpoon slot, Names record, geom layout, rule, or another explicit owner.MatchEntryitself is pure identity and not an owner label store. The Names subsystem owns custom names as a one-to-onematch_id -> custom_namestore. Owners delete their own match entry directly when deleting their record; startup matching GC is only a crash/legacy-data seatbelt over owner roots.
- Build:
mise run buildwrapsmake(incremental, header deps via-MMD -MPin.dfiles). After header changes,mise run rebuildbecause the dep tracking is partial. - Test:
mise run testwrapsmake test, which builds and runs 70 standalone test binaries fromtest/test_*.c. Each test links specificsrc/*.ofiles plus stubs for cross-cutting deps it doesn't exercise. - CI:
.github/workflows/build.ymlrunsmake+make teston every push. - Pre-push hook:
scripts/hooks/pre-pushchecks out each pushed ref tip in a temporary detached worktree and runsmake testthere, so the tested tree matches the ref being pushed without requiring mise; install viabash scripts/install-hooks.sh.