use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use crossbeam_channel::Receiver;
use crossterm::cursor::{Hide, Show};
use crossterm::event::{
DisableMouseCapture, EnableMouseCapture, Event as CtEvent, KeyCode, KeyEvent, KeyEventKind,
KeyModifiers, poll, read,
};
use crossterm::execute;
use crossterm::terminal::{
BeginSynchronizedUpdate, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Paragraph, Tabs};
use tui_big_text::{BigText, PixelSize};
use tui_input::backend::crossterm::EventHandler;
use super::app::{ActiveTab, App};
use super::panes;
use super::theme::{GlyphMode, Skin};
use super::transport::{CertsSnapshot, ListenersSnapshot, Snapshot, TopEvent};
const RENDER_INTERVAL: Duration = Duration::from_millis(33);
pub struct RenderConfig {
pub mouse: bool,
pub tick_once: bool,
pub snapshot_frames: Option<u32>,
pub skin: Option<String>,
pub glyphs: Option<crate::cli::TopGlyphs>,
pub initial_status: Option<String>,
pub lease_status: crate::ctl::top::cardinality::StatusSlot,
}
pub fn run(
cfg: RenderConfig,
snapshots: Receiver<Snapshot>,
events: Receiver<TopEvent>,
listeners: Receiver<ListenersSnapshot>,
certs: Receiver<CertsSnapshot>,
) -> io::Result<()> {
let _panic_guard = PanicHookGuard::install(|| {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen, Show);
});
let signal_quit = Arc::new(AtomicBool::new(false));
let mut signal_handler_status: Option<String> = None;
if let Err(err) = ctrlc::set_handler({
let signal_quit = Arc::clone(&signal_quit);
move || signal_quit.store(true, Ordering::SeqCst)
}) {
signal_handler_status = Some(format!(
"ctrlc handler install failed ({err}); Ctrl-C via keypress still works"
));
}
let mut app = App::new();
let (skin, skin_status) = Skin::resolve(cfg.skin.as_deref());
let glyphs = GlyphMode::resolve(cfg.glyphs);
app.glyphs = glyphs;
if let Some(msg) = skin_status {
app.status = msg;
} else if let Some(msg) = cfg.initial_status {
app.status = msg;
} else if let Some(msg) = signal_handler_status {
app.status = msg;
}
let _guard = RawModeGuard::install(cfg.mouse)?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
let sync_output = std::env::var("SOZU_TOP_SYNC").ok().as_deref() != Some("0");
let mut last_render = Instant::now() - RENDER_INTERVAL;
let mut frames_drawn: u32 = 0;
let snapshot_frames_target = cfg.snapshot_frames;
loop {
if signal_quit.load(Ordering::SeqCst) || app.should_quit {
break;
}
while let Ok(snap) = snapshots.try_recv() {
app.ingest_snapshot(&snap);
}
while let Ok(ev) = events.try_recv() {
app.ingest_event(ev);
}
while let Ok(listeners) = listeners.try_recv() {
app.ingest_listeners(listeners);
}
while let Ok(certs) = certs.try_recv() {
app.ingest_certs(certs);
}
if let Some(msg) = crate::ctl::top::cardinality::take_status(&cfg.lease_status) {
app.status = msg;
app.mark_dirty();
}
let now = Instant::now();
let next_render = last_render + RENDER_INTERVAL;
let timeout = next_render
.saturating_duration_since(now)
.min(Duration::from_millis(50));
if poll(timeout)? {
match read()? {
CtEvent::Key(key) if key.kind == KeyEventKind::Press => {
handle_key(&mut app, key);
}
CtEvent::Resize(_, _) => {
app.mark_dirty();
}
_ => {}
}
}
if last_render.elapsed() >= RENDER_INTERVAL {
app.tick_pulses();
let dirty = app.take_dirty() || app.pulse.has_active();
if !dirty {
continue;
}
if sync_output {
execute!(io::stdout(), BeginSynchronizedUpdate)?;
}
terminal.draw(|f| draw(f, &app, &skin))?;
if sync_output {
execute!(io::stdout(), EndSynchronizedUpdate)?;
}
last_render = Instant::now();
frames_drawn += 1;
if cfg.tick_once && frames_drawn >= 1 {
break;
}
if let Some(target) = snapshot_frames_target {
if frames_drawn >= target {
break;
}
}
}
}
Ok(())
}
fn handle_key(app: &mut App, key: KeyEvent) {
if app.palette_open {
match key.code {
KeyCode::Enter => app.apply_palette(),
KeyCode::Esc => app.cancel_palette(),
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.cancel_palette()
}
_ => {
app.palette_input.handle_event(&CtEvent::Key(key));
app.mark_dirty();
}
}
return;
}
match key.code {
KeyCode::Char(':') => app.open_palette(),
KeyCode::Char('q') | KeyCode::Char('Q') => app.should_quit = true,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.should_quit = true
}
KeyCode::F(10) => app.should_quit = true,
KeyCode::Char('?') | KeyCode::F(1) => {
app.help_visible = !app.help_visible;
app.mark_dirty();
}
KeyCode::F(2) => {
app.glyphs = app.glyphs.cycle();
app.mark_dirty();
}
KeyCode::F(3) | KeyCode::F(4) => app.open_palette(),
KeyCode::F(5) => {
app.paused = !app.paused;
app.mark_dirty();
}
KeyCode::F(6) => match app.active_tab {
ActiveTab::Clusters => {
app.cluster_sort = app.cluster_sort.cycle();
app.mark_dirty();
}
ActiveTab::Backends => {
app.backend_sort = app.backend_sort.cycle();
app.mark_dirty();
}
_ => {}
},
KeyCode::Tab => {
app.active_tab = app.active_tab.cycle(true);
app.mark_dirty();
}
KeyCode::BackTab => {
app.active_tab = app.active_tab.cycle(false);
app.mark_dirty();
}
KeyCode::Char(c @ '1'..='7') => {
if let Some(tab) = ActiveTab::from_digit(c.to_digit(10).unwrap_or(0) as u8) {
app.active_tab = tab;
app.mark_dirty();
}
}
KeyCode::Char('s') if app.active_tab == ActiveTab::Clusters => {
app.cluster_sort = app.cluster_sort.cycle();
app.mark_dirty();
}
KeyCode::Char('S') if app.active_tab == ActiveTab::Clusters => {
app.cluster_sort_reverse = !app.cluster_sort_reverse;
app.mark_dirty();
}
KeyCode::Char('s') if app.active_tab == ActiveTab::Backends => {
app.backend_sort = app.backend_sort.cycle();
app.mark_dirty();
}
KeyCode::Char('S') if app.active_tab == ActiveTab::Backends => {
app.backend_sort_reverse = !app.backend_sort_reverse;
app.mark_dirty();
}
KeyCode::Char('p') | KeyCode::Char('P') => {
app.paused = !app.paused;
app.mark_dirty();
}
_ => {}
}
}
fn draw(f: &mut ratatui::Frame<'_>, app: &App, skin: &Skin) {
let area = f.area();
let alert = app.thresholds.critical_message(&app.overview);
let constraints: Vec<Constraint> = match alert {
Some(_) => vec![
Constraint::Length(3), Constraint::Length(5), Constraint::Min(8), Constraint::Length(1), ],
None => vec![
Constraint::Length(3),
Constraint::Min(8),
Constraint::Length(1),
],
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(area);
draw_tabs(f, chunks[0], app, skin);
if let Some(headline) = alert {
draw_alert(f, chunks[1], skin, headline);
draw_pane(f, chunks[2], app, skin);
draw_fkey_bar(f, chunks[3], app, skin);
} else {
draw_pane(f, chunks[1], app, skin);
draw_fkey_bar(f, chunks[2], app, skin);
}
}
fn draw_alert(f: &mut ratatui::Frame<'_>, area: Rect, skin: &Skin, headline: &str) {
let cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
.split(area);
let big = BigText::builder()
.pixel_size(PixelSize::Quadrant)
.style(Style::default().fg(skin.hot).add_modifier(Modifier::BOLD))
.lines(vec![Line::from(headline.to_owned())])
.build();
f.render_widget(big, cols[0]);
let side = Paragraph::new(vec![
Line::from(Span::styled(
"ALERT",
Style::default().fg(skin.hot).add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
headline.to_owned(),
Style::default().fg(skin.primary),
)),
Line::from(Span::styled(
"see OVERVIEW for context",
Style::default().fg(skin.secondary),
)),
])
.alignment(Alignment::Left);
f.render_widget(side, cols[1]);
}
fn draw_tabs(f: &mut ratatui::Frame<'_>, area: Rect, app: &App, skin: &Skin) {
let titles: Vec<Line<'_>> = ActiveTab::ALL
.iter()
.enumerate()
.map(|(i, t)| {
let n = i + 1;
Line::from(vec![Span::styled(
format!(" {n} {} ", t.label()),
if *t == app.active_tab {
skin.tab_focused()
} else {
skin.tab_unfocused()
},
)])
})
.collect();
let selected = ActiveTab::ALL
.iter()
.position(|t| *t == app.active_tab)
.unwrap_or(0);
let title = format!(
" sōzu top · {} ",
app.last_snapshot_at
.map(|_| "live".to_owned())
.unwrap_or_else(|| "no snapshot yet".into())
);
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(title)
.style(Style::default().fg(skin.muted));
let tabs = Tabs::new(titles)
.select(selected)
.block(block)
.divider(Span::raw(" "));
f.render_widget(tabs, area);
}
fn draw_pane(f: &mut ratatui::Frame<'_>, area: Rect, app: &App, skin: &Skin) {
match app.active_tab {
ActiveTab::Overview => panes::overview::render(f, area, app, skin),
ActiveTab::Clusters => panes::clusters::render(f, area, app, skin),
ActiveTab::Backends => panes::backends::render(f, area, app, skin),
ActiveTab::Listeners => panes::listeners::render(f, area, app, skin),
ActiveTab::Certs => panes::certs::render(f, area, app, skin),
ActiveTab::H2 => panes::h2::render(f, area, app, skin),
ActiveTab::Events => panes::events::render(f, area, app, skin),
}
}
fn draw_fkey_bar(f: &mut ratatui::Frame<'_>, area: Rect, app: &App, skin: &Skin) {
if app.palette_open {
draw_palette(f, area, app, skin);
return;
}
let bindings: &[(&str, &str)] = &[
("F1", "Help"),
("F2", "Glyphs"),
("F3", "Find"),
("F4", "Filter"),
("F5", if app.paused { "Resume" } else { "Pause" }),
("F6", "Sort"),
("F7", "·"),
("F8", "·"),
("F9", "·"),
("F10", "Quit"),
];
let mut spans: Vec<Span<'_>> = Vec::new();
for (k, a) in bindings {
spans.push(Span::styled(format!(" {k} "), skin.fkey_label()));
spans.push(Span::styled(format!(" {a} "), skin.fkey_action()));
}
spans.push(Span::raw(" "));
if let Some(err) = app.palette_error.as_ref() {
spans.push(Span::styled(
format!(" {err} "),
Style::default().fg(skin.hot).add_modifier(Modifier::BOLD),
));
} else {
spans.push(Span::styled(
" : palette ",
Style::default()
.fg(skin.accent)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
format!(" sort: {} ", app.cluster_sort.label()),
Style::default()
.fg(skin.accent)
.add_modifier(Modifier::BOLD),
));
}
let para = Paragraph::new(Line::from(spans)).alignment(Alignment::Left);
f.render_widget(para, area);
}
fn draw_palette(f: &mut ratatui::Frame<'_>, area: Rect, app: &App, skin: &Skin) {
let value = app.palette_input.value();
let line = Line::from(vec![
Span::styled(
" :",
Style::default()
.fg(skin.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(
value.to_owned(),
Style::default()
.fg(skin.primary)
.add_modifier(Modifier::BOLD),
),
Span::styled(
"_ ", Style::default()
.fg(skin.accent)
.add_modifier(Modifier::SLOW_BLINK),
),
Span::styled(
"Enter apply · Esc cancel · :cluster :backend :listener :cert :h2 :event :help :quit",
Style::default().fg(skin.secondary),
),
]);
f.render_widget(Paragraph::new(line).alignment(Alignment::Left), area);
}
struct RawModeGuard {
mouse_enabled: bool,
alt_entered: bool,
}
impl RawModeGuard {
fn install(mouse: bool) -> io::Result<Self> {
enable_raw_mode()?;
let mut guard = Self {
mouse_enabled: false,
alt_entered: false,
};
let mut out = io::stdout();
execute!(out, EnterAlternateScreen, Hide)?;
guard.alt_entered = true;
if mouse {
execute!(out, EnableMouseCapture)?;
guard.mouse_enabled = true;
}
Ok(guard)
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
let mut out = io::stdout();
if self.mouse_enabled {
let _ = execute!(out, DisableMouseCapture);
}
if self.alt_entered {
let _ = execute!(out, Show, LeaveAlternateScreen);
}
let _ = disable_raw_mode();
}
}
type BoxedPanicHook = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + Send + Sync + 'static>;
struct PanicHookGuard {
prior: std::sync::Arc<std::sync::Mutex<Option<BoxedPanicHook>>>,
}
impl PanicHookGuard {
fn install<F>(restore: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
let prior = std::sync::Arc::new(std::sync::Mutex::new(Some(std::panic::take_hook())));
let prior_for_hook = std::sync::Arc::clone(&prior);
std::panic::set_hook(Box::new(move |info| {
restore();
if let Ok(g) = prior_for_hook.lock()
&& let Some(h) = g.as_ref()
{
h(info);
}
}));
Self { prior }
}
}
impl Drop for PanicHookGuard {
fn drop(&mut self) {
if let Ok(mut g) = self.prior.lock()
&& let Some(prior) = g.take()
{
std::panic::set_hook(prior);
}
}
}