Skip to content

Commit 107b294

Browse files
Copilotsawka
andcommitted
feat: add term:osc52 focus/always config override
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
1 parent f85ec45 commit 107b294

10 files changed

Lines changed: 98 additions & 2 deletions

File tree

docs/docs/config.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ wsh editconfig
7373
| term:cursorblink <VersionBadge version="v0.14" /> | bool | when enabled, terminal cursor blinks (default false) |
7474
| term:bellsound <VersionBadge version="v0.14" /> | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
7575
| term:bellindicator <VersionBadge version="v0.14" /> | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default false) |
76+
| term:osc52 <VersionBadge version="v0.14" /> | string | controls OSC 52 clipboard behavior: `focus` (default, requires focused block) or `always` (allows OSC 52 even when block is not focused) |
7677
| term:durable <VersionBadge version="v0.14" /> | bool | makes remote terminal sessions durable across network disconnects (defaults to false) |
7778
| editor:minimapenabled | bool | set to false to disable editor minimap |
7879
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
@@ -147,6 +148,7 @@ For reference, this is the current default configuration (v0.14.0):
147148
"telemetry:enabled": true,
148149
"term:bellsound": false,
149150
"term:bellindicator": false,
151+
"term:osc52": "focus",
150152
"term:cursor": "block",
151153
"term:cursorblink": false,
152154
"term:copyonselect": true,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const { mockWriteText, mockGet } = vi.hoisted(() => ({
4+
mockWriteText: vi.fn(),
5+
mockGet: vi.fn(),
6+
}));
7+
8+
vi.mock("@/app/store/wshclientapi", () => ({ RpcApi: {} }));
9+
vi.mock("@/app/store/wshrpcutil", () => ({ TabRpcClient: {} }));
10+
vi.mock("@/store/services", () => ({}));
11+
vi.mock("@/store/global", () => ({
12+
getApi: vi.fn(),
13+
getBlockMetaKeyAtom: vi.fn(),
14+
getBlockTermDurableAtom: vi.fn(),
15+
getOverrideConfigAtom: vi.fn((_blockId: string, key: string) => ({ key })),
16+
globalStore: { get: mockGet },
17+
recordTEvent: vi.fn(),
18+
WOS: {},
19+
}));
20+
vi.mock("@/util/util", () => ({
21+
base64ToString: (data: string) => Buffer.from(data, "base64").toString("utf8"),
22+
fireAndForget: (fn: () => Promise<void>) => {
23+
void fn();
24+
},
25+
isSshConnName: vi.fn(),
26+
isWslConnName: vi.fn(),
27+
}));
28+
29+
import { handleOsc52Command } from "./osc-handlers";
30+
31+
describe("handleOsc52Command", () => {
32+
beforeEach(() => {
33+
vi.clearAllMocks();
34+
mockWriteText.mockResolvedValue(undefined);
35+
Object.defineProperty(globalThis, "navigator", {
36+
configurable: true,
37+
value: { clipboard: { writeText: mockWriteText } },
38+
});
39+
Object.defineProperty(globalThis, "document", {
40+
configurable: true,
41+
value: { hasFocus: () => true },
42+
});
43+
});
44+
45+
it("rejects unfocused block when term:osc52 is focus", () => {
46+
mockGet.mockImplementation((atom: { key?: string } | undefined) => {
47+
if (atom?.key === "term:osc52") {
48+
return "focus";
49+
}
50+
return false;
51+
});
52+
53+
handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
54+
55+
expect(mockWriteText).not.toHaveBeenCalled();
56+
});
57+
58+
it("allows unfocused block when term:osc52 is always", async () => {
59+
mockGet.mockImplementation((atom: { key?: string } | undefined) => {
60+
if (atom?.key === "term:osc52") {
61+
return "always";
62+
}
63+
return false;
64+
});
65+
66+
handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
67+
await Promise.resolve();
68+
69+
expect(mockWriteText).toHaveBeenCalledWith("Hello");
70+
});
71+
});

frontend/app/view/term/osc-handlers.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33

44
import { RpcApi } from "@/app/store/wshclientapi";
55
import { TabRpcClient } from "@/app/store/wshrpcutil";
6-
import { getApi, getBlockMetaKeyAtom, getBlockTermDurableAtom, globalStore, recordTEvent, WOS } from "@/store/global";
6+
import {
7+
getApi,
8+
getBlockMetaKeyAtom,
9+
getBlockTermDurableAtom,
10+
getOverrideConfigAtom,
11+
globalStore,
12+
recordTEvent,
13+
WOS,
14+
} from "@/store/global";
715
import * as services from "@/store/services";
816
import { base64ToString, fireAndForget, isSshConnName, isWslConnName } from "@/util/util";
917
import debug from "debug";
@@ -114,8 +122,9 @@ export function handleOsc52Command(data: string, blockId: string, loaded: boolea
114122
if (!loaded) {
115123
return true;
116124
}
125+
const osc52Mode = globalStore.get(getOverrideConfigAtom(blockId, "term:osc52")) ?? "focus";
117126
const isBlockFocused = termWrap.nodeModel ? globalStore.get(termWrap.nodeModel.isFocused) : false;
118-
if (!document.hasFocus() || !isBlockFocused) {
127+
if (!document.hasFocus() || (osc52Mode !== "always" && !isBlockFocused)) {
119128
console.log("OSC 52: rejected, window or block not focused");
120129
return true;
121130
}

frontend/types/gotypes.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,7 @@ declare global {
11191119
"term:conndebug"?: string;
11201120
"term:bellsound"?: boolean;
11211121
"term:bellindicator"?: boolean;
1122+
"term:osc52"?: string;
11221123
"term:durable"?: boolean;
11231124
"web:zoom"?: number;
11241125
"web:hidenav"?: boolean;
@@ -1313,6 +1314,7 @@ declare global {
13131314
"term:cursorblink"?: boolean;
13141315
"term:bellsound"?: boolean;
13151316
"term:bellindicator"?: boolean;
1317+
"term:osc52"?: string;
13161318
"term:durable"?: boolean;
13171319
"editor:minimapenabled"?: boolean;
13181320
"editor:stickyscrollenabled"?: boolean;

pkg/waveobj/metaconsts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ const (
121121
MetaKey_TermConnDebug = "term:conndebug"
122122
MetaKey_TermBellSound = "term:bellsound"
123123
MetaKey_TermBellIndicator = "term:bellindicator"
124+
MetaKey_TermOsc52 = "term:osc52"
124125
MetaKey_TermDurable = "term:durable"
125126

126127
MetaKey_WebZoom = "web:zoom"

pkg/waveobj/wtypemeta.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ type MetaTSType struct {
125125
TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug
126126
TermBellSound *bool `json:"term:bellsound,omitempty"`
127127
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
128+
TermOsc52 string `json:"term:osc52,omitempty"`
128129
TermDurable *bool `json:"term:durable,omitempty"`
129130

130131
WebZoom float64 `json:"web:zoom,omitempty"`

pkg/wconfig/defaultconfig/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"telemetry:enabled": true,
3131
"term:bellsound": false,
3232
"term:bellindicator": false,
33+
"term:osc52": "focus",
3334
"term:cursor": "block",
3435
"term:cursorblink": false,
3536
"term:copyonselect": true,

pkg/wconfig/metaconsts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const (
5656
ConfigKey_TermCursorBlink = "term:cursorblink"
5757
ConfigKey_TermBellSound = "term:bellsound"
5858
ConfigKey_TermBellIndicator = "term:bellindicator"
59+
ConfigKey_TermOsc52 = "term:osc52"
5960
ConfigKey_TermDurable = "term:durable"
6061

6162
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"

pkg/wconfig/settingsconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ type SettingsType struct {
107107
TermCursorBlink *bool `json:"term:cursorblink,omitempty"`
108108
TermBellSound *bool `json:"term:bellsound,omitempty"`
109109
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
110+
TermOsc52 string `json:"term:osc52,omitempty" jsonschema:"enum=focus,enum=always"`
110111
TermDurable *bool `json:"term:durable,omitempty"`
111112

112113
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`

schema/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@
151151
"term:bellindicator": {
152152
"type": "boolean"
153153
},
154+
"term:osc52": {
155+
"type": "string",
156+
"enum": [
157+
"focus",
158+
"always"
159+
]
160+
},
154161
"term:durable": {
155162
"type": "boolean"
156163
},

0 commit comments

Comments
 (0)