import { Theme } from "theme.slint"; import { PrimaryButton } from "widgets.slint"; export struct SessionInfo { id: string, name: string, group: string, host: string, port: int, user: string, auth: string, // "password" | "key" kind: string, // "ssh" | "serial" | "telnet" last-used: string, // ISO timestamp or "never" } export struct ConnectionFolderInfo { name: string, expanded: bool, } // --- SessionRow ------------------------------------------------------------ component SessionRow inherits Rectangle { in property session; in property <[string]> folder-names; in property debug-ids: false; callback connect(); callback edit(); callback remove(); callback move-folder(string); height: 34px; border-radius: Theme.radius-sm; background: touch.has-hover ? Theme.bg-hover : transparent; animate background { duration: 120ms; } // Remember where the right-click landed so the popup can appear there. property ctx-x; property ctx-y; touch := TouchArea { mouse-cursor: pointer; clicked => { root.connect(); } pointer-event(e) => { if (e.kind == PointerEventKind.down && e.button == PointerEventButton.right) { ctx-x = self.mouse-x; ctx-y = self.mouse-y; ctx-menu.show(); } } } // Right-click context menu ----------------------------------------------- ctx-menu := PopupWindow { x: ctx-x; y: ctx-y; width: 142px; height: 112px + root.folder-names.length * 28px; Rectangle { background: Theme.bg-panel; border-radius: Theme.radius-sm; border-width: 1px; border-color: Theme.border-strong; drop-shadow-blur: 10px; drop-shadow-color: #00000050; VerticalLayout { padding: 4px; spacing: 2px; // Edit Rectangle { height: 28px; border-radius: Theme.radius-sm; background: edit-ta.has-hover ? Theme.bg-hover : transparent; edit-ta := TouchArea { mouse-cursor: pointer; clicked => { root.edit(); } } HorizontalLayout { height: parent.height; padding-left: 10px; spacing: 8px; alignment: center; Text { width: 16px; height: parent.height; text: "\u{E3C9}"; font-family: "Material Icons"; color: Theme.text-secondary; font-size: Theme.fs-sm; horizontal-alignment: center; vertical-alignment: center; } Text { text: @tr("Edit"); color: Theme.text-primary; font-size: Theme.fs-md; vertical-alignment: center; horizontal-stretch: 1; } } } Rectangle { height: 1px; background: Theme.border-subtle; } // Move to category Rectangle { height: 28px; border-radius: Theme.radius-sm; background: transparent; HorizontalLayout { height: parent.height; padding-left: 10px; spacing: 8px; alignment: center; Text { width: 16px; height: parent.height; text: "\u{E2C8}"; font-family: "Material Icons"; color: Theme.text-secondary; font-size: Theme.fs-sm; horizontal-alignment: center; vertical-alignment: center; } Text { text: @tr("Move to category"); color: Theme.text-primary; font-size: Theme.fs-md; vertical-alignment: center; horizontal-stretch: 1; } } } for folder in root.folder-names : Rectangle { height: 26px; border-radius: Theme.radius-sm; background: folder == root.session.group ? Theme.bg-active : (folder-ta.has-hover ? Theme.bg-hover : transparent); folder-ta := TouchArea { mouse-cursor: pointer; clicked => { root.move-folder(folder); } } Text { x: 24px; width: parent.width - 34px; height: parent.height; text: folder; color: Theme.text-primary; font-size: Theme.fs-sm; vertical-alignment: center; overflow: elide; } } Rectangle { height: 1px; background: Theme.border-subtle; } // Delete (red tint) Rectangle { height: 28px; border-radius: Theme.radius-sm; background: del-ta.has-hover ? #cc333320 : transparent; del-ta := TouchArea { mouse-cursor: pointer; clicked => { root.remove(); } } HorizontalLayout { height: parent.height; padding-left: 10px; spacing: 8px; alignment: center; Text { width: 16px; height: parent.height; text: "\u{E872}"; font-family: "Material Icons"; color: #cc3333; font-size: Theme.fs-sm; horizontal-alignment: center; vertical-alignment: center; } Text { text: @tr("Delete"); color: #cc3333; font-size: Theme.fs-md; vertical-alignment: center; horizontal-stretch: 1; } } } } } } // Row content ------------------------------------------------------------ HorizontalLayout { width: parent.width; height: parent.height; padding-left: 10px; padding-right: 10px; spacing: 8px; Rectangle { width: 24px; height: parent.height; Text { width: parent.width; height: parent.height; text: "\u{2514}"; color: Theme.text-muted; font-size: Theme.fs-sm; horizontal-alignment: right; vertical-alignment: center; } } Rectangle { width: 18px; height: parent.height; Rectangle { y: (parent.height - self.height) / 2; width: parent.width; height: 18px; border-radius: 3px; background: Theme.bg-elevated; Text { width: parent.width; height: parent.height; text: session.kind == "serial" ? "\u{E1B1}" : session.kind == "telnet" ? "\u{E8AC}" : "\u{E875}"; font-family: "Material Icons"; color: Theme.text-secondary; font-size: Theme.fs-sm; horizontal-alignment: center; vertical-alignment: center; } } } Text { height: parent.height; text: session.name; color: Theme.text-primary; font-size: Theme.fs-md; vertical-alignment: center; width: 170px; overflow: elide; } Rectangle { width: 52px; height: parent.height; Rectangle { y: (parent.height - self.height) / 2; width: parent.width; height: 20px; border-radius: Theme.radius-sm; background: Theme.bg-elevated; Text { width: parent.width; height: parent.height; text: session.kind == "serial" ? "SERIAL" : session.kind == "telnet" ? "TELNET" : "SSH"; color: Theme.text-secondary; font-size: Theme.fs-xs; horizontal-alignment: center; vertical-alignment: center; } } } Text { height: parent.height; text: session.kind == "serial" ? session.host : session.host + ":" + session.port; color: Theme.text-secondary; font-size: Theme.fs-sm; vertical-alignment: center; width: 220px; overflow: elide; } Text { height: parent.height; text: session.user; color: Theme.text-secondary; font-size: Theme.fs-sm; vertical-alignment: center; horizontal-stretch: 1; overflow: elide; } if root.debug-ids : Text { height: parent.height; text: "[session-row]"; color: Theme.warning; font-size: Theme.fs-xs; vertical-alignment: center; } } } component FolderSessionSlot inherits Rectangle { in property folder-name; in property folder-expanded: true; in property session; in property <[string]> folder-names; in property debug-ids: false; callback connect-session(string); callback edit-session(string); callback remove-session(string); callback move-session-folder(string, string); height: root.folder-expanded && session.group == root.folder-name ? 34px : 0px; visible: root.folder-expanded && session.group == root.folder-name; if root.folder-expanded && session.group == root.folder-name : SessionRow { width: parent.width; height: parent.height; session: root.session; folder-names: root.folder-names; debug-ids: root.debug-ids; connect => { root.connect-session(root.session.id); } edit => { root.edit-session(root.session.id); } remove => { root.remove-session(root.session.id); } move-folder(folder) => { root.move-session-folder(root.session.id, folder); } } } component FolderSection inherits VerticalLayout { in property folder-name; in property expanded: true; in property <[SessionInfo]> sessions; in property <[string]> folder-names; in property debug-ids: false; callback connect-session(string); callback edit-session(string); callback remove-session(string); callback rename-folder(string); callback delete-folder(string); callback new-folder(); callback toggle-folder(string); callback move-session-folder(string, string); spacing: 2px; Rectangle { width: parent.width; height: 28px; border-radius: Theme.radius-sm; background: header-ta.has-hover ? Theme.bg-hover : transparent; property ctx-x; property ctx-y; header-ta := TouchArea { mouse-cursor: pointer; pointer-event(e) => { if (e.kind == PointerEventKind.down && e.button == PointerEventButton.right) { ctx-x = self.mouse-x; ctx-y = self.mouse-y; folder-menu.show(); } else if (e.kind == PointerEventKind.down && e.button == PointerEventButton.left) { root.toggle-folder(root.folder-name); } } } folder-menu := PopupWindow { x: ctx-x; y: ctx-y; width: 132px; height: 108px; Rectangle { background: Theme.bg-panel; border-radius: Theme.radius-sm; border-width: 1px; border-color: Theme.border-strong; drop-shadow-blur: 10px; drop-shadow-color: #00000050; VerticalLayout { padding: 4px; spacing: 2px; Rectangle { height: 28px; border-radius: Theme.radius-sm; background: new-folder-ta.has-hover ? Theme.bg-hover : transparent; new-folder-ta := TouchArea { mouse-cursor: pointer; clicked => { root.new-folder(); } } Text { x: 10px; width: parent.width - 20px; height: parent.height; text: @tr("New category"); color: Theme.text-primary; font-size: Theme.fs-md; vertical-alignment: center; } } Rectangle { height: 28px; border-radius: Theme.radius-sm; background: rename-folder-ta.has-hover ? Theme.bg-hover : transparent; rename-folder-ta := TouchArea { mouse-cursor: pointer; clicked => { root.rename-folder(root.folder-name); } } Text { x: 10px; width: parent.width - 20px; height: parent.height; text: @tr("Rename"); color: Theme.text-primary; font-size: Theme.fs-md; vertical-alignment: center; } } Rectangle { height: 28px; border-radius: Theme.radius-sm; background: delete-folder-ta.has-hover ? #cc333320 : transparent; delete-folder-ta := TouchArea { mouse-cursor: pointer; clicked => { root.delete-folder(root.folder-name); } } Text { x: 10px; width: parent.width - 20px; height: parent.height; text: @tr("Delete"); color: #cc3333; font-size: Theme.fs-md; vertical-alignment: center; } } } } } HorizontalLayout { width: parent.width; height: parent.height; spacing: 8px; padding-left: 10px; padding-right: 10px; Rectangle { width: 24px; height: parent.height; Text { width: parent.width; height: parent.height; text: root.expanded ? "\u{E5CF}" : "\u{E5CC}"; font-family: "Material Icons"; color: Theme.text-secondary; font-size: 16px; horizontal-alignment: center; vertical-alignment: center; } } Text { height: parent.height; text: root.folder-name; color: Theme.text-secondary; font-size: Theme.fs-sm; font-weight: 600; vertical-alignment: center; horizontal-stretch: 1; overflow: elide; } if root.debug-ids : Text { height: parent.height; text: "[folder-section]"; color: Theme.warning; font-size: Theme.fs-xs; vertical-alignment: center; } } } for session in root.sessions : FolderSessionSlot { width: parent.width; folder-name: root.folder-name; folder-expanded: root.expanded; session: session; folder-names: root.folder-names; debug-ids: root.debug-ids; connect-session(id) => { root.connect-session(id); } edit-session(id) => { root.edit-session(id); } remove-session(id) => { root.remove-session(id); } move-session-folder(id, folder) => { root.move-session-folder(id, folder); } } } // --- Welcome --------------------------------------------------------------- export component Welcome inherits Rectangle { in property app-background-active: false; in property ui-opacity: 0.72; in property <[SessionInfo]> sessions; in property <[ConnectionFolderInfo]> folders; in property <[string]> all-folder-names; in property import-hint; // result text after an import in property debug-ids: false; callback new-session(); callback new-folder(); callback rename-folder(string); callback delete-folder(string); callback toggle-folder(string); callback move-session-folder(string, string); callback import-ssh-config(); callback connect-session(string); callback edit-session(string); callback remove-session(string); background: root.app-background-active ? Theme.bg-root.with-alpha(root.ui-opacity) : Theme.bg-root; clip: true; Flickable { width: parent.width; height: parent.height; viewport-width: self.width; viewport-height: max(self.height, content.preferred-height); content := VerticalLayout { width: root.width; padding: 24px; spacing: 20px; VerticalLayout { spacing: 4px; Text { text: "meatshell"; color: Theme.text-primary; font-size: 28px; font-weight: 700; } Text { text: @tr("meatshell — a lightweight Rust + Slint SSH client by 'lil meatball'"); color: Theme.text-secondary; font-size: Theme.fs-md; wrap: word-wrap; } } HorizontalLayout { spacing: 8px; Rectangle { horizontal-stretch: 1; } PrimaryButton { text: @tr("New SSH"); clicked => { root.new-session(); } } } // Connection management Rectangle { background: Theme.bg-panel; border-radius: Theme.radius-md; border-width: 1px; border-color: Theme.border-subtle; height: max(240px, min(560px, root.height - 190px)); VerticalLayout { padding: 16px; spacing: 12px; HorizontalLayout { spacing: 8px; Text { text: @tr("Connection manager"); color: Theme.text-primary; font-size: Theme.fs-lg; font-weight: 600; vertical-alignment: center; horizontal-stretch: 1; } // Result of the last "Import ~/.ssh/config" (triggered from // the settings menu), e.g. "imported 3". Text { text: root.import-hint; color: Theme.text-muted; font-size: Theme.fs-sm; vertical-alignment: center; overflow: elide; } } Rectangle { vertical-stretch: 1; clip: true; background: transparent; if sessions.length == 0 : Rectangle { width: parent.width; height: parent.height; background: Theme.bg-root; border-radius: Theme.radius-sm; border-width: 1px; border-color: Theme.border-subtle; VerticalLayout { alignment: center; spacing: 6px; Text { text: @tr("No sessions yet"); color: Theme.text-secondary; font-size: Theme.fs-md; horizontal-alignment: center; } Text { text: @tr("Use the plus button above to add SSH, Serial, or Telnet"); color: Theme.text-muted; font-size: Theme.fs-sm; horizontal-alignment: center; wrap: word-wrap; } } } if root.folders.length > 0 : Flickable { width: parent.width; height: parent.height; viewport-width: self.width; viewport-height: max(self.height, folder-list.preferred-height); folder-list := VerticalLayout { width: parent.width; spacing: 4px; for folder in root.folders : FolderSection { width: parent.width; folder-name: folder.name; expanded: folder.expanded; sessions: root.sessions; folder-names: root.all-folder-names; debug-ids: root.debug-ids; connect-session(id) => { root.connect-session(id); } edit-session(id) => { root.edit-session(id); } remove-session(id) => { root.remove-session(id); } rename-folder(name) => { root.rename-folder(name); } delete-folder(name) => { root.delete-folder(name); } new-folder() => { root.new-folder(); } toggle-folder(name) => { root.toggle-folder(name); } move-session-folder(id, folder) => { root.move-session-folder(id, folder); } } } } } } } // Filler Rectangle { vertical-stretch: 1; } } } }