use std::{
io::stdout,
net::SocketAddr,
sync::{Arc, mpsc::channel},
thread,
time::Duration,
};
use anyhow::{Context, Result, anyhow, bail};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
};
use ratatui::DefaultTerminal;
use serde::Serialize;
use serialport::{SerialPortType, available_ports};
use crate::{
attached::Local,
client,
config::Config,
console::{Console, ConsoleSpec},
control::{Control, Role},
log::{ConsoleLog, DEFAULT_RETENTION_DAYS},
mcp, picker, probe,
registry::Registry,
remote,
ring::DEFAULT_RING_CAP,
runner::Runner,
session, wsl,
};
const ATTACH_SENTINEL: &str = "\0usbipd-attach:";
const CONSOLE_SENTINEL: &str = "\0console:";
const BACKLOG_LINES: usize = 2000;
const DEFAULT_BAUD: u32 = 115200;
const BAUDS: [u32; 8] = [9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600];
const READY_WAIT: Duration = Duration::from_secs(5);
pub fn run(eol: Vec<u8>, mcp_bind: SocketAddr, remote: bool) -> Result<()> {
let daemon = client::find_daemon(mcp_bind);
if remote && daemon.is_none() {
bail!("no smon answering through the tunnel at {mcp_bind}");
}
let control = Arc::new(Control::new(Role::Tui));
let mut terminal = ratatui::init();
let result = execute!(stdout(), EnableMouseCapture)
.context("enabling mouse capture")
.and_then(|()| pick_and_attach(&mut terminal, &eol, mcp_bind, daemon.as_ref(), remote, &control));
let released = execute!(stdout(), DisableMouseCapture).context("releasing mouse capture");
ratatui::restore();
if control.stopping() {
println!("smon: stopped by an update, start it again to use the new version");
}
result.and(released)
}
fn pick_and_attach(
terminal: &mut DefaultTerminal,
eol: &[u8],
mcp_bind: SocketAddr,
daemon: Option<&client::Daemon>,
remote: bool,
control: &Arc<Control>,
) -> Result<()> {
loop {
let Some(choice) = select_port(terminal, daemon, remote)? else {
return Ok(());
};
if let Some(console) = choice.strip_prefix(CONSOLE_SENTINEL) {
let Some(daemon) = daemon else {
bail!("no smon to attach {console} to");
};
let mut attached = remote::attach(&daemon.addr.to_string(), console)?;
return session::run(terminal, &mut attached, control);
}
let device = choice;
let Some(baud) = pick_baud(terminal, &device)? else {
continue; };
let mut config = Config::load();
config.baud.insert(device.clone(), baud);
config.save()?;
if let Some(daemon) = daemon {
let name = adopt(daemon, &device, baud, eol)?;
let mut attached = remote::attach(&daemon.addr.to_string(), &name)?;
return session::run(terminal, &mut attached, control);
}
return attach(terminal, &device, baud, eol, mcp_bind, control);
}
}
fn attach(
terminal: &mut DefaultTerminal,
device: &str,
baud: u32,
eol: &[u8],
mcp_bind: SocketAddr,
control: &Arc<Control>,
) -> Result<()> {
let log = ConsoleLog::open(device, DEFAULT_RETENTION_DAYS, None)?;
let (inject_tx, inject_rx) = channel();
let console = Console::new(
ConsoleSpec {
device: device.to_string(),
label: None,
baud,
eol: eol.to_vec(),
ring_cap: DEFAULT_RING_CAP,
bridge: None,
},
log,
inject_tx,
);
let runner = Runner::start(Arc::clone(&console), inject_rx, true)?;
let server = match start_server(&console, mcp_bind, Arc::clone(control)) {
Ok(server) => server,
Err(e) => {
runner.stop();
return Err(e);
}
};
let mut local = Local::new(Arc::clone(&console), BACKLOG_LINES);
let result = session::run(terminal, &mut local, control);
if !server.stop() {
console.note("mcp server had already stopped");
}
runner.stop();
result
}
fn adopt(daemon: &client::Daemon, device: &str, baud: u32, eol: &[u8]) -> Result<String> {
let request = AdoptRequest {
device,
baud,
eol: eol_name(eol),
};
let body = client::call(daemon.addr, "console_adopt", &serde_json::to_string(&request)?)?;
let adopted: mcp::StatusResult =
serde_json::from_str(&body).with_context(|| format!("bad console_adopt reply: {body}"))?;
Ok(adopted.label.unwrap_or(adopted.port))
}
#[derive(Serialize)]
struct AdoptRequest<'a> {
device: &'a str,
baud: u32,
eol: &'static str,
}
fn eol_name(eol: &[u8]) -> &'static str {
match eol {
b"\r" => "cr",
b"\n" => "lf",
b"\r\n" => "crlf",
_ => "none",
}
}
struct Server {
control: Arc<Control>,
thread: thread::JoinHandle<()>,
}
impl Server {
fn stop(self) -> bool {
let told = self.control.release();
drop(self.thread);
told
}
}
fn start_server(console: &Arc<Console>, bind: SocketAddr, control: Arc<Control>) -> Result<Server> {
let (ready_tx, ready_rx) = channel();
let registry = Registry::new(vec![Arc::clone(console)], 0);
let thread = mcp::spawn(bind, registry, Arc::clone(&control), ready_tx);
match ready_rx.recv_timeout(READY_WAIT) {
Ok(Ok(addr)) => {
console.note(&format!("mcp serving http://{addr}/mcp"));
Ok(Server { control, thread })
}
Ok(Err(e)) => Err(anyhow!("mcp bind failed: {e}")),
Err(e) => Err(anyhow!("mcp server did not start: {e}")),
}
}
fn pick_baud(terminal: &mut DefaultTerminal, device: &str) -> Result<Option<u32>> {
let config = Config::load();
let saved = config.baud.get(device).copied();
let baud_items: Vec<picker::Item> = BAUDS
.iter()
.map(|b| picker::Item {
value: b.to_string(),
label: b.to_string(),
busy: false,
})
.collect();
let Some(choice) = picker::pick(
terminal,
"Select baud rate",
|| baud_items.clone(),
default_baud_index(&BAUDS, saved),
false,
)?
else {
return Ok(None);
};
Ok(Some(choice.parse::<u32>().context("parsing baud rate")?))
}
fn default_baud_index(bauds: &[u32], saved: Option<u32>) -> Option<usize> {
let target = saved.unwrap_or(DEFAULT_BAUD);
bauds
.iter()
.position(|b| *b == target)
.or_else(|| bauds.iter().position(|b| *b == DEFAULT_BAUD))
}
fn select_port(
terminal: &mut DefaultTerminal,
daemon: Option<&client::Daemon>,
remote: bool,
) -> Result<Option<String>> {
let usbipd = if remote { None } else { wsl::detect() };
let mut notice: Option<String> = None;
loop {
let title = match ¬ice {
Some(msg) => format!("Select serial port -- {msg}"),
None => "Select serial port".to_string(),
};
let make_items = || {
let mut items = daemon_items(daemon);
if !remote {
items.extend(serial_port_items());
}
if let Some(u) = &usbipd {
for device in u.serial_devices() {
items.push(picker::Item {
value: format!("{ATTACH_SENTINEL}{}", device.busid),
label: device.attach_label(),
busy: false,
});
}
}
items
};
let value = match picker::pick(terminal, &title, make_items, None, true)? {
Some(v) => v,
None => return Ok(None),
};
let Some(busid) = value.strip_prefix(ATTACH_SENTINEL) else {
return Ok(Some(value));
};
if let Some(u) = &usbipd {
match u.attach(busid) {
Ok(()) => {
wait_for_new_ports(serial_port_items().len());
notice = Some(format!("attached {busid}"));
}
Err(e) => notice = Some(e.to_string()),
}
}
}
}
fn daemon_items(daemon: Option<&client::Daemon>) -> Vec<picker::Item> {
let Some(daemon) = daemon else {
return Vec::new();
};
daemon
.consoles
.iter()
.map(|console| {
let state = if console.connected {
"connected"
} else {
"disconnected"
};
picker::Item {
value: format!("{CONSOLE_SENTINEL}{}", console.name),
label: format!("{} (smon on {}, {state})", console.name, daemon.addr),
busy: false,
}
})
.collect()
}
fn serial_port_items() -> Vec<picker::Item> {
let mut items: Vec<picker::Item> = probe::hold(|| {
available_ports()
.unwrap_or_default()
.into_iter()
.map(|p| {
let label = match &p.port_type {
SerialPortType::UsbPort(info) => {
let product = info.product.as_deref().unwrap_or("USB");
format!("{} ({product})", p.port_name)
}
SerialPortType::BluetoothPort => format!("{} (Bluetooth)", p.port_name),
SerialPortType::PciPort => format!("{} (PCI)", p.port_name),
SerialPortType::Unknown => p.port_name.clone(),
};
let busy = probe::is_busy(&p.port_name);
picker::Item {
value: p.port_name,
label,
busy,
}
})
.collect()
});
items.sort_by_key(|a| port_sort_key(&a.value));
items
}
fn port_sort_key(name: &str) -> (String, Option<u64>) {
let digits = name.chars().rev().take_while(char::is_ascii_digit).count();
let (prefix, number) = name.split_at(name.len() - digits);
(prefix.to_string(), number.parse().ok())
}
fn wait_for_new_ports(baseline: usize) {
for _ in 0..15 {
if serial_port_items().len() > baseline {
return;
}
thread::sleep(Duration::from_millis(200));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn saved_baud_is_preselected() {
assert_eq!(default_baud_index(&BAUDS, Some(57600)), Some(3));
}
#[test]
fn missing_or_unsaved_baud_falls_back_to_default() {
assert_eq!(default_baud_index(&BAUDS, None), Some(4)); assert_eq!(default_baud_index(&BAUDS, Some(12345)), Some(4));
}
#[test]
fn ports_sort_numerically_not_lexically() {
let mut names = vec!["COM10", "COM9", "COM1"];
names.sort_by_key(|a| port_sort_key(a));
assert_eq!(names, ["COM1", "COM9", "COM10"]);
}
}