From faf5df97895b05939e314cc55634b7f90d80e867 Mon Sep 17 00:00:00 2001 From: Vikramjit Borah Date: Tue, 3 Nov 2026 19:00:30 +0530 Subject: [PATCH] terminal-helper: honor $TERMINAL and harden terminal launching - Honor $TERMINAL when set to a supported emulator. - Add --nofork to konsole to prevent detached launcher exit on KDE Plasma. - Add --wait-for-single-instance-window-close for kitty and --no-remote for lxterminal. - Synchronize execution with started/finished handshake in temp directory, eliminating premature script deletion. - Fix kgx parent process signalling. Fixes #87, fixes #96 --- src/terminal-helper | 148 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 24 deletions(-) diff --git a/src/terminal-helper b/src/terminal-helper index ad70a2d..4293524 100755 --- a/src/terminal-helper +++ b/src/terminal-helper @@ -13,8 +13,13 @@ # 2. Distribution-specific mechanisms come next, e.g. x-terminal-emulator # 3. The terminal emulator with best accessibility comes first. # 4. No order is guaranteed/desired for the remaining terminal emulators. +# +# Exit status: +# 0 the command finished +# 1 no terminal emulator was found +# 2 no terminal emulator could be started +# 3 a terminal was started, but the command did not run or did not finish -set -e LAUNCHER_CMD=bash usage() { @@ -36,46 +41,141 @@ done shift $(($OPTIND - 1)) -file="$(mktemp)" -echo "$1" > "$file" +dir="$(mktemp -d)" || exit 1 +trap 'rm -rf -- "$dir"' EXIT + +# The command reports when it starts and when it finished, so this process can +# wait for terminals that hand the command to an already running instance +# (konsole without --nofork, lxterminal, kgx, ...) and return immediately, +# long before the command is even read. +file="$dir/command" +started="$dir/started" +finished="$dir/finished" cmd="${LAUNCHER_CMD} \"$file\"" -echo $cmd +printf '%s\n' "$cmd" + +notify() { + command -v notify-send >/dev/null && + notify-send -t 1500 --app-name=CachyOS "$1" "$2" +} -#declare -a terminals=(x-terminal-emulator mate-terminal gnome-terminal terminator xfce4-terminal urxvt rxvt termit Eterm aterm uxterm xterm roxterm termite lxterminal terminology st qterminal lilyterm tilix terminix konsole kitty guake tilda alacritty) terminal="" declare -A terminals=( ["alacritty"]="alacritty -e $cmd || LIBGL_ALWAYS_SOFTWARE=1 alacritty -e $cmd" - ["kitty"]="kitty $cmd" + ["kitty"]="kitty --wait-for-single-instance-window-close $cmd" ["ptyxis"]="ptyxis -- $cmd" - ["konsole"]="konsole -e $cmd" - ["kgx"]="kgx --wait -- $cmd" + ["konsole"]="konsole --nofork -e $cmd" + # --wait is declared but never read by gnome-console, so it does not + # actually wait; the script quits the terminal itself (see below). + ["kgx"]="kgx --wait -e $cmd" ["gnome-terminal"]="gnome-terminal --wait -- $cmd" ["xfce4-terminal"]="xfce4-terminal --disable-server --command '$cmd'" - ["lxterminal"]="lxterminal -e $cmd" + ["lxterminal"]="lxterminal --no-remote -e $cmd" ["xterm"]="xterm -e $cmd" ["st"]="st $cmd" ["foot"]="foot $cmd" + ["wezterm"]="wezterm start --always-new-process $cmd" + ["terminator"]="terminator -x $cmd" ["rio"]="rio -e $cmd" ["ghostty"]="ghostty -e $cmd" ) -declare -a term_order=("alacritty" "kitty" "ptyxis" "konsole" "kgx" "gnome-terminal" "xfce4-terminal" "lxterminal" "xterm" "st" "foot" "rio" "ghostty") - -if [ -z "$terminal" ] || ! command -v "$terminal" &> /dev/null; then -for entry in ${term_order[@]}; do - if command -v "$entry" > /dev/null 2>&1; then - # gnome-console does not exit on command completion so we need to kill - # it manually - [[ "$entry" == "kgx" ]] && echo "kill -SIGQUIT \$PPID 2>/dev/null" >> "$file" - terminal="$entry" - break; +declare -a term_order=("alacritty" "kitty" "ptyxis" "konsole" "kgx" "gnome-terminal" "xfce4-terminal" "lxterminal" "xterm" "st" "foot" "wezterm" "terminator" "rio" "ghostty") + +# 1. $TERMINAL must come first (per invariants). Only honor it when it is one +# of the terminals below, which guarantees the correct invocation syntax. +if [ -n "$TERMINAL" ]; then + preferred="${TERMINAL##*/}" + if [[ -n "${terminals[$preferred]+_}" ]] && command -v "$preferred" &>/dev/null; then + terminal="$preferred" fi -done fi -if [ -z "$terminal" ]; then - notify-send -t 1500 --app-name=CachyOS "No terminal installed" "Could not find a terminal emulator. Please install one." +# Ordered candidates: $TERMINAL first, then the known terminals that are +# installed. Launch failures fall through to the next candidate. +declare -a candidates=() +if [ -n "$terminal" ]; then + candidates+=("$terminal") +fi +for entry in "${term_order[@]}"; do + [[ "$entry" == "$terminal" ]] && continue + command -v "$entry" >/dev/null 2>&1 && candidates+=("$entry") +done + +if [ ${#candidates[@]} -eq 0 ]; then + notify "No terminal installed" "Could not find a terminal emulator. Please install one." exit 1 fi -eval "${terminals[${terminal}]}" 2>/dev/null || [[ "$terminal" != "kgx" ]] && { rm "$file"; exit 2; } -rm "$file" +failed=1 +for entry in "${candidates[@]}"; do + # Rewrite the command so a failed attempt doesn't leave terminal specific + # lines (e.g. the kgx kill) behind for the next candidate. + { + printf '%s\n' "echo \$\$ >\"$started\"" + printf '%s\n' "$1" + printf '%s\n' ": >\"$finished\"" + if [[ "$entry" == "kgx" ]]; then + # gnome-console does not exit on command completion so we need to + # kill it manually. Only a process that really is kgx may be + # signalled: when the command was handed to a running instance it + # is orphaned, and its parent is then an arbitrary ancestor + # (usually the user systemd manager), so killing $PPID would take + # the session down with it. + printf '%s\n' 'pid=$PPID' + printf '%s\n' 'while (( pid > 1 )); do' + printf '%s\n' ' [[ $(ps -o comm= -p "$pid" 2>/dev/null) == kgx ]] && { kill -SIGQUIT "$pid"; break; }' + printf '%s\n' ' pid=$(ps -o ppid= -p "$pid" 2>/dev/null)' + printf '%s\n' ' [[ -n $pid ]] || break' + printf '%s\n' 'done' + fi + # Normalize the shell exit status: some terminals (foot, st) propagate + # it, and a failed command must not be mistaken for a failed launch. + printf '%s\n' 'exit 0' + } >"$file" || exit 1 + + eval "${terminals[$entry]}" 2>/dev/null + rc=$? + + # The command finished: the status the terminal itself exited with does not + # matter (it is the command's status for foot and st, and 131 for kgx). + if [[ -e "$finished" ]]; then + failed=0 + break + fi + # 126/127 mean the command was canceled or not authorized (e.g. the user + # dismissed the pkexec prompt) — retrying would just prompt again. + if [[ $rc -eq 126 ]] || [[ $rc -eq 127 ]]; then + notify "Command canceled" "The command was not started." + exit 3 + fi + # Nothing has run yet. Give a terminal that returned early a moment to + # actually start the command; a failed launch is retried sooner. + ticks=150 + [[ $rc -ne 0 ]] && ticks=20 + for ((tick = 0; tick < ticks; tick++)); do + [[ -s "$started" ]] && break + sleep 0.1 + done + if [[ ! -s "$started" ]]; then + [[ $rc -ne 0 ]] && continue + notify "Command not started" "The terminal did not run the command." + exit 3 + fi + # ... and then wait for it to finish, so callers can rely on this process + # exiting only once the command is done. + read -r command_pid <"$started" + while [[ ! -e "$finished" ]] && [[ -d "/proc/$command_pid" ]]; do + sleep 0.2 + done + if [[ ! -e "$finished" ]]; then + notify "Command did not finish" "The terminal was closed before the command finished." + exit 3 + fi + failed=0 + break +done + +if ((failed)); then + notify "Failed to launch terminal" "Could not start a terminal emulator to run the command." + exit 2 +fi