From 5e107500433cbe9afda7adcdec66697b873e2704 Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Sun, 10 May 2026 21:26:03 +1200 Subject: [PATCH] Add external editor/file picker, update theme Introduce file/application pickers and external-editor integration, plus a visual refresh. Frontend: add UI for selecting/rescanning installed IDEs, custom editor input, "Open in File Explorer" and "Open in Code Editor" actions, clone destination browse, helper utilities, new icons, and many CSS theme/UX improvements (variables, shadows, scrollbars, selection, refined component styles). State: track installedIdes and scan status. Tauri API: expose browseDirectory, browseApplication and scanInstalledIdes, and wire UI handlers to call them. Backend: add InstalledIde struct and update tauri Cargo manifest and capabilities to allow dialogs. Overall improves editor/workflow integrations and modernizes the app styling. --- frontend/css/base.css | 97 ++++++---- frontend/css/components.css | 248 ++++++++++++++++++------- frontend/js/app.js | 194 +++++++++++++++++++- frontend/js/state.js | 3 + frontend/js/tauri-api.js | 33 ++++ src-tauri/Cargo.lock | 68 +++++++ src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 1 + src-tauri/src/lib.rs | 274 ++++++++++++++++++++++++++++ 9 files changed, 815 insertions(+), 104 deletions(-) diff --git a/frontend/css/base.css b/frontend/css/base.css index db54002..d6f06f4 100644 --- a/frontend/css/base.css +++ b/frontend/css/base.css @@ -1,33 +1,52 @@ :root { - --bg-app: #1c2128; - --bg-panel: #22272e; - --bg-panel-alt: #2a3038; - --bg-hover: #2d3540; - --border: #1e242b; - --text-main: #cdd9e5; - --text-muted: #768390; - --accent: #58a6ff; - --accent-strong: #1f6feb; - --success: #3fb950; - --danger: #f85149; - --radius-md: 6px; - --radius-lg: 8px; - --shadow: 0 8px 24px rgba(1, 4, 9, 0.7); + /* Backgrounds – Gitpub Brand Palette */ + --bg-app: #0F1115; + --bg-panel: #171B21; + --bg-panel-alt: #1E242C; + --bg-panel-lift: #222931; + --bg-hover: #252D38; + + /* Borders */ + --border: #2C3440; + --border-subtle: #1F2730; + + /* Text */ + --text-main: #F3F2EE; + --text-muted: #9CA6B5; + + /* Accent – Amber Ale */ + --accent: #E5A13E; + --accent-strong: #C57B27; + --accent-glow: rgba(229, 161, 62, 0.18); + --accent-subtle: rgba(229, 161, 62, 0.09); + + /* Status */ + --success: #3FB950; + --danger: #F85149; + + /* Shape */ + --radius-md: 7px; + --radius-lg: 11px; + --shadow: 0 8px 24px rgba(0, 0, 0, 0.35); } :root[data-theme="light"] { - --bg-app: #f6f8fa; - --bg-panel: #ffffff; - --bg-panel-alt: #f0f3f6; - --bg-hover: #eaeef2; - --border: #d0d7de; - --text-main: #24292f; - --text-muted: #57606a; - --accent: #0969da; - --accent-strong: #0969da; - --success: #1a7f37; - --danger: #cf222e; - --shadow: 0 8px 24px rgba(31, 35, 40, 0.12); + --bg-app: #FDF8F0; + --bg-panel: #FFFFFF; + --bg-panel-alt: #F5EFE0; + --bg-panel-lift: #FEFCF8; + --bg-hover: #EDDFC8; + --border: #D4C4A0; + --border-subtle: #E8DFC8; + --text-main: #1A120A; + --text-muted: #6B5C40; + --accent: #C57B27; + --accent-strong: #A86020; + --accent-glow: rgba(197, 123, 39, 0.18); + --accent-subtle: rgba(197, 123, 39, 0.09); + --success: #16a34a; + --danger: #dc2626; + --shadow: 0 8px 28px rgba(197, 123, 39, 0.12); } * { @@ -61,17 +80,17 @@ button { color: var(--text-main); padding: 5px 12px; cursor: pointer; - transition: background 0.1s ease, border-color 0.1s ease, opacity 0.1s ease; + transition: background 0.12s ease, border-color 0.12s ease, opacity 0.12s ease, box-shadow 0.12s ease; font-size: 13px; } button:hover { background: var(--bg-hover); - border-color: #6e7681; + border-color: #4a5e78; } button:disabled { - opacity: 0.5; + opacity: 0.45; cursor: default; } @@ -81,36 +100,38 @@ button:disabled:hover { } button.primary { - background: #238636; + background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%); border-color: transparent; color: #ffffff; } button.primary:hover { - background: #2ea043; + opacity: 0.88; border-color: transparent; + box-shadow: 0 0 0 3px var(--accent-glow); } button.primary-blue { - background: var(--accent-strong); + background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%); border-color: transparent; color: #ffffff; } button.primary-blue:hover { - background: #388bfd; + opacity: 0.88; border-color: transparent; + box-shadow: 0 0 0 3px var(--accent-glow); } button.danger { border-color: rgba(248, 81, 73, 0.35); - color: #f85149; + color: var(--danger); background: transparent; } button.danger:hover { background: rgba(248, 81, 73, 0.1); - border-color: #f85149; + border-color: var(--danger); } input, @@ -120,10 +141,10 @@ textarea { padding: 5px 10px; border-radius: var(--radius-md); border: 1px solid var(--border); - background: rgba(0, 0, 0, 0.25); + background: rgba(0, 0, 0, 0.28); color: var(--text-main); font-size: 13px; - transition: border-color 0.1s ease, box-shadow 0.1s ease; + transition: border-color 0.12s ease, box-shadow 0.12s ease; } input:focus, @@ -131,7 +152,7 @@ select:focus, textarea:focus { outline: none; border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.12); + box-shadow: 0 0 0 3px var(--accent-glow); } textarea { diff --git a/frontend/css/components.css b/frontend/css/components.css index 44b8835..5b489a1 100644 --- a/frontend/css/components.css +++ b/frontend/css/components.css @@ -8,7 +8,7 @@ .layout { display: grid; grid-template-rows: 48px 1fr; - grid-template-columns: 272px minmax(0, 1fr); + grid-template-columns: 320px minmax(0, 1fr); width: 100%; height: 100%; } @@ -19,7 +19,7 @@ display: flex; flex-direction: column; background: var(--bg-panel); - border-right: 1px solid rgba(0, 0, 0, 0.35); + border-right: 1px solid var(--border); overflow: hidden; min-height: 0; grid-row: 2; @@ -61,6 +61,20 @@ justify-content: flex-end; } +.gd-path-picker { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; +} + +.gd-editor-picker { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; +} + /* ── Typography ───────────────────────────────────────────────────────────── */ .muted { @@ -141,8 +155,8 @@ align-items: stretch; grid-column: 1 / -1; grid-row: 1; - background: var(--bg-panel); - border-bottom: 1px solid rgba(0, 0, 0, 0.35); + background: linear-gradient(180deg, #222931 0%, var(--bg-panel) 100%); + border-bottom: 1px solid var(--border); flex-shrink: 0; } @@ -150,9 +164,9 @@ .gd-toolbar-left { display: grid; grid-template-columns: 1fr 1fr; - width: 272px; + width: 320px; flex-shrink: 0; - border-right: 1px solid rgba(255, 255, 255, 0.05); + border-right: 1px solid rgba(229, 161, 62, 0.12); } /* Center section – grows */ @@ -169,10 +183,42 @@ align-items: center; gap: 10px; padding: 0 14px; - border-left: 1px solid rgba(255, 255, 255, 0.05); + border-left: 1px solid rgba(229, 161, 62, 0.12); flex-shrink: 0; } +.gd-external-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.gd-icon-action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + padding: 0; + border: 1px solid transparent; + border-radius: var(--radius-md); + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: background 0.1s, color 0.1s, border-color 0.1s; +} + +.gd-icon-action:hover:not(:disabled) { + background: var(--bg-hover); + border-color: var(--border); + color: var(--text-main); +} + +.gd-icon-action:disabled { + opacity: 0.45; + cursor: default; +} + /* Shared toolbar cell (repo + branch buttons) */ .gd-toolbar-cell { display: grid; @@ -192,11 +238,11 @@ } .gd-repo-cell { - border-right: 1px solid rgba(255, 255, 255, 0.05); + border-right: 1px solid rgba(229, 161, 62, 0.12); } .gd-toolbar-cell:hover:not(:disabled) { - background: rgba(177, 186, 196, 0.07); + background: rgba(229, 161, 62, 0.07); } .gd-toolbar-cell:disabled { @@ -275,7 +321,8 @@ .gd-sync-btn:hover:not(:disabled) { background: var(--bg-hover); - border-color: #6e7681; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-glow); } .gd-sync-btn:disabled { @@ -338,7 +385,7 @@ display: flex; background: var(--bg-app); border: 1px solid var(--border); - border-radius: 6px; + border-radius: 8px; padding: 2px; gap: 1px; } @@ -346,13 +393,13 @@ .gd-view-toggle button { border: none; background: transparent; - border-radius: 4px; + border-radius: 6px; font-size: 12px; font-weight: 500; padding: 4px 12px; color: var(--text-muted); cursor: pointer; - transition: background 0.1s, color 0.1s; + transition: background 0.12s, color 0.12s, box-shadow 0.12s; height: 26px; } @@ -363,9 +410,10 @@ } .gd-view-toggle button.active { - background: var(--bg-panel); - color: var(--text-main); + background: var(--accent-subtle); + color: var(--accent); font-weight: 600; + box-shadow: 0 0 0 1px var(--accent-glow); } /* ── Utility menu ─────────────────────────────────────────────────────────── */ @@ -430,6 +478,15 @@ border-color: transparent; } +.gd-utility-menu-item:disabled { + opacity: 0.45; + cursor: default; +} + +.gd-utility-menu-item:disabled:hover { + background: transparent; +} + .gd-utility-menu-item svg { color: var(--text-muted); flex-shrink: 0; @@ -488,7 +545,8 @@ } .branch-menu-item.active { - background: var(--bg-hover); + background: var(--accent-subtle); + color: var(--accent); } .branch-menu-item:hover, @@ -541,7 +599,7 @@ grid-template-columns: 32px 1fr; gap: 5px; padding: 7px 8px 6px; - border-bottom: 1px solid rgba(0, 0, 0, 0.25); + border-bottom: 1px solid var(--border-subtle); flex-shrink: 0; } @@ -571,13 +629,14 @@ height: 26px; padding: 3px 8px; font-size: 12px; - border-color: rgba(110, 118, 129, 0.4); + border-color: var(--border); border-radius: 5px; background: rgba(0, 0, 0, 0.2); } .gd-filter-input:focus { border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-glow); } .gd-changes-filter-row { @@ -587,7 +646,7 @@ gap: 8px; min-height: 28px; padding: 4px 10px; - border-bottom: 1px solid rgba(0, 0, 0, 0.2); + border-bottom: 1px solid var(--border-subtle); flex-shrink: 0; } @@ -603,7 +662,7 @@ .gd-check-all input[type="checkbox"] { width: 13px; padding: 0; - accent-color: #6e7681; + accent-color: var(--accent); } .gd-changes-list { @@ -634,15 +693,20 @@ text-align: left; } -.change-file-row:hover, -.change-file-row.active { +.change-file-row:hover { background: var(--bg-hover); } +.change-file-row.active { + background: var(--accent-subtle); + border-left: 2px solid var(--accent); + padding-left: 10px; +} + .change-file-checkbox { width: 13px; padding: 0; - accent-color: #6e7681; + accent-color: var(--accent); } .change-status { @@ -695,19 +759,24 @@ padding: 7px 12px; border: 0; border-radius: 0; - border-bottom: 1px solid rgba(48, 54, 61, 0.4); + border-bottom: 1px solid var(--border-subtle); background: transparent; text-align: left; cursor: pointer; color: var(--text-main); - transition: background 0.08s; + transition: background 0.1s; } -.gd-history-item:hover, -.gd-history-item.active { +.gd-history-item:hover { background: var(--bg-hover); } +.gd-history-item.active { + background: var(--accent-subtle); + border-left: 2px solid var(--accent); + padding-left: 10px; +} + .gd-history-info { min-width: 0; display: flex; @@ -736,7 +805,7 @@ .gd-commit-area { flex-shrink: 0; padding: 10px; - border-top: 1px solid rgba(0, 0, 0, 0.3); + border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 6px; @@ -759,12 +828,21 @@ width: 100%; height: 30px; font-size: 12px; - background: #0969da !important; - border-color: #0969da !important; + font-weight: 600; + background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%) !important; + border-color: transparent !important; + color: #fff !important; + letter-spacing: 0.01em; + transition: opacity 0.15s ease, box-shadow 0.15s ease !important; +} + +.gd-commit-btn:hover:not(:disabled) { + opacity: 0.9; + box-shadow: 0 0 0 3px var(--accent-glow) !important; } .gd-commit-btn:disabled { - opacity: 0.5; + opacity: 0.4; } /* ── Diff view ────────────────────────────────────────────────────────────── */ @@ -774,7 +852,7 @@ border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: auto; - background: #010409; + background: #0B0D11; } .diff-preview-inline { @@ -787,7 +865,7 @@ display: grid; grid-template-columns: 32px minmax(0, 1fr); min-height: 20px; - color: #c9d1d9; + color: #E6EDF3; } .diff-line code { @@ -797,18 +875,18 @@ .diff-gutter { padding: 2px 8px; - color: #6e7681; + color: #7D8590; text-align: center; user-select: none; - border-right: 1px solid rgba(110, 118, 129, 0.18); + border-right: 1px solid rgba(125, 133, 144, 0.18); } -.diff-line-added { background: rgba(46, 160, 67, 0.14); } -.diff-line-added .diff-gutter { color: var(--success); background: rgba(46, 160, 67, 0.18); } -.diff-line-removed { background: rgba(248, 81, 73, 0.13); } -.diff-line-removed .diff-gutter { color: var(--danger); background: rgba(248, 81, 73, 0.18); } -.diff-line-hunk { color: #a5d6ff; background: rgba(56, 139, 253, 0.14); } -.diff-line-meta { color: var(--text-muted); background: rgba(110, 118, 129, 0.07); } +.diff-line-added { background: rgba(63, 185, 80, 0.1); } +.diff-line-added .diff-gutter { color: var(--success); background: rgba(63, 185, 80, 0.14); } +.diff-line-removed { background: rgba(248, 81, 73, 0.1); } +.diff-line-removed .diff-gutter { color: var(--danger); background: rgba(248, 81, 73, 0.15); } +.diff-line-hunk { color: var(--accent); background: var(--accent-subtle); } +.diff-line-meta { color: var(--text-muted); background: rgba(156, 166, 181, 0.06); } /* ── Workflow empty state ─────────────────────────────────────────────────── */ @@ -829,8 +907,9 @@ display: grid; place-items: center; border-radius: 20px; - background: var(--bg-panel-alt); - color: var(--text-muted); + background: var(--accent-subtle); + color: var(--accent); + border: 1px solid var(--accent-glow); } .workflow-empty-icon svg { @@ -908,7 +987,8 @@ display: grid; place-items: center; padding: 20px; - background: rgba(1, 4, 9, 0.62); + background: rgba(8, 10, 14, 0.75); + backdrop-filter: blur(2px); } .gd-modal { @@ -1034,7 +1114,7 @@ justify-content: space-between; gap: 10px; padding: 8px 0; - border-bottom: 1px solid rgba(48, 54, 61, 0.5); + border-bottom: 1px solid var(--border-subtle); } .gd-modal-list-item:last-child { @@ -1136,7 +1216,7 @@ .gd-server-active { border-color: var(--accent); - background: rgba(47, 129, 247, 0.06); + background: var(--accent-subtle); } .gd-server-name { @@ -1191,7 +1271,8 @@ display: grid; place-items: center; padding: 24px; - background: rgba(1, 4, 9, 0.58); + background: rgba(8, 10, 14, 0.72); + backdrop-filter: blur(2px); } .modal-card { @@ -1219,10 +1300,10 @@ .danger-note { padding: 10px 12px; - border: 1px solid rgba(248, 81, 73, 0.35); + border: 1px solid rgba(248, 81, 73, 0.3); border-radius: var(--radius-md); color: var(--danger); - background: rgba(248, 81, 73, 0.08); + background: rgba(248, 81, 73, 0.07); font-size: 13px; } @@ -1269,7 +1350,7 @@ padding: 7px 12px; border: 0; border-radius: 0; - border-bottom: 1px solid rgba(48, 54, 61, 0.4); + border-bottom: 1px solid var(--border-subtle); background: var(--bg-panel); text-align: left; color: var(--text-main); @@ -1287,7 +1368,7 @@ } .viewer-row.active { - background: rgba(47, 129, 247, 0.07); + background: var(--accent-subtle); } .viewer-row-icon { @@ -1417,8 +1498,11 @@ .markdown-body blockquote { margin: 0 0 12px; padding: 0 12px; - border-left: 3px solid var(--border); + border-left: 3px solid var(--accent); color: var(--text-muted); + background: var(--accent-subtle); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + padding: 4px 12px; } .markdown-body hr { height: 1px; @@ -1440,7 +1524,7 @@ padding: 12px; overflow: auto; border-radius: var(--radius-md); - background: #010409; + background: #0B0D11; } .markdown-body .markdown-code code { padding: 0; @@ -1456,21 +1540,24 @@ margin: 0; padding: 14px 16px; overflow: auto; - background: #010409; - color: var(--text-main); + background: #0B0D11; + color: #E6EDF3; font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace; font-size: 12px; line-height: 1.7; white-space: pre; } -.syntax-keyword { color: #ff7b72; } -.syntax-key { color: #79c0ff; } +.syntax-keyword { color: #FF7B72; } +.syntax-string { color: #A5D6FF; } +.syntax-comment { color: #7D8590; } +.syntax-number { color: #79C0FF; } +.syntax-key { color: #D2A8FF; } /* ── Git output ───────────────────────────────────────────────────────────── */ .git-output { - background: #010409; + background: #0B0D11; border: 1px solid var(--border); border-radius: var(--radius-md); padding: 10px 12px; @@ -1508,11 +1595,11 @@ @media (max-width: 860px) { .layout { - grid-template-columns: 240px minmax(0, 1fr); + grid-template-columns: 272px minmax(0, 1fr); } .gd-toolbar-left { - width: 240px; + width: 272px; } .gd-modal-two-col { @@ -1531,15 +1618,52 @@ @media (max-width: 640px) { .layout { - grid-template-columns: 220px minmax(0, 1fr); + grid-template-columns: 240px minmax(0, 1fr); } .gd-toolbar-left { - width: 220px; + width: 240px; grid-template-columns: 1fr; } .gd-branch-wrap { display: none; } + + /* intentional—branch picker hidden at narrow widths */ +} + +/* ── Custom scrollbars ────────────────────────────────────────────────────── */ + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(229, 161, 62, 0.22); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(229, 161, 62, 0.42); +} + +:root[data-theme="light"] ::-webkit-scrollbar-thumb { + background: rgba(197, 123, 39, 0.2); +} + +:root[data-theme="light"] ::-webkit-scrollbar-thumb:hover { + background: rgba(197, 123, 39, 0.38); +} + +/* ── Accent selection highlight ───────────────────────────────────────────── */ + +::selection { + background: var(--accent-glow); + color: var(--text-main); } diff --git a/frontend/js/app.js b/frontend/js/app.js index ca0b63f..22bb565 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -1,6 +1,8 @@ import { fetchCurrentUser, fetchRepoBranches, fetchRepoContents, fetchRepositories } from "./gitea-api.js"; import { getActiveServer, getState, setSettings, updateSettings, addRecentRepo } from "./state.js"; import { + browseApplication, + browseDirectory, checkoutBranch, commitChanges, createBranch, @@ -24,6 +26,7 @@ import { runGitPush, runGitSync, runGitStatus, + scanInstalledIdes, scanLocalRepos, testGiteaConnection, } from "./tauri-api.js"; @@ -50,6 +53,8 @@ let repoOwnerFilter = "all"; const maxPreviewBytes = 256 * 1024; const defaultRepositoryName = "Gitpub-Desktop"; const defaultBranchName = "main"; +const DEFAULT_EDITOR_VALUE = "__default_code__"; +const CUSTOM_EDITOR_VALUE = "__custom__"; const FOLDER_ICON = ``; const FILE_ICON = ``; @@ -59,6 +64,8 @@ const SYNC_ICON = ``; const PUSH_ICON = ``; const PUBLISH_ICON = ``; +const EXPLORER_ICON = ``; +const EDITOR_ICON = ``; function uid() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; @@ -89,6 +96,17 @@ function repoNameFromPath(path = "") { return path.split(/[/\\]/).filter(Boolean).pop() || path; } +function repoNameFromUrl(url = "") { + const cleanUrl = url.trim().split(/[?#]/)[0].replace(/\/+$/, "").replace(/\.git$/i, ""); + return cleanUrl.split(/[/\\:]/).filter(Boolean).pop() || ""; +} + +function joinDirectoryPath(parent = "", child = "") { + if (!parent || !child) return parent || child; + const separator = parent.includes("\\") ? "\\" : "/"; + return `${parent.replace(/[\\/]+$/, "")}${separator}${child}`; +} + function currentRepositoryName() { return getState().selectedRepoName || defaultRepositoryName; } @@ -121,6 +139,31 @@ function selectedGitPath() { return getState().settings.gitExecutablePath; } +function isDetectedEditorPath(value = "", detectedEditors = []) { + const normalizedValue = value.trim().toLowerCase(); + return detectedEditors.some((editor) => editor.executablePath.toLowerCase() === normalizedValue); +} + +function selectedEditorDropdownValue(state) { + const editorPath = state.settings.externalEditorPath?.trim() || ""; + if (!editorPath) return DEFAULT_EDITOR_VALUE; + return isDetectedEditorPath(editorPath, state.installedIdes) ? editorPath : CUSTOM_EDITOR_VALUE; +} + +function externalEditorOptionsTemplate(state) { + const editorPath = state.settings.externalEditorPath?.trim() || ""; + const selectedValue = selectedEditorDropdownValue(state); + const detectedOptions = state.installedIdes + .map((editor) => ``) + .join(""); + + return ` + + ${detectedOptions} + + `; +} + function statusLabel(status = "") { const labels = { modified: "Modified", @@ -866,7 +909,11 @@ function cloneModalContent(state) {
Destination path
- +
+ + +
+ Choose a parent folder and Gitpub will create the repository folder inside it.
${gitOutput @@ -907,6 +954,8 @@ function serversModalContent(state) { } function settingsModalContent(state) { + const editorDropdownValue = selectedEditorDropdownValue(state); + const customEditorPath = editorDropdownValue === CUSTOM_EDITOR_VALUE ? state.settings.externalEditorPath?.trim() || "" : ""; return `
@@ -925,8 +974,19 @@ function settingsModalContent(state) {
Default clone directory
-
External editor command
- +
Code editor
+
+ + +
+ ${state.installedIdeScanError ? `
${escapeHtml(state.installedIdeScanError)}
` : ""} + ${!state.installedIdeScanLoading && !state.installedIdes.length ? `
No installed IDEs detected yet. You can use the default code command or choose a custom app.
` : ""} +
+ + +
@@ -1119,6 +1179,14 @@ function dashboardView() {
+
+ + +
@@ -1141,6 +1209,14 @@ function dashboardView() { File Viewer + +