No description
  • Rust 99.3%
  • Shell 0.3%
  • Nix 0.2%
  • WGSL 0.2%
Find a file
natsukium 33a731e93e
Some checks failed
fuzz / cargo fuzz smoke (per target) (push) Failing after 4s
fuzz / cargo fuzz nightly long-run (push) Has been skipped
pr / nix flake check (push) Failing after 3s
pr / cargo build / clippy / test / deny (push) Failing after 3s
refactor(grid): extract Damage and Scrollback into submodules
`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.
2026-05-16 11:16:09 +09:00
.claude Stop tracking the per-session Claude scheduled-tasks lock 2026-05-10 13:20:28 +09:00
.forgejo/workflows ci(bench): Criterion perf-regression gate for end_to_end throughput (P12) 2026-05-13 18:33:44 +09:00
crates refactor(grid): extract Damage and Scrollback into submodules 2026-05-16 11:16:09 +09:00
docs perf: bulk-print ASCII runs through Sink::print_str 2026-05-16 11:11:09 +09:00
fuzz test(fuzz): curated seed corpus for the five cargo-fuzz targets (A7) 2026-05-13 18:02:43 +09:00
scripts ci(bench): Criterion perf-regression gate for end_to_end throughput (P12) 2026-05-13 18:33:44 +09:00
.gitignore M2: IPC fuzz target + rehydration snapshot test 2026-05-10 15:50:14 +09:00
Cargo.lock feat(client): native Wayland clipboard via wlr_data_control_v1 2026-05-14 00:16:25 +09:00
Cargo.toml feat(daemon): decode Kitty graphics payloads into ImageEntry 2026-05-12 17:15:48 +09:00
CLAUDE.md docs: align commit-convention notes with actual prefixed practice 2026-05-11 17:32:46 +09:00
clippy.toml Bootstrap virtual Cargo workspace and CI pipeline 2026-05-10 12:42:00 +09:00
deny.toml Bootstrap virtual Cargo workspace and CI pipeline 2026-05-10 12:42:00 +09:00
flake.lock ADR 0007: Nix flake as the canonical development environment 2026-05-10 11:00:38 +09:00
flake.nix test(pty): esctest2 conformance harness with PASS_BASELINE ratchet 2026-05-15 01:08:13 +09:00
LICENSE Lock in license, filesystem layout, OSC 66, and Sixel as permanent reject 2026-05-10 02:19:06 +09:00
README.md docs: streamline README — concise overview with design rationale deferred to docs/ 2026-05-12 10:30:48 +09:00
rustfmt.toml ADR 0007: Nix flake as the canonical development environment 2026-05-10 11:00:38 +09:00

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-sessions enumerates detached sessions; felis --attach <id-prefix> rehydrates one.
  • Cross-host attach over SSH. felis --host user@remote opens 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 / vim on 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 with felis --attach <id>.
  • Cross-host attach. felis --host user@remote over 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, presenterm previews). 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.