diff --git a/CHANGELOG.md b/CHANGELOG.md index 0596ddd..25195e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,19 @@ and this project adheres to attribution (`Co-Authored-By:`, `Claude-Session:`, "Generated with Claude Code") out of commits and pull requests, even if a session's own reminder asks for one. +- `list`'s human table gets a leading `#` column, a 1-based position; `attach` + and `cancel` now take that position in place of a JOBID or NAME + (`sinteractive attach 1`), read against a bare number that no running + session's job id actually matches. + +### Fixed + +- `attach` and launch-and-attach could leak raw escape bytes into the + freshly attached pane on a laggy reconnect: the theme-detection query they + ran for stderr narration shares the terminal with the zellij client + they're about to exec into, so a reply arriving after its 500ms timeout + landed as literal control characters in that pane instead of at a shell + prompt. That narration no longer queries the terminal live. ## [1.4.0] - 2026-09-18 diff --git a/README.md b/README.md index 0a29dd3..019f236 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ environment variables. | | `srun --pty bash` | `sinteractive` | |---|---|---| | Survives SSH disconnects | No — session is lost | Yes — the zellij server keeps it alive | -| Reconnect to session | Not possible | `sinteractive attach JOBID\|NAME` | +| Reconnect to session | Not possible | `sinteractive attach JOBID\|NAME\|#` | | Multiple panes | No | Yes — splits, zoom, scrollback (`Ctrl+b`) | | Mouse and copy | Terminal's own | Mouse on by default; select-to-copy lands in your local clipboard | | Status bar | None | Job id, node, walltime left, your queue, notices (`⚠ N notices`) | diff --git a/crates/sint-core/src/color.rs b/crates/sint-core/src/color.rs index ec5a6ee..e1fe6b0 100644 --- a/crates/sint-core/src/color.rs +++ b/crates/sint-core/src/color.rs @@ -37,6 +37,18 @@ impl Palette { Palette::from_theme(&Theme::detect(fd)) } + /// [`Palette::for_fd`], but through [`Theme::detect_no_query`] — for + /// narration that sits right next to exec'ing into (or was just handed + /// the terminal back from) an interactive zellij client, where the live + /// query's late-reply risk would land in that client's pane instead of + /// at a shell prompt. + pub fn for_fd_no_query(mode: ColorMode, fd: i32) -> Self { + if !colour_wanted(mode, fd) { + return Palette::none(); + } + Palette::from_theme(&Theme::detect_no_query(fd)) + } + /// Every code from `theme`, regardless of the fd. pub fn from_theme(theme: &Theme) -> Self { Palette { diff --git a/crates/sint-core/src/session.rs b/crates/sint-core/src/session.rs index e021411..1190f63 100644 --- a/crates/sint-core/src/session.rs +++ b/crates/sint-core/src/session.rs @@ -168,6 +168,19 @@ pub fn sessions_only(rows: &[JobRow]) -> Vec<&JobRow> { .collect() } +/// A bare number given as a target, read as a 1-based position in `rows` +/// (the order `list` prints its `#` column in) — but only when no row +/// actually has that job id. Real Slurm job ids run into the hundreds of +/// thousands, well past any realistic session count, so a literal job id +/// always wins; this only ever engages for a small number nobody's job has. +pub fn resolve_index(n: u64, rows: &[JobRow]) -> Option { + if rows.iter().any(|r| r.job_id == n) { + return None; + } + let idx = usize::try_from(n).ok()?.checked_sub(1)?; + rows.get(idx).map(|r| r.job_id) +} + #[cfg(test)] mod tests { use super::*; @@ -379,6 +392,26 @@ mod tests { assert_eq!(err, "multiple sinteractive sessions named 'dup': 10 11"); } + #[test] + fn resolve_index_reads_position_when_no_job_has_it() { + let rows = vec![ + row(147845, "sinteractive:web", "RUNNING"), + row(147846, "sinteractive", "RUNNING"), + ]; + assert_eq!(resolve_index(1, &rows), Some(147845)); + assert_eq!(resolve_index(2, &rows), Some(147846)); + assert_eq!(resolve_index(3, &rows), None, "past the end"); + assert_eq!(resolve_index(0, &rows), None, "1-based, no zeroth row"); + } + + #[test] + fn resolve_index_defers_to_a_real_job_id() { + // A running job actually numbered 1 is vanishingly unlikely, but if + // one exists it is the literal target, not "position 1". + let rows = vec![row(1, "sinteractive:web", "RUNNING")]; + assert_eq!(resolve_index(1, &rows), None); + } + #[test] fn sessions_only_filters_by_comment() { let rows = vec![ diff --git a/crates/sint-core/src/theme.rs b/crates/sint-core/src/theme.rs index 5cb8723..a4c21e3 100644 --- a/crates/sint-core/src/theme.rs +++ b/crates/sint-core/src/theme.rs @@ -199,18 +199,30 @@ impl Theme { /// never leaves the terminal in raw mode. Prints nothing except the query /// itself, which the terminal consumes. pub fn detect(fd: i32) -> Theme { - Theme::for_mode(detect_mode(fd)) + Theme::for_mode(detect_mode(fd, true)) + } + + /// [`Theme::detect`], but never sending the live query — env override, + /// then `COLORFGBG`, then dark. + /// + /// For a caller about to exec into (or that just took the terminal back + /// from) an interactive zellij client: `query_background`'s up-to-500ms + /// window for a reply is exactly the window in which a late answer lands + /// as raw escape bytes in that client's freshly drawn pane, rather than + /// as harmless line noise at a shell prompt. + pub fn detect_no_query(fd: i32) -> Theme { + Theme::for_mode(detect_mode(fd, false)) } } -fn detect_mode(fd: i32) -> Mode { +fn detect_mode(fd: i32, query: bool) -> Mode { if let Some(mode) = std::env::var("SINTERACTIVE_THEME") .ok() .and_then(|v| Mode::parse(&v)) { return mode; } - if is_tty(fd) { + if query && is_tty(fd) { if let Some(bg) = background() { return Mode::from_background(bg); } @@ -541,6 +553,28 @@ mod tests { assert_eq!(Theme::detect(-1), Theme::DARK); } + #[test] + fn detect_no_query_follows_the_same_fallback_chain() { + // `is_tty` gates the live query in `detect_mode`, so a non-tty fd + // cannot show `detect_no_query` skipping a query `detect` would have + // sent (that needs a real pty, untestable here) — but it does prove + // the env-override/COLORFGBG/dark fallback chain underneath is the + // same for both. + let _g = lock(); + let _r = EnvRestore::clean(); + let file = tempfile::tempfile().expect("tempfile"); + let fd = file.as_raw_fd(); + + std::env::set_var("SINTERACTIVE_THEME", "light"); + assert_eq!(Theme::detect_no_query(fd), Theme::LIGHT); + + std::env::set_var("SINTERACTIVE_THEME", "auto"); + std::env::set_var("COLORFGBG", "0;15"); + assert_eq!(Theme::detect_no_query(fd).mode, Mode::Light); + std::env::remove_var("COLORFGBG"); + assert_eq!(Theme::detect_no_query(fd), Theme::DARK); + } + #[test] fn theme_lookup() { assert_eq!(Theme::for_mode(Mode::Light).mode, Mode::Light); diff --git a/crates/sint/src/cli.rs b/crates/sint/src/cli.rs index 308fc5b..9c5392f 100644 --- a/crates/sint/src/cli.rs +++ b/crates/sint/src/cli.rs @@ -322,7 +322,7 @@ pub struct TargetArgs { #[derive(Args, Debug, Clone, Default)] pub struct AttachArgs { - /// JOBID or NAME; with no target, your only session + /// JOBID, NAME, or list's #; with no target, your only session pub target: Option, /// Attach over ssh -X (X11 forwarding) instead of srun --overlap #[arg(long)] @@ -341,6 +341,7 @@ pub struct EnsureArgs { #[derive(Args, Debug, Clone, Default)] pub struct CancelArgs { + /// JOBID, NAME, or list's # pub target: String, #[arg(long)] pub json: bool, diff --git a/crates/sint/src/commands/attach.rs b/crates/sint/src/commands/attach.rs index 24de2b3..859f321 100644 --- a/crates/sint/src/commands/attach.rs +++ b/crates/sint/src/commands/attach.rs @@ -1,7 +1,9 @@ //! `sinteractive attach [TARGET]` — reattach (script lines 2208-2258). //! //! No target: your only running session; with none, say how to start one; -//! with several, print the ready-to-run choices and exit 1. The default +//! with several, print the ready-to-run choices and exit 1, each prefixed +//! with its position — TARGET can be that position, as printed in `list`'s +//! `#` column, instead of the job id or name (`Ctx::resolve`). The default //! path goes through Slurm (`srun --overlap --jobid=ID --pty …`), which //! needs no ssh access to the node; `--ssh` forces `ssh -X -t NODE …`, the //! launch path's transport, for X11 forwarding. @@ -17,7 +19,9 @@ use crate::cli::AttachArgs; pub fn run(args: AttachArgs) -> Result { let ctx = Ctx::new(); - let p = ctx.palette(2); + // Narration right before exec'ing into the zellij client on this same + // terminal — no live query (see `Ctx::palette_no_query`). + let p = ctx.palette_no_query(2); let (reset, bold, dim, err, id, warn) = (&p.reset, &p.bold, &p.dim, &p.err, &p.id, &p.warn); // Attaching from inside a session would nest multiplexers. @@ -46,13 +50,13 @@ pub fn run(args: AttachArgs) -> Result { "{warn}{bold}sinteractive:{reset}{warn} you have {} running sessions — pick one:{reset}", many.len() ); - for r in many { + for (i, r) in many.iter().enumerate() { let name = parse_comment(&r.comment) .flatten() .unwrap_or_else(|| r.job_id.to_string()); eprintln!( - " sinteractive attach {id}{name:<18}{reset} {dim}# job {} on {}, up {}{reset}", - r.job_id, r.node, r.elapsed + " {dim}{:>2}{reset} sinteractive attach {id}{name:<18}{reset} {dim}# job {} on {}, up {}{reset}", + i + 1, r.job_id, r.node, r.elapsed ); } return Ok(1); @@ -61,7 +65,7 @@ pub fn run(args: AttachArgs) -> Result { } }; - let job_id = match ctx.resolve(Some(&target)) { + let job_id = match ctx.resolve_with_index(Some(&target)) { Ok(id) => id, Err(e) => { eprintln!("{err}{bold}sinteractive:{reset}{err} {e}{reset}"); diff --git a/crates/sint/src/commands/attach_local.rs b/crates/sint/src/commands/attach_local.rs index ed0df0f..0ede0cc 100644 --- a/crates/sint/src/commands/attach_local.rs +++ b/crates/sint/src/commands/attach_local.rs @@ -140,7 +140,11 @@ pub fn run(session: &str) -> Result { // terminal alone. let detached = detach_other_clients(&zellij, session); if detached > 0 { - let p = Palette::for_fd(ColorMode::from_env(), 2); + // No live terminal query here: the real zellij client is exec'd on + // this same terminal a few lines below, and a reply that arrives + // late would land as raw bytes in its freshly drawn pane instead of + // at a shell prompt. + let p = Palette::for_fd_no_query(ColorMode::from_env(), 2); let s = if detached == 1 { "" } else { "s" }; eprintln!( "{}Detached {detached} other client{s} from this session.{}", diff --git a/crates/sint/src/commands/cancel.rs b/crates/sint/src/commands/cancel.rs index 18aaf60..7dde286 100644 --- a/crates/sint/src/commands/cancel.rs +++ b/crates/sint/src/commands/cancel.rs @@ -39,7 +39,7 @@ pub fn cancel_job(ctx: &Ctx, job_id: u64) -> CancelResult { pub fn run(args: CancelArgs) -> Result { let ctx = Ctx::new(); - let Some(job_id) = ctx.resolve_reporting(Some(&args.target))? else { + let Some(job_id) = ctx.resolve_reporting_with_index(Some(&args.target))? else { return Ok(1); }; diff --git a/crates/sint/src/commands/common.rs b/crates/sint/src/commands/common.rs index 082d9c8..0b7b7f3 100644 --- a/crates/sint/src/commands/common.rs +++ b/crates/sint/src/commands/common.rs @@ -8,7 +8,7 @@ use std::process::{Command, Stdio}; use anyhow::{anyhow, Result}; use sint_core::color::Palette; use sint_core::config::{ColorMode, Config}; -use sint_core::session::{resolve_target, sessions_only, SessionInfo, Target}; +use sint_core::session::{resolve_index, resolve_target, sessions_only, SessionInfo, Target}; use sint_core::slurm::squeue::JobRow; use sint_core::slurm::{Slurm, SlurmError}; use sint_core::state::StateDir; @@ -37,6 +37,17 @@ impl Ctx { Palette::for_fd(ColorMode::from_env(), fd) } + /// [`Ctx::palette`], but never risking the live terminal query (see + /// [`Palette::for_fd_no_query`]). For narration right before exec'ing + /// into an interactive zellij client (`attach`, launch-and-attach): a + /// late reply to the query would otherwise land as raw escape bytes in + /// that client's freshly drawn pane rather than at a shell prompt — the + /// "control characters right after attaching" a laggy reconnect can + /// trigger. + pub fn palette_no_query(&self, fd: i32) -> Palette { + Palette::for_fd_no_query(ColorMode::from_env(), fd) + } + /// The user's RUNNING+PENDING sinteractive sessions. pub fn sessions(&self) -> Result> { let rows = self.slurm.my_jobs(&["RUNNING", "PENDING"])?; @@ -58,6 +69,11 @@ impl Ctx { /// Resolve an optional CLI target: explicit JOBID/NAME, else the current /// session (`SINTERACTIVE_JOB_ID`), else an error naming the fix. + /// + /// A bare number passes through unchecked — deliberately no Slurm call, + /// so a command that only needs the id (`events`) stays Slurm-free. + /// [`Ctx::resolve_with_index`] is the variant that also reads a number + /// as a `list` position, for callers that touch Slurm anyway. pub fn resolve(&self, target: Option<&str>) -> Result { match target { Some(t) => { @@ -75,11 +91,41 @@ impl Ctx { } } + /// [`Ctx::resolve`], but a bare number that no running session actually + /// has is read as a 1-based position in `list`'s order instead of a + /// literal job id (see [`resolve_index`]). Every caller here already + /// talks to Slurm for other reasons (`attach` checks the job is + /// RUNNING, `cancel` runs `scancel`), so the extra lookup adds no new + /// round trip class — unlike [`Ctx::resolve`], which callers such as + /// `events` rely on to stay Slurm-free for a bare job id. + pub fn resolve_with_index(&self, target: Option<&str>) -> Result { + if let Some(t) = target { + if let Target::JobId(id) = Target::parse(t) { + let running = self.running_sessions()?; + return Ok(resolve_index(id, &running).unwrap_or(id)); + } + } + self.resolve(target) + } + /// [`Ctx::resolve`], reporting a no-match or ambiguous name on stderr in /// the 0.x wording (script line 2152) and returning `Ok(None)`; the /// caller then exits 1. A Slurm failure is still an `Err`. pub fn resolve_reporting(&self, target: Option<&str>) -> Result> { - match self.resolve(target) { + self.report_resolve(self.resolve(target)) + } + + /// [`Ctx::resolve_reporting`], but resolving through + /// [`Ctx::resolve_with_index`] so a bare number may also be a `list` + /// position. + pub fn resolve_reporting_with_index(&self, target: Option<&str>) -> Result> { + self.report_resolve(self.resolve_with_index(target)) + } + + /// The error reporting shared by [`Ctx::resolve_reporting`] and + /// [`Ctx::resolve_reporting_with_index`]. + fn report_resolve(&self, result: Result) -> Result> { + match result { Ok(id) => Ok(Some(id)), Err(e) if e.downcast_ref::().is_some() => Err(e), Err(e) => { diff --git a/crates/sint/src/commands/launch.rs b/crates/sint/src/commands/launch.rs index 289bdfb..e1d8198 100644 --- a/crates/sint/src/commands/launch.rs +++ b/crates/sint/src/commands/launch.rs @@ -549,7 +549,9 @@ pub(crate) fn launch(ctx: &Ctx, args: LaunchArgs, created: Option) -> Resu Launched::Failed(code) => return Ok(code), Launched::Ready { job_id, node } => (job_id, node), }; - let p = ctx.palette(2); + // The interactive path below hands this same terminal to an ssh'd + // `__attach`, so no live terminal query — see `Ctx::palette_no_query`. + let p = ctx.palette_no_query(2); let (reset, bold, key, id, ok) = (&p.reset, &p.bold, &p.key, &p.id, &p.ok); let name = args.name.clone().filter(|n| !n.is_empty()); let attach_target = name.unwrap_or_else(|| job_id.to_string()); diff --git a/crates/sint/src/commands/list.rs b/crates/sint/src/commands/list.rs index 8e9e6c3..ec40bc3 100644 --- a/crates/sint/src/commands/list.rs +++ b/crates/sint/src/commands/list.rs @@ -5,6 +5,11 @@ //! `status --json` shape and additionally carry `cwd`. `--full` adds node, //! partition and elapsed/limit to the human table; the default keeps to //! what fits at a glance: job id, name, time remaining, cwd. +//! +//! The human table's leading `#` is a 1-based position, not part of the +//! frozen JSON contract — it exists so `attach`/`status`/`cancel` can take +//! it back as a short target (`Ctx::resolve`, via `resolve_index`) instead +//! of the job id or name. use std::thread; @@ -124,23 +129,25 @@ pub fn run(args: ListArgs) -> Result { let p = ctx.palette(1); if args.full { println!( - "{}{:<10} {:<20} {:<14} {:<12} {:<20} {:<10} CWD{}", - p.dim, "JOBID", "NAME", "NODE", "PARTITION", "ELAPSED/LIMIT", "REMAINING", p.reset + "{}{:<3} {:<10} {:<20} {:<14} {:<12} {:<20} {:<10} CWD{}", + p.dim, "#", "JOBID", "NAME", "NODE", "PARTITION", "ELAPSED/LIMIT", "REMAINING", p.reset ); } else { println!( - "{}{:<10} {:<20} {:<10} CWD{}", - p.dim, "JOBID", "NAME", "REMAINING", p.reset + "{}{:<3} {:<10} {:<20} {:<10} CWD{}", + p.dim, "#", "JOBID", "NAME", "REMAINING", p.reset ); } - for (row, cwd) in rows.iter().zip(&cwds) { + for (i, (row, cwd)) in rows.iter().zip(&cwds).enumerate() { let info = SessionInfo::from_row(row, sint_core::now_epoch()); let name = info.name.as_deref().unwrap_or("-"); let cwd = cwd.as_deref().unwrap_or("-"); let remaining = remaining_cell(info.remaining_seconds, 10, &p); + // The position `attach`/`status`/`cancel` will read back as "#N". + let idx = format!("{}{:<3}{}", p.dim, i + 1, p.reset); if args.full { println!( - "{}{:<10}{} {}{:<20}{} {}{:<14}{} {:<12} {:<20} {remaining} {}{}{}", + "{idx} {}{:<10}{} {}{:<20}{} {}{:<14}{} {:<12} {:<20} {remaining} {}{}{}", p.id, row.job_id, p.reset, @@ -158,7 +165,7 @@ pub fn run(args: ListArgs) -> Result { ); } else { println!( - "{}{:<10}{} {}{:<20}{} {remaining} {}{}{}", + "{idx} {}{:<10}{} {}{:<20}{} {remaining} {}{}{}", p.id, row.job_id, p.reset, p.bold, name, p.reset, p.dim, cwd, p.reset ); } @@ -166,11 +173,11 @@ pub fn run(args: ListArgs) -> Result { println!(); println!( - "{}{:<10}{} sinteractive attach JOBID|NAME", + "{}{:<10}{} sinteractive attach JOBID|NAME|#", p.key, "Reattach:", p.reset ); println!( - "{}{:<10}{} sinteractive cancel JOBID|NAME", + "{}{:<10}{} sinteractive cancel JOBID|NAME|#", p.key, "Cancel:", p.reset ); Ok(0) diff --git a/crates/sint/tests/attach.rs b/crates/sint/tests/attach.rs index 3f78582..49f6099 100644 --- a/crates/sint/tests/attach.rs +++ b/crates/sint/tests/attach.rs @@ -87,6 +87,29 @@ fn attach_by_id_execs_srun_overlap() { assert!(fx.calls_to("ssh").is_empty()); } +#[test] +fn attach_by_index_resolves_lists_position() { + let fx = FakeSlurm::with_jobs(&[ + Job::default(), + Job::new(147846, "sinteractive:worker").node("node02"), + ]); + let _ = fx.sinteractive().args(["attach", "2"]).assert(); + let srun = fx.calls_to("srun"); + assert_eq!(srun.len(), 1, "{srun:?}"); + assert_eq!(srun[0][1], "--jobid=147846"); +} + +#[test] +fn attach_prefers_a_real_job_id_over_the_position() { + // A running job actually numbered 2 is not realistic on a real + // cluster, but if one existed it must win over "position 2". + let fx = FakeSlurm::with_jobs(&[Job::new(2, "sinteractive:early")]); + let _ = fx.sinteractive().args(["attach", "2"]).assert(); + let srun = fx.calls_to("srun"); + assert_eq!(srun.len(), 1, "{srun:?}"); + assert_eq!(srun[0][1], "--jobid=2"); +} + #[test] fn attach_by_name_resolves_the_comment() { let fx = FakeSlurm::with_jobs(&[Job::default()]); diff --git a/crates/sint/tests/reporting.rs b/crates/sint/tests/reporting.rs index 56721b5..71f2dde 100644 --- a/crates/sint/tests/reporting.rs +++ b/crates/sint/tests/reporting.rs @@ -315,18 +315,18 @@ fn list_human_table() { // The fixture's end time is in the past, so remaining clamps at zero // and renders as `0s` (format_short_duration's under-a-minute case). fx.sinteractive().arg("list").assert().success().stdout( - predicate::str::contains("JOBID NAME REMAINING CWD\n") + predicate::str::contains("# JOBID NAME REMAINING CWD\n") .and(predicate::str::contains( - "147845 web 0s -\n", + "1 147845 web 0s -\n", )) .and(predicate::str::contains( - "147846 - 0s -\n", + "2 147846 - 0s -\n", )) .and(predicate::str::contains( - "Reattach: sinteractive attach JOBID|NAME\n", + "Reattach: sinteractive attach JOBID|NAME|#\n", )) .and(predicate::str::contains( - "Cancel: sinteractive cancel JOBID|NAME\n", + "Cancel: sinteractive cancel JOBID|NAME|#\n", )) .and(predicate::str::contains("147900").not()), ); @@ -340,13 +340,13 @@ fn list_human_table_full_adds_node_partition_and_elapsed_limit() { ]); fx.sinteractive().args(["list", "--full"]).assert().success().stdout( predicate::str::contains( - "JOBID NAME NODE PARTITION ELAPSED/LIMIT REMAINING CWD\n", + "# JOBID NAME NODE PARTITION ELAPSED/LIMIT REMAINING CWD\n", ) .and(predicate::str::contains( - "147845 web node01 interactive 1:02:03/8:00:00 0s -\n", + "1 147845 web node01 interactive 1:02:03/8:00:00 0s -\n", )) .and(predicate::str::contains( - "147846 - node03 interactive 1:02:03/8:00:00 0s -\n", + "2 147846 - node03 interactive 1:02:03/8:00:00 0s -\n", )), ); } @@ -370,6 +370,21 @@ fn cancel_by_name_removes_the_job() { assert_eq!(ids, vec!["147846", "147900"]); } +#[test] +fn cancel_by_index_resolves_lists_position() { + // `mixed_queue`'s only RUNNING sinteractive session is 147845 (147846 + // is PENDING, 147900 is not a session), so position 1 is it. + let fx = mixed_queue(); + fx.sinteractive() + .args(["cancel", "1"]) + .assert() + .success() + .stderr(predicate::str::contains( + "Cancelled session 147845 (web) on node01.", + )); + assert_eq!(fx.calls_to("scancel"), vec![vec!["147845".to_string()]]); +} + #[test] fn cancel_json_shape() { let fx = mixed_queue(); diff --git a/docs/index.md b/docs/index.md index 277cd32..c90d398 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,7 +29,7 @@ environment variables. | | `srun --pty bash` | `sinteractive` | |---|---|---| | Survives SSH disconnects | No — session is lost | Yes — the zellij server keeps it alive | -| Reconnect to session | Not possible | `sinteractive attach JOBID\|NAME` | +| Reconnect to session | Not possible | `sinteractive attach JOBID\|NAME\|#` | | Multiple panes | No | Yes — splits, zoom, scrollback (`Ctrl+b`) | | Mouse and copy | Terminal's own | Mouse on by default; select-to-copy lands in your local clipboard | | Status bar | None | Job id, node, walltime left, your queue, notices (`⚠ N notices`) | diff --git a/docs/usage.md b/docs/usage.md index b4d1c4a..90cac7e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -244,20 +244,23 @@ node: ```bash # List your running sessions sinteractive list -# JOBID NAME REMAINING CWD -# 12345 rna-seq 22h 36m ~/projects/rna-seq +# # JOBID NAME REMAINING CWD +# 1 12345 rna-seq 22h 36m ~/projects/rna-seq # Node, partition, and elapsed/limit as well sinteractive list --full -# Reattach +# Reattach — by job id, by name, or by the position `list` just printed sinteractive attach 12345 sinteractive attach rna-seq +sinteractive attach 1 ``` If you have only one session running, a bare `sinteractive attach` goes straight to it — no need to look up the job id first. With several running, -it lists them with ready-to-run commands to pick from. +it lists them with ready-to-run commands to pick from, each prefixed with +the same position number `list` shows, so `sinteractive attach 1` works +without retyping a job id or name. `attach` reconnects through Slurm (`srun --overlap`), which needs no SSH access to the node. `attach --ssh` uses `ssh -X` instead, which is the way