feat: implement __rand_string generator and update jobrunner to use tmux
This commit is contained in:
@@ -2,42 +2,63 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __jobrunner_sessions
|
||||
# __jobrunner_sessions [<tool>]
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Parses `screen -ls` into machine-readable rows, one per active session:
|
||||
# name, PID, state, and start time separated by tabs. Shared by jobrunner
|
||||
# and its completions so both agree on what a session is named. Prints
|
||||
# nothing when no sessions exist.
|
||||
# Parses `tmux list-sessions` or `screen -ls` into machine-readable rows,
|
||||
# one per active session: name, PID, state, and start time separated by tabs.
|
||||
# Shared by jobrunner and its completions so both agree on what a session is
|
||||
# named. Prints nothing when no sessions exist.
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Parsed successfully (including the zero-session case)
|
||||
# 1 screen is not installed
|
||||
# 1 tool (tmux or screen) is not installed
|
||||
#
|
||||
# RETURNS
|
||||
# One line per session: <name>\t<pid>\t<state>\t<started>
|
||||
#
|
||||
# EXAMPLE
|
||||
# __jobrunner_sessions
|
||||
# __jobrunner_sessions | string replace -r '\t.*$' ''
|
||||
function __jobrunner_sessions --description 'List active screen sessions as name/pid/state/started rows'
|
||||
command -q screen; or return 1
|
||||
|
||||
# screen -ls exits 1 when nothing is running; the parse handles that.
|
||||
for line in (command screen -ls 2>/dev/null)
|
||||
# Session rows are tab-indented and start with "<pid>.<name>".
|
||||
set -l m (string match -r '^\s+(\d+)\.([^\t]+)\t(.*)$' -- $line)
|
||||
or continue
|
||||
|
||||
# Trailing fields are parenthesized: "(<date>)\t(<state>)", and some
|
||||
# screen builds omit the date, so read the state off the end.
|
||||
set -l fields (string split \t -- $m[4])
|
||||
set -l state (string trim -c '()' -- $fields[-1])
|
||||
set -l started ""
|
||||
if test (count $fields) -gt 1
|
||||
set started (string trim -c '()' -- $fields[1])
|
||||
# __jobrunner_sessions tmux | string replace -r '\t.*$' ''
|
||||
function __jobrunner_sessions --description 'List active jobrunner sessions as name/pid/state/started rows'
|
||||
set -l tool $argv[1]
|
||||
if test -z "$tool"
|
||||
if command -q tmux
|
||||
set tool tmux
|
||||
else if command -q screen
|
||||
set tool screen
|
||||
else
|
||||
return 1
|
||||
end
|
||||
end
|
||||
|
||||
printf '%s\t%s\t%s\t%s\n' $m[3] $m[2] $state $started
|
||||
if test "$tool" = tmux
|
||||
command -q tmux; or return 1
|
||||
set -l tab (printf '\t')
|
||||
for line in (command tmux list-sessions -F "#{session_name}$tab#{pid}$tab#{?session_attached,Attached,Detached}$tab#{t:session_created}" 2>/dev/null)
|
||||
# tmux output is natively tab-separated.
|
||||
printf '%s\n' $line
|
||||
end
|
||||
else if test "$tool" = screen
|
||||
command -q screen; or return 1
|
||||
# screen -ls exits 1 when nothing is running; the parse handles that.
|
||||
for line in (command screen -ls 2>/dev/null)
|
||||
# Session rows are tab-indented and start with "<pid>.<name>".
|
||||
set -l m (string match -r '^\s+(\d+)\.([^\t]+)\t(.*)$' -- $line)
|
||||
or continue
|
||||
|
||||
# Trailing fields are parenthesized: "(<date>)\t(<state>)", and some
|
||||
# screen builds omit the date, so read the state off the end.
|
||||
set -l fields (string split \t -- $m[4])
|
||||
set -l state (string trim -c '()' -- $fields[-1])
|
||||
set -l started ""
|
||||
if test (count $fields) -gt 1
|
||||
set started (string trim -c '()' -- $fields[1])
|
||||
end
|
||||
|
||||
printf '%s\t%s\t%s\t%s\n' $m[3] $m[2] $state $started
|
||||
end
|
||||
else
|
||||
return 1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __rand_string [COMPONENTS/MODIFIERS]...
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Generates a random, memorable string using a sequence of specified word
|
||||
# categories and formatting modifiers. Words are pulled from curated
|
||||
# plain-text databases bundled in `data/words/`.
|
||||
#
|
||||
# Modifiers like `--separator` and `--case` are evaluated sequentially and
|
||||
# apply only to the components that follow them.
|
||||
#
|
||||
# Supported Components:
|
||||
# <category> A bundled word list (e.g. adjective, animal, color, name, noun)
|
||||
# digits=<N> N random digits (e.g. digits=3 -> 842)
|
||||
# literal=<text> A literal string component
|
||||
#
|
||||
# ARGUMENTS
|
||||
# -s, --separator=<sep> Delimiter for subsequent words (dash, underscore, dot, none, or literal chars)
|
||||
# -c, --case=<casing> Casing for subsequent words (lower, upper, title)
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 String generated successfully
|
||||
# 1 Unknown category or missing word list file
|
||||
#
|
||||
# EXAMPLE
|
||||
# __rand_string adjective animal
|
||||
# __rand_string --case=title color animal --separator=dot digits=4
|
||||
#
|
||||
# NOTES
|
||||
# Falls back to `random choice` if GNU `shuf` is missing, but `shuf` is
|
||||
# much faster for files with >1000 lines.
|
||||
function __rand_string --description 'Generate random, memorable strings from curated word databases'
|
||||
set -l sep "dash"
|
||||
set -l casing "lower"
|
||||
|
||||
# Locate the words directory relative to this function
|
||||
set -l words_dir ""
|
||||
if set -q __fish_config_dir
|
||||
set words_dir "$__fish_config_dir/data/words"
|
||||
else
|
||||
# Fallback if __fish_config_dir isn't available
|
||||
set words_dir "$HOME/.config/fish/data/words"
|
||||
end
|
||||
|
||||
set -l result ""
|
||||
set -l i 1
|
||||
|
||||
while test $i -le (count $argv)
|
||||
set -l arg $argv[$i]
|
||||
set -l handled 0
|
||||
|
||||
# 1. Parse inline modifiers
|
||||
switch $arg
|
||||
case -s --separator
|
||||
set i (math $i + 1)
|
||||
set sep $argv[$i]
|
||||
set handled 1
|
||||
case '--separator=*'
|
||||
set sep (string replace -- "--separator=" "" $arg)
|
||||
set handled 1
|
||||
case '-s*'
|
||||
set sep (string replace -r "^-s" "" $arg)
|
||||
set handled 1
|
||||
case -c --case
|
||||
set i (math $i + 1)
|
||||
set casing $argv[$i]
|
||||
set handled 1
|
||||
case '--case=*'
|
||||
set casing (string replace -- "--case=" "" $arg)
|
||||
set handled 1
|
||||
case '-c*'
|
||||
set casing (string replace -r "^-c" "" $arg)
|
||||
set handled 1
|
||||
end
|
||||
|
||||
if test $handled -eq 1
|
||||
set i (math $i + 1)
|
||||
continue
|
||||
end
|
||||
|
||||
# 2. Resolve separator alias
|
||||
set -l actual_sep $sep
|
||||
switch $sep
|
||||
case dash
|
||||
set actual_sep "-"
|
||||
case underscore
|
||||
set actual_sep "_"
|
||||
case dot
|
||||
set actual_sep "."
|
||||
case none empty
|
||||
set actual_sep ""
|
||||
end
|
||||
|
||||
# 3. Generate the component part
|
||||
set -l part ""
|
||||
switch $arg
|
||||
case 'digits=*'
|
||||
set -l n (string replace -- "digits=" "" $arg)
|
||||
if not string match -qr '^\d+$' -- $n
|
||||
set n 1
|
||||
end
|
||||
for idx in (seq $n)
|
||||
set part "$part"(random 0 9)
|
||||
end
|
||||
case 'literal=*'
|
||||
set part (string replace -- "literal=" "" $arg)
|
||||
case 'string=*'
|
||||
set part (string replace -- "string=" "" $arg)
|
||||
case '*'
|
||||
set -l db "$words_dir/$arg.txt"
|
||||
if not test -f "$db"
|
||||
echo "__rand_string: unknown category or file missing for '$arg'" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
# Fetch random line (shuf is fastest, random choice is portable fallback)
|
||||
if command -q shuf
|
||||
set part (command shuf -n 1 "$db")
|
||||
else
|
||||
set part (random choice (command cat "$db"))
|
||||
end
|
||||
end
|
||||
|
||||
# 4. Apply casing
|
||||
switch $casing
|
||||
case upper
|
||||
set part (string upper $part)
|
||||
case lower
|
||||
set part (string lower $part)
|
||||
case title
|
||||
set part (string replace -r '^(.)' '\U$1' (string lower $part))
|
||||
end
|
||||
|
||||
# 5. Append to result
|
||||
if test -n "$result"
|
||||
set result "$result$actual_sep$part"
|
||||
else
|
||||
set result "$part"
|
||||
end
|
||||
|
||||
set i (math $i + 1)
|
||||
end
|
||||
|
||||
if test -n "$result"
|
||||
echo $result
|
||||
end
|
||||
end
|
||||
+99
-36
@@ -5,24 +5,25 @@
|
||||
# 08-terminal-management
|
||||
#
|
||||
# DEPENDENCIES
|
||||
# screen, __jobrunner_sessions
|
||||
# tmux, screen, __jobrunner_sessions
|
||||
#
|
||||
# SYNOPSIS
|
||||
# jobrunner [<subcommand>] [<name>] [<command>...]
|
||||
# jr [<subcommand>] [<name>] [<command>...]
|
||||
# jobrunner [-t <tool>] [<subcommand>] [<name>] [<command>...]
|
||||
# jr [-t <tool>] [<subcommand>] [<name>] [<command>...]
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Runs, lists, inspects, re-attaches to, and terminates named background
|
||||
# jobs using GNU screen as the process engine. Unlike bkg and detach,
|
||||
# which discard output, a jobrunner job keeps a live terminal you can
|
||||
# return to later — it survives closing the shell, and `attach` restores
|
||||
# it in any subsequent session.
|
||||
# jobs using tmux or GNU screen as the process engine. Unlike bkg and
|
||||
# detach, which discard output, a jobrunner job keeps a live terminal you
|
||||
# can return to later — it survives closing the shell, and `attach`
|
||||
# restores it in any subsequent session.
|
||||
#
|
||||
# Every subcommand has a matching flag form, and the common cases are
|
||||
# inferred: no arguments lists jobs, a lone name attaches to it, and a
|
||||
# name followed by a command runs it.
|
||||
#
|
||||
# ARGUMENTS
|
||||
# -t, --tool <name> Force specific backend (tmux or screen)
|
||||
# run, -r, --run <name> <cmd>... Start a named job in the background
|
||||
# list, -l, --list List all managed background jobs
|
||||
# attach, -a, --attach <name> Re-attach interactively to a job
|
||||
@@ -33,11 +34,11 @@
|
||||
# EXIT STATUS
|
||||
# 0 Command succeeded, or no jobs are running
|
||||
# 1 Invalid arguments, or the named job does not exist
|
||||
# 127 screen is not installed
|
||||
# 127 neither tmux nor screen is installed
|
||||
#
|
||||
# EXAMPLE
|
||||
# jobrunner run build make -j8
|
||||
# jobrunner backup rsync -a ./data remote:/backup/
|
||||
# jobrunner -t screen run backup rsync -a ./data remote:/backup/
|
||||
# jobrunner list
|
||||
# jobrunner logs build
|
||||
# jobrunner build
|
||||
@@ -48,7 +49,7 @@
|
||||
# Commands are executed directly rather than through a shell, so pipes and
|
||||
# redirections must be wrapped explicitly, e.g.
|
||||
# `jobrunner run sync fish -c 'a | b'`.
|
||||
function jobrunner --description 'Manage detached background jobs with GNU screen'
|
||||
function jobrunner --description 'Manage detached background jobs with tmux or GNU screen'
|
||||
set -l c_head (set_color --bold cyan)
|
||||
set -l c_cmd (set_color --bold white)
|
||||
set -l c_arg (set_color cyan)
|
||||
@@ -61,6 +62,29 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
set -l subcmds run list attach kill logs help \
|
||||
-r --run -l --list -a --attach -k --kill -o --output -h --help
|
||||
|
||||
# ╭──────────────────────────────────────────────────────────╮
|
||||
# │ Tool Extraction │
|
||||
# ╰──────────────────────────────────────────────────────────╯
|
||||
set -l tool ""
|
||||
while test (count $argv) -gt 0
|
||||
switch $argv[1]
|
||||
case -t
|
||||
set tool $argv[2]
|
||||
set -e argv[1..2]
|
||||
case --tool
|
||||
set tool $argv[2]
|
||||
set -e argv[1..2]
|
||||
case '--tool=*'
|
||||
set tool (string replace -- "--tool=" "" $argv[1])
|
||||
set -e argv[1]
|
||||
case '-t*'
|
||||
set tool (string replace -r "^-t" "" $argv[1])
|
||||
set -e argv[1]
|
||||
case '*'
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# ╭──────────────────────────────────────────────────────────╮
|
||||
# │ Help │
|
||||
# ╰──────────────────────────────────────────────────────────╯
|
||||
@@ -68,7 +92,7 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
if set -q argv[1]; and contains -- $argv[1] help -h --help
|
||||
echo "$c_head""Usage:$c_rst $c_cmd""jobrunner$c_rst $c_arg""[<subcommand>] [<name>] [<command>...]$c_rst"
|
||||
echo
|
||||
echo " Run and manage named background jobs backed by GNU screen."
|
||||
echo " Run and manage named background jobs backed by tmux or GNU screen."
|
||||
echo
|
||||
echo "$c_head""Subcommands:$c_rst"
|
||||
echo " $c_cmd""run$c_rst $c_arg""<name> <cmd>...$c_rst Start a named job in the background"
|
||||
@@ -78,6 +102,7 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
echo " $c_cmd""logs$c_rst $c_arg""<name>$c_rst Print a job's output without attaching"
|
||||
echo
|
||||
echo "$c_head""Flags:$c_rst"
|
||||
echo " $c_flag-t$c_rst, $c_flag--tool$c_rst $c_arg<name>$c_rst Force specific backend (tmux or screen)"
|
||||
echo " $c_flag-r$c_rst, $c_flag--run$c_rst Same as $c_cmd""run$c_rst"
|
||||
echo " $c_flag-l$c_rst, $c_flag--list$c_rst Same as $c_cmd""list$c_rst"
|
||||
echo " $c_flag-a$c_rst, $c_flag--attach$c_rst Same as $c_cmd""attach$c_rst"
|
||||
@@ -102,9 +127,24 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
# ╭──────────────────────────────────────────────────────────╮
|
||||
# │ Dependency check │
|
||||
# ╰──────────────────────────────────────────────────────────╯
|
||||
if not command -q screen
|
||||
echo "$c_err""jobrunner:$c_rst 'screen' is required but was not found in PATH." >&2
|
||||
return 127
|
||||
if test -n "$tool"
|
||||
if not contains -- $tool tmux screen
|
||||
echo "$c_err""jobrunner:$c_rst invalid tool '$c_arg$tool$c_rst', must be 'tmux' or 'screen'." >&2
|
||||
return 1
|
||||
end
|
||||
if not command -q $tool
|
||||
echo "$c_err""jobrunner:$c_rst '$tool' is required but was not found in PATH." >&2
|
||||
return 127
|
||||
end
|
||||
else
|
||||
if command -q tmux
|
||||
set tool tmux
|
||||
else if command -q screen
|
||||
set tool screen
|
||||
else
|
||||
echo "$c_err""jobrunner:$c_rst neither 'tmux' nor 'screen' was found in PATH." >&2
|
||||
return 127
|
||||
end
|
||||
end
|
||||
|
||||
# ╭──────────────────────────────────────────────────────────╮
|
||||
@@ -116,7 +156,7 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
if not contains -- $cmd $subcmds
|
||||
if test (count $argv) -eq 1
|
||||
# A lone name attaches, but only if that job actually exists.
|
||||
if contains -- $cmd (__jobrunner_sessions | string replace -r '\t.*$' '')
|
||||
if contains -- $cmd (__jobrunner_sessions $tool | string replace -r '\t.*$' '')
|
||||
set argv attach $argv
|
||||
set cmd attach
|
||||
else
|
||||
@@ -148,19 +188,23 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
echo "$c_err""jobrunner:$c_rst job name may not contain '/'." >&2
|
||||
return 1
|
||||
end
|
||||
if contains -- $name (__jobrunner_sessions | string replace -r '\t.*$' '')
|
||||
if contains -- $name (__jobrunner_sessions $tool | string replace -r '\t.*$' '')
|
||||
echo "$c_err""jobrunner:$c_rst job '$c_arg$name$c_rst' is already running." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
command screen -d -m -S $name $task
|
||||
if test "$tool" = tmux
|
||||
command tmux new-session -d -s $name $task
|
||||
else
|
||||
command screen -d -m -S $name $task
|
||||
end
|
||||
or begin
|
||||
echo "$c_err""jobrunner:$c_rst failed to start job '$c_arg$name$c_rst'." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set -l pid
|
||||
for row in (__jobrunner_sessions)
|
||||
for row in (__jobrunner_sessions $tool)
|
||||
set -l f (string split \t -- $row)
|
||||
test "$f[1]" = "$name"; and set pid $f[2]; and break
|
||||
end
|
||||
@@ -173,7 +217,7 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
end
|
||||
|
||||
case list -l --list
|
||||
set -l rows (__jobrunner_sessions)
|
||||
set -l rows (__jobrunner_sessions $tool)
|
||||
if test (count $rows) -eq 0
|
||||
echo "No background jobs running."
|
||||
return 0
|
||||
@@ -192,12 +236,16 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
return 1
|
||||
end
|
||||
set -l name $argv[2]
|
||||
if not contains -- $name (__jobrunner_sessions | string replace -r '\t.*$' '')
|
||||
if not contains -- $name (__jobrunner_sessions $tool | string replace -r '\t.*$' '')
|
||||
echo "$c_err""jobrunner:$c_rst no such job '$c_arg$name$c_rst'." >&2
|
||||
return 1
|
||||
end
|
||||
# -x attaches to an already-attached session instead of failing.
|
||||
command screen -x $name
|
||||
if test "$tool" = tmux
|
||||
command tmux attach-session -t $name
|
||||
else
|
||||
# -x attaches to an already-attached session instead of failing.
|
||||
command screen -x $name
|
||||
end
|
||||
|
||||
case kill -k --kill
|
||||
if test (count $argv) -ne 2
|
||||
@@ -205,11 +253,15 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
return 1
|
||||
end
|
||||
set -l name $argv[2]
|
||||
if not contains -- $name (__jobrunner_sessions | string replace -r '\t.*$' '')
|
||||
if not contains -- $name (__jobrunner_sessions $tool | string replace -r '\t.*$' '')
|
||||
echo "$c_err""jobrunner:$c_rst no such job '$c_arg$name$c_rst'." >&2
|
||||
return 1
|
||||
end
|
||||
command screen -X -S $name quit
|
||||
if test "$tool" = tmux
|
||||
command tmux kill-session -t $name
|
||||
else
|
||||
command screen -X -S $name quit
|
||||
end
|
||||
or begin
|
||||
echo "$c_err""jobrunner:$c_rst failed to terminate job '$c_arg$name$c_rst'." >&2
|
||||
return 1
|
||||
@@ -222,25 +274,36 @@ function jobrunner --description 'Manage detached background jobs with GNU scree
|
||||
return 1
|
||||
end
|
||||
set -l name $argv[2]
|
||||
if not contains -- $name (__jobrunner_sessions | string replace -r '\t.*$' '')
|
||||
if not contains -- $name (__jobrunner_sessions $tool | string replace -r '\t.*$' '')
|
||||
echo "$c_err""jobrunner:$c_rst no such job '$c_arg$name$c_rst'." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
# hardcopy -h dumps scrollback plus the visible screen to a file.
|
||||
# ponytail: snapshot only; add `screen -L` logging if full
|
||||
# since-start history is ever needed.
|
||||
set -l dump (command mktemp)
|
||||
command screen -X -S $name hardcopy -h $dump
|
||||
or begin
|
||||
set -l out
|
||||
if test "$tool" = tmux
|
||||
# capture-pane -S - -p outputs the scrollback and current pane directly to stdout.
|
||||
set out (command tmux capture-pane -t $name -S - -p)
|
||||
or begin
|
||||
echo "$c_err""jobrunner:$c_rst could not read output of '$c_arg$name$c_rst'." >&2
|
||||
return 1
|
||||
end
|
||||
else
|
||||
# hardcopy -h dumps scrollback plus the visible screen to a file.
|
||||
# ponytail: snapshot only; add `screen -L` logging if full
|
||||
# since-start history is ever needed.
|
||||
set -l dump (command mktemp)
|
||||
command screen -X -S $name hardcopy -h $dump
|
||||
or begin
|
||||
command rm -f $dump
|
||||
echo "$c_err""jobrunner:$c_rst could not read output of '$c_arg$name$c_rst'." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set out (command cat $dump)
|
||||
command rm -f $dump
|
||||
echo "$c_err""jobrunner:$c_rst could not read output of '$c_arg$name$c_rst'." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set -l out (command cat $dump)
|
||||
command rm -f $dump
|
||||
# hardcopy pads the dump out to the full window height.
|
||||
# hardcopy (and sometimes tmux) pad the dump with empty lines.
|
||||
while set -q out[1]; and test -z "$out[-1]"
|
||||
set -e out[-1]
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# lstree ~/projects/myapp
|
||||
function lstree --description 'Full recursive tree listing'
|
||||
if which eza >/dev/null 2>&1
|
||||
eza --tree --icons --color=auto --hyperlink $argv
|
||||
eza --tree --icons --color=auto --hyperlink=auto $argv
|
||||
else if which lsd >/dev/null 2>&1
|
||||
lsd --tree --hyperlink=auto $argv
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user