Skip to content

Commit 1c8f364

Browse files
var-ggclaude
andcommitted
feat(settings): Updates section + open-file footer link
Adds the frontend UI for the consolidated Settings surface: - Updates section with a 3-radio group for update_check (Automatic / Manual only / Off), hidden when updaterAvailable is false so Scoop and Microsoft Store users don't see a meaningless knob. Persists immediately on click — no debounce, since one radio press isn't a sweep and the tray dot should react right away. - Footer at the bottom of the Settings window with a small "Open settings.json" link — the de-emphasised escape hatch for hand-edits (corrupted branch_selections, debugging panel_position, etc.). Replaces the tray's old "Open settings file…" entry so the tray menu has only one Settings-related item. AppSettings (TS) now mirrors the extended Rust shape (updateCheck, updaterAvailable). DEFAULT_SETTINGS picks "enabled" + true so a launch before initSettings resolves doesn't briefly hide the section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 86e5211 commit 1c8f364

3 files changed

Lines changed: 185 additions & 1 deletion

File tree

src/components/Settings.tsx

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { useEffect, useRef, useState } from "react";
22
import { invoke } from "@tauri-apps/api/core";
33

4-
import { broadcastSettings, getCurrentSettings } from "../lib/settings";
4+
import {
5+
broadcastSettings,
6+
getCurrentSettings,
7+
type UpdateCheckMode,
8+
} from "../lib/settings";
59

610
/** UI-scale slider bounds — mirror UI_SCALE_MIN/MAX in commands.rs. 100%
711
* is the floor: the diff/timeline default is the most compact legible
@@ -102,6 +106,25 @@ export function Settings() {
102106
}, PERSIST_DELAY_MS);
103107
}
104108

109+
function setUpdateMode(mode: UpdateCheckMode) {
110+
const next = { ...settings, updateCheck: mode };
111+
setSettings(next);
112+
broadcastSettings(next);
113+
// Persist immediately — radio clicks are single events, not a sweep,
114+
// so debounce buys nothing and the user expects the tray dot /
115+
// "Check for updates" item to react right away.
116+
void invoke("set_update_check", { mode });
117+
}
118+
119+
function openSettingsFile() {
120+
void invoke("open_settings_file").catch((err) => {
121+
// Surface in console for triage; the user already gets OS-level
122+
// feedback if the editor fails to launch.
123+
// eslint-disable-next-line no-console
124+
console.error("[gitwink] open_settings_file failed", err);
125+
});
126+
}
127+
105128
// Recording mode: capture the next valid combo and send it to Rust to
106129
// re-bind the global shortcut live. Esc cancels. OS-reserved combos
107130
// (Alt+Tab, Win+L, …) never reach the webview, so the recorder simply
@@ -234,6 +257,60 @@ export function Settings() {
234257
and won't react.
235258
</p>
236259
</section>
260+
261+
{settings.updaterAvailable && (
262+
<section className="settings-section">
263+
<h2 className="settings-section-title">Updates</h2>
264+
<div className="settings-radio-group" role="radiogroup">
265+
{(
266+
[
267+
{
268+
value: "enabled",
269+
label: "Automatic",
270+
hint: "Check on startup + every 24h. Tray dot when one's ready.",
271+
},
272+
{
273+
value: "manual",
274+
label: "Manual only",
275+
hint: 'No background checks; use the tray "Check for updates" entry.',
276+
},
277+
{
278+
value: "disabled",
279+
label: "Off",
280+
hint: 'Updater fully disabled. Tray hides the "Check for updates" entry.',
281+
},
282+
] as const
283+
).map((opt) => (
284+
<label key={opt.value} className="settings-radio">
285+
<input
286+
type="radio"
287+
name="update-check"
288+
value={opt.value}
289+
checked={settings.updateCheck === opt.value}
290+
onChange={() => setUpdateMode(opt.value)}
291+
/>
292+
<span className="settings-radio-label">{opt.label}</span>
293+
<span className="settings-radio-hint">{opt.hint}</span>
294+
</label>
295+
))}
296+
</div>
297+
</section>
298+
)}
299+
300+
<footer className="settings-footer">
301+
<button
302+
type="button"
303+
className="settings-link"
304+
onClick={openSettingsFile}
305+
>
306+
Open settings.json
307+
</button>
308+
<span className="settings-footer-hint">
309+
Reveals the raw config in your default editor. Most knobs above
310+
are mirrored here — auto-managed fields (window positions, repo
311+
state) shouldn't need hand-edits.
312+
</span>
313+
</footer>
237314
</div>
238315
);
239316
}

src/lib/settings.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,23 @@ import { invoke } from "@tauri-apps/api/core";
22
import { emit, listen } from "@tauri-apps/api/event";
33
import { useSyncExternalStore } from "react";
44

5+
/** Self-update behaviour. Mirrors Rust `UpdateCheckMode` (serialized
6+
* lowercase) — "enabled" auto-checks every 24h, "manual" only on tray
7+
* click, "disabled" turns the updater off entirely (no tray dot, no
8+
* "Check for updates" item). */
9+
export type UpdateCheckMode = "enabled" | "manual" | "disabled";
10+
511
/** The user-facing settings slice — mirrors the Rust `AppSettings`. */
612
export interface AppSettings {
713
uiScale: number;
814
diffFontFamily: string | null;
915
panelHotkey: string;
1016
panelPinned: boolean;
17+
updateCheck: UpdateCheckMode;
18+
/** False for Scoop / Microsoft Store installs — the Updates section
19+
* of the Settings window hides because those channels manage their
20+
* own updates. */
21+
updaterAvailable: boolean;
1122
}
1223

1324
/** Built-in diff/code monospace stack — the fallback when no font is
@@ -20,6 +31,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
2031
diffFontFamily: null,
2132
panelHotkey: "CmdOrCtrl+Shift+G",
2233
panelPinned: false,
34+
updateCheck: "enabled",
35+
updaterAvailable: true,
2336
};
2437

2538
/** Timeline row height in px at scale 1.0 — the value the fixed-row

src/styles.css

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,3 +2232,97 @@ body,
22322232
color: rgba(255, 130, 130, 1);
22332233
}
22342234
}
2235+
2236+
/* Update-check radio group — stacks vertically with a short hint per
2237+
option so the trade-offs (auto vs manual vs off) are visible inline
2238+
without a tooltip. */
2239+
.settings-radio-group {
2240+
display: flex;
2241+
flex-direction: column;
2242+
gap: 8px;
2243+
}
2244+
2245+
.settings-radio {
2246+
display: grid;
2247+
grid-template-columns: auto 1fr;
2248+
grid-template-rows: auto auto;
2249+
column-gap: 8px;
2250+
align-items: center;
2251+
cursor: pointer;
2252+
padding: 4px 2px;
2253+
border-radius: 4px;
2254+
}
2255+
2256+
.settings-radio:hover {
2257+
background: rgba(0, 0, 0, 0.03);
2258+
}
2259+
2260+
.settings-radio input {
2261+
grid-row: 1 / span 2;
2262+
margin: 0;
2263+
cursor: pointer;
2264+
}
2265+
2266+
.settings-radio-label {
2267+
font-size: 13px;
2268+
}
2269+
2270+
.settings-radio-hint {
2271+
grid-column: 2;
2272+
font-size: 11px;
2273+
opacity: 0.55;
2274+
}
2275+
2276+
@media (prefers-color-scheme: dark) {
2277+
.settings-radio:hover {
2278+
background: rgba(255, 255, 255, 0.04);
2279+
}
2280+
}
2281+
2282+
/* Footer with the "Open settings.json" escape hatch — visually
2283+
de-emphasised so non-power-users aren't drawn to it, but still
2284+
reachable for the rare hand-edit case (clearing a corrupted
2285+
branch_selections entry, debugging panel_position, etc.). */
2286+
.settings-footer {
2287+
margin-top: 28px;
2288+
padding-top: 14px;
2289+
border-top: 1px solid rgba(0, 0, 0, 0.08);
2290+
display: flex;
2291+
flex-direction: column;
2292+
gap: 4px;
2293+
}
2294+
2295+
.settings-link {
2296+
align-self: flex-start;
2297+
background: none;
2298+
border: none;
2299+
padding: 0;
2300+
color: rgba(80, 130, 240, 0.95);
2301+
font: inherit;
2302+
font-size: 12px;
2303+
cursor: pointer;
2304+
text-decoration: underline;
2305+
text-decoration-color: rgba(80, 130, 240, 0.4);
2306+
}
2307+
2308+
.settings-link:hover {
2309+
text-decoration-color: rgba(80, 130, 240, 0.9);
2310+
}
2311+
2312+
.settings-footer-hint {
2313+
font-size: 11px;
2314+
opacity: 0.5;
2315+
}
2316+
2317+
@media (prefers-color-scheme: dark) {
2318+
.settings-footer {
2319+
border-top-color: rgba(255, 255, 255, 0.08);
2320+
}
2321+
.settings-link {
2322+
color: rgba(140, 180, 255, 0.95);
2323+
text-decoration-color: rgba(140, 180, 255, 0.4);
2324+
}
2325+
.settings-link:hover {
2326+
text-decoration-color: rgba(140, 180, 255, 0.9);
2327+
}
2328+
}

0 commit comments

Comments
 (0)