use crate::app::App;
use crate::ui::theme::{CATPPUCCIN_MOCHA, mocha};
use crate::ui::tr;
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Table},
};
pub fn draw(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(75), Constraint::Percentage(25)])
.split(area);
draw_console_panel(f, app, chunks[0]);
draw_settings_panel(f, app, chunks[1]);
}
fn draw_console_panel(f: &mut Frame, app: &App, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(5), Constraint::Length(3)])
.split(area);
let active_port = app
.get_selected_port()
.unwrap_or_else(|| "NONE".to_string());
let lang = &app.tool_config.language;
let rx_title = tr("serial_rx_title", lang).replace("{}", &active_port);
let console_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(CATPPUCCIN_MOCHA.border_focus))
.title(Span::styled(
rx_title,
Style::default()
.fg(CATPPUCCIN_MOCHA.text)
.add_modifier(Modifier::BOLD),
));
let inner_area = console_block.inner(chunks[0]);
f.render_widget(console_block, chunks[0]);
let max_width = inner_area.width.saturating_sub(2) as usize;
let mut wrapped_lines: Vec<Line> = Vec::new();
for log in &app.logs {
let belongs_to_port = log.contains(&format!("[{}]", active_port));
let is_system =
log.contains("System") || log.contains("PlatformIO") || log.starts_with("---");
if belongs_to_port || is_system {
let fg_color = if log.contains("[TX]") {
mocha::SAPPHIRE
} else if log.contains("FAILED") || log.contains("failed") || log.contains("Error") {
CATPPUCCIN_MOCHA.danger
} else if log.contains("SUCCESS") || log.contains("PASSED") || log.contains("Success") {
CATPPUCCIN_MOCHA.success
} else {
CATPPUCCIN_MOCHA.text
};
let mut clean_log = log.clone();
if let Some(pos) = clean_log.find(']') {
if clean_log.starts_with('[') {
if let Some(pos2) = clean_log[pos + 1..].find(']') {
clean_log = clean_log[pos + 1 + pos2 + 1..].trim().to_string();
} else {
clean_log = clean_log[pos + 1..].trim().to_string();
}
}
}
let sub_lines = wrap_log_line(&clean_log, max_width);
for sub_line in sub_lines {
wrapped_lines.push(Line::from(Span::styled(
sub_line,
Style::default().fg(fg_color),
)));
}
}
}
let height = inner_area.height as usize;
let logs_to_draw = if wrapped_lines.len() > height {
let start = wrapped_lines.len() - height;
wrapped_lines[start..].to_vec()
} else {
wrapped_lines
};
let paragraph = Paragraph::new(logs_to_draw)
.block(Block::default())
.style(Style::default().bg(mocha::MANTLE));
f.render_widget(paragraph, inner_area);
let lang = &app.tool_config.language;
let (input_border, input_title, input_title_style) = if app.serial_is_typing {
(
CATPPUCCIN_MOCHA.success,
tr("serial_typing_mode", lang),
Style::default()
.fg(CATPPUCCIN_MOCHA.success)
.add_modifier(Modifier::BOLD),
)
} else {
(
CATPPUCCIN_MOCHA.border,
tr("serial_send_console", lang),
Style::default().fg(CATPPUCCIN_MOCHA.text_muted),
)
};
let send_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(input_border))
.title(Span::styled(input_title, input_title_style));
let display_text = if app.serial_is_typing {
format!("{}_", app.serial_send_buffer)
} else if app.serial_send_buffer.is_empty() {
tr("serial_placeholder", lang).to_string()
} else {
app.serial_send_buffer.clone()
};
let text_style = if app.serial_send_buffer.is_empty() && !app.serial_is_typing {
Style::default()
.fg(CATPPUCCIN_MOCHA.text_muted)
.add_modifier(Modifier::ITALIC)
} else {
Style::default().fg(CATPPUCCIN_MOCHA.text)
};
let input_p = Paragraph::new(Span::styled(display_text, text_style))
.block(send_block)
.style(Style::default().bg(mocha::MANTLE));
f.render_widget(input_p, chunks[1]);
}
fn draw_settings_panel(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(7), Constraint::Length(8), Constraint::Min(5), ])
.split(area);
app.layout_zones.serial_port_info = chunks[0];
app.layout_zones.serial_options = chunks[1];
let lang = &app.tool_config.language;
let active_port = app
.get_selected_port()
.unwrap_or_else(|| "NONE".to_string());
let info_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(CATPPUCCIN_MOCHA.border))
.title(Span::styled(
tr("serial_port_info", lang),
Style::default()
.fg(CATPPUCCIN_MOCHA.text)
.add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(CATPPUCCIN_MOCHA.panel));
let info_text = vec![
Line::from(vec![
Span::styled(tr("serial_port", lang), Style::default().fg(CATPPUCCIN_MOCHA.text_muted)),
Span::styled(
active_port,
Style::default()
.fg(CATPPUCCIN_MOCHA.text)
.add_modifier(Modifier::BOLD),
),
]),
Line::from(vec![
Span::styled(
tr("serial_baud", lang),
Style::default().fg(CATPPUCCIN_MOCHA.text_muted),
),
Span::styled(
format!("{} bps", app.serial_baud_rate),
Style::default().fg(CATPPUCCIN_MOCHA.success),
),
]),
Line::from(vec![
Span::styled(tr("serial_bits", lang), Style::default().fg(CATPPUCCIN_MOCHA.text_muted)),
Span::styled("8-N-1", Style::default().fg(CATPPUCCIN_MOCHA.text)),
]),
];
f.render_widget(Paragraph::new(info_text).block(info_block), chunks[0]);
let toggles_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(CATPPUCCIN_MOCHA.border))
.title(Span::styled(
tr("serial_options_title", lang),
Style::default()
.fg(CATPPUCCIN_MOCHA.text)
.add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(CATPPUCCIN_MOCHA.panel));
let scroll_check = if app.serial_auto_scroll { "[X]" } else { "[ ]" };
let crlf_check = if app.serial_add_newline { "[X]" } else { "[ ]" };
let rx_hex = if app.serial_hex_mode_rx { "[X]" } else { "[ ]" };
let tx_hex = if app.serial_hex_mode_tx { "[X]" } else { "[ ]" };
let toggles_text = vec![
Line::from(vec![
Span::styled(
format!("{} ", scroll_check),
Style::default().fg(CATPPUCCIN_MOCHA.accent),
),
Span::styled(tr("serial_auto_scroll", lang), Style::default().fg(CATPPUCCIN_MOCHA.text)),
Span::styled("[s]", Style::default().fg(CATPPUCCIN_MOCHA.text_muted)),
]),
Line::from(vec![
Span::styled(
format!("{} ", crlf_check),
Style::default().fg(CATPPUCCIN_MOCHA.accent),
),
Span::styled(tr("serial_send_newline", lang), Style::default().fg(CATPPUCCIN_MOCHA.text)),
Span::styled("[n]", Style::default().fg(CATPPUCCIN_MOCHA.text_muted)),
]),
Line::from(vec![
Span::styled(
format!("{} ", rx_hex),
Style::default().fg(CATPPUCCIN_MOCHA.accent),
),
Span::styled(tr("serial_hex_display", lang), Style::default().fg(CATPPUCCIN_MOCHA.text)),
Span::styled("[h]", Style::default().fg(CATPPUCCIN_MOCHA.text_muted)),
]),
Line::from(vec![
Span::styled(
format!("{} ", tx_hex),
Style::default().fg(CATPPUCCIN_MOCHA.accent),
),
Span::styled(tr("serial_hex_sending", lang), Style::default().fg(CATPPUCCIN_MOCHA.text)),
Span::styled("[t]", Style::default().fg(CATPPUCCIN_MOCHA.text_muted)),
]),
];
f.render_widget(Paragraph::new(toggles_text).block(toggles_block), chunks[1]);
let quick_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(CATPPUCCIN_MOCHA.border))
.title(Span::styled(
tr("serial_quick_commands", lang),
Style::default()
.fg(CATPPUCCIN_MOCHA.text)
.add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(CATPPUCCIN_MOCHA.panel));
let rows = vec![
Row::new(vec![
Cell::from(Span::styled(
"AT",
Style::default()
.fg(mocha::BLUE)
.add_modifier(Modifier::BOLD),
)),
Cell::from(Span::styled(
tr("serial_ping_desc", lang),
Style::default().fg(CATPPUCCIN_MOCHA.text_muted),
)),
]),
Row::new(vec![
Cell::from(Span::styled(
"AT+GMR",
Style::default()
.fg(mocha::BLUE)
.add_modifier(Modifier::BOLD),
)),
Cell::from(Span::styled(
tr("serial_version_desc", lang),
Style::default().fg(CATPPUCCIN_MOCHA.text_muted),
)),
]),
Row::new(vec![
Cell::from(Span::styled(
"RESET",
Style::default().fg(mocha::RED).add_modifier(Modifier::BOLD),
)),
Cell::from(Span::styled(
tr("serial_reboot_desc", lang),
Style::default().fg(CATPPUCCIN_MOCHA.text_muted),
)),
]),
Row::new(vec![
Cell::from(Span::styled(
"help",
Style::default()
.fg(mocha::GREEN)
.add_modifier(Modifier::BOLD),
)),
Cell::from(Span::styled(
tr("serial_list_desc", lang),
Style::default().fg(CATPPUCCIN_MOCHA.text_muted),
)),
]),
];
let table = Table::new(
rows,
[Constraint::Percentage(40), Constraint::Percentage(60)],
)
.block(quick_block)
.header(
Row::new(vec![tr("serial_cmd", lang), tr("serial_desc", lang)]).style(
Style::default()
.bg(mocha::SURFACE0)
.add_modifier(Modifier::BOLD),
),
);
f.render_widget(table, chunks[2]);
}
fn wrap_log_line(line: &str, max_width: usize) -> Vec<String> {
if line.len() <= max_width {
return vec![line.to_string()];
}
let mut result = Vec::new();
let mut current = String::new();
for word in line.split_whitespace() {
if current.is_empty() {
current.push_str(word);
} else if current.len() + 1 + word.len() <= max_width {
current.push(' ');
current.push_str(word);
} else {
result.push(current);
current = word.to_string();
}
}
if !current.is_empty() {
result.push(current);
}
if result.is_empty() {
result.push(line.to_string());
}
result
}