- Rust 97.3%
- Shell 1%
- Nix 0.9%
- Python 0.5%
- Just 0.2%
- Other 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
Some checks failed
fuzz / cargo fuzz nightly long-run (push) Has been skipped
build / build and cache felis (nix) (push) Successful in 1m58s
fuzz / cargo fuzz smoke (per target) (push) Successful in 1m24s
pr / nix flake check (push) Successful in 15s
pr / cargo build / clippy / test / deny (push) Failing after 1m18s
pr / generated code is current (push) Successful in 2s
pr / cargo check (Windows cross) (push) Successful in 58s
pr / cargo check (MSRV) (push) Successful in 1m22s
build / build and cache felis (macos) (push) Successful in 27m2s
A session switch detaches its predecessor, so the daemon closing that stream is the switch completing — on the wire it is indistinguishable from the daemon itself dying, and the client read every such close as the latter and called `event_loop.exit()`. The window vanished mid-work with no crash report and nothing in the log but "daemon disconnected; exiting" while the daemon went on serving. Aborting the superseded reader was the only thing standing between that EOF and the exit, and it is a race it can lose: `JoinHandle::abort` takes effect at the task's next yield, which a reader already returning from EOF has passed. Two switches inside a millisecond — a run/pipe transient exiting straight into a deferred `Reattach` push — are enough to lose it, and by the time the stale EOF lands both switches have finished, so every transition guard reports "nothing in flight". Number each connection instead and stamp the number onto the events whose meaning is scoped to one: a close, a grid frame, an image frame. A stamp that is not the live one is ignored. Window-scoped events (`Reattach`, `SessionExited`, `Evicted`) stay unstamped on purpose — they are addressed to the window, not to the connection that delivered them, and dropping a `Reattach` that arrived just before a switch would lose a `felis sessions switch` the CLI already reported as delivered. The abort stays as an optimisation, no longer as the barrier. |
||
| .cargo | ||
| .claude | ||
| .config | ||
| .forgejo/workflows | ||
| crates | ||
| docs | ||
| fuzz | ||
| nix | ||
| scripts | ||
| share/terminfo | ||
| skills/felis | ||
| .gitignore | ||
| buf.gen.yaml | ||
| buf.yaml | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| clippy.toml | ||
| deny.toml | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| LICENSE | ||
| README.md | ||
| rustfmt.toml | ||
felis
felis is a terminal that redraws the boundary of what a terminal is responsible for. Written in Rust, it draws on kitty for protocol fidelity, alacritty for simplicity, and Kakoune for design discipline.
State lives in a long-running daemon; the client owns only the window. Closing the window keeps the shell running, and the next client — local or across SSH — picks up exactly where the last one left off.
Quick start
felis ships from a Nix flake, so the fastest path needs only the Nix package manager with flakes enabled — no manual toolchain setup, no separate daemon to start.
# Try it without installing anything.
nix run git+https://git.natsukium.com/natsukium/felis
# Or install it to your profile, then just run `felis`.
nix profile install git+https://git.natsukium.com/natsukium/felis
felis
Launching felis opens a window and auto-spawns the background
daemon off $PATH — there is nothing else to start. Close the
window and your shell keeps running; the next felis reattaches
exactly where you left off.
From there:
- Persistent sessions — close the window, then
felis sessions listandfelis attach <id-prefix>to pick a detached shell back up. - Cross-host —
felis --host user@remoteopens a window backed by a remote daemon over plain SSH, orfelis ssh user@remotere-points the window you are in at that daemon (andfelis ssh --localbrings it back). - Scripting —
felis sessions {list, info, send, capture, search, spawn, kill}drives the daemon from shell scripts; see CLI and automation.
The end-user manual — install, getting started, configuration,
keybindings, CLI reference, troubleshooting — is published at
https://felis-docs.pages.dev and maintained in this repository
under docs/, organized on the
Diátaxis map (tutorials, how-to guides,
reference, explanation).
Prefer to build from source or hack on felis? See Building from source.
Features
- Persistent sessions. Closing the client window does not kill
the shell;
felis attach <id-prefix>rehydrates a detached one, and chords cycle or spawn sessions in-window (keybindings). - Cross-host attach over SSH.
felis --host user@remoteopens a window backed by a remote daemon — frames ride plain SSH stdio (ssh <host> felis-daemon relay), no listening network service and no auth layer beyond SSH itself. - CLI for scripted automation.
felis sessions {list, info, send, capture, spawn, search, kill}with first-class--json— see CLI and automation. - Scrollback search.
felis sessions search <id> <pattern>greps your session's history; in-window, Ctrl+Shift+F opens a search bar with highlighted matches andn/Nnavigation. - Modern terminal protocols. True colour, italics, the xterm underline range (curly / dotted / dashed, with underline colour), OSC 8 hyperlinks, mouse SGR, bracketed paste, focus tracking, synchronized output, the Kitty keyboard protocol, OSC 133 semantic prompts, Kitty graphics (animation and shared memory included) and text sizing.
- Real text shaping. Per-codepoint font fallback (CJK + symbols +
colour emoji + Nerd Font), opt-in OpenType features / ligatures,
platform-native IME (AppKit /
zwp_text_input_v3/ TSF / XIM), live font zoom with Ctrl+Wheel. - Familiar selection and scrolling. Linear / rectangle / word / line selection with xterm gestures; Linux PRIMARY auto-copy and middle-click paste (the gesture table).
- Hot config reload. Edit the config and save; theme and font changes apply without restarting the daemon or losing scrollback.
Configuration
One TOML file at the platform-native path, hot-reloaded on save
(and on Ctrl+Shift+R). Every field — font, theme, palette,
cursor, clipboard, window, keymap — is documented with its defaults
and failure behaviour in
the configuration page; the default chord
table and the deliberately-unbound session actions are in
keybindings. A generated JSON Schema
gives TOML-aware editors completion and validation.
Install with Nix
nix run / nix profile install above are the whole story for a
quick try. For a managed setup the flake also exposes the package,
a home-manager module (installs the binaries, writes config.toml
from Nix, wires TERMINFO_DIRS), and a
Stylix hook that drives the
palette, font, and background opacity from the active scheme — all
documented on the install guide.
CLI and automation
felis sessions <verb> exposes every IPC operation a daemon
accepts as a typed shell command — list, info, spawn, send,
capture, search, kill — with --json on every read verb and
exit codes under contract (0 ok, 1 operation-domain failure,
2 no daemon / protocol error). Common patterns — pre-warming a
session, grepping scrollback, snapshotting for a bug report,
reaping stale sessions, editor integration — are collected in
the how-to guides with the verb
table in the CLI reference; the full surface
contract and the wire it rides live in
docs/reference/ipc.md, which
cross-language clients can speak directly.
Status
felis is a prototype, and runnable as one.
Works today:
- Day-to-day shell + editor use. neovim, helix, lazygit, fish, fzf, and similar TUIs behave correctly: mouse, focus tracking, bracketed paste, OSC 8 hyperlinks, OSC 133 semantic prompts, the Kitty keyboard protocol, synchronized output, true colour, italics, and the full xterm underline range.
- Persistent sessions. Close the window, list with
felis sessions list, reattach withfelis attach <id>. - Cross-host attach.
felis --host user@remoteover plain SSH stdio. - CLI for scripted automation.
felis sessions {list, info, send, capture, spawn, search, kill}— see "CLI and automation" above. - Kitty graphics (yazi, mdcat, presenterm previews, mpv video):
protocol, daemon-side image store, renderer atlas, animation
(
a=a/f/c), and shared-memory transmission (t=s) all wired. - Kitty text-sizing protocol (OSC 66): renderer paints scaled multi-cell runs at integer and fractional sizes.
- Scrollback search (REQ-607): daemon-side substring + regex,
exposed via
felis sessions searchand the in-window Ctrl+Shift+F bar with highlighted matches andn/Nnavigation. - OpenType font features and programming-font ligatures, opt-in
via
font.features(defaults off so cell metrics never shift unasked). - IME on macOS / Wayland / Windows / X11; per-codepoint font fallback covering CJK, symbols, colour emoji, and Nerd-Font glyphs.
- xterm-flavoured selection, clipboard, scrollback, and font
zoom (
Cmd+*on macOS,Ctrl+Shift+*elsewhere). - Hot config reload — theme and font changes swap into the running client without restarting the daemon.
- Bidi-override visible markers (Trojan-Source protection).
Not yet (polish and backlog, not core-functionality gaps):
- Read-only attach (lifts the "session must be detached"
limitation on
felis sessions send / capture / search).
The full list of open work lives in
docs/backlog.md.
Design
felis is not defined by how many features it has — it redraws the line of what a terminal is responsible for, pulling inward what the conventional stack pushed out and pushing outward what a terminal conventionally pulls in:
- Pulled inward. Session ownership and persistence — felis's own daemon owns the PTY, grid, and scrollback, so the shell outlives the window — and full-fidelity rendering, because nothing sits between the shell and the screen to flatten Kitty graphics or text sizing.
- Pushed outward. Layout (tabs, panes, splits) to the window manager; extension to external processes over a versioned IPC, never an embedded Lua / Python / Wasm host; intent-guessing (URL detection, content sniffing, smart quoting) nowhere at all — the user's bytes stay the user's bytes.
Inside that boundary felis is maximalist — render everything, fast. At the boundary it is strict — delete before adding. No second system lives inside the terminal.
The full rationale is in docs/explanation/vision.md; the eight
non-negotiable principles, each paired with a concrete violation test, live
in docs/explanation/principles.md.
Building from source
felis is a prototype; to hack on it, build from cargo. The Nix
flake at the repo root is the single source of truth for the
toolchain (nightly Rust, the runtime libs wgpu / winit need, the
formatter, and the pre-commit hook).
# Drop into the dev shell — installs the pre-commit hook automatically.
nix develop
# Run the daemon, then the client.
cargo run -p felis-daemon -- serve &
cargo run -p felis-client
# List detached sessions and reattach by id prefix.
cargo run -p felis-cli -- sessions list
cargo run -p felis-cli -- attach <id-prefix>
# Cross-host attach over SSH.
cargo run -p felis-client -- --host user@remote
Documentation
All documentation — the end-user manual and the design docs — is
published at https://felis-docs.pages.dev and lives in this
repository under docs/ — next to the code it documents,
so it cannot rot; the
felis-docs
repository only holds the site machinery and vendors the content via
a submodule.
For contributors, docs/README.md is the entry
point. The design docs are the source of truth for every feature
decision — each records the rationale and the rejected alternatives
alongside the current design.
License
Apache-2.0. See LICENSE.