summaryrefslogtreecommitdiff
path: root/private_dot_local/bin/executable_tspreed
diff options
context:
space:
mode:
authorLibravatar Martin Michalec <martin@michalec.dev>2026-02-11 02:47:33 +0000
committerLibravatar Martin Michalec <martin@michalec.dev>2026-02-11 02:47:33 +0000
commit2c485ce2b43bd810a88278215b771136a2a17881 (patch)
tree3fb74338b87553d898af7b1795de7322cbc5af63 /private_dot_local/bin/executable_tspreed
parentadd xdg (diff)
downloaddotfiles-2c485ce2b43bd810a88278215b771136a2a17881.tar.gz
add scripts
Diffstat (limited to 'private_dot_local/bin/executable_tspreed')
-rw-r--r--private_dot_local/bin/executable_tspreed441
1 files changed, 441 insertions, 0 deletions
diff --git a/private_dot_local/bin/executable_tspreed b/private_dot_local/bin/executable_tspreed
new file mode 100644
index 0000000..214c367
--- /dev/null
+++ b/private_dot_local/bin/executable_tspreed
@@ -0,0 +1,441 @@
1#!/bin/sh
2
3# Copyright (c) 2020-2024 Nicholas Ivkovic.
4# Licensed under the GNU General Public License version 3 or later. See ./LICENSE, or <https://gnu.org/licenses/gpl.html> if more recent, for details.
5# This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
6
7# Notes about disabled ShellChecks:
8# SC1090 -- Config files do not need to/cannot be checked as they are user-written. Default config is checked separately
9# SC2015 -- Disabled where behaviour resembling an if-then-else is not intended
10# SC3045 -- Disabled where support for non-POSIX options are tested for and handled
11
12# Exit from caught signals
13exit_catch() {
14 [ "$term_presenting" = true ] && end_term 8
15 ! [ "$quietexit" = true ] && printf "%s/%s\n" "$word_num" "$input_words"
16 exit 8
17}
18
19# Exit from error
20# $1 return value
21# $2 error message
22exit_err() {
23 [ "$term_presenting" = true ] && end_term "$1"
24 [ "$1" -eq 3 ] && printf "Terminal does not support capability '%s'\n" "$2" >&2 || printf "%s\n" "$2" >&2
25 [ "$1" -eq 1 ] && printf "Usage: tspreed [-lqhikfbv] [-w wpm] [-n num] [-p style] [-c color] [-K path]\n" >&2
26 printf "See 'man tspreed'\n" >&2
27 exit "$1"
28}
29
30# Move cursor to position
31# $1 row
32# $2 column
33term_move() {
34 tput cup "$1" "$2" 2>/dev/null || printf "%b[%s;%sH" "\033" "$1" "$2"
35}
36
37# Set foreground color
38# $1 color
39term_color() {
40 tput setaf "$1" 2>/dev/null || {
41 # shellcheck disable=SC2015
42 [ "$1" -le 7 ] && printf "%b[3%sm" "\033" "$1" || exit_err 3 "setaf"
43 }
44}
45
46# Move to top row and clear
47term_init_top() {
48 printf "%s%s" "${term_home}" "${term_clear_line}"
49}
50
51# Move to bottom row and clear
52term_init_bottom() {
53 term_move "$term_height" 0 && printf "%s" "${term_clear_line}"
54}
55
56# Initialise output
57init_presentation() {
58 printf "%s" "$term_clear"
59 [ -n "$focuspointer" ] && [ "$focus" = true ] && {
60 term_move $((term_y_center - 1)) "$term_x_center" && printf "%s" "$focus_pointer_1"
61 term_move $((term_y_center + 1)) "$term_x_center" && printf "%s" "$focus_pointer_2"
62 }
63}
64
65# Initialise terminal output
66init_term() {
67 term_height="$(tput lines)" || {
68 [ -n "$LINES" ] && [ "$LINES" -gt 0 ] 2>/dev/null && term_height="$LINES" || term_height=24
69 }
70 term_width="$(tput cols)" || {
71 [ -n "$COLUMNS" ] && [ "$COLUMNS" -gt 0 ] 2>/dev/null && term_width="$COLUMNS" || term_width=80
72 }
73 term_x_center=$((term_width / 2))
74 term_y_center=$((term_height / 2))
75 init_presentation
76}
77
78# Finish terminal output
79# $1 return value of exit function
80end_term() {
81 term_presenting=false
82 if [ "$term_session" = true ]; then
83 tput rmcup 2>/dev/null || exit_err 3 "rmcup"
84 else
85 if [ "$1" -eq 8 ]; then
86 term_move $((term_height - 1)) 0
87 else
88 term_move "$term_height" 0
89 fi
90 fi
91 printf "%s" "$term_reset"
92 [ "$hidecursor" = true ] && {
93 tput cnorm 2>/dev/null || exit_err 3 "cnorm"
94 }
95}
96
97# Get current epoch timestamp in milliseconds
98get_date_ms() {
99 # Epoch timestamp without '%s' taken from https://www.etalabs.net/sh_tricks.html
100 [ "$non_posix_date_s" = true ] && curr_epoch_sec="$(date -u "+%s")" || curr_epoch_sec="$(($(date -u "+((%Y-1600)*365+(%Y-1600)/4-(%Y-1600)/100+(%Y-1600)/400+1%j-1000-135140)*86400+(1%H-100)*3600+(1%M-100)*60+(1%S-100)")))"
101 echo "$(($(date -u "+${curr_epoch_sec}%N / 1000000")))"
102}
103
104# Get substring
105# $1 string
106# $2 index (from 1) to get from
107# $3 number of characters to get (all if not provided)
108get_substr() {
109 if [ "$use_cut" = true ]; then
110 if [ -n "$3" ]; then
111 printf "%s" "$1" | cut -c "$2"-$(($2 - 1 + $3))
112 else
113 printf "%s" "$1" | cut -c "$2"-
114 fi
115 else
116 if [ -n "$3" ]; then
117 printf "%s" "$1" | awk -v pos="$2" -v len="$3" '{ printf("%s", substr($0, pos, len)) }'
118 else
119 printf "%s" "$1" | awk -v pos="$2" '{ printf("%s", substr($0, pos)) }'
120 fi
121 fi
122}
123
124# Perform floating point calculation
125# $1 calculation to perform
126calc_float() {
127 if [ "$use_bc" = true ]; then
128 # No floating point calculation in script should return a negative result, so no handling of negative results implemented
129 printf "scale=3; %s\n" "$1" | bc | sed 's/^\./0./'
130 else
131 awk "BEGIN { print ${1} }"
132 fi
133}
134
135# $1 Word number
136# $2 Total number of words
137print_presentation_info() {
138 printf "%s%b%s" "${1}/${2}" "\t" "$((($1 * 100) / $2))%"
139}
140
141# Import configs
142config_global="/etc/tspreed/tspreed.rc"
143config_local="${XDG_CONFIG_HOME:-$HOME/.config}/tspreed/tspreed.rc"
144# shellcheck disable=SC1090
145[ -f "$config_global" ] && . "$config_global"
146# shellcheck disable=SC1090
147[ -f "$config_local" ] && . "$config_local"
148
149# Init
150input=
151term_presenting=false
152word_first=true
153word_num=0
154word_paused=false
155word_len_average=5 # Based on average English word length of 5.1 letters
156focus_pointer_1=
157focus_pointer_2=
158prev_word_len_exceed=false
159
160# Catch signals
161trap "exit_catch" 2 # SIGINT
162trap "exit_catch" 3 # SIGQUIT
163trap "exit_catch" 6 # SIGABRT
164trap "exit_catch" 14 # SIGALRM
165trap "exit_catch" 15 # SIGTERM
166trap "init_term" WINCH # Terminal emulator resize
167
168# Convert long options to short options
169for arg in "$@"; do
170 shift
171 case "$arg" in
172 "--wpm") set -- "$@" "-w" ;;
173 "--num-start") set -- "$@" "-n" ;;
174 "--separators") set -- "$@" "-s" ;;
175 "--length-vary") set -- "$@" "-l" ;;
176 "--quiet-exit") set -- "$@" "-q" ;;
177 "--hide-cursor") set -- "$@" "-h" ;;
178 "--progress-info") set -- "$@" "-i" ;;
179 "--key-controls") set -- "$@" "-k" ;;
180 "--focus") set -- "$@" "-f" ;;
181 "--focus-pointer") set -- "$@" "-p" ;;
182 "--focus-bold") set -- "$@" "-b" ;;
183 "--focus-color") set -- "$@" "-c" ;;
184 "--dev-key-input") set -- "$@" "-K" ;;
185 "--version") set -- "$@" "-v" ;;
186 *) set -- "$@" "$arg" ;;
187 esac
188done
189OPTIND=1
190
191# Parse options
192while getopts ":w:n:s:qhiklfp:bc:K:vV" opt; do
193 case "$opt" in
194 w) wpm=$OPTARG ;;
195 n) numstart="$OPTARG" ;;
196 s) separators="$OPTARG" ;;
197 l) lengthvary=true ;;
198 q) quietexit=true ;;
199 h) hidecursor=true ;;
200 i) proginfo=true ;;
201 k) keycontrols=true ;;
202 f) focus=true ;;
203 p) focuspointer="$OPTARG" ;;
204 b) focusbold=true ;;
205 c) focuscolor="$OPTARG" ;;
206 K) devkeyinput="$OPTARG" ;;
207 v|V) printf "tspreed 2.6.2\n" && exit 0 ;;
208 \?) exit_err 1 "Invalid option '-${OPTARG}'" ;;
209 :) exit_err 1 "Option -${OPTARG} requires an argument." ;;
210 esac
211done
212
213# Validate and set keyboard input device/path
214[ -z "$devkeyinput" ] && devkeyinput='/dev/tty'
215[ -e "$devkeyinput" ] || exit_err 1 "Invalid keyboard input path '${devkeyinput}'"
216
217# Validate word speed
218[ -z "$wpm" ] && exit_err 1 "WPM not set"
219! { [ "$wpm" -ge 1 ] && [ "$wpm" -le 60000 ]; } 2>/dev/null && exit_err 1 "Invalid WPM '${wpm}'"
220
221# Validate and set nth word as starting word
222[ -n "$numstart" ] && ! [ "$numstart" -ge 1 ] 2>/dev/null && exit_err 1 "Invalid starting word position '${numstart}'"
223[ -z "$numstart" ] && numstart=1
224
225# Validate focus letter options
226[ "$focus" = true ] && {
227 # Validate and set focus letter pointers
228 [ -n "$focuspointer" ] && [ "$focuspointer" != "none" ] && {
229 case "$focuspointer" in
230 line) focus_pointer_1="|" && focus_pointer_2="|" ;;
231 point) focus_pointer_1="v" && focus_pointer_2="^" ;;
232 *) exit_err 1 "Invalid focus letter pointer '${focuspointer}'" ;;
233 esac
234 }
235 # Validate focus letter color
236 [ -n "$focuscolor" ] && ! { [ "$focuscolor" -ge 0 ] && [ "$focuscolor" -le 255 ]; } 2>/dev/null && exit_err 1 "Invalid focus letter color '${focuscolor}'"
237}
238
239# Set IFS
240IFS="$(printf "%s%b" "$IFS" "$separators")"
241
242# Determine non-POSIX capabilities
243# shellcheck disable=SC3045
244read -rs -n 0 -t 0.0 2>/dev/null && non_posix_read=true || non_posix_read=false
245sleep 0e-3 2>/dev/null && non_posix_sleep_enotation=true || non_posix_sleep_enotation=false
246sleep 0.0 2>/dev/null && non_posix_sleep_fractional=true || non_posix_sleep_fractional=false
247[ -n "$(command -v usleep)" ] && non_posix_usleep=true || non_posix_usleep=false
248[ "$(date "+%N")" -ge 0 ] 2>/dev/null && non_posix_date_n=true || non_posix_date_n=false
249[ "$(date "+%s")" -ge 0 ] 2>/dev/null && non_posix_date_s=true || non_posix_date_s=false
250
251# Validate required non-POSIX capabilities
252if [ "$keycontrols" = true ]; then
253 [ "$non_posix_read" = false ] && exit_err 2 "System or shell does not support non-POSIX read(1)"
254 [ "$non_posix_date_n" = false ] && exit_err 2 "System or shell does not support non-POSIX date(1) '%N' format"
255else
256 [ "$non_posix_sleep_enotation" = false ] && [ "$non_posix_sleep_fractional" = false ] && [ "$non_posix_usleep" = false ] && [ "$non_posix_date_n" = false ] && exit_err 2 "System or shell does not support at least one of the required non-POSIX features or commands"
257fi
258
259# Determine terminal capabilities
260term_reset="$(tput sgr0 2>/dev/null || printf "%b[m" "\033")"
261term_bold="$(tput bold 2>/dev/null || printf "%b[1m" "\033")"
262term_home="$(tput home 2>/dev/null || term_move 0 0)"
263term_clear_line="$(tput el 2>/dev/null || printf "%b[K" "\033")"
264[ -n "$(tput smcup 2>/dev/null)" ] && [ -n "$(tput rmcup 2>/dev/null)" ] && term_session=true || term_session=false
265[ "$term_session" = false ] && term_clear="$(printf "%s%s" "$term_home" "$(tput ed 2 2>/dev/null || printf "%b[2J" "\033")")" || term_clear="$(tput clear)" 2>/dev/null
266
267# Determine wide character capabilities
268wide_char="$(printf "%b" "\0316\0251")"
269[ "${#wide_char}" -eq 1 ] && wide_param_expansion=true || wide_param_expansion=false
270[ "$(printf "%s" "$wide_char" | cut -c 1)" = "$wide_char" ] && wide_cut=true || wide_cut=false
271
272# Get input
273while read -r inp; do
274 input="${input}${inp} "
275done
276
277# Get input info
278input_info="$(printf "%s" "$input" | wc -wmc)"
279input_words="$(echo "$input_info" | awk '{ print $1 }')"
280[ "$(echo "$input_info" | awk '{ print $3 - $2 }')" -gt 0 ] && wide_input=true || wide_input=false
281
282# Determine command usage
283{ [ "$wide_input" = false ] || [ "$wide_param_expansion" = true ]; } && use_param_expansion=true || use_param_expansion=false
284{ [ "$wide_input" = false ] || [ "$wide_cut" = true ]; } && use_cut=true || use_cut=false
285[ -n "$(command -v bc)" ] && use_bc=true || use_bc=false
286
287# Init presentation
288if [ "$term_session" = true ]; then
289 tput smcup 2>/dev/null || exit_err 3 "smcup"
290else
291 printf "%s" "$term_clear"
292fi
293term_presenting=true
294[ "$hidecursor" = true ] && {
295 tput civis 2>/dev/null || exit_err 3 "civis"
296}
297init_term
298
299# Present
300for word in $input; do
301
302 # Init word
303 [ "$use_param_expansion" = true ] && word_len=${#word} || word_len="$(printf "%s" "$word" | wc -m)"
304 [ "$word_len" -le 0 ] && continue
305 word_num=$((word_num + 1))
306 [ "$word_num" -lt "$numstart" ] && continue
307 word_x=0
308
309 # Clear word
310 if [ "$prev_word_len_exceed" = true ]; then
311 init_presentation
312 else
313 term_move "$term_y_center" 0
314 printf "%s" "$term_clear_line"
315 fi
316
317 [ "$non_posix_date_n" = true ] && word_start_date="$(get_date_ms)"
318
319 # Highlighted focus letter
320 if [ "$focus" = true ]; then
321
322 # Set focus letter
323 case "$word_len" in
324 1|2) word_focus_pos=1 ;;
325 3|4|5) word_focus_pos=2 ;;
326 6|7|8|9) word_focus_pos=3 ;;
327 10|11|12|13) word_focus_pos=4 ;;
328 *) word_focus_pos=5 ;;
329 esac
330 # Set horizontal position of word
331 word_x=$((term_x_center - word_focus_pos + 1))
332 term_move "$term_y_center" "$word_x"
333
334 # Formatted focus letter
335 if [ "$focusbold" = true ] || [ -n "$focuscolor" ]; then
336
337 # Print start of word
338 [ "$word_focus_pos" -gt 1 ] && printf "%s" "$(get_substr "$word" 1 $((word_focus_pos - 1)))"
339
340 # Print focus letter
341 [ "$focusbold" = true ] && printf "%s" "$term_bold"
342 [ -n "$focuscolor" ] && term_color "$focuscolor"
343 printf "%s" "$(get_substr "$word" "$word_focus_pos" 1)"
344 printf "%s" "$term_reset"
345
346 # Print end of word
347 printf "%s" "$(get_substr "$word" $((word_focus_pos + 1)))"
348
349 # No formatting
350 else
351 printf "%s" "$word"
352 fi
353
354 # No focus letter highlighting
355 else
356 # Set horizontal position of word
357 [ $((word_x + word_len)) -le "$term_width" ] && word_x=$((term_x_center - (word_len / 2)))
358 term_move "$term_y_center" "$word_x"
359 # Print
360 printf "%s" "$word"
361 fi
362
363 # Print presentation information
364 [ "$proginfo" = true ] && term_init_top && print_presentation_info "$word_num" "$input_words"
365
366 # End word
367 term_move "$term_height" 0
368 [ $((word_x + word_len)) -gt "$term_width" ] && prev_word_len_exceed=true || prev_word_len_exceed=false
369
370 # Calculate sleep time
371 # No sleep time calculation should return a negative result, so no handling of negative sleep times implemented
372 sleep_ms_float=$(calc_float "1000 / (${wpm} / 60)")
373 [ "$lengthvary" = true ] && sleep_ms_float="$(calc_float "${sleep_ms_float} * ($([ "$word_len" -gt "$word_len_average" ] && echo "$word_len" || echo $((word_len_average - 1))) / ${word_len_average})")"
374 # Account for word processing time if possible
375 [ "$non_posix_date_n" = true ] && sleep_ms_float=$(calc_float "${sleep_ms_float} + ${word_start_date} - $(get_date_ms)")
376
377 # Convert sleep time to int
378 sleep_ms="${sleep_ms_float%.*}"
379
380 # Sleep first word for minimum 1 second
381 [ "$word_first" = true ] && {
382 word_first=false
383 [ "$sleep_ms" -lt 1000 ] && sleep_ms=1000
384 }
385
386 # Sleep with keyboard controls
387 if [ "$keycontrols" = true ]; then
388 sleep_date=$(($(get_date_ms) + sleep_ms))
389 while [ "$(get_date_ms)" -lt "$sleep_date" ] || [ "$word_paused" = true ]; do
390 # shellcheck disable=SC3045
391 read -rs -n 1 -t 0.001 input_key < "$devkeyinput"
392 case "$input_key" in
393 i)
394 if [ "$proginfo" = true ]; then
395 proginfo=false
396 term_init_top
397 else
398 proginfo=true
399 term_init_top && print_presentation_info "$word_num" "$input_words"
400 fi
401 ;;
402 p)
403 if [ "$word_paused" = true ]; then
404 word_paused=false
405 term_init_bottom
406 # Sleep after unpause for minimum 0.5 seconds
407 [ "$sleep_ms" -lt 500 ] && sleep_ms=500
408 sleep_date=$(($(get_date_ms) + sleep_ms))
409 else
410 word_paused=true
411 term_init_bottom && printf "[ Paused ]"
412 fi
413 ;;
414 q)
415 exit_catch
416 ;;
417 esac
418 done
419 term_init_bottom
420 # Sleep only
421 elif [ "$sleep_ms" -gt 0 ]; then
422 if [ "$non_posix_sleep_enotation" = true ]; then
423 sleep "${sleep_ms}e-3"
424 elif [ "$non_posix_usleep" = true ]; then
425 usleep $((sleep_ms * 1000))
426 elif [ "$non_posix_sleep_fractional" = true ]; then
427 sleep "$(calc_float "${sleep_ms} / 1000")"
428 elif [ "$non_posix_date_n" = true ]; then
429 sleep_date=$(($(get_date_ms) + sleep_ms))
430 while [ "$(get_date_ms)" -lt "$sleep_date" ]; do
431 :
432 done
433 else
434 exit_err 2 "System or shell does not support at least one of the required non-POSIX features or commands"
435 fi
436 fi
437
438done
439
440sleep 1 && end_term 0
441exit 0