File: book.sh 1 #!/bin/sh 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2025 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 -MKiCRS; } 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 [ "$1" = "--" ] && shift 55 56 # add the current screen height to negative height values 57 if [ "${height}" -lt 0 ]; then 58 height="$((height + $(tput lines)))" 59 fi 60 61 # use the current screen height minus 1, when a height either was not 62 # given explicitly, or is clearly too small 63 if [ "${height}" -lt 2 ]; then 64 height="$(($(tput lines) - 1))" 65 fi 66 67 if [ "${height}" -lt 2 ]; then 68 printf "screen/window isn't tall enough to show content\n" > /dev/stderr 69 exit 1 70 fi 71 72 command='awk' 73 if [ -e /usr/bin/gawk ]; then 74 command='gawk' 75 fi 76 77 awk ' 78 # ignore leading UTF-8 BOMs (byte-order marks) 79 FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 80 81 # carriage-returns will ruin side-by-side output, so remove them 82 { gsub(/\r$/, ""); print } 83 ' "$@" | 84 85 # before laying out lines side-by-side, expand all tabs, using 4 as the 86 # tabstop width 87 expand -t 4 | 88 89 ${command} -v height="${height}" ' 90 # remember all lines; carriage-returns are already removed 91 { lines[NR] = $0 } 92 93 # width counts items in the string given, ignoring ANSI-style sequences 94 function width(s) { 95 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", s) 96 return length(s) 97 } 98 99 END { 100 step = height - 1 101 if (NR <= step) { 102 for (i in lines) print lines[i] 103 exit 104 } 105 106 for (i = 1; i <= NR; i += 2 * step) { 107 for (j = 0; j < step; j++) { 108 # w = width(lines[i + j]) 109 # if (maxl < w) maxl = w 110 # w = width(lines[i + step + j]) 111 # if (maxr < w) maxr = w 112 113 l = lines[i + j] 114 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", l) 115 w = length(l) 116 if (maxl < w) maxl = w 117 118 l = lines[i + step + j] 119 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", l) 120 w = length(l) 121 if (maxr < w) maxr = w 122 } 123 } 124 125 # make a separator wide enough to match the length of any output line 126 sep = "································" 127 nsep = length(sep) 128 widest = maxl + 3 + maxr 129 while (nsep < widest) { 130 sep = sep sep 131 nsep *= 2 132 } 133 # separator is used directly, so match the needed width exactly 134 sep = substr(sep, 1, widest) 135 136 # make enough spaces to hand-pad lines later; these spaces can exceed 137 # the max-count needed, since they are always subsliced when used later 138 spaces = " " 139 nspaces = length(spaces) 140 while (nspaces < widest) { 141 spaces = spaces spaces 142 nspaces *= 2 143 } 144 145 # emit lines side by side 146 for (i = 1; i <= NR; i += 2 * step) { 147 # emit a page-bottom/separator line between page-pairs 148 if (i > 1) print sep 149 150 for (j = 0; j < step; j++) { 151 # bottom-pad last page-pair with empty lines, so page-scrolling 152 # on viewers like `less` stays in sync with the page boundaries 153 if (i + j > NR) { 154 print "" 155 continue 156 } 157 158 l = lines[i + j] 159 r = lines[i + step + j] 160 161 #printf "%-*s █ %s\n", maxl, l, r 162 163 # pick/emit lines side by side; hand-pad left pages to align 164 # ANSI-styled text correctly 165 # padl = substr(spaces, 1, maxl - width(l)) 166 s = l 167 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", s) 168 padl = maxl - length(s) 169 printf "%s%*s █ %s\n", l, padl, "", r 170 } 171 } 172 173 # end last page with an empty line, instead of the usual page-separator 174 if (NR % (2 * step) > 0) print "" 175 } 176 ' | less -MKiCRS