All checks were successful
Release / windows-x86_64-gnu (push) Successful in 1h5m41s
1758 lines
77 KiB
Plaintext
1758 lines
77 KiB
Plaintext
// Embedded cross-platform fonts (compile-time): terminal monospace + UI icons.
|
|
// Embedding means Windows / Linux / macOS all render the same glyphs without
|
|
// relying on Consolas / Segoe MDL2 Assets being installed.
|
|
//
|
|
// Cascadia Mono: full box-drawing + block-elements + braille (needed by btop
|
|
// for its sparkline graphs; JetBrains Mono lacks braille → garbled output).
|
|
// Material Icons: open-source icon font replacing Windows-only Segoe MDL2 Assets.
|
|
import "fonts/CascadiaMono-Regular.ttf";
|
|
import "fonts/CascadiaMono-Bold.ttf";
|
|
import "fonts/MaterialIcons-Regular.ttf";
|
|
|
|
import { Theme } from "theme.slint";
|
|
import { Sidebar, DiskInfo, ProcessInfo } from "sidebar.slint";
|
|
import { TabBar, TabInfo } from "tabs.slint";
|
|
import { Welcome, SessionInfo, ConnectionFolderInfo } from "welcome.slint";
|
|
import { SessionDialog, SessionDraft } from "session_dialog.slint";
|
|
import { ConfirmDialog } from "confirm_dialog.slint";
|
|
import { TerminalView, TermSpan, TermMatch, CommandCategory, CommandItem } from "terminal_view.slint";
|
|
import { SftpEntry, SftpTreeNode } from "sftp_panel.slint";
|
|
|
|
export { SessionInfo, ConnectionFolderInfo, SessionDraft, TabInfo, SftpEntry, SftpTreeNode, CommandCategory, CommandItem, ProcessInfo }
|
|
|
|
// --- TerminalState ---------------------------------------------------------
|
|
// Per-terminal view-model exposed to Rust. A TabInfo has the metadata; the
|
|
// TerminalState carries the streaming log + status line. Kept as a parallel
|
|
// model so opening/closing tabs doesn't churn the log strings.
|
|
// One file-transfer record shown in the download manager.
|
|
export struct TransferInfo {
|
|
id: string,
|
|
name: string,
|
|
detail: string, // "2.3 MB/5.0 MB" | "已完成" | "失败"
|
|
percent: float, // 0.0..1.0
|
|
state: int, // 0 active / 1 done / 2 error
|
|
is-upload: bool,
|
|
}
|
|
|
|
export struct TerminalState {
|
|
id: string,
|
|
tab-id: string,
|
|
status: string,
|
|
spans: [TermSpan], // coloured runs of the visible terminal screen
|
|
cursor-row: int, // cursor cell on the grid
|
|
cursor-col: int,
|
|
rows-used: int, // content rows, for viewport sizing
|
|
is-alt-screen: bool,
|
|
mouse-tracking: bool,
|
|
find-matches: [TermMatch], // search-highlight rectangles
|
|
selection: [TermMatch], // drag-selection highlight rectangles
|
|
|
|
// SFTP panel state
|
|
sftp-path: string,
|
|
sftp-entries: [SftpEntry],
|
|
sftp-status: string,
|
|
sftp-loading: bool,
|
|
sftp-tree-nodes: [SftpTreeNode],
|
|
}
|
|
|
|
component ConfigButton inherits Rectangle {
|
|
in property <string> label;
|
|
in property <bool> primary: false;
|
|
in property <bool> selected: false;
|
|
in property <length> button-width: 84px;
|
|
callback clicked();
|
|
|
|
width: button-width;
|
|
height: 30px;
|
|
border-radius: Theme.radius-sm;
|
|
border-width: primary ? 0px : 1px;
|
|
border-color: selected ? Theme.accent : (touch.has-hover ? Theme.border-strong : Theme.border-subtle);
|
|
background: primary
|
|
? (touch.has-hover ? Theme.accent-hover : Theme.accent)
|
|
: selected
|
|
? Theme.bg-active
|
|
: (touch.has-hover ? Theme.bg-hover : Theme.bg-elevated);
|
|
|
|
touch := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => { root.clicked(); }
|
|
}
|
|
|
|
Text {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
text: root.label;
|
|
color: root.primary ? #ffffff : Theme.text-primary;
|
|
font-size: Theme.fs-sm;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
|
|
component ConfigTextField inherits Rectangle {
|
|
in-out property <string> value;
|
|
in property <length> field-width: 86px;
|
|
|
|
width: field-width;
|
|
height: 30px;
|
|
border-radius: Theme.radius-sm;
|
|
border-width: 1px;
|
|
border-color: input.has-focus ? Theme.accent : Theme.border-subtle;
|
|
background: Theme.bg-root;
|
|
|
|
input := TextInput {
|
|
x: 8px;
|
|
width: parent.width - 16px;
|
|
height: parent.height;
|
|
text <=> root.value;
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-sm;
|
|
single-line: true;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
component ConfigValueBox inherits Rectangle {
|
|
in property <string> value;
|
|
in property <string> empty-text;
|
|
|
|
height: 30px;
|
|
border-radius: Theme.radius-sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
background: Theme.bg-root;
|
|
|
|
Text {
|
|
x: 10px;
|
|
width: parent.width - 20px;
|
|
height: parent.height;
|
|
text: root.value == "" ? root.empty-text : root.value;
|
|
color: root.value == "" ? Theme.text-muted : Theme.text-primary;
|
|
font-size: Theme.fs-sm;
|
|
overflow: elide;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
export component AppWindow inherits Window {
|
|
title: "meatshell";
|
|
icon: @image-url("../assets/icon.png");
|
|
default-font-family: Theme.font-ui;
|
|
min-width: 960px;
|
|
min-height: 600px;
|
|
preferred-width: 1200px;
|
|
preferred-height: 760px;
|
|
background: Theme.bg-root;
|
|
|
|
// --- Sidebar data ------------------------------------------------------
|
|
in property <string> connection-state: @tr("Not connected");
|
|
in property <int> conn-state; // 0 gray / 1 green / 2 yellow
|
|
in property <string> resource-title: @tr("Local resources"); // @tr("Local resources") | @tr("Server resources")
|
|
in property <float> cpu-percent;
|
|
in property <float> mem-percent;
|
|
in property <float> swap-percent;
|
|
in property <string> cpu-detail;
|
|
in property <string> mem-detail;
|
|
in property <string> swap-detail;
|
|
// Network panel follows the active live session tab.
|
|
in property <string> net-top-up; // ↑ tx rate text
|
|
in property <string> net-top-down; // ↓ rx rate text
|
|
in property <[float]> net-top-history;
|
|
in property <[string]> net-ifaces; // remote NIC names for the selector
|
|
in property <string> net-selected; // currently selected NIC
|
|
in property <bool> net-show-selector; // only on a live session tab
|
|
in property <[DiskInfo]> disks; // active tab's filesystem usage
|
|
in property <[ProcessInfo]> processes; // top resource consumers
|
|
callback select-net-iface(string);
|
|
|
|
// --- Top-right popups --------------------------------------------------
|
|
in-out property <bool> download-open: false; // download manager popup
|
|
in-out property <bool> settings-open: false; // settings menu popup
|
|
in-out property <bool> config-open: false; // app configuration dialog
|
|
in-out property <bool> about-open: false; // centered About dialog
|
|
in property <string> download-dir; // preset SFTP download folder ("" = ask)
|
|
in property <[TransferInfo]> transfers; // download/upload records (newest first)
|
|
in property <[CommandCategory]> command-categories;
|
|
in property <[CommandItem]> command-items;
|
|
in-out property <string> command-selected-category: "default";
|
|
in-out property <string> command-selected-id;
|
|
in-out property <string> command-category-draft;
|
|
in-out property <string> command-edit-id;
|
|
in-out property <string> command-edit-name;
|
|
in-out property <string> command-edit-command;
|
|
in-out property <bool> command-edit-append-cr: true;
|
|
in-out property <string> command-param1-name;
|
|
in-out property <string> command-param1-value;
|
|
in-out property <string> command-param2-name;
|
|
in-out property <string> command-param2-value;
|
|
in-out property <string> command-param3-name;
|
|
in-out property <string> command-param3-value;
|
|
in-out property <string> command-param4-name;
|
|
in-out property <string> command-param4-value;
|
|
in-out property <string> command-param5-name;
|
|
in-out property <string> command-param5-value;
|
|
in property <[string]> about-libs; // open-source libraries used
|
|
in-out property <bool> lang-en: false; // current UI language (false = 中文)
|
|
// Two-way binding to Theme.dark — Rust reads/writes this property and
|
|
// Slint propagates it to every widget that references Theme colours.
|
|
in-out property <bool> dark-mode <=> Theme.dark;
|
|
callback toggle-theme(); // flip dark ↔ light
|
|
in-out property <length> sftp-panel-height: max(240px, root.height * 0.4); // default: files 2/5, terminal 3/5
|
|
in-out property <length> terminal-font-size: 13px;
|
|
in-out property <length> sftp-file-font-size: 16px;
|
|
in-out property <length> sftp-name-column-width: 180px;
|
|
in-out property <bool> sftp-name-width-manual: false;
|
|
callback sftp-name-column-width-changed(float);
|
|
callback sftp-name-column-auto();
|
|
callback pick-download-dir(); // open a folder picker
|
|
callback open-download-dir(); // reveal the folder in the OS
|
|
callback clear-transfers(); // wipe the transfer history
|
|
callback set-language(string); // switch UI language ("zh" / "en")
|
|
in-out property <string> max-tabs-text: "10";
|
|
in-out property <string> terminal-font-size-text: "13";
|
|
in-out property <string> sftp-font-size-text: "16";
|
|
in-out property <string> background-path-text;
|
|
in-out property <int> background-mode: 1; // 0 none, 1 terminal, 2 fullscreen
|
|
in-out property <string> background-blur-text: "3";
|
|
in-out property <string> terminal-opacity-text: "88";
|
|
in-out property <string> ui-opacity-text: "72";
|
|
in-out property <float> terminal-background-opacity: 0.88;
|
|
in-out property <float> ui-background-opacity: 0.72;
|
|
in-out property <bool> ui-debug-ids: false;
|
|
in property <image> background-image;
|
|
in-out property <string> config-dir-text;
|
|
// WebDAV sync settings
|
|
in-out property <bool> webdav-enabled: false;
|
|
in-out property <string> webdav-url-text;
|
|
in-out property <string> webdav-username-text;
|
|
in-out property <string> webdav-password-text;
|
|
in-out property <string> webdav-status-text;
|
|
in-out property <string> webdav-auto-sync-interval: "0";
|
|
callback webdav-test-connection();
|
|
callback webdav-push();
|
|
callback webdav-pull();
|
|
callback webdav-save-settings(bool, string, string, string, string);
|
|
callback app-config-save(string, string, string, int, string, string, string, bool, string);
|
|
callback app-config-pick-dir();
|
|
callback app-background-pick();
|
|
callback app-background-clear();
|
|
callback debug-click(string, string);
|
|
property <bool> fullscreen-background-active: root.background-mode == 2 && root.background-path-text != "";
|
|
|
|
// --- Session list ------------------------------------------------------
|
|
in property <[SessionInfo]> sessions;
|
|
in property <[ConnectionFolderInfo]> connection-folders;
|
|
in property <[string]> connection-folder-names;
|
|
|
|
// --- Tabs --------------------------------------------------------------
|
|
in property <[TabInfo]> tabs;
|
|
in-out property <string> active-tab-id: "welcome";
|
|
in-out property <int> tab-visible-start: 0;
|
|
in property <[TerminalState]> terminals;
|
|
|
|
// --- Dialog state ------------------------------------------------------
|
|
in-out property <bool> dialog-open: false;
|
|
in-out property <bool> dialog-editing: false;
|
|
in-out property <string> dialog-id;
|
|
in-out property <string> dialog-name;
|
|
in-out property <string> dialog-group: "Default";
|
|
in-out property <string> dialog-host;
|
|
in-out property <string> dialog-port: "22";
|
|
in-out property <string> dialog-user: "root";
|
|
in-out property <string> dialog-auth: "password";
|
|
in-out property <string> dialog-password;
|
|
in-out property <string> dialog-key-path;
|
|
in-out property <string> dialog-proxy;
|
|
in-out property <string> dialog-kind: "ssh";
|
|
in-out property <string> dialog-serial-port;
|
|
in-out property <string> dialog-baud: "115200";
|
|
in-out property <string> dialog-data-bits: "8";
|
|
in-out property <string> dialog-stop-bits: "1";
|
|
in-out property <string> dialog-parity: "none";
|
|
in-out property <string> dialog-flow: "none";
|
|
in-out property <bool> folder-dialog-open: false;
|
|
in-out property <string> folder-dialog-mode: "create";
|
|
in-out property <string> folder-edit-original;
|
|
in-out property <string> folder-draft;
|
|
in-out property <string> folder-error;
|
|
|
|
// Delete-confirmation modal state (#28).
|
|
in-out property <bool> confirm-delete-open: false;
|
|
in-out property <string> confirm-delete-tab;
|
|
in-out property <string> confirm-delete-path;
|
|
|
|
// --- Callbacks up to Rust ---------------------------------------------
|
|
callback new-session-clicked();
|
|
callback connection-folder-create(string);
|
|
callback connection-folder-rename(string, string);
|
|
callback connection-folder-delete(string);
|
|
callback connection-folder-toggle(string);
|
|
callback session-move-folder(string, string);
|
|
callback import-ssh-config(); // import hosts from ~/.ssh/config
|
|
in-out property <string> ssh-import-hint; // result text shown after import
|
|
callback connect-session(string /* session-id */);
|
|
callback edit-session(string /* session-id */);
|
|
callback remove-session(string /* session-id */);
|
|
callback session-dialog-submit(SessionDraft);
|
|
callback session-dialog-cancel();
|
|
callback session-dialog-pick-key();
|
|
|
|
callback tab-selected(string);
|
|
callback tab-closed(string);
|
|
callback new-tab-clicked();
|
|
callback previous-tab-clicked();
|
|
callback next-tab-clicked();
|
|
// Asks Rust to recompute the sidebar (status + resources) for whatever tab
|
|
// is now active. Fired on every active-tab change, UI- or code-driven.
|
|
callback refresh-sidebar();
|
|
changed active-tab-id => { root.refresh-sidebar(); }
|
|
|
|
// Raw keystroke forwarding: Rust converts key+modifiers to PTY bytes.
|
|
callback send-key(string /* tab-id */, string /* key */, bool /* ctrl */, bool /* alt */, bool /* shift */);
|
|
// Notifies Rust of a PTY resize (pixel dimensions; Rust converts to cols/rows).
|
|
callback terminal-resize(string /* tab-id */, float /* width-px */, float /* height-px */);
|
|
|
|
// --- SFTP callbacks ---------------------------------------------------
|
|
// Navigate to a new remote directory (or ".." to go up).
|
|
callback sftp-navigate(string /* tab-id */, string /* path */);
|
|
// Trigger a download: Rust opens a save-folder dialog then transfers.
|
|
callback sftp-download(string /* tab-id */, string /* remote-path */);
|
|
// Trigger an upload: Rust opens a file-picker dialog then transfers.
|
|
callback sftp-upload-clicked(string /* tab-id */, string /* remote-dir */);
|
|
// Refresh the current directory listing.
|
|
callback sftp-refresh(string /* tab-id */, string /* path */);
|
|
// Toggle expand/collapse of a tree node (also navigates to that directory).
|
|
callback sftp-tree-expand(string /* tab-id */, string /* path */);
|
|
callback sftp-delete(string /* tab-id */, string /* path */);
|
|
callback sftp-rename(string /* tab-id */, string /* path */, string /* new-name */);
|
|
callback sftp-edit(string /* tab-id */, string /* path */);
|
|
|
|
// --- Clipboard callbacks ----------------------------------------------
|
|
// Paste clipboard text into the active terminal.
|
|
callback paste-from-clipboard(string /* tab-id */);
|
|
// Copy the current terminal screen buffer to the clipboard.
|
|
callback copy-terminal-text(string /* tab-id */);
|
|
// Clear a session's terminal buffer (its visible screen + scrollback).
|
|
callback clear-terminal(string /* tab-id */);
|
|
// Recompute search highlights for a session's visible screen.
|
|
callback find-query-changed(string /* tab-id */, string /* query */);
|
|
// Scroll a session's scrollback history by N lines (+ = up/older).
|
|
callback terminal-scroll(string /* tab-id */, int /* delta-lines */);
|
|
// Drag-selection lifecycle (grid row/col coordinates).
|
|
callback term-select-start(string /* tab-id */, int /* row */, int /* col */);
|
|
callback term-select-update(string /* tab-id */, int /* row */, int /* col */);
|
|
callback term-select-end(string /* tab-id */);
|
|
callback term-select-autoscroll(string /* tab-id */, int /* dir */);
|
|
callback command-category-select(string /* category-id */);
|
|
callback command-category-add(string /* name */);
|
|
callback command-category-rename(string /* category-id */, string /* name */);
|
|
callback command-category-delete(string /* category-id */);
|
|
callback command-new(string /* category-id */);
|
|
callback command-select(string /* command-id */);
|
|
callback command-save(string /* command-id */, string /* category-id */, string /* name */, string /* command */, bool /* append-cr */);
|
|
callback command-delete(string /* command-id */);
|
|
callback command-run(string /* tab-id */, string /* command-id */, string /* p1 */, string /* p2 */, string /* p3 */, string /* p4 */, string /* p5 */);
|
|
callback command-move(string /* command-id */, string /* category-id */);
|
|
callback terminal-mouse-event(string /* tab-id */, string /* kind */, int /* button */, int /* row */, int /* col */);
|
|
|
|
if root.fullscreen-background-active : Image {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
source: root.background-image;
|
|
image-fit: cover;
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
background: root.fullscreen-background-active
|
|
? Theme.bg-root.with-alpha(root.ui-background-opacity)
|
|
: Theme.bg-root;
|
|
}
|
|
|
|
HorizontalLayout {
|
|
spacing: 0;
|
|
|
|
// Left sidebar
|
|
Sidebar {
|
|
app-background-active: root.fullscreen-background-active;
|
|
ui-opacity: root.ui-background-opacity;
|
|
connection-state: root.connection-state;
|
|
conn-state: root.conn-state;
|
|
resource-title: root.resource-title;
|
|
cpu-percent: root.cpu-percent;
|
|
mem-percent: root.mem-percent;
|
|
swap-percent: root.swap-percent;
|
|
cpu-detail: root.cpu-detail;
|
|
mem-detail: root.mem-detail;
|
|
swap-detail: root.swap-detail;
|
|
net-top-up: root.net-top-up;
|
|
net-top-down: root.net-top-down;
|
|
net-top-history: root.net-top-history;
|
|
net-ifaces: root.net-ifaces;
|
|
net-selected: root.net-selected;
|
|
net-show-selector: root.net-show-selector;
|
|
disks: root.disks;
|
|
processes: root.processes;
|
|
select-net-iface(i) => { root.select-net-iface(i); }
|
|
}
|
|
|
|
// Right side: tabs + content
|
|
VerticalLayout {
|
|
spacing: 0;
|
|
horizontal-stretch: 1;
|
|
|
|
TabBar {
|
|
app-background-active: root.fullscreen-background-active;
|
|
ui-opacity: root.ui-background-opacity;
|
|
tabs: root.tabs;
|
|
active-id: root.active-tab-id;
|
|
visible-start <=> root.tab-visible-start;
|
|
tab-selected(id) => {
|
|
if root.ui-debug-ids { root.debug-click("tabbar", "tab-selected"); }
|
|
root.active-tab-id = id;
|
|
root.tab-selected(id);
|
|
}
|
|
tab-closed(id) => {
|
|
if root.ui-debug-ids { root.debug-click("tabbar", "tab-closed"); }
|
|
root.tab-closed(id);
|
|
}
|
|
new-tab() => {
|
|
if root.ui-debug-ids { root.debug-click("tabbar", "new-tab"); }
|
|
root.new-tab-clicked();
|
|
}
|
|
previous-tab() => {
|
|
if root.ui-debug-ids { root.debug-click("tabbar", "previous-tab"); }
|
|
root.previous-tab-clicked();
|
|
}
|
|
next-tab() => {
|
|
if root.ui-debug-ids { root.debug-click("tabbar", "next-tab"); }
|
|
root.next-tab-clicked();
|
|
}
|
|
}
|
|
|
|
// Content area swaps based on active tab kind. Children are
|
|
// stacked, not laid out horizontally, so hidden terminal tabs never
|
|
// reserve half of the workspace.
|
|
Rectangle {
|
|
vertical-stretch: 1;
|
|
background: root.fullscreen-background-active
|
|
? Theme.bg-root.with-alpha(root.ui-background-opacity)
|
|
: Theme.bg-root;
|
|
clip: true;
|
|
|
|
// Welcome tab
|
|
if root.active-tab-id == "welcome" : Welcome {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
app-background-active: root.fullscreen-background-active;
|
|
ui-opacity: root.ui-background-opacity;
|
|
sessions: root.sessions;
|
|
folders: root.connection-folders;
|
|
all-folder-names: root.connection-folder-names;
|
|
import-hint: root.ssh-import-hint;
|
|
debug-ids: false;
|
|
new-session => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "new-session"); }
|
|
root.new-session-clicked();
|
|
}
|
|
new-folder => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "new-folder"); }
|
|
root.folder-dialog-mode = "create";
|
|
root.folder-edit-original = "";
|
|
root.folder-draft = "";
|
|
root.folder-error = "";
|
|
root.folder-dialog-open = true;
|
|
}
|
|
rename-folder(name) => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "rename-folder"); }
|
|
root.folder-dialog-mode = "rename";
|
|
root.folder-edit-original = name;
|
|
root.folder-draft = name;
|
|
root.folder-error = "";
|
|
root.folder-dialog-open = true;
|
|
}
|
|
delete-folder(name) => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "delete-folder"); }
|
|
root.connection-folder-delete(name);
|
|
}
|
|
toggle-folder(name) => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "toggle-folder"); }
|
|
root.connection-folder-toggle(name);
|
|
}
|
|
move-session-folder(id, folder) => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "move-session-folder"); }
|
|
root.session-move-folder(id, folder);
|
|
}
|
|
import-ssh-config => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "import-ssh-config"); }
|
|
root.import-ssh-config();
|
|
}
|
|
connect-session(id) => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "connect-session"); }
|
|
root.connect-session(id);
|
|
}
|
|
edit-session(id) => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "edit-session"); }
|
|
root.edit-session(id);
|
|
}
|
|
remove-session(id) => {
|
|
if root.ui-debug-ids { root.debug-click("welcome", "remove-session"); }
|
|
root.remove-session(id);
|
|
}
|
|
}
|
|
|
|
// Terminal tabs. Every terminal view fills the same workspace;
|
|
// only the active tab is visible and allowed to emit resize
|
|
// events from its own timer.
|
|
for term[i] in root.terminals : TerminalView {
|
|
x: 0px;
|
|
y: 0px;
|
|
width: parent.width;
|
|
height: parent.height;
|
|
visible: term.tab-id == root.active-tab-id;
|
|
tab-id: term.id;
|
|
status-line: term.status;
|
|
spans: term.spans;
|
|
cursor-row: term.cursor-row;
|
|
cursor-col: term.cursor-col;
|
|
rows-used: term.rows-used;
|
|
is-alt-screen: term.is-alt-screen;
|
|
mouse-tracking: term.mouse-tracking;
|
|
find-matches: term.find-matches;
|
|
selection: term.selection;
|
|
sftp-panel-height <=> root.sftp-panel-height;
|
|
terminal-font-size: root.terminal-font-size;
|
|
click-debug-enabled: root.ui-debug-ids;
|
|
background-path: root.background-path-text;
|
|
background-mode: root.background-mode;
|
|
background-image: root.background-image;
|
|
app-background-active: root.fullscreen-background-active;
|
|
terminal-bg-opacity: root.terminal-background-opacity;
|
|
ui-opacity: root.ui-background-opacity;
|
|
sftp-file-font-size: root.sftp-file-font-size;
|
|
sftp-name-column-width <=> root.sftp-name-column-width;
|
|
sftp-name-width-manual <=> root.sftp-name-width-manual;
|
|
sftp-path: term.sftp-path;
|
|
sftp-entries: term.sftp-entries;
|
|
sftp-status: term.sftp-status;
|
|
sftp-loading: term.sftp-loading;
|
|
sftp-tree-nodes: term.sftp-tree-nodes;
|
|
command-categories: root.command-categories;
|
|
command-items: root.command-items;
|
|
command-selected-category <=> root.command-selected-category;
|
|
command-selected-id <=> root.command-selected-id;
|
|
command-category-draft <=> root.command-category-draft;
|
|
command-edit-id <=> root.command-edit-id;
|
|
command-edit-name <=> root.command-edit-name;
|
|
command-edit-command <=> root.command-edit-command;
|
|
command-edit-append-cr <=> root.command-edit-append-cr;
|
|
command-param1-name <=> root.command-param1-name;
|
|
command-param1-value <=> root.command-param1-value;
|
|
command-param2-name <=> root.command-param2-name;
|
|
command-param2-value <=> root.command-param2-value;
|
|
command-param3-name <=> root.command-param3-name;
|
|
command-param3-value <=> root.command-param3-value;
|
|
command-param4-name <=> root.command-param4-name;
|
|
command-param4-value <=> root.command-param4-value;
|
|
command-param5-name <=> root.command-param5-name;
|
|
command-param5-value <=> root.command-param5-value;
|
|
send-key(key, ctrl, alt, shift) => { root.send-key(term.id, key, ctrl, alt, shift) }
|
|
terminal-resize(w, h) => { root.terminal-resize(term.id, w, h) }
|
|
sftp-navigate(path) => { root.sftp-navigate(term.id, path); }
|
|
sftp-download(path) => { root.sftp-download(term.id, path); }
|
|
sftp-upload-clicked(dir) => { root.sftp-upload-clicked(term.id, dir); }
|
|
sftp-refresh(path) => { root.sftp-refresh(term.id, path); }
|
|
sftp-tree-expand(path) => { root.sftp-tree-expand(term.id, path); }
|
|
// Gate delete behind an in-app confirmation (#28): stash the
|
|
// target and open the modal; the real delete fires on confirm.
|
|
sftp-delete(path) => {
|
|
root.confirm-delete-tab = term.id;
|
|
root.confirm-delete-path = path;
|
|
root.confirm-delete-open = true;
|
|
}
|
|
sftp-rename(path, name) => { root.sftp-rename(term.id, path, name); }
|
|
sftp-edit(path) => { root.sftp-edit(term.id, path); }
|
|
sftp-name-column-width-changed(px) => { root.sftp-name-column-width-changed(px); }
|
|
sftp-name-column-auto() => { root.sftp-name-column-auto(); }
|
|
paste-from-clipboard() => { root.paste-from-clipboard(term.id); }
|
|
copy-terminal-text() => { root.copy-terminal-text(term.id); }
|
|
clear-terminal() => { root.clear-terminal(term.id); }
|
|
find-query-changed(q) => { root.find-query-changed(term.id, q); }
|
|
terminal-scroll(d) => { root.terminal-scroll(term.id, d); }
|
|
select-start(r, c) => { root.term-select-start(term.id, r, c); }
|
|
select-update(r, c) => { root.term-select-update(term.id, r, c); }
|
|
select-end() => { root.term-select-end(term.id); }
|
|
select-autoscroll(dir) => { root.term-select-autoscroll(term.id, dir); }
|
|
command-category-select(id) => { root.command-category-select(id); }
|
|
command-category-add(name) => { root.command-category-add(name); }
|
|
command-category-rename(id, name) => { root.command-category-rename(id, name); }
|
|
command-category-delete(id) => { root.command-category-delete(id); }
|
|
command-new(category) => { root.command-new(category); }
|
|
command-select(id) => { root.command-select(id); }
|
|
command-save(id, category, name, command, append_cr) => {
|
|
root.command-save(id, category, name, command, append_cr);
|
|
}
|
|
command-delete(id) => { root.command-delete(id); }
|
|
command-run(id, p1, p2, p3, p4, p5) => {
|
|
root.command-run(term.id, id, p1, p2, p3, p4, p5);
|
|
}
|
|
command-move(id, category) => { root.command-move(id, category); }
|
|
terminal-mouse-event(kind, button, row, col) => {
|
|
root.terminal-mouse-event(term.id, kind, button, row, col);
|
|
}
|
|
debug-click(parent, button) => { root.debug-click(parent, button); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Top-right buttons: [☀/🌙 theme] [↓ download] [⚙ settings] ----------
|
|
// Settings (gear) — far right.
|
|
Rectangle {
|
|
x: root.width - 34px;
|
|
y: 8px;
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: Theme.radius-sm;
|
|
background: gear-ta.has-hover || root.settings-open ? Theme.bg-hover : transparent;
|
|
gear-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("topbar", "settings"); }
|
|
root.settings-open = !root.settings-open;
|
|
root.download-open = false;
|
|
root.config-open = false;
|
|
}
|
|
}
|
|
Text {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
text: "\u{E8B8}"; // Material Icons settings (gear)
|
|
font-family: "Material Icons";
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-md;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
// Download manager — left of settings.
|
|
Rectangle {
|
|
x: root.width - 64px;
|
|
y: 8px;
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: Theme.radius-sm;
|
|
background: dl-ta.has-hover || root.download-open ? Theme.bg-hover : transparent;
|
|
dl-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("topbar", "downloads"); }
|
|
root.download-open = !root.download-open;
|
|
root.settings-open = false;
|
|
root.config-open = false;
|
|
}
|
|
}
|
|
Text {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
text: "\u{E2C4}"; // Material Icons file_download
|
|
font-family: "Material Icons";
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-md;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
// Theme toggle (sun = currently dark → click for light;
|
|
// moon = currently light → click for dark) — left of download.
|
|
Rectangle {
|
|
x: root.width - 94px;
|
|
y: 8px;
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: Theme.radius-sm;
|
|
background: theme-ta.has-hover ? Theme.bg-hover : transparent;
|
|
theme-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("topbar", "toggle-theme"); }
|
|
root.toggle-theme();
|
|
}
|
|
}
|
|
Text {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
// light_mode ☀ when dark (click → go light)
|
|
// dark_mode 🌙 when light (click → go dark)
|
|
text: Theme.dark ? "\u{E518}" : "\u{E51C}";
|
|
font-family: "Material Icons";
|
|
color: Theme.dark ? #f5c542 : #5856d6; // amber sun / indigo moon
|
|
font-size: Theme.fs-md;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
// --- Download manager popup -------------------------------------------
|
|
// Always present; toggled via `visible` (avoids destroying interactive
|
|
// elements mid-event, which crashes when created/removed at the Window root).
|
|
if root.download-open : TouchArea {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
clicked => { root.download-open = false; }
|
|
}
|
|
|
|
Rectangle {
|
|
x: parent.width - 364px;
|
|
y: 40px;
|
|
width: 350px;
|
|
height: 400px;
|
|
visible: root.download-open;
|
|
background: Theme.bg-panel;
|
|
border-radius: Theme.radius-md;
|
|
border-width: 1px;
|
|
border-color: Theme.border-strong;
|
|
drop-shadow-blur: 16px;
|
|
drop-shadow-color: #00000070;
|
|
|
|
// Swallow pointer events so right-clicks don't fall through to the
|
|
// terminal's context menu underneath.
|
|
TouchArea {}
|
|
|
|
VerticalLayout {
|
|
padding: 14px;
|
|
spacing: 10px;
|
|
|
|
HorizontalLayout {
|
|
Text {
|
|
text: @tr("Download settings");
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-md;
|
|
font-weight: 600;
|
|
horizontal-stretch: 1;
|
|
vertical-alignment: center;
|
|
}
|
|
Rectangle {
|
|
width: 20px; height: 20px;
|
|
border-radius: Theme.radius-sm;
|
|
background: close-ta.has-hover ? Theme.bg-hover : transparent;
|
|
close-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => { root.download-open = false; }
|
|
}
|
|
Text { text: "\u{E5CD}"; font-family: "Material Icons";
|
|
color: Theme.text-secondary; font-size: Theme.fs-sm;
|
|
width: parent.width; height: parent.height;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: @tr("Save downloads to:");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
}
|
|
|
|
// Current path display
|
|
Rectangle {
|
|
height: 26px;
|
|
background: Theme.bg-root;
|
|
border-radius: Theme.radius-sm;
|
|
Text {
|
|
x: 8px;
|
|
width: parent.width - 16px;
|
|
height: parent.height;
|
|
text: root.download-dir == "" ? @tr("(ask every time)") : root.download-dir;
|
|
color: root.download-dir == "" ? Theme.text-muted : Theme.text-primary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
alignment: end;
|
|
|
|
Rectangle {
|
|
width: 96px; height: 28px;
|
|
border-radius: Theme.radius-sm;
|
|
background: pick-ta.has-hover ? Theme.accent-hover : Theme.accent;
|
|
pick-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("downloads", "choose-folder"); }
|
|
root.pick-download-dir();
|
|
}
|
|
}
|
|
Text { text: @tr("Choose folder"); color: #ffffff; font-size: Theme.fs-sm;
|
|
width: parent.width; height: parent.height;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
}
|
|
Rectangle {
|
|
width: 76px; height: 28px;
|
|
border-radius: Theme.radius-sm;
|
|
background: open-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
|
|
open-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("downloads", "open-folder"); }
|
|
root.open-download-dir();
|
|
}
|
|
}
|
|
Text { text: @tr("Open folder"); color: Theme.text-primary; font-size: Theme.fs-sm;
|
|
width: parent.width; height: parent.height;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 1px; background: Theme.border-subtle; }
|
|
|
|
// Transfer records (download/upload progress + history).
|
|
HorizontalLayout {
|
|
Text {
|
|
text: @tr("Transfers");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
horizontal-stretch: 1;
|
|
vertical-alignment: center;
|
|
}
|
|
Rectangle {
|
|
width: 44px; height: 20px;
|
|
border-radius: Theme.radius-sm;
|
|
background: clr-ta.has-hover ? Theme.bg-hover : transparent;
|
|
clr-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("downloads", "clear-transfers"); }
|
|
root.clear-transfers();
|
|
}
|
|
}
|
|
Text { text: @tr("Clear"); color: Theme.text-muted; font-size: Theme.fs-xs;
|
|
width: parent.width; height: parent.height;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
}
|
|
}
|
|
|
|
Flickable {
|
|
vertical-stretch: 1;
|
|
viewport-width: self.width;
|
|
viewport-height: max(self.height, xfer-col.preferred-height);
|
|
xfer-col := VerticalLayout {
|
|
alignment: start;
|
|
spacing: 4px;
|
|
|
|
if root.transfers.length == 0 : Text {
|
|
text: @tr("No transfers yet");
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-xs;
|
|
}
|
|
|
|
for t in root.transfers : Rectangle {
|
|
height: 36px;
|
|
border-radius: Theme.radius-sm;
|
|
background: root.fullscreen-background-active ? Theme.bg-root.with-alpha(root.ui-background-opacity) : Theme.bg-root;
|
|
VerticalLayout {
|
|
padding-left: 8px; padding-right: 8px;
|
|
padding-top: 4px; padding-bottom: 4px;
|
|
spacing: 3px;
|
|
HorizontalLayout {
|
|
spacing: 4px;
|
|
Text {
|
|
text: (t.is-upload ? "↑ " : "↓ ") + t.name;
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-xs;
|
|
horizontal-stretch: 1;
|
|
overflow: elide;
|
|
vertical-alignment: center;
|
|
}
|
|
Text {
|
|
text: t.detail;
|
|
color: t.state == 2 ? Theme.danger
|
|
: t.state == 1 ? Theme.success
|
|
: Theme.text-muted;
|
|
font-size: Theme.fs-xs;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
// progress bar
|
|
Rectangle {
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: Theme.bg-elevated;
|
|
Rectangle {
|
|
x: 0; y: 0; height: parent.height;
|
|
width: parent.width * clamp(t.percent, 0.0, 1.0);
|
|
border-radius: 2px;
|
|
background: t.state == 2 ? Theme.danger
|
|
: t.state == 1 ? Theme.success
|
|
: Theme.accent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Settings menu popup (top-right) ----------------------------------
|
|
Rectangle {
|
|
x: parent.width - 196px;
|
|
y: 40px;
|
|
width: 198px;
|
|
height: 134px;
|
|
visible: root.settings-open;
|
|
background: Theme.bg-panel;
|
|
border-radius: Theme.radius-md;
|
|
border-width: 1px;
|
|
border-color: Theme.border-strong;
|
|
drop-shadow-blur: 14px;
|
|
drop-shadow-color: #00000060;
|
|
|
|
TouchArea {} // swallow events so they don't reach the terminal
|
|
|
|
VerticalLayout {
|
|
padding: 4px;
|
|
spacing: 1px;
|
|
// Import hosts from ~/.ssh/config.
|
|
Rectangle {
|
|
height: 28px;
|
|
border-radius: Theme.radius-sm;
|
|
background: import-ta.has-hover ? Theme.bg-hover : transparent;
|
|
import-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("settings", "import-ssh-config"); }
|
|
root.settings-open = false;
|
|
root.import-ssh-config();
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
height: parent.height;
|
|
padding-left: 8px; spacing: 8px;
|
|
Text { width: 18px; height: parent.height; text: "\u{E890}"; font-family: "Material Icons"; // input/import
|
|
color: Theme.text-secondary; font-size: Theme.fs-sm;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
Text { text: @tr("Import ~/.ssh/config"); color: Theme.text-primary; font-size: Theme.fs-md;
|
|
height: parent.height; vertical-alignment: center; horizontal-stretch: 1; }
|
|
}
|
|
}
|
|
// Language toggle: shows the language you'll switch TO.
|
|
Rectangle {
|
|
height: 28px;
|
|
border-radius: Theme.radius-sm;
|
|
background: lang-ta.has-hover ? Theme.bg-hover : transparent;
|
|
lang-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("settings", "language"); }
|
|
root.settings-open = false;
|
|
root.set-language(root.lang-en ? "zh" : "en");
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
height: parent.height;
|
|
padding-left: 8px; spacing: 8px;
|
|
Text { width: 18px; height: parent.height; text: "\u{E894}"; font-family: "Material Icons"; // language/translate
|
|
color: Theme.text-secondary; font-size: Theme.fs-sm;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
Text { text: root.lang-en ? "切换到中文" : "Switch to English";
|
|
color: Theme.text-primary; font-size: Theme.fs-md;
|
|
height: parent.height; vertical-alignment: center; horizontal-stretch: 1; }
|
|
}
|
|
}
|
|
Rectangle {
|
|
height: 28px;
|
|
border-radius: Theme.radius-sm;
|
|
background: about-ta.has-hover ? Theme.bg-hover : transparent;
|
|
about-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("settings", "about"); }
|
|
root.settings-open = false;
|
|
root.about-open = true;
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
height: parent.height;
|
|
padding-left: 8px; spacing: 8px;
|
|
Text { width: 18px; height: parent.height; text: "\u{E88E}"; font-family: "Material Icons"; // Info
|
|
color: Theme.text-secondary; font-size: Theme.fs-sm;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
Text { text: @tr("About"); color: Theme.text-primary; font-size: Theme.fs-md;
|
|
height: parent.height; vertical-alignment: center; horizontal-stretch: 1; }
|
|
}
|
|
}
|
|
Rectangle {
|
|
height: 28px;
|
|
border-radius: Theme.radius-sm;
|
|
background: config-ta.has-hover ? Theme.bg-hover : transparent;
|
|
config-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("settings", "configuration"); }
|
|
root.settings-open = false;
|
|
root.config-open = true;
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
height: parent.height;
|
|
padding-left: 8px; spacing: 8px;
|
|
Text { width: 18px; height: parent.height; text: "\u{E8B8}"; font-family: "Material Icons";
|
|
color: Theme.text-secondary; font-size: Theme.fs-sm;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
Text { text: @tr("Configuration"); color: Theme.text-primary; font-size: Theme.fs-md;
|
|
height: parent.height; vertical-alignment: center; horizontal-stretch: 1; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Configuration dialog --------------------------------------------
|
|
Rectangle {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
visible: root.config-open;
|
|
background: #00000070;
|
|
TouchArea { clicked => { root.config-open = false; } }
|
|
|
|
Rectangle {
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
width: min(640px, parent.width - 48px);
|
|
height: min(620px, parent.height - 48px);
|
|
background: Theme.bg-panel;
|
|
border-radius: Theme.radius-md;
|
|
border-width: 1px;
|
|
border-color: Theme.border-strong;
|
|
drop-shadow-blur: 20px;
|
|
drop-shadow-color: #00000080;
|
|
|
|
TouchArea {}
|
|
|
|
VerticalLayout {
|
|
padding: 16px;
|
|
spacing: 10px;
|
|
|
|
HorizontalLayout {
|
|
Text {
|
|
text: @tr("Configuration");
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-md;
|
|
font-weight: 600;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
Rectangle {
|
|
width: 22px; height: 22px;
|
|
border-radius: Theme.radius-sm;
|
|
background: cfg-close-ta.has-hover ? Theme.bg-hover : transparent;
|
|
cfg-close-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => { root.config-open = false; }
|
|
}
|
|
Text { text: "\u{E5CD}"; font-family: "Material Icons";
|
|
color: Theme.text-secondary; font-size: Theme.fs-sm;
|
|
width: parent.width; height: parent.height;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("Maximum terminal windows");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.max-tabs-text;
|
|
field-width: 86px;
|
|
}
|
|
Rectangle { horizontal-stretch: 1; }
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("Terminal font size");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.terminal-font-size-text;
|
|
field-width: 72px;
|
|
}
|
|
Text {
|
|
width: 26px;
|
|
height: parent.height;
|
|
text: "px";
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
}
|
|
Text {
|
|
width: 138px;
|
|
height: parent.height;
|
|
text: @tr("File manager font size");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.sftp-font-size-text;
|
|
field-width: 72px;
|
|
}
|
|
Text {
|
|
width: 26px;
|
|
height: parent.height;
|
|
text: "px";
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("Background image");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigValueBox {
|
|
horizontal-stretch: 1;
|
|
value: root.background-path-text;
|
|
empty-text: @tr("No background image");
|
|
}
|
|
ConfigButton {
|
|
label: @tr("Choose");
|
|
button-width: 82px;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("configuration", "choose-background"); }
|
|
root.app-background-pick();
|
|
}
|
|
}
|
|
ConfigButton {
|
|
label: @tr("Clear");
|
|
button-width: 72px;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("configuration", "clear-background"); }
|
|
root.app-background-clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("Background scope");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigButton {
|
|
label: @tr("Terminal");
|
|
button-width: 96px;
|
|
selected: root.background-mode == 1;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("configuration", "background-terminal"); }
|
|
root.background-mode = 1;
|
|
}
|
|
}
|
|
ConfigButton {
|
|
label: @tr("Fullscreen");
|
|
button-width: 104px;
|
|
selected: root.background-mode == 2;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("configuration", "background-fullscreen"); }
|
|
root.background-mode = 2;
|
|
}
|
|
}
|
|
Rectangle { horizontal-stretch: 1; }
|
|
Text {
|
|
height: parent.height;
|
|
text: @tr("Blur");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.background-blur-text;
|
|
field-width: 58px;
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("Terminal opacity");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.terminal-opacity-text;
|
|
field-width: 68px;
|
|
}
|
|
Text {
|
|
width: 24px;
|
|
height: parent.height;
|
|
text: "%";
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
}
|
|
Text {
|
|
width: 108px;
|
|
height: parent.height;
|
|
text: @tr("UI opacity");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.ui-opacity-text;
|
|
field-width: 68px;
|
|
}
|
|
Text {
|
|
width: 24px;
|
|
height: parent.height;
|
|
text: "%";
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
}
|
|
Rectangle { horizontal-stretch: 1; }
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("Click log");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigButton {
|
|
label: root.ui-debug-ids ? @tr("On") : @tr("Off");
|
|
button-width: 76px;
|
|
selected: root.ui-debug-ids;
|
|
clicked => {
|
|
root.ui-debug-ids = !root.ui-debug-ids;
|
|
if root.ui-debug-ids { root.debug-click("configuration", "click-log-on"); }
|
|
}
|
|
}
|
|
Text {
|
|
text: @tr("Writes clicked button/parent ids to the debug log");
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-xs;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("Configuration directory");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigValueBox {
|
|
horizontal-stretch: 1;
|
|
value: root.config-dir-text;
|
|
empty-text: "";
|
|
}
|
|
ConfigButton {
|
|
label: @tr("Choose");
|
|
button-width: 82px;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("configuration", "choose-config-dir"); }
|
|
root.app-config-pick-dir();
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- WebDAV Sync Settings ---
|
|
Rectangle { height: 1px; background: Theme.border-subtle; }
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("WebDAV sync");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
font-weight: 600;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigButton {
|
|
label: root.webdav-enabled ? @tr("On") : @tr("Off");
|
|
button-width: 76px;
|
|
selected: root.webdav-enabled;
|
|
clicked => {
|
|
root.webdav-enabled = !root.webdav-enabled;
|
|
}
|
|
}
|
|
Rectangle { horizontal-stretch: 1; }
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("WebDAV URL");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.webdav-url-text;
|
|
field-width: 300px;
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("WebDAV username");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.webdav-username-text;
|
|
field-width: 160px;
|
|
}
|
|
Text {
|
|
width: 80px;
|
|
height: parent.height;
|
|
text: @tr("Password");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.webdav-password-text;
|
|
field-width: 140px;
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: @tr("Auto-sync interval (min)");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
ConfigTextField {
|
|
value <=> root.webdav-auto-sync-interval;
|
|
field-width: 80px;
|
|
}
|
|
Text {
|
|
height: parent.height;
|
|
text: @tr("0 = off");
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-xs;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Text {
|
|
width: 148px;
|
|
height: parent.height;
|
|
text: root.webdav-status-text;
|
|
color: root.webdav-status-text == @tr("Connection OK") ? Theme.success : Theme.text-muted;
|
|
font-size: Theme.fs-xs;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
horizontal-stretch: 1;
|
|
}
|
|
ConfigButton {
|
|
label: @tr("Test");
|
|
button-width: 72px;
|
|
clicked => { root.webdav-test-connection(); }
|
|
}
|
|
ConfigButton {
|
|
label: @tr("Upload");
|
|
button-width: 82px;
|
|
clicked => { root.webdav-push(); }
|
|
}
|
|
ConfigButton {
|
|
label: @tr("Download");
|
|
button-width: 82px;
|
|
clicked => { root.webdav-pull(); }
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 10px;
|
|
Rectangle { horizontal-stretch: 1; }
|
|
ConfigButton {
|
|
label: @tr("Cancel");
|
|
button-width: 82px;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("configuration", "cancel"); }
|
|
root.config-open = false;
|
|
}
|
|
}
|
|
ConfigButton {
|
|
label: @tr("Save");
|
|
button-width: 82px;
|
|
primary: true;
|
|
clicked => {
|
|
if root.ui-debug-ids { root.debug-click("configuration", "save"); }
|
|
root.webdav-save-settings(
|
|
root.webdav-enabled,
|
|
root.webdav-url-text,
|
|
root.webdav-username-text,
|
|
root.webdav-password-text,
|
|
root.webdav-auto-sync-interval);
|
|
root.app-config-save(
|
|
root.max-tabs-text,
|
|
root.terminal-font-size-text,
|
|
root.sftp-font-size-text,
|
|
root.background-mode,
|
|
root.background-blur-text,
|
|
root.terminal-opacity-text,
|
|
root.ui-opacity-text,
|
|
root.ui-debug-ids,
|
|
root.config-dir-text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- About dialog (centered on the window, with dim backdrop) ---------
|
|
Rectangle {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
visible: root.about-open;
|
|
background: #00000080; // dim backdrop
|
|
TouchArea { clicked => { root.about-open = false; } }
|
|
|
|
Rectangle {
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
width: 380px;
|
|
height: 440px;
|
|
background: Theme.bg-panel;
|
|
border-radius: Theme.radius-md;
|
|
border-width: 1px;
|
|
border-color: Theme.border-strong;
|
|
drop-shadow-blur: 24px;
|
|
drop-shadow-color: #000000a0;
|
|
|
|
// Swallow clicks so the backdrop doesn't close when clicking inside.
|
|
TouchArea {}
|
|
|
|
VerticalLayout {
|
|
padding: 18px;
|
|
spacing: 8px;
|
|
|
|
HorizontalLayout {
|
|
Text {
|
|
text: @tr("About meatshell");
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-lg;
|
|
font-weight: 700;
|
|
horizontal-stretch: 1;
|
|
vertical-alignment: center;
|
|
}
|
|
Rectangle {
|
|
width: 22px; height: 22px;
|
|
border-radius: Theme.radius-sm;
|
|
background: ab-close-ta.has-hover ? Theme.bg-hover : transparent;
|
|
ab-close-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => { root.about-open = false; }
|
|
}
|
|
Text { text: "\u{E5CD}"; font-family: "Material Icons";
|
|
color: Theme.text-secondary; font-size: Theme.fs-sm;
|
|
width: parent.width; height: parent.height;
|
|
horizontal-alignment: center; vertical-alignment: center; }
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: @tr("A lightweight Rust + Slint SSH terminal client by 'lil meatball'.");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
wrap: word-wrap;
|
|
}
|
|
Text {
|
|
text: @tr("Open source · MIT / Apache-2.0");
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-xs;
|
|
}
|
|
|
|
Rectangle { height: 1px; background: Theme.border-subtle; }
|
|
|
|
Text {
|
|
text: @tr("Open-source libraries used");
|
|
color: Theme.text-secondary;
|
|
font-size: Theme.fs-sm;
|
|
}
|
|
|
|
Flickable {
|
|
vertical-stretch: 1;
|
|
viewport-width: self.width;
|
|
viewport-height: max(self.height, libs-col.preferred-height);
|
|
libs-col := VerticalLayout {
|
|
alignment: start;
|
|
spacing: 4px;
|
|
for lib in root.about-libs : Text {
|
|
text: lib;
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-xs;
|
|
wrap: no-wrap;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Connection folder dialog ----------------------------------------
|
|
Rectangle {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
visible: root.folder-dialog-open;
|
|
background: #00000070;
|
|
TouchArea { clicked => { root.folder-dialog-open = false; } }
|
|
|
|
Rectangle {
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
width: min(360px, parent.width - 48px);
|
|
height: 194px;
|
|
background: Theme.bg-panel;
|
|
border-radius: Theme.radius-md;
|
|
border-width: 1px;
|
|
border-color: Theme.border-strong;
|
|
drop-shadow-blur: 20px;
|
|
drop-shadow-color: #00000080;
|
|
|
|
TouchArea {}
|
|
|
|
VerticalLayout {
|
|
padding: 16px;
|
|
spacing: 12px;
|
|
|
|
Text {
|
|
text: root.folder-dialog-mode == "rename" ? @tr("Rename category") : @tr("New category");
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-md;
|
|
font-weight: 600;
|
|
}
|
|
|
|
Rectangle {
|
|
height: 32px;
|
|
border-radius: Theme.radius-sm;
|
|
border-width: 1px;
|
|
border-color: folder-name-input.has-focus ? Theme.accent : Theme.border-subtle;
|
|
background: Theme.bg-root;
|
|
folder-name-input := TextInput {
|
|
x: 10px;
|
|
width: parent.width - 20px;
|
|
height: parent.height;
|
|
text <=> root.folder-draft;
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-md;
|
|
single-line: true;
|
|
vertical-alignment: center;
|
|
edited => { root.folder-error = ""; }
|
|
init => { self.focus(); }
|
|
key-pressed(e) => {
|
|
if (e.text == "\n") {
|
|
if (root.folder-dialog-mode == "rename") {
|
|
root.connection-folder-rename(root.folder-edit-original, root.folder-draft);
|
|
} else {
|
|
root.connection-folder-create(root.folder-draft);
|
|
}
|
|
accept
|
|
} else {
|
|
reject
|
|
}
|
|
}
|
|
}
|
|
if root.folder-draft == "" : Text {
|
|
x: 10px;
|
|
height: parent.height;
|
|
text: @tr("Category name");
|
|
color: Theme.text-muted;
|
|
font-size: Theme.fs-md;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
Text {
|
|
height: 14px;
|
|
text: root.folder-error;
|
|
color: Theme.danger;
|
|
font-size: Theme.fs-xs;
|
|
visible: root.folder-error != "";
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: 28px;
|
|
spacing: 8px;
|
|
Rectangle { horizontal-stretch: 1; }
|
|
Rectangle {
|
|
width: 72px;
|
|
height: parent.height;
|
|
border-radius: Theme.radius-sm;
|
|
background: folder-cancel-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
|
|
folder-cancel-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => { root.folder-dialog-open = false; }
|
|
}
|
|
Text {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
text: @tr("Cancel");
|
|
color: Theme.text-primary;
|
|
font-size: Theme.fs-sm;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
Rectangle {
|
|
width: 72px;
|
|
height: parent.height;
|
|
border-radius: Theme.radius-sm;
|
|
background: folder-create-ta.has-hover ? Theme.accent-hover : Theme.accent;
|
|
folder-create-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
if (root.folder-dialog-mode == "rename") {
|
|
root.connection-folder-rename(root.folder-edit-original, root.folder-draft);
|
|
} else {
|
|
root.connection-folder-create(root.folder-draft);
|
|
}
|
|
}
|
|
}
|
|
Text {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
text: root.folder-dialog-mode == "rename" ? @tr("Rename") : @tr("Create");
|
|
color: #ffffff;
|
|
font-size: Theme.fs-sm;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Modal dialog -----------------------------------------------------
|
|
SessionDialog {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
is-open <=> root.dialog-open;
|
|
is-editing <=> root.dialog-editing;
|
|
draft-id <=> root.dialog-id;
|
|
draft-name <=> root.dialog-name;
|
|
draft-group <=> root.dialog-group;
|
|
draft-host <=> root.dialog-host;
|
|
draft-port <=> root.dialog-port;
|
|
draft-user <=> root.dialog-user;
|
|
draft-auth <=> root.dialog-auth;
|
|
draft-password <=> root.dialog-password;
|
|
draft-key-path <=> root.dialog-key-path;
|
|
draft-proxy <=> root.dialog-proxy;
|
|
draft-kind <=> root.dialog-kind;
|
|
draft-serial-port <=> root.dialog-serial-port;
|
|
draft-baud <=> root.dialog-baud;
|
|
draft-data-bits <=> root.dialog-data-bits;
|
|
draft-stop-bits <=> root.dialog-stop-bits;
|
|
draft-parity <=> root.dialog-parity;
|
|
draft-flow <=> root.dialog-flow;
|
|
folders: root.connection-folder-names;
|
|
submit(draft) => { root.session-dialog-submit(draft); }
|
|
cancel => { root.session-dialog-cancel(); }
|
|
pick-key-file => { root.session-dialog-pick-key(); }
|
|
}
|
|
|
|
// --- Delete confirmation (#28) ----------------------------------------
|
|
ConfirmDialog {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
is-open <=> root.confirm-delete-open;
|
|
title: @tr("Confirm delete");
|
|
message: @tr("Delete this item? This cannot be undone.");
|
|
detail: root.confirm-delete-path;
|
|
confirm-label: @tr("Delete");
|
|
confirm => {
|
|
root.sftp-delete(root.confirm-delete-tab, root.confirm-delete-path);
|
|
root.confirm-delete-open = false;
|
|
}
|
|
cancel => { root.confirm-delete-open = false; }
|
|
}
|
|
}
|