feat(telnet): enhance Telnet form with advanced configuration options (#22)

- Added new props to the TelnetForm component for advanced Telnet settings including raw TCP CLI, enter mode, local echo, force character-at-a-time, and NAWS/SGA options.
- Implemented UI elements for toggling these settings, improving user control over Telnet connections.
- Updated NewSessionPage to manage and persist these new settings for Telnet sessions.
- Enhanced type definitions to accommodate new configuration options for Telnet connections.
This commit is contained in:
Kang committed 2026-06-21 17:36:30 +08:00
1 parent 6946c3a98b
commit 0ae88ae43d
9 files changed
+628 -46

No files matched your search

+6
View File
@@ -224,6 +224,12 @@ mod tests {
port: 23,
ai_execution_profile: AiExecutionProfile::Auto,
backspace_mode: "del".to_string(),
raw_tcp_cli: false,
enter_mode: "cr".to_string(),
local_echo: false,
force_character_at_a_time: false,
send_naws: true,
send_sga: true,
},
group_id: None,
description: None,
+26 -12
View File
@@ -145,15 +145,32 @@ pub async fn create_telnet_session(
Some((guard, cancel_rx)) => (Some(guard), Some(cancel_rx)),
None => (None, None),
};
let (h, p, n, bs_mode) = if let Some(ref cid) = connection_id {
let cfg = if let Some(ref cid) = connection_id {
let conn = config::load_connection_by_id(&app, cid)?;
match conn.config {
config::ConnectionType::Telnet {
host: ref ch,
port: cp,
backspace_mode,
raw_tcp_cli,
enter_mode,
local_echo,
force_character_at_a_time,
send_naws,
send_sga,
..
} => (ch.clone(), cp, conn.name.clone(), backspace_mode),
} => core::TelnetSessionConfig {
host: ch.clone(),
port: cp,
name: conn.name.clone(),
backspace_mode,
raw_tcp_cli,
enter_mode: core::TelnetEnterMode::from_config_value(&enter_mode),
local_echo,
force_character_at_a_time,
send_naws,
send_sga,
},
_ => {
return Err(AppError::Config(
"Connection is not a Telnet connection".to_string(),
@@ -161,22 +178,19 @@ pub async fn create_telnet_session(
}
}
} else {
(
host.ok_or_else(|| AppError::Config("host is required".to_string()))?,
port.unwrap_or(23),
name.unwrap_or_else(|| "Telnet".to_string()),
"del".to_string(),
)
core::TelnetSessionConfig {
host: host.ok_or_else(|| AppError::Config("host is required".to_string()))?,
port: port.unwrap_or(23),
name: name.unwrap_or_else(|| "Telnet".to_string()),
..Default::default()
}
};
let marked_connection_id = connection_id.clone();
let session_id = core::create_telnet_session(
app.clone(),
state.inner().clone(),
h,
p,
cfg,
connection_id,
n,
bs_mode,
Some(window.label().to_string()),
)
.await?;
+97
View File
@@ -55,6 +55,21 @@ pub enum ConnectionType {
ai_execution_profile: AiExecutionProfile,
#[serde(default = "default_backspace_mode_telnet")]
backspace_mode: String,
#[serde(default, skip_serializing_if = "is_false")]
raw_tcp_cli: bool,
#[serde(
default = "default_telnet_enter_mode",
skip_serializing_if = "is_default_telnet_enter_mode"
)]
enter_mode: String,
#[serde(default, skip_serializing_if = "is_false")]
local_echo: bool,
#[serde(default, skip_serializing_if = "is_false")]
force_character_at_a_time: bool,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
send_naws: bool,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
send_sga: bool,
},
Serial {
port_name: String,
@@ -103,9 +118,15 @@ fn default_backspace_mode_serial() -> String {
fn default_backspace_mode_telnet() -> String {
"del".to_string()
}
fn default_telnet_enter_mode() -> String {
"cr".to_string()
}
fn is_ai_execution_profile_auto(value: &AiExecutionProfile) -> bool {
*value == AiExecutionProfile::Auto
}
fn default_true() -> bool {
true
}
// ── Auth block ──────────────────────────────────────────────────────────────
@@ -136,6 +157,12 @@ fn default_auth_mode() -> String {
fn is_false(value: &bool) -> bool {
!*value
}
fn is_true(value: &bool) -> bool {
*value
}
fn is_default_telnet_enter_mode(value: &str) -> bool {
value == "cr"
}
// ── Network block ───────────────────────────────────────────────────────────
@@ -418,6 +445,76 @@ mod tests {
assert!(!x11_forwarding);
}
#[test]
fn telnet_connection_defaults_compatibility_options() {
let connection: SavedConnection = serde_json::from_value(serde_json::json!({
"id": "conn-1",
"name": "Test",
"type": "telnet",
"host": "example.com",
"port": 23
}))
.expect("connection");
let ConnectionType::Telnet {
raw_tcp_cli,
enter_mode,
local_echo,
force_character_at_a_time,
send_naws,
send_sga,
..
} = connection.config
else {
panic!("expected telnet connection");
};
assert!(!raw_tcp_cli);
assert_eq!(enter_mode, "cr");
assert!(!local_echo);
assert!(!force_character_at_a_time);
assert!(send_naws);
assert!(send_sga);
}
#[test]
fn telnet_connection_preserves_compatibility_options() {
let connection: SavedConnection = serde_json::from_value(serde_json::json!({
"id": "conn-1",
"name": "Test",
"type": "telnet",
"host": "example.com",
"port": 8080,
"raw_tcp_cli": true,
"enter_mode": "lf",
"local_echo": true,
"force_character_at_a_time": true,
"send_naws": false,
"send_sga": false
}))
.expect("connection");
let ConnectionType::Telnet {
raw_tcp_cli,
enter_mode,
local_echo,
force_character_at_a_time,
send_naws,
send_sga,
..
} = connection.config
else {
panic!("expected telnet connection");
};
assert!(raw_tcp_cli);
assert_eq!(enter_mode, "lf");
assert!(local_echo);
assert!(force_character_at_a_time);
assert!(!send_naws);
assert!(!send_sga);
}
#[test]
fn post_login_defaults_delay_when_omitted() {
let connection: SavedConnection = serde_json::from_value(serde_json::json!({
+6
View File
@@ -708,6 +708,12 @@ fn prepare_nyaterm_json_session(
port,
ai_execution_profile: AiExecutionProfile::Auto,
backspace_mode,
raw_tcp_cli: false,
enter_mode: "cr".to_string(),
local_echo: false,
force_character_at_a_time: false,
send_naws: true,
send_sga: true,
},
group_path: normalize_optional_group_path(group_path, &context)?,
description: normalize_optional_string(description),
+1 -1
View File
@@ -37,4 +37,4 @@ pub(crate) use session::update_cwd_if_changed;
pub use session::{
SessionCommand, SessionHandle, SessionInfo, SessionManager, SessionType, SharedCwd,
};
pub use telnet::create_telnet_session;
pub use telnet::{TelnetEnterMode, TelnetSessionConfig, create_telnet_session};
+305 -33
View File
@@ -31,18 +31,72 @@ const OPT_ECHO: u8 = 1;
const OPT_SUPPRESS_GO_AHEAD: u8 = 3;
const OPT_NAWS: u8 = 31;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TelnetEnterMode {
Crlf,
Cr,
Lf,
}
impl Default for TelnetEnterMode {
fn default() -> Self {
Self::Cr
}
}
impl TelnetEnterMode {
pub fn from_config_value(value: &str) -> Self {
match value {
"crlf" => Self::Crlf,
"lf" => Self::Lf,
_ => Self::Cr,
}
}
}
#[derive(Debug, Clone)]
pub struct TelnetSessionConfig {
pub host: String,
pub port: u16,
pub name: String,
pub backspace_mode: String,
pub raw_tcp_cli: bool,
pub enter_mode: TelnetEnterMode,
pub local_echo: bool,
pub force_character_at_a_time: bool,
pub send_naws: bool,
pub send_sga: bool,
}
impl Default for TelnetSessionConfig {
fn default() -> Self {
Self {
host: String::new(),
port: 23,
name: "Telnet".to_string(),
backspace_mode: "del".to_string(),
raw_tcp_cli: false,
enter_mode: TelnetEnterMode::Cr,
local_echo: false,
force_character_at_a_time: false,
send_naws: true,
send_sga: true,
}
}
}
/// Respond to a Telnet option negotiation request.
fn negotiate_response(command: u8, option: u8) -> Vec<u8> {
fn negotiate_response(command: u8, option: u8, send_naws: bool, send_sga: bool) -> Vec<u8> {
match command {
WILL => {
if option == OPT_ECHO || option == OPT_SUPPRESS_GO_AHEAD {
if option == OPT_ECHO || (send_sga && option == OPT_SUPPRESS_GO_AHEAD) {
vec![IAC, DO, option]
} else {
vec![IAC, DONT, option]
}
}
DO => {
if option == OPT_NAWS {
if send_naws && option == OPT_NAWS {
vec![IAC, WILL, option]
} else {
vec![IAC, WONT, option]
@@ -69,6 +123,29 @@ fn build_naws(cols: u16, rows: u16) -> Vec<u8> {
]
}
fn maybe_build_naws(cols: u16, rows: u16, config: &TelnetSessionConfig) -> Option<Vec<u8>> {
if config.raw_tcp_cli || !config.send_naws {
None
} else {
Some(build_naws(cols, rows))
}
}
fn unescape_iac_iac(data: &[u8]) -> Vec<u8> {
let mut visible = Vec::with_capacity(data.len());
let mut i = 0;
while i < data.len() {
if data[i] == IAC && i + 1 < data.len() && data[i + 1] == IAC {
visible.push(IAC);
i += 2;
} else {
visible.push(data[i]);
i += 1;
}
}
visible
}
/// Strip IAC sequences from raw data, returning only user-visible bytes.
/// Calls `on_negotiate` for each IAC command/option pair encountered.
fn strip_telnet_commands(data: &[u8], on_negotiate: &mut impl FnMut(u8, u8)) -> Vec<u8> {
@@ -113,16 +190,196 @@ fn strip_telnet_commands(data: &[u8], on_negotiate: &mut impl FnMut(u8, u8)) ->
visible
}
fn normalize_enter_bytes(data: &[u8], enter_mode: TelnetEnterMode) -> Vec<u8> {
let replacement: &[u8] = match enter_mode {
TelnetEnterMode::Crlf => b"\r\n",
TelnetEnterMode::Cr => b"\r",
TelnetEnterMode::Lf => b"\n",
};
let mut normalized = Vec::with_capacity(data.len());
for byte in data {
if *byte == b'\r' {
normalized.extend_from_slice(replacement);
} else {
normalized.push(*byte);
}
}
normalized
}
fn split_write_chunks(data: &[u8], force_character_at_a_time: bool) -> Vec<Vec<u8>> {
if !force_character_at_a_time {
return vec![data.to_vec()];
}
String::from_utf8_lossy(data)
.chars()
.map(|ch| {
let mut buf = [0u8; 4];
ch.encode_utf8(&mut buf).as_bytes().to_vec()
})
.collect()
}
fn local_echo_text(data: &[u8]) -> String {
let mut visible = Vec::with_capacity(data.len());
let mut i = 0;
while i < data.len() {
match data[i] {
0x1b => {
i += 1;
if i < data.len() && data[i] == b'[' {
i += 1;
while i < data.len() && !(0x40..=0x7e).contains(&data[i]) {
i += 1;
}
} else if i < data.len() && data[i] == b']' {
i += 1;
while i < data.len() {
if data[i] == 0x07 {
break;
}
if data[i] == 0x1b && i + 1 < data.len() && data[i + 1] == b'\\' {
i += 1;
break;
}
i += 1;
}
}
}
b'\r' => {
if i + 1 < data.len() && data[i + 1] == b'\n' {
i += 1;
}
visible.extend_from_slice(b"\r\n");
}
b'\n' => visible.extend_from_slice(b"\r\n"),
0x20..=0x7e | b'\t' => visible.push(data[i]),
byte if byte >= 0x80 => visible.push(byte),
_ => {}
}
i += 1;
}
String::from_utf8_lossy(&visible).to_string()
}
#[cfg(test)]
mod tests {
use super::{
DO, IAC, OPT_NAWS, OPT_SUPPRESS_GO_AHEAD, TelnetEnterMode, TelnetSessionConfig, WILL,
maybe_build_naws, negotiate_response, normalize_enter_bytes, split_write_chunks,
strip_telnet_commands,
};
#[test]
fn standard_negotiation_responds_by_default() {
assert_eq!(
negotiate_response(WILL, OPT_SUPPRESS_GO_AHEAD, true, true),
vec![IAC, DO, OPT_SUPPRESS_GO_AHEAD]
);
}
#[test]
fn send_sga_false_rejects_sga() {
assert_ne!(
negotiate_response(WILL, OPT_SUPPRESS_GO_AHEAD, true, false),
vec![IAC, DO, OPT_SUPPRESS_GO_AHEAD]
);
}
#[test]
fn send_naws_false_rejects_naws_negotiation() {
assert_ne!(
negotiate_response(DO, OPT_NAWS, false, true),
vec![IAC, WILL, OPT_NAWS]
);
}
#[test]
fn raw_mode_can_suppress_negotiation_responses() {
let config = TelnetSessionConfig {
raw_tcp_cli: true,
..Default::default()
};
let mut responses = Vec::new();
if !config.raw_tcp_cli {
let _ = strip_telnet_commands(&[IAC, WILL, OPT_SUPPRESS_GO_AHEAD], &mut |cmd, opt| {
responses.push(negotiate_response(
cmd,
opt,
config.send_naws,
config.send_sga,
));
});
}
assert!(responses.is_empty());
}
#[test]
fn send_naws_false_prevents_naws_resize_payload() {
let config = TelnetSessionConfig {
send_naws: false,
..Default::default()
};
assert!(maybe_build_naws(80, 24, &config).is_none());
}
#[test]
fn raw_mode_prevents_naws_resize_payload() {
let config = TelnetSessionConfig {
raw_tcp_cli: true,
..Default::default()
};
assert!(maybe_build_naws(80, 24, &config).is_none());
}
#[test]
fn enter_conversion_maps_carriage_return() {
assert_eq!(
normalize_enter_bytes(b"show\r", TelnetEnterMode::Crlf),
b"show\r\n"
);
assert_eq!(
normalize_enter_bytes(b"show\r", TelnetEnterMode::Cr),
b"show\r"
);
assert_eq!(
normalize_enter_bytes(b"show\r", TelnetEnterMode::Lf),
b"show\n"
);
}
#[test]
fn force_character_at_a_time_preserves_utf8_order() {
let chunks = split_write_chunks("a中\r".as_bytes(), true);
assert_eq!(
chunks,
vec![b"a".to_vec(), "中".as_bytes().to_vec(), b"\r".to_vec()]
);
let joined: Vec<u8> = chunks.into_iter().flatten().collect();
assert_eq!(joined, "a中\r".as_bytes());
}
#[test]
fn strip_telnet_commands_emits_naws_response_request() {
let mut seen = Vec::new();
let visible = strip_telnet_commands(b"hi\xff\xfd\x1f", &mut |cmd, opt| {
seen.push((cmd, opt));
});
assert_eq!(visible, b"hi");
assert_eq!(seen, vec![(DO, OPT_NAWS)]);
}
}
pub async fn create_telnet_session(
app: AppHandle,
manager: Arc<SessionManager>,
host: String,
port: u16,
config: TelnetSessionConfig,
connection_id: Option<String>,
name: String,
backspace_mode: String,
owner_window_label: Option<String>,
) -> AppResult<String> {
let host = config.host.clone();
let port = config.port;
log_event(StructuredLog {
level: StructuredLogLevel::Info,
domain: "session.lifecycle".to_string(),
@@ -144,7 +401,7 @@ pub async fn create_telnet_session(
let session_info = SessionInfo {
id: session_id.clone(),
name,
name: config.name.clone(),
session_type: SessionType::Telnet,
connected: true,
owner_window_label,
@@ -167,17 +424,7 @@ pub async fn create_telnet_session(
let mgr = manager.clone();
tokio::spawn(async move {
telnet_session_task(
app,
sid,
mgr,
cmd_rx,
host,
port,
connection_id,
backspace_mode,
)
.await;
telnet_session_task(app, sid, mgr, cmd_rx, config, connection_id).await;
});
Ok(session_id)
@@ -188,12 +435,12 @@ async fn telnet_session_task(
session_id: String,
manager: Arc<SessionManager>,
mut cmd_rx: mpsc::UnboundedReceiver<SessionCommand>,
host: String,
port: u16,
config: TelnetSessionConfig,
connection_id: Option<String>,
backspace_mode: String,
) {
let backspace_as_bs = backspace_mode == "ctrl_h";
let backspace_as_bs = config.backspace_mode == "ctrl_h";
let host = config.host.clone();
let port = config.port;
let addr = format!("{}:{}", host, port);
let stream = match TcpStream::connect(&addr).await {
Ok(s) => s,
@@ -252,6 +499,7 @@ async fn telnet_session_task(
let (negotiate_tx, mut negotiate_rx) = mpsc::unbounded_channel::<Vec<u8>>();
let reader_config = config.clone();
let reader_handle = tokio::spawn(async move {
let mut buf = [0u8; 4096];
let mut zmodem_detector = ZmodemDetector::new();
@@ -264,13 +512,22 @@ async fn telnet_session_task(
match reader.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
let neg_tx = negotiate_tx.clone();
let visible = strip_telnet_commands(&buf[..n], &mut |cmd, opt| {
let resp = negotiate_response(cmd, opt);
if !resp.is_empty() {
let _ = neg_tx.send(resp);
}
});
let visible = if reader_config.raw_tcp_cli {
unescape_iac_iac(&buf[..n])
} else {
let neg_tx = negotiate_tx.clone();
strip_telnet_commands(&buf[..n], &mut |cmd, opt| {
let resp = negotiate_response(
cmd,
opt,
reader_config.send_naws,
reader_config.send_sga,
);
if !resp.is_empty() {
let _ = neg_tx.send(resp);
}
})
};
if visible.is_empty() {
continue;
}
@@ -418,10 +675,24 @@ async fn telnet_session_task(
if backspace_as_bs {
remap_del_to_bs(&mut data);
}
let data = normalize_enter_bytes(&data, config.enter_mode);
if config.local_echo {
let echoed = local_echo_text(&data);
if !echoed.is_empty() {
output.push_owned(echoed);
}
}
if let Some(ref recorder) = recording_mgr {
recorder.write_input(&session_id, &data);
}
if let Err(e) = writer.write_all(&data).await {
let mut write_failed = None;
for chunk in split_write_chunks(&data, config.force_character_at_a_time) {
if let Err(e) = writer.write_all(&chunk).await {
write_failed = Some(e);
break;
}
}
if let Some(e) = write_failed {
log_rate_limited(StructuredLog {
level: StructuredLogLevel::Warn,
domain: "session.lifecycle".to_string(),
@@ -451,8 +722,9 @@ async fn telnet_session_task(
}
}
Some(SessionCommand::Resize { cols, rows }) => {
let naws = build_naws(cols as u16, rows as u16);
let _ = writer.write_all(&naws).await;
if let Some(naws) = maybe_build_naws(cols as u16, rows as u16, &config) {
let _ = writer.write_all(&naws).await;
}
}
Some(SessionCommand::PauseOutput) => {
let _ = pause_tx.send(true);
+139
View File
@@ -9,6 +9,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
type TelnetEnterMode = "crlf" | "cr" | "lf";
interface TelnetFormProps {
host: string;
@@ -17,6 +21,18 @@ interface TelnetFormProps {
setPort: (v: number) => void;
backspaceMode: string;
setBackspaceMode: (v: string) => void;
rawTcpCli: boolean;
setRawTcpCli: (v: boolean) => void;
enterMode: TelnetEnterMode;
setEnterMode: (v: TelnetEnterMode) => void;
localEcho: boolean;
setLocalEcho: (v: boolean) => void;
forceCharacterAtATime: boolean;
setForceCharacterAtATime: (v: boolean) => void;
sendNaws: boolean;
setSendNaws: (v: boolean) => void;
sendSga: boolean;
setSendSga: (v: boolean) => void;
}
function RequiredMark() {
@@ -30,8 +46,44 @@ export function TelnetForm({
setPort,
backspaceMode,
setBackspaceMode,
rawTcpCli,
setRawTcpCli,
enterMode,
setEnterMode,
localEcho,
setLocalEcho,
forceCharacterAtATime,
setForceCharacterAtATime,
sendNaws,
setSendNaws,
sendSga,
setSendSga,
}: TelnetFormProps) {
const { t } = useTranslation();
const renderSwitchRow = (
label: string,
description: string,
checked: boolean,
onCheckedChange: (checked: boolean) => void,
disabled = false,
) => (
<div className={cn("rounded-md border bg-background/70 px-3 py-2", disabled && "opacity-55")}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-0.5">
<div className="text-xs font-medium">{label}</div>
<p className="text-[0.6875rem] leading-relaxed text-muted-foreground">{description}</p>
</div>
<Switch
className="mt-0.5"
checked={checked}
onCheckedChange={onCheckedChange}
disabled={disabled}
/>
</div>
</div>
);
return (
<div className="space-y-3 w-full">
<div className="flex flex-col gap-3 sm:flex-row">
@@ -77,6 +129,93 @@ export function TelnetForm({
</Select>
</div>
</div>
<div className="rounded-lg border bg-accent/25 p-3">
<div className="space-y-0.5">
<div className="text-xs font-medium">{t("dialog.telnetAdvanced", "Telnet Advanced")}</div>
<p className="text-[0.6875rem] leading-relaxed text-muted-foreground">
{t(
"dialog.telnetRawTcpCliDesc",
"Suitable for non-standard Telnet, embedded debug ports, and device CLIs. When enabled, Telnet negotiation is disabled and input is passed through directly.",
)}
</p>
</div>
<div className="mt-3 grid gap-2">
{renderSwitchRow(
t("dialog.telnetRawTcpCli", "Embedded debug port / Raw TCP CLI"),
t(
"dialog.telnetRawTcpCliLongDesc",
"Disables Telnet negotiation and passes input directly through for TVs, routers, switches, and embedded debug ports.",
),
rawTcpCli,
(checked) => {
setRawTcpCli(checked);
if (checked) {
setEnterMode("cr");
}
},
)}
<div className="grid gap-3 sm:grid-cols-2">
<div>
<Label className="text-xs font-medium text-foreground/80">
{t("dialog.telnetEnterMode", "Enter sends")}
</Label>
<Select
value={enterMode}
onValueChange={(value) => setEnterMode(value as TelnetEnterMode)}
>
<SelectTrigger className="mt-1 h-8 text-xs font-normal">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="crlf">CRLF (\r\n)</SelectItem>
<SelectItem value="cr">CR (\r)</SelectItem>
<SelectItem value="lf">LF (\n)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-2 md:grid-cols-2">
{renderSwitchRow(
t("dialog.telnetLocalEcho", "Local Echo"),
t(
"dialog.telnetLocalEchoDesc",
"Show typed input locally when the device does not echo.",
),
localEcho,
setLocalEcho,
)}
{renderSwitchRow(
t("dialog.telnetForceCharAtATime", "Force character-at-a-time"),
t(
"dialog.telnetForceCharAtATimeDesc",
"Write each input character to the TCP stream immediately.",
),
forceCharacterAtATime,
setForceCharacterAtATime,
)}
{renderSwitchRow(
t("dialog.telnetSendNaws", "Send NAWS"),
t("dialog.telnetSendNawsDesc", "Send terminal size changes in standard Telnet mode."),
sendNaws,
setSendNaws,
rawTcpCli,
)}
{renderSwitchRow(
t("dialog.telnetSendSga", "Send SGA"),
t(
"dialog.telnetSendSgaDesc",
"Accept Suppress Go Ahead negotiation in standard Telnet mode.",
),
sendSga,
setSendSga,
rawTcpCli,
)}
</div>
</div>
</div>
</div>
);
}
+36
View File
@@ -109,6 +109,12 @@ export default function NewSessionPage() {
const [workingDir, setWorkingDir] = useState("");
const [serialBackspaceMode, setSerialBackspaceMode] = useState("ctrl_h");
const [telnetBackspaceMode, setTelnetBackspaceMode] = useState("del");
const [telnetRawTcpCli, setTelnetRawTcpCli] = useState(false);
const [telnetEnterMode, setTelnetEnterMode] = useState<"crlf" | "cr" | "lf">("cr");
const [telnetLocalEcho, setTelnetLocalEcho] = useState(false);
const [telnetForceCharacterAtATime, setTelnetForceCharacterAtATime] = useState(false);
const [telnetSendNaws, setTelnetSendNaws] = useState(true);
const [telnetSendSga, setTelnetSendSga] = useState(true);
useEffect(() => {
invoke<Group[]>("get_groups")
@@ -168,6 +174,12 @@ export default function NewSessionPage() {
setHost(found.host || "");
setTelnetPort(found.port || 23);
setTelnetBackspaceMode(found.backspace_mode || "del");
setTelnetRawTcpCli(found.raw_tcp_cli ?? false);
setTelnetEnterMode(found.enter_mode || "cr");
setTelnetLocalEcho(found.local_echo ?? false);
setTelnetForceCharacterAtATime(found.force_character_at_a_time ?? false);
setTelnetSendNaws(found.send_naws ?? true);
setTelnetSendSga(found.send_sga ?? true);
} else if (found.type === "local_terminal") {
setShellPath(found.shell_path || "powershell.exe");
setShellArgs(found.shell_args || "");
@@ -243,6 +255,12 @@ export default function NewSessionPage() {
setWorkingDir("");
setSerialBackspaceMode("ctrl_h");
setTelnetBackspaceMode("del");
setTelnetRawTcpCli(false);
setTelnetEnterMode("cr");
setTelnetLocalEcho(false);
setTelnetForceCharacterAtATime(false);
setTelnetSendNaws(true);
setTelnetSendSga(true);
setShowIconPicker(false);
setError("");
setConnecting(false);
@@ -518,6 +536,12 @@ export default function NewSessionPage() {
host: normalizedHost,
port: telnetPort,
backspace_mode: telnetBackspaceMode,
raw_tcp_cli: telnetRawTcpCli,
enter_mode: telnetEnterMode,
local_echo: telnetLocalEcho,
force_character_at_a_time: telnetForceCharacterAtATime,
send_naws: telnetSendNaws,
send_sga: telnetSendSga,
}
: {}),
...(currentTab === "local"
@@ -861,6 +885,18 @@ export default function NewSessionPage() {
setPort={setTelnetPort}
backspaceMode={telnetBackspaceMode}
setBackspaceMode={setTelnetBackspaceMode}
rawTcpCli={telnetRawTcpCli}
setRawTcpCli={setTelnetRawTcpCli}
enterMode={telnetEnterMode}
setEnterMode={setTelnetEnterMode}
localEcho={telnetLocalEcho}
setLocalEcho={setTelnetLocalEcho}
forceCharacterAtATime={telnetForceCharacterAtATime}
setForceCharacterAtATime={setTelnetForceCharacterAtATime}
sendNaws={telnetSendNaws}
setSendNaws={setTelnetSendNaws}
sendSga={telnetSendSga}
setSendSga={setTelnetSendSga}
/>
</TabsContent>
+12
View File
@@ -211,6 +211,18 @@ export interface SavedConnection {
stop_bits?: string;
/** Backspace key mode for SSH/Telnet/Serial connections ("ctrl_h" or "del"). */
backspace_mode?: string;
/** Telnet-only: bypass Telnet option negotiation for embedded/raw TCP CLIs. */
raw_tcp_cli?: boolean;
/** Telnet-only: Enter send mode ("crlf", "cr", or "lf"). */
enter_mode?: "crlf" | "cr" | "lf";
/** Telnet-only: locally echo typed input when the remote does not echo. */
local_echo?: boolean;
/** Telnet-only: write each input character to the socket immediately. */
force_character_at_a_time?: boolean;
/** Telnet-only: send NAWS resize subnegotiation in standard Telnet mode. */
send_naws?: boolean;
/** Telnet-only: accept/respond to SGA negotiation in standard Telnet mode. */
send_sga?: boolean;
/** SSH-only: enables X11 forwarding for remote graphical applications. */
x11_forwarding?: boolean;
}