From 7b92b81ecd4d4db7252257f45c3e88bcb7303853 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Sun, 11 Oct 2026 17:54:11 +0100 Subject: [PATCH] Serial/Telnet Send Break and DTR/RTS tab controls; fix Help and hints; drop unused log path; docs --- docs/user-manual.md | 4 +++- rust/app/src/app.rs | 26 +++++++++++++++++++++++++- rust/app/src/chrome.rs | 2 +- rust/app/src/main.rs | 4 +++- rust/app/src/recordings.rs | 2 +- rust/app/src/settings.rs | 4 ++-- rust/app/src/state.rs | 2 ++ rust/app/src/tabs.rs | 23 ++++++++++++++++++++++- rust/core/src/config.rs | 2 -- 9 files changed, 59 insertions(+), 10 deletions(-) diff --git a/docs/user-manual.md b/docs/user-manual.md index 1b8bad8..618cf58 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -148,6 +148,8 @@ Serial profiles open a terminal on a local serial port. The defaults are 115200 baud, 8 data bits, no parity, 1 stop bit, no flow control, CR line endings, and DTR and RTS asserted. +While connected, right-click the tab to **Send Break** or toggle **DTR** and **RTS**. Telnet tabs also offer **Send Break**. + When the adapter is unplugged the session disconnects; with auto-reconnect on, the port is reopened when the device returns. Serial sessions support everything other terminals do: recording, triggers, broadcast, snippets, find and split panes. ### VNC @@ -156,7 +158,7 @@ When the adapter is unplugged the session disconnects; with auto-reconnect on, t - **Display**: **View Only**, **Scale to Fit**, **Shared Session** and **Quality (0-9)**. - **SSH Tunnel**: carry VNC traffic through SSH, with host, port, username and password, public key or SSH agent authentication. -The viewer's toolbar shows the connection state and desktop size, and has buttons to switch between scaled and 1:1 display, refresh the screen, **Paste Clipboard** (send your local clipboard to the remote desktop) and **Ctrl+Alt+Del**. Click the desktop to give it keyboard focus. +The viewer's toolbar shows the connection state and desktop size, and has buttons to switch between scaled and 1:1 display, refresh the screen, **Paste Clipboard** (send your local clipboard to the remote desktop) and **Ctrl+Alt+Del**. Text the remote desktop copies arrives on your local clipboard. Click the desktop to give it keyboard focus. Use SSH tunnelling for VNC whenever possible. If a VNC profile has no tunnel, Nexus Terminal warns that the traffic is unencrypted and offers **Connect Anyway**, **Edit Profile** or **Cancel**. diff --git a/rust/app/src/app.rs b/rust/app/src/app.rs index 95ee1b9..902a389 100644 --- a/rust/app/src/app.rs +++ b/rust/app/src/app.rs @@ -244,7 +244,8 @@ impl App { }; let profile = self.s.svc.sessions.profile(id); let view = self.view_for(&info, profile.as_ref()); - self.s.panes.insert(id.into(), Pane { info, view, recording: false, local_exited: false, connected_since: None, highlight_until: 0.0 }); + let lines = profile.as_ref().and_then(|p| p.serial.as_ref()).map_or((true, true), |c| (c.dtr_on_connect, c.rts_on_connect)); + self.s.panes.insert(id.into(), Pane { info, view, recording: false, local_exited: false, connected_since: None, highlight_until: 0.0, lines }); if tab { self.s.tabs.push(Tab { id: id.into(), activity: false, split: None }); self.s.active = Some(id.into()); @@ -691,6 +692,29 @@ impl App { } } + /// Telnet or serial break. + pub fn send_break(&mut self, id: &str) { + match self.s.svc.sessions.send_break(id) { + Ok(()) => self.s.info("Break sent"), + Err(e) => self.s.error(e.to_string()), + } + } + + /// Toggles a serial modem line: `dtr` true for DTR, false for RTS. + pub fn toggle_line(&mut self, id: &str, dtr: bool) { + let Some((d, r)) = self.s.pane(id).map(|p| p.lines) else { return }; + let (d, r) = if dtr { (!d, r) } else { (d, !r) }; + match self.s.svc.sessions.set_serial_lines(id, d, r) { + Ok(()) => { + if let Some(p) = self.s.pane_mut(id) { + p.lines = (d, r); + } + self.s.info(format!("DTR {} · RTS {}", if d { "on" } else { "off" }, if r { "on" } else { "off" })); + } + Err(e) => self.s.error(e.to_string()), + } + } + pub fn toggle_broadcast(&mut self) { self.s.broadcast = !self.s.broadcast; if !self.s.broadcast { diff --git a/rust/app/src/chrome.rs b/rust/app/src/chrome.rs index 8de6f81..15033f1 100644 --- a/rust/app/src/chrome.rs +++ b/rust/app/src/chrome.rs @@ -77,7 +77,7 @@ pub fn title_bar(app: &mut App, ui: &mut Ui) { app.s.request(Request::ExportEncrypted(vec![])); } if item(ui, icon::FILE_ARROW_DOWN, "Import ~/.ssh/config", "") { - match app.s.svc.import_ssh_config(None, "Imported") { + match app.s.svc.import_ssh_config(None, "SSH Config") { Ok(n) => { app.s.success(format!("Imported {n} host(s) from ~/.ssh/config")); app.s.reload_profiles(); diff --git a/rust/app/src/main.rs b/rust/app/src/main.rs index 0062485..5c8cd87 100644 --- a/rust/app/src/main.rs +++ b/rust/app/src/main.rs @@ -56,7 +56,9 @@ fn main() -> eframe::Result { return Ok(()); } _ => { - println!("usage: nexus-terminal [--theme pro|midnight|light] [--data-dir DIR] [--screenshot out.png] [--size WxH]"); + println!( + "usage: nexus-terminal [--theme pro|midnight|light] [--data-dir DIR] [--connect NAME] [--open WINDOW] [--run-local CMD] [--screenshot out.png] [--size WxH] [--version]" + ); return Ok(()); } } diff --git a/rust/app/src/recordings.rs b/rust/app/src/recordings.rs index d053333..b0d8e74 100644 --- a/rust/app/src/recordings.rs +++ b/rust/app/src/recordings.rs @@ -223,7 +223,7 @@ impl Recordings { ui.separator(); if self.list.is_empty() { if !self.loading { - widgets::empty(ui, icon::RECORD, "No recordings yet", "Use the record button on a terminal tab to capture a session."); + widgets::empty(ui, icon::RECORD, "No recordings yet", "Press Ctrl/Cmd+Shift+R in a terminal, or use Session → Toggle recording."); } return; } diff --git a/rust/app/src/settings.rs b/rust/app/src/settings.rs index e65bf00..ceba6ef 100644 --- a/rust/app/src/settings.rs +++ b/rust/app/src/settings.rs @@ -692,7 +692,7 @@ If your server requires a time-based one-time password (TOTP) during login, stor ## Jump Hosts (Bastions) If you need to reach a server through an intermediate host, add one or more jump hosts in the connection editor. Nexus Terminal tunnels through each hop automatically. ## Port Forwarding -You can set up **local**, **remote**, and **dynamic** (SOCKS proxy) port forwards in the connection editor. These start automatically when the session connects. +You can set up **local** and **remote** port forwards in the connection editor. These start automatically when the session connects. ## Proxy Support Open the **Proxy** section in the profile editor to connect through a **SOCKS5** or **HTTP CONNECT** proxy, with optional username/password authentication. The proxy is used to reach the SSH server (or the first jump host if jump hosts are configured). ## SFTP File Browser @@ -738,7 +738,7 @@ Enable **Auto-Reconnect** in the Advanced section to reconnect automatically whe - **Device** — pick the port from the list (refresh after plugging in an adapter), e.g. `/dev/ttyUSB0`, `/dev/cu.usbserial-…` or `COM3`. - **Line settings** — baud rate (default 115200), data bits, parity, stop bits and flow control (none, XON/XOFF, RTS/CTS). - **Behaviour** — line ending, local echo, encoding, and whether DTR/RTS are raised on connect. -- **Tab controls** — send a break and toggle DTR/RTS from the tab. +- **Tab controls** — right-click the tab to send a break or toggle DTR/RTS. - **Auto-reconnect** — unplugging the adapter disconnects the session; it reopens the port when it comes back. On Linux your user usually needs to be in the `dialout` (or `uucp`) group. On macOS use the `/dev/cu.*` device. A \"busy\" error means another program has the port open.", diff --git a/rust/app/src/state.rs b/rust/app/src/state.rs index b218276..ca153e7 100644 --- a/rust/app/src/state.rs +++ b/rust/app/src/state.rs @@ -67,6 +67,8 @@ pub struct Pane { pub connected_since: Option>, /// Bumped to make trigger highlights flash. pub highlight_until: f64, + /// Serial DTR / RTS as last set (starts from the profile's on-connect settings). + pub lines: (bool, bool), } /// A tab. Its session is `id`; `split` lays out extra panes (which have no tab of their own). diff --git a/rust/app/src/tabs.rs b/rust/app/src/tabs.rs index 2269960..6bfea48 100644 --- a/rust/app/src/tabs.rs +++ b/rust/app/src/tabs.rs @@ -95,7 +95,7 @@ fn strip(app: &mut App, ui: &mut Ui) { for id in &order { let Some(p) = app.s.pane(id) else { continue }; let p_info = p.info.clone(); - let (p_recording, p_since) = (p.recording, p.connected_since); + let (p_recording, p_since, lines) = (p.recording, p.connected_since, p.lines); let p = PaneInfo { info: p_info, recording: p_recording, connected_since: p_since }; let tab_activity = app.s.tab(id).is_some_and(|tb| tb.activity); let active = app.s.active.as_deref() == Some(id.as_str()); @@ -203,6 +203,24 @@ fn strip(app: &mut App, ui: &mut Ui) { ui.close(); } } + if matches!(p.info.protocol, Protocol::Telnet | Protocol::Serial) { + ui.separator(); + if ui.button(format!("{} Send Break", icon::LIGHTNING)).clicked() { + menu_action = Some((id.clone(), "break")); + ui.close(); + } + } + if p.info.protocol == Protocol::Serial { + let (d, r) = lines; + if ui.button(format!("{} DTR", if d { icon::CHECK_SQUARE } else { icon::SQUARE })).clicked() { + menu_action = Some((id.clone(), "dtr")); + ui.close(); + } + if ui.button(format!("{} RTS", if r { icon::CHECK_SQUARE } else { icon::SQUARE })).clicked() { + menu_action = Some((id.clone(), "rts")); + ui.close(); + } + } }); } if !ui.input(|i| i.pointer.primary_down()) { @@ -284,6 +302,9 @@ fn tab_action(app: &mut App, id: &str, a: &str) { } } "reconnect" => app.reconnect(id), + "break" => app.send_break(id), + "dtr" => app.toggle_line(id, true), + "rts" => app.toggle_line(id, false), "close" => app.request_close(id), _ => {} } diff --git a/rust/core/src/config.rs b/rust/core/src/config.rs index 59b3525..af90e2b 100644 --- a/rust/core/src/config.rs +++ b/rust/core/src/config.rs @@ -11,7 +11,6 @@ pub struct AppConfig { pub db_path: PathBuf, pub vault_dir: PathBuf, pub recordings_dir: PathBuf, - pub log_path: PathBuf, /// Set when this run copied data across from Alchementary Link. pub migrated_from: Option, } @@ -35,7 +34,6 @@ impl AppConfig { db_path: data_dir.join("nexus.db"), vault_dir: data_dir.join("vault"), recordings_dir: data_dir.join("recordings"), - log_path: data_dir.join("nexus.log"), data_dir, migrated_from: None, }