File: book.sh 1 #!/bin/sh 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2024 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the “Software”), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 # book [page-height...] [filenames...] 27 # 28 # Layout lines on 2 page-like side-by-side columns, just like a book. 29 # 30 # Book shows lays out text-lines the same way pairs of pages are laid out in 31 # books, letting you take advantage of wide screens. Every pair of pages ends 32 # with a special dotted line to visually separate it from the next pair. 33 # 34 # If you're using Linux or MacOS, you may find this cmd-line shortcut useful: 35 # 36 # # Like A Book lays lines as pairs of pages, the same way books do it 37 # lab() { book "$(($(tput lines) - 1))" "$@" | less -JMKiCRS; } 38 39 40 case "$1" in 41 -h|--h|-help|--help) 42 awk '/^# +book/, /^$/ { gsub(/^# ?/, ""); print }' "$0" 43 exit 0 44 ;; 45 esac 46 47 height=0 48 # detect a leading number, and use it as a height value 49 if [ "$(echo "$1" | grep -E '^[+-]?[0-9]+$' 2> /dev/null)" ]; then 50 height="$1" 51 shift 52 fi 53 54 # add the current screen height to negative height values 55 if [ "${height}" -lt 0 ]; then 56 height="$((${height} + $(tput lines)))" 57 fi 58 59 # use the current screen height minus 1, when a height either was not 60 # given explicitly, or is clearly too small 61 if [ "${height}" -lt 2 ]; then 62 height="$(($(tput lines) - 1))" 63 fi 64 65 if [ "${height}" -lt 2 ]; then 66 printf "screen/window isn't tall enough to show content\n" > /dev/stderr 67 exit 1 68 fi 69 70 awk ' 71 # ignore leading UTF-8 BOMs (byte-order marks) 72 FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 73 74 # carriage-returns will ruin side-by-side output, so remove them 75 { gsub(/\r$/, ""); print } 76 ' "$@" | 77 78 # before laying out lines side-by-side, expand all tabs, using 4 as the 79 # tabstop width 80 expand -t 4 | 81 82 awk -v height="${height}" ' 83 # remember all lines; carriage-returns are already removed 84 { lines[NR] = $0 } 85 86 # width counts items in the string given, ignoring ANSI-style sequences 87 function width(s) { 88 gsub(/\x1b\[([0-9]*[A-HJKST]|[0-9;]*m)/, "", s) 89 return length(s) 90 } 91 92 END { 93 step = height - 1 94 if (NR <= step) { 95 for (i in lines) print lines[i] 96 exit 97 } 98 99 for (i = 1; i <= NR; i += 2 * step) { 100 for (j = 0; j < step; j++) { 101 l = width(lines[i + j]) 102 if (maxl < l) maxl = l 103 l = width(lines[i + step + j]) 104 if (maxr < l) maxr = l 105 } 106 } 107 108 # make a separator wide enough to match the length of any output line 109 sep = "································" 110 nsep = length(sep) 111 widest = maxl + 3 + maxr 112 while (nsep < widest) { 113 sep = sep sep 114 nsep *= 2 115 } 116 # separator is used directly, so match the needed width exactly 117 sep = substr(sep, 1, widest) 118 119 # make enough spaces to hand-pad lines later; these spaces can exceed 120 # the max-count needed, since they are always subsliced when used later 121 spaces = " " 122 nspaces = length(spaces) 123 while (nspaces < widest) { 124 spaces = spaces spaces 125 nspaces *= 2 126 } 127 128 # emit lines side by side 129 for (i = 1; i <= NR; i += 2 * step) { 130 # emit a page-bottom/separator line between page-pairs 131 if (i > 1) print sep 132 133 for (j = 0; j < step; j++) { 134 # bottom-pad last page-pair with empty lines, so page-scrolling 135 # on viewers like `less` stays in sync with the page boundaries 136 if (i + j > NR) { 137 print "" 138 continue 139 } 140 141 l = lines[i + j] 142 r = lines[i + step + j] 143 144 #printf "%-*s █ %s\n", maxl, l, r 145 146 # pick/emit lines side by side; hand-pad left pages to align 147 # ANSI-styled text correctly 148 padl = substr(spaces, 1, maxl - width(l)) 149 printf "%s%s █ %s\n", l, padl, r 150 } 151 } 152 153 # end last page with an empty line, instead of the usual page-separator 154 if (NR % (2 * step) > 0) print "" 155 } 156 '