use super::bounded_buffer::BoundedBuffer;
use super::tui_layout::{draw_dashboard_footer, SidebarTui};
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame,
};
pub type LogBuffer = BoundedBuffer<String>;
pub struct DevModeTui {
log_buffer: LogBuffer,
scroll_offset: usize,
current_mode: String,
running_nodes: Vec<String>,
key_pressed: Option<char>,
dashboard_url: String,
}
impl DevModeTui {
pub fn new(log_buffer: LogBuffer, current_mode: String, running_nodes: Vec<String>, dashboard_url: String) -> Self {
Self {
log_buffer,
scroll_offset: 0,
current_mode,
running_nodes,
key_pressed: None,
dashboard_url,
}
}
pub fn update_status(&mut self, mode: String, running_nodes: Vec<String>) {
self.current_mode = mode;
self.running_nodes = running_nodes;
}
fn draw_instructions(&self, f: &mut Frame, area: ratatui::layout::Rect) {
let mut text = vec![
Line::from(vec![Span::styled(
"đ MECHA10 DEVELOPMENT MODE",
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(vec![
Span::styled("Mode: ", Style::default().add_modifier(Modifier::BOLD)),
Span::styled(
&self.current_mode,
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
),
]),
Line::from(""),
];
if !self.running_nodes.is_empty() {
text.push(Line::from(vec![Span::styled(
"Running Nodes:",
Style::default().add_modifier(Modifier::BOLD),
)]));
for node in &self.running_nodes {
text.push(Line::from(vec![
Span::raw(" â
"),
Span::styled(node, Style::default().fg(Color::Green)),
]));
}
text.push(Line::from(""));
}
text.extend(vec![
Line::from("âââââââââââââââââââââââââââââ"),
Line::from(""),
Line::from(vec![Span::styled(
"Commands:",
Style::default().add_modifier(Modifier::BOLD),
)]),
]);
let current_mode_lower = self.current_mode.to_lowercase();
if current_mode_lower == "simulation" {
text.push(Line::from(vec![Span::styled(
" x - Stop simulation mode",
Style::default().fg(Color::Yellow),
)]));
} else if current_mode_lower == "teleop" {
text.push(Line::from(" s - Switch to simulation mode"));
} else {
text.push(Line::from(" s - Switch to simulation mode"));
text.push(Line::from(" t - Switch to teleop mode"));
}
text.extend(vec![
Line::from(""),
Line::from(" j/k - Scroll logs (or PgUp/PgDn)"),
Line::from(""),
Line::from(vec![Span::styled(
" q / Ctrl+C/Ctrl+X - Exit",
Style::default().fg(Color::Red),
)]),
]);
if let Some(key) = self.key_pressed {
text.push(Line::from(""));
text.push(Line::from(vec![
Span::raw("Last key: "),
Span::styled(key.to_string(), Style::default().fg(Color::Yellow)),
]));
}
let paragraph = Paragraph::new(text).block(
Block::default()
.borders(Borders::ALL)
.title(" Dev Mode ")
.border_style(Style::default().fg(Color::Cyan)),
);
f.render_widget(paragraph, area);
}
fn draw_logs_sidebar(&self, f: &mut Frame, area: ratatui::layout::Rect) {
let logs = self.log_buffer.get_all();
let items: Vec<ListItem> = logs
.iter()
.skip(self.scroll_offset)
.map(|log| {
let (style, icon) = if log.contains("ERROR") {
(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), "â")
} else if log.contains("WARN") {
(Style::default().fg(Color::Yellow), "â ī¸ ")
} else if log.contains("HEALTH:") && log.contains(" is Running") {
(Style::default().fg(Color::Green), "đŠē")
} else if log.contains("HEALTH:") && log.contains(" is Stopped") {
(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), "đŠē")
} else if log.contains("HEALTH:") {
(Style::default().fg(Color::Yellow), "đ")
} else if log.contains("MODE:") {
(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), "âĄ")
} else if log.contains("STARTED") || log.contains("â
") {
(Style::default().fg(Color::Green), "â
")
} else if log.contains("STOPPED") || log.contains("đ") {
(Style::default().fg(Color::Red), "đ")
} else if log.contains("INFO") {
(Style::default().fg(Color::Green), "âšī¸ ")
} else if log.contains("DEBUG") {
(Style::default().fg(Color::Blue), "đ")
} else {
(Style::default().fg(Color::White), " ")
};
let log_trimmed = log.trim_start();
let already_has_icon = log_trimmed.starts_with("â
")
|| log_trimmed.starts_with("â")
|| log_trimmed.starts_with("â ī¸")
|| log_trimmed.starts_with("đ")
|| log_trimmed.starts_with("âļī¸")
|| log_trimmed.starts_with("đ")
|| log_trimmed.starts_with("đŠē");
let formatted_log = if already_has_icon {
format!(" {}", log)
} else {
format!("{} {}", icon, log)
};
ListItem::new(formatted_log).style(style)
})
.collect();
let title = if logs.is_empty() {
" System Logs (waiting...) ".to_string()
} else {
format!(" System Logs ({}) ", logs.len())
};
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(Style::default().fg(Color::Yellow)),
);
f.render_widget(list, area);
}
pub fn handle_key(&mut self, key: KeyEvent) -> Option<char> {
match key.code {
KeyCode::PageUp | KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.scroll_offset = self.scroll_offset.saturating_sub(10);
None
}
KeyCode::PageDown | KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let logs = self.log_buffer.get_all();
self.scroll_offset = (self.scroll_offset + 10).min(logs.len().saturating_sub(1));
None
}
KeyCode::Char('k') if key.modifiers.is_empty() => {
self.scroll_offset = self.scroll_offset.saturating_sub(1);
None
}
KeyCode::Char('j') if key.modifiers.is_empty() => {
let logs = self.log_buffer.get_all();
self.scroll_offset = (self.scroll_offset + 1).min(logs.len().saturating_sub(1));
None
}
KeyCode::Char(c) => {
self.key_pressed = Some(c);
Some(c)
}
_ => None,
}
}
}
impl SidebarTui for DevModeTui {
fn draw_main(&mut self, f: &mut Frame, area: ratatui::layout::Rect) {
self.draw_instructions(f, area);
}
fn draw_sidebar(&self, f: &mut Frame, area: ratatui::layout::Rect) {
self.draw_logs_sidebar(f, area);
}
fn draw_footer(&self, f: &mut Frame, area: ratatui::layout::Rect) {
draw_dashboard_footer(f, area, &self.dashboard_url, "q/Ctrl+C/X");
}
}
#[allow(dead_code)]
pub async fn subscribe_to_logs(_log_buffer: LogBuffer, _redis_url: String) -> Result<()> {
Ok(())
}