# make sure to put a shebang here readc() { # arg: # adapted from https://unix.stackexchange.com/a/464963 if test -t 0 ; then # if stdin is a tty device, put it out of icanon, set min and # time to sane value, but don't otherwise touch other input or # or local settings (echo, isig, icrnl...). Take a backup of the # previous settings beforehand. saved_tty_settings=$(stty -g) stty -icanon min 1 time 0 fi # Sanitize any variable that's passed in case "$1" in c) var=_c ;; var) var=_var ;; '') var=REPLY ;; *) var="$1" ;; esac # Zero-out the variable eval "$var=" # Read one full *character*, not *byte* while :; do c=$(dd bs=1 count=1 2>/dev/null; echo .) c=${c%.} # Break on EOF test -n "$c" || break eval "$var=\"\${$var}$c\"" # Break when we have a full character (wc -m) test "$(eval printf %s "\"\$$var\"" | wc -m)" -eq 0 || break done # Round-trip sanitized variables test "x$var" = x_c && eval c="\$$var" test "x$var" = x_var && eval var="\$$var" if [ -t 0 ]; then # restore settings saved earlier if stdin is a tty device. stty "$saved_tty_settings" fi } setup_terminal() { # Setup the terminal for the TUI. # '\e[?1049h': Use alternative screen buffer. # '\e[?7l': Disable line wrapping. # '\e[?25l': Hide the cursor. # '\e[2J': Clear the screen. # '\e[1;Nr': Limit scrolling to scrolling area. # Also sets cursor to (0,0). printf '\e[?1049h\e[?7l\e[2J\e[1;%sr' "$((LINES - 1))" # Hide echoing of user input stty -echo } reset_terminal() { # Reset the terminal to a useable state (undo all changes). # '\e[?7h': Re-enable line wrapping. # '\e[?25h': Unhide the cursor. # '\e[2J': Clear the terminal. # '\e[;r': Set the scroll region to its default value. # Also sets cursor to (0,0). # '\e[?1049l: Restore main screen buffer. printf '\e[?7h\e[?25h\e[2J\e[;r\e[?1049l' # Show user input. stty echo } printlines() { # printlines file min max if test -n "$DEBUG"; then echo --- >&2 else clear fi sed -n "$2,$3p" "$1" notify_flush } notify() { # notify STRING notification="$notification$1" } notify_flush() { # notify_flush winch printf '\e[s\e[%s;%sH' 0 "$c" printf '%s' "$notification" printf '\e[u' notification= } scroll() { # scroll nlines -- MUTATES GLOBAL STATE arg="${1:-0}" case "$arg" in +*|-*) n=$((n+${arg#+})) ;; 0) ;; *) n=$1 ;; esac if test $n -gt $((max - l)) then n=$((max - l)) notify '_' fi if test $n -lt $min then notify '^' n=$min fi m=$((n+l)) if test $m -ge $max then m=$max fi # echo "min:$min max:$max n:$n m:$m l:$l" >&2 } ctty() { # ctty ## Get the controlling TTY interface. if test -t 2 then # only works if not redirecting stderr tty <&2 else echo /proc/self/fd/1 # probably only works on linux fi } winch() { stty size > "$wf" read -r l c < "$wf" # echo l:$l c:$c >&2 l=$((l-2)) # last line } cleanup() { rm "$bf" "$wf" reset_terminal exit } vellum() { # vellum [-n NUM] < INPUT # Set up buffer bf="$(mktemp)" # buffer cat "${@:--}" > "$bf" # Redirect input to the controlling terminal ## XXX: This assumes that you haven't redirected stderr. exec < "$(ctty)" max=$(wc -l < "$bf") min=1 # min = minimum accessible line num. n=$min # n = num. first line to show # Set up terminal setup_terminal wf="$(mktemp)" # winch file winch; trap winch WINCH m=$((n+l)) # m = num. last line to show trap cleanup EXIT INT QUIT # Main loop while :; do scroll printlines "$bf" $n $m readc key >/dev/null 2>&1 case "$key" in ' ') scroll +$l ;; '') scroll -$l ;; j) scroll +1 ;; k) scroll -1 ;; g) scroll $min ;; G) scroll $max ;; q) break ;; esac done break echo } test -n "$DEBUG" && { set -x; exec 2> /tmp/vellum.log; } test -z "$SOURCE" && vellum "$@"