mod app;
mod graph;
mod overview;
mod ui;
mod widget_panel;
use std::io::stdout;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::Result;
use ratatui::crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent,
KeyEventKind, KeyModifiers, MouseButton, MouseEventKind,
};
use ratatui::crossterm::execute;
use ratatui::layout::{Constraint, Layout, Position, Rect};
use rigtop_core::demo::DemoSource;
use rigtop_core::hosts::{HostKind, HostSpec};
use rigtop_core::remote::RemoteSource;
use rigtop_core::widget::{Widget, start_widgets};
use rigtop_core::worker::{Command, Worker};
use rigtop_core::{Config, DataSource, LocalSource};
use app::App;
use overview::View;
const INPUT_POLL: Duration = Duration::from_millis(50);
pub struct HostTui {
pub spec: HostSpec,
worker: Worker,
pub app: App,
sent_limit: Option<Option<usize>>,
}
pub fn run(config: Config, interval: Duration, specs: Vec<HostSpec>) -> Result<()> {
let mut hosts: Vec<HostTui> = specs
.into_iter()
.map(|spec| {
let source_config = config.clone();
let kind = spec.kind.clone();
let worker = Worker::spawn(
move || -> Box<dyn DataSource> {
match kind {
HostKind::Remote(target) => Box::new(RemoteSource::new(target)),
HostKind::Local => Box::new(LocalSource::with_config(&source_config)),
HostKind::Demo(host) => Box::new(DemoSource::new(host)),
}
},
interval,
);
let mut app = App::new(config.clone(), interval);
if !spec.is_local() {
app.waiting_message = format!("connecting to {}…", spec.name);
}
HostTui {
spec,
worker,
app,
sent_limit: None,
}
})
.collect();
let widgets = start_widgets(&config.widgets, || {});
let mut screen = Screen {
view: if hosts.len() > 1 {
View::Overview
} else {
View::Host(0)
},
include_local: config.overview.include_local,
processes: config.overview.processes,
tabs: Vec::new(),
quit: false,
show_widgets: true,
};
let mut terminal = ratatui::init();
execute!(stdout(), EnableMouseCapture)?;
let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = execute!(stdout(), DisableMouseCapture);
previous_hook(info);
}));
let result = event_loop(&mut terminal, &mut hosts, &mut screen, &widgets);
let _ = execute!(stdout(), DisableMouseCapture);
ratatui::restore();
result
}
struct Screen {
view: View,
include_local: bool,
processes: usize,
tabs: Vec<(Rect, View)>,
quit: bool,
show_widgets: bool,
}
impl Screen {
fn step(&mut self, hosts: usize, forward: bool) {
let count = hosts + 1;
let position = match self.view {
View::Overview => 0,
View::Host(i) => i + 1,
};
let next = if forward {
(position + 1) % count
} else {
(position + count - 1) % count
};
self.view = if next == 0 {
View::Overview
} else {
View::Host(next - 1)
};
}
fn handle_key(&mut self, key: KeyEvent, hosts: usize) -> bool {
match key.code {
KeyCode::Tab => self.step(hosts, true),
KeyCode::BackTab => self.step(hosts, false),
KeyCode::Char('0') => self.view = View::Overview,
KeyCode::Char(c @ '1'..='9') => {
let index = c as usize - '1' as usize;
if index < hosts {
self.view = View::Host(index);
}
}
KeyCode::Char('l') if self.view == View::Overview => {
self.include_local = !self.include_local
}
KeyCode::Char('q') if self.view == View::Overview => self.quit = true,
_ => return false,
}
true
}
}
fn event_loop(
terminal: &mut ratatui::DefaultTerminal,
hosts: &mut [HostTui],
screen: &mut Screen,
widgets: &[Widget],
) -> Result<()> {
let multi = hosts.len() > 1;
let mut dirty = true;
let mut status_visible = false;
let mut drawn_second = 0;
while !screen.quit {
for host in hosts.iter_mut() {
while let Some(event) = host.worker.try_recv() {
host.app.apply(event);
dirty = true;
}
}
let second = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if second != drawn_second {
drawn_second = second;
dirty = true;
}
let active_status = match screen.view {
View::Host(i) => hosts[i].app.status().is_some(),
View::Overview => false,
};
if status_visible != active_status {
status_visible = active_status;
dirty = true;
}
if dirty {
terminal.draw(|frame| {
let area = widget_column(frame, frame.area(), screen, widgets);
let body = if multi {
let [bar, body] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
screen.tabs = overview::draw_tabs(frame, bar, hosts, screen.view);
body
} else {
area
};
match screen.view {
View::Overview => overview::draw_overview(
frame,
body,
hosts,
screen.include_local,
screen.processes,
),
View::Host(i) => ui::draw_in(frame, &mut hosts[i].app, body),
}
})?;
dirty = false;
}
if event::poll(INPUT_POLL)? {
match event::read()? {
TermEvent::Key(key) if key.kind != KeyEventKind::Release => {
handle_key(key, hosts, screen, multi, !widgets.is_empty());
dirty = true;
}
TermEvent::Mouse(mouse) => {
let clicked_tab = (mouse.kind == MouseEventKind::Down(MouseButton::Left))
.then(|| {
let position = Position::new(mouse.column, mouse.row);
screen.tabs.iter().find(|(rect, _)| rect.contains(position))
})
.flatten();
if let Some((_, view)) = clicked_tab {
screen.view = *view;
} else if let View::Host(i) = screen.view {
hosts[i].app.handle_mouse(mouse);
}
dirty = true;
}
TermEvent::Resize(..) => dirty = true,
_ => {}
}
}
if let View::Host(i) = screen.view
&& hosts[i].app.should_quit
{
screen.quit = true;
}
for (index, host) in hosts.iter_mut().enumerate() {
let limit = if screen.view == View::Host(index) {
None
} else {
Some(screen.processes)
};
if host.sent_limit != Some(limit) {
host.worker.send(Command::SetProcessLimit(limit));
host.sent_limit = Some(limit);
}
}
}
Ok(())
}
fn widget_column(
frame: &mut ratatui::Frame,
area: Rect,
screen: &Screen,
widgets: &[Widget],
) -> Rect {
if widgets.is_empty() || !screen.show_widgets || area.width < widget_panel::MIN_TERMINAL_WIDTH {
return area;
}
let [main, side] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(widget_panel::WIDTH)])
.areas(area);
widget_panel::draw(frame, side, widgets);
main
}
fn handle_key(
key: KeyEvent,
hosts: &mut [HostTui],
screen: &mut Screen,
multi: bool,
has_widgets: bool,
) {
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
screen.quit = true;
return;
}
let busy = match screen.view {
View::Host(i) => hosts[i].app.editing_filter || hosts[i].app.confirm.is_some(),
View::Overview => false,
};
if has_widgets && !busy && key.code == KeyCode::Char('w') {
screen.show_widgets = !screen.show_widgets;
return;
}
if multi && !busy && screen.handle_key(key, hosts.len()) {
return;
}
if let View::Host(i) = screen.view {
if let Some(command) = hosts[i].app.handle_key(key) {
hosts[i].worker.send(command);
}
let span = hosts[i].app.graph_span;
for host in hosts.iter_mut() {
host.app.graph_span = span;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn screen() -> Screen {
Screen {
view: View::Overview,
include_local: true,
processes: 10,
tabs: Vec::new(),
quit: false,
show_widgets: true,
}
}
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn tab_and_digits_switch_views() {
let mut screen = screen();
assert!(screen.handle_key(key(KeyCode::Tab), 2));
assert_eq!(screen.view, View::Host(0));
screen.handle_key(key(KeyCode::Tab), 2);
screen.handle_key(key(KeyCode::Tab), 2);
assert_eq!(screen.view, View::Overview, "wraps around");
screen.handle_key(key(KeyCode::BackTab), 2);
assert_eq!(screen.view, View::Host(1));
screen.handle_key(key(KeyCode::Char('1')), 2);
assert_eq!(screen.view, View::Host(0));
screen.handle_key(key(KeyCode::Char('5')), 2);
assert_eq!(screen.view, View::Host(0));
screen.handle_key(key(KeyCode::Char('0')), 2);
assert_eq!(screen.view, View::Overview);
}
#[test]
fn overview_keys_toggle_local_and_quit() {
let mut screen = screen();
screen.handle_key(key(KeyCode::Char('l')), 2);
assert!(!screen.include_local);
screen.view = View::Host(0);
assert!(!screen.handle_key(key(KeyCode::Char('q')), 2));
screen.view = View::Overview;
screen.handle_key(key(KeyCode::Char('q')), 2);
assert!(screen.quit);
}
}