diff --git a/apps/staged/src-tauri/src/lib.rs b/apps/staged/src-tauri/src/lib.rs index 3d5e1fcc7..b7b810a14 100644 --- a/apps/staged/src-tauri/src/lib.rs +++ b/apps/staged/src-tauri/src/lib.rs @@ -1930,6 +1930,13 @@ pub fn run() { true, Some("CmdOrCtrl+Shift+G"), )?; + let delete_project_item = MenuItem::with_id( + handle, + "delete_project", + "Delete Project", + true, + Some("CmdOrCtrl+Backspace"), + )?; let zoom_in_item = MenuItem::with_id(handle, "zoom_in", "Zoom In", true, Some("CmdOrCtrl+="))?; let zoom_out_item = @@ -1982,6 +1989,7 @@ pub fn run() { &PredefinedMenuItem::cut(handle, None)?, &PredefinedMenuItem::copy(handle, None)?, &PredefinedMenuItem::paste(handle, None)?, + &delete_project_item, &PredefinedMenuItem::select_all(handle, None)?, &PredefinedMenuItem::separator(handle)?, &find_item, @@ -2169,6 +2177,7 @@ pub fn run() { "find" => Some("menu:find"), "find_next" => Some("menu:find-next"), "find_previous" => Some("menu:find-previous"), + "delete_project" => Some("menu:delete-project"), "zoom_in" => Some("menu:zoom-in"), "zoom_out" => Some("menu:zoom-out"), "zoom_reset" => Some("menu:zoom-reset"), diff --git a/apps/staged/src/App.svelte b/apps/staged/src/App.svelte index 6a522d64a..13cc22ba4 100644 --- a/apps/staged/src/App.svelte +++ b/apps/staged/src/App.svelte @@ -60,6 +60,7 @@ let unlistenFind: UnlistenFn | undefined; let unlistenFindNext: UnlistenFn | undefined; let unlistenFindPrevious: UnlistenFn | undefined; + let unlistenDeleteProject: UnlistenFn | undefined; let unlistenZoomIn: UnlistenFn | undefined; let unlistenZoomOut: UnlistenFn | undefined; let unlistenZoomReset: UnlistenFn | undefined; @@ -138,6 +139,24 @@ return true; } + function isTextInputActive(): boolean { + const target = document.activeElement; + if (!(target instanceof HTMLElement)) return false; + if (target.isContentEditable) return true; + return ( + target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT' + ); + } + + function requestDeleteCurrentProject(): boolean { + if (isTextInputActive()) return false; + if (navigation.currentRoute.kind !== 'project') return false; + + const event = new CustomEvent('staged:delete-current-project', { cancelable: true }); + window.dispatchEvent(event); + return event.defaultPrevented; + } + async function logUpdater(message: string) { console.warn(message); try { @@ -261,6 +280,9 @@ unlistenFindPrevious = listenToEvent('menu:find-previous', () => { if (!triggerShortcut('search-find-previous')) runSearchShortcut('previous'); }); + unlistenDeleteProject = listenToEvent('menu:delete-project', () => { + triggerShortcut('app-delete-project'); + }); unlistenZoomIn = listenToEvent('menu:zoom-in', () => { if (!triggerShortcut('view-increase-size')) increaseSize(); }); @@ -340,6 +362,14 @@ modifiers: { meta: true }, handler: navigateBack, }, + { + id: 'app-delete-project', + description: 'Remove current project', + category: 'app', + keys: ['Backspace', 'Delete'], + modifiers: { meta: true }, + handler: requestDeleteCurrentProject, + }, { id: 'search-find', description: 'Find in open note/session', @@ -449,6 +479,7 @@ unlistenFind?.(); unlistenFindNext?.(); unlistenFindPrevious?.(); + unlistenDeleteProject?.(); unlistenZoomIn?.(); unlistenZoomOut?.(); unlistenZoomReset?.(); diff --git a/apps/staged/src/lib/features/projects/ProjectHome.svelte b/apps/staged/src/lib/features/projects/ProjectHome.svelte index c27400089..415689cb5 100644 --- a/apps/staged/src/lib/features/projects/ProjectHome.svelte +++ b/apps/staged/src/lib/features/projects/ProjectHome.svelte @@ -115,6 +115,9 @@ let branchToDelete = $state<{ branch: Branch; project: Project } | null>(null); let deletingBranches = $state>(new Set()); let deletingProjectNames = $state>(new Map()); + // Guards the delete shortcut while the async safe-to-delete check is in flight, + // before projectToDelete/deletingProjectNames are set, so a held key only deletes once. + let deleteShortcutPending = $state(false); // Setup errors come from the shared workspace lifecycle orchestrator. let worktreeErrors = $derived(workspaceLifecycle.getWorktreeErrors()); @@ -138,6 +141,8 @@ const onCacheStale = () => loadData(); window.addEventListener('staged:new-project', onNewProject); window.addEventListener('cache-stale', onCacheStale); + const onDeleteCurrentProject = (event: Event) => handleDeleteCurrentProjectShortcut(event); + window.addEventListener('staged:delete-current-project', onDeleteCurrentProject); const unlistenDetection = listenToRepoActionsDetection((event) => { const matchingProjectIds = projects @@ -273,6 +278,7 @@ loadGeneration++; window.removeEventListener('staged:new-project', onNewProject); window.removeEventListener('cache-stale', onCacheStale); + window.removeEventListener('staged:delete-current-project', onDeleteCurrentProject); unlistenDetection(); unlistenProjectRepoAdded(); unlistenPrStatus(); @@ -871,6 +877,26 @@ } } + function handleDeleteCurrentProjectShortcut(event: Event) { + if ( + !selectedProject || + deleteShortcutPending || + selectedProjectDeleting || + projectToDelete || + branchToDelete || + showNewProjectModal || + showAddRepoModal + ) { + return; + } + + event.preventDefault(); + deleteShortcutPending = true; + void handleDeleteProjectRequest(selectedProject).finally(() => { + deleteShortcutPending = false; + }); + } + async function confirmDeleteProject() { if (!projectToDelete) return; const id = projectToDelete.id;