#!/usr/bin/env dash # compat: +ash +bash +dash -hush -ksh -mksh -oksh -osh -posh +yash +zsh # NOTE: many shells work, but some of them fail to restore tty settings. # fuck termcap, all my homies hate termcap. glug() ( # note the subshell syntax. this allows us to abuse globals like crazy. [ $# = 0 ] || { printf '%s does not take arguments.\n' glug >&2; return 2; } _here="$PWD" repo="$PWD" i=0 while ! [ -d .git ] 2>/dev/null; do # no one has git repositories with directories deeper than 10 levels, right? [ $((i+=1)) -lt 10 ] || { printf '%s: failed to find a .git directory\n' glug >&2; return 2; } cd .. repo="$PWD" done cd "$_here" detect_size() { # globals: $COLUMNS, $LINES _size="$(stty size)" COLUMNS="${_size#* }" LINES="${_size%% *}" if [ "$COLUMNS" -gt 0 ] 2>/dev/null && [ "$LINES" -gt 0 ] 2>/dev/null; then : # pass else printf '%s: failed to determine terminal size\n' glug >&2 return 2 fi unset _size } [ -n "$ZSH_VERSION" ] || detect_size if [ -n "$ZSH_VERSION" ] || [ -n "$BASH_VERSION" ]; then esc=$'\e' nl=$'\n' cr=$'\r' else esc="$(printf '\033')" # shells strip trailing newlines from captures, so work around that: nl="$(printf '\nx')"; nl="${nl%x}" cr="$(printf '\rx')"; cr="${cr%x}" fi limit=200 # maximum number of git commits to look back on choices=0 selection=1 alt=0 ret=0 err= alt_on() { # globals: $alt printf '\033[?1049h' # alt screen on (smcup) alt=1 printf '\033[?1h\033=' # no idea but less does it so it must be important (smkx) printf '\r' # move cursor to start of line (carriage return) printf '\033[H' # move cursor to first line } alt_off() { # globals: $alt printf '\r' # move cursor to start of line (carriage return) #printf '\033[K' # erase everything after the cursor (el) printf '\033[?1l\033>' # no idea but less does it so it must be important (rmkx) printf '\033[?1049l' # alt screen off (rmcup) alt=0 } glug_enter() { # globals: $alt [ $alt = 1 ] || alt_on } glug_exit() { # globals: $alt, $err, $ret [ $alt = 0 ] || alt_off [ -z "$err" ] || printf '%s\n' "$err" >&2 exit $ret } die() { # globals: $ret, $err ret=$? err="$@" [ $? = 0 ] && exit 1 || exit $? } read_byte() { # NOTE: run this function in a subshell, else everything will blow up. old="$(stty -g)" trap 'stty "$old"' INT EXIT stty -icanon -echo dd ibs=1 count=1 2>/dev/null || exit echo _ # append an underscore so that newlines are preserved in captures } \033[m \033[33m%s\033[m ' "$commit" printf ' \0337 \033[7m>\033[m \033[33m%s\033[m ' "$commit" else printf ' \033[90m%s\033[m ' "$commit" fi # TODO: truncate message as necessary. printf '%s\n' "$message" : $((linesleft-=1)) if [ $i = $selection ]; then gds="$(git diff --stat "$commit~" "$commit" --)" if [ $? = 0 ]; then : $((linesleft-=2)) # reserve two lines for present_choices format_stats : $((linesleft+=2)) # ... else printf ' \033[91merror:\033[31m failed to run `git diff --stat`\n' : $((linesleft-=1)) fi fi [ $linesleft -ge 2 ] || break done if [ $linesleft -ge 1 ]; then printf '\033[%s;0H' $LINES linesleft=1 printf ' \033[33m(\033[97m%s\033[33m/\033[97m%s\033[33m)\033[m ' "$selection" "$choices" : $((linesleft-=1)) fi eval "commit=\$choice_$selection" # set $commit to something useful } handle_input() { # globals: $dirty # TODO: handle escape codes (especially arrow keys, page up/down) # TODO: reset button that checks LINES and COLUMNS again. printf '\0338' # restore cursor to prompt position (rc) input="$(read_byte)" || return if [ "${input%_}" = "$input" ]; then # no magic underscore, something went wrong. return 1 else input="${input%_}" fi while [ -n "$input" ]; do # replace newlines with spaces for my own sanity. # of course, this makes spaces act the same as newlines. if [ "${input#$nl}" != "$input" ]; then input=" ${input#$nl}" fi if [ "${input#[! gGjkq]}" != "$input" ]; then _seg="${input%%[ gGjkq]*}" elif [ "${input# }" != "$input" ]; then _seg=' ' alt_off git diff "$commit~" "$commit" -- || exit alt_on dirty=1 elif [ "${input#g}" != "$input" ]; then _seg=g [ $selection = 1 ] || dirty=1 selection=1 elif [ "${input#G}" != "$input" ]; then _seg=G [ $selection = $choices ] || dirty=1 selection=$choices elif [ "${input#j}" != "$input" ]; then _seg=j [ $selection = $choices ] || dirty=1 [ $((selection+=1)) -le $choices ] || selection=$choices elif [ "${input#k}" != "$input" ]; then _seg=k [ $selection = 1 ] || dirty=1 [ $((selection-=1)) -ge 1 ] || selection=1 elif [ "${input#q}" != "$input" ]; then _seg=q dirty=0 # stop redrawing and just get outta here return 1 else break # unreachable fi input="${input#$_seg}" done unset _seg } trap glug_exit INT EXIT && glug_enter # TODO: invoke git with --no-config or whatever? # TODO: --decorate=full, and color it. logs="$(git log --oneline --first-parent -n $limit 2>&1)" || die "$logs" parse_logs [ $choices -gt 0 ] || { printf '%s: no commits\n' >&2; return 2; } present_choices dirty=0 while handle_input; do if [ $dirty = 1 ]; then printf '\r' # move cursor to start of line (carriage return) printf '\033[H' # move cursor to first line printf '\033[J' # clear the rest of the screen present_choices dirty=0 fi done ) 1<>/dev/tty <&1 # ensure this interacts with a terminal instead of a pipe [ -n "${preload+-}" ] || glug "$@"