No description
  • Rust 95.9%
  • Nix 4.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
natsukium 8171adad95
All checks were successful
ci / cargo fmt / clippy / test / deny (push) Successful in 21s
docs: follow felis-web-core's split and rename
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
2026-08-15 01:21:39 +09:00
.claude chore(skills): add collaborator notes, doc skills, and the fcast skill 2026-07-17 14:51:24 +09:00
.forgejo/workflows ci(fcast): run fmt/clippy/test/deny on push and PR 2026-06-21 10:24:41 +09:00
docs docs(fcast): split the spec into a reference/explanation tree 2026-07-17 14:50:56 +09:00
examples docs: follow felis-web-core's split and rename 2026-08-15 01:21:39 +09:00
player docs: follow felis-web-core's split and rename 2026-08-15 01:21:39 +09:00
skills/fcast chore(skills): add collaborator notes, doc skills, and the fcast skill 2026-07-17 14:51:24 +09:00
src docs: follow felis-web-core's split and rename 2026-08-15 01:21:39 +09:00
tests docs: follow felis-web-core's split and rename 2026-08-15 01:21:39 +09:00
.gitignore feat: felis-fcast — .fcast format, recorder, and replay design 2026-06-20 18:27:33 +09:00
Cargo.lock chore(fcast): pin felis deps to an explicit rev and follow grid style-interning 2026-07-17 10:23:34 +09:00
Cargo.toml refactor(fcast): drop the unused postcard feature from felis-protocol 2026-07-17 14:51:48 +09:00
CLAUDE.md chore(skills): add collaborator notes, doc skills, and the fcast skill 2026-07-17 14:51:24 +09:00
clippy.toml chore(fcast): adopt felis core's lint policy and clear it under -D warnings 2026-06-21 10:24:32 +09:00
deny.toml chore(fcast): adopt felis core's lint policy and clear it under -D warnings 2026-06-21 10:24:32 +09:00
flake.lock feat: felis-fcast — .fcast format, recorder, and replay design 2026-06-20 18:27:33 +09:00
flake.nix docs: follow felis-web-core's split and rename 2026-08-15 01:21:39 +09:00
LICENSE chore(fcast): declare the MSRV and ship the Apache-2.0 LICENSE file 2026-06-21 22:50:11 +09:00
README.md docs: follow felis-web-core's split and rename 2026-08-15 01:21:39 +09:00
rustfmt.toml chore(fcast): adopt felis core's lint policy and clear it under -D warnings 2026-06-21 10:24:32 +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-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 (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"

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.