#!/usr/bin/env bash # timer-pick — TUI time picker # Install: ~/.config/scripts/timer-pick (companion: timer-run in same dir) SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" set -uo pipefail ESC=$'\e'; CSI="${ESC}[" cup() { printf "${CSI}%d;%dH" "$1" "$2"; } cls() { printf "${CSI}2J${CSI}H"; } hide() { printf "${CSI}?25l"; } show() { printf "${CSI}?25h"; } b="${CSI}1m" ; d="${CSI}2m" ; r="${CSI}0m" cy="${CSI}96m"; gn="${CSI}92m" yl="${CSI}93m"; wh="${CSI}97m"; gy="${CSI}90m" rv="${CSI}7m" FIELDS=(HH MM SS) MAX=(99 59 59) VALUES=(0 0 0) ACTIVE=1 LABEL="" TW=34; IW=32; CW=10 BR=2; BC=2 hl() { printf '─%.0s' $(seq 1 "$1"); } redraw() { local row=$BR col=$BC cup $row $col; printf "${d}╭$(hl $IW)╮${r}" (( row++ )); cup $row $col printf "${d}│${r}${b}${cy} ⏱ Set Timer%-*s${r}${d}│${r}" $(( IW - 14 )) "" (( row++ )); cup $row $col printf "${d}├$(hl $CW)┬$(hl $CW)┬$(hl $CW)┤${r}" # labels (( row++ )); cup $row $col; printf "${d}│${r}" for i in 0 1 2; do local lpad=$(( (CW - 2) / 2 )) rpad=$(( CW - 2 - (CW-2)/2 )) (( i == ACTIVE )) \ && printf "%*s${yl}${b}${FIELDS[$i]}${r}%*s" $lpad "" $rpad "" \ || printf "%*s${gy}${FIELDS[$i]}${r}%*s" $lpad "" $rpad "" (( i < 2 )) && printf "${d}│${r}" done; printf "${d}│${r}" # up arrows (( row++ )); cup $row $col; printf "${d}│${r}" for i in 0 1 2; do local lpad=$(( (CW-1) / 2 )) rpad=$(( CW - 1 - (CW-1)/2 )) (( i == ACTIVE )) \ && printf "%*s${yl}${b}▲${r}%*s" $lpad "" $rpad "" \ || printf "%*s${gy}▲${r}%*s" $lpad "" $rpad "" (( i < 2 )) && printf "${d}│${r}" done; printf "${d}│${r}" # values (( row++ )); cup $row $col; printf "${d}│${r}" for i in 0 1 2; do local vstr; printf -v vstr '%02d' "${VALUES[$i]}" local lpad=$(( (CW-4) / 2 )) rpad=$(( CW - 4 - (CW-4)/2 )) (( i == ACTIVE )) \ && printf "%*s${rv}${wh}${b} %s ${r}%*s" $lpad "" "$vstr" $rpad "" \ || printf "%*s${d} %s ${r}%*s" $lpad "" "$vstr" $rpad "" (( i < 2 )) && printf "${d}│${r}" done; printf "${d}│${r}" # down arrows (( row++ )); cup $row $col; printf "${d}│${r}" for i in 0 1 2; do local lpad=$(( (CW-1) / 2 )) rpad=$(( CW - 1 - (CW-1)/2 )) (( i == ACTIVE )) \ && printf "%*s${yl}${b}▼${r}%*s" $lpad "" $rpad "" \ || printf "%*s${gy}▼${r}%*s" $lpad "" $rpad "" (( i < 2 )) && printf "${d}│${r}" done; printf "${d}│${r}" (( row++ )); cup $row $col; printf "${d}├$(hl $IW)┤${r}" (( row++ )); cup $row $col local ld="${LABEL:-(optional)}" printf "${d}│${r} ${gy}label:${r} ${cy}%-*s${r}${d}│${r}" $(( IW - 8 )) "$ld" (( row++ )); cup $row $col; printf "${d}├$(hl $IW)┤${r}" (( row++ )); cup $row $col printf "${d}│${r} ${gy}h/l${r} field ${gy}j/k${r} value ${gy}[N]j/k${r} ${d}│${r}" (( row++ )); cup $row $col printf "${d}│${r} ${gy}i${r} label ${gy}0/G${r} min/max ${gn}↵${r} start ${d}│${r}" (( row++ )); cup $row $col; printf "${d}╰$(hl $IW)╯${r}" cup $(( row + 1 )) 1 } read_label() { show local lrow=$(( BR + 9 )) lcol=$(( BC + 9 )) cup $lrow $lcol; printf "${CSI}K" local buf="" ch while IFS= read -rsn1 ch; do case "$ch" in $'\n'|$'\r'|$'\e') break ;; $'\x7f'|$'\b') [[ -n $buf ]] && buf="${buf%?}" cup $lrow $lcol; printf "${CSI}K%s" "$buf" ;; *) [[ ${#buf} -lt $(( IW - 10 )) ]] && buf+="$ch" && printf "%s" "$ch" ;; esac done LABEL="$buf"; hide } cleanup() { # only reset tty if we actually changed it [[ "${TTY_SAVED:-0}" == "1" ]] && stty sane 2>/dev/null show cls tput cup 0 0 2>/dev/null } main() { # Wait for kitty to fully attach the pty — critical when launched via Hyprland # keybind where the window and pty aren't synchronised at script start. local tries=0 until [[ -t 0 && -t 1 ]]; do sleep 0.05 (( tries++ )) (( tries > 40 )) && exit 1 # give up after 2s done hide; cls stty -echo -icanon min 1 time 0 TTY_SAVED=1 trap cleanup EXIT INT TERM redraw local ch seq count_buf="" while true; do IFS= read -rsn1 ch || continue if [[ $ch == $'\e' ]]; then IFS= read -rsn2 -t 0.05 seq || seq="" ch="${ch}${seq}" fi if [[ $ch =~ ^[1-9]$ ]]; then count_buf+="$ch"; redraw; continue; fi local N=1 [[ -n $count_buf && $count_buf =~ ^[0-9]+$ ]] && N=$count_buf case "$ch" in $'\t'|$'\e[C'|l) ACTIVE=$(( (ACTIVE + 1) % 3 )); count_buf="" ;; $'\e[Z'|$'\e[D'|h) ACTIVE=$(( (ACTIVE + 2) % 3 )); count_buf="" ;; '^') ACTIVE=0; count_buf="" ;; '$') ACTIVE=2; count_buf="" ;; $'\e[A'|k) local mx=${MAX[$ACTIVE]} v=${VALUES[$ACTIVE]} local nv=$(( v + N )); (( nv > mx )) && nv=$mx VALUES[$ACTIVE]=$nv; count_buf="" ;; $'\e[B'|j) local v=${VALUES[$ACTIVE]} local nv=$(( v - N )); (( nv < 0 )) && nv=0 VALUES[$ACTIVE]=$nv; count_buf="" ;; 0) if [[ -n $count_buf ]]; then count_buf+="0"; redraw; continue; fi VALUES[$ACTIVE]=0; count_buf="" ;; G) VALUES[$ACTIVE]=${MAX[$ACTIVE]}; count_buf="" ;; i) count_buf=""; read_label ;; ''|$'\n') count_buf="" local total=$(( VALUES[0]*3600 + VALUES[1]*60 + VALUES[2] )) if (( total == 0 )); then redraw; continue; fi TTY_SAVED=0 stty sane 2>/dev/null; show; cls; tput cup 0 0 2>/dev/null # SIG_IGN for SIGHUP is inherited through exec, eliminating the race # between fork() and setsid() creating its own session. Without this, # kitty closing the pty on exit sends SIGHUP to the foreground process # group (which includes the child in non-interactive bash) before setsid # has a chance to move it to a new session. ( trap '' SIGHUP; exec setsid bash "$SCRIPTS_DIR/timer-run" "$total" "$LABEL" /dev/null 2>&1 ) & exit 0 ;; q|Q) exit 0 ;; *) count_buf="" ;; esac redraw done } main