#![forbid(unsafe_code)]
mod args;
mod signal;
use std::io;
use std::path::Path;
use std::process::ExitCode;
use clap::Parser;
use rtcom_config::{ModalStyle, Profile};
use rtcom_core::{
LineEndingConfig, LineEndingMapper, ModemLineSnapshot, SerialDevice, SerialPortDevice, Session,
UucpLock,
};
use rtcom_tui::profile_bridge::{parse_line_ending, serial_config_to_section};
use rtcom_tui::{summarise, TuiApp};
use tracing_subscriber::EnvFilter;
use crate::args::Cli;
use crate::signal::SignalListener;
fn main() -> ExitCode {
let cli = Cli::parse();
init_tracing(cli.verbose);
let profile_path = cli
.config
.clone()
.or_else(rtcom_config::default_profile_path);
let profile = load_profile(profile_path.as_deref(), cli.quiet);
let serial_cfg = cli.to_serial_config(&profile);
if cli.save {
let Some(path) = profile_path.as_ref() else {
eprintln!(
"rtcom: --save requested but no profile path is available \
(pass `-c PATH` or set HOME/XDG_CONFIG_HOME)"
);
return ExitCode::from(1);
};
let updated = Profile {
serial: serial_config_to_section(&serial_cfg),
line_endings: profile.line_endings.clone(),
modem: profile.modem.clone(),
screen: profile.screen.clone(),
};
if let Err(err) = rtcom_config::write(path, &updated) {
eprintln!("rtcom: --save failed: {err}");
return ExitCode::from(1);
}
if !cli.quiet {
eprintln!("rtcom: saved profile to {}", path.display());
}
}
let lock = match UucpLock::acquire(&cli.device) {
Ok(lock) => lock,
Err(err) => {
eprintln!("rtcom: {err}");
return ExitCode::from(1);
}
};
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(err) => {
eprintln!("rtcom: failed to start tokio runtime: {err}");
return ExitCode::from(1);
}
};
let exit_code = runtime.block_on(async_main(cli, profile, profile_path, serial_cfg));
drop(lock);
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
ExitCode::from(exit_code as u8)
}
async fn async_main(
cli: Cli,
profile: Profile,
profile_path: Option<std::path::PathBuf>,
serial_cfg: rtcom_core::SerialConfig,
) -> i32 {
let mut device = match SerialPortDevice::open(&cli.device, serial_cfg) {
Ok(d) => d,
Err(err) => {
eprintln!("rtcom: open {} failed: {err}", cli.device);
return 1;
}
};
if let Err(err) = apply_initial_lines(&mut device, &cli) {
eprintln!("rtcom: failed to set initial DTR/RTS state: {err}");
return 1;
}
let initial_dtr = !cli.lower_dtr;
let initial_rts = !cli.lower_rts;
let line_endings = resolved_line_endings(&cli, &profile);
let session = Session::new(device)
.with_omap(LineEndingMapper::new(line_endings.omap))
.with_imap(LineEndingMapper::new(line_endings.imap))
.with_initial_dtr(initial_dtr)
.with_initial_rts(initial_rts);
let bus = session.bus().clone();
let cancel = session.cancellation_token();
let tui_rx = bus.subscribe();
let listener = match SignalListener::install(cancel.clone()) {
Ok(l) => l,
Err(err) => {
tracing::error!(%err, "failed to install signal handlers");
return 1;
}
};
let mut app = TuiApp::new(bus.clone());
app.set_device_summary(cli.device.clone(), summarise(&serial_cfg));
app.set_serial_config(serial_cfg);
app.set_line_endings(line_endings);
app.set_modem_lines(ModemLineSnapshot {
dtr: initial_dtr,
rts: initial_rts,
});
app.set_modal_style(profile_modal_style(&profile));
app.set_wheel_scroll_lines(profile.screen.wheel_scroll_lines);
app.set_cli_overrides(cli_override_labels(&cli));
let session_handle = tokio::spawn(session.run());
let tui_result = rtcom_tui::run(app, bus, tui_rx, cancel.clone(), profile_path, profile).await;
cancel.cancel();
match session_handle.await {
Ok(Ok(())) => {}
Ok(Err(err)) => {
tracing::error!(error = %err, "session returned error");
}
Err(err) => {
tracing::error!(error = %err, "session task panicked");
}
}
if let Err(err) = tui_result {
tracing::error!(error = %err, "tui exited with error");
return 1;
}
listener.exit_code()
}
fn init_tracing(verbosity: u8) {
let default_level = match verbosity {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(io::stderr)
.init();
}
fn apply_initial_lines(device: &mut SerialPortDevice, cli: &Cli) -> Result<(), rtcom_core::Error> {
if cli.lower_dtr {
device.set_dtr(false)?;
} else if cli.raise_dtr {
device.set_dtr(true)?;
}
if cli.lower_rts {
device.set_rts(false)?;
} else if cli.raise_rts {
device.set_rts(true)?;
}
Ok(())
}
fn load_profile(path: Option<&Path>, quiet: bool) -> Profile {
let Some(path) = path else {
return Profile::default();
};
if !path.exists() {
return Profile::default();
}
match rtcom_config::read(path) {
Ok(p) => p,
Err(err) => {
if !quiet {
eprintln!(
"rtcom: profile at {} unreadable ({err}); using defaults",
path.display()
);
}
Profile::default()
}
}
}
fn resolved_line_endings(cli: &Cli, profile: &Profile) -> LineEndingConfig {
LineEndingConfig {
omap: cli.resolved_omap(profile),
imap: cli.resolved_imap(profile),
emap: parse_line_ending(&profile.line_endings.emap),
}
}
const fn profile_modal_style(profile: &Profile) -> ModalStyle {
profile.screen.modal_style
}
fn cli_override_labels(cli: &Cli) -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
if cli.baud.is_some() {
out.push("-b");
}
if cli.data_bits.is_some() {
out.push("-d");
}
if cli.stop_bits.is_some() {
out.push("-s");
}
if cli.parity.is_some() {
out.push("-p");
}
if cli.flow.is_some() {
out.push("-f");
}
if cli.omap.is_some() || cli.imap.is_some() || cli.emap.is_some() {
out.push("--omap/--imap/--emap");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use rtcom_core::LineEnding;
#[test]
fn profile_modal_style_picks_up_screen_section() {
let mut profile = Profile::default();
profile.screen.modal_style = ModalStyle::Fullscreen;
assert_eq!(profile_modal_style(&profile), ModalStyle::Fullscreen);
}
#[test]
fn cli_override_labels_empty_when_nothing_is_overridden() {
let cli = Cli::parse_from(["rtcom", "/dev/x"]);
assert!(cli_override_labels(&cli).is_empty());
}
#[test]
fn cli_override_labels_lists_every_set_flag() {
let cli = Cli::parse_from([
"rtcom", "/dev/x", "-b", "9600", "-d", "7", "-s", "2", "-p", "even", "-f", "hw",
"--omap", "crlf",
]);
let labels = cli_override_labels(&cli);
assert_eq!(
labels,
vec!["-b", "-d", "-s", "-p", "-f", "--omap/--imap/--emap"]
);
}
#[test]
fn cli_override_labels_collapses_line_ending_flags_into_single_label() {
let cli = Cli::parse_from(["rtcom", "/dev/x", "--imap", "igncr"]);
let labels = cli_override_labels(&cli);
assert_eq!(labels, vec!["--omap/--imap/--emap"]);
}
#[test]
fn resolved_line_endings_cli_overrides_profile() {
let cli = Cli::parse_from(["rtcom", "/dev/x", "--omap", "crlf", "--imap", "igncr"]);
let mut profile = Profile::default();
profile.line_endings.emap = "lfcr".into();
let le = resolved_line_endings(&cli, &profile);
assert_eq!(le.omap, LineEnding::AddCrToLf);
assert_eq!(le.imap, LineEnding::DropCr);
assert_eq!(le.emap, LineEnding::AddLfToCr);
}
}