Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Refreshed the visual theme and tightened keybindings ahead of the first release. Every widget now has its own accent color when focused — cyan for the script viewer, green for stdout, red for stderr, purple for custom panels, and orange for the filter sidebar — and focused borders switched from rounded to double so the active panel is immediately obvious. Marked job rows use an inverted style (dark text on a colored background) instead of the old underline, the cursor symbol changed from a filled circle to a triangle, and the base palette was nudged darker overall. Several single-key shortcuts that conflicted with text input were moved behind `Ctrl`: widget selector is now `Ctrl+W`, column config is `Ctrl+C`, select-all is `Ctrl+A`, cancel is `Ctrl+X`, and reset is `Ctrl+R` in both the field and filter dialogs; the same treatment was applied to the add (`Ctrl+A`) and delete (`Ctrl+D`) actions in the widget selector. The layout engine was reworked so the right column holds up to three widgets and any overflow spills into the left column below the table on an aligned grid, replacing the earlier scheme that squeezed up to four panels on the right and tacked extras onto a half-height bottom strip; the total visible panel cap is now five (2×3−1) to guarantee the table always keeps at least one grid row. Widget titles were simplified by dropping the job ID suffix, and the "Execution Script" label was shortened to "Script". The README was rewritten with a full keybinding reference split by context, an updated architecture tree, and expanded feature descriptions. [PR #6](https://github.com/fedonman/sqwatch/pull/6)

### Added

- Three operations that previously blocked the main thread and froze the UI — `scontrol show job` lookups, periodic `squeue` refreshes, and script file loading with optional `bat` highlighting — were moved into dedicated background threads. A new `JobDetailResolver` runs a single `scontrol` call per job and caches up to 64 results, replacing the duplicate per-widget calls that each blocked for 100–500 ms; widgets now show a "Loading…" placeholder until the detail arrives, and the resolver deduplicates rapid requests by draining the channel and keeping only the latest job ID. A new `JobFetcher` runs `squeue` in its own lightweight tokio runtime so the 1-second auto-refresh and filter-apply no longer stall rendering; the old synchronous `reload_jobs` was split into `reload_jobs_sync` (used once at startup) and a non-blocking `submit_reload` path whose results are picked up on the next timer tick. The script widget's `load_content` was similarly offloaded to a background thread so that file reads and `bat` invocations never touch the render path, with a new `poll_updates` method that mirrors the pattern already used by the output and custom widgets. The input processing loop now drains all pending signals on each iteration and collapses consecutive `Timer` events into a single tick, which eliminates the multi-second freeze that occurred when switching back to the terminal after the window had been unfocused and hundreds of stale timers had piled up in the channel. `Ctrl+C` while any content widget (script, stdout, stderr, or custom) is focused now copies the widget's content to the system clipboard via the OSC 52 escape sequence and flashes a confirmation in the titlebar; the binding works over SSH and inside tmux without requiring X11 or Wayland, and `Esc` remains the key for returning focus to the table. The script widget gained `PageUp`/`PageDown` and `Ctrl+U`/`Ctrl+D` scrolling to match the other widgets, and all four content widget types now show `PgUp/Dn Scroll` and `Ctrl+C Copy` hints in the statusbar. [PR #5](https://github.com/fedonman/sqwatch/pull/5)
Expand Down
118 changes: 84 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# sqwatch
# sqwatch - SLURM Queue Watcher

A lightweight terminal UI for watching and managing SLURM job queues in real time.

`sqwatch` gives you a live, interactive dashboard for your SLURM cluster right in the terminal. Browse jobs, inspect scripts and logs, filter by any field, and cancel jobs — all without leaving the command line.

## Features

- **Live queue view** — Auto-refreshing job table with color-coded states (pending, running, failed, completed, suspended, out of memory, etc.). Job selection and cursor position are preserved across refresh cycles.
- **Flexible filtering** — Filter by user (regex), job name (regex), state, partition, QoS, or node. Partitions, QoS, and nodes are populated from the cluster automatically. Filter settings are persisted to disk and restored on launch.
- **Live queue view** — Auto-refreshing job table with color-coded states (pending, running, failed, completed, suspended, out of memory, etc.). Job selection and cursor position are preserved across refresh cycles. Job fetching runs in a background thread so the UI never stalls.
- **Flexible filtering** — Persistent sidebar for filtering by user (regex), job name (regex), state, partition, QoS, or node. Partitions, QoS, and nodes are populated from the cluster automatically. Filter settings are persisted to disk and restored on launch.
- **Column configuration** — Choose which `squeue` fields to display, reorder them, and define multi-level sort priorities. Column settings are also persisted.
- **Script inspector** — Read the submission script of any job, with syntax highlighting via [`bat`](https://github.com/sharkdp/bat) if available. Falls back to plain text with line numbers.
- **Script inspector** — View the submission script of any job, with syntax highlighting via [`bat`](https://github.com/sharkdp/bat) if available. Falls back to plain text with line numbers. Script content loads in a background thread.
- **Log viewer** — Tail stdout/stderr logs in real time with automatic file watching via `notify`.
- **Custom output widgets** — Define additional file-watching panels for arbitrary job output files, with automatic JSON pretty-printing.
- **Widget layout** — Toggle visibility of individual panels (filters, script, stdout, stderr, custom widgets) and persist your preferred layout.
- **Clipboard support** — Copy widget contents to the system clipboard via OSC 52, which works over SSH and inside tmux without X11/Wayland.
- **Bulk actions** — Select one or many jobs and cancel them in batch with confirmation. Errors from `scancel` are reported through the flash notification bar.

## Requirements
Expand Down Expand Up @@ -51,65 +54,112 @@ Settings are stored in `~/.config/sqwatch/` (or `$XDG_CONFIG_HOME/sqwatch/`):
|------|----------|
| `filters.json` | Saved filter presets (user, states, partitions, QoS, nodes, name pattern) |
| `columns.json` | Visible columns and sort order |
| `layout.json` | Widget visibility and custom widget definitions |

Press `Ctrl+S` inside the filter or column dialog to persist the current configuration.
Press `Ctrl+S` inside the filter sidebar, column dialog, or widget selector to persist the current configuration.

## Keybindings

### Main View
### Global

| Key | Action |
|-----|--------|
| `Tab` | Cycle focus to next visible widget |
| `Shift+Tab` | Cycle focus to previous visible widget |
| `w` | Open widget selector (toggle panel visibility) |
| `c` | Open column / sort configuration (when table is focused) |
| `Esc` | Return focus to table, or quit if already on table |
| `Ctrl+C` | Copy focused widget contents to clipboard, or quit if on table |

### Job Table

| Key | Action |
|-----|--------|
| `Up` / `Down` | Navigate job list |
| `Space` | Toggle selection on focused job |
| `a` | Select / deselect all |
| `s` | View job script |
| `v` | View job log (stdout/stderr) |
| `f` | Open filter dialog |
| `c` | Open column / sort configuration |
| `x` | Cancel selected jobs (with confirmation) |
| `Esc` / `Ctrl+C` | Close overlay or quit |

### Script / Log Viewer
### Script / Log / Custom Widgets

| Key | Action |
|-----|--------|
| `Up` / `Down` | Scroll content |
| `Shift+Up` / `Shift+Down` | Switch to previous/next job |
| `Esc` | Close viewer |
| `PageUp` / `PageDown` | Scroll one page |
| `Ctrl+U` / `Ctrl+D` | Scroll one page (vim-style) |
| `Shift+Up` / `Shift+Down` | Switch to previous/next job in the table |

### Filter Sidebar

| Key | Action |
|-----|--------|
| `Up` / `Down` | Navigate between fields and filter sections |
| `Enter` | Edit text field or toggle checkbox |
| `Space` | Toggle checkbox item |
| `Ctrl+S` | Save filter settings to disk |

### Column / Sort Configuration

| Key | Action |
|-----|--------|
| `Up` / `Down` | Navigate within a list |
| `Left` / `Right` | Switch between pool, active, and sort lists |
| `Enter` | Add field to selected / sort, or toggle sort order |
| `Del` | Remove field from list |
| `Shift+Up` / `Shift+Down` | Reorder items |
| `Tab` | Cycle between lists |
| `r` | Reset to defaults |
| `Ctrl+S` | Save column settings to disk |
| `Esc` | Close |

### Widget Selector

| Key | Action |
|-----|--------|
| `Up` / `Down` | Navigate widget list |
| `Enter` / `Space` | Toggle widget visibility |
| `Ctrl+S` | Save layout to disk |
| `Esc` | Close |

## Architecture

The project is organized into four modules:

```
src/
├── main.rs # Entry point — terminal setup and teardown
├── dashboard.rs # Central orchestrator — event loop, state, rendering
├── main.rs # Entry point — terminal setup and teardown
├── dashboard.rs # Central orchestrator — event loop, state, rendering
├── backend/
│ ├── mod.rs # Job and JobState data types
│ ├── commands.rs # Async wrappers around SLURM CLI tools
│ └── query.rs # squeue invocation and output parsing
│ ├── mod.rs # Job and JobState data types
│ ├── commands.rs # Async wrappers around SLURM CLI tools
│ └── query.rs # squeue invocation and output parsing
├── core/
│ ├── input.rs # Keyboard/mouse/timer event loop (crossbeam channels)
│ ├── config.rs # Filter and column persistence (JSON, XDG paths)
│ └── live_file.rs # File watcher for live log tailing (notify crate)
│ ├── mod.rs
│ ├── input.rs # Keyboard/mouse/timer event loop (crossbeam channels)
│ ├── config.rs # Filter, column, and layout persistence (JSON, XDG paths)
│ ├── job_fetcher.rs # Background thread for periodic squeue refreshes
│ ├── job_detail.rs # Background scontrol cache (LRU, max 64 entries)
│ └── live_file.rs # File watcher for live log tailing (notify crate)
└── views/
├── chrome.rs # Titlebar, statusbar, and layout framing
├── job_table.rs # Job list table with selection and sorting
├── search.rs # Filter dialog (multi-tab, selectable lists)
├── fields.rs # Column and sort configuration dialog
├── script_pane.rs # Job script viewer with optional bat highlighting
└── output_pane.rs # Live log viewer (stdout/stderr)
├── mod.rs
├── chrome.rs # Titlebar, statusbar, and layout framing
├── job_table.rs # Job list table with selection and sorting
├── filter_tree.rs # Persistent filter sidebar with regex text fields and checkbox lists
├── fields.rs # Column and sort configuration dialog
├── script_widget.rs # Job script viewer with optional bat highlighting
├── output_widget.rs # Live log viewer (stdout/stderr)
├── custom_widget.rs # User-defined file-watching panels
├── widget_selector.rs # Panel visibility toggle popup
└── theme.rs # Centralized color constants
```

**Dashboard** is the central hub. It owns all view components, the query parameters, the tokio runtime for async SLURM commands, and the input event channel. The main loop is: receive input signal → dispatch to the appropriate handler → redraw.

**Backend** wraps all SLURM interactions. Commands are executed asynchronously via `async-process` and dispatched through a shared tokio runtime. The query module builds `squeue` invocations with dynamic format strings and parses the pipe-delimited output.

**Core** handles cross-cutting concerns: the input loop runs on a dedicated thread, multiplexing keyboard, mouse, resize, and timer events into a single `crossbeam` channel. The config module manages JSON persistence for filters and columns. The live file watcher uses `notify` to detect log file changes for real-time tailing.
**Core** handles cross-cutting concerns: the input loop runs on a dedicated thread, multiplexing keyboard, mouse, resize, and timer events into a single `crossbeam` channel. Background workers (`job_fetcher` and `job_detail`) run SLURM queries off the main thread, communicating results back via crossbeam channels polled on timer ticks. The config module manages JSON persistence for filters, columns, and layout. The live file watcher uses `notify` to detect log file changes for real-time tailing.

**Views** are pure rendering components. Each one receives a `Frame` and `Rect` from ratatui and draws itself. Overlay panes (script, log, filter, columns) are rendered on top of the main job table via popup regions.
**Views** are pure rendering components. Each one receives a `Frame` and `Rect` from ratatui and draws itself. The filter sidebar is a persistent side panel, while overlays (column config, widget selector) are rendered on top of the main layout via popup regions.

## Developers

Expand All @@ -127,17 +177,17 @@ cargo build
### Running checks

```sh
cargo fmt --all --check # Formatting (requires nightly rustfmt)
cargo fmt --all --check # Formatting (requires nightly rustfmt)
cargo clippy --all-targets -- -D warnings # Linting
cargo test # Tests
cargo deny check # License and advisory audit
cargo test # Tests
cargo deny check # License and advisory audit
```

### Project conventions

- **Edition 2024** — uses let-chains and other modern Rust features.
- **No `unsafe`** — the codebase is entirely safe Rust.
- **Async for SLURM commands only** — the TUI event loop is synchronous; async is used solely for non-blocking SLURM CLI calls via `async-process` + tokio.
- **Async for SLURM commands only** — the TUI event loop is synchronous; async is used solely for non-blocking SLURM CLI calls via `async-process` + tokio. Background workers use crossbeam channels, not async, to communicate with the dashboard.
- **`color-eyre`** for error handling — `Result<()>` flows from `main()` through the dashboard.

## Contributing
Expand Down
23 changes: 9 additions & 14 deletions src/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,11 +626,12 @@ impl Dashboard {
self.flash(format!("{} contents copied", title), 3);
}
}
FocusWidget::Sidebar => {
self.focus = FocusWidget::Table;
}
FocusWidget::Table => {
self.alive = false;
FocusWidget::Sidebar | FocusWidget::Table => {
self.field_sel = FieldSelector::new(
self.visible_fields.clone(),
self.sort_fields.clone(),
);
self.field_sel.visible = true;
}
}
return;
Expand All @@ -643,16 +644,10 @@ impl Dashboard {
self.cycle_focus_reverse();
return;
}
(_, KeyCode::Char('w')) => {
(KeyModifiers::CONTROL, KeyCode::Char('w')) => {
self.widget_sel.visible = true;
return;
}
(_, KeyCode::Char('c')) if self.focus == FocusWidget::Table => {
self.field_sel =
FieldSelector::new(self.visible_fields.clone(), self.sort_fields.clone());
self.field_sel.visible = true;
return;
}
_ => {}
}

Expand All @@ -679,14 +674,14 @@ impl Dashboard {
self.table.advance();
}
(_, KeyCode::Char(' ')) => self.table.flip_selection(),
(_, KeyCode::Char('a')) => {
(KeyModifiers::CONTROL, KeyCode::Char('a')) => {
if self.table.everything_marked() {
self.table.unmark_all();
} else {
self.table.mark_all();
}
}
(_, KeyCode::Char('x')) => {
(KeyModifiers::CONTROL, KeyCode::Char('x')) => {
self.confirming_cancel = true;
}
_ => {}
Expand Down
Loading