Configuration

TL;DR: Put overrides in ~/.config/fut/config.toml, then press Ctrl-b Shift-R. Everything under ui is declarative. trusted_commands, extension directories, and trusted project recipes can execute programs and should be treated like shell scripts.

Fut’s global presentation configuration is safe, declarative, and non-executable. Separately, trusted_commands, explicitly configured local extension directories, and trusted project recipes are executable boundaries; see Security boundary.

Fut refuses to start or attach another interactive Fut client from inside one of its terminals. If nesting is intentional, set FUT_ALLOW_NESTED in the command’s environment to force it, for example FUT_ALLOW_NESTED=1 fut.

Runtime location

Fut keeps its private Unix socket, lock, and daemon log under $XDG_RUNTIME_DIR/fut when XDG_RUNTIME_DIR is an absolute path. Otherwise it uses ${TMPDIR:-/tmp}/fut-UID. Set FUT_RUNTIME_DIR to an absolute directory to override that location, FUT_SOCKET to an absolute socket pathname, or pass --socket explicitly. Fut rejects empty or relative explicit overrides; empty or relative XDG and temporary-directory values are ignored according to their fallback semantics. Run fut doctor to inspect the resolved location without creating it.

Location and lifecycle

Fut checks, in order:

  1. config.toml in the absolute directory passed with --config-dir;
  2. the absolute path in FUT_CONFIG;
  3. $XDG_CONFIG_HOME/fut/config.toml when XDG_CONFIG_HOME is absolute;
  4. ~/.config/fut/config.toml.

A missing implicit file, including one selected through --config-dir, uses defaults and is not created. Pass --no-config to use built-in defaults without reading any configuration file; it cannot be combined with --config-dir. --config-dir and FUT_CONFIG must be absolute; FUT_CONFIG must exist. Configuration is loaded before an interactive client changes terminal state. Press Ctrl-b Shift-R to reload the invoking client’s global configuration and then the trusted project extension configuration for the session focused when reload began. Valid global bindings and layout commit first; if the project step fails, Fut keeps those global changes, leaves the project’s previous extension configuration active, and explicitly reports the partial success. Control commands and shell completion do not load UI configuration; fut doctor reads it without creating runtime state.

Files must be regular UTF-8 files no larger than 64 KiB. Unknown fields, invalid values, unsafe control or bidirectional-formatting characters, ambiguous segments, and out-of-scope tokens are errors.

Editor completion

Fut publishes a JSON Schema for global configuration. Add this Taplo schema directive as the first line of config.toml to get validation, documentation, and completion in compatible TOML editors:

#:schema https://fut.sh/schemas/config.json

The schema offers built-in action completion for [ui.bindings] and [ui.hotkeys]. Installed extension command slugs and extension-owned settings remain dynamic and are validated by Fut when it loads the configuration.

Closing a pane, tab, workspace, or session asks for confirmation by default. While the prompt is open, it exclusively owns keyboard, paste, and pointer input; resizing the host terminal remains available. Set ui.confirm_close = false to perform those close actions immediately. This setting is client-local and applies to both keyboard commands and contextual menus. Interactive rename and close actions can change only the session to which that client is attached and resources within it. A tab, workspace, or session close can finish only partway if a later terminal cannot close; Fut reports how many terminals exited and keeps the target marked closing because those exits cannot be undone.

Terminal-native alerts are configured at the top level. They are separate from coding-agent lifecycle notifications:

[alerts]
signal_outer_terminal = true # Optional; default false.

signal_outer_terminal is client-local and emits one BEL when a newly unseen terminal bell arrives; whether that BEL is audible, visual, or ignored remains under the containing terminal emulator’s control. It is deliberately off by default.

Terminal scrollback

Fut retains up to 100 MiB of scrollback per terminal by default. Storage grows as output arrives; the full budget is not allocated up front. Set a smaller or larger budget in bytes:

[terminal]
scrollback_bytes = 104857600 # 100 MiB (default); 1073741824 is 1 GiB.

The budget measures terminal storage, not plain-text output or a fixed number of lines. Actual storage is subject to internal page sizes and minimum screen storage. Set 0 to disable scrollback.

Pane terminals use the configuration loaded when the daemon starts. Restart the daemon to apply a changed budget to panes; client configuration reload does not resize existing history buffers. Newly opened command popups use the client’s current setting after Ctrl-b Shift-R. Alternate-screen applications manage their own history.

Example

This example is intentionally customized; it is not a dump of the defaults. Omit any field to keep its default value.

#:schema https://fut.sh/schemas/config.json

extensions = [
  "extensions/review-status",
  "extensions/run",
]

[alerts]
signal_outer_terminal = false

[ui]
pane_layout = "splits" # "splits" or "accordion"
confirm_close = true   # Require confirmation before closing panes, tabs, or workspaces.
prefix = "ctrl-b"

[ui.bindings]
open_command_bar = "space"
"run:restart" = "r"
# open_messages = "m"
# open_project = "S"
# reload_config = "R"
# reload_project_config = "P"
# enter_copy_mode = "["
# open_navigator = "s"
# open_agents = "a"
# open_left_sidebar = "w"
# open_right_sidebar = "]"
# open_tab_bar = "t"
# open_notifications = "u"
# focus_next_notification = "prefix"
# create_workspace = "C"
# create_tab = "c"
# rename_session = "S"   # These rename and close actions have no default binding.
# rename_workspace = "W"
# rename_tab = "T"
# close_session = "q"
# close_workspace = "Q"
# close_tab = "X"
# focus_last_tab = "ctrl-t"
# focus_last_workspace = "ctrl-w"
# focus_last_session = "ctrl-s"
# close_pane = "x"

[ui.hotkeys]
# These run immediately, without the prefix.
# "ctrl-t" = "open_tab_bar"
# "ctrl-f" = "open_navigator"
# "ctrl-r" = "run:restart"

[trusted_commands.git_diff]
title = "Repository diff"
binding = "g"
program = "~/.dotfiles/tmux/tmux.symlink/git_diff_popup.sh"
# args = ["--optional-argument"]
size = { width = 120, height = 40 }

[ui.icons]
preset = "nerd_font" # "ascii", "unicode", or "nerd_font"
# current = "*"      # Every icon may be overridden.
# closing = "x"
# overflow = "..."
# workspace = "W"
# tab = "T"
# zoom = "zoom"
# notification = "• " # Prefix before the unread notification count; spacing is preserved.
# vertical_divider = "|"
# pill_left = ""       # Focused tab/workspace pill caps; empty outside "nerd_font".
# pill_right = ""

[ui.spinner]
style = "dots" # Browse the built-in styles with `fut --ui-playground`.
# interval = 80 # Optional frame delay in milliseconds.
# frames = ["-", "\\", "|", "/"] # Optional custom frames; overrides style.

[ui.styles.current]
foreground = "blue"
add_modifiers = ["reversed"]
remove_modifiers = ["underlined"]

[ui.styles.selected]
background = "dark_gray"
remove_modifiers = ["reversed"]

# Navigator resource levels default to red, blue, green, and magenta.
[ui.styles.session]
foreground = "red"

[ui.styles.workspace]
foreground = "blue"

[ui.styles.tab]
foreground = "green"

[ui.styles.pane]
foreground = "magenta"

[ui.styles.divider]
foreground = "dark_gray"

[ui.styles.attention]
foreground = "yellow"
add_modifiers = ["bold"]

[ui.styles.activity]
foreground = "light_cyan"

[ui.styles.added]
foreground = "green"

[ui.styles.deleted]
foreground = "red"

[ui.tab_bar]
position = "top" # "top" or "bottom"
left = [
  { segments = [{ token = "workspace.icon", suffix = " " }, { token = "workspace.name", max_width = 20 }], style = "muted", priority = 200 },
]
center = [
  { segments = [{ component = "tabs" }], priority = 100 },
]
right = [
  { segments = [{ token = "workspace.extension.run.status", inverted = true, pill = true }], priority = 220 },
  { segments = [{ token = "client.zoom", suffix = " " }], priority = 255 },
  { segments = [{ token = "client.help" }], style = "muted", priority = 0 },
]

[ui.tab_bar.item]
segments = [
  { text = " " },
  { token = "tab.index" },
  { token = "tab.name", prefix = " " },
  { token = "tab.closing", prefix = " " },
  { token = "tab.activity", prefix = " " },
  { text = " " },
]

[ui.sidebar.left]
width = 28
display = "expanded" # "expanded" or "minimized"
visibility = "automatic" # "visible", "automatic", or "hidden"
components = [
  { component = "workspaces", size = "fill", header = [{ token = "session.name", style = "current" }], footer = [{ token = "sidebar.status", style = "muted" }], row = { left = [{ text = " " }], body = [{ token = "workspace.index" }, { token = "workspace.name", prefix = " " }], right = [{ token = "workspace.tab_count" }, { token = "workspace.closing", prefix = " " }, { text = " " }], detail = [{ text = "    " }, { token = "workspace.git_branch", style = "muted" }, { token = "workspace.git_added", prefix = " " }, { token = "workspace.git_deleted", prefix = " " }] } },
]

[ui.sidebar.right]
width = 28
display = "expanded"
visibility = "automatic"
components = [
  { component = "agents", size = "fill", scope = "session" },
]

An explicitly empty array hides that lane or format.

Local extensions

extensions is an explicit list of trusted directory paths, loaded in order. Paths beginning with ~/ are resolved from your home directory, relative paths are resolved from the directory containing config.toml, and absolute paths continue to work unchanged. Fut appends packages enabled through its Fut-owned local managed store; it never rewrites this array or any other part of config.toml. The complete merged set is accepted or rejected atomically, and references to extension tokens are validated with it. Fut does not discover or download extensions. See Extensions for managed install commands, trust boundaries, hooks, tokens, limits, payloads, and examples. Third-party authors should use the Extension authoring guide as the API contract.

The global file, a trusted project recipe, and each workspace’s .fut/config.toml may configure a loaded extension with the same namespaced table. Values layer global → trusted project → workspace:

[extension.run]
command = ["just", "run"]
signal = "STATUS:READY" # optional literal readiness marker
click = "owner"         # optional "owner" (default) or "logs"

Workspace values recursively override trusted project and global defaults for explicit commands invoked there. Unknown extension IDs and oversized data are errors; the extension owns validation of its inner keys and values. Fut gives extensions the trusted global/project layers separately; the bundled automatic behaviors use only those values. Workspace-local values remain inert in the bundled extensions until an explicit command is invoked. See the bundled run extension for optional trusted auto_start and manual run:restart, and the bundled wt extension for optional existing-worktree discovery.

Bindings

Set ui.prefix to change the default ctrl-b prefix. Bindings are unique suffixes after it. Set a built-in action or quoted extension command slug under [ui.bindings]; see the complete defaults table for action names. To run an action immediately without entering the prefix, map the key to the action under [ui.hotkeys] instead. Direct hotkeys are additional: the action’s prefixed binding continues to work.

The command palette also includes rename-session, rename-workspace, rename-tab, kill-session, kill-workspace, and kill-tab. They have no default bindings; configure the corresponding rename_* or close_* keys to add shortcuts. kill-pane remains bound through close_pane by default.

A prefix, suffix, or hotkey may be one printable character, ctrl-a through ctrl-z, space, enter, tab, esc, up, or down. Suffixes also accept prefix, meaning the configured prefix key again. A hotkey cannot be the configured prefix or share a key with another hotkey. Pause after the prefix to see the effective bindings, or press prefix then : to search the command palette.

An [extension_commands."EXTENSION:COMMAND"] table can set args to replace the arguments supplied after that extension command’s manifest executable. The arguments are direct strings with no shell interpolation. An empty array passes no arguments, and unknown qualified command slugs are rejected.

Trusted commands

Each [trusted_commands.NAME] table requires title and an executable program, plus optional binding, hotkey, string array args, size, and activate_opened values. binding follows the prefix; hotkey runs the command directly. program supports ~ and ~/ home-directory expansion. size and activate_opened have the same semantics as an extension command; omitting size preserves the full-terminal surface, while activation defaults to false. Running a command opens a dashed frame containing a temporary PTY, inherits the focused pane process’s live working directory, and sends normal terminal input to the command. The frame names the command and identifies the temporary surface; when the process exits, Fut restores the previous panes, focus, and geometry. A bound trusted command may take a built-in’s default key, which unbinds that built-in unless it is explicitly rebound under ui.bindings. Explicit binding and hotkey collisions and duplicate command keys are rejected. Commands appear in the command palette; prefixed command bindings also appear in delayed which-key help. Configuration reload replaces them atomically.

Agents is a read-only projection of terminals with an explicit agent integration. Its scope is tab, workspace, session, or global. Tab, workspace, and session filters use fresh live focus ancestry when available and otherwise fall back to the selected IDs; global needs no focus anchor. Rows under any closing session, workspace, tab, or pane are omitted; screen detection alone does not add a row. Rows show idle, working, blocked, and daemon-wide unread-completed activity plus session/workspace/tab context. Enter navigates with the row’s typed pane ID; global safely permits cross-session destinations. The separate Notifications dialog remains the unread-attention surface.

Each sidebar width is independently 4 through 80 cells and includes its one-cell inner divider. Each tagged built-in entry uses component = "workspaces" or component = "agents" and has either size = "fill" or a positive fixed row count; each side may contain at most one fill and at most one Workspaces component. Left-drag either visible divider to resize only that side. A docked drag preserves at least 40 terminal columns after accounting for the other docked side; an open drawer’s divider is also draggable, while a hidden drawer is not. Dragged widths belong only to that attached client: they are not written to configuration, and both configured widths return on reattach or configuration reload. The active workspace is marked with a bullet.

Display, visibility, components, and relevance are independent per side. display = "expanded" uses that side’s configured width, while display = "minimized" uses a fixed six-cell rail. Minimized Workspaces and Agents rows keep stable numeric positions plus current/activity markers. Opening a minimized sidebar temporarily expands its drawer to the configured width; the rail itself is not draggable, but its open drawer remains resizable. Press m inside a component to toggle its side. visibility = "visible" docks when geometry permits. visibility = "automatic" docks when that side has a relevant configured component: Workspaces with multiple live workspaces, or Agents with an integrated terminal in their configured scope. visibility = "hidden" leaves the side as an on-demand drawer. Press h to cycle that side through visible → automatic → hidden. With the default Workspaces footer, an open sidebar shows h, m, and ? on separate lines with their current states; the nerd_font preset adds matching visibility, width, and help icons.

The defaults are left Automatic with one Workspaces fill, and right Automatic with one session-scoped Agents fill; an empty Agents projection therefore consumes no right-side width. Both sides dock when their combined widths leave 40 terminal columns. If only one can fit, allocation is deterministic: left is considered before right. Any hidden, irrelevant, or non-fitting side remains available as its own edge drawer without reducing terminal geometry. The current side’s display and visibility appear in the default footer. sidebar.display and sidebar.visibility expose those labels to custom sidebar chrome. vertical_divider must be exactly one grapheme and one display cell.

Segments, groups, and components

Every segment sets exactly one of:

Token segments may also set prefix, suffix, max_width, style, inverted = true, and pill = true. Prefix and suffix are emitted only when the token is nonempty. Inversion uses terminal reverse-video after normal style composition: the semantic foreground becomes the fill and the glyph uses the underlying terminal background, so it adapts to light and dark themes. A pill requires inversion and adds the configured pill_left and pill_right glyphs in that fill color around the complete segment, including its affixes. If either cap is empty, as in the Unicode and ASCII presets, Fut renders only the inverted content. Empty tokens emit neither affixes nor caps. Text segments accept style but not token-only options. Components must be the only segment in their group and do not accept segment options.

Tab-item content uses the intrinsic display-cell width of its configured segments. The default format supplies one cell of padding on each side; add or remove text segments to adjust that spacing. The Nerd Font preset additionally reserves one cell at each end of every item so its active-tab pill does not move neighboring tabs. New tabs are created unnamed, so their shared title follows the foreground process in the oldest surviving pane. Focusing another pane does not rename the tab. Name a tab with fut tab new --name or Ctrl-b r to keep that title fixed; renaming it to an empty string restores automatic naming.

Tab-bar lanes contain groups. A group has segments, an optional semantic style, and a priority from 0 through 255. The tabs component is flexible and keeps its active or keyboard-selected item visible. Groups with higher priority than tabs reserve their complete width first. Tabs then grow toward their complete preferred width. Lower-priority groups appear only in remaining space. Left and right lanes stay edge-aligned; the center lane is geometrically centered and clamped between them. Groups never overlap. By default the right lane names the current workspace, truncated at 20 cells.

Workspace rows have intrinsic left and right lanes; body receives the remaining cells and truncates safely. Expanded default rows show the workspace number and name, reserve leading and trailing padding, and use the same current style as the focused tab instead of a separate active marker. With Nerd Font pill caps, only that title line becomes a pill. The minimized rail keeps its compact active-workspace bullet. A nonempty detail format adds a second full-width line. Workspaces are unnamed unless explicitly named, presenting as their live location — the work tree (or directory) every open pane is inside, or multiple when panes disagree. The default detail aligns under the row name and shows the Git branch at that live location with its short working-tree diff (+N inserted, -N deleted). The daemon collects those values with bounded background git processes and publishes them atomically into the shared resource snapshot, so rendering never waits, attached clients agree, and non-Git locations simply stay empty. Set detail = [] for compact one-line rows. Header and footer are optional single-line segment lists. At tiny heights Fut preserves resource rows over decorative header/footer content, while switching and error status remains visible.

All widths are terminal display cells. Dynamic values are sanitized and truncated at grapheme boundaries. Bars never wrap.

Spinners and effects

Run fut --ui-playground to browse the built-in non-emoji spinner styles at their intended speed and preview plain, pulse, wave, inverted, and pill token treatments—including effect combinations—with the active theme. The standalone playground reads the normal configuration but does not start or contact a daemon. Search by typing and move with the arrow keys or Ctrl-j/Ctrl-k.

Choose a named style or provide a custom, equal-width frame list:

[ui.spinner]
style = "circle_halves"
# interval = 80 # Override the preset's frame delay.

# Or define a custom spinner. `style` is ignored when frames are present.
# frames = ["-", "\\", "|", "/"]
# interval = 100

The interval must be 16–2000 milliseconds. Custom lists accept 1–256 safe, nonempty frames, each 1–32 display cells wide; every frame must have the same display width so surrounding UI does not jump. The selected spinner is shared by agent activity and extension tokens declared with presentation = "spinner".

Pulse alternates the complete text between normal and faint. Wave moves the faint treatment across its characters. Neither effect changes glyph weight; affixes participate while pill caps retain a stable fill.

Styles

The fixed semantic roles are:

Each style accepts:

foreground = "yellow"
background = "default"
add_modifiers = ["bold", "underlined"]
remove_modifiers = ["dim"]

Modifiers are bold, dim, italic, underlined, reversed, and crossed_out.

Colors may be default, ANSI names such as red, blue, gray, dark_gray, or light_cyan, an indexed color such as index:123, or exact RGB such as #12abef. Indexed colors remain references to the containing terminal’s palette; RGB colors remain exact.

Styles compose in this order: normal, group style, token style (activity, attention, added, or deleted when supplied), segment style, current, attention, closing, selected. Later foreground/background values replace earlier ones; modifiers are added or removed in sequence. An inverted token adds reverse-video after this composition without discarding the composed colors or modifiers.

Icons

unicode is the default and requires no private-use glyphs. ascii uses ASCII for the configurable resource icons; ordinary built-in help and truncation text may still use Unicode. nerd_font opts into a small Nerd Fonts v3-oriented resource/state set, and additionally uses its Powerline half-circle caps for focused tabs, expanded workspaces, and token segments configured with pill = true. Every icon can be overridden under [ui.icons].

Fut cannot reliably detect the active terminal font. Use fut doctor for an honest visual probe; it never claims that an installed font is active.

Security boundary

Everything under ui remains non-executable: it has no shell commands, file reads, environment interpolation, networking, functions, or expression language, and presentation tokens resolve only daemon-validated materialized values and actions. trusted_commands is a deliberate executable trust boundary. Fut executes its program directly with the configured args and the client’s environment; it does not invoke a shell unless you explicitly configure one. Explicit extension roots, enabled managed extensions, and namespaced project tables are the equivalent trust decision for extension manifests, their packaged commands, token click actions, and values those commands may execute. Managed installation only copies a local package or fetches one explicitly pinned Git commit before validating it; it runs no package scripts, and enablement is the explicit trust decision. An explicit project recipe path is also a global trust decision; a repository .fut/project.toml becomes executable only after fut trust [PATH] approves that canonical file’s exact current bytes. Only configure commands, extension directories, and recipes you trust. Commands never run during configuration parsing or rendering; token commands require an explicit click, resource hooks run only from committed daemon mutations, client hooks run only for attachment lifecycle transitions, and recipe tabs, panes, and commands run only while bootstrapping a project session. Trusted recipe extension settings may deliberately make lifecycle hooks perform work such as opening existing worktrees or starting a managed command when the project session is created. The same-user Unix socket is the authorization boundary for token publication, just as it is for other Fut control commands; Fut does not claim to authenticate the publishing process beyond that boundary.