No description
  • Rust 96.2%
  • Nix 3.8%
Find a file
natsukium 5c6457ca81
docs(format): record compression and future seek as out-of-envelope concerns
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
2026-06-21 01:46:42 +09:00
examples feat(fcast): land the .fcast parser core, harden record, fix doc drift 2026-06-21 00:13:16 +09:00
player feat(recorder): record with SCROLL_OP so replays can scroll back 2026-06-20 23:40:41 +09:00
src feat(serve): native pseudo-daemon that replays a .fcast over the wire 2026-06-21 00:57:07 +09:00
tests feat(serve): native pseudo-daemon that replays a .fcast over the wire 2026-06-21 00:57:07 +09:00
.gitignore feat: felis-fcast — .fcast format, recorder, and replay design 2026-06-20 18:27:33 +09:00
Cargo.lock feat(serve): native pseudo-daemon that replays a .fcast over the wire 2026-06-21 00:57:07 +09:00
Cargo.toml feat(serve): native pseudo-daemon that replays a .fcast over the wire 2026-06-21 00:57:07 +09:00
flake.lock feat: felis-fcast — .fcast format, recorder, and replay design 2026-06-20 18:27:33 +09:00
flake.nix feat: felis-fcast — .fcast format, recorder, and replay design 2026-06-20 18:27:33 +09:00
FORMAT.md docs(format): record compression and future seek as out-of-envelope concerns 2026-06-21 01:46:42 +09:00
README.md feat(serve): native pseudo-daemon that replays a .fcast over the wire 2026-06-21 00:57:07 +09:00

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 (parseCast) 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.