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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

/target
/lcov.info

# Packaging build artifacts
/packaging/archlinux/*.pkg.tar.*
Expand Down
20 changes: 12 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@

## Project Structure & Module Organization
- `src/main.rs`: thin binary entry point only (argument parsing, wiring, startup/shutdown).
- `src/`: feature modules. Keep logic out of `main.rs`.
- `src/lib.rs`: crate root; re-exports public API for integration tests.
- `src/cli.rs`: command-line argument parsing (`clap`).
- `src/app.rs`: mutable application state (selection, filter, collapsed pids).
- `src/runtime.rs`: key mapping, action dispatch, event loop, terminal setup/restore.
- `src/ui.rs`: rendering only (ratatui widgets).
- `src/process.rs`: process discovery, filter compilation, sort mapping.
- `src/signal.rs`: signal mapping and send helpers.
- `src/model.rs`: shared data types (`ProcRow`).
- `src/tree.rs`: tree display order and collapse logic.
- `src/debug_tui.rs`: deterministic debug/demo mode (no real processes).
- `tests/`: integration tests.
- `.github/workflows/`: CI for formatting, linting, tests, docs, and coverage.
- `Cargo.toml`: crate metadata/dependencies.
- `packaging/`: distro packaging files (Arch/Gentoo).
- `target/`: generated build artifacts; never commit.

Current state is still mostly monolithic. Ongoing refactors must split code into focused modules, such as:
- `src/app.rs`: app state and event loop orchestration.
- `src/ui.rs`: rendering only.
- `src/process.rs`: process discovery/filter/sort mapping.
- `src/signal.rs`: signal mapping and send helpers.
- `src/terminal.rs`: terminal setup/restore lifecycle.
Future refactor: extract `src/terminal.rs` for terminal setup/restore lifecycle (currently in `runtime.rs`).

Rules:
Module rules:
- Keep modules small and cohesive.
- Avoid cross-module cycles and hidden shared mutable state.
- Prefer pure functions for business logic; isolate side effects at boundaries.
Expand Down
33 changes: 33 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use ratatui::widgets::TableState;

use crate::{
model::ProcRow,
process::FilterSpec,
signal::signal_from_digit,
tree::{display_order_indices, display_rows},
};
Expand All @@ -33,11 +34,22 @@ struct ProcessIdentity {
start_time: u64,
}

/// State for the interactive `/` filter prompt.
#[derive(Debug)]
pub struct FilterInput {
/// Raw text the user has typed so far.
pub text: String,
/// Compiled substring spec for `text`; `None` when `text` is empty.
pub compiled: Option<FilterSpec>,
}

/// Mutable application state shared between input handling and rendering.
#[derive(Debug)]
pub struct App {
/// Optional process filter supplied from argv.
pub filter: Option<String>,
/// Compiled form of the active CLI filter (substring or regex).
pub compiled_filter: Option<FilterSpec>,
/// Current table rows.
pub rows: Vec<ProcRow>,
/// Selected row index in the process table.
Expand All @@ -48,6 +60,8 @@ pub struct App {
pub pending_confirmation: Option<SignalConfirmation>,
/// Pids whose visible descendants are hidden in tree mode.
pub collapsed_pids: HashSet<i32>,
/// Active interactive filter prompt; `Some` while the user is typing `/`.
pub filter_input: Option<FilterInput>,
}

/// Pending signal action that requires user confirmation.
Expand All @@ -73,11 +87,13 @@ impl App {

Self {
filter,
compiled_filter: None,
rows,
table_state,
status: String::new(),
pending_confirmation: None,
collapsed_pids: HashSet::new(),
filter_input: None,
}
}

Expand All @@ -86,6 +102,15 @@ impl App {
self.filter.as_deref()
}

/// Return the compiled filter that should be used for row matching and highlighting.
/// Prefers the interactive filter input when active, falls back to the CLI filter.
pub fn active_filter(&self) -> Option<&FilterSpec> {
self.filter_input
.as_ref()
.and_then(|fi| fi.compiled.as_ref())
.or(self.compiled_filter.as_ref())
}

/// Replace row data, keep selection bounded, and clear status text.
pub fn refresh(&mut self, rows: Vec<ProcRow>) {
self.apply_rows(rows);
Expand Down Expand Up @@ -332,6 +357,14 @@ impl App {
.cloned()
}

/// Select the first visible row; clears selection when the list is empty.
/// Call after filtering changes the row set to ensure the viewport starts at the top.
pub fn select_first(&mut self) {
let visible_count = self.visible_row_count();
self.table_state
.select(if visible_count == 0 { None } else { Some(0) });
}

fn visible_row_count(&self) -> usize {
display_order_indices(&self.rows, &self.collapsed_pids).len()
}
Expand Down
13 changes: 8 additions & 5 deletions src/debug_tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! Hidden synthetic-data TUI used for local UI development.

use std::{
cell::Cell,
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
Expand All @@ -37,7 +38,9 @@ const MAX_DEBUG_ROWS: usize = 21;
/// Run the hidden debug-only TUI with synthetic process rows.
pub(crate) fn run() -> Result<()> {
let mut terminal = setup_terminal()?;
let mut seed = initial_seed();
// Use Cell so the closure captures by shared reference, satisfying HRTB.
let seed = Cell::new(next_seed(initial_seed()));
let initial_rows = build_debug_rows(seed.get());
let mut draw = |app: &mut App| -> Result<()> {
terminal.draw(|frame| ui::render(frame, app))?;
Ok(())
Expand All @@ -49,12 +52,12 @@ pub(crate) fn run() -> Result<()> {
Ok(None)
}
};
let mut refresh_rows = || {
seed = next_seed(seed);
build_debug_rows(seed)
let mut refresh_rows = |_: Option<&crate::process::FilterSpec>| {
seed.set(next_seed(seed.get()));
build_debug_rows(seed.get())
};
let mut sender = debug_signal_sender;
let mut app = App::with_rows(None, refresh_rows());
let mut app = App::with_rows(None, initial_rows);
app.status = "debug tui: synthetic rows only".to_string();
let result = run_event_loop(
&mut app,
Expand Down
Loading
Loading