use crate::config::MergedConfig;
use crate::serial::{
get_timestamp, parse_data_bits, parse_flow_control, parse_parity, parse_stop_bits,
};
use inline_colorization::*;
use std::fs::OpenOptions;
use std::io::{self, BufWriter, Read, Write};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
terminal,
};
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
match chars.peek() {
Some('[') => {
chars.next();
for ch in chars.by_ref() {
if ch.is_ascii_alphabetic() {
break;
}
}
}
_ => {
chars.next();
}
}
} else {
out.push(c);
}
}
out
}
pub fn run_normal_mode(
config: MergedConfig,
port_name: String,
) -> Result<(), Box<dyn std::error::Error>> {
let data_bits =
parse_data_bits(config.data_bits).map_err(|e| format!("Configuration error: {}", e))?;
let stop_bits =
parse_stop_bits(config.stop_bits).map_err(|e| format!("Configuration error: {}", e))?;
let parity = parse_parity(&config.parity).map_err(|e| format!("Configuration error: {}", e))?;
let flow_control = parse_flow_control(&config.flow_control)
.map_err(|e| format!("Configuration error: {}", e))?;
let mut port = serialport::new(&port_name, config.baud)
.timeout(Duration::from_millis(config.timeout_ms))
.data_bits(data_bits)
.stop_bits(stop_bits)
.parity(parity)
.flow_control(flow_control)
.open()
.map_err(|e| format!("Failed to open port {}: {}", port_name, e))?;
let _ = port.write_data_terminal_ready(false);
thread::sleep(Duration::from_millis(config.reset_delay_ms));
let _ = port.write_all(b"\r");
let _ = port.flush();
if config.zephyr {
thread::sleep(Duration::from_millis(100));
let _ = port.write_all(b"shell echo off\r");
let _ = port.flush();
thread::sleep(Duration::from_millis(100));
}
let log_writer = if let Some(log_path) = &config.log_file {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
.map_err(|e| format!("Failed to open log file {}: {}", log_path, e))?;
Some(BufWriter::new(file))
} else {
None
};
println!(
"{color_green} ComChan connected to {} at {} baud{color_reset}",
port_name, config.baud
);
if config.verbose {
println!(
"{color_blue}⚙️ Config: {} data bits, {} stop bits, {} parity, {} flow control{color_reset}",
config.data_bits, config.stop_bits, config.parity, config.flow_control
);
if let Some(log_path) = &config.log_file {
println!("{color_blue} Logging to: {}{color_reset}", log_path);
}
}
println!("{color_green} Listening… (Ctrl+C to exit, Ctrl+L to clear screen){color_reset}\n");
let (input_tx, input_rx) = mpsc::channel::<String>();
let (ctrl_tx, ctrl_rx) = mpsc::channel::<u8>();
thread::spawn(move || {
terminal::enable_raw_mode().ok();
let mut line_buf = String::new();
loop {
if event::poll(Duration::from_millis(10)).unwrap_or(false) {
match event::read() {
Ok(Event::Key(KeyEvent {
code, modifiers, ..
})) => match (code, modifiers) {
(KeyCode::Char('l'), KeyModifiers::CONTROL) => {
print!("\x1bc\x1b[5 q");
io::stdout().flush().ok();
ctrl_tx.send(b'\r').ok();
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => break,
(KeyCode::Enter, _) => {
let _ = input_tx.send(line_buf.clone());
line_buf.clear();
}
(KeyCode::Backspace, _) => {
line_buf.pop();
print!("\x08 \x08");
io::stdout().flush().ok();
}
(KeyCode::Char(c), _) => {
line_buf.push(c);
print!("{}", c);
io::stdout().flush().ok();
}
_ => {}
},
Err(_) => break,
_ => {}
}
}
}
terminal::disable_raw_mode().ok();
});
let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
println!("\n{color_yellow} Shutting down ComChan…{color_reset}");
r.store(false, std::sync::atomic::Ordering::SeqCst);
})?;
let mut buffer = [0u8; 1024];
let mut log_writer = log_writer;
let mut last_sent: Option<String> = None;
let mut line_acc = String::new();
while running.load(std::sync::atomic::Ordering::SeqCst) {
match port.read(&mut buffer) {
Ok(n) if n > 0 => {
let raw = &buffer[..n];
let text = String::from_utf8_lossy(raw);
line_acc.push_str(&text);
let mut suppress = false;
if let Some(ref sent) = last_sent {
let clean_acc = strip_ansi(&line_acc).trim().to_string();
if clean_acc == *sent {
suppress = true;
last_sent = None;
line_acc.clear();
}
}
if suppress {
continue;
}
if config.verbose {
let mut remaining = text.as_ref();
while let Some(pos) = remaining.find('\n') {
let chunk = &remaining[..=pos];
let clean = strip_ansi(chunk);
if !clean.trim().is_empty() {
print!("[{}] {}", get_timestamp(), chunk);
} else {
print!("{}", chunk);
}
remaining = &remaining[pos + 1..];
}
if !remaining.is_empty() {
print!("{}", remaining);
}
} else {
io::stdout().write_all(raw)?;
}
io::stdout().flush()?;
if let Some(ref mut writer) = log_writer {
let mut remaining = text.as_ref();
while let Some(pos) = remaining.find('\n') {
let chunk = &remaining[..=pos];
let clean = strip_ansi(chunk);
writeln!(writer, "RX [{}]: {}", get_timestamp(), clean.trim_end())?;
remaining = &remaining[pos + 1..];
}
writer.flush()?;
}
if line_acc.contains('\n') {
line_acc.clear();
}
}
Ok(_) => {}
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {}
Err(e) => {
eprintln!("{color_red}❌ Serial read error: {e}{color_reset}");
if let Some(ref mut writer) = log_writer {
writeln!(writer, "ERROR [{}]: {}", get_timestamp(), e)?;
writer.flush()?;
}
}
}
if let Ok(input) = input_rx.try_recv() {
let clean = input.trim_end();
if !clean.is_empty() {
let message = format!("{}\r", clean);
if let Err(e) = port.write_all(message.as_bytes()) {
eprintln!("{color_red}❌ Write error: {e}{color_reset}");
if let Some(ref mut writer) = log_writer {
writeln!(writer, "ERROR [{}]: Write error: {}", get_timestamp(), e)?;
writer.flush()?;
}
continue;
}
port.flush()?;
last_sent = Some(clean.to_string());
line_acc.clear();
if config.verbose {
print!("\r\n[{}] Sent: {}\r\n", get_timestamp(), clean);
io::stdout().flush()?;
}
if let Some(ref mut writer) = log_writer {
writeln!(writer, "TX [{}]: {}", get_timestamp(), clean)?;
writer.flush()?;
}
thread::sleep(Duration::from_millis(100));
}
}
if let Ok(byte) = ctrl_rx.try_recv() {
let _ = port.write_all(&[byte]);
let _ = port.flush();
}
thread::sleep(Duration::from_millis(10));
}
println!("\r\n{color_green} ComChan disconnected cleanly{color_reset}");
terminal::disable_raw_mode().ok();
Ok(())
}