pub mod app;
pub mod event;
pub mod log_buffer;
pub mod log_layer;
pub mod progress;
pub mod terminal_guard;
pub mod views;
use crate::executor::MultiNodeStreamManager;
use crate::utils::get_log_buffer;
use anyhow::Result;
use app::{TuiApp, ViewMode};
use log_buffer::LogBuffer;
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use terminal_guard::TerminalGuard;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TuiExitReason {
UserQuit,
AllTasksCompleted,
}
pub async fn run_tui(
manager: &mut MultiNodeStreamManager,
cluster_name: &str,
command: &str,
_batch_mode: bool, ) -> Result<TuiExitReason> {
let log_buffer = get_log_buffer().unwrap_or_else(|| Arc::new(Mutex::new(LogBuffer::default())));
run_tui_with_log_buffer(manager, cluster_name, command, _batch_mode, log_buffer).await
}
pub async fn run_tui_with_log_buffer(
manager: &mut MultiNodeStreamManager,
cluster_name: &str,
command: &str,
_batch_mode: bool,
log_buffer: Arc<Mutex<LogBuffer>>,
) -> Result<TuiExitReason> {
let _terminal_guard = TerminalGuard::new()?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = TuiApp::with_log_buffer(log_buffer);
let exit_reason =
run_event_loop(&mut terminal, &mut app, manager, cluster_name, command).await?;
terminal.show_cursor()?;
Ok(exit_reason)
}
const MIN_TERMINAL_WIDTH: u16 = 40;
const MIN_TERMINAL_HEIGHT: u16 = 10;
async fn run_event_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut TuiApp,
manager: &mut MultiNodeStreamManager,
cluster_name: &str,
command: &str,
) -> Result<TuiExitReason> {
app.mark_needs_redraw();
loop {
manager.poll_all();
let streams = manager.streams();
let data_changed = app.check_data_changes(streams);
let log_changed = app.check_log_updates();
let size = terminal.size()?;
let size_ok = size.width >= MIN_TERMINAL_WIDTH && size.height >= MIN_TERMINAL_HEIGHT;
if app.should_redraw() || data_changed || log_changed {
if !size_ok {
terminal.draw(render_size_error)?;
} else {
terminal.draw(|f| render_ui(f, app, manager, cluster_name, command))?;
}
}
if let Some(key) = event::poll_event(Duration::from_millis(100))? {
event::handle_key_event(app, key, manager.total_count());
app.mark_needs_redraw();
}
if manager.all_complete() {
app.mark_all_tasks_completed();
}
if app.should_quit {
let exit_reason = if app.all_tasks_completed {
TuiExitReason::AllTasksCompleted
} else {
TuiExitReason::UserQuit
};
return Ok(exit_reason);
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
fn render_ui(
f: &mut ratatui::Frame,
app: &TuiApp,
manager: &MultiNodeStreamManager,
cluster_name: &str,
command: &str,
) {
let (main_area, log_area) =
views::log_panel::calculate_layout(f.area(), app.log_panel_height, app.log_panel_visible);
match &app.view_mode {
ViewMode::Summary => {
views::summary::render_in_area(
f,
main_area,
manager,
cluster_name,
command,
app.all_tasks_completed,
);
}
ViewMode::Detail(idx) => {
if let Some(stream) = manager.streams().get(*idx) {
let scroll = app.get_scroll(*idx);
views::detail::render_in_area(
f,
main_area,
stream,
*idx,
scroll,
app.follow_mode,
app.all_tasks_completed,
);
}
}
ViewMode::Split(indices) => {
views::split::render_in_area(f, main_area, manager, indices);
}
ViewMode::Diff(a, b) => {
let streams = manager.streams();
if let (Some(stream_a), Some(stream_b)) = (streams.get(*a), streams.get(*b)) {
views::diff::render_in_area(f, main_area, stream_a, stream_b, *a, *b, 0);
}
}
}
if let Some(log_area) = log_area {
views::log_panel::render(
f,
log_area,
&app.log_buffer,
app.log_scroll_offset,
app.log_show_timestamps,
);
}
if app.show_help {
render_help_overlay(f, app);
}
}
fn render_help_overlay(f: &mut ratatui::Frame, app: &TuiApp) {
use ratatui::{
layout::Alignment,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
};
let area = centered_rect(60, 60, f.area());
f.render_widget(Clear, area);
let help_items = app.get_help_text();
let mut lines = vec![
Line::from(Span::styled(
"Keyboard Shortcuts",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
];
for (key, description) in help_items {
lines.push(Line::from(vec![
Span::styled(format!(" {key:<12} "), Style::default().fg(Color::Yellow)),
Span::raw(description),
]));
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Press ? or Esc to close",
Style::default().fg(Color::Gray),
)));
let help = Paragraph::new(lines)
.block(
Block::default()
.title(" Help ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
)
.alignment(Alignment::Left);
f.render_widget(help, area);
}
fn render_size_error(f: &mut ratatui::Frame) {
use ratatui::{
layout::Alignment,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
let message = vec![
Line::from(""),
Line::from(Span::styled(
"Terminal too small!",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(format!(
"Minimum size: {MIN_TERMINAL_WIDTH}x{MIN_TERMINAL_HEIGHT}"
)),
Line::from(format!(
"Current size: {}x{}",
f.area().width,
f.area().height
)),
Line::from(""),
Line::from("Please resize your terminal"),
Line::from("or press 'q' to quit"),
];
let paragraph = Paragraph::new(message)
.block(
Block::default()
.title(" Error ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Red)),
)
.alignment(Alignment::Center);
let area = if f.area().width >= 30 && f.area().height >= 8 {
centered_rect(80, 60, f.area())
} else {
f.area()
};
f.render_widget(paragraph, area);
}
fn centered_rect(
percent_x: u16,
percent_y: u16,
r: ratatui::layout::Rect,
) -> ratatui::layout::Rect {
use ratatui::layout::{Constraint, Direction, Layout};
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}