use std::{fs::File, io::Write, sync::MutexGuard};
use chrono::Local;
use ratatui::{
Frame,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph},
};
use strum::IntoEnumIterator;
use super::error::{Result, TuiError};
pub(super) trait TuiLogManager: Sync + Send + 'static {
type State;
fn get_max_tui_log_len(&self) -> usize;
fn get_log_components_mut(
state: &mut Self::State,
) -> (
Option<&mut File>,
&mut Vec<String>,
&mut usize,
Rect,
&mut usize,
);
fn get_state(&self) -> MutexGuard<'_, Self::State>;
fn max_scroll_down(rect: &Rect, entries_len: usize) -> usize {
let visible_height = rect.height.saturating_sub(2) as usize; entries_len.saturating_sub(visible_height)
}
fn add_log_entry(&self, entry: String) -> Result<()> {
let mut state_guard = self.get_state();
let max_tui_log_len = self.get_max_tui_log_len();
let (mut log_file, log_entries, log_max_line_width, log_rect, log_v_scroll) =
Self::get_log_components_mut(&mut state_guard);
let timestamp = Local::now().format("%Y-%m-%dT%H:%M:%S%.3f%:z").to_string();
let lines: Vec<&str> = entry.lines().collect();
if lines.is_empty() {
return Ok(());
}
let mut log_entry = Vec::new();
for (i, line) in lines.iter().enumerate() {
let log_entry_line = if i == 0 {
format!("[{:<29}] {}", timestamp, line)
} else {
format!("{}{}", " ".repeat(32), line)
};
if let Some(log_file) = log_file.as_mut() {
writeln!(log_file, "{}", log_entry_line).map_err(TuiError::LogFileWrite)?;
log_file.flush().map_err(TuiError::LogFileWrite)?;
}
log_entry.push(log_entry_line)
}
for entry_line in log_entry.into_iter().rev() {
*log_max_line_width = (*log_max_line_width).max(entry_line.len());
log_entries.insert(0, entry_line);
}
if *log_v_scroll != 0 {
*log_v_scroll = log_v_scroll.saturating_add(lines.len());
}
if log_entries.len() > max_tui_log_len {
log_entries.truncate(max_tui_log_len);
let max_scroll = Self::max_scroll_down(&log_rect, log_entries.len());
*log_v_scroll = (*log_v_scroll).min(max_scroll);
}
Ok(())
}
}
pub(super) trait TuiView: TuiLogManager {
type UiMessage;
type TuiPane: IntoEnumIterator;
fn render(&self, f: &mut Frame);
fn handle_ui_message(&self, message: Self::UiMessage) -> Result<bool>;
fn max_scroll_right(rect: &Rect, max_line_width: usize) -> usize {
let visible_width = rect.width.saturating_sub(4) as usize; max_line_width.saturating_sub(visible_width)
}
fn get_main_area(f: &mut Frame) -> Rect {
let frame_rect = f.area();
Rect {
x: frame_rect.x,
y: frame_rect.y,
width: frame_rect.width,
height: frame_rect.height.saturating_sub(1), }
}
fn get_help_area(f: &mut Frame) -> Rect {
let frame_rect = f.area();
Rect {
x: frame_rect.x,
y: frame_rect.y + frame_rect.height.saturating_sub(1), width: frame_rect.width,
height: 1,
}
}
fn get_pane_render_info(
state: &Self::State,
pane: Self::TuiPane,
) -> (&'static str, &Vec<String>, usize, usize, Rect, bool);
fn render_pane(f: &mut Frame, state: &Self::State, pane: Self::TuiPane) {
let (title, lines, v_scroll, h_scroll, rect, is_active) =
Self::get_pane_render_info(state, pane);
let list_items: Vec<ListItem> = lines
.iter()
.skip(v_scroll)
.map(|item| {
let content = if h_scroll >= item.len() {
String::new()
} else {
item.chars().skip(h_scroll).collect()
};
ListItem::new(Line::from(vec![Span::raw(content)]))
})
.collect();
let border_style = if is_active {
Style::default().fg(Color::Cyan)
} else {
Style::default()
};
let list = List::new(list_items).block(
Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(border_style),
);
f.render_widget(list, rect);
}
fn help_text() -> &'static str {
" Ctrl+C shutdown | Tab switch panes | Up/Down/Left/Right scroll | 'b' bottom | 't' top"
}
fn render_panes(f: &mut Frame, state: &Self::State) {
for pane in Self::TuiPane::iter() {
Self::render_pane(f, state, pane);
}
let help_area = Self::get_help_area(f);
let help_paragraph =
Paragraph::new(Self::help_text()).style(Style::default().fg(Color::Gray));
f.render_widget(help_paragraph, help_area);
}
fn get_pane_data_mut(
state: &mut Self::State,
pane: Self::TuiPane,
) -> (&mut Vec<String>, &mut usize, &mut usize);
fn update_pane_content(&self, pane: Self::TuiPane, content: String) {
let mut state_guard = self.get_state();
let mut new_lines: Vec<String> = content.lines().map(|line| line.to_string()).collect();
new_lines.push("".to_string());
let max_line_width = new_lines.iter().map(|line| line.len()).max().unwrap_or(0);
let (lines, max_width_ref, v_scroll) = Self::get_pane_data_mut(&mut state_guard, pane);
*max_width_ref = max_line_width;
if new_lines.len() != lines.len() {
if *v_scroll >= new_lines.len() && !new_lines.is_empty() {
*v_scroll = new_lines.len().saturating_sub(1);
}
}
*lines = new_lines;
}
fn get_active_scroll_data(state: &Self::State) -> (usize, usize, &Rect, usize, usize);
fn get_active_scroll_mut(state: &mut Self::State) -> (&mut usize, &mut usize);
fn scroll_up(&self) {
let mut state_guard = self.get_state();
let (v_scroll, _) = Self::get_active_scroll_mut(&mut state_guard);
*v_scroll = v_scroll.saturating_sub(1);
}
fn scroll_down(&self) {
let mut state_guard = self.get_state();
let (curr_v_scroll, _, rect, lines_len, _) = Self::get_active_scroll_data(&state_guard);
let max_v = Self::max_scroll_down(rect, lines_len);
if curr_v_scroll < max_v {
let (v_scroll, _) = Self::get_active_scroll_mut(&mut state_guard);
*v_scroll += 1;
}
}
fn scroll_left(&self) {
let mut state_guard = self.get_state();
let (_, h_scroll) = Self::get_active_scroll_mut(&mut state_guard);
*h_scroll = h_scroll.saturating_sub(1);
}
fn scroll_right(&self) {
let mut state_guard = self.get_state();
let (_, current_h_scroll, rect, _, max_line_width) =
Self::get_active_scroll_data(&state_guard);
let max_h = Self::max_scroll_right(rect, max_line_width);
if current_h_scroll < max_h {
let (_, h_scroll) = Self::get_active_scroll_mut(&mut state_guard);
*h_scroll += 1;
}
}
fn reset_scroll(&self) {
let mut state_guard = self.get_state();
let (v_scroll, h_scroll) = Self::get_active_scroll_mut(&mut state_guard);
*v_scroll = 0;
*h_scroll = 0;
}
fn scroll_to_bottom(&self) {
let mut state_guard = self.get_state();
let (_, _, rect, lines_len, _) = Self::get_active_scroll_data(&state_guard);
let max_v_scroll = Self::max_scroll_down(rect, lines_len);
let (v_scroll, h_scroll) = Self::get_active_scroll_mut(&mut state_guard);
*v_scroll = max_v_scroll;
*h_scroll = 0;
}
fn switch_pane(&self);
fn select_chart(&self, _index: u8) {}
}