feat(desktop): cross-platform quit action (#5266)
This implements consistent cross-platform quit functionality that triggers graceful application shutdown through native native commands. Closes FE-919 The Cmd + Q quit shortcut was previously broken across platforms with inconsistent behavior, working on Windows 11 but failing on macOS, AppImage Linux, and Windows 10. The implementation was mixed and unreliable.
This commit is contained in:
parent
636d350c0f
commit
1f158a19ff
5 changed files with 75 additions and 6 deletions
11
packages/hoppscotch-common/src/components.d.ts
vendored
11
packages/hoppscotch-common/src/components.d.ts
vendored
|
|
@ -208,8 +208,10 @@ declare module 'vue' {
|
|||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
|
||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideCircleCheck: typeof import('~icons/lucide/circle-check')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
|
|
@ -217,11 +219,14 @@ declare module 'vue' {
|
|||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucidePlusCircle: typeof import('~icons/lucide/plus-circle')['default']
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideTriangleAlert: typeof import('~icons/lucide/triangle-alert')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||
IconLucideX: typeof import('~icons/lucide/x')['default']
|
||||
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
|
||||
ImportExportCorsErrorModal: typeof import('./components/importExport/CorsErrorModal.vue')['default']
|
||||
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
|
||||
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
|
||||
ImportExportImportExportStepsAllCollectionImport: typeof import('./components/importExport/ImportExportSteps/AllCollectionImport.vue')['default']
|
||||
|
|
@ -248,9 +253,6 @@ declare module 'vue' {
|
|||
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
|
||||
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
|
||||
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
||||
ModalsNativeCACertificates: typeof import('./../../hoppscotch-selfhost-desktop/src/components/modals/NativeCACertificates.vue')['default']
|
||||
ModalsNativeClientCertificates: typeof import('./../../hoppscotch-selfhost-desktop/src/components/modals/NativeClientCertificates.vue')['default']
|
||||
ModalsNativeClientCertsAdd: typeof import('./../../hoppscotch-selfhost-desktop/src/components/modals/NativeClientCertsAdd.vue')['default']
|
||||
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
|
||||
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
|
||||
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
|
||||
|
|
@ -262,7 +264,6 @@ declare module 'vue' {
|
|||
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
||||
SettingsExtensionSubtitle: typeof import('./components/settings/ExtensionSubtitle.vue')['default']
|
||||
SettingsNative: typeof import('./components/settings/Native.vue')['default']
|
||||
SettingsNativeInterceptor: typeof import('./../../hoppscotch-selfhost-desktop/src/components/settings/NativeInterceptor.vue')['default']
|
||||
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
||||
Share: typeof import('./components/share/index.vue')['default']
|
||||
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
|
|||
import { HoppGQLSaveContext } from "./graphql/document"
|
||||
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
||||
import { computed } from "vue"
|
||||
import { getKernelMode } from "@hoppscotch/kernel"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
|
||||
export type HoppAction =
|
||||
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
||||
|
|
@ -85,6 +87,7 @@ export type HoppAction =
|
|||
| "tab.duplicate-tab" // Duplicate REST request
|
||||
| "gql.request.open" // Open GraphQL request
|
||||
| "agent.open-registration-modal" // Open Hoppscotch Agent registration modal
|
||||
| "app.quit" // Quit app
|
||||
|
||||
/**
|
||||
* Defines the arguments, if present for a given type that is required to be passed on
|
||||
|
|
@ -320,3 +323,36 @@ export function defineActionHandler<A extends HoppAction>(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: This Sets up core app-level action handlers that
|
||||
* should be available throughout the app's lifecycle.
|
||||
* These handlers are bound immediately when the actions module
|
||||
* is imported and don't depend on some component lifecycle.
|
||||
*/
|
||||
function setupCoreActionHandlers() {
|
||||
//
|
||||
// This action is triggered by either by
|
||||
// keyboard shortcut: Cmd+Q (macOS) / Ctrl+Q (Windows/Linux)
|
||||
// or `invokeAction("app.quit")`
|
||||
//
|
||||
// Desktop calls native `quit_app` command to close the app,
|
||||
// and it's a no-op on web app.
|
||||
//
|
||||
// @see {@link https://docs.rs/tauri/latest/tauri/struct.AppHandle.html#method.exit}
|
||||
// for native `app.exit` docs.
|
||||
bindAction("app.quit", async () => {
|
||||
if (getKernelMode() === "desktop") {
|
||||
try {
|
||||
await invoke("quit_app")
|
||||
} catch (error) {
|
||||
console.error("Failed to quit application:", error)
|
||||
// NOTE: Don't call window.close() here as a fallback because
|
||||
// if the native command fails, it likely means this not in
|
||||
// the proper context, and window.close() would close the wrong thing.
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setupCoreActionHandlers()
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const desktopBindings: {
|
|||
"ctrl-alt-right": "tab.next",
|
||||
"ctrl-alt-0": "tab.switch-to-last",
|
||||
"ctrl-alt-9": "tab.switch-to-first",
|
||||
"ctrl-q": "app.quit",
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,6 +17,25 @@ fn hopp_auth_port() -> u16 {
|
|||
*SERVER_PORT.get().expect("Server port not initialized")
|
||||
}
|
||||
|
||||
/// Gracefully quit the Hoppscotch Desktop
|
||||
///
|
||||
/// This command is invoked from the frontend when the user triggers
|
||||
/// the quit action (typically via Cmd+Q/Ctrl+Q keyboard shortcut).
|
||||
///
|
||||
/// It performs a clean shutdown by logging the quit request
|
||||
/// for debugging and then calling `app.exit(0)` which triggers
|
||||
/// v2's graceful shutdown.
|
||||
///
|
||||
/// It basically trigger `RunEvent::ExitRequested`
|
||||
/// followed by `RunEvent::Exit` events.
|
||||
/// See https://docs.rs/tauri/latest/tauri/struct.AppHandle.html#method.exit
|
||||
#[tauri::command]
|
||||
fn quit_app(app: tauri::AppHandle) -> Result<(), String> {
|
||||
tracing::info!("Quit command received, shutting down application");
|
||||
app.exit(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
|
|
@ -83,7 +102,8 @@ pub fn run() {
|
|||
.invoke_handler(tauri::generate_handler![
|
||||
hopp_auth_port,
|
||||
updater::check_updates_available,
|
||||
updater::install_updates_and_restart
|
||||
updater::install_updates_and_restart,
|
||||
quit_app,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
|
|
@ -221,6 +221,17 @@ async function initApp() {
|
|||
let shortcutEvent: string | null = null
|
||||
|
||||
if (
|
||||
isCtrlOrCmd &&
|
||||
!e.shiftKey &&
|
||||
!e.altKey &&
|
||||
e.key.toLowerCase() === "q"
|
||||
) {
|
||||
// Ctrl/Cmd + Q - Quit Application
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
shortcutEvent = "ctrl-q"
|
||||
} else if (
|
||||
isCtrlOrCmd &&
|
||||
!e.shiftKey &&
|
||||
!e.altKey &&
|
||||
|
|
|
|||
Loading…
Reference in a new issue