use std::io::IsTerminal as _;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use tokio::sync::oneshot;
use tracing::error;
mod data;
mod log;
mod observer;
mod render;
mod state;
#[cfg(test)]
mod tests;
pub use data::{Dashboard, Snapshot, SourceRow, TileDot, TileRequest};
pub use observer::observe;
use state::LogView;
const PAGE: usize = 10;
static DASHBOARD: OnceLock<Arc<Dashboard>> = OnceLock::new();
static ON_SCREEN: AtomicBool = AtomicBool::new(false);
#[must_use]
pub fn is_available() -> bool {
std::io::stdout().is_terminal()
}
pub fn install(log_filter: &str) -> Arc<Dashboard> {
let dashboard = Arc::new(Dashboard::new());
crate::logging::init_tracing_into(log_filter, dashboard.log());
assert!(
DASHBOARD.set(Arc::clone(&dashboard)).is_ok(),
"the dashboard is installed once per process"
);
dashboard
}
#[must_use]
pub fn is_installed() -> bool {
DASHBOARD.get().is_some()
}
fn installed() -> Option<&'static Arc<Dashboard>> {
DASHBOARD.get()
}
pub fn restore_terminal() {
if ON_SCREEN.swap(false, Ordering::SeqCst) {
ratatui::restore();
}
}
pub fn run(dashboard: Arc<Dashboard>, quit: oneshot::Sender<()>) {
std::thread::Builder::new()
.name("martin-tui".to_owned())
.spawn(move || {
let mut terminal = ratatui::init();
ON_SCREEN.store(true, Ordering::SeqCst);
let result = show(&mut terminal, &dashboard);
restore_terminal();
if let Err(e) = result {
error!("The dashboard stopped: {e}");
}
let _ = quit.send(());
})
.expect("failed to spawn the dashboard thread");
}
fn show(terminal: &mut ratatui::DefaultTerminal, dashboard: &Dashboard) -> std::io::Result<()> {
let mut log = LogView::default();
loop {
let view = dashboard.snapshot();
terminal.draw(|frame| render::frame(frame, &view, log))?;
if !event::poll(Duration::from_millis(100))? {
continue;
}
let Event::Key(key) = event::read()? else {
continue;
};
if key.kind != KeyEventKind::Press {
continue;
}
let control = key.modifiers.contains(KeyModifiers::CONTROL);
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc)
|| (control && key.code == KeyCode::Char('c'))
{
return Ok(());
}
if key.code == KeyCode::Char('c') {
dashboard.clear();
}
if key.code == KeyCode::Char('l') {
log.toggle_size();
}
let lines = view.log.len();
if matches!(key.code, KeyCode::Up | KeyCode::Char('k')) {
log.scroll_back(1, lines);
} else if key.code == KeyCode::PageUp {
log.scroll_back(PAGE, lines);
} else if matches!(key.code, KeyCode::Down | KeyCode::Char('j')) {
log.scroll_forward(1, lines);
} else if key.code == KeyCode::PageDown {
log.scroll_forward(PAGE, lines);
} else if key.code == KeyCode::Home {
log.scroll_to_oldest(lines);
} else if key.code == KeyCode::End {
log.follow();
}
}
}