Cofi is a keyboard-driven window switcher for X11/Linux with native GTK UI.
Scope. This document defines product behavior — what cofi does from the user's seat: window-list rules, search semantics, command catalog, keyboard model, slot/tile/run-mode behavior. For system shape (how the binary is wired internally) see docs/architecture.md. For design decisions and their history see docs/adr/. For domain terminology see docs/glossary.md.
Cofi displays a list of open windows on the system.
- Show all normal application windows
- Exclude docks, desktop backgrounds, and cofi's own window
- Display format: 5 fixed-width columns, monospace font
- Harpoon slot indicator (1 char: digit/letter if assigned, blank otherwise)
- Desktop indicator:
[0-9]or[S]for sticky - Instance name (20 chars, truncated)
- Window title (67 chars, truncated)
- Class name (18 chars, truncated)
- Display order: bottom-up (first entry at bottom, fzf-style)
- Selection indicator:
>prefix on selected row
Windows are ordered by most recently used (MRU). The most recently focused window appears first.
- When a window gains focus, it moves to the front of the list
- When a window closes, it is removed without disrupting order of remaining windows
When cofi opens, the selection starts on the second entry (index 1) — the previously active window.
- Pressing Enter immediately switches to that previous window, enabling quick Alt-Tab-style toggling
- No swap of data structures is needed; this is purely about initial selection placement
- On the Windows tab only, pressing
.with an empty query repeats the last successful non-empty query-driven window activation for this session - Repeat re-runs the saved query against the current live window list and immediately activates the current top match
.with a non-empty query inserts a literal period normally- If no repeatable query has been stored yet, or the saved query has no current matches,
.is a no-op and cofi stays open
Real-time filtering as the user types. Case-insensitive.
The search matches against the full displayed row as the user sees it — desktop indicator, instance name, window title, class name — joined into a single string. Display order = search order: if a column moves, what the scorer sees moves with it. This enables queries like "2ter" to find terminals on desktop 2, or combining any visible fields.
Single-pass fzf FuzzyMatchV2 (src/fzf_algo.c) over the joined display row. The algorithm itself rewards word-boundary and consecutive-character matches via internal position bonuses — there are no separate match stages. Higher scores rank higher; non-matches are dropped.
Examples of what the user effectively gets, all from one scoring pass:
"comm"→ "Commodoro" (word-boundary boost)"ddl"→ "Daniel Dario Lukic" (initials, via consecutive boundaries)"th"→ "Thunderbird" (subsequence)
History. Until 2026-03-26 cofi used a multi-stage fzy cascade (word-boundary → initials → subsequence → fuzzy). It was replaced by a single fzf v2 pass on the full row. See ADR-0004.
Applied after the fzf score:
- Windows on the current desktop receive a constant bonus.
- Normal windows rank above special windows (docks, dialogs, etc.) — special windows pinned to bottom.
- Results update instantly as the user types.
- Empty query shows all windows in MRU order (no scoring).
- Selection resets to the default position (index 1) when the filter changes.
- Up/Down (or Ctrl+k/j) — move selection through the window list
- Enter — activate the selected window
- Escape — close cofi without switching
- Tab / Shift+Tab — cycle through visible tabs. Since TFD-545, tabs have three visibility states:
- PINNED — always visible: Windows, Apps
- SURFACED — shown once Tab-cycled into (e.g. via
:show <tab>command), then dismissed on hide - HIDDEN — secondary tabs (Sessions, Workspaces, Harpoon, Matching, Layouts, Config, Hotkeys, Rules, Calc, Sinks, Run, Proc, Projects, Profiles, Bookmarks) not reached by Tab unless surfaced
Tabs are surfaced programmatically by
:show <verb>commands or explicit prefix flows; Tab/Shift+Tab only cycles PINNED + currently-SURFACED tabs.
- Typing any character starts filtering immediately (no mode switch needed)
- Borderless (no title bar or window decorations)
- Always on top of other windows
- Skips taskbar and pager
- Centered on screen
- Auto-closes when it loses focus (configurable)
- Window list (top) — scrollable text view
- Search entry (bottom) — single-line input
When the user selects a window and presses Enter:
- Switch to the window's desktop/workspace if different from current
- Raise and focus the selected window
- Close cofi
Cofi runs as a daemon and registers global hotkeys via XGrabKey on the X11 root window.
Hotkey bindings are stored in hotkeys.json as {key, command} pairs. Each binding maps a key combination to a cofi command string.
- Default bindings:
Mod1+Tab→show windows!Mod1+grave→show command!Mod1+BackSpace→show workspaces!
- Hotkey format:
Modifier+KeyNameusing X11 names (XStringToKeysym)- Modifiers:
Mod1,Mod2,Mod3,Mod4,Control,Shift
- Modifiers:
- CapsLock/NumLock variants grabbed automatically
- Retry/Exit dialog on grab failure (e.g. another app owns the key)
- When cofi is already visible, show-mode hotkeys switch mode inline (no reopen)
- Pressing a hotkey while visible cancels any pending focus-loss close timer
Hotkey command strings support a ! suffix to control dispatch behavior:
- With
!— command executes immediately. If it needs UI (e.g. overlay dialog), cofi opens automatically. If it doesn't need UI (e.g.:jw 3), it runs silently on the active X11 window. - Without
!— cofi opens with the command prefilled in the command line, letting the user edit or add arguments before pressing Enter.
Examples:
"show windows!"— opens cofi in windows mode immediately"show run!"— opens cofi in run mode immediately"jw 3!"— jumps to workspace 3 silently, no cofi window"maw"— opens cofi with:mawprefilled, user types direction + Enter
Shell run entry triggered by typing ! in the search field or via show run.
- Mode indicator changes from
>to! !is indicator-only; run entry text is raw command text (no literal!prefix)- Enter launches the trimmed command detached via the user's shell, then closes cofi
- Bare
!(legacy tolerated by parser) or whitespace-only commands are a no-op - Run history is session-only, separate from command history, and navigable with Up/Down
- Deleting back to empty exits run mode cleanly
extract_run_command()remains backward-compatible and still tolerates legacy!fooinput- Run mode never updates repeat-last-query state
Calculator modal triggered by typing = in the search field or via :calc.
- Mode indicator changes from
>to= =is indicator-only; expression text is entered directly without a leading=- Enter evaluates the expression, copies the result to the clipboard, and keeps cofi open
- Deleting back to empty exits calc mode cleanly
:calc <expression>evaluates an expression directly from command mode
Emoji picker triggered via :emoji or :show emoji.
- 1,870 emoji entries sourced from the gemoji dataset
- Empty query shows all emoji in MRU-first order (recently picked appear at top)
- Typing filters by name, aliases, and keywords using weighted fzf scoring: name matches rank highest, then aliases, then keywords
- Enter copies the selected emoji glyph to the clipboard and closes cofi
- Pick history persists across restarts
Hidden Projects surface triggered via :projects, :project, :tmux, :tx, :zj, :zellij, :show tmux, or :show projects.
- Tmux sessions are listed from
tmux list-sessions -F '#{session_name}\t#{session_windows}\t#{session_attached}' - Zellij sessions are listed from
zellij list-sessions --shortwhenzellijis available - Zoxide folders are listed from
zoxide query -lwhenzoxideis available - The logical list keeps tmux sessions first, then zellij sessions, then zoxide folders; because cofi renders provider rows bottom-up, folders appear above sessions
- Tmux sessions are marked
[t]; zellij sessions are marked[z]; zoxide folders are marked[d]; locate fallback folders are marked[~] - Filtering is fzf-style over the full rendered row, including the marker, so
tcofiandzcofican disambiguate matching tmux and zellij session names - Missing session tools or empty session lists show a non-actionable status row only when there are no zoxide folder rows
- When a query of 3+ characters is active, cofi also runs
plocate/locateasynchronously against a basename glob derived from the query, optionally narrows candidates to configured search roots, then ranks accepted directory candidates in-process with the same fzf scorer plus a small shorter-path tiebreak; tmux/zellij/zoxide results still get a fixed priority bonus over locate rows in the merged list, and filesystem folders are appended as[~]rows - Enter on a tmux session launches a detected terminal detached from cofi, then runs
tmux attach-session -t =<session> - Enter on a zellij session launches a detected terminal detached from cofi, then runs
zellij attach --create <session> - Enter on a folder opens the folder in Caja when available, otherwise the system file opener
Deleteon a tmux or zellij session opens a kill confirmation dialog; folder and status rows are ignoredF2orCtrl+Ron a tmux session opens a rename dialog; zellij, folder, and status rows are ignoredInsertorCtrl+Nopens a new-session dialog; on folder rows it starts in that folder, otherwise it starts in$HOMEShift+Insertopens the new-session dialog with zellij preselectedCtrl+keyassigns a per-Projects slot for the selected tmux session, zellij session, or folderCtrl+Nis reserved for new session; useCtrl+Shift+Nto assign Projects slotnCtrl+Ris reserved for rename; useCtrl+Shift+Rto assign Projects slotr
Alt+keyrecalls a per-Projects slot; session slots open the session and folder slots open the folder- Tmux session names are treated as exact tmux targets and shell-quoted before launch
- Zellij session names are shell-quoted before attach or kill commands
- Folder-derived session names use the folder basename with non
[A-Za-z0-9_-]characters replaced by_ :tmux <session>attaches an existing session by exact name; it does not create a new tmux session:tmux <key>recalls a Projects slot only when no exact session named<key>exists
The Projects tab supports remote host sessions via Ctrl+S.
Ctrl+S— open the remote host overlay to enter a hostname oruser@host- After confirming the host, cofi fetches the remote tmux session list asynchronously and merges the results into the project list
- Remote sessions are marked
[REMOTE:host]and saved to~/.config/cofi/projects.json; they reappear across restarts even when the host is unreachable - Enter on a saved remote session attaches the session via SSH, or creates a new one if it no longer exists
- Additional overlay actions: new session, open sftp, open terminal in session directory
- If the remote session is already open in a local window, cofi activates that window instead of launching a new terminal
Ctrl+D/Delete— shows a delete confirmation; removes the remote entry from the saved list
Only one cofi instance runs at a time.
- Primary guard is a Unix domain socket at
$XDG_RUNTIME_DIR/cofi.sock(fallback/tmp/cofi.sock) cofiwith no delegate flags returnsalready running+ exit 1 when daemon exists- Delegate flags (
--windows,--workspaces,--harpoon,--matchingwith--namesalias,--command,--run,--applications) connect to the daemon, send a 1-byte opcode, and exit 0 - If no daemon exists, cofi binds the socket, starts daemon mode, and applies the requested startup delegate mode
- Stale socket path is recovered by connect-fail → unlink → bind retry
- Daemon (default) — starts hidden, registers hotkeys, waits. Window shown only on hotkey press.
- Delegate launch — any delegate flag opens the requested tab/mode via socket; no separate no-daemon mode exists.
The window list updates automatically in real-time:
- New windows appear immediately
- Closed windows disappear immediately
- No polling — purely event-driven
Assign windows to persistent slots for direct access.
- 36 slots available: 0-9 and a-z
- Ctrl+[key] — assign the selected window to that slot
- Ctrl+j, Ctrl+k, Ctrl+u are reserved for navigation
- Ctrl+Shift+j/k/u overrides the reservation and assigns anyway
- Alt+[key] — activate the window in that slot directly (no exceptions)
- Alt+digit behavior depends on
digit_slot_mode(see below) - Alt+letter always activates global harpoon slots
- Alt+digit behavior depends on
- Assignments persist across cofi restarts (stored in config)
- When an assigned window closes, the slot attempts to reassign to a matching window (by class/title pattern)
- Harpoon tab shows all current assignments
- Harpoon indicators visible in the window list
When digit_slot_mode is set to per-workspace, Alt+1-9 activates the Nth window on the current workspace.
- Only meaningfully visible windows are numbered. Excluded:
- Minimized or shaded windows
- Cofi's own window
- Non-normal windows such as docks
- Windows whose visible content area is too small after accounting for occlusion by windows above them in the stacking order
- Visibility is based on the window content rect, not decoration leaks such as titlebars or borders
- A window must pass all of these checks to receive a digit slot:
- total visible content fraction meets the configured threshold
- largest single visible content fragment also meets that threshold
- largest visible fragment is at least 8x8 px
- The threshold is configured by
slot_occlusion_thresholdas an integer percent (5means 5%) - Windows are auto-numbered by screen position: left-to-right, top-to-bottom
- No manual assignment needed — numbering updates automatically as windows move, resize, minimize, or restore
- Alt+N is a no-op if the current workspace has fewer than N visible windows
- Letter-based harpoon slots (Alt+a-z) remain global and unaffected
After slot assignment, numbered overlays appear briefly on each assigned window.
- Overlays are independent X11 windows (visible even after cofi hides)
- Overlay position is the center of the largest visible content fragment, not the raw window center
- Duration controlled by
slot_overlay_duration_msconfig (default 750, 0 = disabled) - Catppuccin-themed: dark translucent background, opaque light text
Slots auto-assign on every Alt+digit press — no manual step needed. Windows are re-scanned each time, so slots stay correct after moves/resizes.
Typical workflow: Alt+Tab (open cofi) → Alt+1 (jump). That's it.
Also available for explicit control:
- CLI flag —
cofi --assign-slots- For use with external hotkeys (WM, sxhkd, etc.)
- Assigns slots on the current workspace and exits silently
digit_slot_mode controls what Alt+digit does:
default— global harpoon slots (existing behavior)per-workspace— workspace-scoped window slots by positionworkspaces— switch to workspace N
Replaces the old quick_workspace_slots boolean.
Assign custom names to windows that override the displayed title.
- Display format: "custom_name - original_title"
- Names persist across cofi restarts (stored in config)
- When a named window closes, the name attempts to reassign to a matching window
- Matching tab (Ctrl+E edit name, Ctrl+P edit pattern, Ctrl+D delete — works on both active and orphaned entries)
- Searchable — custom names are included in filter matching
Title-pattern automation rules triggered via :rules or :rl.
- Rules map window-title glob patterns to cofi command sequences (e.g., auto-tile or workspace-assign windows matching a pattern)
- Rules fire automatically on window-open and window-title-change events
- Auto-restore for saved layouts is configured as a rule whose action is
rl(e.g.,cofi* → rl). Layout payload lives inlayouts.json, keyed by match_id; the rule controls when to restore, the layout controls what to restore. Composable with other actions:rl,sb+,ab+restores geometry then sets sticky + always-above. Ctrl+A— add a new ruleCtrl+E— edit the selected rule commandsCtrl+P— edit the selected rule patternCtrl+O— toggle the selected rule's once flagCtrl+N— toggle the selected rule's new-only flagCtrl+D— delete the selected ruleCtrl+X— replay the selected rule against all currently open matching windows (explicit, stateless)Ctrl+Shift+X— replay all stored rules in stored order against all currently open windows
Saving or editing a rule does not immediately replay it; changes apply only to subsequent window transitions. Use Ctrl+X / Ctrl+Shift+X for one-shot replay.
The flag column shows rule firing mode at a glance: O = once-armed (will fire once then disarm), Ø = once-fired (already consumed), N = new-only (fires only on new windows / title-change transitions, not on existing-window dispatch).
Advanced mode is enabled by rules_show_all_tags. When enabled, :rules shows a trailing tag column naming the owning subsystem (for example geom). Geom-tagged rules are hidden from :rules by default.
- Tagged rules are read-only in
:rules; edit/delete/toggle actions fall through so mutations happen in the owning tab instead. The shortcut hint reflects this dynamically when a tagged row is selected.
Layout-rule migration note (TFD-784 PR2): tag only the restore rules you want geom to manage with "tag": "geom" in ~/.config/cofi/rules.json. Hand-authored/composite user rules with custom commands should remain untagged unless you specifically want them hidden from :rules and synced with layout enable/disable state.
Vim-style command entry triggered by typing : in the search field.
- Mode indicator changes from
>to: - Supports compact no-space syntax:
:cw2is equivalent to:cw 2 - Command history: last 10 commands, navigable with Up/Down
- Help available via
:helpwith paged output - Help/config display persists while typing (dismissed on Esc)
- Prefix candidate strip (TFD-546): while typing a command verb prefix, the second display header line shows up to 16 matching candidate verbs sorted by length then alphabetically. The current best match is wrapped in
[...]. Candidates update live and disappear once the prefix resolves to a unique verb. - When entered from hidden/delegated flows, target selection is pinned by
command_target_id: capture active window ID beforeshow_window(), thenenter_command_mode()resolves that ID in the current filtered list
:cw [N|dir](:change-workspace) — move selected window to workspace N or direction (h/j/k/l):pw(:pull-window,:p) — pull selected window to current workspace:cl(:close-window,:c) — close selected window:save-layout(:sl) — save the selected window's position, size and workspace:restore-layout(:rl) — restore the saved position, size and workspace:delete-layout(:dl) — forget the saved layout for the selected window:sw(:swap-windows) — swap two windows (positions and sizes):maw [N|dir](:move-all-to-workspace) — move all windows from current workspace to target:mw [toggle|on|off](:max,:maximize-window [toggle|on|off]) — set maximize on the selected window (defaulttoggle):miw(:min,:minimize-window) — toggle minimize (restore if already minimized, closes cofi)
:twor:t(:tile-window) — tile selected window- Halves:
L,R,T,B(50%) - Quarters:
l1,r1,l2,r2(25%) - Two-thirds:
L2,R2(66%) - Three-quarters:
L3,R3(75%) - Center:
cwith optional size - Grid positions: 2x2 or 3x2
- Halves:
:jw [N|dir](:jump-workspace,:j) — jump to workspace N or direction (h/j/k/l):rw [N](:rename-workspace) — rename workspace N (current if omitted)
:sb [toggle|on|off](:skip-taskbar [toggle|on|off]) — set skip taskbar (defaulttoggle):aot [toggle|on|off](:at,:always-on-top) — set always on top (defaulttoggle):ab [toggle|on|off](:always-below) — set always below (defaulttoggle):ew [toggle|on|off](:every-workspace) — set sticky (all desktops, defaulttoggle)
:tm [N](:toggle-monitor [N]) — move window to the next monitor, or to explicit monitorNwhen provided (for example:tm0,:tm1):hm [toggle|on|off](:horizontal-maximize-window [toggle|on|off]) — set horizontal maximize on the selected window (defaulttoggle):vm [toggle|on|off](:vertical-maximize-window [toggle|on|off]) — set vertical maximize on the selected window (defaulttoggle)
:mouse(:m,:ma,:ms,:mh) — mouse control with subcommand:away,show,hide
:an(:assign-name,:n) — assign custom name to selected window (cofi-internal alias):rename <title>(:rn) — force the selected window's X11 title (_NET_WM_NAME/WM_NAME); apps that self-title may overwrite it
:set <key> <value>— set a config option at runtime (also acceptskey=value):config(:conf,:cfg) — switch to the interactive Config tab:show(:s) — switch cofi mode:windows,command,run,workspaces,harpoon,matching(withnamesalias),config,rules,apps/applications,bluetooth,emoji,projects,calc,proc,sinks,sessions,profiles,bookmarks:rules(:rl) — switch to the interactive Rules tab
:hotkeys(:hotkey,:hk) — show all hotkey bindings:hotkeys <key> <command>— bind a hotkey (e.g.:hotkeys Mod4+1 jw 1):hotkeys <key>— unbind a hotkey- Optional sugar keywords accepted:
list,set,add,del,rm,remove
:js N(:jump-slot) — jump to the Nth visible window on the current workspace by screen position (1–9)
:help(:h,:?) — show command help with paged output
Position and resize the selected window to predefined screen regions.
- Half screen: left, right, top, bottom (50%)
- Quarters: four corners (25%)
- Two-thirds: left or right (66%)
- Three-quarters: left or right (75%)
- Center: with configurable size
- Grid positions: 2x2 or 3x2 layout
- Multi-monitor aware: tiles relative to the window's current monitor
- Respects work area (excludes panels, docks, taskbars)
- Respects window size hints (e.g., terminal minimum sizes)
Launch installed desktop applications from a dedicated Apps tab.
- Data source: XDG desktop applications via GLib/GIO app-info APIs
- Excludes hidden / invalid /
NoDisplayapps - Enter launches the selected app detached and closes cofi
--applicationsstarts directly on the Apps tab:show appsswitches to the Apps tab\surfaces the Apps tab from the entry field
Six system actions are available as fixed entries in the Apps tab:
- Lock — screen lock via shell fallback chain:
xdg-screensaver→mate-screensaver-command→xscreensaver-command→loginctl→ logind D-BusSession.Lock - Suspend / Hibernate / Logout / Reboot / Shutdown — via logind D-Bus (
org.freedesktop.login1)
Apps tab matching is local to the Apps launcher and does not reuse Windows-tab MRU/fuzzy ranking.
- Ranking priority:
name>generic_name>keywords generic_nameandkeywordsare token-matched to avoid cross-token false positives- Alphabetical order is only used as a tie-breaker within the same ranking tier
Launch executables discovered on the user PATH from a dedicated hidden tab.
:show pathswitches to the Path tab:path,:binaries,:bin, and:exesurface the same tab$surfaces the Path tab from the entry field- Enter launches the selected executable in a terminal by default (TFD-564)
- Hidden by default; it is not Tab-cycled until surfaced
- Query filters the cached PATH binary list by substring (case-insensitive pre-filter), then ranks surviving matches by fzf score descending.
- Cache cap: 4096 entries (MAX_PATH_BINS). On overflow a single warning is logged; later PATH entries are dropped.
- Filter output cap: 512 entries (MAX_APPS). Scoring runs across ALL substring-matched entries first; the cap is applied at copy-out after sorting, not during scoring.
- All PATH directories are watched via GFileMonitor (cap: 64 directories). File creates/deletes in PATH dirs update the cache incrementally.
- Basename dedupe: when the same binary name appears in multiple PATH dirs, the first occurrence (highest-priority PATH dir) wins.
When launching a terminal app or Path-tab executable, the terminal is detected via this priority chain:
$TERMINALenv var (if the named binary resolves in PATH)- Desktop-environment configured terminal:
- MATE, GNOME, Cinnamon →
gsettings get org.gnome.desktop.default-applications.terminal exec - KDE →
konsole(hardcoded) - XFCE →
xfconf-query -c xfce4-terminal -p /default-terminal(falls back toxfce4-terminalbinary if the query returns nothing)
- MATE, GNOME, Cinnamon →
x-terminal-emulator(Debian alternatives system), then hardcoded candidate list: mate-terminal, gnome-terminal, konsole, alacritty, kitty, foot, wezterm, urxvt, xterm- Final fallback:
xterm
Desktop-app launches use detach_launch_properly; Path-tab executable
launches use the terminal detach helper:
- Primary:
systemd-run --user --scope -- <argv>— places the launched process in its own transient systemd scope, completely outside cofi's cgroup. - Fallback: fork + setsid + double-fork + execvp. An errno-pipe (
pipe()+FD_CLOEXECon write end) propagates exec failure back to the parent — a non-empty pipe read means execvp failed. - All launched apps survive cofi stop/restart.
- Terminal=true entries (htop, vim, etc.) are launched as
{term, -e, sh, -c, cmd}. The explicitsh -cwrapper ensures consistent behavior across all terminals regardless of how they split the-eargument. - Terminal=false (GUI) entries are parsed with
g_shell_parse_argvand spawned directly viadetach_launch_argv_array— no shell is involved. Shell metacharacters ($(), backticks,;) inExec=do not execute. - Malformed
Exec=strings (unbalanced quotes) are logged and rejected without crashing.
View and interact with workspaces via the Workspaces tab.
- List all workspaces with window counts
- Switch to a workspace by selecting it and pressing Enter
- Rename workspaces with custom names (persistent)
- Display as grid (configurable rows via
workspaces_per_row, 0 = single row) - Directional navigation: HJKL and arrow keys in workspace overlays
Claude and Codex coding-session browser triggered via :sessions or :session.
- Indexes active and archived Claude/Codex project session directories on the local filesystem
- Query syntax:
terms | refine— the left side of|starts a live cancellable corpus search across session content; the right side refines the already-returned session rows - Enter resumes the selected session
Ctrl+R— rename the selected session (available for Claude/Codex source sessions)Ctrl+D/Delete— delete the selected session with a confirmation prompt
Audio output sink selector triggered via :sinks or :sink.
- Lists all available PulseAudio/PipeWire output sinks
- Enter activates the selected sink as the system default audio output and closes cofi
Ctrl+[key]— assign the selected sink to a per-Sinks slotAlt+[key]— recall and activate a saved sink slot without opening the tab:sinks @SLOTor:sinks SINK— activate a sink directly from command mode
Process manager triggered via :proc or :ps.
- Lists running system processes, refreshed automatically every 1.5 seconds
- Filtering narrows by process name or PID in real time
- Enter signals the selected process
Browser profile launcher triggered via :profiles, :chrome, or :browser.
- Lists Chrome (and compatible) browser profiles read from the browser's local state file
- Enter opens the selected profile in a dedicated browser window and closes cofi
Ctrl+[key]— assign the selected profile to a per-Profiles slotAlt+[key]— recall and launch a saved profile slot without opening the tab:profiles @SLOTor:profiles PROFILE— launch a profile directly from command mode- Browser executable resolved from
$PATH; falls back togoogle-chrome-stable
Chrome bookmarks across every discovered profile, triggered via :bookmarks or :bm.
- Walks
~/.config/google-chrome/<profile>/Bookmarksfor every Chrome profile reported by browser-profiles discovery, skippingGuest Profile. - Default profile bookmarks appear first, then remaining profiles in profile-tab order (active_time desc, case-insensitive name tie-break); within each profile, rows follow the
roots.{bookmark_bar, other, synced}walk order. - Each row shows five cells:
[bm], profile label, bookmark name, folder breadcrumb (display-truncated to 10 chars with leading…so the deepest folder stays visible), and URL. - Match-string corpus is
[bm] <profile_label> <name> <folder_breadcrumb> <url>. Scoring: bookmarks share the windows tab's tiered ranker viatier_score_stringover the combined match-string corpus ("[bm] profile_label name folder_breadcrumb url"). A direct word-boundary match (e.g. typing a profile name that appears verbatim in the row) ranks above any indirect fuzzy match regardless of field. Load-order is the tiebreaker. - URL scheme allow-list:
http,https,ftp,file. Skipsjavascript:,chrome://,chrome-extension://,data:, and empty URLs. - Row cap: 20000 across all profiles. Overflow logs a single WARN and stops appending; remaining profiles are partially included.
- Enter opens the URL in a new Chrome window (
chrome --profile-directory=<dir> --new-window <url>); Shift+Enter opens without--new-windowso Chrome reuses an existing window/process (default behavior). - Shortcut hint:
Enter=Open new window Shift+Enter=Reuse Chrome Ctrl+key=Assign slot Alt+key=Recall slot Ctrl+[key]assigns the selected bookmark to a per-Bookmarks slot;Alt+[key]recalls and launches a saved bookmark slot without opening the tab.:bookmarks @SLOTor:bookmarks QUERYlaunches a bookmark directly from command mode. Slot payload format:bookmark:chrome:<profile_dir>:<url>(colons inside the URL are preserved). Row identity:bookmark:<profile_dir>:<url>.- No caching, no
inotify: every tab entry re-reads the Bookmarks files.
Nineteen tabs exist; visibility is controlled per-tab (TFD-545):
- PINNED (always shown, always Tab-reachable): Windows, Apps
- HIDDEN by default (only surfaced by
:show <verb>or explicit flows): Path, Bluetooth, Files, Sessions, Workspaces, Harpoon, Matching, Layouts, Config, Hotkeys, Rules, Calc, Sinks, Run, Proc, Projects, Profiles, Bookmarks
Tab/Shift+Tab cycles PINNED tabs plus any currently-SURFACED tabs. Secondary tabs do not appear in Tab cycling until surfaced.
- Windows — main window list with search and MRU ordering (PINNED)
- Apps — installed desktop application launcher + system actions (PINNED)
- Path — PATH executable launcher (HIDDEN by default)
- Bluetooth — paired BlueZ device list; Enter toggles connect/disconnect (HIDDEN by default)
- Files — fuzzy file finder under
$HOMEviafd(HIDDEN by default) - Sessions — Claude/Codex session search and resume (HIDDEN by default)
- Workspaces — workspace list and management (HIDDEN by default)
- Harpoon — harpoon slot assignments (Ctrl+P edit pattern, Ctrl+D delete) (HIDDEN by default)
- Matching — custom window name assignments (Ctrl+E edit name, Ctrl+P edit pattern, Ctrl+D delete) (HIDDEN by default)
- Layouts — saved window layouts (Ctrl+D/Delete delete, Ctrl+L workspace restore, Ctrl+T enable/disable, Ctrl+P edit pattern) (HIDDEN by default)
- Config — all config options (Ctrl+T toggle/cycle, Ctrl+E edit) (HIDDEN by default)
- Hotkeys — hotkey bindings (Ctrl+E edit, Ctrl+D delete) (HIDDEN by default)
- Rules — title-pattern automation rules (Ctrl+A add, Ctrl+E edit commands, Ctrl+P edit pattern, Ctrl+D delete, Ctrl+X replay selected, Ctrl+Shift+X replay all) (HIDDEN by default)
- Calc — calculator modal (HIDDEN by default)
- Sinks — audio sink selection (HIDDEN by default)
- Run — command runner modal (HIDDEN by default)
- Proc — process manager (HIDDEN by default)
- Projects — tmux/zellij sessions and zoxide folders (HIDDEN by default)
- Profiles — browser profile launcher (HIDDEN by default)
- Bookmarks — Chrome bookmarks across every profile (HIDDEN by default)
- Selection state is preserved per tab when switching
Bluetooth is a hidden-by-default provider tab surfaced by :bt,
:bluetooth, or :show bluetooth.
- Scope is paired BlueZ devices only. v1 does not scan, pair, unpair, or manage adapter power state.
- The first column is a persistent connected-state glyph:
🟢means the device is currently connected,⚪means it is not. - The trailing state column is transient feedback only:
[…connecting…],[connected],[failed], and[disconnected]flash briefly, then clear. - Devices are listed in deterministic alphabetical order by alias, with object path as the tiebreaker for identical aliases.
- The adapter column is hidden when exactly one powered adapter exists, and is shown when multiple powered adapters are present.
- Enter toggles connect/disconnect.
cforces connect,dforces disconnect, andrrefreshes immediately. - Refresh is async and poll-driven in v1; the active tab re-queries BlueZ on a 3-second cadence without blocking cofi.
Files is a hidden-by-default provider tab surfaced by :files or
:show files.
- Scope is any file under
$HOME, discovered by an asyncfdscan and cached once per daemon lifetime until refreshed. - Hidden files are included, but
fdstill honors.gitignoreand.fdignoreby default, which naturally suppresses many build and dependency artifacts. - Rows show basename first and full path second.
- Enter opens the selected file via
xdg-openand closes cofi. rcancels any in-flight scan and refreshes the cache.
Stored in ~/.config/cofi/:
options.json— application settings- Window position (9 positions: top, center, bottom, corners)
- Close on focus loss (on/off)
- Workspace grid columns (
workspaces_per_row, 0 = single row) - Tiling grid columns (2 or 3)
- Digit slot mode (
default/per-workspace/workspaces) - Slot overlay duration in ms (default 750, 0 = disabled)
- Ripple effect (on/off, default on)
- Projects executable overrides:
projects.tmux_pathprojects.zellij_pathprojects.zoxide_pathprojects.file_explorer_path- Empty means resolve the executable from cofi's process
PATH; non-empty values must be absolute executable file paths.
hotkeys.json— system hotkey bindings as{key, command}pairs (up to 64)- Managed via
:hotkeyscommand or by editing the file directly - Auto-generated with default show-mode bindings on first run
- Managed via
harpoon.json— harpoon slot assignments with match patternsmatching.json— custom window matching entriesclass_name/instance/typeare optional anchors. When set, they must exactly match the live window property; when empty, they are unconstrained.- Auto-created entries (
:sl, harpoon-assign, name-assign) capture these three anchors from the source window for precise matching. - User-authored rules currently remain title-focused; future editing paths can deliberately leave anchors empty for title-only behavior.
rules.json— window-title rules (match_id,pattern,commands) in stored order- Legacy
pattern-only rules migrate on load by creating a matching MatchEntry (wildcard semantics, empty class/instance/type anchors); next save writesmatch_idwhile retainingpatternas a compatibility cache for one release.
- Legacy
Runtime config changes via :set <key> <value> are saved to options.json immediately. View current config with :config.
--align ALIGNMENT— window position (center, top, bottom, top_left, top_right, left, right, bottom_left, bottom_right)--no-auto-close— keep cofi open when it loses focus--windows/-W— delegate to Windows tab--workspaces— delegate to Workspaces tab--harpoon— delegate to Harpoon tab--matching— delegate to Matching tab (--namesalias kept for compatibility)--command— delegate to command mode--run— delegate to run mode--applications— delegate to Apps tab--assign-slots— assign workspace window slots and exit--log-level LEVEL— set log verbosity (trace, debug, info, warn, error, fatal)--log-file FILE— write logs to file--no-log— disable logging--version— show version--help— show usage--help-commands/-H— show command mode help
When a window is activated (via Alt-Tab, harpoon, workspace switch), a visual ripple highlights the target window.
- Circle ripple: expanding ring centered on the target window
- Vsync-locked 60fps via GdkFrameClock + Cairo on a single ARGB popup window
- 200ms duration with ease-out easing
- Ring expands from 10% to 30% of screen height, 16px stroke width
- Color: light blue-white (#f0f0ff), full brightness for 80% then fades out
- Cancelled immediately on workspace switch to prevent accumulation
- Configurable:
ripple_enabledin options.json (default true) - Graceful fallback: disabled when no ARGB visual available (no compositor)
- HiDPI-safe: X11 pixel coordinates for positioning, GDK logical coordinates for drawing
Commands support no-space compact form: :cw2 = :cw 2, :mawk = :maw k.
For window-state commands (sb, ab, aot/at, ew, mw, hm, vm), compact +/- are supported: :sb+ = :sb on, :ew- = :ew off, :mw+ = :mw on, :hm- = :hm off.
Rules tab save/edit/delete are persistence-only (rules.json): saving a rule does not immediately replay it. Automatic runtime rule behavior remains transition-based. Manual replay actions are explicit and stateless: Ctrl+X replays selected rule against all currently open matching windows; Ctrl+Shift+X replays all stored rules in stored order against current open windows.
- Compact forms defined in a data-driven table (COMPACT_FORMS) mapping command names to accepted suffix characters
- Parser picks the longest matching command name to resolve prefix ambiguity
- Exact command names are never split:
:mawstays as command "maw" with no arg, not "m" + "aw" - Aliases participate in matching:
:j3matches alias "j" for "jw", producing "jw" + "3"
The maintained startup path is the systemd user service installed by
mise run install / make install or mise run install-dev /
make install-dev. These install to /usr/local/bin by default. The explicit
local variants are mise run install-local / make install-local and
mise run install-dev-local / make install-dev-local. The service template is
scripts/cofi.service.