#![cfg(feature = "tui")]
use std::path::PathBuf;
use crate::{
apply_acknowledgments_or_exit, disclose, limits, load_config, load_report_from_input,
read_events, trace_not_found_exit, tui,
};
pub(crate) fn require_terminal_or_exit() {
use std::io::IsTerminal;
if !std::io::stdout().is_terminal() {
eprintln!("Error: the interactive TUI requires a terminal (stdout is not a TTY)");
std::process::exit(1);
}
}
fn exit_with_error(err: &dyn std::fmt::Display) -> ! {
eprintln!(
"Error: {}",
sentinel_core::text_safety::sanitize_for_terminal(&err.to_string())
);
std::process::exit(1);
}
pub(crate) fn launch_unified_tui(
report: sentinel_core::report::Report,
mut traces: Vec<sentinel_core::correlate::Trace>,
detect_config: sentinel_core::detect::DetectConfig,
initial_view: tui::View,
focus_trace_id: Option<&str>,
) {
if traces.is_empty() {
let mut trace_ids: std::collections::BTreeSet<String> =
report.findings.iter().map(|f| f.trace_id.clone()).collect();
if let Some(tid) = focus_trace_id {
trace_ids.insert(tid.to_string());
}
traces = trace_ids
.into_iter()
.map(|tid| sentinel_core::correlate::Trace {
trace_id: tid,
spans: vec![],
})
.collect();
}
let summary = tui::AnalyzeSummary {
green_summary: report.green_summary,
quality_gate: report.quality_gate,
analysis: report.analysis,
};
let mut app = tui::App::new(report.findings, traces, detect_config)
.with_correlations(report.correlations)
.with_summary(summary)
.with_initial_view(initial_view);
if let Some(tid) = focus_trace_id {
app = app.with_focus_trace(tid);
}
if let Err(e) = tui::run(&mut app) {
eprintln!("TUI error: {e}");
std::process::exit(1);
}
}
pub(crate) fn cmd_inspect(
input: &std::path::Path,
config_path: Option<&std::path::Path>,
acknowledgments_path: Option<&std::path::Path>,
no_acknowledgments: bool,
) {
require_terminal_or_exit();
let config = load_config(config_path);
let raw = read_events(Some(input), limits::MAX_BATCH_INPUT_BYTES);
let detect_config = sentinel_core::detect::DetectConfig::from(&config);
let (mut report, traces) = load_report_from_input(&raw, &config);
apply_acknowledgments_or_exit(
&mut report,
&config,
acknowledgments_path,
no_acknowledgments,
);
launch_unified_tui(report, traces, detect_config, tui::View::Inspect, None);
}
pub(crate) fn cmd_analyze_tui(
input: Option<&std::path::Path>,
config_path: Option<&std::path::Path>,
acknowledgments_path: Option<&std::path::Path>,
no_acknowledgments: bool,
) {
require_terminal_or_exit();
let config = load_config(config_path);
let raw = read_events(input, limits::MAX_BATCH_INPUT_BYTES);
let detect_config = sentinel_core::detect::DetectConfig::from(&config);
let (mut report, traces) = load_report_from_input(&raw, &config);
apply_acknowledgments_or_exit(
&mut report,
&config,
acknowledgments_path,
no_acknowledgments,
);
launch_unified_tui(report, traces, detect_config, tui::View::Analyze, None);
}
pub(crate) fn cmd_explain_tui(
input: &std::path::Path,
trace_id: &str,
config_path: Option<&std::path::Path>,
) {
require_terminal_or_exit();
let config = load_config(config_path);
let raw = read_events(Some(input), limits::MAX_BATCH_INPUT_BYTES);
let detect_config = sentinel_core::detect::DetectConfig::from(&config);
let (mut report, traces) = load_report_from_input(&raw, &config);
let known = traces.iter().any(|t| t.trace_id == trace_id)
|| report.findings.iter().any(|f| f.trace_id == trace_id);
if !known {
let available: Vec<&str> = if traces.is_empty() {
report
.findings
.iter()
.map(|f| f.trace_id.as_str())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect()
} else {
traces.iter().map(|t| t.trace_id.as_str()).collect()
};
trace_not_found_exit(trace_id, available.into_iter());
}
apply_acknowledgments_or_exit(&mut report, &config, None, false);
launch_unified_tui(
report,
traces,
detect_config,
tui::View::Explain,
Some(trace_id),
);
}
pub(crate) fn cmd_disclose_tui(
input: Vec<PathBuf>,
org_config: &std::path::Path,
strict_attribution: bool,
) {
use sentinel_core::report::periodic::aggregator::archive_time_range;
use sentinel_core::report::periodic::org_config as org_config_loader;
require_terminal_or_exit();
let org =
org_config_loader::load_from_path(org_config).unwrap_or_else(|err| exit_with_error(&err));
let archive_range = archive_time_range(&input).unwrap_or_else(|err| exit_with_error(&err));
let state = disclose::DiscloseState::new(
input,
org,
org_config.to_path_buf(),
strict_attribution,
archive_range,
chrono::Utc::now().date_naive(),
);
let config = load_config(None);
let detect_config = sentinel_core::detect::DetectConfig::from(&config);
let mut app = tui::App::new(Vec::new(), Vec::new(), detect_config)
.with_disclose(state)
.with_initial_view(tui::View::Disclose);
if let Err(e) = tui::run(&mut app) {
eprintln!("TUI error: {e}");
std::process::exit(1);
}
}