- Rust 84.6%
- Python 11.9%
- Nix 3.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci / cargo fmt / clippy / test / deny (push) Successful in 23s
Reviewed-on: #5 |
||
| .forgejo/workflows | ||
| docs | ||
| scripts | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| clippy.toml | ||
| deny.toml | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
| rustfmt.toml | ||
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
(
ShadowScreen) 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 travel as key events that the felis daemon encodes against the session's own keyboard modes (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 a parked 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.