// 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 label; in property primary: false; in property selected: false; in property 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 value; in property 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 value; in property 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 connection-state: @tr("Not connected"); in property conn-state; // 0 gray / 1 green / 2 yellow in property resource-title: @tr("Local resources"); // @tr("Local resources") | @tr("Server resources") in property cpu-percent; in property mem-percent; in property swap-percent; in property cpu-detail; in property mem-detail; in property swap-detail; // Network panel follows the active live session tab. in property net-top-up; // ↑ tx rate text in property 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 net-selected; // currently selected NIC in property 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 download-open: false; // download manager popup in-out property settings-open: false; // settings menu popup in-out property config-open: false; // app configuration dialog in-out property about-open: false; // centered About dialog in property 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 command-selected-category: "default"; in-out property command-selected-id; in-out property command-category-draft; in-out property command-edit-id; in-out property command-edit-name; in-out property command-edit-command; in-out property command-edit-append-cr: true; in-out property command-param1-name; in-out property command-param1-value; in-out property command-param2-name; in-out property command-param2-value; in-out property command-param3-name; in-out property command-param3-value; in-out property command-param4-name; in-out property command-param4-value; in-out property command-param5-name; in-out property command-param5-value; in property <[string]> about-libs; // open-source libraries used in-out property 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 dark-mode <=> Theme.dark; callback toggle-theme(); // flip dark ↔ light in-out property sftp-panel-height: max(240px, root.height * 0.4); // default: files 2/5, terminal 3/5 in-out property terminal-font-size: 13px; in-out property sftp-file-font-size: 16px; in-out property sftp-name-column-width: 180px; in-out property 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 max-tabs-text: "10"; in-out property terminal-font-size-text: "13"; in-out property sftp-font-size-text: "16"; in-out property background-path-text; in-out property background-mode: 1; // 0 none, 1 terminal, 2 fullscreen in-out property background-blur-text: "3"; in-out property terminal-opacity-text: "88"; in-out property ui-opacity-text: "72"; in-out property terminal-background-opacity: 0.88; in-out property ui-background-opacity: 0.72; in-out property ui-debug-ids: false; in property background-image; in-out property config-dir-text; 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 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 active-tab-id: "welcome"; in-out property tab-visible-start: 0; in property <[TerminalState]> terminals; // --- Dialog state ------------------------------------------------------ in-out property dialog-open: false; in-out property dialog-editing: false; in-out property dialog-id; in-out property dialog-name; in-out property dialog-group: "Default"; in-out property dialog-host; in-out property dialog-port: "22"; in-out property dialog-user: "root"; in-out property dialog-auth: "password"; in-out property dialog-password; in-out property dialog-key-path; in-out property dialog-proxy; in-out property dialog-kind: "ssh"; in-out property dialog-serial-port; in-out property dialog-baud: "115200"; in-out property dialog-data-bits: "8"; in-out property dialog-stop-bits: "1"; in-out property dialog-parity: "none"; in-out property dialog-flow: "none"; in-out property folder-dialog-open: false; in-out property folder-dialog-mode: "create"; in-out property folder-edit-original; in-out property folder-draft; in-out property folder-error; // Delete-confirmation modal state (#28). in-out property confirm-delete-open: false; in-out property confirm-delete-tab; in-out property 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 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(500px, 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(); } } } 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.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; } } }