use super::log_buffer::LogBuffer;
use super::views::log_panel::{
DEFAULT_LOG_PANEL_HEIGHT, MAX_LOG_PANEL_HEIGHT, MIN_LOG_PANEL_HEIGHT,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ViewMode {
Summary,
Detail(usize),
Split(Vec<usize>),
Diff(usize, usize),
}
pub struct TuiApp {
pub view_mode: ViewMode,
pub scroll_positions: HashMap<usize, usize>,
pub follow_mode: bool,
pub should_quit: bool,
pub show_help: bool,
pub needs_redraw: bool,
pub last_data_sizes: HashMap<usize, (usize, usize)>, pub all_tasks_completed: bool,
pub log_buffer: Arc<Mutex<LogBuffer>>,
pub log_panel_visible: bool,
pub log_panel_height: u16,
pub log_scroll_offset: usize,
pub log_show_timestamps: bool,
}
impl TuiApp {
pub fn new() -> Self {
Self::with_log_buffer(Arc::new(Mutex::new(LogBuffer::default())))
}
pub fn with_log_buffer(log_buffer: Arc<Mutex<LogBuffer>>) -> Self {
Self {
view_mode: ViewMode::Summary,
scroll_positions: HashMap::new(),
follow_mode: true, should_quit: false,
show_help: false,
needs_redraw: true, last_data_sizes: HashMap::new(),
all_tasks_completed: false,
log_buffer,
log_panel_visible: false, log_panel_height: DEFAULT_LOG_PANEL_HEIGHT,
log_scroll_offset: 0,
log_show_timestamps: false, }
}
pub fn check_data_changes(&mut self, streams: &[crate::executor::NodeStream]) -> bool {
let mut has_changes = false;
for (idx, stream) in streams.iter().enumerate() {
let new_sizes = (stream.stdout().len(), stream.stderr().len());
if let Some(&old_sizes) = self.last_data_sizes.get(&idx) {
if old_sizes != new_sizes {
has_changes = true;
self.last_data_sizes.insert(idx, new_sizes);
self.needs_redraw = true;
}
} else {
self.last_data_sizes.insert(idx, new_sizes);
has_changes = true;
self.needs_redraw = true;
}
}
has_changes
}
pub fn mark_needs_redraw(&mut self) {
self.needs_redraw = true;
}
pub fn should_redraw(&mut self) -> bool {
if self.needs_redraw {
self.needs_redraw = false;
true
} else {
false
}
}
pub fn show_summary(&mut self) {
self.view_mode = ViewMode::Summary;
self.needs_redraw = true;
}
pub fn show_detail(&mut self, node_index: usize, num_nodes: usize) {
if node_index < num_nodes {
self.view_mode = ViewMode::Detail(node_index);
self.needs_redraw = true;
}
}
pub fn show_split(&mut self, indices: Vec<usize>, num_nodes: usize) {
let valid_indices: Vec<_> = indices
.into_iter()
.filter(|&i| i < num_nodes)
.take(4)
.collect();
if valid_indices.len() >= 2 {
self.view_mode = ViewMode::Split(valid_indices);
self.needs_redraw = true;
}
}
pub fn show_diff(&mut self, node_a: usize, node_b: usize, num_nodes: usize) {
if node_a < num_nodes && node_b < num_nodes && node_a != node_b {
self.view_mode = ViewMode::Diff(node_a, node_b);
self.needs_redraw = true;
}
}
pub fn toggle_follow(&mut self) {
self.follow_mode = !self.follow_mode;
self.needs_redraw = true;
}
pub fn toggle_help(&mut self) {
self.show_help = !self.show_help;
self.needs_redraw = true;
}
pub fn get_scroll(&self, node_index: usize) -> usize {
self.scroll_positions.get(&node_index).copied().unwrap_or(0)
}
pub fn set_scroll(&mut self, node_index: usize, position: usize) {
const MAX_SCROLL_ENTRIES: usize = 100;
if self.scroll_positions.len() >= MAX_SCROLL_ENTRIES
&& !self.scroll_positions.contains_key(&node_index)
{
if let Some(first_key) = self.scroll_positions.keys().next().copied() {
self.scroll_positions.remove(&first_key);
}
}
self.scroll_positions.insert(node_index, position);
}
pub fn scroll_up(&mut self, lines: usize) {
if let ViewMode::Detail(idx) = self.view_mode {
let pos = self.get_scroll(idx);
self.set_scroll(idx, pos.saturating_sub(lines));
self.follow_mode = false;
self.needs_redraw = true;
}
}
pub fn scroll_down(&mut self, lines: usize, max_lines: usize) {
if let ViewMode::Detail(idx) = self.view_mode {
let pos = self.get_scroll(idx);
let new_pos = (pos + lines).min(max_lines);
self.set_scroll(idx, new_pos);
self.follow_mode = false;
self.needs_redraw = true;
}
}
pub fn next_node(&mut self, num_nodes: usize) {
if let ViewMode::Detail(idx) = self.view_mode {
let next = (idx + 1) % num_nodes;
self.view_mode = ViewMode::Detail(next);
self.needs_redraw = true;
}
}
pub fn prev_node(&mut self, num_nodes: usize) {
if let ViewMode::Detail(idx) = self.view_mode {
let prev = if idx == 0 { num_nodes - 1 } else { idx - 1 };
self.view_mode = ViewMode::Detail(prev);
self.needs_redraw = true;
}
}
pub fn quit(&mut self) {
self.should_quit = true;
}
pub fn mark_all_tasks_completed(&mut self) {
if !self.all_tasks_completed {
self.all_tasks_completed = true;
self.needs_redraw = true;
}
}
pub fn toggle_log_panel(&mut self) {
self.log_panel_visible = !self.log_panel_visible;
self.log_scroll_offset = 0; self.needs_redraw = true;
}
pub fn increase_log_panel_height(&mut self) {
if self.log_panel_height < MAX_LOG_PANEL_HEIGHT {
self.log_panel_height += 1;
self.needs_redraw = true;
}
}
pub fn decrease_log_panel_height(&mut self) {
if self.log_panel_height > MIN_LOG_PANEL_HEIGHT {
self.log_panel_height -= 1;
self.needs_redraw = true;
}
}
pub fn scroll_log_up(&mut self, lines: usize) {
if let Ok(buffer) = self.log_buffer.lock() {
let max_offset = buffer.len().saturating_sub(1);
self.log_scroll_offset = (self.log_scroll_offset + lines).min(max_offset);
}
self.needs_redraw = true;
}
pub fn scroll_log_down(&mut self, lines: usize) {
self.log_scroll_offset = self.log_scroll_offset.saturating_sub(lines);
self.needs_redraw = true;
}
pub fn toggle_log_timestamps(&mut self) {
self.log_show_timestamps = !self.log_show_timestamps;
self.needs_redraw = true;
}
pub fn check_log_updates(&mut self) -> bool {
if let Ok(mut buffer) = self.log_buffer.lock()
&& buffer.take_has_new_entries()
{
self.needs_redraw = true;
return true;
}
false
}
pub fn get_help_text(&self) -> Vec<(&'static str, &'static str)> {
let mut help = vec![
("q", "Quit"),
("Esc", "Back to summary"),
("?", "Toggle help"),
("l", "Toggle log panel"),
];
match &self.view_mode {
ViewMode::Summary => {
help.extend_from_slice(&[
("1-9", "View node detail"),
("s", "Split view (2-4 nodes)"),
("d", "Diff view (compare 2 nodes)"),
]);
}
ViewMode::Detail(_) => {
help.extend_from_slice(&[
("↑/↓", "Scroll up/down"),
("←/→", "Previous/next node"),
("f", "Toggle auto-scroll"),
("PgUp/PgDn", "Scroll page"),
("Home/End", "Scroll to top/bottom"),
("1-9", "Jump to node N"),
]);
}
ViewMode::Split(_) => {
help.extend_from_slice(&[("1-4", "Focus on node")]);
}
ViewMode::Diff(_, _) => {
help.extend_from_slice(&[("↑/↓", "Sync scroll")]);
}
}
help.push(("", "")); help.push(("── Log Panel ──", ""));
if self.log_panel_visible {
help.extend_from_slice(&[
("j/k", "Scroll log up/down"),
("+/-", "Resize panel (3-10 lines)"),
("t", "Toggle timestamps"),
]);
} else {
help.push(("l", "Press to show log panel"));
}
help
}
}
impl Default for TuiApp {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_creation() {
let app = TuiApp::new();
assert_eq!(app.view_mode, ViewMode::Summary);
assert!(app.follow_mode);
assert!(!app.should_quit);
}
#[test]
fn test_switch_to_detail() {
let mut app = TuiApp::new();
app.show_detail(2, 5);
assert_eq!(app.view_mode, ViewMode::Detail(2));
let prev_mode = app.view_mode.clone();
app.show_detail(10, 5);
assert_eq!(app.view_mode, prev_mode);
}
#[test]
fn test_scroll_positions() {
let mut app = TuiApp::new();
app.set_scroll(0, 10);
assert_eq!(app.get_scroll(0), 10);
assert_eq!(app.get_scroll(1), 0); }
#[test]
fn test_scroll_up_down() {
let mut app = TuiApp::new();
app.show_detail(0, 5);
app.set_scroll(0, 10);
app.scroll_up(3);
assert_eq!(app.get_scroll(0), 7);
assert!(!app.follow_mode);
app.scroll_down(5, 20);
assert_eq!(app.get_scroll(0), 12);
}
#[test]
fn test_node_navigation() {
let mut app = TuiApp::new();
app.show_detail(1, 5);
app.next_node(5);
assert_eq!(app.view_mode, ViewMode::Detail(2));
app.prev_node(5);
assert_eq!(app.view_mode, ViewMode::Detail(1));
app.show_detail(4, 5);
app.next_node(5);
assert_eq!(app.view_mode, ViewMode::Detail(0));
app.show_detail(0, 5);
app.prev_node(5);
assert_eq!(app.view_mode, ViewMode::Detail(4));
}
#[test]
fn test_split_view() {
let mut app = TuiApp::new();
app.show_split(vec![0, 1, 2], 5);
assert_eq!(app.view_mode, ViewMode::Split(vec![0, 1, 2]));
app.show_split(vec![0], 5);
assert_eq!(app.view_mode, ViewMode::Split(vec![0, 1, 2]));
app.show_split(vec![0, 1, 10, 11], 5);
assert_eq!(app.view_mode, ViewMode::Split(vec![0, 1]));
}
#[test]
fn test_diff_view() {
let mut app = TuiApp::new();
app.show_diff(0, 1, 5);
assert_eq!(app.view_mode, ViewMode::Diff(0, 1));
app.show_diff(2, 2, 5);
assert_eq!(app.view_mode, ViewMode::Diff(0, 1)); }
#[test]
fn test_toggle_follow() {
let mut app = TuiApp::new();
assert!(app.follow_mode);
app.toggle_follow();
assert!(!app.follow_mode);
app.toggle_follow();
assert!(app.follow_mode);
}
#[test]
fn test_log_panel_toggle() {
let mut app = TuiApp::new();
assert!(!app.log_panel_visible);
app.toggle_log_panel();
assert!(app.log_panel_visible);
app.toggle_log_panel();
assert!(!app.log_panel_visible);
}
#[test]
fn test_log_panel_height() {
let mut app = TuiApp::new();
let initial_height = app.log_panel_height;
app.increase_log_panel_height();
assert_eq!(app.log_panel_height, initial_height + 1);
app.decrease_log_panel_height();
assert_eq!(app.log_panel_height, initial_height);
for _ in 0..20 {
app.decrease_log_panel_height();
}
assert_eq!(app.log_panel_height, MIN_LOG_PANEL_HEIGHT);
for _ in 0..20 {
app.increase_log_panel_height();
}
assert_eq!(app.log_panel_height, MAX_LOG_PANEL_HEIGHT);
}
#[test]
fn test_log_scroll() {
use super::super::log_buffer::LogEntry;
use tracing::Level;
let buffer = Arc::new(Mutex::new(LogBuffer::new(100)));
{
let mut b = buffer.lock().unwrap();
for i in 0..10 {
b.push(LogEntry::new(
Level::INFO,
"test".to_string(),
format!("msg {i}"),
));
}
}
let mut app = TuiApp::with_log_buffer(buffer);
assert_eq!(app.log_scroll_offset, 0);
app.scroll_log_up(3);
assert_eq!(app.log_scroll_offset, 3);
app.scroll_log_down(1);
assert_eq!(app.log_scroll_offset, 2);
app.scroll_log_down(10);
assert_eq!(app.log_scroll_offset, 0);
}
#[test]
fn test_log_timestamps_toggle() {
let mut app = TuiApp::new();
assert!(!app.log_show_timestamps);
app.toggle_log_timestamps();
assert!(app.log_show_timestamps);
app.toggle_log_timestamps();
assert!(!app.log_show_timestamps);
}
}