mirror of
https://github.com/notwa/rc
synced 2024-11-12 15:59:02 -08:00
348 lines
11 KiB
Bash
Executable file
348 lines
11 KiB
Bash
Executable file
#!/usr/bin/env dash
|
|
# compat: +ash +bash +dash +zsh
|
|
# 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
|
|
} </dev/tty # ensure this interacts with a terminal instead of a pipe
|
|
|
|
escape_message() {
|
|
# arguments: $message
|
|
_msg=
|
|
while _seg="${message%%\'*}"; [ "$_seg" != "$message" ]; do
|
|
_msg="$_msg$_seg'\''"
|
|
message="${message#*\'}"
|
|
done
|
|
message="$_msg$message"
|
|
unset _msg _seg
|
|
}
|
|
|
|
parse_logs() {
|
|
# arguments: $logs (clobbered)
|
|
# globals: $choices
|
|
logs="$logs$nl" # otherwise the last entry is dropped
|
|
while _next="${logs#*$nl}"; [ "$logs" != "$_next" ]; do
|
|
_line="${logs%%$nl*}"
|
|
#_line="${logs%$cr}" # TODO: handle this more robustly.
|
|
logs="$_next"
|
|
|
|
commit="${_line%% *}"
|
|
[ -z "${commit##[a-f0-9]*}" ] || die "invalid commit: $commit"
|
|
message="${_line#* }"
|
|
escape_message
|
|
|
|
#printf '~%s~\n' "$_line"
|
|
#printf 'COMMIT="%s"; MESSAGE="%s";\n' "$commit" "$message"
|
|
|
|
: $((choices+=1))
|
|
eval "choice_$choices=$commit"
|
|
eval "commit_$commit='$message'"
|
|
done
|
|
unset _line _next
|
|
}
|
|
|
|
colorize_pathy() {
|
|
# arguments: $pathy (clobbered)
|
|
printf '%s' "$pathy" # TODO
|
|
}
|
|
|
|
colorize_diffy() {
|
|
# arguments: $diffy (clobbered)
|
|
while [ -n "$diffy" ]; do
|
|
if [ "${diffy#[!0-9+-]}" != "$diffy" ]; then
|
|
_seg="${diffy%%[0-9+-]*}"
|
|
printf '\033[m ' # NOTE: this replaces *all* unexpected characters.
|
|
elif [ "${diffy#[0-9]}" != "$diffy" ]; then
|
|
_seg="${diffy%%[!0-9]*}"
|
|
printf '\033[97m%4s' "$_seg"
|
|
elif [ "${diffy#+}" != "$diffy" ]; then
|
|
_seg="${diffy%%[!+]*}"
|
|
printf '\033[92m%s' "$_seg"
|
|
elif [ "${diffy#-}" != "$diffy" ]; then
|
|
_seg="${diffy%%[!-]*}"
|
|
printf '\033[91m%s' "$_seg"
|
|
else
|
|
break # unreachable
|
|
fi
|
|
diffy="${diffy#$_seg}"
|
|
done
|
|
unset _seg
|
|
}
|
|
|
|
colorize_summary() {
|
|
# arguments: $gds (clobbered)
|
|
_color=33
|
|
[ "${gds%insertion*}" = "$gds" ]; _ins=$? # _ins=1 when insertion is found
|
|
while [ -n "$gds" ]; do
|
|
if [ "${gds#[!(),0-9+-]}" != "$gds" ]; then
|
|
_seg="${gds%%[(),0-9+-]*}"
|
|
printf '\033[%sm%s' "$_color" "$_seg"
|
|
elif [ "${gds#[()+-]}" != "$gds" ]; then
|
|
_seg="${gds%%[!()+-]*}"
|
|
case "$_seg" in
|
|
('(+)') printf '\033[m(\033[92m+\033[m)';;
|
|
('(-)') printf '\033[m(\033[91m-\033[m)';;
|
|
(*) printf '%s' "$_seg";;
|
|
esac
|
|
elif [ "${gds#,}" != "$gds" ]; then
|
|
_seg="${gds%%[!,]*}"
|
|
printf '\033[%sm%s' "$_color" "$_seg"
|
|
: $((_color-=1)) # how convenient that the color codes line up!
|
|
[ $_ins = 1 ] || : $((_color-=1)) # no insertions; skip to red
|
|
elif [ "${gds#[0-9]}" != "$gds" ]; then
|
|
_seg="${gds%%[!0-9]*}"
|
|
printf '\033[97m%s' "$_seg"
|
|
else
|
|
break # unreachable
|
|
fi
|
|
gds="${gds#$_seg}"
|
|
done
|
|
unset _color _ins _seg
|
|
}
|
|
|
|
format_stats() {
|
|
# arguments: $gds (clobbered)
|
|
# globals: $linesleft
|
|
[ $linesleft -gt 1 ] || return
|
|
while _line="${gds%%$nl*}"; [ "$_line" != "$gds" ]; do
|
|
gds="${gds#*$nl}"
|
|
[ $linesleft -gt 1 ] || continue
|
|
# NOTE: assumes no filepaths contain a '|' character.
|
|
pathy="${_line%|*}"
|
|
diffy="${_line#*|}"
|
|
colorize_pathy
|
|
printf '\033[m|'
|
|
colorize_diffy
|
|
unset pathy diffy
|
|
printf '\033[m\n'
|
|
: $((linesleft-=1))
|
|
done
|
|
colorize_summary
|
|
printf '\n'
|
|
: $((linesleft-=1))
|
|
unset _line
|
|
}
|
|
|
|
present_choices() {
|
|
# arguments: $selection
|
|
# globally influential: $choices
|
|
linesleft=$LINES
|
|
i=$((selection-2))
|
|
while [ $((i+=1)) -le $choices ]; do
|
|
if [ $i -eq 0 ]; then
|
|
printf '%s\n' 'welcome to glug!' # TODO: make this pretty. print cwd?
|
|
: $((linesleft-=1))
|
|
continue
|
|
elif [ $i -lt 0 ]; then
|
|
printf '\n'
|
|
: $((linesleft-=1))
|
|
continue
|
|
fi
|
|
|
|
eval "commit=\$choice_$i"
|
|
eval "message=\$commit_$commit"
|
|
|
|
if [ $i = $selection ]; then
|
|
#printf ' \033[95m >\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 "$@"
|