| 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 | |
| 13 | interface TbTab { |
| 14 | id?: number; |
| 15 | url?: string; |
| 16 | title?: string; |
| 17 | } |
| 18 | |
| 19 | interface TbInjectionResult<T = unknown> { |
| 20 | result?: T; |
| 21 | frameId?: number; |
| 22 | } |
| 23 | |
| 24 | interface 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 | |
| 31 | interface 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 | |
| 37 | interface 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 | |
| 89 | declare var browser: TbExtensionApi | undefined; |
| 90 | declare 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 | |
| 102 | interface 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. */ |
| 111 | interface 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 | |
| 121 | interface 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 | */ |
| 139 | interface 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. */ |
| 149 | interface 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 | |
| 158 | type 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. */ |
| 166 | interface 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. */ |
| 175 | type TbPickedFormat = "css" | "xpath" | "id" | "testid" | "text" | "href"; |
| 176 | |
| 177 | /** What picker.js's describe() returns — page-controlled, all of it. */ |
| 178 | interface 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 | |
| 198 | interface TbTerminalTheme { |
| 199 | [color: string]: string; |
| 200 | } |
| 201 | |
| 202 | interface 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 | |
| 214 | declare 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 | |
| 232 | declare 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. |
| 246 | declare var module: { exports: any } | undefined; |
| 247 | |
| 248 | declare const Themes: typeof import("../lib/theme.js"); |
| 249 | declare const Sanitize: typeof import("../lib/sanitize.js"); |
| 250 | declare const Shot: typeof import("../lib/shot.js"); |
| 251 | |
| 252 | /** Set by picker.js inside the *page*, not here — see cancelPick(). */ |
| 253 | interface Window { |
| 254 | __tbPickerActive?: (() => void) | null; |
| 255 | } |