syswatch-terminal-diagnostics

v2026.09.24

SysWatch is a single-host system diagnostics TUI for macOS and Linux with twelve tabs, plain-English insights, and session scrubbing.

GitHub
Install command
npx skhub add reason-machines/syswatch-terminal-diagnostics
Markdown
SKILL.md

SysWatch Terminal Diagnostics

Skill by ara.so — Daily 2026 Skills collection.

SysWatch is a single-host system diagnostics TUI written in Rust for macOS and Linux. It consolidates what you'd normally get from htop, iostat, vm_stat, powermetrics, launchctl, and many other tools into twelve navigable tabs, with plain-English anomaly detection and a session-wide scrubber.


Install

git clone https://github.com/matthart1983/syswatch.git && cd syswatch
cargo build --release
./target/release/syswatch

Requirements: Rust 1.75+. No extra system dependencies on Linux. macOS links against system frameworks automatically.

Crates.io, Homebrew, and pre-built binaries are planned for the v0.1 release.


Running SysWatch

# Default — 1 Hz refresh
./target/release/syswatch

# 2 Hz refresh (500 ms tick)
syswatch --tick 500

# Boot directly into a specific tab
syswatch --tab procs
syswatch --tab cpu
syswatch --tab insights

Key Bindings

1 2 3 4 5 6 7 8 9   Overview / CPU / Mem / Disks / FS / Procs / GPU / Power / Services
0 - +               Net / Timeline / Insights
Tab / Shift-Tab     Cycle tabs forward / backward
↑ / ↓               Select row (Procs, Services tabs)
s                   Cycle sort column (Procs, Services tabs)
← / →               Scrub session backward / forward (Timeline tab)
Home / End          Jump to oldest sample / return to live
p                   Pause collection
q / Ctrl-C          Quit

Tabs Reference

KeyTabData Source / Replaces
1OverviewDashboard of all subsystems
2CPUhtop CPU panel, mpstat, top -d
3Memoryfree, vm_stat, htop mem panel
4Disksiostat, iotop (aggregate)
5Filesystemsdf -h, df -i, mount
6Procshtop, ps auxf, pstree
7GPUioreg AGXAccelerator / /sys/class/drm
8Powerpmset, ioreg AppleSmartBattery / /sys/class/power_supply
9Serviceslaunchctl list / systemctl list-units
0Netnettop, iftop
-TimelineSession log + scrubber
+InsightsPlain-English anomaly cards

Architecture Overview

src/
├── main.rs              CLI entry point + arg parsing
├── app.rs               Event loop, tab state, scrub plumbing
├── collect/
│   ├── collector.rs     sysinfo-backed CPU/Mem/Procs + dispatch
│   ├── gpu.rs           system_profiler / sysfs DRM
│   ├── power.rs         ioreg / pmset / sysfs power_supply
│   ├── services.rs      launchctl / systemctl
│   └── ring.rs          Bounded history ring + nth_back for scrubbing
├── insights/            Pure functions over (History, &Snapshot)
├── tabs/                One file per tab — thin renderers over the model
└── ui/
    ├── chrome.rs        Header, tab bar, footer
    ├── palette.rs       Single color source of truth
    └── widgets.rs       block_bar, sparkline, panel helpers

Refresh model:

  • 1 Hz fast loop: CPU, Memory, Procs, Net, IO
  • 5 s slow loop: Power, Services (subprocess-heavy on macOS)
  • CPU budget target: < 0.5% at idle

Extending SysWatch: Adding a Collector

Collectors live in src/collect/. Each one populates a typed Snapshot struct and is called from collector.rs.

// src/collect/my_subsystem.rs

use crate::collect::ring::Ring;

#[derive(Debug, Clone)]
pub struct MySnapshot {
    pub value: f64,
    pub label: String,
}

pub struct MyCollector {
    history: Ring<MySnapshot>,
}

impl MyCollector {
    pub fn new(capacity: usize) -> Self {
        Self {
            history: Ring::new(capacity),
        }
    }

    pub fn collect(&mut self) -> MySnapshot {
        // Replace with real data collection
        let snap = MySnapshot {
            value: 42.0,
            label: "example".to_string(),
        };
        self.history.push(snap.clone());
        snap
    }

    /// Returns the nth most recent snapshot (for scrubbing).
    pub fn nth_back(&self, n: usize) -> Option<&MySnapshot> {
        self.history.nth_back(n)
    }
}

Register it in collector.rs and call collect() in the fast or slow loop as appropriate.


Extending SysWatch: Adding a Tab Renderer

Tab renderers live in src/tabs/. They receive the current (or scrubbed) snapshot and render into a ratatui Frame.

// src/tabs/my_tab.rs
use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Style},
    widgets::{Block, Borders, Paragraph},
    Frame,
};
use crate::collect::my_subsystem::MySnapshot;

pub fn render(f: &mut Frame, area: Rect, snap: &MySnapshot) {
    let block = Block::default()
        .title(" My Tab ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::Cyan));

    let text = Paragraph::new(format!(
        "Value: {:.2}\nLabel: {}",
        snap.value, snap.label
    ))
    .block(block);

    f.render_widget(text, area);
}

Wire it into app.rs's tab dispatch match arm and add the tab label to ui/chrome.rs.


Extending SysWatch: Adding an Insight

Insights are pure functions in src/insights/. They take history + the latest snapshot and return zero or more anomaly cards.

// src/insights/my_insight.rs
use crate::collect::my_subsystem::MySnapshot;

#[derive(Debug, Clone)]
pub struct InsightCard {
    pub title: String,
    pub body: String,
    pub suggested_tab: &'static str,
}

pub fn check(snap: &MySnapshot) -> Vec<InsightCard> {
    let mut cards = vec![];

    if snap.value > 90.0 {
        cards.push(InsightCard {
            title: "High value detected".to_string(),
            body: format!(
                "Current value is {:.1}, which exceeds the 90.0 threshold.",
                snap.value
            ),
            suggested_tab: "my_tab",
        });
    }

    cards
}

Register the check in insights/mod.rs so it's included in the Insights tab and Overview badge.


Using the Ring Buffer (Session Scrubbing)

The Ring<T> type in src/collect/ring.rs is the backbone of session scrubbing. Any collector that wraps its history in a Ring gets scrubbing for free when the tab renderer calls nth_back.

use crate::collect::ring::Ring;

// Create a ring holding 3600 samples (1 hour at 1 Hz)
let mut ring: Ring<f64> = Ring::new(3600);

// Push a new sample each tick
ring.push(42.0);

// In scrub mode, app.rs tracks `scrub_offset: usize`
// 0 = live, N = N ticks in the past
let scrub_offset = 5; // 5 seconds ago
if let Some(val) = ring.nth_back(scrub_offset) {
    println!("Value 5s ago: {}", val);
}

In app.rs, the ←/→ keys increment/decrement scrub_offset, and every tab renderer receives the offset so they all show the same point in time.


Optional Cargo Features

# Cargo.toml — enable NVIDIA GPU stats (requires NVML / nvidia-smi)
[features]
gpu-nvidia = ["nvml-wrapper"]

# Enable SMART disk health (requires smartctl in PATH)
smart = []

Build with a feature:

cargo build --release --features gpu-nvidia
cargo build --release --features smart
cargo build --release --features gpu-nvidia,smart

Platform Notes

macOS

  • GPU utilization and used memory on Apple Silicon: available without sudo via ioreg AGXAccelerator PerformanceStatistics.
  • Fan speeds, per-component power, GPU temperature: require sudo powermetrics. SysWatch shows available data and displays a one-line note where sudo is needed — it never prompts.
  • Thermal zone temps require IOReport private FFI (deferred).

Linux

  • Thermal zones: available for free via sysfs (/sys/class/thermal/).
  • GPU data: read from /sys/class/drm.
  • Power supply: /sys/class/power_supply.
  • No elevated privileges required for core functionality.

Common Patterns

Checking live vs. scrubbed state in a tab

// In app.rs, scrub_offset == 0 means "live"
pub struct App {
    pub scrub_offset: usize,
    // ...
}

// In a tab renderer:
pub fn render(f: &mut Frame, area: Rect, app: &App) {
    let snap = if app.scrub_offset == 0 {
        app.collector.latest()
    } else {
        app.collector.nth_back(app.scrub_offset)
            .unwrap_or_else(|| app.collector.latest())
    };
    // render snap...
}

Pausing collection

The p key sets app.paused = true. The event loop skips collect() calls but the UI still redraws on keypress, so scrubbing works while paused.

if !app.paused {
    app.collector.collect();
}

Troubleshooting

SymptomFix
cargo build fails on macOS with framework linker errorsEnsure Xcode Command Line Tools are installed: xcode-select --install
GPU tab shows "no data" on Apple SiliconNormal without sudo for temp/power; util + memory should appear via ioreg
Services tab is slow to updateExpected — launchctl/systemctl are subprocess-heavy; they run on the 5 s slow loop
Timeline scrubbing shows stale dataThe ring capacity is set at startup; scrubbing is limited to collected history
Net tab shows no interfacesMay need to run as a user with access to network stats; check netwatch-sdk compatibility
syswatch --tab X not recognizedUse lowercase tab names: cpu, mem, disks, fs, procs, gpu, power, services, net, timeline, insights, overview

Anti-Goals (What SysWatch Will Not Do)

  • Not multi-host — use NetWatch's web dashboard for fleet monitoring.
  • Not a daemon — no background collector, no Prometheus push.
  • Not interactive — read-only by design; does not kill, renice, unmount, or restart anything.
  • Not a log search UI — surfaces OOM kills as signals; does not index logs.
  • No smooth charts — block sparklines and real numbers only.

Related Projects

  • NetWatch — sibling network diagnostics TUI, same chrome and palette.
  • netwatch-sdk — shared parsers for net interface counters and aggregate disk IO used by both tools.
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

Not specified

Source path

skills/syswatch-terminal-diagnostics

Default branch

main

Latest commit

2384a00

Tree SHA

ac47bb5