use crate::cli::DashboardArgs;
use crate::color::ColorProfile;
use crate::commands::watch::{SessionArgs, run_registry};
use crate::core::dashboard_file::load;
use crate::core::registry::Registry;
use crate::exit::AppResult;
use crate::theme::Palette;
pub fn run(args: DashboardArgs, profile: ColorProfile, palette: Palette) -> AppResult {
let registry = load(&args.file)?;
let session = SessionArgs {
once: args.once,
clear: args.clear,
no_hide_cursor: args.no_hide_cursor,
no_sync: args.no_sync,
wrap: false,
max_height: args.max_height,
snapshot_dir: args.snapshot_dir.clone(),
snapshot_ansi: args.snapshot_ansi,
live_tail: dashboard_suffix(args.once, registry.len()),
help_heading: "rat dashboard — keys",
help_extra: pane_help(®istry),
resize_respawn: true,
};
run_registry(registry, session, profile, palette)
}
fn dashboard_suffix(once: bool, sources: usize) -> String {
if once {
return String::new();
}
match sources {
1 => " · 1 source · ? help".to_string(),
n => format!(" · {n} sources · ? help"),
}
}
fn pane_help(registry: &Registry) -> Vec<String> {
let mut lines = vec![String::new(), " panes:".to_string()];
for id in registry.ids() {
let spec = registry.spec(id);
lines.push(format!(
" {} {}",
spec.name,
crate::commands::watch::cadence_label(spec)
));
for trigger in &spec.triggers {
lines.push(format!(" {trigger}"));
}
}
if registry
.ids()
.any(|id| !registry.spec(id).triggers.is_empty())
{
lines.extend(LOOPING_HELP.iter().map(|l| (*l).to_string()));
}
lines
}
const LOOPING_HELP: &[&str] = &[
"",
" looping panes:",
" A pane marked `· looping` is still running — nothing has been",
" stopped. rat cannot see who writes a file, only that a watched",
" path changes while the dashboard is busy and never while it is",
" idle, which is what a pane whose own command touches another",
" pane's trigger looks like.",
"",
" The fix is in the declaration: give the command a guard so it",
" writes only when the content changed, or point the trigger at a",
" path no pane writes. A trigger fires on mtime, not on content,",
" so writing identical bytes still fires — the guard has to skip",
" the write, not just the change.",
"",
" The absence of the badge is weaker than its presence. rat stays",
" silent whenever it cannot tell: when a write cannot be placed",
" against the commands that were running, when a reader's evidence",
" was lost, or when the dashboard was too busy to judge. No badge",
" means no loop was proved, not that there is none.",
];
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::core::box_model::{BorderPreset, Sides};
use crate::core::registry::{LayoutNode, Overflow, PaneBox, PaneWidth, SourceId, SourceSpec};
use crate::core::trigger::TriggerSpec;
fn registry(triggers: bool) -> Registry {
let spec = |name: &str, path: &str| SourceSpec {
name: name.to_string(),
command: vec!["true".to_string()],
shell: false,
interval: (!triggers).then(|| Duration::from_secs(5)),
triggers: if triggers {
vec![TriggerSpec::File(std::path::PathBuf::from(path))]
} else {
Vec::new()
},
debounce: Duration::from_millis(250),
};
let pane = || PaneBox {
height: 5,
width: PaneWidth::Weight(1),
overflow: Overflow::KeepTop,
border: BorderPreset::Rounded,
padding: Sides::default(),
title: None,
chrome: true,
};
Registry::panes(
vec![spec("a", "./sa"), spec("b", "./sb")],
vec![pane(), pane()],
LayoutNode::Row(vec![
LayoutNode::Pane(SourceId(0)),
LayoutNode::Pane(SourceId(1)),
]),
1,
0,
)
.expect("a valid two-pane registry")
}
#[test]
fn the_help_explains_the_badge_and_names_the_fix() {
let lines = pane_help(®istry(true));
let text = lines.iter().map(|l| l.trim()).collect::<Vec<_>>().join(" ");
assert!(text.contains("looping"), "got {text}");
assert!(text.contains("nothing has been stopped"), "got {text}");
assert!(text.contains("mtime, not on content"), "got {text}");
assert!(text.contains("no loop was proved"), "got {text}");
}
#[test]
fn the_help_stays_inside_the_width_the_key_table_already_sets() {
for line in pane_help(®istry(true)) {
assert!(
line.chars().count() <= 74,
"{} cells: {line:?}",
line.chars().count()
);
}
}
#[test]
fn the_help_is_unchanged_when_no_pane_has_a_trigger() {
assert_eq!(
pane_help(®istry(false)),
vec![
String::new(),
" panes:".to_string(),
" a every 5s".to_string(),
" b every 5s".to_string(),
]
);
}
}