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
+25 -1
View File
@@ -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 {
+1 -1
View File
@@ -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();
+3 -1
View File
@@ -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(());
}
}
+1 -1
View File
@@ -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;
}
+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)
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.",
+2
View File
@@ -67,6 +67,8 @@ pub struct Pane {
pub connected_since: Option<chrono::DateTime<chrono::Local>>,
/// 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).
+22 -1
View File
@@ -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),
_ => {}
}
-2
View File
@@ -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<PathBuf>,
}
@@ -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,
}