use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::{
Frame,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
};
use serialport::{DataBits, FlowControl, Parity, StopBits};
use crate::serial::BAUD_RATES;
use crate::state::App;
use super::common::{help_spans, sep_span, wrap_spans_to_lines};
const SETTINGS: &[&str] = &[
"Baud Rate",
"Data Bits",
"Stop Bits",
"Parity",
"Flow Control",
];
pub fn draw(app: &App, frame: &mut Frame) {
let area = frame.area();
let width = 52u16.min(area.width);
let list_height = SETTINGS.len() as u16;
let help_height = 1u16;
let height = (list_height + help_height + 3).min(area.height);
let x = (area.width.saturating_sub(width)) / 2;
let y = (area.height.saturating_sub(height)) / 2;
let popup_area = Rect {
x,
y,
width,
height,
};
frame.render_widget(Clear, popup_area);
let block = Block::new()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(211, 69, 21)))
.title(Span::styled(
" settings ",
Style::default().fg(Color::Rgb(211, 69, 21)).bold(),
));
let inner = block.inner(popup_area);
frame.render_widget(block, popup_area);
let list_area = Rect {
x: inner.x,
y: inner.y,
width: inner.width,
height: list_height.min(inner.height),
};
let help_area = Rect {
x: inner.x,
y: inner.y + list_area.height,
width: inner.width,
height: inner.height.saturating_sub(list_area.height),
};
let items: Vec<ListItem> = SETTINGS
.iter()
.enumerate()
.map(|(i, &name)| {
let is_baud_editing = i == 0 && app.settings_baud_input.is_some();
let value_span = if is_baud_editing {
let input = app.settings_baud_input.as_deref().unwrap_or("");
Span::styled(
format!("{}_", input),
Style::default().fg(Color::Yellow).bold(),
)
} else {
Span::styled(setting_value(app, i), Style::default().bold())
};
ListItem::new(Line::from(vec![
Span::styled(
format!(" {:<13}", name),
Style::default().fg(Color::DarkGray),
),
sep_span(),
Span::raw(" "),
value_span,
]))
})
.collect();
let list = List::new(items)
.highlight_style(Style::default().reversed())
.highlight_symbol("");
let mut state = ListState::default().with_selected(Some(app.settings_cursor));
frame.render_stateful_widget(list, list_area, &mut state);
if inner.height > list_area.height {
let help_spans = if app.settings_baud_input.is_some() {
help_spans(&[("0-9", "type"), ("Enter", "apply"), ("Esc", "cancel")])
} else {
help_spans(&[("↑↓", "navigate"), ("←→", "cycle"), ("Esc", "close")])
};
let lines = wrap_spans_to_lines(help_spans, help_area.width + 2);
frame.render_widget(Paragraph::new(ratatui::text::Text::from(lines)), help_area);
}
}
pub fn handle_key(app: &mut App, code: KeyCode, _modifiers: KeyModifiers) {
if app.settings_baud_input.is_some() {
match code {
KeyCode::Esc => {
app.settings_baud_input = None;
}
KeyCode::Enter => {
let baud = app
.settings_baud_input
.as_deref()
.unwrap_or("")
.parse::<u32>()
.unwrap_or(0);
if baud > 0 {
app.port_config.baud = baud;
app.apply_port_config();
}
app.settings_baud_input = None;
}
KeyCode::Backspace => {
if let Some(ref mut s) = app.settings_baud_input {
s.pop();
}
}
KeyCode::Char(c) if c.is_ascii_digit() => {
if let Some(ref mut s) = app.settings_baud_input {
s.push(c);
}
}
_ => {}
}
return;
}
match code {
KeyCode::Esc => {
app.show_settings = false;
}
KeyCode::Up | KeyCode::Char('k') => {
if app.settings_cursor > 0 {
app.settings_cursor -= 1;
} else {
app.settings_cursor = SETTINGS.len() - 1;
}
}
KeyCode::Down | KeyCode::Char('j') => {
app.settings_cursor = (app.settings_cursor + 1) % SETTINGS.len();
}
KeyCode::Left | KeyCode::Char('h') => cycle_setting(app, -1),
KeyCode::Right | KeyCode::Char('l') => cycle_setting(app, 1),
KeyCode::Enter => {
if app.settings_cursor == 0 {
app.settings_baud_input = Some(app.port_config.baud.to_string());
} else {
cycle_setting(app, 1);
}
}
_ => {}
}
}
fn setting_value(app: &App, index: usize) -> String {
match index {
0 => app.port_config.baud.to_string(),
1 => match app.port_config.data_bits {
DataBits::Five => "5".to_string(),
DataBits::Six => "6".to_string(),
DataBits::Seven => "7".to_string(),
DataBits::Eight => "8".to_string(),
},
2 => match app.port_config.stop_bits {
StopBits::One => "1".to_string(),
StopBits::Two => "2".to_string(),
},
3 => match app.port_config.parity {
Parity::None => "None".to_string(),
Parity::Odd => "Odd".to_string(),
Parity::Even => "Even".to_string(),
},
4 => match app.port_config.flow_control {
FlowControl::None => "None".to_string(),
FlowControl::Software => "Software (XON/XOFF)".to_string(),
FlowControl::Hardware => "Hardware (RTS/CTS)".to_string(),
},
_ => String::new(),
}
}
fn cycle_setting(app: &mut App, dir: i32) {
match app.settings_cursor {
0 => {
let current = BAUD_RATES
.iter()
.position(|&r| r == app.port_config.baud)
.unwrap_or(4) as i32;
let next = (current + dir).rem_euclid(BAUD_RATES.len() as i32) as usize;
app.port_config.baud = BAUD_RATES[next];
app.apply_port_config();
}
1 => {
let opts = [
DataBits::Five,
DataBits::Six,
DataBits::Seven,
DataBits::Eight,
];
let current = opts
.iter()
.position(|&d| d == app.port_config.data_bits)
.unwrap_or(3) as i32;
let next = (current + dir).rem_euclid(opts.len() as i32) as usize;
app.port_config.data_bits = opts[next];
app.apply_port_config();
}
2 => {
let opts = [StopBits::One, StopBits::Two];
let current = opts
.iter()
.position(|&s| s == app.port_config.stop_bits)
.unwrap_or(0) as i32;
let next = (current + dir).rem_euclid(opts.len() as i32) as usize;
app.port_config.stop_bits = opts[next];
app.apply_port_config();
}
3 => {
let opts = [Parity::None, Parity::Even, Parity::Odd];
let current = opts
.iter()
.position(|&p| p == app.port_config.parity)
.unwrap_or(0) as i32;
let next = (current + dir).rem_euclid(opts.len() as i32) as usize;
app.port_config.parity = opts[next];
app.apply_port_config();
}
4 => {
let opts = [
FlowControl::None,
FlowControl::Software,
FlowControl::Hardware,
];
let current = opts
.iter()
.position(|&f| f == app.port_config.flow_control)
.unwrap_or(0) as i32;
let next = (current + dir).rem_euclid(opts.len() as i32) as usize;
app.port_config.flow_control = opts[next];
app.apply_port_config();
}
_ => {}
}
}