455 lines
18 KiB
Plaintext
455 lines
18 KiB
Plaintext
import { Theme } from "theme.slint";
|
|
import { LabeledInput, PrimaryButton, GhostButton } from "widgets.slint";
|
|
|
|
export struct SessionDraft {
|
|
id: string,
|
|
name: string,
|
|
group: string,
|
|
kind: string, // "ssh" | "serial" | "telnet"
|
|
host: string,
|
|
port: int,
|
|
user: string,
|
|
auth: string, // "password" | "key"
|
|
password: string,
|
|
private-key-path: string,
|
|
proxy: string, // "" | "socks5://host:port" | "http://host:port"
|
|
// Serial-only (ignored unless kind == "serial")
|
|
serial-port: string,
|
|
baud-rate: int,
|
|
data-bits: int,
|
|
stop-bits: int,
|
|
parity: string, // "none" | "odd" | "even"
|
|
flow-control: string, // "none" | "hardware" | "software"
|
|
}
|
|
|
|
// A single segment in a segmented selector (auth toggle, baud presets, …).
|
|
component SegOption inherits Rectangle {
|
|
in property <string> label;
|
|
in property <bool> active;
|
|
callback clicked();
|
|
height: 30px;
|
|
horizontal-stretch: 1;
|
|
border-radius: Theme.radius-sm;
|
|
border-width: 1px;
|
|
background: active ? Theme.accent : Theme.bg-root;
|
|
border-color: active ? Theme.accent : Theme.border-subtle;
|
|
Text {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
text: root.label;
|
|
color: active ? white : Theme.text-primary;
|
|
font-size: Theme.fs-md;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => { root.clicked(); }
|
|
}
|
|
}
|
|
|
|
component FolderOption inherits Rectangle {
|
|
in property <string> label;
|
|
in property <bool> active;
|
|
callback clicked();
|
|
|
|
height: 30px;
|
|
border-radius: Theme.radius-sm;
|
|
border-width: 1px;
|
|
border-color: active ? Theme.accent : Theme.border-subtle;
|
|
background: active ? Theme.bg-active : Theme.bg-root;
|
|
|
|
HorizontalLayout {
|
|
height: parent.height;
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
spacing: 8px;
|
|
Text {
|
|
width: 18px;
|
|
height: parent.height;
|
|
text: "\u{E2C7}";
|
|
font-family: "Material Icons";
|
|
color: active ? Theme.accent : Theme.text-muted;
|
|
font-size: Theme.fs-sm;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
Text {
|
|
height: parent.height;
|
|
text: root.label;
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-md;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
overflow: elide;
|
|
}
|
|
if active : Text {
|
|
width: 18px;
|
|
height: parent.height;
|
|
text: "\u{E5CA}";
|
|
font-family: "Material Icons";
|
|
color: Theme.accent;
|
|
font-size: Theme.fs-sm;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => { root.clicked(); }
|
|
}
|
|
}
|
|
|
|
// Modal dialog for creating / editing a session (SSH / Serial / Telnet).
|
|
// The dialog is purely presentational — it stores values in mutable
|
|
// properties and emits `submit` / `cancel` callbacks up to the main window.
|
|
export component SessionDialog inherits Rectangle {
|
|
in-out property <bool> is-open: false;
|
|
in-out property <bool> is-editing: false;
|
|
in-out property <string> draft-id;
|
|
in-out property <string> draft-name;
|
|
in-out property <string> draft-group: "Default";
|
|
in-out property <string> draft-kind: "ssh";
|
|
in-out property <string> draft-host;
|
|
in-out property <string> draft-port: "22";
|
|
in-out property <string> draft-user: "root";
|
|
in-out property <string> draft-auth: "password";
|
|
in-out property <string> draft-password;
|
|
in-out property <string> draft-key-path;
|
|
in-out property <string> draft-proxy;
|
|
in-out property <string> draft-serial-port;
|
|
in-out property <string> draft-baud: "115200";
|
|
in-out property <string> draft-data-bits: "8";
|
|
in-out property <string> draft-stop-bits: "1";
|
|
in-out property <string> draft-parity: "none";
|
|
in-out property <string> draft-flow: "none";
|
|
in property <[string]> folders;
|
|
|
|
callback submit(SessionDraft);
|
|
callback cancel();
|
|
callback pick-key-file(); // open a native file picker for the private key
|
|
|
|
visible: is-open;
|
|
background: #000000aa;
|
|
|
|
// Swallow clicks on the backdrop
|
|
TouchArea {
|
|
width: 100%; height: 100%;
|
|
clicked => { root.cancel(); }
|
|
}
|
|
|
|
Rectangle {
|
|
width: min(440px, parent.width - 48px);
|
|
height: min(620px, parent.height - 48px);
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
background: Theme.bg-panel;
|
|
border-radius: Theme.radius-lg;
|
|
border-width: 1px;
|
|
border-color: Theme.border-strong;
|
|
drop-shadow-blur: 24px;
|
|
drop-shadow-color: #00000080;
|
|
|
|
// Prevent backdrop click from propagating when user clicks inside the card
|
|
TouchArea {
|
|
width: 100%; height: 100%;
|
|
}
|
|
|
|
VerticalLayout {
|
|
padding: 20px;
|
|
spacing: 12px;
|
|
|
|
Text {
|
|
text: is-editing ? @tr("Edit session") : @tr("New session");
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-xl;
|
|
font-weight: 600;
|
|
}
|
|
|
|
Flickable {
|
|
vertical-stretch: 1;
|
|
viewport-width: self.width;
|
|
viewport-height: max(self.height, form-col.preferred-height);
|
|
|
|
form-col := VerticalLayout {
|
|
width: parent.width;
|
|
alignment: start;
|
|
spacing: 12px;
|
|
|
|
// Connection type selector (always shown).
|
|
VerticalLayout {
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("Connection type");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
}
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 6px;
|
|
SegOption {
|
|
label: "SSH";
|
|
active: root.draft-kind == "ssh";
|
|
clicked => { root.draft-kind = "ssh"; }
|
|
}
|
|
SegOption {
|
|
label: @tr("Serial");
|
|
active: root.draft-kind == "serial";
|
|
clicked => { root.draft-kind = "serial"; }
|
|
}
|
|
SegOption {
|
|
label: "Telnet";
|
|
active: root.draft-kind == "telnet";
|
|
clicked => { root.draft-kind = "telnet"; }
|
|
}
|
|
}
|
|
}
|
|
|
|
LabeledInput {
|
|
label: @tr("Name");
|
|
placeholder: @tr("e.g. production web-01");
|
|
value <=> root.draft-name;
|
|
}
|
|
|
|
VerticalLayout {
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("Folder");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
}
|
|
VerticalLayout {
|
|
spacing: 6px;
|
|
for folder in root.folders : FolderOption {
|
|
label: folder;
|
|
active: root.draft-group == folder;
|
|
clicked => { root.draft-group = folder; }
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── SSH / Telnet share host + port ──────────────────────────────
|
|
if root.draft-kind != "serial" : HorizontalLayout {
|
|
spacing: 10px;
|
|
LabeledInput {
|
|
label: @tr("Host / IP");
|
|
placeholder: "192.168.1.10";
|
|
value <=> root.draft-host;
|
|
horizontal-stretch: 2;
|
|
}
|
|
LabeledInput {
|
|
label: @tr("Port");
|
|
placeholder: root.draft-kind == "telnet" ? "23" : "22";
|
|
value <=> root.draft-port;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
|
|
// ── SSH-only: user + auth ───────────────────────────────────────
|
|
if root.draft-kind == "ssh" : LabeledInput {
|
|
label: @tr("Username");
|
|
placeholder: "root";
|
|
value <=> root.draft-user;
|
|
}
|
|
|
|
if root.draft-kind == "ssh" : VerticalLayout {
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("Authentication");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
}
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 6px;
|
|
SegOption {
|
|
label: @tr("Password");
|
|
active: root.draft-auth == "password";
|
|
clicked => { root.draft-auth = "password"; }
|
|
}
|
|
SegOption {
|
|
label: @tr("Private key");
|
|
active: root.draft-auth == "key";
|
|
clicked => { root.draft-auth = "key"; }
|
|
}
|
|
}
|
|
}
|
|
|
|
if root.draft-kind == "ssh" && root.draft-auth == "password" : LabeledInput {
|
|
label: @tr("Password");
|
|
placeholder: root.is-editing ? @tr("Leave blank to keep the current password") : "••••••••";
|
|
password: true;
|
|
value <=> root.draft-password;
|
|
}
|
|
|
|
if root.draft-kind == "ssh" && root.draft-auth == "key" : VerticalLayout {
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("Private key file");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
}
|
|
HorizontalLayout {
|
|
height: 32px;
|
|
spacing: 8px;
|
|
Rectangle {
|
|
height: 32px;
|
|
horizontal-stretch: 1;
|
|
border-radius: Theme.radius-sm;
|
|
border-width: 1px;
|
|
border-color: keyin.has-focus ? Theme.accent : Theme.border-subtle;
|
|
background: Theme.bg-root;
|
|
keyin := TextInput {
|
|
x: 10px; y: 0px;
|
|
width: parent.width - 20px;
|
|
height: parent.height;
|
|
text <=> root.draft-key-path;
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-md;
|
|
vertical-alignment: center;
|
|
single-line: true;
|
|
}
|
|
if root.draft-key-path == "" : Text {
|
|
x: 10px; y: 0px;
|
|
height: parent.height;
|
|
text: "/home/you/.ssh/id_ed25519";
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-md;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
Rectangle {
|
|
width: 64px; height: 32px;
|
|
border-radius: Theme.radius-sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
background: browse-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
|
|
Text {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
text: @tr("Browse…");
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-sm;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
browse-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => { root.pick-key-file(); }
|
|
}
|
|
}
|
|
}
|
|
Text {
|
|
text: @tr("Pick the private key itself (not the .pub public key)");
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-xs;
|
|
}
|
|
}
|
|
|
|
// ── Serial-only fields ──────────────────────────────────────────
|
|
if root.draft-kind == "serial" : LabeledInput {
|
|
label: @tr("Serial port");
|
|
placeholder: "COM3 / /dev/ttyUSB0";
|
|
value <=> root.draft-serial-port;
|
|
}
|
|
|
|
if root.draft-kind == "serial" : LabeledInput {
|
|
label: @tr("Baud rate");
|
|
placeholder: "115200";
|
|
value <=> root.draft-baud;
|
|
}
|
|
|
|
if root.draft-kind == "serial" : VerticalLayout {
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("Data bits / Stop bits / Parity");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
}
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 6px;
|
|
SegOption { label: "7"; active: root.draft-data-bits == "7"; clicked => { root.draft-data-bits = "7"; } }
|
|
SegOption { label: "8"; active: root.draft-data-bits == "8"; clicked => { root.draft-data-bits = "8"; } }
|
|
Rectangle { width: 8px; }
|
|
SegOption { label: "1"; active: root.draft-stop-bits == "1"; clicked => { root.draft-stop-bits = "1"; } }
|
|
SegOption { label: "2"; active: root.draft-stop-bits == "2"; clicked => { root.draft-stop-bits = "2"; } }
|
|
Rectangle { width: 8px; }
|
|
SegOption { label: "N"; active: root.draft-parity == "none"; clicked => { root.draft-parity = "none"; } }
|
|
SegOption { label: "O"; active: root.draft-parity == "odd"; clicked => { root.draft-parity = "odd"; } }
|
|
SegOption { label: "E"; active: root.draft-parity == "even"; clicked => { root.draft-parity = "even"; } }
|
|
}
|
|
}
|
|
|
|
if root.draft-kind == "serial" : VerticalLayout {
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("Flow control");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
}
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 6px;
|
|
SegOption { label: @tr("None"); active: root.draft-flow == "none"; clicked => { root.draft-flow = "none"; } }
|
|
SegOption { label: @tr("Hardware"); active: root.draft-flow == "hardware"; clicked => { root.draft-flow = "hardware"; } }
|
|
SegOption { label: @tr("Software"); active: root.draft-flow == "software"; clicked => { root.draft-flow = "software"; } }
|
|
}
|
|
}
|
|
|
|
// ── Optional outbound proxy (SSH / Telnet only) ─────────────────
|
|
if root.draft-kind != "serial" : VerticalLayout {
|
|
spacing: 4px;
|
|
LabeledInput {
|
|
label: @tr("Proxy (optional)");
|
|
placeholder: "socks5://127.0.0.1:1080";
|
|
value <=> root.draft-proxy;
|
|
}
|
|
Text {
|
|
text: @tr("Leave blank to use $ALL_PROXY or connect directly");
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-xs;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
alignment: end;
|
|
spacing: 8px;
|
|
GhostButton {
|
|
text: @tr("Cancel");
|
|
clicked => { root.cancel(); }
|
|
}
|
|
PrimaryButton {
|
|
text: is-editing ? @tr("Save") : @tr("Create");
|
|
clicked => {
|
|
root.submit({
|
|
id: root.draft-id,
|
|
name: root.draft-name,
|
|
group: root.draft-group,
|
|
kind: root.draft-kind,
|
|
host: root.draft-host,
|
|
port: root.draft-port.to-float(),
|
|
user: root.draft-user,
|
|
auth: root.draft-auth,
|
|
password: root.draft-password,
|
|
private-key-path: root.draft-key-path,
|
|
proxy: root.draft-proxy,
|
|
serial-port: root.draft-serial-port,
|
|
baud-rate: root.draft-baud.to-float(),
|
|
data-bits: root.draft-data-bits.to-float(),
|
|
stop-bits: root.draft-stop-bits.to-float(),
|
|
parity: root.draft-parity,
|
|
flow-control: root.draft-flow,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|