- Rust 95.9%
- Nix 4.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci / cargo fmt / clippy / test / deny (push) Successful in 21s
The web client became `felis-web-component`, with `felis-web-gateway`
carved out of it. Nothing here consumes either — the sibling references
are all prose and the flake comment — so this is a pointer update.
The sample generators are the exception, because their strings are the
demo's *content*. `hello.fcast`'s banner is regenerated to name the new
repo, and both generators grow a width assertion.
That assertion is the real find. `row_cells` pads a short line to COLS
but never clips a long one, so an overlong string ships a `RowDelta`
wider than the dims the header declares — a silently malformed
recording. This banner has now been rewritten by two repo renames and
has been over 48 columns since the first of them, which is how long a
generated fixture can be wrong without anyone noticing. It fails loudly
at generation time now, and the banner is short enough to fit.
Regenerate the recordings this way (they live in the sibling repo):
cargo run -p felis-fcast --example gen_hello_fcast \
> ../felis-web-component/web/public/hello.fcast
fmt, clippy -D warnings and 21 tests green.
Assisted-by: Claude Code Opus 5
|
||
| .claude | ||
| .forgejo/workflows | ||
| docs | ||
| examples | ||
| player | ||
| skills/fcast | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| clippy.toml | ||
| deny.toml | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
| rustfmt.toml | ||
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-component); this repo owns the format, a capture
tool, and a replay source.
What's here
| Path | What | Status |
|---|---|---|
docs/ |
The spec and design record: reference/format.md (the .fcast envelope), reference/pseudo-daemon.md (the serve wire contract), reference/cli.md, and the explanation/ twins. |
✅ |
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-component's one connection (fcastSource in felis-web-component/web/felis-terminal.ts), not an extracted npm package — see docs/explanation/pseudo-daemon.md. |
❌ 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"
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 — it keeps one connection and only the source under
it varies. felis-fcast serve is the deepest form: an OS process
indistinguishable from the daemon. The browser form (fcastSource in
felis-web-component) is the shallower in-bundle source. Both share the format,
not code.
The concept, its three realizations by depth, and why the native replay
lib and the browser package stay out of scope are in
docs/explanation/pseudo-daemon.md;
the exact serve wire contract is in
docs/reference/pseudo-daemon.md.
Build
The Nix flake is the single source of truth for the toolchain (nightly
Rust matching felis, plus node + tsgo for the player). The felis crates
are git dependencies pinned by Cargo.lock — the same arrangement
felis-web-component uses — so this repo is self-contained and needs no sibling
../felis checkout; cargo update follows felis main.
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.