Support shift+cmd/ctrl-click for external terminal links (#495)
This commit is contained in:
parent
49c7b788c3
commit
aeb8002df7
|
|
@ -6,15 +6,20 @@ const openFileUriMock = vi.fn()
|
|||
const openFileMock = vi.fn()
|
||||
const authorizeExternalPathMock = vi.fn()
|
||||
const statMock = vi.fn().mockResolvedValue({ isDirectory: false })
|
||||
const setActiveWorktreeMock = vi.fn()
|
||||
const createBrowserTabMock = vi.fn()
|
||||
|
||||
const deps = { worktreeId: 'wt-1', worktreePath: '/tmp' }
|
||||
const storeState = {
|
||||
settings: undefined as { openLinksInApp?: boolean } | undefined,
|
||||
setActiveWorktree: setActiveWorktreeMock,
|
||||
createBrowserTab: createBrowserTabMock,
|
||||
openFile: openFileMock
|
||||
}
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: {
|
||||
getState: () => ({
|
||||
setActiveWorktree: vi.fn(),
|
||||
openFile: openFileMock
|
||||
})
|
||||
getState: () => storeState
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
@ -28,6 +33,7 @@ function setPlatform(userAgent: string): void {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
storeState.settings = undefined
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
shell: {
|
||||
|
|
@ -75,6 +81,27 @@ describe('handleOscLink', () => {
|
|||
expect(openUrlMock).toHaveBeenCalledWith('https://example.com/')
|
||||
})
|
||||
|
||||
it('keeps cmd/ctrl+click in Orca when the in-app browser setting is enabled', () => {
|
||||
setPlatform('Macintosh')
|
||||
storeState.settings = { openLinksInApp: true }
|
||||
|
||||
handleOscLink('https://example.com', { metaKey: true, ctrlKey: false, shiftKey: false }, deps)
|
||||
|
||||
expect(createBrowserTabMock).toHaveBeenCalledWith('wt-1', 'https://example.com/')
|
||||
expect(setActiveWorktreeMock).toHaveBeenCalledWith('wt-1')
|
||||
expect(openUrlMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses the system browser for shift+cmd/ctrl+click even when Orca browser tabs are enabled', () => {
|
||||
setPlatform('Windows')
|
||||
storeState.settings = { openLinksInApp: true }
|
||||
|
||||
handleOscLink('https://example.com', { metaKey: false, ctrlKey: true, shiftKey: true }, deps)
|
||||
|
||||
expect(openUrlMock).toHaveBeenCalledWith('https://example.com/')
|
||||
expect(createBrowserTabMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens file links in Orca instead of via shell when the platform modifier is pressed', async () => {
|
||||
setPlatform('Windows')
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,23 @@ export type LinkHandlerDeps = {
|
|||
pathExistsCache: Map<string, boolean>
|
||||
}
|
||||
|
||||
type TerminalLinkEvent = Pick<MouseEvent, 'metaKey' | 'ctrlKey'> &
|
||||
Partial<Pick<MouseEvent, 'shiftKey'>>
|
||||
|
||||
function isMacPlatform(): boolean {
|
||||
return navigator.userAgent.includes('Mac')
|
||||
}
|
||||
|
||||
export function getTerminalFileOpenHint(): string {
|
||||
return isMacPlatform() ? '⌘+click to open' : 'Ctrl+click to open'
|
||||
}
|
||||
|
||||
export function getTerminalUrlOpenHint(): string {
|
||||
return isMacPlatform()
|
||||
? '⌘+click to open or ⇧⌘+click for system browser'
|
||||
: 'Ctrl+click to open or Shift+Ctrl+click for system browser'
|
||||
}
|
||||
|
||||
export function openDetectedFilePath(
|
||||
filePath: string,
|
||||
line: number | null,
|
||||
|
|
@ -152,13 +169,13 @@ export function createFilePathLinkProvider(
|
|||
export function isTerminalLinkActivation(
|
||||
event: Pick<MouseEvent, 'metaKey' | 'ctrlKey'> | undefined
|
||||
): boolean {
|
||||
const isMac = navigator.userAgent.includes('Mac')
|
||||
const isMac = isMacPlatform()
|
||||
return isMac ? Boolean(event?.metaKey) : Boolean(event?.ctrlKey)
|
||||
}
|
||||
|
||||
export function handleOscLink(
|
||||
rawText: string,
|
||||
event: Pick<MouseEvent, 'metaKey' | 'ctrlKey'> | undefined,
|
||||
event: TerminalLinkEvent | undefined,
|
||||
deps: Pick<LinkHandlerDeps, 'worktreeId' | 'worktreePath'>
|
||||
): void {
|
||||
if (!isTerminalLinkActivation(event)) {
|
||||
|
|
@ -177,7 +194,9 @@ export function handleOscLink(
|
|||
// Why: when the user opts into Orca's browser tabs, terminal links should
|
||||
// stay worktree-scoped instead of escaping to the system browser. We still
|
||||
// fall back externally when the setting is off or no worktree owns the pane.
|
||||
if (store.settings?.openLinksInApp && deps.worktreeId) {
|
||||
// Shift is the explicit escape hatch for "open this one in my system browser"
|
||||
// without forcing the user to toggle the global in-app browser preference.
|
||||
if (store.settings?.openLinksInApp && deps.worktreeId && !event?.shiftKey) {
|
||||
store.setActiveWorktree(deps.worktreeId)
|
||||
store.createBrowserTab(deps.worktreeId, parsed.toString())
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@ import { useEffect, useRef } from 'react'
|
|||
import type { IDisposable } from '@xterm/xterm'
|
||||
import { PaneManager } from '@/lib/pane-manager/pane-manager'
|
||||
import { useAppStore } from '@/store'
|
||||
import { createFilePathLinkProvider, handleOscLink } from './terminal-link-handlers'
|
||||
import {
|
||||
createFilePathLinkProvider,
|
||||
getTerminalFileOpenHint,
|
||||
getTerminalUrlOpenHint,
|
||||
handleOscLink
|
||||
} from './terminal-link-handlers'
|
||||
import type { LinkHandlerDeps } from './terminal-link-handlers'
|
||||
import type { GlobalSettings, TerminalLayoutSnapshot } from '../../../../shared/types'
|
||||
import { resolveTerminalFontWeights } from '../../../../shared/terminal-fonts'
|
||||
|
|
@ -209,13 +214,13 @@ export function useTerminalPaneLifecycle({
|
|||
getPtyIdForPane: (paneId) => paneTransportsRef.current.get(paneId)?.getPtyId() ?? null
|
||||
})
|
||||
|
||||
const isMac = navigator.userAgent.includes('Mac')
|
||||
const openLinkHint = isMac ? '⌘+click to open' : 'Ctrl+click to open'
|
||||
const fileOpenLinkHint = getTerminalFileOpenHint()
|
||||
const urlOpenLinkHint = getTerminalUrlOpenHint()
|
||||
|
||||
const manager = new PaneManager(container, {
|
||||
onPaneCreated: (pane) => {
|
||||
const linkProviderDisposable = pane.terminal.registerLinkProvider(
|
||||
createFilePathLinkProvider(pane.id, linkDeps, pane.linkTooltip, openLinkHint)
|
||||
createFilePathLinkProvider(pane.id, linkDeps, pane.linkTooltip, fileOpenLinkHint)
|
||||
)
|
||||
linkProviderDisposablesRef.current.set(pane.id, linkProviderDisposable)
|
||||
pane.terminal.options.linkHandler = {
|
||||
|
|
@ -225,7 +230,7 @@ export function useTerminalPaneLifecycle({
|
|||
// GitHub owner/repo#issue references emitted by CLI tools) — same
|
||||
// behaviour as the WebLinksAddon provides for plain-text URLs.
|
||||
hover: (_event, text) => {
|
||||
pane.linkTooltip.textContent = `${text} (${openLinkHint})`
|
||||
pane.linkTooltip.textContent = `${text} (${urlOpenLinkHint})`
|
||||
pane.linkTooltip.style.display = ''
|
||||
},
|
||||
leave: () => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ import { safeFit } from './pane-tree-ops'
|
|||
|
||||
const ENABLE_WEBGL_RENDERER = true
|
||||
|
||||
function getTerminalUrlOpenHint(): string {
|
||||
return navigator.userAgent.includes('Mac')
|
||||
? '⌘+click to open or ⇧⌘+click for system browser'
|
||||
: 'Ctrl+click to open or Shift+Ctrl+click for system browser'
|
||||
}
|
||||
|
||||
export function createPaneDOM(
|
||||
id: number,
|
||||
options: PaneManagerOptions,
|
||||
|
|
@ -65,8 +71,7 @@ export function createPaneDOM(
|
|||
const fitAddon = new FitAddon()
|
||||
const searchAddon = new SearchAddon()
|
||||
const unicode11Addon = new Unicode11Addon()
|
||||
const isMac = navigator.userAgent.includes('Mac')
|
||||
const openLinkHint = isMac ? '⌘+click to open' : 'Ctrl+click to open'
|
||||
const openLinkHint = getTerminalUrlOpenHint()
|
||||
|
||||
// URL tooltip element — Ghostty-style bottom-left hint on hover
|
||||
const linkTooltip = document.createElement('div')
|
||||
|
|
|
|||
Loading…
Reference in New Issue