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.
This commit is contained in:
2026-05-10 21:26:03 +12:00
parent ac7fc231a0
commit 5e10750043
9 changed files with 815 additions and 104 deletions
+59 -38
View File
@@ -1,33 +1,52 @@
:root { :root {
--bg-app: #1c2128; /* Backgrounds Gitpub Brand Palette */
--bg-panel: #22272e; --bg-app: #0F1115;
--bg-panel-alt: #2a3038; --bg-panel: #171B21;
--bg-hover: #2d3540; --bg-panel-alt: #1E242C;
--border: #1e242b; --bg-panel-lift: #222931;
--text-main: #cdd9e5; --bg-hover: #252D38;
--text-muted: #768390;
--accent: #58a6ff; /* Borders */
--accent-strong: #1f6feb; --border: #2C3440;
--success: #3fb950; --border-subtle: #1F2730;
--danger: #f85149;
--radius-md: 6px; /* Text */
--radius-lg: 8px; --text-main: #F3F2EE;
--shadow: 0 8px 24px rgba(1, 4, 9, 0.7); --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"] { :root[data-theme="light"] {
--bg-app: #f6f8fa; --bg-app: #FDF8F0;
--bg-panel: #ffffff; --bg-panel: #FFFFFF;
--bg-panel-alt: #f0f3f6; --bg-panel-alt: #F5EFE0;
--bg-hover: #eaeef2; --bg-panel-lift: #FEFCF8;
--border: #d0d7de; --bg-hover: #EDDFC8;
--text-main: #24292f; --border: #D4C4A0;
--text-muted: #57606a; --border-subtle: #E8DFC8;
--accent: #0969da; --text-main: #1A120A;
--accent-strong: #0969da; --text-muted: #6B5C40;
--success: #1a7f37; --accent: #C57B27;
--danger: #cf222e; --accent-strong: #A86020;
--shadow: 0 8px 24px rgba(31, 35, 40, 0.12); --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); color: var(--text-main);
padding: 5px 12px; padding: 5px 12px;
cursor: pointer; 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; font-size: 13px;
} }
button:hover { button:hover {
background: var(--bg-hover); background: var(--bg-hover);
border-color: #6e7681; border-color: #4a5e78;
} }
button:disabled { button:disabled {
opacity: 0.5; opacity: 0.45;
cursor: default; cursor: default;
} }
@@ -81,36 +100,38 @@ button:disabled:hover {
} }
button.primary { button.primary {
background: #238636; background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%);
border-color: transparent; border-color: transparent;
color: #ffffff; color: #ffffff;
} }
button.primary:hover { button.primary:hover {
background: #2ea043; opacity: 0.88;
border-color: transparent; border-color: transparent;
box-shadow: 0 0 0 3px var(--accent-glow);
} }
button.primary-blue { button.primary-blue {
background: var(--accent-strong); background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%);
border-color: transparent; border-color: transparent;
color: #ffffff; color: #ffffff;
} }
button.primary-blue:hover { button.primary-blue:hover {
background: #388bfd; opacity: 0.88;
border-color: transparent; border-color: transparent;
box-shadow: 0 0 0 3px var(--accent-glow);
} }
button.danger { button.danger {
border-color: rgba(248, 81, 73, 0.35); border-color: rgba(248, 81, 73, 0.35);
color: #f85149; color: var(--danger);
background: transparent; background: transparent;
} }
button.danger:hover { button.danger:hover {
background: rgba(248, 81, 73, 0.1); background: rgba(248, 81, 73, 0.1);
border-color: #f85149; border-color: var(--danger);
} }
input, input,
@@ -120,10 +141,10 @@ textarea {
padding: 5px 10px; padding: 5px 10px;
border-radius: var(--radius-md); border-radius: var(--radius-md);
border: 1px solid var(--border); border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.25); background: rgba(0, 0, 0, 0.28);
color: var(--text-main); color: var(--text-main);
font-size: 13px; 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, input:focus,
@@ -131,7 +152,7 @@ select:focus,
textarea:focus { textarea:focus {
outline: none; outline: none;
border-color: var(--accent); 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 { textarea {
+186 -62
View File
@@ -8,7 +8,7 @@
.layout { .layout {
display: grid; display: grid;
grid-template-rows: 48px 1fr; grid-template-rows: 48px 1fr;
grid-template-columns: 272px minmax(0, 1fr); grid-template-columns: 320px minmax(0, 1fr);
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@@ -19,7 +19,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--bg-panel); background: var(--bg-panel);
border-right: 1px solid rgba(0, 0, 0, 0.35); border-right: 1px solid var(--border);
overflow: hidden; overflow: hidden;
min-height: 0; min-height: 0;
grid-row: 2; grid-row: 2;
@@ -61,6 +61,20 @@
justify-content: flex-end; 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 ───────────────────────────────────────────────────────────── */ /* ── Typography ───────────────────────────────────────────────────────────── */
.muted { .muted {
@@ -141,8 +155,8 @@
align-items: stretch; align-items: stretch;
grid-column: 1 / -1; grid-column: 1 / -1;
grid-row: 1; grid-row: 1;
background: var(--bg-panel); background: linear-gradient(180deg, #222931 0%, var(--bg-panel) 100%);
border-bottom: 1px solid rgba(0, 0, 0, 0.35); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -150,9 +164,9 @@
.gd-toolbar-left { .gd-toolbar-left {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
width: 272px; width: 320px;
flex-shrink: 0; 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 */ /* Center section grows */
@@ -169,10 +183,42 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 0 14px; 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; 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) */ /* Shared toolbar cell (repo + branch buttons) */
.gd-toolbar-cell { .gd-toolbar-cell {
display: grid; display: grid;
@@ -192,11 +238,11 @@
} }
.gd-repo-cell { .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) { .gd-toolbar-cell:hover:not(:disabled) {
background: rgba(177, 186, 196, 0.07); background: rgba(229, 161, 62, 0.07);
} }
.gd-toolbar-cell:disabled { .gd-toolbar-cell:disabled {
@@ -275,7 +321,8 @@
.gd-sync-btn:hover:not(:disabled) { .gd-sync-btn:hover:not(:disabled) {
background: var(--bg-hover); background: var(--bg-hover);
border-color: #6e7681; border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-glow);
} }
.gd-sync-btn:disabled { .gd-sync-btn:disabled {
@@ -338,7 +385,7 @@
display: flex; display: flex;
background: var(--bg-app); background: var(--bg-app);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 8px;
padding: 2px; padding: 2px;
gap: 1px; gap: 1px;
} }
@@ -346,13 +393,13 @@
.gd-view-toggle button { .gd-view-toggle button {
border: none; border: none;
background: transparent; background: transparent;
border-radius: 4px; border-radius: 6px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
padding: 4px 12px; padding: 4px 12px;
color: var(--text-muted); color: var(--text-muted);
cursor: pointer; cursor: pointer;
transition: background 0.1s, color 0.1s; transition: background 0.12s, color 0.12s, box-shadow 0.12s;
height: 26px; height: 26px;
} }
@@ -363,9 +410,10 @@
} }
.gd-view-toggle button.active { .gd-view-toggle button.active {
background: var(--bg-panel); background: var(--accent-subtle);
color: var(--text-main); color: var(--accent);
font-weight: 600; font-weight: 600;
box-shadow: 0 0 0 1px var(--accent-glow);
} }
/* ── Utility menu ─────────────────────────────────────────────────────────── */ /* ── Utility menu ─────────────────────────────────────────────────────────── */
@@ -430,6 +478,15 @@
border-color: transparent; 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 { .gd-utility-menu-item svg {
color: var(--text-muted); color: var(--text-muted);
flex-shrink: 0; flex-shrink: 0;
@@ -488,7 +545,8 @@
} }
.branch-menu-item.active { .branch-menu-item.active {
background: var(--bg-hover); background: var(--accent-subtle);
color: var(--accent);
} }
.branch-menu-item:hover, .branch-menu-item:hover,
@@ -541,7 +599,7 @@
grid-template-columns: 32px 1fr; grid-template-columns: 32px 1fr;
gap: 5px; gap: 5px;
padding: 7px 8px 6px; padding: 7px 8px 6px;
border-bottom: 1px solid rgba(0, 0, 0, 0.25); border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -571,13 +629,14 @@
height: 26px; height: 26px;
padding: 3px 8px; padding: 3px 8px;
font-size: 12px; font-size: 12px;
border-color: rgba(110, 118, 129, 0.4); border-color: var(--border);
border-radius: 5px; border-radius: 5px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
} }
.gd-filter-input:focus { .gd-filter-input:focus {
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-glow);
} }
.gd-changes-filter-row { .gd-changes-filter-row {
@@ -587,7 +646,7 @@
gap: 8px; gap: 8px;
min-height: 28px; min-height: 28px;
padding: 4px 10px; padding: 4px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.2); border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -603,7 +662,7 @@
.gd-check-all input[type="checkbox"] { .gd-check-all input[type="checkbox"] {
width: 13px; width: 13px;
padding: 0; padding: 0;
accent-color: #6e7681; accent-color: var(--accent);
} }
.gd-changes-list { .gd-changes-list {
@@ -634,15 +693,20 @@
text-align: left; text-align: left;
} }
.change-file-row:hover, .change-file-row:hover {
.change-file-row.active {
background: var(--bg-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 { .change-file-checkbox {
width: 13px; width: 13px;
padding: 0; padding: 0;
accent-color: #6e7681; accent-color: var(--accent);
} }
.change-status { .change-status {
@@ -695,19 +759,24 @@
padding: 7px 12px; padding: 7px 12px;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
border-bottom: 1px solid rgba(48, 54, 61, 0.4); border-bottom: 1px solid var(--border-subtle);
background: transparent; background: transparent;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
color: var(--text-main); color: var(--text-main);
transition: background 0.08s; transition: background 0.1s;
} }
.gd-history-item:hover, .gd-history-item:hover {
.gd-history-item.active {
background: var(--bg-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 { .gd-history-info {
min-width: 0; min-width: 0;
display: flex; display: flex;
@@ -736,7 +805,7 @@
.gd-commit-area { .gd-commit-area {
flex-shrink: 0; flex-shrink: 0;
padding: 10px; padding: 10px;
border-top: 1px solid rgba(0, 0, 0, 0.3); border-top: 1px solid var(--border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
@@ -759,12 +828,21 @@
width: 100%; width: 100%;
height: 30px; height: 30px;
font-size: 12px; font-size: 12px;
background: #0969da !important; font-weight: 600;
border-color: #0969da !important; 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 { .gd-commit-btn:disabled {
opacity: 0.5; opacity: 0.4;
} }
/* ── Diff view ────────────────────────────────────────────────────────────── */ /* ── Diff view ────────────────────────────────────────────────────────────── */
@@ -774,7 +852,7 @@
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
overflow: auto; overflow: auto;
background: #010409; background: #0B0D11;
} }
.diff-preview-inline { .diff-preview-inline {
@@ -787,7 +865,7 @@
display: grid; display: grid;
grid-template-columns: 32px minmax(0, 1fr); grid-template-columns: 32px minmax(0, 1fr);
min-height: 20px; min-height: 20px;
color: #c9d1d9; color: #E6EDF3;
} }
.diff-line code { .diff-line code {
@@ -797,18 +875,18 @@
.diff-gutter { .diff-gutter {
padding: 2px 8px; padding: 2px 8px;
color: #6e7681; color: #7D8590;
text-align: center; text-align: center;
user-select: none; 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 { background: rgba(63, 185, 80, 0.1); }
.diff-line-added .diff-gutter { color: var(--success); background: rgba(46, 160, 67, 0.18); } .diff-line-added .diff-gutter { color: var(--success); background: rgba(63, 185, 80, 0.14); }
.diff-line-removed { background: rgba(248, 81, 73, 0.13); } .diff-line-removed { background: rgba(248, 81, 73, 0.1); }
.diff-line-removed .diff-gutter { color: var(--danger); background: rgba(248, 81, 73, 0.18); } .diff-line-removed .diff-gutter { color: var(--danger); background: rgba(248, 81, 73, 0.15); }
.diff-line-hunk { color: #a5d6ff; background: rgba(56, 139, 253, 0.14); } .diff-line-hunk { color: var(--accent); background: var(--accent-subtle); }
.diff-line-meta { color: var(--text-muted); background: rgba(110, 118, 129, 0.07); } .diff-line-meta { color: var(--text-muted); background: rgba(156, 166, 181, 0.06); }
/* ── Workflow empty state ─────────────────────────────────────────────────── */ /* ── Workflow empty state ─────────────────────────────────────────────────── */
@@ -829,8 +907,9 @@
display: grid; display: grid;
place-items: center; place-items: center;
border-radius: 20px; border-radius: 20px;
background: var(--bg-panel-alt); background: var(--accent-subtle);
color: var(--text-muted); color: var(--accent);
border: 1px solid var(--accent-glow);
} }
.workflow-empty-icon svg { .workflow-empty-icon svg {
@@ -908,7 +987,8 @@
display: grid; display: grid;
place-items: center; place-items: center;
padding: 20px; padding: 20px;
background: rgba(1, 4, 9, 0.62); background: rgba(8, 10, 14, 0.75);
backdrop-filter: blur(2px);
} }
.gd-modal { .gd-modal {
@@ -1034,7 +1114,7 @@
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
padding: 8px 0; 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 { .gd-modal-list-item:last-child {
@@ -1136,7 +1216,7 @@
.gd-server-active { .gd-server-active {
border-color: var(--accent); border-color: var(--accent);
background: rgba(47, 129, 247, 0.06); background: var(--accent-subtle);
} }
.gd-server-name { .gd-server-name {
@@ -1191,7 +1271,8 @@
display: grid; display: grid;
place-items: center; place-items: center;
padding: 24px; padding: 24px;
background: rgba(1, 4, 9, 0.58); background: rgba(8, 10, 14, 0.72);
backdrop-filter: blur(2px);
} }
.modal-card { .modal-card {
@@ -1219,10 +1300,10 @@
.danger-note { .danger-note {
padding: 10px 12px; 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); border-radius: var(--radius-md);
color: var(--danger); color: var(--danger);
background: rgba(248, 81, 73, 0.08); background: rgba(248, 81, 73, 0.07);
font-size: 13px; font-size: 13px;
} }
@@ -1269,7 +1350,7 @@
padding: 7px 12px; padding: 7px 12px;
border: 0; border: 0;
border-radius: 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); background: var(--bg-panel);
text-align: left; text-align: left;
color: var(--text-main); color: var(--text-main);
@@ -1287,7 +1368,7 @@
} }
.viewer-row.active { .viewer-row.active {
background: rgba(47, 129, 247, 0.07); background: var(--accent-subtle);
} }
.viewer-row-icon { .viewer-row-icon {
@@ -1417,8 +1498,11 @@
.markdown-body blockquote { .markdown-body blockquote {
margin: 0 0 12px; margin: 0 0 12px;
padding: 0 12px; padding: 0 12px;
border-left: 3px solid var(--border); border-left: 3px solid var(--accent);
color: var(--text-muted); color: var(--text-muted);
background: var(--accent-subtle);
border-radius: 0 var(--radius-md) var(--radius-md) 0;
padding: 4px 12px;
} }
.markdown-body hr { .markdown-body hr {
height: 1px; height: 1px;
@@ -1440,7 +1524,7 @@
padding: 12px; padding: 12px;
overflow: auto; overflow: auto;
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: #010409; background: #0B0D11;
} }
.markdown-body .markdown-code code { .markdown-body .markdown-code code {
padding: 0; padding: 0;
@@ -1456,21 +1540,24 @@
margin: 0; margin: 0;
padding: 14px 16px; padding: 14px 16px;
overflow: auto; overflow: auto;
background: #010409; background: #0B0D11;
color: var(--text-main); color: #E6EDF3;
font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace; font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace;
font-size: 12px; font-size: 12px;
line-height: 1.7; line-height: 1.7;
white-space: pre; white-space: pre;
} }
.syntax-keyword { color: #ff7b72; } .syntax-keyword { color: #FF7B72; }
.syntax-key { color: #79c0ff; } .syntax-string { color: #A5D6FF; }
.syntax-comment { color: #7D8590; }
.syntax-number { color: #79C0FF; }
.syntax-key { color: #D2A8FF; }
/* ── Git output ───────────────────────────────────────────────────────────── */ /* ── Git output ───────────────────────────────────────────────────────────── */
.git-output { .git-output {
background: #010409; background: #0B0D11;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
padding: 10px 12px; padding: 10px 12px;
@@ -1508,11 +1595,11 @@
@media (max-width: 860px) { @media (max-width: 860px) {
.layout { .layout {
grid-template-columns: 240px minmax(0, 1fr); grid-template-columns: 272px minmax(0, 1fr);
} }
.gd-toolbar-left { .gd-toolbar-left {
width: 240px; width: 272px;
} }
.gd-modal-two-col { .gd-modal-two-col {
@@ -1531,15 +1618,52 @@
@media (max-width: 640px) { @media (max-width: 640px) {
.layout { .layout {
grid-template-columns: 220px minmax(0, 1fr); grid-template-columns: 240px minmax(0, 1fr);
} }
.gd-toolbar-left { .gd-toolbar-left {
width: 220px; width: 240px;
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.gd-branch-wrap { .gd-branch-wrap {
display: none; 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);
} }
+189 -3
View File
@@ -1,6 +1,8 @@
import { fetchCurrentUser, fetchRepoBranches, fetchRepoContents, fetchRepositories } from "./gitea-api.js"; import { fetchCurrentUser, fetchRepoBranches, fetchRepoContents, fetchRepositories } from "./gitea-api.js";
import { getActiveServer, getState, setSettings, updateSettings, addRecentRepo } from "./state.js"; import { getActiveServer, getState, setSettings, updateSettings, addRecentRepo } from "./state.js";
import { import {
browseApplication,
browseDirectory,
checkoutBranch, checkoutBranch,
commitChanges, commitChanges,
createBranch, createBranch,
@@ -24,6 +26,7 @@ import {
runGitPush, runGitPush,
runGitSync, runGitSync,
runGitStatus, runGitStatus,
scanInstalledIdes,
scanLocalRepos, scanLocalRepos,
testGiteaConnection, testGiteaConnection,
} from "./tauri-api.js"; } from "./tauri-api.js";
@@ -50,6 +53,8 @@ let repoOwnerFilter = "all";
const maxPreviewBytes = 256 * 1024; const maxPreviewBytes = 256 * 1024;
const defaultRepositoryName = "Gitpub-Desktop"; const defaultRepositoryName = "Gitpub-Desktop";
const defaultBranchName = "main"; const defaultBranchName = "main";
const DEFAULT_EDITOR_VALUE = "__default_code__";
const CUSTOM_EDITOR_VALUE = "__custom__";
const FOLDER_ICON = `<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path fill="#54aeff" d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/></svg>`; const FOLDER_ICON = `<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path fill="#54aeff" d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/></svg>`;
const FILE_ICON = `<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path fill="#848d97" d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></svg>`; const FILE_ICON = `<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path fill="#848d97" d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></svg>`;
@@ -59,6 +64,8 @@ const SYNC_ICON = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="
const PULL_ICON = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M7.25 1.75a.75.75 0 0 1 1.5 0v8.69l2.72-2.72a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734l-4 4a.75.75 0 0 1-1.06 0l-4-4A.749.749 0 0 1 4.53 7.72l2.72 2.72V1.75ZM2.75 14a.75.75 0 0 1 .75-.75h9a.75.75 0 0 1 0 1.5h-9a.75.75 0 0 1-.75-.75Z"/></svg>`; const PULL_ICON = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M7.25 1.75a.75.75 0 0 1 1.5 0v8.69l2.72-2.72a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734l-4 4a.75.75 0 0 1-1.06 0l-4-4A.749.749 0 0 1 4.53 7.72l2.72 2.72V1.75ZM2.75 14a.75.75 0 0 1 .75-.75h9a.75.75 0 0 1 0 1.5h-9a.75.75 0 0 1-.75-.75Z"/></svg>`;
const PUSH_ICON = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M7.47 3.22a.75.75 0 0 1 1.06 0l4 4A.749.749 0 0 1 12 8.5a.749.749 0 0 1-.53-.22L8.75 5.56v8.69a.75.75 0 0 1-1.5 0V5.56L4.53 8.28A.749.749 0 0 1 3.255 7.954a.749.749 0 0 1 .215-.734l4-4ZM2.75 2a.75.75 0 0 1 .75-.75h9a.75.75 0 0 1 0 1.5h-9A.75.75 0 0 1 2.75 2Z"/></svg>`; const PUSH_ICON = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M7.47 3.22a.75.75 0 0 1 1.06 0l4 4A.749.749 0 0 1 12 8.5a.749.749 0 0 1-.53-.22L8.75 5.56v8.69a.75.75 0 0 1-1.5 0V5.56L4.53 8.28A.749.749 0 0 1 3.255 7.954a.749.749 0 0 1 .215-.734l4-4ZM2.75 2a.75.75 0 0 1 .75-.75h9a.75.75 0 0 1 0 1.5h-9A.75.75 0 0 1 2.75 2Z"/></svg>`;
const PUBLISH_ICON = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 1.25a.75.75 0 0 1 .75.75v1.13A3.001 3.001 0 0 1 11 6v.5h1.25A1.75 1.75 0 0 1 14 8.25v4A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-4A1.75 1.75 0 0 1 3.75 6.5H5V6a3.001 3.001 0 0 1 2.25-2.87V2A.75.75 0 0 1 8 1.25ZM6.5 6v.5h3V6a1.5 1.5 0 0 0-3 0Zm-.53 4.28 1.5-1.5a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 1 1-1.06 1.06l-.22-.22v1.13a.75.75 0 0 1-1.5 0v-1.13l-.22.22a.75.75 0 0 1-1.06-1.06Z"/></svg>`; const PUBLISH_ICON = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 1.25a.75.75 0 0 1 .75.75v1.13A3.001 3.001 0 0 1 11 6v.5h1.25A1.75 1.75 0 0 1 14 8.25v4A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-4A1.75 1.75 0 0 1 3.75 6.5H5V6a3.001 3.001 0 0 1 2.25-2.87V2A.75.75 0 0 1 8 1.25ZM6.5 6v.5h3V6a1.5 1.5 0 0 0-3 0Zm-.53 4.28 1.5-1.5a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 1 1-1.06 1.06l-.22-.22v1.13a.75.75 0 0 1-1.5 0v-1.13l-.22.22a.75.75 0 0 1-1.06-1.06Z"/></svg>`;
const EXPLORER_ICON = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M1.75 2A1.75 1.75 0 0 0 0 3.75v8.5C0 13.216.784 14 1.75 14h12.5A1.75 1.75 0 0 0 16 12.25v-6.5A1.75 1.75 0 0 0 14.25 4H7.5a.25.25 0 0 1-.2-.1l-.9-1.2A1.75 1.75 0 0 0 5 2H1.75Zm0 1.5H5a.25.25 0 0 1 .2.1l.9 1.2c.331.441.85.7 1.4.7h6.75a.25.25 0 0 1 .25.25v6.5a.25.25 0 0 1-.25.25H1.75a.25.25 0 0 1-.25-.25v-8.5a.25.25 0 0 1 .25-.25Z"/></svg>`;
const EDITOR_ICON = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-7.7 7.7a1.75 1.75 0 0 1-.744.44l-3.018.862a.75.75 0 0 1-.927-.927l.862-3.018a1.75 1.75 0 0 1 .44-.744l7.527-7.873ZM12.427 2.487a.25.25 0 0 0-.354 0l-.963.963 1.44 1.44.963-.963a.25.25 0 0 0 0-.354l-1.086-1.086ZM11.49 5.95l-1.44-1.44-5.503 5.503a.25.25 0 0 0-.063.106l-.558 1.955 1.955-.558a.25.25 0 0 0 .106-.063L11.49 5.95ZM1 14.25a.75.75 0 0 1 .75-.75h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1-.75-.75Z"/></svg>`;
function uid() { function uid() {
return `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
@@ -89,6 +96,17 @@ function repoNameFromPath(path = "") {
return path.split(/[/\\]/).filter(Boolean).pop() || 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() { function currentRepositoryName() {
return getState().selectedRepoName || defaultRepositoryName; return getState().selectedRepoName || defaultRepositoryName;
} }
@@ -121,6 +139,31 @@ function selectedGitPath() {
return getState().settings.gitExecutablePath; 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) => `<option value="${escapeHtml(editor.executablePath)}" ${selectedValue === editor.executablePath ? "selected" : ""}>${escapeHtml(editor.name)}</option>`)
.join("");
return `
<option value="${DEFAULT_EDITOR_VALUE}" ${selectedValue === DEFAULT_EDITOR_VALUE ? "selected" : ""}>VS Code command (code)</option>
${detectedOptions}
<option value="${CUSTOM_EDITOR_VALUE}" ${selectedValue === CUSTOM_EDITOR_VALUE ? "selected" : ""}>Custom application…</option>
`;
}
function statusLabel(status = "") { function statusLabel(status = "") {
const labels = { const labels = {
modified: "Modified", modified: "Modified",
@@ -866,7 +909,11 @@ function cloneModalContent(state) {
</div> </div>
<div> <div>
<div class="label">Destination path</div> <div class="label">Destination path</div>
<div class="gd-path-picker">
<input id="clone-destination-input" placeholder="${escapeHtml(state.settings.defaultCloneDirectory || "/Users/me/code/repo")}" value="${escapeHtml(state.cloneDestinationInput || state.settings.defaultCloneDirectory)}" /> <input id="clone-destination-input" placeholder="${escapeHtml(state.settings.defaultCloneDirectory || "/Users/me/code/repo")}" value="${escapeHtml(state.cloneDestinationInput || state.settings.defaultCloneDirectory)}" />
<button id="clone-destination-browse-btn" type="button">Browse</button>
</div>
<span class="muted" style="font-size:12px">Choose a parent folder and Gitpub will create the repository folder inside it.</span>
</div> </div>
<button id="clone-btn" class="primary" type="button">Clone Repository</button> <button id="clone-btn" class="primary" type="button">Clone Repository</button>
${gitOutput ${gitOutput
@@ -907,6 +954,8 @@ function serversModalContent(state) {
} }
function settingsModalContent(state) { function settingsModalContent(state) {
const editorDropdownValue = selectedEditorDropdownValue(state);
const customEditorPath = editorDropdownValue === CUSTOM_EDITOR_VALUE ? state.settings.externalEditorPath?.trim() || "" : "";
return ` return `
<div class="gd-modal-two-col"> <div class="gd-modal-two-col">
<div class="gd-modal-section"> <div class="gd-modal-section">
@@ -925,8 +974,19 @@ function settingsModalContent(state) {
<input id="git-path-input" value="${escapeHtml(state.settings.gitExecutablePath)}" placeholder="git or /usr/bin/git" /> <input id="git-path-input" value="${escapeHtml(state.settings.gitExecutablePath)}" placeholder="git or /usr/bin/git" />
<div class="label">Default clone directory</div> <div class="label">Default clone directory</div>
<input id="default-clone-dir-input" value="${escapeHtml(state.settings.defaultCloneDirectory)}" placeholder="/Users/me/code" /> <input id="default-clone-dir-input" value="${escapeHtml(state.settings.defaultCloneDirectory)}" placeholder="/Users/me/code" />
<div class="label">External editor command</div> <div class="label">Code editor</div>
<input id="external-editor-input" value="${escapeHtml(state.settings.externalEditorPath)}" placeholder="code" /> <div class="gd-editor-picker">
<select id="external-editor-select">
${externalEditorOptionsTemplate(state)}
</select>
<button id="rescan-editors-btn" type="button" ${state.installedIdeScanLoading ? "disabled" : ""}>${state.installedIdeScanLoading ? "Scanning..." : "Rescan"}</button>
</div>
${state.installedIdeScanError ? `<div class="viewer-error" style="margin-top:6px">${escapeHtml(state.installedIdeScanError)}</div>` : ""}
${!state.installedIdeScanLoading && !state.installedIdes.length ? `<div class="muted" style="font-size:12px;margin-top:4px">No installed IDEs detected yet. You can use the default <code>code</code> command or choose a custom app.</div>` : ""}
<div id="custom-editor-row" class="gd-path-picker ${editorDropdownValue === CUSTOM_EDITOR_VALUE ? "" : "hidden"}" style="margin-top:8px">
<input id="custom-editor-input" value="${escapeHtml(customEditorPath)}" placeholder="Choose an application or executable" />
<button id="custom-editor-browse-btn" type="button">Browse</button>
</div>
<div class="gd-modal-divider"></div> <div class="gd-modal-divider"></div>
@@ -1119,6 +1179,14 @@ function dashboardView() {
</div> </div>
<div class="gd-toolbar-right"> <div class="gd-toolbar-right">
<div class="gd-external-actions" role="group" aria-label="Repository actions">
<button id="open-file-explorer-btn" class="gd-icon-action" type="button" ${!hasLocalRepo ? "disabled" : ""} title="Open in File Explorer" aria-label="Open in File Explorer">
${EXPLORER_ICON}
</button>
<button id="open-code-editor-btn" class="gd-icon-action" type="button" ${!hasLocalRepo ? "disabled" : ""} title="Open in Code Editor" aria-label="Open in Code Editor">
${EDITOR_ICON}
</button>
</div>
<div class="gd-view-toggle" role="group" aria-label="View mode"> <div class="gd-view-toggle" role="group" aria-label="View mode">
<button class="${activeView === "changes" ? "active" : ""}" data-view="changes" type="button">Changes</button> <button class="${activeView === "changes" ? "active" : ""}" data-view="changes" type="button">Changes</button>
<button class="${activeView === "history" ? "active" : ""}" data-view="history" type="button">History</button> <button class="${activeView === "history" ? "active" : ""}" data-view="history" type="button">History</button>
@@ -1141,6 +1209,14 @@ function dashboardView() {
<svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></svg> <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></svg>
File Viewer File Viewer
</button> </button>
<button class="gd-utility-menu-item" id="open-file-explorer-menu-btn" type="button" role="menuitem" ${!hasLocalRepo ? "disabled" : ""}>
${EXPLORER_ICON}
Open in File Explorer
</button>
<button class="gd-utility-menu-item" id="open-code-editor-menu-btn" type="button" role="menuitem" ${!hasLocalRepo ? "disabled" : ""}>
${EDITOR_ICON}
Open in Code Editor
</button>
<div class="gd-utility-separator"></div> <div class="gd-utility-separator"></div>
<button class="gd-utility-menu-item" data-open-modal="servers" type="button" role="menuitem"> <button class="gd-utility-menu-item" data-open-modal="servers" type="button" role="menuitem">
<svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M1.5 1.75a.25.25 0 0 1 .25-.25h12.5a.25.25 0 0 1 .25.25v4a.25.25 0 0 1-.25.25H1.75a.25.25 0 0 1-.25-.25v-4Zm.25-1.75A1.75 1.75 0 0 0 0 1.75v4C0 6.716.784 7.5 1.75 7.5h12.5A1.75 1.75 0 0 0 16 5.75v-4A1.75 1.75 0 0 0 14.25 0H1.75ZM0 10.25C0 9.284.784 8.5 1.75 8.5h12.5A1.75 1.75 0 0 1 16 10.25v4A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25v-4Zm1.5.25a.25.25 0 0 0-.25.25v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25H1.75Z"/></svg> <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M1.5 1.75a.25.25 0 0 1 .25-.25h12.5a.25.25 0 0 1 .25.25v4a.25.25 0 0 1-.25.25H1.75a.25.25 0 0 1-.25-.25v-4Zm.25-1.75A1.75 1.75 0 0 0 0 1.75v4C0 6.716.784 7.5 1.75 7.5h12.5A1.75 1.75 0 0 0 16 5.75v-4A1.75 1.75 0 0 0 14.25 0H1.75ZM0 10.25C0 9.284.784 8.5 1.75 8.5h12.5A1.75 1.75 0 0 1 16 10.25v4A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25v-4Zm1.5.25a.25.25 0 0 0-.25.25v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25H1.75Z"/></svg>
@@ -1870,6 +1946,62 @@ async function openLocalViewer() {
} }
} }
async function refreshInstalledIdes() {
const state = getState();
state.installedIdeScanLoading = true;
state.installedIdeScanError = "";
render();
try {
state.installedIdes = await scanInstalledIdes();
} catch (error) {
state.installedIdes = [];
state.installedIdeScanError = `Unable to scan installed IDEs: ${error.message}`;
} finally {
state.installedIdeScanLoading = false;
render();
}
}
async function openSelectedRepoInFileExplorer() {
const state = getState();
if (!state.selectedRepoPath) {
gitOutput = "Select or enter a local repository path first.";
render();
return;
}
try {
await openInFileExplorer(state.selectedRepoPath);
gitOutput = `Opened in File Explorer:\n${state.selectedRepoPath}`;
} catch (error) {
gitOutput = `Open in File Explorer failed: ${error.message}`;
} finally {
utilityMenuOpen = false;
render();
}
}
async function openSelectedRepoInCodeEditor() {
const state = getState();
if (!state.selectedRepoPath) {
gitOutput = "Select or enter a local repository path first.";
render();
return;
}
const editorCommand = state.settings.externalEditorPath?.trim() || "code";
try {
await openInExternalEditor(state.selectedRepoPath, editorCommand);
gitOutput = `Opened in Code Editor with "${editorCommand}":\n${state.selectedRepoPath}`;
} catch (error) {
gitOutput = `Open in Code Editor failed: ${error.message}`;
} finally {
utilityMenuOpen = false;
render();
}
}
async function runRepoCommand(actionName, runner, operation = "") { async function runRepoCommand(actionName, runner, operation = "") {
const state = getState(); const state = getState();
if (!state.selectedRepoPath) { if (!state.selectedRepoPath) {
@@ -1916,6 +2048,9 @@ function bindDashboardEvents() {
activeModal = btn.dataset.openModal; activeModal = btn.dataset.openModal;
utilityMenuOpen = false; utilityMenuOpen = false;
render(); render();
if (activeModal === "settings" && !state.installedIdeScanLoading && !state.installedIdes.length) {
refreshInstalledIdes();
}
}); });
}); });
@@ -1926,6 +2061,11 @@ function bindDashboardEvents() {
render(); render();
}); });
document.getElementById("open-file-explorer-btn")?.addEventListener("click", openSelectedRepoInFileExplorer);
document.getElementById("open-file-explorer-menu-btn")?.addEventListener("click", openSelectedRepoInFileExplorer);
document.getElementById("open-code-editor-btn")?.addEventListener("click", openSelectedRepoInCodeEditor);
document.getElementById("open-code-editor-menu-btn")?.addEventListener("click", openSelectedRepoInCodeEditor);
// Modal close // Modal close
document.getElementById("modal-close-btn")?.addEventListener("click", () => { document.getElementById("modal-close-btn")?.addEventListener("click", () => {
activeModal = ""; activeModal = "";
@@ -2245,6 +2385,23 @@ function bindDashboardEvents() {
render(); render();
}); });
document.getElementById("clone-destination-browse-btn")?.addEventListener("click", async () => {
state.cloneUrlInput = document.getElementById("clone-url-input")?.value?.trim() || "";
let selectedDirectory = "";
try {
selectedDirectory = await browseDirectory(state.settings.defaultCloneDirectory || "");
} catch (error) {
gitOutput = `Could not open folder picker: ${error.message}`;
render();
return;
}
if (!selectedDirectory) return;
const repoName = repoNameFromUrl(state.cloneUrlInput);
state.cloneDestinationInput = repoName ? joinDirectoryPath(selectedDirectory, repoName) : selectedDirectory;
render();
});
document.getElementById("clone-btn")?.addEventListener("click", async () => { document.getElementById("clone-btn")?.addEventListener("click", async () => {
state.cloneUrlInput = document.getElementById("clone-url-input")?.value?.trim() || ""; state.cloneUrlInput = document.getElementById("clone-url-input")?.value?.trim() || "";
state.cloneDestinationInput = document.getElementById("clone-destination-input")?.value?.trim() || ""; state.cloneDestinationInput = document.getElementById("clone-destination-input")?.value?.trim() || "";
@@ -2273,12 +2430,41 @@ function bindDashboardEvents() {
}); });
// Settings modal handlers // Settings modal handlers
document.getElementById("external-editor-select")?.addEventListener("change", (event) => {
document.getElementById("custom-editor-row")?.classList.toggle("hidden", event.target.value !== CUSTOM_EDITOR_VALUE);
});
document.getElementById("rescan-editors-btn")?.addEventListener("click", () => refreshInstalledIdes());
document.getElementById("custom-editor-browse-btn")?.addEventListener("click", async () => {
const currentValue = document.getElementById("custom-editor-input")?.value?.trim() || "";
let selectedApplication = "";
try {
selectedApplication = await browseApplication(currentValue);
} catch (error) {
settingsNotice = `Could not open application picker: ${error.message}`;
render();
return;
}
if (!selectedApplication) return;
const input = document.getElementById("custom-editor-input");
if (input) input.value = selectedApplication;
});
document.getElementById("save-basic-settings-btn")?.addEventListener("click", () => { document.getElementById("save-basic-settings-btn")?.addEventListener("click", () => {
const editorSelection = document.getElementById("external-editor-select")?.value || DEFAULT_EDITOR_VALUE;
const customEditorPath = document.getElementById("custom-editor-input")?.value?.trim() || "";
const externalEditorPath =
editorSelection === DEFAULT_EDITOR_VALUE
? ""
: editorSelection === CUSTOM_EDITOR_VALUE
? customEditorPath
: editorSelection;
updateSettings({ updateSettings({
theme: document.getElementById("theme-select")?.value || "dark", theme: document.getElementById("theme-select")?.value || "dark",
gitExecutablePath: document.getElementById("git-path-input")?.value?.trim() || "", gitExecutablePath: document.getElementById("git-path-input")?.value?.trim() || "",
defaultCloneDirectory: document.getElementById("default-clone-dir-input")?.value?.trim() || "", defaultCloneDirectory: document.getElementById("default-clone-dir-input")?.value?.trim() || "",
externalEditorPath: document.getElementById("external-editor-input")?.value?.trim() || "", externalEditorPath,
activeServerId: document.getElementById("default-server-select")?.value || null, activeServerId: document.getElementById("default-server-select")?.value || null,
autoFetchOnRepoOpen: Boolean(document.getElementById("auto-fetch-checkbox")?.checked), autoFetchOnRepoOpen: Boolean(document.getElementById("auto-fetch-checkbox")?.checked),
}); });
+3
View File
@@ -16,6 +16,9 @@ const state = {
localRepoScanResults: [], localRepoScanResults: [],
localRepoScanLoading: false, localRepoScanLoading: false,
localRepoScanError: "", localRepoScanError: "",
installedIdes: [],
installedIdeScanLoading: false,
installedIdeScanError: "",
cloneUrlInput: "", cloneUrlInput: "",
cloneDestinationInput: "", cloneDestinationInput: "",
commitMessage: "", commitMessage: "",
+33
View File
@@ -1,4 +1,5 @@
const invoke = window.__TAURI__?.core?.invoke; const invoke = window.__TAURI__?.core?.invoke;
const dialog = window.__TAURI__?.dialog;
function ensureInvoke() { function ensureInvoke() {
if (!invoke) { if (!invoke) {
@@ -6,6 +7,33 @@ function ensureInvoke() {
} }
} }
function ensureDialog() {
if (!dialog?.open) {
throw new Error("Tauri dialog API is not available.");
}
}
export async function browseDirectory(defaultPath = "") {
ensureDialog();
return dialog.open({
directory: true,
multiple: false,
defaultPath: defaultPath || undefined,
});
}
export async function browseApplication(defaultPath = "") {
ensureDialog();
return dialog.open({
directory: false,
multiple: false,
defaultPath: defaultPath || undefined,
filters: [
{ name: "Applications", extensions: ["exe", "cmd", "bat", "app"] },
],
});
}
export async function runGitClone(repoUrl, destinationPath, gitPath) { export async function runGitClone(repoUrl, destinationPath, gitPath) {
ensureInvoke(); ensureInvoke();
return invoke("git_clone", { return invoke("git_clone", {
@@ -131,6 +159,11 @@ export async function openInExternalEditor(repoPath, editorPath) {
return invoke("open_in_external_editor", { repoPath, editorPath }); return invoke("open_in_external_editor", { repoPath, editorPath });
} }
export async function scanInstalledIdes() {
ensureInvoke();
return invoke("scan_installed_ides");
}
export async function scanLocalRepos(roots = [], allowedRemoteUrls = [], gitPath = "", maxDepth = 4, maxResults = 200) { export async function scanLocalRepos(roots = [], allowedRemoteUrls = [], gitPath = "", maxDepth = 4, maxResults = 200) {
ensureInvoke(); ensureInvoke();
return invoke("scan_local_repos", { return invoke("scan_local_repos", {
+68
View File
@@ -1297,6 +1297,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-opener", "tauri-plugin-opener",
] ]
@@ -2254,6 +2255,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2",
"libc",
"objc2", "objc2",
"objc2-core-foundation", "objc2-core-foundation",
] ]
@@ -2920,6 +2922,30 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.14" version = "0.17.14"
@@ -3675,6 +3701,48 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "tauri-plugin-dialog"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.18",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
dependencies = [
"anyhow",
"dunce",
"glob",
"log",
"objc2-foundation",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 1.1.2+spec-1.1.0",
"url",
]
[[package]] [[package]]
name = "tauri-plugin-opener" name = "tauri-plugin-opener"
version = "2.5.4" version = "2.5.4"
+1
View File
@@ -23,4 +23,5 @@ tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
tauri-plugin-dialog = "2.7.1"
+1
View File
@@ -5,6 +5,7 @@
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": [
"core:default", "core:default",
"dialog:default",
"opener:default" "opener:default"
] ]
} }
+274
View File
@@ -126,6 +126,14 @@ struct GitCommitDetail {
diff: String, diff: String,
} }
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct InstalledIde {
id: String,
name: String,
executable_path: String,
}
fn normalize_api_base_url(server_url: &str) -> Result<String, String> { fn normalize_api_base_url(server_url: &str) -> Result<String, String> {
// Normalize user input so every backend consistently resolves to /api/v1. // Normalize user input so every backend consistently resolves to /api/v1.
let trimmed = server_url.trim().trim_end_matches('/'); let trimmed = server_url.trim().trim_end_matches('/');
@@ -240,6 +248,194 @@ fn validate_git_path(path: &str) -> Result<String, String> {
Ok(normalized) Ok(normalized)
} }
fn add_installed_ide(
ides: &mut Vec<InstalledIde>,
seen: &mut HashSet<String>,
name: &str,
executable_path: PathBuf,
) {
if !executable_path.is_file() {
return;
}
let executable_path = executable_path.to_string_lossy().to_string();
let key = executable_path.to_lowercase();
if seen.insert(key) {
ides.push(InstalledIde {
id: format!(
"{}:{}",
name.to_lowercase().replace(' ', "-"),
executable_path
),
name: name.to_string(),
executable_path,
});
}
}
fn env_path(name: &str) -> Option<PathBuf> {
env::var_os(name).map(PathBuf::from)
}
fn add_relative_ide_candidate(
ides: &mut Vec<InstalledIde>,
seen: &mut HashSet<String>,
env_name: &str,
relative_path: &str,
name: &str,
) {
if let Some(base_path) = env_path(env_name) {
add_installed_ide(ides, seen, name, base_path.join(relative_path));
}
}
fn add_path_ide_candidate(
ides: &mut Vec<InstalledIde>,
seen: &mut HashSet<String>,
command_names: &[&str],
name: &str,
) {
if let Some(executable_path) = find_executable_on_path(command_names) {
add_installed_ide(ides, seen, name, executable_path);
}
}
fn find_executable_on_path(command_names: &[&str]) -> Option<PathBuf> {
let path_values = env::var_os("PATH")?;
let extensions: Vec<String> = if cfg!(windows) {
env::var("PATHEXT")
.unwrap_or_else(|_| ".EXE;.CMD;.BAT".to_string())
.split(';')
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.collect()
} else {
vec!["".to_string()]
};
for dir in env::split_paths(&path_values) {
for command_name in command_names {
let command_path = Path::new(command_name);
let has_extension = command_path.extension().is_some();
let candidates = if has_extension {
vec![dir.join(command_name)]
} else {
extensions
.iter()
.map(|extension| dir.join(format!("{command_name}{extension}")))
.collect()
};
for candidate in candidates {
if candidate.is_file() {
return Some(candidate);
}
}
}
}
None
}
#[cfg(target_os = "windows")]
fn add_windows_ide_candidates(ides: &mut Vec<InstalledIde>, seen: &mut HashSet<String>) {
let candidates = [
(
"LOCALAPPDATA",
"Programs\\Microsoft VS Code\\Code.exe",
"Visual Studio Code",
),
(
"LOCALAPPDATA",
"Programs\\Microsoft VS Code Insiders\\Code - Insiders.exe",
"Visual Studio Code Insiders",
),
("LOCALAPPDATA", "Programs\\Cursor\\Cursor.exe", "Cursor"),
(
"LOCALAPPDATA",
"Programs\\Windsurf\\Windsurf.exe",
"Windsurf",
),
(
"LOCALAPPDATA",
"Programs\\VSCodium\\VSCodium.exe",
"VSCodium",
),
("LOCALAPPDATA", "Programs\\Zed\\Zed.exe", "Zed"),
(
"ProgramFiles",
"Microsoft VS Code\\Code.exe",
"Visual Studio Code",
),
(
"ProgramFiles",
"Microsoft VS Code Insiders\\Code - Insiders.exe",
"Visual Studio Code Insiders",
),
("ProgramFiles", "Cursor\\Cursor.exe", "Cursor"),
("ProgramFiles", "Windsurf\\Windsurf.exe", "Windsurf"),
("ProgramFiles", "VSCodium\\VSCodium.exe", "VSCodium"),
(
"ProgramFiles",
"Sublime Text\\sublime_text.exe",
"Sublime Text",
),
("ProgramFiles", "Notepad++\\notepad++.exe", "Notepad++"),
("ProgramFiles(x86)", "Notepad++\\notepad++.exe", "Notepad++"),
];
for (env_name, relative_path, name) in candidates {
add_relative_ide_candidate(ides, seen, env_name, relative_path, name);
}
for env_name in ["ProgramFiles", "ProgramFiles(x86)"] {
if let Some(jetbrains_root) = env_path(env_name).map(|path| path.join("JetBrains")) {
add_jetbrains_candidates(ides, seen, &jetbrains_root);
}
}
}
#[cfg(not(target_os = "windows"))]
fn add_windows_ide_candidates(_ides: &mut Vec<InstalledIde>, _seen: &mut HashSet<String>) {}
fn add_jetbrains_candidates(ides: &mut Vec<InstalledIde>, seen: &mut HashSet<String>, root: &Path) {
let Ok(entries) = fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let app_dir = entry.path();
if !app_dir.is_dir() {
continue;
}
let dir_name = app_dir
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("JetBrains IDE")
.to_string();
let candidates = [
("IntelliJ IDEA", "idea64.exe"),
("WebStorm", "webstorm64.exe"),
("PhpStorm", "phpstorm64.exe"),
("PyCharm", "pycharm64.exe"),
("Rider", "rider64.exe"),
("RustRover", "rustrover64.exe"),
("GoLand", "goland64.exe"),
];
for (label, exe) in candidates {
let label_lower = label.to_lowercase();
let name = if dir_name.to_lowercase().contains(label_lower.as_str()) {
dir_name.clone()
} else {
label.to_string()
};
add_installed_ide(ides, seen, &name, app_dir.join("bin").join(exe));
}
}
}
fn status_kind(x: &str, y: &str) -> String { fn status_kind(x: &str, y: &str) -> String {
match (x, y) { match (x, y) {
("?", "?") => "untracked", ("?", "?") => "untracked",
@@ -1259,6 +1455,82 @@ fn open_in_external_editor(repo_path: String, editor_path: String) -> Result<(),
Ok(()) Ok(())
} }
#[tauri::command]
fn scan_installed_ides() -> Result<Vec<InstalledIde>, String> {
let mut ides = Vec::new();
let mut seen = HashSet::new();
add_windows_ide_candidates(&mut ides, &mut seen);
#[cfg(target_os = "macos")]
{
let candidates = [
(
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
"Visual Studio Code",
),
(
"/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders",
"Visual Studio Code Insiders",
),
("/Applications/Cursor.app/Contents/Resources/app/bin/cursor", "Cursor"),
("/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf", "Windsurf"),
("/Applications/VSCodium.app/Contents/Resources/app/bin/codium", "VSCodium"),
("/Applications/Zed.app/Contents/MacOS/zed", "Zed"),
("/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", "Sublime Text"),
];
for (path, name) in candidates {
add_installed_ide(&mut ides, &mut seen, name, PathBuf::from(path));
}
}
add_path_ide_candidate(
&mut ides,
&mut seen,
&["code", "Code.exe", "code.cmd"],
"Visual Studio Code",
);
add_path_ide_candidate(
&mut ides,
&mut seen,
&["code-insiders", "Code - Insiders.exe", "code-insiders.cmd"],
"Visual Studio Code Insiders",
);
add_path_ide_candidate(
&mut ides,
&mut seen,
&["cursor", "Cursor.exe", "cursor.cmd"],
"Cursor",
);
add_path_ide_candidate(
&mut ides,
&mut seen,
&["windsurf", "Windsurf.exe", "windsurf.cmd"],
"Windsurf",
);
add_path_ide_candidate(
&mut ides,
&mut seen,
&["codium", "VSCodium.exe", "codium.cmd"],
"VSCodium",
);
add_path_ide_candidate(&mut ides, &mut seen, &["zed", "Zed.exe"], "Zed");
add_path_ide_candidate(
&mut ides,
&mut seen,
&["subl", "sublime_text.exe"],
"Sublime Text",
);
ides.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then(a.executable_path.cmp(&b.executable_path))
});
Ok(ides)
}
#[tauri::command] #[tauri::command]
fn scan_local_repos( fn scan_local_repos(
roots: Option<Vec<String>>, roots: Option<Vec<String>>,
@@ -1558,6 +1830,7 @@ fn test_gitea_connection(
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
git_clone, git_clone,
@@ -1582,6 +1855,7 @@ pub fn run() {
commit_detail, commit_detail,
open_in_file_explorer, open_in_file_explorer,
open_in_external_editor, open_in_external_editor,
scan_installed_ides,
scan_local_repos, scan_local_repos,
local_repo_branches, local_repo_branches,
local_repo_tree, local_repo_tree,