mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
Restore modifier-click behavior for terminal URLs (#159)
* Restore modifier-click behavior for terminal URLs * Fix terminal URL hover preview * Add shortcut hint to terminal URL preview * Tweak terminal URL shortcut hint styling * Simplify terminal URL hover copy * Fix terminal link handler test environment
This commit is contained in:
parent
33ff96a54d
commit
7cf92c6e49
5 changed files with 110 additions and 14 deletions
|
|
@ -0,0 +1,65 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { handleOscLink, isTerminalLinkActivation } from './terminal-link-handlers'
|
||||
|
||||
const openUrlMock = vi.fn()
|
||||
const openFileUriMock = vi.fn()
|
||||
|
||||
function setPlatform(userAgent: string): void {
|
||||
vi.stubGlobal('navigator', { userAgent })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
shell: {
|
||||
openUrl: openUrlMock,
|
||||
openFileUri: openFileUriMock
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('isTerminalLinkActivation', () => {
|
||||
it('requires cmd on macOS', () => {
|
||||
setPlatform('Macintosh')
|
||||
|
||||
expect(isTerminalLinkActivation({ metaKey: true, ctrlKey: false })).toBe(true)
|
||||
expect(isTerminalLinkActivation({ metaKey: false, ctrlKey: true })).toBe(false)
|
||||
expect(isTerminalLinkActivation(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('requires ctrl on non-macOS platforms', () => {
|
||||
setPlatform('Windows')
|
||||
|
||||
expect(isTerminalLinkActivation({ metaKey: false, ctrlKey: true })).toBe(true)
|
||||
expect(isTerminalLinkActivation({ metaKey: true, ctrlKey: false })).toBe(false)
|
||||
expect(isTerminalLinkActivation(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleOscLink', () => {
|
||||
it('opens http links only when the platform modifier is pressed', () => {
|
||||
setPlatform('Macintosh')
|
||||
|
||||
handleOscLink('https://example.com', { metaKey: false, ctrlKey: false })
|
||||
expect(openUrlMock).not.toHaveBeenCalled()
|
||||
|
||||
handleOscLink('https://example.com', { metaKey: true, ctrlKey: false })
|
||||
expect(openUrlMock).toHaveBeenCalledWith('https://example.com/')
|
||||
})
|
||||
|
||||
it('opens file links only when the platform modifier is pressed', () => {
|
||||
setPlatform('Windows')
|
||||
|
||||
handleOscLink('file:///tmp/test.txt', { metaKey: false, ctrlKey: false })
|
||||
expect(openFileUriMock).not.toHaveBeenCalled()
|
||||
|
||||
handleOscLink('file:///tmp/test.txt', { metaKey: false, ctrlKey: true })
|
||||
expect(openFileUriMock).toHaveBeenCalledWith('file:///tmp/test.txt')
|
||||
})
|
||||
})
|
||||
|
|
@ -125,7 +125,19 @@ export function createFilePathLinkProvider(paneId: number, deps: LinkHandlerDeps
|
|||
}
|
||||
}
|
||||
|
||||
export function handleOscLink(rawText: string): void {
|
||||
export function isTerminalLinkActivation(event: Pick<MouseEvent, 'metaKey' | 'ctrlKey'> | undefined): boolean {
|
||||
const isMac = navigator.userAgent.includes('Mac')
|
||||
return isMac ? Boolean(event?.metaKey) : Boolean(event?.ctrlKey)
|
||||
}
|
||||
|
||||
export function handleOscLink(
|
||||
rawText: string,
|
||||
event: Pick<MouseEvent, 'metaKey' | 'ctrlKey'> | undefined
|
||||
): void {
|
||||
if (!isTerminalLinkActivation(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
let parsed: URL
|
||||
try {
|
||||
parsed = new URL(rawText)
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export function useTerminalPaneLifecycle({
|
|||
linkProviderDisposablesRef.current.set(pane.id, linkProviderDisposable)
|
||||
pane.terminal.options.linkHandler = {
|
||||
allowNonHttpProtocols: true,
|
||||
activate: (_event, text) => handleOscLink(text)
|
||||
activate: (event, text) => handleOscLink(text, event as MouseEvent | undefined)
|
||||
}
|
||||
applyAppearance(manager)
|
||||
connectPanePty(pane, manager, ptyDeps)
|
||||
|
|
@ -211,8 +211,11 @@ export function useTerminalPaneLifecycle({
|
|||
cursorBlink: currentSettings?.terminalCursorBlink ?? true
|
||||
}
|
||||
},
|
||||
onLinkClick: (url) => {
|
||||
void window.api.shell.openUrl(url)
|
||||
onLinkClick: (event, url) => {
|
||||
if (!event) {
|
||||
return
|
||||
}
|
||||
void handleOscLink(url, event)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export function createPaneDOM(
|
|||
xtermContainer.style.height = `calc(100% - ${TERMINAL_PADDING}px)`
|
||||
xtermContainer.style.marginTop = `${TERMINAL_PADDING}px`
|
||||
xtermContainer.style.marginLeft = `${TERMINAL_PADDING}px`
|
||||
xtermContainer.style.position = 'relative'
|
||||
container.appendChild(xtermContainer)
|
||||
|
||||
// Build terminal options
|
||||
|
|
@ -61,16 +62,18 @@ 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'
|
||||
|
||||
// URL tooltip element — Ghostty-style bottom-left hint on hover
|
||||
const linkTooltip = document.createElement('div')
|
||||
linkTooltip.className = 'pane-link-tooltip'
|
||||
linkTooltip.classList.add('xterm-hover')
|
||||
linkTooltip.style.cssText =
|
||||
'display:none;position:absolute;bottom:4px;left:8px;z-index:40;' +
|
||||
'padding:2px 8px;border-radius:4px;font-size:11px;font-family:inherit;' +
|
||||
'padding:5px 8px;border-radius:4px;font-size:11px;font-family:inherit;' +
|
||||
'color:#a1a1aa;background:rgba(24,24,27,0.85);border:1px solid rgba(63,63,70,0.6);' +
|
||||
'pointer-events:none;max-width:80%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'
|
||||
container.appendChild(linkTooltip)
|
||||
|
||||
// Ghostty-style drag handle — appears at top of pane on hover when 2+ panes
|
||||
const dragHandle = document.createElement('div')
|
||||
|
|
@ -79,15 +82,16 @@ export function createPaneDOM(
|
|||
attachPaneDrag(dragHandle, id, dragState, dragCallbacks)
|
||||
|
||||
const webLinksAddon = new WebLinksAddon(
|
||||
options.onLinkClick ? (_event, uri) => options.onLinkClick!(uri) : undefined,
|
||||
options.onLinkClick ? (event, uri) => options.onLinkClick!(event, uri) : undefined,
|
||||
{
|
||||
hover: (event, uri) => {
|
||||
if (event.type === 'mouseover' && uri) {
|
||||
linkTooltip.textContent = uri
|
||||
hover: (_event, uri) => {
|
||||
if (uri) {
|
||||
linkTooltip.textContent = `${uri} (${openLinkHint})`
|
||||
linkTooltip.style.display = ''
|
||||
} else {
|
||||
linkTooltip.style.display = 'none'
|
||||
}
|
||||
},
|
||||
leave: () => {
|
||||
linkTooltip.style.display = 'none'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -97,6 +101,7 @@ export function createPaneDOM(
|
|||
terminal,
|
||||
container,
|
||||
xtermContainer,
|
||||
linkTooltip,
|
||||
fitAddon,
|
||||
searchAddon,
|
||||
unicode11Addon,
|
||||
|
|
@ -117,10 +122,20 @@ export function createPaneDOM(
|
|||
|
||||
/** Open terminal into its container and load addons. Must be called after the container is in the DOM. */
|
||||
export function openTerminal(pane: ManagedPaneInternal): void {
|
||||
const { terminal, xtermContainer, fitAddon, searchAddon, unicode11Addon, webLinksAddon } = pane
|
||||
const {
|
||||
terminal,
|
||||
xtermContainer,
|
||||
linkTooltip,
|
||||
fitAddon,
|
||||
searchAddon,
|
||||
unicode11Addon,
|
||||
webLinksAddon
|
||||
} = pane
|
||||
|
||||
// Open terminal into DOM
|
||||
terminal.open(xtermContainer)
|
||||
const linkTooltipContainer = terminal.element ?? xtermContainer
|
||||
linkTooltipContainer.appendChild(linkTooltip)
|
||||
|
||||
// Load addons (order matters: WebGL must be after open())
|
||||
terminal.loadAddon(fitAddon)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type PaneManagerOptions = {
|
|||
onActivePaneChange?: (pane: ManagedPane) => void
|
||||
onLayoutChanged?: () => void
|
||||
terminalOptions?: (paneId: number) => Partial<ITerminalOptions>
|
||||
onLinkClick?: (url: string) => void
|
||||
onLinkClick?: (event: MouseEvent | undefined, url: string) => void
|
||||
}
|
||||
|
||||
export type PaneStyleOptions = {
|
||||
|
|
@ -42,6 +42,7 @@ export type ManagedPane = {
|
|||
|
||||
export type ManagedPaneInternal = {
|
||||
xtermContainer: HTMLElement
|
||||
linkTooltip: HTMLElement
|
||||
webglAddon: WebglAddon | null
|
||||
unicode11Addon: Unicode11Addon
|
||||
webLinksAddon: WebLinksAddon
|
||||
|
|
|
|||
Loading…
Reference in a new issue