From 824dce79d00cb80eff32dd73afe40749707d4bd3 Mon Sep 17 00:00:00 2001 From: Shreyas Date: Tue, 16 Dec 2025 17:25:56 +0530 Subject: [PATCH] feat(desktop): url focus and mru tab shortcuts (#5683) Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com> --- packages/hoppscotch-common/locales/en.json | 9 +- .../src/components/http/Request.vue | 6 + .../src/components/smart/EnvInput.vue | 12 + .../hoppscotch-common/src/helpers/actions.ts | 3 + .../src/helpers/keybindings.ts | 6 + .../src/helpers/shortcuts.ts | 15 ++ .../hoppscotch-common/src/pages/graphql.vue | 8 + .../hoppscotch-common/src/pages/index.vue | 8 + .../spotlight/searchers/tab.searcher.ts | 27 ++ .../tab/__tests__/tab.service.spec.ts | 243 ++++++++++++++++++ .../src/services/tab/index.ts | 23 ++ .../hoppscotch-common/src/services/tab/tab.ts | 123 +++++++++ packages/hoppscotch-desktop/.gitignore | 1 + packages/hoppscotch-selfhost-web/src/main.ts | 30 ++- 14 files changed, 508 insertions(+), 6 deletions(-) diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index 82dcd403..89c39870 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -1412,7 +1412,8 @@ "send_request": "Send Request", "share_request": "Share Request", "show_code": "Generate code snippet", - "title": "Request" + "title": "Request", + "focus_url": "Focus URL bar" }, "response": { "copy": "Copy response to clipboard", @@ -1427,7 +1428,9 @@ "next_tab": "Next Tab", "previous_tab": "Previous Tab", "first_tab": "Switch to First Tab", - "last_tab": "Switch to Last Tab" + "last_tab": "Switch to Last Tab", + "mru_switch": "Switch to recent tab (MRU)", + "mru_switch_reverse": "Switch to previous recent tab (MRU)" }, "theme": { "black": "Switch theme to Black Mode", @@ -1524,6 +1527,8 @@ "previous": "Switch to previous tab", "switch_to_first": "Switch to first tab", "switch_to_last": "Switch to last tab", + "mru_switch": "Switch to recent tab", + "mru_switch_reverse": "Switch to previous recent tab", "title": "Tabs" }, "workspace": { diff --git a/packages/hoppscotch-common/src/components/http/Request.vue b/packages/hoppscotch-common/src/components/http/Request.vue index cf0d41de..fe7fb9fe 100644 --- a/packages/hoppscotch-common/src/components/http/Request.vue +++ b/packages/hoppscotch-common/src/components/http/Request.vue @@ -56,6 +56,7 @@ class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition" > (null) const clearAll = ref(null) const copyRequestAction = ref(null) const saveRequestAction = ref(null) +const urlInput = ref<{ focus: () => void } | null>(null) const history = useReadonlyStream(restHistory$, []) @@ -667,6 +669,10 @@ defineActionHandler("request.show-code", () => { showCodegenModal.value = true }) +defineActionHandler("request.focus-url", () => { + urlInput.value?.focus() +}) + const isCustomMethod = computed(() => { return ( tab.value.document.request.method === "CUSTOM" || diff --git a/packages/hoppscotch-common/src/components/smart/EnvInput.vue b/packages/hoppscotch-common/src/components/smart/EnvInput.vue index 73f143a3..69253cc4 100644 --- a/packages/hoppscotch-common/src/components/smart/EnvInput.vue +++ b/packages/hoppscotch-common/src/components/smart/EnvInput.vue @@ -658,6 +658,18 @@ const triggerTextSelection = () => { }) }) } + +/** + * Focuses the input editor + */ +const focusInput = () => { + view.value?.focus() +} + +defineExpose({ + focus: focusInput, +}) + onMounted(() => { if (editor.value) { if (!view.value) initView(editor.value) diff --git a/packages/hoppscotch-common/src/helpers/actions.ts b/packages/hoppscotch-common/src/helpers/actions.ts index 704962b2..77f09ce7 100644 --- a/packages/hoppscotch-common/src/helpers/actions.ts +++ b/packages/hoppscotch-common/src/helpers/actions.ts @@ -70,6 +70,9 @@ export type HoppAction = | "tab.switch-to-first" // Switch to first tab | "tab.switch-to-last" // Switch to last tab | "tab.reopen-closed" // Reopen recently closed tab + | "tab.mru-switch" // Switch to MRU tab (Ctrl/Cmd+Alt+]) + | "tab.mru-switch-reverse" // Switch to previous MRU tab (Ctrl/Cmd+Alt+[) + | "request.focus-url" // Focus the URL bar | "collection.new" // Create root collection | "flyouts.chat.open" // Shows the keybinds flyout | "flyouts.keybinds.toggle" // Shows the keybinds flyout diff --git a/packages/hoppscotch-common/src/helpers/keybindings.ts b/packages/hoppscotch-common/src/helpers/keybindings.ts index dad787d1..636b0f65 100644 --- a/packages/hoppscotch-common/src/helpers/keybindings.ts +++ b/packages/hoppscotch-common/src/helpers/keybindings.ts @@ -44,6 +44,7 @@ type Key = | "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left" | "right" | "/" | "?" | "." | "enter" | "tab" | "delete" | "backspace" + | "[" | "]" /* eslint-enable */ type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}` @@ -108,6 +109,9 @@ const desktopBindings: { "ctrl-alt-0": "tab.switch-to-last", "ctrl-alt-9": "tab.switch-to-first", "ctrl-q": "app.quit", + "ctrl-alt-u": "request.focus-url", + "ctrl-alt-]": "tab.mru-switch", + "ctrl-alt-[": "tab.mru-switch-reverse", } /** @@ -314,6 +318,8 @@ function getPressedKey(ev: KeyboardEvent): Key | null { // Check if slash, period or enter if (key === "/" || key === "." || key === "enter") return key + if (key === "[" || key === "]") return key + // If no other cases match, this is not a valid key return null } diff --git a/packages/hoppscotch-common/src/helpers/shortcuts.ts b/packages/hoppscotch-common/src/helpers/shortcuts.ts index 75d592ac..b6aa1f5b 100644 --- a/packages/hoppscotch-common/src/helpers/shortcuts.ts +++ b/packages/hoppscotch-common/src/helpers/shortcuts.ts @@ -174,6 +174,11 @@ export function getShortcuts(t: (x: string) => string): ShortcutDef[] { // Desktop-only shortcuts const desktopShortcuts: ShortcutDef[] = [ + { + keys: [getPlatformSpecialKey(), getPlatformAlternateKey(), "U"], + label: t("shortcut.request.focus_url"), + section: t("shortcut.request.title"), + }, { keys: [getPlatformSpecialKey(), "T"], label: t("shortcut.tabs.new_tab"), @@ -204,6 +209,16 @@ export function getShortcuts(t: (x: string) => string): ShortcutDef[] { label: t("shortcut.tabs.last_tab"), section: t("shortcut.tabs.title"), }, + { + keys: [getPlatformSpecialKey(), getPlatformAlternateKey(), "]"], + label: t("shortcut.tabs.mru_switch"), + section: t("shortcut.tabs.title"), + }, + { + keys: [getPlatformSpecialKey(), getPlatformAlternateKey(), "["], + label: t("shortcut.tabs.mru_switch_reverse"), + section: t("shortcut.tabs.title"), + }, ] // Return base shortcuts + platform-specific shortcuts diff --git a/packages/hoppscotch-common/src/pages/graphql.vue b/packages/hoppscotch-common/src/pages/graphql.vue index 890dd94e..44679995 100644 --- a/packages/hoppscotch-common/src/pages/graphql.vue +++ b/packages/hoppscotch-common/src/pages/graphql.vue @@ -270,4 +270,12 @@ defineActionHandler("tab.switch-to-last", () => { defineActionHandler("tab.reopen-closed", () => { tabs.reopenClosedTab() }) + +defineActionHandler("tab.mru-switch", () => { + tabs.goToMRUTab() +}) + +defineActionHandler("tab.mru-switch-reverse", () => { + tabs.goToPreviousMRUTab() +}) diff --git a/packages/hoppscotch-common/src/pages/index.vue b/packages/hoppscotch-common/src/pages/index.vue index 13132936..bfcb216a 100644 --- a/packages/hoppscotch-common/src/pages/index.vue +++ b/packages/hoppscotch-common/src/pages/index.vue @@ -448,6 +448,14 @@ defineActionHandler("tab.reopen-closed", () => { tabs.reopenClosedTab() }) +defineActionHandler("tab.mru-switch", () => { + tabs.goToMRUTab() +}) + +defineActionHandler("tab.mru-switch-reverse", () => { + tabs.goToPreviousMRUTab() +}) + useService(RequestInspectorService) useService(EnvironmentInspectorService) useService(ResponseInspectorService) diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/tab.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/tab.searcher.ts index a8938d6a..c5eec15c 100644 --- a/packages/hoppscotch-common/src/services/spotlight/searchers/tab.searcher.ts +++ b/packages/hoppscotch-common/src/services/spotlight/searchers/tab.searcher.ts @@ -146,6 +146,31 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService< this.isOnlyTab.value ), }, + tab_mru_switch: { + text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.mru_switch")], + alternates: ["tab", "recent", "mru", "switch"], + icon: markRaw(IconArrowRight), + excludeFromSearch: computed( + () => + !this.showAction.value || + !this.isDesktopMode.value || + this.isOnlyTab.value + ), + }, + tab_mru_switch_reverse: { + text: [ + this.t("spotlight.tab.title"), + this.t("spotlight.tab.mru_switch_reverse"), + ], + alternates: ["tab", "recent", "mru", "previous", "switch"], + icon: markRaw(IconArrowLeft), + excludeFromSearch: computed( + () => + !this.showAction.value || + !this.isDesktopMode.value || + this.isOnlyTab.value + ), + }, }) // TODO: Constructors are no longer recommended as of dioc > 3, use onServiceInit instead @@ -184,5 +209,7 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService< 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") + if (id === "tab_mru_switch") invokeAction("tab.mru-switch") + if (id === "tab_mru_switch_reverse") invokeAction("tab.mru-switch-reverse") } } diff --git a/packages/hoppscotch-common/src/services/tab/__tests__/tab.service.spec.ts b/packages/hoppscotch-common/src/services/tab/__tests__/tab.service.spec.ts index ed61fb9e..96272dd4 100644 --- a/packages/hoppscotch-common/src/services/tab/__tests__/tab.service.spec.ts +++ b/packages/hoppscotch-common/src/services/tab/__tests__/tab.service.spec.ts @@ -23,6 +23,14 @@ class MockTabService extends TabService<{ request: string }> { this.watchCurrentTabID() } + + public getMRUOrder(): string[] { + return [...this.mruOrder] + } + + public getMRUNavigationIndex(): number { + return this.mruNavigationIndex + } } describe("TabService", () => { @@ -341,4 +349,239 @@ describe("TabService", () => { ]) }) }) + + describe("MRU Tab Navigation", () => { + it("should track MRU order when switching tabs", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const tab2 = service.createNewTab({ request: "second request" }) + const tab3 = service.createNewTab({ request: "third request" }) + + expect(service.getMRUOrder()[0]).toEqual(tab3.id) + + service.setActiveTab("test") + expect(service.getMRUOrder()[0]).toEqual("test") + expect(service.getMRUOrder()[1]).toEqual(tab3.id) + expect(service.getMRUOrder()[2]).toEqual(tab2.id) + + service.setActiveTab(tab2.id) + expect(service.getMRUOrder()[0]).toEqual(tab2.id) + expect(service.getMRUOrder()[1]).toEqual("test") + expect(service.getMRUOrder()[2]).toEqual(tab3.id) + }) + + it("should navigate forward through MRU list with goToMRUTab", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const tab2 = service.createNewTab({ request: "second request" }) + const tab3 = service.createNewTab({ request: "third request" }) + + service.setActiveTab("test") + service.setActiveTab(tab2.id) + service.setActiveTab(tab3.id) + + expect(service.getActiveTab()?.id).toEqual(tab3.id) + + service.goToMRUTab() + expect(service.getActiveTab()?.id).toEqual(tab2.id) + + service.goToMRUTab() + expect(service.getActiveTab()?.id).toEqual("test") + + service.goToMRUTab() + expect(service.getActiveTab()?.id).toEqual(tab3.id) + }) + + it("should navigate backward through MRU list with goToPreviousMRUTab", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const tab2 = service.createNewTab({ request: "second request" }) + const tab3 = service.createNewTab({ request: "third request" }) + + service.setActiveTab("test") + service.setActiveTab(tab2.id) + service.setActiveTab(tab3.id) + + expect(service.getActiveTab()?.id).toEqual(tab3.id) + + service.goToPreviousMRUTab() + expect(service.getActiveTab()?.id).toEqual("test") + + service.goToPreviousMRUTab() + expect(service.getActiveTab()?.id).toEqual(tab2.id) + + service.goToPreviousMRUTab() + expect(service.getActiveTab()?.id).toEqual(tab3.id) + }) + + it("should allow bidirectional MRU navigation", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const tab2 = service.createNewTab({ request: "second request" }) + const tab3 = service.createNewTab({ request: "third request" }) + + service.setActiveTab("test") + service.setActiveTab(tab2.id) + service.setActiveTab(tab3.id) + + service.goToMRUTab() // -> tab2 + service.goToMRUTab() // -> test + expect(service.getActiveTab()?.id).toEqual("test") + + service.goToPreviousMRUTab() // -> tab2 + expect(service.getActiveTab()?.id).toEqual(tab2.id) + + service.goToMRUTab() // -> test + expect(service.getActiveTab()?.id).toEqual("test") + }) + + it("should handle MRU navigation with single tab", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const originalActiveTab = service.getActiveTab() + + service.goToMRUTab() + expect(service.getActiveTab()?.id).toEqual(originalActiveTab?.id) + + service.goToPreviousMRUTab() + expect(service.getActiveTab()?.id).toEqual(originalActiveTab?.id) + }) + + it("should remove closed tabs from MRU order", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const tab2 = service.createNewTab({ request: "second request" }) + const tab3 = service.createNewTab({ request: "third request" }) + + service.setActiveTab("test") + service.setActiveTab(tab2.id) + service.setActiveTab(tab3.id) + + expect(service.getMRUOrder()).toContain(tab2.id) + + service.closeTab(tab2.id) + + expect(service.getMRUOrder()).not.toContain(tab2.id) + expect(service.getMRUOrder().length).toEqual(2) + }) + + it("should reset MRU navigation index when tab is explicitly activated", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const tab2 = service.createNewTab({ request: "second request" }) + const tab3 = service.createNewTab({ request: "third request" }) + + service.setActiveTab("test") + service.setActiveTab(tab2.id) + service.setActiveTab(tab3.id) + + service.goToMRUTab() + expect(service.getMRUNavigationIndex()).toEqual(1) + + service.setActiveTab("test") + + expect(service.getMRUNavigationIndex()).toEqual(-1) + }) + + it("should commit MRU navigation and update order", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const tab2 = service.createNewTab({ request: "second request" }) + const tab3 = service.createNewTab({ request: "third request" }) + + service.setActiveTab("test") + service.setActiveTab(tab2.id) + service.setActiveTab(tab3.id) + + service.goToMRUTab() + expect(service.getActiveTab()?.id).toEqual(tab2.id) + expect(service.getMRUNavigationIndex()).toEqual(1) + + service.commitMRUNavigation() + + expect(service.getMRUOrder()[0]).toEqual(tab2.id) + expect(service.getMRUNavigationIndex()).toEqual(-1) + }) + + it("should reset MRU navigation without committing", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + service.createNewTab({ request: "second request" }) + service.createNewTab({ request: "third request" }) + + service.goToMRUTab() + expect(service.getMRUNavigationIndex()).toBeGreaterThan(-1) + + service.resetMRUNavigation() + + expect(service.getMRUNavigationIndex()).toEqual(-1) + }) + + it("should not add background tabs to MRU until activated", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const backgroundTab = service.createNewTab( + { request: "background request" }, + false + ) + + // (it will be added when first activated) + const mruBeforeActivation = service.getMRUOrder() + + expect(mruBeforeActivation).not.toContain(backgroundTab.id) + + expect(mruBeforeActivation[0]).toEqual("test") + + expect(mruBeforeActivation.length).toEqual(1) + + service.setActiveTab(backgroundTab.id) + + expect(service.getMRUOrder()[0]).toEqual(backgroundTab.id) + + expect(service.getMRUOrder().length).toEqual(2) + }) + + it("should initialize MRU order from persisted state", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const persistedState = { + lastActiveTabID: "persisted-tab-2", + orderedDocs: [ + { tabID: "persisted-tab-1", doc: { request: "request 1" } }, + { tabID: "persisted-tab-2", doc: { request: "request 2" } }, + { tabID: "persisted-tab-3", doc: { request: "request 3" } }, + ], + } + + service.loadTabsFromPersistedState(persistedState) + + expect(service.getMRUOrder().length).toEqual(3) + + expect(service.getMRUOrder()[0]).toEqual("persisted-tab-2") + }) + + it("should handle closeOtherTabs correctly with MRU", () => { + const container = new TestContainer() + const service = container.bind(MockTabService) + + const tab2 = service.createNewTab({ request: "second request" }) + service.createNewTab({ request: "third request" }) + + service.closeOtherTabs(tab2.id) + + expect(service.getMRUOrder()).toEqual([tab2.id]) + expect(service.getMRUNavigationIndex()).toEqual(-1) + }) + }) }) diff --git a/packages/hoppscotch-common/src/services/tab/index.ts b/packages/hoppscotch-common/src/services/tab/index.ts index 71679765..0891827a 100644 --- a/packages/hoppscotch-common/src/services/tab/index.ts +++ b/packages/hoppscotch-common/src/services/tab/index.ts @@ -132,6 +132,29 @@ export interface TabService { */ reopenClosedTab(): boolean + /** + * Navigates forward through the MRU list (to older tabs). + * Each call moves one step forward in the MRU history. + */ + goToMRUTab(): void + + /** + * Navigates backward through the MRU list (to more recent tabs). + * Each call moves one step backward in the MRU history. + */ + goToPreviousMRUTab(): void + + /** + * Commits the current MRU navigation selection. + * Should be called when the modifier key is released to finalize the tab switch. + */ + commitMRUNavigation(): void + + /** + * Resets MRU navigation state without committing. + */ + resetMRUNavigation(): void + /** * Gets a computed reference to a persistable tab state. * @returns A computed reference to a persistable tab state object. diff --git a/packages/hoppscotch-common/src/services/tab/tab.ts b/packages/hoppscotch-common/src/services/tab/tab.ts index 12ac0b07..f01eeb6c 100644 --- a/packages/hoppscotch-common/src/services/tab/tab.ts +++ b/packages/hoppscotch-common/src/services/tab/tab.ts @@ -28,6 +28,13 @@ export abstract class TabService protected recentlyClosedTabs: Array<{ tab: HoppTab; index: number }> = [] protected readonly MAX_CLOSED_TABS_HISTORY = 10 + // MRU (Most Recently Used) tracking + // mruOrder[0] is the most recently used tab, mruOrder[n-1] is the least recently used + protected mruOrder: string[] = ["test"] + // Navigation index for cycling through MRU list while modifier key is held + // -1 means not currently navigating, 0+ means current position in mruOrder + protected mruNavigationIndex: number = -1 + public currentTabID = refWithControl("test", { onBeforeChange: (newTabID) => { if (!newTabID || !this.tabMap.has(newTabID)) { @@ -80,6 +87,8 @@ export abstract class TabService if (switchToIt) { this.setActiveTab(id) } + // Note: We don't add to mruOrder here if switchToIt is false + // The tab will be added to mruOrder when it's first activated via setActiveTab return tab } @@ -94,12 +103,27 @@ export abstract class TabService public setActiveTab(tabID: string): void { this.currentTabID.value = tabID + this.updateMRUOrder(tabID) + } + + private updateMRUOrder(tabID: string): void { + const index = this.mruOrder.indexOf(tabID) + if (index > -1) { + this.mruOrder.splice(index, 1) + } + this.mruOrder.unshift(tabID) + + // Reset navigation index when a tab is explicitly activated + // This ensures the next MRU navigation starts fresh + this.mruNavigationIndex = -1 } public loadTabsFromPersistedState(data: PersistableTabState): void { if (data) { this.tabMap.clear() this.tabOrdering.value = [] + this.mruOrder = [] + this.mruNavigationIndex = -1 for (const doc of data.orderedDocs) { this.tabMap.set(doc.tabID, { @@ -108,6 +132,7 @@ export abstract class TabService }) this.tabOrdering.value.push(doc.tabID) + this.mruOrder.push(doc.tabID) } this.setActiveTab(data.lastActiveTabID) @@ -175,6 +200,14 @@ export abstract class TabService this.addToRecentlyClosedTabs(tab, tabIndex) + const mruIndex = this.mruOrder.indexOf(tabID) + if (mruIndex > -1) { + this.mruOrder.splice(mruIndex, 1) + } + + // Reset navigation index when tabs change + this.mruNavigationIndex = -1 + this.tabOrdering.value.splice(tabIndex, 1) nextTick(() => { @@ -191,6 +224,8 @@ export abstract class TabService } this.tabOrdering.value = [tabID] + this.mruOrder = [tabID] + this.mruNavigationIndex = -1 this.tabMap.forEach((_, id) => { if (id !== tabID) this.tabMap.delete(id) @@ -249,6 +284,94 @@ export abstract class TabService return true } + /** + * Navigates forward through the MRU list (to older tabs). + * Each call moves one step forward in the MRU history. + * + * Behavior: + * - First call: moves from current tab (index 0) to second most recent (index 1) + * - Subsequent calls: continue moving forward through history + * - Wraps around to the beginning when reaching the end + */ + public goToMRUTab(): void { + // Clean up any stale entries + this.mruOrder = this.mruOrder.filter((tabID) => this.tabMap.has(tabID)) + + if (this.mruOrder.length <= 1) return + + // If not currently navigating, start from position 0 (current tab) + if (this.mruNavigationIndex === -1) { + this.mruNavigationIndex = 0 + } + + // Move forward in MRU list (toward older tabs) + this.mruNavigationIndex = + (this.mruNavigationIndex + 1) % this.mruOrder.length + + const targetTabID = this.mruOrder[this.mruNavigationIndex] + if (targetTabID) { + // Use currentTabID directly to avoid resetting navigation state + this.currentTabID.value = targetTabID + } + } + + /** + * Navigates backward through the MRU list (to more recent tabs). + * Each call moves one step backward in the MRU history. + * + * Behavior: + * - Moves backward from current position in MRU navigation + * - Wraps around to the end when reaching the beginning + */ + public goToPreviousMRUTab(): void { + // Clean up any stale entries + this.mruOrder = this.mruOrder.filter((tabID) => this.tabMap.has(tabID)) + + if (this.mruOrder.length <= 1) return + + // If not currently navigating, start from position 0 (current tab) + if (this.mruNavigationIndex === -1) { + this.mruNavigationIndex = 0 + } + + // Move backward in MRU list (toward more recent tabs) + this.mruNavigationIndex = + this.mruNavigationIndex === 0 + ? this.mruOrder.length - 1 + : this.mruNavigationIndex - 1 + + const targetTabID = this.mruOrder[this.mruNavigationIndex] + if (targetTabID) { + // Use currentTabID directly to avoid resetting navigation state + this.currentTabID.value = targetTabID + } + } + + /** + * Commits the current MRU navigation selection. + * Should be called when the modifier key is released to finalize the tab switch. + * This moves the selected tab to the front of the MRU order. + */ + public commitMRUNavigation(): void { + if (this.mruNavigationIndex > 0) { + const selectedTabID = this.mruOrder[this.mruNavigationIndex] + if (selectedTabID) { + // Move the selected tab to the front of MRU order + this.mruOrder.splice(this.mruNavigationIndex, 1) + this.mruOrder.unshift(selectedTabID) + } + } + this.mruNavigationIndex = -1 + } + + /** + * Resets MRU navigation state without committing. + * Useful if navigation is cancelled. + */ + public resetMRUNavigation(): void { + this.mruNavigationIndex = -1 + } + private addToRecentlyClosedTabs(tab: HoppTab, index: number): void { this.recentlyClosedTabs.push({ tab, index }) diff --git a/packages/hoppscotch-desktop/.gitignore b/packages/hoppscotch-desktop/.gitignore index 99a74d3f..f5e62d36 100644 --- a/packages/hoppscotch-desktop/.gitignore +++ b/packages/hoppscotch-desktop/.gitignore @@ -18,6 +18,7 @@ manifest.json src-tauri/hopp_bundle.zip src-tauri/hopp_manifest.json src-tauri/hoppscotch-desktop-data +target # Editor directories and files .vscode/* diff --git a/packages/hoppscotch-selfhost-web/src/main.ts b/packages/hoppscotch-selfhost-web/src/main.ts index 23d758d3..d78a7f52 100644 --- a/packages/hoppscotch-selfhost-web/src/main.ts +++ b/packages/hoppscotch-selfhost-web/src/main.ts @@ -368,14 +368,36 @@ async function initApp() { } else if ( isCtrlOrCmd && !e.shiftKey && - !e.altKey && - e.key.toLowerCase() === "l" + e.altKey && + e.code === "KeyU" ) { - // Ctrl/Cmd + L - Focus Address Bar + // Ctrl/Cmd + Alt + U - Focus URL Bar e.preventDefault() e.stopPropagation() e.stopImmediatePropagation() - shortcutEvent = "focus-url" + shortcutEvent = "ctrl-alt-u" + } else if ( + isCtrlOrCmd && + !e.shiftKey && + e.altKey && + e.code === "BracketRight" + ) { + // Ctrl/Cmd + Alt + ] - MRU Tab Switch + e.preventDefault() + e.stopPropagation() + e.stopImmediatePropagation() + shortcutEvent = "ctrl-alt-]" + } else if ( + isCtrlOrCmd && + !e.shiftKey && + e.altKey && + e.code === "BracketLeft" + ) { + // Ctrl/Cmd + Alt + [ - MRU Tab Switch (Reverse) + e.preventDefault() + e.stopPropagation() + e.stopImmediatePropagation() + shortcutEvent = "ctrl-alt-[" } if (shortcutEvent) {