RecordDeck 1.0.0 The edit pass for a terminal recording Techlosoft - Screen Studio Lite WHAT IT IS ========== You recorded a nine minute session. The demo is ninety seconds. RecordDeck is the timeline editor that gets you from one to the other without re-recording: it cuts ranges out, trims the head and tail, speeds up the boring parts, freezes on the good ones, and collapses dead air. It reads a SessionForge cast, applies an edit decision list, and writes a NEW cast. The original file is opened read only and is never touched. The rest of the line covers the other jobs. SessionForge records the session. ScreenFlow renders a cast to an animated GIF. CaptureStudio pulls still frames. RecordDeck only edits the timeline - it does not record, render or capture anything, and it deliberately shares no engine with the tools that do. Two things make it more than a script that rewrites timestamps: 1. All time is exact. Every timestamp and every duration is carried as an exact rational, never as a float. A chain of cuts and x3 speed changes lands on the duration arithmetic says it should, to the microsecond, and the tests assert that with no epsilon. 2. Cutting does not corrupt the picture. When a cut removes output, the removed bytes are replayed through a small terminal-state model and a synthetic escape sequence is inserted at the cut point, so the surviving recording still renders with the cursor and the colours it expects. INSTALL ======= Pre-built binaries are in dist/. There is nothing to install - copy the one for your platform anywhere on your PATH and run it. dist/recorddeck-linux-amd64 Linux, x86-64 dist/recorddeck-darwin-arm64 macOS, Apple Silicon dist/recorddeck-darwin-amd64 macOS, Intel dist/recorddeck-windows-amd64.exe Windows, x86-64 On macOS and Linux you may need to mark it executable: chmod +x recorddeck-linux-amd64 To build from source you need Go 1.24 or newer. Standard library only, no third-party modules, no cgo, so no network access is required: go build -o recorddeck . go test ./... COMMANDS ======== recorddeck info [--top N] [--json] recorddeck preview [--edl ] [--op ""]... [--width N] [--json] recorddeck apply --out [--edl ] [--op ""]... [--force] [--json] recorddeck help | -h | --help | version info What is in this recording: duration, event count, byte volume, the longest dead pauses with their timestamps, how much a `gap --max` of various sizes would save, and a ready-to-paste EDL an editor would plausibly start from. Reads the file and writes nothing. preview Resolves an EDL against the cast and prints the resulting timeline as a text ruler, with the exact resulting duration and every synthetic prelude the edit would insert. Writes nothing. This is the command to run while you are still deciding. apply Writes a NEW cast with the edit applied. The input is opened read only. An existing --out is refused unless --force is given, and --out is refused outright if it names the input file, with or without --force. FLAGS --edl An edit decision list. JSON or the compact command form; the format is detected from the first non-space character. --op "" One compact-form operation, given inline. Repeatable. Applied after everything in --edl. --out Output cast for apply. Required. Never the input. --force Overwrite an existing --out. Off by default. --top N How many pauses info lists. Default 5. --width N Ruler width in columns for preview. Default 64. --json Machine-readable output. Accepted by info, preview and apply. Short forms -e, -o and -w are accepted for --edl, --out and --width. Flags may appear before or after positional arguments; either order works. THE EDIT DECISION LIST ====================== Five operations. All times refer to the SOURCE timeline - the timestamps in the input file - so adding an operation at the top of an EDL never forces you to recompute the ones below it. cut A-B Remove the source range [A,B) and close the gap. trim --start A --end B Keep only this window, rebased so A becomes 0. speed A-B xN Play the range N times faster. N is an exact rational, so x3 is one third, not 0.33333. hold AT --for D Freeze the picture at AT for D. Nothing is printed during a hold; the next event is simply pushed D later. gap --max D Collapse EVERY dead pause longer than D down to exactly D. The single most useful operation on a real recording. COMPACT FORM - one operation per line, # starts a comment: # nine minutes into ninety seconds trim --start 0:12 --end 8:40 gap --max 1.2 cut 2:05-3:40 speed 4:00-6:30 x6 hold 7:10 --for 2 JSON FORM - the same thing, for tools that generate EDLs: {"version":1,"ops":[ {"op":"trim","start":"0:12","end":"8:40"}, {"op":"gap","max":"1.2"}, {"op":"cut","start":"2:05","end":"3:40"}, {"op":"speed","start":"4:00","end":"6:30","factor":"6"}, {"op":"hold","at":"7:10","for":"2"} ]} Times may be given as any of: 12 seconds 12.5 seconds 12.5s seconds 500ms milliseconds 250us microseconds 2m minutes 1:30 mm:ss 1:30.250 mm:ss.mmm 0:01:05 hh:mm:ss 1/3 an exact fraction of a second Ranges may be written A-B or A..B. Speed factors may be written 4, x4, X4, 4x, *4, x4, 1.5 or 3/2. RULES, ENFORCED - cut and speed ranges must be listed in ascending source order. - cut and speed ranges must not overlap each other. - A range must start strictly before it ends. - At most one trim and at most one gap per EDL. - A hold must not land inside a cut: there is no frame left to freeze. - A cut or speed range entirely outside the trim window is an error, not a silent no-op. Every one of these is a clear error naming both offending operations, not a guess about what you meant. ORDER OF APPLICATION ==================== The pipeline is fixed and documented, so the same EDL always means the same thing regardless of the order you happened to type the lines in: 1. trim choose the retained window, rebase it to zero 2. cut remove ranges, closing the gaps speed scale ranges by 1/N 3. gap collapse whatever dead pauses remain 4. hold insert the freezes gap runs before hold on purpose: a hold is a pause you asked for, and collapsing it again would be absurd. Everything a hold inserts survives gap collapsing. Steps 1 and 2 together form one piecewise-linear map from source time to output time. Steps 3 and 4 are two step functions applied on top of it. All three are pure functions of the source time, which is why every event, every marker and the end of the timeline are computed through exactly the same arithmetic and cannot drift apart from one another. EXACT TIMING ============ This is the part most tools get wrong, so it is worth being specific. Every timestamp in the input is parsed from its literal decimal text into an exact rational. It is never passed through float64. Cuts subtract exact rationals; speed changes multiply by an exact rational reciprocal; holds add exact rationals. The internal timeline is therefore exact at every step, however long the chain of operations. There is exactly ONE quantisation in the whole program: when a timestamp is written to a file it is rounded to the nearest microsecond, half away from zero, because the cast format stores six decimal places. That rounding is applied once per timestamp, to the exactly-computed value - never to a running total - so the error on any timestamp is at most half a microsecond and CANNOT ACCUMULATE. A recording with a hundred thousand events is as accurate at the end as at the start. Where an edit produces a duration that is not a whole number of microseconds - `speed 0-1 x3` gives one third of a second - preview and apply say so: note : some timestamps are not a whole number of microseconds and will be rounded once, to the nearest microsecond, on write and both commands print the unrounded value as an exact fraction: exact value : 4059847/750000 seconds Two invariants are checked on EVERY apply, not only in the tests, and a failure refuses the edit rather than writing a bad file: - Timestamps are non-negative and never go backwards. - Every inter-event interval equals an independently computed integral of the scale function over the corresponding source interval, minus any collapsed dead air, plus any inserted hold. Exactly. No tolerance. TERMINAL STATE MODEL ==================== When a cut removes output, the bytes it removes may have moved the cursor or changed the colour. Delete them naively and everything after the cut renders in the wrong place, or in the wrong colour, forever. RecordDeck replays the removed bytes through a small terminal-state model and emits a synthetic PRELUDE escape sequence at the cut point that puts the terminal into the state the surviving recording expects. The prelude is a real event in the output file, on stream "o", sharing a timestamp with the event it precedes, so it consumes no time and cannot break monotonicity. It is emitted only when something actually changed. Trimming the head of a recording gets the same treatment. at 0:02.304 (source 0:12.420) ESC[5;1H cursor 4,1 -> 5,1 WHAT THE MODEL TRACKS - Cursor row and column. - SGR state: bold, dim, italic, underline, blink, reverse, hidden, strikethrough, foreground colour and background colour. That is all. There is no screen buffer and no character content. SEQUENCES IT UNDERSTANDS, EXHAUSTIVELY Printable text one cell per rune, wrapping at the right margin. LF (\n) down one row AND to column 0. LF is modelled as CR+LF because that is what the tty driver's ONLCR does to it on the way to the screen; a cast recorded from a PTY that already contains CRLF lands in the same place. CR (\r) column 0. BS (\b) one column left, floor at 0. TAB (\t) next multiple of 8 columns. Other C0, DEL consumed, no modelled effect. CSI n A / B / C / D cursor up / down / forward / back CSI n E / F next line / previous line (column 0) CSI n G, CSI n ` cursor horizontal absolute CSI n d line position absolute CSI r ; c H, CSI r ; c f cursor position CSI s, CSI u save / restore cursor ESC 7, ESC 8 DECSC / DECRC (cursor and SGR) ESC D, ESC M, ESC E index, reverse index, next line ESC c full reset CSI ... m SGR: 0 reset; 1 bold; 2 dim; 3 italic; 4 underline; 5/6 blink; 7 reverse; 8 hidden; 9 strike; 21/22 bold+dim off; 23, 24, 25, 27, 28, 29 the matching offs; 30-37 and 90-97 foreground; 39 default foreground; 40-47 and 100-107 background; 49 default background; 38;5;n and 48;5;n 256-colour; 38;2;r;g;b and 48;2;r;g;b truecolour. CSI J, K, L, M, P, X, S, T, @, r recognised and consumed. They change screen CONTENT, which is not modelled, and they do not move the cursor, so they need no prelude. CSI ? ... , CSI > ... , CSI = ... private/DEC modes: consumed, not modelled. OSC (ESC ]), DCS, PM, APC consumed whole, to BEL or ST. Charset designation (ESC ( etc.) consumed, ignored. Anything else consumed safely. An unrecognised sequence can never desynchronise the parser and turn escape bytes into printable cells. DELIBERATE APPROXIMATIONS - One cell per rune. East Asian wide characters, zero-width joiners and combining marks are counted as one column each, so a line full of CJK text tracks the cursor column optimistically. - Wrapping is immediate, not deferred. A real terminal leaves the cursor in the last column until the next character arrives; this model wraps as soon as the margin is crossed. The two differ only in the single-cell case of a cut landing exactly at the right margin. - The row clamps at the last line rather than scrolling, which is what a real terminal's cursor does; but because there is no screen buffer, RecordDeck cannot know that scrolling moved the surviving text upward. - Scroll regions (DECSTBM), the alternate screen buffer, origin mode and character sets are recognised and ignored, not modelled. If you cut in the middle of a full-screen TUI redraw, the prelude will restore the cursor and the colours but it cannot restore the screen contents that the removed frames drew. Cut at natural boundaries. QUICK START =========== # what is in here, and where would an editor cut? recorddeck info session.jsonl # try an edit without writing anything recorddeck preview session.jsonl --op "gap --max 1.5" # commit it to a new file recorddeck apply session.jsonl --op "gap --max 1.5" --out tight.jsonl # a real edit, from a file recorddeck apply session.jsonl --edl demo.edl --out demo.jsonl # watch the result with the recorder that made it sessionforge replay demo.jsonl EXAMPLE OUTPUT ============== $ recorddeck preview deploy.jsonl --edl demo.edl RecordDeck preview file : deploy.jsonl edl : demo.edl (5 operation(s)) source : 0:00.000 -> 0:16.332 17 events edited : 0:00.000 -> 0:05.413 11 events (7 removed, 1 prelude(s) inserted) source |_##.......................#xxxxxx...............~~~~H.........#.| 0:16.332 edited |#...#...#........#.........###HHHHHHHHHHHHHHHHHHH.........#....#| 0:05.413 legend # output . idle ~ sped up x cut H hold _ trimmed away OPERATIONS trim 0:00.002 -> 0:16.333 keeps only this window gap --max 0.800000s collapses every longer pause cut 0:06.909 -> 0:08.419 removes 1.510000s speed 0:12.420 -> 0:13.325 x3 (0.905000s becomes 0.301667s) hold 0:13.325 freezes for 1.500000s SYNTHETIC PRELUDES AT CUT POINTS at 0:02.304 (source 0:12.420) ESC[5;1H cursor 4,1 -> 5,1 RESULT duration : 5.413129s (was 16.332892s) exact value : 4059847/750000 seconds change : 33.1% of the original The two rulers are the point of preview. The top one is the source timeline with the operations drawn on it; the bottom one is what you will actually watch. Each is drawn over its OWN duration, so they are not to a common scale - the edited ruler being just as wide is what makes the compression legible. HOW THE TIMELINE IS COMPUTED ============================ The retained window is chopped at every cut and speed boundary into segments, each with a constant scale factor: keep scale 1 cut scale 0 speed scale 1/N The output time of any source instant t is the integral of the scale function from the start of the window to t: raw(t) = sum over segments of (overlap of [window_start, t] with segment) * segment scale Dead-pause collapsing is applied on top as a step function of raw output time: each pause longer than --max contributes (pause - max) to a running shift, and everything at or after that point moves earlier by the accumulated shift. The end of the timeline is treated as one final point, so trailing dead air is capped exactly like any other pause. Holds are a second step function, of source time: an event is pushed later by the total duration of every hold strictly before it. A hold at exactly time t freezes AFTER the frame at t, so the event at t itself does not move. final(t) = raw(t) - gapShift(raw(t)) + holdShift(t) Because raw, gapShift and holdShift are all monotonic non-decreasing, so is final, which is why the edited timeline cannot go backwards no matter how the operations are combined. An event inside a cut range is removed. An event exactly at the cut's start is removed; an event exactly at its end is kept. Ranges are half open, [A,B). The reported DURATION is the length of the whole edited timeline, including any trailing dead air after the last event. That is what the footer of the output file records, and it is what SessionForge's replay takes to complete. EDGE CASES, HANDLED EXPLICITLY ============================== empty EDL The output is byte-for-byte identical to the input, including the footer. Nothing is "helpfully" reformatted. cast with no footer A recording that was cut short stays footerless; no footer is invented. cast with no events Handled. Duration 0, no division by zero. gap --max 0 Legal. Every pause disappears and the whole recording plays as fast as the player can write. speed with N < 1 Legal, and slows that range down. x0.5 doubles it. hold at 0 Legal. The whole recording is pushed later. cut reaching past the end of the recording Clipped to the timeline. Cutting from 5 to 999 on a 20 second recording removes everything after 5. trim --start with no --end Trims the head only; the end stays where it was. non-UTF-8 payloads Preserved exactly. A chunk stored base64 in the input is written back base64, byte for byte. output == input Refused, with or without --force. WHAT IT DOES - AND WHAT IT DOES NOT =================================== IMPLEMENTED, AND WORKING TODAY - Reading and writing the SessionForge cast format, losslessly. Read a cast and write it back and you get the same bytes, including the header's unknown keys, base64 payloads, the footer, and every escape. - cut, trim, speed, hold and gap, in any combination. - Exact rational arithmetic end to end, with a single documented microsecond quantisation at write time that cannot accumulate. - Runtime verification of monotonicity and of the duration arithmetic on every apply, not only under test. - Synthetic cursor and SGR preludes at cut points, from a real ANSI state tracker over the documented subset above. - EDLs in JSON or a compact command form, plus inline --op operations. - Validation that rejects overlapping, reversed and out-of-order operations with an error naming both offenders. - info with the longest pauses, gap-collapse projections and a suggested EDL. - preview with a two-line text ruler of the source and edited timelines. - --json on info, preview and apply. - Refusal to overwrite an existing output without --force, using an O_EXCL create so the refusal is not a racy stat, and outright refusal to write over the input. SCOPE / WHAT THIS DOES NOT DO ============================= Read this part. It is the honest boundary of the tool. - NO AUDIO. There is no audio track, no narration, no music, no ducking, no sync. A cast file has no audio in it and RecordDeck adds none. - NO VIDEO FORMATS. It does not read or write MP4, WebM, MOV, GIF, APNG, SVG or anything else. Input is a SessionForge cast, output is a SessionForge cast. If you want a GIF, edit here and render with ScreenFlow. - NO SCREEN CAPTURE. It never records anything. It cannot see your screen, your terminal, your webcam or your microphone. Use SessionForge to record. - NO TERMINAL EMULATION. The state model covers the documented subset of ANSI/SGR above - cursor position and character attributes - and nothing else. It is NOT a VT100 emulator. There is no screen buffer, no scrollback, no scroll region, no alternate screen, no line-wrap history, no character sets, no mouse or bracketed-paste modes. It cannot tell you what the screen looked like at any instant, only where the cursor was and what colour was in force. - NO GUI TIMELINE. There is no window, no waveform, no scrubber, no drag handles, no keyframes, no undo stack. The interface is an EDL text file and a text ruler in your terminal. - NO PLAYBACK. RecordDeck never writes to your terminal in real time. `sessionforge replay` does that. - NO CONTENT EDITING. It edits WHEN things happened, never WHAT was printed. It will not redact a password, rewrite a hostname, fix a typo, re-wrap output or change a single byte of any surviving event. The only bytes it ever adds are the synthetic preludes at cut points, and preview shows you each one before you commit. - NO AUTOMATIC EDITING. It does not detect "interesting" moments, generate an EDL for you, or apply a house style. info SUGGESTS a starting EDL from the pause structure; you decide. - NO INPUT STREAM. SessionForge records output only ("o" and "e"), so there is no keystroke track to edit and no "i" events to handle. - NO MULTI-CAST WORK. No concatenation, no splitting into several files, no picture-in-picture, no overlaying two recordings, no transitions, no fades, no titles, no annotations. - NO ASCIINEMA GUARANTEE. The format is close to asciicast v2 and RecordDeck matches SessionForge exactly, but SessionForge itself does not claim asciinema compatibility (separate "e" stream, trailing footer, "b64" fourth field). RecordDeck inherits that caveat unchanged and adds no conversion of its own. NOT IMPLEMENTED - ROADMAP These are absent, not stubbed: - Reordering: moving a range to a different point in the timeline. Every operation today is subtractive or a scale; nothing is ever moved. - Cross-fades and dissolves between cut points, which need a screen model RecordDeck does not have. - Screen-content-aware cuts: reconstructing what the terminal displayed and emitting a full redraw at the cut point instead of a cursor-and-colour prelude. This is the honest fix for cutting inside a TUI redraw, and it needs a real emulator. - Per-event editing: dropping, splitting or merging individual chunks. - A curses timeline UI for scrubbing and marking cut points by eye. - Direct asciicast v2 import and export, verified against the real player. EXIT CODES ========== 0 Success. Explicit help exits 0. 1 Bad invocation (unknown command, missing required flag, invalid EDL, overlapping operations) or an I/O error (missing input, malformed cast, output already exists without --force, output is the input). Every error message goes to stderr and starts with "recorddeck: ". Malformed cast files report the offending line number; malformed EDLs report the offending line and the text of the operation. CAST FILE FORMAT ================ RecordDeck reads and writes the SessionForge format unchanged. JSON Lines, UTF-8. Line 1 is a header object: {"version":1,"command":["./deploy.sh"],"started_at":"2026-08-11T07:02:24Z", "shell":"/bin/bash","width":80,"height":24,"title":"prod deploy"} Every following line is one event array: [elapsed_seconds, "o"|"e", "chunk of text"] [0.002058, "o", "$ deploy --env prod\n"] [0.403030, "o", "checking credentials ... "] A chunk that is not valid UTF-8 is stored base64 with a fourth field marking it: [0.008778, "o", "AAECAwQF", "b64"] The last line is a footer object holding the outcome: {"exit_code":0,"duration":16.333167335} WHAT RECORDDECK PRESERVES header written back verbatim, byte for byte, including any keys this version does not know about. RecordDeck adds no provenance key of its own, precisely so that an unedited file round trips. events payload bytes, stream tag and base64-ness are preserved exactly. Only the timestamp is ever rewritten. footer preserved verbatim for an identity edit. For a real edit, only the "duration" value is replaced, with the new timeline length; exit_code, signal, interrupted and any other keys survive. WHAT IT CANONICALISES Blank lines are dropped, and timestamps are written with exactly six decimal places, which is what SessionForge itself writes. A cast whose timestamps carry more than six decimals is rounded to the microsecond on write, once. In practice this means: any cast that SessionForge produced round trips byte for byte. A hand-written cast with 1e-3 style timestamps or nine decimal places round trips its CONTENT exactly, with timestamps normalised.