No description
  • Rust 86.2%
  • Python 10.2%
  • Nix 3.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
natsukium 9ef97168a5
All checks were successful
ci / cargo fmt / clippy / test / deny (push) Successful in 34s
refactor: paint rows through felis-grid's shared ANSI re-encoder
The local SGR emitter had drifted from the grid it renders. It never
learned underline styles, underline colour, conceal, or overline, and
neither it nor its felis-side counterpart emitted OSC 8 — so a curly
underline or a hyperlink the shell produced rendered correctly in the
GUI client and silently died on the way through here. Nothing catches
that: the grid grows an SGR dimension upstream and a copy downstream
just keeps not emitting it.

felis-grid now owns one encoder for every consumer that turns cell
state back into bytes, with the host-specific decisions passed in
rather than baked in, so what stays here is what is genuinely local: a
pane's column geometry, the cursor addressing, and the image overlay.
`Caps` keeps the probe and hands the encoder the colour-depth bit;
graphics support never leaves this crate, since it drives the overlay
rather than any row of text.

Two behaviours change with the shared encoder. An unresolvable cluster
handle now holds its column with a space instead of U+FFFD — the handle
is transient (it resolves when the interning message lands), and a
momentary blank reads better than a momentary replacement glyph. A wide
glyph whose right half falls outside the pane is blanked rather than
printing two columns into one, which previously bled a column into the
neighbouring pane.

The PTY harness gains the coverage that would have caught the drift:
hyperlinks and the underline dimensions, asserted on the composited
frame. Its hyperlink send settles longer than the others because the
anchor registry arrives as its own message, and a row that lands before
its table resolves to no link by design.
2026-08-15 10:33:39 +09:00
.forgejo/workflows ci(tui): add fmt/clippy/test/deny workflow 2026-06-21 10:27:22 +09:00
docs feat(layout): add directional focus so columns can be scrolled 2026-06-21 10:16:57 +09:00
scripts refactor: paint rows through felis-grid's shared ANSI re-encoder 2026-08-15 10:33:39 +09:00
src refactor: paint rows through felis-grid's shared ANSI re-encoder 2026-08-15 10:33:39 +09:00
.gitignore build: add flake-parts dev environment 2026-06-20 20:56:37 +09:00
Cargo.lock refactor: paint rows through felis-grid's shared ANSI re-encoder 2026-08-15 10:33:39 +09:00
Cargo.toml refactor: paint rows through felis-grid's shared ANSI re-encoder 2026-08-15 10:33:39 +09:00
clippy.toml chore(tui): adopt felis core's lint, format, and deny policy 2026-06-21 10:27:07 +09:00
deny.toml chore(tui): adopt felis core's lint, format, and deny policy 2026-06-21 10:27:07 +09:00
flake.lock build: add flake-parts dev environment 2026-06-20 20:56:37 +09:00
flake.nix chore(tui): provide cargo-deny in the devShell 2026-06-21 10:27:13 +09:00
LICENSE chore(tui): relicense to Apache-2.0 to match the felis ecosystem 2026-06-21 22:50:44 +09:00
README.md feat(layout): add directional focus so columns can be scrolled 2026-06-21 10:16:57 +09:00
rustfmt.toml chore(tui): adopt felis core's lint, format, and deny policy 2026-06-21 10:27:07 +09:00

felis-tui

A felis-native terminal multiplexer and TUI client.

felis is a GPU terminal emulator with a daemon/client split. Its sessions already survive the window closing — the daemon is the persistence layer — and felis deliberately refuses tabs, splits, and panes, delegating layout to the window manager (felis principle 1).

That delegation assumes a first-class WM. On Windows, on macOS, and over a bare SSH login, you may not have one. felis-tui fills that gap: it attaches to the felis daemon from any host terminal and provides the persistence and the splits that felis itself won't — while losing as little of felis's fidelity as the host terminal allows.

Why not just use tmux?

A multiplexer running inside another terminal can only render what that host terminal supports — that part is unavoidable. The difference is where the fidelity is lost:

  • tmux/zellij/dvtm re-parse the PTY byte stream with their own, necessarily incomplete, VT parser. They lose information at the parse step and again at the render step.
  • felis-tui consumes felis's already-parsed, structured grid (ShadowGrid) over the felis protocol. There is no re-parse. The only fidelity loss is the final re-encode into the host terminal's capabilities — the theoretical minimum.

That is the whole bet, and it is recorded as a hard non-goal in docs/non-goals.md: felis-tui never re-parses a PTY byte stream. The day it wants to, it has become tmux and lost its reason to exist.

Status

Working multiplexer: each pane is its own felis session over its own connection, mirrored and composited into one host terminal. The host is tiled into a scrollable row of columns (the niri model), each column a vertical stack of panes. By default widths auto-fit the host so it behaves like a snug tiling WM (presets: even-horizontal, even-vertical, main-vertical, tiled); resize a column and it switches to free, scrollable widths, panning to keep the focused column in view. Keys are encoded through felis's own key_encode (full Kitty keyboard protocol), and colour is down-mapped to the host (true-colour passed through, else nearest xterm-256).

On a Kitty-graphics-capable host, images are re-emitted as Kitty graphics clipped to their pane — the thing tmux/zellij can't do, because felis-tui has the placement geometry as structured data rather than a re-parsed byte stream. On other hosts images are dropped client-side ("the client owns pixels"). Behaviour and config live in docs/roadmap.md.

cargo run            # first pane re-attaches an idle session, or spawns a shell

Prefix is Ctrl-A, then:

key action
h new pane in a new column beside the focus
v new pane below the focus, in the same column
s swap layout — cycle the preset, re-arranging the panes
> / < grow / shrink the focused column (switches to free, scrollable widths)
f toggle snug auto-fit ↔ free (scrollable) widths
p / n focus the column to the left / right — scrolls it into view
k / j focus up / down within the current column
o focus the next pane (cycle)
x close (detach) the focused pane — its shell keeps running
d detach all and quit
Ctrl-A send a literal Ctrl-A to the focused pane

Detaching never kills a shell: the session lives in the daemon, so closing felis-tui (or a pane) parks it for the next attach.

Config

Optional, at ~/.config/felis-tui/config.toml (or $XDG_CONFIG_HOME/felis-tui/config.toml). The [keys] table overlays the defaults, so you only list what you change. Keys bind to a fixed action set — there is no scripting, by design (see docs/non-goals.md).

prefix = "C-a"          # the multiplexer prefix chord
layout = "even-horizontal"  # startup preset (see below)

[keys]                  # key -> action (overlays defaults)
h   = "split-horizontal"
v   = "split-vertical"
s   = "swap-layout"
">" = "grow-width"
"<" = "shrink-width"
f   = "toggle-fit"
o   = "focus-next"
p   = "focus-left"
n   = "focus-right"
k   = "focus-up"
j   = "focus-down"
x   = "close"
d   = "detach"
"C-a" = "literal-prefix"

[theme]
separator = 8           # ANSI-256 index for pane separators

Actions: split-horizontal (new column), split-vertical (new pane in the column), swap-layout, grow-width, shrink-width, toggle-fit, focus-next, focus-left, focus-right, focus-up, focus-down, close, detach, literal-prefix. Key chords are a single character with an optional C- (Ctrl) or M- (Alt) prefix.

Layout presets (for layout and what swap-layout cycles through): even-horizontal, even-vertical, main-vertical, tiled.

Not yet

  • Pixel-level cropping of images that overflow a pane (today an overflowing or natural-size placement is skipped rather than cropped), scrollback-anchored placements, animation, and placeholders on incapable hosts.
  • Query-based graphics detection (today it is env markers).
  • Smooth scrolling: a partial "sliver" of the next column at the edge, and per-pane horizontal sub-scroll for a column wider than the host (today scrolling is whole-column).
  • Moving panes/columns (not just focusing them), and per-pane height resize within a column (today resize is column width only).
  • Scrollback viewport, copy mode, search.