From a3f02deaff4e8fd70eac4a3f8c0be89ad947527a Mon Sep 17 00:00:00 2001 From: Jay Hesselberth Date: Wed, 26 Aug 2026 14:22:35 -0600 Subject: [PATCH 1/2] feat: colour the launch and teardown narration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Launching a session prints a dozen lines of which exactly one matters — that it is ready, and where. Everything else is progress narration that has to be read past. Dimming the narration and leaving the result bright makes the block scannable without changing what it says. Four roles, not a rainbow. Teal marks identifiers — job ids and node names — echoing the #2DBFB8 the status bar already uses for those same things, so the launch output and the session you land in agree about what an identifier is. Dim carries progress. Yellow marks keys and warnings, red marks errors, and green marks the two moments worth noticing: the session coming up and a cancel landing. The auto-detection tests standard error, not standard output, because that is where all of this is written. The two differ in exactly the case that matters: `sinteractive --detach ... > file` should still narrate to the terminal, while `2>log` must not collect escape sequences. SINTERACTIVE_COLOR takes always/never/auto and NO_COLOR is honoured whatever its value, with an explicit always winning over both for piping into something that renders escapes itself. With colour off every variable is the empty string rather than each use being guarded, so one set of format strings serves both cases and there is no second, less-tested code path. Two messages were tightened while here, since they were being rewritten anyway: "Interactive job with ID N submitted, please wait" is now "Submitted job N, waiting for it to start", and the detach block's prose "To reconnect:" / "To cancel the job:" became an aligned "Reconnect:" / "Cancel:" pair matching the one already shown at launch. Verified that a redirect collects no escapes at all, and that NO_COLOR and TERM=dumb both suppress colour on a real pty. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 20 ++++++++++ README.md | 5 +++ man/sinteractive.1 | 18 +++++++++ sinteractive | 99 +++++++++++++++++++++++++++++++--------------- 4 files changed, 110 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df15b04..89ab9d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,26 @@ and this project adheres to ### Added +- Colour in the launch and teardown narration. Four roles rather than a + rainbow: teal for identifiers (job ids, node names), echoing the `#2DBFB8` + the status bar already uses for the same things so the two agree about what + an identifier is; dim for progress, which leaves `✓ Session … is ready` the + only bright line in the block; yellow for keys and warnings; red for errors. + + `SINTERACTIVE_COLOR` takes `auto`/`always`/`never` (default `auto`), and + `NO_COLOR` is honoured whatever its value. The `auto` test is on **standard + error**, not standard output, because that is where the narration goes — the + two differ in exactly the case that matters, since `--detach ... > file` + should still narrate to the terminal while `2>log` must stay free of + escapes. With colour off every variable is empty rather than being guarded + at each use, so one set of format strings serves both and there is no + second, less-tested path. + + Two messages were tightened in passing: `Interactive job with ID N + submitted, please wait` is now `Submitted job N, waiting for it to start`, + and the detach block's prose `To reconnect:` / `To cancel the job:` became an + aligned `Reconnect:` / `Cancel:` pair matching the one shown at launch. + - Storage quota in the notice line. A session shows a red `OVER QUOTA` warning above its status line, with the overage, while the user is past their hard limit, re-checked every `SINTERACTIVE_QUOTA_POLL` seconds (default 600). diff --git a/README.md b/README.md index 1aef522..741a154 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,11 @@ Set personal defaults in your `~/.bashrc`; explicit flags always win. | `SINTERACTIVE_MEM` | Default memory (e.g. `16G`) | `8G` | | `SINTERACTIVE_MOUSE` | `on`/`1`/`true`/`yes` enables mouse support | off | | `SINTERACTIVE_TMUX` | Path to the `tmux` binary on the compute node | `/usr/local/bin/tmux` | +| `SINTERACTIVE_COLOR` | `auto`/`always`/`never` for launch and teardown output; `NO_COLOR` also honoured | `auto` | +| `SINTERACTIVE_QUOTA_POLL` | Seconds between storage-quota checks | `600` | +| `SINTERACTIVE_QUOTA_FILE` | Pipe-delimited file of hard quotas | `/cluster/scripts/quota_current.txt` | +| `SINTERACTIVE_QUOTA_HOSTS` | Quota daemons to sum usage across | Bodhi's `172.20.8.110-118` | +| `SINTERACTIVE_QUOTA_PORT` | Port those daemons listen on | `9878` | ```bash # Example: always use mouse mode and a bigger default allocation diff --git a/man/sinteractive.1 b/man/sinteractive.1 index 8a6f447..8d8c373 100644 --- a/man/sinteractive.1 +++ b/man/sinteractive.1 @@ -622,6 +622,24 @@ Port those daemons listen on. Default: 9878. .TP .B SINTERACTIVE_QUOTA_TIMEOUT Seconds to wait for each daemon's reply. Default: 5. +.TP +.B SINTERACTIVE_COLOR +.BR auto | always | never +for the launch and teardown narration. Default: +.BR auto , +which colours only when standard error is a terminal and +.B TERM +is neither +.B dumb +nor empty. The test is on standard error rather than standard output because +that is where the narration goes: a redirect of stdout still narrates to your +terminal, and +.B 2>log +stays free of escapes. +.B NO_COLOR +(any value, per no\-color.org) disables colour; +.B SINTERACTIVE_COLOR=always +overrides both, for piping into something that renders escapes itself. .PP Exported inside a session: .B SINTERACTIVE_JOB_ID diff --git a/sinteractive b/sinteractive index 2a6e299..e48a96b 100755 --- a/sinteractive +++ b/sinteractive @@ -23,6 +23,44 @@ VERSION='0.4.0' # Basename for error messages ($0 is the full install path — noisy). PROG="${0##*/}" +# --------------------------------------------------------------------------- +# Colour for the launch and teardown narration. +# +# Everything here is written to stderr, so the decision hangs on whether +# *stderr* is a terminal — not stdout. The two differ in exactly the case that +# matters: `sinteractive --detach ... > file` still narrates to a terminal, and +# `sinteractive ... 2>log` must not write escapes into the log. +# +# When colour is off every variable is the empty string rather than being +# guarded at each use, so one set of printf strings serves both cases and +# there is no second, less-tested code path. +# +# SINTERACTIVE_COLOR takes always/never/auto (default auto); NO_COLOR is +# honoured whatever its value, per no-color.org, and TERM=dumb opts out too. +# An explicit SINTERACTIVE_COLOR=always wins over both, for piping into +# something that renders escapes itself (`less -R`, CI logs that colourise). +# --------------------------------------------------------------------------- +C_RST='' C_BOLD='' C_DIM='' C_OK='' C_WARN='' C_ERR='' C_ID='' C_KEY='' +function init_colors { + local want="${SINTERACTIVE_COLOR:-auto}" + case "$want" in + never | no | 0) return 0 ;; + always | yes | 1) ;; + *) + [[ -n "${NO_COLOR+x}" ]] && return 0 + [[ -t 2 ]] || return 0 + [[ "${TERM:-}" == dumb || -z "${TERM:-}" ]] && return 0 + ;; + esac + C_RST=$'\e[0m' C_BOLD=$'\e[1m' C_DIM=$'\e[2m' + C_OK=$'\e[32m' C_WARN=$'\e[33m' C_ERR=$'\e[31m' + # Teal for identifiers — job ids, node names — echoing the #2DBFB8 the + # status bar uses for the same things, so the launch narration and the + # session you land in agree about what is an identifier. + C_ID=$'\e[1;36m' C_KEY=$'\e[1;33m' +} +init_colors + # Convert time shorthand (e.g. 8h, 30m, 2d, 1d12h, 1h30m) to HH:MM:SS function parse_time { local input="$1" @@ -536,8 +574,8 @@ function main { fitted=$(seconds_to_slurm_time "$maint_fit") mapfile -d '' -t rewritten < <(replace_time_in_args "$fitted" "$@") set -- "${rewritten[@]}" - echo "Maintenance (${maint_name}) starts $(date -d "@$maint_start" '+%a %b %-d %H:%M')." - echo "Shortened the request from ${maint_orig} to ${fitted} so the session ends before it." + echo "${C_WARN}${C_BOLD}!${C_RST} ${C_WARN}Maintenance (${maint_name}) starts $(date -d "@$maint_start" '+%a %b %-d %H:%M').${C_RST}" + echo " ${C_DIM}Shortened the request from${C_RST} ${maint_orig} ${C_DIM}to${C_RST} ${C_BOLD}${fitted}${C_RST} ${C_DIM}so the session ends before it.${C_RST}" echo '' fi @@ -565,12 +603,12 @@ function main { if [[ -n "$rname" ]]; then echo 1>&2 "Note: you already have a running session named '${rname}' (job ${rjob} on ${rnode})." else - echo 1>&2 "Note: you already have a running session (job ${rjob} on ${rnode})." + echo 1>&2 "${C_DIM}Note: you already have a running session (job ${rjob} on ${rnode}).${C_RST}" fi - echo 1>&2 " Reattach with 'sinteractive --attach ${rname:-${rjob}}'; starting a new session..." + echo 1>&2 "${C_DIM} Reattach with 'sinteractive --attach ${rname:-${rjob}}'; starting a new session...${C_RST}" else - echo 1>&2 "Note: you already have ${n_running} running sessions ('sinteractive --list' shows them)." - echo 1>&2 " Starting a new session..." + echo 1>&2 "${C_DIM}Note: you already have ${n_running} running sessions ('sinteractive --list' shows them).${C_RST}" + echo 1>&2 "${C_DIM} Starting a new session...${C_RST}" fi fi @@ -625,7 +663,7 @@ function main { fi scontrol update JobId="${JOB_ID}" Comment="${job_comment}" - echo 1>&2 "Interactive job with ID ${JOB_ID} submitted, please wait." + echo 1>&2 "${C_DIM}Submitted job${C_RST} ${C_ID}${JOB_ID}${C_RST}${C_DIM}, waiting for it to start.${C_RST}" # While the job is pending, show why (squeue's pend reason) and Slurm's # estimated start time. The scheduler is polled every 5 seconds; on a TTY a @@ -679,9 +717,10 @@ function main { fi fi fi - printf 1>&2 '\r\033[K %s %s%s (%s elapsed)' \ - "${frames[spin % 10]}" "$why" "$est" \ - "$(format_short_duration $((now - wait_start)))" + printf 1>&2 '\r\033[K %s%s%s %s%s%s %s(%s elapsed)%s' \ + "$C_ID" "${frames[spin % 10]}" "$C_RST" \ + "$C_DIM" "${why}${est}" "$C_RST" \ + "$C_DIM" "$(format_short_duration $((now - wait_start)))" "$C_RST" spin=$((spin + 1)) sleep 0.5 else @@ -693,7 +732,7 @@ function main { batchhost=$(squeue --jobs "${JOB_ID}" --noheader --Format batchhost | xargs) echo 1>&2 '' - echo 1>&2 'Connecting to sinteractive session, please wait...' + echo 1>&2 "${C_DIM}Allocated ${C_RST}${C_ID}${batchhost}${C_RST}${C_DIM} — bringing up the session...${C_RST}" # A RUNNING job only means the allocation exists; the batch script still # has to bring up the tmux server. Poll for the session (a single SSH that @@ -728,11 +767,11 @@ function main { fi local attach_target="${session_name:-${JOB_ID}}" echo 1>&2 '' - echo 1>&2 "Session ${JOB_ID} is ready on ${batchhost}." + echo 1>&2 "${C_OK}${C_BOLD}✓${C_RST} ${C_BOLD}Session ${C_RST}${C_ID}${JOB_ID}${C_RST}${C_BOLD} is ready on ${C_RST}${C_ID}${batchhost}${C_RST}${C_BOLD}.${C_RST}" echo 1>&2 '' - echo 1>&2 " Attach: sinteractive --attach ${attach_target}" - echo 1>&2 " Status: sinteractive --status ${attach_target}" - echo 1>&2 " Cancel: sinteractive --cancel ${attach_target}" + echo 1>&2 " ${C_KEY}Attach:${C_RST} sinteractive --attach ${attach_target}" + echo 1>&2 " ${C_KEY}Status:${C_RST} sinteractive --status ${attach_target}" + echo 1>&2 " ${C_KEY}Cancel:${C_RST} sinteractive --cancel ${attach_target}" return fi @@ -747,18 +786,14 @@ function main { if [[ "${post_state}" == 'RUNNING' ]]; then echo 1>&2 '' - echo 1>&2 "Disconnected from sinteractive session." - echo 1>&2 "Your job ${JOB_ID} is still running on ${batchhost}." + echo 1>&2 "${C_WARN}${C_BOLD}⠿${C_RST} ${C_BOLD}Detached.${C_RST} ${C_DIM}Job${C_RST} ${C_ID}${JOB_ID}${C_RST} ${C_DIM}is still running on${C_RST} ${C_ID}${batchhost}${C_RST}${C_DIM}.${C_RST}" echo 1>&2 '' local attach_target="${JOB_ID}" if [[ -n "${session_name}" ]]; then attach_target="${session_name}" fi - echo 1>&2 "To reconnect:" - echo 1>&2 " sinteractive --attach ${attach_target}" - echo 1>&2 '' - echo 1>&2 "To cancel the job:" - echo 1>&2 " scancel ${JOB_ID}" + echo 1>&2 " ${C_KEY}Reconnect:${C_RST} sinteractive --attach ${attach_target}" + echo 1>&2 " ${C_KEY}Cancel:${C_RST} scancel ${JOB_ID}" # Show other running sinteractive sessions. Pipe-delimited -o output # (not --Format columns) so long comments can't run into the next field @@ -770,14 +805,14 @@ function main { if [[ -n "$other_running" ]]; then echo 1>&2 '' - echo 1>&2 'Other running sinteractive sessions:' - printf 1>&2 ' %-10s %-14s %-10s %-10s\n' 'JOBID' 'NODE' 'ELAPSED' 'TIMELIMIT' + echo 1>&2 "${C_DIM}Other running sinteractive sessions:${C_RST}" + printf 1>&2 " ${C_DIM}%-10s %-14s %-10s %-10s${C_RST}\\n" 'JOBID' 'NODE' 'ELAPSED' 'TIMELIMIT' while IFS='|' read -r jobid comment node elapsed limit; do - printf 1>&2 ' %-10s %-14s %-10s %-10s\n' "$jobid" "$node" "$elapsed" "$limit" + printf 1>&2 " ${C_ID}%-10s${C_RST} %-14s %-10s %-10s\\n" "$jobid" "$node" "$elapsed" "$limit" done <<<"$other_running" fi else - echo 1>&2 "Session ended. Job ${JOB_ID} is no longer running." + echo 1>&2 "${C_DIM}Session ended. Job${C_RST} ${C_ID}${JOB_ID}${C_RST} ${C_DIM}is no longer running.${C_RST}" # Show any remaining sinteractive sessions. Pipe-delimited -o output # (not --Format columns) so long comments can't run into the next field @@ -788,13 +823,13 @@ function main { if [[ -n "$remaining" ]]; then echo 1>&2 '' - echo 1>&2 'You have other sinteractive sessions still running:' - printf 1>&2 ' %-10s %-14s %-10s %-10s\n' 'JOBID' 'NODE' 'ELAPSED' 'TIMELIMIT' + echo 1>&2 "${C_WARN}You have other sinteractive sessions still running:${C_RST}" + printf 1>&2 " ${C_DIM}%-10s %-14s %-10s %-10s${C_RST}\\n" 'JOBID' 'NODE' 'ELAPSED' 'TIMELIMIT' while IFS='|' read -r jobid comment node elapsed limit; do - printf 1>&2 ' %-10s %-14s %-10s %-10s\n' "$jobid" "$node" "$elapsed" "$limit" + printf 1>&2 " ${C_ID}%-10s${C_RST} %-14s %-10s %-10s\\n" "$jobid" "$node" "$elapsed" "$limit" done <<<"$remaining" echo 1>&2 '' - echo 1>&2 'To free slots, cancel sessions you no longer need:' + echo 1>&2 "${C_DIM}To free slots, cancel sessions you no longer need:${C_RST}" while IFS='|' read -r jobid _rest; do echo 1>&2 " scancel $jobid" done <<<"$remaining" @@ -1907,7 +1942,7 @@ function fit_maintenance_window { local maint_when maint_when=$(date -d "@$start_epoch" "+%a %b %-d %H:%M") echo 1>&2 "" - echo 1>&2 "Error: maintenance starts too soon to open a session." + echo 1>&2 "${C_ERR}${C_BOLD}Error:${C_RST}${C_ERR} maintenance starts too soon to open a session.${C_RST}" echo 1>&2 "" echo 1>&2 " Reservation: ${name}" echo 1>&2 " Starts: ${maint_when} (in $(format_short_duration $gap_secs))" @@ -2053,7 +2088,7 @@ function cancel_session { local desc="session ${jobid}" [[ -n "$name" ]] && desc+=" (${name})" - echo 1>&2 "Cancelled ${desc}${node:+ on ${node}}." + echo 1>&2 "${C_OK}✓${C_RST} Cancelled ${desc}${node:+ on ${C_ID}${node}${C_RST}}." exit 0 } From 34d8f6a69cac2476e99e6145fba60a78c89f98ef Mon Sep 17 00:00:00 2001 From: Jay Hesselberth Date: Wed, 26 Aug 2026 14:23:23 -0600 Subject: [PATCH 2/2] chore(release): v0.5.0 Storage quota and maintenance-shortened sessions in the notice line, and colour in the launch and teardown narration. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 5 ++++- docs/deploy.md | 2 +- man/sinteractive.1 | 2 +- pixi.toml | 2 +- sinteractive | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89ab9d5..3fe462a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to ## [Unreleased] +## [0.5.0] - 2026-08-26 + ### Added - Colour in the launch and teardown narration. Four roles rather than a @@ -469,7 +471,8 @@ First tagged release. installers (user, system-wide, and per-node fan-out), and a `bodhi-compute` Claude Code skill. -[Unreleased]: https://github.com/rnabioco/sinteractive/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/rnabioco/sinteractive/compare/v0.5.0...HEAD +[0.5.0]: https://github.com/rnabioco/sinteractive/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/rnabioco/sinteractive/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/rnabioco/sinteractive/compare/v0.2.2...v0.3.0 [0.2.2]: https://github.com/rnabioco/sinteractive/compare/v0.2.1...v0.2.2 diff --git a/docs/deploy.md b/docs/deploy.md index a7ebf29..a297c69 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -67,7 +67,7 @@ script there, so a copy may be executing while you install. ```bash make nodes-check -# compute00 sinteractive=0.4.0 assets=yes tmux 3.7c +# compute00 sinteractive=0.5.0 assets=yes tmux 3.7c # compute01 sinteractive=unknown assets=no tmux 3.7b # compgpu01 unreachable ``` diff --git a/man/sinteractive.1 b/man/sinteractive.1 index 8d8c373..c5d6da2 100644 --- a/man/sinteractive.1 +++ b/man/sinteractive.1 @@ -1,4 +1,4 @@ -.TH SINTERACTIVE 1 "August 2026" "sinteractive 0.4.0" "User Commands" +.TH SINTERACTIVE 1 "August 2026" "sinteractive 0.5.0" "User Commands" .SH NAME sinteractive \- start an interactive tmux session on a Slurm compute node .SH SYNOPSIS diff --git a/pixi.toml b/pixi.toml index a185739..35dc1b2 100644 --- a/pixi.toml +++ b/pixi.toml @@ -1,7 +1,7 @@ [workspace] name = "sinteractive" # Keep in sync with VERSION in `sinteractive` and the release tag (vX.Y.Z). -version = "0.4.0" +version = "0.5.0" description = "Documentation site for sinteractive" channels = ["conda-forge"] platforms = ["linux-64", "osx-arm64"] diff --git a/sinteractive b/sinteractive index e48a96b..93b2257 100755 --- a/sinteractive +++ b/sinteractive @@ -19,7 +19,7 @@ set -u TMUX_BIN="${SINTERACTIVE_TMUX:-/usr/local/bin/tmux}" JOB_ID='' # Reported by --version; keep in sync with the release tag (vX.Y.Z). -VERSION='0.4.0' +VERSION='0.5.0' # Basename for error messages ($0 is the full install path — noisy). PROG="${0##*/}"