feat(desktop): native tab keyboard shortcuts (#5190)
This commit is contained in:
parent
aa5b540412
commit
78e623a847
13 changed files with 426 additions and 15 deletions
|
|
@ -39,6 +39,7 @@ in {
|
|||
nodePackages.prisma
|
||||
prisma-engines
|
||||
cargo-edit
|
||||
cargo-tauri
|
||||
] ++ lib.optionals pkgs.stdenv.isDarwin darwinPackages
|
||||
++ lib.optionals pkgs.stdenv.isLinux linuxPackages;
|
||||
|
||||
|
|
|
|||
|
|
@ -1199,6 +1199,10 @@
|
|||
"close_others": "Close all other tabs",
|
||||
"duplicate": "Duplicate current tab",
|
||||
"new_tab": "Open a new tab",
|
||||
"next": "Switch to next tab",
|
||||
"previous": "Switch to previous tab",
|
||||
"switch_to_first": "Switch to first tab",
|
||||
"switch_to_last": "Switch to last tab",
|
||||
"title": "Tabs"
|
||||
},
|
||||
"workspace": {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"@noble/curves": "1.6.0",
|
||||
"@scure/base": "1.1.9",
|
||||
"@shopify/lang-jsonc": "1.0.0",
|
||||
"@tauri-apps/api": "2.1.1",
|
||||
"@tauri-apps/plugin-store": "2.2.0",
|
||||
"@types/hawk": "9.0.6",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ export type HoppAction =
|
|||
| "tab.close-current" // Close current tab
|
||||
| "tab.close-other" // Close other tabs
|
||||
| "tab.open-new" // Open new tab
|
||||
| "tab.next" // Switch to next tab
|
||||
| "tab.prev" // Switch to previous tab
|
||||
| "tab.switch-to-first" // Switch to first tab
|
||||
| "tab.switch-to-last" // Switch to last tab
|
||||
| "tab.reopen-closed" // Reopen recently closed tab
|
||||
| "collection.new" // Create root collection
|
||||
| "flyouts.chat.open" // Shows the keybinds flyout
|
||||
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { onBeforeUnmount, onMounted } from "vue"
|
|||
import { HoppActionWithOptionalArgs, invokeAction } from "./actions"
|
||||
import { isAppleDevice } from "./platformutils"
|
||||
import { isDOMElement, isTypableElement } from "./utils/dom"
|
||||
import { getKernelMode } from "@hoppscotch/kernel"
|
||||
import { listen } from "@tauri-apps/api/event"
|
||||
|
||||
/**
|
||||
* This variable keeps track whether keybindings are being accepted
|
||||
|
|
@ -10,6 +12,11 @@ import { isDOMElement, isTypableElement } from "./utils/dom"
|
|||
*/
|
||||
let keybindingsEnabled = true
|
||||
|
||||
/**
|
||||
* Unlisten function for Tauri event
|
||||
*/
|
||||
let unlistenTauriEvent: (() => void) | null = null
|
||||
|
||||
/**
|
||||
* Alt is also regarded as macOS OPTION (⌥) key
|
||||
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
|
||||
|
|
@ -30,7 +37,7 @@ type Key =
|
|||
| "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
|
||||
| "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3"
|
||||
| "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
|
||||
| "right" | "/" | "?" | "." | "enter"
|
||||
| "right" | "/" | "?" | "." | "enter" | "tab"
|
||||
/* eslint-enable */
|
||||
|
||||
type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`
|
||||
|
|
@ -39,7 +46,8 @@ type SingleCharacterShortcutKey = `${Key}`
|
|||
|
||||
type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey
|
||||
|
||||
export const bindings: {
|
||||
// Base bindings available on all platforms
|
||||
const baseBindings: {
|
||||
[_ in ShortcutKey]?: HoppActionWithOptionalArgs
|
||||
} = {
|
||||
"ctrl-enter": "request.send-cancel",
|
||||
|
|
@ -71,18 +79,68 @@ export const bindings: {
|
|||
"ctrl-shift-l": "editor.format",
|
||||
}
|
||||
|
||||
// Desktop-only bindings
|
||||
const desktopBindings: {
|
||||
[_ in ShortcutKey]?: HoppActionWithOptionalArgs
|
||||
} = {
|
||||
"ctrl-w": "tab.close-current",
|
||||
"ctrl-t": "tab.open-new",
|
||||
"ctrl-alt-left": "tab.prev",
|
||||
"ctrl-alt-right": "tab.next",
|
||||
"ctrl-alt-0": "tab.switch-to-last",
|
||||
"ctrl-alt-9": "tab.switch-to-first",
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bindings based on the current kernel mode
|
||||
*/
|
||||
function getActiveBindings(): typeof baseBindings {
|
||||
const kernelMode = getKernelMode()
|
||||
|
||||
if (kernelMode === "desktop") {
|
||||
return {
|
||||
...baseBindings,
|
||||
...desktopBindings,
|
||||
}
|
||||
}
|
||||
|
||||
return baseBindings
|
||||
}
|
||||
|
||||
export const bindings = getActiveBindings()
|
||||
|
||||
/**
|
||||
* A composable that hooks to the caller component's
|
||||
* lifecycle and hooks to the keyboard events to fire
|
||||
* the appropriate actions based on keybindings
|
||||
*/
|
||||
export function hookKeybindingsListener() {
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
// Listen for Tauri events (desktop only)
|
||||
if (getKernelMode() === "desktop") {
|
||||
try {
|
||||
unlistenTauriEvent = await listen(
|
||||
"hoppscotch_desktop_shortcut",
|
||||
(ev) => {
|
||||
console.info("Tauri shortcut ev", ev)
|
||||
handleTauriShortcut(ev.payload as string)
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to setup Tauri event listener:", error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
|
||||
if (unlistenTauriEvent) {
|
||||
unlistenTauriEvent()
|
||||
unlistenTauriEvent = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -93,13 +151,27 @@ function handleKeyDown(ev: KeyboardEvent) {
|
|||
const binding = generateKeybindingString(ev)
|
||||
if (!binding) return
|
||||
|
||||
const boundAction = bindings[binding]
|
||||
const activeBindings = getActiveBindings()
|
||||
const boundAction = activeBindings[binding]
|
||||
if (!boundAction) return
|
||||
|
||||
ev.preventDefault()
|
||||
invokeAction(boundAction, undefined, "keypress")
|
||||
}
|
||||
|
||||
function handleTauriShortcut(shortcut: string) {
|
||||
console.info("Tauri shortcut:", shortcut)
|
||||
|
||||
// Do not check keybinds if the mode is disabled
|
||||
if (!keybindingsEnabled) return
|
||||
|
||||
const activeBindings = getActiveBindings()
|
||||
const boundAction = activeBindings[shortcut as ShortcutKey]
|
||||
if (!boundAction) return
|
||||
|
||||
invokeAction(boundAction, undefined, "keypress")
|
||||
}
|
||||
|
||||
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||
const target = ev.target
|
||||
|
||||
|
|
@ -140,6 +212,9 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
|
|||
return key.slice(5) as Key
|
||||
}
|
||||
|
||||
// Check for Tab key
|
||||
if (key === "tab") return "tab"
|
||||
|
||||
// Check letter keys
|
||||
const isLetter = key.length === 1 && key >= "a" && key <= "z"
|
||||
if (isLetter) return key as Key
|
||||
|
|
|
|||
|
|
@ -240,11 +240,34 @@ defineActionHandler("request.rename", () => {
|
|||
defineActionHandler("tab.duplicate-tab", ({ tabID }) => {
|
||||
duplicateTab(tabID ?? currentTabID.value)
|
||||
})
|
||||
|
||||
defineActionHandler("tab.close-current", () => {
|
||||
removeTab(currentTabID.value)
|
||||
})
|
||||
|
||||
defineActionHandler("tab.close-other", () => {
|
||||
tabs.closeOtherTabs(currentTabID.value)
|
||||
})
|
||||
|
||||
defineActionHandler("tab.open-new", addNewTab)
|
||||
|
||||
defineActionHandler("tab.next", () => {
|
||||
tabs.goToNextTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.prev", () => {
|
||||
tabs.goToPreviousTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.switch-to-first", () => {
|
||||
tabs.goToFirstTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.switch-to-last", () => {
|
||||
tabs.goToLastTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.reopen-closed", () => {
|
||||
tabs.reopenClosedTab()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -401,17 +401,41 @@ defineActionHandler("request.rename", () => {
|
|||
if (tabs.currentActiveTab.value.document.type === "request")
|
||||
openReqRenameModal(tabs.currentActiveTab.value.id)
|
||||
})
|
||||
|
||||
defineActionHandler("tab.duplicate-tab", ({ tabID }) => {
|
||||
duplicateTab(tabID ?? currentTabID.value)
|
||||
})
|
||||
|
||||
defineActionHandler("tab.close-current", () => {
|
||||
removeTab(currentTabID.value)
|
||||
})
|
||||
|
||||
defineActionHandler("tab.close-other", () => {
|
||||
tabs.closeOtherTabs(currentTabID.value)
|
||||
})
|
||||
|
||||
defineActionHandler("tab.open-new", addNewTab)
|
||||
|
||||
defineActionHandler("tab.next", () => {
|
||||
tabs.goToNextTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.prev", () => {
|
||||
tabs.goToPreviousTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.switch-to-first", () => {
|
||||
tabs.goToFirstTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.switch-to-last", () => {
|
||||
tabs.goToLastTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.reopen-closed", () => {
|
||||
tabs.reopenClosedTab()
|
||||
})
|
||||
|
||||
useService(RequestInspectorService)
|
||||
useService(EnvironmentInspectorService)
|
||||
useService(ResponseInspectorService)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,15 @@ import IconCopy from "~icons/lucide/copy"
|
|||
import IconCopyPlus from "~icons/lucide/copy-plus"
|
||||
import IconXCircle from "~icons/lucide/x-circle"
|
||||
import IconXSquare from "~icons/lucide/x-square"
|
||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||
import IconArrowRight from "~icons/lucide/arrow-right"
|
||||
import IconChevronsLeft from "~icons/lucide/chevrons-left"
|
||||
import IconChevronsRight from "~icons/lucide/chevrons-right"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { Container } from "dioc"
|
||||
import { getKernelMode } from "@hoppscotch/kernel"
|
||||
|
||||
type Doc = {
|
||||
text: string | string[]
|
||||
|
|
@ -53,6 +58,8 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
|||
: this.restTab.getActiveTabs().value.length === 1
|
||||
)
|
||||
|
||||
private isDesktopMode = computed(() => getKernelMode() === "desktop")
|
||||
|
||||
private documents: Record<string, Doc> = reactive({
|
||||
duplicate_tab: {
|
||||
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.duplicate")],
|
||||
|
|
@ -88,6 +95,39 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
|||
icon: markRaw(IconCopyPlus),
|
||||
excludeFromSearch: computed(() => !this.showAction.value),
|
||||
},
|
||||
// NOTE: Desktop-only actions
|
||||
tab_prev: {
|
||||
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.previous")],
|
||||
alternates: ["tab", "previous", "prev", "switch"],
|
||||
icon: markRaw(IconArrowLeft),
|
||||
excludeFromSearch: computed(
|
||||
() => !this.showAction.value || !this.isDesktopMode.value || this.isOnlyTab.value
|
||||
),
|
||||
},
|
||||
tab_next: {
|
||||
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.next")],
|
||||
alternates: ["tab", "next", "switch"],
|
||||
icon: markRaw(IconArrowRight),
|
||||
excludeFromSearch: computed(
|
||||
() => !this.showAction.value || !this.isDesktopMode.value || this.isOnlyTab.value
|
||||
),
|
||||
},
|
||||
tab_switch_to_first: {
|
||||
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.switch_to_first")],
|
||||
alternates: ["tab", "first", "switch", "go to first"],
|
||||
icon: markRaw(IconChevronsLeft),
|
||||
excludeFromSearch: computed(
|
||||
() => !this.showAction.value || !this.isDesktopMode.value || this.isOnlyTab.value
|
||||
),
|
||||
},
|
||||
tab_switch_to_last: {
|
||||
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.switch_to_last")],
|
||||
alternates: ["tab", "last", "switch", "go to last"],
|
||||
icon: markRaw(IconChevronsRight),
|
||||
excludeFromSearch: computed(
|
||||
() => !this.showAction.value || !this.isDesktopMode.value || this.isOnlyTab.value
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: Constructors are no longer recommended as of dioc > 3, use onServiceInit instead
|
||||
|
|
@ -122,5 +162,9 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
|||
if (id === "close_current_tab") invokeAction("tab.close-current")
|
||||
if (id === "close_other_tabs") invokeAction("tab.close-other")
|
||||
if (id === "open_new_tab") invokeAction("tab.open-new")
|
||||
if (id === "tab_prev") invokeAction("tab.prev")
|
||||
if (id === "tab_next") invokeAction("tab.next")
|
||||
if (id === "tab_switch_to_first") invokeAction("tab.switch-to-first")
|
||||
if (id === "tab_switch_to_last") invokeAction("tab.switch-to-last")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,39 @@ export interface TabService<Doc> {
|
|||
*/
|
||||
closeOtherTabs(tabID: string): void
|
||||
|
||||
/**
|
||||
* Navigates to the next tab in the tab order.
|
||||
*/
|
||||
goToNextTab(): void
|
||||
|
||||
/**
|
||||
* Navigates to the previous tab in the tab order.
|
||||
*/
|
||||
goToPreviousTab(): void
|
||||
|
||||
/**
|
||||
* NOTE: Currently inert, plumbing is done, some platform issues around shortcuts, WIP for future.
|
||||
* Navigates to a tab by its index position (1-based).
|
||||
* @param index - The 1-based index of the tab to navigate to.
|
||||
*/
|
||||
goToTabByIndex(index: number): void
|
||||
|
||||
/**
|
||||
* Navigates to the first tab in the tab order.
|
||||
*/
|
||||
goToFirstTab(): void
|
||||
|
||||
/**
|
||||
* Navigates to the last tab in the tab order.
|
||||
*/
|
||||
goToLastTab(): void
|
||||
|
||||
/**
|
||||
* Reopens the most recently closed tab.
|
||||
* @returns True if a tab was reopened, false if no closed tabs are available.
|
||||
*/
|
||||
reopenClosedTab(): boolean
|
||||
|
||||
/**
|
||||
* Gets a computed reference to a persistable tab state.
|
||||
* @returns A computed reference to a persistable tab state object.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export abstract class TabService<Doc>
|
|||
HoppTab<Doc>
|
||||
> // TODO: The implicit cast is necessary as the reactive unwraps the inner types, creating weird type errors, this needs to be refactored and removed
|
||||
protected tabOrdering = ref<string[]>(["test"])
|
||||
protected recentlyClosedTabs: Array<{ tab: HoppTab<Doc>; index: number }> = []
|
||||
protected readonly MAX_CLOSED_TABS_HISTORY = 10
|
||||
|
||||
public currentTabID = refWithControl("test", {
|
||||
onBeforeChange: (newTabID) => {
|
||||
|
|
@ -168,7 +170,12 @@ export abstract class TabService<Doc>
|
|||
return
|
||||
}
|
||||
|
||||
this.tabOrdering.value.splice(this.tabOrdering.value.indexOf(tabID), 1)
|
||||
const tabIndex = this.tabOrdering.value.indexOf(tabID)
|
||||
const tab = this.tabMap.get(tabID)!
|
||||
|
||||
this.addToRecentlyClosedTabs(tab, tabIndex)
|
||||
|
||||
this.tabOrdering.value.splice(tabIndex, 1)
|
||||
|
||||
nextTick(() => {
|
||||
this.tabMap.delete(tabID)
|
||||
|
|
@ -192,6 +199,64 @@ export abstract class TabService<Doc>
|
|||
this.currentTabID.value = tabID
|
||||
}
|
||||
|
||||
public goToNextTab(): void {
|
||||
const currentIndex = this.tabOrdering.value.indexOf(this.currentTabID.value)
|
||||
const nextIndex = (currentIndex + 1) % this.tabOrdering.value.length
|
||||
const nextTabID = this.tabOrdering.value[nextIndex]
|
||||
this.setActiveTab(nextTabID)
|
||||
}
|
||||
|
||||
public goToPreviousTab(): void {
|
||||
const currentIndex = this.tabOrdering.value.indexOf(this.currentTabID.value)
|
||||
const prevIndex =
|
||||
currentIndex === 0 ? this.tabOrdering.value.length - 1 : currentIndex - 1
|
||||
const prevTabID = this.tabOrdering.value[prevIndex]
|
||||
this.setActiveTab(prevTabID)
|
||||
}
|
||||
|
||||
// NOTE: Currently inert, plumbing is done, some platform issues around shortcuts, WIP for future.
|
||||
public goToTabByIndex(index: number): void {
|
||||
if (index >= 1 && index <= this.tabOrdering.value.length) {
|
||||
const tabID = this.tabOrdering.value[index - 1]
|
||||
this.setActiveTab(tabID)
|
||||
}
|
||||
}
|
||||
|
||||
public goToFirstTab(): void {
|
||||
const firstTabID = this.tabOrdering.value[0]
|
||||
this.setActiveTab(firstTabID)
|
||||
}
|
||||
|
||||
public goToLastTab(): void {
|
||||
const lastTabID = this.tabOrdering.value[this.tabOrdering.value.length - 1]
|
||||
this.setActiveTab(lastTabID)
|
||||
}
|
||||
|
||||
public reopenClosedTab(): boolean {
|
||||
if (this.recentlyClosedTabs.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { tab, index } = this.recentlyClosedTabs.pop()!
|
||||
|
||||
this.tabMap.set(tab.id, tab)
|
||||
|
||||
const insertIndex = Math.min(index, this.tabOrdering.value.length)
|
||||
this.tabOrdering.value.splice(insertIndex, 0, tab.id)
|
||||
|
||||
this.setActiveTab(tab.id)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private addToRecentlyClosedTabs(tab: HoppTab<Doc>, index: number): void {
|
||||
this.recentlyClosedTabs.push({ tab, index })
|
||||
|
||||
if (this.recentlyClosedTabs.length > this.MAX_CLOSED_TABS_HISTORY) {
|
||||
this.recentlyClosedTabs.shift()
|
||||
}
|
||||
}
|
||||
|
||||
public persistableTabState = computed<PersistableTabState<Doc>>(() => ({
|
||||
lastActiveTabID: this.currentTabID.value,
|
||||
orderedDocs: this.tabOrdering.value.map((tabID) => {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ pub mod logger;
|
|||
pub mod server;
|
||||
pub mod updater;
|
||||
|
||||
use std::ops::Not;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use tauri::Emitter;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri_plugin_appload::VendorConfigBuilder;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_window_state::StateFlags;
|
||||
|
|
@ -21,7 +23,143 @@ fn hopp_auth_port() -> u16 {
|
|||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let new_tab = MenuItemBuilder::with_id("new_tab", "New Tab")
|
||||
.accelerator("CmdOrCtrl+T")
|
||||
.build(app)
|
||||
.expect("Failed to build new tab menu item");
|
||||
|
||||
let close_tab = MenuItemBuilder::with_id("close_tab", "Close Tab")
|
||||
.accelerator("CmdOrCtrl+W")
|
||||
.build(app)
|
||||
.expect("Failed to build close tab menu item");
|
||||
|
||||
let reopen_tab = MenuItemBuilder::with_id("reopen_tab", "Reopen Closed Tab")
|
||||
.accelerator("CmdOrCtrl+Shift+T")
|
||||
.build(app)
|
||||
.expect("Failed to build reopen tab menu item");
|
||||
|
||||
let file_menu = SubmenuBuilder::new(app, "File")
|
||||
.item(&new_tab)
|
||||
.item(&close_tab)
|
||||
.item(&reopen_tab)
|
||||
.build()
|
||||
.expect("Failed to build file menu");
|
||||
|
||||
let focus_url = MenuItemBuilder::with_id("focus_url", "Focus Address Bar")
|
||||
.accelerator("CmdOrCtrl+L")
|
||||
.build(app)
|
||||
.expect("Failed to build focus URL menu item");
|
||||
|
||||
let view_menu = SubmenuBuilder::new(app, "View")
|
||||
.item(&focus_url)
|
||||
.build()
|
||||
.expect("Failed to build view menu");
|
||||
|
||||
let next_tab = MenuItemBuilder::with_id("next_tab", "Next Tab")
|
||||
.accelerator("CmdOrCtrl+Alt+Right")
|
||||
.build(app)
|
||||
.expect("Failed to build next tab menu item");
|
||||
|
||||
let prev_tab = MenuItemBuilder::with_id("prev_tab", "Previous Tab")
|
||||
.accelerator("CmdOrCtrl+Alt+Left")
|
||||
.build(app)
|
||||
.expect("Failed to build previous tab menu item");
|
||||
|
||||
let tab_first = MenuItemBuilder::with_id("tab_first", "Switch to First Tab")
|
||||
.accelerator("CmdOrCtrl+Alt+9")
|
||||
.build(app)
|
||||
.expect("Failed to build first tab menu item");
|
||||
|
||||
let tab_last = MenuItemBuilder::with_id("tab_last", "Switch to Last Tab")
|
||||
.accelerator("CmdOrCtrl+Alt+0")
|
||||
.build(app)
|
||||
.expect("Failed to build last tab menu item");
|
||||
|
||||
let tabs_menu = SubmenuBuilder::new(app, "Tabs")
|
||||
.item(&next_tab)
|
||||
.item(&prev_tab)
|
||||
.separator()
|
||||
.item(&tab_first)
|
||||
.item(&tab_last)
|
||||
.build()
|
||||
.expect("Failed to build tabs menu");
|
||||
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(&file_menu)
|
||||
.item(&view_menu)
|
||||
.item(&tabs_menu)
|
||||
.build()
|
||||
.expect("Failed to build menu");
|
||||
|
||||
app.set_menu(menu).expect("Failed to set menu");
|
||||
|
||||
let handle = app.handle().clone();
|
||||
app.on_menu_event(move |app, event| {
|
||||
let event_id = event.id().as_ref();
|
||||
let webview_windows = app.webview_windows();
|
||||
let perhaps_window = webview_windows
|
||||
.iter()
|
||||
// Any window that's not `main` since there can
|
||||
// only be one window open at the moment.
|
||||
//
|
||||
// NOTE: Generally this should use `get_focused_window`
|
||||
// but that requires `unstable` flag currently
|
||||
// and causes arrow keys to insert escape sequences on MacOS
|
||||
// see: https://github.com/hoppscotch/hoppscotch/pull/5108
|
||||
.filter(|kv| kv.0.eq_ignore_ascii_case("main").not())
|
||||
.last()
|
||||
.map(|lw| lw.1);
|
||||
|
||||
if let Some(window) = perhaps_window {
|
||||
let shortcut_event = match event_id {
|
||||
"new_tab" => {
|
||||
tracing::info!("New Tab menu item triggered (CMD+T/Ctrl+T)");
|
||||
Some("ctrl-t")
|
||||
}
|
||||
"close_tab" => {
|
||||
tracing::info!("Close Tab menu item triggered (CMD+W/Ctrl+W)");
|
||||
Some("ctrl-w")
|
||||
}
|
||||
"reopen_tab" => {
|
||||
tracing::info!(
|
||||
"Reopen Tab menu item triggered (CMD+Shift+T/Ctrl+Shift+T)"
|
||||
);
|
||||
Some("ctrl-shift-t")
|
||||
}
|
||||
"next_tab" => {
|
||||
tracing::info!("Next Tab menu item triggered (CMD+Alt+Right/Ctrl+Alt+Right)");
|
||||
Some("ctrl-alt-right")
|
||||
}
|
||||
"prev_tab" => {
|
||||
tracing::info!(
|
||||
"Previous Tab menu item triggered (CMD+Alt+Left/Ctrl+Alt+Left)"
|
||||
);
|
||||
Some("ctrl-alt-left")
|
||||
}
|
||||
"tab_first" => {
|
||||
tracing::info!("Switch to First Tab menu item triggered (CMD+Alt+9/Ctrl+Alt+9)");
|
||||
Some("ctrl-alt-9")
|
||||
}
|
||||
"tab_last" => {
|
||||
tracing::info!("Switch to Last Tab menu item triggered (CMD+Alt+0/Ctrl+Alt+0)");
|
||||
Some("ctrl-alt-0")
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(shortcut) = shortcut_event {
|
||||
window
|
||||
.emit("hoppscotch_desktop_shortcut", shortcut)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::error!(
|
||||
error.message = %e,
|
||||
shortcut = shortcut,
|
||||
"Failed to emit shortcut event from menu"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let server_port = portpicker::pick_unused_port().expect("Cannot find unused port");
|
||||
tracing::info!("Selected server port: {}", server_port);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ fn main() {
|
|||
|
||||
println!("Starting Hoppscotch Desktop...");
|
||||
|
||||
return hoppscotch_desktop_lib::run()
|
||||
return hoppscotch_desktop_lib::run();
|
||||
};
|
||||
|
||||
let Ok(LogGuard(guard)) = logger::setup(&log_file_path) else {
|
||||
|
|
@ -34,7 +34,7 @@ fn main() {
|
|||
|
||||
println!("Starting Hoppscotch Desktop...");
|
||||
|
||||
return hoppscotch_desktop_lib::run()
|
||||
return hoppscotch_desktop_lib::run();
|
||||
};
|
||||
|
||||
// This keeps the guard alive, this is scoped to `main`
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ importers:
|
|||
version: 0.2.1(eslint@9.27.0(jiti@2.4.2))(terser@5.39.2)(typescript@5.6.3)(vite@5.4.9(@types/node@22.7.5)(sass@1.80.3)(terser@5.39.2))(vue@3.5.12(typescript@5.6.3))
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
version: 2.1.1
|
||||
'@tauri-apps/plugin-shell':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
|
|
@ -545,6 +545,9 @@ importers:
|
|||
'@shopify/lang-jsonc':
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
'@tauri-apps/api':
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
'@tauri-apps/plugin-store':
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0
|
||||
|
|
@ -5797,9 +5800,6 @@ packages:
|
|||
resolution: {integrity: sha512-6unsZDOdlXTmauU3NhWhn+Cx0rODV+rvNvTdvolE5Kls5ybA6cqndQENDt1+FS0tF7ozCP66jwWoH6a5h90BrA==}
|
||||
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||
|
||||
'@tauri-apps/api@2.0.2':
|
||||
resolution: {integrity: sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==}
|
||||
|
||||
'@tauri-apps/api@2.1.1':
|
||||
resolution: {integrity: sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==}
|
||||
|
||||
|
|
@ -20075,8 +20075,6 @@ snapshots:
|
|||
|
||||
'@tauri-apps/api@1.5.1': {}
|
||||
|
||||
'@tauri-apps/api@2.0.2': {}
|
||||
|
||||
'@tauri-apps/api@2.1.1': {}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@1.5.6':
|
||||
|
|
@ -20179,7 +20177,7 @@ snapshots:
|
|||
|
||||
'@tauri-apps/plugin-shell@2.0.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.0.2
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@tauri-apps/plugin-shell@2.0.1':
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in a new issue