Skip to content

Commit 00470b9

Browse files
committed
Updates: Surface "Check for updates" in Settings and Cmdr menu
Two ways to manually trigger an update check, both reflecting the same shared state with continuous version-aware status. - Settings > Updates: button at top of section, disabled while `status !== 'idle'`. Status text below cycles through Checking… / No updates found (current: vX.Y.Z) / Update found, downloading vX.Y.Z (current: vA.B.C)… / Installing vX.Y.Z (current: vA.B.C)… For errors, renders a "Send error report" link calling `openErrorReportDialog` with the formatted error context. - Cmdr menu > Check for updates… (right after "Enter license key…"): wired as `app.checkForUpdates`. Frontend calls `runMenuTriggeredCheck()` which fires `addToast(UpdateCheckToastContent, { id: 'update-check', timeoutMs: 10000 })`, awaits `checkForUpdates()`, then dismisses on `ready` so it doesn't overlap with the persistent restart toast. Toast dedup auto-supersedes phase changes. - Split macOS path into separate `downloading` and `installing` substates (two distinct invokes). Non-macOS keeps a single `downloading` phase since the plugin's `downloadAndInstall()` is fused. - Track `previousVersion` (`getVersion()` snapshot at check start) and `nextVersion` (target) on the state so UIs can format current/target deltas. - Extract `updateState` into its own `update-state.svelte.ts` to break an import cycle (toast components read it; the updater imports the toast components). Re-exported from `updater.svelte` so existing call sites are unchanged. - Pure `formatUpdateStatus()` formatter shared between Settings + toast. - Tests: 6 formatter tests, 4 `runMenuTriggeredCheck` tests, 5 `UpdateCheckToastContent` tests, 6 `UpdatesSection` tests. - SF Symbol `arrow.down.circle` for the macOS menu item; Linux equivalent at the bottom of the Edit submenu.
1 parent 0d83a7b commit 00470b9

15 files changed

Lines changed: 686 additions & 55 deletions

apps/desktop/src-tauri/src/menu/linux.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ use tauri::{
66
};
77

88
use super::{
9-
ABOUT_ID, CLOSE_OTHER_TABS_ID, CLOSE_TAB_ID, COMMAND_PALETTE_ID, COPY_FILENAME_ID, COPY_PATH_ID, DESELECT_ALL_ID,
10-
EDIT_COPY_ID, EDIT_CUT_ID, EDIT_ID, EDIT_PASTE_ID, EDIT_PASTE_MOVE_ID, ENTER_LICENSE_KEY_ID, FILE_COPY_ID,
11-
FILE_DELETE_ID, FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID, FILE_VIEW_ID, GET_INFO_ID,
12-
GO_BACK_ID, GO_FORWARD_ID, GO_PARENT_ID, HELP_SEND_ERROR_REPORT_ID, MenuItems, NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID,
13-
PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, SEARCH_FILES_ID, SELECT_ALL_ID, SETTINGS_ID,
14-
SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SWAP_PANES_ID, SWITCH_PANE_ID, VIEW_MODE_BRIEF_ID, VIEW_MODE_FULL_ID,
15-
ViewMode, build_sort_submenu, copy_path_accelerator, register_item, show_in_file_manager_accelerator,
16-
show_in_file_manager_label,
9+
ABOUT_ID, CHECK_FOR_UPDATES_ID, CLOSE_OTHER_TABS_ID, CLOSE_TAB_ID, COMMAND_PALETTE_ID, COPY_FILENAME_ID,
10+
COPY_PATH_ID, DESELECT_ALL_ID, EDIT_COPY_ID, EDIT_CUT_ID, EDIT_ID, EDIT_PASTE_ID, EDIT_PASTE_MOVE_ID,
11+
ENTER_LICENSE_KEY_ID, FILE_COPY_ID, FILE_DELETE_ID, FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID,
12+
FILE_VIEW_ID, GET_INFO_ID, GO_BACK_ID, GO_FORWARD_ID, GO_PARENT_ID, HELP_SEND_ERROR_REPORT_ID, MenuItems,
13+
NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID, PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, SEARCH_FILES_ID,
14+
SELECT_ALL_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SWAP_PANES_ID, SWITCH_PANE_ID,
15+
VIEW_MODE_BRIEF_ID, VIEW_MODE_FULL_ID, ViewMode, build_sort_submenu, copy_path_accelerator, register_item,
16+
show_in_file_manager_accelerator, show_in_file_manager_label,
1717
};
1818

1919
/// Linux menu: builds all menus from scratch, matching the macOS menu structure.
@@ -99,6 +99,13 @@ pub(crate) fn build_menu_linux<R: Runtime>(
9999
"Enter &license key..."
100100
};
101101
let license_item = MenuItem::with_id(app, ENTER_LICENSE_KEY_ID, license_label, true, None::<&str>)?;
102+
let check_for_updates_item = MenuItem::with_id(
103+
app,
104+
CHECK_FOR_UPDATES_ID,
105+
"Check for &updates\u{2026}",
106+
true,
107+
None::<&str>,
108+
)?;
102109

103110
let edit_menu = Submenu::with_items(
104111
app,
@@ -120,6 +127,7 @@ pub(crate) fn build_menu_linux<R: Runtime>(
120127
&PredefinedMenuItem::separator(app)?,
121128
&settings_item,
122129
&license_item,
130+
&check_for_updates_item,
123131
],
124132
)?;
125133
menu.append(&edit_menu)?;
@@ -274,7 +282,7 @@ pub(crate) fn build_menu_linux<R: Runtime>(
274282

275283
// Edit menu positions: cut(0), copy(1), paste(2), move_here(3), sep(4),
276284
// select_all(5), deselect_all(6), sep(7), copy_path(8), copy_filename(9),
277-
// sep(10), search_files(11), sep(12), settings(13), license(14)
285+
// sep(10), search_files(11), sep(12), settings(13), license(14), check_for_updates(15)
278286
register_item(&mut items, EDIT_CUT_ID, &edit_cut_item, &edit_menu, 0);
279287
register_item(&mut items, EDIT_COPY_ID, &edit_copy_item, &edit_menu, 1);
280288
register_item(&mut items, EDIT_PASTE_ID, &edit_paste_item, &edit_menu, 2);
@@ -285,6 +293,13 @@ pub(crate) fn build_menu_linux<R: Runtime>(
285293
register_item(&mut items, COPY_FILENAME_ID, &copy_filename_item, &edit_menu, 9);
286294
register_item(&mut items, SEARCH_FILES_ID, &search_files_item, &edit_menu, 11);
287295
register_item(&mut items, SETTINGS_ID, &settings_item, &edit_menu, 13);
296+
register_item(
297+
&mut items,
298+
CHECK_FOR_UPDATES_ID,
299+
&check_for_updates_item,
300+
&edit_menu,
301+
15,
302+
);
288303

289304
// View menu positions: full(0), brief(1), sep(2), hidden(3), sort(4), sep(5),
290305
// switch(6), swap(7), sep(8), palette(9)

apps/desktop/src-tauri/src/menu/macos.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ use tauri::{
99
};
1010

1111
use super::{
12-
ABOUT_ID, CLOSE_OTHER_TABS_ID, CLOSE_TAB_ID, COMMAND_PALETTE_ID, COPY_FILENAME_ID, COPY_PATH_ID, DESELECT_ALL_ID,
13-
EDIT_COPY_ID, EDIT_CUT_ID, EDIT_ID, EDIT_PASTE_ID, EDIT_PASTE_MOVE_ID, ENTER_LICENSE_KEY_ID, FILE_COPY_ID,
14-
FILE_DELETE_ID, FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID, FILE_VIEW_ID, GET_INFO_ID,
15-
GO_BACK_ID, GO_FORWARD_ID, GO_PARENT_ID, HELP_SEND_ERROR_REPORT_ID, MenuItems, NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID,
16-
PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, SEARCH_FILES_ID, SELECT_ALL_ID, SETTINGS_ID,
17-
SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SWAP_PANES_ID, SWITCH_PANE_ID, VIEW_MODE_BRIEF_ID, VIEW_MODE_FULL_ID,
18-
ViewMode, build_sort_submenu, copy_path_accelerator, register_item, show_in_file_manager_accelerator,
19-
show_in_file_manager_label,
12+
ABOUT_ID, CHECK_FOR_UPDATES_ID, CLOSE_OTHER_TABS_ID, CLOSE_TAB_ID, COMMAND_PALETTE_ID, COPY_FILENAME_ID,
13+
COPY_PATH_ID, DESELECT_ALL_ID, EDIT_COPY_ID, EDIT_CUT_ID, EDIT_ID, EDIT_PASTE_ID, EDIT_PASTE_MOVE_ID,
14+
ENTER_LICENSE_KEY_ID, FILE_COPY_ID, FILE_DELETE_ID, FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID,
15+
FILE_VIEW_ID, GET_INFO_ID, GO_BACK_ID, GO_FORWARD_ID, GO_PARENT_ID, HELP_SEND_ERROR_REPORT_ID, MenuItems,
16+
NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID, PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, SEARCH_FILES_ID,
17+
SELECT_ALL_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SWAP_PANES_ID, SWITCH_PANE_ID,
18+
VIEW_MODE_BRIEF_ID, VIEW_MODE_FULL_ID, ViewMode, build_sort_submenu, copy_path_accelerator, register_item,
19+
show_in_file_manager_accelerator, show_in_file_manager_label,
2020
};
2121

2222
pub(crate) fn build_menu_macos<R: Runtime>(
@@ -35,6 +35,13 @@ pub(crate) fn build_menu_macos<R: Runtime>(
3535
"Enter license key..."
3636
};
3737
let license_item = MenuItem::with_id(app, ENTER_LICENSE_KEY_ID, license_label, true, None::<&str>)?;
38+
let check_for_updates_item = MenuItem::with_id(
39+
app,
40+
CHECK_FOR_UPDATES_ID,
41+
"Check for updates\u{2026}",
42+
true,
43+
None::<&str>,
44+
)?;
3845
let settings_item = MenuItem::with_id(app, SETTINGS_ID, "Settings...", true, Some("Cmd+,"))?;
3946

4047
let app_menu = Submenu::with_items(
@@ -44,6 +51,7 @@ pub(crate) fn build_menu_macos<R: Runtime>(
4451
&[
4552
&about_item,
4653
&license_item,
54+
&check_for_updates_item,
4755
&PredefinedMenuItem::separator(app)?,
4856
&settings_item,
4957
&PredefinedMenuItem::separator(app)?,
@@ -331,6 +339,10 @@ pub(crate) fn build_menu_macos<R: Runtime>(
331339
0,
332340
);
333341

342+
// cmdr menu positions: about(0), license(1), check_for_updates(2), sep(3), settings(4),
343+
// sep(5), hide(6), hide_others(7), show_all(8), sep(9), quit(10)
344+
register_item(&mut items, CHECK_FOR_UPDATES_ID, &check_for_updates_item, &app_menu, 2);
345+
334346
Ok(MenuItems {
335347
menu,
336348
show_hidden_files: show_hidden_item,
@@ -449,6 +461,7 @@ fn set_macos_menu_icons_inner() {
449461
"cmdr" => &[
450462
("Enter license key\u{2026}", "key"),
451463
("See license details\u{2026}", "key"),
464+
("Check for updates\u{2026}", "arrow.down.circle"),
452465
("Settings\u{2026}", "gearshape"),
453466
],
454467
"File" => &[

apps/desktop/src-tauri/src/menu/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ pub fn menu_id_to_command(menu_id: &str) -> Option<(&'static str, CommandScope)>
8888
COMMAND_PALETTE_ID => Some(("app.commandPalette", CommandScope::FileScoped)),
8989
SEARCH_FILES_ID => Some(("search.open", CommandScope::FileScoped)),
9090
HELP_SEND_ERROR_REPORT_ID => Some(("help.sendErrorReport", CommandScope::App)),
91+
CHECK_FOR_UPDATES_ID => Some(("app.checkForUpdates", CommandScope::App)),
9192

9293
// Pane commands (file-scoped)
9394
SWITCH_PANE_ID => Some(("pane.switch", CommandScope::FileScoped)),
@@ -150,6 +151,7 @@ pub fn command_id_to_menu_id(command_id: &str) -> Option<&'static str> {
150151
"app.commandPalette" => Some(COMMAND_PALETTE_ID),
151152
"search.open" => Some(SEARCH_FILES_ID),
152153
"help.sendErrorReport" => Some(HELP_SEND_ERROR_REPORT_ID),
154+
"app.checkForUpdates" => Some(CHECK_FOR_UPDATES_ID),
153155
"pane.switch" => Some(SWITCH_PANE_ID),
154156
"pane.swap" => Some(SWAP_PANES_ID),
155157
"nav.back" => Some(GO_BACK_ID),
@@ -305,6 +307,9 @@ pub const SETTINGS_ID: &str = "settings";
305307
/// Menu item ID for "Send error report…" (under the Help menu).
306308
pub const HELP_SEND_ERROR_REPORT_ID: &str = "help_send_error_report";
307309

310+
/// Menu item ID for "Check for updates…" (under the Cmdr / Help menu).
311+
pub const CHECK_FOR_UPDATES_ID: &str = "check_for_updates";
312+
308313
/// Platform-aware accelerator for "Copy path to clipboard".
309314
/// On macOS: Ctrl+Cmd+C. On Linux: Ctrl+Shift+C (Ctrl+Cmd+C becomes Ctrl+Ctrl+C which is broken).
310315
#[cfg(target_os = "macos")]
@@ -984,6 +989,7 @@ mod tests {
984989
"selection.selectAll",
985990
"selection.deselectAll",
986991
"help.sendErrorReport",
992+
"app.checkForUpdates",
987993
];
988994

989995
for command_id in &command_ids {

apps/desktop/src/lib/commands/command-registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ export const commands: Command[] = [
3232
shortcuts: ['⌘⇧P'],
3333
},
3434
{ id: 'app.settings', name: 'Open settings', scope: 'App', showInPalette: true, shortcuts: ['⌘,'] },
35+
{
36+
id: 'app.checkForUpdates',
37+
name: 'Check for updates…',
38+
scope: 'App',
39+
showInPalette: true,
40+
shortcuts: [],
41+
description: 'Check whether a newer version of Cmdr is available, and download it if so',
42+
},
3543
{
3644
id: 'help.sendErrorReport',
3745
name: 'Send error report…',

apps/desktop/src/lib/settings/sections/UpdatesSection.svelte

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import SettingSwitch from '../components/SettingSwitch.svelte'
55
import { getSettingDefinition } from '$lib/settings'
66
import { createShouldShow } from '$lib/settings/settings-search'
7+
import Button from '$lib/ui/Button.svelte'
8+
import { updateState, checkForUpdates } from '$lib/updates/updater.svelte'
9+
import { formatUpdateStatus } from '$lib/updates/update-status-text'
10+
import { openErrorReportDialog } from '$lib/error-reporter/error-report-flow.svelte'
711
812
interface Props {
913
searchQuery: string
@@ -16,9 +20,33 @@
1620
const autoCheckDef = getSettingDefinition('updates.autoCheck') ?? { label: '', description: '' }
1721
const crashReportsDef = getSettingDefinition('updates.crashReports') ?? { label: '', description: '' }
1822
const errorReportsDef = getSettingDefinition('updates.errorReports') ?? { label: '', description: '' }
23+
24+
const statusText = $derived(formatUpdateStatus(updateState))
25+
const buttonDisabled = $derived(updateState.status !== 'idle')
26+
27+
function handleCheckForUpdates() {
28+
void checkForUpdates()
29+
}
30+
31+
function handleSendErrorReport() {
32+
openErrorReportDialog(`Update check failed: ${updateState.error ?? ''}`)
33+
}
1934
</script>
2035

2136
<SettingsSection title="Updates">
37+
<div class="check-row">
38+
<Button variant="secondary" size="mini" onclick={handleCheckForUpdates} disabled={buttonDisabled}>
39+
Check for updates
40+
</Button>
41+
<div class="status">
42+
{#if updateState.error !== null}
43+
<span class="error-message">Error: {updateState.error}</span>
44+
<button class="link-button" onclick={handleSendErrorReport}>Send error report</button>
45+
{:else if statusText}
46+
<span class="status-text">{statusText}</span>
47+
{/if}
48+
</div>
49+
</div>
2250
{#if shouldShow('updates.autoCheck')}
2351
<SettingRow
2452
id="updates.autoCheck"
@@ -50,3 +78,45 @@
5078
</SettingRow>
5179
{/if}
5280
</SettingsSection>
81+
82+
<style>
83+
.check-row {
84+
display: flex;
85+
flex-direction: column;
86+
gap: var(--spacing-xs);
87+
margin-bottom: var(--spacing-md);
88+
}
89+
90+
.status {
91+
display: flex;
92+
flex-direction: column;
93+
gap: var(--spacing-xs);
94+
font-size: var(--font-size-sm);
95+
color: var(--color-text-secondary);
96+
min-height: 1.4em;
97+
}
98+
99+
.status-text {
100+
line-height: 1.4;
101+
}
102+
103+
.error-message {
104+
color: var(--color-text-primary);
105+
line-height: 1.4;
106+
}
107+
108+
.link-button {
109+
background: none;
110+
border: none;
111+
padding: 0;
112+
font-size: var(--font-size-xs);
113+
color: var(--color-text-tertiary);
114+
cursor: default;
115+
text-align: left;
116+
align-self: flex-start;
117+
}
118+
119+
.link-button:hover {
120+
color: var(--color-text-secondary);
121+
}
122+
</style>

0 commit comments

Comments
 (0)