108 lines · 4.0 KB · TypeScript
RawDownload
import type { ExecutionEnvironmentPlatformOs } from "@t3tools/contracts"; export type PendingModifier = "ctrl" | "meta";export type HostPlatform = "mac" | "linux" | "windows" | "unknown"; /** Upper bound of `TerminalWriteInput.data`; longer writes are rejected by the server. */export const TERMINAL_WRITE_MAX_LENGTH = 65_536; export type ModifiedTerminalInput =  | { readonly kind: "write"; readonly data: string }  | { readonly kind: "paste" }; // C0 controls other than tab, LF, and CR, plus DEL.// eslint-disable-next-line no-control-regex -- Pasted text must not carry raw terminal controls.const UNSAFE_PASTE_BYTES = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g; /** * Encodes a key pressed while the toolbar's one-shot ctrl modifier is armed * into the control byte a terminal expects. */export function applyCtrlModifier(input: string): string {  const firstCharacter = input[0];  if (!firstCharacter) {    return input;  }   const lowerCharacter = firstCharacter.toLowerCase();  if (lowerCharacter >= "a" && lowerCharacter <= "z") {    return String.fromCharCode(lowerCharacter.charCodeAt(0) - 96);  }   if (firstCharacter === "@") return "\u0000";  if (firstCharacter === "[") return "\u001b";  if (firstCharacter === "\\") return "\u001c";  if (firstCharacter === "]") return "\u001d";  if (firstCharacter === "^") return "\u001e";  if (firstCharacter === "_") return "\u001f";  if (firstCharacter === "?") return "\u007f";   return input;} /** * Resolves what a keypress means once a toolbar modifier is armed. The host's * paste chord (cmd+v on a macOS host, ctrl+v elsewhere) pastes the device * clipboard instead of reaching the remote shell as a raw control byte, which * matches what the web terminal does with the same chord. Forwarding the byte * is never what a phone user means: PowerShell binds ctrl+v to paste from the * host machine's clipboard, so the shell inserts whatever the desktop last * copied rather than the text on the phone. */export function resolveModifiedTerminalInput(input: {  readonly data: string;  readonly modifier: PendingModifier;  readonly hostPlatform: HostPlatform;}): ModifiedTerminalInput {  const pasteModifier: PendingModifier = input.hostPlatform === "mac" ? "meta" : "ctrl";  if (input.modifier === pasteModifier && input.data.toLowerCase() === "v") {    return { kind: "paste" };  }   return {    kind: "write",    data: input.modifier === "ctrl" ? applyCtrlModifier(input.data) : `\u001b${input.data}`,  };} /** * Encodes clipboard text for the remote pty the way the web terminal does when * bracketed paste is off: unsafe control bytes become spaces (which also * defuses an embedded bracketed-paste end marker, since its ESC goes too) and * line breaks become carriage returns, since a bare LF is Ctrl+J to a raw-mode * TUI. The native mobile surface does not expose DECSET 2004, so mobile never * wraps a paste in bracketed-paste markers. */export function encodeTerminalPaste(text: string): string {  return text.replace(UNSAFE_PASTE_BYTES, " ").replace(/\r\n|\n/g, "\r");} /** * Splits terminal input into writes the wire contract accepts, never cutting * through a surrogate pair so every chunk stays valid UTF-16. */export function chunkTerminalWrite(data: string): ReadonlyArray<string> {  const chunks: string[] = [];  let start = 0;  while (start < data.length) {    let end = Math.min(start + TERMINAL_WRITE_MAX_LENGTH, data.length);    const last = data.charCodeAt(end - 1);    if (end < data.length && last >= 0xd800 && last <= 0xdbff) {      end -= 1;    }    chunks.push(data.slice(start, end));    start = end;  }  return chunks;} /** * Maps the OS reported by the environment descriptor onto the toolbar's host * layout. Returns null for "unknown" so callers can fall back to a weaker signal. */export function hostPlatformFromOs(os: ExecutionEnvironmentPlatformOs | null): HostPlatform | null {  if (os === "darwin") return "mac";  if (os === "linux") return "linux";  if (os === "windows") return "windows";  return null;}