use std::sync::mpsc::{self, Receiver, Sender};
use std::time::{Duration, Instant};
use crate::buffer::ScrollbackBuffer;
use crate::config::AppConfig;
use crate::filter::LineFilter;
use crate::history::CommandHistory;
use crate::logging::SessionLogger;
use crate::macros::MacroManager;
use crate::mouse::{LayoutRegions, TextSelection};
use crate::search::Search;
use crate::serial::config::SerialConfig;
use crate::serial::connection::{SerialConnection, SerialEvent};
use crate::serial::detector::{self, PortInfo};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Mode {
Normal,
Input,
Search,
PortSelect,
Settings,
Help,
MacroSelect,
Filter,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
Disconnected,
Connected(String),
Reconnecting(String),
Error(String),
}
pub struct App {
pub mode: Mode,
pub should_quit: bool,
pub buffer: ScrollbackBuffer,
pub input_text: String,
pub input_cursor: usize,
pub line_ending: String,
pub serial_config: SerialConfig,
pub connection_state: ConnectionState,
connection: Option<SerialConnection>,
serial_rx: Option<Receiver<SerialEvent>>,
serial_tx: Option<Sender<SerialEvent>>,
pub scroll_offset: usize,
pub follow_output: bool,
pub rx_bytes: u64,
pub tx_bytes: u64,
pub available_ports: Vec<PortInfo>,
pub port_select_index: usize,
pub show_timestamps: bool,
pub hex_mode: bool,
pub show_line_endings: bool,
pub history: CommandHistory,
pub search: Search,
pub logger: SessionLogger,
pub auto_reconnect: bool,
reconnect_port: Option<String>,
last_reconnect_attempt: Option<Instant>,
reconnect_delay: Duration,
pub status_message: Option<(String, Instant)>,
pub filter: LineFilter,
pub macros: MacroManager,
pub macro_select_index: usize,
pub settings_field: usize,
pub layout: LayoutRegions,
pub selection: TextSelection,
pub ghost_suggestion: Option<String>,
pub app_config: AppConfig,
pub last_command_sent: Option<Instant>,
pub last_response_time: Option<Duration>,
pub quicksend: Vec<String>,
pub show_sent: bool,
pub filter_input: String,
pub filter_mode_is_exclude: bool,
pub filter_select_index: usize,
pub hex_input_mode: bool,
}
impl App {
pub fn new(serial_config: SerialConfig, line_ending: String, app_config: AppConfig) -> Self {
let history = CommandHistory::new(500);
let quicksend = history.top_commands(8);
Self {
mode: Mode::Input,
should_quit: false,
buffer: ScrollbackBuffer::new(10000),
input_text: String::new(),
input_cursor: 0,
line_ending,
serial_config,
connection_state: ConnectionState::Disconnected,
connection: None,
serial_rx: None,
serial_tx: None,
scroll_offset: 0,
follow_output: true,
rx_bytes: 0,
tx_bytes: 0,
available_ports: Vec::new(),
port_select_index: 0,
show_timestamps: true,
hex_mode: false,
show_line_endings: false,
history,
search: Search::new(),
logger: SessionLogger::new(),
auto_reconnect: true,
reconnect_port: None,
last_reconnect_attempt: None,
reconnect_delay: Duration::from_secs(1),
status_message: None,
filter: LineFilter::new(),
macros: MacroManager::new(),
macro_select_index: 0,
settings_field: 0,
layout: LayoutRegions::default(),
selection: TextSelection::new(),
ghost_suggestion: None,
app_config,
last_command_sent: None,
last_response_time: None,
quicksend,
show_sent: true,
filter_input: String::new(),
filter_mode_is_exclude: false,
filter_select_index: 0,
hex_input_mode: false,
}
}
pub fn connect(&mut self, port_name: &str) {
self.disconnect_internal(false);
let (tx, rx) = mpsc::channel();
match SerialConnection::open(port_name, &self.serial_config, tx.clone()) {
Ok(conn) => {
self.connection_state = ConnectionState::Connected(port_name.to_string());
self.connection = Some(conn);
self.serial_rx = Some(rx);
self.serial_tx = Some(tx);
self.reconnect_port = Some(port_name.to_string());
self.set_status(format!("Connected to {}", port_name));
self.app_config.connection.last_port = Some(port_name.to_string());
self.app_config.save();
}
Err(e) => {
self.connection_state = ConnectionState::Error(e.to_string());
}
}
}
fn disconnect_internal(&mut self, keep_reconnect: bool) {
if let Some(conn) = self.connection.take() {
self.rx_bytes += conn.rx_bytes;
self.tx_bytes += conn.tx_bytes();
conn.close();
}
self.serial_rx = None;
self.serial_tx = None;
if !keep_reconnect {
self.connection_state = ConnectionState::Disconnected;
self.reconnect_port = None;
}
}
pub fn disconnect(&mut self) {
self.disconnect_internal(false);
self.set_status("Disconnected".to_string());
}
pub fn toggle_connection(&mut self) {
match &self.connection_state {
ConnectionState::Connected(_) => {
self.disconnect();
}
ConnectionState::Reconnecting(_) => {
self.reconnect_port = None;
self.connection_state = ConnectionState::Disconnected;
self.set_status("Reconnection cancelled".to_string());
}
_ => {
self.open_port_selector();
}
}
}
pub fn send_command(&mut self) {
if self.input_text.is_empty() {
return;
}
let text = self.input_text.clone();
if self.hex_input_mode {
match Self::parse_hex_bytes(&text) {
Ok(bytes) => {
if self.show_sent {
self.buffer.push_sent_line(format!("HEX: {}", text));
}
if let Some(conn) = &self.connection {
match conn.write(&bytes) {
Ok(_) => {
self.tx_bytes = conn.tx_bytes();
self.last_command_sent = Some(Instant::now());
}
Err(_) => {
self.connection_state =
ConnectionState::Error("Write failed".to_string());
}
}
}
}
Err(e) => {
self.set_status(format!("Hex parse error: {}", e));
return; }
}
} else {
let line_ending = self.line_ending.clone();
let data = format!("{}{}", text, line_ending);
if self.show_sent {
self.buffer.push_sent_line(text.clone());
}
if let Some(conn) = &self.connection {
match conn.write(data.as_bytes()) {
Ok(_) => {
self.tx_bytes = conn.tx_bytes();
self.last_command_sent = Some(Instant::now());
}
Err(_) => {
self.connection_state =
ConnectionState::Error("Write failed".to_string());
}
}
}
}
self.history.push(text);
self.history.reset_navigation();
self.input_text.clear();
self.input_cursor = 0;
self.ghost_suggestion = None;
self.update_quicksend();
}
pub fn update_quicksend(&mut self) {
self.quicksend = self.history.top_commands(8);
}
pub fn send_quicksend(&mut self, index: usize) {
if let Some(cmd) = self.quicksend.get(index).cloned() {
self.input_text = cmd;
self.input_cursor = self.input_text.len();
self.send_command();
}
}
pub fn poll_serial(&mut self) -> bool {
let mut changed = false;
if let Some((_, time)) = &self.status_message {
if time.elapsed() > Duration::from_secs(3) {
self.status_message = None;
changed = true;
}
}
let rx = match &self.serial_rx {
Some(rx) => rx,
None => {
self.try_reconnect();
return changed;
}
};
loop {
match rx.try_recv() {
Ok(SerialEvent::Data(data, received_at)) => {
self.rx_bytes += data.len() as u64;
self.logger.log_bytes(&data);
self.buffer.push_bytes(&data);
if self.follow_output {
self.scroll_offset = 0;
}
if let Some(sent_at) = self.last_command_sent.take() {
self.last_response_time = Some(received_at.duration_since(sent_at));
}
changed = true;
}
Ok(SerialEvent::Disconnected) => {
let port = match &self.connection_state {
ConnectionState::Connected(p) => p.clone(),
_ => String::new(),
};
self.disconnect_internal(true);
if self.auto_reconnect && !port.is_empty() {
self.reconnect_port = Some(port.clone());
self.connection_state = ConnectionState::Reconnecting(port);
self.set_status("Port disconnected, reconnecting...".to_string());
} else {
self.connection_state =
ConnectionState::Error("Port disconnected".to_string());
}
break;
}
Ok(SerialEvent::Error(e)) => {
let port = match &self.connection_state {
ConnectionState::Connected(p) => p.clone(),
_ => String::new(),
};
self.disconnect_internal(true);
if self.auto_reconnect && !port.is_empty() {
self.reconnect_port = Some(port.clone());
self.connection_state = ConnectionState::Reconnecting(port);
} else {
self.connection_state = ConnectionState::Error(e);
}
break;
}
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => {
let port = match &self.connection_state {
ConnectionState::Connected(p) => p.clone(),
_ => String::new(),
};
self.disconnect_internal(true);
if self.auto_reconnect && !port.is_empty() {
self.reconnect_port = Some(port.clone());
self.connection_state = ConnectionState::Reconnecting(port);
} else {
self.connection_state =
ConnectionState::Error("Reader thread died".to_string());
}
break;
}
}
}
changed
}
fn try_reconnect(&mut self) {
let port = match &self.reconnect_port {
Some(p) => p.clone(),
None => return,
};
if !matches!(self.connection_state, ConnectionState::Reconnecting(_)) {
return;
}
if let Some(last) = &self.last_reconnect_attempt {
if last.elapsed() < self.reconnect_delay {
return;
}
}
self.last_reconnect_attempt = Some(Instant::now());
let (tx, rx) = mpsc::channel();
match SerialConnection::open(&port, &self.serial_config, tx.clone()) {
Ok(conn) => {
self.connection_state = ConnectionState::Connected(port.clone());
self.connection = Some(conn);
self.serial_rx = Some(rx);
self.serial_tx = Some(tx);
self.set_status(format!("Reconnected to {}", port));
}
Err(_) => {
}
}
}
pub fn auto_detect_baud(&mut self, port_name: &str) {
self.set_status("Auto-detecting baud rate...".to_string());
match crate::serial::auto_detect::auto_detect_baud(port_name) {
Some(rate) => {
self.serial_config.baud_rate = rate;
self.app_config.defaults.baud_rate = rate;
self.app_config.save();
self.set_status(format!("Detected baud rate: {}", rate));
}
None => {
self.set_status("Could not detect baud rate — no readable data".to_string());
}
}
}
pub fn open_port_selector(&mut self) {
self.available_ports = detector::available_ports();
self.port_select_index = 0;
self.mode = Mode::PortSelect;
}
pub fn connect_selected_port(&mut self) {
if let Some(port) = self.available_ports.get(self.port_select_index) {
let port_name = port.name.clone();
self.mode = Mode::Normal;
self.connect(&port_name);
}
}
pub fn scroll_up(&mut self, lines: usize) {
let view_height = self.layout.terminal_view.3 as usize; let max_scroll = self.buffer.display_len().saturating_sub(view_height);
self.scroll_offset = (self.scroll_offset + lines).min(max_scroll);
if max_scroll > 0 {
self.follow_output = false;
}
}
pub fn scroll_down(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_sub(lines);
if self.scroll_offset == 0 {
self.follow_output = true;
}
}
pub fn scroll_to_bottom(&mut self) {
self.scroll_offset = 0;
self.follow_output = true;
}
pub fn scroll_to_top(&mut self) {
let max_scroll = self.buffer.display_len().saturating_sub(1);
self.scroll_offset = max_scroll;
self.follow_output = false;
}
pub fn scroll_to_line(&mut self, line_index: usize) {
let total = self.buffer.display_len();
if total == 0 {
return;
}
self.scroll_offset = total.saturating_sub(line_index + 1);
self.follow_output = false;
}
pub fn input_char(&mut self, c: char) {
self.input_text.insert(self.input_cursor, c);
self.input_cursor += 1;
self.update_ghost();
}
pub fn input_backspace(&mut self) {
if self.input_cursor > 0 {
self.input_cursor -= 1;
self.input_text.remove(self.input_cursor);
self.update_ghost();
}
}
pub fn input_delete(&mut self) {
if self.input_cursor < self.input_text.len() {
self.input_text.remove(self.input_cursor);
self.update_ghost();
}
}
pub fn input_cursor_left(&mut self) {
self.input_cursor = self.input_cursor.saturating_sub(1);
}
pub fn input_cursor_right(&mut self) {
self.input_cursor = (self.input_cursor + 1).min(self.input_text.len());
}
pub fn input_cursor_home(&mut self) {
self.input_cursor = 0;
}
pub fn input_cursor_end(&mut self) {
self.input_cursor = self.input_text.len();
}
pub fn update_ghost(&mut self) {
if self.input_cursor != self.input_text.len() || self.input_text.is_empty() {
self.ghost_suggestion = None;
return;
}
self.ghost_suggestion = self.history.suggest(&self.input_text).map(|s| s.to_string());
}
pub fn accept_suggestion(&mut self) {
if let Some(suggestion) = self.ghost_suggestion.take() {
self.input_text = suggestion;
self.input_cursor = self.input_text.len();
}
}
pub fn history_previous(&mut self) {
if let Some(text) = self.history.previous(&self.input_text) {
self.input_text = text.to_string();
self.input_cursor = self.input_text.len();
}
}
pub fn history_next(&mut self) {
if let Some(text) = self.history.next() {
self.input_text = text.to_string();
self.input_cursor = self.input_text.len();
}
}
pub fn start_search(&mut self) {
self.search.activate();
self.mode = Mode::Search;
}
pub fn search_char(&mut self, c: char) {
self.search.push_char(c);
self.search.execute(&self.buffer);
if let Some(line) = self.search.current_line() {
self.scroll_to_line(line);
}
}
pub fn search_backspace(&mut self) {
self.search.pop_char();
self.search.execute(&self.buffer);
}
pub fn search_next(&mut self) {
if let Some(line) = self.search.next_match() {
self.scroll_to_line(line);
}
}
pub fn search_prev(&mut self) {
if let Some(line) = self.search.prev_match() {
self.scroll_to_line(line);
}
}
pub fn end_search(&mut self) {
self.search.deactivate();
self.mode = Mode::Normal;
}
pub fn toggle_hex_mode(&mut self) {
self.hex_mode = !self.hex_mode;
}
pub fn toggle_line_endings(&mut self) {
self.show_line_endings = !self.show_line_endings;
}
pub fn toggle_logging(&mut self) {
match self.logger.toggle() {
Ok(Some(path)) => {
self.set_status(format!("Logging to {}", path.display()));
}
Ok(None) => {
self.set_status("Logging stopped".to_string());
}
Err(e) => {
self.set_status(format!("Log error: {}", e));
}
}
}
pub fn clear_buffer(&mut self) {
self.buffer.clear();
self.scroll_offset = 0;
self.follow_output = true;
self.search.deactivate();
self.set_status("Buffer cleared".to_string());
}
fn set_status(&mut self, msg: String) {
self.status_message = Some((msg, Instant::now()));
}
pub fn set_status_pub(&mut self, msg: String) {
self.set_status(msg);
}
pub fn total_rx_bytes(&self) -> u64 {
self.rx_bytes
}
pub fn total_tx_bytes(&self) -> u64 {
self.tx_bytes
}
pub fn is_connected(&self) -> bool {
matches!(self.connection_state, ConnectionState::Connected(_))
}
pub fn is_reconnecting(&self) -> bool {
matches!(self.connection_state, ConnectionState::Reconnecting(_))
}
pub fn add_filter_include(&mut self, pattern: &str) {
match self.filter.add_include(pattern) {
Ok(_) => self.set_status(format!("Filter +{}", pattern)),
Err(e) => self.set_status(e),
}
}
pub fn add_filter_exclude(&mut self, pattern: &str) {
match self.filter.add_exclude(pattern) {
Ok(_) => self.set_status(format!("Filter -{}", pattern)),
Err(e) => self.set_status(e),
}
}
pub fn clear_filters(&mut self) {
self.filter.clear();
self.set_status("Filters cleared".to_string());
}
pub fn open_filter_popup(&mut self) {
self.filter_input.clear();
self.filter_select_index = 0;
self.mode = Mode::Filter;
}
pub fn submit_filter(&mut self) {
if self.filter_input.is_empty() {
return;
}
let pattern = self.filter_input.clone();
if self.filter_mode_is_exclude {
self.add_filter_exclude(&pattern);
} else {
self.add_filter_include(&pattern);
}
self.filter_input.clear();
}
pub fn remove_filter(&mut self, index: usize) {
self.filter.remove(index);
if self.filter.count() == 0 {
self.set_status("All filters removed".to_string());
}
if self.filter_select_index >= self.filter.count() && self.filter_select_index > 0 {
self.filter_select_index -= 1;
}
}
pub fn open_macro_selector(&mut self) {
self.macro_select_index = 0;
self.mode = Mode::MacroSelect;
}
pub fn send_text(&mut self, text: &str) {
let line_ending = self.line_ending.clone();
let data = format!("{}{}", text, line_ending);
if let Some(conn) = &self.connection {
match conn.write(data.as_bytes()) {
Ok(_) => {
self.tx_bytes = conn.tx_bytes();
}
Err(_) => {
self.connection_state =
ConnectionState::Error("Write failed".to_string());
}
}
}
}
pub fn execute_macro(&mut self, name: &str) {
if let Some(m) = self.macros.get(name) {
let commands: Vec<String> = m.commands.iter().map(|c| c.text.clone()).collect();
self.set_status(format!("Running macro: {}", name));
for cmd in commands {
self.send_text(&cmd);
}
} else {
self.set_status(format!("Macro not found: {}", name));
}
}
pub fn execute_selected_macro(&mut self) {
let macros = self.macros.list();
if let Some(m) = macros.get(self.macro_select_index) {
let name = m.name.clone();
self.execute_macro(&name);
}
}
pub fn open_settings(&mut self) {
self.settings_field = 0;
self.mode = Mode::Settings;
}
pub fn settings_next_value(&mut self) {
use serialport::*;
match self.settings_field {
0 => {
let rates = crate::ui::settings::BAUD_RATES;
let current_idx = rates.iter().position(|&r| r == self.serial_config.baud_rate);
let next_idx = match current_idx {
Some(i) => (i + 1) % rates.len(),
None => 0,
};
self.serial_config.baud_rate = rates[next_idx];
}
1 => {
self.serial_config.data_bits = match self.serial_config.data_bits {
DataBits::Five => DataBits::Six,
DataBits::Six => DataBits::Seven,
DataBits::Seven => DataBits::Eight,
DataBits::Eight => DataBits::Five,
};
}
2 => {
self.serial_config.parity = match self.serial_config.parity {
Parity::None => Parity::Odd,
Parity::Odd => Parity::Even,
Parity::Even => Parity::None,
};
}
3 => {
self.serial_config.stop_bits = match self.serial_config.stop_bits {
StopBits::One => StopBits::Two,
StopBits::Two => StopBits::One,
};
}
4 => {
self.serial_config.flow_control = match self.serial_config.flow_control {
FlowControl::None => FlowControl::Software,
FlowControl::Software => FlowControl::Hardware,
FlowControl::Hardware => FlowControl::None,
};
}
5 => {
self.line_ending = match self.line_ending.as_str() {
"\r\n" => "\n".to_string(),
"\n" => "\r".to_string(),
"\r" => "\r\n".to_string(),
_ => "\r\n".to_string(),
};
}
_ => {}
}
}
pub fn settings_prev_value(&mut self) {
use serialport::*;
match self.settings_field {
0 => {
let rates = crate::ui::settings::BAUD_RATES;
let current_idx = rates.iter().position(|&r| r == self.serial_config.baud_rate);
let next_idx = match current_idx {
Some(0) | None => rates.len() - 1,
Some(i) => i - 1,
};
self.serial_config.baud_rate = rates[next_idx];
}
1 => {
self.serial_config.data_bits = match self.serial_config.data_bits {
DataBits::Five => DataBits::Eight,
DataBits::Six => DataBits::Five,
DataBits::Seven => DataBits::Six,
DataBits::Eight => DataBits::Seven,
};
}
2 => {
self.serial_config.parity = match self.serial_config.parity {
Parity::None => Parity::Even,
Parity::Odd => Parity::None,
Parity::Even => Parity::Odd,
};
}
3 => {
self.serial_config.stop_bits = match self.serial_config.stop_bits {
StopBits::One => StopBits::Two,
StopBits::Two => StopBits::One,
};
}
4 => {
self.serial_config.flow_control = match self.serial_config.flow_control {
FlowControl::None => FlowControl::Hardware,
FlowControl::Software => FlowControl::None,
FlowControl::Hardware => FlowControl::Software,
};
}
5 => {
self.line_ending = match self.line_ending.as_str() {
"\r\n" => "\r".to_string(),
"\n" => "\r\n".to_string(),
"\r" => "\n".to_string(),
_ => "\r\n".to_string(),
};
}
_ => {}
}
}
pub fn apply_settings(&mut self) {
self.mode = Mode::Normal;
let summary = self.serial_config.summary();
self.set_status(format!("Settings: {}", summary));
self.sync_config_to_disk();
if let ConnectionState::Connected(port) = &self.connection_state {
let port = port.clone();
self.disconnect();
self.connect(&port);
}
}
fn sync_config_to_disk(&mut self) {
self.app_config.defaults.baud_rate = self.serial_config.baud_rate;
self.app_config.defaults.data_bits = match self.serial_config.data_bits {
serialport::DataBits::Five => 5,
serialport::DataBits::Six => 6,
serialport::DataBits::Seven => 7,
serialport::DataBits::Eight => 8,
};
self.app_config.defaults.parity = match self.serial_config.parity {
serialport::Parity::None => "none".to_string(),
serialport::Parity::Odd => "odd".to_string(),
serialport::Parity::Even => "even".to_string(),
};
self.app_config.defaults.stop_bits = match self.serial_config.stop_bits {
serialport::StopBits::One => 1,
serialport::StopBits::Two => 2,
};
self.app_config.defaults.flow_control = match self.serial_config.flow_control {
serialport::FlowControl::None => "none".to_string(),
serialport::FlowControl::Software => "software".to_string(),
serialport::FlowControl::Hardware => "hardware".to_string(),
};
self.app_config.defaults.line_ending = match self.line_ending.as_str() {
"\n" => "lf".to_string(),
"\r" => "cr".to_string(),
_ => "crlf".to_string(),
};
self.app_config.save();
}
pub fn input_cursor_word_left(&mut self) {
let chars: Vec<char> = self.input_text.chars().collect();
if self.input_cursor == 0 {
return;
}
let mut pos = self.input_cursor;
while pos > 0 && !chars[pos - 1].is_alphanumeric() {
pos -= 1;
}
while pos > 0 && chars[pos - 1].is_alphanumeric() {
pos -= 1;
}
self.input_cursor = pos;
}
pub fn input_cursor_word_right(&mut self) {
let chars: Vec<char> = self.input_text.chars().collect();
let len = chars.len();
if self.input_cursor >= len {
return;
}
let mut pos = self.input_cursor;
while pos < len && chars[pos].is_alphanumeric() {
pos += 1;
}
while pos < len && !chars[pos].is_alphanumeric() {
pos += 1;
}
self.input_cursor = pos;
}
pub fn input_delete_word_back(&mut self) {
if self.input_cursor == 0 {
return;
}
let old_cursor = self.input_cursor;
self.input_cursor_word_left();
let new_cursor = self.input_cursor;
let chars: Vec<char> = self.input_text.chars().collect();
self.input_text = chars[..new_cursor]
.iter()
.chain(chars[old_cursor..].iter())
.collect();
self.update_ghost();
}
pub fn input_kill_line(&mut self) {
self.input_text.clear();
self.input_cursor = 0;
self.ghost_suggestion = None;
}
pub fn toggle_hex_input(&mut self) {
self.hex_input_mode = !self.hex_input_mode;
if self.hex_input_mode {
self.set_status("Hex input mode ON — type space-separated hex bytes".to_string());
} else {
self.set_status("Hex input mode OFF".to_string());
}
}
fn parse_hex_bytes(input: &str) -> Result<Vec<u8>, String> {
let cleaned: String = input.chars().filter(|c| !c.is_whitespace()).collect();
if cleaned.is_empty() {
return Err("Empty hex input".to_string());
}
if cleaned.len() % 2 != 0 {
return Err("Odd number of hex digits".to_string());
}
let mut bytes = Vec::with_capacity(cleaned.len() / 2);
for i in (0..cleaned.len()).step_by(2) {
let byte_str = &cleaned[i..i + 2];
match u8::from_str_radix(byte_str, 16) {
Ok(b) => bytes.push(b),
Err(_) => return Err(format!("Invalid hex byte: {}", byte_str)),
}
}
Ok(bytes)
}
}