pub mod alerts_panel;
pub mod app;
pub mod dashboard;
pub mod diff_viewer;
pub mod files_panel;
pub mod live;
pub mod network_panel;
pub mod summary;
pub mod summary_panel;
pub mod theme;
use crate::events::Event;
use crate::ui::app::{App, Tab};
use crate::ui::live::SessionStats;
use anyhow::Result;
use crossterm::{
event::{self, Event as CEvent, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Frame, Terminal};
use std::io;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::mpsc;
pub async fn run_dashboard(
mut rx: mpsc::Receiver<Event>,
_agent_label: String,
_agent_pid: Option<u32>,
no_color: bool,
) -> Result<SessionStats> {
let app = Arc::new(Mutex::new(App::new(None, no_color)));
let app_writer = app.clone();
let receiver_handle = tokio::spawn(async move {
while let Some(event) = rx.recv().await {
if let Ok(mut state) = app_writer.lock() {
state.ingest_event(event);
}
}
});
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
loop {
{
let state = app.lock().unwrap();
terminal.draw(|f: &mut Frame| render_frame(f, &state))?;
}
while event::poll(Duration::ZERO)? {
if let Ok(CEvent::Key(key)) = event::read() {
let mut state = app.lock().unwrap();
handle_key(&mut state, key.code, key.modifiers);
}
}
if app.lock().unwrap().should_quit {
break;
}
tokio::time::sleep(Duration::from_millis(33)).await;
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
drop(terminal);
receiver_handle.abort();
let state = app.lock().unwrap();
let stats = SessionStats {
event_count: state.events.len(),
risk_score: state.risk.score,
start: std::time::Instant::now(),
events: state.events.iter().cloned().collect(),
files_read: state.stats.files_read,
files_written: state.stats.files_written,
net_connections: state.stats.net_connections,
net_unknown: state.stats.net_unknown,
commands: state.stats.commands_total,
secrets: state.stats.secrets_accessed,
alerts: state.findings.len(),
clipboard_reads: state.stats.clipboard_reads,
};
Ok(stats)
}
fn render_frame(frame: &mut Frame, app: &App) {
let area = frame.area();
match app.active_tab {
Tab::Dashboard => dashboard::render(frame, area, app),
Tab::Files => files_panel::render(frame, area, app),
Tab::Network => network_panel::render(frame, area, app),
Tab::Diffs => diff_viewer::render(frame, area, app),
Tab::Summary => summary_panel::render(frame, area, app),
Tab::Alerts => alerts_panel::render(frame, area, app),
}
}
fn handle_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers) {
match code {
KeyCode::Char('q') => app.should_quit = true,
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
app.should_quit = true;
}
KeyCode::Esc => {
if app.active_tab == Tab::Dashboard {
app.should_quit = true;
} else {
app.switch_tab(Tab::Dashboard);
}
}
KeyCode::Char('f') => app.switch_tab(Tab::Files),
KeyCode::Char('n') => app.switch_tab(Tab::Network),
KeyCode::Char('d') => app.switch_tab(Tab::Diffs),
KeyCode::Char('s') => app.switch_tab(Tab::Summary),
KeyCode::Char('a') => app.switch_tab(Tab::Alerts),
KeyCode::Char('1') => app.switch_tab(Tab::Dashboard),
KeyCode::Char('2') => app.switch_tab(Tab::Files),
KeyCode::Char('3') => app.switch_tab(Tab::Network),
KeyCode::Char('4') => app.switch_tab(Tab::Diffs),
KeyCode::Char('5') => app.switch_tab(Tab::Summary),
KeyCode::Char('6') => app.switch_tab(Tab::Alerts),
KeyCode::Tab => app.switch_tab(app.active_tab.next()),
KeyCode::BackTab => app.switch_tab(app.active_tab.prev()),
KeyCode::Char('j') | KeyCode::Down => app.scroll_down(),
KeyCode::Char('k') | KeyCode::Up => app.scroll_up(),
KeyCode::Char('G') => app.scroll_top(),
KeyCode::Char('g') => app.scroll_offset = 999_999,
KeyCode::PageUp => {
for _ in 0..10 {
app.scroll_down();
}
}
KeyCode::PageDown => {
for _ in 0..10 {
app.scroll_up();
}
}
_ => {}
}
}