#!/bin/sh -eu # # tgitui -- A terminal-based GIT log and stash viewer # # Copyright © 2021 Samuel Lidén Borell # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # TODO truncate long lines esc=$(printf '\033') enter=$(printf '\012') ctrl_l=$(printf '\014') redraw_list() { printf '\033[%d;1H' $start_row >&2 i=0 while [ $i != $height ]; do offsi=$((i + scroll)) if [ $offsi -gt $max ]; then printf '\033[K\n' >&2 else item=$(eval "printf '%s\n' \"\$item$offsi\"") if [ $offsi = $selected ]; then printf ' \033[7m %10s \033[0m\033[K\n' "$item" >&2 else printf ' %s\033[K\n' "$item" >&2 fi fi i=$((i + 1)) done printf '\033[%d;4H' $((start_row + selected - scroll)) >&2 } pipe_choice() { local selected=$(printf '%s\n' "$3" | cut -d ';' -f 1 | cut -d ':' -f 1) local scroll=$(printf '%s\n' "$3" | cut -d ';' -f 1 | cut -d ':' -f 2) # signals appear to be delayed #trap 'cols=$(tput cols); rows=$(tput lines); last=$((first + height - 1)); redraw_list' WINCH local cols=$(tput cols) local rows=$(tput lines) local start_row=$1 local height=$((rows - $2)) local i=0 local first=$scroll local last=$((first + height - 1)) printf '\033[%d;1H\033[2J' $start_row >&2 while read item; do eval "local item$i=\$item" if [ $i = $selected ]; then printf ' \033[7m %s \033[0m\n' "$item" >&2 elif [ $i -ge $first ] && [ $i -le $last ]; then printf ' %s\n' "$item" >&2 fi i=$((i + 1)) done printf '\033[%d;4H' $((start_row + selected - scroll)) >&2 local max=$((i - 1)) while true; do keyseq=$(dd bs=1 count=1 2>/dev/null <&3) case "$keyseq" in $enter) eval "printf '%s\n' \"$selected:$scroll;\$item$selected\"" break;; q|Q) break;; $ctrl_l) cols=$(tput cols) rows=$(tput lines) last=$((first + height - 1)) redraw_list >&2;; $esc) keyseq=$(dd bs=1 count=1 2>/dev/null <&3) case "$keyseq" in [) keyseq=$(dd bs=1 count=1 2>/dev/null <&3) case "$keyseq" in A) # Up key [ $selected != 0 ] || continue if [ $selected = $first ]; then selected=$((selected - 1)) first=$((first - height)) last=$((last - height)) scroll=$((scroll - height)) redraw_list else row=$((start_row + selected-1 - scroll)) item=$(eval "printf '%s\n' \"\$item$selected\"") selected=$((selected - 1)) previtem=$(eval "printf '%s\n' \"\$item$selected\"") printf '\033[%d;1H \033[7m %s \033[0m\033[K\n %s \033[K\033[%d;4H' $row "$previtem" "$item" $row >&2 fi ;; B) # Down key [ $selected != $max ] || continue if [ $selected = $last ]; then selected=$((selected + 1)) first=$((first + height)) last=$((last + height)) scroll=$((scroll + height)) redraw_list else row=$((start_row + selected - scroll)) item=$(eval "printf '%s\n' \"\$item$selected\"") selected=$((selected + 1)) nextitem=$(eval "printf '%s\n' \"\$item$selected\"") printf '\033[%d;1H %s\033[K\n \033[7m %s \033[0m\033[K\033[%d;4H' $row "$item" "$nextitem" $((row+1)) >&2 fi ;; 1|H) # Home [ $selected != 0 ] || continue selected=0 first=0 last=$((height-1)) scroll=0 redraw_list ;; 4|F) # End [ $selected != $max ] || continue selected=$max first=$((max - (max % height) )) last=$((first + height-1)) scroll=$first redraw_list ;; 5) # Page Up [ $selected != 0 ] || continue selected=$((selected - height)) if [ $selected -lt 0 ]; then selected=0 first=0 last=$((height-1)) scroll=0 else first=$((first - height)) last=$((last - height)) scroll=$((scroll - height)) fi redraw_list ;; 6) # Page Down [ $selected != $max ] || continue selected=$((selected + height)) if [ $selected -gt $max ]; then selected=$max first=$((max - (max % height) )) last=$((first + height-1)) scroll=$first else first=$((first + height)) last=$((last + height)) scroll=$((scroll + height)) fi redraw_list ;; #*) # echo KEY:$keyseq >&2 ;; esac;; esac;; esac done # trap - WINCH } dummyitems() { cat <&2 saved_stty=$(stty -g) stty "$saved_stty" stty -echo -ctlecho -icanon } cleanup() { local rows=$(tput lines) printf '\033[%d;1H\033[?1049l\033[?1l\033[?2004l' $((rows-1)) >&2 stty "$saved_stty" } find_git_root() { root='.' while [ ! -e "$root/.git" ] && [ x$(realpath "$root") != / ]; do root="$root/.." done echo "$root" } if [ $# -lt 1 ]; then cat < Ask while true; do printf '\033[20G<-- [A]dd more or [U]stage this file? ' case $(dd bs=1 count=1) in a|A) changetype=M break;; u|U) changetype=A break;; q|Q) break 2;; esac done fi cleanup #echo "ch='$changetype' file='$file'">&2 #read dummy case "$changetype" in M) # Modified, not added --> Add git add -p "$file";; \?\?) # Untracked --> Add git add "$file";; A) # Added --> Unstage git rm --cached -f "$file";; *) echo "BUG: Don't know what to do with that file (changetype=$changetype)" >&2 esac setup_terminal done;; c|commits) shift trap cleanup EXIT setup_terminal liststate=0:0 while true; do { sel=$( git log --oneline "$@" | pipe_choice 2 3 "$liststate" ); } 3<&1 if [ -z "$sel" ]; then break; fi liststate=$(printf '%s\n' "$sel" | cut -d ';' -f 1) id=$(printf '%s\n' "$sel" | cut -d ';' -f 2- | cut -d ' ' -f 1) cleanup # it appears that GIT returns an error code when viewing the initial commit git show "$id" || true setup_terminal done;; d|diff) shift trap cleanup EXIT setup_terminal liststate=0:0 while true; do { sel=$( git diff --stat "$@" | head -n -1 | pipe_choice 2 3 "$liststate" ); } 3<&1 if [ -z "$sel" ]; then break; fi liststate=$(printf '%s\n' "$sel" | cut -d ';' -f 1) file=$(printf '%s\n' "$sel" | cut -d ';' -f 2- | cut -d ' ' -f 1) cleanup git diff "$@" -- $(find_git_root)/"$file" || true setup_terminal done;; s|stashed) shift trap cleanup EXIT setup_terminal liststate=0:0 while true; do { sel=$( git stash list "$@" | pipe_choice 2 3 "$liststate" ); } 3<&1 if [ -z "$sel" ]; then break; fi liststate=$(printf '%s\n' "$sel" | cut -d ';' -f 1) id=$(printf '%s\n' "$sel" | cut -d ';' -f 2- | cut -d ':' -f 1) cleanup git show "$id" || true setup_terminal done;; *) echo "invalid sub-command: $1" >&2 exit 1;; esac