RSSAmplifier

Morgan Davis · Jun 11, 2026

Fancy, Flicker-Free File Watcher

0
Sign in to vote or save

Morgan Davis · Morgan Davis

By Morgan Davis

11 Jun 2026 • updated 40d

alt-text-for-flicker-free-file-watcher-2

Everyone who lives in a terminal has typed some version of this:

while true; do clear; cat some-file.txt; sleep 1; done

It works, but it is ugly. Every cycle wipes the screen, the output strobes, and you lose your scrollback the moment the loop fires. fw.sh does the same job properly: it redraws a file in place, highlights what changed, and stays out of your way. It is a single POSIX shell script with no dependencies beyond the standard userland tools, and it runs identically on FreeBSD and Linux. Nothing to compile, nothing to install.

Features

  • In-place redraw, no flicker. No full-screen clear between frames. The cursor is rehomed and the previous frame is overwritten line by line, leaving your scrollback above it untouched.
  • Change highlighting that persists. Lines that differ from the last frame are highlighted and stay highlighted until the next change, instead of flashing for one frame. The diff is positional for in-place edits and append-aware for growing files, so a scrolling log does not paint the whole screen as “changed.”
  • Top and bottom views. Watch the head or tail (default) of a file, switchable live with t / b. While watching the top, if new lines land below the fold the status-bar indicator turns orange; press b and exactly those new lines are highlighted.
  • Correct under real conditions. Tabs are expanded and over-wide lines folded so the redraw never drifts. Output is clipped to the terminal height and self-corrects on resize. Render time is subtracted from the interval so timing stays steady; fractional intervals (-n 0.5) work.
  • Survives a vanishing file. On log rotation, or when the file does not yet exist, it shows a “waiting” message and resumes cleanly when the file returns, rather than erroring out.
  • Scripting hooks. -x exits on first change; -q N exits after N idle cycles.

Common usage

fw.sh -t state.json                    # watch top of a file, 1s refresh
fw.sh -n 0.5 access.log                # tail a log, twice a second
fw.sh -q 10 import.lock && next-step   # wait until a file goes idle, then continue
fw.sh -x watched-file && reload        # trigger on first change

While it is running, a few single keys control the view without restarting: t and b switch between the top and bottom of the file (pressing b also highlights anything appended while you were watching the top), s toggles the status bar, and q quits. Ctrl-C works too, and either way the terminal is left in a clean state.

How it compares

The shell one-liner clears the whole screen every cycle, cannot highlight changes, and destroys scrollback. fw.sh fixes all three while staying just as portable.

watch cat file (procps-ng) repaints the entire region each cycle, and its --differences highlighting is character-level and only lasts until the next refresh. It is also Linux-centric: on FreeBSD the base watch is an unrelated tty tool, so you have to install the port and live with the name collision. fw.sh is built for files, persists highlights, distinguishes appends from edits, and runs the same on both systems with nothing to install.

tail -f and less +F are great for append-only logs but do only that. They will not show in-place edits to a rewritten file (config files, state dumps, JSON), have no change highlighting, and no top-of-file mode. fw.sh handles both append and rewrite cases.

Tools like viddy and watchexec are excellent but are compiled binaries you have to install. The whole point of fw.sh is that it is a short script you can drop onto any box with a POSIX shell, including a minimal FreeBSD install or a bare container.

Where it falls short

It is a shell script. It is not the tool for sub-50ms polling or enormous files watched in their entirety. For its actual job, watching a file tick over while you keep an eye on it, the overhead is invisible and the portability is worth far more than the microseconds a compiled tool would save.

fw.sh

#!/bin/sh
#
# fw.sh - in-place file watcher with change highlighting
#
usage() {
    cat >&2 << 'USAGE'
Usage: fw.sh [options] <file>
Options:
  -c           clear the display before starting
  -n <sec>     refresh interval in seconds (default: 1, fractions ok: 0.5)
  -s           disable status bar
  -t           show top of file
  -b           show bottom of file (default)
  -x           exit on change: quit as soon as file content changes
  -q <cycles>  quit after N cycles with no change (e.g. -q 10 at default = 10s idle)
  keys:        q=quit  t=top  b=bottom  s=status
USAGE
    exit 1
}
INTERVAL=1
NO_STATUS=0
FROM_END=1
EXIT_ON_CHANGE=0
IDLE_QUIT=0
CLEAR_FIRST=0
while getopts cn:stbxq: opt; do
    case $opt in
        c) CLEAR_FIRST=1 ;;
        n) INTERVAL=$OPTARG ;;
        s) NO_STATUS=1 ;;
        t) FROM_END=0 ;;
        b) FROM_END=1 ;;
        x) EXIT_ON_CHANGE=1 ;;
        q) IDLE_QUIT=$OPTARG ;;
        *) usage ;;
    esac
done
shift $((OPTIND - 1))
[ -z "$1" ] && usage
FILE=$1
FILE_BASE=$(basename -- "$FILE")
# Absolute path for the status bar, resolved once. The file may not exist yet,
# so resolve the (existing) directory and append the basename rather than
# realpath'ing the file directly, which would fail for a missing target.
FILE_PATH="$(cd -- "$(dirname -- "$FILE")" 2>/dev/null && pwd)/$FILE_BASE"
[ "$FILE_PATH" = "/$FILE_BASE" ] && FILE_PATH=$FILE   # dir unresolvable: fall back
# Platform-specific helpers resolved once at startup.
case $(uname) in
    Linux)
        stat_mtime() { stat -c '%y' -- "$1" 2>/dev/null | cut -c1-19; }
        epoch_ms()   { date +%s%3N; }
        ;;
    *)
        stat_mtime() { stat -f '%Sm' -t '%Y-%m-%d %H:%M:%S' -- "$1" 2>/dev/null; }
        # FreeBSD date lacks %3N; systime() is second-granular but sufficient.
        epoch_ms()   { awk 'BEGIN { print int(systime() * 1000) }'; }
        ;;
esac
# Terminal escape sequences.
ESC=$(printf '\033')
HL_ON="${ESC}[48;5;58m"       # changed-line background
HL_OFF="${ESC}[0m"
CLR_EOL="${ESC}[K"
DIM_ON="${ESC}[38;5;242m"     # gray text for retained content when file is gone
SB_BG="${ESC}[48;5;238m"      # status bar triangle: neutral gray
SB_BG_NEW="${ESC}[48;5;166m"  # status bar triangle: orange (new content below)
SB_BG_GONE="${ESC}[48;5;160m" # status bar triangle: red (file missing)
# The bar body uses an explicit light background with dark text rather than
# reverse video, so layered colors (red metrics, blue keys) render predictably
# instead of fighting the terminal's reverse-video implementation. Waiting
# mode reuses this same body scheme; only the leading indicator block is red.
BAR_BG="${ESC}[48;5;253m"     # bar background: light gray
BAR_FG="${ESC}[38;5;235m"     # bar text: dark gray
# Changed-metric marker: red text, restoring the bar's dark text after.
SB_RED_ON="${ESC}[38;5;160m"
SB_RED_OFF="$BAR_FG"
# "Ln"/"Ch" unit labels: dimmer gray so the numeric value in front of them
# stands out. Filename: bold, brighter than the bar's normal dark-gray text.
LABEL_ON="${ESC}[38;5;243m"
LABEL_OFF="$BAR_FG"
NAME_ON="${ESC}[1m${ESC}[38;5;25m"
NAME_OFF="${ESC}[22m${BAR_FG}"
# Key legend, built once. KEYS_W is the plain (escape-free) display width that
# drives layout. First letter of each word is bold blue; the rest of the word
# is the same gray as the Ln/Ch labels rather than the bar's default text color.
# Shared by both bars now that waiting mode uses the same body scheme.
KEYS_W=23   # 'quit top bottom status ' incl trailing space
_kon="${ESC}[1m${ESC}[38;5;27m"; _koff="${ESC}[22m${LABEL_ON}"
KEYS_BAR="${_kon}q${_koff}uit ${_kon}t${_koff}op ${_kon}b${_koff}ottom ${_kon}s${_koff}tatus ${LABEL_OFF}"
hide_cursor() { printf '\033[?25l'; }
show_cursor() { printf '\033[?25h'; }
cursor_up()   { [ "$1" -gt 0 ] && printf '\033[%dA' "$1"; return 0; }
cleanup()     { stty "$saved_tty"; show_cursor; printf '\n'; exit; }
cleanup_sig() { stty "$saved_tty"; show_cursor; exit; }
# slice: head or tail $1 lines from stdin according to FROM_END.
slice() {
    if [ "$FROM_END" -eq 1 ]; then tail -n "$1"; else head -n "$1"; fi
}
# count_rows: line count of stdin, counting a final unterminated line.
count_rows() { awk 'END { print NR }'; }
# fold_and_clip: fold $1 (raw unwrapped text) to term_cols-1, clip to max_rows.
# Sets: content, content_lines. Shared by read_view and gone-mode reflow.
fold_and_clip() {
    _rv=$(printf '%s' "$1" | fold -w $(( term_cols - 1 )))
    if [ -z "$_rv" ]; then
        content=''; content_lines=0
        return
    fi
    _n=$(printf '%s\n' "$_rv" | count_rows)
    if [ "$_n" -gt "$max_rows" ]; then
        content=$(printf '%s\n' "$_rv" | slice "$max_rows")
        content_lines=$max_rows
    else
        content=$_rv
        content_lines=$_n
    fi
}
# read_view: read up to max_rows lines from the relevant end of FILE, expand
# tabs, fold to width, clip to the viewport.
# Sets: content, content_lines, raw_unwrapped (pre-fold, real newlines only --
# gone-mode reflow needs this since fold can't undo a prior narrower wrap).
# Folds at term_cols-1 to dodge eat_newline_glitch char-drop on some
# terminals when a full-width line is followed by CLR_EOL+newline in one write;
# bar_build reserves the same column so bar and content stay visually matched.
read_view() {
    raw_unwrapped=$(slice "$max_rows" < "$FILE" 2>/dev/null | expand)
    fold_and_clip "$raw_unwrapped"
}
# fold_count: number of display rows the last $1 lines of FILE occupy.
fold_count() {
    tail -n "$1" -- "$FILE" 2>/dev/null | expand | fold -w $(( term_cols - 1 )) | count_rows
}
# compute_highlight: decide which rows to highlight this frame.
# Sets: new_lines, hl_from, diff_mode, hl_persist_from (may update b_override).
compute_highlight() {
    new_lines=0
    if [ "$FROM_END" -eq 1 ]; then
        if [ "$b_override" -ge 0 ]; then
            # Entered bottom via 'b': highlight lines appended since top entry.
            _ref=$b_override
            b_override=-1
            [ "$total_lines" -gt "$_ref" ] && new_lines=$(fold_count $(( total_lines - _ref )))
        elif [ "$skip_diff" -eq 0 ] && [ "$prev_total_lines" -gt 0 ]; then
            if [ "$total_lines" -gt "$prev_total_lines" ]; then
                new_lines=$(fold_count $(( total_lines - prev_total_lines )))
            elif [ "$hl_persist_from" -gt 0 ]; then
                # Reuse persisted anchor so the highlight stays visible.
                new_lines=$(( content_lines - hl_persist_from + 1 ))
            fi
        fi
        [ "$new_lines" -gt "$content_lines" ] && new_lines=$content_lines
        [ "$new_lines" -gt 0 ] && hl_persist_from=$(( content_lines - new_lines + 1 ))
    fi
    hl_from=$(( content_lines - new_lines + 1 ))
    # append wins over skip_diff so the 'b'-reveal survives the mode switch.
    if [ "$new_lines" -gt 0 ]; then
        diff_mode=append
    elif [ "$skip_diff" -eq 1 ] || [ -z "$prev_content" ]; then
        diff_mode=none
    else
        diff_mode=positional
    fi
}
# render_content: awk pass drawing content with diff highlighting + clear-EOL.
# When $1 is "dim", the whole block is drawn gray (retained content, file gone).
render_content() {
    [ "$content_lines" -gt 0 ] || return 0
    printf '%s\n' "$content" | fw_prev="$prev_content" awk \
        -v mode="$diff_mode" -v hl_from="$hl_from" \
        -v hl_on="$HL_ON" -v hl_off="$HL_OFF" -v clr="$CLR_EOL" \
        -v dim="${1:-}" -v dim_on="$DIM_ON" -v total="$content_lines" '
    BEGIN { n = split(ENVIRON["fw_prev"], parr, "\n") }
    {
        if (dim == "dim") { printf "%s%s%s%s", dim_on, $0, hl_off, clr }
        else {
            hot = 0
            if (mode == "append")          { if (NR >= hl_from)              hot = 1 }
            else if (mode == "positional") { if (NR > n || $0 != parr[NR])   hot = 1 }
            if (hot) printf "%s%s%s%s", hl_on, $0, hl_off, clr
            else     printf "%s%s", $0, clr
        }
        if (NR < total) printf "\n"   # no trailing newline -> no bottom scroll
    }'
}
# sb_reset: clear the status bar red-marking baseline and flags.
sb_reset() { sb_mtime=''; sb_bytes=''; sb_lines=''; sb_red_m=0; sb_red_b=0; sb_red_l=0; }
# draw_block: shared redraw for both live and gone-file rendering. Rehomes
# over the previous block, draws content (mode $1: '' normal, 'dim' gray),
# draws the status bar via the named function ($2: statusbar_str or
# statusbar_gone), then wipes any leftover rows from a taller previous block.
# Both callers share prev_block_lines/block_lines bookkeeping so there is one
# rehome/wipe implementation instead of two that can drift out of sync.
draw_block() {
    cursor_up $(( prev_block_lines - 1 ))
    printf '\r'
    render_content "$1"
    if [ "$NO_STATUS" -eq 0 ]; then
        "$2"
        [ "$content_lines" -gt 0 ] && printf '\n'
        printf '%s' "$sb"
        block_lines=$(( content_lines + 1 ))
    else
        block_lines=$content_lines
    fi
    if [ "$block_lines" -lt "$prev_block_lines" ]; then
        _leftover=$(( prev_block_lines - block_lines ))
        _i=0
        while [ $_i -lt "$_leftover" ]; do
            printf '\n%s' "$CLR_EOL"
            _i=$(( _i + 1 ))
        done
        cursor_up "$_leftover"
    fi
    prev_block_lines=$block_lines
}
# force_add: unconditionally append token $2 (width $1) to $_body, prefixing a
# 2-space separator if $_body already holds a token. Skips zero-width tokens
# (missing data, e.g. mtime not yet known). Width and text are passed
# separately so a styled (ANSI-wrapped) $2 never corrupts the width math.
force_add() {
    [ "$1" -eq 0 ] && return
    if [ "$_has" -eq 1 ]; then _body="${_body}  "; _w=$(( _w + 2 )); fi
    _body="${_body}$2"; _w=$(( _w + $1 )); _has=1
}
# stackw: add token width $2 (0 = absent) onto running width $1, accounting for
# the 2-column separator. Result in $_sw. Pure arithmetic, used to size
# candidate layouts before committing to one (see bar_build).
stackw() {
    if [ "$2" -eq 0 ]; then _sw=$1; return; fi
    if [ "$1" -gt 0 ]; then _sw=$(( $1 + 2 + $2 )); else _sw=$2; fi
}
# bar_build: shared bar-body layout into $_body, padded to exactly $_field
# columns. Reads bb_* globals set by the caller.
#
# Display order: mtime, name, lines, chars, then a gap, then the keys legend
# right-justified against the field's right edge.
#
# Fit ladder, most complete to least:
#   1. mtime + full path  + lines + chars + keys
#   2. mtime + basename   + lines + chars + keys   (path -> basename)
#   3. mtime + basename   + lines          + keys   (drop chars)
#   4. mtime + basename                    + keys   (drop lines)
#   5. mtime + basename                              (drop keys)
#   6. hard-clip whatever level 5 produced           (name may be cut mid-string)
# Path is replaced by basename before anything else is dropped; only after
# that substitution does the ladder start shedding whole fields.
bar_build() {
    _field=$(( term_cols - 3 ))   # usable width after the 3-char indicator block
    _avail=$(( _field - 1 ))      # reserve 1 column for the leading space
    _core=$bb_pfx_w
    if [ "$bb_mt_w" -gt 0 ]; then
        if [ "$_core" -gt 0 ]; then _core=$(( _core + 2 + bb_mt_w ))
        else _core=$bb_mt_w; fi
    fi
    _pw=${#FILE_PATH}; _bw=${#FILE_BASE}
    stackw "$_core" "$_pw"; stackw "$_sw" "$bb_ln_w"; stackw "$_sw" "$bb_ch_w"; stackw "$_sw" "$KEYS_W"; _w1=$_sw
    stackw "$_core" "$_bw"; stackw "$_sw" "$bb_ln_w"; stackw "$_sw" "$bb_ch_w"; stackw "$_sw" "$KEYS_W"; _w2=$_sw
    stackw "$_core" "$_bw"; stackw "$_sw" "$bb_ln_w";                          stackw "$_sw" "$KEYS_W"; _w3=$_sw
    stackw "$_core" "$_bw";                                                    stackw "$_sw" "$KEYS_W"; _w4=$_sw
    stackw "$_core" "$_bw";                                                                             _w5=$_sw
    if   [ "$_w1" -le "$_avail" ]; then _lvl=1
    elif [ "$_w2" -le "$_avail" ]; then _lvl=2
    elif [ "$_w3" -le "$_avail" ]; then _lvl=3
    elif [ "$_w4" -le "$_avail" ]; then _lvl=4
    elif [ "$_w5" -le "$_avail" ]; then _lvl=5
    else _lvl=6
    fi
    _body=' '; _w=1; _has=0   # leading space counted from the start
    force_add "$bb_pfx_w" "$bb_pfx_s"
    force_add "$bb_mt_w" "$bb_mt_s"
    case $_lvl in
        1) force_add "$_pw" "${NAME_ON}${FILE_PATH}${NAME_OFF}"; force_add "$bb_ln_w" "$bb_ln_s"
           force_add "$bb_ch_w" "$bb_ch_s" ;;
        2) force_add "$_bw" "${NAME_ON}${FILE_BASE}${NAME_OFF}"; force_add "$bb_ln_w" "$bb_ln_s"
           force_add "$bb_ch_w" "$bb_ch_s" ;;
        3) force_add "$_bw" "${NAME_ON}${FILE_BASE}${NAME_OFF}"; force_add "$bb_ln_w" "$bb_ln_s" ;;
        4|5) force_add "$_bw" "${NAME_ON}${FILE_BASE}${NAME_OFF}" ;;
        6) force_add "$_bw" "$FILE_BASE" ;;  # plain: level 6 may hard-clip mid-string
    esac
    # Keys legend, right-justified with a minimum 2-column gap, for every
    # level that included it in its fit check (1-4).
    if [ "$_lvl" -le 4 ]; then
        _gap=$(( _field - _w - KEYS_W ))
        _body="${_body}$(printf '%*s' "$_gap" '')${bb_keys_s}"
        _w=$_field
    fi
    if [ "$_w" -le "$_field" ]; then
        _body=$(printf '%s%*s' "$_body" $(( _field - _w )) '')
    else
        # Level 6: even mtime+basename overflow. Truncate whatever remains.
        _body=$(printf '%-*.*s' "$_field" "$_field" "$_body")
    fi
}
# today_hhmmss: given a "YYYY-MM-DD HH:MM:SS" timestamp, strip the date part
# when it matches the current date, saving 11 columns in the common case.
today_hhmmss() {
    _today=$(date +%Y-%m-%d)
    if [ "${1%% *}" = "$_today" ]; then printf '%s' "${1#* }"; else printf '%s' "$1"; fi
}
# statusbar_str: build the status bar line into $sb (no cursor movement).
# Changed metric fields are marked red and persist until the next metric change.
statusbar_str() {
    [ "$FROM_END" -eq 1 ] && _arrow='▼' || _arrow='▲'
    [ "$new_below" -eq 1 ] && _bg=$SB_BG_NEW || _bg=$SB_BG
    # Recompute red flags only when a metric actually differs from the baseline,
    # so the no-change repaints that maintain content highlights keep the red.
    # A real change always moves mtime, guaranteeing at least one red field.
    if [ "$mod_time" != "$sb_mtime" ] || [ "$byte_count" != "$sb_bytes" ] \
        || [ "$total_lines" != "$sb_lines" ]; then
        if [ -n "$sb_mtime" ]; then
            sb_red_m=0; [ "$mod_time"    != "$sb_mtime" ] && sb_red_m=1
            sb_red_b=0; [ "$byte_count"  != "$sb_bytes" ] && sb_red_b=1
            sb_red_l=0; [ "$total_lines" != "$sb_lines" ] && sb_red_l=1
        else
            sb_red_m=0; sb_red_b=0; sb_red_l=0
        fi
        sb_mtime=$mod_time; sb_bytes=$byte_count; sb_lines=$total_lines
    fi
    _mt=$(today_hhmmss "$mod_time")
    bb_pfx_w=0; bb_pfx_s=''
    bb_mt_w=${#_mt}; bb_mt_s=$_mt
    [ "$sb_red_m" -eq 1 ] && bb_mt_s="${SB_RED_ON}${_mt}${SB_RED_OFF}"
    _ln_n=$total_lines; [ "$sb_red_l" -eq 1 ] && _ln_n="${SB_RED_ON}${total_lines}${SB_RED_OFF}"
    bb_ln_w=$(( ${#total_lines} + 3 )); bb_ln_s="${_ln_n}${LABEL_ON} Ln${LABEL_OFF}"
    _ch_n=$byte_count; [ "$sb_red_b" -eq 1 ] && _ch_n="${SB_RED_ON}${byte_count}${SB_RED_OFF}"
    bb_ch_w=$(( ${#byte_count} + 3 )); bb_ch_s="${_ch_n}${LABEL_ON} Ch${LABEL_OFF}"
    bb_keys_s=$KEYS_BAR
    bar_build
    sb=$(printf "${_bg} %s ${HL_OFF}${BAR_BG}${BAR_FG}%s${HL_OFF}" "$_arrow" "$_body")
}
# statusbar_gone: "file missing" bar. Same priority ladder and composer as the
# normal bar; the banner is the mandatory prefix, last-seen mtime/lines/chars
# are omitted entirely (width 0) until the file has been seen at least once.
statusbar_gone() {
    bb_pfx_s=' WAITING FOR FILE'; bb_pfx_w=${#bb_pfx_s}
    if [ -n "$sb_mtime" ]; then
        bb_mt_s="last seen $(today_hhmmss "$sb_mtime")"; bb_mt_w=${#bb_mt_s}
        bb_ln_s="${sb_lines}${LABEL_ON} Ln${LABEL_OFF}"; bb_ln_w=$(( ${#sb_lines} + 3 ))
        bb_ch_s="${sb_bytes}${LABEL_ON} Ch${LABEL_OFF}"; bb_ch_w=$(( ${#sb_bytes} + 3 ))
    else
        bb_mt_w=0; bb_mt_s=''
        bb_ln_w=0; bb_ln_s=''
        bb_ch_w=0; bb_ch_s=''
    fi
    bb_keys_s=$KEYS_BAR
    bar_build
    sb=$(printf "${SB_BG_GONE} ! ${HL_OFF}${BAR_BG}${BAR_FG}%s${HL_OFF}" "$_body")
}
# poll_keys: wait out the rest of the interval, handling keypresses. Also
# checks for terminal resize every 100ms tick (not just once per interval),
# so response time to a resize is bounded by the tick rate, not by -n.
# Reads global render_start. Returns after one interval, a handled key, or a
# detected resize.
poll_keys() {
    _ms=$(( interval_ms - ($(epoch_ms) - render_start) ))
    [ "$_ms" -lt 0 ] && _ms=0
    _ticks=$(( _ms / 100 ))
    [ "$_ticks" -lt 1 ] && _ticks=1
    _i=0
    while [ $_i -lt $_ticks ]; do
        case $(dd if=/dev/tty bs=1 count=1 2>/dev/null) in
            q) cleanup ;;
            t) FROM_END=0; skip_diff=1; hl_persist_from=0; t_entered_lines=$total_lines; return ;;
            b) FROM_END=1; skip_diff=1; hl_persist_from=0; new_below=0
               b_override=$t_entered_lines; t_entered_lines=-1; return ;;
            s) NO_STATUS=$(( 1 - NO_STATUS )); skip_diff=1; return ;;
        esac
        if [ "$(tput cols)" -ne "$prev_term_cols" ] || [ "$(tput lines)" -ne "$prev_term_rows" ]; then
            return
        fi
        sleep 0.1
        _i=$(( _i + 1 ))
    done
}
trap cleanup_sig INT TERM
saved_tty=$(stty -g)
stty -icanon -echo min 0 time 0
interval_ms=$(awk "BEGIN { printf \"%d\", $INTERVAL * 1000 }")
hide_cursor
[ "$CLEAR_FIRST" -eq 1 ] && printf '\033[2J\033[H'
prev_block_lines=0
prev_content=''
prev_total_lines=0
prev_mod_time=''
skip_diff=0
idle_count=0
hl_persist_from=0
new_below=0        # orange-latch: new content below fold in top mode
t_entered_lines=-1 # line count frozen at top-mode entry (-1 = unset)
b_override=-1      # one-shot 'b'-reveal baseline (-1 = inactive)
block_lines=0
gone=0             # 1 while the file is missing (content retained + dimmed)
raw_unwrapped=''   # set by read_view; persists across gone-state ticks unchanged
prev_term_rows=0   # detect resize to force a full repaint
prev_term_cols=0
sb_reset
while true; do
    render_start=$(epoch_ms)
    term_rows=$(tput lines)
    term_cols=$(tput cols)
    status_rows=0
    [ "$NO_STATUS" -eq 0 ] && status_rows=1
    max_rows=$(( term_rows - status_rows ))
    # Any width change clears + redraws from top instead of rehoming in place.
    # Polling only sees endpoints: a drag can dip narrower between ticks and
    # settle back at/above the start, but the terminal already hard-wrapped
    # on-screen rows during that dip, so a computed cursor_up would undershoot
    # regardless of net direction. Height-only change still repaints in place.
    if [ "$prev_term_rows" -ne 0 ] \
        && { [ "$term_cols" -ne "$prev_term_cols" ] || [ "$term_rows" -ne "$prev_term_rows" ]; }; then
        if [ "$term_cols" -ne "$prev_term_cols" ]; then
            printf '\033[2J\033[H'
            prev_block_lines=0
            hl_persist_from=0   # row-offset anchors invalid after refold
            b_override=-1
        fi
        skip_diff=1
    fi
    prev_term_rows=$term_rows
    prev_term_cols=$term_cols
    # Missing file: reflow raw_unwrapped to current width, dim it, show the
    # waiting banner via the same draw_block as live content. Redraw only on
    # first entry or resize (skip_diff); static otherwise.
    if [ ! -f "$FILE" ]; then
        if [ "$gone" -eq 0 ] || [ "$skip_diff" -eq 1 ]; then
            fold_and_clip "$raw_unwrapped"
            draw_block dim statusbar_gone
            gone=1
            skip_diff=0
        fi
        poll_keys
        continue
    fi
    # File returned after being gone: force a clean full repaint.
    if [ "$gone" -eq 1 ]; then
        gone=0
        skip_diff=1
        prev_content=''
        prev_total_lines=0
        prev_mod_time=''
        sb_reset
    fi
    read_view
    byte_count=$(wc -c < "$FILE" | tr -d ' ')
    # awk counts a final unterminated line; wc -l would undercount by one for
    # files without a trailing newline (e.g. serialized PHP session data).
    total_lines=$(count_rows < "$FILE")
    mod_time=$(stat_mtime "$FILE")
    # Capture baseline at top-mode entry for the 'b'-reveal highlight.
    [ "$FROM_END" -eq 0 ] && [ "$t_entered_lines" -lt 0 ] && t_entered_lines=$total_lines
    # Change detection drives -x / -q and the skip-repaint fast path.
    changed=0
    [ "$content" != "$prev_content" ] && changed=1
    # -x exits after this frame renders (so the change is visible), not now.
    exit_after=0
    [ "$EXIT_ON_CHANGE" -eq 1 ] && [ -n "$prev_content" ] && [ "$changed" -eq 1 ] && exit_after=1
    if [ "$IDLE_QUIT" -gt 0 ]; then
        if [ "$changed" -eq 0 ]; then
            idle_count=$(( idle_count + 1 ))
            [ "$idle_count" -ge "$IDLE_QUIT" ] && cleanup
        else
            idle_count=0
        fi
    fi
    # Latch orange when content is appended below the fold in top mode. Done
    # before the fast-path guard because such an append leaves the visible head
    # unchanged (changed=0) yet still needs the indicator.
    [ "$FROM_END" -eq 0 ] && [ "$prev_total_lines" -gt 0 ] \
        && [ "$total_lines" -gt "$prev_total_lines" ] && new_below=1
    # Fast path: nothing changed and no highlight to maintain. Repaint only the
    # status bar if it needs it (mod_time moved on touch, or the orange latch
    # just flipped), preserving any on-screen content highlights.
    if [ "$changed" -eq 0 ] && [ "$skip_diff" -eq 0 ] && [ "$b_override" -lt 0 ] \
        && [ "$hl_persist_from" -eq 0 ] && [ "$block_lines" -eq "$prev_block_lines" ]; then
        if [ "$NO_STATUS" -eq 0 ] && [ "$mod_time" != "$prev_mod_time" ]; then
            statusbar_str
            # After any render the cursor rests on the status bar line itself,
            # so redraw in place at column 0 -- no vertical movement.
            printf '\r%s' "$sb"
            prev_mod_time=$mod_time
        fi
        prev_total_lines=$total_lines
        poll_keys
        continue
    fi
    compute_highlight
    draw_block '' statusbar_str
    [ "$changed" -eq 1 ] && prev_content=$content
    prev_total_lines=$total_lines
    prev_mod_time=$mod_time
    skip_diff=0
    # -x: the changed frame is now on screen; exit cleanly.
    [ "$exit_after" -eq 1 ] && cleanup
    poll_keys
done
Explore

Read the original on morgandavis.net

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.