Serial/Telnet Send Break and DTR/RTS tab controls; fix Help and hints; drop unused log path; docs

This commit is contained in:
Andy Dixon
2026-10-11 17:54:11 +01:00
parent 8d798124e9
commit 7b92b81ecd
9 changed files with 59 additions and 10 deletions
+3 -1
View File
@@ -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. 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. 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 ### 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)**. - **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. - **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**. 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**.
+25 -1
View File
@@ -244,7 +244,8 @@ impl App {
}; };
let profile = self.s.svc.sessions.profile(id); let profile = self.s.svc.sessions.profile(id);
let view = self.view_for(&info, profile.as_ref()); 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 { if tab {
self.s.tabs.push(Tab { id: id.into(), activity: false, split: None }); self.s.tabs.push(Tab { id: id.into(), activity: false, split: None });
self.s.active = Some(id.into()); 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) { pub fn toggle_broadcast(&mut self) {
self.s.broadcast = !self.s.broadcast; self.s.broadcast = !self.s.broadcast;
if !self.s.broadcast { if !self.s.broadcast {
+1 -1
View File
@@ -77,7 +77,7 @@ pub fn title_bar(app: &mut App, ui: &mut Ui) {
app.s.request(Request::ExportEncrypted(vec![])); app.s.request(Request::ExportEncrypted(vec![]));
} }
if item(ui, icon::FILE_ARROW_DOWN, "Import ~/.ssh/config", "") { 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) => { Ok(n) => {
app.s.success(format!("Imported {n} host(s) from ~/.ssh/config")); app.s.success(format!("Imported {n} host(s) from ~/.ssh/config"));
app.s.reload_profiles(); app.s.reload_profiles();
+3 -1
View File
@@ -56,7 +56,9 @@ fn main() -> eframe::Result {
return Ok(()); 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(()); return Ok(());
} }
} }
+1 -1
View File
@@ -223,7 +223,7 @@ impl Recordings {
ui.separator(); ui.separator();
if self.list.is_empty() { if self.list.is_empty() {
if !self.loading { 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; return;
} }
+2 -2
View File
@@ -692,7 +692,7 @@ If your server requires a time-based one-time password (TOTP) during login, stor
## Jump Hosts (Bastions) ## 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. 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 ## 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 ## 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). 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 ## 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`. - **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). - **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. - **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. - **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.", 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.",
+2
View File
@@ -67,6 +67,8 @@ pub struct Pane {
pub connected_since: Option<chrono::DateTime<chrono::Local>>, pub connected_since: Option<chrono::DateTime<chrono::Local>>,
/// Bumped to make trigger highlights flash. /// Bumped to make trigger highlights flash.
pub highlight_until: f64, 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). /// A tab. Its session is `id`; `split` lays out extra panes (which have no tab of their own).
+22 -1
View File
@@ -95,7 +95,7 @@ fn strip(app: &mut App, ui: &mut Ui) {
for id in &order { for id in &order {
let Some(p) = app.s.pane(id) else { continue }; let Some(p) = app.s.pane(id) else { continue };
let p_info = p.info.clone(); 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 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 tab_activity = app.s.tab(id).is_some_and(|tb| tb.activity);
let active = app.s.active.as_deref() == Some(id.as_str()); 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(); 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()) { 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), "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), "close" => app.request_close(id),
_ => {} _ => {}
} }
-2
View File
@@ -11,7 +11,6 @@ pub struct AppConfig {
pub db_path: PathBuf, pub db_path: PathBuf,
pub vault_dir: PathBuf, pub vault_dir: PathBuf,
pub recordings_dir: PathBuf, pub recordings_dir: PathBuf,
pub log_path: PathBuf,
/// Set when this run copied data across from Alchementary Link. /// Set when this run copied data across from Alchementary Link.
pub migrated_from: Option<PathBuf>, pub migrated_from: Option<PathBuf>,
} }
@@ -35,7 +34,6 @@ impl AppConfig {
db_path: data_dir.join("nexus.db"), db_path: data_dir.join("nexus.db"),
vault_dir: data_dir.join("vault"), vault_dir: data_dir.join("vault"),
recordings_dir: data_dir.join("recordings"), recordings_dir: data_dir.join("recordings"),
log_path: data_dir.join("nexus.log"),
data_dir, data_dir,
migrated_from: None, migrated_from: None,
} }