package

github.com/plambert/scroll.cr

1.2.1 / published Sep 15, 2026 / repository

scroll

Copy STDIN to STDOUT unchanged while showing the last N lines of the stream in a live, in-place display on STDERR. It is a pipeline filter — like an interactive tail, but the stream keeps flowing through to the next command.

long-running-build | scroll -20 > build.log

scroll showing the tail of a build while the output goes to a file

Two guarantees drive the design:

  • The STDOUT copy is never slowed by the display. STDOUT is written on a tight path that hands buffers to a separate render fiber. When the terminal can't keep up, the display drops intermediate states; the STDOUT copy runs at full speed.
  • The display never shows non-contiguous output. Each frame is a contiguous run of the most recent complete lines. When lines are skipped (because the display fell behind), the window resets to the newest contiguous segment rather than splicing a pre-gap line onto a post-gap one. Control and escape bytes are stripped from the display so a hostile stream cannot corrupt the terminal — the bytes on STDOUT are always untouched.

The display is only drawn when STDERR is a terminal; when STDERR is redirected, scroll is a plain cat (use --force to draw anyway).

Installation

Every version tag publishes binaries for Linux and macOS on the releases page. The Linux builds are static; the macOS builds need nothing but the system libraries.

tar xzf scroll-1.1.1-darwin-aarch64.tar.gz
install scroll-1.1.1-darwin-aarch64/scroll /usr/local/bin/

SHA256SUMS on the release verifies the tarballs.

To build from source instead:

shards build --release
# copies to ./bin/scroll

Usage

Usage: scroll [options]

Show a live tail of a stream on STDERR while copying it to STDOUT

Options:
  --lines, -n COUNT               Lines to show (default: 10)
  --interval MS                   Minimum ms between redraws (default: 40)
  --force                         Draw the display even when STDERR is not a TTY
  --sanitize                      Strip control/escape bytes from the display (--no-sanitize to keep them)
  --final                         On EOF, also show a trailing line that has no newline
  --null                          Consume input without copying it to STDOUT (--no-null forces the copy)

Following a file:
  --file, -f PATH                 Follow PATH like `tail -F`, starting with its last -N lines (implies --null)
  --from-start                    Stream the whole existing file before following
  --poll MS                       Ms between polls while waiting for data (default: 250)
  --pid PID                       Exit cleanly once process PID is gone
  --watch-proc                    Exit once no process holds the file open for writing (Linux only)
  --watch-proc-timeout SEC        Idle seconds before --watch-proc exits (default: 10)

Sorting:
  --sort, -s                      Show the top N of the whole stream, not the last N (STDOUT keeps input order)
  --reverse, -r                   Reverse the order (keep the smallest instead of the largest)
  --sort-by SPEC                  Sort key: a 1-based column number, or a /regex/ (implies --sort)
  --human                         Compare keys as human numbers, e.g. 1k < 2M (implies --sort)

Progress:
  --progress                      Show a progress line under the tail
  --size BYTES                    Expected input size, e.g. 500M (1024-based); implies --progress
  --size-lines COUNT              Expected input size in lines; implies --progress
  --file-size PATH                Take the expected input size from the size of PATH; implies --progress
  --name NAME                     Label to show in the progress line; implies --progress
  --terminal-progress             Drive the terminal's own progress indicator (--no- skips even the query)
  --color WHEN                    Colorize the progress line (-c is --color on, -C is --color off)
  --progress-charset SET          Bar glyphs: unicode draws eighth-of-a-column steps, ascii stays in ASCII

Alternate screen:
  --fullscreen                    Draw on the alternate screen: faster, uses the whole screen, ignores -N
  --leave                         On exit, echo the last -N lines of the alternate screen onto the main one

A bare -N is shorthand for --lines N (e.g. -20 means --lines 20).

tail -f access.log | scroll | grep -v healthcheck > filtered.log

--null consumes the input and writes nothing, for when only the display is wanted. The wc -c below is there to show that STDOUT stayed empty:

scroll with --null, writing nothing to STDOUT

Following a file

--file/-f follows a path the way tail -F does, reading appended data live and reopening across truncation and rotation, instead of reading STDIN. It opens on the last -N lines already in the file, so a follow starts with something on screen rather than waiting for the next write; --from-start streams the whole file instead. File mode implies --null, so nothing is written to STDOUT unless --no-null asks for it.

scroll -f /var/log/app.log --pid "$(pgrep -f app)"

scroll following a file, starting with the lines already in it

--pid ends the run once that process is gone. On Linux, --watch-proc ends it once no process holds the file open for writing, after --watch-proc-timeout idle seconds.

Sorting

--sort/-s shows the top N lines of the whole stream rather than the last N. STDOUT stays a byte-for-byte copy in input order; only the display is reordered.

du -sh * | scroll --null --sort --human

scroll keeping the largest lines of the whole stream

--sort-by picks the key: a 1-based whitespace column, or a /regex/ whose key is the named capture sort, else the first group, else the whole match. --human compares keys as human numbers (1k < 2M, 1024-based), and --reverse/-r keeps the smallest instead of the largest.

Progress

--progress adds a progress line under the tail. With no size given it shows the bytes and lines read and the rate of each:

106K · 20K ln · 15M/s · 2.7M ln/s

Tell it how much input to expect — --size in bytes (an integer or a 1024-based suffixed number such as 500M or 1.1k), --size-lines in lines, or --file-size to take the size from a file — and it adds a percentage, a bar, and an ETA:

 81% ███████████████████▉░░░░░ 106K/130K · eta 4s · 24M/s · 20K ln · 4.4M ln/s

Any of those options turns the progress line on by itself, as does a --name label. Giving both a byte size and a line count warns and uses the byte size.

xz -dc archive.tar.xz | scroll --size 4.2G --name archive.tar.xz > /dev/null

the progress line with a bar, an ETA, and a label

A name takes the space the stats leave, and scrolls horizontally when the terminal is too narrow to show it whole. A narrow terminal gives up stats fields before the bar and the name lose room.

Terminals that show a progress indicator of their own — in a tab, a dock icon, or a taskbar — are driven along with the line, through OSC 9;4. Since a terminal that does not know that sequence would print it into the display, one has to name itself first: scroll asks with XTVERSION and waits 100ms for an answer naming ghostty, kitty, or iTerm2. --terminal-progress and --no-terminal-progress answer for it, and skip the question entirely.

The line is colorized when STDERR is a terminal that can show it — -c forces color on, -C off, and --color on|off|auto says the same thing at length. NO_COLOR and a $TERM of dumb turn auto off. In color the bar is drawn as background, so the filled part and the track meet with no gap between glyphs, and the leading column takes one of the eighth-blocks for a seventh of a column of extra resolution. --progress-charset ascii keeps the whole line in ASCII.

Alternate screen

--fullscreen draws on the terminal's alternate screen, which appends lines instead of repainting a window and so keeps up with a much faster stream. It uses the whole screen, which is why -N means nothing there.

make 2>&1 | scroll --fullscreen > build.log

scroll on the alternate screen, leaving the last lines behind

On exit the screen is torn down and the original screen and scrollback come back untouched, leaving nothing behind. --leave echoes the last -N lines onto the main screen, for a run whose tail is worth keeping — the one thing -N still means here.

Shell completion

eval "$(scroll --shell-completion bash)"   # or zsh, fish

Development

shards build --no-debug --error-trace   # dev build
crystal spec --error-trace              # run tests
crystal tool format                     # format
ameba                                   # lint

Contributing

  1. Fork it (https://github.com/plambert/scroll.cr/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

API