Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/web/src/components/ThreadTerminalDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import {
} from "~/terminal/ghostty/surface";
import { type GhosttyColor, type GhosttyTheme } from "~/terminal/ghostty/core";
import { useOpenInPreferredEditor } from "../editorPreferences";
import { isTerminalLinkActivation, isTerminalUrl, resolvePathLinkTarget } from "../terminal-links";
import { isTerminalUrl, resolvePathLinkTarget } from "../terminal-links";
import {
isDiffToggleShortcut,
isTerminalClearShortcut,
Expand Down Expand Up @@ -747,7 +747,6 @@ export function TerminalViewport({
}

function handleLinkActivate(text: string, event: MouseEvent): void {
if (!isTerminalLinkActivation(event)) return;
const latestTerminal = terminalRef.current;
if (!latestTerminal) return;
if (isTerminalUrl(text)) {
Expand All @@ -768,6 +767,7 @@ export function TerminalViewport({
threadRef,
openPreview,
fallbackToBrowser,
forceBrowser: event.metaKey || event.ctrlKey,
});
return;
}
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/components/preview/openTerminalLinkInPreview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe("openTerminalLinkInPreview", () => {
threadRef,
openPreview,
fallbackToBrowser,
forceBrowser: false,
});

expect(fallbackToBrowser).toHaveBeenCalledOnce();
Expand All @@ -93,6 +94,7 @@ describe("openTerminalLinkInPreview", () => {
threadRef,
openPreview,
fallbackToBrowser,
forceBrowser: false,
});

expect(openPreview).toHaveBeenCalledOnce();
Expand All @@ -114,6 +116,7 @@ describe("openTerminalLinkInPreview", () => {
threadRef,
openPreview,
fallbackToBrowser: vi.fn(),
forceBrowser: false,
});

await vi.waitFor(() => expect(browserDefaultsMocks.resolve).toHaveBeenCalledOnce());
Expand Down Expand Up @@ -143,6 +146,7 @@ describe("openTerminalLinkInPreview", () => {
threadRef,
openPreview: async () => AsyncResult.failure(cause),
fallbackToBrowser,
forceBrowser: false,
});

expect(fallbackToBrowser).toHaveBeenCalledOnce();
Expand All @@ -167,9 +171,26 @@ describe("openTerminalLinkInPreview", () => {
threadRef,
openPreview: async () => AsyncResult.failure(Cause.interrupt()),
fallbackToBrowser,
forceBrowser: false,
});

expect(reportError).not.toHaveBeenCalled();
expect(fallbackToBrowser).not.toHaveBeenCalled();
});

it("opens in the system browser when Ctrl or Command is held", async () => {
const fallbackToBrowser = vi.fn();
const openPreview = vi.fn(async () => AsyncResult.success(snapshot));

await openTerminalLinkInPreview({
url: "https://example.com/docs",
threadRef,
openPreview,
fallbackToBrowser,
forceBrowser: true,
});

expect(fallbackToBrowser).toHaveBeenCalledOnce();
expect(openPreview).not.toHaveBeenCalled();
});
});
10 changes: 5 additions & 5 deletions apps/web/src/components/preview/openTerminalLinkInPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,19 @@ interface OpenTerminalLinkInPreviewInput<E> {
readonly threadRef: ScopedThreadRef;
readonly openPreview: OpenPreviewMutation<E>;
readonly fallbackToBrowser: () => void;
/** Cmd/Ctrl-click bypasses the preference and opens in the system browser. */
readonly forceBrowser: boolean;
}

/**
* Opens a terminal hyperlink where the "Open links in" setting says. Terminal
* links are activated with the platform modifier already held, so unlike chat
* links the modifier cannot double as the system-browser override; the setting
* alone decides, and the system browser is the fallback whenever the in-app
* one cannot take the URL.
* Opens a terminal hyperlink where the "Open links in" setting says, unless a
* Cmd/Ctrl-click explicitly requests the system browser.
*/
export async function openTerminalLinkInPreview<E>(
input: OpenTerminalLinkInPreviewInput<E>,
): Promise<void> {
const supportsPreview =
!input.forceBrowser &&
isWebUrl(input.url) &&
isPreviewSupportedInRuntime() &&
input.threadRef.threadId.length > 0 &&
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/settings/IntegrationsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ function BrowserLinkTargetSetting({ disabled }: { readonly disabled: boolean })
return (
<SettingsRow
{...searchableSetting("browser-link-target")}
description="Where links in the chat and terminal open. Hold ⌘ or Ctrl while clicking a chat link to open it in your default browser either way."
description="Where links in the chat and terminal open. Hold ⌘ or Ctrl while clicking a link to open it in your default browser either way."
resetAction={
!disabled && linkTarget !== DEFAULT_BROWSER_LINK_TARGET ? (
<SettingResetButton
Expand Down
14 changes: 0 additions & 14 deletions apps/web/src/terminal/ghostty/surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
isTerminalCompositionCommitInput,
isTerminalCompositionKey,
isTerminalCopyShortcut,
isTerminalLinkPointerGesture,
isTerminalPasteShortcut,
loadTerminalFontFamily,
primeTerminalCopyInput,
Expand Down Expand Up @@ -876,19 +875,6 @@ describe("terminalWheelArrowData", () => {
});
});

describe("isTerminalLinkPointerGesture", () => {
it("uses Command on macOS and Control elsewhere", () => {
expect(isTerminalLinkPointerGesture({ ctrlKey: false, metaKey: true }, "MacIntel")).toBe(true);
expect(isTerminalLinkPointerGesture({ ctrlKey: true, metaKey: false }, "MacIntel")).toBe(false);
expect(isTerminalLinkPointerGesture({ ctrlKey: true, metaKey: false }, "Linux x86_64")).toBe(
true,
);
expect(isTerminalLinkPointerGesture({ ctrlKey: false, metaKey: true }, "Linux x86_64")).toBe(
false,
);
});
});

describe("advanceTerminalSelectionClickSequence", () => {
it("recognizes stationary double and triple pointer presses without PointerEvent.detail", () => {
const first = advanceTerminalSelectionClickSequence(null, {
Expand Down
27 changes: 2 additions & 25 deletions apps/web/src/terminal/ghostty/surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,15 +475,6 @@ export function terminalWheelArrowData(rows: number, applicationCursorKeys: bool
return sequence.repeat(Math.abs(rows));
}

export function isTerminalLinkPointerGesture(
event: Pick<MouseEvent, "ctrlKey" | "metaKey">,
platform = navigator.platform,
): boolean {
return isMacPlatform(platform)
? event.metaKey && !event.ctrlKey
: event.ctrlKey && !event.metaKey;
}

export function ghosttyMouseButton(button: number): number | null {
switch (button) {
case 0:
Expand Down Expand Up @@ -602,7 +593,6 @@ export class GhosttyTerminalSurface {
private linkActivationPointerId: number | null = null;
private hoveredLink: TerminalLinkWithRange | null = null;
private hoverPointer: { x: number; y: number } | null = null;
private linkModifierActive = false;
private selectionClickSequence: TerminalSelectionClickSequence | null = null;
private selectionMoved = false;
private composing = false;
Expand Down Expand Up @@ -1026,7 +1016,6 @@ export class GhosttyTerminalSurface {
}

private readonly onKeyDown = (event: KeyboardEvent) => {
this.updateLinkModifier(event);
// Presses handled outside the terminal must also swallow their release:
// beforeKey runs side effects (keybindings, navigation sends), so it cannot
// be consulted again on keyup, and Kitty report-event-types sessions would
Expand Down Expand Up @@ -1129,7 +1118,6 @@ export class GhosttyTerminalSurface {
};

private readonly onKeyUp = (event: KeyboardEvent) => {
this.updateLinkModifier(event);
if (this.suppressedKeyCodes.delete(event.code)) return;
if (isTerminalCompositionKey(event, this.composing)) {
return;
Expand All @@ -1151,7 +1139,6 @@ export class GhosttyTerminalSurface {

private readonly onBlur = () => {
this.focused = false;
this.linkModifierActive = false;
this.refreshHoveredLink();
// Suppressions survive blur deliberately: a shortcut that moves focus (for
// example terminal-toggle) must still swallow its own keyup if focus comes
Expand Down Expand Up @@ -1272,7 +1259,7 @@ export class GhosttyTerminalSurface {
return;
}
if (event.button !== 0) return;
if (isTerminalLinkPointerGesture(event)) {
if (this.linkAt(event.clientX, event.clientY)) {
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
event.preventDefault();
event.stopPropagation();
this.linkActivationPointerId = event.pointerId;
Expand Down Expand Up @@ -1329,7 +1316,6 @@ export class GhosttyTerminalSurface {
) {
event.preventDefault();
this.hoverPointer = { x: event.clientX, y: event.clientY };
this.linkModifierActive = isTerminalLinkPointerGesture(event);
// A drag whose press was already sent to the terminal application cannot
// turn into link activation midway through, so link feedback would lie.
this.setHoveredLink(null);
Expand Down Expand Up @@ -1404,14 +1390,6 @@ export class GhosttyTerminalSurface {

private updateHoverCursor(event: PointerEvent): void {
this.hoverPointer = { x: event.clientX, y: event.clientY };
this.linkModifierActive = isTerminalLinkPointerGesture(event);
this.refreshHoveredLink();
}

private updateLinkModifier(event: Pick<KeyboardEvent, "ctrlKey" | "metaKey">): void {
const active = isTerminalLinkPointerGesture(event);
if (active === this.linkModifierActive) return;
this.linkModifierActive = active;
this.refreshHoveredLink();
}

Expand All @@ -1428,7 +1406,7 @@ export class GhosttyTerminalSurface {

private refreshHoveredLink(): void {
const pointer = this.hoverPointer;
const link = pointer && this.linkModifierActive ? this.linkAt(pointer.x, pointer.y) : null;
const link = pointer ? this.linkAt(pointer.x, pointer.y) : null;
this.setHoveredLink(link);
}

Expand Down Expand Up @@ -1475,7 +1453,6 @@ export class GhosttyTerminalSurface {
this.clearHoveredLink();
} else {
this.hoverPointer = { x: event.clientX, y: event.clientY };
this.linkModifierActive = isTerminalLinkPointerGesture(event);
this.refreshHoveredLink();
}
return;
Expand Down
Loading