anvilsign in

collin/browser-terminal-extension

1// Ambient declarations for the globals these scripts run against.
2//
3// Hand-written rather than pulled from @types/chrome on purpose: this file is
4// the *inventory* of extension API surface termbridge touches. Adding a call
5// means adding it here first, which is a small, useful speed bump — the
6// manifest's permission list has to grow with it. It is also why `npm run
7// check` needs exactly one dependency.
8//
9// Chrome and Firefox differ: `browser.*` is promise-based everywhere, and
10// Chrome's `chrome.*` has been promise-based for MV3. Everything below is typed
11// as promises because that is the only way it is called here.
12
13interface TbTab {
14 id?: number;
15 url?: string;
16 title?: string;
17}
18
19interface TbInjectionResult<T = unknown> {
20 result?: T;
21 frameId?: number;
22}
23
24interface TbMessageSender {
25 id?: string;
26 /** Set for content scripts and only for content scripts — the check that
27 keeps a hostile page from reaching the sidebar's message handler. */
28 tab?: TbTab;
29}
30
31interface TbStorageArea {
32 get(keys?: string | string[] | null): Promise<Record<string, any>>;
33 set(items: Record<string, any>): Promise<void>;
34 remove(keys: string | string[]): Promise<void>;
35}
36
37interface TbExtensionApi {
38 runtime: {
39 id: string;
40 sendMessage(message: any): Promise<any>;
41 onMessage: {
42 addListener(
43 cb: (message: any, sender: TbMessageSender) => void | boolean | Promise<any>,
44 ): void;
45 };
46 };
47 storage: { local: TbStorageArea };
48 tabs: {
49 query(info: { active?: boolean; currentWindow?: boolean }): Promise<TbTab[]>;
50 create(props: { url: string }): Promise<TbTab>;
51 /** The visible area of the active tab, as a PNG data URL. Needs a host
52 permission for the page, exactly as the picker's injection does. */
53 captureVisibleTab(options: { format: "png" }): Promise<string>;
54 };
55 permissions: {
56 request(perms: { origins?: string[]; permissions?: string[] }): Promise<boolean>;
57 };
58 scripting: {
59 /**
60 * `Awaited<T>`: a func that returns a promise is awaited by the browser,
61 * and `result` is what it resolved to — which is the whole reason the
62 * picker can hand its value back as a return value.
63 */
64 executeScript<T, A extends any[] = []>(injection: {
65 target: { tabId: number | undefined };
66 func: (...args: A) => T;
67 /** Structured-cloned into the page. Keep it small — a screenshot goes
68 through here as base64. */
69 args?: A;
70 }): Promise<TbInjectionResult<Awaited<T>>[]>;
71 };
72 /** Firefox only: writes an image to the clipboard without a focused
73 document, which the async clipboard API refuses to do. */
74 clipboard?: {
75 setImageData(image: ArrayBuffer, type: "png" | "jpeg"): Promise<void>;
76 };
77 /** Absent in Firefox, and absent in Chrome until the browser is new enough. */
78 sidePanel?: {
79 setPanelBehavior?(behavior: {
80 openPanelOnActionClick: boolean;
81 }): Promise<void>;
82 };
83 /** Absent when no keyboard shortcuts are declared in the manifest. */
84 commands?: {
85 onCommand: { addListener(cb: (command: string) => void): void };
86 };
87}
88
89declare var browser: TbExtensionApi | undefined;
90declare var chrome: TbExtensionApi | undefined;
91
92// --- the daemon's wire protocol ---------------------------------------------
93//
94// The other half of these shapes is Rust: `Snapshot`, `SessionInfo` and `Agent`
95// in daemon/src/status.rs, and the `ok` frame built in daemon/src/server.rs.
96// Change one side and this side has to move with it — which is most of why
97// they are written down at all.
98//
99// Everything here arrives over a socket. It is *claimed* structure, not
100// guaranteed structure, and it is only ever assigned through textContent.
101
102interface TbSessionInfo {
103 name: string;
104 /** Window count. 0 when it isn't known yet — the `ok` frame sends names only. */
105 windows: number;
106 /** Some tmux client, anywhere, is on this session. */
107 attached: boolean;
108}
109
110/** One window of the attached session — one tab. */
111interface TbWindowInfo {
112 /** tmux window id (`@3`). Stable; the index is not. */
113 id: string;
114 index: number;
115 name: string;
116 active: boolean;
117 panes: number;
118 activity: boolean;
119}
120
121interface TbAgent {
122 /** tmux pane id (`%12`), stable for the pane's lifetime. */
123 pane: string;
124 /** tmux window id (`@3`) — the join to TbWindowInfo. */
125 window_id: string;
126 window: string;
127 name: string;
128 state: "working" | "waiting" | "idle" | "unknown";
129 mode?: string | null;
130 tool?: string | null;
131 message?: string | null;
132 title?: string | null;
133}
134
135/**
136 * First frame after a successful auth. It carries session *names* only — the
137 * window list needs the control channel, which is up a moment later.
138 */
139interface TbOkFrame {
140 type: "ok";
141 profile: string;
142 tmux: boolean;
143 defaultSession?: string;
144 /** Names only — window counts arrive with the first status frame. */
145 sessions?: string[];
146}
147
148/** Pushed whenever tmux says something changed. */
149interface TbStatusFrame {
150 type: "status";
151 session?: string;
152 sessions?: TbSessionInfo[];
153 /** Windows of `session`, in index order. */
154 windows?: TbWindowInfo[];
155 agents?: TbAgent[];
156}
157
158type TbFrame =
159 | TbOkFrame
160 | TbStatusFrame
161 | { type: "exit"; code: number }
162 | { type: "error"; reason: string }
163 | { type: "tmux-error"; reason: string };
164
165/** The element's box in CSS pixels, relative to the viewport. */
166interface TbPickedRect {
167 x: number;
168 y: number;
169 width: number;
170 height: number;
171}
172
173/** The identifier flavours the panel can show — the string-valued keys of
174 TbPicked, and the only ones that may reach the terminal. */
175type TbPickedFormat = "css" | "xpath" | "id" | "testid" | "text" | "href";
176
177/** What picker.js's describe() returns — page-controlled, all of it. */
178interface TbPicked {
179 /** Where to crop the tab screenshot. Absent on picks made before 0.0.2. */
180 rect?: TbPickedRect;
181 /** Viewport size at pick time, which fixes the screenshot's scale factor. */
182 viewport?: { w: number; h: number };
183 css: string;
184 xpath: string;
185 id: string | null;
186 testid: string | null;
187 tag: string;
188 text: string | null;
189 href: string | null;
190 pageUrl: string;
191}
192
193// --- vendored xterm.js ------------------------------------------------------
194//
195// The vendored build ships no types. Only the handful of members used here are
196// declared; anything else is a deliberate error rather than a silent `any`.
197
198interface TbTerminalTheme {
199 [color: string]: string;
200}
201
202interface TbTerminalOptions {
203 fontFamily?: string;
204 fontSize?: number;
205 lineHeight?: number;
206 cursorBlink?: boolean;
207 scrollback?: number;
208 allowProposedApi?: boolean;
209 convertEol?: boolean;
210 macOptionIsMeta?: boolean;
211 theme?: TbTerminalTheme;
212}
213
214declare class Terminal {
215 constructor(options?: TbTerminalOptions);
216 readonly cols: number;
217 readonly rows: number;
218 readonly element?: HTMLElement;
219 options: TbTerminalOptions;
220 open(parent: HTMLElement): void;
221 write(data: string | Uint8Array): void;
222 focus(): void;
223 clear(): void;
224 loadAddon(addon: unknown): void;
225 attachCustomKeyEventHandler(handler: (e: KeyboardEvent) => boolean): void;
226 onData(cb: (data: string) => void): { dispose(): void };
227 /** Bytes xterm could not represent as UTF-16 — each char is one byte. */
228 onBinary(cb: (data: string) => void): { dispose(): void };
229 onResize(cb: (size: { cols: number; rows: number }) => void): { dispose(): void };
230}
231
232declare namespace FitAddon {
233 class FitAddon {
234 fit(): void;
235 proposeDimensions(): { cols: number; rows: number } | undefined;
236 }
237}
238
239// --- our own files, as the browser sees them --------------------------------
240//
241// lib/theme.js and lib/sanitize.js are loaded as classic scripts here and
242// require()d by their node --test files, so each one ends with a CommonJS tail
243// guarded on `typeof module`. That tail is enough to make TypeScript treat them
244// as modules, so their globals have to be re-stated — from the files
245// themselves, so the shapes cannot drift.
246declare var module: { exports: any } | undefined;
247
248declare const Themes: typeof import("../lib/theme.js");
249declare const Sanitize: typeof import("../lib/sanitize.js");
250declare const Shot: typeof import("../lib/shot.js");
251
252/** Set by picker.js inside the *page*, not here — see cancelPick(). */
253interface Window {
254 __tbPickerActive?: (() => void) | null;
255}