Skip to content
Source
Sign inSign up
Open History to see the last commit

terminalInput.ts

88 lines3.0 KB

1import type { ExecutionEnvironmentPlatformOs } from "@t3tools/contracts";
2​
3export type PendingModifier = "ctrl" | "meta";
4export type HostPlatform = "mac" | "linux" | "windows" | "unknown";
5​
6/** Upper bound of `TerminalWriteInput.data`; longer writes are rejected by the server. */
7export const TERMINAL_WRITE_MAX_LENGTH = 65_536;
8​
9export type ModifiedTerminalInput =
10 | { readonly kind: "write"; readonly data: string }
11 | { readonly kind: "paste" };
12​
13const UNSAFE_PASTE_BYTES = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
14​
15function applyCtrlModifier(input: string): string {
16 const firstCharacter = input[0];
17 if (!firstCharacter) {
18 return input;
19 }
20​
21 const lowerCharacter = firstCharacter.toLowerCase();
22 if (lowerCharacter >= "a" && lowerCharacter <= "z") {
23 return String.fromCharCode(lowerCharacter.charCodeAt(0) - 96);
24 }
25​
26 if (firstCharacter === "@") return "\u0000";
27 if (firstCharacter === "[") return "\u001b";
28 if (firstCharacter === "\\") return "\u001c";
29 if (firstCharacter === "]") return "\u001d";
30 if (firstCharacter === "^") return "\u001e";
31 if (firstCharacter === "_") return "\u001f";
32 if (firstCharacter === "?") return "\u007f";
33​
34 return input;
36​
37/** Use the device clipboard for host paste chords; forwarding Ctrl+V to PowerShell pastes the host clipboard. */
38export function resolveModifiedTerminalInput(input: {
39 readonly data: string;
40 readonly modifier: PendingModifier;
41 readonly hostPlatform: HostPlatform;
42}): ModifiedTerminalInput {
43 const pasteModifier: PendingModifier = input.hostPlatform === "mac" ? "meta" : "ctrl";
44 if (input.modifier === pasteModifier && input.data.toLowerCase() === "v") {
45 return { kind: "paste" };
46 }
47​
48 if (
49 input.data.length === 3 &&
50 input.data.startsWith("\u001b[") &&
51 /[ABCD]/.test(input.data.charAt(2))
52 ) {
53 return { kind: "write", data: `\u001b[1;${input.modifier === "ctrl" ? 5 : 3}${input.data[2]}` };
54 }
55​
56 return {
57 kind: "write",
58 data: input.modifier === "ctrl" ? applyCtrlModifier(input.data) : `\u001b${input.data}`,
59 };
61​
62/** No DECSET 2004 state, so omit bracket markers. Strip unsafe controls and use CR; raw-mode TUIs read LF as Ctrl+J. */
63export function encodeTerminalPaste(text: string): string {
64 return text.replace(UNSAFE_PASTE_BYTES, " ").replace(/\r\n|\n/g, "\r");
66​
67export function chunkTerminalWrite(data: string): ReadonlyArray<string> {
68 const chunks: string[] = [];
69 let start = 0;
70 while (start < data.length) {
71 let end = Math.min(start + TERMINAL_WRITE_MAX_LENGTH, data.length);
72 const last = data.charCodeAt(end - 1);
73 if (end < data.length && last >= 0xd800 && last <= 0xdbff) {
74 end -= 1;
75 }
76 chunks.push(data.slice(start, end));
77 start = end;
78 }
79 return chunks;
81​
82/** Returns null for unknown OSes so callers can try weaker signals. */
83export function hostPlatformFromOs(os: ExecutionEnvironmentPlatformOs | null): HostPlatform | null {
84 if (os === "darwin") return "mac";
85 if (os === "linux") return "linux";
86 if (os === "windows") return "windows";
87 return null;