- Rust 96.2%
- Nix 3.8%
felis-web will be CDN-delivered, raising whether a .fcast should itself be compressed. Pin the decision in the spec: compression is a transport/storage concern (HTTP Content-Encoding), never a property of the format, so the envelope and version stay untouched while delivery evolves. Likewise note that seek is gated by keyframe semantics — a mid-session rehydrate burst is already a legal frame — so both seek and segmented delivery can arrive later without an envelope bump. Recorded now, while there are no external consumers, to keep the format from foreclosing these paths. Assisted-by: Claude Code Opus 4.8 |
||
|---|---|---|
| examples | ||
| player | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| FORMAT.md | ||
| README.md | ||
felis-fcast
The .fcast session-cast format for felis — a recording of
one felis session (the daemon→client frame stream, timestamped) plus the
tools that produce and replay it. Think asciinema's .cast, but the
frames are real felis-protocol wire
messages, so a player replays through felis's actual grid/renderer rather
than re-parsing a terminal.
This is a downstream, producer-neutral piece of the felis ecosystem: felis
core stays unchanged — recording is a client-core attach that tees
frames, not a felis-core feature. The renderer lives elsewhere
(felis-web); this repo owns the format, a capture
tool, and a replay source.
What's here
| Path | What | Status |
|---|---|---|
FORMAT.md |
The .fcast envelope spec (NDJSON header + [t, kind, msg] frames). Producer-neutral; the frame msg schema is owned by felis-protocol, not here. |
✅ |
src/main.rs (felis-fcast record) |
A native client-core attach that records a daemon→client frame stream to a .fcast. The reference producer. |
✅ |
examples/gen_{hello,scroll}_fcast.rs |
Hand-authored sample .fcast generators (exercise felis-grid's JSON row encoder; scroll also drives the SCROLL_OP scrollback path). |
✅ |
player/ |
Placeholder. The browser replay belongs to the renderer, not here: it is a frame source behind felis-web's one connection (fcastSource in felis-web/web/felis-terminal.ts), not an extracted npm package — see "The pseudo-daemon" below. |
❌ not a felis-fcast deliverable |
src/lib.rs (felis-fcast lib) |
The Rust replay core: the .fcast parser (parse → Cast) and the serve pseudo-daemon (serve::run) built on it. |
✅ |
felis-fcast serve |
The native pseudo-daemon: read a .fcast and speak the daemon wire protocol on a socket, so any existing native client (cli/tui) attaches and renders it unmodified. The deepest form of the pseudo-daemon. |
✅ |
The "pseudo-daemon": an adapter to the daemon-facing protocol
A pseudo-daemon is an adapter from a .fcast to a daemon-facing
protocol, so a client replays through its normal daemon path with no
replay-specific code. A client keeps one connection; only the
transport/source under it varies:
┌─ live : gateway / socket → a real daemon
one connection ─┤
└─ replay : a .fcast pseudo-daemon — recorded frames,
paced by their timestamps
The replay source emits frames in the daemon's frame-stream shape, so the renderer can't tell a recording from a live session. The same idea has a few realizations, differing only in how deep the impersonation goes:
| realization | seam it speaks | client sees | lives in |
|---|---|---|---|
browser fcastSource |
in-memory Frame stream |
a replay source under its one connection | felis-web |
| native replay lib | a Rust frame source / Connection |
a replay source under its one connection | src/lib.rs (deferred — serve covers native) |
felis-fcast serve |
the raw daemon wire protocol on a socket | a real daemon (uses its live path) | this repo ✅ |
serve is the deepest form — an OS process indistinguishable from the
daemon, so the client needs no replay code at all. The browser can't be a
socket process (static-file demos have no server), so its form is an
in-bundle source under felis-web's single connection — logically the same,
just shallower. Pacing — the one time-bound piece — is the thin per-runtime
edge (JS event loop vs tokio). The shared contract under all of them is
FORMAT.md.
Build
The Nix flake is the single source of truth for the toolchain (nightly
Rust matching felis, plus node + tsgo for the player). It depends on the
sibling ../felis checkout via path dependencies — the same arrangement
felis-web uses. When felis is published these become git dependencies.
nix develop
# record against any running daemon (lifecycle is the caller's job):
cargo run -p felis-fcast -- record --socket /path/to/daemon.sock --out demo.fcast \
--command 'printf "hello\n"; uname -sr'
# or author a sample by hand:
cargo run -p felis-fcast --example gen_hello_fcast > hello.fcast
cargo run -p felis-fcast --example gen_scroll_fcast > scroll.fcast
# run the unit + integration tests (parser, writer round-trip, and a real
# felis client attaching to `serve` — no external daemon needed):
cargo test
Replay a recording (serve)
serve is the native pseudo-daemon: it reads a .fcast and speaks the
daemon's wire protocol on a socket, so an unmodified felis client renders the
recording through its normal attach path.
nix develop
# bind a socket and serve the recording (one session, paced off its timestamps):
cargo run -p felis-fcast -- serve --socket /tmp/replay.sock --in scroll.fcast
# then, from a felis client pointed at that socket, attach to session 1:
felis --socket /tmp/replay.sock sessions attach 1
--speed F scales playback (e.g. --speed 2 for 2×); --session-id N sets
the advertised session id. After the last frame serve holds the connection
open showing the final screen — like attaching to an idle live session — until
the client detaches.