- Rust 99.3%
- Shell 0.3%
- Nix 0.2%
- WGSL 0.2%
`felis-grid/src/lib.rs` grew to 11.9k lines, well past the point where new contributors can hold the whole file in their head. The two cleanest seams sit at the top of the file: `Damage` (bitset damage tracker, ~150 lines) and `Scrollback` (slab-backed history ring, ~150 lines). Both are self-contained — they own their storage, their public surface is small (mark / clear / iter / push / row), and nothing outside their own impl reads their private fields. Move both into their own files (`src/damage.rs`, `src/scrollback.rs`) and re-export through `lib.rs`. The only access-level change is `Damage::resize`, which goes from private to `pub(crate)` so `Grid::resize` in `lib.rs` can still call it across the module boundary. The public API is unchanged: `pub use damage::Damage` and `pub use scrollback::Scrollback` keep the existing import paths (`felis_grid::Damage`, `felis_grid::Scrollback`) working. lib.rs drops from 11883 to 11546 lines (-337). No behaviour change; the full felis-grid test suite (299 unit + 100 integration tests) passes, and `damage_merge` / `scrollback_push` benches show no regression beyond criterion's run-to-run noise. Why these two first: both have one obvious owner (the Grid struct embeds them as fields), no shared mutable state with the rest of lib.rs, and no cross-impl trait surface. The harder extractions — `impl Sink for Grid` (~1100 lines) and the OSC parser helpers (~600 lines) — touch private Grid state and are deferred. |
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| crates | ||
| docs | ||
| fuzz | ||
| scripts | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| clippy.toml | ||
| deny.toml | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
| rustfmt.toml | ||
felis
A GPU-accelerated terminal emulator 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.
Features
- Persistent sessions. Closing the client window does not kill
the shell.
felis --list-sessionsenumerates detached sessions;felis --attach <id-prefix>rehydrates one. - Cross-host attach over SSH.
felis --host user@remoteopens a window backed by a remote daemon — plain SSH stdio, no relay and no extra auth layer. - 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.
- Real text shaping. Per-grapheme font fallback (CJK + colour
emoji + Nerd Font), 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. On Linux, drag auto-copies
to PRIMARY and middle-click pastes it back. The wheel scrolls
scrollback on the primary screen and pages
less/man/vimon the alternate screen. - Hot config reload. Edit the config and save; theme and font changes apply without restarting the daemon or losing scrollback.
Design
A few decisions shape what felis is and what it deliberately is not.
One shell per window
The window manager already knows how to tile and stack windows; adding tabs and splits inside the terminal would be a worse, non-portable copy of that. felis declines to compete with the window manager.
State outlives the window
Closing a terminal window should not destroy your work. By separating the daemon (state) from the client (presentation), felis makes "the shell continues running while the window does not" a property of the terminal itself, not of an extra multiplexer layered on top.
IPC, not embedded scripts
There is no Lua, no Python, no Wasm host inside felis. The versioned IPC carries every action a client can ask the daemon to perform, so automation gets to be a language choice — yours, not ours.
Limited smartness
URL detection, content sniffing, and smart quoting are context-dependent: they help in most cases and surprise the user in the rest. felis treats the user's bytes as the user's bytes, and provides off-switches when it must choose for them.
Compatibility, where it holds up
felis follows xterm and kitty conventions for selection, clipboard, and scrolling because decades of muscle memory have built around them. Where conventions disagree or self-consistency demands otherwise, self-consistency wins.
The full list of principles, each paired with a concrete violation
test, lives in docs/principles.md.
Quick start
felis is a prototype and runs from cargo today. The Nix flake
at the repo root provides 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-client -- --list-sessions
cargo run -p felis-client -- --attach <id-prefix>
# Cross-host attach over SSH.
cargo run -p felis-client -- --host user@remote
Configuration lives at the platform-native path —
~/.config/felis/config.toml on Linux,
~/Library/Application Support/felis/config.toml on macOS,
%APPDATA%\felis\config.toml on Windows. See
docs/config.md for the schema.
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 --list-sessions, reattach withfelis --attach <id>. - Cross-host attach.
felis --host user@remoteover plain SSH stdio. - IME on macOS / Wayland / Windows / X11; per-grapheme font fallback covering CJK, 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:
- On-screen rendering of Kitty graphics (
yazi,mdcat,presentermpreviews). The protocol is parsed end-to-end and the daemon stores images, but the renderer's image atlas is still under construction. - Scaled text from Kitty text-sizing escapes. Parsed and tracked, but the renderer's scaled-glyph path is pending.
- OpenType font features and programming-font ligatures.
- Search over the scrollback.
- VT parser SIMD fast-path.
Per-task accounting lives in
docs/implementation-plan.md.
Documentation
docs/README.md is the entry point. The design
docs are the source of truth for every feature decision;
architectural decisions live in
docs/decisions/ as numbered ADRs.
License
Apache-2.0. See LICENSE.