lesh/lesh

232 lines
5.5 KiB
Bash
Executable File

#!/bin/bash
# Lesh: a pure bash (I think) pager
# Lesh is moreish
# Intended to behave like less, more or less
# Currently lesh can:
# read a file or pipe and display text
# tell you if you didn't give it anything to read
# scroll down/up with j/k or the down and up arrows
# page up and page down with page up and page down
# jump to the start/end of the file with g/G
# quit with q
#
# It doesn't mess up your terminal when terminated
# It does when killed (printf '\e[;r' to fix)
# allow terminal size to be read, then run useless command to populate variables
shopt -s checkwinsize; (:;:)
# exit with error if file not provided
# and there's nothing being piped
if [[ ! -f "$1" ]] && [[ -t 0 ]]; then
printf '%s\n' "nothing to read" >&2
exit 1
fi
# if reading from a pipe, duplicate /dev/stdin
# then redirect the terminal back in order to read keypresses
[[ ! -t 0 ]] && exec 3<&0 && exec </dev/tty
# use the size of the terminal to determine the length of the scroll region
# and the width to fold text to
get_terminal_size() {
scroll_size=$((LINES - 1))
width=$COLUMNS
}
read_file() {
mapfile -t file < "${file_name:-/proc/self/fd/3}"
}
# fold lines from file to $width, in a separate array (text)
fold_text() {
# without setting IFS, "${file[*]}" is all one line
IFS=$'\n'
while read -r -n "$width" file_line; do
text+=("$file_line")
done <<< "${file[*]}"
text_length="${#text[@]}"
last_line=$((text_length-scroll_size))
}
setup_terminal() {
# save cursor position, then switch to alternate screen
printf '\e7\e[?1049h'
}
# move cursor to start/end of scroll region, or the prompt at the bottom
cursor() {
case "$1" in
start)
printf '\e[H'
;;
end)
printf '\e[%s;H' "$scroll_size"
;;
prompt)
printf '\e[%s;H' "$LINES"
;;
esac
}
# print a specified line of the folded text
# if that line is outside the contents of $text, print ~
print_line() {
if [ "$1" -lt 0 ] || [ "$1" -gt "$text_length" ]; then
printf '~'
else
printf '%s' "${text[$1]}"
fi
}
# fill the scroll region with lines of $text
# starting at $line
draw_lines() {
cursor prompt
printf '\e[1J'
cursor start
print_line "$line"
for ((i=line+1; i<line+scroll_size; i++)); do
printf '\n'
print_line "$i"
done
}
# not used for anything yet, but looks like less
# show if end of file reached
draw_prompt() {
cursor prompt
# clear line
printf '\e[2K'
if [ "$line" -ge "$last_line" ]; then
printf '\e[7m%s\e[0m' "(END)"
else
printf '%s' ":"
fi
}
draw_screen() {
# set scroll region, which moves cursor to start
printf '\e[1;%sr' "$scroll_size"
draw_lines
draw_prompt
}
# print file name as given in argument
# intended to be erased when any key is pressed
print_message() {
printf '\r\e[7m%s' "$file_name"
[ "$line" -ge "$last_line" ] && printf " (END)"
printf '\e[m'
}
# scroll the displayed text down, and print the next line up
scroll_up() {
cursor start
line=$((line-1))
printf '\EM'
print_line "$line"
}
# scroll the displayed text up, and print the next line down
scroll_down() {
cursor end
line=$((line+1))
printf '\n'
print_line "$((line-1+scroll_size))"
}
quit() {
# reset scroll region, switch back to main screen, restore cursor
printf '\e[;r\e[?1049l\e8'
exit
}
# infinite loop waiting for single character input
# or escape sequences from (e.g) arrow keys
key_loop() {
while read -rs -n 1; do
# when escape recieved, read any characters sent immediately after
# if there are none, then the escape key was pressed, but is ignored
if [ "$REPLY" = $'\e' ]; then
read -rs -t 0.0001
fi
case $REPLY in
k|"[A")
# if at start of the text, do nothing
[ ! "$line" = 0 ] && scroll_up
draw_prompt
;;
j|"[B")
# if at the end of text, do nothing
[ ! "$line" -ge "$last_line" ] && scroll_down
draw_prompt
;;
"[5~")
# page up
if [ ! "$line" = 0 ]; then
line=$((line-scroll_size))
[ "$line" -lt 0 ] && line=0
draw_lines
fi
draw_prompt
;;
"[6~")
# page down
if [ ! "$line" -ge "$last_line" ]; then
line=$((line+scroll_size))
[ "$line" -gt "$last_line" ] && line="$last_line"
draw_lines
fi
draw_prompt
;;
g)
if [ ! "$line" = 0 ]; then
line=0
draw_lines
fi
draw_prompt
;;
G)
if [ ! "$line" = "$last_line" ]; then
line="$last_line"
draw_lines
fi
draw_prompt
;;
q)
break
;;
*)
draw_prompt
;;
esac
done
}
main() {
read_file
fold_text
setup_terminal
draw_screen
[ "$file_name" ] && print_message
key_loop
quit
}
trap quit EXIT INT HUP
# set or initialise variables
declare -a file text
file_name="$1"
line=0
get_terminal_size
main