authorclover caruso <git@paperclover.net> 2026-10-13 11:10:31-07:00
committerclover caruso <git@paperclover.net> 2026-10-16 16:52:04-07:00
log06b388143eb05c2ed94bc04901560d243bb58823
treec02e3bb385f484720ac9b6458fecc19b016798ef
parent134f2f0765bf03e98da10fc3f7c1614dbe99a9fe
signature Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ (~clover)

feat: replace T3's terminal with @clo/terminal

Clover wants terminal output to select like ordinary text and new terminals to open beside the chat. Replace the canvas renderer with @clo/terminal used by chat panels, provider sign-in, and font previews. Preserve PTY sessions, restoration, themes, fonts, IME, clipboard behavior, and destination permissions. Default new terminals to tabs in the right panel; expose Terminal position in General settings and honor Bottom placement and existing sessions. Route toolbar, shortcut, script, and revealed terminals through their owning panel. Keep output selection across focus changes, require modified clicks for complete host paths, tolerate late writes after disposal, and retain input focus during font loading so Cmd+J reaches the terminal. Keep the engine, fonts, and notices in one package home. Close idle shells and exited terminals directly. Refresh scoped subprocess metadata before every explicit close; keep confirmation for running programs and unknown activity across drawer controls, panel tabs, split surfaces, and keyboard shortcuts. Match the rendered grid to Ghostty's integer-pixel mouse geometry so clicks, drags, and wheel reports reach the visible cell. Give asynchronous mounts separate owned elements so obsolete setup cannot strip a replacement's styles, steal focus, or resize its PTY. Assisted-by: gpt-6.1-sol

137 files changed, 38402 insertions(+), 8258 deletions(-)

apps/web/package.json+1-1
......@@ -4,7 +4,6 @@
44 "private": true,
55 "type": "module",
66 "scripts": {
7 "build:ghostty-wasm": "bash scripts/build-libghostty-wasm.sh",
87 "dev": "vp dev",
98 "build": "vp build",
109 "preview": "vp preview",
......@@ -15,6 +14,7 @@
1514 "@base-ui/react": "^1.4.1",
1615 "@clerk/electron": "catalog:",
1716 "@clerk/react": "catalog:",
17 "@clo/terminal": "workspace:*",
1818 "@daypicker/react": "^10.0.1",
1919 "@dnd-kit/core": "^6.3.1",
2020 "@dnd-kit/modifiers": "^9.0.0",
apps/web/scripts/build-libghostty-wasm.sh deleted-123
......@@ -1,123 +0,0 @@
1#!/usr/bin/env bash
2
3set -euo pipefail
4
5SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6WEB_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
7REPO_DIR="$(cd "${WEB_DIR}/../.." && pwd)"
8CANONICAL_VENDOR_DIR="${REPO_DIR}/native/libghostty-vt"
9VENDOR_DIR="${WEB_DIR}/src/terminal/ghostty/vendor"
10
11GHOSTTY_REVISION="$(tr -d '[:space:]' < "${CANONICAL_VENDOR_DIR}/VERSION")"
12GHOSTTY_SOURCE_DIR="${GHOSTTY_SOURCE_DIR:-${HOME}/.cache/t3code/ghostty-${GHOSTTY_REVISION:0:8}}"
13GHOSTTY_ZIG_VERSION="${GHOSTTY_ZIG_VERSION:-0.15.2}"
14GHOSTTY_ZIG="${GHOSTTY_ZIG:-}"
15
16log() {
17 printf '[libghostty-vt-wasm] %s\n' "$*"
18}
19
20die() {
21 printf '[libghostty-vt-wasm] error: %s\n' "$*" >&2
22 exit 1
23}
24
25require_cmd() {
26 command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
27}
28
29ensure_zig() {
30 if [[ -n "${GHOSTTY_ZIG}" ]]; then
31 [[ -x "${GHOSTTY_ZIG}" ]] || die "GHOSTTY_ZIG is not executable: ${GHOSTTY_ZIG}"
32 return
33 fi
34 if command -v zig >/dev/null 2>&1 && [[ "$(zig version)" == "${GHOSTTY_ZIG_VERSION}" ]]; then
35 GHOSTTY_ZIG="$(command -v zig)"
36 return
37 fi
38
39 local host_os host_arch cache_dir
40 host_os="$(uname -s | tr '[:upper:]' '[:lower:]')"
41 host_arch="$(uname -m)"
42 case "${host_os}" in
43 darwin) host_os="macos" ;;
44 linux) ;;
45 *) die "unsupported host OS for Zig download: ${host_os}" ;;
46 esac
47 case "${host_arch}" in
48 arm64) host_arch="aarch64" ;;
49 aarch64 | x86_64) ;;
50 *) die "unsupported host architecture: ${host_arch}" ;;
51 esac
52
53 cache_dir="${HOME}/.cache/t3code/zig-${GHOSTTY_ZIG_VERSION}"
54 GHOSTTY_ZIG="${cache_dir}/zig"
55 if [[ -x "${GHOSTTY_ZIG}" ]]; then
56 return
57 fi
58
59 require_cmd curl
60 require_cmd tar
61 mkdir -p "${cache_dir}"
62 log "downloading Zig ${GHOSTTY_ZIG_VERSION}"
63 curl -fsSL \
64 "https://ziglang.org/download/${GHOSTTY_ZIG_VERSION}/zig-${host_arch}-${host_os}-${GHOSTTY_ZIG_VERSION}.tar.xz" \
65 | tar -xJ --strip-components=1 -C "${cache_dir}"
66}
67
68ensure_ghostty_source() {
69 require_cmd git
70 if [[ ! -d "${GHOSTTY_SOURCE_DIR}/.git" ]]; then
71 mkdir -p "$(dirname "${GHOSTTY_SOURCE_DIR}")"
72 log "cloning Ghostty ${GHOSTTY_REVISION}"
73 git clone --filter=blob:none --no-checkout https://github.com/ghostty-org/ghostty.git \
74 "${GHOSTTY_SOURCE_DIR}"
75 fi
76
77 # A cached checkout may still be on a previously pinned revision; converge on
78 # the pinned one instead of failing the rebuild.
79 local actual_revision
80 actual_revision="$(git -C "${GHOSTTY_SOURCE_DIR}" rev-parse HEAD 2>/dev/null || echo none)"
81 if [[ "${actual_revision}" != "${GHOSTTY_REVISION}" ]]; then
82 log "checking out Ghostty ${GHOSTTY_REVISION}"
83 git -C "${GHOSTTY_SOURCE_DIR}" fetch --depth=1 origin "${GHOSTTY_REVISION}"
84 git -C "${GHOSTTY_SOURCE_DIR}" checkout --detach "${GHOSTTY_REVISION}"
85 fi
86
87 actual_revision="$(git -C "${GHOSTTY_SOURCE_DIR}" rev-parse HEAD)"
88 [[ "${actual_revision}" == "${GHOSTTY_REVISION}" ]] || \
89 die "expected Ghostty ${GHOSTTY_REVISION}, found ${actual_revision}"
90}
91
92ensure_zig
93ensure_ghostty_source
94
95build_root="$(mktemp -d)"
96trap 'rm -rf "${build_root}"' EXIT
97
98log "building ${GHOSTTY_REVISION} for wasm32-freestanding"
99(
100 cd "${GHOSTTY_SOURCE_DIR}"
101 # The pinned revision rides along as semver build metadata so the artifact
102 # identifies its own provenance through ghostty_build_info(); mobile's
103 # VERSION file stays the single source of truth for the pin.
104 "${GHOSTTY_ZIG}" build \
105 -Demit-lib-vt \
106 -Dtarget=wasm32-freestanding \
107 -Doptimize=ReleaseSmall \
108 -Dstrip=true \
109 -Dlib-version-string="0.1.0-dev+${GHOSTTY_REVISION}" \
110 -p "${build_root}"
111)
112
113mkdir -p "${VENDOR_DIR}"
114cp "${build_root}/bin/ghostty-vt.wasm" "${VENDOR_DIR}/ghostty-vt.wasm"
115"${GHOSTTY_ZIG}" build-exe \
116 "${SCRIPT_DIR}/ghostty-write-pty.zig" \
117 -target wasm32-freestanding \
118 -O ReleaseSmall \
119 -fno-entry \
120 -rdynamic \
121 -femit-bin="${VENDOR_DIR}/ghostty-write-pty.wasm"
122chmod 0644 "${VENDOR_DIR}/ghostty-write-pty.wasm"
123log "wrote ${VENDOR_DIR}/ghostty-vt.wasm"
apps/web/src/components/ChatView.tsx+97-58
......@@ -566,6 +566,7 @@ import {
566566 shouldRefocusComposerOnWindowFocus,
567567} from "./ChatView.logic";
568568import { useLocalStorage } from "~/hooks/useLocalStorage";
569import { useTerminalPlacement } from "~/hooks/useTerminalPlacement";
569570import { useComposerHandleContext } from "../composerHandleContext";
570571import {
571572 awaitAttachmentUploads,
......@@ -1953,6 +1954,7 @@ export default function ChatView(props: ChatViewProps) {
19531954 // When set, the thread-change reset effect will open the sidebar instead of closing it.
19541955 // Used by "Implement in a new thread" to carry the sidebar-open intent across navigation.
19551956 const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0);
1957 const [terminalPlacement] = useTerminalPlacement();
19561958 const [pullRequestDialogState, setPullRequestDialogState] =
19571959 useState<PullRequestDialogState | null>(null);
19581960 const [terminalUiLaunchContext, setTerminalUiLaunchContext] =
......@@ -2326,6 +2328,9 @@ export default function ChatView(props: ChatViewProps) {
23262328 const activeRightPanelSurface = useRightPanelStore((state) =>
23272329 selectActiveRightPanelSurface(state.byThreadKey, activeThreadRef),
23282330 );
2331 const terminalVisible =
2332 terminalUiState.terminalOpen ||
2333 (rightPanelState.isOpen && activeRightPanelSurface?.kind === "terminal");
23292334 const activePreviewState = useThreadPreviewState(activeThreadRef);
23302335 const activePreviewServerEpoch = activePreviewState.serverEpoch;
23312336 const previewSessionsReady = !activeEnvironmentServerBrowser || activePreviewState.listLoaded;
......@@ -4805,8 +4810,69 @@ export default function ChatView(props: ChatViewProps) {
48054810 },
48064811 [activeThreadRef, storeSetTerminalOpen],
48074812 );
4813 const addTerminalSurface = useCallback(() => {
4814 if (!hasTerminalWriteAccess() || !activeThreadRef || !activeThreadId || !activeProject) return;
4815 const cwd = gitCwd ?? activeProject.workspaceRoot;
4816 const terminalId = allocateTerminalId();
4817 useRightPanelStore.getState().openTerminal(activeThreadRef, terminalId);
4818 setTerminalFocusRequestId((value) => value + 1);
4819 void openTerminal({
4820 environmentId: activeThreadRef.environmentId,
4821 input: {
4822 threadId: activeThreadId,
4823 terminalId,
4824 cwd,
4825 ...(activeThreadWorktreePath != null ? { worktreePath: activeThreadWorktreePath } : {}),
4826 env: projectScriptRuntimeEnv({
4827 project: { cwd: activeProject.workspaceRoot },
4828 worktreePath: activeThreadWorktreePath,
4829 }),
4830 },
4831 });
4832 }, [
4833 activeProject,
4834 activeThreadId,
4835 activeThreadRef,
4836 activeThreadWorktreePath,
4837 allocateTerminalId,
4838 gitCwd,
4839 openTerminal,
4840 hasTerminalWriteAccess,
4841 ]);
48084842 const toggleTerminalVisibility = useCallback(() => {
48094843 if (!activeThreadRef) return;
4844 if (terminalPlacement === "right" && terminalUiState.terminalIds.length === 0) {
4845 const panels = useRightPanelStore.getState();
4846 const panelState = selectThreadRightPanelState(panels.byThreadKey, activeThreadRef);
4847 const surface = panelState.surfaces.find((entry) => entry.kind === "terminal");
4848 if (surface) {
4849 if (panelState.isOpen && panelState.activeSurfaceId === surface.id) {
4850 panels.close(activeThreadRef);
4851 } else {
4852 if (
4853 !readEnvironmentScope(environmentId, AuthTerminalReadScope) &&
4854 !hasTerminalWriteAccess()
4855 )
4856 return;
4857 panels.activateSurface(activeThreadRef, surface.id);
4858 setTerminalFocusRequestId((value) => value + 1);
4859 }
4860 } else {
4861 if (
4862 !readEnvironmentScope(environmentId, AuthTerminalReadScope) &&
4863 !hasTerminalWriteAccess()
4864 )
4865 return;
4866 const existingTerminalId = runningTerminalIds[0] ?? activeServerOrderedTerminalIds[0];
4867 if (existingTerminalId !== undefined) {
4868 panels.openTerminal(activeThreadRef, existingTerminalId);
4869 setTerminalFocusRequestId((value) => value + 1);
4870 } else {
4871 addTerminalSurface();
4872 }
4873 }
4874 return;
4875 }
48104876 const nextOpen = !terminalUiState.terminalOpen;
48114877 if (
48124878 nextOpen &&
......@@ -4850,6 +4916,8 @@ export default function ChatView(props: ChatViewProps) {
48504916 }
48514917 setTerminalOpen(nextOpen);
48524918 }, [
4919 addTerminalSurface,
4920 terminalPlacement,
48534921 activeServerOrderedTerminalIds,
48544922 activeProject,
48554923 activeThreadId,
......@@ -5026,7 +5094,6 @@ export default function ChatView(props: ChatViewProps) {
50265094 cwd: targetCwd,
50275095 worktreePath: targetWorktreePath,
50285096 });
5029 setTerminalOpen(true);
50305097 if (!activeThreadRef) {
50315098 return;
50325099 }
......@@ -5058,10 +5125,18 @@ export default function ChatView(props: ChatViewProps) {
50585125 env: runtimeEnv,
50595126 };
50605127
5061 if (shouldCreateNewTerminal) {
5062 storeNewTerminal(activeThreadRef, targetTerminalId);
5128 if (
5129 terminalPlacement === "right" &&
5130 !terminalUiState.terminalIds.includes(targetTerminalId)
5131 ) {
5132 useRightPanelStore.getState().openTerminal(activeThreadRef, targetTerminalId);
50635133 } else {
5064 storeSetActiveTerminal(activeThreadRef, targetTerminalId);
5134 setTerminalOpen(true);
5135 if (shouldCreateNewTerminal) {
5136 storeNewTerminal(activeThreadRef, targetTerminalId);
5137 } else {
5138 storeSetActiveTerminal(activeThreadRef, targetTerminalId);
5139 }
50655140 }
50665141
50675142 const openResult = await openTerminal({ environmentId, input: openTerminalInput });
......@@ -5132,6 +5207,8 @@ export default function ChatView(props: ChatViewProps) {
51325207 canReuseTerminal,
51335208 allocateTerminalId,
51345209 runningTerminalIds,
5210 terminalPlacement,
5211 terminalUiState.terminalIds,
51355212 terminalUiState.activeTerminalId,
51365213 writeTerminal,
51375214 ],
......@@ -5928,35 +6005,6 @@ export default function ChatView(props: ChatViewProps) {
59286005 createBrowserSurface,
59296006 previewPanelOpen,
59306007 ]);
5931 const addTerminalSurface = useCallback(() => {
5932 if (!hasTerminalWriteAccess() || !activeThreadRef || !activeThreadId || !activeProject) return;
5933 const cwd = gitCwd ?? activeProject.workspaceRoot;
5934 const terminalId = allocateTerminalId();
5935 useRightPanelStore.getState().openTerminal(activeThreadRef, terminalId);
5936 setTerminalFocusRequestId((value) => value + 1);
5937 void openTerminal({
5938 environmentId: activeThreadRef.environmentId,
5939 input: {
5940 threadId: activeThreadId,
5941 terminalId,
5942 cwd,
5943 ...(activeThreadWorktreePath != null ? { worktreePath: activeThreadWorktreePath } : {}),
5944 env: projectScriptRuntimeEnv({
5945 project: { cwd: activeProject.workspaceRoot },
5946 worktreePath: activeThreadWorktreePath,
5947 }),
5948 },
5949 });
5950 }, [
5951 activeProject,
5952 activeThreadId,
5953 activeThreadRef,
5954 activeThreadWorktreePath,
5955 allocateTerminalId,
5956 gitCwd,
5957 openTerminal,
5958 hasTerminalWriteAccess,
5959 ]);
59606008 const splitPanelTerminal = useCallback(
59616009 (direction: "horizontal" | "vertical" = "horizontal") => {
59626010 if (
......@@ -6042,27 +6090,25 @@ export default function ChatView(props: ChatViewProps) {
60426090 );
60436091 const requestCloseTerminal = useCallback(
60446092 (terminalId: string) => {
6045 if (!hasTerminalWriteAccess()) return;
6046 const label = activeTerminalLabelsById.get(terminalId) ?? getTerminalLabel(terminalId);
6047 void confirmTerminalClose([label]).then((confirmed) => {
6093 if (!hasTerminalWriteAccess() || !activeThreadRef) return;
6094 void confirmTerminalClose(activeThreadRef, [terminalId]).then((confirmed) => {
60486095 if (confirmed && readEnvironmentScope(environmentId, AuthTerminalOperateScope)) {
60496096 closeTerminal(terminalId);
60506097 }
60516098 });
60526099 },
6053 [hasTerminalWriteAccess, activeTerminalLabelsById, closeTerminal, environmentId],
6100 [hasTerminalWriteAccess, activeThreadRef, closeTerminal, environmentId],
60546101 );
60556102 const requestClosePanelTerminal = useCallback(
60566103 (terminalId: string) => {
6057 if (!hasTerminalWriteAccess()) return;
6058 const label = activeTerminalLabelsById.get(terminalId) ?? getTerminalLabel(terminalId);
6059 void confirmTerminalClose([label]).then((confirmed) => {
6104 if (!hasTerminalWriteAccess() || !activeThreadRef) return;
6105 void confirmTerminalClose(activeThreadRef, [terminalId]).then((confirmed) => {
60606106 if (confirmed && readEnvironmentScope(environmentId, AuthTerminalOperateScope)) {
60616107 closePanelTerminal(terminalId);
60626108 }
60636109 });
60646110 },
6065 [hasTerminalWriteAccess, activeTerminalLabelsById, closePanelTerminal, environmentId],
6111 [hasTerminalWriteAccess, activeThreadRef, closePanelTerminal, environmentId],
60666112 );
60676113 const activateRightPanelSurface = useCallback(
60686114 (surface: RightPanelSurface) => {
......@@ -6206,26 +6252,19 @@ export default function ChatView(props: ChatViewProps) {
62066252 finishClose();
62076253 return;
62086254 }
6209 const activeLabel =
6210 activeTerminalLabelsById.get(surface.activeTerminalId) ??
6211 getTerminalLabel(surface.activeTerminalId);
6212 const otherLabels = surface.terminalIds
6213 .filter((terminalId) => terminalId !== surface.activeTerminalId)
6214 .map(
6215 (terminalId) => activeTerminalLabelsById.get(terminalId) ?? getTerminalLabel(terminalId),
6216 );
6217 void confirmTerminalClose([activeLabel, ...otherLabels]).then((confirmed) => {
6255 const otherTerminalIds = surface.terminalIds.filter(
6256 (terminalId) => terminalId !== surface.activeTerminalId,
6257 );
6258 void confirmTerminalClose(activeThreadRef, [
6259 surface.activeTerminalId,
6260 ...otherTerminalIds,
6261 ]).then((confirmed) => {
62186262 if (confirmed) {
62196263 finishClose();
62206264 }
62216265 });
62226266 },
6223 [
6224 activeThreadRef,
6225 activeTerminalLabelsById,
6226 closeAfterAgentBrowserConfirmation,
6227 finishRightPanelSurfaceClose,
6228 ],
6267 [activeThreadRef, closeAfterAgentBrowserConfirmation, finishRightPanelSurfaceClose],
62296268 );
62306269 const closeOtherRightPanelSurfaces = useCallback(
62316270 (surface: RightPanelSurface) => {
......@@ -11340,7 +11379,7 @@ export default function ChatView(props: ChatViewProps) {
1134011379 };
1134111380 const panelToggleControlProps = {
1134211381 terminalAvailable: activeProject !== null,
11343 terminalOpen: terminalUiState.terminalOpen,
11382 terminalOpen: terminalVisible,
1134411383 terminalShortcutLabel: shortcutLabelForCommand(keybindings, "terminal.toggle"),
1134511384 threadPanelOpen,
1134611385 threadPanelPresentation,
......@@ -11874,7 +11913,7 @@ export default function ChatView(props: ChatViewProps) {
1187411913 resolvedTheme={resolvedTheme}
1187511914 settings={settings}
1187611915 keybindings={keybindings}
11877 terminalOpen={Boolean(terminalUiState.terminalOpen)}
11916 terminalOpen={terminalVisible}
1187811917 gitCwd={gitCwd}
1187911918 pullRequestProjectId={
1188011919 supportsPullRequests ? (activeProject?.id ?? null) : null
apps/web/src/components/ThreadTerminalDrawer.permissions.test.tsx+39-17
......@@ -1,4 +1,5 @@
11import {
2 AuthOrchestrationOperateScope,
23 AuthTerminalOperateScope,
34 EnvironmentId,
45 ThreadId,
......@@ -9,23 +10,16 @@ import { nextTerminalAttachSeedState } from "@t3tools/client-runtime/state/termi
910import { AsyncResult } from "effect/reactivity";
1011import { act, create, type ReactTestRenderer } from "react-test-renderer";
1112import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
12import type {
13 GhosttyTerminalSurface,
14 GhosttyTerminalSurfaceOptions,
15} from "~/terminal/ghostty/surface";
13import type { TerminalSurface, TerminalSurfaceOptions } from "~/terminal/TerminalSurface";
1614
1715const state = vi.hoisted(() => ({
1816 allowed: true,
17 editorAllowed: true,
1918 listeners: new Set<() => void>(),
2019 resize: vi.fn(),
2120 otherCommand: vi.fn(),
2221 createSurface:
23 vi.fn<
24 (
25 mount: HTMLElement,
26 options: GhosttyTerminalSurfaceOptions,
27 ) => Promise<GhosttyTerminalSurface>
28 >(),
22 vi.fn<(mount: HTMLElement, options: TerminalSurfaceOptions) => Promise<TerminalSurface>>(),
2923}));
3024
3125vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null }));
......@@ -47,14 +41,20 @@ vi.mock("../state/use-atom-command", () => ({
4741vi.mock("../state/terminalSessions", () => ({
4842 useAttachedTerminalSession: () => session,
4943}));
50vi.mock("~/terminal/ghostty/surface", () => ({
51 GhosttyTerminalSurface: { create: state.createSurface },
44vi.mock("~/terminal/TerminalSurface", () => ({
45 TerminalSurface: { create: state.createSurface },
5246}));
5347vi.mock("../state/session", async () => {
5448 const { useSyncExternalStore } = await import("react");
55 const readEnvironmentScope = (id: EnvironmentId | null, scope: AuthEnvironmentScope) =>
56 id !== null &&
57 (scope !== AuthTerminalOperateScope || id !== threadRef.environmentId || state.allowed);
49 const readEnvironmentScope = (id: EnvironmentId | null, scope: AuthEnvironmentScope) => {
50 const granted =
51 scope === AuthTerminalOperateScope
52 ? state.allowed
53 : scope === AuthOrchestrationOperateScope
54 ? state.editorAllowed
55 : true;
56 return id !== null && (id !== threadRef.environmentId || granted);
57 };
5858 return {
5959 readEnvironmentScope,
6060 useEnvironmentScope: (id: EnvironmentId | null, scope: AuthEnvironmentScope) =>
......@@ -79,6 +79,7 @@ let renderer: ReactTestRenderer | undefined;
7979
8080beforeEach(() => {
8181 state.allowed = true;
82 state.editorAllowed = true;
8283 state.listeners.clear();
8384 state.resize.mockReset().mockResolvedValue(AsyncResult.success(undefined));
8485 state.otherCommand.mockReset().mockResolvedValue(AsyncResult.success(undefined));
......@@ -106,7 +107,7 @@ afterEach(async () => {
106107 vi.unstubAllGlobals();
107108});
108109
109it("rechecks the target terminal grant when a retained surface callback reports a resize", async () => {
110async function renderViewport(cwd = "/repo") {
110111 await act(async () => {
111112 renderer = create(
112113 <TerminalViewport
......@@ -115,7 +116,7 @@ it("rechecks the target terminal grant when a retained surface callback reports
115116 threadId={threadRef.threadId}
116117 terminalId="terminal-1"
117118 terminalLabel="Terminal 1"
118 cwd="/repo"
119 cwd={cwd}
119120 onSessionExited={state.otherCommand}
120121 focusRequestId={0}
121122 autoFocus={false}
......@@ -127,6 +128,10 @@ it("rechecks the target terminal grant when a retained surface callback reports
127128 { createNodeMock: () => ({ closest: () => null, contains: () => false }) },
128129 );
129130 });
131}
132
133it("rechecks the target terminal grant when a retained surface callback reports a resize", async () => {
134 await renderViewport();
130135 expect(state.createSurface).toHaveBeenCalledOnce();
131136 const onResize = state.createSurface.mock.calls[0]![1].onResize;
132137 if (!onResize) throw new Error("The terminal did not register its resize callback.");
......@@ -150,4 +155,21 @@ it("rechecks the target terminal grant when a retained surface callback reports
150155 environmentId: threadRef.environmentId,
151156 input: { threadId: threadRef.threadId, terminalId: "terminal-1", cols: 100, rows: 30 },
152157 });
158
159 await act(async () => renderer!.unmount());
160 renderer = undefined;
161 onResize(120, 40);
162 expect(state.resize).toHaveBeenCalledTimes(2);
163});
164
165it("keeps host paths outside cwd available while enforcing the live editor grant", async () => {
166 await renderViewport("/Users/clo/repo");
167 const validatePath = state.createSurface.mock.calls[0]![1].validatePath!;
168 expect(validatePath(String.raw`C:\repo\src\file.ts`)).toBe(true);
169 expect(validatePath(String.raw`\\server\share\file.ts`)).toBe(true);
170 expect(validatePath("~/src/file.ts")).toBe(true);
171 state.editorAllowed = false;
172 expect(validatePath(String.raw`C:\repo\src\file.ts`)).toBe(false);
173 expect(validatePath(String.raw`\\server\share\file.ts`)).toBe(false);
174 expect(validatePath("~/src/file.ts")).toBe(false);
153175});
apps/web/src/components/ThreadTerminalDrawer.test.ts+5-71
......@@ -8,18 +8,15 @@ import {
88} from "@t3tools/client-runtime/state/terminal";
99
1010import {
11 shouldClearTerminalSelectionAction,
1211 shouldHandleTerminalExit,
1312 terminalContextMenuItems,
1413 synchronizeTerminalOutput,
1514 terminalSelectionLineRange,
16 terminalSelectionMenuItems,
1715 terminalThemeFromApp,
1816} from "./ThreadTerminalDrawer";
1917
2018describe("terminal selection menus", () => {
2119 it("omits Add to chat when the terminal has no chat target", () => {
22 expect(terminalSelectionMenuItems().map(({ id }) => id)).toEqual(["add-to-chat", "copy"]);
2320 expect(terminalContextMenuItems({ hasSelection: true }).map(({ id }) => id)).toEqual([
2421 "add-to-chat",
2522 "copy",
......@@ -28,9 +25,6 @@ describe("terminal selection menus", () => {
2825 "scroll-to-bottom",
2926 ]);
3027
31 expect(terminalSelectionMenuItems({ canAddToChat: false }).map(({ id }) => id)).toEqual([
32 "copy",
33 ]);
3428 expect(
3529 terminalContextMenuItems({ hasSelection: true, canAddToChat: false }).map(({ id }) => id),
3630 ).toEqual(["copy", "paste", "select-all", "scroll-to-bottom"]);
......@@ -59,34 +53,8 @@ describe("terminalThemeFromApp", () => {
5953 const root = { classList: { contains: () => false } };
6054 const body = {};
6155 const drawer = {};
62 let canvasColor = "#000";
63 const colors: Record<string, [number, number, number, number]> = {
64 "#000": [0, 0, 0, 255],
65 "#fff": [255, 255, 255, 255],
66 "#ddd": [221, 221, 221, 255],
67 "#111": [17, 17, 17, 255],
68 };
69
70 vi.stubGlobal("document", {
71 documentElement: root,
72 body,
73 querySelector: () => drawer,
74 createElement: () => ({
75 width: 0,
76 height: 0,
77 getContext: () => ({
78 clearRect: () => undefined,
79 fillRect: () => undefined,
80 get fillStyle() {
81 return canvasColor;
82 },
83 set fillStyle(value: string) {
84 canvasColor = value;
85 },
86 getImageData: () => ({ data: colors[canvasColor] ?? [0, 0, 0, 0] }),
87 }),
88 }),
89 });
56 vi.stubGlobal("document", { documentElement: root, body, querySelector: () => drawer });
57
9058 vi.stubGlobal("getComputedStyle", (element: object) => {
9159 const local = element === drawer;
9260 const values = local
......@@ -110,47 +78,13 @@ describe("terminalThemeFromApp", () => {
11078
11179 const theme = terminalThemeFromApp();
11280
113 expect(theme.background).toEqual({ r: 0, g: 0, b: 0 });
114 expect(theme.foreground).toEqual({ r: 255, g: 255, b: 255 });
115 expect(theme.cursor).toEqual({ r: 221, g: 221, b: 221 });
81 expect(theme.background).toBe("#000");
82 expect(theme.foreground).toBe("#fff");
83 expect(theme.cursor).toBe("#ddd");
11684 });
11785});
11886
11987describe("terminal selection actions", () => {
120 it("clears a pending or currently owned menu when the selection disappears", () => {
121 expect(
122 shouldClearTerminalSelectionAction({
123 actionPending: true,
124 openMenuRequestId: null,
125 currentRequestId: 4,
126 }),
127 ).toBe(true);
128 expect(
129 shouldClearTerminalSelectionAction({
130 actionPending: false,
131 openMenuRequestId: 4,
132 currentRequestId: 4,
133 }),
134 ).toBe(true);
135 });
136
137 it("does not let an old selection popup cancel its replacement right-click menu", () => {
138 expect(
139 shouldClearTerminalSelectionAction({
140 actionPending: false,
141 openMenuRequestId: 3,
142 currentRequestId: 4,
143 }),
144 ).toBe(false);
145 expect(
146 shouldClearTerminalSelectionAction({
147 actionPending: false,
148 openMenuRequestId: null,
149 currentRequestId: 4,
150 }),
151 ).toBe(false);
152 });
153
15488 it("uses Ghostty's physical screen range for visually wrapped selections", () => {
15589 expect(
15690 terminalSelectionLineRange({
apps/web/src/components/ThreadTerminalDrawer.tsx+60-226
......@@ -50,17 +50,18 @@ import { readTextFromClipboard, writeTextToClipboard } from "~/hooks/useCopyToCl
5050import { cn } from "~/lib/utils";
5151import { type TerminalContextSelection } from "~/lib/terminalContext";
5252import {
53 observeSelectionActions,
54 resolveSelectionActionPosition,
55 type SelectionActionPoint,
56} from "~/lib/selectionActions";
57import {
58 GhosttyTerminalSurface,
59 type GhosttyTerminalSurfaceOptions,
60} from "~/terminal/ghostty/surface";
61import { type GhosttyColor, type GhosttyTheme } from "~/terminal/ghostty/core";
53 TerminalSurface,
54 type TerminalSurfaceOptions,
55 type TerminalTheme,
56} from "~/terminal/TerminalSurface";
6257import { useOpenInPreferredEditor } from "../editorPreferences";
63import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks";
58import {
59 formatFilePathPosition,
60 parseFileUrlHref,
61 resolvePathLinkTarget,
62 splitFilePathPosition,
63} from "@t3tools/shared/fileLinks";
64import { safeDecodeURIComponent } from "@t3tools/shared/markdownLinks";
6465import { isTerminalUrl } from "../terminal-links";
6566import {
6667 isDiffToggleShortcut,
......@@ -109,12 +110,12 @@ function clampDrawerHeight(height: number, maxHeight = maxDrawerHeight()): numbe
109110 return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight);
110111}
111112
112function writeSystemMessage(terminal: GhosttyTerminalSurface, message: string): void {
113function writeSystemMessage(terminal: TerminalSurface, message: string): void {
113114 terminal.write(`\r\n[terminal] ${message}\r\n`);
114115}
115116
116117export function writeTerminalOutputUpdate(
117 terminal: Pick<GhosttyTerminalSurface, "resetAndWrite" | "write">,
118 terminal: Pick<TerminalSurface, "resetAndWrite" | "write">,
118119 update: TerminalOutputUpdate,
119120): void {
120121 if (update.type === "reset") {
......@@ -125,44 +126,16 @@ export function writeTerminalOutputUpdate(
125126}
126127
127128export function synchronizeTerminalOutput(
128 terminal: Pick<GhosttyTerminalSurface, "resetAndWrite" | "write" | "clearSelection">,
129 terminal: Pick<TerminalSurface, "resetAndWrite" | "write">,
129130 session: Pick<TerminalSessionState, "output" | "version">,
130131 cursor: TerminalOutputCursor,
131132): TerminalOutputCursor {
132133 if (session.version === 0) return cursor;
133134 const update = readTerminalOutputUpdate(session.output, cursor);
134135 writeTerminalOutputUpdate(terminal, update);
135 terminal.clearSelection();
136136 return update.cursor;
137137}
138138
139function parseTerminalColor(value: string, fallback: GhosttyColor): GhosttyColor {
140 if (typeof document === "undefined") return fallback;
141
142 const canvas = document.createElement("canvas");
143 canvas.width = 1;
144 canvas.height = 1;
145 const context = canvas.getContext("2d", { willReadFrequently: true });
146 if (!context) return fallback;
147
148 context.clearRect(0, 0, 1, 1);
149 context.fillStyle = value;
150 context.fillRect(0, 0, 1, 1);
151 const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data;
152 if (alpha === 0) return fallback;
153
154 return {
155 r: red ?? fallback.r,
156 g: green ?? fallback.g,
157 b: blue ?? fallback.b,
158 };
159}
160
161function hexToGhosttyColor(hex: string): GhosttyColor {
162 const value = Number.parseInt(hex.slice(1), 16);
163 return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
164}
165
166139// VS Code's default ANSI colors.
167140const TERMINAL_PALETTES = {
168141 light: [
......@@ -182,7 +155,7 @@ const TERMINAL_PALETTES = {
182155 "#bc05bc",
183156 "#0598bc",
184157 "#a5a5a5",
185 ].map(hexToGhosttyColor),
158 ],
186159 dark: [
187160 "#000000",
188161 "#cd3131",
......@@ -200,7 +173,7 @@ const TERMINAL_PALETTES = {
200173 "#d670d6",
201174 "#29b8db",
202175 "#e5e5e5",
203 ].map(hexToGhosttyColor),
176 ],
204177};
205178
206179function runtimeEnvSignature(runtimeEnv: Record<string, string> | undefined): string {
......@@ -235,7 +208,7 @@ function terminalFontOptions(family: string, size: number): { family?: string; s
235208 return trimmed.length > 0 ? { family: trimmed, size } : { size };
236209}
237210
238export function terminalThemeFromApp(mountElement?: HTMLElement | null): GhosttyTheme {
211export function terminalThemeFromApp(mountElement?: HTMLElement | null): TerminalTheme {
239212 const drawerSurface =
240213 mountElement?.closest("[data-thread-terminal-drawer]") ??
241214 document.querySelector("[data-thread-terminal-drawer]") ??
......@@ -282,18 +255,9 @@ export function terminalThemeFromApp(mountElement?: HTMLElement | null): Ghostty
282255 isDark ? "rgba(180, 203, 255, 0.25)" : "rgba(37, 63, 99, 0.2)",
283256 );
284257 return {
285 background: parseTerminalColor(
286 terminalBackground,
287 isDark ? { r: 14, g: 18, b: 24 } : { r: 255, g: 255, b: 255 },
288 ),
289 foreground: parseTerminalColor(
290 terminalForeground,
291 isDark ? { r: 237, g: 241, b: 247 } : { r: 28, g: 33, b: 41 },
292 ),
293 cursor: parseTerminalColor(
294 terminalCursor,
295 isDark ? { r: 180, g: 203, b: 255 } : { r: 38, g: 56, b: 78 },
296 ),
258 background: terminalBackground,
259 foreground: terminalForeground,
260 cursor: terminalCursor,
297261 palette: TERMINAL_PALETTES[isDark ? "dark" : "light"],
298262 selectionBackground: terminalSelection,
299263 };
......@@ -317,24 +281,6 @@ export type TerminalContextMenuAction =
317281 | "select-all"
318282 | "scroll-to-bottom";
319283
320/** Post-selection popup: available selection actions, always enabled. */
321export function terminalSelectionMenuItems(options?: {
322 canAddToChat?: boolean;
323}): ContextMenuItem<"add-to-chat" | "copy">[] {
324 return [
325 ...(options?.canAddToChat === false
326 ? []
327 : ([{ id: "add-to-chat", label: "Add to chat" }] satisfies ContextMenuItem<"add-to-chat">[])),
328 { id: "copy", label: "Copy" },
329 ];
330}
331
332/**
333 * Right-click menu for selection, clipboard, and local scrollback actions.
334 * Paste is always offered: the browser
335 * (and Electron's default editing menu) can only paste into an editable
336 * element, so a canvas terminal never gets a usable entry from them.
337 */
338284export function terminalContextMenuItems(options: {
339285 hasSelection: boolean;
340286 canAddToChat?: boolean;
......@@ -342,31 +288,16 @@ export function terminalContextMenuItems(options: {
342288}): ContextMenuItem<TerminalContextMenuAction>[] {
343289 const { hasSelection, canAddToChat = true } = options;
344290 return [
345 ...terminalSelectionMenuItems({ canAddToChat }).map((item) => ({
346 ...item,
347 disabled: !hasSelection,
348 })),
291 ...(canAddToChat
292 ? [{ id: "add-to-chat" as const, label: "Add to chat", disabled: !hasSelection }]
293 : []),
294 { id: "copy", label: "Copy", disabled: !hasSelection },
349295 { id: "paste", label: "Paste", ...(options.readOnly ? { disabled: true } : {}) },
350296 { id: "select-all", label: "Select all" },
351297 { id: "scroll-to-bottom", label: "Jump to latest" },
352298 ];
353299}
354300
355/**
356 * An empty selection change may only cancel a selection-action flow that is
357 * still current: a pending popup timer, or an open popup whose request id has
358 * not been superseded. A popup already superseded by a right-click keeps its
359 * menu promise unsettled for a moment; treating it as active would cancel the
360 * newer context-menu flow instead.
361 */
362export function shouldClearTerminalSelectionAction(options: {
363 actionPending: boolean;
364 openMenuRequestId: number | null;
365 currentRequestId: number;
366}): boolean {
367 return options.actionPending || options.openMenuRequestId === options.currentRequestId;
368}
369
370301export function shouldHandleTerminalExit(
371302 current: TerminalSessionState["status"],
372303 synchronized: TerminalSessionState["status"],
......@@ -427,14 +358,13 @@ export function TerminalViewport({
427358 keybindings,
428359}: TerminalViewportProps) {
429360 const containerRef = useRef<HTMLDivElement>(null);
430 const terminalRef = useRef<GhosttyTerminalSurface | null>(null);
361 const terminalRef = useRef<TerminalSurface | null>(null);
431362 const visibleRef = useRef(visible);
432363 const environmentId = threadRef.environmentId;
433364 const canOperateTerminal = useEnvironmentScope(environmentId, AuthTerminalOperateScope);
434365 const hasTerminalWriteAccess = useEffectEvent(() =>
435366 readEnvironmentScope(environmentId, AuthTerminalOperateScope),
436367 );
437 const canOpenHostEditor = useEnvironmentScope(environmentId, AuthOrchestrationOperateScope);
438368 const canActivateTerminalLink = useEffectEvent(
439369 (text: string) =>
440370 isTerminalUrl(text) || readEnvironmentScope(environmentId, AuthOrchestrationOperateScope),
......@@ -455,11 +385,7 @@ export function TerminalViewport({
455385 reportFailure: false,
456386 });
457387 const hasHandledExitRef = useRef(false);
458 const selectionActionRequestIdRef = useRef(0);
459 // Holds the request id of the selection popup currently on screen, so a
460 // popup that was superseded (but whose menu promise has not settled yet)
461 // cannot be mistaken for the active flow.
462 const openSelectionMenuRequestIdRef = useRef<number | null>(null);
388 const contextMenuRequestIdRef = useRef(0);
463389 const keybindingsRef = useRef(keybindings);
464390 const runtimeEnvKey = useMemo(() => runtimeEnvSignature(runtimeEnv), [runtimeEnv]);
465391 const handleSessionExited = useEffectEvent(() => {
......@@ -518,7 +444,7 @@ export function TerminalViewport({
518444 const outputCursorRef = useRef<TerminalOutputCursor>(INITIAL_TERMINAL_OUTPUT_CURSOR);
519445 const synchronizedStatusRef = useRef<TerminalSessionState["status"]>("closed");
520446 const synchronizeTerminalStatus = useEffectEvent(
521 (terminal: GhosttyTerminalSurface, status: TerminalSessionState["status"], version: number) => {
447 (terminal: TerminalSurface, status: TerminalSessionState["status"], version: number) => {
522448 const synchronized = synchronizedStatusRef.current;
523449 if (version > 0 && status === "running") {
524450 hasHandledExitRef.current = false;
......@@ -559,11 +485,6 @@ export function TerminalViewport({
559485 if (terminalRef.current) terminalRef.current.input.readOnly = !canOperateTerminal;
560486 }, [canOperateTerminal]);
561487
562 // A grant can change while the pointer remains over a link.
563 useEffect(() => {
564 terminalRef.current?.refreshLinkActivation();
565 }, [canOpenHostEditor]);
566
567488 useEffect(() => {
568489 if (resizeSessionGeneration === null) return;
569490 // The first fit can finish before authorization or the attach snapshot.
......@@ -590,32 +511,32 @@ export function TerminalViewport({
590511 const localApi = readLocalApi();
591512 let cancelled = false;
592513 let teardown: (() => void) | null = null;
593 let setupTerminal: GhosttyTerminalSurface | null = null;
514 let setupTerminal: TerminalSurface | null = null;
594515 let setupCleanups: Array<() => void> = [];
595 let selectionActions: ReturnType<typeof observeSelectionActions> | null = null;
596516
597517 const setup = async (): Promise<(() => void) | null> => {
598518 const setupFont = terminalFontRef.current;
599 const terminalOptions: GhosttyTerminalSurfaceOptions = {
519 const terminalOptions: TerminalSurfaceOptions = {
600520 theme: terminalThemeFromApp(mount),
601521 font: terminalFontOptions(setupFont.family, setupFont.size),
602522 get visible() {
603523 return visibleRef.current;
604524 },
605 onData: (data) => handleData(data),
606 onResize: (cols, rows) => void resizeTerminal(cols, rows),
607 onSelectionChange: () => handleSelectionChange(),
525 onData: (data) => {
526 if (!cancelled) handleData(data);
527 },
528 onResize: (cols, rows) => {
529 if (!cancelled) void resizeTerminal(cols, rows);
530 },
608531 beforeKey: (event) => handleBeforeKey(event),
609 onLinkActivate: (text, event) => handleLinkActivate(text, event),
532 onLinkActivate: (text) => handleLinkActivate(text),
610533 canActivateLink: (text) => canActivateTerminalLink(text),
611 // The surface listens from construction, so a right-click can land
612 // while `create` is still awaiting WASM — before the handler below it
613 // exists. The ref is only assigned once that setup has run.
534 validatePath: (path) => canActivateTerminalLink(resolvePathLinkTarget(path, cwd)),
614535 onContextMenu: (event) => {
615536 if (terminalRef.current) void showTerminalContextMenu(event);
616537 },
617538 };
618 const terminal = await GhosttyTerminalSurface.create(mount, terminalOptions);
539 const terminal = await TerminalSurface.create(mount, terminalOptions);
619540 if (cancelled) {
620541 terminal.dispose();
621542 return null;
......@@ -660,23 +581,7 @@ export function TerminalViewport({
660581 terminal.focus();
661582 }
662583
663 const dismissSelectionAction = (supersede = false) => {
664 const ownsMenu =
665 openSelectionMenuRequestIdRef.current === selectionActionRequestIdRef.current;
666 // Passive cancellation must not invalidate a newer right-click flow.
667 if (supersede || ownsMenu) selectionActionRequestIdRef.current += 1;
668 if (ownsMenu) void localApi?.contextMenu.close();
669 };
670 const clearSelectionAction = () => {
671 selectionActions?.cancel();
672 dismissSelectionAction(true);
673 };
674 setupCleanups.push(clearSelectionAction);
675
676 const readSelectionAction = (
677 pointer: SelectionActionPoint | null = null,
678 ): {
679 position: { x: number; y: number };
584 const readSelectionAction = (): {
680585 clipboardText: string;
681586 selection: TerminalContextSelection;
682587 } | null => {
......@@ -692,15 +597,7 @@ export function TerminalViewport({
692597 return null;
693598 }
694599 const { lineStart, lineEnd } = terminalSelectionLineRange(selectionPosition);
695 const bounds = mountElement.getBoundingClientRect();
696 const position = resolveSelectionActionPosition({
697 bounds,
698 selectionRect: activeTerminal.getSelectionEndClientRect(),
699 pointer,
700 viewport: { width: window.innerWidth, height: window.innerHeight },
701 });
702600 return {
703 position,
704601 clipboardText: selectionText,
705602 selection: {
706603 terminalId,
......@@ -718,10 +615,9 @@ export function TerminalViewport({
718615 terminalRef.current?.focus();
719616 };
720617
721 // A selection-action flow that was superseded while its async work ran
722 // must go silent: no error message, no focus steal.
618 // A superseded menu must not report errors or reclaim focus.
723619 const reportIfCurrent = (requestId: number, error: unknown, fallback: string) => {
724 if (requestId !== selectionActionRequestIdRef.current) return;
620 if (requestId !== contextMenuRequestIdRef.current) return;
725621 const activeTerminal = terminalRef.current;
726622 if (activeTerminal) {
727623 writeSystemMessage(activeTerminal, error instanceof Error ? error.message : fallback);
......@@ -729,7 +625,7 @@ export function TerminalViewport({
729625 };
730626
731627 const focusIfCurrent = (requestId: number) => {
732 if (requestId === selectionActionRequestIdRef.current) {
628 if (requestId === contextMenuRequestIdRef.current) {
733629 terminalRef.current?.focus();
734630 }
735631 };
......@@ -748,13 +644,9 @@ export function TerminalViewport({
748644 const activeTerminal = terminalRef.current;
749645 if (!activeTerminal) return;
750646 try {
751 // The surface owns the read so it can claim the paste race before it
752 // starts: a paste shortcut fired while the menu read is in flight
753 // supersedes this paste instead of landing alongside it.
754 await activeTerminal.pasteFromClipboard(
755 () => readTextFromClipboard("terminal input"),
756 () => requestId === selectionActionRequestIdRef.current,
757 );
647 const text = await readTextFromClipboard("terminal input");
648 if (requestId === contextMenuRequestIdRef.current && hasTerminalWriteAccess())
649 activeTerminal.paste(text);
758650 } catch (error) {
759651 reportIfCurrent(requestId, error, "Unable to read the clipboard");
760652 return;
......@@ -764,14 +656,9 @@ export function TerminalViewport({
764656
765657 const showTerminalContextMenu = async (event: MouseEvent) => {
766658 if (!localApi || !terminalRef.current) return;
767 // Own the gesture before anything async: leaving the default alive lets
768 // the browser (or Electron's editing menu) answer with a Paste entry
769 // that is permanently disabled over the terminal canvas.
770659 event.preventDefault();
771 // A right-click supersedes a selection popup that is pending or open.
772 clearSelectionAction();
660 const requestId = ++contextMenuRequestIdRef.current;
773661 const selectionAction = readSelectionAction();
774 const requestId = selectionActionRequestIdRef.current;
775662 let clicked: TerminalContextMenuAction | null;
776663 try {
777664 clicked = await localApi.contextMenu.show(
......@@ -787,7 +674,7 @@ export function TerminalViewport({
787674 focusIfCurrent(requestId);
788675 return;
789676 }
790 if (requestId !== selectionActionRequestIdRef.current || clicked === null) {
677 if (requestId !== contextMenuRequestIdRef.current || clicked === null) {
791678 return;
792679 }
793680 switch (clicked) {
......@@ -813,44 +700,6 @@ export function TerminalViewport({
813700 }
814701 };
815702
816 const showSelectionAction = async (pointer: SelectionActionPoint | null) => {
817 if (!localApi) {
818 clearSelectionAction();
819 return;
820 }
821 if (openSelectionMenuRequestIdRef.current === selectionActionRequestIdRef.current) {
822 return;
823 }
824 const nextAction = readSelectionAction(pointer);
825 if (!nextAction) {
826 clearSelectionAction();
827 return;
828 }
829 const requestId = ++selectionActionRequestIdRef.current;
830 openSelectionMenuRequestIdRef.current = requestId;
831 const clicked = await localApi.contextMenu
832 .show(
833 terminalSelectionMenuItems({ canAddToChat: canAddSelectionToChat() }),
834 nextAction.position,
835 )
836 .finally(() => {
837 if (openSelectionMenuRequestIdRef.current === requestId) {
838 openSelectionMenuRequestIdRef.current = null;
839 }
840 });
841 if (requestId !== selectionActionRequestIdRef.current || clicked === null) {
842 return;
843 }
844 switch (clicked) {
845 case "add-to-chat":
846 if (canAddSelectionToChat()) addSelectionToChat(nextAction.selection);
847 return;
848 case "copy":
849 await copySelection(nextAction.clipboardText, requestId);
850 return;
851 }
852 };
853
854703 const sendTerminalInput = async (data: string, fallbackError: string) => {
855704 if (!hasTerminalWriteAccess()) return;
856705 const activeTerminal = terminalRef.current;
......@@ -904,7 +753,7 @@ export function TerminalViewport({
904753 return false;
905754 }
906755
907 function handleLinkActivate(text: string, event: MouseEvent): void {
756 function handleLinkActivate(text: string): void {
908757 if (!canActivateTerminalLink(text)) return;
909758 const latestTerminal = terminalRef.current;
910759 if (!latestTerminal) return;
......@@ -930,7 +779,7 @@ export function TerminalViewport({
930779 threadRef,
931780 openPreview,
932781 fallbackToBrowser,
933 forceBrowser: event.metaKey || event.ctrlKey,
782 forceBrowser: false,
934783 }).catch((error: unknown) => {
935784 toastManager.add(
936785 stackedThreadToast({
......@@ -942,7 +791,13 @@ export function TerminalViewport({
942791 });
943792 return;
944793 }
945 const target = resolvePathLinkTarget(text, cwd);
794 const fileUrl = parseFileUrlHref(text);
795 const path = fileUrl
796 ? formatFilePathPosition(
797 splitFilePathPosition(safeDecodeURIComponent(fileUrl.path), fileUrl.hash),
798 )
799 : text;
800 const target = resolvePathLinkTarget(path, cwd);
946801 void (async () => {
947802 const result = await openTerminalPath(target);
948803 if (result._tag === "Success" || isAtomCommandInterrupted(result)) {
......@@ -969,28 +824,6 @@ export function TerminalViewport({
969824 })();
970825 }
971826
972 function handleSelectionChange(): void {
973 if (terminalRef.current?.hasSelection()) {
974 return;
975 }
976 const shouldClear = shouldClearTerminalSelectionAction({
977 actionPending: selectionActions?.pending ?? false,
978 openMenuRequestId: openSelectionMenuRequestIdRef.current,
979 currentRequestId: selectionActionRequestIdRef.current,
980 });
981 if (!shouldClear) return;
982 clearSelectionAction();
983 }
984
985 selectionActions = observeSelectionActions({
986 element: mount,
987 onSelection: (pointer) => {
988 void showSelectionAction(pointer);
989 },
990 onDismiss: (reason) => dismissSelectionAction(reason === "interaction"),
991 });
992 setupCleanups.push(() => selectionActions?.dispose());
993
994827 const themeObserver = new MutationObserver(() => {
995828 const activeTerminal = terminalRef.current;
996829 if (!activeTerminal) return;
......@@ -1017,6 +850,7 @@ export function TerminalViewport({
1017850 setupCleanups = [];
1018851 setupTerminal = null;
1019852 return () => {
853 contextMenuRequestIdRef.current += 1;
1020854 for (const cleanup of cleanups.toReversed()) cleanup();
1021855 if (terminalRef.current === terminal) terminalRef.current = null;
1022856 terminal.dispose();
......@@ -1109,6 +943,7 @@ export function TerminalViewport({
1109943 <div
1110944 ref={containerRef}
1111945 tabIndex={-1}
946 aria-label={terminalLabel}
1112947 className="relative h-full w-full overflow-hidden bg-(--terminal-background)"
1113948 />
1114949 );
......@@ -1419,14 +1254,13 @@ export default function ThreadTerminalDrawer({
14191254 const confirmCloseTerminal = useCallback(
14201255 (terminalId: string) => {
14211256 if (!canOperateTerminal) return;
1422 const label = terminalLabelById.get(terminalId) ?? getTerminalLabel(terminalId);
1423 void confirmTerminalClose([label]).then((confirmed) => {
1257 void confirmTerminalClose(threadRef, [terminalId]).then((confirmed) => {
14241258 if (confirmed && readEnvironmentScope(threadRef.environmentId, AuthTerminalOperateScope)) {
14251259 onCloseTerminal(terminalId);
14261260 }
14271261 });
14281262 },
1429 [canOperateTerminal, onCloseTerminal, terminalLabelById, threadRef.environmentId],
1263 [canOperateTerminal, onCloseTerminal, threadRef],
14301264 );
14311265
14321266 useEffect(() => {
apps/web/src/components/chat/PanelLayoutControls.tsx+3-3
......@@ -90,7 +90,7 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({
9090 className="shrink-0 [-webkit-app-region:no-drag]"
9191 pressed={terminalOpen}
9292 onPressedChange={onToggleTerminal}
93 aria-label="Toggle terminal drawer"
93 aria-label="Toggle terminal"
9494 variant="ghost"
9595 size="sm"
9696 disabled={!terminalAvailable}
......@@ -100,8 +100,8 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({
100100 </TooltipTrigger>
101101 <TooltipPopup side="bottom">
102102 {terminalAvailable
103 ? `Toggle terminal drawer${terminalShortcutLabel ? ` (${terminalShortcutLabel})` : ""}`
104 : "Terminal drawer is unavailable"}
103 ? `Toggle terminal${terminalShortcutLabel ? ` (${terminalShortcutLabel})` : ""}`
104 : "Terminal is unavailable"}
105105 </TooltipPopup>
106106 </Tooltip>
107107 ) : null}
apps/web/src/components/settings/ProviderAuthTerminal.test.tsx created+87
......@@ -0,0 +1,87 @@
1import { act, create, type ReactTestRenderer } from "react-test-renderer";
2import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
3
4const state = vi.hoisted(() => ({
5 create: vi.fn(),
6 write: vi.fn(),
7 resetAndWrite: vi.fn(),
8 dispose: vi.fn(),
9}));
10vi.mock("../../terminal/TerminalSurface", () => ({ TerminalSurface: { create: state.create } }));
11vi.mock("../ThreadTerminalDrawer", () => ({ terminalThemeFromApp: () => ({}) }));
12
13import ProviderAuthTerminal from "./ProviderAuthTerminal";
14
15let renderer: ReactTestRenderer | undefined;
16
17beforeEach(() => {
18 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
19 state.create.mockReset();
20 state.write.mockReset();
21 state.resetAndWrite.mockReset();
22 state.dispose.mockReset();
23});
24
25afterEach(async () => {
26 await act(async () => renderer?.unmount());
27 renderer = undefined;
28 vi.unstubAllGlobals();
29});
30
31it("replays the latest sign-in output after async setup, then appends live deltas", async () => {
32 let finish: (() => void) | undefined;
33 state.create.mockReturnValue(
34 new Promise((resolve) => {
35 finish = () => resolve(state);
36 }),
37 );
38 const onResponse = vi.fn();
39 await act(async () => {
40 renderer = create(
41 <ProviderAuthTerminal output="old" outputOffset={3} onResponse={onResponse} />,
42 {
43 createNodeMock: () => ({}),
44 },
45 );
46 });
47 await act(async () => {
48 renderer!.update(
49 <ProviderAuthTerminal output="old🌱" outputOffset={5} onResponse={onResponse} />,
50 );
51 });
52 await act(async () => finish!());
53 expect(state.resetAndWrite).toHaveBeenCalledExactlyOnceWith("old🌱");
54 expect(state.write).not.toHaveBeenCalled();
55 await act(async () => {
56 renderer!.update(
57 <ProviderAuthTerminal output="old🌱next" outputOffset={9} onResponse={onResponse} />,
58 );
59 });
60 expect(state.write).toHaveBeenCalledExactlyOnceWith("next");
61 await act(async () => {
62 renderer!.update(
63 <ProviderAuthTerminal output="fresh" outputOffset={2} onResponse={onResponse} />,
64 );
65 });
66 expect(state.resetAndWrite).toHaveBeenLastCalledWith("fresh");
67});
68
69it("disposes a terminal whose setup completes after sign-in closes", async () => {
70 let finish: (() => void) | undefined;
71 state.create.mockReturnValue(
72 new Promise((resolve) => {
73 finish = () => resolve(state);
74 }),
75 );
76 await act(async () => {
77 renderer = create(<ProviderAuthTerminal output="hello" onResponse={vi.fn()} />, {
78 createNodeMock: () => ({}),
79 });
80 });
81 await act(async () => renderer!.unmount());
82 renderer = undefined;
83 await act(async () => finish!());
84 expect(state.dispose).toHaveBeenCalledOnce();
85 expect(state.resetAndWrite).not.toHaveBeenCalled();
86 expect(state.write).not.toHaveBeenCalled();
87});
apps/web/src/components/settings/ProviderAuthTerminal.tsx+4-5
......@@ -1,6 +1,6 @@
11import type { ProviderAuthResponse } from "@t3tools/contracts";
22import { useEffect, useRef, useState } from "react";
3import { GhosttyTerminalSurface } from "../../terminal/ghostty/surface";
3import { TerminalSurface } from "../../terminal/TerminalSurface";
44import { ensureLocalApi } from "../../localApi";
55import { terminalThemeFromApp } from "../ThreadTerminalDrawer";
66
......@@ -15,7 +15,7 @@ export default function ProviderAuthTerminal({
1515 readonly onResponse: (response: Extract<ProviderAuthResponse, { type: "terminal" }>) => void;
1616}) {
1717 const mount = useRef<HTMLDivElement>(null);
18 const surface = useRef<GhosttyTerminalSurface | null>(null);
18 const surface = useRef<TerminalSurface | null>(null);
1919 const latest = useRef({ output, offset: outputOffset ?? output.length, onResponse });
2020 const written = useRef(0);
2121 const [error, setError] = useState<string | null>(null);
......@@ -32,13 +32,12 @@ export default function ProviderAuthTerminal({
3232 const element = mount.current;
3333 if (!element) return;
3434 let disposed = false;
35 void GhosttyTerminalSurface.create(element, {
35 void TerminalSurface.create(element, {
3636 theme: terminalThemeFromApp(element),
3737 font: { size: 13 },
3838 onData: (data) => latest.current.onResponse({ type: "terminal", data }),
3939 onResize: (cols, rows) =>
4040 latest.current.onResponse({ type: "terminal", data: "", size: { cols, rows } }),
41 onSelectionChange: () => {},
4241 beforeKey: (event) => event.key !== "Tab",
4342 onLinkActivate: (url) => {
4443 if (/^https?:\/\//i.test(url))
......@@ -53,7 +52,7 @@ export default function ProviderAuthTerminal({
5352 return;
5453 }
5554 surface.current = terminal;
56 terminal.write(latest.current.output);
55 terminal.resetAndWrite(latest.current.output);
5756 written.current = latest.current.offset;
5857 })
5958 .catch(() => setError("Could not load the sign-in terminal. Cancel and retry sign-in."));
apps/web/src/components/settings/SettingsFontPreviews.tsx+6-15
......@@ -7,13 +7,7 @@ import { useTheme } from "../../hooks/useTheme";
77import { DISCONNECTED_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder";
88import { resolveDiffThemeName, type DiffThemeName } from "../../lib/diffRendering";
99import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting";
10import { GhosttyTerminalSurface } from "~/terminal/ghostty/surface";
11
12// The font previews are the real surfaces, not lookalikes: the composer's
13// Tiptap editor, the diff panel's file diff, and the Ghostty canvas
14// renderer. Each already consumes the appearance font tokens (or, for the
15// terminal, the settings passed down as props), so what the row shows is
16// exactly what the app renders.
10import { TerminalSurface } from "~/terminal/TerminalSurface";
1711
1812const EMPTY_SKILLS: ReadonlyArray<never> = [];
1913
......@@ -22,7 +16,7 @@ const EMPTY_SKILLS: ReadonlyArray<never> = [];
2216// text and pills exactly as the real composer draws them.
2317const PROMPT_PREVIEW_TEXT =
2418 "Use $frontend-design to fix the flaky test in " +
25 "[surface.test.ts](apps/web/src/terminal/ghostty/surface.test.ts) and align the header with " +
19 "[TerminalSurface.test.ts](apps/web/src/terminal/TerminalSurface.test.ts) and align the header with " +
2620 "[SettingsPanels.tsx](apps/web/src/components/settings/SettingsPanels.tsx) before shipping.";
2721
2822function noop() {}
......@@ -174,14 +168,12 @@ function previewTerminalFont(family: string, size: number): { family?: string; s
174168}
175169
176170/**
177 * The real Ghostty canvas renderer against a local echo loop instead of a
178 * PTY: keys print, Enter starts a new prompt line, Backspace erases. That
179 * exercises the same glyph atlas, cell metrics, and monospace gate the
180 * terminal drawer uses.
171 * The DOM terminal against a local echo loop: keys print, Enter starts a new
172 * prompt line, and Backspace erases.
181173 */
182174export function TerminalFontPreview({ family, size }: { family: string; size: number }) {
183175 const mountRef = useRef<HTMLDivElement>(null);
184 const surfaceRef = useRef<GhosttyTerminalSurface | null>(null);
176 const surfaceRef = useRef<TerminalSurface | null>(null);
185177 const fontRef = useRef({ family, size });
186178 const { theme, resolvedTheme } = useTheme();
187179
......@@ -234,12 +226,11 @@ export function TerminalFontPreview({ family, size }: { family: string; size: nu
234226 lineLength += printable.length;
235227 };
236228
237 void GhosttyTerminalSurface.create(mount, {
229 void TerminalSurface.create(mount, {
238230 theme: terminalThemeFromApp(mount),
239231 font: previewTerminalFont(fontRef.current.family, fontRef.current.size),
240232 onData: echo,
241233 onResize: noop,
242 onSelectionChange: noop,
243234 // Tab keeps walking the settings page instead of feeding the echo loop.
244235 beforeKey: (event) => event.key !== "Tab",
245236 onLinkActivate: noop,
apps/web/src/components/settings/SettingsPanels.tsx+35
......@@ -1,4 +1,5 @@
11import { performDesktopUpdate } from "../desktopUpdate.actions";
2import { useTerminalPlacement } from "~/hooks/useTerminalPlacement";
23import { SettingsGroup } from "./SettingsGroup";
34import { useScopedSettingsWriteAllowed } from "./useScopedSettings";
45import { Spinner } from "~/components/ui/spinner";
......@@ -2123,6 +2124,7 @@ function LegacyFeaturesSection() {
21232124}
21242125
21252126export function GeneralSettingsPanel() {
2127 const [terminalPlacement, setTerminalPlacement] = useTerminalPlacement();
21262128 const modifierLabel = isMacPlatform(navigator.platform) ? "⌘" : "Ctrl";
21272129 const sendShortcutOptions = [
21282130 { value: "enter", label: "Enter" },
......@@ -2660,6 +2662,39 @@ export function GeneralSettingsPanel() {
26602662 }
26612663 />
26622664
2665 <SettingsRow
2666 {...searchableSetting("terminal-position")}
2667 resetAction={
2668 terminalPlacement !== "right" ? (
2669 <SettingResetButton
2670 label="terminal position"
2671 onClick={() => setTerminalPlacement("right")}
2672 />
2673 ) : null
2674 }
2675 description="Where new terminal sessions open. Existing sessions stay in their panel."
2676 control={
2677 <Select
2678 value={terminalPlacement}
2679 onValueChange={(value) => {
2680 if (value === "right" || value === "bottom") setTerminalPlacement(value);
2681 }}
2682 >
2683 <SelectTrigger size="sm" className="w-full sm:w-40" aria-label="Terminal position">
2684 <SelectValue>{terminalPlacement === "right" ? "Right" : "Bottom"}</SelectValue>
2685 </SelectTrigger>
2686 <SelectPopup align="end" alignItemWithTrigger={false}>
2687 <SelectItem hideIndicator value="right">
2688 Right
2689 </SelectItem>
2690 <SelectItem hideIndicator value="bottom">
2691 Bottom
2692 </SelectItem>
2693 </SelectPopup>
2694 </Select>
2695 }
2696 />
2697
26632698 <SettingsRow
26642699 {...searchableSetting("proactive-panels")}
26652700 description="Open linked pull requests first. Otherwise, open Changes for edits to at least 3 files or 50 lines."
apps/web/src/components/settings/settingsSearch.ts+6
......@@ -429,6 +429,12 @@ export const SETTINGS_SEARCH_ITEMS = [
429429 to: "/settings/general",
430430 searchTerms: ["stacked split side by side unified inline view"],
431431 },
432 {
433 id: "terminal-position",
434 title: "Terminal position",
435 to: "/settings/general",
436 searchTerms: ["right bottom dock placement panel shell"],
437 },
432438 {
433439 id: "proactive-panels",
434440 title: "Proactive panels",
apps/web/src/hooks/useTerminalPlacement.ts created+14
......@@ -0,0 +1,14 @@
1import * as Schema from "effect/Schema";
2
3import { getLocalStorageItem, useLocalStorage } from "./useLocalStorage";
4
5const TERMINAL_PLACEMENT_STORAGE_KEY = "t3code.terminalPlacement";
6const TerminalPlacement = Schema.Literals(["right", "bottom"]);
7
8export function useTerminalPlacement() {
9 return useLocalStorage(TERMINAL_PLACEMENT_STORAGE_KEY, "right", TerminalPlacement);
10}
11
12export function getTerminalPlacement() {
13 return getLocalStorageItem(TERMINAL_PLACEMENT_STORAGE_KEY, TerminalPlacement) ?? "right";
14}
apps/web/src/lib/revealThreadTerminal.test.ts+67-4
......@@ -1,6 +1,6 @@
11import { scopeThreadRef } from "@t3tools/client-runtime/environment";
22import { EnvironmentId, ThreadId } from "@t3tools/contracts";
3import { beforeEach, expect, it } from "vite-plus/test";
3import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
44
55import { selectThreadRightPanelState, useRightPanelStore } from "../rightPanelStore";
66import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore";
......@@ -9,7 +9,13 @@ import { revealThreadTerminal } from "./revealThreadTerminal";
99const threadRef = scopeThreadRef(EnvironmentId.make("local"), ThreadId.make("settled-thread"));
1010const remoteRef = scopeThreadRef(EnvironmentId.make("remote"), threadRef.threadId);
1111
12const preferences = new Map<string, string>();
13
1214beforeEach(() => {
15 preferences.clear();
16 vi.stubGlobal("window", {
17 localStorage: { getItem: (key: string) => preferences.get(key) ?? null },
18 });
1319 useTerminalUiStateStore.setState({
1420 terminalUiStateByThreadKey: {},
1521 suppressedTerminalIdsByThreadKey: {},
......@@ -17,7 +23,26 @@ beforeEach(() => {
1723 useRightPanelStore.setState({ byThreadKey: {} });
1824});
1925
20it("reveals a server-created setup terminal without adding a default shell", () => {
26afterEach(() => vi.unstubAllGlobals());
27
28it("reveals a server-created setup terminal on the right without adding a default shell", () => {
29 revealThreadTerminal(threadRef, "setup-install");
30 expect(
31 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef),
32 ).toMatchObject({
33 isOpen: true,
34 surfaces: [{ terminalIds: ["setup-install"], activeTerminalId: "setup-install" }],
35 });
36 expect(
37 selectThreadTerminalUiState(
38 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
39 threadRef,
40 ).terminalIds,
41 ).toEqual([]);
42});
43
44it("honors an explicit bottom preference for an unowned terminal", () => {
45 preferences.set("t3code.terminalPlacement", '"bottom"');
2146 revealThreadTerminal(threadRef, "setup-install");
2247 expect(
2348 selectThreadTerminalUiState(
......@@ -27,8 +52,24 @@ it("reveals a server-created setup terminal without adding a default shell", ()
2752 ).toMatchObject({
2853 terminalOpen: true,
2954 terminalIds: ["setup-install"],
30 activeTerminalId: "setup-install",
3155 });
56 expect(
57 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces,
58 ).toEqual([]);
59});
60
61it("preserves an existing drawer session when the default becomes right", () => {
62 useTerminalUiStateStore.getState().ensureTerminal(threadRef, "setup-install", { open: false });
63 revealThreadTerminal(threadRef, "setup-install");
64 expect(
65 selectThreadTerminalUiState(
66 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
67 threadRef,
68 ),
69 ).toMatchObject({ terminalOpen: true, terminalIds: ["setup-install"] });
70 expect(
71 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces,
72 ).toEqual([]);
3273});
3374
3475it("reopens the existing panel and selects a running terminal in its split", () => {
......@@ -64,9 +105,31 @@ it("reopens the existing panel and selects a running terminal in its split", ()
64105});
65106
66107it("reveals terminals in their own environment without changing another environment's drawer", () => {
108 useTerminalUiStateStore.getState().ensureTerminal(threadRef, "default", { open: false });
67109 revealThreadTerminal(threadRef, "default");
68110 revealThreadTerminal(remoteRef, "setup-install");
69111 const state = useTerminalUiStateStore.getState().terminalUiStateByThreadKey;
70112 expect(selectThreadTerminalUiState(state, threadRef).terminalIds).toEqual(["default"]);
71 expect(selectThreadTerminalUiState(state, remoteRef).terminalIds).toEqual(["setup-install"]);
113 expect(selectThreadTerminalUiState(state, remoteRef).terminalIds).toEqual([]);
114 expect(
115 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, remoteRef).surfaces,
116 ).toMatchObject([{ terminalIds: ["setup-install"] }]);
117});
118
119it("adds and activates a terminal peer tab in the existing browser sidebar without duplicating it", () => {
120 const panels = useRightPanelStore.getState();
121 panels.openBrowser(threadRef, "existing-browser");
122 revealThreadTerminal(threadRef, "setup-install");
123 revealThreadTerminal(threadRef, "setup-install");
124 const sidebar = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef);
125 expect(sidebar.isOpen).toBe(true);
126 expect(sidebar.activeSurfaceId).toBe("terminal:setup-install");
127 expect(sidebar.surfaces.map((surface) => surface.kind)).toEqual(["preview", "terminal"]);
128 expect(sidebar.surfaces[0]).toMatchObject({ resourceId: "existing-browser" });
129 expect(
130 selectThreadTerminalUiState(
131 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
132 threadRef,
133 ).terminalIds,
134 ).toEqual([]);
72135});
apps/web/src/lib/revealThreadTerminal.ts+9-2
......@@ -1,7 +1,8 @@
1import { getTerminalPlacement } from "~/hooks/useTerminalPlacement";
12import type { ScopedThreadRef } from "@t3tools/contracts";
23
34import { selectThreadRightPanelState, useRightPanelStore } from "../rightPanelStore";
4import { useTerminalUiStateStore } from "../terminalUiStateStore";
5import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore";
56
67/** Reveal an existing terminal in the panel that owns it, without opening another PTY. */
78export function revealThreadTerminal(threadRef: ScopedThreadRef, terminalId: string) {
......@@ -14,7 +15,13 @@ export function revealThreadTerminal(threadRef: ScopedThreadRef, terminalId: str
1415 panels.activateSurface(threadRef, surface.id);
1516 return;
1617 }
17 useTerminalUiStateStore.getState().ensureTerminal(threadRef, terminalId, {
18 const terminalUi = useTerminalUiStateStore.getState();
19 const drawer = selectThreadTerminalUiState(terminalUi.terminalUiStateByThreadKey, threadRef);
20 if (!drawer.terminalIds.includes(terminalId) && getTerminalPlacement() === "right") {
21 panels.openTerminal(threadRef, terminalId);
22 return;
23 }
24 terminalUi.ensureTerminal(threadRef, terminalId, {
1825 open: true,
1926 active: true,
2027 });
apps/web/src/lib/terminalCloseConfirm.test.ts+152-39
......@@ -1,65 +1,145 @@
1import { EnvironmentId, ThreadId, type TerminalSummary } from "@t3tools/contracts";
12import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
23
3const { confirmMock, readLocalApiMock } = vi.hoisted(() => {
4 const confirmMock = vi.fn<(message: string, options?: unknown) => Promise<boolean>>();
5 const readLocalApiMock = vi.fn<
6 () =>
7 | {
8 dialogs: { confirm: (message: string, options?: unknown) => Promise<boolean> };
9 }
10 | undefined
11 >();
12 return { confirmMock, readLocalApiMock };
13});
4const { confirmMock, readLocalApiMock, metadataSnapshotMock, executeAtomQueryMock, registry } =
5 vi.hoisted(() => ({
6 confirmMock: vi.fn<(message: string, options?: unknown) => Promise<boolean>>(),
7 readLocalApiMock: vi.fn(),
8 metadataSnapshotMock: vi.fn(() => "metadata-snapshot"),
9 executeAtomQueryMock: vi.fn(),
10 registry: {},
11 }));
1412
15vi.mock("~/localApi", () => ({
16 readLocalApi: () => readLocalApiMock(),
13vi.mock("~/localApi", () => ({ readLocalApi: readLocalApiMock }));
14vi.mock("~/rpc/atomRegistry", () => ({ appAtomRegistry: registry }));
15vi.mock("~/state/terminal", () => ({
16 terminalEnvironment: { metadataSnapshot: metadataSnapshotMock },
17}));
18vi.mock("@t3tools/client-runtime/state/runtime", () => ({
19 executeAtomQuery: executeAtomQueryMock,
1720}));
1821
1922import { confirmTerminalClose, isTerminalCloseConfirmPending } from "./terminalCloseConfirm";
2023
24const threadRef = {
25 environmentId: EnvironmentId.make("secondary"),
26 threadId: ThreadId.make("thread"),
27};
28const terminal: TerminalSummary = {
29 threadId: threadRef.threadId,
30 terminalId: "term-1",
31 cwd: "/repo",
32 worktreePath: null,
33 status: "running",
34 pid: 123,
35 exitCode: null,
36 exitSignal: null,
37 hasRunningSubprocess: true,
38 label: "Terminal 1",
39 updatedAt: "2026-10-15T00:00:00.000Z",
40};
41
42function metadata(terminals: ReadonlyArray<TerminalSummary>) {
43 return { _tag: "Success", value: terminals };
44}
45
2146describe("terminal close confirmation", () => {
2247 beforeEach(() => {
23 confirmMock.mockReset();
24 readLocalApiMock.mockReset();
48 vi.clearAllMocks();
2549 readLocalApiMock.mockReturnValue({ dialogs: { confirm: confirmMock } });
50 executeAtomQueryMock.mockResolvedValue(metadata([terminal]));
51 confirmMock.mockResolvedValue(true);
2652 });
2753
28 it("tracks pending state until the confirmation settles", async () => {
29 let settle: (value: boolean) => void = () => undefined;
30 confirmMock.mockImplementation(() => new Promise<boolean>((resolve) => (settle = resolve)));
54 it("closes an idle shell without a dialog or a pending confirmation after the lookup", async () => {
55 executeAtomQueryMock.mockResolvedValue(
56 metadata([{ ...terminal, hasRunningSubprocess: false }]),
57 );
3158
59 await expect(confirmTerminalClose(threadRef, ["term-1"])).resolves.toBe(true);
60
61 expect(confirmMock).not.toHaveBeenCalled();
3262 expect(isTerminalCloseConfirmPending()).toBe(false);
63 });
3364
34 const confirmation = confirmTerminalClose(["Terminal 1"]);
35 expect(isTerminalCloseConfirmPending()).toBe(true);
65 it("closes an exited terminal without prompting", async () => {
66 executeAtomQueryMock.mockResolvedValue(
67 metadata([{ ...terminal, status: "exited", pid: null, hasRunningSubprocess: false }]),
68 );
3669
37 settle(true);
38 await expect(confirmation).resolves.toBe(true);
39 expect(isTerminalCloseConfirmPending()).toBe(false);
70 await expect(confirmTerminalClose(threadRef, ["term-1"])).resolves.toBe(true);
71 expect(confirmMock).not.toHaveBeenCalled();
4072 });
4173
42 it("clears pending state and resolves false when the dialog rejects", async () => {
43 let reject: (reason?: unknown) => void = () => undefined;
44 confirmMock.mockImplementation(
45 () =>
46 new Promise<boolean>((_resolve, rejectPromise) => {
47 reject = rejectPromise;
48 }),
74 it("refreshes the exact environment before each close so a newly started program prompts", async () => {
75 executeAtomQueryMock
76 .mockResolvedValueOnce(metadata([{ ...terminal, hasRunningSubprocess: false }]))
77 .mockResolvedValueOnce(metadata([terminal]));
78
79 await expect(confirmTerminalClose(threadRef, ["term-1"])).resolves.toBe(true);
80 expect(confirmMock).not.toHaveBeenCalled();
81 confirmMock.mockResolvedValueOnce(false);
82 await expect(confirmTerminalClose(threadRef, ["term-1"])).resolves.toBe(false);
83
84 expect(metadataSnapshotMock).toHaveBeenNthCalledWith(1, {
85 environmentId: threadRef.environmentId,
86 input: null,
87 });
88 expect(executeAtomQueryMock).toHaveBeenNthCalledWith(2, registry, "metadata-snapshot", {
89 refresh: true,
90 reportFailure: false,
91 signal: expect.any(AbortSignal),
92 });
93 expect(confirmMock).toHaveBeenCalledExactlyOnceWith(
94 'Close terminal "Terminal 1"?\nThis stops the running process and clears its history.',
95 { variant: "destructive" },
4996 );
97 });
5098
51 const confirmation = confirmTerminalClose(["Terminal 1"]);
52 expect(isTerminalCloseConfirmPending()).toBe(true);
99 it("does not let another thread's idle terminal suppress the warning", async () => {
100 executeAtomQueryMock.mockResolvedValue(
101 metadata([{ ...terminal, threadId: "other-thread", hasRunningSubprocess: false }]),
102 );
53103
54 reject(new Error("dialog failed"));
55 await expect(confirmation).resolves.toBe(false);
56 expect(isTerminalCloseConfirmPending()).toBe(false);
104 await confirmTerminalClose(threadRef, ["term-1"]);
105 expect(confirmMock).toHaveBeenCalledOnce();
57106 });
58107
59 it("names every terminal in a multi-terminal close", async () => {
60 confirmMock.mockResolvedValue(true);
108 it.each([metadata([]), { _tag: "Failure" }])(
109 "warns when terminal activity is unknown: %j",
110 async (result) => {
111 executeAtomQueryMock.mockResolvedValue(result);
112 confirmMock.mockResolvedValue(false);
61113
62 await expect(confirmTerminalClose(["Terminal 1", "Development server"])).resolves.toBe(true);
114 await expect(confirmTerminalClose(threadRef, ["term-1"])).resolves.toBe(false);
115 expect(confirmMock).toHaveBeenCalledOnce();
116 },
117 );
118
119 it("closes a split surface directly only when every terminal is known idle", async () => {
120 executeAtomQueryMock.mockResolvedValue(
121 metadata([
122 { ...terminal, hasRunningSubprocess: false },
123 { ...terminal, terminalId: "term-2", hasRunningSubprocess: false },
124 ]),
125 );
126
127 await expect(confirmTerminalClose(threadRef, ["term-1", "term-2"])).resolves.toBe(true);
128 expect(confirmMock).not.toHaveBeenCalled();
129
130 await confirmTerminalClose(threadRef, ["term-1", "term-3"]);
131 expect(confirmMock).toHaveBeenCalledOnce();
132 });
133
134 it("names every terminal when a split surface includes a running program", async () => {
135 executeAtomQueryMock.mockResolvedValue(
136 metadata([
137 { ...terminal, hasRunningSubprocess: false },
138 { ...terminal, terminalId: "term-2", label: "Development server" },
139 ]),
140 );
141
142 await expect(confirmTerminalClose(threadRef, ["term-1", "term-2"])).resolves.toBe(true);
63143 expect(confirmMock).toHaveBeenCalledWith(
64144 [
65145 "Close 2 terminals?",
......@@ -69,10 +149,43 @@ describe("terminal close confirmation", () => {
69149 );
70150 });
71151
152 it("tracks pending state through the activity lookup and until the confirmation settles", async () => {
153 let completeLookup: (value: ReturnType<typeof metadata>) => void = () => undefined;
154 executeAtomQueryMock.mockImplementationOnce(
155 () => new Promise<ReturnType<typeof metadata>>((resolve) => (completeLookup = resolve)),
156 );
157 let settle: (value: boolean) => void = () => undefined;
158 const dialogOpened = new Promise<void>((resolve) => {
159 confirmMock.mockImplementationOnce(() => {
160 resolve();
161 return new Promise<boolean>((resolveConfirmation) => (settle = resolveConfirmation));
162 });
163 });
164
165 expect(isTerminalCloseConfirmPending()).toBe(false);
166 const confirmation = confirmTerminalClose(threadRef, ["term-1"]);
167 expect(isTerminalCloseConfirmPending()).toBe(true);
168
169 completeLookup(metadata([terminal]));
170 await dialogOpened;
171 expect(isTerminalCloseConfirmPending()).toBe(true);
172 settle(true);
173 await expect(confirmation).resolves.toBe(true);
174 expect(isTerminalCloseConfirmPending()).toBe(false);
175 });
176
177 it("clears pending state and resolves false when the dialog rejects", async () => {
178 confirmMock.mockRejectedValueOnce(new Error("dialog failed"));
179
180 await expect(confirmTerminalClose(threadRef, ["term-1"])).resolves.toBe(false);
181 expect(isTerminalCloseConfirmPending()).toBe(false);
182 });
183
72184 it("closes without prompting when no local API is available", async () => {
73185 readLocalApiMock.mockReturnValue(undefined);
74186
75 await expect(confirmTerminalClose(["Terminal 1"])).resolves.toBe(true);
187 await expect(confirmTerminalClose(threadRef, ["term-1"])).resolves.toBe(true);
76188 expect(confirmMock).not.toHaveBeenCalled();
189 expect(executeAtomQueryMock).not.toHaveBeenCalled();
77190 });
78191});
apps/web/src/lib/terminalCloseConfirm.ts+28-8
......@@ -1,25 +1,45 @@
1import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime";
2import type { ScopedThreadRef } from "@t3tools/contracts";
3import { resolveTerminalSessionLabel } from "@t3tools/shared/terminalLabels";
4
15import { readLocalApi } from "~/localApi";
6import { appAtomRegistry } from "~/rpc/atomRegistry";
7import { terminalEnvironment } from "~/state/terminal";
28
39let pendingConfirmations = 0;
410
5/** Whether a terminal-close confirmation is currently waiting on the user. */
611export function isTerminalCloseConfirmPending(): boolean {
712 return pendingConfirmations > 0;
813}
914
10/**
11 * Confirmation for individual terminal close actions: drawer buttons, panel
12 * buttons, the `terminal.close` keybinding, and closing a terminal surface from
13 * the tab strip. Auto-exit cleanup and bulk tab closes skip this path and close
14 * directly.
15 */
1615export async function confirmTerminalClose(
17 labels: readonly [string, ...string[]],
16 threadRef: ScopedThreadRef,
17 terminalIds: readonly [string, ...string[]],
1818): Promise<boolean> {
1919 const localApi = readLocalApi();
2020 if (!localApi) return true;
2121 pendingConfirmations += 1;
2222 try {
23 const metadata = await executeAtomQuery(
24 appAtomRegistry,
25 terminalEnvironment.metadataSnapshot({ environmentId: threadRef.environmentId, input: null }),
26 { refresh: true, reportFailure: false, signal: AbortSignal.timeout(3_000) },
27 );
28 const terminals = terminalIds.map((terminalId) =>
29 metadata._tag === "Success"
30 ? metadata.value.find(
31 (terminal) =>
32 terminal.threadId === threadRef.threadId && terminal.terminalId === terminalId,
33 )
34 : undefined,
35 );
36 if (terminals.every((terminal) => terminal !== undefined && !terminal.hasRunningSubprocess)) {
37 return true;
38 }
39
40 const labels = terminalIds.map((terminalId, index) =>
41 resolveTerminalSessionLabel(terminalId, terminals[index]),
42 );
2343 return await localApi.dialogs.confirm(
2444 labels.length === 1
2545 ? [
apps/web/src/terminal-links.test.ts+3-192
......@@ -1,203 +1,14 @@
1import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks";
21import { describe, expect, it } from "vite-plus/test";
32
4import {
5 collectWrappedTerminalLinkLine,
6 extractTerminalLinks,
7 isTerminalLinkActivation,
8 isTerminalUrl,
9 type TerminalBufferLineLike,
10} from "./terminal-links";
3import { isTerminalLinkActivation, isTerminalUrl } from "./terminal-links";
114
12function createBufferLine(text: string, isWrapped = false): TerminalBufferLineLike {
13 return {
14 isWrapped,
15 translateToString: (trimRight = false) => (trimRight ? text.replace(/\s+$/u, "") : text),
16 };
17}
18
19describe("extractTerminalLinks", () => {
20 it("finds http urls and path tokens", () => {
21 const line =
22 "failed at https://example.com/docs and src/components/ThreadTerminalDrawer.tsx:42";
23 expect(extractTerminalLinks(line)).toEqual([
24 {
25 kind: "url",
26 text: "https://example.com/docs",
27 start: 10,
28 end: 34,
29 },
30 {
31 kind: "path",
32 text: "src/components/ThreadTerminalDrawer.tsx:42",
33 start: 39,
34 end: 81,
35 },
36 ]);
37 });
38
39 it("classifies uppercase schemes as URLs at activation time too", () => {
5describe("isTerminalUrl", () => {
6 it("recognizes HTTP schemes without treating workspace paths as URLs", () => {
407 expect(isTerminalUrl("HTTPS://example.com/docs")).toBe(true);
418 expect(isTerminalUrl("Http://example.com")).toBe(true);
429 expect(isTerminalUrl("src/components/main.ts")).toBe(false);
4310 expect(isTerminalUrl("httpsdocs/readme.md")).toBe(false);
4411 });
45
46 it("finds URLs regardless of scheme casing", () => {
47 expect(extractTerminalLinks("open HTTPS://example.com/docs")).toEqual([
48 {
49 kind: "url",
50 text: "HTTPS://example.com/docs",
51 start: 5,
52 end: 29,
53 },
54 ]);
55 });
56
57 it("trims trailing punctuation from links", () => {
58 const line = "(https://example.com/docs), ./src/main.ts:12.";
59 expect(extractTerminalLinks(line)).toEqual([
60 {
61 kind: "url",
62 text: "https://example.com/docs",
63 start: 1,
64 end: 25,
65 },
66 {
67 kind: "path",
68 text: "./src/main.ts:12",
69 start: 28,
70 end: 44,
71 },
72 ]);
73 });
74
75 it("finds Windows absolute paths with forward slashes", () => {
76 const line = "see C:/Users/someone/project/src/file.ts:42 for details";
77 const path = "C:/Users/someone/project/src/file.ts:42";
78 const start = line.indexOf(path);
79 expect(extractTerminalLinks(line)).toEqual([
80 {
81 kind: "path",
82 text: path,
83 start,
84 end: start + path.length,
85 },
86 ]);
87 });
88
89 it("trims trailing punctuation from Windows forward-slash paths", () => {
90 const line = "(C:/tmp/x.ts).";
91 expect(extractTerminalLinks(line)).toEqual([
92 {
93 kind: "path",
94 text: "C:/tmp/x.ts",
95 start: 1,
96 end: 12,
97 },
98 ]);
99 });
100
101 it("keeps a trailing colon on URLs", () => {
102 expect(extractTerminalLinks("GET https://example.test/items/foo:")).toEqual([
103 { kind: "url", text: "https://example.test/items/foo:", start: 4, end: 35 },
104 ]);
105 });
106
107 it.each([
108 ["./main.go:10:5: undefined: x", "./main.go:10:5"],
109 ["/home/dev/app/src/main.c:10:5: error: expected ';'", "/home/dev/app/src/main.c:10:5"],
110 ["C:\\dev\\app\\src\\main.c:10:5: error: expected ';'", "C:\\dev\\app\\src\\main.c:10:5"],
111 ["wrote ./out/report.txt:", "./out/report.txt"],
112 ])("drops the colon that ends a compiler diagnostic location in %s", (line, text) => {
113 const start = line.indexOf(text);
114 expect(extractTerminalLinks(line)).toEqual([
115 { kind: "path", text, start, end: start + text.length },
116 ]);
117 });
118});
119
120describe("collectWrappedTerminalLinkLine", () => {
121 it("reconstructs a wrapped line from any physical row", () => {
122 const firstSegment = "see https://example.com/a";
123 const secondSegment = "/bc?x=1";
124 const lines = [
125 createBufferLine("prompt> "),
126 createBufferLine(firstSegment),
127 createBufferLine(secondSegment, true),
128 createBufferLine("done"),
129 ];
130
131 const fromFirstRow = collectWrappedTerminalLinkLine(2, (index) => lines[index]);
132 const fromWrappedRow = collectWrappedTerminalLinkLine(3, (index) => lines[index]);
133
134 expect(fromFirstRow).toEqual({
135 text: `${firstSegment}${secondSegment}`,
136 segments: [
137 {
138 bufferLineNumber: 2,
139 text: firstSegment,
140 startIndex: 0,
141 endIndex: firstSegment.length,
142 },
143 {
144 bufferLineNumber: 3,
145 text: secondSegment,
146 startIndex: firstSegment.length,
147 endIndex: firstSegment.length + secondSegment.length,
148 },
149 ],
150 });
151 expect(fromWrappedRow).toEqual(fromFirstRow);
152 });
153
154 it("preserves trailing spaces on continued segments for downstream offsets", () => {
155 const firstSegment = "prefix ";
156 const secondSegment = "https://example.com/path";
157 const lines = [createBufferLine(firstSegment), createBufferLine(secondSegment, true)];
158
159 const wrappedLine = collectWrappedTerminalLinkLine(2, (index) => lines[index]);
160
161 expect(wrappedLine?.text).toBe(`${firstSegment}${secondSegment}`);
162 expect(extractTerminalLinks(wrappedLine?.text ?? "")).toEqual([
163 {
164 kind: "url",
165 text: secondSegment,
166 start: firstSegment.length,
167 end: firstSegment.length + secondSegment.length,
168 },
169 ]);
170 });
171});
172
173describe("resolvePathLinkTarget", () => {
174 it("resolves relative paths against cwd", () => {
175 expect(
176 resolvePathLinkTarget(
177 "src/components/ThreadTerminalDrawer.tsx:42:7",
178 "/Users/julius/project",
179 ),
180 ).toBe("/Users/julius/project/src/components/ThreadTerminalDrawer.tsx:42:7");
181 });
182
183 it("keeps absolute paths unchanged", () => {
184 expect(
185 resolvePathLinkTarget("/Users/julius/project/src/main.ts:12", "/Users/julius/project"),
186 ).toBe("/Users/julius/project/src/main.ts:12");
187 });
188
189 it("keeps Windows absolute paths with forward slashes unchanged", () => {
190 expect(
191 resolvePathLinkTarget("C:/Users/julius/project/src/main.ts:12", "C:\\Users\\julius\\project"),
192 ).toBe("C:/Users/julius/project/src/main.ts:12");
193 });
194
195 it("keeps the line and column of a compiler diagnostic", () => {
196 const [link] = extractTerminalLinks("/Users/julius/project/main.c:10:5: error: expected ';'");
197 expect(resolvePathLinkTarget(link?.text ?? "", "/Users/julius/project")).toBe(
198 "/Users/julius/project/main.c:10:5",
199 );
200 });
20112});
20213
20314describe("isTerminalLinkActivation", () => {
apps/web/src/terminal-links.ts-146
......@@ -1,155 +1,9 @@
11import { isMacPlatform } from "./lib/utils";
22
3export type TerminalLinkKind = "url" | "path";
4
5export interface TerminalLinkMatch {
6 kind: TerminalLinkKind;
7 text: string;
8 start: number;
9 end: number;
10}
11
12export interface TerminalBufferLineLike {
13 readonly isWrapped?: boolean;
14 translateToString(trimRight?: boolean): string;
15}
16
17export interface WrappedTerminalLinkLineSegment {
18 bufferLineNumber: number;
19 text: string;
20 startIndex: number;
21 endIndex: number;
22}
23
24export interface WrappedTerminalLinkLine {
25 text: string;
26 segments: ReadonlyArray<WrappedTerminalLinkLineSegment>;
27}
28
29const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/giu;
30const FILE_PATH_PATTERN =
31 /(?:~\/|\.{1,2}\/|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}/g;
32const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/;
33// Paths also drop a trailing colon: compilers end `file:line:col:` with one.
34const TRAILING_PATH_PUNCTUATION_PATTERN = /[.,;:!?]+$/;
35
36function trimClosingDelimiters(value: string, kind: TerminalLinkKind): string {
37 let output = value.replace(
38 kind === "path" ? TRAILING_PATH_PUNCTUATION_PATTERN : TRAILING_PUNCTUATION_PATTERN,
39 "",
40 );
41 if (output.length === 0) return output;
42
43 const trimUnbalanced = (open: string, close: string) => {
44 while (output.endsWith(close)) {
45 const opens = output.split(open).length - 1;
46 const closes = output.split(close).length - 1;
47 if (opens >= closes) return;
48 output = output.slice(0, -1);
49 }
50 };
51
52 trimUnbalanced("(", ")");
53 trimUnbalanced("[", "]");
54 trimUnbalanced("{", "}");
55 return output;
56}
57
58function overlaps(a: { start: number; end: number }, b: { start: number; end: number }): boolean {
59 return a.start < b.end && b.start < a.end;
60}
61
62function collectMatches(
63 line: string,
64 kind: TerminalLinkKind,
65 pattern: RegExp,
66 existing: TerminalLinkMatch[],
67): TerminalLinkMatch[] {
68 const matches: TerminalLinkMatch[] = [];
69 pattern.lastIndex = 0;
70
71 for (const rawMatch of line.matchAll(pattern)) {
72 const raw = rawMatch[0];
73 const start = rawMatch.index ?? -1;
74 if (start < 0 || raw.length === 0) continue;
75
76 const trimmed = trimClosingDelimiters(raw, kind);
77 if (trimmed.length === 0) continue;
78 if (kind === "path" && isTerminalUrl(trimmed)) continue;
79
80 const candidate: TerminalLinkMatch = {
81 kind,
82 text: trimmed,
83 start,
84 end: start + trimmed.length,
85 };
86
87 const collides = [...existing, ...matches].some((other) => overlaps(candidate, other));
88 if (collides) continue;
89
90 matches.push(candidate);
91 }
92
93 return matches;
94}
95
96export function extractTerminalLinks(line: string): TerminalLinkMatch[] {
97 const urlMatches = collectMatches(line, "url", URL_PATTERN, []);
98 const pathMatches = collectMatches(line, "path", FILE_PATH_PATTERN, urlMatches);
99 return [...urlMatches, ...pathMatches].toSorted((a, b) => a.start - b.start);
100}
101
1023export function isTerminalUrl(value: string): boolean {
1034 return /^https?:\/\//iu.test(value);
1045}
1056
106export function collectWrappedTerminalLinkLine(
107 bufferLineNumber: number,
108 getLine: (bufferLineIndex: number) => TerminalBufferLineLike | null | undefined,
109): WrappedTerminalLinkLine | null {
110 const anchorLine = getLine(bufferLineNumber - 1);
111 if (!anchorLine) return null;
112
113 let startBufferLineNumber = bufferLineNumber;
114 let startLine = anchorLine;
115
116 while (startBufferLineNumber > 1 && startLine.isWrapped) {
117 const previousLine = getLine(startBufferLineNumber - 2);
118 if (!previousLine) return null;
119 startBufferLineNumber -= 1;
120 startLine = previousLine;
121 }
122
123 const segments: WrappedTerminalLinkLineSegment[] = [];
124 let nextStartIndex = 0;
125 let currentBufferLineNumber = startBufferLineNumber;
126
127 while (true) {
128 const currentLine = getLine(currentBufferLineNumber - 1);
129 if (!currentLine) break;
130
131 const nextLine = getLine(currentBufferLineNumber);
132 const hasWrappedContinuation = nextLine?.isWrapped === true;
133 const text = currentLine.translateToString(!hasWrappedContinuation);
134
135 segments.push({
136 bufferLineNumber: currentBufferLineNumber,
137 text,
138 startIndex: nextStartIndex,
139 endIndex: nextStartIndex + text.length,
140 });
141 nextStartIndex += text.length;
142
143 if (!hasWrappedContinuation) break;
144 currentBufferLineNumber += 1;
145 }
146
147 return {
148 text: segments.map((segment) => segment.text).join(""),
149 segments,
150 };
151}
152
1537export function isTerminalLinkActivation(
1548 event: Pick<MouseEvent, "metaKey" | "ctrlKey">,
1559 platform = typeof navigator === "undefined" ? "" : navigator.platform,
apps/web/src/terminal/TerminalSurface.test.ts created+690
......@@ -0,0 +1,690 @@
1// @vitest-environment jsdom
2// @effect-diagnostics nodeBuiltinImport:off - loads the vendored WASM for isolated DOM tests.
3import * as NodeFS from "node:fs";
4import * as NodePath from "node:path";
5import * as NodeURL from "node:url";
6import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
7
8import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks";
9
10import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
11import { TerminalSurface, type TerminalSurfaceOptions } from "./TerminalSurface";
12
13vi.mock("~/hooks/useCopyToClipboard", () => ({ writeTextToClipboard: vi.fn(async () => {}) }));
14
15const bytes = new Uint8Array(
16 NodeFS.readFileSync(
17 NodePath.resolve(
18 NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)),
19 "../../../../packages/terminal/vendor/ghostty-vt.wasm",
20 ),
21 ),
22);
23const surfaces = new Set<TerminalSurface>();
24const frames = new Map<number, FrameRequestCallback>();
25let nextFrame = 0;
26let width = 640;
27let height = 192;
28let platform = "MacIntel";
29
30function paint() {
31 const pending = [...frames.values()];
32 frames.clear();
33 pending.forEach((callback) => callback(performance.now()));
34}
35
36async function mount(
37 options: Partial<TerminalSurfaceOptions> = {},
38 host = document.createElement("div"),
39) {
40 document.body.append(host);
41 const onData = vi.fn();
42 const onResize = vi.fn();
43 const onLinkActivate = vi.fn();
44 const onContextMenu = vi.fn();
45 const surface = await TerminalSurface.create(host, {
46 theme: {
47 foreground: "#ffffff",
48 background: "#000000",
49 cursor: "#dddddd",
50 palette: Array(16).fill("#aaaaaa"),
51 selectionBackground: "#333333",
52 },
53 onData,
54 onResize,
55 onLinkActivate,
56 onContextMenu,
57 beforeKey: () => true,
58 ...options,
59 });
60 surfaces.add(surface);
61 paint();
62 const terminalRoot = surface.input.parentElement!;
63 return { surface, host: terminalRoot, onData, onResize, onLinkActivate, onContextMenu };
64}
65
66function select(host: HTMLElement, text = "hello") {
67 const row = [...host.querySelectorAll(".clo-term-row")].find((row) =>
68 row.textContent?.includes(text),
69 );
70 expect(row).toBeDefined();
71 const walker = document.createTreeWalker(row!, NodeFilter.SHOW_TEXT);
72 const range = document.createRange();
73 const start = row!.textContent!.indexOf(text);
74 const end = start + text.length;
75 let offset = 0;
76 let node = walker.nextNode();
77 while (node) {
78 const length = node.textContent!.length;
79 if (offset <= start && start < offset + length) range.setStart(node, start - offset);
80 if (offset < end && end <= offset + length) {
81 range.setEnd(node, end - offset);
82 break;
83 }
84 offset += length;
85 node = walker.nextNode();
86 }
87 const selection = document.getSelection()!;
88 selection.removeAllRanges();
89 selection.addRange(range);
90}
91
92// Model the Chromium textarea focus behavior that loses native output selection.
93function nativeInputFocus(surface: TerminalSurface) {
94 return vi.spyOn(surface.input, "focus").mockImplementation(function (
95 this: HTMLTextAreaElement,
96 options?: FocusOptions,
97 ) {
98 HTMLElement.prototype.focus.call(this, options);
99 this.ownerDocument.getSelection()?.removeAllRanges();
100 });
101}
102
103function key(element: HTMLElement, key: string, options: KeyboardEventInit = {}) {
104 const event = new KeyboardEvent("keydown", {
105 key,
106 code: key === "c" ? "KeyC" : key,
107 bubbles: true,
108 cancelable: true,
109 ...options,
110 });
111 element.dispatchEvent(event);
112 return event;
113}
114
115beforeEach(() => {
116 document.body.tabIndex = -1;
117 document.body.focus();
118 frames.clear();
119 width = 640;
120 height = 192;
121 platform = "MacIntel";
122 vi.spyOn(navigator, "platform", "get").mockImplementation(() => platform);
123 vi.stubGlobal(
124 "fetch",
125 vi.fn(async () => new Response(bytes)),
126 );
127 vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
128 const id = ++nextFrame;
129 frames.set(id, callback);
130 return id;
131 });
132 vi.stubGlobal("cancelAnimationFrame", (id: number) => frames.delete(id));
133 vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null);
134 vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function (
135 this: HTMLElement,
136 ) {
137 return this.classList.contains("clo-term-measure")
138 ? new DOMRect(0, 0, 256, 16)
139 : new DOMRect(0, 0, width, height);
140 });
141});
142
143afterEach(() => {
144 surfaces.forEach((surface) => surface.dispose());
145 surfaces.clear();
146 document.body.replaceChildren();
147 document.body.removeAttribute("tabindex");
148 vi.restoreAllMocks();
149 vi.unstubAllGlobals();
150 vi.clearAllMocks();
151});
152
153describe("T3 DOM terminal adapter", () => {
154 it("renders real VT output as selectable DOM and leaves selection unchanged on unrelated output", async () => {
155 const { surface, host, onContextMenu } = await mount();
156 surface.write("hello 🌱\r\n");
157 paint();
158 expect(host.textContent).toContain("hello 🌱");
159 expect(host.querySelector("canvas")).toBeNull();
160 select(host);
161 document.dispatchEvent(new Event("selectionchange"));
162 expect(surface.getSelection()).toBe("hello");
163 expect(surface.getSelectionPosition()).toEqual({ start: { y: 0 }, end: { y: 0 } });
164 surface.write("next line");
165 paint();
166 expect(surface.getSelection()).toBe("hello");
167 expect(onContextMenu).not.toHaveBeenCalled();
168 host.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, cancelable: true }));
169 expect(onContextMenu).toHaveBeenCalledOnce();
170 });
171
172 it("copies only an owned DOM selection and keeps Ctrl-C as SIGINT without selection", async () => {
173 const { surface, host, onData } = await mount();
174 surface.write("hello");
175 paint();
176 select(host);
177 expect(key(surface.input, "c", { metaKey: true }).defaultPrevented).toBe(true);
178 await Promise.resolve();
179 expect(writeTextToClipboard).toHaveBeenCalledExactlyOnceWith("hello", "terminal selection");
180 expect(onData).not.toHaveBeenCalled();
181 surface.clearSelection();
182 key(surface.input, "c", { ctrlKey: true });
183 expect(onData).toHaveBeenCalledExactlyOnceWith("\x03");
184 vi.mocked(writeTextToClipboard).mockClear();
185 const composer = document.createElement("textarea");
186 document.body.append(composer);
187 key(composer, "c", { metaKey: true });
188 expect(writeTextToClipboard).not.toHaveBeenCalled();
189 });
190
191 it("commits IME once, brackets paste and sends control keys through the VT encoder", async () => {
192 const { surface, onData } = await mount();
193 const input = surface.input;
194 input.dispatchEvent(new CompositionEvent("compositionstart", { bubbles: true }));
195 key(input, "Process", { isComposing: true, keyCode: 229 });
196 input.value = "にほん";
197 input.dispatchEvent(
198 new InputEvent("input", {
199 bubbles: true,
200 isComposing: true,
201 inputType: "insertCompositionText",
202 }),
203 );
204 input.dispatchEvent(new CompositionEvent("compositionend", { bubbles: true, data: "日本" }));
205 input.value = "日本";
206 input.dispatchEvent(
207 new InputEvent("input", { bubbles: true, inputType: "insertFromComposition" }),
208 );
209 expect(onData).toHaveBeenCalledExactlyOnceWith("日本");
210 onData.mockClear();
211 surface.write("\x1b[?2004h");
212 const paste = new Event("paste", { bubbles: true, cancelable: true });
213 Object.defineProperty(paste, "clipboardData", { value: { getData: () => "a🌱b" } });
214 input.dispatchEvent(paste);
215 expect(onData).toHaveBeenCalledExactlyOnceWith("\x1b[200~a🌱b\x1b[201~");
216 onData.mockClear();
217 key(input, "Backspace");
218 expect(onData).toHaveBeenCalledExactlyOnceWith("\x7f");
219 });
220
221 it("keeps read-only terminals selectable and blocks native keyboard and IME writes", async () => {
222 const { surface, host, onData } = await mount();
223 surface.write("hello");
224 paint();
225 surface.input.readOnly = true;
226 select(host);
227 key(surface.input, "c", { metaKey: true });
228 key(surface.input, "c", { ctrlKey: true });
229 surface.clearSelection();
230 key(surface.input, "Enter");
231 surface.input.dispatchEvent(
232 new CompositionEvent("compositionend", { data: "秘密", bubbles: true }),
233 );
234 expect(onData).not.toHaveBeenCalled();
235 expect(writeTextToClipboard).toHaveBeenCalled();
236 });
237
238 it("requires modifier-click and preserves OSC 8 identity and file positions", async () => {
239 const { surface, host, onLinkActivate } = await mount({
240 validatePath: (path) => path === "src/file.ts",
241 });
242 surface.write(
243 "\x1b]8;;https://example.test/oauth\x1b\\sign in\x1b]8;;\x1b\\\r\nsrc/file.ts:12:3",
244 );
245 paint();
246 const links = host.querySelectorAll(".clo-term-link");
247 expect(links.length).toBe(2);
248 links[0]!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
249 expect(onLinkActivate).not.toHaveBeenCalled();
250 links[0]!.dispatchEvent(
251 new MouseEvent("click", { bubbles: true, cancelable: true, metaKey: true }),
252 );
253 expect(onLinkActivate.mock.calls[0]?.[0]).toBe("https://example.test/oauth");
254 platform = "Linux x86_64";
255 links[1]!.dispatchEvent(
256 new MouseEvent("click", { bubbles: true, cancelable: true, ctrlKey: true }),
257 );
258 expect(onLinkActivate.mock.calls[1]?.[0]).toBe("src/file.ts:12:3");
259 });
260
261 it("denies program clipboard writes without touching the system clipboard", async () => {
262 const { surface } = await mount();
263 surface.write("\x1b]52;c;c2VjcmV0\x07");
264 expect(writeTextToClipboard).not.toHaveBeenCalled();
265 });
266
267 it("recognizes uppercase HTTP schemes and honors live link permission", async () => {
268 let allowed = true;
269 const { surface, host, onLinkActivate } = await mount({ canActivateLink: () => allowed });
270 surface.write("HTTPS://example.test/login");
271 paint();
272 const link = host.querySelector(".clo-term-link")!;
273 expect(link).not.toBeNull();
274 allowed = false;
275 link.dispatchEvent(new MouseEvent("click", { bubbles: true, metaKey: true }));
276 expect(onLinkActivate).not.toHaveBeenCalled();
277 allowed = true;
278 link.dispatchEvent(new MouseEvent("click", { bubbles: true, metaKey: true }));
279 expect(onLinkActivate.mock.calls[0]?.[0]).toBe("HTTPS://example.test/login");
280 });
281
282 it("activates complete URLs from either fragment of a wrapped line", async () => {
283 width = 160;
284 const { surface, host, onLinkActivate } = await mount();
285 const url = "https://example.test/long/path/that/wraps";
286 surface.write(url);
287 paint();
288 const links = host.querySelectorAll(".clo-term-link");
289 expect(links.length).toBeGreaterThan(1);
290 links[0]!.dispatchEvent(new MouseEvent("click", { bubbles: true, metaKey: true }));
291 links[1]!.dispatchEvent(new MouseEvent("click", { bubbles: true, metaKey: true }));
292 expect(onLinkActivate.mock.calls.map(([target]) => target)).toEqual([url, url]);
293 });
294
295 it("lets application shortcuts and native Tab bubble without VT encoding", async () => {
296 const { surface, host, onData } = await mount({ beforeKey: () => false });
297 const parent = vi.fn();
298 host.parentElement!.addEventListener("keydown", parent);
299 expect(key(surface.input, "Tab").defaultPrevented).toBe(false);
300 expect(parent).toHaveBeenCalledOnce();
301 expect(onData).not.toHaveBeenCalled();
302 });
303
304 it("suppresses replay replies while live queries still reach the PTY", async () => {
305 const { surface, onData } = await mount();
306 surface.resetAndWrite("hello\x1b[6n");
307 expect(onData).not.toHaveBeenCalled();
308 surface.write("\x1b[6n");
309 expect(onData).toHaveBeenCalledExactlyOnceWith("\x1b[1;6R");
310 });
311
312 it("keeps output drag, pane activation, modifier keys and copy on the root until actual typing", async () => {
313 const { surface, host, onData, onContextMenu } = await mount();
314 surface.write("hello");
315 paint();
316 const inputFocus = nativeInputFocus(surface);
317 const row = host.querySelector(".clo-term-row")!;
318 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
319 expect(document.activeElement).toBe(host);
320 surface.focus();
321 expect(inputFocus).not.toHaveBeenCalled();
322 select(host);
323 row.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
324 expect(document.activeElement).toBe(host);
325 expect(surface.getSelection()).toBe("hello");
326 key(host, "Shift", { shiftKey: true });
327 key(host, "Control", { ctrlKey: true });
328 key(host, "Alt", { altKey: true });
329 key(host, "Meta", { metaKey: true });
330 key(host, "c", { metaKey: true });
331 expect(writeTextToClipboard).toHaveBeenCalledExactlyOnceWith("hello", "terminal selection");
332 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 2 }));
333 host.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, cancelable: true }));
334 expect(onContextMenu).toHaveBeenCalledOnce();
335 surface.focus();
336 expect(inputFocus).not.toHaveBeenCalled();
337 expect(surface.getSelection()).toBe("hello");
338 key(host, "a");
339 expect(document.activeElement).toBe(surface.input);
340 expect(surface.getSelection()).toBe("");
341 expect(onData).toHaveBeenCalledExactlyOnceWith("a");
342 });
343
344 it("keeps shortcut focus and keyboard input available while terminal fonts load", async () => {
345 const style = document.createElement("style");
346 style.textContent = NodeFS.readFileSync(
347 NodePath.resolve(
348 NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)),
349 "../../../../packages/terminal/src/renderer/terminal.css",
350 ),
351 "utf8",
352 );
353 document.head.append(style);
354 let finishFonts!: () => void;
355 const fontReady = new Promise<void>((resolve) => {
356 finishFonts = resolve;
357 });
358 let reportFontLoad!: () => void;
359 const fontStarted = new Promise<void>((resolve) => {
360 reportFontLoad = resolve;
361 });
362 const fonts = Object.assign(new EventTarget(), {
363 load: async () => {
364 reportFontLoad();
365 await fontReady;
366 return [{}];
367 },
368 check: () => true,
369 });
370 Object.defineProperty(document, "fonts", { configurable: true, value: fonts });
371 const host = document.createElement("div");
372 host.tabIndex = -1;
373 const mounting = mount({}, host);
374 await fontStarted;
375 const focus = HTMLElement.prototype.focus;
376 vi.spyOn(HTMLElement.prototype, "focus").mockImplementation(function (
377 this: HTMLElement,
378 options?: FocusOptions,
379 ) {
380 // Browsers refuse focus on visibility:hidden elements; jsdom does not.
381 if (getComputedStyle(this).visibility !== "hidden") focus.call(this, options);
382 });
383 try {
384 host.focus();
385 const input = host.querySelector("textarea");
386 if (!input) throw new Error("Terminal input missing");
387 expect(document.activeElement).toBe(input);
388 expect(getComputedStyle(host.querySelector(".clo-term-viewport")!).visibility).toBe("hidden");
389 key(input, "a");
390 finishFonts();
391 const { surface, onData } = await mounting;
392 expect(document.activeElement).toBe(surface.input);
393 expect(onData).toHaveBeenCalledExactlyOnceWith("a");
394 expect(getComputedStyle(host.querySelector(".clo-term-viewport")!).visibility).toBe(
395 "visible",
396 );
397 } finally {
398 finishFonts();
399 await mounting;
400 Reflect.deleteProperty(document, "fonts");
401 style.remove();
402 }
403 });
404
405 it("keeps a replacement surface intact when an older async mount finishes and disposes later", async () => {
406 const style = document.createElement("style");
407 style.textContent = ".clo-term { font-family: monospace; font-size: 12px; }";
408 document.head.append(style);
409 let finishFirst!: () => void;
410 let finishSecond!: () => void;
411 let startedFirst!: () => void;
412 let startedSecond!: () => void;
413 const firstReady = new Promise<void>((resolve) => {
414 finishFirst = resolve;
415 });
416 const secondReady = new Promise<void>((resolve) => {
417 finishSecond = resolve;
418 });
419 const firstStarted = new Promise<void>((resolve) => {
420 startedFirst = resolve;
421 });
422 const secondStarted = new Promise<void>((resolve) => {
423 startedSecond = resolve;
424 });
425 let loads = 0;
426 Object.defineProperty(document, "fonts", {
427 configurable: true,
428 value: Object.assign(new EventTarget(), {
429 load: async () => {
430 const first = loads++ < 4;
431 if (first) startedFirst();
432 else startedSecond();
433 await (first ? firstReady : secondReady);
434 return [{}];
435 },
436 check: () => true,
437 }),
438 });
439 const container = document.createElement("div");
440 container.className = "terminal-container";
441 container.setAttribute("aria-label", "Replacement terminal");
442 const first = mount({ font: { size: 12 } }, container);
443 await firstStarted;
444 const second = mount({ font: { size: 14 } }, container);
445 await secondStarted;
446 try {
447 finishSecond();
448 const current = await second;
449 const root = current.surface.input.parentElement!;
450 current.surface.write("new output");
451 paint();
452 finishFirst();
453 const obsolete = await first;
454 obsolete.surface.dispose();
455 expect(root.classList.contains("clo-term")).toBe(true);
456 expect(root.style.getPropertyValue("--clo-term-font-size")).toBe("14px");
457 expect(container.contains(current.surface.input)).toBe(true);
458 expect(root.textContent).toContain("new output");
459 key(current.surface.input, "x");
460 expect(current.onData).toHaveBeenCalledExactlyOnceWith("x");
461 expect(current.surface.input.getAttribute("aria-label")).toBe("Replacement terminal");
462 current.surface.dispose();
463 expect(container.childElementCount).toBe(0);
464 expect(container.className).toBe("terminal-container");
465 } finally {
466 finishFirst();
467 finishSecond();
468 await Promise.all([first, second]);
469 Reflect.deleteProperty(document, "fonts");
470 style.remove();
471 }
472 });
473
474 it("focuses the native input for plain clicks, keyboard focus and first IME entry", async () => {
475 const { surface, host, onData } = await mount();
476 surface.write("hello");
477 paint();
478 const inputFocus = nativeInputFocus(surface);
479 host.focus();
480 expect(document.activeElement).toBe(surface.input);
481 expect(surface.input.getAttribute("aria-label")).toBe("Terminal input");
482 const row = host.querySelector(".clo-term-row")!;
483 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
484 expect(document.activeElement).toBe(host);
485 row.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
486 expect(document.activeElement).toBe(surface.input);
487 inputFocus.mockClear();
488 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
489 select(host);
490 row.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
491 expect(inputFocus).not.toHaveBeenCalled();
492 key(host, "Process", { keyCode: 229, isComposing: true });
493 expect(document.activeElement).toBe(surface.input);
494 expect(surface.getSelection()).toBe("");
495 surface.input.dispatchEvent(new CompositionEvent("compositionstart", { bubbles: true }));
496 surface.input.dispatchEvent(
497 new CompositionEvent("compositionend", { bubbles: true, data: "日本" }),
498 );
499 expect(onData).toHaveBeenCalledExactlyOnceWith("日本");
500 });
501
502 it("moves to native input for browser paste without consuming the shortcut as VT data", async () => {
503 const { surface, host, onData } = await mount();
504 surface.write("hello\x1b[?2004h");
505 paint();
506 nativeInputFocus(surface);
507 const row = host.querySelector(".clo-term-row")!;
508 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
509 select(host);
510 row.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
511 const shortcut = key(host, "v", { metaKey: true });
512 expect(shortcut.defaultPrevented).toBe(false);
513 expect(document.activeElement).toBe(surface.input);
514 expect(surface.getSelection()).toBe("");
515 expect(onData).not.toHaveBeenCalled();
516 const paste = new Event("paste", { bubbles: true, cancelable: true });
517 Object.defineProperty(paste, "clipboardData", { value: { getData: () => "日本" } });
518 surface.input.dispatchEvent(paste);
519 expect(onData).toHaveBeenCalledExactlyOnceWith("\x1b[200~日本\x1b[201~");
520 });
521
522 it("focuses TUI input before claiming a press and retains the Shift selection override through release", async () => {
523 const { surface, host, onData } = await mount();
524 surface.write("hello\x1b[?1000h\x1b[?1006h");
525 paint();
526 const inputFocus = nativeInputFocus(surface);
527 const row = host.querySelector(".clo-term-row")!;
528 const press = new MouseEvent("mousedown", { bubbles: true, cancelable: true });
529 row.dispatchEvent(press);
530 expect(press.defaultPrevented).toBe(true);
531 expect(document.activeElement).toBe(surface.input);
532 expect(onData).toHaveBeenCalledOnce();
533 row.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
534 onData.mockClear();
535 inputFocus.mockClear();
536 const selectPress = new MouseEvent("mousedown", {
537 bubbles: true,
538 cancelable: true,
539 shiftKey: true,
540 });
541 row.dispatchEvent(selectPress);
542 expect(selectPress.defaultPrevented).toBe(false);
543 expect(document.activeElement).toBe(host);
544 row.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, buttons: 1 }));
545 select(host);
546 const release = new MouseEvent("mouseup", { bubbles: true, cancelable: true });
547 row.dispatchEvent(release);
548 expect(release.defaultPrevented).toBe(false);
549 expect(surface.getSelection()).toBe("hello");
550 expect(inputFocus).not.toHaveBeenCalled();
551 expect(onData).not.toHaveBeenCalled();
552 });
553
554 it("finishes or cancels outside output gestures and removes document listeners on dispose", async () => {
555 const { surface, host } = await mount();
556 surface.write("hello");
557 paint();
558 const row = host.querySelector(".clo-term-row")!;
559 const inputFocus = nativeInputFocus(surface);
560 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
561 document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
562 expect(document.activeElement).toBe(surface.input);
563 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
564 document.dispatchEvent(new Event("pointercancel"));
565 surface.focus();
566 expect(document.activeElement).toBe(surface.input);
567 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
568 host.ownerDocument.defaultView!.dispatchEvent(new Event("blur"));
569 surface.focus();
570 expect(document.activeElement).toBe(surface.input);
571 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
572 inputFocus.mockClear();
573 surface.dispose();
574 document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
575 expect(inputFocus).not.toHaveBeenCalled();
576 });
577
578 it("keeps native copy selection through readonly output gestures but allows explicit paste into writable input", async () => {
579 const { surface, host, onData } = await mount();
580 surface.write("hello\x1b[?2004h");
581 paint();
582 nativeInputFocus(surface);
583 const row = host.querySelector(".clo-term-row")!;
584 surface.input.readOnly = true;
585 row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
586 select(host);
587 row.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
588 key(host, "a");
589 key(host, "Process", { keyCode: 229 });
590 surface.focus();
591 expect(document.activeElement).toBe(host);
592 expect(surface.getSelection()).toBe("hello");
593 expect(onData).not.toHaveBeenCalled();
594 surface.input.readOnly = false;
595 surface.paste("new input");
596 expect(document.activeElement).toBe(surface.input);
597 expect(surface.getSelection()).toBe("");
598 expect(onData).toHaveBeenCalledExactlyOnceWith("\x1b[200~new input\x1b[201~");
599 });
600
601 it("ignores retained async writes and replay after terminal disposal", async () => {
602 const { surface, host, onData } = await mount();
603 let rejectFailure!: (error: Error) => void;
604 const failure = new Promise<void>((_, reject) => {
605 rejectFailure = reject;
606 });
607 const pendingWrite = failure.catch((error: unknown) => surface.write(String(error)));
608 surface.write("hello");
609 surface.dispose();
610 rejectFailure(new Error("late RPC failure"));
611 await expect(pendingWrite).resolves.toBeUndefined();
612 expect(() => surface.resetAndWrite("old history\x1b[6n")).not.toThrow();
613 paint();
614 expect(host.childElementCount).toBe(0);
615 expect(onData).not.toHaveBeenCalled();
616 });
617
618 it("activates complete Windows, UNC and home paths with exact canonical validation", async () => {
619 const paths = [
620 String.raw`C:\repo\src\file.ts`,
621 String.raw`\\server\share\file.ts`,
622 "~/src/file.ts",
623 ];
624 const canonical = new Set([
625 String.raw`C:\repo\src\file.ts`,
626 String.raw`\\server\share\file.ts`,
627 "/Users/clo/src/file.ts",
628 ]);
629 let allowed = true;
630 const validatePath = vi.fn(
631 (path: string) => allowed && canonical.has(resolvePathLinkTarget(path, "/Users/clo/repo")),
632 );
633 const { surface, host, onLinkActivate } = await mount({ validatePath });
634 surface.write(paths.map((path) => `${path}:12:3`).join("\r\n"));
635 paint();
636 const links = [...host.querySelectorAll(".clo-term-link")];
637 expect(links).toHaveLength(3);
638 expect(validatePath.mock.calls.map(([path]) => path)).toEqual(paths);
639 links.forEach((link) => link.dispatchEvent(new MouseEvent("click", { bubbles: true })));
640 expect(onLinkActivate).not.toHaveBeenCalled();
641 links.forEach((link) =>
642 link.dispatchEvent(new MouseEvent("click", { bubbles: true, metaKey: true })),
643 );
644 expect(onLinkActivate.mock.calls.map(([target]) => target)).toEqual(
645 paths.map((path) => `${path}:12:3`),
646 );
647 allowed = false;
648 links[0]!.dispatchEvent(new MouseEvent("click", { bubbles: true, metaKey: true }));
649 expect(onLinkActivate).toHaveBeenCalledTimes(3);
650 });
651
652 it("invalidates a queued frame when hidden without consuming the next visible frame", async () => {
653 const { surface, host } = await mount();
654 surface.write("hello");
655 const stale = [...frames.values()][0]!;
656 frames.clear();
657 surface.setVisible(false);
658 surface.setVisible(true);
659 stale(performance.now());
660 expect(host.textContent).not.toContain("hello");
661 expect(frames.size).toBe(1);
662 surface.write(" world");
663 expect(frames.size).toBe(1);
664 paint();
665 expect(host.textContent).toContain("hello world");
666 });
667
668 it("retains hidden geometry and output until shown, then fits and disposes every DOM node", async () => {
669 const { surface, host, onResize } = await mount({ visible: false });
670 expect(onResize).not.toHaveBeenCalled();
671 surface.write("hello");
672 paint();
673 expect(host.querySelectorAll(".clo-term-row").length).toBe(0);
674 surface.setVisible(true);
675 paint();
676 expect(host.textContent).toContain("hello");
677 expect(onResize).toHaveBeenCalledWith(78, 12);
678 onResize.mockClear();
679 surface.setVisible(false);
680 width = 160;
681 height = 64;
682 expect(surface.fit()).toEqual({ cols: 78, rows: 12 });
683 expect(onResize).not.toHaveBeenCalled();
684 surface.setVisible(true);
685 expect(onResize).toHaveBeenCalledWith(18, 4);
686 surface.dispose();
687 expect(host.childElementCount).toBe(0);
688 expect(() => surface.write("after dispose")).not.toThrow();
689 });
690});
apps/web/src/terminal/TerminalSurface.ts created+276
......@@ -0,0 +1,276 @@
1import { GhosttyModule, mountTerminal, type TerminalHandle } from "@clo/terminal";
2import wasmUrl from "@clo/terminal/ghostty-vt.wasm?url";
3import "@clo/terminal/terminal.css";
4
5import { isTerminalLinkActivation } from "~/terminal-links";
6import { readLocalApi } from "~/localApi";
7import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
8
9export interface TerminalTheme {
10 foreground: string;
11 background: string;
12 cursor: string;
13 palette: readonly string[];
14 selectionBackground: string;
15}
16
17export interface TerminalSurfaceOptions {
18 theme: TerminalTheme;
19 font?: { family?: string; size: number };
20 visible?: boolean;
21 onData: (data: string) => void;
22 onResize: (cols: number, rows: number) => void;
23 beforeKey: (event: KeyboardEvent) => boolean;
24 onLinkActivate: (text: string, event: MouseEvent) => void;
25 validatePath?: (path: string) => boolean;
26 canActivateLink?: (text: string) => boolean;
27 onContextMenu?: (event: MouseEvent) => void;
28}
29
30let modulePromise: Promise<GhosttyModule> | null = null;
31const ANSI_NAMES = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"];
32const ANSI_TOKENS = [...ANSI_NAMES, ...ANSI_NAMES.map((name) => `bright-${name}`)];
33const decoder = new TextDecoder();
34
35function applyTheme(mount: HTMLElement, theme: TerminalTheme) {
36 const style = mount.style;
37 style.setProperty("--clo-term-fg", theme.foreground);
38 style.setProperty("--clo-term-bg", theme.background);
39 style.setProperty("--clo-term-cursor", theme.cursor);
40 style.setProperty("--clo-term-selection", theme.selectionBackground);
41 theme.palette.forEach((color, index) =>
42 style.setProperty(`--clo-term-ansi-${ANSI_TOKENS[index]}`, color),
43 );
44}
45
46export class TerminalSurface {
47 readonly input: HTMLTextAreaElement;
48 private readonly cleanups: (() => void)[] = [];
49 private disposed = false;
50 private replaying = false;
51
52 private constructor(
53 private readonly mount: HTMLElement,
54 private readonly handle: TerminalHandle,
55 private readonly options: TerminalSurfaceOptions,
56 container: HTMLElement,
57 ) {
58 this.input = handle.view.input;
59 this.input.setAttribute("aria-label", mount.getAttribute("aria-label") ?? "Terminal input");
60 const beforeKey = (event: KeyboardEvent) => {
61 if (event.isComposing || event.keyCode === 229) return;
62 const copy =
63 isTerminalLinkActivation(event) && !event.altKey && event.key.toLowerCase() === "c";
64 const selection = copy ? this.getSelection() : "";
65 if (selection !== "") {
66 event.preventDefault();
67 event.stopImmediatePropagation();
68 this.copyText(selection);
69 }
70 };
71 const contextMenu = (event: MouseEvent) => {
72 if (event.defaultPrevented) return;
73 if (options.onContextMenu) {
74 options.onContextMenu(event);
75 return;
76 }
77 const api = readLocalApi();
78 const text = this.getSelection();
79 if (!api || text === "") return;
80 event.preventDefault();
81 void api.contextMenu
82 .show([{ id: "copy", label: "Copy" }], { x: event.clientX, y: event.clientY })
83 .then((action) => {
84 if (action === "copy" && !this.disposed) this.copyText(text);
85 })
86 .catch((error: unknown) => {
87 if (!this.disposed)
88 this.write(
89 `\r\n[terminal] ${error instanceof Error ? error.message : "Unable to open terminal menu"}\r\n`,
90 );
91 });
92 };
93 mount.addEventListener("keydown", beforeKey, true);
94 mount.addEventListener("contextmenu", contextMenu);
95 const focus = () => this.focus();
96 container.addEventListener("focus", focus);
97 this.cleanups.push(() => mount.removeEventListener("keydown", beforeKey, true));
98 this.cleanups.push(() => mount.removeEventListener("contextmenu", contextMenu));
99 this.cleanups.push(() => container.removeEventListener("focus", focus));
100 }
101
102 static async create(container: HTMLElement, options: TerminalSurfaceOptions) {
103 modulePromise ??= GhosttyModule.fetch(wasmUrl).catch((error: unknown) => {
104 modulePromise = null;
105 throw error;
106 });
107 const module = await modulePromise;
108 const mount = container.ownerDocument.createElement("div");
109 mount.style.position = "absolute";
110 mount.style.inset = "0";
111 const label = container.getAttribute("aria-label");
112 if (label !== null) mount.setAttribute("aria-label", label);
113 applyTheme(mount, options.theme);
114 const style = mount.style;
115 style.setProperty("--clo-term-font-size", `${options.font?.size ?? 12}px`);
116 if (options.font?.family) style.setProperty("--clo-term-font", options.font.family);
117 container.append(mount);
118 let surface: TerminalSurface | undefined;
119 let handle: TerminalHandle | undefined;
120 try {
121 handle = mountTerminal(mount, module, {
122 visible: options.visible !== false,
123 onData: (bytes) => {
124 if (!surface?.replaying) options.onData(decoder.decode(bytes));
125 },
126 onResize: ({ cols, rows }) => options.onResize(cols, rows),
127 onBeforeKey: (event) => options.beforeKey(event),
128 maxScrollbackBytes: 16 * 1024 * 1024,
129 links: {
130 ...(options.validatePath ? { validate: options.validatePath } : {}),
131 requireModifier: true,
132 onActivate: (link, event) => {
133 if (!isTerminalLinkActivation(event)) return;
134 if (options.canActivateLink?.(link.target) === false) return;
135 const target =
136 link.kind === "path" && link.line !== null
137 ? `${link.target}:${link.line}${link.column !== null ? `:${link.column}` : ""}`
138 : link.target;
139 options.onLinkActivate(target, event);
140 },
141 },
142 });
143 surface = new TerminalSurface(mount, handle, options, container);
144 if (container.ownerDocument.activeElement === container) surface.focus();
145 await handle.ready;
146 return surface;
147 } catch (error) {
148 if (surface) surface.dispose();
149 else {
150 handle?.dispose();
151 mount.remove();
152 }
153 throw error;
154 }
155 }
156
157 private copyText(text: string) {
158 void writeTextToClipboard(text, "terminal selection").catch((error: unknown) => {
159 if (!this.disposed)
160 this.write(
161 `\r\n[terminal] ${error instanceof Error ? error.message : "Unable to copy terminal selection"}\r\n`,
162 );
163 });
164 }
165
166 write(data: string) {
167 if (this.disposed) return;
168 this.handle.write(data);
169 }
170
171 resetAndWrite(data: string) {
172 if (this.disposed) return;
173 this.clearSelection();
174 this.replaying = true;
175 try {
176 this.handle.write(`\x1bc\x1b[3J${data}`);
177 this.handle.terminal.scrollToBottom();
178 } finally {
179 this.replaying = false;
180 }
181 }
182
183 setTheme(theme: TerminalTheme) {
184 applyTheme(this.mount, theme);
185 this.handle.view.refreshMetrics();
186 }
187
188 setFont(font: { family?: string; size: number }) {
189 this.mount.style.setProperty("--clo-term-font-size", `${font.size}px`);
190 if (font.family) this.mount.style.setProperty("--clo-term-font", font.family);
191 else this.mount.style.removeProperty("--clo-term-font");
192 this.handle.view.refreshMetrics();
193 }
194
195 fit() {
196 this.handle.view.refreshMetrics();
197 return this.handle.view.fit();
198 }
199 resendSize() {
200 const { cols, rows } = this.fit();
201 this.options.onResize(cols, rows);
202 }
203 focus() {
204 this.handle.view.focus();
205 }
206 setVisible(visible: boolean) {
207 this.handle.view.setVisible(visible);
208 }
209 isAtBottom() {
210 const scrollbar = this.handle.terminal.scrollbar();
211 return scrollbar.offset >= scrollbar.total - scrollbar.len;
212 }
213 scrollToBottom() {
214 this.handle.terminal.scrollToBottom();
215 this.handle.view.scheduleFrame();
216 }
217
218 getSelection() {
219 const selection = this.mount.ownerDocument.getSelection();
220 if (
221 !selection ||
222 !this.mount.contains(selection.anchorNode) ||
223 !this.mount.contains(selection.focusNode)
224 )
225 return "";
226 return selection.toString();
227 }
228
229 hasSelection() {
230 return this.getSelection() !== "";
231 }
232
233 getSelectionPosition() {
234 const selection = this.mount.ownerDocument.getSelection();
235 if (!selection || !this.hasSelection()) return null;
236 const row = (node: Node | null) =>
237 (node instanceof Element ? node : node?.parentElement)?.closest<HTMLElement>(".clo-term-row")
238 ?.dataset.screenRow;
239 const start = row(selection.anchorNode);
240 const end = row(selection.focusNode);
241 if (start === undefined || end === undefined) return null;
242 return {
243 start: { y: Math.min(Number(start), Number(end)) },
244 end: { y: Math.max(Number(start), Number(end)) },
245 };
246 }
247
248 clearSelection() {
249 if (this.hasSelection()) this.mount.ownerDocument.getSelection()?.removeAllRanges();
250 }
251
252 selectAll() {
253 const rows = this.mount.querySelector(".clo-term-rows");
254 if (!rows) return;
255 const range = this.mount.ownerDocument.createRange();
256 range.selectNodeContents(rows);
257 const selection = this.mount.ownerDocument.getSelection();
258 selection?.removeAllRanges();
259 selection?.addRange(range);
260 }
261
262 paste(text: string) {
263 if (!this.input.readOnly) {
264 this.input.focus({ preventScroll: true });
265 this.options.onData(decoder.decode(this.handle.terminal.encodePaste(text)));
266 }
267 }
268
269 dispose() {
270 if (this.disposed) return;
271 this.disposed = true;
272 this.cleanups.forEach((cleanup) => cleanup());
273 this.handle.dispose();
274 this.mount.remove();
275 }
276}
apps/web/src/terminal/ghostty/README.md deleted-19
......@@ -1,19 +0,0 @@
1# Ghostty web terminal
2
3This directory is the browser adapter for the same official `libghostty-vt` C ABI used by Android.
4It is intentionally not an xterm compatibility layer.
5
6- `runtime.ts` owns the singleton WebAssembly instance and runtime ABI layouts.
7- `ghostty-write-pty.wasm` is a 112-byte callback trampoline for terminal-generated PTY replies.
8- `core.ts` owns per-terminal Ghostty handles and translates the C ABI into render snapshots.
9- `renderer.ts` batches backgrounds and style runs into a Canvas 2D frame.
10- `surface.ts` owns browser input, IME, selection, scrolling, sizing, links, and cursor blinking.
11- `fonts/` vendors the symbols-only Nerd Font (MIT) the surface registers lazily, so
12 prompt glyphs render without a locally installed Nerd Font.
13- `vendor/` holds only the artifacts, reproducibly generated by
14 `apps/web/scripts/build-libghostty-wasm.sh`. The upstream pin and license live once, at
15 `native/libghostty-vt/` at the repository root; the wasm embeds the pinned revision
16 in its build info and the ABI test verifies it against mobile's `VERSION`.
17
18Keep browser behavior here and terminal transport in the existing client runtime. Do not add React
19state to the render loop. Both WASM artifacts are ordinary read-only assets, not executables.
apps/web/src/terminal/ghostty/core.test.ts deleted-420
......@@ -1,420 +0,0 @@
1import { afterEach, describe, expect, it, vi } from "vite-plus/test";
2import {
3 applyTerminalAttachStreamEvent,
4 DEFAULT_MAX_TERMINAL_BUFFER_BYTES,
5 INITIAL_TERMINAL_OUTPUT_CURSOR,
6 nextTerminalAttachSeedState,
7 readTerminalOutputUpdate,
8 terminalOutputText,
9 type TerminalBufferState,
10} from "@t3tools/client-runtime/state/terminal";
11
12import { writeTerminalOutputUpdate } from "../../components/ThreadTerminalDrawer";
13import { GHOSTTY_CELL_WIDE, GhosttyTerminalCore, ghosttyCellText } from "./core";
14import { loadGhosttyRuntime } from "./runtime";
15
16vi.mock("./vendor/ghostty-vt.wasm?url", async () => ({
17 default: (await import("./vendor/ghostty-vt.wasm?inline")).default,
18}));
19vi.mock("./vendor/ghostty-write-pty.wasm?url&no-inline", async () => ({
20 default: (await import("./vendor/ghostty-write-pty.wasm?inline")).default,
21}));
22
23function codepointView(codepoints: ReadonlyArray<number>): DataView {
24 const view = new DataView(new ArrayBuffer(codepoints.length * 4));
25 codepoints.forEach((codepoint, index) => view.setUint32(index * 4, codepoint, true));
26 return view;
27}
28
29describe("ghosttyCellText", () => {
30 it("converts oversized grapheme clusters without hitting engine spread limits", () => {
31 // A program printing one base character followed by a huge run of
32 // combining marks packs the whole cluster into one cell; spreading that
33 // many arguments into String.fromCodePoint once overflows the call stack.
34 const graphemeLength = 130_000;
35 const view = new DataView(new ArrayBuffer(graphemeLength * 4));
36 for (let index = 0; index < graphemeLength; index += 1) {
37 view.setUint32(index * 4, index === 0 ? "a".codePointAt(0)! : 0x301, true);
38 }
39 const text = ghosttyCellText(view, graphemeLength);
40 expect(text.length).toBe(graphemeLength);
41 expect(text.codePointAt(0)).toBe("a".codePointAt(0));
42 expect(text.codePointAt(1)).toBe(0x301);
43 expect(text.codePointAt(graphemeLength - 1)).toBe(0x301);
44 });
45
46 it("converts small clusters including astral codepoints", () => {
47 const text = ghosttyCellText(codepointView([0x1f642, 0x20e3]), 2);
48 expect([...text]).toEqual(["\u{1F642}", "\u{20E3}"]);
49 });
50
51 it("converts a single astral codepoint", () => {
52 expect(ghosttyCellText(codepointView([0x1f642]), 1)).toBe("🙂");
53 });
54
55 it("returns an empty string for empty cells", () => {
56 expect(ghosttyCellText(codepointView([]), 0)).toBe("");
57 });
58});
59
60describe("GhosttyTerminalCore snapshots", () => {
61 const cores = new Set<GhosttyTerminalCore>();
62
63 async function createCore(onData: (data: string) => void = () => {}) {
64 const core = await GhosttyTerminalCore.create(
65 12,
66 3,
67 8,
68 16,
69 {
70 foreground: { r: 255, g: 255, b: 255 },
71 background: { r: 0, g: 0, b: 0 },
72 cursor: { r: 255, g: 255, b: 255 },
73 },
74 onData,
75 );
76 cores.add(core);
77 return core;
78 }
79
80 function createSession(history: string) {
81 return applyTerminalAttachStreamEvent(nextTerminalAttachSeedState(), {
82 type: "snapshot",
83 snapshot: {
84 threadId: "terminal-stream-test",
85 terminalId: "term-1",
86 cwd: "/repo",
87 worktreePath: null,
88 status: "running",
89 pid: 123,
90 history,
91 exitCode: null,
92 exitSignal: null,
93 label: "Terminal",
94 updatedAt: "2026-09-04T00:00:00.000Z",
95 },
96 });
97 }
98
99 function append(state: TerminalBufferState, data: string) {
100 return applyTerminalAttachStreamEvent(state, {
101 type: "output",
102 threadId: "terminal-stream-test",
103 terminalId: "term-1",
104 data,
105 });
106 }
107
108 afterEach(() => {
109 for (const core of cores) core.dispose();
110 cores.clear();
111 vi.restoreAllMocks();
112 });
113
114 it("preserves styles, wide cells, and selection after shared memory grows", async () => {
115 const core = await createCore();
116 const runtime = await loadGhosttyRuntime();
117 const grapheme = `e${"\u0301".repeat(64)}`;
118 core.write(`\x1b[1;3;4;8;9;53;38;2;123;45;67;48;2;9;8;7m${grapheme}\x1b[0m界🙂`);
119 const cells = core.snapshot().rowData[0]!.cells;
120 expect(cells[0]).toEqual({
121 text: grapheme,
122 wide: 0,
123 foreground: { r: 123, g: 45, b: 67 },
124 background: { r: 9, g: 8, b: 7 },
125 bold: true,
126 italic: true,
127 invisible: true,
128 strikethrough: true,
129 overline: true,
130 underline: true,
131 selected: false,
132 });
133 expect(cells.slice(1, 5).map(({ text, wide }) => ({ text, wide }))).toEqual([
134 { text: "界", wide: 0 },
135 { text: "", wide: GHOSTTY_CELL_WIDE.spacerTail },
136 { text: "🙂", wide: 0 },
137 { text: "", wide: GHOSTTY_CELL_WIDE.spacerTail },
138 ]);
139
140 runtime.memory.grow(1);
141 core.setSelection({ x: 0, y: 0 }, { x: 2, y: 0 });
142 expect(core.snapshot().rowData[0]!.cells[0]).toEqual({ ...cells[0], selected: true });
143 core.clearSelection();
144 expect(core.snapshot().rowData[0]!.cells[0]).toEqual(cells[0]);
145
146 core.resetAndWrite("\x1b[2;7;38;2;40;100;200;48;2;12;34;56mC\x1b[0m");
147 expect(core.snapshot().rowData[0]!.cells[0]).toMatchObject({
148 text: "C",
149 foreground: { r: 22, g: 59, b: 112 },
150 background: { r: 40, g: 100, b: 200 },
151 bold: false,
152 underline: false,
153 selected: false,
154 });
155 });
156
157 it("resolves foreground and background independently without retaining prior cell colors", async () => {
158 const core = await createCore();
159 core.write(
160 "\x1b[38;2;123;45;67mF\x1b[39;48;2;9;8;7mB\x1b[0mD\x1b[38;2;12;34;56;48;2;78;90;123mC\x1b[0mE",
161 );
162 expect(
163 core
164 .snapshot()
165 .rowData[0]!.cells.slice(0, 6)
166 .map(({ text, foreground, background }) => ({ text, foreground, background })),
167 ).toEqual([
168 { text: "F", foreground: { r: 123, g: 45, b: 67 }, background: { r: 0, g: 0, b: 0 } },
169 { text: "B", foreground: { r: 255, g: 255, b: 255 }, background: { r: 9, g: 8, b: 7 } },
170 { text: "D", foreground: { r: 255, g: 255, b: 255 }, background: { r: 0, g: 0, b: 0 } },
171 { text: "C", foreground: { r: 12, g: 34, b: 56 }, background: { r: 78, g: 90, b: 123 } },
172 { text: "E", foreground: { r: 255, g: 255, b: 255 }, background: { r: 0, g: 0, b: 0 } },
173 { text: "", foreground: { r: 255, g: 255, b: 255 }, background: { r: 0, g: 0, b: 0 } },
174 ]);
175 });
176
177 it("paints ANSI colors from the theme palette and repaints them on theme change", async () => {
178 const core = await createCore();
179 const theme = {
180 foreground: { r: 255, g: 255, b: 255 },
181 background: { r: 0, g: 0, b: 0 },
182 cursor: { r: 255, g: 255, b: 255 },
183 };
184 const palette = Array.from({ length: 16 }, (_, index) => ({ r: index, g: 1, b: 2 }));
185 core.write("\x1b[31mR\x1b[38;5;196mX\x1b[0m");
186 core.setTheme({ ...theme, palette });
187 let cells = core.snapshot().rowData[0]!.cells;
188 expect(cells[0]!.foreground).toEqual({ r: 1, g: 1, b: 2 });
189 expect(cells[1]!.foreground).toEqual({ r: 255, g: 0, b: 0 });
190
191 core.setTheme({ ...theme, palette: palette.map((color) => ({ ...color, g: 9 })) });
192 cells = core.snapshot().rowData[0]!.cells;
193 expect(cells[0]!.foreground).toEqual({ r: 1, g: 9, b: 2 });
194 expect(cells[1]!.foreground).toEqual({ r: 255, g: 0, b: 0 });
195 });
196
197 it("reuses a grown grapheme buffer and releases it on disposal", async () => {
198 const core = await createCore();
199 const runtime = await loadGhosttyRuntime();
200 core.write("ASCII");
201 core.snapshot();
202
203 const grapheme = `z${"\u0301".repeat(256)}`;
204 core.resetAndWrite(`${grapheme}X`);
205 const alloc = vi.spyOn(runtime, "alloc");
206 const free = vi.spyOn(runtime, "free");
207 expect(
208 core
209 .snapshot()
210 .rowData[0]!.cells.slice(0, 2)
211 .map((cell) => cell.text),
212 ).toEqual([grapheme, "X"]);
213 expect(alloc).toHaveBeenCalledTimes(1);
214 const allocation = alloc.mock.results[0]!;
215 if (allocation.type !== "return") throw new Error("Grapheme allocation did not return");
216 const buffer = allocation.value;
217 const capacity = alloc.mock.calls[0]![0];
218
219 core.write("\rQ\u0301");
220 alloc.mockClear();
221 expect(core.snapshot().rowData[0]!.cells[0]!.text).toBe("Q\u0301");
222 expect(alloc).not.toHaveBeenCalled();
223 core.dispose();
224 expect(free).toHaveBeenCalledWith(buffer, capacity);
225 });
226
227 it.each(["varied", "identical"] as const)(
228 "preserves Ghostty state through a full MiB of %s output without rollover resets",
229 async (kind) => {
230 const [core, reference] = await Promise.all([createCore(), createCore()]);
231 const initial = "\x1b[31m";
232 let state = createSession(initial);
233 const first = readTerminalOutputUpdate(state.output, INITIAL_TERMINAL_OUTPUT_CURSOR);
234 writeTerminalOutputUpdate(core, first);
235 reference.resetAndWrite(initial);
236 let cursor = first.cursor;
237 const reset = vi.spyOn(core, "resetAndWrite");
238 const inputs: string[] = [];
239 let receivedCharacters = 0;
240
241 for (let index = 0; index < 128; index += 1) {
242 const data =
243 kind === "identical"
244 ? "x".repeat(8192)
245 : `${index.toString().padStart(4, "0")}\r\n${"x".repeat(8186)}`;
246 inputs.push(data);
247 state = append(state, data);
248 const update = readTerminalOutputUpdate(state.output, cursor);
249 if (update.type !== "append") throw new Error(`Expected append, received ${update.type}`);
250 receivedCharacters += update.data.length;
251 writeTerminalOutputUpdate(core, update);
252 cursor = update.cursor;
253 }
254
255 reference.write(inputs.join(""));
256 expect(receivedCharacters).toBe(1024 * 1024);
257 expect(state.output.retainedBytes).toBe(DEFAULT_MAX_TERMINAL_BUFFER_BYTES);
258 expect(reset).not.toHaveBeenCalled();
259 expect(core.snapshot()).toEqual(reference.snapshot());
260 },
261 );
262
263 it("preserves Unicode and split ANSI parser state across batched renderer reads", async () => {
264 const [core, reference] = await Promise.all([createCore(), createCore()]);
265 let state = createSession("");
266 const first = readTerminalOutputUpdate(state.output, INITIAL_TERMINAL_OUTPUT_CURSOR);
267 writeTerminalOutputUpdate(core, first);
268 let cursor = first.cursor;
269 const reset = vi.spyOn(core, "resetAndWrite");
270 const inputs = [
271 `${"a".repeat(16_383)}🙂`,
272 "\x1b[3",
273 "1m",
274 "e",
275 "\u0301界🙂",
276 "\x1b[0",
277 "m\r\n",
278 "\x1b]8;;https://t3.codes\x1b",
279 "\\link",
280 "\x1b]8;;\x1b",
281 "\\\x1b[?1049h",
282 "alternate",
283 "\x1b[?1049l",
284 "\r\nend",
285 ];
286 let received = "";
287 for (const [index, data] of inputs.entries()) {
288 state = append(state, data);
289 if (index % 3 !== 0 && index !== inputs.length - 1) continue;
290 const update = readTerminalOutputUpdate(state.output, cursor);
291 if (update.type !== "append") throw new Error(`Expected append, received ${update.type}`);
292 received += update.data;
293 writeTerminalOutputUpdate(core, update);
294 cursor = update.cursor;
295 }
296
297 reference.write(inputs.join(""));
298 expect(received).toBe(inputs.join(""));
299 expect(reset).not.toHaveBeenCalled();
300 expect(core.snapshot()).toEqual(reference.snapshot());
301 });
302
303 it("answers a live VT query when batched reads cross a chunk compaction", async () => {
304 const replies: string[] = [];
305 const core = await createCore((data) => replies.push(data));
306 core.write("\x1b[5n");
307 expect(replies).toEqual(["\x1b[0n"]);
308 replies.length = 0;
309
310 let state = createSession("");
311 const initial = readTerminalOutputUpdate(state.output, INITIAL_TERMINAL_OUTPUT_CURSOR);
312 writeTerminalOutputUpdate(core, initial);
313 let cursor = initial.cursor;
314 for (let index = 0; index < 1000; index += 1) {
315 state = append(state, "x");
316 const update = readTerminalOutputUpdate(state.output, cursor);
317 writeTerminalOutputUpdate(core, update);
318 cursor = update.cursor;
319 }
320 for (let index = 0; index < 24; index += 1) state = append(state, "x");
321 state = append(state, "\x1b[5n");
322 const update = readTerminalOutputUpdate(state.output, cursor);
323 writeTerminalOutputUpdate(core, update);
324
325 expect({ type: update.type, replies }).toEqual({ type: "append", replies: ["\x1b[0n"] });
326 });
327
328 it("recovers a lagging renderer once from bounded output and resumes appending", async () => {
329 const [core, reference] = await Promise.all([createCore(), createCore()]);
330 let state = createSession("\x1b[31mold");
331 const initial = readTerminalOutputUpdate(state.output, INITIAL_TERMINAL_OUTPUT_CURSOR);
332 writeTerminalOutputUpdate(core, initial);
333 const reset = vi.spyOn(core, "resetAndWrite");
334 const data = "line\r\n".repeat(8192);
335 for (let index = 0; index < 16; index += 1) state = append(state, data);
336
337 const recovery = readTerminalOutputUpdate(state.output, initial.cursor);
338 if (recovery.type !== "reset") throw new Error(`Expected reset, received ${recovery.type}`);
339 expect(new TextEncoder().encode(recovery.data).byteLength).toBe(
340 DEFAULT_MAX_TERMINAL_BUFFER_BYTES,
341 );
342 writeTerminalOutputUpdate(core, recovery);
343 reference.resetAndWrite(recovery.data);
344 expect(core.snapshot()).toEqual(reference.snapshot());
345
346 state = append(state, "\r\nlatest");
347 const next = readTerminalOutputUpdate(state.output, recovery.cursor);
348 expect(next.type).toBe("append");
349 writeTerminalOutputUpdate(core, next);
350 reference.write("\r\nlatest");
351 expect(reset).toHaveBeenCalledTimes(1);
352 expect(core.snapshot()).toEqual(reference.snapshot());
353 });
354
355 it("replays the latest retained output when WASM arrives after several events", async () => {
356 const pendingCore = createCore();
357 let state = createSession("before");
358 state = append(state, "\r\nduring ");
359 state = append(state, "🙂 load");
360 const core = await pendingCore;
361 const first = readTerminalOutputUpdate(state.output, INITIAL_TERMINAL_OUTPUT_CURSOR);
362 writeTerminalOutputUpdate(core, first);
363 const reference = await createCore();
364 reference.resetAndWrite(terminalOutputText(state.output));
365 const reset = vi.spyOn(core, "resetAndWrite");
366
367 state = append(state, "\r\nafter");
368 const next = readTerminalOutputUpdate(state.output, first.cursor);
369 expect(next).toMatchObject({ type: "append", data: "\r\nafter" });
370 writeTerminalOutputUpdate(core, next);
371 reference.write("\r\nafter");
372 expect(reset).not.toHaveBeenCalled();
373 expect(core.snapshot()).toEqual(reference.snapshot());
374 });
375
376 it("resets real Ghostty for a repeated snapshot, clear, restart, and a fresh attach", async () => {
377 const [core, reference] = await Promise.all([createCore(), createCore()]);
378 let state = createSession("hello");
379 const initial = readTerminalOutputUpdate(state.output, INITIAL_TERMINAL_OUTPUT_CURSOR);
380 writeTerminalOutputUpdate(core, initial);
381 let cursor = initial.cursor;
382
383 const snapshot = {
384 threadId: "terminal-stream-test",
385 terminalId: "term-1",
386 cwd: "/repo",
387 worktreePath: null,
388 status: "running" as const,
389 pid: 456,
390 history: "hello",
391 exitCode: null,
392 exitSignal: null,
393 label: "Terminal",
394 updatedAt: "2026-09-04T00:00:01.000Z",
395 };
396 const events = [
397 { type: "snapshot", snapshot },
398 { type: "cleared", threadId: snapshot.threadId, terminalId: snapshot.terminalId },
399 { type: "restarted", threadId: snapshot.threadId, terminalId: snapshot.terminalId, snapshot },
400 ] as const;
401 for (const event of events) {
402 core.write("\r\nstale local text");
403 state = applyTerminalAttachStreamEvent(state, event);
404 const update = readTerminalOutputUpdate(state.output, cursor);
405 expect(update.type).toBe("reset");
406 writeTerminalOutputUpdate(core, update);
407 cursor = update.cursor;
408 reference.resetAndWrite(event.type === "cleared" ? "" : "hello");
409 expect(core.snapshot()).toEqual(reference.snapshot());
410 }
411
412 core.write("\r\nstale local text");
413 state = createSession("hello");
414 const reattached = readTerminalOutputUpdate(state.output, cursor);
415 expect(reattached.type).toBe("reset");
416 writeTerminalOutputUpdate(core, reattached);
417 reference.resetAndWrite("hello");
418 expect(core.snapshot()).toEqual(reference.snapshot());
419 });
420});
apps/web/src/terminal/ghostty/core.ts deleted-1290
......@@ -1,1290 +0,0 @@
1import {
2 type GhosttyKeyboardLayoutMap,
3 ghosttyConsumedMods,
4 ghosttyKeyForCode,
5 ghosttyUnshiftedCodepoint,
6 loadGhosttyKeyboardLayoutMap,
7} from "./keyCodes";
8import { GhosttyRuntime, loadGhosttyRuntime } from "./runtime";
9
10const GHOSTTY_SUCCESS = 0;
11const GHOSTTY_OUT_OF_SPACE = -3;
12const MAX_SCROLLBACK_ROWS = 10_000;
13// wasm32 C ABI layout for GhosttyTerminalSelectionFormatOptions at the
14// libghostty-vt revision pinned alongside this module.
15const SELECTION_FORMAT_OPTIONS_SIZE = 16;
16const PALETTE_BYTES = 256 * 3;
17
18const RENDER_DATA = {
19 cols: 1,
20 rows: 2,
21 dirty: 3,
22 rowIterator: 4,
23 background: 5,
24 foreground: 6,
25 cursor: 7,
26 cursorHasValue: 8,
27 cursorStyle: 10,
28 cursorVisible: 11,
29 cursorBlinking: 12,
30 cursorInViewport: 14,
31 cursorX: 15,
32 cursorY: 16,
33} as const;
34
35const ROW_DATA = {
36 dirty: 1,
37 raw: 2,
38 cells: 3,
39} as const;
40
41const CELL_DATA = {
42 raw: 1,
43 style: 2,
44 graphemesLength: 3,
45 graphemes: 4,
46 background: 5,
47 foreground: 6,
48 selected: 7,
49} as const;
50
51// wasm32 arrays for the official row_cells_get_multi API. Optional colors
52// come last because an unset color stops the query at that field.
53const CELL_QUERY = {
54 keys: 0,
55 values: 20,
56 graphemesLength: 40,
57 selected: 44,
58 background: 48,
59 foreground: 52,
60 written: 56,
61 size: 60,
62} as const;
63
64const RAW_CELL_DATA = {
65 wide: 3,
66} as const;
67
68export const GHOSTTY_CELL_WIDE = {
69 narrow: 0,
70 wide: 1,
71 spacerTail: 2,
72 spacerHead: 3,
73} as const;
74
75export interface GhosttyColor {
76 readonly r: number;
77 readonly g: number;
78 readonly b: number;
79}
80
81export interface GhosttyTheme {
82 readonly foreground: GhosttyColor;
83 readonly background: GhosttyColor;
84 readonly cursor: GhosttyColor;
85 readonly palette?: readonly GhosttyColor[];
86 /** CSS color the renderer overlays on selected cells; not sent to Ghostty. */
87 readonly selectionBackground?: string;
88}
89
90export interface GhosttyCell {
91 readonly text: string;
92 readonly wide: number;
93 readonly foreground: GhosttyColor;
94 readonly background: GhosttyColor;
95 readonly bold: boolean;
96 readonly italic: boolean;
97 readonly invisible: boolean;
98 readonly strikethrough: boolean;
99 readonly overline: boolean;
100 readonly underline: boolean;
101 readonly selected: boolean;
102}
103
104export interface GhosttyRow {
105 readonly cells: readonly GhosttyCell[];
106 readonly text: string;
107 readonly isWrapContinuation: boolean;
108 /** Whether this row soft-wraps onto the next row. */
109 readonly wrapsToNext: boolean;
110}
111
112export interface GhosttySnapshot {
113 readonly cols: number;
114 readonly rows: number;
115 readonly foreground: GhosttyColor;
116 readonly background: GhosttyColor;
117 readonly cursor: GhosttyColor;
118 readonly cursorX: number;
119 readonly cursorY: number;
120 readonly cursorVisible: boolean;
121 readonly cursorBlinking: boolean;
122 readonly cursorStyle: number;
123 readonly dirtyRows: ReadonlySet<number>;
124 readonly rowData: readonly GhosttyRow[];
125}
126
127export interface GhosttySelectionRange {
128 readonly viewport: {
129 readonly start: { readonly x: number; readonly y: number };
130 readonly end: { readonly x: number; readonly y: number };
131 };
132 readonly screen: {
133 readonly start: { readonly x: number; readonly y: number };
134 readonly end: { readonly x: number; readonly y: number };
135 };
136}
137
138export interface GhosttyScrollbar {
139 readonly total: number;
140 readonly offset: number;
141 readonly len: number;
142}
143
144/** Grid position tagged with its Ghostty coordinate space: 1 viewport, 2 screen. */
145export interface GhosttyPointInput {
146 readonly x: number;
147 readonly y: number;
148 readonly tag?: 1 | 2;
149}
150
151export interface GhosttyMouseInput {
152 readonly action: "press" | "release" | "motion";
153 readonly button: number | null;
154 readonly mods: number;
155 readonly x: number;
156 readonly y: number;
157 readonly screenWidth: number;
158 readonly screenHeight: number;
159 readonly cellWidth: number;
160 readonly cellHeight: number;
161 readonly paddingLeft: number;
162 readonly paddingRight: number;
163 readonly paddingTop: number;
164 readonly paddingBottom: number;
165 readonly anyButtonPressed: boolean;
166}
167
168const decoder = new TextDecoder();
169const encoder = new TextEncoder();
170
171function blend(foreground: GhosttyColor, background: GhosttyColor): GhosttyColor {
172 const channel = (front: number, back: number) => Math.floor((front * 155 + back * 100) / 255);
173 return {
174 r: channel(foreground.r, background.r),
175 g: channel(foreground.g, background.g),
176 b: channel(foreground.b, background.b),
177 };
178}
179
180function sameColor(left: GhosttyColor, right: GhosttyColor): boolean {
181 return left.r === right.r && left.g === right.g && left.b === right.b;
182}
183
184/**
185 * A terminal program can print one base character followed by a huge run of
186 * combining marks, packing hundreds of thousands of codepoints into a single
187 * cell that still fits the scrollback buffer. Engines cap spread-call
188 * arguments far below that, so convert in bounded chunks instead of spreading
189 * every codepoint into String.fromCodePoint at once.
190 */
191export function ghosttyCellText(codepointView: DataView, graphemeLength: number): string {
192 if (graphemeLength === 1) return String.fromCodePoint(codepointView.getUint32(0, true));
193 const CHUNK_SIZE = 4_096;
194 let text = "";
195 for (let start = 0; start < graphemeLength; start += CHUNK_SIZE) {
196 const count = Math.min(CHUNK_SIZE, graphemeLength - start);
197 const codes = new Array<number>(count);
198 for (let index = 0; index < count; index += 1) {
199 codes[index] = codepointView.getUint32((start + index) * 4, true);
200 }
201 text += String.fromCodePoint(...codes);
202 }
203 return text;
204}
205
206export class GhosttyTerminalCore {
207 private readonly runtime: GhosttyRuntime;
208 private terminalSlot = 0;
209 private terminal = 0;
210 private renderStateSlot = 0;
211 private renderState = 0;
212 private rowIteratorSlot = 0;
213 private rowCellsSlot = 0;
214 private keyEncoderSlot = 0;
215 private keyEncoder = 0;
216 private keyEventSlot = 0;
217 private keyEvent = 0;
218 private mouseEncoderSlot = 0;
219 private mouseEncoder = 0;
220 private mouseEventSlot = 0;
221 private mouseEvent = 0;
222 private ptyWriterId = 0;
223 private ptyWriter: ((data: string) => void) | null = null;
224 private scratch = 0;
225 private graphemes = 0;
226 private graphemeCapacity = 0;
227 private style = 0;
228 private cellQuery = 0;
229 private scrollbar = 0;
230 private rows: GhosttyRow[] = [];
231 private disposed = false;
232 private keyboardLayoutMap: GhosttyKeyboardLayoutMap | undefined;
233
234 private constructor(runtime: GhosttyRuntime) {
235 this.runtime = runtime;
236 void loadGhosttyKeyboardLayoutMap().then((layoutMap) => {
237 if (!this.disposed) this.keyboardLayoutMap = layoutMap;
238 });
239 }
240
241 static async create(
242 cols: number,
243 rows: number,
244 cellWidth: number,
245 cellHeight: number,
246 theme: GhosttyTheme,
247 onPtyData: (data: string) => void,
248 ): Promise<GhosttyTerminalCore> {
249 const core = new GhosttyTerminalCore(await loadGhosttyRuntime());
250 try {
251 core.initialize(cols, rows, cellWidth, cellHeight, theme, onPtyData);
252 return core;
253 } catch (error) {
254 core.dispose();
255 throw error;
256 }
257 }
258
259 private initialize(
260 cols: number,
261 rows: number,
262 cellWidth: number,
263 cellHeight: number,
264 theme: GhosttyTheme,
265 onPtyData: (data: string) => void,
266 ): void {
267 const optionsSize = this.runtime.layout("GhosttyTerminalOptions").size;
268 const options = this.runtime.alloc(optionsSize);
269 this.runtime.setField(options, "GhosttyTerminalOptions", "cols", cols);
270 this.runtime.setField(options, "GhosttyTerminalOptions", "rows", rows);
271 this.runtime.setField(options, "GhosttyTerminalOptions", "max_scrollback", MAX_SCROLLBACK_ROWS);
272 this.terminalSlot = this.runtime.allocOpaque();
273 const terminalResult = this.runtime.call("ghostty_terminal_new", 0, this.terminalSlot, options);
274 this.runtime.free(options, optionsSize);
275 this.assertSuccess("ghostty_terminal_new", terminalResult);
276 this.terminal = this.runtime.readPointer(this.terminalSlot);
277 this.applyDefaultCursorBlink();
278 this.ptyWriter = onPtyData;
279 this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, onPtyData);
280
281 this.renderStateSlot = this.runtime.allocOpaque();
282 this.assertSuccess(
283 "ghostty_render_state_new",
284 this.runtime.call("ghostty_render_state_new", 0, this.renderStateSlot),
285 );
286 this.renderState = this.runtime.readPointer(this.renderStateSlot);
287
288 this.rowIteratorSlot = this.runtime.allocOpaque();
289 this.assertSuccess(
290 "ghostty_render_state_row_iterator_new",
291 this.runtime.call("ghostty_render_state_row_iterator_new", 0, this.rowIteratorSlot),
292 );
293 this.rowCellsSlot = this.runtime.allocOpaque();
294 this.assertSuccess(
295 "ghostty_render_state_row_cells_new",
296 this.runtime.call("ghostty_render_state_row_cells_new", 0, this.rowCellsSlot),
297 );
298
299 this.keyEncoderSlot = this.runtime.allocOpaque();
300 this.assertSuccess(
301 "ghostty_key_encoder_new",
302 this.runtime.call("ghostty_key_encoder_new", 0, this.keyEncoderSlot),
303 );
304 this.keyEncoder = this.runtime.readPointer(this.keyEncoderSlot);
305 this.keyEventSlot = this.runtime.allocOpaque();
306 this.assertSuccess(
307 "ghostty_key_event_new",
308 this.runtime.call("ghostty_key_event_new", 0, this.keyEventSlot),
309 );
310 this.keyEvent = this.runtime.readPointer(this.keyEventSlot);
311
312 this.mouseEncoderSlot = this.runtime.allocOpaque();
313 this.assertSuccess(
314 "ghostty_mouse_encoder_new",
315 this.runtime.call("ghostty_mouse_encoder_new", 0, this.mouseEncoderSlot),
316 );
317 this.mouseEncoder = this.runtime.readPointer(this.mouseEncoderSlot);
318 this.mouseEventSlot = this.runtime.allocOpaque();
319 this.assertSuccess(
320 "ghostty_mouse_event_new",
321 this.runtime.call("ghostty_mouse_event_new", 0, this.mouseEventSlot),
322 );
323 this.mouseEvent = this.runtime.readPointer(this.mouseEventSlot);
324
325 this.scratch = this.runtime.alloc(16);
326 const styleSize = this.runtime.layout("GhosttyStyle").size;
327 this.style = this.runtime.alloc(styleSize);
328 this.runtime.setField(this.style, "GhosttyStyle", "size", styleSize);
329 this.cellQuery = this.runtime.alloc(CELL_QUERY.size);
330 const queryView = this.runtime.memoryDataView();
331 const cellFields = [
332 [CELL_DATA.style, this.style],
333 [CELL_DATA.graphemesLength, this.cellQuery + CELL_QUERY.graphemesLength],
334 [CELL_DATA.selected, this.cellQuery + CELL_QUERY.selected],
335 [CELL_DATA.background, this.cellQuery + CELL_QUERY.background],
336 [CELL_DATA.foreground, this.cellQuery + CELL_QUERY.foreground],
337 ] as const;
338 for (const [index, [key, pointer]] of cellFields.entries()) {
339 queryView.setUint32(this.cellQuery + CELL_QUERY.keys + index * 4, key, true);
340 queryView.setUint32(this.cellQuery + CELL_QUERY.values + index * 4, pointer, true);
341 }
342 this.scrollbar = this.runtime.alloc(this.runtime.layout("GhosttyTerminalScrollbar").size);
343 this.setTheme(theme);
344 this.resize(cols, rows, cellWidth, cellHeight);
345 }
346
347 write(data: string | Uint8Array): void {
348 this.ensureActive();
349 const bytes = typeof data === "string" ? encoder.encode(data) : data;
350 if (bytes.length === 0) return;
351 const pointer = this.runtime.alloc(bytes.length);
352 this.runtime.bytes(pointer, bytes.length).set(bytes);
353 this.runtime.call("ghostty_terminal_vt_write", this.terminal, pointer, bytes.length);
354 this.runtime.free(pointer, bytes.length);
355 }
356
357 resetAndWrite(data: string): void {
358 this.ensureActive();
359 this.runtime.call("ghostty_terminal_reset", this.terminal);
360 // RIS returns the cursor to Ghostty's built-in steady default, so the
361 // embedder default has to be applied again before the replay runs.
362 this.applyDefaultCursorBlink();
363 this.rows = [];
364 if (data.length === 0) return;
365 const writer = this.ptyWriter;
366 if (this.ptyWriterId !== 0) {
367 this.runtime.detachPtyWriter(this.terminal, this.ptyWriterId);
368 this.ptyWriterId = 0;
369 }
370 try {
371 this.write(data);
372 } finally {
373 if (writer !== null && !this.disposed) {
374 this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, writer);
375 }
376 }
377 }
378
379 resize(cols: number, rows: number, cellWidth: number, cellHeight: number): void {
380 this.ensureActive();
381 this.assertSuccess(
382 "ghostty_terminal_resize",
383 this.runtime.call(
384 "ghostty_terminal_resize",
385 this.terminal,
386 Math.max(1, Math.min(65_535, cols)),
387 Math.max(1, Math.min(65_535, rows)),
388 Math.max(1, Math.round(cellWidth)),
389 Math.max(1, Math.round(cellHeight)),
390 ),
391 );
392 }
393
394 /**
395 * Ghostty's built-in default cursor is steady, while the xterm.js renderer
396 * this replaced ran with `cursorBlink: true`. Option 23 is the embedder's
397 * default blink, which is the state a session starts in and returns to on
398 * DECSCUSR reset (CSI 0 q), so programs that ask for a specific cursor
399 * through DECSCUSR or DEC mode 12 still win.
400 */
401 private applyDefaultCursorBlink(): void {
402 const blink = this.runtime.alloc(1);
403 this.runtime.bytes(blink, 1)[0] = 1;
404 this.runtime.call("ghostty_terminal_set", this.terminal, 23, blink);
405 this.runtime.free(blink, 1);
406 }
407
408 setTheme(theme: GhosttyTheme): void {
409 this.ensureActive();
410 const color = this.runtime.alloc(3);
411 for (const [option, value] of [
412 [11, theme.foreground],
413 [12, theme.background],
414 [13, theme.cursor],
415 ] as const) {
416 this.runtime.bytes(color, 3).set([value.r, value.g, value.b]);
417 this.runtime.call("ghostty_terminal_set", this.terminal, option, color);
418 }
419 this.runtime.free(color, 3);
420 if (theme.palette) this.setPalette(theme.palette);
421 }
422
423 private setPalette(colors: readonly GhosttyColor[]): void {
424 // Ghostty takes all 256 entries; keep its defaults for 16-255.
425 const palette = this.runtime.alloc(PALETTE_BYTES);
426 this.runtime.call("ghostty_terminal_get", this.terminal, 25, palette);
427 const bytes = this.runtime.bytes(palette, PALETTE_BYTES);
428 colors.slice(0, 16).forEach((color, index) => {
429 bytes.set([color.r, color.g, color.b], index * 3);
430 });
431 this.runtime.call("ghostty_terminal_set", this.terminal, 14, palette);
432 this.runtime.free(palette, PALETTE_BYTES);
433 }
434
435 scroll(deltaRows: number): void {
436 this.ensureActive();
437 const layout = this.runtime.layout("GhosttyTerminalScrollViewport");
438 const scroll = this.runtime.alloc(layout.size);
439 this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 2);
440 const value = layout.fields.value!;
441 this.runtime.view(scroll + value.offset, value.size).setInt32(0, deltaRows, true);
442 this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll);
443 this.runtime.free(scroll, layout.size);
444 }
445
446 scrollToBottom(): void {
447 this.ensureActive();
448 const layout = this.runtime.layout("GhosttyTerminalScrollViewport");
449 const scroll = this.runtime.alloc(layout.size);
450 this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 1);
451 this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll);
452 this.runtime.free(scroll, layout.size);
453 }
454
455 isViewportActive(): boolean {
456 this.ensureActive();
457 this.runtime.bytes(this.scratch, 1)[0] = 0;
458 return (
459 this.runtime.call("ghostty_terminal_get", this.terminal, 32, this.scratch) ===
460 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
461 );
462 }
463
464 scrollbarState(): GhosttyScrollbar | null {
465 this.ensureActive();
466 const layout = this.runtime.layout("GhosttyTerminalScrollbar");
467 this.runtime.bytes(this.scrollbar, layout.size).fill(0);
468 if (
469 this.runtime.call("ghostty_terminal_get", this.terminal, 9, this.scrollbar) !==
470 GHOSTTY_SUCCESS
471 ) {
472 return null;
473 }
474 return {
475 total: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "total"),
476 offset: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "offset"),
477 len: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "len"),
478 };
479 }
480
481 isMouseTracking(): boolean {
482 this.ensureActive();
483 this.runtime.bytes(this.scratch, 1)[0] = 0;
484 return (
485 this.runtime.call("ghostty_terminal_get", this.terminal, 11, this.scratch) ===
486 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
487 );
488 }
489
490 isMouseAnyEventTracking(): boolean {
491 this.ensureActive();
492 this.runtime.bytes(this.scratch, 1)[0] = 0;
493 return (
494 this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1003, this.scratch) ===
495 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
496 );
497 }
498
499 isAlternateScreen(): boolean {
500 this.ensureActive();
501 this.runtime.bytes(this.scratch, 4).fill(0);
502 return (
503 this.runtime.call("ghostty_terminal_get", this.terminal, 6, this.scratch) ===
504 GHOSTTY_SUCCESS && this.runtime.view(this.scratch, 4).getUint32(0, true) === 1
505 );
506 }
507
508 isApplicationCursorKeys(): boolean {
509 this.ensureActive();
510 this.runtime.bytes(this.scratch, 1)[0] = 0;
511 return (
512 this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1, this.scratch) ===
513 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
514 );
515 }
516
517 encodeKey(event: KeyboardEvent, action: "press" | "release" = "press"): string {
518 this.ensureActive();
519 this.runtime.call("ghostty_key_encoder_setopt_from_terminal", this.keyEncoder, this.terminal);
520 this.runtime.call(
521 "ghostty_key_event_set_action",
522 this.keyEvent,
523 action === "release" ? 0 : event.repeat ? 2 : 1,
524 );
525 this.runtime.call("ghostty_key_event_set_key", this.keyEvent, ghosttyKeyForCode(event.code));
526 const mods =
527 (event.shiftKey ? 1 : 0) |
528 (event.ctrlKey ? 1 << 1 : 0) |
529 (event.altKey ? 1 << 2 : 0) |
530 (event.metaKey ? 1 << 3 : 0) |
531 (event.getModifierState("CapsLock") ? 1 << 4 : 0) |
532 (event.getModifierState("NumLock") ? 1 << 5 : 0);
533 this.runtime.call("ghostty_key_event_set_mods", this.keyEvent, mods);
534 this.runtime.call(
535 "ghostty_key_event_set_consumed_mods",
536 this.keyEvent,
537 ghosttyConsumedMods(event),
538 );
539 this.runtime.call("ghostty_key_event_set_composing", this.keyEvent, event.isComposing ? 1 : 0);
540 this.runtime.call(
541 "ghostty_key_event_set_unshifted_codepoint",
542 this.keyEvent,
543 ghosttyUnshiftedCodepoint(event, this.keyboardLayoutMap),
544 );
545
546 const text = event.key.length === 1 ? event.key : "";
547 const textBytes = encoder.encode(text);
548 const textPointer = textBytes.length === 0 ? 0 : this.runtime.alloc(textBytes.length);
549 if (textPointer !== 0) this.runtime.bytes(textPointer, textBytes.length).set(textBytes);
550 this.runtime.call("ghostty_key_event_set_utf8", this.keyEvent, textPointer, textBytes.length);
551
552 const written = this.runtime.call("ghostty_wasm_alloc_usize");
553 const encoded = this.encodeOutput(written, (output, outputSize) =>
554 this.runtime.call(
555 "ghostty_key_encoder_encode",
556 this.keyEncoder,
557 this.keyEvent,
558 output,
559 outputSize,
560 written,
561 ),
562 );
563 this.runtime.call("ghostty_wasm_free_usize", written);
564 if (textPointer !== 0) this.runtime.free(textPointer, textBytes.length);
565 return encoded;
566 }
567
568 encodePaste(data: string): string {
569 this.ensureActive();
570 const input = encoder.encode(data);
571 if (input.length === 0) return "";
572 const inputPointer = this.runtime.alloc(input.length);
573 this.runtime.bytes(inputPointer, input.length).set(input);
574 this.runtime.bytes(this.scratch, 1)[0] = 0;
575 const bracketed =
576 this.runtime.call("ghostty_terminal_mode_get", this.terminal, 2004, this.scratch) ===
577 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0;
578 const written = this.runtime.call("ghostty_wasm_alloc_usize");
579 let encoded = "";
580 const sizeResult = this.runtime.call(
581 "ghostty_paste_encode",
582 inputPointer,
583 input.length,
584 bracketed ? 1 : 0,
585 0,
586 0,
587 written,
588 );
589 const outputSize = this.runtime.view(written, 4).getUint32(0, true);
590 if (sizeResult === GHOSTTY_OUT_OF_SPACE && outputSize > 0) {
591 const output = this.runtime.alloc(outputSize);
592 const result = this.runtime.call(
593 "ghostty_paste_encode",
594 inputPointer,
595 input.length,
596 bracketed ? 1 : 0,
597 output,
598 outputSize,
599 written,
600 );
601 const outputLength = this.runtime.view(written, 4).getUint32(0, true);
602 encoded =
603 result === GHOSTTY_SUCCESS ? decoder.decode(this.runtime.bytes(output, outputLength)) : "";
604 this.runtime.free(output, outputSize);
605 }
606 this.runtime.call("ghostty_wasm_free_usize", written);
607 this.runtime.free(inputPointer, input.length);
608 return encoded;
609 }
610
611 encodeMouse(input: GhosttyMouseInput): string {
612 this.ensureActive();
613 this.runtime.call(
614 "ghostty_mouse_encoder_setopt_from_terminal",
615 this.mouseEncoder,
616 this.terminal,
617 );
618
619 const sizeLayout = this.runtime.layout("GhosttyMouseEncoderSize");
620 const size = this.runtime.alloc(sizeLayout.size);
621 for (const [field, value] of [
622 ["size", sizeLayout.size],
623 ["screen_width", input.screenWidth],
624 ["screen_height", input.screenHeight],
625 ["cell_width", input.cellWidth],
626 ["cell_height", input.cellHeight],
627 ["padding_top", input.paddingTop],
628 ["padding_bottom", input.paddingBottom],
629 ["padding_right", input.paddingRight],
630 ["padding_left", input.paddingLeft],
631 ] as const) {
632 this.runtime.setField(size, "GhosttyMouseEncoderSize", field, Math.max(0, Math.round(value)));
633 }
634 this.runtime.call("ghostty_mouse_encoder_setopt", this.mouseEncoder, 2, size);
635 this.runtime.free(size, sizeLayout.size);
636
637 this.runtime.bytes(this.scratch, 1)[0] = input.anyButtonPressed ? 1 : 0;
638 this.runtime.call("ghostty_mouse_encoder_setopt", this.mouseEncoder, 3, this.scratch);
639 this.runtime.bytes(this.scratch, 1)[0] = 1;
640 this.runtime.call("ghostty_mouse_encoder_setopt", this.mouseEncoder, 4, this.scratch);
641
642 this.runtime.call(
643 "ghostty_mouse_event_set_action",
644 this.mouseEvent,
645 input.action === "press" ? 0 : input.action === "release" ? 1 : 2,
646 );
647 if (input.button === null) {
648 this.runtime.call("ghostty_mouse_event_clear_button", this.mouseEvent);
649 } else {
650 this.runtime.call("ghostty_mouse_event_set_button", this.mouseEvent, input.button);
651 }
652 this.runtime.call("ghostty_mouse_event_set_mods", this.mouseEvent, input.mods);
653 const positionLayout = this.runtime.layout("GhosttyMousePosition");
654 const position = this.runtime.alloc(positionLayout.size);
655 const positionView = this.runtime.view(position, positionLayout.size);
656 positionView.setFloat32(positionLayout.fields.x!.offset, input.x, true);
657 positionView.setFloat32(positionLayout.fields.y!.offset, input.y, true);
658 this.runtime.call("ghostty_mouse_event_set_position", this.mouseEvent, position);
659 this.runtime.free(position, positionLayout.size);
660
661 const written = this.runtime.call("ghostty_wasm_alloc_usize");
662 const encoded = this.encodeOutput(written, (output, outputSize) =>
663 this.runtime.call(
664 "ghostty_mouse_encoder_encode",
665 this.mouseEncoder,
666 this.mouseEvent,
667 output,
668 outputSize,
669 written,
670 ),
671 );
672 this.runtime.call("ghostty_wasm_free_usize", written);
673 return encoded;
674 }
675
676 setSelection(anchor: GhosttyPointInput, end: GhosttyPointInput): void {
677 this.ensureActive();
678 const selectionLayout = this.runtime.layout("GhosttySelection");
679 const gridRefSize = this.runtime.layout("GhosttyGridRef").size;
680 const selection = this.runtime.alloc(selectionLayout.size);
681 let start = 0;
682 let endRef = 0;
683 try {
684 this.runtime.setField(selection, "GhosttySelection", "size", selectionLayout.size);
685 start = this.gridRef(anchor.x, anchor.y, anchor.tag ?? 1);
686 endRef = this.gridRef(end.x, end.y, end.tag ?? 1);
687 const startField = selectionLayout.fields.start!;
688 const endField = selectionLayout.fields.end!;
689 this.runtime
690 .bytes(selection + startField.offset, startField.size)
691 .set(this.runtime.bytes(start, startField.size));
692 this.runtime
693 .bytes(selection + endField.offset, endField.size)
694 .set(this.runtime.bytes(endRef, endField.size));
695 this.runtime.call("ghostty_terminal_set", this.terminal, 21, selection);
696 } finally {
697 this.runtime.free(start, gridRefSize);
698 this.runtime.free(endRef, gridRefSize);
699 this.runtime.free(selection, selectionLayout.size);
700 }
701 }
702
703 selectAll(): GhosttySelectionRange["screen"] | null {
704 this.ensureActive();
705 const layout = this.runtime.layout("GhosttySelection");
706 const selection = this.runtime.alloc(layout.size);
707 try {
708 this.runtime.setField(selection, "GhosttySelection", "size", layout.size);
709 if (
710 this.runtime.call("ghostty_terminal_select_all", this.terminal, selection) !==
711 GHOSTTY_SUCCESS
712 ) {
713 return null;
714 }
715 const start = this.pointFromGridRef(selection + layout.fields.start!.offset, 2);
716 const end = this.pointFromGridRef(selection + layout.fields.end!.offset, 2);
717 if (!start || !end) return null;
718 this.runtime.call("ghostty_terminal_set", this.terminal, 21, selection);
719 return { start, end };
720 } finally {
721 this.runtime.free(selection, layout.size);
722 }
723 }
724
725 selectWord(col: number, row: number): GhosttySelectionRange | null {
726 return this.selectAt(
727 "GhosttyTerminalSelectWordOptions",
728 "ghostty_terminal_select_word",
729 col,
730 row,
731 );
732 }
733
734 selectLine(col: number, row: number): GhosttySelectionRange | null {
735 return this.selectAt(
736 "GhosttyTerminalSelectLineOptions",
737 "ghostty_terminal_select_line",
738 col,
739 row,
740 );
741 }
742
743 hyperlinkAt(col: number, row: number): string | null {
744 this.ensureActive();
745 const ref = this.gridRef(col, row);
746 const written = this.runtime.call("ghostty_wasm_alloc_usize");
747 const sizeResult = this.runtime.call("ghostty_grid_ref_hyperlink_uri", ref, 0, 0, written);
748 const outputSize = this.runtime.view(written, 4).getUint32(0, true);
749 let hyperlink: string | null = null;
750 if (sizeResult === GHOSTTY_OUT_OF_SPACE && outputSize > 0) {
751 const output = this.runtime.alloc(outputSize);
752 const result = this.runtime.call(
753 "ghostty_grid_ref_hyperlink_uri",
754 ref,
755 output,
756 outputSize,
757 written,
758 );
759 const outputLength = this.runtime.view(written, 4).getUint32(0, true);
760 if (result === GHOSTTY_SUCCESS && outputLength > 0) {
761 hyperlink = decoder.decode(this.runtime.bytes(output, outputLength));
762 }
763 this.runtime.free(output, outputSize);
764 }
765 this.runtime.call("ghostty_wasm_free_usize", written);
766 this.runtime.free(ref, this.runtime.layout("GhosttyGridRef").size);
767 return hyperlink;
768 }
769
770 clearSelection(): void {
771 this.ensureActive();
772 this.runtime.call("ghostty_terminal_set", this.terminal, 21, 0);
773 }
774
775 snapshot(): GhosttySnapshot {
776 this.ensureActive();
777 this.assertSuccess(
778 "ghostty_render_state_update",
779 this.runtime.call("ghostty_render_state_update", this.renderState, this.terminal),
780 );
781 const cols = this.getU16(RENDER_DATA.cols);
782 const rowCount = this.getU16(RENDER_DATA.rows);
783 const dirty = this.getU32(RENDER_DATA.dirty);
784 const foreground = this.getColor(RENDER_DATA.foreground, { r: 229, g: 231, b: 235 });
785 const background = this.getColor(RENDER_DATA.background, { r: 0, g: 0, b: 0 });
786 const cursorHasValue = this.getBool(RENDER_DATA.cursorHasValue);
787 const cursor = cursorHasValue ? this.getColor(RENDER_DATA.cursor, foreground) : foreground;
788 const cursorInViewport = this.getBool(RENDER_DATA.cursorInViewport);
789 const cursorVisible = this.getBool(RENDER_DATA.cursorVisible) && cursorInViewport;
790 const cursorX = cursorInViewport ? this.getU16(RENDER_DATA.cursorX) : -1;
791 const cursorY = cursorInViewport ? this.getU16(RENDER_DATA.cursorY) : -1;
792
793 if (this.rows.length !== rowCount || this.rows.some((row) => row.cells.length !== cols)) {
794 this.rows = Array.from({ length: rowCount }, () => ({
795 cells: Array.from({ length: cols }, () => this.emptyCell(foreground, background)),
796 text: "",
797 isWrapContinuation: false,
798 wrapsToNext: false,
799 }));
800 }
801
802 const dirtyRows = new Set<number>();
803 if (dirty !== 0) {
804 this.assertSuccess(
805 "ghostty_render_state_get(row iterator)",
806 this.runtime.call(
807 "ghostty_render_state_get",
808 this.renderState,
809 RENDER_DATA.rowIterator,
810 this.rowIteratorSlot,
811 ),
812 );
813 const iterator = this.runtime.readPointer(this.rowIteratorSlot);
814 let rowIndex = 0;
815 while (
816 rowIndex < rowCount &&
817 this.runtime.call("ghostty_render_state_row_iterator_next", iterator) !== 0
818 ) {
819 const rowDirty = dirty === 2 || this.getRowBool(iterator, ROW_DATA.dirty);
820 if (rowDirty) {
821 this.rows[rowIndex] = this.readRow(iterator, cols, foreground, background);
822 dirtyRows.add(rowIndex);
823 this.runtime.bytes(this.scratch, 1)[0] = 0;
824 this.runtime.call("ghostty_render_state_row_set", iterator, 0, this.scratch);
825 }
826 rowIndex += 1;
827 }
828 this.runtime.view(this.scratch, 4).setUint32(0, 0, true);
829 this.runtime.call("ghostty_render_state_set", this.renderState, 0, this.scratch);
830 }
831
832 return {
833 cols,
834 rows: rowCount,
835 foreground,
836 background,
837 cursor,
838 cursorX,
839 cursorY,
840 cursorVisible,
841 cursorBlinking: this.getBool(RENDER_DATA.cursorBlinking),
842 cursorStyle: this.getU32(RENDER_DATA.cursorStyle),
843 dirtyRows,
844 rowData: this.rows,
845 };
846 }
847
848 selectionText(): string {
849 this.ensureActive();
850 const options = this.runtime.alloc(SELECTION_FORMAT_OPTIONS_SIZE);
851 const optionsView = this.runtime.view(options, SELECTION_FORMAT_OPTIONS_SIZE);
852 optionsView.setUint32(0, SELECTION_FORMAT_OPTIONS_SIZE, true);
853 optionsView.setUint32(4, 0, true);
854 optionsView.setUint8(8, 1);
855 optionsView.setUint8(9, 1);
856 optionsView.setUint32(12, 0, true);
857 const written = this.runtime.call("ghostty_wasm_alloc_usize");
858 const sizeResult = this.runtime.call(
859 "ghostty_terminal_selection_format_buf",
860 this.terminal,
861 options,
862 0,
863 0,
864 written,
865 );
866 const outputSize = this.runtime.view(written, 4).getUint32(0, true);
867 let text = "";
868 if (sizeResult === GHOSTTY_OUT_OF_SPACE && outputSize > 0) {
869 const output = this.runtime.alloc(outputSize);
870 const result = this.runtime.call(
871 "ghostty_terminal_selection_format_buf",
872 this.terminal,
873 options,
874 output,
875 outputSize,
876 written,
877 );
878 const outputLength = this.runtime.view(written, 4).getUint32(0, true);
879 if (result === GHOSTTY_SUCCESS) {
880 text = decoder.decode(this.runtime.bytes(output, outputLength));
881 }
882 this.runtime.free(output, outputSize);
883 }
884 this.runtime.call("ghostty_wasm_free_usize", written);
885 this.runtime.free(options, SELECTION_FORMAT_OPTIONS_SIZE);
886 return text;
887 }
888
889 viewportPointToScreen(col: number, row: number): { x: number; y: number } | null {
890 return this.convertPoint(col, row, 1, 2);
891 }
892
893 screenPointToViewport(col: number, row: number): { x: number; y: number } | null {
894 return this.convertPoint(col, row, 2, 1);
895 }
896
897 private convertPoint(
898 col: number,
899 row: number,
900 fromTag: 1 | 2,
901 toTag: 1 | 2,
902 ): { x: number; y: number } | null {
903 this.ensureActive();
904 const ref = this.gridRef(col, row, fromTag);
905 const point = this.pointFromGridRef(ref, toTag);
906 this.runtime.free(ref, this.runtime.layout("GhosttyGridRef").size);
907 return point;
908 }
909
910 dispose(): void {
911 if (this.disposed) return;
912 this.disposed = true;
913 if (this.mouseEvent) this.runtime.call("ghostty_mouse_event_free", this.mouseEvent);
914 if (this.mouseEncoder) this.runtime.call("ghostty_mouse_encoder_free", this.mouseEncoder);
915 if (this.keyEvent) this.runtime.call("ghostty_key_event_free", this.keyEvent);
916 if (this.keyEncoder) this.runtime.call("ghostty_key_encoder_free", this.keyEncoder);
917 if (this.rowCellsSlot) {
918 const cells = this.runtime.readPointer(this.rowCellsSlot);
919 if (cells) this.runtime.call("ghostty_render_state_row_cells_free", cells);
920 }
921 if (this.rowIteratorSlot) {
922 const iterator = this.runtime.readPointer(this.rowIteratorSlot);
923 if (iterator) this.runtime.call("ghostty_render_state_row_iterator_free", iterator);
924 }
925 if (this.renderState) this.runtime.call("ghostty_render_state_free", this.renderState);
926 if (this.terminal) {
927 if (this.ptyWriterId) this.runtime.detachPtyWriter(this.terminal, this.ptyWriterId);
928 this.runtime.call("ghostty_terminal_free", this.terminal);
929 }
930 if (this.style) this.runtime.free(this.style, this.runtime.layout("GhosttyStyle").size);
931 if (this.cellQuery) this.runtime.free(this.cellQuery, CELL_QUERY.size);
932 if (this.scrollbar) {
933 this.runtime.free(this.scrollbar, this.runtime.layout("GhosttyTerminalScrollbar").size);
934 }
935 if (this.scratch) this.runtime.free(this.scratch, 16);
936 if (this.graphemes) this.runtime.free(this.graphemes, this.graphemeCapacity);
937 for (const slot of [
938 this.mouseEventSlot,
939 this.mouseEncoderSlot,
940 this.keyEventSlot,
941 this.keyEncoderSlot,
942 this.rowCellsSlot,
943 this.rowIteratorSlot,
944 this.renderStateSlot,
945 this.terminalSlot,
946 ]) {
947 this.runtime.freeOpaque(slot);
948 }
949 }
950
951 private encodeOutput(
952 written: number,
953 encode: (output: number, outputSize: number) => number,
954 ): string {
955 const sizeResult = encode(0, 0);
956 const outputSize = this.runtime.view(written, 4).getUint32(0, true);
957 if (sizeResult === GHOSTTY_SUCCESS && outputSize === 0) return "";
958 if (sizeResult !== GHOSTTY_OUT_OF_SPACE || outputSize === 0) return "";
959
960 const output = this.runtime.alloc(outputSize);
961 const result = encode(output, outputSize);
962 const outputLength = this.runtime.view(written, 4).getUint32(0, true);
963 const encoded =
964 result === GHOSTTY_SUCCESS ? decoder.decode(this.runtime.bytes(output, outputLength)) : "";
965 this.runtime.free(output, outputSize);
966 return encoded;
967 }
968
969 private readRow(
970 iterator: number,
971 cols: number,
972 defaultForeground: GhosttyColor,
973 defaultBackground: GhosttyColor,
974 ): GhosttyRow {
975 this.assertSuccess(
976 "ghostty_render_state_row_get(raw)",
977 this.runtime.call("ghostty_render_state_row_get", iterator, ROW_DATA.raw, this.scratch),
978 );
979 const rawRow = this.runtime.view(this.scratch, 8).getBigUint64(0, true);
980 this.runtime.bytes(this.scratch + 8, 1)[0] = 0;
981 this.assertSuccess(
982 "ghostty_row_get(wrap continuation)",
983 this.runtime.call("ghostty_row_get", rawRow, 2, this.scratch + 8),
984 );
985 const isWrapContinuation = this.runtime.bytes(this.scratch + 8, 1)[0] !== 0;
986 this.runtime.bytes(this.scratch + 8, 1)[0] = 0;
987 this.assertSuccess(
988 "ghostty_row_get(wrap)",
989 this.runtime.call("ghostty_row_get", rawRow, 1, this.scratch + 8),
990 );
991 const wrapsToNext = this.runtime.bytes(this.scratch + 8, 1)[0] !== 0;
992
993 this.assertSuccess(
994 "ghostty_render_state_row_get(cells)",
995 this.runtime.call(
996 "ghostty_render_state_row_get",
997 iterator,
998 ROW_DATA.cells,
999 this.rowCellsSlot,
1000 ),
1001 );
1002 const cellsIterator = this.runtime.readPointer(this.rowCellsSlot);
1003 const { size: styleSize, fields: styleFields } = this.runtime.layout("GhosttyStyle");
1004 const cells: GhosttyCell[] = [];
1005 while (
1006 cells.length < cols &&
1007 this.runtime.call("ghostty_render_state_row_cells_next", cellsIterator) !== 0
1008 ) {
1009 this.runtime.bytes(this.style, styleSize).fill(0);
1010 this.runtime.setField(this.style, "GhosttyStyle", "size", styleSize);
1011 const result = this.runtime.call(
1012 "ghostty_render_state_row_cells_get_multi",
1013 cellsIterator,
1014 5,
1015 this.cellQuery + CELL_QUERY.keys,
1016 this.cellQuery + CELL_QUERY.values,
1017 this.cellQuery + CELL_QUERY.written,
1018 );
1019 const queryView = this.runtime.memoryDataView();
1020 const written = queryView.getUint32(this.cellQuery + CELL_QUERY.written, true);
1021 if (written < 3) this.assertSuccess("ghostty_render_state_row_cells_get_multi", result);
1022 const graphemeLength = queryView.getUint32(this.cellQuery + CELL_QUERY.graphemesLength, true);
1023 const selected = queryView.getUint8(this.cellQuery + CELL_QUERY.selected) !== 0;
1024 let background =
1025 written >= 4 ? this.readColor(this.cellQuery + CELL_QUERY.background) : defaultBackground;
1026 let foreground =
1027 written === 5
1028 ? this.readColor(this.cellQuery + CELL_QUERY.foreground)
1029 : written === 3
1030 ? this.getCellColor(cellsIterator, CELL_DATA.foreground, defaultForeground)
1031 : defaultForeground;
1032 let text = "";
1033 if (graphemeLength > 0) {
1034 const bufferSize = graphemeLength * 4;
1035 if (bufferSize > this.graphemeCapacity) {
1036 const capacity = Math.max(bufferSize, this.graphemeCapacity * 2);
1037 const buffer = this.runtime.alloc(capacity);
1038 this.runtime.free(this.graphemes, this.graphemeCapacity);
1039 this.graphemes = buffer;
1040 this.graphemeCapacity = capacity;
1041 }
1042 if (
1043 this.runtime.call(
1044 "ghostty_render_state_row_cells_get",
1045 cellsIterator,
1046 CELL_DATA.graphemes,
1047 this.graphemes,
1048 ) === GHOSTTY_SUCCESS
1049 ) {
1050 // Read through a DataView: the byte-array allocator guarantees no
1051 // 4-byte alignment, which a Uint32Array view would require.
1052 const codepointView = this.runtime.view(this.graphemes, bufferSize);
1053 text = ghosttyCellText(codepointView, graphemeLength);
1054 }
1055 }
1056 let wide = 0;
1057 if (text.length === 0 && cells.at(-1)?.text.length) {
1058 this.assertSuccess(
1059 "ghostty_render_state_row_cells_get(raw)",
1060 this.runtime.call(
1061 "ghostty_render_state_row_cells_get",
1062 cellsIterator,
1063 CELL_DATA.raw,
1064 this.scratch,
1065 ),
1066 );
1067 const rawCell = this.runtime.view(this.scratch, 8).getBigUint64(0, true);
1068 this.runtime.view(this.scratch + 8, 4).setUint32(0, 0, true);
1069 this.assertSuccess(
1070 "ghostty_cell_get(wide)",
1071 this.runtime.call("ghostty_cell_get", rawCell, RAW_CELL_DATA.wide, this.scratch + 8),
1072 );
1073 wide = this.runtime.view(this.scratch + 8, 4).getUint32(0, true);
1074 }
1075 // Read the style after allocation and ABI calls, which can grow WASM memory.
1076 const styleView = this.runtime.memoryDataView();
1077 if (styleView.getUint8(this.style + styleFields.inverse!.offset) !== 0) {
1078 [foreground, background] = [background, foreground];
1079 }
1080 if (styleView.getUint8(this.style + styleFields.faint!.offset) !== 0) {
1081 foreground = blend(foreground, background);
1082 }
1083 cells.push({
1084 text,
1085 wide,
1086 foreground,
1087 background,
1088 bold: styleView.getUint8(this.style + styleFields.bold!.offset) !== 0,
1089 italic: styleView.getUint8(this.style + styleFields.italic!.offset) !== 0,
1090 invisible: styleView.getUint8(this.style + styleFields.invisible!.offset) !== 0,
1091 strikethrough: styleView.getUint8(this.style + styleFields.strikethrough!.offset) !== 0,
1092 overline: styleView.getUint8(this.style + styleFields.overline!.offset) !== 0,
1093 underline: styleView.getInt32(this.style + styleFields.underline!.offset, true) !== 0,
1094 selected,
1095 });
1096 }
1097 while (cells.length < cols) cells.push(this.emptyCell(defaultForeground, defaultBackground));
1098 return {
1099 cells,
1100 text: cells
1101 .map((cell) => cell.text || " ")
1102 .join("")
1103 .trimEnd(),
1104 isWrapContinuation,
1105 wrapsToNext,
1106 };
1107 }
1108
1109 private gridRef(col: number, row: number, tag: 1 | 2 = 1): number {
1110 const pointLayout = this.runtime.layout("GhosttyPoint");
1111 const point = this.runtime.alloc(pointLayout.size);
1112 this.runtime.setField(point, "GhosttyPoint", "tag", tag);
1113 const pointValue = pointLayout.fields.value!;
1114 const valueOffset = pointValue.offset;
1115 const view = this.runtime.view(point + valueOffset, pointValue.size);
1116 view.setUint16(0, Math.max(0, col), true);
1117 view.setUint32(4, Math.max(0, row), true);
1118 const gridRefSize = this.runtime.layout("GhosttyGridRef").size;
1119 const gridRef = this.runtime.alloc(gridRefSize);
1120 this.runtime.setField(gridRef, "GhosttyGridRef", "size", gridRefSize);
1121 const result = this.runtime.call("ghostty_terminal_grid_ref", this.terminal, point, gridRef);
1122 this.runtime.free(point, pointLayout.size);
1123 if (result !== GHOSTTY_SUCCESS) {
1124 this.runtime.free(gridRef, gridRefSize);
1125 this.assertSuccess("ghostty_terminal_grid_ref", result);
1126 }
1127 return gridRef;
1128 }
1129
1130 private selectAt(
1131 optionsName: "GhosttyTerminalSelectWordOptions" | "GhosttyTerminalSelectLineOptions",
1132 operation: "ghostty_terminal_select_word" | "ghostty_terminal_select_line",
1133 col: number,
1134 row: number,
1135 ): GhosttySelectionRange | null {
1136 this.ensureActive();
1137 const optionsLayout = this.runtime.layout(optionsName);
1138 const selectionLayout = this.runtime.layout("GhosttySelection");
1139 const options = this.runtime.alloc(optionsLayout.size);
1140 let ref = 0;
1141 let selection = 0;
1142 let range: GhosttySelectionRange | null = null;
1143 try {
1144 this.runtime.setField(options, optionsName, "size", optionsLayout.size);
1145 ref = this.gridRef(col, row);
1146 const refField = optionsLayout.fields.ref!;
1147 this.runtime
1148 .bytes(options + refField.offset, refField.size)
1149 .set(this.runtime.bytes(ref, refField.size));
1150 selection = this.runtime.alloc(selectionLayout.size);
1151 this.runtime.setField(selection, "GhosttySelection", "size", selectionLayout.size);
1152 const result = this.runtime.call(operation, this.terminal, options, selection);
1153 if (result === GHOSTTY_SUCCESS) {
1154 const start = selection + selectionLayout.fields.start!.offset;
1155 const end = selection + selectionLayout.fields.end!.offset;
1156 const viewportStart = this.pointFromGridRef(start, 1);
1157 const viewportEnd = this.pointFromGridRef(end, 1);
1158 const screenStart = this.pointFromGridRef(start, 2);
1159 const screenEnd = this.pointFromGridRef(end, 2);
1160 if (viewportStart && viewportEnd && screenStart && screenEnd) {
1161 range = {
1162 viewport: { start: viewportStart, end: viewportEnd },
1163 screen: { start: screenStart, end: screenEnd },
1164 };
1165 }
1166 this.runtime.call("ghostty_terminal_set", this.terminal, 21, selection);
1167 }
1168 } finally {
1169 this.runtime.free(selection, selectionLayout.size);
1170 this.runtime.free(ref, this.runtime.layout("GhosttyGridRef").size);
1171 this.runtime.free(options, optionsLayout.size);
1172 }
1173 return range;
1174 }
1175
1176 private pointFromGridRef(ref: number, tag: 1 | 2): { x: number; y: number } | null {
1177 const coordinateLayout = this.runtime.layout("GhosttyPointCoordinate");
1178 const coordinate = this.runtime.alloc(coordinateLayout.size);
1179 const result = this.runtime.call(
1180 "ghostty_terminal_point_from_grid_ref",
1181 this.terminal,
1182 ref,
1183 tag,
1184 coordinate,
1185 );
1186 const point =
1187 result === GHOSTTY_SUCCESS
1188 ? {
1189 x: this.runtime.readField(coordinate, "GhosttyPointCoordinate", "x"),
1190 y: this.runtime.readField(coordinate, "GhosttyPointCoordinate", "y"),
1191 }
1192 : null;
1193 this.runtime.free(coordinate, coordinateLayout.size);
1194 return point;
1195 }
1196
1197 private getU16(data: number): number {
1198 this.runtime.bytes(this.scratch, 2).fill(0);
1199 this.assertSuccess(
1200 "ghostty_render_state_get",
1201 this.runtime.call("ghostty_render_state_get", this.renderState, data, this.scratch),
1202 );
1203 return this.runtime.view(this.scratch, 2).getUint16(0, true);
1204 }
1205
1206 private getU32(data: number): number {
1207 this.runtime.bytes(this.scratch, 4).fill(0);
1208 this.assertSuccess(
1209 "ghostty_render_state_get",
1210 this.runtime.call("ghostty_render_state_get", this.renderState, data, this.scratch),
1211 );
1212 return this.runtime.view(this.scratch, 4).getUint32(0, true);
1213 }
1214
1215 private getBool(data: number): boolean {
1216 this.runtime.bytes(this.scratch, 1)[0] = 0;
1217 this.assertSuccess(
1218 "ghostty_render_state_get",
1219 this.runtime.call("ghostty_render_state_get", this.renderState, data, this.scratch),
1220 );
1221 return this.runtime.bytes(this.scratch, 1)[0] !== 0;
1222 }
1223
1224 private getColor(data: number, fallback: GhosttyColor): GhosttyColor {
1225 this.runtime.bytes(this.scratch, 3).fill(0);
1226 const result = this.runtime.call(
1227 "ghostty_render_state_get",
1228 this.renderState,
1229 data,
1230 this.scratch,
1231 );
1232 return result === GHOSTTY_SUCCESS ? this.readColor(this.scratch) : fallback;
1233 }
1234
1235 private getRowBool(iterator: number, data: number): boolean {
1236 this.runtime.bytes(this.scratch, 1)[0] = 0;
1237 return (
1238 this.runtime.call("ghostty_render_state_row_get", iterator, data, this.scratch) ===
1239 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
1240 );
1241 }
1242
1243 private getCellColor(iterator: number, data: number, fallback: GhosttyColor): GhosttyColor {
1244 this.runtime.bytes(this.scratch, 3).fill(0);
1245 const result = this.runtime.call(
1246 "ghostty_render_state_row_cells_get",
1247 iterator,
1248 data,
1249 this.scratch,
1250 );
1251 return result === GHOSTTY_SUCCESS ? this.readColor(this.scratch) : fallback;
1252 }
1253
1254 private readColor(pointer: number): GhosttyColor {
1255 const view = this.runtime.memoryDataView();
1256 return {
1257 r: view.getUint8(pointer),
1258 g: view.getUint8(pointer + 1),
1259 b: view.getUint8(pointer + 2),
1260 };
1261 }
1262
1263 private emptyCell(foreground: GhosttyColor, background: GhosttyColor): GhosttyCell {
1264 return {
1265 text: "",
1266 wide: 0,
1267 foreground,
1268 background,
1269 bold: false,
1270 italic: false,
1271 invisible: false,
1272 strikethrough: false,
1273 overline: false,
1274 underline: false,
1275 selected: false,
1276 };
1277 }
1278
1279 private assertSuccess(operation: string, result: number): void {
1280 if (result !== GHOSTTY_SUCCESS) throw new Error(`${operation} failed with result ${result}`);
1281 }
1282
1283 private ensureActive(): void {
1284 if (this.disposed) throw new Error("libghostty-vt terminal has been disposed");
1285 }
1286}
1287
1288export function ghosttyColorsEqual(left: GhosttyColor, right: GhosttyColor): boolean {
1289 return sameColor(left, right);
1290}
apps/web/src/terminal/ghostty/fonts/LICENSE deleted-21
......@@ -1,21 +0,0 @@
1The MIT License (MIT)
2
3Copyright (c) 2014 Ryan L McIntyre
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
apps/web/src/terminal/ghostty/fonts/SymbolsNerdFontMono-Regular.woff2 deleted
Binary files a/apps/web/src/terminal/ghostty/fonts/SymbolsNerdFontMono-Regular.woff2 and /dev/null differ
apps/web/src/terminal/ghostty/keyCodes.test.ts deleted-65
......@@ -1,65 +0,0 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { ghosttyConsumedMods, ghosttyKeyForCode, ghosttyUnshiftedCodepoint } from "./keyCodes";
4
5describe("ghosttyKeyForCode", () => {
6 it("keeps the tail of the pinned Ghostty key enum in order", () => {
7 expect(ghosttyKeyForCode("F25")).toBe(ghosttyKeyForCode("F24") + 1);
8 expect(ghosttyKeyForCode("PrintScreen")).toBe(ghosttyKeyForCode("FnLock") + 1);
9 expect(ghosttyKeyForCode("Pause")).toBe(ghosttyKeyForCode("ScrollLock") + 1);
10 expect(ghosttyKeyForCode("Paste")).toBe(ghosttyKeyForCode("Cut") + 1);
11 });
12});
13
14describe("ghosttyConsumedMods", () => {
15 const shifted = { altKey: false, ctrlKey: false, key: "@", metaKey: false, shiftKey: true };
16
17 it("only consumes a lone Shift producing a character", () => {
18 expect(ghosttyConsumedMods(shifted)).toBe(1);
19 expect(ghosttyConsumedMods({ ...shifted, ctrlKey: true })).toBe(0);
20 expect(ghosttyConsumedMods({ ...shifted, key: "Tab" })).toBe(0);
21 // Deliberate: Shift+Space collapses to Space so it still types one.
22 expect(ghosttyConsumedMods({ ...shifted, key: " " })).toBe(1);
23 });
24});
25
26describe("ghosttyUnshiftedCodepoint", () => {
27 it("provides the logical base character for Kitty keyboard encoding", () => {
28 expect(ghosttyUnshiftedCodepoint({ code: "KeyC", key: "c", shiftKey: false })).toBe(
29 "c".codePointAt(0),
30 );
31 expect(ghosttyUnshiftedCodepoint({ code: "KeyC", key: "C", shiftKey: true })).toBe(
32 "c".codePointAt(0),
33 );
34 expect(ghosttyUnshiftedCodepoint({ code: "Digit1", key: "!", shiftKey: true })).toBe(
35 "1".codePointAt(0),
36 );
37 expect(ghosttyUnshiftedCodepoint({ code: "Slash", key: "?", shiftKey: true })).toBe(
38 "/".codePointAt(0),
39 );
40 expect(ghosttyUnshiftedCodepoint({ code: "Digit1", key: "&", shiftKey: false })).toBe(
41 "&".codePointAt(0),
42 );
43 expect(ghosttyUnshiftedCodepoint({ code: "Enter", key: "Enter", shiftKey: false })).toBe(0);
44 });
45
46 it("reports unknown instead of the shifted character without layout data", () => {
47 expect(ghosttyUnshiftedCodepoint({ code: "Digit7", key: "/", shiftKey: true })).toBe(0);
48 expect(ghosttyUnshiftedCodepoint({ code: "KeyD", key: "Д", shiftKey: true })).toBe(
49 "д".codePointAt(0),
50 );
51 });
52
53 it("prefers the active browser layout over US physical key positions", () => {
54 const layoutMap = new Map([
55 ["Digit1", "&"],
56 ["KeyC", "j"],
57 ]);
58 expect(ghosttyUnshiftedCodepoint({ code: "Digit1", key: "1", shiftKey: true }, layoutMap)).toBe(
59 "&".codePointAt(0),
60 );
61 expect(ghosttyUnshiftedCodepoint({ code: "KeyC", key: "J", shiftKey: true }, layoutMap)).toBe(
62 "j".codePointAt(0),
63 );
64 });
65});
apps/web/src/terminal/ghostty/keyCodes.ts deleted-267
......@@ -1,267 +0,0 @@
1// This order mirrors GhosttyKey in ghostty/vt/key/event.h. The values are
2// intentionally derived from the official W3C-aligned enum instead of
3// maintaining a second keyboard protocol.
4const ghosttyKeyboardCodes = [
5 "Unidentified",
6 "Backquote",
7 "Backslash",
8 "BracketLeft",
9 "BracketRight",
10 "Comma",
11 "Digit0",
12 "Digit1",
13 "Digit2",
14 "Digit3",
15 "Digit4",
16 "Digit5",
17 "Digit6",
18 "Digit7",
19 "Digit8",
20 "Digit9",
21 "Equal",
22 "IntlBackslash",
23 "IntlRo",
24 "IntlYen",
25 "KeyA",
26 "KeyB",
27 "KeyC",
28 "KeyD",
29 "KeyE",
30 "KeyF",
31 "KeyG",
32 "KeyH",
33 "KeyI",
34 "KeyJ",
35 "KeyK",
36 "KeyL",
37 "KeyM",
38 "KeyN",
39 "KeyO",
40 "KeyP",
41 "KeyQ",
42 "KeyR",
43 "KeyS",
44 "KeyT",
45 "KeyU",
46 "KeyV",
47 "KeyW",
48 "KeyX",
49 "KeyY",
50 "KeyZ",
51 "Minus",
52 "Period",
53 "Quote",
54 "Semicolon",
55 "Slash",
56 "AltLeft",
57 "AltRight",
58 "Backspace",
59 "CapsLock",
60 "ContextMenu",
61 "ControlLeft",
62 "ControlRight",
63 "Enter",
64 "MetaLeft",
65 "MetaRight",
66 "ShiftLeft",
67 "ShiftRight",
68 "Space",
69 "Tab",
70 "Convert",
71 "KanaMode",
72 "NonConvert",
73 "Delete",
74 "End",
75 "Help",
76 "Home",
77 "Insert",
78 "PageDown",
79 "PageUp",
80 "ArrowDown",
81 "ArrowLeft",
82 "ArrowRight",
83 "ArrowUp",
84 "NumLock",
85 "Numpad0",
86 "Numpad1",
87 "Numpad2",
88 "Numpad3",
89 "Numpad4",
90 "Numpad5",
91 "Numpad6",
92 "Numpad7",
93 "Numpad8",
94 "Numpad9",
95 "NumpadAdd",
96 "NumpadBackspace",
97 "NumpadClear",
98 "NumpadClearEntry",
99 "NumpadComma",
100 "NumpadDecimal",
101 "NumpadDivide",
102 "NumpadEnter",
103 "NumpadEqual",
104 "NumpadMemoryAdd",
105 "NumpadMemoryClear",
106 "NumpadMemoryRecall",
107 "NumpadMemoryStore",
108 "NumpadMemorySubtract",
109 "NumpadMultiply",
110 "NumpadParenLeft",
111 "NumpadParenRight",
112 "NumpadSubtract",
113 "NumpadSeparator",
114 "NumpadArrowUp",
115 "NumpadArrowDown",
116 "NumpadArrowRight",
117 "NumpadArrowLeft",
118 "NumpadBegin",
119 "NumpadHome",
120 "NumpadEnd",
121 "NumpadInsert",
122 "NumpadDelete",
123 "NumpadPageUp",
124 "NumpadPageDown",
125 "Escape",
126 "F1",
127 "F2",
128 "F3",
129 "F4",
130 "F5",
131 "F6",
132 "F7",
133 "F8",
134 "F9",
135 "F10",
136 "F11",
137 "F12",
138 "F13",
139 "F14",
140 "F15",
141 "F16",
142 "F17",
143 "F18",
144 "F19",
145 "F20",
146 "F21",
147 "F22",
148 "F23",
149 "F24",
150 "F25",
151 "Fn",
152 "FnLock",
153 "PrintScreen",
154 "ScrollLock",
155 "Pause",
156 "BrowserBack",
157 "BrowserFavorites",
158 "BrowserForward",
159 "BrowserHome",
160 "BrowserRefresh",
161 "BrowserSearch",
162 "BrowserStop",
163 "Eject",
164 "LaunchApp1",
165 "LaunchApp2",
166 "LaunchMail",
167 "MediaPlayPause",
168 "MediaSelect",
169 "MediaStop",
170 "MediaTrackNext",
171 "MediaTrackPrevious",
172 "Power",
173 "Sleep",
174 "AudioVolumeDown",
175 "AudioVolumeMute",
176 "AudioVolumeUp",
177 "WakeUp",
178 "Copy",
179 "Cut",
180 "Paste",
181] as const;
182
183const codeToGhosttyKey = new Map<string, number>(
184 ghosttyKeyboardCodes.map((code, index) => [code, index]),
185);
186
187export function ghosttyKeyForCode(code: string): number {
188 return codeToGhosttyKey.get(code) ?? 0;
189}
190
191export interface GhosttyKeyboardLayoutMap {
192 get(code: string): string | undefined;
193}
194
195const shiftedToUnshiftedCharacter = new Map<string, string>([
196 ["!", "1"],
197 ["@", "2"],
198 ["#", "3"],
199 ["$", "4"],
200 ["%", "5"],
201 ["^", "6"],
202 ["&", "7"],
203 ["*", "8"],
204 ["(", "9"],
205 [")", "0"],
206 ["~", "`"],
207 ["_", "-"],
208 ["+", "="],
209 ["{", "["],
210 ["}", "]"],
211 ["|", "\\"],
212 [":", ";"],
213 ['"', "'"],
214 ["<", ","],
215 [">", "."],
216 ["?", "/"],
217]);
218
219let keyboardLayoutMapPromise: Promise<GhosttyKeyboardLayoutMap | undefined> | undefined;
220
221export function loadGhosttyKeyboardLayoutMap(): Promise<GhosttyKeyboardLayoutMap | undefined> {
222 if (keyboardLayoutMapPromise) return keyboardLayoutMapPromise;
223 const browserNavigator = globalThis.navigator as
224 | (Navigator & {
225 readonly keyboard?: {
226 getLayoutMap(): Promise<GhosttyKeyboardLayoutMap>;
227 };
228 })
229 | undefined;
230 const keyboard = browserNavigator?.keyboard;
231 const promise = keyboard?.getLayoutMap().catch(() => undefined) ?? Promise.resolve(undefined);
232 keyboardLayoutMapPromise = promise;
233 return promise;
234}
235
236// Browsers do not expose consumed modifiers; treat Shift as consumed for
237// unchorded character input.
238export function ghosttyConsumedMods(
239 event: Pick<KeyboardEvent, "altKey" | "ctrlKey" | "key" | "metaKey" | "shiftKey">,
240): number {
241 if (!event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return 0;
242 return [...event.key].length === 1 ? 1 : 0;
243}
244
245export function ghosttyUnshiftedCodepoint(
246 event: Pick<KeyboardEvent, "code" | "key" | "shiftKey">,
247 layoutMap?: GhosttyKeyboardLayoutMap,
248): number {
249 if ([...event.key].length !== 1) return 0;
250 const layoutCharacter = layoutMap?.get(event.code);
251 if (layoutCharacter && [...layoutCharacter].length === 1) {
252 return layoutCharacter.codePointAt(0) ?? 0;
253 }
254 if (/^[A-Z]$/u.test(event.key)) return event.key.charCodeAt(0) + 32;
255 if (event.shiftKey) {
256 const unshiftedCharacter = shiftedToUnshiftedCharacter.get(event.key);
257 if (unshiftedCharacter) return unshiftedCharacter.codePointAt(0) ?? 0;
258 const lowercase = event.key.toLowerCase();
259 if (lowercase !== event.key && [...lowercase].length === 1) {
260 return lowercase.codePointAt(0) ?? 0;
261 }
262 // Without layout data the unshifted form of a shifted key is unknowable;
263 // reporting the shifted character as unshifted corrupts Kitty alternate keys.
264 return 0;
265 }
266 return event.key.codePointAt(0) ?? 0;
267}
apps/web/src/terminal/ghostty/renderer.test.ts deleted-285
......@@ -1,285 +0,0 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { GHOSTTY_CELL_WIDE, type GhosttyCell, type GhosttySnapshot } from "./core";
4import {
5 ghosttyTextRunEnd,
6 measureGhosttyCell,
7 renderGhosttySnapshot,
8 terminalGridSize,
9} from "./renderer";
10
11const cell = (text: string, wide = 0): GhosttyCell => ({
12 text,
13 wide,
14 foreground: { r: 255, g: 255, b: 255 },
15 background: { r: 0, g: 0, b: 0 },
16 bold: false,
17 italic: false,
18 invisible: false,
19 strikethrough: false,
20 overline: false,
21 underline: false,
22 selected: false,
23});
24
25describe("terminalGridSize", () => {
26 it("matches the mobile renderer's cell-and-padding sizing model", () => {
27 expect(terminalGridSize(808, 408, { width: 10, height: 20, baseline: 15 }, 4)).toEqual({
28 cols: 80,
29 rows: 20,
30 });
31 });
32
33 it("never sends an invalid zero-sized terminal to libghostty", () => {
34 expect(terminalGridSize(0, 0, { width: 10, height: 20, baseline: 15 }, 4)).toEqual({
35 cols: 1,
36 rows: 1,
37 });
38 });
39});
40
41describe("measureGhosttyCell", () => {
42 it("uses descender-aware metrics and the mobile terminal line-height", () => {
43 const measureText = (text: string) =>
44 text === "M"
45 ? { width: 7.2, actualBoundingBoxAscent: 9, actualBoundingBoxDescent: 0 }
46 : { width: 14.4, actualBoundingBoxAscent: 9, actualBoundingBoxDescent: 3 };
47 const context = {
48 font: "",
49 measureText,
50 } as unknown as CanvasRenderingContext2D;
51
52 expect(measureGhosttyCell(context, 12, "monospace")).toEqual({
53 width: 7.2,
54 height: 16,
55 baseline: 11,
56 });
57 });
58});
59
60describe("ghosttyTextRunEnd", () => {
61 it("includes wide spacer tails in the visual clip without rendering spaces", () => {
62 const cells = [
63 cell("界", GHOSTTY_CELL_WIDE.wide),
64 cell("", GHOSTTY_CELL_WIDE.spacerTail),
65 cell("🙂", GHOSTTY_CELL_WIDE.wide),
66 cell("", GHOSTTY_CELL_WIDE.spacerTail),
67 cell(""),
68 ];
69 expect(ghosttyTextRunEnd(cells, 0, () => true)).toBe(4);
70 });
71});
72
73describe("renderGhosttySnapshot", () => {
74 it("underlines every cell in a hovered wrapped link", () => {
75 const fillRectCalls: number[][] = [];
76 const context = {
77 canvas: { width: 200, height: 80 },
78 beginPath: () => {},
79 clip: () => {},
80 fillRect: (...args: number[]) => fillRectCalls.push(args),
81 fillText: () => {},
82 rect: () => {},
83 resetTransform: () => {},
84 restore: () => {},
85 save: () => {},
86 set fillStyle(_value: string) {},
87 set font(_value: string) {},
88 set textBaseline(_value: string) {},
89 } as unknown as CanvasRenderingContext2D;
90 const snapshot: GhosttySnapshot = {
91 cols: 4,
92 rows: 2,
93 foreground: { r: 255, g: 255, b: 255 },
94 background: { r: 0, g: 0, b: 0 },
95 cursor: { r: 255, g: 255, b: 255 },
96 cursorX: -1,
97 cursorY: -1,
98 cursorVisible: false,
99 cursorBlinking: false,
100 cursorStyle: 1,
101 dirtyRows: new Set([0, 1]),
102 rowData: [0, 1].map(() => ({
103 cells: [cell("a"), cell("b"), cell("c"), cell("d")],
104 text: "abcd",
105 isWrapContinuation: false,
106 wrapsToNext: false,
107 })),
108 };
109
110 renderGhosttySnapshot({
111 context,
112 snapshot,
113 metrics: { width: 10, height: 20, baseline: 15 },
114 fontSize: 12,
115 fontFamily: "monospace",
116 padding: 4,
117 forceFull: false,
118 cursorOn: false,
119 hoveredLinkRange: { start: { x: 2, y: 0 }, end: { x: 1, y: 1 } },
120 });
121
122 expect(fillRectCalls.filter(([, , , height]) => height === 1)).toEqual([
123 [24, 22, 10, 1],
124 [34, 22, 10, 1],
125 [4, 42, 10, 1],
126 [14, 42, 10, 1],
127 ]);
128 });
129
130 it("constrains text runs and cursor glyphs to their terminal cells", () => {
131 const fillTextCalls: unknown[][] = [];
132 const context = {
133 canvas: { width: 200, height: 40 },
134 beginPath: () => {},
135 clip: () => {},
136 fillRect: () => {},
137 fillText: (...args: unknown[]) => fillTextCalls.push(args),
138 rect: () => {},
139 resetTransform: () => {},
140 restore: () => {},
141 save: () => {},
142 set fillStyle(_value: string) {},
143 set font(_value: string) {},
144 set textBaseline(_value: string) {},
145 } as unknown as CanvasRenderingContext2D;
146 const cells = [cell("a"), cell("b"), cell("x")];
147 const snapshot: GhosttySnapshot = {
148 cols: 3,
149 rows: 1,
150 foreground: { r: 255, g: 255, b: 255 },
151 background: { r: 0, g: 0, b: 0 },
152 cursor: { r: 255, g: 255, b: 255 },
153 cursorX: 2,
154 cursorY: 0,
155 cursorVisible: true,
156 cursorBlinking: false,
157 cursorStyle: 1,
158 dirtyRows: new Set([0]),
159 rowData: [{ cells, text: "abx", isWrapContinuation: false, wrapsToNext: false }],
160 };
161
162 renderGhosttySnapshot({
163 context,
164 snapshot,
165 metrics: { width: 7.2, height: 16, baseline: 11 },
166 fontSize: 12,
167 fontFamily: "monospace",
168 padding: 4,
169 forceFull: false,
170 cursorOn: true,
171 });
172
173 expect(fillTextCalls).toEqual([
174 ["abx", 4, 15, 21.6],
175 ["x", 18.4, 15, 7.2],
176 ]);
177 });
178
179 it("repaints the cell without an overlay during the blink off phase", () => {
180 const fillTextCalls: unknown[][] = [];
181 const context = {
182 canvas: { width: 200, height: 40 },
183 beginPath: () => {},
184 clip: () => {},
185 fillRect: () => {},
186 fillText: (...args: unknown[]) => fillTextCalls.push(args),
187 rect: () => {},
188 resetTransform: () => {},
189 restore: () => {},
190 save: () => {},
191 set fillStyle(_value: string) {},
192 set font(_value: string) {},
193 set textBaseline(_value: string) {},
194 } as unknown as CanvasRenderingContext2D;
195 const snapshot: GhosttySnapshot = {
196 cols: 3,
197 rows: 1,
198 foreground: { r: 255, g: 255, b: 255 },
199 background: { r: 0, g: 0, b: 0 },
200 cursor: { r: 255, g: 255, b: 255 },
201 cursorX: 2,
202 cursorY: 0,
203 cursorVisible: true,
204 cursorBlinking: true,
205 cursorStyle: 1,
206 dirtyRows: new Set(),
207 rowData: [
208 {
209 cells: [cell("a"), cell("b"), cell("x")],
210 text: "abx",
211 isWrapContinuation: false,
212 wrapsToNext: false,
213 },
214 ],
215 };
216
217 renderGhosttySnapshot({
218 context,
219 snapshot,
220 metrics: { width: 7.2, height: 16, baseline: 11 },
221 fontSize: 12,
222 fontFamily: "monospace",
223 padding: 4,
224 forceFull: false,
225 cursorOn: false,
226 });
227
228 // The cursor row still repaints so the block disappears, but the inverted
229 // glyph the on phase draws over the cell is gone.
230 expect(fillTextCalls).toEqual([["abx", 4, 15, 21.6]]);
231 });
232
233 it("repaints the previous cursor row after the cursor moves", () => {
234 const clearedRows: number[] = [];
235 const context = {
236 canvas: { width: 200, height: 80 },
237 beginPath: () => {},
238 clip: () => {},
239 fillRect: (_left: number, top: number, _width: number, height: number) => {
240 if (height === 16) clearedRows.push(top);
241 },
242 fillText: () => {},
243 rect: () => {},
244 resetTransform: () => {},
245 restore: () => {},
246 save: () => {},
247 set fillStyle(_value: string) {},
248 set font(_value: string) {},
249 set textBaseline(_value: string) {},
250 } as unknown as CanvasRenderingContext2D;
251 const snapshot: GhosttySnapshot = {
252 cols: 1,
253 rows: 3,
254 foreground: { r: 255, g: 255, b: 255 },
255 background: { r: 0, g: 0, b: 0 },
256 cursor: { r: 255, g: 255, b: 255 },
257 cursorX: 0,
258 cursorY: 2,
259 cursorVisible: true,
260 cursorBlinking: false,
261 cursorStyle: 1,
262 dirtyRows: new Set(),
263 rowData: [0, 1, 2].map(() => ({
264 cells: [cell("")],
265 text: "",
266 isWrapContinuation: false,
267 wrapsToNext: false,
268 })),
269 };
270
271 renderGhosttySnapshot({
272 context,
273 snapshot,
274 metrics: { width: 7.2, height: 16, baseline: 11 },
275 fontSize: 12,
276 fontFamily: "monospace",
277 padding: 4,
278 forceFull: false,
279 cursorOn: true,
280 previousCursorY: 0,
281 });
282
283 expect(clearedRows).toEqual([4, 36, 36]);
284 });
285});
apps/web/src/terminal/ghostty/renderer.ts deleted-272
......@@ -1,272 +0,0 @@
1import {
2 GHOSTTY_CELL_WIDE,
3 ghosttyColorsEqual,
4 type GhosttyCell,
5 type GhosttyColor,
6 type GhosttySnapshot,
7} from "./core";
8
9export interface GhosttyCellMetrics {
10 readonly width: number;
11 readonly height: number;
12 readonly baseline: number;
13}
14
15export interface GhosttyCellRange {
16 readonly start: { readonly x: number; readonly y: number };
17 readonly end: { readonly x: number; readonly y: number };
18}
19
20const DEFAULT_SELECTION_BACKGROUND = "rgba(72, 122, 191, 0.35)";
21
22function cssColor(color: GhosttyColor): string {
23 return `rgb(${color.r}, ${color.g}, ${color.b})`;
24}
25
26function sameTextStyle(left: GhosttyCell, right: GhosttyCell): boolean {
27 // Selection deliberately does not participate: it only tints the background
28 // overlay, and splitting a text run at a selection boundary visibly shifts
29 // glyph spacing whenever the face's true advance differs from the cell width.
30 return (
31 ghosttyColorsEqual(left.foreground, right.foreground) &&
32 left.bold === right.bold &&
33 left.italic === right.italic &&
34 left.invisible === right.invisible
35 );
36}
37
38export function ghosttyTextRunEnd(
39 cells: readonly GhosttyCell[],
40 start: number,
41 sameStyle: (cell: GhosttyCell) => boolean,
42): number {
43 let end = start + 1;
44 while (end < cells.length) {
45 const next = cells[end];
46 if (!next) break;
47 if (next.wide === GHOSTTY_CELL_WIDE.spacerTail) {
48 end += 1;
49 continue;
50 }
51 if (next.text.length === 0 || !sameStyle(next)) break;
52 end += 1;
53 }
54 return end;
55}
56
57function fontForCell(cell: GhosttyCell, fontSize: number, fontFamily: string): string {
58 const style = cell.italic ? "italic" : "normal";
59 const weight = cell.bold ? "700" : "400";
60 return `${style} ${weight} ${fontSize}px ${fontFamily}`;
61}
62
63export function measureGhosttyCell(
64 context: CanvasRenderingContext2D,
65 fontSize: number,
66 fontFamily: string,
67): GhosttyCellMetrics {
68 context.font = `normal 400 ${fontSize}px ${fontFamily}`;
69 const widthMeasurement = context.measureText("M");
70 const verticalMeasurement = context.measureText("Mg");
71 const ascent = verticalMeasurement.actualBoundingBoxAscent || fontSize;
72 const descent = verticalMeasurement.actualBoundingBoxDescent;
73 const glyphHeight = ascent + descent;
74 const height = Math.max(1, Math.round(fontSize * 1.35), Math.ceil(glyphHeight));
75 return {
76 width: Math.max(1, widthMeasurement.width),
77 height,
78 baseline: Math.round((height - glyphHeight) / 2 + ascent),
79 };
80}
81
82export function terminalGridSize(
83 width: number,
84 height: number,
85 metrics: GhosttyCellMetrics,
86 padding: number,
87): { cols: number; rows: number } {
88 return {
89 cols: Math.max(1, Math.floor((width - padding * 2) / metrics.width)),
90 rows: Math.max(1, Math.floor((height - padding * 2) / metrics.height)),
91 };
92}
93
94export function renderGhosttySnapshot(options: {
95 readonly context: CanvasRenderingContext2D;
96 readonly snapshot: GhosttySnapshot;
97 readonly metrics: GhosttyCellMetrics;
98 readonly fontSize: number;
99 readonly fontFamily: string;
100 readonly padding: number;
101 readonly forceFull: boolean;
102 readonly cursorOn: boolean;
103 readonly previousCursorY?: number | null;
104 readonly focused?: boolean;
105 readonly selectionBackground?: string;
106 readonly hoveredLinkRange?: GhosttyCellRange | null;
107 /** Vertical origin of row 0; defaults to the horizontal padding. */
108 readonly originY?: number;
109}): void {
110 const {
111 context,
112 snapshot,
113 metrics,
114 fontSize,
115 fontFamily,
116 padding,
117 forceFull,
118 cursorOn,
119 previousCursorY,
120 } = options;
121 const focused = options.focused ?? true;
122 const selectionBackground = options.selectionBackground ?? DEFAULT_SELECTION_BACKGROUND;
123 const hoveredLinkRange = options.hoveredLinkRange ?? null;
124 const originY = options.originY ?? padding;
125 const rowsToDraw = forceFull
126 ? Array.from({ length: snapshot.rows }, (_, index) => index)
127 : [...snapshot.dirtyRows];
128 if (
129 previousCursorY !== null &&
130 previousCursorY !== undefined &&
131 previousCursorY >= 0 &&
132 !rowsToDraw.includes(previousCursorY)
133 ) {
134 rowsToDraw.push(previousCursorY);
135 }
136 if (snapshot.cursorVisible && snapshot.cursorY >= 0 && !rowsToDraw.includes(snapshot.cursorY)) {
137 rowsToDraw.push(snapshot.cursorY);
138 }
139
140 if (forceFull) {
141 context.save();
142 context.resetTransform();
143 context.fillStyle = cssColor(snapshot.background);
144 context.fillRect(0, 0, context.canvas.width, context.canvas.height);
145 context.restore();
146 }
147
148 context.textBaseline = "alphabetic";
149 for (const rowIndex of rowsToDraw) {
150 const row = snapshot.rowData[rowIndex];
151 if (!row) continue;
152 const top = originY + rowIndex * metrics.height;
153
154 context.fillStyle = cssColor(snapshot.background);
155 context.fillRect(padding, top, snapshot.cols * metrics.width, metrics.height);
156
157 let backgroundStart = 0;
158 while (backgroundStart < row.cells.length) {
159 const first = row.cells[backgroundStart];
160 if (!first) break;
161 let backgroundEnd = backgroundStart + 1;
162 while (backgroundEnd < row.cells.length) {
163 const next = row.cells[backgroundEnd];
164 if (
165 !next ||
166 next.selected !== first.selected ||
167 !ghosttyColorsEqual(next.background, first.background)
168 ) {
169 break;
170 }
171 backgroundEnd += 1;
172 }
173 if (first.selected || !ghosttyColorsEqual(first.background, snapshot.background)) {
174 const left = padding + backgroundStart * metrics.width;
175 const width = (backgroundEnd - backgroundStart) * metrics.width;
176 if (!ghosttyColorsEqual(first.background, snapshot.background)) {
177 context.fillStyle = cssColor(first.background);
178 context.fillRect(left, top, width, metrics.height);
179 }
180 if (first.selected) {
181 context.fillStyle = selectionBackground;
182 context.fillRect(left, top, width, metrics.height);
183 }
184 }
185 backgroundStart = backgroundEnd;
186 }
187
188 let runStart = 0;
189 while (runStart < row.cells.length) {
190 const first = row.cells[runStart];
191 if (!first) break;
192 if (first.text.length === 0) {
193 runStart += 1;
194 continue;
195 }
196 const runEnd = ghosttyTextRunEnd(row.cells, runStart, (cell) => sameTextStyle(cell, first));
197 const text = row.cells
198 .slice(runStart, runEnd)
199 .map((cell) => cell.text)
200 .join("");
201 if (!first.invisible && text.trim().length > 0) {
202 context.save();
203 context.beginPath();
204 context.rect(
205 padding + runStart * metrics.width,
206 top,
207 (runEnd - runStart) * metrics.width,
208 metrics.height,
209 );
210 context.clip();
211 context.font = fontForCell(first, fontSize, fontFamily);
212 context.fillStyle = cssColor(first.foreground);
213 context.fillText(
214 text,
215 padding + runStart * metrics.width,
216 top + metrics.baseline,
217 (runEnd - runStart) * metrics.width,
218 );
219 context.restore();
220 }
221 runStart = runEnd;
222 }
223
224 for (let column = 0; column < row.cells.length; column += 1) {
225 const cell = row.cells[column];
226 const hoveredLink =
227 hoveredLinkRange !== null &&
228 rowIndex >= hoveredLinkRange.start.y &&
229 rowIndex <= hoveredLinkRange.end.y &&
230 (rowIndex > hoveredLinkRange.start.y || column >= hoveredLinkRange.start.x) &&
231 (rowIndex < hoveredLinkRange.end.y || column <= hoveredLinkRange.end.x);
232 if (!cell || (!cell.underline && !cell.strikethrough && !cell.overline && !hoveredLink)) {
233 continue;
234 }
235 context.fillStyle = cssColor(cell.foreground);
236 const left = padding + column * metrics.width;
237 if (cell.underline || hoveredLink) {
238 context.fillRect(left, top + metrics.height - 2, metrics.width, 1);
239 }
240 if (cell.strikethrough) {
241 context.fillRect(left, top + Math.floor(metrics.height * 0.55), metrics.width, 1);
242 }
243 if (cell.overline) context.fillRect(left, top + 1, metrics.width, 1);
244 }
245 }
246
247 if (cursorOn && snapshot.cursorVisible && snapshot.cursorX >= 0 && snapshot.cursorY >= 0) {
248 const left = padding + snapshot.cursorX * metrics.width;
249 const top = originY + snapshot.cursorY * metrics.height;
250 context.fillStyle = cssColor(snapshot.cursor);
251 if (!focused) {
252 // An unfocused terminal draws a hollow cursor so the active pane is obvious.
253 context.strokeStyle = cssColor(snapshot.cursor);
254 context.strokeRect(left + 0.5, top + 0.5, metrics.width - 1, metrics.height - 1);
255 } else if (snapshot.cursorStyle === 0) {
256 context.fillRect(left, top, 2, metrics.height);
257 } else if (snapshot.cursorStyle === 2) {
258 context.fillRect(left, top + metrics.height - 2, metrics.width, 2);
259 } else if (snapshot.cursorStyle === 3) {
260 context.strokeStyle = cssColor(snapshot.cursor);
261 context.strokeRect(left + 0.5, top + 0.5, metrics.width - 1, metrics.height - 1);
262 } else {
263 context.fillRect(left, top, metrics.width, metrics.height);
264 const cell = snapshot.rowData[snapshot.cursorY]?.cells[snapshot.cursorX];
265 if (cell?.text) {
266 context.font = fontForCell(cell, fontSize, fontFamily);
267 context.fillStyle = cssColor(snapshot.background);
268 context.fillText(cell.text, left, top + metrics.baseline, metrics.width);
269 }
270 }
271 }
272}
apps/web/src/terminal/ghostty/runtime.ts deleted-233
......@@ -1,233 +0,0 @@
1import ghosttyWasmUrl from "./vendor/ghostty-vt.wasm?url";
2import ghosttyWritePtyWasmUrl from "./vendor/ghostty-write-pty.wasm?url&no-inline";
3
4type WasmFunction = (...args: Array<number | bigint>) => number;
5
6interface TypeField {
7 readonly offset: number;
8 readonly size: number;
9 readonly type: string;
10}
11
12interface TypeLayout {
13 readonly size: number;
14 readonly align: number;
15 readonly fields: Readonly<Record<string, TypeField>>;
16}
17
18type TypeLayouts = Readonly<Record<string, TypeLayout>>;
19
20const textDecoder = new TextDecoder();
21
22export class GhosttyRuntime {
23 readonly memory: WebAssembly.Memory;
24 readonly layouts: TypeLayouts;
25 private readonly exports: WebAssembly.Exports;
26 private memoryView: DataView;
27 private readonly ptyWriters = new Map<number, (data: string) => void>();
28 private nextPtyWriterId = 1;
29 private writePtyFunctionIndex = 0;
30
31 private constructor(instance: WebAssembly.Instance) {
32 this.exports = instance.exports;
33 const memory = instance.exports.memory;
34 if (!(memory instanceof WebAssembly.Memory)) {
35 throw new Error("libghostty-vt did not export WebAssembly memory");
36 }
37 this.memory = memory;
38 this.memoryView = new DataView(memory.buffer);
39 const jsonPointer = this.call("ghostty_type_json");
40 const bytes = new Uint8Array(memory.buffer);
41 let end = jsonPointer;
42 while (end < bytes.length && bytes[end] !== 0) end += 1;
43 this.layouts = JSON.parse(textDecoder.decode(bytes.subarray(jsonPointer, end))) as TypeLayouts;
44 }
45
46 static async load(): Promise<GhosttyRuntime> {
47 const response = await fetch(ghosttyWasmUrl);
48 if (!response.ok) {
49 throw new Error(`Unable to load libghostty-vt (${response.status})`);
50 }
51 let instance: WebAssembly.Instance | undefined;
52 const imports = {
53 env: {
54 log: (pointer: number, length: number) => {
55 if (!instance) return;
56 const memory = instance.exports.memory;
57 if (!(memory instanceof WebAssembly.Memory)) return;
58 const message = textDecoder.decode(new Uint8Array(memory.buffer, pointer, length));
59 console.debug("[libghostty-vt]", message);
60 },
61 },
62 };
63 const result = await WebAssembly.instantiate(await response.arrayBuffer(), imports);
64 instance = result.instance;
65 const runtime = new GhosttyRuntime(result.instance);
66 await runtime.installWritePtyTrampoline();
67 return runtime;
68 }
69
70 call(name: string, ...args: Array<number | bigint>): number {
71 const fn = this.exports[name];
72 if (typeof fn !== "function") {
73 throw new Error(`libghostty-vt export is unavailable: ${name}`);
74 }
75 return (fn as WasmFunction)(...args);
76 }
77
78 layout(name: string): TypeLayout {
79 const layout = this.layouts[name];
80 if (!layout) throw new Error(`libghostty-vt type layout is unavailable: ${name}`);
81 return layout;
82 }
83
84 alloc(size: number): number {
85 const pointer = this.call("ghostty_wasm_alloc_u8_array", size);
86 if (pointer === 0) throw new Error(`libghostty-vt failed to allocate ${size} bytes`);
87 new Uint8Array(this.memory.buffer, pointer, size).fill(0);
88 return pointer;
89 }
90
91 free(pointer: number, size: number): void {
92 if (pointer !== 0) this.call("ghostty_wasm_free_u8_array", pointer, size);
93 }
94
95 allocOpaque(): number {
96 const pointer = this.call("ghostty_wasm_alloc_opaque");
97 if (pointer === 0) throw new Error("libghostty-vt failed to allocate an opaque pointer");
98 // The slot is uninitialized until a *_new call writes it; zero it so dispose
99 // paths that run after a partial initialization never free a garbage pointer.
100 new DataView(this.memory.buffer).setUint32(pointer, 0, true);
101 return pointer;
102 }
103
104 freeOpaque(pointer: number): void {
105 if (pointer !== 0) this.call("ghostty_wasm_free_opaque", pointer);
106 }
107
108 readPointer(slot: number): number {
109 return this.memoryDataView().getUint32(slot, true);
110 }
111
112 attachPtyWriter(terminal: number, writer: (data: string) => void): number {
113 if (this.writePtyFunctionIndex === 0) {
114 throw new Error("libghostty-vt PTY callback trampoline is unavailable");
115 }
116 const id = this.nextPtyWriterId++;
117 this.ptyWriters.set(id, writer);
118 this.call("ghostty_terminal_set", terminal, 0, id);
119 this.call("ghostty_terminal_set", terminal, 1, this.writePtyFunctionIndex);
120 return id;
121 }
122
123 detachPtyWriter(terminal: number, id: number): void {
124 this.call("ghostty_terminal_set", terminal, 1, 0);
125 this.call("ghostty_terminal_set", terminal, 0, 0);
126 this.ptyWriters.delete(id);
127 }
128
129 view(pointer: number, size?: number): DataView {
130 return new DataView(this.memory.buffer, pointer, size);
131 }
132
133 bytes(pointer: number, size: number): Uint8Array {
134 return new Uint8Array(this.memory.buffer, pointer, size);
135 }
136
137 /** Reuse scalar reads across cells, refreshing after any terminal grows shared WASM memory. */
138 memoryDataView(): DataView {
139 if (this.memoryView.buffer !== this.memory.buffer) {
140 this.memoryView = new DataView(this.memory.buffer);
141 }
142 return this.memoryView;
143 }
144
145 setField(pointer: number, structName: string, fieldName: string, value: number): void {
146 const field = this.layout(structName).fields[fieldName];
147 if (!field) throw new Error(`libghostty-vt field is unavailable: ${structName}.${fieldName}`);
148 const view = this.memoryDataView();
149 const offset = pointer + field.offset;
150 switch (field.type) {
151 case "bool":
152 case "u8":
153 view.setUint8(offset, value);
154 return;
155 case "u16":
156 view.setUint16(offset, value, true);
157 return;
158 case "i32":
159 view.setInt32(offset, value, true);
160 return;
161 case "u32":
162 case "enum":
163 view.setUint32(offset, value, true);
164 return;
165 case "u64":
166 view.setBigUint64(offset, BigInt(value), true);
167 return;
168 default:
169 throw new Error(`Unsupported libghostty-vt field type: ${field.type}`);
170 }
171 }
172
173 readField(pointer: number, structName: string, fieldName: string): number {
174 const field = this.layout(structName).fields[fieldName];
175 if (!field) throw new Error(`libghostty-vt field is unavailable: ${structName}.${fieldName}`);
176 const view = this.memoryDataView();
177 const offset = pointer + field.offset;
178 switch (field.type) {
179 case "bool":
180 case "u8":
181 return view.getUint8(offset);
182 case "u16":
183 return view.getUint16(offset, true);
184 case "i32":
185 return view.getInt32(offset, true);
186 case "u32":
187 case "enum":
188 return view.getUint32(offset, true);
189 case "u64":
190 return Number(view.getBigUint64(offset, true));
191 default:
192 throw new Error(`Unsupported libghostty-vt field type: ${field.type}`);
193 }
194 }
195
196 private async installWritePtyTrampoline(): Promise<void> {
197 const response = await fetch(ghosttyWritePtyWasmUrl);
198 if (!response.ok) {
199 throw new Error(`Unable to load the libghostty-vt PTY trampoline (${response.status})`);
200 }
201 const result = await WebAssembly.instantiate(await response.arrayBuffer(), {
202 env: {
203 t3_write_pty: (_terminal: number, userdata: number, pointer: number, length: number) => {
204 const writer = this.ptyWriters.get(userdata);
205 if (!writer || length === 0) return;
206 writer(textDecoder.decode(new Uint8Array(this.memory.buffer, pointer, length)));
207 },
208 },
209 });
210 const trampoline = result.instance.exports.ghostty_write_pty;
211 const table = this.exports.__indirect_function_table;
212 if (typeof trampoline !== "function" || !(table instanceof WebAssembly.Table)) {
213 throw new Error("libghostty-vt did not expose its callback table");
214 }
215 const index = table.length;
216 // grow-then-set instead of grow(1, fn): WebKit stores a grow init value
217 // with broken type information and every later call_indirect through the
218 // entry traps with a signature mismatch. table.set canonicalizes correctly.
219 table.grow(1);
220 table.set(index, trampoline);
221 this.writePtyFunctionIndex = index;
222 }
223}
224
225let runtimePromise: Promise<GhosttyRuntime> | null = null;
226
227export function loadGhosttyRuntime(): Promise<GhosttyRuntime> {
228 runtimePromise ??= GhosttyRuntime.load().catch((error) => {
229 runtimePromise = null;
230 throw error;
231 });
232 return runtimePromise;
233}
apps/web/src/terminal/ghostty/runtimeAbi.test.ts deleted-717
......@@ -1,717 +0,0 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import wasmDataUrl from "./vendor/ghostty-vt.wasm?inline";
4import writePtyWasmDataUrl from "./vendor/ghostty-write-pty.wasm?inline";
5import pinnedVersion from "../../../../../native/libghostty-vt/VERSION?raw";
6import { ghosttyKeyForCode } from "./keyCodes";
7
8type WasmFunction = (...args: number[]) => number;
9
10function decodeWasmDataUrl(dataUrl: string): Uint8Array {
11 const encoded = dataUrl.split(",", 2)[1];
12 if (!encoded) throw new Error("The vendored Ghostty WASM data URL is invalid");
13 return Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
14}
15
16describe("vendored libghostty-vt WebAssembly", () => {
17 it("stays pinned to the canonical revision and size budget", async () => {
18 const wasm = decodeWasmDataUrl(wasmDataUrl);
19 expect(wasm.byteLength).toBeLessThan(750_000);
20
21 // The artifact carries its own provenance: the build embeds the pinned
22 // revision as semver build metadata, so the repository's canonical VERSION
23 // file is the single source of truth and drift is caught here without a copy.
24 const result = await WebAssembly.instantiate(wasm.buffer as ArrayBuffer, {
25 env: { log: () => {} },
26 });
27 const instance = result instanceof WebAssembly.Instance ? result : result.instance;
28 const memory = instance.exports.memory as WebAssembly.Memory;
29 const call = (name: string, ...args: number[]) =>
30 (instance.exports[name] as WasmFunction)(...args);
31 const out = call("ghostty_wasm_alloc_u8_array", 8);
32 expect(call("ghostty_build_info", 10, out)).toBe(0);
33 const view = new DataView(memory.buffer, out, 8);
34 const embeddedRevision = new TextDecoder().decode(
35 new Uint8Array(memory.buffer, view.getUint32(0, true), view.getUint32(4, true)),
36 );
37 call("ghostty_wasm_free_u8_array", out, 8);
38 expect(embeddedRevision).toBe(pinnedVersion.trim());
39 });
40
41 it("creates, writes multi-codepoint graphemes, and frees repeated terminals", async () => {
42 const bytes = decodeWasmDataUrl(wasmDataUrl);
43 let memory: WebAssembly.Memory | null = null;
44 const instantiated = await WebAssembly.instantiate(bytes.buffer as ArrayBuffer, {
45 env: {
46 log: () => {},
47 },
48 });
49 const instance =
50 instantiated instanceof WebAssembly.Instance ? instantiated : instantiated.instance;
51 const exports = instance.exports;
52 memory = exports.memory as WebAssembly.Memory;
53 const call = (name: string, ...args: number[]) => (exports[name] as WasmFunction)(...args);
54 const jsonPointer = call("ghostty_type_json");
55 const jsonBytes = new Uint8Array(memory.buffer, jsonPointer);
56 const jsonEnd = jsonBytes.indexOf(0);
57 const layouts = JSON.parse(new TextDecoder().decode(jsonBytes.subarray(0, jsonEnd))) as Record<
58 string,
59 { size: number }
60 >;
61 const optionsSize = layouts.GhosttyTerminalOptions?.size;
62 expect(optionsSize).toBe(8);
63 if (optionsSize === undefined) throw new Error("GhosttyTerminalOptions layout is missing");
64
65 for (let iteration = 0; iteration < 25; iteration += 1) {
66 const options = call("ghostty_wasm_alloc_u8_array", optionsSize);
67 const optionsView = new DataView(memory.buffer, options, optionsSize);
68 optionsView.setUint16(0, 80, true);
69 optionsView.setUint16(2, 24, true);
70 optionsView.setUint32(4, 5_000, true);
71 const terminalSlot = call("ghostty_wasm_alloc_opaque");
72 expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0);
73 const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true);
74
75 const input = new TextEncoder().encode("e\u0301 👨‍👩‍👧‍👦 العربية\r\n");
76 const inputPointer = call("ghostty_wasm_alloc_u8_array", input.length);
77 new Uint8Array(memory.buffer, inputPointer, input.length).set(input);
78 call("ghostty_terminal_vt_write", terminal, inputPointer, input.length);
79 call("ghostty_wasm_free_u8_array", inputPointer, input.length);
80 call("ghostty_terminal_free", terminal);
81 call("ghostty_wasm_free_opaque", terminalSlot);
82 call("ghostty_wasm_free_u8_array", options, optionsSize);
83 }
84 });
85
86 it("blinks the default cursor until a program asks for a steady one", async () => {
87 const result = await WebAssembly.instantiate(
88 decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer,
89 { env: { log: () => {} } },
90 );
91 const instance = result instanceof WebAssembly.Instance ? result : result.instance;
92 const memory = instance.exports.memory as WebAssembly.Memory;
93 const call = (name: string, ...args: number[]) =>
94 (instance.exports[name] as WasmFunction)(...args);
95 const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size);
96 const options = alloc(8);
97 const optionsView = new DataView(memory.buffer, options, 8);
98 optionsView.setUint16(0, 80, true);
99 optionsView.setUint16(2, 24, true);
100 const terminalSlot = call("ghostty_wasm_alloc_opaque");
101 expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0);
102 const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true);
103 const renderStateSlot = call("ghostty_wasm_alloc_opaque");
104 expect(call("ghostty_render_state_new", 0, renderStateSlot)).toBe(0);
105 const renderState = new DataView(memory.buffer).getUint32(renderStateSlot, true);
106 const scratch = alloc(4);
107
108 const blinking = () => {
109 expect(call("ghostty_render_state_update", renderState, terminal)).toBe(0);
110 expect(call("ghostty_render_state_get", renderState, 12, scratch)).toBe(0);
111 return new DataView(memory.buffer, scratch, 4).getUint8(0) !== 0;
112 };
113 const write = (data: string) => {
114 const bytes = new TextEncoder().encode(data);
115 const pointer = alloc(bytes.length);
116 new Uint8Array(memory.buffer, pointer, bytes.length).set(bytes);
117 call("ghostty_terminal_vt_write", terminal, pointer, bytes.length);
118 call("ghostty_wasm_free_u8_array", pointer, bytes.length);
119 };
120 const setDefaultCursorBlink = (blink: boolean) => {
121 const value = alloc(1);
122 new Uint8Array(memory.buffer, value, 1)[0] = blink ? 1 : 0;
123 expect(call("ghostty_terminal_set", terminal, 23, value)).toBe(0);
124 call("ghostty_wasm_free_u8_array", value, 1);
125 };
126
127 // Ghostty's own default is a steady cursor, so the blink the web terminal
128 // inherited from xterm.js only exists because option 23 asks for it.
129 expect(blinking()).toBe(false);
130 setDefaultCursorBlink(true);
131 expect(blinking()).toBe(true);
132
133 // Programs still own the cursor: DECSCUSR steady block and DEC mode 12 both
134 // stop the blink, and DECSCUSR reset returns to the embedder default.
135 write("\u001b[2 q");
136 expect(blinking()).toBe(false);
137 write("\u001b[0 q");
138 expect(blinking()).toBe(true);
139 write("\u001b[?12l");
140 expect(blinking()).toBe(false);
141 write("\u001b[?12h");
142 expect(blinking()).toBe(true);
143
144 // RIS restores Ghostty's built-in steady default rather than the embedder's,
145 // which is why the core reapplies the option around a session replay.
146 call("ghostty_terminal_reset", terminal);
147 expect(blinking()).toBe(false);
148 setDefaultCursorBlink(true);
149 expect(blinking()).toBe(true);
150
151 call("ghostty_wasm_free_u8_array", scratch, 4);
152 call("ghostty_render_state_free", renderState);
153 call("ghostty_wasm_free_opaque", renderStateSlot);
154 call("ghostty_terminal_free", terminal);
155 call("ghostty_wasm_free_opaque", terminalSlot);
156 call("ghostty_wasm_free_u8_array", options, 8);
157 });
158
159 it("reports and scrolls the viewport with Ghostty's scrollbar state", async () => {
160 const result = await WebAssembly.instantiate(
161 decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer,
162 { env: { log: () => {} } },
163 );
164 const instance = result instanceof WebAssembly.Instance ? result : result.instance;
165 const memory = instance.exports.memory as WebAssembly.Memory;
166 const call = (name: string, ...args: number[]) =>
167 (instance.exports[name] as WasmFunction)(...args);
168 const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size);
169 const options = alloc(8);
170 const optionsView = new DataView(memory.buffer, options, 8);
171 optionsView.setUint16(0, 80, true);
172 optionsView.setUint16(2, 10, true);
173 optionsView.setUint32(4, 1_000, true);
174 const terminalSlot = call("ghostty_wasm_alloc_opaque");
175 expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0);
176 const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true);
177 const input = new TextEncoder().encode(
178 Array.from({ length: 50 }, (_, index) => `${index + 1}\r\n`).join(""),
179 );
180 const inputPointer = alloc(input.length);
181 new Uint8Array(memory.buffer, inputPointer, input.length).set(input);
182 call("ghostty_terminal_vt_write", terminal, inputPointer, input.length);
183
184 const scrollbar = alloc(24);
185 const scrollbarView = new DataView(memory.buffer, scrollbar, 24);
186 expect(call("ghostty_terminal_get", terminal, 9, scrollbar)).toBe(0);
187 expect([0, 8, 16].map((offset) => Number(scrollbarView.getBigUint64(offset, true)))).toEqual([
188 51, 41, 10,
189 ]);
190
191 const scroll = alloc(24);
192 const scrollView = new DataView(memory.buffer, scroll, 24);
193 scrollView.setUint32(0, 2, true);
194 scrollView.setInt32(8, -5, true);
195 call("ghostty_terminal_scroll_viewport", terminal, scroll);
196 expect(call("ghostty_terminal_get", terminal, 9, scrollbar)).toBe(0);
197 expect(Number(scrollbarView.getBigUint64(8, true))).toBe(36);
198
199 call("ghostty_wasm_free_u8_array", scroll, 24);
200 call("ghostty_wasm_free_u8_array", scrollbar, 24);
201 call("ghostty_wasm_free_u8_array", inputPointer, input.length);
202 call("ghostty_terminal_free", terminal);
203 call("ghostty_wasm_free_opaque", terminalSlot);
204 call("ghostty_wasm_free_u8_array", options, 8);
205 });
206
207 it("routes terminal-generated replies through the shared callback table", async () => {
208 const mainResult = await WebAssembly.instantiate(
209 decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer,
210 { env: { log: () => {} } },
211 );
212 const main = mainResult instanceof WebAssembly.Instance ? mainResult : mainResult.instance;
213 const memory = main.exports.memory as WebAssembly.Memory;
214 let reply = "";
215 const trampolineResult = await WebAssembly.instantiate(
216 decodeWasmDataUrl(writePtyWasmDataUrl).buffer as ArrayBuffer,
217 {
218 env: {
219 t3_write_pty: (_terminal: number, _userdata: number, pointer: number, length: number) => {
220 reply += new TextDecoder().decode(new Uint8Array(memory.buffer, pointer, length));
221 },
222 },
223 },
224 );
225 const trampoline =
226 trampolineResult instanceof WebAssembly.Instance
227 ? trampolineResult
228 : trampolineResult.instance;
229 const table = main.exports.__indirect_function_table as WebAssembly.Table;
230 const callbackIndex = table.length;
231 table.grow(1, trampoline.exports.ghostty_write_pty as CallableFunction);
232 const call = (name: string, ...args: number[]) => (main.exports[name] as WasmFunction)(...args);
233 const options = call("ghostty_wasm_alloc_u8_array", 8);
234 const optionsView = new DataView(memory.buffer, options, 8);
235 optionsView.setUint16(0, 80, true);
236 optionsView.setUint16(2, 24, true);
237 const terminalSlot = call("ghostty_wasm_alloc_opaque");
238 expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0);
239 const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true);
240 call("ghostty_terminal_set", terminal, 0, 1);
241 call("ghostty_terminal_set", terminal, 1, callbackIndex);
242 const query = new TextEncoder().encode("\u001b[5n");
243 const queryPointer = call("ghostty_wasm_alloc_u8_array", query.length);
244 new Uint8Array(memory.buffer, queryPointer, query.length).set(query);
245 call("ghostty_terminal_vt_write", terminal, queryPointer, query.length);
246
247 expect(reply).toBe("\u001b[0n");
248 reply = "";
249 call("ghostty_terminal_set", terminal, 1, 0);
250 call("ghostty_terminal_set", terminal, 0, 0);
251 call("ghostty_terminal_vt_write", terminal, queryPointer, query.length);
252 expect(reply).toBe("");
253 call("ghostty_terminal_set", terminal, 0, 1);
254 call("ghostty_terminal_set", terminal, 1, callbackIndex);
255 call("ghostty_terminal_vt_write", terminal, queryPointer, query.length);
256 expect(reply).toBe("\u001b[0n");
257 call("ghostty_wasm_free_u8_array", queryPointer, query.length);
258 call("ghostty_terminal_free", terminal);
259 call("ghostty_wasm_free_opaque", terminalSlot);
260 call("ghostty_wasm_free_u8_array", options, 8);
261 });
262
263 it("formats the active selection with Ghostty's copy semantics", async () => {
264 const result = await WebAssembly.instantiate(
265 decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer,
266 { env: { log: () => {} } },
267 );
268 const instance = result instanceof WebAssembly.Instance ? result : result.instance;
269 const memory = instance.exports.memory as WebAssembly.Memory;
270 const call = (name: string, ...args: number[]) =>
271 (instance.exports[name] as WasmFunction)(...args);
272 const options = call("ghostty_wasm_alloc_u8_array", 8);
273 const optionsView = new DataView(memory.buffer, options, 8);
274 optionsView.setUint16(0, 80, true);
275 optionsView.setUint16(2, 24, true);
276 const terminalSlot = call("ghostty_wasm_alloc_opaque");
277 expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0);
278 const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true);
279 const input = new TextEncoder().encode("a\r\n\r\nb");
280 const inputPointer = call("ghostty_wasm_alloc_u8_array", input.length);
281 new Uint8Array(memory.buffer, inputPointer, input.length).set(input);
282 call("ghostty_terminal_vt_write", terminal, inputPointer, input.length);
283
284 const selection = call("ghostty_wasm_alloc_u8_array", 32);
285 new DataView(memory.buffer, selection, 32).setUint32(0, 32, true);
286 expect(call("ghostty_terminal_select_all", terminal, selection)).toBe(0);
287 expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0);
288 const formatOptions = call("ghostty_wasm_alloc_u8_array", 16);
289 const formatView = new DataView(memory.buffer, formatOptions, 16);
290 formatView.setUint32(0, 16, true);
291 formatView.setUint8(8, 1);
292 formatView.setUint8(9, 1);
293 const written = call("ghostty_wasm_alloc_usize");
294 expect(
295 call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written),
296 ).toBe(-3);
297 const outputSize = new DataView(memory.buffer, written, 4).getUint32(0, true);
298 const output = call("ghostty_wasm_alloc_u8_array", outputSize);
299 expect(
300 call(
301 "ghostty_terminal_selection_format_buf",
302 terminal,
303 formatOptions,
304 output,
305 outputSize,
306 written,
307 ),
308 ).toBe(0);
309 expect(new TextDecoder().decode(new Uint8Array(memory.buffer, output, outputSize))).toBe(
310 "a\n\nb",
311 );
312
313 call("ghostty_wasm_free_u8_array", output, outputSize);
314 call("ghostty_wasm_free_usize", written);
315 call("ghostty_wasm_free_u8_array", formatOptions, 16);
316 call("ghostty_wasm_free_u8_array", selection, 32);
317 call("ghostty_wasm_free_u8_array", inputPointer, input.length);
318 call("ghostty_terminal_free", terminal);
319 call("ghostty_wasm_free_opaque", terminalSlot);
320 call("ghostty_wasm_free_u8_array", options, 8);
321 });
322
323 it("formats a cell-drag selection installed from screen grid refs", async () => {
324 const result = await WebAssembly.instantiate(
325 decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer,
326 { env: { log: () => {} } },
327 );
328 const instance = result instanceof WebAssembly.Instance ? result : result.instance;
329 const memory = instance.exports.memory as WebAssembly.Memory;
330 const call = (name: string, ...args: number[]) =>
331 (instance.exports[name] as WasmFunction)(...args);
332 const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size);
333 const free = (pointer: number, size: number) =>
334 call("ghostty_wasm_free_u8_array", pointer, size);
335 const options = alloc(8);
336 new DataView(memory.buffer, options, 8).setUint16(0, 80, true);
337 new DataView(memory.buffer, options, 8).setUint16(2, 24, true);
338 const terminalSlot = call("ghostty_wasm_alloc_opaque");
339 expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0);
340 const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true);
341 const input = new TextEncoder().encode("hello\r\nworld");
342 const inputPointer = alloc(input.length);
343 new Uint8Array(memory.buffer, inputPointer, input.length).set(input);
344 call("ghostty_terminal_vt_write", terminal, inputPointer, input.length);
345
346 const gridRefAt = (x: number, y: number) => {
347 const point = alloc(24);
348 const pointView = new DataView(memory.buffer, point, 24);
349 pointView.setUint32(0, 2, true);
350 pointView.setUint16(8, x, true);
351 pointView.setUint32(12, y, true);
352 const ref = alloc(12);
353 new DataView(memory.buffer, ref, 12).setUint32(0, 12, true);
354 expect(call("ghostty_terminal_grid_ref", terminal, point, ref)).toBe(0);
355 free(point, 24);
356 return ref;
357 };
358 const start = gridRefAt(0, 0);
359 const end = gridRefAt(4, 1);
360 const selection = alloc(32);
361 const selectionBytes = new Uint8Array(memory.buffer, selection, 32);
362 selectionBytes.fill(0);
363 new DataView(memory.buffer, selection, 32).setUint32(0, 32, true);
364 selectionBytes.set(new Uint8Array(memory.buffer, start, 12), 4);
365 selectionBytes.set(new Uint8Array(memory.buffer, end, 12), 16);
366 expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0);
367
368 const formatOptions = alloc(16);
369 new Uint8Array(memory.buffer, formatOptions, 16).fill(0);
370 const formatView = new DataView(memory.buffer, formatOptions, 16);
371 formatView.setUint32(0, 16, true);
372 formatView.setUint8(8, 1);
373 formatView.setUint8(9, 1);
374 const written = call("ghostty_wasm_alloc_usize");
375 expect(
376 call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written),
377 ).toBe(-3);
378 const outputSize = new DataView(memory.buffer, written, 4).getUint32(0, true);
379 const output = alloc(outputSize);
380 expect(
381 call(
382 "ghostty_terminal_selection_format_buf",
383 terminal,
384 formatOptions,
385 output,
386 outputSize,
387 written,
388 ),
389 ).toBe(0);
390 expect(new TextDecoder().decode(new Uint8Array(memory.buffer, output, outputSize))).toBe(
391 "hello\nworld",
392 );
393
394 free(output, outputSize);
395 call("ghostty_wasm_free_usize", written);
396 free(formatOptions, 16);
397 free(selection, 32);
398 free(end, 12);
399 free(start, 12);
400 free(inputPointer, input.length);
401 call("ghostty_terminal_free", terminal);
402 call("ghostty_wasm_free_opaque", terminalSlot);
403 free(options, 8);
404 });
405
406 it("uses Ghostty for mouse encoding, word selection, and OSC 8 hit testing", async () => {
407 const result = await WebAssembly.instantiate(
408 decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer,
409 { env: { log: () => {} } },
410 );
411 const instance = result instanceof WebAssembly.Instance ? result : result.instance;
412 const memory = instance.exports.memory as WebAssembly.Memory;
413 const call = (name: string, ...args: number[]) =>
414 (instance.exports[name] as WasmFunction)(...args);
415 const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size);
416 const free = (pointer: number, size: number) =>
417 call("ghostty_wasm_free_u8_array", pointer, size);
418
419 const terminalOptions = alloc(8);
420 const terminalOptionsView = new DataView(memory.buffer, terminalOptions, 8);
421 terminalOptionsView.setUint16(0, 80, true);
422 terminalOptionsView.setUint16(2, 24, true);
423 const terminalSlot = call("ghostty_wasm_alloc_opaque");
424 expect(call("ghostty_terminal_new", 0, terminalSlot, terminalOptions)).toBe(0);
425 const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true);
426 const input = new TextEncoder().encode(
427 "\u001b[?1000h\u001b[?1006h\u001b]8;;https://t3.codes/docs\u001b\\linked\u001b]8;;\u001b\\ plain",
428 );
429 const inputPointer = alloc(input.length);
430 new Uint8Array(memory.buffer, inputPointer, input.length).set(input);
431 call("ghostty_terminal_vt_write", terminal, inputPointer, input.length);
432
433 const modeFlag = alloc(1);
434 new Uint8Array(memory.buffer, modeFlag, 1)[0] = 0;
435 expect(call("ghostty_terminal_get", terminal, 11, modeFlag)).toBe(0);
436 expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(1);
437 new Uint8Array(memory.buffer, modeFlag, 1)[0] = 1;
438 expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0);
439 expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(0);
440 const anyEventInput = new TextEncoder().encode("\u001b[?1003h");
441 const anyEventPointer = alloc(anyEventInput.length);
442 new Uint8Array(memory.buffer, anyEventPointer, anyEventInput.length).set(anyEventInput);
443 call("ghostty_terminal_vt_write", terminal, anyEventPointer, anyEventInput.length);
444 expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0);
445 expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(1);
446 const anyEventReset = new TextEncoder().encode("\u001b[?1003l\u001b[?1000h");
447 const anyEventResetPointer = alloc(anyEventReset.length);
448 new Uint8Array(memory.buffer, anyEventResetPointer, anyEventReset.length).set(anyEventReset);
449 call("ghostty_terminal_vt_write", terminal, anyEventResetPointer, anyEventReset.length);
450 expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0);
451 expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(0);
452 free(anyEventResetPointer, anyEventReset.length);
453 free(anyEventPointer, anyEventInput.length);
454 free(modeFlag, 1);
455
456 const point = alloc(24);
457 const pointView = new DataView(memory.buffer, point, 24);
458 pointView.setUint32(0, 1, true);
459 pointView.setUint16(8, 1, true);
460 pointView.setUint32(12, 0, true);
461 const gridRef = alloc(12);
462 new DataView(memory.buffer, gridRef, 12).setUint32(0, 12, true);
463 expect(call("ghostty_terminal_grid_ref", terminal, point, gridRef)).toBe(0);
464
465 const written = call("ghostty_wasm_alloc_usize");
466 expect(call("ghostty_grid_ref_hyperlink_uri", gridRef, 0, 0, written)).toBe(-3);
467 const hyperlinkSize = new DataView(memory.buffer, written, 4).getUint32(0, true);
468 const hyperlink = alloc(hyperlinkSize);
469 expect(call("ghostty_grid_ref_hyperlink_uri", gridRef, hyperlink, hyperlinkSize, written)).toBe(
470 0,
471 );
472 expect(new TextDecoder().decode(new Uint8Array(memory.buffer, hyperlink, hyperlinkSize))).toBe(
473 "https://t3.codes/docs",
474 );
475
476 const wordOptions = alloc(24);
477 const wordOptionsView = new DataView(memory.buffer, wordOptions, 24);
478 wordOptionsView.setUint32(0, 24, true);
479 new Uint8Array(memory.buffer, wordOptions + 4, 12).set(
480 new Uint8Array(memory.buffer, gridRef, 12),
481 );
482 const selection = alloc(32);
483 new DataView(memory.buffer, selection, 32).setUint32(0, 32, true);
484 expect(call("ghostty_terminal_select_word", terminal, wordOptions, selection)).toBe(0);
485 expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0);
486 const formatOptions = alloc(16);
487 const formatView = new DataView(memory.buffer, formatOptions, 16);
488 formatView.setUint32(0, 16, true);
489 formatView.setUint8(8, 1);
490 formatView.setUint8(9, 1);
491 expect(
492 call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written),
493 ).toBe(-3);
494 const selectionSize = new DataView(memory.buffer, written, 4).getUint32(0, true);
495 const selectionText = alloc(selectionSize);
496 expect(
497 call(
498 "ghostty_terminal_selection_format_buf",
499 terminal,
500 formatOptions,
501 selectionText,
502 selectionSize,
503 written,
504 ),
505 ).toBe(0);
506 expect(
507 new TextDecoder().decode(new Uint8Array(memory.buffer, selectionText, selectionSize)),
508 ).toBe("linked");
509
510 const lineOptions = alloc(28);
511 const lineOptionsView = new DataView(memory.buffer, lineOptions, 28);
512 lineOptionsView.setUint32(0, 28, true);
513 new Uint8Array(memory.buffer, lineOptions + 4, 12).set(
514 new Uint8Array(memory.buffer, gridRef, 12),
515 );
516 expect(call("ghostty_terminal_select_line", terminal, lineOptions, selection)).toBe(0);
517 expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0);
518 expect(
519 call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written),
520 ).toBe(-3);
521 const lineSelectionSize = new DataView(memory.buffer, written, 4).getUint32(0, true);
522 const lineSelectionText = alloc(lineSelectionSize);
523 expect(
524 call(
525 "ghostty_terminal_selection_format_buf",
526 terminal,
527 formatOptions,
528 lineSelectionText,
529 lineSelectionSize,
530 written,
531 ),
532 ).toBe(0);
533 expect(
534 new TextDecoder().decode(new Uint8Array(memory.buffer, lineSelectionText, lineSelectionSize)),
535 ).toBe("linked plain");
536
537 const mouseEncoderSlot = call("ghostty_wasm_alloc_opaque");
538 const mouseEventSlot = call("ghostty_wasm_alloc_opaque");
539 expect(call("ghostty_mouse_encoder_new", 0, mouseEncoderSlot)).toBe(0);
540 expect(call("ghostty_mouse_event_new", 0, mouseEventSlot)).toBe(0);
541 const view = new DataView(memory.buffer);
542 const mouseEncoder = view.getUint32(mouseEncoderSlot, true);
543 const mouseEvent = view.getUint32(mouseEventSlot, true);
544 call("ghostty_mouse_encoder_setopt_from_terminal", mouseEncoder, terminal);
545 const mouseSize = alloc(36);
546 const mouseSizeView = new DataView(memory.buffer, mouseSize, 36);
547 for (const [offset, value] of [
548 [0, 36],
549 [4, 800],
550 [8, 480],
551 [12, 10],
552 [16, 20],
553 ] as const) {
554 mouseSizeView.setUint32(offset, value, true);
555 }
556 call("ghostty_mouse_encoder_setopt", mouseEncoder, 2, mouseSize);
557 call("ghostty_mouse_event_set_action", mouseEvent, 0);
558 call("ghostty_mouse_event_set_button", mouseEvent, 1);
559 const mousePosition = new DataView(new ArrayBuffer(8));
560 mousePosition.setFloat32(0, 15, true);
561 mousePosition.setFloat32(4, 25, true);
562 const mousePositionPointer = alloc(8);
563 new Uint8Array(memory.buffer, mousePositionPointer, 8).set(
564 new Uint8Array(mousePosition.buffer),
565 );
566 call("ghostty_mouse_event_set_position", mouseEvent, mousePositionPointer);
567 const mouseOutput = alloc(128);
568 expect(
569 call("ghostty_mouse_encoder_encode", mouseEncoder, mouseEvent, mouseOutput, 128, written),
570 ).toBe(0);
571 const mouseOutputSize = new DataView(memory.buffer, written, 4).getUint32(0, true);
572 expect(
573 new TextDecoder().decode(new Uint8Array(memory.buffer, mouseOutput, mouseOutputSize)),
574 ).toBe("\u001b[<0;2;2M");
575
576 call("ghostty_mouse_event_free", mouseEvent);
577 call("ghostty_mouse_encoder_free", mouseEncoder);
578 call("ghostty_wasm_free_opaque", mouseEventSlot);
579 call("ghostty_wasm_free_opaque", mouseEncoderSlot);
580 free(mousePositionPointer, 8);
581 free(mouseOutput, 128);
582 free(mouseSize, 36);
583 free(lineSelectionText, lineSelectionSize);
584 free(lineOptions, 28);
585 free(selectionText, selectionSize);
586 free(formatOptions, 16);
587 free(selection, 32);
588 free(wordOptions, 24);
589 free(hyperlink, hyperlinkSize);
590 call("ghostty_wasm_free_usize", written);
591 free(gridRef, 12);
592 free(point, 24);
593 free(inputPointer, input.length);
594 call("ghostty_terminal_free", terminal);
595 call("ghostty_wasm_free_opaque", terminalSlot);
596 free(terminalOptions, 8);
597 });
598
599 it("encodes modified printable keys in Kitty keyboard mode", async () => {
600 const result = await WebAssembly.instantiate(
601 decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer,
602 { env: { log: () => {} } },
603 );
604 const instance = result instanceof WebAssembly.Instance ? result : result.instance;
605 const memory = instance.exports.memory as WebAssembly.Memory;
606 const call = (name: string, ...args: number[]) =>
607 (instance.exports[name] as WasmFunction)(...args);
608 const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size);
609 const free = (pointer: number, size: number) =>
610 call("ghostty_wasm_free_u8_array", pointer, size);
611
612 const terminalOptions = alloc(8);
613 const terminalOptionsView = new DataView(memory.buffer, terminalOptions, 8);
614 terminalOptionsView.setUint16(0, 80, true);
615 terminalOptionsView.setUint16(2, 24, true);
616 const terminalSlot = call("ghostty_wasm_alloc_opaque");
617 expect(call("ghostty_terminal_new", 0, terminalSlot, terminalOptions)).toBe(0);
618 const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true);
619 const kittyMode = new TextEncoder().encode("\u001b[>1u");
620 const kittyModePointer = alloc(kittyMode.length);
621 new Uint8Array(memory.buffer, kittyModePointer, kittyMode.length).set(kittyMode);
622 call("ghostty_terminal_vt_write", terminal, kittyModePointer, kittyMode.length);
623
624 const encoderSlot = call("ghostty_wasm_alloc_opaque");
625 const eventSlot = call("ghostty_wasm_alloc_opaque");
626 expect(call("ghostty_key_encoder_new", 0, encoderSlot)).toBe(0);
627 expect(call("ghostty_key_event_new", 0, eventSlot)).toBe(0);
628 const view = new DataView(memory.buffer);
629 const keyEncoder = view.getUint32(encoderSlot, true);
630 const keyEvent = view.getUint32(eventSlot, true);
631 call("ghostty_key_encoder_setopt_from_terminal", keyEncoder, terminal);
632 call("ghostty_key_event_set_action", keyEvent, 1);
633 call("ghostty_key_event_set_key", keyEvent, ghosttyKeyForCode("KeyC"));
634 call("ghostty_key_event_set_mods", keyEvent, 1 << 1);
635 call("ghostty_key_event_set_consumed_mods", keyEvent, 0);
636 call("ghostty_key_event_set_composing", keyEvent, 0);
637 call("ghostty_key_event_set_unshifted_codepoint", keyEvent, "c".codePointAt(0)!);
638 const text = new TextEncoder().encode("c");
639 const textPointer = alloc(text.length);
640 new Uint8Array(memory.buffer, textPointer, text.length).set(text);
641 call("ghostty_key_event_set_utf8", keyEvent, textPointer, text.length);
642
643 const written = call("ghostty_wasm_alloc_usize");
644 expect(call("ghostty_key_encoder_encode", keyEncoder, keyEvent, 0, 0, written)).toBe(-3);
645 const outputSize = new DataView(memory.buffer, written, 4).getUint32(0, true);
646 const output = alloc(outputSize);
647 expect(
648 call("ghostty_key_encoder_encode", keyEncoder, keyEvent, output, outputSize, written),
649 ).toBe(0);
650 const outputLength = new DataView(memory.buffer, written, 4).getUint32(0, true);
651 expect(new TextDecoder().decode(new Uint8Array(memory.buffer, output, outputLength))).toBe(
652 "\u001b[99;5u",
653 );
654
655 const remappedText = new TextEncoder().encode("j");
656 const remappedTextPointer = alloc(remappedText.length);
657 new Uint8Array(memory.buffer, remappedTextPointer, remappedText.length).set(remappedText);
658 call("ghostty_key_event_set_unshifted_codepoint", keyEvent, "j".codePointAt(0)!);
659 call("ghostty_key_event_set_utf8", keyEvent, remappedTextPointer, remappedText.length);
660 expect(call("ghostty_key_encoder_encode", keyEncoder, keyEvent, 0, 0, written)).toBe(-3);
661 const remappedOutputSize = new DataView(memory.buffer, written, 4).getUint32(0, true);
662 const remappedOutput = alloc(remappedOutputSize);
663 expect(
664 call(
665 "ghostty_key_encoder_encode",
666 keyEncoder,
667 keyEvent,
668 remappedOutput,
669 remappedOutputSize,
670 written,
671 ),
672 ).toBe(0);
673 const remappedOutputLength = new DataView(memory.buffer, written, 4).getUint32(0, true);
674 expect(
675 new TextDecoder().decode(new Uint8Array(memory.buffer, remappedOutput, remappedOutputLength)),
676 ).toBe("\u001b[106;5u");
677
678 // Without the Kitty report-event-types flag a release encodes nothing, so
679 // the surface's keyup handler stays silent for legacy sessions.
680 call("ghostty_key_event_set_action", keyEvent, 0);
681 expect(call("ghostty_key_encoder_encode", keyEncoder, keyEvent, 0, 0, written)).toBe(0);
682 expect(new DataView(memory.buffer, written, 4).getUint32(0, true)).toBe(0);
683
684 // With report-event-types enabled the same release encodes an event-typed code.
685 const reportEvents = new TextEncoder().encode("\u001b[>3u");
686 const reportEventsPointer = alloc(reportEvents.length);
687 new Uint8Array(memory.buffer, reportEventsPointer, reportEvents.length).set(reportEvents);
688 call("ghostty_terminal_vt_write", terminal, reportEventsPointer, reportEvents.length);
689 call("ghostty_key_encoder_setopt_from_terminal", keyEncoder, terminal);
690 expect(call("ghostty_key_encoder_encode", keyEncoder, keyEvent, 0, 0, written)).toBe(-3);
691 const releaseSize = new DataView(memory.buffer, written, 4).getUint32(0, true);
692 const releaseOutput = alloc(releaseSize);
693 expect(
694 call("ghostty_key_encoder_encode", keyEncoder, keyEvent, releaseOutput, releaseSize, written),
695 ).toBe(0);
696 const releaseLength = new DataView(memory.buffer, written, 4).getUint32(0, true);
697 expect(
698 new TextDecoder().decode(new Uint8Array(memory.buffer, releaseOutput, releaseLength)),
699 ).toBe("\u001b[106;5:3u");
700 free(releaseOutput, releaseSize);
701 free(reportEventsPointer, reportEvents.length);
702
703 free(remappedOutput, remappedOutputSize);
704 free(remappedTextPointer, remappedText.length);
705 free(output, outputSize);
706 call("ghostty_wasm_free_usize", written);
707 free(textPointer, text.length);
708 call("ghostty_key_event_free", keyEvent);
709 call("ghostty_key_encoder_free", keyEncoder);
710 call("ghostty_wasm_free_opaque", eventSlot);
711 call("ghostty_wasm_free_opaque", encoderSlot);
712 free(kittyModePointer, kittyMode.length);
713 call("ghostty_terminal_free", terminal);
714 call("ghostty_wasm_free_opaque", terminalSlot);
715 free(terminalOptions, 8);
716 });
717});
apps/web/src/terminal/ghostty/surface.test.ts deleted-1541
......@@ -1,1541 +0,0 @@
1import { afterEach, describe, expect, it, vi } from "vite-plus/test";
2
3import { GhosttyTerminalCore, type GhosttyCell, type GhosttyRow } from "./core";
4import {
5 DEFAULT_TERMINAL_FONT_FAMILY,
6 DEFAULT_TERMINAL_FONT_SIZE,
7 advanceTerminalSelectionClickSequence,
8 applyTerminalCopyEvent,
9 clearPrimedTerminalCopyInput,
10 ghosttyMouseButton,
11 isTerminalAltGraphText,
12 isTerminalCompositionCommitInput,
13 isTerminalCompositionKey,
14 isTerminalCopyShortcut,
15 isTerminalPasteShortcut,
16 loadTerminalFontFamily,
17 primeTerminalCopyInput,
18 resolveTerminalMouseData,
19 resolveTerminalMouseTrackingState,
20 shouldBlinkTerminalCursor,
21 shouldReportTerminalMouse,
22 terminalGridCellAt,
23 terminalScrollbarGeometry,
24 terminalScrollbarOffsetAtPointer,
25 terminalLinkAtPositionWithRange,
26 terminalContentOriginY,
27 terminalFontFamily,
28 terminalFontSize,
29 terminalWheelArrowData,
30 terminalWheelDeltaRows,
31 GhosttyTerminalSurface,
32 type GhosttyTerminalSurfaceOptions,
33} from "./surface";
34
35vi.mock("./vendor/ghostty-vt.wasm?url", async () => ({
36 default: (await import("./vendor/ghostty-vt.wasm?inline")).default,
37}));
38vi.mock("./vendor/ghostty-write-pty.wasm?url&no-inline", async () => ({
39 default: (await import("./vendor/ghostty-write-pty.wasm?inline")).default,
40}));
41
42describe("GhosttyTerminalSurface visibility", () => {
43 const surfaces = new Set<GhosttyTerminalSurface>();
44
45 function key(
46 surface: GhosttyTerminalSurface,
47 key: string,
48 code: string,
49 modifiers: Partial<
50 Pick<
51 KeyboardEvent,
52 | "altKey"
53 | "ctrlKey"
54 | "metaKey"
55 | "shiftKey"
56 | "isComposing"
57 | "keyCode"
58 | "getModifierState"
59 >
60 > = {},
61 type = "keydown",
62 ) {
63 const event = Object.assign(new Event(type, { cancelable: true }), {
64 key,
65 code,
66 altKey: false,
67 ctrlKey: false,
68 metaKey: false,
69 shiftKey: false,
70 isComposing: false,
71 keyCode: 0,
72 getModifierState: () => false,
73 ...modifiers,
74 });
75 surface.input.dispatchEvent(event);
76 return event;
77 }
78
79 // Keep the real surface, renderer, and WASM core. Only browser layout and
80 // scheduling are replaced so tests can count work while the terminal is hidden.
81 function createHarness() {
82 vi.useFakeTimers();
83 const frames = new Map<number, FrameRequestCallback>();
84 const resizeTargets = new Map<(entries: { target: Element }[]) => void, Set<Element>>();
85 const paint = vi.fn((_operation: string, _args: ReadonlyArray<unknown>) => {});
86 let frameId = 0;
87 const requestFrame = vi.fn((callback: FrameRequestCallback) => {
88 frames.set(++frameId, callback);
89 return frameId;
90 });
91
92 class TerminalTestElement extends EventTarget {
93 style: Record<string, string> = {};
94 parentElement: TerminalTestElement | null = null;
95 clientWidth = 168;
96 clientHeight = 104;
97 width = 300;
98 height = 150;
99 value = "";
100 private readonly captures = new Set<number>();
101
102 setAttribute() {}
103 append(...children: TerminalTestElement[]) {
104 for (const child of children) child.parentElement = this;
105 }
106 replaceChildren(...children: TerminalTestElement[]) {
107 this.append(...children);
108 }
109 remove() {
110 this.parentElement = null;
111 }
112 getContext() {
113 return context;
114 }
115 focus() {
116 this.dispatchEvent(new Event("focus"));
117 }
118 setPointerCapture(pointerId: number) {
119 this.captures.add(pointerId);
120 }
121 hasPointerCapture(pointerId: number) {
122 return this.captures.has(pointerId);
123 }
124 releasePointerCapture(pointerId: number) {
125 this.captures.delete(pointerId);
126 }
127 getBoundingClientRect() {
128 return { left: 0, top: 0, right: 168, bottom: 104, width: 168, height: 104 };
129 }
130 }
131
132 const canvas = new TerminalTestElement();
133 const mount = new TerminalTestElement();
134 const context = {
135 canvas,
136 beginPath() {},
137 clip() {},
138 rect() {},
139 resetTransform() {},
140 restore() {},
141 save() {},
142 setTransform() {},
143 fillRect: (...args: number[]) => paint("fillRect", args),
144 strokeRect: (...args: number[]) => paint("strokeRect", args),
145 fillText: (...args: [string, number, number, number?]) => paint("fillText", args),
146 measureText: (text: string) => ({
147 width: text.length * 8,
148 actualBoundingBoxAscent: 9,
149 actualBoundingBoxDescent: 3,
150 }),
151 };
152 vi.stubGlobal("document", {
153 createElement: (tag: string) => (tag === "canvas" ? canvas : new TerminalTestElement()),
154 fonts: Object.assign(new EventTarget(), { load: async () => [], add() {} }),
155 });
156 vi.stubGlobal(
157 "window",
158 Object.assign(new EventTarget(), {
159 devicePixelRatio: 1,
160 innerHeight: 104,
161 requestAnimationFrame: requestFrame,
162 cancelAnimationFrame: (id: number) => frames.delete(id),
163 setTimeout,
164 clearTimeout,
165 setInterval,
166 clearInterval,
167 matchMedia: () => Object.assign(new EventTarget(), { matches: false }),
168 }),
169 );
170 vi.stubGlobal(
171 "ResizeObserver",
172 class {
173 constructor(private readonly callback: (entries: { target: Element }[]) => void) {
174 resizeTargets.set(callback, new Set());
175 }
176 observe(target: Element) {
177 resizeTargets.get(this.callback)?.add(target);
178 }
179 unobserve(target: Element) {
180 resizeTargets.get(this.callback)?.delete(target);
181 }
182 disconnect() {
183 resizeTargets.delete(this.callback);
184 }
185 },
186 );
187 const snapshot = vi.spyOn(GhosttyTerminalCore.prototype, "snapshot");
188 const onData = vi.fn<(data: string) => void>();
189
190 return {
191 mount,
192 frames,
193 paint,
194 requestFrame,
195 snapshot,
196 onData,
197 get renderedSnapshot() {
198 const result = snapshot.mock.results.at(-1);
199 if (result?.type !== "return") throw new Error("No terminal snapshot was rendered");
200 return result.value;
201 },
202 flushFrame() {
203 const queued = [...frames.values()];
204 frames.clear();
205 for (const callback of queued) callback(0);
206 },
207 resize() {
208 for (const [callback, targets] of resizeTargets) {
209 callback([...targets].map((target) => ({ target })));
210 }
211 },
212 pointer(
213 type: string,
214 clientX: number,
215 buttons: number,
216 modifiers: boolean | Partial<Pick<MouseEvent, "ctrlKey" | "metaKey" | "shiftKey">> = {},
217 button = 0,
218 clientY = 5,
219 ) {
220 canvas.dispatchEvent(
221 Object.assign(new Event(type, { cancelable: true }), {
222 clientX,
223 clientY,
224 pointerId: 1,
225 button,
226 buttons,
227 ...(typeof modifiers === "boolean" ? { shiftKey: modifiers } : modifiers),
228 }),
229 );
230 },
231 touch(type: string, clientY: number, pointerId = 2) {
232 canvas.dispatchEvent(
233 Object.assign(new Event(type, { cancelable: true }), {
234 clientX: 40,
235 clientY,
236 pointerId,
237 pointerType: "touch",
238 isPrimary: pointerId === 2,
239 button: 0,
240 buttons: type === "pointerup" ? 0 : 1,
241 shiftKey: false,
242 }),
243 );
244 },
245 async create(options: Partial<GhosttyTerminalSurfaceOptions> = {}) {
246 const surface = await GhosttyTerminalSurface.create(mount as unknown as HTMLElement, {
247 theme: {
248 foreground: { r: 255, g: 255, b: 255 },
249 background: { r: 0, g: 0, b: 0 },
250 cursor: { r: 255, g: 255, b: 255 },
251 },
252 onData,
253 onResize() {},
254 onSelectionChange() {},
255 beforeKey: () => false,
256 onLinkActivate() {},
257 ...options,
258 get visible() {
259 return options.visible ?? true;
260 },
261 });
262 surfaces.add(surface);
263 return surface;
264 },
265 };
266 }
267
268 afterEach(() => {
269 for (const surface of surfaces) surface.dispose();
270 surfaces.clear();
271 vi.useRealTimers();
272 vi.unstubAllGlobals();
273 vi.restoreAllMocks();
274 });
275
276 it.each([
277 { platform: "Linux x86_64", primary: { ctrlKey: true } },
278 { platform: "MacIntel", primary: { metaKey: true } },
279 ])(
280 "navigates history from terminal input without sending keys ($platform)",
281 async ({ platform, primary }) => {
282 vi.stubGlobal("navigator", { platform });
283 const harness = createHarness();
284 const surface = await harness.create({ beforeKey: () => true });
285 surface.write(Array.from({ length: 20 }, (_, index) => `line ${index}`).join("\r\n"));
286 harness.flushFrame();
287 const latest = harness.renderedSnapshot.rowData[0]?.text;
288 expect(surface.isAtBottom()).toBe(true);
289
290 expect(key(surface, "PageUp", "PageUp", { shiftKey: true }).defaultPrevented).toBe(true);
291 harness.flushFrame();
292 expect(harness.renderedSnapshot.rowData[0]?.text).toContain("line 8");
293 expect(surface.isAtBottom()).toBe(false);
294 key(surface, "PageDown", "PageDown", { shiftKey: true });
295 harness.flushFrame();
296 expect(harness.renderedSnapshot.rowData[0]?.text).toBe(latest);
297
298 key(surface, "Home", "Home", { ...primary, shiftKey: true });
299 harness.flushFrame();
300 expect(harness.renderedSnapshot.rowData[0]?.text).toContain("line 0");
301 key(surface, "End", "End", { ...primary, shiftKey: true });
302 harness.flushFrame();
303 expect(harness.renderedSnapshot.rowData[0]?.text).toBe(latest);
304 expect(surface.isAtBottom()).toBe(true);
305 expect(harness.onData).not.toHaveBeenCalled();
306 },
307 );
308
309 it("forwards navigation presses and releases when scrollback state is unavailable", async () => {
310 vi.stubGlobal("navigator", { platform: "Linux x86_64" });
311 const harness = createHarness();
312 const surface = await harness.create({ beforeKey: () => true });
313 surface.write("history\x1b[>31u");
314 vi.spyOn(GhosttyTerminalCore.prototype, "scrollbarState").mockReturnValue(null);
315
316 for (const [name, modifiers] of [
317 ["PageUp", { shiftKey: true }],
318 ["Home", { ctrlKey: true, shiftKey: true }],
319 ] as const) {
320 key(surface, name, name, modifiers);
321 key(surface, name, name, modifiers, "keyup");
322 }
323 expect(harness.onData).toHaveBeenCalledTimes(4);
324 expect(harness.onData.mock.calls[1]?.[0]).toContain(":3");
325 expect(harness.onData.mock.calls[3]?.[0]).toContain(":3");
326 });
327
328 it("selects full history and keeps selection coordinates available outside the viewport", async () => {
329 vi.stubGlobal("navigator", { platform: "Linux x86_64" });
330 const harness = createHarness();
331 const surface = await harness.create({ beforeKey: () => true });
332 const lines = Array.from({ length: 20 }, (_, index) => `line ${index}`);
333 surface.write(lines.join("\r\n"));
334 harness.flushFrame();
335 surface.selectAll();
336 harness.flushFrame();
337 expect(surface.getSelection()).toBe(lines.join("\n"));
338 expect(surface.getSelectionPosition()).toMatchObject({ start: { x: 0, y: 0 } });
339 expect(surface.getSelectionPosition()?.end.y).toBe(19);
340 expect(surface.getSelectionEndClientRect()).not.toBeNull();
341 expect(harness.renderedSnapshot.rowData.every((row) => row.cells[0]?.selected)).toBe(true);
342
343 key(surface, "Home", "Home", { ctrlKey: true, shiftKey: true });
344 harness.flushFrame();
345 surface.selectAll();
346 expect(surface.getSelection()).toBe(lines.join("\n"));
347 expect(surface.getSelectionPosition()?.end.y).toBe(19);
348 expect(surface.getSelectionEndClientRect()).toBeNull();
349 surface.clearSelection();
350 expect(surface.getSelection()).toBe("");
351 expect(surface.getSelectionPosition()).toBeNull();
352 });
353
354 it.each([
355 { platform: "Linux x86_64", modifiers: { ctrlKey: true, shiftKey: true } },
356 { platform: "MacIntel", modifiers: { metaKey: true } },
357 ])("selects all through the local shortcut ($platform)", async ({ platform, modifiers }) => {
358 vi.stubGlobal("navigator", { platform });
359 const harness = createHarness();
360 const surface = await harness.create({ beforeKey: () => true });
361 surface.write("hello\r\nworld");
362 harness.flushFrame();
363 expect(key(surface, "a", "KeyA", modifiers).defaultPrevented).toBe(true);
364 expect(surface.getSelection()).toBe("hello\nworld");
365 expect(surface.getSelectionPosition()).not.toBeNull();
366 expect(harness.onData).not.toHaveBeenCalled();
367 });
368
369 it("preserves shell Ctrl+A and ordinary application navigation", async () => {
370 vi.stubGlobal("navigator", { platform: "Linux x86_64" });
371 const harness = createHarness();
372 const surface = await harness.create({ beforeKey: () => true });
373 key(surface, "a", "KeyA", { ctrlKey: true });
374 key(surface, "PageUp", "PageUp");
375 key(surface, "PageDown", "PageDown");
376 key(surface, "Home", "Home");
377 key(surface, "End", "End");
378 expect(harness.onData.mock.calls.map(([data]) => data)).toEqual([
379 "\x01",
380 "\x1b[5~",
381 "\x1b[6~",
382 "\x1b[H",
383 "\x1b[F",
384 ]);
385 expect(surface.hasSelection()).toBe(false);
386 });
387
388 it("leaves modified history keys with alternate-screen applications", async () => {
389 vi.stubGlobal("navigator", { platform: "Linux x86_64" });
390 const harness = createHarness();
391 const surface = await harness.create({ beforeKey: () => true });
392 surface.write("\x1b[?1049happ");
393 key(surface, "PageUp", "PageUp", { shiftKey: true });
394 key(surface, "End", "End", { ctrlKey: true, shiftKey: true });
395 expect(harness.onData.mock.calls.map(([data]) => data)).toEqual(["\x1b[5;2~", "\x1b[1;6F"]);
396 surface.selectAll();
397 expect(surface.getSelection()).toBe("app");
398 surface.write("\x1b[?1049l");
399 expect(surface.getSelection()).toBe("");
400 });
401
402 it("suppresses local shortcut releases in Kitty sessions, including read-only observers", async () => {
403 vi.stubGlobal("navigator", { platform: "Linux x86_64" });
404 const harness = createHarness();
405 const surface = await harness.create({ beforeKey: () => true });
406 surface.input.readOnly = true;
407 surface.write("history\x1b[>31u");
408 for (const [name, code, modifiers] of [
409 ["PageUp", "PageUp", { shiftKey: true }],
410 ["Home", "Home", { ctrlKey: true, shiftKey: true }],
411 ["a", "KeyA", { ctrlKey: true, shiftKey: true }],
412 ] as const) {
413 key(surface, name, code, modifiers);
414 key(surface, name, code, modifiers, "keyup");
415 }
416 expect(surface.getSelection()).toBe("history");
417 expect(harness.onData).not.toHaveBeenCalled();
418
419 // An ordinary application key still has both press and release reports.
420 key(surface, "PageUp", "PageUp");
421 key(surface, "PageUp", "PageUp", {}, "keyup");
422 expect(harness.onData).toHaveBeenCalledTimes(2);
423 expect(harness.onData.mock.calls[1]?.[0]).toContain(":3");
424 });
425
426 it("honors host interception, IME candidates, and AltGr before local shortcuts", async () => {
427 vi.stubGlobal("navigator", { platform: "Linux x86_64" });
428 const harness = createHarness();
429 let allow = false;
430 const surface = await harness.create({ beforeKey: () => allow });
431 surface.write("hello\x1b[>31u");
432 key(surface, "a", "KeyA", { ctrlKey: true, shiftKey: true });
433 key(surface, "a", "KeyA", {}, "keyup");
434 expect(surface.hasSelection()).toBe(false);
435 allow = true;
436 surface.input.value = "候補";
437 key(surface, "a", "KeyA", { ctrlKey: true, shiftKey: true, isComposing: true });
438 key(surface, "a", "KeyA", {}, "keyup");
439 key(surface, "a", "KeyA", { ctrlKey: true, shiftKey: true, keyCode: 229 });
440 key(surface, "a", "KeyA", {}, "keyup");
441 key(surface, "a", "KeyA", {
442 ctrlKey: true,
443 shiftKey: true,
444 altKey: true,
445 getModifierState: (modifier) => modifier === "AltGraph",
446 });
447 key(surface, "a", "KeyA", {}, "keyup");
448 expect(surface.input.value).toBe("候補");
449 expect(surface.hasSelection()).toBe(false);
450 expect(harness.onData).not.toHaveBeenCalled();
451 });
452
453 it.each([
454 { platform: "Linux x86_64", modifiers: { ctrlKey: true, metaKey: false } },
455 { platform: "MacIntel", modifiers: { ctrlKey: false, metaKey: true } },
456 ])(
457 "gates path links and preserves selection and URLs ($platform)",
458 async ({ platform, modifiers }) => {
459 vi.stubGlobal("navigator", { platform });
460 const harness = createHarness();
461 vi.spyOn(Event.prototype, "timeStamp", "get").mockImplementation(() => Date.now());
462 let canOpenPaths = false;
463 const openLink = vi.fn();
464 const surface = await harness.create({
465 canActivateLink: (text) => text.startsWith("https://") || canOpenPaths,
466 onLinkActivate: openLink,
467 });
468 surface.write("/repo/file.ts");
469 harness.flushFrame();
470 harness.pointer("pointermove", 5, 0, modifiers);
471 expect(surface.canvas.style.cursor).toBe("");
472 harness.pointer("pointerdown", 5, 1, modifiers);
473 harness.pointer("pointerup", 5, 0, modifiers);
474 expect(openLink).not.toHaveBeenCalled();
475
476 vi.advanceTimersByTime(501);
477 harness.pointer("pointerdown", 5, 1);
478 harness.pointer("pointermove", 37, 1);
479 harness.pointer("pointerup", 37, 0);
480 expect(surface.getSelection()).toBe("/repo");
481
482 vi.advanceTimersByTime(501);
483 harness.pointer("pointermove", 5, 0, modifiers);
484 canOpenPaths = true;
485 surface.refreshLinkActivation();
486 expect(surface.canvas.style.cursor).toBe("pointer");
487 harness.pointer("pointerdown", 5, 1, modifiers);
488 harness.pointer("pointerup", 5, 0, modifiers);
489 expect(openLink).toHaveBeenCalledExactlyOnceWith("/repo/file.ts", expect.any(Event));
490
491 vi.advanceTimersByTime(501);
492 harness.pointer("pointerdown", 5, 1, modifiers);
493 canOpenPaths = false;
494 surface.refreshLinkActivation();
495 expect(surface.canvas.style.cursor).toBe("");
496 harness.pointer("pointerup", 5, 0, modifiers);
497 expect(openLink).toHaveBeenCalledOnce();
498
499 vi.advanceTimersByTime(501);
500 surface.resetAndWrite("https://t3.codes");
501 harness.flushFrame();
502 harness.pointer("pointermove", 5, 0, modifiers);
503 expect(surface.canvas.style.cursor).toBe("pointer");
504 harness.pointer("pointerdown", 5, 1, modifiers);
505 harness.pointer("pointerup", 5, 0, modifiers);
506 expect(openLink).toHaveBeenLastCalledWith("https://t3.codes", expect.any(Event));
507 },
508 );
509
510 it("resends an unchanged grid when authorization and attachment become ready", async () => {
511 const harness = createHarness();
512 let canOperate = false;
513 let attached = false;
514 const resizePty = vi.fn<(cols: number, rows: number) => void>();
515 const surface = await harness.create({
516 onResize: (cols, rows) => {
517 if (canOperate && attached) resizePty(cols, rows);
518 },
519 });
520 vi.advanceTimersByTime(150);
521 expect(resizePty).not.toHaveBeenCalled();
522
523 canOperate = true;
524 surface.resendSize();
525 vi.advanceTimersByTime(150);
526 expect(resizePty).not.toHaveBeenCalled();
527
528 attached = true;
529 surface.resendSize();
530 vi.advanceTimersByTime(150);
531 expect(resizePty).toHaveBeenCalledExactlyOnceWith(20, 6);
532 surface.fit();
533 vi.advanceTimersByTime(150);
534 expect(resizePty).toHaveBeenCalledOnce();
535
536 canOperate = false;
537 harness.mount.clientWidth = 248;
538 surface.fit();
539 vi.advanceTimersByTime(150);
540 expect(resizePty).toHaveBeenCalledOnce();
541 });
542
543 it("sends the settled grid when a resize drag is released", async () => {
544 const harness = createHarness();
545 const onResize = vi.fn<(cols: number, rows: number) => void>();
546 const surface = await harness.create({ onResize });
547 vi.advanceTimersByTime(150);
548 onResize.mockClear();
549
550 // Mid-drag, the grid reflows locally while the PTY waits for the debounce.
551 harness.mount.clientWidth = 120;
552 harness.resize();
553 expect(surface.cols).toBe(14);
554 expect(onResize).not.toHaveBeenCalled();
555
556 // The release writes the final width before the observer reports it.
557 harness.mount.clientWidth = 96;
558 window.dispatchEvent(new Event("pointerup"));
559 expect(onResize).toHaveBeenCalledExactlyOnceWith(11, 6);
560
561 harness.resize();
562 vi.advanceTimersByTime(150);
563 window.dispatchEvent(new Event("pointerup"));
564 expect(onResize).toHaveBeenCalledOnce();
565 });
566
567 it("replays the current grid on reveal when a hidden host becomes ready", async () => {
568 const harness = createHarness();
569 const onResize = vi.fn<(cols: number, rows: number) => void>();
570 const surface = await harness.create({ onResize });
571 vi.advanceTimersByTime(150);
572 onResize.mockClear();
573
574 surface.setVisible(false);
575 surface.resendSize();
576 vi.advanceTimersByTime(150);
577 expect(onResize).not.toHaveBeenCalled();
578
579 surface.setVisible(true);
580 vi.advanceTimersByTime(150);
581 expect(onResize).toHaveBeenCalledExactlyOnceWith(20, 6);
582 });
583
584 it("stops hidden snapshots and paint while preserving live VT replies and the next cursor", async () => {
585 const harness = createHarness();
586 const surface = await harness.create();
587 surface.focus();
588 surface.write("ready\x1b[1 q");
589 harness.flushFrame();
590 vi.advanceTimersByTime(500);
591 harness.flushFrame();
592 surface.write("queued");
593 expect(harness.frames.size).toBe(1);
594 surface.setVisible(false);
595 expect(harness.frames.size).toBe(0);
596 expect(vi.getTimerCount()).toBe(0);
597 harness.snapshot.mockClear();
598 harness.paint.mockClear();
599 harness.requestFrame.mockClear();
600
601 surface.write("\x1b[2J\x1b[H\x1b[3");
602 surface.write("1mhidden");
603 surface.write("界🙂\x1b[0m\x1b[5n\x1b[6n");
604 surface.fit();
605 vi.advanceTimersByTime(2_000);
606 harness.flushFrame();
607
608 expect(harness.onData.mock.calls).toEqual([["\x1b[0n"], ["\x1b[1;11R"]]);
609 expect(harness.snapshot).not.toHaveBeenCalled();
610 expect(harness.paint).not.toHaveBeenCalled();
611 expect(harness.requestFrame).not.toHaveBeenCalled();
612
613 surface.setVisible(true);
614 expect(harness.snapshot).toHaveBeenCalledTimes(1);
615 expect(harness.renderedSnapshot).toMatchObject({ cursorX: 10, cursorY: 0 });
616 expect(harness.renderedSnapshot.rowData[0]?.text).toContain("hidden");
617 expect(harness.paint.mock.calls).toContainEqual(["fillRect", [84, 4, 8, 16]]);
618 expect(harness.frames.size).toBe(0);
619 });
620
621 it("scrolls touch drags in both directions without selecting or sending input", async () => {
622 const harness = createHarness();
623 const surface = await harness.create();
624 surface.write(Array.from({ length: 50 }, (_, i) => `row ${i}\r\n`).join(""));
625 harness.flushFrame();
626 const bottom = harness.renderedSnapshot.rowData[0]?.text;
627 harness.touch("pointerdown", 20);
628 for (let y = 22; y <= 68; y += 2) harness.touch("pointermove", y);
629 harness.flushFrame();
630 expect(harness.renderedSnapshot.rowData[0]?.text).not.toBe(bottom);
631 expect(surface.getSelection()).toBe("");
632 harness.touch("pointermove", 20);
633 harness.touch("pointerup", 20);
634 harness.flushFrame();
635 expect(harness.renderedSnapshot.rowData[0]?.text).toBe(bottom);
636 expect(harness.onData).not.toHaveBeenCalled();
637 });
638
639 it("sends touch scrolling to alternate-screen applications", async () => {
640 const harness = createHarness();
641 const surface = await harness.create();
642 surface.write("\x1b[?1049h\x1b[?1h");
643 harness.touch("pointerdown", 20);
644 harness.touch("pointermove", 52);
645 harness.touch("pointerup", 52);
646 expect(harness.onData.mock.calls).toEqual([["\x1bOA\x1bOA"]]);
647 });
648
649 it("reports touch scrolling and taps to mouse-tracking applications", async () => {
650 const harness = createHarness();
651 const surface = await harness.create();
652 surface.write("\x1b[?1000h\x1b[?1006h");
653 harness.touch("pointerdown", 20);
654 harness.touch("pointermove", 52);
655 harness.touch("pointerup", 52);
656 expect(harness.onData.mock.calls.map(([data]) => data)).toEqual([
657 "\x1b[<64;5;4M",
658 "\x1b[<64;5;4M",
659 ]);
660 harness.onData.mockClear();
661 harness.touch("pointerdown", 20);
662 harness.touch("pointerup", 20);
663 expect(harness.onData.mock.calls.map(([data]) => data)).toEqual([
664 "\x1b[<0;5;2M",
665 "\x1b[<0;5;2m",
666 ]);
667 });
668
669 it("opens a link on touch tap but not on drag or cancellation", async () => {
670 const harness = createHarness();
671 const onLinkActivate = vi.fn();
672 const surface = await harness.create({ onLinkActivate });
673 surface.write("https://example.com");
674 harness.flushFrame();
675 harness.touch("pointerdown", 5);
676 harness.touch("pointerup", 5);
677 expect(onLinkActivate).toHaveBeenCalledTimes(1);
678 harness.touch("pointerdown", 5);
679 harness.touch("pointermove", 37);
680 harness.touch("pointerup", 37);
681 harness.touch("pointerdown", 5);
682 harness.touch("pointercancel", 5);
683 expect(onLinkActivate).toHaveBeenCalledTimes(1);
684 });
685
686 it("ends a canceled touch drag and ignores a second finger", async () => {
687 const harness = createHarness();
688 const surface = await harness.create();
689 surface.write(Array.from({ length: 50 }, (_, i) => `row ${i}\r\n`).join(""));
690 harness.flushFrame();
691 const bottom = harness.renderedSnapshot.rowData[0]?.text;
692 harness.touch("pointerdown", 20);
693 harness.touch("pointerdown", 20, 3);
694 harness.touch("pointermove", 68, 3);
695 harness.touch("pointercancel", 20);
696 harness.touch("pointermove", 68);
697 harness.flushFrame();
698 expect(harness.renderedSnapshot.rowData[0]?.text).toBe(bottom);
699 harness.touch("pointerdown", 20);
700 harness.touch("pointermove", 68);
701 harness.touch("pointerup", 68);
702 harness.flushFrame();
703 expect(harness.renderedSnapshot.rowData[0]?.text).not.toBe(bottom);
704 });
705
706 it("keeps the selection on reveal and applies a hidden selection clear", async () => {
707 const harness = createHarness();
708 const surface = await harness.create();
709 surface.write("hello world");
710 harness.flushFrame();
711 harness.pointer("pointerdown", 5, 1);
712 harness.pointer("pointermove", 37, 1);
713 harness.pointer("pointerup", 37, 0);
714 harness.flushFrame();
715 expect(surface.getSelection()).toBe("hello");
716 const position = surface.getSelectionPosition();
717
718 surface.setVisible(false);
719 harness.snapshot.mockClear();
720 surface.setVisible(true);
721 expect(harness.snapshot).toHaveBeenCalledTimes(1);
722 expect(surface.getSelectionPosition()).toEqual(position);
723 expect(
724 harness.renderedSnapshot.rowData[0]?.cells.slice(0, 5).every((cell) => cell.selected),
725 ).toBe(true);
726
727 surface.setVisible(false);
728 harness.snapshot.mockClear();
729 surface.clearSelection();
730 surface.write("!");
731 expect(harness.snapshot).not.toHaveBeenCalled();
732 surface.setVisible(true);
733 expect(harness.snapshot).toHaveBeenCalledTimes(1);
734 expect(surface.getSelection()).toBe("");
735 expect(surface.getSelectionPosition()).toBeNull();
736 expect(harness.renderedSnapshot.rowData[0]?.cells.some((cell) => cell.selected)).toBe(false);
737 });
738
739 it("scrolls a selection drag held at the terminal edge, even when the window ends there", async () => {
740 const harness = createHarness();
741 const surface = await harness.create();
742 surface.write(Array.from({ length: 40 }, (_, index) => `line ${index}`).join("\r\n"));
743 harness.flushFrame();
744 const latest = harness.renderedSnapshot.rowData[0]?.text;
745
746 harness.pointer("pointerdown", 5, 1, {}, 0, 50);
747 harness.pointer("pointermove", 5, 1, {}, 0, 1);
748 vi.advanceTimersByTime(400);
749 harness.flushFrame();
750 expect(surface.isAtBottom()).toBe(false);
751
752 harness.pointer("pointermove", 5, 1, {}, 0, 103);
753 vi.advanceTimersByTime(2_000);
754 harness.pointer("pointerup", 5, 0, {}, 0, 103);
755 harness.flushFrame();
756 expect(surface.isAtBottom()).toBe(true);
757 expect(harness.renderedSnapshot.rowData[0]?.text).toBe(latest);
758 expect(surface.getSelection()).not.toBe("");
759 expect(surface.getSelectionPosition()?.end.y).toBe(39);
760 });
761
762 it("pastes the terminal selection, and only that, on a Linux middle click", async () => {
763 const harness = createHarness();
764 const readText = vi.fn(async () => "clipboard text");
765 vi.stubGlobal("navigator", { platform: "Linux x86_64", clipboard: { readText } });
766 const surface = await harness.create();
767 surface.write("hello world");
768 harness.flushFrame();
769 harness.pointer("pointerdown", 5, 1);
770 harness.pointer("pointermove", 37, 1);
771 harness.pointer("pointerup", 37, 0);
772 expect(surface.getSelection()).toBe("hello");
773
774 harness.onData.mockClear();
775 harness.pointer("pointerdown", 5, 4, false, 1);
776 await vi.waitFor(() => expect(harness.onData).toHaveBeenCalled());
777 expect(harness.onData.mock.calls.at(-1)?.[0]).toBe("hello");
778 expect(surface.getSelection()).toBe("hello");
779
780 // Without a selection there is no primary buffer to paste; the clipboard
781 // holds what the user copied and must not be substituted.
782 surface.clearSelection();
783 harness.pointer("pointerdown", 5, 4, false, 1);
784 expect(readText).not.toHaveBeenCalled();
785 });
786
787 it("starts a selection when dragging from a link", async () => {
788 const harness = createHarness();
789 const onLinkActivate = vi.fn();
790 const surface = await harness.create({ onLinkActivate });
791 surface.write("https://example.com");
792 harness.flushFrame();
793
794 harness.pointer("pointerdown", 5, 1);
795 harness.pointer("pointermove", 37, 1);
796 harness.pointer("pointerup", 37, 0);
797
798 expect(onLinkActivate).not.toHaveBeenCalled();
799 expect(surface.getSelection()).toBe("https");
800 });
801
802 it("keeps a link click active through slight pointer movement", async () => {
803 const harness = createHarness();
804 const onLinkActivate = vi.fn();
805 const surface = await harness.create({ onLinkActivate });
806 surface.write("https://example.com");
807 harness.flushFrame();
808
809 harness.pointer("pointerdown", 5, 1);
810 harness.pointer("pointermove", 6, 1);
811 harness.pointer("pointerup", 6, 0);
812
813 expect(onLinkActivate).toHaveBeenCalledOnce();
814 });
815
816 it("uses repeated link clicks for word and line selection", async () => {
817 const harness = createHarness();
818 const onLinkActivate = vi.fn();
819 const surface = await harness.create({ onLinkActivate });
820 surface.write("https://example.com tail");
821 harness.flushFrame();
822
823 harness.pointer("pointerdown", 5, 1);
824 harness.pointer("pointerup", 5, 0);
825 harness.pointer("pointerdown", 5, 1);
826 harness.pointer("pointerup", 5, 0);
827 expect(onLinkActivate).toHaveBeenCalledOnce();
828 expect(surface.getSelection()).not.toBe("");
829
830 harness.pointer("pointerdown", 5, 1);
831 harness.pointer("pointerup", 5, 0);
832 expect(onLinkActivate).toHaveBeenCalledOnce();
833 expect(surface.getSelection()).toBe("https://example.com tail");
834 });
835
836 it("uses Shift drags over links for selection", async () => {
837 const harness = createHarness();
838 const onLinkActivate = vi.fn();
839 const surface = await harness.create({ onLinkActivate });
840 surface.write("https://example.com");
841 harness.flushFrame();
842
843 harness.pointer("pointerdown", 5, 1, true);
844 harness.pointer("pointermove", 37, 1, true);
845 harness.pointer("pointerup", 37, 0, true);
846 expect(onLinkActivate).not.toHaveBeenCalled();
847 expect(surface.getSelection()).toBe("https");
848 });
849
850 it("does not activate a link replaced before pointer release", async () => {
851 const harness = createHarness();
852 const onLinkActivate = vi.fn();
853 const surface = await harness.create({ onLinkActivate });
854 surface.write("https://first.example");
855 harness.flushFrame();
856
857 harness.pointer("pointerdown", 5, 1);
858 surface.write("\x1b[2J\x1b[Hhttps://second.example");
859 harness.flushFrame();
860 harness.pointer("pointerup", 5, 0);
861
862 expect(onLinkActivate).not.toHaveBeenCalled();
863 });
864
865 it("stops zero-size mounts and repaints when the same size returns", async () => {
866 const harness = createHarness();
867 const surface = await harness.create();
868 surface.focus();
869 surface.write("visible");
870 harness.flushFrame();
871 harness.snapshot.mockClear();
872 harness.paint.mockClear();
873 harness.mount.clientWidth = 0;
874 surface.write("!");
875 harness.flushFrame();
876 harness.requestFrame.mockClear();
877 for (let index = 0; index < 8; index += 1) surface.write("x");
878 vi.advanceTimersByTime(2_000);
879
880 expect(harness.snapshot).not.toHaveBeenCalled();
881 expect(harness.paint).not.toHaveBeenCalled();
882 expect(harness.requestFrame).not.toHaveBeenCalled();
883 harness.mount.clientWidth = 168;
884 harness.resize();
885 expect(harness.snapshot).toHaveBeenCalledTimes(1);
886 expect(harness.renderedSnapshot.rowData[0]?.text).toContain("visible!xxxxxxxx");
887 expect(harness.paint).toHaveBeenCalled();
888 });
889
890 it.each([false, true])(
891 "uses visibility %s if it changes while WASM initializes",
892 async (visible) => {
893 const harness = createHarness();
894 let markCreated!: () => void;
895 const created = new Promise<void>((resolve) => {
896 markCreated = resolve;
897 });
898 let finishLoading!: () => void;
899 const release = new Promise<void>((resolve) => {
900 finishLoading = resolve;
901 });
902 const create = GhosttyTerminalCore.create;
903 vi.spyOn(GhosttyTerminalCore, "create").mockImplementation(async (...args) => {
904 const core = await create(...args);
905 markCreated();
906 await release;
907 return core;
908 });
909 let currentVisible = !visible;
910 const pending = harness.create({
911 get visible() {
912 return currentVisible;
913 },
914 });
915 await created;
916 currentVisible = visible;
917 harness.paint.mockClear();
918 finishLoading();
919 const surface = await pending;
920
921 expect(harness.snapshot).toHaveBeenCalledTimes(visible ? 1 : 0);
922 if (!visible) expect(harness.paint).not.toHaveBeenCalled();
923 surface.write("ready\x1b[5n");
924 expect(harness.onData).toHaveBeenCalledWith("\x1b[0n");
925 if (!visible) {
926 expect(harness.frames.size).toBe(0);
927 surface.setVisible(true);
928 } else {
929 harness.flushFrame();
930 }
931 expect(harness.renderedSnapshot.rowData[0]?.text).toContain("ready");
932 },
933 );
934});
935
936const cell = (text: string): GhosttyCell => ({
937 text,
938 wide: 0,
939 foreground: { r: 255, g: 255, b: 255 },
940 background: { r: 0, g: 0, b: 0 },
941 bold: false,
942 italic: false,
943 invisible: false,
944 strikethrough: false,
945 overline: false,
946 underline: false,
947 selected: false,
948});
949
950describe("isTerminalAltGraphText", () => {
951 it("defers printable AltGr output to the textarea input event", () => {
952 expect(
953 isTerminalAltGraphText({
954 key: "@",
955 getModifierState: (modifier) => modifier === "AltGraph",
956 }),
957 ).toBe(true);
958 expect(
959 isTerminalAltGraphText({
960 key: "ArrowRight",
961 getModifierState: (modifier) => modifier === "AltGraph",
962 }),
963 ).toBe(false);
964 });
965});
966
967describe("terminalGridCellAt", () => {
968 const options = {
969 bounds: { left: 100, top: 200 },
970 cols: 3,
971 rows: 2,
972 metrics: { width: 10, height: 20 },
973 padding: 4,
974 originY: 24,
975 };
976
977 it("maps points inside the rendered grid without clamping its padding", () => {
978 expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 224 })).toEqual({
979 x: 0,
980 y: 0,
981 });
982 expect(terminalGridCellAt({ ...options, clientX: 133, clientY: 263 })).toEqual({
983 x: 2,
984 y: 1,
985 });
986 expect(terminalGridCellAt({ ...options, clientX: 103, clientY: 224 })).toBeNull();
987 expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 223 })).toBeNull();
988 expect(terminalGridCellAt({ ...options, clientX: 134, clientY: 224 })).toBeNull();
989 expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 264 })).toBeNull();
990 });
991});
992
993describe("shouldBlinkTerminalCursor", () => {
994 const blinking = {
995 focused: true,
996 cursorBlinking: true,
997 cursorVisible: true,
998 reducedMotion: false,
999 };
1000
1001 it("blinks a focused visible cursor the terminal asked to blink", () => {
1002 expect(shouldBlinkTerminalCursor(blinking)).toBe(true);
1003 });
1004
1005 it("holds the cursor steady when blinking would be unwanted", () => {
1006 // Unfocused surfaces draw a steady hollow cursor, DECSCUSR steady styles and
1007 // DEC mode 12 turn blinking off, a hidden cursor has nothing to toggle, and
1008 // reduced-motion readers get no permanently animating element.
1009 expect(shouldBlinkTerminalCursor({ ...blinking, focused: false })).toBe(false);
1010 expect(shouldBlinkTerminalCursor({ ...blinking, cursorBlinking: false })).toBe(false);
1011 expect(shouldBlinkTerminalCursor({ ...blinking, cursorVisible: false })).toBe(false);
1012 expect(shouldBlinkTerminalCursor({ ...blinking, reducedMotion: true })).toBe(false);
1013 });
1014});
1015
1016describe("terminalLinkAtPositionWithRange", () => {
1017 it("maps terminal cells to UTF-16 offsets after a wide emoji", () => {
1018 const cells = [
1019 cell("🙂"),
1020 cell(""),
1021 ...Array.from("https://t3.codes", (character) => cell(character)),
1022 ];
1023 const row: GhosttyRow = {
1024 cells,
1025 text: cells
1026 .map((value) => value.text || " ")
1027 .join("")
1028 .trimEnd(),
1029 isWrapContinuation: false,
1030 wrapsToNext: false,
1031 };
1032
1033 expect(terminalLinkAtPositionWithRange([row], 0, 2)?.text).toBe("https://t3.codes");
1034 expect(terminalLinkAtPositionWithRange([row], 0, cells.length - 1)?.text).toBe(
1035 "https://t3.codes",
1036 );
1037 expect(terminalLinkAtPositionWithRange([row], 0, 0)).toBeNull();
1038 expect(terminalLinkAtPositionWithRange([row], 0, 8)?.range).toEqual({
1039 start: { x: 2, y: 0 },
1040 end: { x: cells.length - 1, y: 0 },
1041 });
1042 });
1043
1044 it("uses shared path matching and reconstructs soft-wrapped links", () => {
1045 const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({
1046 cells: Array.from(text.padEnd(16), (character) => cell(character)),
1047 text: text.trimEnd(),
1048 isWrapContinuation,
1049 wrapsToNext,
1050 });
1051 const rows = [
1052 row("https://example.", false),
1053 row("com/reference", true),
1054 row("~/project/file", false),
1055 row("C:\\repo\\file.ts", false),
1056 ];
1057
1058 expect(terminalLinkAtPositionWithRange(rows, 0, 8)?.text).toBe("https://example.com/reference");
1059 expect(terminalLinkAtPositionWithRange(rows, 1, 4)?.text).toBe("https://example.com/reference");
1060 expect(terminalLinkAtPositionWithRange(rows, 2, 2)?.text).toBe("~/project/file");
1061 expect(terminalLinkAtPositionWithRange(rows, 3, 4)?.text).toBe("C:\\repo\\file.ts");
1062 expect(terminalLinkAtPositionWithRange(rows, 1, 4)).toEqual({
1063 text: "https://example.com/reference",
1064 range: {
1065 start: { x: 0, y: 0 },
1066 end: { x: 12, y: 1 },
1067 },
1068 });
1069 });
1070
1071 it("refuses links truncated at the viewport edges instead of mis-resolving", () => {
1072 const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({
1073 cells: Array.from(text.padEnd(16), (character) => cell(character)),
1074 text: text.trimEnd(),
1075 isWrapContinuation,
1076 wrapsToNext,
1077 });
1078 // The head of the wrapped line scrolled above the viewport.
1079 const headCut = [row("ple.com/missing", true), row("head", true)];
1080 expect(terminalLinkAtPositionWithRange(headCut, 0, 4)).toBeNull();
1081 // The bottom row soft-wraps on below the viewport.
1082 const tailCut = [row("https://t3.codes", false, true)];
1083 expect(terminalLinkAtPositionWithRange(tailCut, 0, 8)).toBeNull();
1084 // A partial bottom row is provably complete and still resolves.
1085 const complete = [row("https://t3.codes", false), row("", false)];
1086 expect(terminalLinkAtPositionWithRange(complete, 0, 8)?.text).toBe("https://t3.codes");
1087 // A wide grapheme earlier in the row must not break truncation detection:
1088 // the soft-wrap flag decides, not string-length-versus-cell-count.
1089 const wideFull: GhosttyRow = {
1090 cells: [
1091 { ...cell("🙂"), wide: 1 },
1092 { ...cell(""), wide: 2 },
1093 ...Array.from("https://t3.code", (character) => cell(character)),
1094 ],
1095 text: "🙂 https://t3.code",
1096 isWrapContinuation: false,
1097 wrapsToNext: true,
1098 };
1099 expect(terminalLinkAtPositionWithRange([wideFull], 0, 8)).toBeNull();
1100 // Unwritten trailing cells prove the bottom row is complete.
1101 const unwrittenTail: GhosttyRow = {
1102 cells: [
1103 ...Array.from("https://t3.codes", (character) => cell(character)),
1104 cell(""),
1105 cell(""),
1106 ],
1107 text: "https://t3.codes",
1108 isWrapContinuation: false,
1109 wrapsToNext: false,
1110 };
1111 expect(terminalLinkAtPositionWithRange([unwrittenTail], 0, 8)?.text).toBe("https://t3.codes");
1112 });
1113});
1114
1115describe("isTerminalCopyShortcut", () => {
1116 const event = (overrides: Partial<Parameters<typeof isTerminalCopyShortcut>[0]> = {}) => ({
1117 ctrlKey: false,
1118 key: "c",
1119 metaKey: false,
1120 shiftKey: false,
1121 ...overrides,
1122 });
1123
1124 it("keeps Ctrl+C available for SIGINT on macOS", () => {
1125 expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false);
1126 expect(isTerminalCopyShortcut(event({ metaKey: true }), "MacIntel")).toBe(true);
1127 });
1128
1129 it("copies with Ctrl+C and Ctrl+Shift+C elsewhere", () => {
1130 expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(true);
1131 expect(isTerminalCopyShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe(
1132 true,
1133 );
1134 expect(isTerminalCopyShortcut(event({}), "Linux x86_64")).toBe(false);
1135 });
1136
1137 it("uses the produced character instead of the physical key position", () => {
1138 expect(isTerminalCopyShortcut(event({ key: "C", metaKey: true }), "MacIntel")).toBe(true);
1139 expect(isTerminalCopyShortcut(event({ key: "j", metaKey: true }), "MacIntel")).toBe(false);
1140 });
1141
1142 it("supports the conventional Ctrl+Insert copy shortcut", () => {
1143 expect(isTerminalCopyShortcut(event({ key: "Insert", ctrlKey: true }), "Linux x86_64")).toBe(
1144 true,
1145 );
1146 expect(isTerminalCopyShortcut(event({ key: "Insert" }), "Linux x86_64")).toBe(false);
1147 expect(
1148 isTerminalCopyShortcut(
1149 event({ key: "Insert", ctrlKey: true, shiftKey: true }),
1150 "Linux x86_64",
1151 ),
1152 ).toBe(false);
1153 expect(isTerminalCopyShortcut(event({ key: "Insert", ctrlKey: true }), "MacIntel")).toBe(false);
1154 });
1155});
1156
1157describe("applyTerminalCopyEvent", () => {
1158 it("writes the selection and claims the fallback when clipboardData is present", () => {
1159 const setData = vi.fn();
1160 expect(applyTerminalCopyEvent("ls -la", { setData })).toEqual({
1161 preventDefault: true,
1162 claimWriteFallback: true,
1163 });
1164 expect(setData).toHaveBeenCalledWith("text/plain", "ls -la");
1165 });
1166
1167 it("leaves the writeText fallback alive when clipboardData is missing", () => {
1168 // Electron's edit-menu Copy often delivers a copy event with no
1169 // clipboardData. Claiming that event used to skip writeText and copy the
1170 // empty IME textarea, which is the blank clipboard users paste.
1171 expect(applyTerminalCopyEvent("ls -la", null)).toEqual({
1172 preventDefault: false,
1173 claimWriteFallback: false,
1174 });
1175 expect(applyTerminalCopyEvent("", { setData: vi.fn() })).toEqual({
1176 preventDefault: false,
1177 claimWriteFallback: false,
1178 });
1179 });
1180
1181 it("primes the current selection before a copy event with no clipboardData", () => {
1182 const input = {
1183 value: "stale",
1184 selectionStart: 0,
1185 selectionEnd: 0,
1186 select() {
1187 this.selectionStart = 0;
1188 this.selectionEnd = this.value.length;
1189 },
1190 };
1191 primeTerminalCopyInput(input, "git status");
1192 expect(applyTerminalCopyEvent("git status", null)).toEqual({
1193 preventDefault: false,
1194 claimWriteFallback: false,
1195 });
1196 expect(input.value).toBe("git status");
1197 expect(input.selectionStart).toBe(0);
1198 expect(input.selectionEnd).toBe(10);
1199 });
1200});
1201
1202describe("primeTerminalCopyInput", () => {
1203 it("selects the Ghostty selection in the hidden textarea so native copy has text", () => {
1204 const input = {
1205 value: "",
1206 selectionStart: 0,
1207 selectionEnd: 0,
1208 select() {
1209 this.selectionStart = 0;
1210 this.selectionEnd = this.value.length;
1211 },
1212 };
1213 primeTerminalCopyInput(input, "git status");
1214 expect(input.value).toBe("git status");
1215 expect(input.selectionStart).toBe(0);
1216 expect(input.selectionEnd).toBe(10);
1217 clearPrimedTerminalCopyInput(input, "git status");
1218 expect(input.value).toBe("");
1219 });
1220
1221 it("does not wipe an IME candidate that replaced the primed copy", () => {
1222 const input = { value: "", select() {} };
1223 primeTerminalCopyInput(input, "git status");
1224 input.value = "あ";
1225 clearPrimedTerminalCopyInput(input, "git status");
1226 expect(input.value).toBe("あ");
1227 });
1228});
1229
1230describe("isTerminalPasteShortcut", () => {
1231 const event = (overrides: Partial<Parameters<typeof isTerminalPasteShortcut>[0]> = {}) => ({
1232 ctrlKey: false,
1233 key: "v",
1234 metaKey: false,
1235 shiftKey: false,
1236 ...overrides,
1237 });
1238
1239 it("uses Cmd+V on macOS", () => {
1240 expect(isTerminalPasteShortcut(event({ metaKey: true }), "MacIntel")).toBe(true);
1241 expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false);
1242 });
1243
1244 it("preserves Ctrl+V and uses Ctrl+Shift+V elsewhere", () => {
1245 expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(false);
1246 expect(isTerminalPasteShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe(
1247 true,
1248 );
1249 });
1250
1251 it("supports the conventional Shift+Insert paste shortcut", () => {
1252 expect(isTerminalPasteShortcut(event({ key: "Insert", shiftKey: true }), "Linux x86_64")).toBe(
1253 true,
1254 );
1255 expect(isTerminalPasteShortcut(event({ key: "Insert" }), "Linux x86_64")).toBe(false);
1256 expect(
1257 isTerminalPasteShortcut(
1258 event({ key: "Insert", ctrlKey: true, shiftKey: true }),
1259 "Linux x86_64",
1260 ),
1261 ).toBe(false);
1262 expect(isTerminalPasteShortcut(event({ key: "Insert", shiftKey: true }), "MacIntel")).toBe(
1263 false,
1264 );
1265 });
1266});
1267
1268describe("isTerminalCompositionCommitInput", () => {
1269 it("identifies browser composition follow-up input", () => {
1270 expect(isTerminalCompositionCommitInput({ inputType: "" })).toBe(true);
1271 expect(isTerminalCompositionCommitInput({ inputType: "insertCompositionText" })).toBe(true);
1272 expect(isTerminalCompositionCommitInput({ inputType: "insertFromComposition" })).toBe(true);
1273 });
1274
1275 it("keeps a fast repeated input as legitimate text", () => {
1276 expect(isTerminalCompositionCommitInput({ inputType: "insertText" })).toBe(false);
1277 });
1278});
1279
1280describe("isTerminalCompositionKey", () => {
1281 const event = (
1282 overrides: Partial<Pick<KeyboardEvent, "isComposing" | "key" | "keyCode">> = {},
1283 ) => ({
1284 isComposing: false,
1285 key: "a",
1286 keyCode: 65,
1287 ...overrides,
1288 });
1289
1290 it("treats in-progress IME keydowns as composition so the copy primer cannot wipe them", () => {
1291 expect(isTerminalCompositionKey(event({ isComposing: true }), false)).toBe(true);
1292 expect(isTerminalCompositionKey(event(), true)).toBe(true);
1293 expect(isTerminalCompositionKey(event({ key: "Process" }), false)).toBe(true);
1294 expect(isTerminalCompositionKey(event({ keyCode: 229 }), false)).toBe(true);
1295 });
1296
1297 it("lets ordinary keydowns clear a primed copy", () => {
1298 expect(isTerminalCompositionKey(event(), false)).toBe(false);
1299 });
1300});
1301
1302describe("application mouse reporting", () => {
1303 const event = (overrides: Partial<Parameters<typeof shouldReportTerminalMouse>[1]> = {}) => ({
1304 ctrlKey: false,
1305 metaKey: false,
1306 shiftKey: false,
1307 ...overrides,
1308 });
1309
1310 it("keeps Shift and link activation modifiers available to the browser", () => {
1311 expect(shouldReportTerminalMouse(true, event())).toBe(true);
1312 expect(shouldReportTerminalMouse(true, event({ shiftKey: true }))).toBe(false);
1313 expect(shouldReportTerminalMouse(true, event({ ctrlKey: true }))).toBe(false);
1314 expect(shouldReportTerminalMouse(true, event({ metaKey: true }))).toBe(false);
1315 expect(shouldReportTerminalMouse(false, event())).toBe(false);
1316 });
1317
1318 it("maps browser buttons to Ghostty's button enum", () => {
1319 expect([0, 1, 2, 3, 4, 5].map(ghosttyMouseButton)).toEqual([1, 3, 2, 4, 5, null]);
1320 });
1321
1322 it("drops repeated motion reports until another mouse action resets the cell", () => {
1323 const first = resolveTerminalMouseData("motion", "\u001b[<35;8;4M", "");
1324 expect(first).toEqual({ send: true, nextMotionData: "\u001b[<35;8;4M" });
1325
1326 const duplicate = resolveTerminalMouseData("motion", "\u001b[<35;8;4M", first.nextMotionData);
1327 expect(duplicate).toEqual({ send: false, nextMotionData: "\u001b[<35;8;4M" });
1328
1329 const press = resolveTerminalMouseData("press", "\u001b[<0;8;4M", duplicate.nextMotionData);
1330 expect(press).toEqual({ send: true, nextMotionData: "" });
1331
1332 expect(resolveTerminalMouseData("motion", "\u001b[<35;8;4M", press.nextMotionData)).toEqual({
1333 send: true,
1334 nextMotionData: "\u001b[<35;8;4M",
1335 });
1336 });
1337
1338 it("clears the motion baseline when application mouse tracking changes", () => {
1339 expect(resolveTerminalMouseTrackingState(true, false, "\u001b[<35;8;4M")).toEqual({
1340 tracking: false,
1341 motionData: "",
1342 });
1343 expect(resolveTerminalMouseTrackingState(false, true, "\u001b[<35;8;4M")).toEqual({
1344 tracking: true,
1345 motionData: "",
1346 });
1347 expect(resolveTerminalMouseTrackingState(true, true, "\u001b[<35;8;4M")).toEqual({
1348 tracking: true,
1349 motionData: "\u001b[<35;8;4M",
1350 });
1351 expect(resolveTerminalMouseTrackingState(false, false, "\u001b[<35;8;4M")).toEqual({
1352 tracking: false,
1353 motionData: "\u001b[<35;8;4M",
1354 });
1355 });
1356});
1357
1358describe("terminal font resolution", () => {
1359 it("validates the requested face after its styles load", async () => {
1360 let loaded = false;
1361 const load = vi.fn(async () => {
1362 loaded = true;
1363 return [];
1364 });
1365 const resolve = vi.fn(() => {
1366 expect(loaded).toBe(true);
1367 return DEFAULT_TERMINAL_FONT_FAMILY;
1368 });
1369
1370 await expect(
1371 loadTerminalFontFamily("Proportional Test", 12, {
1372 load,
1373 resolve,
1374 }),
1375 ).resolves.toBe(DEFAULT_TERMINAL_FONT_FAMILY);
1376 expect(load).toHaveBeenCalledTimes(4);
1377 expect(resolve).toHaveBeenCalledWith("Proportional Test");
1378 });
1379
1380 it("keeps the glyph fallbacks behind a custom text face", () => {
1381 expect(terminalFontFamily()).toBe(DEFAULT_TERMINAL_FONT_FAMILY);
1382 expect(terminalFontFamily(" ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY);
1383 const custom = terminalFontFamily('"Fira Code"');
1384 expect(custom.startsWith('"Fira Code", ')).toBe(true);
1385 expect(custom).toContain('"Symbols Nerd Font Mono"');
1386 expect(custom.endsWith("monospace")).toBe(true);
1387 });
1388
1389 it("ignores proportional families the cell grid cannot lay out", () => {
1390 // jsdom has no canvas metrics, so the probe answers "monospace" and the
1391 // family is kept; the guard is exercised in the browser instead. Assert the
1392 // shape stays intact so a rejected face still yields a usable stack.
1393 const stack = terminalFontFamily("Helvetica Neue");
1394 expect(stack.endsWith("monospace")).toBe(true);
1395 });
1396
1397 it("quotes families the canvas font shorthand would otherwise reject", () => {
1398 expect(terminalFontFamily("3270 Nerd Font").startsWith('"3270 Nerd Font", ')).toBe(true);
1399 expect(terminalFontFamily("M+ 1m").startsWith('"M+ 1m", ')).toBe(true);
1400 expect(terminalFontFamily("Cascadia Code, Menlo").startsWith('"Cascadia Code", Menlo, ')).toBe(
1401 true,
1402 );
1403 expect(terminalFontFamily(" , ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY);
1404 });
1405
1406 it("clamps requested font sizes to the supported range", () => {
1407 expect(terminalFontSize()).toBe(DEFAULT_TERMINAL_FONT_SIZE);
1408 expect(terminalFontSize(Number.NaN)).toBe(DEFAULT_TERMINAL_FONT_SIZE);
1409 expect(terminalFontSize(13.4)).toBe(13);
1410 expect(terminalFontSize(2)).toBe(6);
1411 expect(terminalFontSize(90)).toBe(32);
1412 });
1413});
1414
1415describe("terminalContentOriginY", () => {
1416 it("stays top-anchored like a fresh terminal until scrollback exists", () => {
1417 expect(terminalContentOriginY(100, 4, 5, 16, false)).toBe(4);
1418 });
1419
1420 it("pins the grid to the bottom by moving the sub-row slack above row 0", () => {
1421 // 100px mount, 4px padding, 5 rows of 16px: 92 - 80 = 12px slack on top.
1422 expect(terminalContentOriginY(100, 4, 5, 16, true)).toBe(16);
1423 // Exact fit keeps the origin at the padding.
1424 expect(terminalContentOriginY(88, 4, 5, 16, true)).toBe(4);
1425 // A mount smaller than the grid never pushes the origin above the padding.
1426 expect(terminalContentOriginY(80, 4, 5, 16, true)).toBe(4);
1427 });
1428
1429 it("keeps the prompt stationary while a drag crosses row boundaries", () => {
1430 // Growing the mount pixel by pixel: the bottom edge (origin + rows*cell)
1431 // tracks the mount bottom exactly until a new row fits.
1432 for (let height = 88; height < 104; height += 1) {
1433 const rows = Math.max(1, Math.floor((height - 8) / 16));
1434 const origin = terminalContentOriginY(height, 4, rows, 16, true);
1435 expect(origin + rows * 16).toBe(height - 4);
1436 }
1437 });
1438});
1439
1440describe("terminalWheelDeltaRows", () => {
1441 it("converts line-mode deltas into whole rows", () => {
1442 const result = terminalWheelDeltaRows({ deltaY: 3, deltaMode: 1 }, 16, 24, 0);
1443 expect(result.rows).toBe(3);
1444 expect(result.remainder).toBe(0);
1445 });
1446
1447 it("accumulates fractional pixel deltas across events", () => {
1448 let remainder = 0;
1449 let scrolled = 0;
1450 for (let index = 0; index < 4; index += 1) {
1451 const result = terminalWheelDeltaRows({ deltaY: 5, deltaMode: 0 }, 16, 24, remainder);
1452 remainder = result.remainder;
1453 scrolled += result.rows;
1454 }
1455 expect(scrolled).toBe(1);
1456 expect(remainder).toBeCloseTo(4 / 16);
1457 });
1458
1459 it("keeps direction for negative page-mode deltas", () => {
1460 const result = terminalWheelDeltaRows({ deltaY: -1, deltaMode: 2 }, 16, 24, 0);
1461 expect(result.rows).toBe(-24);
1462 expect(result.remainder).toBe(0);
1463 });
1464});
1465
1466describe("terminalWheelArrowData", () => {
1467 it("emits one arrow per row honoring application cursor keys", () => {
1468 expect(terminalWheelArrowData(-2, false)).toBe("\u001b[A\u001b[A");
1469 expect(terminalWheelArrowData(3, false)).toBe("\u001b[B\u001b[B\u001b[B");
1470 expect(terminalWheelArrowData(-1, true)).toBe("\u001bOA");
1471 expect(terminalWheelArrowData(0, true)).toBe("");
1472 });
1473});
1474
1475describe("advanceTerminalSelectionClickSequence", () => {
1476 it("recognizes stationary double and triple pointer presses without PointerEvent.detail", () => {
1477 const first = advanceTerminalSelectionClickSequence(null, {
1478 clientX: 20,
1479 clientY: 30,
1480 timeStamp: 1_000,
1481 });
1482 const second = advanceTerminalSelectionClickSequence(first, {
1483 clientX: 22,
1484 clientY: 29,
1485 timeStamp: 1_200,
1486 });
1487 const third = advanceTerminalSelectionClickSequence(second, {
1488 clientX: 21,
1489 clientY: 31,
1490 timeStamp: 1_400,
1491 });
1492
1493 expect([first.count, second.count, third.count]).toEqual([1, 2, 3]);
1494 });
1495
1496 it("starts over after movement, delay, or a completed triple click", () => {
1497 const previous = { count: 3, time: 1_000, x: 20, y: 30 };
1498 expect(
1499 advanceTerminalSelectionClickSequence(previous, {
1500 clientX: 20,
1501 clientY: 30,
1502 timeStamp: 1_100,
1503 }).count,
1504 ).toBe(1);
1505 expect(
1506 advanceTerminalSelectionClickSequence(previous, {
1507 clientX: 30,
1508 clientY: 30,
1509 timeStamp: 1_100,
1510 }).count,
1511 ).toBe(1);
1512 expect(
1513 advanceTerminalSelectionClickSequence(previous, {
1514 clientX: 20,
1515 clientY: 30,
1516 timeStamp: 1_501,
1517 }).count,
1518 ).toBe(1);
1519 });
1520});
1521
1522describe("terminal scrollbar", () => {
1523 it("maps Ghostty's viewport state to a proportional thumb", () => {
1524 expect(terminalScrollbarGeometry({ total: 100, offset: 40, len: 20 }, 200)).toEqual({
1525 thumbHeight: 40,
1526 thumbTop: 80,
1527 maxOffset: 80,
1528 });
1529 expect(terminalScrollbarGeometry({ total: 100, offset: 0, len: 100 }, 200)).toBeNull();
1530 });
1531
1532 it("keeps the thumb usable for large scrollback and maps dragging back to rows", () => {
1533 const state = { total: 10_000, offset: 0, len: 20 };
1534 expect(terminalScrollbarGeometry(state, 200)).toEqual({
1535 thumbHeight: 18,
1536 thumbTop: 0,
1537 maxOffset: 9_980,
1538 });
1539 expect(terminalScrollbarOffsetAtPointer(state, 200, 191, 9)).toBe(9_980);
1540 });
1541});
apps/web/src/terminal/ghostty/surface.ts deleted-2195
......@@ -1,2195 +0,0 @@
1import { isMacPlatform } from "../../lib/utils";
2import { SELECTION_MULTI_CLICK_INTERVAL_MS } from "../../lib/selectionActions";
3import { collectWrappedTerminalLinkLine, extractTerminalLinks } from "../../terminal-links";
4import {
5 GhosttyTerminalCore,
6 type GhosttyScrollbar,
7 type GhosttySnapshot,
8 type GhosttyTheme,
9} from "./core";
10import {
11 measureGhosttyCell,
12 renderGhosttySnapshot,
13 terminalGridSize,
14 type GhosttyCellRange,
15 type GhosttyCellMetrics,
16} from "./renderer";
17import symbolsFontUrl from "./fonts/SymbolsNerdFontMono-Regular.woff2?url";
18import { isMonospaceFamily } from "../../appearanceFonts";
19import { observeResize } from "../../lib/observeResize";
20
21export const DEFAULT_TERMINAL_FONT_SIZE = 12;
22const MIN_TERMINAL_FONT_SIZE = 6;
23const MAX_TERMINAL_FONT_SIZE = 32;
24// The glyph fallbacks only supply symbols the text faces are missing (powerline
25// separators, devicons, and other private-use prompt symbols), so shells
26// configured for a locally installed Nerd Font keep their prompt glyphs no
27// matter which text face is active.
28const TERMINAL_GLYPH_FALLBACKS =
29 '"Symbols Nerd Font Mono", "Symbols Nerd Font", "JetBrainsMono Nerd Font", ' +
30 '"JetBrainsMono NF", "FiraCode Nerd Font", "Hack Nerd Font", "MesloLGS NF", ' +
31 '"CaskaydiaCove Nerd Font", "PowerlineSymbols", monospace';
32// The platform's own monospace faces; concrete names only, because an
33// unknown keyword (like ui-monospace) makes canvas font shorthand parsing
34// reject the whole string.
35export const DEFAULT_TERMINAL_FONT_FAMILY =
36 '"SF Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", ' + TERMINAL_GLYPH_FALLBACKS;
37const CONTENT_PADDING = 4;
38const MIN_SCROLLBAR_THUMB_HEIGHT = 18;
39/** Half a blink cycle: the visible and hidden phases are equally long. */
40const CURSOR_BLINK_INTERVAL_MS = 500;
41const TERMINAL_FONT_LOAD_TEXT = "iMW0@# .";
42const TERMINAL_FONT_LOAD_VARIANTS = [
43 "normal 400",
44 "normal 700",
45 "italic 400",
46 "italic 700",
47] as const;
48
49/** Requested terminal font; omitted fields fall back to the defaults. */
50export interface GhosttyTerminalFont {
51 readonly family?: string;
52 readonly size?: number;
53}
54
55let symbolsFontLoad: Promise<void> | null = null;
56
57/**
58 * Register the bundled symbols-only Nerd Font once per page. It loads lazily
59 * with the first terminal, and because it carries no regular text glyphs it
60 * composes with any text face without changing metrics — prompt symbols and
61 * devicons render even on machines without a locally installed Nerd Font.
62 */
63function ensureTerminalSymbolsFont(): Promise<void> {
64 if (symbolsFontLoad !== null) return symbolsFontLoad;
65 symbolsFontLoad = (async () => {
66 try {
67 const face = new FontFace("Symbols Nerd Font Mono", `url(${symbolsFontUrl})`);
68 document.fonts.add(await face.load());
69 } catch {
70 // Locally installed fallback faces still apply.
71 }
72 })();
73 return symbolsFontLoad;
74}
75
76function quoteTerminalFontFamilies(list: string): string {
77 return list
78 .split(",")
79 .map((name) => {
80 const bare = name.trim();
81 if (bare.length === 0) return "";
82 if (/^(['"]).*\1$/.test(bare)) return bare;
83 if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(bare)) return bare;
84 return `"${bare.replaceAll('"', "")}"`;
85 })
86 .filter((name) => name.length > 0)
87 .join(", ");
88}
89
90function uncheckedTerminalFontFamily(family?: string): string {
91 const custom = family === undefined ? "" : quoteTerminalFontFamilies(family);
92 return custom.length === 0
93 ? DEFAULT_TERMINAL_FONT_FAMILY
94 : `${custom}, ${TERMINAL_GLYPH_FALLBACKS}`;
95}
96
97export function terminalFontFamily(family?: string): string {
98 // Quote non-ident names ("3270 Nerd Font", "M+ 1m"): an unquoted one makes
99 // the whole canvas font string invalid and the assignment silently no-ops.
100 const custom = family === undefined ? "" : quoteTerminalFontFamilies(family);
101 if (custom.length === 0) return DEFAULT_TERMINAL_FONT_FAMILY;
102 // The grid places the cursor and selection on one cell advance, so a
103 // proportional face would draw its text narrower than its own cells. Refuse
104 // it here rather than render a ragged grid with a stranded cursor.
105 if (!isMonospaceFamily(custom)) return DEFAULT_TERMINAL_FONT_FAMILY;
106 // A custom face keeps the glyph fallbacks so prompt symbols stay covered.
107 return uncheckedTerminalFontFamily(custom);
108}
109
110/** Load every style the renderer can request, then validate the actual face. */
111export async function loadTerminalFontFamily(
112 family: string | undefined,
113 size: number,
114 environment?: {
115 readonly load: (font: string, text: string) => Promise<unknown>;
116 readonly resolve: (family: string | undefined) => string;
117 },
118): Promise<string> {
119 const candidate = uncheckedTerminalFontFamily(family);
120 const load =
121 environment?.load ?? ((font: string, text: string) => document.fonts.load(font, text));
122 try {
123 await Promise.all(
124 TERMINAL_FONT_LOAD_VARIANTS.map((variant) =>
125 load(`${variant} ${size}px ${candidate}`, TERMINAL_FONT_LOAD_TEXT),
126 ),
127 );
128 } catch {
129 // The fixed-width fallback stack remains available if a face cannot load.
130 }
131 return (environment?.resolve ?? terminalFontFamily)(family);
132}
133
134export function terminalFontSize(size?: number): number {
135 if (size === undefined || !Number.isFinite(size)) return DEFAULT_TERMINAL_FONT_SIZE;
136 return Math.max(MIN_TERMINAL_FONT_SIZE, Math.min(MAX_TERMINAL_FONT_SIZE, Math.round(size)));
137}
138
139/**
140 * Whether the cursor should keep toggling. An unfocused surface draws a steady
141 * hollow cursor instead of blinking, and a reduced-motion reader gets a steady
142 * cursor too rather than a permanently animating element.
143 */
144export function shouldBlinkTerminalCursor(state: {
145 readonly focused: boolean;
146 readonly cursorBlinking: boolean;
147 readonly cursorVisible: boolean;
148 readonly reducedMotion: boolean;
149}): boolean {
150 return state.focused && state.cursorBlinking && state.cursorVisible && !state.reducedMotion;
151}
152
153/**
154 * Vertical origin of the grid inside the mount. While content is shorter than
155 * the viewport the grid sits at the top like a fresh terminal. Once scrollback
156 * exists the prompt lives on the bottom row, so the grid anchors to the bottom
157 * edge instead: the sub-row remainder moves above row 0 and resizing within a
158 * row boundary keeps the prompt pinned instead of snapping up and down.
159 */
160export function terminalContentOriginY(
161 mountHeight: number,
162 padding: number,
163 rows: number,
164 cellHeight: number,
165 anchorBottom: boolean,
166): number {
167 if (!anchorBottom) return padding;
168 const slack = mountHeight - padding * 2 - rows * cellHeight;
169 return padding + Math.max(0, slack);
170}
171
172export interface TerminalScrollbarGeometry {
173 readonly thumbHeight: number;
174 readonly thumbTop: number;
175 readonly maxOffset: number;
176}
177
178export function terminalScrollbarGeometry(
179 state: GhosttyScrollbar,
180 trackHeight: number,
181): TerminalScrollbarGeometry | null {
182 const total = Math.max(0, state.total);
183 const len = Math.max(0, Math.min(state.len, total));
184 const maxOffset = Math.max(0, total - len);
185 if (trackHeight <= 0 || len <= 0 || maxOffset === 0) return null;
186 const thumbHeight = Math.min(
187 trackHeight,
188 Math.max(MIN_SCROLLBAR_THUMB_HEIGHT, (trackHeight * len) / total),
189 );
190 const travel = Math.max(0, trackHeight - thumbHeight);
191 const offset = Math.max(0, Math.min(state.offset, maxOffset));
192 return {
193 thumbHeight,
194 thumbTop: travel * (offset / maxOffset),
195 maxOffset,
196 };
197}
198
199export function terminalScrollbarOffsetAtPointer(
200 state: GhosttyScrollbar,
201 trackHeight: number,
202 pointerY: number,
203 pointerOffset: number,
204): number {
205 const geometry = terminalScrollbarGeometry(state, trackHeight);
206 if (geometry === null) return 0;
207 const travel = Math.max(0, trackHeight - geometry.thumbHeight);
208 if (travel === 0) return 0;
209 const thumbTop = Math.max(0, Math.min(pointerY - pointerOffset, travel));
210 return Math.round((thumbTop / travel) * geometry.maxOffset);
211}
212
213export function terminalGridCellAt(options: {
214 bounds: { left: number; top: number };
215 clientX: number;
216 clientY: number;
217 cols: number;
218 rows: number;
219 metrics: Pick<GhosttyCellMetrics, "width" | "height">;
220 padding: number;
221 originY: number;
222}): { x: number; y: number } | null {
223 const { bounds, clientX, clientY, cols, rows, metrics, padding, originY } = options;
224 const gridX = clientX - bounds.left - padding;
225 const gridY = clientY - bounds.top - originY;
226 if (gridX < 0 || gridY < 0 || gridX >= cols * metrics.width || gridY >= rows * metrics.height) {
227 return null;
228 }
229 return {
230 x: Math.floor(gridX / metrics.width),
231 y: Math.floor(gridY / metrics.height),
232 };
233}
234
235function terminalRowText(row: GhosttySnapshot["rowData"][number], trimRight: boolean): string {
236 const text = row.cells.map((cell) => cell.text || " ").join("");
237 return trimRight ? text.trimEnd() : text;
238}
239
240function terminalColumnOffset(row: GhosttySnapshot["rowData"][number], column: number): number {
241 let offset = 0;
242 for (let cellIndex = 0; cellIndex < column; cellIndex += 1) {
243 offset += row.cells[cellIndex]?.text.length || 1;
244 }
245 return offset;
246}
247
248export interface TerminalLinkWithRange {
249 readonly text: string;
250 readonly range: GhosttyCellRange;
251}
252
253function isSameTerminalLink(
254 left: TerminalLinkWithRange,
255 right: TerminalLinkWithRange | null,
256): boolean {
257 return (
258 right?.text === left.text &&
259 right.range.start.x === left.range.start.x &&
260 right.range.start.y === left.range.start.y &&
261 right.range.end.x === left.range.end.x &&
262 right.range.end.y === left.range.end.y
263 );
264}
265
266function terminalColumnAtOffset(row: GhosttySnapshot["rowData"][number], offset: number): number {
267 for (let column = 0; column < row.cells.length; column += 1) {
268 const nextOffset = terminalColumnOffset(row, column + 1);
269 if (offset < nextOffset) return column;
270 }
271 return Math.max(0, row.cells.length - 1);
272}
273
274export function terminalLinkAtPositionWithRange(
275 rows: GhosttySnapshot["rowData"],
276 rowIndex: number,
277 column: number,
278): TerminalLinkWithRange | null {
279 const wrappedLine = collectWrappedTerminalLinkLine(rowIndex + 1, (index) => {
280 const row = rows[index];
281 if (!row) return null;
282 return {
283 isWrapped: row.isWrapContinuation,
284 translateToString: (trimRight = false) => terminalRowText(row, trimRight),
285 };
286 });
287 if (!wrappedLine) return null;
288 // Only viewport rows are available: a wrapped line whose head scrolled above
289 // the viewport would resolve a truncated match into a wrong link.
290 const firstSegment = wrappedLine.segments[0];
291 if (firstSegment && rows[firstSegment.bufferLineNumber - 1]?.isWrapContinuation) {
292 return null;
293 }
294 const segment = wrappedLine.segments.find((value) => value.bufferLineNumber === rowIndex + 1);
295 const row = rows[rowIndex];
296 if (!segment || !row) return null;
297 const lastSegment = wrappedLine.segments.at(-1);
298 const lastRow = lastSegment ? rows[lastSegment.bufferLineNumber - 1] : undefined;
299 // Ghostty's soft-wrap flag is authoritative: when the last collected row
300 // still wraps onward, its continuation is outside the viewport.
301 const continuesBelowViewport = lastRow !== undefined && lastRow.wrapsToNext;
302 const offset = segment.startIndex + terminalColumnOffset(row, column);
303 for (const match of extractTerminalLinks(wrappedLine.text)) {
304 if (offset >= match.start && offset < match.end) {
305 // A truncated tail must not activate as a complete link.
306 if (match.end === wrappedLine.text.length && continuesBelowViewport) return null;
307 const startSegment = wrappedLine.segments.find(
308 (value) => match.start >= value.startIndex && match.start < value.endIndex,
309 );
310 const endSegment = wrappedLine.segments.find(
311 (value) => match.end - 1 >= value.startIndex && match.end - 1 < value.endIndex,
312 );
313 const startRow = startSegment ? rows[startSegment.bufferLineNumber - 1] : undefined;
314 const endRow = endSegment ? rows[endSegment.bufferLineNumber - 1] : undefined;
315 if (!startSegment || !endSegment || !startRow || !endRow) return null;
316 return {
317 text: match.text,
318 range: {
319 start: {
320 x: terminalColumnAtOffset(startRow, match.start - startSegment.startIndex),
321 y: startSegment.bufferLineNumber - 1,
322 },
323 end: {
324 x: terminalColumnAtOffset(endRow, match.end - 1 - endSegment.startIndex),
325 y: endSegment.bufferLineNumber - 1,
326 },
327 },
328 };
329 }
330 }
331 return null;
332}
333
334export function isTerminalCopyShortcut(
335 event: Pick<KeyboardEvent, "ctrlKey" | "key" | "metaKey" | "shiftKey">,
336 platform = navigator.platform,
337) {
338 const key = event.key.toLowerCase();
339 if (key === "insert" && !isMacPlatform(platform)) {
340 return event.ctrlKey && !event.shiftKey && !event.metaKey;
341 }
342 if (key !== "c") return false;
343 return isMacPlatform(platform) ? event.metaKey : event.ctrlKey;
344}
345
346/**
347 * Canvas terminals have no DOM selection. Native copy and Electron's Edit
348 * menu `role: "copy"` both read the focused textarea, so an empty IME field
349 * writes blankness to the clipboard. Park the Ghostty selection there first.
350 */
351export function primeTerminalCopyInput(
352 input: Pick<HTMLTextAreaElement, "value" | "select">,
353 selection: string,
354): void {
355 input.value = selection;
356 if (selection.length === 0) return;
357 input.select();
358}
359
360export function clearPrimedTerminalCopyInput(
361 input: Pick<HTMLTextAreaElement, "value">,
362 primedSelection: string,
363): void {
364 // Only blank the copy we parked. The same textarea holds the IME candidate;
365 // wiping whatever is there would cancel CJK composition.
366 if (primedSelection.length === 0 || input.value !== primedSelection) return;
367 input.value = "";
368}
369
370/**
371 * Only a copy event that actually received the selection may cancel the
372 * clipboard.writeText fallback. Claiming without clipboardData (Electron's
373 * menu Copy) used to preventDefault an empty write and skip the fallback,
374 * which is how Cmd+C copied blankness.
375 */
376export function applyTerminalCopyEvent(
377 selection: string,
378 clipboardData: { setData: (type: string, data: string) => void } | null | undefined,
379): { preventDefault: boolean; claimWriteFallback: boolean } {
380 if (selection.length === 0 || !clipboardData) {
381 return { preventDefault: false, claimWriteFallback: false };
382 }
383 clipboardData.setData("text/plain", selection);
384 return { preventDefault: true, claimWriteFallback: true };
385}
386
387export function isTerminalPasteShortcut(
388 event: Pick<KeyboardEvent, "ctrlKey" | "key" | "metaKey" | "shiftKey">,
389 platform = navigator.platform,
390) {
391 const key = event.key.toLowerCase();
392 if (key === "insert" && !isMacPlatform(platform)) {
393 return event.shiftKey && !event.ctrlKey && !event.metaKey;
394 }
395 if (key !== "v") return false;
396 return isMacPlatform(platform) ? event.metaKey : event.ctrlKey && event.shiftKey;
397}
398
399/**
400 * Middle-click paste is an X11/Wayland convention. macOS and Windows have no
401 * primary selection and use the button for autoscroll, so only desktops that
402 * expect the gesture get it.
403 */
404function isMiddleClickPastePlatform(): boolean {
405 return /linux|bsd/i.test(navigator.platform);
406}
407
408export function isTerminalCompositionCommitInput(event: Pick<InputEvent, "inputType">): boolean {
409 return (
410 event.inputType === "" ||
411 event.inputType === "insertCompositionText" ||
412 event.inputType === "insertFromComposition"
413 );
414}
415
416/** IME keydowns must not touch the hidden textarea; it holds the candidate. */
417export function isTerminalCompositionKey(
418 event: Pick<KeyboardEvent, "isComposing" | "key" | "keyCode">,
419 composing: boolean,
420): boolean {
421 return event.isComposing || composing || event.key === "Process" || event.keyCode === 229;
422}
423
424export function isTerminalAltGraphText(
425 event: Pick<KeyboardEvent, "getModifierState" | "key">,
426): boolean {
427 return event.getModifierState("AltGraph") && [...event.key].length === 1;
428}
429
430export function shouldReportTerminalMouse(
431 tracking: boolean,
432 event: Pick<MouseEvent, "ctrlKey" | "metaKey" | "shiftKey">,
433): boolean {
434 return tracking && !event.shiftKey && !event.ctrlKey && !event.metaKey;
435}
436
437type TerminalMouseAction = "press" | "release" | "motion";
438
439export function resolveTerminalMouseData(
440 action: TerminalMouseAction,
441 data: string,
442 previousMotionData: string,
443): { readonly send: boolean; readonly nextMotionData: string } {
444 const nextMotionData = action === "motion" ? data : "";
445 return {
446 send: data.length > 0 && (action !== "motion" || data !== previousMotionData),
447 nextMotionData,
448 };
449}
450
451export function resolveTerminalMouseTrackingState(
452 previousTracking: boolean,
453 tracking: boolean,
454 motionData: string,
455): { readonly tracking: boolean; readonly motionData: string } {
456 return {
457 tracking,
458 motionData: previousTracking === tracking ? motionData : "",
459 };
460}
461
462export function terminalWheelDeltaRows(
463 event: Pick<WheelEvent, "deltaY" | "deltaMode">,
464 cellHeight: number,
465 viewportRows: number,
466 remainder: number,
467): { readonly rows: number; readonly remainder: number } {
468 // deltaMode: 0 pixels, 1 lines, 2 pages.
469 const pixels =
470 event.deltaMode === 1
471 ? event.deltaY * cellHeight
472 : event.deltaMode === 2
473 ? event.deltaY * viewportRows * cellHeight
474 : event.deltaY;
475 const total = remainder + pixels / cellHeight;
476 const rows = Math.trunc(total);
477 return { rows, remainder: total - rows };
478}
479
480export function terminalWheelArrowData(rows: number, applicationCursorKeys: boolean): string {
481 if (rows === 0) return "";
482 const sequence =
483 rows < 0
484 ? applicationCursorKeys
485 ? "\u001bOA"
486 : "\u001b[A"
487 : applicationCursorKeys
488 ? "\u001bOB"
489 : "\u001b[B";
490 return sequence.repeat(Math.abs(rows));
491}
492
493export function ghosttyMouseButton(button: number): number | null {
494 switch (button) {
495 case 0:
496 return 1;
497 case 1:
498 return 3;
499 case 2:
500 return 2;
501 case 3:
502 return 4;
503 case 4:
504 return 5;
505 default:
506 return null;
507 }
508}
509
510export interface TerminalSelectionClickSequence {
511 readonly count: number;
512 readonly time: number;
513 readonly x: number;
514 readonly y: number;
515}
516
517const TERMINAL_LINK_DRAG_THRESHOLD_PX = 4;
518
519export function advanceTerminalSelectionClickSequence(
520 previous: TerminalSelectionClickSequence | null,
521 event: Pick<PointerEvent, "clientX" | "clientY" | "timeStamp">,
522): TerminalSelectionClickSequence {
523 const repeats =
524 previous !== null &&
525 event.timeStamp - previous.time <= SELECTION_MULTI_CLICK_INTERVAL_MS &&
526 Math.hypot(event.clientX - previous.x, event.clientY - previous.y) <=
527 TERMINAL_LINK_DRAG_THRESHOLD_PX;
528 return {
529 count: repeats ? (previous.count >= 3 ? 1 : previous.count + 1) : 1,
530 time: event.timeStamp,
531 x: event.clientX,
532 y: event.clientY,
533 };
534}
535
536export interface GhosttySelectionPosition {
537 readonly start: { readonly x: number; readonly y: number };
538 readonly end: { readonly x: number; readonly y: number };
539}
540
541export interface GhosttyTerminalSurfaceOptions {
542 readonly theme: GhosttyTheme;
543 readonly font?: GhosttyTerminalFont;
544 /** Read after font and WASM loading. Hosts can supply a getter for the latest value. */
545 readonly visible?: boolean;
546 readonly onData: (data: string) => void;
547 readonly onResize: (cols: number, rows: number) => void;
548 readonly onSelectionChange: () => void;
549 readonly beforeKey: (event: KeyboardEvent) => boolean;
550 readonly onLinkActivate: (text: string, event: MouseEvent) => void;
551 readonly canActivateLink?: (text: string) => boolean;
552 /**
553 * A right-click the running application did not claim through mouse
554 * reporting. The host owns the menu, so it also owns preventing the browser
555 * default — whose Paste entry can never reach a canvas terminal.
556 */
557 readonly onContextMenu?: (event: MouseEvent) => void;
558}
559
560export class GhosttyTerminalSurface {
561 readonly canvas: HTMLCanvasElement;
562 readonly input: HTMLTextAreaElement;
563 readonly scrollbar: HTMLDivElement;
564 cols = 1;
565 rows = 1;
566
567 private readonly mount: HTMLElement;
568 private readonly context: CanvasRenderingContext2D;
569 private readonly core: GhosttyTerminalCore;
570 private readonly options: GhosttyTerminalSurfaceOptions;
571 private visible: boolean;
572 private hasSize = false;
573 private metrics: GhosttyCellMetrics;
574 private fontFamily: string;
575 private requestedFontFamily: string | undefined;
576 private fontSize: number;
577 private fontEpoch = 0;
578 private pendingFontEpoch: number | null = null;
579 private readonly stopObservingResize: () => void;
580 private readonly scrollbarThumb: HTMLDivElement;
581 private snapshot: GhosttySnapshot | null = null;
582 private frame = 0;
583 private cursorTimer: number | null = null;
584 private compositionInputToSuppress: string | null = null;
585 private compositionSuppressionTimer: number | null = null;
586 private cursorOn = true;
587 private renderedCursorY: number | null = null;
588 private forceFullRender = true;
589 private scrollbarDirty = true;
590 private scrollbarState: GhosttyScrollbar | null = null;
591 private scrollbarPointerId: number | null = null;
592 private scrollbarPointerOffset = 0;
593 private disposed = false;
594 private resizeNotifyTimer: number | null = null;
595 private originY = CONTENT_PADDING;
596 private mountHeight = 0;
597 private selectionEnd: { x: number; y: number } | null = null;
598 private selectionAnchorScreen: { x: number; y: number } | null = null;
599 private selectionEndScreen: { x: number; y: number } | null = null;
600 private selectionMode: "cell" | "word" | "line" = "cell";
601 // Word/line selection base in screen coordinates so streaming output cannot
602 // shift the origin of a drag selection.
603 private selectionBase: {
604 start: { x: number; y: number };
605 end: { x: number; y: number };
606 } | null = null;
607 private selectionScrollTimer: number | null = null;
608 private selectionScrollDelta = 0;
609 private selectionPointer: { x: number; y: number } | null = null;
610 private mouseReportingPointerId: number | null = null;
611 private mouseReportingButton: number | null = null;
612 private linkActivationPointerId: number | null = null;
613 private linkActivationLink: TerminalLinkWithRange | null = null;
614 private linkActivationOrigin: {
615 x: number;
616 y: number;
617 clickCount: number;
618 } | null = null;
619 private hoveredLink: TerminalLinkWithRange | null = null;
620 private hoverPointer: { x: number; y: number } | null = null;
621 private selectionClickSequence: TerminalSelectionClickSequence | null = null;
622 private selectionMoved = false;
623 private composing = false;
624 private focused = false;
625 private resizeNotified = false;
626 private canvasConfigured = false;
627 private theme: GhosttyTheme;
628 private readonly suppressedKeyCodes = new Set<string>();
629 private pasteShortcutToken = 0;
630 private copyShortcutToken = 0;
631 private clearSelectionAfterCopy = false;
632 private primedCopySelection = "";
633 private wheelRemainder = 0;
634 private touchScroll: {
635 pointerId: number;
636 startX: number;
637 startY: number;
638 lastY: number;
639 remainder: number;
640 moved: boolean;
641 } | null = null;
642 private lastMouseMotionData = "";
643 private mouseAnyEventTracking = false;
644 private dprMedia: MediaQueryList | null = null;
645 // Read live on every blink decision, and watched so that dropping the
646 // preference restarts a blink cycle that has no timer left to notice it.
647 private readonly reducedMotionMedia = window.matchMedia?.("(prefers-reduced-motion: reduce)");
648 private inputLeft = -1;
649 private inputTop = -1;
650
651 private constructor(
652 mount: HTMLElement,
653 canvas: HTMLCanvasElement,
654 input: HTMLTextAreaElement,
655 scrollbar: HTMLDivElement,
656 scrollbarThumb: HTMLDivElement,
657 context: CanvasRenderingContext2D,
658 core: GhosttyTerminalCore,
659 metrics: GhosttyCellMetrics,
660 fontFamily: string,
661 options: GhosttyTerminalSurfaceOptions,
662 ) {
663 this.mount = mount;
664 this.canvas = canvas;
665 this.input = input;
666 this.scrollbar = scrollbar;
667 this.scrollbarThumb = scrollbarThumb;
668 this.context = context;
669 this.core = core;
670 this.mouseAnyEventTracking = core.isMouseAnyEventTracking();
671 this.metrics = metrics;
672 this.options = options;
673 this.visible = options.visible ?? true;
674 this.theme = options.theme;
675 this.fontFamily = fontFamily;
676 this.requestedFontFamily = options.font?.family;
677 this.fontSize = terminalFontSize(options.font?.size);
678 this.installEvents();
679 this.watchDevicePixelRatio();
680 this.reducedMotionMedia?.addEventListener("change", this.onReducedMotionChange);
681 document.fonts.addEventListener("loadingdone", this.onFontsLoaded);
682 this.stopObservingResize = observeResize(mount, () => this.fit());
683 }
684
685 static async create(
686 mount: HTMLElement,
687 options: GhosttyTerminalSurfaceOptions,
688 ): Promise<GhosttyTerminalSurface> {
689 const canvas = document.createElement("canvas");
690 canvas.className = "block size-full cursor-text";
691 canvas.style.touchAction = "pan-x pinch-zoom";
692 canvas.setAttribute("aria-hidden", "true");
693
694 const input = document.createElement("textarea");
695 input.className = "t3-ghostty-input";
696 input.setAttribute("aria-label", "Terminal input");
697 input.autocapitalize = "off";
698 input.autocomplete = "off";
699 input.spellcheck = false;
700 input.style.cssText =
701 "position:absolute;left:4px;top:4px;width:1px;height:1px;opacity:0;padding:0;border:0;resize:none;pointer-events:none;";
702
703 const scrollbar = document.createElement("div");
704 scrollbar.className =
705 "group absolute top-1 right-px bottom-1 z-1 w-[var(--app-scrollbar-width)] cursor-default touch-none";
706 scrollbar.setAttribute("role", "scrollbar");
707 scrollbar.setAttribute("aria-label", "Terminal scrollback");
708 scrollbar.setAttribute("aria-orientation", "vertical");
709 scrollbar.tabIndex = 0;
710 scrollbar.hidden = true;
711 const scrollbarThumb = document.createElement("div");
712 scrollbarThumb.className =
713 "absolute inset-x-px top-0 rounded-[3px] bg-[var(--app-scrollbar-thumb)] transition-[background-color] duration-[120ms] ease-[ease-out] group-hover:bg-[var(--app-scrollbar-thumb-hover)] group-focus-visible:bg-[var(--app-scrollbar-thumb-hover)]";
714 scrollbar.append(scrollbarThumb);
715 mount.replaceChildren(canvas, input, scrollbar);
716
717 const context = canvas.getContext("2d", { alpha: false });
718 if (!context) throw new Error("Canvas 2D is unavailable");
719 // An opaque canvas backing store initializes to solid black, and the font
720 // and WASM loads below leave it on screen for the whole setup window; paint
721 // the theme background first so the mount never flashes a black box.
722 context.fillStyle = `rgb(${options.theme.background.r}, ${options.theme.background.g}, ${options.theme.background.b})`;
723 context.fillRect(0, 0, canvas.width, canvas.height);
724 const fontSize = terminalFontSize(options.font?.size);
725 try {
726 // Cell metrics must come from the faces that will render; measuring before
727 // the bundled webfonts load would size the grid from a fallback font.
728 await ensureTerminalSymbolsFont();
729 } catch {
730 // Metrics fall back to whichever faces are already available.
731 }
732 const fontFamily = await loadTerminalFontFamily(options.font?.family, fontSize);
733 const metrics = measureGhosttyCell(context, fontSize, fontFamily);
734 const grid = terminalGridSize(mount.clientWidth, mount.clientHeight, metrics, CONTENT_PADDING);
735 const core = await GhosttyTerminalCore.create(
736 grid.cols,
737 grid.rows,
738 metrics.width,
739 metrics.height,
740 options.theme,
741 options.onData,
742 );
743 const surface = new GhosttyTerminalSurface(
744 mount,
745 canvas,
746 input,
747 scrollbar,
748 scrollbarThumb,
749 context,
750 core,
751 metrics,
752 fontFamily,
753 options,
754 );
755 surface.fit();
756 return surface;
757 }
758
759 /** Pause canvas work without interrupting output parsing or terminal replies. */
760 setVisible(visible: boolean): void {
761 if (this.disposed || this.visible === visible) return;
762 this.visible = visible;
763 this.cursorOn = true;
764 this.forceFullRender = true;
765 this.scrollbarDirty = true;
766 if (!visible) {
767 this.cancelRender();
768 this.setSelectionAutoscroll(0);
769 return;
770 }
771 this.fit();
772 }
773
774 write(data: string): void {
775 if (this.disposed) return;
776 this.core.write(data);
777 this.synchronizeMouseTrackingState();
778 // Restart the blink cycle from the visible phase so the cursor never sits
779 // invisible through a stream of output or a burst of typing echo.
780 this.cursorOn = true;
781 this.scrollbarDirty = true;
782 this.requestRender();
783 }
784
785 resetAndWrite(data: string): void {
786 if (this.disposed) return;
787 this.lastMouseMotionData = "";
788 this.core.resetAndWrite(data);
789 this.synchronizeMouseTrackingState();
790 // A replayed session starts from the visible phase like any other write:
791 // reattaching mid-blink must not open on an invisible cursor.
792 this.cursorOn = true;
793 this.forceFullRender = true;
794 this.scrollbarDirty = true;
795 this.requestRender();
796 }
797
798 setTheme(theme: GhosttyTheme): void {
799 if (this.disposed) return;
800 this.theme = theme;
801 this.core.setTheme(theme);
802 this.forceFullRender = true;
803 this.requestRender();
804 }
805
806 /** Re-evaluate link feedback after the host's available actions change. */
807 refreshLinkActivation(): void {
808 if (this.disposed) return;
809 this.refreshHoveredLink();
810 }
811
812 async setFont(font: GhosttyTerminalFont): Promise<void> {
813 if (this.disposed) return;
814 const fontSize = terminalFontSize(font.size);
815 // The fields only change together with their metrics after the load, and
816 // the epoch lets the newest overlapping call win regardless of load order.
817 const epoch = ++this.fontEpoch;
818 this.pendingFontEpoch = epoch;
819 const fontFamily = await loadTerminalFontFamily(font.family, fontSize);
820 if (this.disposed || epoch !== this.fontEpoch) return;
821 this.pendingFontEpoch = null;
822 this.fontFamily = fontFamily;
823 this.requestedFontFamily = font.family;
824 this.fontSize = fontSize;
825 this.applyFontMetrics();
826 }
827
828 private applyFontMetrics(): void {
829 this.metrics = measureGhosttyCell(this.context, this.fontSize, this.fontFamily);
830 this.core.resize(this.cols, this.rows, this.metrics.width, this.metrics.height);
831 // Cached IME textarea coordinates are stale in the new cell geometry.
832 this.inputLeft = -1;
833 this.inputTop = -1;
834 this.forceFullRender = true;
835 this.scrollbarDirty = true;
836 this.fit();
837 this.requestRender();
838 }
839
840 private readonly onReducedMotionChange = () => {
841 if (this.disposed) return;
842 // Nothing else wakes an idle steady cursor: the blink timer only reschedules
843 // from a render, and reduced motion is exactly the state that stopped it.
844 this.cursorOn = true;
845 this.requestRender();
846 };
847
848 private readonly onFontsLoaded = () => {
849 if (this.disposed) return;
850 // The explicit load validates every style and applies the newest request.
851 // Its own loading events must not revalidate the previously applied face.
852 if (this.pendingFontEpoch !== null) return;
853 // A face may become available after an earlier fallback measurement. Run
854 // the fixed-width guard again before using its newly loaded metrics.
855 const fontFamily = terminalFontFamily(this.requestedFontFamily);
856 if (fontFamily !== this.fontFamily) {
857 this.fontFamily = fontFamily;
858 this.applyFontMetrics();
859 return;
860 }
861 // A face that finished loading after the initial measurement changes glyph
862 // advances; re-measure and refit so the grid matches what actually renders.
863 const metrics = measureGhosttyCell(this.context, this.fontSize, this.fontFamily);
864 if (
865 metrics.width === this.metrics.width &&
866 metrics.height === this.metrics.height &&
867 metrics.baseline === this.metrics.baseline
868 ) {
869 return;
870 }
871 this.applyFontMetrics();
872 };
873
874 /** Replay the measured grid after the host becomes ready to resize its PTY. */
875 resendSize(): void {
876 this.resizeNotified = false;
877 this.fit();
878 }
879
880 fit(): boolean {
881 if (this.disposed || !this.visible) return false;
882 const width = this.mount.clientWidth;
883 const height = this.mount.clientHeight;
884 if (width <= 0 || height <= 0) {
885 this.hasSize = false;
886 this.forceFullRender = true;
887 this.cancelRender();
888 return false;
889 }
890 this.hasSize = true;
891 const ratio = window.devicePixelRatio || 1;
892 const pixelWidth = Math.max(1, Math.round(width * ratio));
893 const pixelHeight = Math.max(1, Math.round(height * ratio));
894 let shouldRender = false;
895 // The DPR transform must be installed even when the target size happens to
896 // equal the canvas default 300x150 backing store, so the first fit always
897 // schedules a canvas configuration.
898 if (
899 this.canvas.width !== pixelWidth ||
900 this.canvas.height !== pixelHeight ||
901 !this.canvasConfigured
902 ) {
903 this.canvas.width = pixelWidth;
904 this.canvas.height = pixelHeight;
905 this.context.setTransform(ratio, 0, 0, ratio, 0, 0);
906 this.canvasConfigured = true;
907 this.forceFullRender = true;
908 this.scrollbarDirty = true;
909 shouldRender = true;
910 }
911 const grid = terminalGridSize(width, height, this.metrics, CONTENT_PADDING);
912 this.mountHeight = height;
913 // onResize is the only PTY resize channel, so the first successful fit must
914 // notify even when the measured grid equals the 1x1 construction sentinel.
915 if (grid.cols !== this.cols || grid.rows !== this.rows || !this.resizeNotified) {
916 this.cols = grid.cols;
917 this.rows = grid.rows;
918 this.core.resize(grid.cols, grid.rows, this.metrics.width, this.metrics.height);
919 this.notifyResize();
920 this.forceFullRender = true;
921 this.scrollbarDirty = true;
922 shouldRender = true;
923 }
924 // Rendering synchronously keeps the repaint inside the same frame as the
925 // layout change: ResizeObserver fires before paint, so the browser never
926 // composites the old backing store stretched into the new element box.
927 if (shouldRender || this.forceFullRender) this.renderFrame();
928 return true;
929 }
930
931 /**
932 * The local grid reflows immediately, but the PTY only hears about settled
933 * dimensions: notifying on every drag step makes the shell reprint its
934 * prompt mid-drag, which reads as jitter.
935 */
936 private notifyResize(): void {
937 this.resizeNotified = true;
938 if (this.resizeNotifyTimer !== null) window.clearTimeout(this.resizeNotifyTimer);
939 this.resizeNotifyTimer = window.setTimeout(() => {
940 this.resizeNotifyTimer = null;
941 if (!this.disposed) this.options.onResize(this.cols, this.rows);
942 }, 150);
943 }
944
945 /**
946 * Releasing a panel, drawer, or split divider settles the size, so the PTY
947 * hears it now instead of after the debounce. Otherwise typing right after
948 * the release reaches a shell that still lays out the old width.
949 */
950 private readonly onWindowPointerRelease = () => {
951 if (this.disposed || this.resizeNotifyTimer === null) return;
952 // The drag hook writes the release position before this bubbles to window,
953 // so fit now rather than wait a frame for the observer.
954 this.fit();
955 if (this.resizeNotifyTimer === null) return;
956 window.clearTimeout(this.resizeNotifyTimer);
957 this.resizeNotifyTimer = null;
958 this.options.onResize(this.cols, this.rows);
959 };
960
961 focus(): void {
962 if (this.disposed || !this.visible) return;
963 this.input.focus({ preventScroll: true });
964 }
965
966 /**
967 * Pastes clipboard text read by the host (context menu) with the same
968 * bracketed-paste encoding as a native paste event. The read joins the same
969 * race the paste shortcut uses — the token is claimed before it starts — so
970 * a shortcut or native paste arriving during the read supersedes this one
971 * instead of both reaching the shell.
972 */
973 async pasteFromClipboard(
974 readText: () => Promise<string>,
975 isCurrent: () => boolean = () => true,
976 ): Promise<void> {
977 const token = ++this.pasteShortcutToken;
978 const text = await readText();
979 if (this.disposed || this.pasteShortcutToken !== token || !isCurrent()) return;
980 // As in every paste path, delivering bumps the token so a clipboard read
981 // still in flight cannot land after this text reaches the shell.
982 this.pasteShortcutToken += 1;
983 if (text.length === 0) return;
984 const encoded = this.core.encodePaste(text);
985 if (encoded.length > 0) this.options.onData(encoded);
986 }
987
988 /**
989 * Middle-click pastes the terminal's own selection, which is the only
990 * primary-selection-like buffer a browser can read. It goes through
991 * pasteFromClipboard so it joins the same paste race as every other path.
992 * With nothing selected here there is no buffer to paste, and CLIPBOARD is
993 * deliberately not substituted: middle-click must never emit text the user
994 * only ever copied.
995 */
996 private pasteTerminalSelection(): void {
997 const selection = this.getSelection();
998 if (selection.length === 0) return;
999 void this.pasteFromClipboard(() => Promise.resolve(selection));
1000 }
1001
1002 hasSelection(): boolean {
1003 return this.core.selectionText().length > 0;
1004 }
1005
1006 getSelection(): string {
1007 return this.core.selectionText();
1008 }
1009
1010 /** Select the active screen's entire history, including rows outside the viewport. */
1011 selectAll(): void {
1012 this.clearPrimedCopy();
1013 const range = this.core.selectAll();
1014 this.selectionAnchorScreen = range?.start ?? null;
1015 this.selectionEndScreen = range?.end ?? null;
1016 this.selectionEnd = range ? this.core.screenPointToViewport(range.end.x, range.end.y) : null;
1017 this.selectionMode = "cell";
1018 this.selectionBase = null;
1019 this.setSelectionAutoscroll(0);
1020 this.options.onSelectionChange();
1021 this.forceFullRender = true;
1022 this.requestRender();
1023 }
1024
1025 getSelectionPosition(): GhosttySelectionPosition | null {
1026 if (!this.selectionAnchorScreen || !this.selectionEndScreen || !this.hasSelection())
1027 return null;
1028 const before =
1029 this.selectionAnchorScreen.y < this.selectionEndScreen.y ||
1030 (this.selectionAnchorScreen.y === this.selectionEndScreen.y &&
1031 this.selectionAnchorScreen.x <= this.selectionEndScreen.x);
1032 return before
1033 ? { start: this.selectionAnchorScreen, end: this.selectionEndScreen }
1034 : { start: this.selectionEndScreen, end: this.selectionAnchorScreen };
1035 }
1036
1037 getSelectionEndClientRect(): { readonly right: number; readonly bottom: number } | null {
1038 const position = this.getSelectionPosition();
1039 if (!position) return null;
1040 const viewportEnd = this.core.screenPointToViewport(position.end.x, position.end.y);
1041 if (
1042 !viewportEnd ||
1043 viewportEnd.x < 0 ||
1044 viewportEnd.x >= this.cols ||
1045 viewportEnd.y < 0 ||
1046 viewportEnd.y >= this.rows
1047 ) {
1048 return null;
1049 }
1050 const bounds = this.canvas.getBoundingClientRect();
1051 return {
1052 right: bounds.left + CONTENT_PADDING + (viewportEnd.x + 1) * this.metrics.width,
1053 bottom: bounds.top + this.originY + (viewportEnd.y + 1) * this.metrics.height,
1054 };
1055 }
1056
1057 clearSelection(): void {
1058 this.clearPrimedCopy();
1059 this.core.clearSelection();
1060 this.selectionEnd = null;
1061 this.selectionAnchorScreen = null;
1062 this.selectionEndScreen = null;
1063 this.selectionMode = "cell";
1064 this.selectionBase = null;
1065 this.setSelectionAutoscroll(0);
1066 this.options.onSelectionChange();
1067 // Selection highlights span rows Ghostty may not mark dirty for this change.
1068 this.forceFullRender = true;
1069 this.requestRender();
1070 }
1071
1072 scrollToBottom(): void {
1073 this.core.scrollToBottom();
1074 this.forceFullRender = true;
1075 this.scrollbarDirty = true;
1076 this.requestRender();
1077 }
1078
1079 isAtBottom(): boolean {
1080 return this.core.isViewportActive();
1081 }
1082
1083 dispose(): void {
1084 if (this.disposed) return;
1085 this.disposed = true;
1086 this.stopObservingResize();
1087 document.fonts.removeEventListener("loadingdone", this.onFontsLoaded);
1088 this.dprMedia?.removeEventListener("change", this.onDevicePixelRatioChange);
1089 this.dprMedia = null;
1090 this.reducedMotionMedia?.removeEventListener("change", this.onReducedMotionChange);
1091 if (this.selectionScrollTimer !== null) window.clearInterval(this.selectionScrollTimer);
1092 if (this.resizeNotifyTimer !== null) {
1093 window.clearTimeout(this.resizeNotifyTimer);
1094 this.resizeNotifyTimer = null;
1095 // Flush the settled dimensions so the PTY keeps the final size even when
1096 // the surface unmounts inside the debounce window.
1097 this.options.onResize(this.cols, this.rows);
1098 }
1099 this.cancelRender();
1100 if (this.compositionSuppressionTimer !== null) {
1101 window.clearTimeout(this.compositionSuppressionTimer);
1102 }
1103 this.removeEvents();
1104 this.core.dispose();
1105 if (
1106 this.canvas.parentElement === this.mount ||
1107 this.input.parentElement === this.mount ||
1108 this.scrollbar.parentElement === this.mount
1109 ) {
1110 this.canvas.remove();
1111 this.input.remove();
1112 this.scrollbar.remove();
1113 }
1114 }
1115
1116 private readonly onKeyDown = (event: KeyboardEvent) => {
1117 // Presses handled outside the terminal must also swallow their release:
1118 // beforeKey runs side effects (keybindings, navigation sends), so it cannot
1119 // be consulted again on keyup, and Kitty report-event-types sessions would
1120 // otherwise receive a release for a press the shell never saw.
1121 if (isTerminalAltGraphText(event) || !this.options.beforeKey(event)) {
1122 this.suppressedKeyCodes.add(event.code);
1123 return;
1124 }
1125 // IME candidates belong to the textarea, even if the key also resembles
1126 // a local shortcut. Safari signals the initial composition with code 229.
1127 if (isTerminalCompositionKey(event, this.composing)) {
1128 this.suppressedKeyCodes.add(event.code);
1129 return;
1130 }
1131 const mac = isMacPlatform(navigator.platform);
1132 const primaryModifier = mac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
1133 const selectAllShortcut =
1134 event.key.toLowerCase() === "a" &&
1135 primaryModifier &&
1136 !event.altKey &&
1137 (mac ? !event.shiftKey : event.shiftKey);
1138 if (selectAllShortcut) {
1139 event.preventDefault();
1140 event.stopPropagation();
1141 this.suppressedKeyCodes.add(event.code);
1142 this.selectAll();
1143 return;
1144 }
1145 // Shift+PageUp/Down pages history; Ctrl+Shift+Home/End (Cmd on macOS)
1146 // jumps to its edges. Full-screen applications retain these keys.
1147 const pageHistory =
1148 event.shiftKey &&
1149 !event.altKey &&
1150 !event.ctrlKey &&
1151 !event.metaKey &&
1152 (event.key === "PageUp" || event.key === "PageDown");
1153 const jumpHistory =
1154 event.shiftKey &&
1155 !event.altKey &&
1156 primaryModifier &&
1157 (event.key === "Home" || event.key === "End");
1158 if ((pageHistory || jumpHistory) && !this.core.isAlternateScreen()) {
1159 const state = this.readScrollbarState();
1160 if (state !== null) {
1161 event.preventDefault();
1162 event.stopPropagation();
1163 this.suppressedKeyCodes.add(event.code);
1164 const delta =
1165 event.key === "Home"
1166 ? -state.offset
1167 : event.key === "End"
1168 ? state.total - state.len - state.offset
1169 : Math.max(1, state.len) * (event.key === "PageUp" ? -1 : 1);
1170 this.scrollViewport(delta);
1171 return;
1172 }
1173 }
1174 if (isTerminalCopyShortcut(event) && this.hasSelection()) {
1175 // A plain Ctrl+C/Cmd+C fires the browser's native copy event, caught in
1176 // onCopyEvent; not preventing the default keeps that path alive. WebKit
1177 // omits the keyboard copy event without a DOM selection, so race the
1178 // clipboard write against it the same way paste races its read. Ctrl+Shift+C
1179 // and Ctrl+Insert have no native copy event (Chrome binds the former to
1180 // inspect), so synthesize one with execCommand("copy").
1181 const selection = this.getSelection();
1182 this.primeCopy(selection);
1183 if (event.shiftKey || event.key.toLowerCase() === "insert") {
1184 event.preventDefault();
1185 document.execCommand("copy");
1186 } else {
1187 // A plain Ctrl+C is also SIGINT on non-mac: clear the selection once
1188 // it copies so the next Ctrl+C reaches the shell. The Shift chord and
1189 // Cmd+C are copy-only, so they keep the selection; resetting the flag
1190 // up front also drops any clear owed by an earlier gesture that never
1191 // completed.
1192 this.clearSelectionAfterCopy = !event.shiftKey && !isMacPlatform(navigator.platform);
1193 const clipboard = navigator.clipboard;
1194 if (typeof clipboard?.writeText === "function") {
1195 // Defer the write past the default action: the native copy event
1196 // (dispatched synchronously with the default action) claims the
1197 // token first when it actually writes, and the write covers browsers
1198 // whose shortcut produces no copy event. The primed textarea is what
1199 // Electron's edit-menu Copy reads if it runs after this handler.
1200 const token = ++this.copyShortcutToken;
1201 void Promise.resolve().then(() => {
1202 if (this.disposed || this.copyShortcutToken !== token) return;
1203 void clipboard.writeText(selection).then(
1204 () => {
1205 // The write may have been superseded while in flight; only
1206 // touch the selection if this gesture still owns the token.
1207 if (this.disposed || this.copyShortcutToken !== token) return;
1208 if (this.clearSelectionAfterCopy) {
1209 this.clearSelectionAfterCopy = false;
1210 this.clearSelection();
1211 }
1212 },
1213 () => {
1214 // The write failed and the native event has already had its
1215 // chance, so nothing copied and no clear is owed by this
1216 // gesture; a newer one may have just set the flag, so only
1217 // drop it if this gesture still owns the token.
1218 if (this.copyShortcutToken === token) {
1219 this.clearSelectionAfterCopy = false;
1220 }
1221 },
1222 );
1223 });
1224 }
1225 }
1226 this.suppressedKeyCodes.add(event.code);
1227 return;
1228 }
1229 if (isTerminalPasteShortcut(event)) {
1230 this.suppressedKeyCodes.add(event.code);
1231 const clipboard = navigator.clipboard;
1232 if (typeof clipboard?.readText === "function") {
1233 // Race the async clipboard read against the browser's own paste event:
1234 // the native event (dispatched synchronously with the default action)
1235 // always claims the token first when it fires, and the read covers
1236 // browsers whose paste shortcut produces no paste event. Not preventing
1237 // the default keeps the native path alive when the read is denied.
1238 const token = ++this.pasteShortcutToken;
1239 void clipboard.readText().then(
1240 (text) => {
1241 if (this.disposed || this.pasteShortcutToken !== token) return;
1242 this.pasteShortcutToken += 1;
1243 if (text.length > 0) this.options.onData(this.core.encodePaste(text));
1244 },
1245 () => {
1246 // Clipboard read denied; the native paste event remains the path.
1247 },
1248 );
1249 }
1250 return;
1251 }
1252 this.clearPrimedCopy();
1253 const data = this.core.encodeKey(event);
1254 if (data.length === 0) return;
1255 this.suppressedKeyCodes.delete(event.code);
1256 event.preventDefault();
1257 event.stopPropagation();
1258 this.options.onData(data);
1259 };
1260
1261 private readonly onKeyUp = (event: KeyboardEvent) => {
1262 if (this.suppressedKeyCodes.delete(event.code)) return;
1263 if (isTerminalCompositionKey(event, this.composing)) {
1264 return;
1265 }
1266 // Ghostty's encoder only emits release codes when the terminal enabled the
1267 // Kitty report-event-types flag, so legacy sessions send nothing here.
1268 const data = this.core.encodeKey(event, "release");
1269 if (data.length === 0) return;
1270 event.preventDefault();
1271 event.stopPropagation();
1272 this.options.onData(data);
1273 };
1274
1275 private readonly onFocus = () => {
1276 this.focused = true;
1277 this.cursorOn = true;
1278 this.requestRender();
1279 };
1280
1281 private readonly onBlur = () => {
1282 this.focused = false;
1283 this.refreshHoveredLink();
1284 // Suppressions survive blur deliberately: a shortcut that moves focus (for
1285 // example terminal-toggle) must still swallow its own keyup if focus comes
1286 // back before release. Stale entries are harmless — an encoding keydown
1287 // always removes its code first.
1288 // The steady unfocused hollow cursor must not inherit an off blink phase.
1289 this.cursorOn = true;
1290 this.requestRender();
1291 };
1292
1293 private readonly onDevicePixelRatioChange = () => {
1294 this.watchDevicePixelRatio();
1295 this.fit();
1296 };
1297
1298 private watchDevicePixelRatio(): void {
1299 this.dprMedia?.removeEventListener("change", this.onDevicePixelRatioChange);
1300 // A resolution media query only fires once for the ratio it was created at,
1301 // so re-arm it after every change (monitor moves, browser zoom).
1302 this.dprMedia = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
1303 this.dprMedia.addEventListener("change", this.onDevicePixelRatioChange);
1304 }
1305
1306 private primeCopy(selection: string): void {
1307 this.primedCopySelection = selection;
1308 primeTerminalCopyInput(this.input, selection);
1309 }
1310
1311 private clearPrimedCopy(): void {
1312 clearPrimedTerminalCopyInput(this.input, this.primedCopySelection);
1313 this.primedCopySelection = "";
1314 }
1315
1316 private readonly onCopyEvent = (event: ClipboardEvent) => {
1317 const selection = this.hasSelection() ? this.getSelection() : this.input.value;
1318 // Menu-role Copy never hits the keydown primer. The native action reads
1319 // this.input, so park the current selection first — including when
1320 // clipboardData is missing and we must not preventDefault.
1321 this.primeCopy(selection);
1322 const result = applyTerminalCopyEvent(selection, event.clipboardData);
1323 if (result.preventDefault) event.preventDefault();
1324 if (result.claimWriteFallback) {
1325 // The native event actually wrote the selection; drop the in-flight
1326 // writeText so a late resolution cannot clobber a later user copy.
1327 this.copyShortcutToken += 1;
1328 if (this.clearSelectionAfterCopy) {
1329 this.clearSelectionAfterCopy = false;
1330 this.clearSelection();
1331 }
1332 }
1333 };
1334
1335 private readonly onPaste = (event: ClipboardEvent) => {
1336 // Always suppress the browser's default insertion: content the textarea
1337 // would receive (for example an html-only clipboard converted to text)
1338 // leaks through onInput without bracketed-paste encoding.
1339 event.preventDefault();
1340 const data = event.clipboardData?.getData("text/plain") ?? "";
1341 if (data.length === 0) return;
1342 // The native paste won the race with actual text; a pending clipboard read
1343 // must not double. An empty native paste leaves the read as the only path.
1344 this.pasteShortcutToken += 1;
1345 this.options.onData(this.core.encodePaste(data));
1346 };
1347
1348 private readonly onCompositionStart = () => {
1349 this.clearPrimedCopy();
1350 this.clearCompositionInputSuppression();
1351 this.composing = true;
1352 };
1353
1354 private readonly onCompositionEnd = (event: CompositionEvent) => {
1355 this.composing = false;
1356 const data = this.input.value || event.data;
1357 if (data.length > 0) this.options.onData(data);
1358 this.input.value = "";
1359 this.compositionInputToSuppress = data;
1360 this.compositionSuppressionTimer = window.setTimeout(() => {
1361 this.compositionInputToSuppress = null;
1362 this.compositionSuppressionTimer = null;
1363 }, 100);
1364 };
1365
1366 private readonly onInput = (event: Event) => {
1367 const inputEvent = event as InputEvent;
1368 if (this.composing || inputEvent.isComposing) return;
1369 const data = this.input.value || inputEvent.data || "";
1370 if (data === this.compositionInputToSuppress && isTerminalCompositionCommitInput(inputEvent)) {
1371 this.clearCompositionInputSuppression();
1372 this.input.value = "";
1373 return;
1374 }
1375 this.clearCompositionInputSuppression();
1376 if (data.length > 0) this.options.onData(data);
1377 this.input.value = "";
1378 };
1379
1380 private clearCompositionInputSuppression(): void {
1381 if (this.compositionSuppressionTimer !== null) {
1382 window.clearTimeout(this.compositionSuppressionTimer);
1383 this.compositionSuppressionTimer = null;
1384 }
1385 this.compositionInputToSuppress = null;
1386 }
1387
1388 private readonly onPointerDown = (event: PointerEvent) => {
1389 if (event.pointerType === "touch") {
1390 event.preventDefault();
1391 if (!event.isPrimary || this.touchScroll !== null) return;
1392 this.touchScroll = {
1393 pointerId: event.pointerId,
1394 startX: event.clientX,
1395 startY: event.clientY,
1396 lastY: event.clientY,
1397 remainder: 0,
1398 moved: false,
1399 };
1400 this.clearHoveredLink();
1401 this.canvas.setPointerCapture(event.pointerId);
1402 return;
1403 }
1404 this.focus();
1405 if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) {
1406 const button = ghosttyMouseButton(event.button);
1407 if (button === null) return;
1408 event.preventDefault();
1409 event.stopPropagation();
1410 this.clearHoveredLink("default");
1411 this.mouseReportingPointerId = event.pointerId;
1412 this.mouseReportingButton = button;
1413 this.sendMouse("press", button, event);
1414 this.canvas.setPointerCapture(event.pointerId);
1415 return;
1416 }
1417 if (event.button === 1 && isMiddleClickPastePlatform()) {
1418 // Left uncancelled on purpose: cancelling pointerdown drops the
1419 // compatibility mousedown, which is what activates a split pane.
1420 this.pasteTerminalSelection();
1421 return;
1422 }
1423 if (event.button !== 0) return;
1424 const clickCount = this.recordSelectionClick(event);
1425 const link = this.linkAt(event.clientX, event.clientY);
1426 if (link && !event.shiftKey && clickCount === 1) {
1427 event.preventDefault();
1428 event.stopPropagation();
1429 this.linkActivationPointerId = event.pointerId;
1430 this.linkActivationLink = link;
1431 this.linkActivationOrigin = {
1432 x: event.clientX,
1433 y: event.clientY,
1434 clickCount,
1435 };
1436 this.canvas.setPointerCapture(event.pointerId);
1437 return;
1438 }
1439 this.clearHoveredLink();
1440 this.beginSelection(event, clickCount);
1441 this.canvas.setPointerCapture(event.pointerId);
1442 };
1443
1444 private recordSelectionClick(
1445 event: Pick<PointerEvent, "clientX" | "clientY" | "timeStamp">,
1446 ): number {
1447 this.selectionClickSequence = advanceTerminalSelectionClickSequence(
1448 this.selectionClickSequence,
1449 event,
1450 );
1451 return this.selectionClickSequence.count;
1452 }
1453
1454 private beginSelection(event: { clientX: number; clientY: number }, clickCount: number): void {
1455 const cell = this.cellAt(event.clientX, event.clientY);
1456 this.selectionMoved = false;
1457 this.selectionMode = clickCount >= 3 ? "line" : clickCount === 2 ? "word" : "cell";
1458 const range =
1459 this.selectionMode === "line"
1460 ? this.core.selectLine(cell.x, cell.y)
1461 : this.selectionMode === "word"
1462 ? this.core.selectWord(cell.x, cell.y)
1463 : null;
1464 if (range) {
1465 this.selectionBase = range.screen;
1466 this.selectionEnd = range.viewport.end;
1467 this.selectionAnchorScreen = range.screen.start;
1468 this.selectionEndScreen = range.screen.end;
1469 this.options.onSelectionChange();
1470 } else {
1471 this.selectionMode = "cell";
1472 this.selectionBase = null;
1473 this.selectionEnd = cell;
1474 const screen = this.core.viewportPointToScreen(cell.x, cell.y);
1475 this.selectionAnchorScreen = screen;
1476 this.selectionEndScreen = screen;
1477 if (screen) {
1478 this.core.setSelection({ ...screen, tag: 2 }, { ...screen, tag: 2 });
1479 } else {
1480 this.core.setSelection(cell, cell);
1481 }
1482 }
1483 this.forceFullRender = true;
1484 this.requestRender();
1485 }
1486
1487 private readonly onPointerMove = (event: PointerEvent) => {
1488 if (event.pointerType === "touch") {
1489 const touch = this.touchScroll;
1490 if (touch?.pointerId !== event.pointerId) return;
1491 event.preventDefault();
1492 touch.moved ||=
1493 Math.hypot(event.clientX - touch.startX, event.clientY - touch.startY) >
1494 TERMINAL_LINK_DRAG_THRESHOLD_PX;
1495 if (!touch.moved) return;
1496 const delta = terminalWheelDeltaRows(
1497 { deltaY: touch.lastY - event.clientY, deltaMode: 0 },
1498 this.metrics.height,
1499 this.rows,
1500 touch.remainder,
1501 );
1502 touch.lastY = event.clientY;
1503 touch.remainder = delta.remainder;
1504 this.scrollRows(delta.rows, event);
1505 return;
1506 }
1507 if (this.linkActivationPointerId === event.pointerId) {
1508 const origin = this.linkActivationOrigin;
1509 if (
1510 origin === null ||
1511 Math.hypot(event.clientX - origin.x, event.clientY - origin.y) <=
1512 TERMINAL_LINK_DRAG_THRESHOLD_PX
1513 ) {
1514 return;
1515 }
1516 this.linkActivationPointerId = null;
1517 this.linkActivationLink = null;
1518 this.linkActivationOrigin = null;
1519 this.clearHoveredLink();
1520 this.beginSelection(
1521 {
1522 clientX: origin.x,
1523 clientY: origin.y,
1524 },
1525 origin.clickCount,
1526 );
1527 }
1528 // Hover motion is only reportable in any-event tracking (DEC 1003); normal and
1529 // button-event tracking never report motion without a captured pressed button.
1530 const anyEventTracking = this.synchronizeMouseTrackingState();
1531 if (
1532 this.mouseReportingPointerId === event.pointerId ||
1533 shouldReportTerminalMouse(anyEventTracking, event)
1534 ) {
1535 event.preventDefault();
1536 this.hoverPointer = { x: event.clientX, y: event.clientY };
1537 // A drag whose press was already sent to the terminal application cannot
1538 // turn into link activation midway through, so link feedback would lie.
1539 this.setHoveredLink(null);
1540 this.canvas.style.cursor = "default";
1541 this.sendMouse("motion", this.buttonFromButtons(event.buttons), event);
1542 return;
1543 }
1544 this.lastMouseMotionData = "";
1545 if (!this.selectionAnchorScreen || !this.canvas.hasPointerCapture(event.pointerId)) {
1546 this.updateHoverCursor(event);
1547 return;
1548 }
1549 this.clearHoveredLink();
1550 this.selectionPointer = { x: event.clientX, y: event.clientY };
1551 const bounds = this.canvas.getBoundingClientRect();
1552 const top = Math.max(bounds.top, 0) + CONTENT_PADDING;
1553 const bottom = Math.min(bounds.bottom, window.innerHeight) - CONTENT_PADDING;
1554 this.setSelectionAutoscroll(event.clientY < top ? -1 : event.clientY > bottom ? 1 : 0);
1555 const cell = this.cellAt(event.clientX, event.clientY);
1556 if (cell.x === this.selectionEnd?.x && cell.y === this.selectionEnd.y) return;
1557 this.extendSelectionTo(event.clientX, event.clientY);
1558 };
1559
1560 private extendSelectionTo(clientX: number, clientY: number): void {
1561 const anchorScreen = this.selectionAnchorScreen;
1562 if (anchorScreen === null) return;
1563 const cell = this.cellAt(clientX, clientY);
1564 this.selectionMoved = true;
1565 this.selectionEnd = cell;
1566 const range =
1567 this.selectionMode === "line"
1568 ? this.core.selectLine(cell.x, cell.y)
1569 : this.selectionMode === "word"
1570 ? this.core.selectWord(cell.x, cell.y)
1571 : null;
1572 const cellScreen = this.core.viewportPointToScreen(cell.x, cell.y);
1573 if (cellScreen === null) return;
1574 const base = this.selectionBase;
1575 const beforeBase =
1576 base !== null &&
1577 (cellScreen.y < base.start.y ||
1578 (cellScreen.y === base.start.y && cellScreen.x < base.start.x));
1579 const anchor = base === null ? anchorScreen : beforeBase ? base.end : base.start;
1580 const end = range === null ? cellScreen : beforeBase ? range.screen.start : range.screen.end;
1581 this.selectionAnchorScreen = anchor;
1582 this.selectionEndScreen = end;
1583 this.core.setSelection({ ...anchor, tag: 2 }, { ...end, tag: 2 });
1584 this.options.onSelectionChange();
1585 this.forceFullRender = true;
1586 this.requestRender();
1587 }
1588
1589 private setSelectionAutoscroll(delta: number): void {
1590 this.selectionScrollDelta = delta;
1591 if (delta === 0) {
1592 if (this.selectionScrollTimer !== null) {
1593 window.clearInterval(this.selectionScrollTimer);
1594 this.selectionScrollTimer = null;
1595 }
1596 return;
1597 }
1598 if (this.selectionScrollTimer !== null) return;
1599 // Dragging past the edge scrolls the viewport and keeps extending the
1600 // selection into the newly revealed rows, like xterm's drag scroller.
1601 this.selectionScrollTimer = window.setInterval(() => {
1602 if (this.disposed || this.selectionScrollDelta === 0) return;
1603 this.scrollViewport(this.selectionScrollDelta);
1604 const pointer = this.selectionPointer;
1605 if (pointer) this.extendSelectionTo(pointer.x, pointer.y);
1606 }, 80);
1607 }
1608
1609 private updateHoverCursor(event: PointerEvent): void {
1610 this.hoverPointer = { x: event.clientX, y: event.clientY };
1611 this.refreshHoveredLink();
1612 }
1613
1614 private readonly onPointerLeave = () => {
1615 this.lastMouseMotionData = "";
1616 this.clearHoveredLink();
1617 };
1618
1619 private clearHoveredLink(cursor = ""): void {
1620 this.hoverPointer = null;
1621 this.setHoveredLink(null);
1622 this.canvas.style.cursor = cursor;
1623 }
1624
1625 private refreshHoveredLink(): void {
1626 const pointer = this.hoverPointer;
1627 const link = pointer ? this.linkAt(pointer.x, pointer.y) : null;
1628 this.setHoveredLink(link);
1629 }
1630
1631 private setHoveredLink(link: TerminalLinkWithRange | null): void {
1632 const previous = this.hoveredLink;
1633 const unchanged =
1634 previous?.text === link?.text &&
1635 previous?.range.start.x === link?.range.start.x &&
1636 previous?.range.start.y === link?.range.start.y &&
1637 previous?.range.end.x === link?.range.end.x &&
1638 previous?.range.end.y === link?.range.end.y;
1639 this.canvas.style.cursor = link ? "pointer" : "";
1640 if (unchanged) return;
1641 this.hoveredLink = link;
1642 this.forceFullRender = true;
1643 this.requestRender();
1644 }
1645
1646 private readonly onPointerUp = (event: PointerEvent) => {
1647 if (event.pointerType === "touch") {
1648 const touch = this.touchScroll;
1649 if (touch?.pointerId !== event.pointerId) return;
1650 this.touchScroll = null;
1651 event.preventDefault();
1652 if (this.canvas.hasPointerCapture(event.pointerId)) {
1653 this.canvas.releasePointerCapture(event.pointerId);
1654 }
1655 if (event.type === "pointercancel" || touch.moved) return;
1656 this.focus();
1657 if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) {
1658 this.sendMouse("press", ghosttyMouseButton(0), event);
1659 this.sendMouse("release", ghosttyMouseButton(0), event);
1660 } else {
1661 const link = this.linkAt(event.clientX, event.clientY);
1662 if (link) this.options.onLinkActivate(link.text, event);
1663 else this.clearSelection();
1664 }
1665 return;
1666 }
1667 this.setSelectionAutoscroll(0);
1668 if (this.linkActivationPointerId === event.pointerId) {
1669 event.preventDefault();
1670 event.stopPropagation();
1671 const link = this.linkActivationLink;
1672 this.linkActivationPointerId = null;
1673 this.linkActivationLink = null;
1674 this.linkActivationOrigin = null;
1675 if (this.canvas.hasPointerCapture(event.pointerId)) {
1676 this.canvas.releasePointerCapture(event.pointerId);
1677 }
1678 if (event.type !== "pointercancel") {
1679 if (link && isSameTerminalLink(link, this.linkAt(event.clientX, event.clientY))) {
1680 this.options.onLinkActivate(link.text, event);
1681 }
1682 }
1683 return;
1684 }
1685 if (this.mouseReportingPointerId === event.pointerId) {
1686 event.preventDefault();
1687 event.stopPropagation();
1688 this.sendMouse("release", this.mouseReportingButton, event);
1689 this.mouseReportingPointerId = null;
1690 this.mouseReportingButton = null;
1691 if (this.canvas.hasPointerCapture(event.pointerId)) {
1692 this.canvas.releasePointerCapture(event.pointerId);
1693 }
1694 if (event.type === "pointercancel") {
1695 this.clearHoveredLink();
1696 } else {
1697 this.hoverPointer = { x: event.clientX, y: event.clientY };
1698 this.refreshHoveredLink();
1699 }
1700 return;
1701 }
1702 if (this.canvas.hasPointerCapture(event.pointerId)) {
1703 this.canvas.releasePointerCapture(event.pointerId);
1704 }
1705 if (event.button === 1 && isMiddleClickPastePlatform()) {
1706 event.preventDefault();
1707 return;
1708 }
1709 if (event.button !== 0) return;
1710 if (!this.selectionMoved && this.selectionMode === "cell") {
1711 this.clearSelection();
1712 }
1713 this.options.onSelectionChange();
1714 };
1715
1716 private readonly onWheel = (event: WheelEvent) => {
1717 if (event.deltaY === 0) return;
1718 event.preventDefault();
1719 const delta = terminalWheelDeltaRows(
1720 event,
1721 this.metrics.height,
1722 this.rows,
1723 this.wheelRemainder,
1724 );
1725 this.wheelRemainder = delta.remainder;
1726 this.scrollRows(delta.rows, event);
1727 };
1728
1729 private scrollRows(rows: number, event: MouseEvent): void {
1730 if (rows === 0) return;
1731 const magnitude = Math.abs(rows);
1732 if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) {
1733 const button = rows < 0 ? 4 : 5;
1734 for (let index = 0; index < magnitude; index += 1) {
1735 this.sendMouse("press", button, event);
1736 }
1737 return;
1738 }
1739 if (this.core.isAlternateScreen()) {
1740 // The alternate screen has no scrollback: translate wheel motion into
1741 // arrow keys so full-screen apps like vim and less scroll, matching xterm.
1742 this.options.onData(terminalWheelArrowData(rows, this.core.isApplicationCursorKeys()));
1743 return;
1744 }
1745 this.scrollViewport(rows);
1746 }
1747
1748 private readonly onMouseDown = (event: MouseEvent) => {
1749 // Cancelling the middle button here stops autoscroll while still letting
1750 // the event bubble to the drawer handler that activates a split pane.
1751 if (event.button === 0 || (event.button === 1 && isMiddleClickPastePlatform())) {
1752 event.preventDefault();
1753 }
1754 this.focus();
1755 };
1756
1757 /**
1758 * Chromium pastes PRIMARY into the focused editable on a middle mouseup, and
1759 * the hidden textarea is focused, so leaving the default alive would deliver
1760 * a second paste through onPaste on top of the one onPointerDown sent.
1761 */
1762 private readonly onMouseUp = (event: MouseEvent) => {
1763 if (event.button === 1 && isMiddleClickPastePlatform()) event.preventDefault();
1764 };
1765
1766 private readonly onContextMenu = (event: MouseEvent) => {
1767 if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) {
1768 event.preventDefault();
1769 return;
1770 }
1771 this.options.onContextMenu?.(event);
1772 };
1773
1774 private readonly onScrollbarPointerDown = (event: PointerEvent) => {
1775 if (event.button !== 0) return;
1776 const state = this.readScrollbarState();
1777 if (state === null) return;
1778 const bounds = this.scrollbar.getBoundingClientRect();
1779 const geometry = terminalScrollbarGeometry(state, bounds.height);
1780 if (geometry === null) return;
1781 event.preventDefault();
1782 event.stopPropagation();
1783 this.scrollbarPointerId = event.pointerId;
1784 this.scrollbarPointerOffset =
1785 event.target === this.scrollbarThumb
1786 ? event.clientY - bounds.top - geometry.thumbTop
1787 : geometry.thumbHeight / 2;
1788 this.scrollbar.setPointerCapture(event.pointerId);
1789 this.scrollbarToPointer(event.clientY, bounds);
1790 };
1791
1792 private readonly onScrollbarPointerMove = (event: PointerEvent) => {
1793 if (event.pointerId !== this.scrollbarPointerId || this.scrollbarState === null) return;
1794 event.preventDefault();
1795 this.scrollbarToPointer(event.clientY, this.scrollbar.getBoundingClientRect());
1796 };
1797
1798 private readonly onScrollbarPointerUp = (event: PointerEvent) => {
1799 if (event.pointerId !== this.scrollbarPointerId) return;
1800 event.preventDefault();
1801 this.scrollbarPointerId = null;
1802 if (this.scrollbar.hasPointerCapture(event.pointerId)) {
1803 this.scrollbar.releasePointerCapture(event.pointerId);
1804 }
1805 };
1806
1807 private readonly onScrollbarKeyDown = (event: KeyboardEvent) => {
1808 const state = this.readScrollbarState();
1809 if (state === null) return;
1810 let delta = 0;
1811 switch (event.key) {
1812 case "ArrowUp":
1813 delta = -1;
1814 break;
1815 case "ArrowDown":
1816 delta = 1;
1817 break;
1818 case "PageUp":
1819 delta = -Math.max(1, state.len);
1820 break;
1821 case "PageDown":
1822 delta = Math.max(1, state.len);
1823 break;
1824 case "Home":
1825 delta = -state.offset;
1826 break;
1827 case "End":
1828 delta = state.total - state.len - state.offset;
1829 break;
1830 default:
1831 return;
1832 }
1833 event.preventDefault();
1834 event.stopPropagation();
1835 this.scrollViewport(delta);
1836 };
1837
1838 private installEvents(): void {
1839 this.input.addEventListener("keydown", this.onKeyDown);
1840 this.input.addEventListener("keyup", this.onKeyUp);
1841 this.input.addEventListener("focus", this.onFocus);
1842 this.input.addEventListener("blur", this.onBlur);
1843 this.input.addEventListener("input", this.onInput);
1844 this.input.addEventListener("paste", this.onPaste);
1845 this.input.addEventListener("copy", this.onCopyEvent);
1846 this.input.addEventListener("compositionstart", this.onCompositionStart);
1847 this.input.addEventListener("compositionend", this.onCompositionEnd);
1848 this.canvas.addEventListener("pointerdown", this.onPointerDown);
1849 this.canvas.addEventListener("pointermove", this.onPointerMove);
1850 this.canvas.addEventListener("pointerleave", this.onPointerLeave);
1851 this.canvas.addEventListener("pointerup", this.onPointerUp);
1852 this.canvas.addEventListener("pointercancel", this.onPointerUp);
1853 this.canvas.addEventListener("wheel", this.onWheel, { passive: false });
1854 this.canvas.addEventListener("mousedown", this.onMouseDown);
1855 this.canvas.addEventListener("mouseup", this.onMouseUp);
1856 this.canvas.addEventListener("contextmenu", this.onContextMenu);
1857 this.scrollbar.addEventListener("pointerdown", this.onScrollbarPointerDown);
1858 this.scrollbar.addEventListener("pointermove", this.onScrollbarPointerMove);
1859 this.scrollbar.addEventListener("pointerup", this.onScrollbarPointerUp);
1860 this.scrollbar.addEventListener("pointercancel", this.onScrollbarPointerUp);
1861 this.scrollbar.addEventListener("keydown", this.onScrollbarKeyDown);
1862 window.addEventListener("pointerup", this.onWindowPointerRelease);
1863 window.addEventListener("pointercancel", this.onWindowPointerRelease);
1864 }
1865
1866 private removeEvents(): void {
1867 this.input.removeEventListener("keydown", this.onKeyDown);
1868 this.input.removeEventListener("keyup", this.onKeyUp);
1869 this.input.removeEventListener("focus", this.onFocus);
1870 this.input.removeEventListener("blur", this.onBlur);
1871 this.input.removeEventListener("input", this.onInput);
1872 this.input.removeEventListener("paste", this.onPaste);
1873 this.input.removeEventListener("copy", this.onCopyEvent);
1874 this.input.removeEventListener("compositionstart", this.onCompositionStart);
1875 this.input.removeEventListener("compositionend", this.onCompositionEnd);
1876 this.canvas.removeEventListener("pointerdown", this.onPointerDown);
1877 this.canvas.removeEventListener("pointermove", this.onPointerMove);
1878 this.canvas.removeEventListener("pointerleave", this.onPointerLeave);
1879 this.canvas.removeEventListener("pointerup", this.onPointerUp);
1880 this.canvas.removeEventListener("pointercancel", this.onPointerUp);
1881 this.canvas.removeEventListener("wheel", this.onWheel);
1882 this.canvas.removeEventListener("mousedown", this.onMouseDown);
1883 this.canvas.removeEventListener("mouseup", this.onMouseUp);
1884 this.canvas.removeEventListener("contextmenu", this.onContextMenu);
1885 this.scrollbar.removeEventListener("pointerdown", this.onScrollbarPointerDown);
1886 this.scrollbar.removeEventListener("pointermove", this.onScrollbarPointerMove);
1887 this.scrollbar.removeEventListener("pointerup", this.onScrollbarPointerUp);
1888 this.scrollbar.removeEventListener("pointercancel", this.onScrollbarPointerUp);
1889 this.scrollbar.removeEventListener("keydown", this.onScrollbarKeyDown);
1890 window.removeEventListener("pointerup", this.onWindowPointerRelease);
1891 window.removeEventListener("pointercancel", this.onWindowPointerRelease);
1892 }
1893
1894 private scrollViewport(deltaRows: number): void {
1895 let delta = Math.trunc(deltaRows);
1896 const state = this.readScrollbarState();
1897 if (state !== null) {
1898 const maxOffset = Math.max(0, state.total - state.len);
1899 const offset = Math.max(0, Math.min(state.offset + delta, maxOffset));
1900 delta = offset - state.offset;
1901 this.scrollbarState = { ...state, offset };
1902 }
1903 if (delta === 0) return;
1904 this.core.scroll(delta);
1905 this.forceFullRender = true;
1906 this.scrollbarDirty = true;
1907 this.requestRender();
1908 }
1909
1910 private scrollbarToPointer(clientY: number, bounds: DOMRect): void {
1911 const state = this.scrollbarState;
1912 if (state === null) return;
1913 const offset = terminalScrollbarOffsetAtPointer(
1914 state,
1915 bounds.height,
1916 clientY - bounds.top,
1917 this.scrollbarPointerOffset,
1918 );
1919 this.scrollViewport(offset - state.offset);
1920 }
1921
1922 private updateScrollbar(): void {
1923 const state = this.readScrollbarState();
1924 const geometry =
1925 state === null
1926 ? null
1927 : terminalScrollbarGeometry(
1928 state,
1929 Math.max(0, this.mount.clientHeight - CONTENT_PADDING * 2),
1930 );
1931 this.scrollbar.hidden = geometry === null;
1932 if (state === null || geometry === null) return;
1933 this.scrollbar.setAttribute("aria-valuemin", "0");
1934 this.scrollbar.setAttribute("aria-valuemax", String(geometry.maxOffset));
1935 this.scrollbar.setAttribute(
1936 "aria-valuenow",
1937 String(Math.max(0, Math.min(state.offset, geometry.maxOffset))),
1938 );
1939 this.scrollbarThumb.style.height = `${geometry.thumbHeight}px`;
1940 this.scrollbarThumb.style.transform = `translateY(${geometry.thumbTop}px)`;
1941 }
1942
1943 private readScrollbarState(): GhosttyScrollbar | null {
1944 const state = this.core.scrollbarState();
1945 this.scrollbarState = state;
1946 return state;
1947 }
1948
1949 private requestRender(): void {
1950 if (this.disposed || !this.visible || !this.hasSize || this.frame !== 0) return;
1951 this.frame = window.requestAnimationFrame(() => {
1952 this.frame = 0;
1953 this.renderFrame();
1954 });
1955 }
1956
1957 private cancelRender(): void {
1958 if (this.frame !== 0) {
1959 window.cancelAnimationFrame(this.frame);
1960 this.frame = 0;
1961 }
1962 if (this.cursorTimer !== null) {
1963 window.clearTimeout(this.cursorTimer);
1964 this.cursorTimer = null;
1965 }
1966 }
1967
1968 private renderFrame(): void {
1969 if (this.disposed || !this.visible) return;
1970 if (this.frame !== 0) {
1971 window.cancelAnimationFrame(this.frame);
1972 this.frame = 0;
1973 }
1974 // Hidden thread drawers stay mounted so switching back is instant, but a
1975 // display:none canvas has nothing to show. Ghostty keeps parsing; the
1976 // ResizeObserver refits and repaints in full once the mount has a size.
1977 if (this.mount.clientWidth === 0 || this.mount.clientHeight === 0) {
1978 this.hasSize = false;
1979 this.forceFullRender = true;
1980 this.cancelRender();
1981 return;
1982 }
1983 this.snapshot = this.core.snapshot();
1984 // A cursor that is not blinking right now must be drawn, never caught in an
1985 // off phase left behind by a blink that has since been turned off.
1986 if (!this.blinkEnabled()) this.cursorOn = true;
1987 // The origin only moves together with a forced full repaint: partial
1988 // dirty-row redraws must never composite rows at a shifted origin over
1989 // rows painted at the previous one. Bottom anchoring starts once
1990 // scrollback exists, i.e. when the prompt actually lives at the bottom.
1991 const scrollState = this.readScrollbarState();
1992 const anchorBottom = scrollState !== null && scrollState.total > scrollState.len;
1993 const nextOriginY = terminalContentOriginY(
1994 this.mountHeight,
1995 CONTENT_PADDING,
1996 this.rows,
1997 this.metrics.height,
1998 anchorBottom,
1999 );
2000 if (nextOriginY !== this.originY) {
2001 this.originY = nextOriginY;
2002 this.forceFullRender = true;
2003 }
2004 this.refreshHoveredLink();
2005 renderGhosttySnapshot({
2006 context: this.context,
2007 snapshot: this.snapshot,
2008 metrics: this.metrics,
2009 fontSize: this.fontSize,
2010 fontFamily: this.fontFamily,
2011 padding: CONTENT_PADDING,
2012 originY: this.originY,
2013 forceFull: this.forceFullRender,
2014 cursorOn: this.cursorOn,
2015 previousCursorY: this.renderedCursorY,
2016 focused: this.focused,
2017 hoveredLinkRange: this.hoveredLink?.range ?? null,
2018 ...(this.theme.selectionBackground !== undefined
2019 ? { selectionBackground: this.theme.selectionBackground }
2020 : {}),
2021 });
2022 this.positionInput();
2023 this.renderedCursorY =
2024 this.cursorOn && this.snapshot.cursorVisible && this.snapshot.cursorY >= 0
2025 ? this.snapshot.cursorY
2026 : null;
2027 if (this.scrollbarDirty) {
2028 this.scrollbarDirty = false;
2029 this.updateScrollbar();
2030 }
2031 this.forceFullRender = false;
2032 this.scheduleCursorBlink();
2033 }
2034
2035 private scheduleCursorBlink(): void {
2036 if (this.cursorTimer !== null) window.clearTimeout(this.cursorTimer);
2037 this.cursorTimer = null;
2038 if (!this.blinkEnabled()) return;
2039 this.cursorTimer = window.setTimeout(() => {
2040 this.cursorTimer = null;
2041 this.cursorOn = !this.cursorOn;
2042 this.requestRender();
2043 }, CURSOR_BLINK_INTERVAL_MS);
2044 }
2045
2046 private blinkEnabled(): boolean {
2047 const snapshot = this.snapshot;
2048 if (!snapshot || !this.visible || !this.hasSize) return false;
2049 return shouldBlinkTerminalCursor({
2050 focused: this.focused,
2051 cursorBlinking: snapshot.cursorBlinking,
2052 cursorVisible: snapshot.cursorVisible,
2053 reducedMotion: this.reducedMotionMedia?.matches ?? false,
2054 });
2055 }
2056
2057 private positionInput(): void {
2058 const snapshot = this.snapshot;
2059 if (!snapshot || !snapshot.cursorVisible || snapshot.cursorX < 0 || snapshot.cursorY < 0) {
2060 return;
2061 }
2062 // The IME candidate window anchors to the textarea, so it must follow the
2063 // terminal cursor for composition to appear where the user is typing.
2064 const left = CONTENT_PADDING + snapshot.cursorX * this.metrics.width;
2065 const top = this.originY + snapshot.cursorY * this.metrics.height;
2066 if (left === this.inputLeft && top === this.inputTop) return;
2067 this.inputLeft = left;
2068 this.inputTop = top;
2069 this.input.style.left = `${left}px`;
2070 this.input.style.top = `${top}px`;
2071 this.input.style.height = `${this.metrics.height}px`;
2072 }
2073
2074 private cellAt(clientX: number, clientY: number): { x: number; y: number } {
2075 const bounds = this.canvas.getBoundingClientRect();
2076 return {
2077 x: Math.max(
2078 0,
2079 Math.min(
2080 this.cols - 1,
2081 Math.floor((clientX - bounds.left - CONTENT_PADDING) / this.metrics.width),
2082 ),
2083 ),
2084 y: Math.max(
2085 0,
2086 Math.min(
2087 this.rows - 1,
2088 Math.floor((clientY - bounds.top - this.originY) / this.metrics.height),
2089 ),
2090 ),
2091 };
2092 }
2093
2094 private linkAt(clientX: number, clientY: number): TerminalLinkWithRange | null {
2095 const link = this.findLinkAt(clientX, clientY);
2096 return link && this.options.canActivateLink?.(link.text) !== false ? link : null;
2097 }
2098
2099 private findLinkAt(clientX: number, clientY: number): TerminalLinkWithRange | null {
2100 if (!this.snapshot) return null;
2101 const cell = terminalGridCellAt({
2102 bounds: this.canvas.getBoundingClientRect(),
2103 clientX,
2104 clientY,
2105 cols: this.cols,
2106 rows: this.rows,
2107 metrics: this.metrics,
2108 padding: CONTENT_PADDING,
2109 originY: this.originY,
2110 });
2111 if (!cell) return null;
2112 const explicitHyperlink = this.core.hyperlinkAt(cell.x, cell.y);
2113 if (explicitHyperlink) {
2114 const start = { ...cell };
2115 const end = { ...cell };
2116 while (true) {
2117 const previous =
2118 start.x > 0
2119 ? { x: start.x - 1, y: start.y }
2120 : start.y > 0 && this.snapshot.rowData[start.y]?.isWrapContinuation
2121 ? { x: this.cols - 1, y: start.y - 1 }
2122 : null;
2123 if (!previous || this.core.hyperlinkAt(previous.x, previous.y) !== explicitHyperlink) break;
2124 start.x = previous.x;
2125 start.y = previous.y;
2126 }
2127 while (true) {
2128 const next =
2129 end.x + 1 < this.cols
2130 ? { x: end.x + 1, y: end.y }
2131 : end.y + 1 < this.rows && this.snapshot.rowData[end.y]?.wrapsToNext
2132 ? { x: 0, y: end.y + 1 }
2133 : null;
2134 if (!next || this.core.hyperlinkAt(next.x, next.y) !== explicitHyperlink) break;
2135 end.x = next.x;
2136 end.y = next.y;
2137 }
2138 return {
2139 text: explicitHyperlink,
2140 range: { start, end },
2141 };
2142 }
2143 return terminalLinkAtPositionWithRange(this.snapshot.rowData, cell.y, cell.x);
2144 }
2145
2146 private sendMouse(action: TerminalMouseAction, button: number | null, event: MouseEvent): void {
2147 const bounds = this.canvas.getBoundingClientRect();
2148 const data = this.core.encodeMouse({
2149 action,
2150 button,
2151 mods:
2152 (event.shiftKey ? 1 : 0) |
2153 (event.ctrlKey ? 1 << 1 : 0) |
2154 (event.altKey ? 1 << 2 : 0) |
2155 (event.metaKey ? 1 << 3 : 0),
2156 x: Math.max(0, event.clientX - bounds.left),
2157 y: Math.max(0, event.clientY - bounds.top),
2158 screenWidth: bounds.width,
2159 screenHeight: bounds.height,
2160 cellWidth: this.metrics.width,
2161 cellHeight: this.metrics.height,
2162 paddingLeft: CONTENT_PADDING,
2163 paddingRight: CONTENT_PADDING,
2164 paddingTop: this.originY,
2165 paddingBottom: Math.max(0, bounds.height - this.originY - this.rows * this.metrics.height),
2166 anyButtonPressed: event.buttons !== 0,
2167 });
2168 const resolution = resolveTerminalMouseData(action, data, this.lastMouseMotionData);
2169 this.lastMouseMotionData = resolution.nextMotionData;
2170 if (resolution.send) this.options.onData(data);
2171 }
2172
2173 private synchronizeMouseTrackingState(): boolean {
2174 // Output writes can toggle DEC 1003 without moving the pointer. Keep the
2175 // previous mode so the next same-cell motion starts a fresh tracking session.
2176 const tracking = this.core.isMouseAnyEventTracking();
2177 const state = resolveTerminalMouseTrackingState(
2178 this.mouseAnyEventTracking,
2179 tracking,
2180 this.lastMouseMotionData,
2181 );
2182 this.mouseAnyEventTracking = state.tracking;
2183 this.lastMouseMotionData = state.motionData;
2184 return tracking;
2185 }
2186
2187 private buttonFromButtons(buttons: number): number | null {
2188 if ((buttons & 1) !== 0) return 1;
2189 if ((buttons & 4) !== 0) return 3;
2190 if ((buttons & 2) !== 0) return 2;
2191 if ((buttons & 8) !== 0) return 4;
2192 if ((buttons & 16) !== 0) return 5;
2193 return null;
2194 }
2195}
apps/web/src/terminal/ghostty/vendor/ghostty-vt.wasm deleted
Binary files a/apps/web/src/terminal/ghostty/vendor/ghostty-vt.wasm and /dev/null differ
apps/web/src/terminal/ghostty/vendor/ghostty-write-pty.wasm deleted
Binary files a/apps/web/src/terminal/ghostty/vendor/ghostty-write-pty.wasm and /dev/null differ
docs/internals/terminal-runtime.md+9-9
......@@ -36,16 +36,16 @@ bound during startup.
3636
3737## Renderer ownership
3838
39Android and web use the same `libghostty-vt` C ABI for terminal behavior. Platform
40adapters own drawing and input integration, and React stays out of terminal frames.
41The web adapter shares one WebAssembly instance per browser tab while each terminal
42owns and frees its own handles. The canonical upstream pin is
43[`native/libghostty-vt/VERSION`](../../native/libghostty-vt/VERSION); both native and
44web artifacts must be rebuilt when it changes. Web embeds the revision in its build
45info so the ABI check can detect drift without a second pin.
39Android keeps its native libghostty-vt adapter. Web uses the vendored
40`@clo/terminal` package, whose Ghostty ABI pin and engine artifact live together in
41[`packages/terminal/vendor`](../../packages/terminal/vendor). Its DOM renderer owns
42frames and native selection; the T3 adapter owns environment transport and local
43interaction policy. One WebAssembly instance serves each browser tab, with separate
44terminal handles freed on disposal. The Android pin remains independent at
45[`native/libghostty-vt/VERSION`](../../native/libghostty-vt/VERSION).
4646
4747Restoring scrollback must not send terminal replies to the current shell. Historical
4848device queries can otherwise provoke fresh replies that appear as junk at the
4949prompt. The server strips query/response traffic from retained history, and the
50[web renderer](../../apps/web/src/terminal/ghostty/core.ts) detaches its PTY writer
51during replay. Preserve both protections when changing retention or renderer code.
50[web adapter](../../apps/web/src/terminal/TerminalSurface.ts) suppresses its PTY data
51sink during replay. Preserve both protections when changing retention or renderer code.
packages/terminal/LICENSE created+15
......@@ -0,0 +1,15 @@
1ISC License
2
3Copyright (c) 2026 clover caruso
4
5Permission to use, copy, modify, and/or distribute this software for any
6purpose with or without fee is hereby granted, provided that the above
7copyright notice and this permission notice appear in all copies.
8
9THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15PERFORMANCE OF THIS SOFTWARE.
packages/terminal/README.md created+176
......@@ -0,0 +1,176 @@
1# @clo/terminal
2
3Browser-pure terminal emulation on the vendored libghostty-vt WebAssembly core. The package includes a headless terminal, a native-selection DOM renderer, thin React and Solid adapters, explicit CSS, pinned fonts, and the wasm artifact. Transport remains caller-owned: pty bytes go in, terminal-bound bytes come out.
4
5## Install
6
7```sh
8pnpm add @clo/terminal
9```
10
11Add `react` or `solid-js` only when using that adapter. Import the renderer stylesheet once:
12
13```ts
14import "@clo/terminal/terminal.css";
15```
16
17Vite and compatible bundlers can resolve the exported wasm as a URL:
18
19```ts
20import { GhosttyModule } from "@clo/terminal";
21import wasmUrl from "@clo/terminal/ghostty-vt.wasm?url";
22
23const module = await GhosttyModule.fetch(wasmUrl);
24```
25
26`GhosttyModule.load(bytes)` remains available when the host owns artifact loading. One module can host many terminals.
27
28## Vanilla DOM
29
30```ts
31import { mountTerminal } from "@clo/terminal";
32
33const terminal = mountTerminal(document.querySelector("#terminal")!, module, {
34 onData: (bytes) => socket.send(bytes),
35 onResize: ({ cols, rows }) => resizePty(cols, rows),
36});
37
38socket.binaryType = "arraybuffer";
39const receive = ({ data }: MessageEvent<string | ArrayBuffer>) =>
40 terminal.write(typeof data === "string" ? data : new Uint8Array(data));
41socket.addEventListener("message", receive);
42await terminal.ready;
43
44// Later:
45socket.removeEventListener("message", receive);
46terminal.dispose();
47```
48
49`write` parses pty output and schedules a DOM frame. `onData` combines keystrokes, paste, mouse reports, and terminal query responses so all terminal-bound bytes use one transport path.
50
51## React
52
53```tsx
54import { TerminalPane } from "@clo/terminal/react";
55
56<TerminalPane
57 module={module}
58 onData={(bytes) => socket.send(bytes)}
59 onResize={({ cols, rows }) => resizePty(cols, rows)}
60 onReady={(handle) => {
61 socket.binaryType = "arraybuffer";
62 const receive = ({ data }: MessageEvent<string | ArrayBuffer>) =>
63 handle.write(typeof data === "string" ? data : new Uint8Array(data));
64 socket.addEventListener("message", receive);
65 return () => socket.removeEventListener("message", receive);
66 }}
67/>;
68```
69
70## Solid
71
72```tsx
73import { GhosttyModule } from "@clo/terminal";
74import { Show, createSignal } from "solid-js";
75import { TerminalPane } from "@clo/terminal/solid";
76
77const [module, setModule] = createSignal<GhosttyModule>();
78void GhosttyModule.fetch(wasmUrl).then(setModule);
79
80<Show when={module()}>
81 {(loaded) => (
82 <TerminalPane
83 module={loaded()}
84 onData={(bytes) => socket.send(bytes)}
85 onResize={({ cols, rows }) => resizePty(cols, rows)}
86 onReady={(handle) => {
87 socket.binaryType = "arraybuffer";
88 const receive = ({ data }: MessageEvent<string | ArrayBuffer>) =>
89 handle.write(typeof data === "string" ? data : new Uint8Array(data));
90 socket.addEventListener("message", receive);
91 return () => socket.removeEventListener("message", receive);
92 }}
93 />
94 )}
95</Show>;
96```
97
98Both framework components own disposal and remount only when `module` changes. Geometry, parser, and renderer options are mount-time values; callback props remain live. `AnsiText` in each adapter renders a static escaped ANSI snapshot without creating an emulator.
99
100## Public API
101
102| Entry | Surface |
103| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104| `@clo/terminal` | `GhosttyModule`, `mountTerminal`, `Terminal`, `TerminalView`, frame/cell types, ANSI conversion, link detection, key mapping, run helpers, predictive-echo engine, and shell-mark/command utilities |
105| `@clo/terminal/react` | `TerminalPane`, `AnsiText`, and their props |
106| `@clo/terminal/solid` | `TerminalPane`, `AnsiText`, and their props |
107| `@clo/terminal/terminal.css` | DOM renderer structure, pinned font faces, cursor/blink/scrollbar behavior, and CSS theme seams |
108| `@clo/terminal/ghostty-vt.wasm` | pinned libghostty-vt artifact for caller-controlled loading |
109
110### Mounted handle
111
112`mountTerminal` and adapter `onReady` callbacks expose:
113
114- `write(data)` feeds pty output and schedules paint.
115- `ready` resolves after initial fonts, measurement, fit, and resize reporting settle.
116- `terminal` exposes headless state, scrollback, input encoders, snapshots, title/pwd, and shell command history.
117- `view` exposes scheduling, fit, resize observation, metric refresh, inspection, and direct paint controls.
118- `dispose()` releases the renderer lease and wasm resources; framework adapters call it automatically.
119
120### Mount options
121
122The high-level mount and both framework adapters share the same options: initial `cols`/`rows`, byte-bounded scrollback, shell integration and command retention, scrollbar/scrollback/mouse/sticky behavior, links, predictive echo, resize, title/pwd/bell/shell-command events, clipboard policy, and the unified `onData` transport callback.
123
124### Headless and advanced APIs
125
126`Terminal` can run without the DOM. `snapshot()` is a destructive dirty-state read; use `acquireFrameReader()` when attaching a renderer and release it before another consumer. `TerminalView` is exported for custom lifecycle ownership. Pure utilities cover ANSI-to-HTML, canonical cells/runs, URL/path/OSC 8 links, DOM key translation, OSC scanning, shell-mark parsing, command tracking, and sticky-command selection.
127
128## Predictive echo
129
130Over a remote pty, every keystroke waits a round trip before a glyph appears. `predict` paints typed characters speculatively, in the manner of [mosh](https://mosh.org), and reconciles them against real output as it arrives.
131
132```ts
133mountTerminal(element, module, {
134 onData: (bytes) => socket.send(bytes),
135 predict: { mode: "prompt", minLatencyMs: 25 },
136});
137```
138
139`predict: true` is shorthand for the defaults. It is **off** unless you ask for it.
140
141Unlike mosh, this does not guess whether the far end echoes — it requires proof. Predictions run only inside a shell-integration readline region (OSC 133/633 `B` seen, `C` not yet), so an uninstrumented shell, a `sh` session, and every full-screen application predict nothing at all. Within that region only single-column printable ASCII is predicted, and backspace only un-types the engine's own predictions. Wide characters, combining marks, line wraps, paste, mouse input, control chords, and resize all drop the outstanding speculation rather than extend it, and any contradiction drops all of it.
142
143Display is adaptive. Predictions are recorded from the first keystroke but painted only once the smoothed round trip reaches `minLatencyMs` (default 25ms), so a local pty never shows one. Set `minLatencyMs: 0` to predict immediately.
144
145| Option | Default | Meaning |
146| ---------------- | ------- | ---------------------------------------------------------- |
147| `mode` | `off` | `off`, or `prompt` for the readline-region gate |
148| `minLatencyMs` | `25` | Round trip below which predictions are recorded, not shown |
149| `timeoutMs` | `1000` | Unconfirmed prediction lifetime |
150| `underline` | `true` | Mark unconfirmed predictions as speculative |
151| `maxPredictions` | `128` | Cap on outstanding predictions |
152
153Speculative glyphs render as `.clo-term-run.predicted` and are themed with `--clo-term-predicted-opacity` and `--clo-term-predicted-underline`. They never enter terminal state: `Terminal.text()`, snapshots, and the command model only ever describe what the pty actually sent. `view.predictions` exposes the engine for diagnostics, including the measured `latencyMs`.
154
155`PredictionEngine` is also exported standalone. It is pure — plain screen data in, an overlay out, time supplied as an argument — so a different renderer can reuse it.
156
157## Styling
158
159Set theme values on the mounted element or an ancestor:
160
161```css
162.my-terminal {
163 --clo-term-bg: #11111b;
164 --clo-term-fg: #cdd6f4;
165 --clo-term-cursor: #f5e0dc;
166 --clo-term-font-size: 14px;
167 --clo-term-line-height: 1.35;
168 --clo-term-scrollbar-thumb: rgb(205 214 244 / 28%);
169}
170```
171
172Call `handle.view.refreshMetrics()` after changing a loaded font family or metric-affecting CSS variable.
173
174## Security
175
176Links are inert unless `links.onActivate` is supplied. Clipboard writes are denied unless `onClipboardWrite` synchronously returns `true`; the package never reads the clipboard. The terminal performs no navigation, socket, pty, filesystem, Node, Electron, or VS Code work.
packages/terminal/fonts/NOTICES.md created+58
......@@ -0,0 +1,58 @@
1# terminal symbol notices
2
3This file covers `__clo_terminal_symbols_3_4_0-Regular.woff2`, derived from
4Symbols Nerd Font Mono v3.4.0 at Nerd Fonts commit
5`fa7b859994228a9c8759f99c55a8d31ee92a1b5e`.
6
7## Modifications
8
9Nerd Fonts assembled and normalized the component glyphs into its monospaced
10Symbols Only font. Clover selected the exact cmap listed in
11`SYMBOLS-CODEPOINTS.txt`, removed standard Powerline and unlicensed Font Logos,
12and converted the result to WOFF2 with fonttools. No retained outline was
13intentionally edited. This notice records those modifications for CC BY 4.0
14and Apache 2.0 compliance.
15
16## Retained components
17
18| Codepoints | Component | Terms and attribution |
19| ------------------------------------------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
20| `U+23FB-23FE`, `U+2B58` | IEC Power Symbols | Nerd Fonts audit: MIT; Nerd Fonts MIT notices |
21| `U+2630`, `U+E0A3`, source cmap in `U+E0B4-E0D7` | Powerline Extra Symbols | MIT; copyright 2015 Ryan L McIntyre |
22| `U+2665`, `U+26A1`, source cmap in `U+F400-F533` | Octicons | MIT; copyright 2023 GitHub Inc. |
23| `U+276C-2771`, `U+EE00-EE0B` | Nerd Fonts heavy brackets and progress indicators | Nerd Fonts audit: MIT; copyright Ryan L McIntyre |
24| `U+E000-E00A` | Pomicons | SIL OFL 1.1; copyright 2021 Gabriele Lana; Reserved Font Name Pomicons |
25| `U+E200-E2A9` | Font Awesome Extension | MIT; copyright 2017 Andre Luiz Gava |
26| `U+E300-E3E3` | Weather Icons | SIL OFL 1.1; designs by Lukas Bischoff, v1.1-forward art by Erik Flowers |
27| source cmap in `U+E5FA-E6B8` | Seti-UI and Nerd Fonts custom icons | Nerd Fonts audit: MIT; Nerd Fonts MIT notices |
28| source cmap in `U+E700-E8EF` | Devicons 2.16.0.custom | MIT; based on Devicons v2.16.0, copyright 2015 konpa |
29| source cmap in `U+EA60-EC1E` | Codicons 0.0.35 | CC BY 4.0; Microsoft, source at `https://github.com/microsoft/vscode-codicons` |
30| source cmap in `U+ED00-EFCE`, `U+F000-F2FF` | Font Awesome plus Nerd Fonts progress indicators | Font Awesome notice, SIL OFL 1.1 for its source font, and CC BY 4.0 for icons per the Nerd Fonts audit; copyright 2022 Fonticons, Inc. |
31| `U+F0001-F1AF0` | Material Design Icons | Apache 2.0 for the desktop font; Pictogrammers notice; source at `https://github.com/Templarian/MaterialDesign-Font` |
32
33`U+F300-F381` is Font Logos, classified as unlicensed by the pinned Nerd Fonts
34audit, and is not retained. Standard Powerline `U+E0A0-E0A2` and
35`U+E0B0-E0B3` is also not retained. Accordingly, its separate Free License is
36not a license of this subset.
37
38## Files
39
40- `licenses/NerdFonts-Symbols-Only-MIT.txt`: exact Symbols Only license.
41- `licenses/NerdFonts-LICENSE.txt`: exact Nerd Fonts repository MIT/OFL text.
42- `licenses/NerdFonts-license-audit.md`: exact pinned upstream license audit.
43- `licenses/CC-BY-4.0.txt`: exact CC BY 4.0 legal code retained by Codicons.
44- `licenses/Font-Awesome-LICENSE.txt`: exact Font Awesome notice and licenses.
45- `licenses/Material-Design-Icons-NOTICE.txt`: exact Pictogrammers notice.
46- `licenses/Apache-2.0.txt`: Apache License 2.0 text linked by that notice.
47- `licenses/Pomicons-OFL.txt`: exact Pomicons copyright and OFL 1.1.
48- `licenses/Weather-Icons-OFL.txt`: exact Weather Icons OFL 1.1 file retained by Nerd Fonts.
49- `licenses/Octicons-MIT.txt`: exact Octicons MIT notice retained by Nerd Fonts.
50- `licenses/Powerline-Extra-MIT.txt`: exact Powerline Extra MIT notice retained by Nerd Fonts.
51- `licenses/Devicons-MIT.txt`: Devicons v2.16.0 MIT notice at commit `6910f0503efdd315c8f9b858234310c06e04d9c0`.
52- `licenses/Font-Awesome-Extension-MIT.txt`: upstream MIT notice at commit `09d80249058ee8018a45da30add4339289dbb466`.
53
54The source Weather Icons TTF is byte-identical to upstream git blob
55`948f0a5d26e899539e7821aa5a500932f76ba582`; its metadata names Erik Flowers
56and Lukas Bischoff. The retained Nerd Fonts source README identifies Devicons
57as `2.16.0.custom` and Codicons as `0.0.35`. These facts close attribution that
58is abbreviated in the component license text.
packages/terminal/fonts/OFL.txt created+110
......@@ -0,0 +1,110 @@
1Copyright (c) 2015-2026, Renzhi Li (aka. Belleve Invis, belleve@typeof.net)
2
3This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
5This license is copied below, and is also available with a FAQ at:
6http://scripts.sil.org/OFL
7
8--------------------------
9
10
11SIL Open Font License v1.1
12====================================================
13
14
15Preamble
16----------
17
18The goals of the Open Font License (OFL) are to stimulate worldwide
19development of collaborative font projects, to support the font creation
20efforts of academic and linguistic communities, and to provide a free and
21open framework in which fonts may be shared and improved in partnership
22with others.
23
24The OFL allows the licensed fonts to be used, studied, modified and
25redistributed freely as long as they are not sold by themselves. The
26fonts, including any derivative works, can be bundled, embedded,
27redistributed and/or sold with any software provided that any reserved
28names are not used by derivative works. The fonts and derivatives,
29however, cannot be released under any other type of license. The
30requirement for fonts to remain under this license does not apply
31to any document created using the fonts or their derivatives.
32
33
34Definitions
35-------------
36
37`"Font Software"` refers to the set of files released by the Copyright
38Holder(s) under this license and clearly marked as such. This may
39include source files, build scripts and documentation.
40
41`"Reserved Font Name"` refers to any names specified as such after the
42copyright statement(s).
43
44`"Original Version"` refers to the collection of Font Software components as
45distributed by the Copyright Holder(s).
46
47`"Modified Version"` refers to any derivative made by adding to, deleting,
48or substituting -- in part or in whole -- any of the components of the
49Original Version, by changing formats or by porting the Font Software to a
50new environment.
51
52`"Author"` refers to any designer, engineer, programmer, technical
53writer or other person who contributed to the Font Software.
54
55
56Permission & Conditions
57------------------------
58
59Permission is hereby granted, free of charge, to any person obtaining
60a copy of the Font Software, to use, study, copy, merge, embed, modify,
61redistribute, and sell modified and unmodified copies of the Font
62Software, subject to the following conditions:
63
641. Neither the Font Software nor any of its individual components,
65 in Original or Modified Versions, may be sold by itself.
66
672. Original or Modified Versions of the Font Software may be bundled,
68 redistributed and/or sold with any software, provided that each copy
69 contains the above copyright notice and this license. These can be
70 included either as stand-alone text files, human-readable headers or
71 in the appropriate machine-readable metadata fields within text or
72 binary files as long as those fields can be easily viewed by the user.
73
743. No Modified Version of the Font Software may use the Reserved Font
75 Name(s) unless explicit written permission is granted by the corresponding
76 Copyright Holder. This restriction only applies to the primary font name as
77 presented to the users.
78
794. The name(s) of the Copyright Holder(s) or the Author(s) of the Font
80 Software shall not be used to promote, endorse or advertise any
81 Modified Version, except to acknowledge the contribution(s) of the
82 Copyright Holder(s) and the Author(s) or with their explicit written
83 permission.
84
855. The Font Software, modified or unmodified, in part or in whole,
86 must be distributed entirely under this license, and must not be
87 distributed under any other license. The requirement for fonts to
88 remain under this license does not apply to any document created
89 using the Font Software.
90
91
92
93Termination
94-----------
95
96This license becomes null and void if any of the above conditions are
97not met.
98
99
100 DISCLAIMER
101
102 THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
103 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
104 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
105 OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
106 COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
107 INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
108 DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
109 FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
110 OTHER DEALINGS IN THE FONT SOFTWARE.
packages/terminal/fonts/PRIMARY-CODEPOINTS.txt created+4660
......@@ -0,0 +1,4660 @@
1U+000D
2U+0020
3U+0021
4U+0022
5U+0023
6U+0024
7U+0025
8U+0026
9U+0027
10U+0028
11U+0029
12U+002A
13U+002B
14U+002C
15U+002D
16U+002E
17U+002F
18U+0030
19U+0031
20U+0032
21U+0033
22U+0034
23U+0035
24U+0036
25U+0037
26U+0038
27U+0039
28U+003A
29U+003B
30U+003C
31U+003D
32U+003E
33U+003F
34U+0040
35U+0041
36U+0042
37U+0043
38U+0044
39U+0045
40U+0046
41U+0047
42U+0048
43U+0049
44U+004A
45U+004B
46U+004C
47U+004D
48U+004E
49U+004F
50U+0050
51U+0051
52U+0052
53U+0053
54U+0054
55U+0055
56U+0056
57U+0057
58U+0058
59U+0059
60U+005A
61U+005B
62U+005C
63U+005D
64U+005E
65U+005F
66U+0060
67U+0061
68U+0062
69U+0063
70U+0064
71U+0065
72U+0066
73U+0067
74U+0068
75U+0069
76U+006A
77U+006B
78U+006C
79U+006D
80U+006E
81U+006F
82U+0070
83U+0071
84U+0072
85U+0073
86U+0074
87U+0075
88U+0076
89U+0077
90U+0078
91U+0079
92U+007A
93U+007B
94U+007C
95U+007D
96U+007E
97U+00A0
98U+00A1
99U+00A2
100U+00A3
101U+00A4
102U+00A5
103U+00A6
104U+00A7
105U+00A8
106U+00A9
107U+00AA
108U+00AB
109U+00AC
110U+00AD
111U+00AE
112U+00AF
113U+00B0
114U+00B1
115U+00B2
116U+00B3
117U+00B4
118U+00B5
119U+00B6
120U+00B7
121U+00B8
122U+00B9
123U+00BA
124U+00BB
125U+00BC
126U+00BD
127U+00BE
128U+00BF
129U+00C0
130U+00C1
131U+00C2
132U+00C3
133U+00C4
134U+00C5
135U+00C6
136U+00C7
137U+00C8
138U+00C9
139U+00CA
140U+00CB
141U+00CC
142U+00CD
143U+00CE
144U+00CF
145U+00D0
146U+00D1
147U+00D2
148U+00D3
149U+00D4
150U+00D5
151U+00D6
152U+00D7
153U+00D8
154U+00D9
155U+00DA
156U+00DB
157U+00DC
158U+00DD
159U+00DE
160U+00DF
161U+00E0
162U+00E1
163U+00E2
164U+00E3
165U+00E4
166U+00E5
167U+00E6
168U+00E7
169U+00E8
170U+00E9
171U+00EA
172U+00EB
173U+00EC
174U+00ED
175U+00EE
176U+00EF
177U+00F0
178U+00F1
179U+00F2
180U+00F3
181U+00F4
182U+00F5
183U+00F6
184U+00F7
185U+00F8
186U+00F9
187U+00FA
188U+00FB
189U+00FC
190U+00FD
191U+00FE
192U+00FF
193U+0100
194U+0101
195U+0102
196U+0103
197U+0104
198U+0105
199U+0106
200U+0107
201U+0108
202U+0109
203U+010A
204U+010B
205U+010C
206U+010D
207U+010E
208U+010F
209U+0110
210U+0111
211U+0112
212U+0113
213U+0114
214U+0115
215U+0116
216U+0117
217U+0118
218U+0119
219U+011A
220U+011B
221U+011C
222U+011D
223U+011E
224U+011F
225U+0120
226U+0121
227U+0122
228U+0123
229U+0124
230U+0125
231U+0126
232U+0127
233U+0128
234U+0129
235U+012A
236U+012B
237U+012C
238U+012D
239U+012E
240U+012F
241U+0130
242U+0131
243U+0132
244U+0133
245U+0134
246U+0135
247U+0136
248U+0137
249U+0138
250U+0139
251U+013A
252U+013B
253U+013C
254U+013D
255U+013E
256U+013F
257U+0140
258U+0141
259U+0142
260U+0143
261U+0144
262U+0145
263U+0146
264U+0147
265U+0148
266U+0149
267U+014A
268U+014B
269U+014C
270U+014D
271U+014E
272U+014F
273U+0150
274U+0151
275U+0152
276U+0153
277U+0154
278U+0155
279U+0156
280U+0157
281U+0158
282U+0159
283U+015A
284U+015B
285U+015C
286U+015D
287U+015E
288U+015F
289U+0160
290U+0161
291U+0162
292U+0163
293U+0164
294U+0165
295U+0166
296U+0167
297U+0168
298U+0169
299U+016A
300U+016B
301U+016C
302U+016D
303U+016E
304U+016F
305U+0170
306U+0171
307U+0172
308U+0173
309U+0174
310U+0175
311U+0176
312U+0177
313U+0178
314U+0179
315U+017A
316U+017B
317U+017C
318U+017D
319U+017E
320U+017F
321U+0180
322U+0181
323U+0182
324U+0183
325U+0184
326U+0185
327U+0186
328U+0187
329U+0188
330U+0189
331U+018A
332U+018B
333U+018C
334U+018D
335U+018E
336U+018F
337U+0190
338U+0191
339U+0192
340U+0193
341U+0194
342U+0195
343U+0196
344U+0197
345U+0198
346U+0199
347U+019A
348U+019B
349U+019C
350U+019D
351U+019E
352U+019F
353U+01A0
354U+01A1
355U+01A2
356U+01A3
357U+01A4
358U+01A5
359U+01A6
360U+01A7
361U+01A8
362U+01A9
363U+01AA
364U+01AB
365U+01AC
366U+01AD
367U+01AE
368U+01AF
369U+01B0
370U+01B1
371U+01B2
372U+01B3
373U+01B4
374U+01B5
375U+01B6
376U+01B7
377U+01B8
378U+01B9
379U+01BA
380U+01BB
381U+01BC
382U+01BD
383U+01BE
384U+01BF
385U+01C0
386U+01C1
387U+01C2
388U+01C3
389U+01C4
390U+01C5
391U+01C6
392U+01C7
393U+01C8
394U+01C9
395U+01CA
396U+01CB
397U+01CC
398U+01CD
399U+01CE
400U+01CF
401U+01D0
402U+01D1
403U+01D2
404U+01D3
405U+01D4
406U+01D5
407U+01D6
408U+01D7
409U+01D8
410U+01D9
411U+01DA
412U+01DB
413U+01DC
414U+01DD
415U+01DE
416U+01DF
417U+01E0
418U+01E1
419U+01E2
420U+01E3
421U+01E4
422U+01E5
423U+01E6
424U+01E7
425U+01E8
426U+01E9
427U+01EA
428U+01EB
429U+01EC
430U+01ED
431U+01EE
432U+01EF
433U+01F0
434U+01F1
435U+01F2
436U+01F3
437U+01F4
438U+01F5
439U+01F6
440U+01F7
441U+01F8
442U+01F9
443U+01FA
444U+01FB
445U+01FC
446U+01FD
447U+01FE
448U+01FF
449U+0200
450U+0201
451U+0202
452U+0203
453U+0204
454U+0205
455U+0206
456U+0207
457U+0208
458U+0209
459U+020A
460U+020B
461U+020C
462U+020D
463U+020E
464U+020F
465U+0210
466U+0211
467U+0212
468U+0213
469U+0214
470U+0215
471U+0216
472U+0217
473U+0218
474U+0219
475U+021A
476U+021B
477U+021C
478U+021D
479U+021E
480U+021F
481U+0220
482U+0221
483U+0222
484U+0223
485U+0224
486U+0225
487U+0226
488U+0227
489U+0228
490U+0229
491U+022A
492U+022B
493U+022C
494U+022D
495U+022E
496U+022F
497U+0230
498U+0231
499U+0232
500U+0233
501U+0234
502U+0235
503U+0236
504U+0237
505U+0238
506U+0239
507U+023A
508U+023B
509U+023C
510U+023D
511U+023E
512U+023F
513U+0240
514U+0241
515U+0242
516U+0243
517U+0244
518U+0245
519U+0246
520U+0247
521U+0248
522U+0249
523U+024A
524U+024B
525U+024C
526U+024D
527U+024E
528U+024F
529U+0250
530U+0251
531U+0252
532U+0253
533U+0254
534U+0255
535U+0256
536U+0257
537U+0258
538U+0259
539U+025A
540U+025B
541U+025C
542U+025D
543U+025E
544U+025F
545U+0260
546U+0261
547U+0262
548U+0263
549U+0264
550U+0265
551U+0266
552U+0267
553U+0268
554U+0269
555U+026A
556U+026B
557U+026C
558U+026D
559U+026E
560U+026F
561U+0270
562U+0271
563U+0272
564U+0273
565U+0274
566U+0275
567U+0276
568U+0277
569U+0278
570U+0279
571U+027A
572U+027B
573U+027C
574U+027D
575U+027E
576U+027F
577U+0280
578U+0281
579U+0282
580U+0283
581U+0284
582U+0285
583U+0286
584U+0287
585U+0288
586U+0289
587U+028A
588U+028B
589U+028C
590U+028D
591U+028E
592U+028F
593U+0290
594U+0291
595U+0292
596U+0293
597U+0294
598U+0295
599U+0296
600U+0297
601U+0298
602U+0299
603U+029A
604U+029B
605U+029C
606U+029D
607U+029E
608U+029F
609U+02A0
610U+02A1
611U+02A2
612U+02A3
613U+02A4
614U+02A5
615U+02A6
616U+02A7
617U+02A8
618U+02A9
619U+02AA
620U+02AB
621U+02AC
622U+02AD
623U+02AE
624U+02AF
625U+02B0
626U+02B1
627U+02B2
628U+02B3
629U+02B4
630U+02B5
631U+02B6
632U+02B7
633U+02B8
634U+02B9
635U+02BA
636U+02BB
637U+02BC
638U+02BD
639U+02BE
640U+02BF
641U+02C0
642U+02C1
643U+02C2
644U+02C3
645U+02C4
646U+02C5
647U+02C6
648U+02C7
649U+02C8
650U+02C9
651U+02CA
652U+02CB
653U+02CC
654U+02CD
655U+02CE
656U+02CF
657U+02D0
658U+02D1
659U+02D2
660U+02D3
661U+02D4
662U+02D5
663U+02D6
664U+02D7
665U+02D8
666U+02D9
667U+02DA
668U+02DB
669U+02DC
670U+02DD
671U+02DE
672U+02DF
673U+02E0
674U+02E1
675U+02E2
676U+02E3
677U+02E4
678U+02E5
679U+02E6
680U+02E7
681U+02E8
682U+02E9
683U+02EA
684U+02EB
685U+02EC
686U+02ED
687U+02EE
688U+02EF
689U+02F0
690U+02F1
691U+02F2
692U+02F3
693U+02F4
694U+02F5
695U+02F6
696U+02F7
697U+02F8
698U+02F9
699U+02FA
700U+02FB
701U+02FC
702U+02FD
703U+02FE
704U+02FF
705U+0300
706U+0301
707U+0302
708U+0303
709U+0304
710U+0305
711U+0306
712U+0307
713U+0308
714U+0309
715U+030A
716U+030B
717U+030C
718U+030D
719U+030E
720U+030F
721U+0310
722U+0311
723U+0312
724U+0313
725U+0314
726U+0315
727U+0316
728U+0317
729U+0318
730U+0319
731U+031A
732U+031B
733U+031C
734U+031D
735U+031E
736U+031F
737U+0320
738U+0321
739U+0322
740U+0323
741U+0324
742U+0325
743U+0326
744U+0327
745U+0328
746U+0329
747U+032A
748U+032B
749U+032C
750U+032D
751U+032E
752U+032F
753U+0330
754U+0331
755U+0332
756U+0333
757U+0334
758U+0335
759U+0336
760U+0337
761U+0338
762U+0339
763U+033A
764U+033B
765U+033C
766U+033D
767U+033E
768U+033F
769U+0340
770U+0341
771U+0342
772U+0343
773U+0344
774U+0345
775U+0346
776U+0347
777U+0348
778U+0349
779U+034A
780U+034B
781U+034C
782U+034D
783U+034E
784U+034F
785U+0350
786U+0351
787U+0352
788U+0353
789U+0354
790U+0355
791U+0356
792U+0357
793U+0358
794U+0359
795U+035A
796U+035B
797U+035C
798U+035D
799U+035E
800U+035F
801U+0360
802U+0361
803U+0362
804U+0363
805U+0364
806U+0365
807U+0366
808U+0367
809U+0368
810U+0369
811U+036A
812U+036B
813U+036C
814U+036D
815U+036E
816U+036F
817U+0370
818U+0371
819U+0372
820U+0373
821U+0374
822U+0375
823U+0376
824U+0377
825U+037A
826U+037B
827U+037C
828U+037D
829U+037E
830U+037F
831U+0384
832U+0385
833U+0386
834U+0387
835U+0388
836U+0389
837U+038A
838U+038C
839U+038E
840U+038F
841U+0390
842U+0391
843U+0392
844U+0393
845U+0394
846U+0395
847U+0396
848U+0397
849U+0398
850U+0399
851U+039A
852U+039B
853U+039C
854U+039D
855U+039E
856U+039F
857U+03A0
858U+03A1
859U+03A3
860U+03A4
861U+03A5
862U+03A6
863U+03A7
864U+03A8
865U+03A9
866U+03AA
867U+03AB
868U+03AC
869U+03AD
870U+03AE
871U+03AF
872U+03B0
873U+03B1
874U+03B2
875U+03B3
876U+03B4
877U+03B5
878U+03B6
879U+03B7
880U+03B8
881U+03B9
882U+03BA
883U+03BB
884U+03BC
885U+03BD
886U+03BE
887U+03BF
888U+03C0
889U+03C1
890U+03C2
891U+03C3
892U+03C4
893U+03C5
894U+03C6
895U+03C7
896U+03C8
897U+03C9
898U+03CA
899U+03CB
900U+03CC
901U+03CD
902U+03CE
903U+03CF
904U+03D0
905U+03D1
906U+03D2
907U+03D3
908U+03D4
909U+03D5
910U+03D6
911U+03D7
912U+03D8
913U+03D9
914U+03DA
915U+03DB
916U+03DC
917U+03DD
918U+03DE
919U+03DF
920U+03E0
921U+03E1
922U+03F0
923U+03F1
924U+03F2
925U+03F3
926U+03F4
927U+03F5
928U+03F6
929U+03F7
930U+03F8
931U+03F9
932U+03FA
933U+03FB
934U+03FC
935U+03FD
936U+03FE
937U+03FF
938U+0400
939U+0401
940U+0402
941U+0403
942U+0404
943U+0405
944U+0406
945U+0407
946U+0408
947U+0409
948U+040A
949U+040B
950U+040C
951U+040D
952U+040E
953U+040F
954U+0410
955U+0411
956U+0412
957U+0413
958U+0414
959U+0415
960U+0416
961U+0417
962U+0418
963U+0419
964U+041A
965U+041B
966U+041C
967U+041D
968U+041E
969U+041F
970U+0420
971U+0421
972U+0422
973U+0423
974U+0424
975U+0425
976U+0426
977U+0427
978U+0428
979U+0429
980U+042A
981U+042B
982U+042C
983U+042D
984U+042E
985U+042F
986U+0430
987U+0431
988U+0432
989U+0433
990U+0434
991U+0435
992U+0436
993U+0437
994U+0438
995U+0439
996U+043A
997U+043B
998U+043C
999U+043D
1000U+043E
1001U+043F
1002U+0440
1003U+0441
1004U+0442
1005U+0443
1006U+0444
1007U+0445
1008U+0446
1009U+0447
1010U+0448
1011U+0449
1012U+044A
1013U+044B
1014U+044C
1015U+044D
1016U+044E
1017U+044F
1018U+0450
1019U+0451
1020U+0452
1021U+0453
1022U+0454
1023U+0455
1024U+0456
1025U+0457
1026U+0458
1027U+0459
1028U+045A
1029U+045B
1030U+045C
1031U+045D
1032U+045E
1033U+045F
1034U+0460
1035U+0461
1036U+0462
1037U+0463
1038U+0464
1039U+0465
1040U+0466
1041U+0467
1042U+0468
1043U+0469
1044U+046A
1045U+046B
1046U+046C
1047U+046D
1048U+046E
1049U+046F
1050U+0470
1051U+0471
1052U+0472
1053U+0473
1054U+0474
1055U+0475
1056U+0476
1057U+0477
1058U+0478
1059U+0479
1060U+047A
1061U+047B
1062U+047C
1063U+047D
1064U+047E
1065U+047F
1066U+0480
1067U+0481
1068U+0482
1069U+0483
1070U+0484
1071U+0485
1072U+0486
1073U+0487
1074U+0488
1075U+0489
1076U+048A
1077U+048B
1078U+048C
1079U+048D
1080U+048E
1081U+048F
1082U+0490
1083U+0491
1084U+0492
1085U+0493
1086U+0494
1087U+0495
1088U+0496
1089U+0497
1090U+0498
1091U+0499
1092U+049A
1093U+049B
1094U+049C
1095U+049D
1096U+049E
1097U+049F
1098U+04A0
1099U+04A1
1100U+04A2
1101U+04A3
1102U+04A4
1103U+04A5
1104U+04A6
1105U+04A7
1106U+04A8
1107U+04A9
1108U+04AA
1109U+04AB
1110U+04AC
1111U+04AD
1112U+04AE
1113U+04AF
1114U+04B0
1115U+04B1
1116U+04B2
1117U+04B3
1118U+04B4
1119U+04B5
1120U+04B6
1121U+04B7
1122U+04B8
1123U+04B9
1124U+04BA
1125U+04BB
1126U+04BC
1127U+04BD
1128U+04BE
1129U+04BF
1130U+04C0
1131U+04C1
1132U+04C2
1133U+04C3
1134U+04C4
1135U+04C5
1136U+04C6
1137U+04C7
1138U+04C8
1139U+04C9
1140U+04CA
1141U+04CB
1142U+04CC
1143U+04CD
1144U+04CE
1145U+04CF
1146U+04D0
1147U+04D1
1148U+04D2
1149U+04D3
1150U+04D4
1151U+04D5
1152U+04D6
1153U+04D7
1154U+04D8
1155U+04D9
1156U+04DA
1157U+04DB
1158U+04DC
1159U+04DD
1160U+04DE
1161U+04DF
1162U+04E0
1163U+04E1
1164U+04E2
1165U+04E3
1166U+04E4
1167U+04E5
1168U+04E6
1169U+04E7
1170U+04E8
1171U+04E9
1172U+04EA
1173U+04EB
1174U+04EC
1175U+04ED
1176U+04EE
1177U+04EF
1178U+04F0
1179U+04F1
1180U+04F2
1181U+04F3
1182U+04F4
1183U+04F5
1184U+04F6
1185U+04F7
1186U+04F8
1187U+04F9
1188U+04FA
1189U+04FB
1190U+04FC
1191U+04FD
1192U+04FE
1193U+04FF
1194U+0500
1195U+0501
1196U+0502
1197U+0503
1198U+0504
1199U+0505
1200U+0506
1201U+0507
1202U+0508
1203U+0509
1204U+050A
1205U+050B
1206U+050C
1207U+050D
1208U+050E
1209U+050F
1210U+0510
1211U+0511
1212U+0512
1213U+0513
1214U+0514
1215U+0515
1216U+0516
1217U+0517
1218U+0518
1219U+0519
1220U+051A
1221U+051B
1222U+051C
1223U+051D
1224U+051E
1225U+051F
1226U+0520
1227U+0521
1228U+0522
1229U+0523
1230U+0524
1231U+0525
1232U+0526
1233U+0527
1234U+0528
1235U+0529
1236U+052A
1237U+052B
1238U+052C
1239U+052D
1240U+052E
1241U+052F
1242U+0531
1243U+0532
1244U+0533
1245U+0534
1246U+0535
1247U+0536
1248U+0537
1249U+0538
1250U+0539
1251U+053A
1252U+053B
1253U+053C
1254U+053D
1255U+053E
1256U+053F
1257U+0540
1258U+0541
1259U+0542
1260U+0543
1261U+0544
1262U+0545
1263U+0546
1264U+0547
1265U+0548
1266U+0549
1267U+054A
1268U+054B
1269U+054C
1270U+054D
1271U+054E
1272U+054F
1273U+0550
1274U+0551
1275U+0552
1276U+0553
1277U+0554
1278U+0555
1279U+0556
1280U+0559
1281U+055A
1282U+055B
1283U+055C
1284U+055D
1285U+055E
1286U+055F
1287U+0560
1288U+0561
1289U+0562
1290U+0563
1291U+0564
1292U+0565
1293U+0566
1294U+0567
1295U+0568
1296U+0569
1297U+056A
1298U+056B
1299U+056C
1300U+056D
1301U+056E
1302U+056F
1303U+0570
1304U+0571
1305U+0572
1306U+0573
1307U+0574
1308U+0575
1309U+0576
1310U+0577
1311U+0578
1312U+0579
1313U+057A
1314U+057B
1315U+057C
1316U+057D
1317U+057E
1318U+057F
1319U+0580
1320U+0581
1321U+0582
1322U+0583
1323U+0584
1324U+0585
1325U+0586
1326U+0587
1327U+0588
1328U+0589
1329U+058A
1330U+058D
1331U+058E
1332U+058F
1333U+0E3F
1334U+10FB
1335U+1AB0
1336U+1AB1
1337U+1AB2
1338U+1AB3
1339U+1AB4
1340U+1AB5
1341U+1AB7
1342U+1AB8
1343U+1ABB
1344U+1ABC
1345U+1ABD
1346U+1ABF
1347U+1AC0
1348U+1AC1
1349U+1AC2
1350U+1AC3
1351U+1AC4
1352U+1AC5
1353U+1AC6
1354U+1AC7
1355U+1AC8
1356U+1AC9
1357U+1ACA
1358U+1ACB
1359U+1ACC
1360U+1ACD
1361U+1ACE
1362U+1ACF
1363U+1AD9
1364U+1ADA
1365U+1ADB
1366U+1ADC
1367U+1ADD
1368U+1AE0
1369U+1AE1
1370U+1AE2
1371U+1AE3
1372U+1AE4
1373U+1AE5
1374U+1AE6
1375U+1AE7
1376U+1AE8
1377U+1AE9
1378U+1AEA
1379U+1AEB
1380U+1C80
1381U+1C81
1382U+1C82
1383U+1C83
1384U+1C84
1385U+1C85
1386U+1C86
1387U+1C87
1388U+1C88
1389U+1C89
1390U+1C8A
1391U+1D00
1392U+1D01
1393U+1D02
1394U+1D03
1395U+1D04
1396U+1D05
1397U+1D06
1398U+1D07
1399U+1D08
1400U+1D09
1401U+1D0A
1402U+1D0B
1403U+1D0C
1404U+1D0D
1405U+1D0E
1406U+1D0F
1407U+1D10
1408U+1D11
1409U+1D12
1410U+1D13
1411U+1D14
1412U+1D15
1413U+1D16
1414U+1D17
1415U+1D18
1416U+1D19
1417U+1D1A
1418U+1D1B
1419U+1D1C
1420U+1D1D
1421U+1D1E
1422U+1D1F
1423U+1D20
1424U+1D21
1425U+1D22
1426U+1D23
1427U+1D24
1428U+1D25
1429U+1D26
1430U+1D27
1431U+1D28
1432U+1D29
1433U+1D2A
1434U+1D2B
1435U+1D2C
1436U+1D2D
1437U+1D2E
1438U+1D2F
1439U+1D30
1440U+1D31
1441U+1D32
1442U+1D33
1443U+1D34
1444U+1D35
1445U+1D36
1446U+1D37
1447U+1D38
1448U+1D39
1449U+1D3A
1450U+1D3B
1451U+1D3C
1452U+1D3D
1453U+1D3E
1454U+1D3F
1455U+1D40
1456U+1D41
1457U+1D42
1458U+1D43
1459U+1D44
1460U+1D45
1461U+1D46
1462U+1D47
1463U+1D48
1464U+1D49
1465U+1D4A
1466U+1D4B
1467U+1D4C
1468U+1D4D
1469U+1D4E
1470U+1D4F
1471U+1D50
1472U+1D51
1473U+1D52
1474U+1D53
1475U+1D54
1476U+1D55
1477U+1D56
1478U+1D57
1479U+1D58
1480U+1D59
1481U+1D5A
1482U+1D5B
1483U+1D5C
1484U+1D5D
1485U+1D5E
1486U+1D5F
1487U+1D60
1488U+1D61
1489U+1D62
1490U+1D63
1491U+1D64
1492U+1D65
1493U+1D66
1494U+1D67
1495U+1D68
1496U+1D69
1497U+1D6A
1498U+1D6B
1499U+1D6C
1500U+1D6D
1501U+1D6E
1502U+1D6F
1503U+1D70
1504U+1D71
1505U+1D72
1506U+1D73
1507U+1D74
1508U+1D75
1509U+1D76
1510U+1D77
1511U+1D78
1512U+1D79
1513U+1D7A
1514U+1D7B
1515U+1D7C
1516U+1D7D
1517U+1D7E
1518U+1D7F
1519U+1D80
1520U+1D81
1521U+1D82
1522U+1D83
1523U+1D84
1524U+1D85
1525U+1D86
1526U+1D87
1527U+1D88
1528U+1D89
1529U+1D8A
1530U+1D8B
1531U+1D8C
1532U+1D8D
1533U+1D8E
1534U+1D8F
1535U+1D90
1536U+1D91
1537U+1D92
1538U+1D93
1539U+1D94
1540U+1D95
1541U+1D96
1542U+1D97
1543U+1D98
1544U+1D99
1545U+1D9A
1546U+1D9B
1547U+1D9C
1548U+1D9D
1549U+1D9E
1550U+1D9F
1551U+1DA0
1552U+1DA1
1553U+1DA2
1554U+1DA3
1555U+1DA4
1556U+1DA5
1557U+1DA6
1558U+1DA7
1559U+1DA8
1560U+1DA9
1561U+1DAA
1562U+1DAB
1563U+1DAC
1564U+1DAD
1565U+1DAE
1566U+1DAF
1567U+1DB0
1568U+1DB1
1569U+1DB2
1570U+1DB3
1571U+1DB4
1572U+1DB5
1573U+1DB6
1574U+1DB7
1575U+1DB8
1576U+1DB9
1577U+1DBA
1578U+1DBB
1579U+1DBC
1580U+1DBD
1581U+1DBE
1582U+1DBF
1583U+1DC3
1584U+1DC4
1585U+1DC5
1586U+1DC6
1587U+1DC7
1588U+1DCA
1589U+1DCB
1590U+1DCC
1591U+1DCD
1592U+1DCE
1593U+1DCF
1594U+1DD3
1595U+1DD4
1596U+1DD5
1597U+1DD7
1598U+1DD8
1599U+1DD9
1600U+1DDA
1601U+1DDB
1602U+1DDC
1603U+1DDD
1604U+1DDE
1605U+1DDF
1606U+1DE0
1607U+1DE1
1608U+1DE2
1609U+1DE3
1610U+1DE4
1611U+1DE5
1612U+1DE6
1613U+1DE7
1614U+1DE8
1615U+1DE9
1616U+1DEA
1617U+1DEB
1618U+1DEC
1619U+1DED
1620U+1DEE
1621U+1DEF
1622U+1DF0
1623U+1DF1
1624U+1DF2
1625U+1DF3
1626U+1DF4
1627U+1DF5
1628U+1DF6
1629U+1DF7
1630U+1DF8
1631U+1DF9
1632U+1DFA
1633U+1DFB
1634U+1DFC
1635U+1DFD
1636U+1DFE
1637U+1DFF
1638U+1E00
1639U+1E01
1640U+1E02
1641U+1E03
1642U+1E04
1643U+1E05
1644U+1E06
1645U+1E07
1646U+1E08
1647U+1E09
1648U+1E0A
1649U+1E0B
1650U+1E0C
1651U+1E0D
1652U+1E0E
1653U+1E0F
1654U+1E10
1655U+1E11
1656U+1E12
1657U+1E13
1658U+1E14
1659U+1E15
1660U+1E16
1661U+1E17
1662U+1E18
1663U+1E19
1664U+1E1A
1665U+1E1B
1666U+1E1C
1667U+1E1D
1668U+1E1E
1669U+1E1F
1670U+1E20
1671U+1E21
1672U+1E22
1673U+1E23
1674U+1E24
1675U+1E25
1676U+1E26
1677U+1E27
1678U+1E28
1679U+1E29
1680U+1E2A
1681U+1E2B
1682U+1E2C
1683U+1E2D
1684U+1E2E
1685U+1E2F
1686U+1E30
1687U+1E31
1688U+1E32
1689U+1E33
1690U+1E34
1691U+1E35
1692U+1E36
1693U+1E37
1694U+1E38
1695U+1E39
1696U+1E3A
1697U+1E3B
1698U+1E3C
1699U+1E3D
1700U+1E3E
1701U+1E3F
1702U+1E40
1703U+1E41
1704U+1E42
1705U+1E43
1706U+1E44
1707U+1E45
1708U+1E46
1709U+1E47
1710U+1E48
1711U+1E49
1712U+1E4A
1713U+1E4B
1714U+1E4C
1715U+1E4D
1716U+1E4E
1717U+1E4F
1718U+1E50
1719U+1E51
1720U+1E52
1721U+1E53
1722U+1E54
1723U+1E55
1724U+1E56
1725U+1E57
1726U+1E58
1727U+1E59
1728U+1E5A
1729U+1E5B
1730U+1E5C
1731U+1E5D
1732U+1E5E
1733U+1E5F
1734U+1E60
1735U+1E61
1736U+1E62
1737U+1E63
1738U+1E64
1739U+1E65
1740U+1E66
1741U+1E67
1742U+1E68
1743U+1E69
1744U+1E6A
1745U+1E6B
1746U+1E6C
1747U+1E6D
1748U+1E6E
1749U+1E6F
1750U+1E70
1751U+1E71
1752U+1E72
1753U+1E73
1754U+1E74
1755U+1E75
1756U+1E76
1757U+1E77
1758U+1E78
1759U+1E79
1760U+1E7A
1761U+1E7B
1762U+1E7C
1763U+1E7D
1764U+1E7E
1765U+1E7F
1766U+1E80
1767U+1E81
1768U+1E82
1769U+1E83
1770U+1E84
1771U+1E85
1772U+1E86
1773U+1E87
1774U+1E88
1775U+1E89
1776U+1E8A
1777U+1E8B
1778U+1E8C
1779U+1E8D
1780U+1E8E
1781U+1E8F
1782U+1E90
1783U+1E91
1784U+1E92
1785U+1E93
1786U+1E94
1787U+1E95
1788U+1E96
1789U+1E97
1790U+1E98
1791U+1E99
1792U+1E9A
1793U+1E9B
1794U+1E9C
1795U+1E9D
1796U+1E9E
1797U+1E9F
1798U+1EA0
1799U+1EA1
1800U+1EA2
1801U+1EA3
1802U+1EA4
1803U+1EA5
1804U+1EA6
1805U+1EA7
1806U+1EA8
1807U+1EA9
1808U+1EAA
1809U+1EAB
1810U+1EAC
1811U+1EAD
1812U+1EAE
1813U+1EAF
1814U+1EB0
1815U+1EB1
1816U+1EB2
1817U+1EB3
1818U+1EB4
1819U+1EB5
1820U+1EB6
1821U+1EB7
1822U+1EB8
1823U+1EB9
1824U+1EBA
1825U+1EBB
1826U+1EBC
1827U+1EBD
1828U+1EBE
1829U+1EBF
1830U+1EC0
1831U+1EC1
1832U+1EC2
1833U+1EC3
1834U+1EC4
1835U+1EC5
1836U+1EC6
1837U+1EC7
1838U+1EC8
1839U+1EC9
1840U+1ECA
1841U+1ECB
1842U+1ECC
1843U+1ECD
1844U+1ECE
1845U+1ECF
1846U+1ED0
1847U+1ED1
1848U+1ED2
1849U+1ED3
1850U+1ED4
1851U+1ED5
1852U+1ED6
1853U+1ED7
1854U+1ED8
1855U+1ED9
1856U+1EDA
1857U+1EDB
1858U+1EDC
1859U+1EDD
1860U+1EDE
1861U+1EDF
1862U+1EE0
1863U+1EE1
1864U+1EE2
1865U+1EE3
1866U+1EE4
1867U+1EE5
1868U+1EE6
1869U+1EE7
1870U+1EE8
1871U+1EE9
1872U+1EEA
1873U+1EEB
1874U+1EEC
1875U+1EED
1876U+1EEE
1877U+1EEF
1878U+1EF0
1879U+1EF1
1880U+1EF2
1881U+1EF3
1882U+1EF4
1883U+1EF5
1884U+1EF6
1885U+1EF7
1886U+1EF8
1887U+1EF9
1888U+1EFA
1889U+1EFB
1890U+1EFC
1891U+1EFD
1892U+1EFE
1893U+1EFF
1894U+1F00
1895U+1F01
1896U+1F02
1897U+1F03
1898U+1F04
1899U+1F05
1900U+1F06
1901U+1F07
1902U+1F08
1903U+1F09
1904U+1F0A
1905U+1F0B
1906U+1F0C
1907U+1F0D
1908U+1F0E
1909U+1F0F
1910U+1F10
1911U+1F11
1912U+1F12
1913U+1F13
1914U+1F14
1915U+1F15
1916U+1F18
1917U+1F19
1918U+1F1A
1919U+1F1B
1920U+1F1C
1921U+1F1D
1922U+1F20
1923U+1F21
1924U+1F22
1925U+1F23
1926U+1F24
1927U+1F25
1928U+1F26
1929U+1F27
1930U+1F28
1931U+1F29
1932U+1F2A
1933U+1F2B
1934U+1F2C
1935U+1F2D
1936U+1F2E
1937U+1F2F
1938U+1F30
1939U+1F31
1940U+1F32
1941U+1F33
1942U+1F34
1943U+1F35
1944U+1F36
1945U+1F37
1946U+1F38
1947U+1F39
1948U+1F3A
1949U+1F3B
1950U+1F3C
1951U+1F3D
1952U+1F3E
1953U+1F3F
1954U+1F40
1955U+1F41
1956U+1F42
1957U+1F43
1958U+1F44
1959U+1F45
1960U+1F48
1961U+1F49
1962U+1F4A
1963U+1F4B
1964U+1F4C
1965U+1F4D
1966U+1F50
1967U+1F51
1968U+1F52
1969U+1F53
1970U+1F54
1971U+1F55
1972U+1F56
1973U+1F57
1974U+1F59
1975U+1F5B
1976U+1F5D
1977U+1F5F
1978U+1F60
1979U+1F61
1980U+1F62
1981U+1F63
1982U+1F64
1983U+1F65
1984U+1F66
1985U+1F67
1986U+1F68
1987U+1F69
1988U+1F6A
1989U+1F6B
1990U+1F6C
1991U+1F6D
1992U+1F6E
1993U+1F6F
1994U+1F70
1995U+1F71
1996U+1F72
1997U+1F73
1998U+1F74
1999U+1F75
2000U+1F76
2001U+1F77
2002U+1F78
2003U+1F79
2004U+1F7A
2005U+1F7B
2006U+1F7C
2007U+1F7D
2008U+1F80
2009U+1F81
2010U+1F82
2011U+1F83
2012U+1F84
2013U+1F85
2014U+1F86
2015U+1F87
2016U+1F88
2017U+1F89
2018U+1F8A
2019U+1F8B
2020U+1F8C
2021U+1F8D
2022U+1F8E
2023U+1F8F
2024U+1F90
2025U+1F91
2026U+1F92
2027U+1F93
2028U+1F94
2029U+1F95
2030U+1F96
2031U+1F97
2032U+1F98
2033U+1F99
2034U+1F9A
2035U+1F9B
2036U+1F9C
2037U+1F9D
2038U+1F9E
2039U+1F9F
2040U+1FA0
2041U+1FA1
2042U+1FA2
2043U+1FA3
2044U+1FA4
2045U+1FA5
2046U+1FA6
2047U+1FA7
2048U+1FA8
2049U+1FA9
2050U+1FAA
2051U+1FAB
2052U+1FAC
2053U+1FAD
2054U+1FAE
2055U+1FAF
2056U+1FB0
2057U+1FB1
2058U+1FB2
2059U+1FB3
2060U+1FB4
2061U+1FB6
2062U+1FB7
2063U+1FB8
2064U+1FB9
2065U+1FBA
2066U+1FBB
2067U+1FBC
2068U+1FBD
2069U+1FBE
2070U+1FBF
2071U+1FC0
2072U+1FC1
2073U+1FC2
2074U+1FC3
2075U+1FC4
2076U+1FC6
2077U+1FC7
2078U+1FC8
2079U+1FC9
2080U+1FCA
2081U+1FCB
2082U+1FCC
2083U+1FCD
2084U+1FCE
2085U+1FCF
2086U+1FD0
2087U+1FD1
2088U+1FD2
2089U+1FD3
2090U+1FD6
2091U+1FD7
2092U+1FD8
2093U+1FD9
2094U+1FDA
2095U+1FDB
2096U+1FDD
2097U+1FDE
2098U+1FDF
2099U+1FE0
2100U+1FE1
2101U+1FE2
2102U+1FE3
2103U+1FE4
2104U+1FE5
2105U+1FE6
2106U+1FE7
2107U+1FE8
2108U+1FE9
2109U+1FEA
2110U+1FEB
2111U+1FEC
2112U+1FED
2113U+1FEE
2114U+1FEF
2115U+1FF2
2116U+1FF3
2117U+1FF4
2118U+1FF6
2119U+1FF7
2120U+1FF8
2121U+1FF9
2122U+1FFA
2123U+1FFB
2124U+1FFC
2125U+1FFD
2126U+1FFE
2127U+2000
2128U+2001
2129U+2002
2130U+2003
2131U+2004
2132U+2005
2133U+2006
2134U+2007
2135U+2008
2136U+2009
2137U+200A
2138U+200B
2139U+200C
2140U+200D
2141U+2010
2142U+2011
2143U+2012
2144U+2013
2145U+2014
2146U+2015
2147U+2016
2148U+2017
2149U+2018
2150U+2019
2151U+201A
2152U+201B
2153U+201C
2154U+201D
2155U+201E
2156U+201F
2157U+2020
2158U+2021
2159U+2022
2160U+2023
2161U+2024
2162U+2025
2163U+2026
2164U+2027
2165U+2028
2166U+2029
2167U+202F
2168U+2030
2169U+2031
2170U+2032
2171U+2033
2172U+2034
2173U+2035
2174U+2036
2175U+2037
2176U+2038
2177U+2039
2178U+203A
2179U+203B
2180U+203C
2181U+203D
2182U+203E
2183U+203F
2184U+2040
2185U+2041
2186U+2042
2187U+2043
2188U+2044
2189U+2045
2190U+2046
2191U+2047
2192U+2048
2193U+2049
2194U+204A
2195U+204B
2196U+204C
2197U+204D
2198U+204E
2199U+204F
2200U+2050
2201U+2051
2202U+2052
2203U+2053
2204U+2054
2205U+2055
2206U+2056
2207U+2057
2208U+2058
2209U+2059
2210U+205A
2211U+205B
2212U+205C
2213U+205D
2214U+205E
2215U+205F
2216U+2060
2217U+2061
2218U+2062
2219U+2063
2220U+2064
2221U+2070
2222U+2071
2223U+2074
2224U+2075
2225U+2076
2226U+2077
2227U+2078
2228U+2079
2229U+207A
2230U+207B
2231U+207C
2232U+207D
2233U+207E
2234U+207F
2235U+2080
2236U+2081
2237U+2082
2238U+2083
2239U+2084
2240U+2085
2241U+2086
2242U+2087
2243U+2088
2244U+2089
2245U+208A
2246U+208B
2247U+208C
2248U+208D
2249U+208E
2250U+2090
2251U+2091
2252U+2092
2253U+2093
2254U+2094
2255U+2095
2256U+2096
2257U+2097
2258U+2098
2259U+2099
2260U+209A
2261U+209B
2262U+209C
2263U+20A1
2264U+20A3
2265U+20A4
2266U+20A5
2267U+20A6
2268U+20A7
2269U+20A8
2270U+20A9
2271U+20AA
2272U+20AB
2273U+20AC
2274U+20AD
2275U+20AE
2276U+20AF
2277U+20B1
2278U+20B2
2279U+20B3
2280U+20B4
2281U+20B5
2282U+20B8
2283U+20B9
2284U+20BA
2285U+20BC
2286U+20BD
2287U+20BE
2288U+20BF
2289U+20C0
2290U+20D0
2291U+20D1
2292U+20D2
2293U+20D3
2294U+20D6
2295U+20D7
2296U+20D8
2297U+20DB
2298U+20DC
2299U+20DD
2300U+20E1
2301U+20E5
2302U+20E6
2303U+20E8
2304U+20E9
2305U+20EB
2306U+20EC
2307U+20ED
2308U+20EE
2309U+20EF
2310U+20F0
2311U+2100
2312U+2101
2313U+2102
2314U+2103
2315U+2105
2316U+2106
2317U+2107
2318U+2108
2319U+2109
2320U+210C
2321U+210D
2322U+210E
2323U+210F
2324U+2111
2325U+2113
2326U+2115
2327U+2116
2328U+2117
2329U+2118
2330U+2119
2331U+211A
2332U+211C
2333U+211D
2334U+2120
2335U+2121
2336U+2122
2337U+2124
2338U+2126
2339U+2127
2340U+2128
2341U+2129
2342U+212A
2343U+212B
2344U+212D
2345U+212E
2346U+2132
2347U+2135
2348U+2136
2349U+2137
2350U+2138
2351U+2139
2352U+213A
2353U+213B
2354U+213C
2355U+213D
2356U+213E
2357U+213F
2358U+2140
2359U+2141
2360U+2142
2361U+2143
2362U+2144
2363U+2145
2364U+2146
2365U+2147
2366U+2148
2367U+2149
2368U+214B
2369U+214D
2370U+214E
2371U+214F
2372U+2150
2373U+2151
2374U+2152
2375U+2153
2376U+2154
2377U+2155
2378U+2156
2379U+2157
2380U+2158
2381U+2159
2382U+215A
2383U+215B
2384U+215C
2385U+215D
2386U+215E
2387U+215F
2388U+2160
2389U+2161
2390U+2162
2391U+2163
2392U+2164
2393U+2165
2394U+2166
2395U+2167
2396U+2168
2397U+2169
2398U+216A
2399U+216B
2400U+216C
2401U+216D
2402U+216E
2403U+216F
2404U+2170
2405U+2171
2406U+2172
2407U+2173
2408U+2174
2409U+2175
2410U+2176
2411U+2177
2412U+2178
2413U+2179
2414U+217A
2415U+217B
2416U+217C
2417U+217D
2418U+217E
2419U+217F
2420U+2180
2421U+2181
2422U+2182
2423U+2183
2424U+2184
2425U+2185
2426U+2186
2427U+2187
2428U+2188
2429U+2189
2430U+218A
2431U+218B
2432U+2190
2433U+2191
2434U+2192
2435U+2193
2436U+2194
2437U+2195
2438U+2196
2439U+2197
2440U+2198
2441U+2199
2442U+219A
2443U+219B
2444U+219C
2445U+219D
2446U+219E
2447U+219F
2448U+21A0
2449U+21A1
2450U+21A2
2451U+21A3
2452U+21A4
2453U+21A5
2454U+21A6
2455U+21A7
2456U+21A8
2457U+21A9
2458U+21AA
2459U+21AB
2460U+21AC
2461U+21AD
2462U+21AE
2463U+21AF
2464U+21B0
2465U+21B1
2466U+21B2
2467U+21B3
2468U+21B4
2469U+21B5
2470U+21B6
2471U+21B7
2472U+21B8
2473U+21B9
2474U+21BA
2475U+21BB
2476U+21BC
2477U+21BD
2478U+21BE
2479U+21BF
2480U+21C0
2481U+21C1
2482U+21C2
2483U+21C3
2484U+21C4
2485U+21C5
2486U+21C6
2487U+21C7
2488U+21C8
2489U+21C9
2490U+21CA
2491U+21CB
2492U+21CC
2493U+21CD
2494U+21CE
2495U+21CF
2496U+21D0
2497U+21D1
2498U+21D2
2499U+21D3
2500U+21D4
2501U+21D5
2502U+21D6
2503U+21D7
2504U+21D8
2505U+21D9
2506U+21DA
2507U+21DB
2508U+21DC
2509U+21DD
2510U+21DE
2511U+21DF
2512U+21E0
2513U+21E1
2514U+21E2
2515U+21E3
2516U+21E4
2517U+21E5
2518U+21E6
2519U+21E7
2520U+21E8
2521U+21E9
2522U+21EA
2523U+21EB
2524U+21EC
2525U+21ED
2526U+21EE
2527U+21EF
2528U+21F0
2529U+21F1
2530U+21F2
2531U+21F3
2532U+21F4
2533U+21F5
2534U+21F6
2535U+21F7
2536U+21F8
2537U+21F9
2538U+21FA
2539U+21FB
2540U+21FC
2541U+21FD
2542U+21FE
2543U+21FF
2544U+2200
2545U+2201
2546U+2202
2547U+2203
2548U+2204
2549U+2205
2550U+2206
2551U+2207
2552U+2208
2553U+2209
2554U+220A
2555U+220B
2556U+220C
2557U+220D
2558U+220E
2559U+220F
2560U+2210
2561U+2211
2562U+2212
2563U+2213
2564U+2214
2565U+2215
2566U+2216
2567U+2217
2568U+2218
2569U+2219
2570U+221A
2571U+221B
2572U+221C
2573U+221D
2574U+221E
2575U+221F
2576U+2220
2577U+2221
2578U+2222
2579U+2223
2580U+2224
2581U+2225
2582U+2226
2583U+2227
2584U+2228
2585U+2229
2586U+222A
2587U+222B
2588U+222C
2589U+222D
2590U+222E
2591U+222F
2592U+2230
2593U+2231
2594U+2232
2595U+2233
2596U+2234
2597U+2235
2598U+2236
2599U+2237
2600U+2238
2601U+2239
2602U+223A
2603U+223B
2604U+223C
2605U+223D
2606U+223E
2607U+223F
2608U+2240
2609U+2241
2610U+2242
2611U+2243
2612U+2244
2613U+2245
2614U+2246
2615U+2247
2616U+2248
2617U+2249
2618U+224A
2619U+224B
2620U+224C
2621U+224D
2622U+224E
2623U+224F
2624U+2250
2625U+2251
2626U+2252
2627U+2253
2628U+2254
2629U+2255
2630U+2256
2631U+2257
2632U+2258
2633U+2259
2634U+225A
2635U+225B
2636U+225C
2637U+225D
2638U+225E
2639U+225F
2640U+2260
2641U+2261
2642U+2262
2643U+2263
2644U+2264
2645U+2265
2646U+2266
2647U+2267
2648U+2268
2649U+2269
2650U+226A
2651U+226B
2652U+226C
2653U+226D
2654U+226E
2655U+226F
2656U+2270
2657U+2271
2658U+2272
2659U+2273
2660U+2274
2661U+2275
2662U+2276
2663U+2277
2664U+2278
2665U+2279
2666U+227A
2667U+227B
2668U+227C
2669U+227D
2670U+227E
2671U+227F
2672U+2280
2673U+2281
2674U+2282
2675U+2283
2676U+2284
2677U+2285
2678U+2286
2679U+2287
2680U+2288
2681U+2289
2682U+228A
2683U+228B
2684U+228C
2685U+228D
2686U+228E
2687U+228F
2688U+2290
2689U+2291
2690U+2292
2691U+2293
2692U+2294
2693U+2295
2694U+2296
2695U+2297
2696U+2298
2697U+2299
2698U+229A
2699U+229B
2700U+229C
2701U+229D
2702U+229E
2703U+229F
2704U+22A0
2705U+22A1
2706U+22A2
2707U+22A3
2708U+22A4
2709U+22A5
2710U+22A6
2711U+22A7
2712U+22A8
2713U+22A9
2714U+22AA
2715U+22AB
2716U+22AC
2717U+22AD
2718U+22AE
2719U+22AF
2720U+22B0
2721U+22B1
2722U+22B2
2723U+22B3
2724U+22B4
2725U+22B5
2726U+22B6
2727U+22B7
2728U+22B8
2729U+22B9
2730U+22BA
2731U+22BB
2732U+22BC
2733U+22BD
2734U+22BE
2735U+22BF
2736U+22C0
2737U+22C1
2738U+22C2
2739U+22C3
2740U+22C4
2741U+22C5
2742U+22C6
2743U+22C7
2744U+22C8
2745U+22C9
2746U+22CA
2747U+22CB
2748U+22CC
2749U+22CD
2750U+22CE
2751U+22CF
2752U+22D0
2753U+22D1
2754U+22D2
2755U+22D3
2756U+22D4
2757U+22D5
2758U+22D6
2759U+22D7
2760U+22D8
2761U+22D9
2762U+22DA
2763U+22DB
2764U+22DC
2765U+22DD
2766U+22DE
2767U+22DF
2768U+22E0
2769U+22E1
2770U+22E2
2771U+22E3
2772U+22E4
2773U+22E5
2774U+22E6
2775U+22E7
2776U+22E8
2777U+22E9
2778U+22EA
2779U+22EB
2780U+22EC
2781U+22ED
2782U+22EE
2783U+22EF
2784U+22F0
2785U+22F1
2786U+22F2
2787U+22F3
2788U+22F4
2789U+22F5
2790U+22F6
2791U+22F7
2792U+22F8
2793U+22F9
2794U+22FA
2795U+22FB
2796U+22FC
2797U+22FD
2798U+22FE
2799U+22FF
2800U+2300
2801U+2301
2802U+2302
2803U+2303
2804U+2304
2805U+2305
2806U+2306
2807U+2307
2808U+2308
2809U+2309
2810U+230A
2811U+230B
2812U+230C
2813U+230D
2814U+230E
2815U+230F
2816U+2310
2817U+2311
2818U+2312
2819U+2313
2820U+2314
2821U+2315
2822U+2316
2823U+2317
2824U+2318
2825U+2319
2826U+231A
2827U+231B
2828U+231C
2829U+231D
2830U+231E
2831U+231F
2832U+2320
2833U+2321
2834U+2322
2835U+2323
2836U+2324
2837U+2325
2838U+2326
2839U+2327
2840U+2328
2841U+2329
2842U+232A
2843U+232B
2844U+232D
2845U+232E
2846U+232F
2847U+2330
2848U+2331
2849U+2332
2850U+2333
2851U+2334
2852U+2335
2853U+2336
2854U+2337
2855U+2338
2856U+2339
2857U+233A
2858U+233B
2859U+233C
2860U+233D
2861U+233E
2862U+233F
2863U+2340
2864U+2341
2865U+2342
2866U+2343
2867U+2344
2868U+2345
2869U+2346
2870U+2347
2871U+2348
2872U+2349
2873U+234A
2874U+234B
2875U+234C
2876U+234D
2877U+234E
2878U+234F
2879U+2350
2880U+2351
2881U+2352
2882U+2353
2883U+2354
2884U+2355
2885U+2356
2886U+2357
2887U+2358
2888U+2359
2889U+235A
2890U+235B
2891U+235C
2892U+235D
2893U+235E
2894U+235F
2895U+2360
2896U+2361
2897U+2362
2898U+2363
2899U+2364
2900U+2365
2901U+2366
2902U+2367
2903U+2368
2904U+2369
2905U+236A
2906U+236B
2907U+236C
2908U+236D
2909U+236E
2910U+236F
2911U+2370
2912U+2371
2913U+2372
2914U+2373
2915U+2374
2916U+2375
2917U+2376
2918U+2377
2919U+2378
2920U+2379
2921U+237A
2922U+237B
2923U+237D
2924U+237E
2925U+237F
2926U+2380
2927U+2381
2928U+2382
2929U+2383
2930U+2384
2931U+2385
2932U+2386
2933U+2387
2934U+2388
2935U+2389
2936U+238A
2937U+238B
2938U+238D
2939U+2394
2940U+2395
2941U+239B
2942U+239C
2943U+239D
2944U+239E
2945U+239F
2946U+23A0
2947U+23A1
2948U+23A2
2949U+23A3
2950U+23A4
2951U+23A5
2952U+23A6
2953U+23A7
2954U+23A8
2955U+23A9
2956U+23AA
2957U+23AB
2958U+23AC
2959U+23AD
2960U+23AE
2961U+23B0
2962U+23B1
2963U+23B2
2964U+23B3
2965U+23B8
2966U+23B9
2967U+23BA
2968U+23BB
2969U+23BC
2970U+23BD
2971U+23BE
2972U+23BF
2973U+23C0
2974U+23C1
2975U+23C2
2976U+23C3
2977U+23C4
2978U+23C5
2979U+23C6
2980U+23C7
2981U+23C8
2982U+23C9
2983U+23CA
2984U+23CB
2985U+23CC
2986U+23CE
2987U+23CF
2988U+23DA
2989U+23DB
2990U+23E2
2991U+23E4
2992U+23E5
2993U+23E8
2994U+23E9
2995U+23EA
2996U+23EB
2997U+23EC
2998U+23ED
2999U+23EE
3000U+23EF
3001U+23F0
3002U+23F1
3003U+23F2
3004U+23F3
3005U+23F4
3006U+23F5
3007U+23F6
3008U+23F7
3009U+23F8
3010U+23F9
3011U+23FA
3012U+23FB
3013U+23FC
3014U+23FD
3015U+23FE
3016U+23FF
3017U+2400
3018U+2401
3019U+2402
3020U+2403
3021U+2404
3022U+2405
3023U+2406
3024U+2407
3025U+2408
3026U+2409
3027U+240A
3028U+240B
3029U+240C
3030U+240D
3031U+240E
3032U+240F
3033U+2410
3034U+2411
3035U+2412
3036U+2413
3037U+2414
3038U+2415
3039U+2416
3040U+2417
3041U+2418
3042U+2419
3043U+241A
3044U+241B
3045U+241C
3046U+241D
3047U+241E
3048U+241F
3049U+2420
3050U+2421
3051U+2422
3052U+2423
3053U+2424
3054U+2425
3055U+2426
3056U+2427
3057U+2428
3058U+2429
3059U+2440
3060U+2441
3061U+2442
3062U+2443
3063U+2444
3064U+2445
3065U+2460
3066U+2461
3067U+2462
3068U+2463
3069U+2464
3070U+2465
3071U+2466
3072U+2467
3073U+2468
3074U+2469
3075U+246A
3076U+246B
3077U+246C
3078U+246D
3079U+246E
3080U+246F
3081U+2470
3082U+2471
3083U+2472
3084U+2473
3085U+2474
3086U+2475
3087U+2476
3088U+2477
3089U+2478
3090U+2479
3091U+247A
3092U+247B
3093U+247C
3094U+247D
3095U+247E
3096U+247F
3097U+2480
3098U+2481
3099U+2482
3100U+2483
3101U+2484
3102U+2485
3103U+2486
3104U+2487
3105U+2488
3106U+2489
3107U+248A
3108U+248B
3109U+248C
3110U+248D
3111U+248E
3112U+248F
3113U+2490
3114U+2491
3115U+2492
3116U+2493
3117U+2494
3118U+2495
3119U+2496
3120U+2497
3121U+2498
3122U+2499
3123U+249A
3124U+249B
3125U+249C
3126U+249D
3127U+249E
3128U+249F
3129U+24A0
3130U+24A1
3131U+24A2
3132U+24A3
3133U+24A4
3134U+24A5
3135U+24A6
3136U+24A7
3137U+24A8
3138U+24A9
3139U+24AA
3140U+24AB
3141U+24AC
3142U+24AD
3143U+24AE
3144U+24AF
3145U+24B0
3146U+24B1
3147U+24B2
3148U+24B3
3149U+24B4
3150U+24B5
3151U+24B6
3152U+24B7
3153U+24B8
3154U+24B9
3155U+24BA
3156U+24BB
3157U+24BC
3158U+24BD
3159U+24BE
3160U+24BF
3161U+24C0
3162U+24C1
3163U+24C2
3164U+24C3
3165U+24C4
3166U+24C5
3167U+24C6
3168U+24C7
3169U+24C8
3170U+24C9
3171U+24CA
3172U+24CB
3173U+24CC
3174U+24CD
3175U+24CE
3176U+24CF
3177U+24D0
3178U+24D1
3179U+24D2
3180U+24D3
3181U+24D4
3182U+24D5
3183U+24D6
3184U+24D7
3185U+24D8
3186U+24D9
3187U+24DA
3188U+24DB
3189U+24DC
3190U+24DD
3191U+24DE
3192U+24DF
3193U+24E0
3194U+24E1
3195U+24E2
3196U+24E3
3197U+24E4
3198U+24E5
3199U+24E6
3200U+24E7
3201U+24E8
3202U+24E9
3203U+24EA
3204U+24EB
3205U+24EC
3206U+24ED
3207U+24EE
3208U+24EF
3209U+24F0
3210U+24F1
3211U+24F2
3212U+24F3
3213U+24F4
3214U+24F5
3215U+24F6
3216U+24F7
3217U+24F8
3218U+24F9
3219U+24FA
3220U+24FB
3221U+24FC
3222U+24FD
3223U+24FE
3224U+24FF
3225U+2500
3226U+2501
3227U+2502
3228U+2503
3229U+2504
3230U+2505
3231U+2506
3232U+2507
3233U+2508
3234U+2509
3235U+250A
3236U+250B
3237U+250C
3238U+250D
3239U+250E
3240U+250F
3241U+2510
3242U+2511
3243U+2512
3244U+2513
3245U+2514
3246U+2515
3247U+2516
3248U+2517
3249U+2518
3250U+2519
3251U+251A
3252U+251B
3253U+251C
3254U+251D
3255U+251E
3256U+251F
3257U+2520
3258U+2521
3259U+2522
3260U+2523
3261U+2524
3262U+2525
3263U+2526
3264U+2527
3265U+2528
3266U+2529
3267U+252A
3268U+252B
3269U+252C
3270U+252D
3271U+252E
3272U+252F
3273U+2530
3274U+2531
3275U+2532
3276U+2533
3277U+2534
3278U+2535
3279U+2536
3280U+2537
3281U+2538
3282U+2539
3283U+253A
3284U+253B
3285U+253C
3286U+253D
3287U+253E
3288U+253F
3289U+2540
3290U+2541
3291U+2542
3292U+2543
3293U+2544
3294U+2545
3295U+2546
3296U+2547
3297U+2548
3298U+2549
3299U+254A
3300U+254B
3301U+254C
3302U+254D
3303U+254E
3304U+254F
3305U+2550
3306U+2551
3307U+2552
3308U+2553
3309U+2554
3310U+2555
3311U+2556
3312U+2557
3313U+2558
3314U+2559
3315U+255A
3316U+255B
3317U+255C
3318U+255D
3319U+255E
3320U+255F
3321U+2560
3322U+2561
3323U+2562
3324U+2563
3325U+2564
3326U+2565
3327U+2566
3328U+2567
3329U+2568
3330U+2569
3331U+256A
3332U+256B
3333U+256C
3334U+256D
3335U+256E
3336U+256F
3337U+2570
3338U+2571
3339U+2572
3340U+2573
3341U+2574
3342U+2575
3343U+2576
3344U+2577
3345U+2578
3346U+2579
3347U+257A
3348U+257B
3349U+257C
3350U+257D
3351U+257E
3352U+257F
3353U+2580
3354U+2581
3355U+2582
3356U+2583
3357U+2584
3358U+2585
3359U+2586
3360U+2587
3361U+2588
3362U+2589
3363U+258A
3364U+258B
3365U+258C
3366U+258D
3367U+258E
3368U+258F
3369U+2590
3370U+2591
3371U+2592
3372U+2593
3373U+2594
3374U+2595
3375U+2596
3376U+2597
3377U+2598
3378U+2599
3379U+259A
3380U+259B
3381U+259C
3382U+259D
3383U+259E
3384U+259F
3385U+25A0
3386U+25A1
3387U+25A2
3388U+25A3
3389U+25A4
3390U+25A5
3391U+25A6
3392U+25A7
3393U+25A8
3394U+25A9
3395U+25AA
3396U+25AB
3397U+25AC
3398U+25AD
3399U+25AE
3400U+25AF
3401U+25B0
3402U+25B1
3403U+25B2
3404U+25B3
3405U+25B4
3406U+25B5
3407U+25B6
3408U+25B7
3409U+25B8
3410U+25B9
3411U+25BA
3412U+25BB
3413U+25BC
3414U+25BD
3415U+25BE
3416U+25BF
3417U+25C0
3418U+25C1
3419U+25C2
3420U+25C3
3421U+25C4
3422U+25C5
3423U+25C6
3424U+25C7
3425U+25C8
3426U+25C9
3427U+25CA
3428U+25CB
3429U+25CC
3430U+25CD
3431U+25CE
3432U+25CF
3433U+25D0
3434U+25D1
3435U+25D2
3436U+25D3
3437U+25D4
3438U+25D5
3439U+25D6
3440U+25D7
3441U+25D8
3442U+25D9
3443U+25DA
3444U+25DB
3445U+25DC
3446U+25DD
3447U+25DE
3448U+25DF
3449U+25E0
3450U+25E1
3451U+25E2
3452U+25E3
3453U+25E4
3454U+25E5
3455U+25E6
3456U+25E7
3457U+25E8
3458U+25E9
3459U+25EA
3460U+25EB
3461U+25EC
3462U+25ED
3463U+25EE
3464U+25EF
3465U+25F0
3466U+25F1
3467U+25F2
3468U+25F3
3469U+25F4
3470U+25F5
3471U+25F6
3472U+25F7
3473U+25F8
3474U+25F9
3475U+25FA
3476U+25FB
3477U+25FC
3478U+25FD
3479U+25FE
3480U+25FF
3481U+2600
3482U+2605
3483U+2606
3484U+2607
3485U+2609
3486U+2610
3487U+2611
3488U+2612
3489U+2613
3490U+261C
3491U+261D
3492U+261E
3493U+261F
3494U+2621
3495U+2626
3496U+2628
3497U+2630
3498U+2631
3499U+2632
3500U+2633
3501U+2634
3502U+2635
3503U+2636
3504U+2637
3505U+2639
3506U+263A
3507U+263B
3508U+263C
3509U+263F
3510U+2640
3511U+2641
3512U+2642
3513U+2660
3514U+2661
3515U+2662
3516U+2663
3517U+2664
3518U+2665
3519U+2666
3520U+2667
3521U+2669
3522U+266A
3523U+266B
3524U+266C
3525U+266D
3526U+266E
3527U+266F
3528U+267E
3529U+2680
3530U+2681
3531U+2682
3532U+2683
3533U+2684
3534U+2685
3535U+2686
3536U+2687
3537U+2688
3538U+2689
3539U+268A
3540U+268B
3541U+268C
3542U+268D
3543U+268E
3544U+268F
3545U+2690
3546U+2691
3547U+2699
3548U+269B
3549U+26A0
3550U+26A1
3551U+26AA
3552U+26AB
3553U+26AC
3554U+26AD
3555U+26AE
3556U+26AF
3557U+26B2
3558U+26CB
3559U+26DA
3560U+26DD
3561U+26DE
3562U+26ED
3563U+26EE
3564U+26EF
3565U+26F6
3566U+2705
3567U+2713
3568U+2714
3569U+2715
3570U+2716
3571U+2717
3572U+2718
3573U+271A
3574U+271B
3575U+271C
3576U+271D
3577U+2720
3578U+2721
3579U+2722
3580U+2723
3581U+2724
3582U+2725
3583U+2726
3584U+2727
3585U+2731
3586U+2732
3587U+2733
3588U+2734
3589U+2736
3590U+2737
3591U+2738
3592U+2739
3593U+273A
3594U+273B
3595U+273C
3596U+273D
3597U+273E
3598U+273F
3599U+2740
3600U+2741
3601U+2742
3602U+2743
3603U+2744
3604U+2745
3605U+2746
3606U+2747
3607U+2748
3608U+2749
3609U+274A
3610U+274B
3611U+274C
3612U+274E
3613U+2756
3614U+2758
3615U+2759
3616U+275A
3617U+2761
3618U+2764
3619U+276C
3620U+276D
3621U+276E
3622U+276F
3623U+2770
3624U+2771
3625U+2776
3626U+2777
3627U+2778
3628U+2779
3629U+277A
3630U+277B
3631U+277C
3632U+277D
3633U+277E
3634U+277F
3635U+2780
3636U+2781
3637U+2782
3638U+2783
3639U+2784
3640U+2785
3641U+2786
3642U+2787
3643U+2788
3644U+2789
3645U+278A
3646U+278B
3647U+278C
3648U+278D
3649U+278E
3650U+278F
3651U+2790
3652U+2791
3653U+2792
3654U+2793
3655U+2794
3656U+279C
3657U+279D
3658U+279E
3659U+27A1
3660U+27A2
3661U+27A3
3662U+27A4
3663U+27A5
3664U+27A6
3665U+27A7
3666U+27B0
3667U+27BF
3668U+27C2
3669U+27CA
3670U+27CB
3671U+27CD
3672U+27D2
3673U+27D3
3674U+27D4
3675U+27D5
3676U+27D6
3677U+27D7
3678U+27D8
3679U+27D9
3680U+27DA
3681U+27DB
3682U+27DC
3683U+27DF
3684U+27E0
3685U+27E1
3686U+27E6
3687U+27E7
3688U+27E8
3689U+27E9
3690U+27EA
3691U+27EB
3692U+27EE
3693U+27EF
3694U+27F0
3695U+27F1
3696U+27F2
3697U+27F3
3698U+27F4
3699U+2800
3700U+2801
3701U+2802
3702U+2803
3703U+2804
3704U+2805
3705U+2806
3706U+2807
3707U+2808
3708U+2809
3709U+280A
3710U+280B
3711U+280C
3712U+280D
3713U+280E
3714U+280F
3715U+2810
3716U+2811
3717U+2812
3718U+2813
3719U+2814
3720U+2815
3721U+2816
3722U+2817
3723U+2818
3724U+2819
3725U+281A
3726U+281B
3727U+281C
3728U+281D
3729U+281E
3730U+281F
3731U+2820
3732U+2821
3733U+2822
3734U+2823
3735U+2824
3736U+2825
3737U+2826
3738U+2827
3739U+2828
3740U+2829
3741U+282A
3742U+282B
3743U+282C
3744U+282D
3745U+282E
3746U+282F
3747U+2830
3748U+2831
3749U+2832
3750U+2833
3751U+2834
3752U+2835
3753U+2836
3754U+2837
3755U+2838
3756U+2839
3757U+283A
3758U+283B
3759U+283C
3760U+283D
3761U+283E
3762U+283F
3763U+2840
3764U+2841
3765U+2842
3766U+2843
3767U+2844
3768U+2845
3769U+2846
3770U+2847
3771U+2848
3772U+2849
3773U+284A
3774U+284B
3775U+284C
3776U+284D
3777U+284E
3778U+284F
3779U+2850
3780U+2851
3781U+2852
3782U+2853
3783U+2854
3784U+2855
3785U+2856
3786U+2857
3787U+2858
3788U+2859
3789U+285A
3790U+285B
3791U+285C
3792U+285D
3793U+285E
3794U+285F
3795U+2860
3796U+2861
3797U+2862
3798U+2863
3799U+2864
3800U+2865
3801U+2866
3802U+2867
3803U+2868
3804U+2869
3805U+286A
3806U+286B
3807U+286C
3808U+286D
3809U+286E
3810U+286F
3811U+2870
3812U+2871
3813U+2872
3814U+2873
3815U+2874
3816U+2875
3817U+2876
3818U+2877
3819U+2878
3820U+2879
3821U+287A
3822U+287B
3823U+287C
3824U+287D
3825U+287E
3826U+287F
3827U+2880
3828U+2881
3829U+2882
3830U+2883
3831U+2884
3832U+2885
3833U+2886
3834U+2887
3835U+2888
3836U+2889
3837U+288A
3838U+288B
3839U+288C
3840U+288D
3841U+288E
3842U+288F
3843U+2890
3844U+2891
3845U+2892
3846U+2893
3847U+2894
3848U+2895
3849U+2896
3850U+2897
3851U+2898
3852U+2899
3853U+289A
3854U+289B
3855U+289C
3856U+289D
3857U+289E
3858U+289F
3859U+28A0
3860U+28A1
3861U+28A2
3862U+28A3
3863U+28A4
3864U+28A5
3865U+28A6
3866U+28A7
3867U+28A8
3868U+28A9
3869U+28AA
3870U+28AB
3871U+28AC
3872U+28AD
3873U+28AE
3874U+28AF
3875U+28B0
3876U+28B1
3877U+28B2
3878U+28B3
3879U+28B4
3880U+28B5
3881U+28B6
3882U+28B7
3883U+28B8
3884U+28B9
3885U+28BA
3886U+28BB
3887U+28BC
3888U+28BD
3889U+28BE
3890U+28BF
3891U+28C0
3892U+28C1
3893U+28C2
3894U+28C3
3895U+28C4
3896U+28C5
3897U+28C6
3898U+28C7
3899U+28C8
3900U+28C9
3901U+28CA
3902U+28CB
3903U+28CC
3904U+28CD
3905U+28CE
3906U+28CF
3907U+28D0
3908U+28D1
3909U+28D2
3910U+28D3
3911U+28D4
3912U+28D5
3913U+28D6
3914U+28D7
3915U+28D8
3916U+28D9
3917U+28DA
3918U+28DB
3919U+28DC
3920U+28DD
3921U+28DE
3922U+28DF
3923U+28E0
3924U+28E1
3925U+28E2
3926U+28E3
3927U+28E4
3928U+28E5
3929U+28E6
3930U+28E7
3931U+28E8
3932U+28E9
3933U+28EA
3934U+28EB
3935U+28EC
3936U+28ED
3937U+28EE
3938U+28EF
3939U+28F0
3940U+28F1
3941U+28F2
3942U+28F3
3943U+28F4
3944U+28F5
3945U+28F6
3946U+28F7
3947U+28F8
3948U+28F9
3949U+28FA
3950U+28FB
3951U+28FC
3952U+28FD
3953U+28FE
3954U+28FF
3955U+2900
3956U+2901
3957U+2902
3958U+2903
3959U+2904
3960U+2905
3961U+2906
3962U+2907
3963U+2908
3964U+2909
3965U+290A
3966U+290B
3967U+290C
3968U+290D
3969U+290E
3970U+290F
3971U+2910
3972U+2911
3973U+2912
3974U+2913
3975U+2914
3976U+2915
3977U+2916
3978U+2917
3979U+2918
3980U+2919
3981U+291A
3982U+291B
3983U+291C
3984U+291D
3985U+291E
3986U+291F
3987U+2920
3988U+2921
3989U+2922
3990U+2923
3991U+2924
3992U+2925
3993U+2926
3994U+2933
3995U+2940
3996U+2941
3997U+2947
3998U+2948
3999U+294A
4000U+294B
4001U+294C
4002U+294D
4003U+294E
4004U+294F
4005U+2950
4006U+2951
4007U+2952
4008U+2953
4009U+2954
4010U+2955
4011U+2956
4012U+2957
4013U+2958
4014U+2959
4015U+295A
4016U+295B
4017U+295C
4018U+295D
4019U+295E
4020U+295F
4021U+2960
4022U+2961
4023U+2962
4024U+2963
4025U+2964
4026U+2965
4027U+296E
4028U+296F
4029U+2980
4030U+2981
4031U+2982
4032U+2983
4033U+2984
4034U+2987
4035U+2988
4036U+2989
4037U+298A
4038U+2999
4039U+299A
4040U+29A2
4041U+29A3
4042U+29B0
4043U+29B1
4044U+29B2
4045U+29B3
4046U+29B4
4047U+29B5
4048U+29B6
4049U+29B7
4050U+29B8
4051U+29B9
4052U+29BA
4053U+29BB
4054U+29BC
4055U+29BD
4056U+29BE
4057U+29BF
4058U+29C4
4059U+29C5
4060U+29C6
4061U+29C7
4062U+29C8
4063U+29C9
4064U+29CA
4065U+29CB
4066U+29CD
4067U+29CE
4068U+29D1
4069U+29D2
4070U+29D3
4071U+29D4
4072U+29D5
4073U+29D6
4074U+29D7
4075U+29D8
4076U+29D9
4077U+29DA
4078U+29DB
4079U+29DF
4080U+29E2
4081U+29E3
4082U+29E4
4083U+29E5
4084U+29E6
4085U+29E7
4086U+29E8
4087U+29E9
4088U+29EB
4089U+29F4
4090U+29F5
4091U+29F6
4092U+29F7
4093U+29F8
4094U+29F9
4095U+29FA
4096U+29FB
4097U+2A00
4098U+2A01
4099U+2A02
4100U+2A05
4101U+2A06
4102U+2A09
4103U+2A11
4104U+2A12
4105U+2A13
4106U+2A14
4107U+2A15
4108U+2A16
4109U+2A1D
4110U+2A1E
4111U+2A1F
4112U+2A20
4113U+2A21
4114U+2A22
4115U+2A23
4116U+2A24
4117U+2A25
4118U+2A26
4119U+2A2A
4120U+2A2B
4121U+2A2C
4122U+2A2D
4123U+2A2E
4124U+2A2F
4125U+2A30
4126U+2A31
4127U+2A32
4128U+2A34
4129U+2A35
4130U+2A36
4131U+2A38
4132U+2A3C
4133U+2A3D
4134U+2A3E
4135U+2A3F
4136U+2A40
4137U+2A41
4138U+2A42
4139U+2A43
4140U+2A4C
4141U+2A4D
4142U+2A4E
4143U+2A4F
4144U+2A53
4145U+2A54
4146U+2A57
4147U+2A58
4148U+2A5A
4149U+2A5B
4150U+2A5C
4151U+2A5D
4152U+2A5E
4153U+2A5F
4154U+2A60
4155U+2A61
4156U+2A62
4157U+2A63
4158U+2A64
4159U+2A65
4160U+2A66
4161U+2A67
4162U+2A68
4163U+2A69
4164U+2A6A
4165U+2A6B
4166U+2A6C
4167U+2A6D
4168U+2A6E
4169U+2A6F
4170U+2A70
4171U+2A71
4172U+2A72
4173U+2A73
4174U+2A7D
4175U+2A7E
4176U+2A7F
4177U+2A80
4178U+2A85
4179U+2A86
4180U+2A87
4181U+2A88
4182U+2A89
4183U+2A8A
4184U+2A8D
4185U+2A8E
4186U+2A95
4187U+2A96
4188U+2A97
4189U+2A98
4190U+2A99
4191U+2A9A
4192U+2A9D
4193U+2A9E
4194U+2AAA
4195U+2AAB
4196U+2AAC
4197U+2AAD
4198U+2AAE
4199U+2AAF
4200U+2AB0
4201U+2AB1
4202U+2AB2
4203U+2AB3
4204U+2AB4
4205U+2AB5
4206U+2AB6
4207U+2AB7
4208U+2AB8
4209U+2AB9
4210U+2ABA
4211U+2ABD
4212U+2ABE
4213U+2ACF
4214U+2AD0
4215U+2AD1
4216U+2AD2
4217U+2AD9
4218U+2ADA
4219U+2ADB
4220U+2ADC
4221U+2ADD
4222U+2ADE
4223U+2ADF
4224U+2AE0
4225U+2AE2
4226U+2AE3
4227U+2AE4
4228U+2AE5
4229U+2AE6
4230U+2AE7
4231U+2AE8
4232U+2AE9
4233U+2AEA
4234U+2AEB
4235U+2AEC
4236U+2AED
4237U+2AEE
4238U+2AEF
4239U+2AF0
4240U+2AF1
4241U+2AF2
4242U+2AF3
4243U+2AF4
4244U+2AF5
4245U+2AF6
4246U+2AFC
4247U+2AFD
4248U+2B00
4249U+2B01
4250U+2B02
4251U+2B03
4252U+2B04
4253U+2B05
4254U+2B06
4255U+2B07
4256U+2B08
4257U+2B09
4258U+2B0A
4259U+2B0B
4260U+2B0C
4261U+2B0D
4262U+2B0E
4263U+2B0F
4264U+2B10
4265U+2B11
4266U+2B12
4267U+2B13
4268U+2B14
4269U+2B15
4270U+2B16
4271U+2B17
4272U+2B18
4273U+2B19
4274U+2B1A
4275U+2B1B
4276U+2B1C
4277U+2B1D
4278U+2B1E
4279U+2B1F
4280U+2B20
4281U+2B21
4282U+2B22
4283U+2B23
4284U+2B24
4285U+2B25
4286U+2B26
4287U+2B27
4288U+2B28
4289U+2B29
4290U+2B2A
4291U+2B2B
4292U+2B2C
4293U+2B2D
4294U+2B2E
4295U+2B2F
4296U+2B30
4297U+2B31
4298U+2B32
4299U+2B34
4300U+2B35
4301U+2B36
4302U+2B37
4303U+2B38
4304U+2B39
4305U+2B3A
4306U+2B3B
4307U+2B3C
4308U+2B3D
4309U+2B3E
4310U+2B3F
4311U+2B45
4312U+2B46
4313U+2B4D
4314U+2B50
4315U+2B51
4316U+2B52
4317U+2B53
4318U+2B54
4319U+2B55
4320U+2B58
4321U+2B59
4322U+2B60
4323U+2B61
4324U+2B62
4325U+2B63
4326U+2B64
4327U+2B65
4328U+2B66
4329U+2B67
4330U+2B68
4331U+2B69
4332U+2B6A
4333U+2B6B
4334U+2B6C
4335U+2B6D
4336U+2B6E
4337U+2B6F
4338U+2B70
4339U+2B71
4340U+2B72
4341U+2B73
4342U+2B7A
4343U+2B7B
4344U+2B7C
4345U+2B7D
4346U+2B7E
4347U+2B7F
4348U+2B80
4349U+2B81
4350U+2B82
4351U+2B83
4352U+2B84
4353U+2B85
4354U+2B86
4355U+2B87
4356U+2B8C
4357U+2B8D
4358U+2B8E
4359U+2B8F
4360U+2B90
4361U+2B91
4362U+2B92
4363U+2B93
4364U+2B94
4365U+2B95
4366U+2BA0
4367U+2BA1
4368U+2BA2
4369U+2BA3
4370U+2BA4
4371U+2BA5
4372U+2BA6
4373U+2BA7
4374U+2BB8
4375U+2BB9
4376U+2BBA
4377U+2BBB
4378U+2BBC
4379U+2BBD
4380U+2BBE
4381U+2BBF
4382U+2BC0
4383U+2BC1
4384U+2BC2
4385U+2BC3
4386U+2BC4
4387U+2BC5
4388U+2BC6
4389U+2BC7
4390U+2BC8
4391U+2BCA
4392U+2BCB
4393U+2BCC
4394U+2BCD
4395U+2BCE
4396U+2BCF
4397U+2BD0
4398U+2BD1
4399U+2BD2
4400U+2BE8
4401U+2BE9
4402U+2BEA
4403U+2BEB
4404U+2BEC
4405U+2BED
4406U+2BEE
4407U+2BEF
4408U+2BFA
4409U+2BFB
4410U+2BFC
4411U+2BFD
4412U+2BFE
4413U+2BFF
4414U+1FB00
4415U+1FB01
4416U+1FB02
4417U+1FB03
4418U+1FB04
4419U+1FB05
4420U+1FB06
4421U+1FB07
4422U+1FB08
4423U+1FB09
4424U+1FB0A
4425U+1FB0B
4426U+1FB0C
4427U+1FB0D
4428U+1FB0E
4429U+1FB0F
4430U+1FB10
4431U+1FB11
4432U+1FB12
4433U+1FB13
4434U+1FB14
4435U+1FB15
4436U+1FB16
4437U+1FB17
4438U+1FB18
4439U+1FB19
4440U+1FB1A
4441U+1FB1B
4442U+1FB1C
4443U+1FB1D
4444U+1FB1E
4445U+1FB1F
4446U+1FB20
4447U+1FB21
4448U+1FB22
4449U+1FB23
4450U+1FB24
4451U+1FB25
4452U+1FB26
4453U+1FB27
4454U+1FB28
4455U+1FB29
4456U+1FB2A
4457U+1FB2B
4458U+1FB2C
4459U+1FB2D
4460U+1FB2E
4461U+1FB2F
4462U+1FB30
4463U+1FB31
4464U+1FB32
4465U+1FB33
4466U+1FB34
4467U+1FB35
4468U+1FB36
4469U+1FB37
4470U+1FB38
4471U+1FB39
4472U+1FB3A
4473U+1FB3B
4474U+1FB3C
4475U+1FB3D
4476U+1FB3E
4477U+1FB3F
4478U+1FB40
4479U+1FB41
4480U+1FB42
4481U+1FB43
4482U+1FB44
4483U+1FB45
4484U+1FB46
4485U+1FB47
4486U+1FB48
4487U+1FB49
4488U+1FB4A
4489U+1FB4B
4490U+1FB4C
4491U+1FB4D
4492U+1FB4E
4493U+1FB4F
4494U+1FB50
4495U+1FB51
4496U+1FB52
4497U+1FB53
4498U+1FB54
4499U+1FB55
4500U+1FB56
4501U+1FB57
4502U+1FB58
4503U+1FB59
4504U+1FB5A
4505U+1FB5B
4506U+1FB5C
4507U+1FB5D
4508U+1FB5E
4509U+1FB5F
4510U+1FB60
4511U+1FB61
4512U+1FB62
4513U+1FB63
4514U+1FB64
4515U+1FB65
4516U+1FB66
4517U+1FB67
4518U+1FB68
4519U+1FB69
4520U+1FB6A
4521U+1FB6B
4522U+1FB6C
4523U+1FB6D
4524U+1FB6E
4525U+1FB6F
4526U+1FB70
4527U+1FB71
4528U+1FB72
4529U+1FB73
4530U+1FB74
4531U+1FB75
4532U+1FB76
4533U+1FB77
4534U+1FB78
4535U+1FB79
4536U+1FB7A
4537U+1FB7B
4538U+1FB7C
4539U+1FB7D
4540U+1FB7E
4541U+1FB7F
4542U+1FB80
4543U+1FB81
4544U+1FB82
4545U+1FB83
4546U+1FB84
4547U+1FB85
4548U+1FB86
4549U+1FB87
4550U+1FB88
4551U+1FB89
4552U+1FB8A
4553U+1FB8B
4554U+1FB8C
4555U+1FB8D
4556U+1FB8E
4557U+1FB8F
4558U+1FB90
4559U+1FB91
4560U+1FB92
4561U+1FB94
4562U+1FB95
4563U+1FB96
4564U+1FB97
4565U+1FB98
4566U+1FB99
4567U+1FB9A
4568U+1FB9B
4569U+1FB9C
4570U+1FB9D
4571U+1FB9E
4572U+1FB9F
4573U+1FBA0
4574U+1FBA1
4575U+1FBA2
4576U+1FBA3
4577U+1FBA4
4578U+1FBA5
4579U+1FBA6
4580U+1FBA7
4581U+1FBA8
4582U+1FBA9
4583U+1FBAA
4584U+1FBAB
4585U+1FBAC
4586U+1FBAD
4587U+1FBAE
4588U+1FBAF
4589U+1FBB0
4590U+1FBB1
4591U+1FBB2
4592U+1FBB3
4593U+1FBB4
4594U+1FBB5
4595U+1FBB6
4596U+1FBB7
4597U+1FBB8
4598U+1FBB9
4599U+1FBBA
4600U+1FBBB
4601U+1FBBC
4602U+1FBBD
4603U+1FBBE
4604U+1FBBF
4605U+1FBC0
4606U+1FBC1
4607U+1FBC2
4608U+1FBC3
4609U+1FBC4
4610U+1FBC5
4611U+1FBC6
4612U+1FBC7
4613U+1FBC8
4614U+1FBC9
4615U+1FBCA
4616U+1FBCC
4617U+1FBCE
4618U+1FBCF
4619U+1FBD0
4620U+1FBD1
4621U+1FBD2
4622U+1FBD3
4623U+1FBD4
4624U+1FBD5
4625U+1FBD6
4626U+1FBD7
4627U+1FBD8
4628U+1FBD9
4629U+1FBDA
4630U+1FBDB
4631U+1FBDC
4632U+1FBDD
4633U+1FBDE
4634U+1FBDF
4635U+1FBE0
4636U+1FBE1
4637U+1FBE2
4638U+1FBE3
4639U+1FBE4
4640U+1FBE5
4641U+1FBE6
4642U+1FBE7
4643U+1FBE8
4644U+1FBE9
4645U+1FBEA
4646U+1FBEB
4647U+1FBEC
4648U+1FBED
4649U+1FBEE
4650U+1FBEF
4651U+1FBF0
4652U+1FBF1
4653U+1FBF2
4654U+1FBF3
4655U+1FBF4
4656U+1FBF5
4657U+1FBF6
4658U+1FBF7
4659U+1FBF8
4660U+1FBF9
packages/terminal/fonts/README.md created+143
......@@ -0,0 +1,143 @@
1# terminal fonts
2
3## Primary font
4
5The four WOFF2 files are a reproducible subset of **Iosevka Fixed Extended
6v34.8.1** (Renzhi Li / Belleve Invis), taken from the release's own WebFont
7package:
8
9`https://github.com/be5invis/Iosevka/releases/download/v34.8.1/PkgWebFont-IosevkaFixed-34.8.1.zip`
10
11The downloaded zip is 211,996,653 bytes with SHA-256
12`a3a03a7ce96f92403c8fa0e20ab24642553755c8fcf69ed3c74157f1ffdf8995`. The four
13`TTF/` members it is subset from:
14
15| Source member | SHA-256 |
16| --------------------------------------------- | ------------------------------------------------------------------ |
17| `TTF/IosevkaFixed-Extended.ttf` | `7182070f18c093a65f4c7861aecb454b6a33bfcd365e07e3521f03a1d6c79cd7` |
18| `TTF/IosevkaFixed-ExtendedSemiBold.ttf` | `a4bebd5003e7763684c232660da6579293e903605c77e2f4cc1791618db51cf6` |
19| `TTF/IosevkaFixed-ExtendedItalic.ttf` | `0defbf681658b460c4b4d4a43ef3145798f2485cdde5e8c06663e8b7c08d8062` |
20| `TTF/IosevkaFixed-ExtendedSemiBoldItalic.ttf` | `f3006b75315f3e5af68d992f227d1cf55feb31f3c74156c70780638b737af61a` |
21
22The zip is acquisition material and must remain outside the repository.
23
24**Fixed Extended, and neither half of that is cosmetic.** _Extended_ is the
250.600 em advance — the same one JetBrains Mono v2.304 shipped before it, so the
26cell geometry did not move; Iosevka's default width is 0.500 em and reads
27compressed at terminal sizes. _Fixed_ is the variant with no glyph wider than
28one cell: `Iosevka Term` carries 23 double-advance glyphs (the long arrows,
29`U+27F5-27FF`), and Ghostty reports those codepoints as one column, so in a
30renderer that owns the cell they paint into their neighbour.
31
32| File | Upstream file | Style | Weight | Bytes | SHA-256 |
33| ---------------------------------------------------- | --------------------------------------------- | ------ | ------ | ------: | ------------------------------------------------------------------ |
34| `__clo_terminal_primary_34_8_1-Regular.woff2` | `TTF/IosevkaFixed-Extended.ttf` | normal | 400 | 186,152 | `4a45de347bcb9ed97220ca20d8e0a33d6d926995208800fa57138bd54a9273bb` |
35| `__clo_terminal_primary_34_8_1-SemiBold.woff2` | `TTF/IosevkaFixed-ExtendedSemiBold.ttf` | normal | 600 | 186,040 | `1883786f7f33c891724b6dcae9ae22dcbe69336fc73d34f27ff352410d27f259` |
36| `__clo_terminal_primary_34_8_1-Italic.woff2` | `TTF/IosevkaFixed-ExtendedItalic.ttf` | italic | 400 | 210,368 | `2e143f2d688fbf3a815fe6dd31af3a74c0a5f71e2744fdecbb0b32ae543e99de` |
37| `__clo_terminal_primary_34_8_1-SemiBoldItalic.woff2` | `TTF/IosevkaFixed-ExtendedSemiBoldItalic.ttf` | italic | 600 | 210,340 | `d871135db2d8ec47cb3e3ffe6fa85d1e55e187f9bc81828f8a82afd6e76cbe8a` |
38
39Copyright (c) 2015-2026, Renzhi Li (aka. Belleve Invis, belleve@typeof.net).
40Distributed under the SIL Open Font License 1.1 in [`OFL.txt`](OFL.txt).
41Iosevka reserves no font name, so the subset carries no naming obligation; the
42private `__clo_terminal_primary_34_8_1` family name is this package's, not a
43claim about the upstream face.
44
45The checked-in manifest [`PRIMARY-CODEPOINTS.txt`](PRIMARY-CODEPOINTS.txt) is
46the sorted intersection of the source cmap with the retained ranges in
47[`subset-primary.py`](subset-primary.py). It contains 4,660 codepoints, is
4832,867 bytes, and has SHA-256
49`ebe06abbcb42d87fe2d2f37f805ab0413b69c8dbb438dded94e4a8f4c2832672`.
50The ranges are `U+0000-2BFF` and `U+1FB00-1FBFF`: every script and symbol block
51a cell can hold, plus Symbols for Legacy Computing. That keeps braille,
52technical, dingbats and the sextants the previous primary had none of, and drops
53mathematical alphanumerics, musical notation, emoji and the historic scripts —
542,899 of the source font's 7,559 codepoints, and most of its bytes.
55
56Regenerate the manifest and the four faces with these exact commands from
57`sys/lib/terminal`, given the extracted TTFs in approved temporary storage:
58
59```sh
60python fonts/subset-primary.py "$TTF_DIR/IosevkaFixed-Extended.ttf" \
61 "$CLEAN_DIR/PRIMARY-CODEPOINTS.txt"
62for pair in Extended:Regular ExtendedSemiBold:SemiBold \
63 ExtendedItalic:Italic ExtendedSemiBoldItalic:SemiBoldItalic; do
64 pyftsubset "$TTF_DIR/IosevkaFixed-${pair%%:*}.ttf" \
65 --output-file="$CLEAN_DIR/__clo_terminal_primary_34_8_1-${pair##*:}.woff2" \
66 --flavor=woff2 \
67 --unicodes-file="$CLEAN_DIR/PRIMARY-CODEPOINTS.txt" \
68 --layout-features='' \
69 --name-IDs=0,1,2,3,4,5,6 \
70 --name-languages=0x409 \
71 --no-name-legacy \
72 --no-harfbuzz-repacker \
73 --no-recalc-timestamp \
74 --canonical-order
75done
76```
77
78Two builds from separate clean directories produced byte-identical manifests
79and WOFF2 files. The recorded build used the exact pins in
80[`symbols-requirements.txt`](symbols-requirements.txt): `fonttools==4.51.0` and
81`brotli==1.1.0`. Run the commands twice with distinct empty `CLEAN_DIR` values
82and require byte equality plus the hashes above before replacing checked-in
83outputs.
84
85## Terminal symbols
86
87`__clo_terminal_symbols_3_4_0-Regular.woff2` is a subset of Symbols Nerd Font
88Mono v3.4.0. The v3.4.0 tag resolves to Nerd Fonts commit
89`fa7b859994228a9c8759f99c55a8d31ee92a1b5e`; the source artifact is Git blob
90`d898eea9dc37acee648303e15b13fea4eb437d8e` at:
91
92`https://raw.githubusercontent.com/ryanoasis/nerd-fonts/fa7b859994228a9c8759f99c55a8d31ee92a1b5e/patched-fonts/NerdFontsSymbolsOnly/SymbolsNerdFontMono-Regular.ttf`
93
94The downloaded TTF is 2,507,556 bytes with SHA-256
95`f0f624d9b474bea1662cf7e862d44aebe1ae1f6c7f9cb7a0ca5d0e5ac9561c60`.
96Its embedded unique id is `Symbols Nerd Font Mono 3.4.0` and its version is
97`Version 001.000;Nerd Fonts 3.4.0`. The input TTF is acquisition material and
98must remain outside the repository.
99
100The checked-in manifest [`SYMBOLS-CODEPOINTS.txt`](SYMBOLS-CODEPOINTS.txt) is
101the sorted intersection of the source cmap with the retained ranges in
102[`subset-symbols.py`](subset-symbols.py). It contains 10,273 codepoints, is
10378,807 bytes, and has SHA-256
104`128c0e17659bd21ea91edf73006304b368fb714d38f12c0d7b005d713052eac1`.
105The range policy excludes primary-owned box/block `U+2500-U+259F`, standard
106Powerline `U+E0A0-U+E0A2` and `U+E0B0-U+E0B3`, and unlicensed Font Logos
107`U+F300-U+F381`. The box/block exclusion has an empty intersection with this
108source cmap; the other exclusions remove 7 and 130 source codepoints.
109
110The output WOFF2 is 1,083,700 bytes with SHA-256
111`a41598870ee7b32226cc76fdaab59edca51ba04e30d2c9a0bfa453615e3cca8c`.
112Two builds from separate clean directories produced byte-identical manifests
113and WOFF2 files. The recorded build used Python 3.9.6 with the exact pins in
114[`symbols-requirements.txt`](symbols-requirements.txt): `fonttools==4.51.0`
115and `brotli==1.1.0`.
116
117Given an input TTF in approved temporary storage, regenerate the manifest and
118font with these exact commands from `sys/lib/terminal`:
119
120```sh
121python fonts/subset-symbols.py "$INPUT_TTF" "$CLEAN_DIR/SYMBOLS-CODEPOINTS.txt"
122pyftsubset "$INPUT_TTF" \
123 --output-file="$CLEAN_DIR/__clo_terminal_symbols_3_4_0-Regular.woff2" \
124 --flavor=woff2 \
125 --unicodes-file="$CLEAN_DIR/SYMBOLS-CODEPOINTS.txt" \
126 --layout-features='' \
127 --name-IDs=0,1,2,3,4,5,6 \
128 --name-languages=0x409 \
129 --no-name-legacy \
130 --no-harfbuzz-repacker \
131 --no-recalc-timestamp \
132 --canonical-order
133```
134
135Run both commands twice with distinct empty `CLEAN_DIR` values and require
136byte equality plus the hashes above before replacing checked-in outputs.
137
138Nerd Fonts marks Symbols Only as MIT, while its own audit assigns retained
139glyph components additional MIT, CC BY 4.0, Apache 2.0, and SIL OFL 1.1
140terms. The subset is distributed with every applicable component term rather
141than treated as MIT-only. [`NOTICES.md`](NOTICES.md) maps retained ranges to
142the exact notices under [`licenses/`](licenses/). Font Logos is absent because
143the pinned audit records that component as unlicensed.
packages/terminal/fonts/SYMBOLS-CODEPOINTS.txt created+10273
......@@ -0,0 +1,10273 @@
1U+23FB
2U+23FC
3U+23FD
4U+23FE
5U+2630
6U+2665
7U+26A1
8U+276C
9U+276D
10U+276E
11U+276F
12U+2770
13U+2771
14U+2B58
15U+E000
16U+E001
17U+E002
18U+E003
19U+E004
20U+E005
21U+E006
22U+E007
23U+E008
24U+E009
25U+E00A
26U+E0A3
27U+E0B4
28U+E0B5
29U+E0B6
30U+E0B7
31U+E0B8
32U+E0B9
33U+E0BA
34U+E0BB
35U+E0BC
36U+E0BD
37U+E0BE
38U+E0BF
39U+E0C0
40U+E0C1
41U+E0C2
42U+E0C3
43U+E0C4
44U+E0C5
45U+E0C6
46U+E0C7
47U+E0C8
48U+E0CA
49U+E0CC
50U+E0CD
51U+E0CE
52U+E0CF
53U+E0D0
54U+E0D1
55U+E0D2
56U+E0D4
57U+E0D6
58U+E0D7
59U+E200
60U+E201
61U+E202
62U+E203
63U+E204
64U+E205
65U+E206
66U+E207
67U+E208
68U+E209
69U+E20A
70U+E20B
71U+E20C
72U+E20D
73U+E20E
74U+E20F
75U+E210
76U+E211
77U+E212
78U+E213
79U+E214
80U+E215
81U+E216
82U+E217
83U+E218
84U+E219
85U+E21A
86U+E21B
87U+E21C
88U+E21D
89U+E21E
90U+E21F
91U+E220
92U+E221
93U+E222
94U+E223
95U+E224
96U+E225
97U+E226
98U+E227
99U+E228
100U+E229
101U+E22A
102U+E22B
103U+E22C
104U+E22D
105U+E22E
106U+E22F
107U+E230
108U+E231
109U+E232
110U+E233
111U+E234
112U+E235
113U+E236
114U+E237
115U+E238
116U+E239
117U+E23A
118U+E23B
119U+E23C
120U+E23D
121U+E23E
122U+E23F
123U+E240
124U+E241
125U+E242
126U+E243
127U+E244
128U+E245
129U+E246
130U+E247
131U+E248
132U+E249
133U+E24A
134U+E24B
135U+E24C
136U+E24D
137U+E24E
138U+E24F
139U+E250
140U+E251
141U+E252
142U+E253
143U+E254
144U+E255
145U+E256
146U+E257
147U+E258
148U+E259
149U+E25A
150U+E25B
151U+E25C
152U+E25D
153U+E25E
154U+E25F
155U+E260
156U+E261
157U+E262
158U+E263
159U+E264
160U+E265
161U+E266
162U+E267
163U+E268
164U+E269
165U+E26A
166U+E26B
167U+E26C
168U+E26D
169U+E26E
170U+E26F
171U+E270
172U+E271
173U+E272
174U+E273
175U+E274
176U+E275
177U+E276
178U+E277
179U+E278
180U+E279
181U+E27A
182U+E27B
183U+E27C
184U+E27D
185U+E27E
186U+E27F
187U+E280
188U+E281
189U+E282
190U+E283
191U+E284
192U+E285
193U+E286
194U+E287
195U+E288
196U+E289
197U+E28A
198U+E28B
199U+E28C
200U+E28D
201U+E28E
202U+E28F
203U+E290
204U+E291
205U+E292
206U+E293
207U+E294
208U+E295
209U+E296
210U+E297
211U+E298
212U+E299
213U+E29A
214U+E29B
215U+E29C
216U+E29D
217U+E29E
218U+E29F
219U+E2A0
220U+E2A1
221U+E2A2
222U+E2A3
223U+E2A4
224U+E2A5
225U+E2A6
226U+E2A7
227U+E2A8
228U+E2A9
229U+E300
230U+E301
231U+E302
232U+E303
233U+E304
234U+E305
235U+E306
236U+E307
237U+E308
238U+E309
239U+E30A
240U+E30B
241U+E30C
242U+E30D
243U+E30E
244U+E30F
245U+E310
246U+E311
247U+E312
248U+E313
249U+E314
250U+E315
251U+E316
252U+E317
253U+E318
254U+E319
255U+E31A
256U+E31B
257U+E31C
258U+E31D
259U+E31E
260U+E31F
261U+E320
262U+E321
263U+E322
264U+E323
265U+E324
266U+E325
267U+E326
268U+E327
269U+E328
270U+E329
271U+E32A
272U+E32B
273U+E32C
274U+E32D
275U+E32E
276U+E32F
277U+E330
278U+E331
279U+E332
280U+E333
281U+E334
282U+E335
283U+E336
284U+E337
285U+E338
286U+E339
287U+E33A
288U+E33B
289U+E33C
290U+E33D
291U+E33E
292U+E33F
293U+E340
294U+E341
295U+E342
296U+E343
297U+E344
298U+E345
299U+E346
300U+E347
301U+E348
302U+E349
303U+E34A
304U+E34B
305U+E34C
306U+E34D
307U+E34E
308U+E34F
309U+E350
310U+E351
311U+E352
312U+E353
313U+E354
314U+E355
315U+E356
316U+E357
317U+E358
318U+E359
319U+E35A
320U+E35B
321U+E35C
322U+E35D
323U+E35E
324U+E35F
325U+E360
326U+E361
327U+E362
328U+E363
329U+E364
330U+E365
331U+E366
332U+E367
333U+E368
334U+E369
335U+E36A
336U+E36B
337U+E36C
338U+E36D
339U+E36E
340U+E36F
341U+E370
342U+E371
343U+E372
344U+E373
345U+E374
346U+E375
347U+E376
348U+E377
349U+E378
350U+E379
351U+E37A
352U+E37B
353U+E37C
354U+E37D
355U+E37E
356U+E37F
357U+E380
358U+E381
359U+E382
360U+E383
361U+E384
362U+E385
363U+E386
364U+E387
365U+E388
366U+E389
367U+E38A
368U+E38B
369U+E38C
370U+E38D
371U+E38E
372U+E38F
373U+E390
374U+E391
375U+E392
376U+E393
377U+E394
378U+E395
379U+E396
380U+E397
381U+E398
382U+E399
383U+E39A
384U+E39B
385U+E39C
386U+E39D
387U+E39E
388U+E39F
389U+E3A0
390U+E3A1
391U+E3A2
392U+E3A3
393U+E3A4
394U+E3A5
395U+E3A6
396U+E3A7
397U+E3A8
398U+E3A9
399U+E3AA
400U+E3AB
401U+E3AC
402U+E3AD
403U+E3AE
404U+E3AF
405U+E3B0
406U+E3B1
407U+E3B2
408U+E3B3
409U+E3B4
410U+E3B5
411U+E3B6
412U+E3B7
413U+E3B8
414U+E3B9
415U+E3BA
416U+E3BB
417U+E3BC
418U+E3BD
419U+E3BE
420U+E3BF
421U+E3C0
422U+E3C1
423U+E3C2
424U+E3C3
425U+E3C4
426U+E3C5
427U+E3C6
428U+E3C7
429U+E3C8
430U+E3C9
431U+E3CA
432U+E3CB
433U+E3CC
434U+E3CD
435U+E3CE
436U+E3CF
437U+E3D0
438U+E3D1
439U+E3D2
440U+E3D3
441U+E3D4
442U+E3D5
443U+E3D6
444U+E3D7
445U+E3D8
446U+E3D9
447U+E3DA
448U+E3DB
449U+E3DC
450U+E3DD
451U+E3DE
452U+E3DF
453U+E3E0
454U+E3E1
455U+E3E2
456U+E3E3
457U+E5FA
458U+E5FB
459U+E5FC
460U+E5FD
461U+E5FE
462U+E5FF
463U+E600
464U+E601
465U+E602
466U+E603
467U+E604
468U+E605
469U+E606
470U+E607
471U+E608
472U+E609
473U+E60A
474U+E60B
475U+E60C
476U+E60D
477U+E60E
478U+E60F
479U+E610
480U+E611
481U+E612
482U+E613
483U+E614
484U+E615
485U+E616
486U+E617
487U+E618
488U+E619
489U+E61A
490U+E61B
491U+E61C
492U+E61D
493U+E61E
494U+E61F
495U+E620
496U+E621
497U+E622
498U+E623
499U+E624
500U+E625
501U+E626
502U+E627
503U+E628
504U+E629
505U+E62A
506U+E62B
507U+E62C
508U+E62D
509U+E62E
510U+E62F
511U+E630
512U+E631
513U+E632
514U+E633
515U+E634
516U+E635
517U+E636
518U+E637
519U+E638
520U+E639
521U+E63A
522U+E63B
523U+E63C
524U+E63D
525U+E63E
526U+E63F
527U+E640
528U+E641
529U+E642
530U+E643
531U+E644
532U+E645
533U+E646
534U+E647
535U+E648
536U+E649
537U+E64A
538U+E64B
539U+E64C
540U+E64D
541U+E64E
542U+E64F
543U+E650
544U+E651
545U+E652
546U+E653
547U+E654
548U+E655
549U+E656
550U+E657
551U+E658
552U+E659
553U+E65A
554U+E65B
555U+E65C
556U+E65D
557U+E65E
558U+E65F
559U+E660
560U+E661
561U+E662
562U+E663
563U+E664
564U+E665
565U+E666
566U+E667
567U+E668
568U+E669
569U+E66A
570U+E66B
571U+E66C
572U+E66D
573U+E66E
574U+E66F
575U+E670
576U+E671
577U+E672
578U+E673
579U+E674
580U+E675
581U+E676
582U+E677
583U+E678
584U+E679
585U+E67A
586U+E67B
587U+E67C
588U+E67D
589U+E67E
590U+E67F
591U+E680
592U+E681
593U+E682
594U+E683
595U+E684
596U+E685
597U+E686
598U+E687
599U+E688
600U+E689
601U+E68A
602U+E68B
603U+E68C
604U+E68D
605U+E68E
606U+E68F
607U+E690
608U+E691
609U+E692
610U+E693
611U+E694
612U+E695
613U+E696
614U+E697
615U+E698
616U+E699
617U+E69A
618U+E69B
619U+E69C
620U+E69D
621U+E69E
622U+E69F
623U+E6A0
624U+E6A1
625U+E6A2
626U+E6A3
627U+E6A4
628U+E6A5
629U+E6A6
630U+E6A7
631U+E6A8
632U+E6A9
633U+E6AA
634U+E6AB
635U+E6AC
636U+E6AD
637U+E6AE
638U+E6AF
639U+E6B0
640U+E6B1
641U+E6B2
642U+E6B3
643U+E6B4
644U+E6B5
645U+E6B6
646U+E6B7
647U+E6B8
648U+E700
649U+E701
650U+E702
651U+E703
652U+E704
653U+E705
654U+E706
655U+E707
656U+E708
657U+E709
658U+E70A
659U+E70B
660U+E70C
661U+E70D
662U+E70E
663U+E70F
664U+E710
665U+E711
666U+E712
667U+E713
668U+E714
669U+E715
670U+E716
671U+E717
672U+E718
673U+E719
674U+E71A
675U+E71B
676U+E71C
677U+E71D
678U+E71E
679U+E71F
680U+E720
681U+E721
682U+E722
683U+E723
684U+E724
685U+E725
686U+E726
687U+E727
688U+E728
689U+E729
690U+E72A
691U+E72B
692U+E72C
693U+E72D
694U+E72E
695U+E72F
696U+E730
697U+E731
698U+E732
699U+E733
700U+E734
701U+E735
702U+E736
703U+E737
704U+E738
705U+E739
706U+E73A
707U+E73B
708U+E73C
709U+E73D
710U+E73E
711U+E73F
712U+E740
713U+E741
714U+E742
715U+E743
716U+E744
717U+E745
718U+E746
719U+E747
720U+E748
721U+E749
722U+E74A
723U+E74B
724U+E74C
725U+E74D
726U+E74E
727U+E74F
728U+E750
729U+E751
730U+E752
731U+E753
732U+E754
733U+E755
734U+E756
735U+E757
736U+E758
737U+E759
738U+E75A
739U+E75B
740U+E75C
741U+E75D
742U+E75E
743U+E75F
744U+E760
745U+E761
746U+E762
747U+E763
748U+E764
749U+E765
750U+E766
751U+E767
752U+E768
753U+E769
754U+E76A
755U+E76B
756U+E76C
757U+E76D
758U+E76E
759U+E76F
760U+E770
761U+E771
762U+E772
763U+E773
764U+E774
765U+E775
766U+E776
767U+E777
768U+E778
769U+E779
770U+E77A
771U+E77B
772U+E77C
773U+E77D
774U+E77E
775U+E77F
776U+E780
777U+E781
778U+E782
779U+E783
780U+E784
781U+E785
782U+E786
783U+E787
784U+E788
785U+E789
786U+E78A
787U+E78B
788U+E78C
789U+E78D
790U+E78E
791U+E78F
792U+E790
793U+E791
794U+E792
795U+E793
796U+E794
797U+E795
798U+E796
799U+E797
800U+E798
801U+E799
802U+E79A
803U+E79B
804U+E79C
805U+E79D
806U+E79E
807U+E79F
808U+E7A0
809U+E7A1
810U+E7A2
811U+E7A3
812U+E7A4
813U+E7A5
814U+E7A6
815U+E7A7
816U+E7A8
817U+E7A9
818U+E7AA
819U+E7AB
820U+E7AC
821U+E7AD
822U+E7AE
823U+E7AF
824U+E7B0
825U+E7B1
826U+E7B2
827U+E7B3
828U+E7B4
829U+E7B5
830U+E7B6
831U+E7B7
832U+E7B8
833U+E7B9
834U+E7BA
835U+E7BB
836U+E7BC
837U+E7BD
838U+E7BE
839U+E7BF
840U+E7C0
841U+E7C1
842U+E7C2
843U+E7C3
844U+E7C4
845U+E7C5
846U+E7C6
847U+E7C7
848U+E7C8
849U+E7C9
850U+E7CA
851U+E7CB
852U+E7CC
853U+E7CD
854U+E7CE
855U+E7CF
856U+E7D0
857U+E7D1
858U+E7D2
859U+E7D3
860U+E7D4
861U+E7D5
862U+E7D6
863U+E7D7
864U+E7D8
865U+E7D9
866U+E7DA
867U+E7DB
868U+E7DC
869U+E7DD
870U+E7DE
871U+E7DF
872U+E7E0
873U+E7E1
874U+E7E2
875U+E7E3
876U+E7E4
877U+E7E5
878U+E7E6
879U+E7E7
880U+E7E8
881U+E7E9
882U+E7EA
883U+E7EB
884U+E7EC
885U+E7ED
886U+E7EE
887U+E7EF
888U+E7F0
889U+E7F1
890U+E7F2
891U+E7F3
892U+E7F4
893U+E7F5
894U+E7F6
895U+E7F7
896U+E7F8
897U+E7F9
898U+E7FA
899U+E7FB
900U+E7FC
901U+E7FD
902U+E7FE
903U+E7FF
904U+E800
905U+E801
906U+E802
907U+E803
908U+E804
909U+E805
910U+E806
911U+E807
912U+E808
913U+E809
914U+E80A
915U+E80B
916U+E80C
917U+E80D
918U+E80E
919U+E80F
920U+E810
921U+E811
922U+E812
923U+E813
924U+E814
925U+E815
926U+E816
927U+E817
928U+E818
929U+E819
930U+E81A
931U+E81B
932U+E81C
933U+E81D
934U+E81E
935U+E81F
936U+E820
937U+E821
938U+E822
939U+E823
940U+E824
941U+E825
942U+E826
943U+E827
944U+E828
945U+E829
946U+E82A
947U+E82B
948U+E82C
949U+E82D
950U+E82E
951U+E82F
952U+E830
953U+E831
954U+E832
955U+E833
956U+E834
957U+E835
958U+E836
959U+E837
960U+E838
961U+E839
962U+E83A
963U+E83B
964U+E83C
965U+E83D
966U+E83E
967U+E83F
968U+E840
969U+E841
970U+E842
971U+E843
972U+E844
973U+E845
974U+E846
975U+E847
976U+E848
977U+E849
978U+E84A
979U+E84B
980U+E84C
981U+E84D
982U+E84E
983U+E84F
984U+E850
985U+E851
986U+E852
987U+E853
988U+E854
989U+E855
990U+E856
991U+E857
992U+E858
993U+E859
994U+E85A
995U+E85B
996U+E85C
997U+E85D
998U+E85E
999U+E85F
1000U+E860
1001U+E861
1002U+E862
1003U+E863
1004U+E864
1005U+E865
1006U+E866
1007U+E867
1008U+E868
1009U+E869
1010U+E86A
1011U+E86B
1012U+E86C
1013U+E86D
1014U+E86E
1015U+E86F
1016U+E870
1017U+E871
1018U+E872
1019U+E873
1020U+E874
1021U+E875
1022U+E876
1023U+E877
1024U+E878
1025U+E879
1026U+E87A
1027U+E87B
1028U+E87C
1029U+E87D
1030U+E87E
1031U+E87F
1032U+E880
1033U+E881
1034U+E882
1035U+E883
1036U+E884
1037U+E885
1038U+E886
1039U+E887
1040U+E888
1041U+E889
1042U+E88A
1043U+E88B
1044U+E88C
1045U+E88D
1046U+E88E
1047U+E88F
1048U+E890
1049U+E891
1050U+E892
1051U+E893
1052U+E894
1053U+E895
1054U+E896
1055U+E897
1056U+E898
1057U+E899
1058U+E89A
1059U+E89B
1060U+E89C
1061U+E89D
1062U+E89E
1063U+E89F
1064U+E8A0
1065U+E8A1
1066U+E8A2
1067U+E8A3
1068U+E8A4
1069U+E8A5
1070U+E8A6
1071U+E8A7
1072U+E8A8
1073U+E8A9
1074U+E8AA
1075U+E8AB
1076U+E8AC
1077U+E8AD
1078U+E8AE
1079U+E8AF
1080U+E8B0
1081U+E8B1
1082U+E8B2
1083U+E8B3
1084U+E8B4
1085U+E8B5
1086U+E8B6
1087U+E8B7
1088U+E8B8
1089U+E8B9
1090U+E8BA
1091U+E8BB
1092U+E8BC
1093U+E8BD
1094U+E8BE
1095U+E8BF
1096U+E8C0
1097U+E8C1
1098U+E8C2
1099U+E8C3
1100U+E8C4
1101U+E8C5
1102U+E8C6
1103U+E8C7
1104U+E8C8
1105U+E8C9
1106U+E8CA
1107U+E8CB
1108U+E8CC
1109U+E8CD
1110U+E8CE
1111U+E8CF
1112U+E8D0
1113U+E8D1
1114U+E8D2
1115U+E8D3
1116U+E8D4
1117U+E8D5
1118U+E8D6
1119U+E8D7
1120U+E8D8
1121U+E8D9
1122U+E8DA
1123U+E8DB
1124U+E8DC
1125U+E8DD
1126U+E8DE
1127U+E8DF
1128U+E8E0
1129U+E8E1
1130U+E8E2
1131U+E8E3
1132U+E8E4
1133U+E8E5
1134U+E8E6
1135U+E8E7
1136U+E8E8
1137U+E8E9
1138U+E8EA
1139U+E8EB
1140U+E8EC
1141U+E8ED
1142U+E8EE
1143U+E8EF
1144U+EA60
1145U+EA61
1146U+EA62
1147U+EA63
1148U+EA64
1149U+EA65
1150U+EA66
1151U+EA67
1152U+EA68
1153U+EA69
1154U+EA6A
1155U+EA6B
1156U+EA6C
1157U+EA6D
1158U+EA6E
1159U+EA6F
1160U+EA70
1161U+EA71
1162U+EA72
1163U+EA73
1164U+EA74
1165U+EA75
1166U+EA76
1167U+EA77
1168U+EA78
1169U+EA79
1170U+EA7A
1171U+EA7B
1172U+EA7C
1173U+EA7D
1174U+EA7E
1175U+EA7F
1176U+EA80
1177U+EA81
1178U+EA82
1179U+EA83
1180U+EA84
1181U+EA85
1182U+EA86
1183U+EA87
1184U+EA88
1185U+EA8A
1186U+EA8B
1187U+EA8C
1188U+EA8F
1189U+EA90
1190U+EA91
1191U+EA92
1192U+EA93
1193U+EA94
1194U+EA95
1195U+EA96
1196U+EA97
1197U+EA98
1198U+EA99
1199U+EA9A
1200U+EA9B
1201U+EA9C
1202U+EA9D
1203U+EA9E
1204U+EA9F
1205U+EAA0
1206U+EAA1
1207U+EAA2
1208U+EAA3
1209U+EAA4
1210U+EAA5
1211U+EAA6
1212U+EAA7
1213U+EAA8
1214U+EAA9
1215U+EAAA
1216U+EAAB
1217U+EAAC
1218U+EAAD
1219U+EAAE
1220U+EAAF
1221U+EAB0
1222U+EAB1
1223U+EAB2
1224U+EAB3
1225U+EAB4
1226U+EAB5
1227U+EAB6
1228U+EAB7
1229U+EAB8
1230U+EAB9
1231U+EABA
1232U+EABB
1233U+EABC
1234U+EABD
1235U+EABE
1236U+EABF
1237U+EAC0
1238U+EAC1
1239U+EAC2
1240U+EAC3
1241U+EAC4
1242U+EAC5
1243U+EAC6
1244U+EAC7
1245U+EAC9
1246U+EACC
1247U+EACD
1248U+EACE
1249U+EACF
1250U+EAD0
1251U+EAD1
1252U+EAD2
1253U+EAD3
1254U+EAD4
1255U+EAD5
1256U+EAD6
1257U+EAD7
1258U+EAD8
1259U+EAD9
1260U+EADA
1261U+EADB
1262U+EADC
1263U+EADD
1264U+EADE
1265U+EADF
1266U+EAE0
1267U+EAE1
1268U+EAE2
1269U+EAE3
1270U+EAE4
1271U+EAE5
1272U+EAE6
1273U+EAE7
1274U+EAE8
1275U+EAE9
1276U+EAEA
1277U+EAEB
1278U+EAEC
1279U+EAED
1280U+EAEE
1281U+EAEF
1282U+EAF0
1283U+EAF1
1284U+EAF2
1285U+EAF3
1286U+EAF4
1287U+EAF5
1288U+EAF6
1289U+EAF7
1290U+EAF8
1291U+EAF9
1292U+EAFA
1293U+EAFB
1294U+EAFC
1295U+EAFD
1296U+EAFE
1297U+EAFF
1298U+EB00
1299U+EB01
1300U+EB02
1301U+EB03
1302U+EB04
1303U+EB05
1304U+EB06
1305U+EB07
1306U+EB08
1307U+EB09
1308U+EB0B
1309U+EB0C
1310U+EB0D
1311U+EB0E
1312U+EB0F
1313U+EB10
1314U+EB11
1315U+EB12
1316U+EB13
1317U+EB14
1318U+EB15
1319U+EB16
1320U+EB17
1321U+EB18
1322U+EB19
1323U+EB1A
1324U+EB1B
1325U+EB1C
1326U+EB1D
1327U+EB1E
1328U+EB1F
1329U+EB20
1330U+EB21
1331U+EB22
1332U+EB23
1333U+EB24
1334U+EB25
1335U+EB26
1336U+EB27
1337U+EB28
1338U+EB29
1339U+EB2A
1340U+EB2B
1341U+EB2C
1342U+EB2D
1343U+EB2E
1344U+EB2F
1345U+EB30
1346U+EB31
1347U+EB32
1348U+EB33
1349U+EB34
1350U+EB35
1351U+EB36
1352U+EB37
1353U+EB38
1354U+EB39
1355U+EB3A
1356U+EB3B
1357U+EB3C
1358U+EB3D
1359U+EB3E
1360U+EB3F
1361U+EB40
1362U+EB41
1363U+EB42
1364U+EB43
1365U+EB44
1366U+EB45
1367U+EB46
1368U+EB47
1369U+EB48
1370U+EB49
1371U+EB4A
1372U+EB4B
1373U+EB4C
1374U+EB4D
1375U+EB4E
1376U+EB50
1377U+EB51
1378U+EB52
1379U+EB53
1380U+EB54
1381U+EB55
1382U+EB56
1383U+EB57
1384U+EB58
1385U+EB59
1386U+EB5A
1387U+EB5B
1388U+EB5C
1389U+EB5D
1390U+EB5E
1391U+EB5F
1392U+EB60
1393U+EB61
1394U+EB62
1395U+EB63
1396U+EB64
1397U+EB65
1398U+EB66
1399U+EB67
1400U+EB68
1401U+EB69
1402U+EB6A
1403U+EB6B
1404U+EB6C
1405U+EB6D
1406U+EB6E
1407U+EB6F
1408U+EB70
1409U+EB71
1410U+EB72
1411U+EB73
1412U+EB74
1413U+EB75
1414U+EB76
1415U+EB77
1416U+EB78
1417U+EB79
1418U+EB7A
1419U+EB7B
1420U+EB7C
1421U+EB7D
1422U+EB7E
1423U+EB7F
1424U+EB80
1425U+EB81
1426U+EB82
1427U+EB83
1428U+EB84
1429U+EB85
1430U+EB86
1431U+EB87
1432U+EB88
1433U+EB89
1434U+EB8A
1435U+EB8B
1436U+EB8C
1437U+EB8D
1438U+EB8E
1439U+EB8F
1440U+EB90
1441U+EB91
1442U+EB92
1443U+EB93
1444U+EB94
1445U+EB95
1446U+EB96
1447U+EB97
1448U+EB98
1449U+EB99
1450U+EB9A
1451U+EB9B
1452U+EB9C
1453U+EB9D
1454U+EB9E
1455U+EB9F
1456U+EBA0
1457U+EBA1
1458U+EBA2
1459U+EBA3
1460U+EBA4
1461U+EBA5
1462U+EBA6
1463U+EBA7
1464U+EBA8
1465U+EBA9
1466U+EBAA
1467U+EBAB
1468U+EBAC
1469U+EBAD
1470U+EBAE
1471U+EBAF
1472U+EBB0
1473U+EBB1
1474U+EBB2
1475U+EBB3
1476U+EBB4
1477U+EBB5
1478U+EBB6
1479U+EBB7
1480U+EBB8
1481U+EBB9
1482U+EBBA
1483U+EBBB
1484U+EBBC
1485U+EBBD
1486U+EBBE
1487U+EBBF
1488U+EBC0
1489U+EBC1
1490U+EBC2
1491U+EBC3
1492U+EBC4
1493U+EBC5
1494U+EBC6
1495U+EBC7
1496U+EBC8
1497U+EBC9
1498U+EBCA
1499U+EBCB
1500U+EBCC
1501U+EBCD
1502U+EBCE
1503U+EBCF
1504U+EBD0
1505U+EBD1
1506U+EBD2
1507U+EBD3
1508U+EBD4
1509U+EBD5
1510U+EBD6
1511U+EBD7
1512U+EBD8
1513U+EBD9
1514U+EBDA
1515U+EBDB
1516U+EBDC
1517U+EBDD
1518U+EBDE
1519U+EBDF
1520U+EBE0
1521U+EBE1
1522U+EBE2
1523U+EBE3
1524U+EBE4
1525U+EBE5
1526U+EBE6
1527U+EBE7
1528U+EBE8
1529U+EBE9
1530U+EBEA
1531U+EBEB
1532U+EBEC
1533U+EBED
1534U+EBEE
1535U+EBEF
1536U+EBF0
1537U+EBF1
1538U+EBF2
1539U+EBF3
1540U+EBF4
1541U+EBF5
1542U+EBF6
1543U+EBF7
1544U+EBF8
1545U+EBF9
1546U+EBFA
1547U+EBFB
1548U+EBFC
1549U+EBFD
1550U+EBFE
1551U+EBFF
1552U+EC00
1553U+EC01
1554U+EC02
1555U+EC03
1556U+EC04
1557U+EC05
1558U+EC06
1559U+EC07
1560U+EC08
1561U+EC09
1562U+EC0A
1563U+EC0B
1564U+EC0C
1565U+EC0D
1566U+EC0E
1567U+EC0F
1568U+EC10
1569U+EC11
1570U+EC12
1571U+EC13
1572U+EC14
1573U+EC15
1574U+EC16
1575U+EC17
1576U+EC18
1577U+EC19
1578U+EC1A
1579U+EC1B
1580U+EC1C
1581U+EC1D
1582U+EC1E
1583U+ED00
1584U+ED01
1585U+ED02
1586U+ED03
1587U+ED04
1588U+ED05
1589U+ED06
1590U+ED07
1591U+ED08
1592U+ED09
1593U+ED0A
1594U+ED0B
1595U+ED0C
1596U+ED0D
1597U+ED0E
1598U+ED0F
1599U+ED10
1600U+ED11
1601U+ED12
1602U+ED13
1603U+ED14
1604U+ED15
1605U+ED16
1606U+ED17
1607U+ED18
1608U+ED19
1609U+ED1A
1610U+ED1B
1611U+ED1C
1612U+ED1D
1613U+ED1E
1614U+ED1F
1615U+ED20
1616U+ED21
1617U+ED22
1618U+ED23
1619U+ED24
1620U+ED25
1621U+ED26
1622U+ED27
1623U+ED28
1624U+ED29
1625U+ED2A
1626U+ED2B
1627U+ED2C
1628U+ED2D
1629U+ED2E
1630U+ED2F
1631U+ED30
1632U+ED31
1633U+ED32
1634U+ED33
1635U+ED34
1636U+ED35
1637U+ED36
1638U+ED37
1639U+ED38
1640U+ED39
1641U+ED3A
1642U+ED3B
1643U+ED3C
1644U+ED3D
1645U+ED3E
1646U+ED3F
1647U+ED40
1648U+ED41
1649U+ED42
1650U+ED43
1651U+ED44
1652U+ED45
1653U+ED46
1654U+ED47
1655U+ED48
1656U+ED49
1657U+ED4A
1658U+ED4B
1659U+ED4C
1660U+ED4D
1661U+ED4E
1662U+ED4F
1663U+ED50
1664U+ED51
1665U+ED52
1666U+ED53
1667U+ED54
1668U+ED55
1669U+ED56
1670U+ED57
1671U+ED58
1672U+ED59
1673U+ED5A
1674U+ED5B
1675U+ED5C
1676U+ED5D
1677U+ED5E
1678U+ED5F
1679U+ED60
1680U+ED61
1681U+ED62
1682U+ED63
1683U+ED64
1684U+ED65
1685U+ED66
1686U+ED67
1687U+ED68
1688U+ED69
1689U+ED6A
1690U+ED6B
1691U+ED6C
1692U+ED6D
1693U+ED6E
1694U+ED6F
1695U+ED70
1696U+ED71
1697U+ED72
1698U+ED73
1699U+ED74
1700U+ED75
1701U+ED76
1702U+ED77
1703U+ED78
1704U+ED79
1705U+ED7A
1706U+ED7B
1707U+ED7C
1708U+ED7D
1709U+ED7E
1710U+ED7F
1711U+ED80
1712U+ED81
1713U+ED82
1714U+ED83
1715U+ED84
1716U+ED85
1717U+ED86
1718U+ED87
1719U+ED88
1720U+ED89
1721U+ED8A
1722U+ED8B
1723U+ED8C
1724U+ED8D
1725U+ED8E
1726U+ED8F
1727U+ED90
1728U+ED91
1729U+ED92
1730U+ED93
1731U+ED94
1732U+ED95
1733U+ED96
1734U+ED97
1735U+ED98
1736U+ED99
1737U+ED9A
1738U+ED9B
1739U+ED9C
1740U+ED9D
1741U+ED9E
1742U+ED9F
1743U+EDA0
1744U+EDA1
1745U+EDA2
1746U+EDA3
1747U+EDA4
1748U+EDA5
1749U+EDA6
1750U+EDA7
1751U+EDA8
1752U+EDA9
1753U+EDAA
1754U+EDAB
1755U+EDAC
1756U+EDAD
1757U+EDAE
1758U+EDAF
1759U+EDB0
1760U+EDB1
1761U+EDB2
1762U+EDB3
1763U+EDB4
1764U+EDB5
1765U+EDB6
1766U+EDB7
1767U+EDB8
1768U+EDB9
1769U+EDBA
1770U+EDBB
1771U+EDBC
1772U+EDBD
1773U+EDBE
1774U+EDBF
1775U+EDC0
1776U+EDC1
1777U+EDC2
1778U+EDC3
1779U+EDC4
1780U+EDC5
1781U+EDC6
1782U+EDC7
1783U+EDC8
1784U+EDC9
1785U+EDCA
1786U+EDCB
1787U+EDCC
1788U+EDCD
1789U+EDCE
1790U+EDCF
1791U+EDD0
1792U+EDD1
1793U+EDD2
1794U+EDD3
1795U+EDD4
1796U+EDD5
1797U+EDD6
1798U+EDD7
1799U+EDD8
1800U+EDD9
1801U+EDDA
1802U+EDDB
1803U+EDDC
1804U+EDDD
1805U+EDDE
1806U+EDDF
1807U+EDE0
1808U+EDE1
1809U+EDE2
1810U+EDE3
1811U+EDE4
1812U+EDE5
1813U+EDE6
1814U+EDE7
1815U+EDE8
1816U+EDE9
1817U+EDEA
1818U+EDEB
1819U+EDEC
1820U+EDED
1821U+EDEE
1822U+EDEF
1823U+EDF0
1824U+EDF1
1825U+EDF2
1826U+EDF3
1827U+EDF4
1828U+EDF5
1829U+EDF6
1830U+EDF7
1831U+EDF8
1832U+EDF9
1833U+EDFA
1834U+EDFB
1835U+EDFC
1836U+EDFD
1837U+EDFE
1838U+EDFF
1839U+EE00
1840U+EE01
1841U+EE02
1842U+EE03
1843U+EE04
1844U+EE05
1845U+EE06
1846U+EE07
1847U+EE08
1848U+EE09
1849U+EE0A
1850U+EE0B
1851U+EE0C
1852U+EE0D
1853U+EE0E
1854U+EE0F
1855U+EE10
1856U+EE11
1857U+EE12
1858U+EE13
1859U+EE14
1860U+EE15
1861U+EE16
1862U+EE17
1863U+EE18
1864U+EE19
1865U+EE1A
1866U+EE1B
1867U+EE1C
1868U+EE1D
1869U+EE1E
1870U+EE1F
1871U+EE20
1872U+EE21
1873U+EE22
1874U+EE23
1875U+EE24
1876U+EE25
1877U+EE26
1878U+EE27
1879U+EE28
1880U+EE29
1881U+EE2A
1882U+EE2B
1883U+EE2C
1884U+EE2D
1885U+EE2E
1886U+EE2F
1887U+EE30
1888U+EE31
1889U+EE32
1890U+EE33
1891U+EE34
1892U+EE35
1893U+EE36
1894U+EE37
1895U+EE38
1896U+EE39
1897U+EE3A
1898U+EE3B
1899U+EE3C
1900U+EE3D
1901U+EE3E
1902U+EE3F
1903U+EE40
1904U+EE41
1905U+EE42
1906U+EE43
1907U+EE44
1908U+EE45
1909U+EE46
1910U+EE47
1911U+EE48
1912U+EE49
1913U+EE4A
1914U+EE4B
1915U+EE4C
1916U+EE4D
1917U+EE4E
1918U+EE4F
1919U+EE50
1920U+EE51
1921U+EE52
1922U+EE53
1923U+EE54
1924U+EE55
1925U+EE56
1926U+EE57
1927U+EE58
1928U+EE59
1929U+EE5A
1930U+EE5B
1931U+EE5C
1932U+EE5D
1933U+EE5E
1934U+EE5F
1935U+EE60
1936U+EE61
1937U+EE62
1938U+EE63
1939U+EE64
1940U+EE65
1941U+EE66
1942U+EE67
1943U+EE68
1944U+EE69
1945U+EE6A
1946U+EE6B
1947U+EE6C
1948U+EE6D
1949U+EE6E
1950U+EE6F
1951U+EE70
1952U+EE71
1953U+EE72
1954U+EE73
1955U+EE74
1956U+EE75
1957U+EE76
1958U+EE77
1959U+EE78
1960U+EE79
1961U+EE7A
1962U+EE7B
1963U+EE7C
1964U+EE7D
1965U+EE7E
1966U+EE7F
1967U+EE80
1968U+EE81
1969U+EE82
1970U+EE83
1971U+EE84
1972U+EE85
1973U+EE86
1974U+EE87
1975U+EE88
1976U+EE89
1977U+EE8A
1978U+EE8B
1979U+EE8C
1980U+EE8D
1981U+EE8E
1982U+EE8F
1983U+EE90
1984U+EE91
1985U+EE92
1986U+EE93
1987U+EE94
1988U+EE95
1989U+EE96
1990U+EE97
1991U+EE98
1992U+EE99
1993U+EE9A
1994U+EE9B
1995U+EE9C
1996U+EE9D
1997U+EE9E
1998U+EE9F
1999U+EEA0
2000U+EEA1
2001U+EEA2
2002U+EEA3
2003U+EEA4
2004U+EEA5
2005U+EEA6
2006U+EEA7
2007U+EEA8
2008U+EEA9
2009U+EEAA
2010U+EEAB
2011U+EEAC
2012U+EEAD
2013U+EEAE
2014U+EEAF
2015U+EEB0
2016U+EEB1
2017U+EEB2
2018U+EEB3
2019U+EEB4
2020U+EEB5
2021U+EEB6
2022U+EEB7
2023U+EEB8
2024U+EEB9
2025U+EEBA
2026U+EEBB
2027U+EEBC
2028U+EEBD
2029U+EEBE
2030U+EEBF
2031U+EEC0
2032U+EEC1
2033U+EEC2
2034U+EEC3
2035U+EEC4
2036U+EEC5
2037U+EEC6
2038U+EEC7
2039U+EEC8
2040U+EEC9
2041U+EECA
2042U+EECB
2043U+EECC
2044U+EECD
2045U+EECE
2046U+EECF
2047U+EED0
2048U+EED1
2049U+EED2
2050U+EED3
2051U+EED4
2052U+EED5
2053U+EED6
2054U+EED7
2055U+EED8
2056U+EED9
2057U+EEDA
2058U+EEDB
2059U+EEDC
2060U+EEDD
2061U+EEDE
2062U+EEDF
2063U+EEE0
2064U+EEE1
2065U+EEE2
2066U+EEE3
2067U+EEE4
2068U+EEE5
2069U+EEE6
2070U+EEE7
2071U+EEE8
2072U+EEE9
2073U+EEEA
2074U+EEEB
2075U+EEEC
2076U+EEED
2077U+EEEE
2078U+EEEF
2079U+EEF0
2080U+EEF1
2081U+EEF2
2082U+EEF3
2083U+EEF4
2084U+EEF5
2085U+EEF6
2086U+EEF7
2087U+EEF8
2088U+EEF9
2089U+EEFA
2090U+EEFB
2091U+EEFC
2092U+EEFD
2093U+EEFE
2094U+EEFF
2095U+EF00
2096U+EF01
2097U+EF02
2098U+EF03
2099U+EF04
2100U+EF05
2101U+EF06
2102U+EF07
2103U+EF08
2104U+EF09
2105U+EF0A
2106U+EF0B
2107U+EF0C
2108U+EF0D
2109U+EF0E
2110U+EF0F
2111U+EF10
2112U+EF11
2113U+EF12
2114U+EF13
2115U+EF14
2116U+EF15
2117U+EF16
2118U+EF17
2119U+EF18
2120U+EF19
2121U+EF1A
2122U+EF1B
2123U+EF1C
2124U+EF1D
2125U+EF1E
2126U+EF1F
2127U+EF20
2128U+EF21
2129U+EF22
2130U+EF23
2131U+EF24
2132U+EF25
2133U+EF26
2134U+EF27
2135U+EF28
2136U+EF29
2137U+EF2A
2138U+EF2B
2139U+EF2C
2140U+EF2D
2141U+EF2E
2142U+EF2F
2143U+EF30
2144U+EF31
2145U+EF32
2146U+EF33
2147U+EF34
2148U+EF35
2149U+EF36
2150U+EF37
2151U+EF38
2152U+EF39
2153U+EF3A
2154U+EF3B
2155U+EF3C
2156U+EF3D
2157U+EF3E
2158U+EF3F
2159U+EF40
2160U+EF41
2161U+EF42
2162U+EF43
2163U+EF44
2164U+EF45
2165U+EF46
2166U+EF47
2167U+EF48
2168U+EF49
2169U+EF4A
2170U+EF4B
2171U+EF4C
2172U+EF4D
2173U+EF4E
2174U+EF4F
2175U+EF50
2176U+EF51
2177U+EF52
2178U+EF53
2179U+EF54
2180U+EF55
2181U+EF56
2182U+EF57
2183U+EF58
2184U+EF59
2185U+EF5A
2186U+EF5B
2187U+EF5C
2188U+EF5D
2189U+EF5E
2190U+EF5F
2191U+EF60
2192U+EF61
2193U+EF62
2194U+EF63
2195U+EF64
2196U+EF65
2197U+EF66
2198U+EF67
2199U+EF68
2200U+EF69
2201U+EF6A
2202U+EF6B
2203U+EF6C
2204U+EF6D
2205U+EF6E
2206U+EF6F
2207U+EF70
2208U+EF71
2209U+EF72
2210U+EF73
2211U+EF74
2212U+EF75
2213U+EF76
2214U+EF77
2215U+EF78
2216U+EF79
2217U+EF7A
2218U+EF7B
2219U+EF7C
2220U+EF7D
2221U+EF7E
2222U+EF7F
2223U+EF80
2224U+EF81
2225U+EF82
2226U+EF83
2227U+EF84
2228U+EF85
2229U+EF86
2230U+EF87
2231U+EF88
2232U+EF89
2233U+EF8A
2234U+EF8B
2235U+EF8C
2236U+EF8D
2237U+EF8E
2238U+EF8F
2239U+EF90
2240U+EF91
2241U+EF92
2242U+EF93
2243U+EF94
2244U+EF95
2245U+EF96
2246U+EF97
2247U+EF98
2248U+EF99
2249U+EF9A
2250U+EF9B
2251U+EF9C
2252U+EF9D
2253U+EF9E
2254U+EF9F
2255U+EFA0
2256U+EFA1
2257U+EFA2
2258U+EFA3
2259U+EFA4
2260U+EFA5
2261U+EFA6
2262U+EFA7
2263U+EFA8
2264U+EFA9
2265U+EFAA
2266U+EFAB
2267U+EFAC
2268U+EFAD
2269U+EFAE
2270U+EFAF
2271U+EFB0
2272U+EFB1
2273U+EFB2
2274U+EFB3
2275U+EFB4
2276U+EFB5
2277U+EFB6
2278U+EFB7
2279U+EFB8
2280U+EFB9
2281U+EFBA
2282U+EFBB
2283U+EFBC
2284U+EFBD
2285U+EFBE
2286U+EFBF
2287U+EFC0
2288U+EFC1
2289U+EFC2
2290U+EFC3
2291U+EFC4
2292U+EFC5
2293U+EFC6
2294U+EFC7
2295U+EFC8
2296U+EFC9
2297U+EFCA
2298U+EFCB
2299U+EFCC
2300U+EFCD
2301U+EFCE
2302U+F000
2303U+F001
2304U+F002
2305U+F003
2306U+F004
2307U+F005
2308U+F006
2309U+F007
2310U+F008
2311U+F009
2312U+F00A
2313U+F00B
2314U+F00C
2315U+F00D
2316U+F00E
2317U+F00F
2318U+F010
2319U+F011
2320U+F012
2321U+F013
2322U+F014
2323U+F015
2324U+F016
2325U+F017
2326U+F018
2327U+F019
2328U+F01A
2329U+F01B
2330U+F01C
2331U+F01D
2332U+F01E
2333U+F01F
2334U+F020
2335U+F021
2336U+F022
2337U+F023
2338U+F024
2339U+F025
2340U+F026
2341U+F027
2342U+F028
2343U+F029
2344U+F02A
2345U+F02B
2346U+F02C
2347U+F02D
2348U+F02E
2349U+F02F
2350U+F030
2351U+F031
2352U+F032
2353U+F033
2354U+F034
2355U+F035
2356U+F036
2357U+F037
2358U+F038
2359U+F039
2360U+F03A
2361U+F03B
2362U+F03C
2363U+F03D
2364U+F03E
2365U+F03F
2366U+F040
2367U+F041
2368U+F042
2369U+F043
2370U+F044
2371U+F045
2372U+F046
2373U+F047
2374U+F048
2375U+F049
2376U+F04A
2377U+F04B
2378U+F04C
2379U+F04D
2380U+F04E
2381U+F04F
2382U+F050
2383U+F051
2384U+F052
2385U+F053
2386U+F054
2387U+F055
2388U+F056
2389U+F057
2390U+F058
2391U+F059
2392U+F05A
2393U+F05B
2394U+F05C
2395U+F05D
2396U+F05E
2397U+F05F
2398U+F060
2399U+F061
2400U+F062
2401U+F063
2402U+F064
2403U+F065
2404U+F066
2405U+F067
2406U+F068
2407U+F069
2408U+F06A
2409U+F06B
2410U+F06C
2411U+F06D
2412U+F06E
2413U+F06F
2414U+F070
2415U+F071
2416U+F072
2417U+F073
2418U+F074
2419U+F075
2420U+F076
2421U+F077
2422U+F078
2423U+F079
2424U+F07A
2425U+F07B
2426U+F07C
2427U+F07D
2428U+F07E
2429U+F07F
2430U+F080
2431U+F081
2432U+F082
2433U+F083
2434U+F084
2435U+F085
2436U+F086
2437U+F087
2438U+F088
2439U+F089
2440U+F08A
2441U+F08B
2442U+F08C
2443U+F08D
2444U+F08E
2445U+F08F
2446U+F090
2447U+F091
2448U+F092
2449U+F093
2450U+F094
2451U+F095
2452U+F096
2453U+F097
2454U+F098
2455U+F099
2456U+F09A
2457U+F09B
2458U+F09C
2459U+F09D
2460U+F09E
2461U+F09F
2462U+F0A0
2463U+F0A1
2464U+F0A2
2465U+F0A3
2466U+F0A4
2467U+F0A5
2468U+F0A6
2469U+F0A7
2470U+F0A8
2471U+F0A9
2472U+F0AA
2473U+F0AB
2474U+F0AC
2475U+F0AD
2476U+F0AE
2477U+F0AF
2478U+F0B0
2479U+F0B1
2480U+F0B2
2481U+F0B3
2482U+F0B4
2483U+F0B5
2484U+F0B6
2485U+F0B7
2486U+F0B8
2487U+F0B9
2488U+F0BA
2489U+F0BB
2490U+F0BC
2491U+F0BD
2492U+F0BE
2493U+F0BF
2494U+F0C0
2495U+F0C1
2496U+F0C2
2497U+F0C3
2498U+F0C4
2499U+F0C5
2500U+F0C6
2501U+F0C7
2502U+F0C8
2503U+F0C9
2504U+F0CA
2505U+F0CB
2506U+F0CC
2507U+F0CD
2508U+F0CE
2509U+F0CF
2510U+F0D0
2511U+F0D1
2512U+F0D2
2513U+F0D3
2514U+F0D4
2515U+F0D5
2516U+F0D6
2517U+F0D7
2518U+F0D8
2519U+F0D9
2520U+F0DA
2521U+F0DB
2522U+F0DC
2523U+F0DD
2524U+F0DE
2525U+F0DF
2526U+F0E0
2527U+F0E1
2528U+F0E2
2529U+F0E3
2530U+F0E4
2531U+F0E5
2532U+F0E6
2533U+F0E7
2534U+F0E8
2535U+F0E9
2536U+F0EA
2537U+F0EB
2538U+F0EC
2539U+F0ED
2540U+F0EE
2541U+F0EF
2542U+F0F0
2543U+F0F1
2544U+F0F2
2545U+F0F3
2546U+F0F4
2547U+F0F5
2548U+F0F6
2549U+F0F7
2550U+F0F8
2551U+F0F9
2552U+F0FA
2553U+F0FB
2554U+F0FC
2555U+F0FD
2556U+F0FE
2557U+F0FF
2558U+F100
2559U+F101
2560U+F102
2561U+F103
2562U+F104
2563U+F105
2564U+F106
2565U+F107
2566U+F108
2567U+F109
2568U+F10A
2569U+F10B
2570U+F10C
2571U+F10D
2572U+F10E
2573U+F10F
2574U+F110
2575U+F111
2576U+F112
2577U+F113
2578U+F114
2579U+F115
2580U+F116
2581U+F117
2582U+F118
2583U+F119
2584U+F11A
2585U+F11B
2586U+F11C
2587U+F11D
2588U+F11E
2589U+F11F
2590U+F120
2591U+F121
2592U+F122
2593U+F123
2594U+F124
2595U+F125
2596U+F126
2597U+F127
2598U+F128
2599U+F129
2600U+F12A
2601U+F12B
2602U+F12C
2603U+F12D
2604U+F12E
2605U+F12F
2606U+F130
2607U+F131
2608U+F132
2609U+F133
2610U+F134
2611U+F135
2612U+F136
2613U+F137
2614U+F138
2615U+F139
2616U+F13A
2617U+F13B
2618U+F13C
2619U+F13D
2620U+F13E
2621U+F13F
2622U+F140
2623U+F141
2624U+F142
2625U+F143
2626U+F144
2627U+F145
2628U+F146
2629U+F147
2630U+F148
2631U+F149
2632U+F14A
2633U+F14B
2634U+F14C
2635U+F14D
2636U+F14E
2637U+F14F
2638U+F150
2639U+F151
2640U+F152
2641U+F153
2642U+F154
2643U+F155
2644U+F156
2645U+F157
2646U+F158
2647U+F159
2648U+F15A
2649U+F15B
2650U+F15C
2651U+F15D
2652U+F15E
2653U+F15F
2654U+F160
2655U+F161
2656U+F162
2657U+F163
2658U+F164
2659U+F165
2660U+F166
2661U+F167
2662U+F168
2663U+F169
2664U+F16A
2665U+F16B
2666U+F16C
2667U+F16D
2668U+F16E
2669U+F16F
2670U+F170
2671U+F171
2672U+F172
2673U+F173
2674U+F174
2675U+F175
2676U+F176
2677U+F177
2678U+F178
2679U+F179
2680U+F17A
2681U+F17B
2682U+F17C
2683U+F17D
2684U+F17E
2685U+F17F
2686U+F180
2687U+F181
2688U+F182
2689U+F183
2690U+F184
2691U+F185
2692U+F186
2693U+F187
2694U+F188
2695U+F189
2696U+F18A
2697U+F18B
2698U+F18C
2699U+F18D
2700U+F18E
2701U+F18F
2702U+F190
2703U+F191
2704U+F192
2705U+F193
2706U+F194
2707U+F195
2708U+F196
2709U+F197
2710U+F198
2711U+F199
2712U+F19A
2713U+F19B
2714U+F19C
2715U+F19D
2716U+F19E
2717U+F19F
2718U+F1A0
2719U+F1A1
2720U+F1A2
2721U+F1A3
2722U+F1A4
2723U+F1A5
2724U+F1A6
2725U+F1A7
2726U+F1A8
2727U+F1A9
2728U+F1AA
2729U+F1AB
2730U+F1AC
2731U+F1AD
2732U+F1AE
2733U+F1AF
2734U+F1B0
2735U+F1B1
2736U+F1B2
2737U+F1B3
2738U+F1B4
2739U+F1B5
2740U+F1B6
2741U+F1B7
2742U+F1B8
2743U+F1B9
2744U+F1BA
2745U+F1BB
2746U+F1BC
2747U+F1BD
2748U+F1BE
2749U+F1BF
2750U+F1C0
2751U+F1C1
2752U+F1C2
2753U+F1C3
2754U+F1C4
2755U+F1C5
2756U+F1C6
2757U+F1C7
2758U+F1C8
2759U+F1C9
2760U+F1CA
2761U+F1CB
2762U+F1CC
2763U+F1CD
2764U+F1CE
2765U+F1CF
2766U+F1D0
2767U+F1D1
2768U+F1D2
2769U+F1D3
2770U+F1D4
2771U+F1D5
2772U+F1D6
2773U+F1D7
2774U+F1D8
2775U+F1D9
2776U+F1DA
2777U+F1DB
2778U+F1DC
2779U+F1DD
2780U+F1DE
2781U+F1DF
2782U+F1E0
2783U+F1E1
2784U+F1E2
2785U+F1E3
2786U+F1E4
2787U+F1E5
2788U+F1E6
2789U+F1E7
2790U+F1E8
2791U+F1E9
2792U+F1EA
2793U+F1EB
2794U+F1EC
2795U+F1ED
2796U+F1EE
2797U+F1EF
2798U+F1F0
2799U+F1F1
2800U+F1F2
2801U+F1F3
2802U+F1F4
2803U+F1F5
2804U+F1F6
2805U+F1F7
2806U+F1F8
2807U+F1F9
2808U+F1FA
2809U+F1FB
2810U+F1FC
2811U+F1FD
2812U+F1FE
2813U+F1FF
2814U+F200
2815U+F201
2816U+F202
2817U+F203
2818U+F204
2819U+F205
2820U+F206
2821U+F207
2822U+F208
2823U+F209
2824U+F20A
2825U+F20B
2826U+F20C
2827U+F20D
2828U+F20E
2829U+F20F
2830U+F210
2831U+F211
2832U+F212
2833U+F213
2834U+F214
2835U+F215
2836U+F216
2837U+F217
2838U+F218
2839U+F219
2840U+F21A
2841U+F21B
2842U+F21C
2843U+F21D
2844U+F21E
2845U+F21F
2846U+F220
2847U+F221
2848U+F222
2849U+F223
2850U+F224
2851U+F225
2852U+F226
2853U+F227
2854U+F228
2855U+F229
2856U+F22A
2857U+F22B
2858U+F22C
2859U+F22D
2860U+F22E
2861U+F22F
2862U+F230
2863U+F231
2864U+F232
2865U+F233
2866U+F234
2867U+F235
2868U+F236
2869U+F237
2870U+F238
2871U+F239
2872U+F23A
2873U+F23B
2874U+F23C
2875U+F23D
2876U+F23E
2877U+F23F
2878U+F240
2879U+F241
2880U+F242
2881U+F243
2882U+F244
2883U+F245
2884U+F246
2885U+F247
2886U+F248
2887U+F249
2888U+F24A
2889U+F24B
2890U+F24C
2891U+F24D
2892U+F24E
2893U+F24F
2894U+F250
2895U+F251
2896U+F252
2897U+F253
2898U+F254
2899U+F255
2900U+F256
2901U+F257
2902U+F258
2903U+F259
2904U+F25A
2905U+F25B
2906U+F25C
2907U+F25D
2908U+F25E
2909U+F25F
2910U+F260
2911U+F261
2912U+F262
2913U+F263
2914U+F264
2915U+F265
2916U+F266
2917U+F267
2918U+F268
2919U+F269
2920U+F26A
2921U+F26B
2922U+F26C
2923U+F26D
2924U+F26E
2925U+F26F
2926U+F270
2927U+F271
2928U+F272
2929U+F273
2930U+F274
2931U+F275
2932U+F276
2933U+F277
2934U+F278
2935U+F279
2936U+F27A
2937U+F27B
2938U+F27C
2939U+F27D
2940U+F27E
2941U+F27F
2942U+F280
2943U+F281
2944U+F282
2945U+F283
2946U+F284
2947U+F285
2948U+F286
2949U+F287
2950U+F288
2951U+F289
2952U+F28A
2953U+F28B
2954U+F28C
2955U+F28D
2956U+F28E
2957U+F28F
2958U+F290
2959U+F291
2960U+F292
2961U+F293
2962U+F294
2963U+F295
2964U+F296
2965U+F297
2966U+F298
2967U+F299
2968U+F29A
2969U+F29B
2970U+F29C
2971U+F29D
2972U+F29E
2973U+F29F
2974U+F2A0
2975U+F2A1
2976U+F2A2
2977U+F2A3
2978U+F2A4
2979U+F2A5
2980U+F2A6
2981U+F2A7
2982U+F2A8
2983U+F2A9
2984U+F2AA
2985U+F2AB
2986U+F2AC
2987U+F2AD
2988U+F2AE
2989U+F2AF
2990U+F2B0
2991U+F2B1
2992U+F2B2
2993U+F2B3
2994U+F2B4
2995U+F2B5
2996U+F2B6
2997U+F2B7
2998U+F2B8
2999U+F2B9
3000U+F2BA
3001U+F2BB
3002U+F2BC
3003U+F2BD
3004U+F2BE
3005U+F2BF
3006U+F2C0
3007U+F2C1
3008U+F2C2
3009U+F2C3
3010U+F2C4
3011U+F2C5
3012U+F2C6
3013U+F2C7
3014U+F2C8
3015U+F2C9
3016U+F2CA
3017U+F2CB
3018U+F2CC
3019U+F2CD
3020U+F2CE
3021U+F2CF
3022U+F2D0
3023U+F2D1
3024U+F2D2
3025U+F2D3
3026U+F2D4
3027U+F2D5
3028U+F2D6
3029U+F2D7
3030U+F2D8
3031U+F2D9
3032U+F2DA
3033U+F2DB
3034U+F2DC
3035U+F2DD
3036U+F2DE
3037U+F2DF
3038U+F2E0
3039U+F2E1
3040U+F2E2
3041U+F2E3
3042U+F2E4
3043U+F2E5
3044U+F2E6
3045U+F2E7
3046U+F2E8
3047U+F2E9
3048U+F2EA
3049U+F2EB
3050U+F2EC
3051U+F2ED
3052U+F2EE
3053U+F2EF
3054U+F2F0
3055U+F2F1
3056U+F2F2
3057U+F2F3
3058U+F2F4
3059U+F2F5
3060U+F2F6
3061U+F2F7
3062U+F2F8
3063U+F2F9
3064U+F2FA
3065U+F2FB
3066U+F2FC
3067U+F2FD
3068U+F2FE
3069U+F2FF
3070U+F400
3071U+F401
3072U+F402
3073U+F403
3074U+F404
3075U+F405
3076U+F406
3077U+F407
3078U+F408
3079U+F409
3080U+F40A
3081U+F40B
3082U+F40C
3083U+F40D
3084U+F40E
3085U+F40F
3086U+F410
3087U+F411
3088U+F412
3089U+F413
3090U+F414
3091U+F415
3092U+F416
3093U+F417
3094U+F418
3095U+F419
3096U+F41A
3097U+F41B
3098U+F41C
3099U+F41D
3100U+F41E
3101U+F41F
3102U+F420
3103U+F421
3104U+F422
3105U+F423
3106U+F424
3107U+F425
3108U+F426
3109U+F427
3110U+F428
3111U+F429
3112U+F42A
3113U+F42B
3114U+F42C
3115U+F42D
3116U+F42E
3117U+F42F
3118U+F430
3119U+F431
3120U+F432
3121U+F433
3122U+F434
3123U+F435
3124U+F436
3125U+F437
3126U+F438
3127U+F439
3128U+F43A
3129U+F43B
3130U+F43C
3131U+F43D
3132U+F43E
3133U+F43F
3134U+F440
3135U+F441
3136U+F442
3137U+F443
3138U+F444
3139U+F445
3140U+F446
3141U+F447
3142U+F448
3143U+F449
3144U+F44A
3145U+F44B
3146U+F44C
3147U+F44D
3148U+F44E
3149U+F44F
3150U+F450
3151U+F451
3152U+F452
3153U+F453
3154U+F454
3155U+F455
3156U+F456
3157U+F457
3158U+F458
3159U+F459
3160U+F45A
3161U+F45B
3162U+F45C
3163U+F45D
3164U+F45E
3165U+F45F
3166U+F460
3167U+F461
3168U+F462
3169U+F463
3170U+F464
3171U+F465
3172U+F466
3173U+F467
3174U+F468
3175U+F469
3176U+F46A
3177U+F46B
3178U+F46C
3179U+F46D
3180U+F46E
3181U+F46F
3182U+F470
3183U+F471
3184U+F472
3185U+F473
3186U+F474
3187U+F475
3188U+F476
3189U+F477
3190U+F478
3191U+F479
3192U+F47A
3193U+F47B
3194U+F47C
3195U+F47D
3196U+F47E
3197U+F47F
3198U+F480
3199U+F481
3200U+F482
3201U+F483
3202U+F484
3203U+F485
3204U+F486
3205U+F487
3206U+F488
3207U+F489
3208U+F48A
3209U+F48B
3210U+F48C
3211U+F48D
3212U+F48E
3213U+F48F
3214U+F490
3215U+F491
3216U+F492
3217U+F493
3218U+F494
3219U+F495
3220U+F496
3221U+F497
3222U+F498
3223U+F499
3224U+F49A
3225U+F49B
3226U+F49C
3227U+F49D
3228U+F49E
3229U+F49F
3230U+F4A0
3231U+F4A1
3232U+F4A2
3233U+F4A3
3234U+F4A4
3235U+F4A5
3236U+F4A6
3237U+F4A7
3238U+F4A8
3239U+F4A9
3240U+F4AA
3241U+F4AB
3242U+F4AC
3243U+F4AD
3244U+F4AE
3245U+F4AF
3246U+F4B0
3247U+F4B1
3248U+F4B2
3249U+F4B3
3250U+F4B4
3251U+F4B5
3252U+F4B6
3253U+F4B7
3254U+F4B8
3255U+F4B9
3256U+F4BA
3257U+F4BB
3258U+F4BC
3259U+F4BD
3260U+F4BE
3261U+F4BF
3262U+F4C0
3263U+F4C1
3264U+F4C2
3265U+F4C3
3266U+F4C4
3267U+F4C5
3268U+F4C6
3269U+F4C7
3270U+F4C8
3271U+F4C9
3272U+F4CA
3273U+F4CB
3274U+F4CC
3275U+F4CD
3276U+F4CE
3277U+F4CF
3278U+F4D0
3279U+F4D1
3280U+F4D2
3281U+F4D3
3282U+F4D4
3283U+F4D5
3284U+F4D6
3285U+F4D7
3286U+F4D8
3287U+F4D9
3288U+F4DA
3289U+F4DB
3290U+F4DC
3291U+F4DD
3292U+F4DE
3293U+F4DF
3294U+F4E0
3295U+F4E1
3296U+F4E2
3297U+F4E3
3298U+F4E4
3299U+F4E5
3300U+F4E6
3301U+F4E7
3302U+F4E8
3303U+F4E9
3304U+F4EA
3305U+F4EB
3306U+F4EC
3307U+F4ED
3308U+F4EE
3309U+F4EF
3310U+F4F0
3311U+F4F1
3312U+F4F2
3313U+F4F3
3314U+F4F4
3315U+F4F5
3316U+F4F6
3317U+F4F7
3318U+F4F8
3319U+F4F9
3320U+F4FA
3321U+F4FB
3322U+F4FC
3323U+F4FD
3324U+F4FE
3325U+F4FF
3326U+F500
3327U+F501
3328U+F502
3329U+F503
3330U+F504
3331U+F505
3332U+F506
3333U+F507
3334U+F508
3335U+F509
3336U+F50A
3337U+F50B
3338U+F50C
3339U+F50D
3340U+F50E
3341U+F50F
3342U+F510
3343U+F511
3344U+F512
3345U+F513
3346U+F514
3347U+F515
3348U+F516
3349U+F517
3350U+F518
3351U+F519
3352U+F51A
3353U+F51B
3354U+F51C
3355U+F51D
3356U+F51E
3357U+F51F
3358U+F520
3359U+F521
3360U+F522
3361U+F523
3362U+F524
3363U+F525
3364U+F526
3365U+F527
3366U+F528
3367U+F529
3368U+F52A
3369U+F52B
3370U+F52C
3371U+F52D
3372U+F52E
3373U+F52F
3374U+F530
3375U+F531
3376U+F532
3377U+F533
3378U+F0001
3379U+F0002
3380U+F0003
3381U+F0004
3382U+F0005
3383U+F0006
3384U+F0007
3385U+F0008
3386U+F0009
3387U+F000A
3388U+F000B
3389U+F000C
3390U+F000D
3391U+F000E
3392U+F000F
3393U+F0010
3394U+F0011
3395U+F0012
3396U+F0013
3397U+F0014
3398U+F0015
3399U+F0016
3400U+F0017
3401U+F0018
3402U+F0019
3403U+F001A
3404U+F001B
3405U+F001C
3406U+F001D
3407U+F001E
3408U+F001F
3409U+F0020
3410U+F0021
3411U+F0022
3412U+F0023
3413U+F0024
3414U+F0025
3415U+F0026
3416U+F0027
3417U+F0028
3418U+F0029
3419U+F002A
3420U+F002B
3421U+F002C
3422U+F002D
3423U+F002E
3424U+F002F
3425U+F0030
3426U+F0031
3427U+F0032
3428U+F0033
3429U+F0034
3430U+F0035
3431U+F0036
3432U+F0037
3433U+F0038
3434U+F0039
3435U+F003A
3436U+F003B
3437U+F003C
3438U+F003D
3439U+F003E
3440U+F003F
3441U+F0040
3442U+F0041
3443U+F0042
3444U+F0043
3445U+F0044
3446U+F0045
3447U+F0046
3448U+F0047
3449U+F0048
3450U+F0049
3451U+F004A
3452U+F004B
3453U+F004C
3454U+F004D
3455U+F004E
3456U+F004F
3457U+F0050
3458U+F0051
3459U+F0052
3460U+F0053
3461U+F0054
3462U+F0055
3463U+F0056
3464U+F0057
3465U+F0058
3466U+F0059
3467U+F005A
3468U+F005B
3469U+F005C
3470U+F005D
3471U+F005E
3472U+F005F
3473U+F0060
3474U+F0061
3475U+F0062
3476U+F0063
3477U+F0064
3478U+F0065
3479U+F0066
3480U+F0067
3481U+F0068
3482U+F0069
3483U+F006A
3484U+F006B
3485U+F006C
3486U+F006D
3487U+F006E
3488U+F006F
3489U+F0070
3490U+F0071
3491U+F0072
3492U+F0073
3493U+F0074
3494U+F0075
3495U+F0076
3496U+F0077
3497U+F0078
3498U+F0079
3499U+F007A
3500U+F007B
3501U+F007C
3502U+F007D
3503U+F007E
3504U+F007F
3505U+F0080
3506U+F0081
3507U+F0082
3508U+F0083
3509U+F0084
3510U+F0085
3511U+F0086
3512U+F0087
3513U+F0088
3514U+F0089
3515U+F008A
3516U+F008B
3517U+F008C
3518U+F008D
3519U+F008E
3520U+F008F
3521U+F0090
3522U+F0091
3523U+F0092
3524U+F0093
3525U+F0094
3526U+F0095
3527U+F0096
3528U+F0097
3529U+F0098
3530U+F0099
3531U+F009A
3532U+F009B
3533U+F009C
3534U+F009D
3535U+F009E
3536U+F009F
3537U+F00A0
3538U+F00A1
3539U+F00A2
3540U+F00A3
3541U+F00A4
3542U+F00A5
3543U+F00A6
3544U+F00A7
3545U+F00A8
3546U+F00A9
3547U+F00AA
3548U+F00AB
3549U+F00AC
3550U+F00AD
3551U+F00AE
3552U+F00AF
3553U+F00B0
3554U+F00B1
3555U+F00B2
3556U+F00B3
3557U+F00B4
3558U+F00B5
3559U+F00B6
3560U+F00B7
3561U+F00B8
3562U+F00B9
3563U+F00BA
3564U+F00BB
3565U+F00BC
3566U+F00BD
3567U+F00BE
3568U+F00BF
3569U+F00C0
3570U+F00C1
3571U+F00C2
3572U+F00C3
3573U+F00C4
3574U+F00C5
3575U+F00C6
3576U+F00C7
3577U+F00C8
3578U+F00C9
3579U+F00CA
3580U+F00CB
3581U+F00CC
3582U+F00CD
3583U+F00CE
3584U+F00CF
3585U+F00D0
3586U+F00D1
3587U+F00D2
3588U+F00D3
3589U+F00D4
3590U+F00D5
3591U+F00D6
3592U+F00D7
3593U+F00D8
3594U+F00D9
3595U+F00DA
3596U+F00DB
3597U+F00DC
3598U+F00DD
3599U+F00DE
3600U+F00DF
3601U+F00E0
3602U+F00E1
3603U+F00E2
3604U+F00E3
3605U+F00E4
3606U+F00E5
3607U+F00E6
3608U+F00E7
3609U+F00E8
3610U+F00E9
3611U+F00EA
3612U+F00EB
3613U+F00EC
3614U+F00ED
3615U+F00EE
3616U+F00EF
3617U+F00F0
3618U+F00F1
3619U+F00F2
3620U+F00F3
3621U+F00F4
3622U+F00F5
3623U+F00F6
3624U+F00F7
3625U+F00F8
3626U+F00F9
3627U+F00FA
3628U+F00FB
3629U+F00FC
3630U+F00FD
3631U+F00FE
3632U+F00FF
3633U+F0100
3634U+F0101
3635U+F0102
3636U+F0103
3637U+F0104
3638U+F0105
3639U+F0106
3640U+F0107
3641U+F0108
3642U+F0109
3643U+F010A
3644U+F010B
3645U+F010C
3646U+F010D
3647U+F010E
3648U+F010F
3649U+F0110
3650U+F0111
3651U+F0112
3652U+F0113
3653U+F0114
3654U+F0115
3655U+F0116
3656U+F0117
3657U+F0118
3658U+F0119
3659U+F011A
3660U+F011B
3661U+F011C
3662U+F011D
3663U+F011E
3664U+F011F
3665U+F0120
3666U+F0121
3667U+F0122
3668U+F0123
3669U+F0124
3670U+F0125
3671U+F0126
3672U+F0127
3673U+F0128
3674U+F0129
3675U+F012A
3676U+F012B
3677U+F012C
3678U+F012D
3679U+F012E
3680U+F012F
3681U+F0130
3682U+F0131
3683U+F0132
3684U+F0133
3685U+F0134
3686U+F0135
3687U+F0136
3688U+F0137
3689U+F0138
3690U+F0139
3691U+F013A
3692U+F013B
3693U+F013C
3694U+F013D
3695U+F013E
3696U+F013F
3697U+F0140
3698U+F0141
3699U+F0142
3700U+F0143
3701U+F0144
3702U+F0145
3703U+F0146
3704U+F0147
3705U+F0148
3706U+F0149
3707U+F014A
3708U+F014B
3709U+F014C
3710U+F014D
3711U+F014E
3712U+F014F
3713U+F0150
3714U+F0151
3715U+F0152
3716U+F0153
3717U+F0154
3718U+F0155
3719U+F0156
3720U+F0157
3721U+F0158
3722U+F0159
3723U+F015A
3724U+F015B
3725U+F015C
3726U+F015D
3727U+F015E
3728U+F015F
3729U+F0160
3730U+F0161
3731U+F0162
3732U+F0163
3733U+F0164
3734U+F0165
3735U+F0166
3736U+F0167
3737U+F0168
3738U+F0169
3739U+F016A
3740U+F016B
3741U+F016C
3742U+F016D
3743U+F016E
3744U+F016F
3745U+F0170
3746U+F0171
3747U+F0172
3748U+F0173
3749U+F0174
3750U+F0175
3751U+F0176
3752U+F0177
3753U+F0178
3754U+F0179
3755U+F017A
3756U+F017B
3757U+F017C
3758U+F017D
3759U+F017E
3760U+F017F
3761U+F0180
3762U+F0181
3763U+F0182
3764U+F0183
3765U+F0184
3766U+F0185
3767U+F0186
3768U+F0187
3769U+F0188
3770U+F0189
3771U+F018A
3772U+F018B
3773U+F018C
3774U+F018D
3775U+F018E
3776U+F018F
3777U+F0190
3778U+F0191
3779U+F0192
3780U+F0193
3781U+F0194
3782U+F0195
3783U+F0196
3784U+F0197
3785U+F0198
3786U+F0199
3787U+F019A
3788U+F019B
3789U+F019C
3790U+F019D
3791U+F019E
3792U+F019F
3793U+F01A0
3794U+F01A1
3795U+F01A2
3796U+F01A3
3797U+F01A4
3798U+F01A5
3799U+F01A6
3800U+F01A7
3801U+F01A8
3802U+F01A9
3803U+F01AA
3804U+F01AB
3805U+F01AC
3806U+F01AD
3807U+F01AE
3808U+F01AF
3809U+F01B0
3810U+F01B1
3811U+F01B2
3812U+F01B3
3813U+F01B4
3814U+F01B5
3815U+F01B6
3816U+F01B7
3817U+F01B8
3818U+F01B9
3819U+F01BA
3820U+F01BB
3821U+F01BC
3822U+F01BD
3823U+F01BE
3824U+F01BF
3825U+F01C0
3826U+F01C1
3827U+F01C2
3828U+F01C3
3829U+F01C4
3830U+F01C5
3831U+F01C6
3832U+F01C7
3833U+F01C8
3834U+F01C9
3835U+F01CA
3836U+F01CB
3837U+F01CC
3838U+F01CD
3839U+F01CE
3840U+F01CF
3841U+F01D0
3842U+F01D1
3843U+F01D2
3844U+F01D3
3845U+F01D4
3846U+F01D5
3847U+F01D6
3848U+F01D7
3849U+F01D8
3850U+F01D9
3851U+F01DA
3852U+F01DB
3853U+F01DC
3854U+F01DD
3855U+F01DE
3856U+F01DF
3857U+F01E0
3858U+F01E1
3859U+F01E2
3860U+F01E3
3861U+F01E4
3862U+F01E5
3863U+F01E6
3864U+F01E7
3865U+F01E8
3866U+F01E9
3867U+F01EA
3868U+F01EB
3869U+F01EC
3870U+F01ED
3871U+F01EE
3872U+F01EF
3873U+F01F0
3874U+F01F1
3875U+F01F2
3876U+F01F3
3877U+F01F4
3878U+F01F5
3879U+F01F6
3880U+F01F7
3881U+F01F8
3882U+F01F9
3883U+F01FA
3884U+F01FB
3885U+F01FC
3886U+F01FD
3887U+F01FE
3888U+F01FF
3889U+F0200
3890U+F0201
3891U+F0202
3892U+F0203
3893U+F0204
3894U+F0205
3895U+F0206
3896U+F0207
3897U+F0208
3898U+F0209
3899U+F020A
3900U+F020B
3901U+F020C
3902U+F020D
3903U+F020E
3904U+F020F
3905U+F0210
3906U+F0211
3907U+F0212
3908U+F0213
3909U+F0214
3910U+F0215
3911U+F0216
3912U+F0217
3913U+F0218
3914U+F0219
3915U+F021A
3916U+F021B
3917U+F021C
3918U+F021D
3919U+F021E
3920U+F021F
3921U+F0220
3922U+F0221
3923U+F0222
3924U+F0223
3925U+F0224
3926U+F0225
3927U+F0226
3928U+F0227
3929U+F0228
3930U+F0229
3931U+F022A
3932U+F022B
3933U+F022C
3934U+F022D
3935U+F022E
3936U+F022F
3937U+F0230
3938U+F0231
3939U+F0232
3940U+F0233
3941U+F0234
3942U+F0235
3943U+F0236
3944U+F0237
3945U+F0238
3946U+F0239
3947U+F023A
3948U+F023B
3949U+F023C
3950U+F023D
3951U+F023E
3952U+F023F
3953U+F0240
3954U+F0241
3955U+F0242
3956U+F0243
3957U+F0244
3958U+F0245
3959U+F0246
3960U+F0247
3961U+F0248
3962U+F0249
3963U+F024A
3964U+F024B
3965U+F024C
3966U+F024D
3967U+F024E
3968U+F024F
3969U+F0250
3970U+F0251
3971U+F0252
3972U+F0253
3973U+F0254
3974U+F0255
3975U+F0256
3976U+F0257
3977U+F0258
3978U+F0259
3979U+F025A
3980U+F025B
3981U+F025C
3982U+F025D
3983U+F025E
3984U+F025F
3985U+F0260
3986U+F0261
3987U+F0262
3988U+F0263
3989U+F0264
3990U+F0265
3991U+F0266
3992U+F0267
3993U+F0268
3994U+F0269
3995U+F026A
3996U+F026B
3997U+F026C
3998U+F026D
3999U+F026E
4000U+F026F
4001U+F0270
4002U+F0271
4003U+F0272
4004U+F0273
4005U+F0274
4006U+F0275
4007U+F0276
4008U+F0277
4009U+F0278
4010U+F0279
4011U+F027A
4012U+F027B
4013U+F027C
4014U+F027D
4015U+F027E
4016U+F027F
4017U+F0280
4018U+F0281
4019U+F0282
4020U+F0283
4021U+F0284
4022U+F0285
4023U+F0286
4024U+F0287
4025U+F0288
4026U+F0289
4027U+F028A
4028U+F028B
4029U+F028C
4030U+F028D
4031U+F028E
4032U+F028F
4033U+F0290
4034U+F0291
4035U+F0292
4036U+F0293
4037U+F0294
4038U+F0295
4039U+F0296
4040U+F0297
4041U+F0298
4042U+F0299
4043U+F029A
4044U+F029B
4045U+F029C
4046U+F029D
4047U+F029E
4048U+F029F
4049U+F02A0
4050U+F02A1
4051U+F02A2
4052U+F02A3
4053U+F02A4
4054U+F02A5
4055U+F02A6
4056U+F02A7
4057U+F02A8
4058U+F02A9
4059U+F02AA
4060U+F02AB
4061U+F02AC
4062U+F02AD
4063U+F02AE
4064U+F02AF
4065U+F02B0
4066U+F02B1
4067U+F02B2
4068U+F02B3
4069U+F02B4
4070U+F02B5
4071U+F02B6
4072U+F02B7
4073U+F02B8
4074U+F02B9
4075U+F02BA
4076U+F02BB
4077U+F02BC
4078U+F02BD
4079U+F02BE
4080U+F02BF
4081U+F02C0
4082U+F02C1
4083U+F02C2
4084U+F02C3
4085U+F02C4
4086U+F02C5
4087U+F02C6
4088U+F02C7
4089U+F02C8
4090U+F02C9
4091U+F02CA
4092U+F02CB
4093U+F02CC
4094U+F02CD
4095U+F02CE
4096U+F02CF
4097U+F02D0
4098U+F02D1
4099U+F02D2
4100U+F02D3
4101U+F02D4
4102U+F02D5
4103U+F02D6
4104U+F02D7
4105U+F02D8
4106U+F02D9
4107U+F02DA
4108U+F02DB
4109U+F02DC
4110U+F02DD
4111U+F02DE
4112U+F02DF
4113U+F02E0
4114U+F02E1
4115U+F02E2
4116U+F02E3
4117U+F02E4
4118U+F02E5
4119U+F02E6
4120U+F02E7
4121U+F02E8
4122U+F02E9
4123U+F02EA
4124U+F02EB
4125U+F02EC
4126U+F02ED
4127U+F02EE
4128U+F02EF
4129U+F02F0
4130U+F02F1
4131U+F02F2
4132U+F02F3
4133U+F02F4
4134U+F02F5
4135U+F02F6
4136U+F02F7
4137U+F02F8
4138U+F02F9
4139U+F02FA
4140U+F02FB
4141U+F02FC
4142U+F02FD
4143U+F02FE
4144U+F02FF
4145U+F0300
4146U+F0301
4147U+F0302
4148U+F0303
4149U+F0304
4150U+F0305
4151U+F0306
4152U+F0307
4153U+F0308
4154U+F0309
4155U+F030A
4156U+F030B
4157U+F030C
4158U+F030D
4159U+F030E
4160U+F030F
4161U+F0310
4162U+F0311
4163U+F0312
4164U+F0313
4165U+F0314
4166U+F0315
4167U+F0316
4168U+F0317
4169U+F0318
4170U+F0319
4171U+F031A
4172U+F031B
4173U+F031C
4174U+F031D
4175U+F031E
4176U+F031F
4177U+F0320
4178U+F0321
4179U+F0322
4180U+F0323
4181U+F0324
4182U+F0325
4183U+F0326
4184U+F0327
4185U+F0328
4186U+F0329
4187U+F032A
4188U+F032B
4189U+F032C
4190U+F032D
4191U+F032E
4192U+F032F
4193U+F0330
4194U+F0331
4195U+F0332
4196U+F0333
4197U+F0334
4198U+F0335
4199U+F0336
4200U+F0337
4201U+F0338
4202U+F0339
4203U+F033A
4204U+F033B
4205U+F033C
4206U+F033D
4207U+F033E
4208U+F033F
4209U+F0340
4210U+F0341
4211U+F0342
4212U+F0343
4213U+F0344
4214U+F0345
4215U+F0346
4216U+F0347
4217U+F0348
4218U+F0349
4219U+F034A
4220U+F034B
4221U+F034C
4222U+F034D
4223U+F034E
4224U+F034F
4225U+F0350
4226U+F0351
4227U+F0352
4228U+F0353
4229U+F0354
4230U+F0355
4231U+F0356
4232U+F0357
4233U+F0358
4234U+F0359
4235U+F035A
4236U+F035B
4237U+F035C
4238U+F035D
4239U+F035E
4240U+F035F
4241U+F0360
4242U+F0361
4243U+F0362
4244U+F0363
4245U+F0364
4246U+F0365
4247U+F0366
4248U+F0367
4249U+F0368
4250U+F0369
4251U+F036A
4252U+F036B
4253U+F036C
4254U+F036D
4255U+F036E
4256U+F036F
4257U+F0370
4258U+F0371
4259U+F0372
4260U+F0373
4261U+F0374
4262U+F0375
4263U+F0376
4264U+F0377
4265U+F0378
4266U+F0379
4267U+F037A
4268U+F037B
4269U+F037C
4270U+F037D
4271U+F037E
4272U+F037F
4273U+F0380
4274U+F0381
4275U+F0382
4276U+F0383
4277U+F0384
4278U+F0385
4279U+F0386
4280U+F0387
4281U+F0388
4282U+F0389
4283U+F038A
4284U+F038B
4285U+F038C
4286U+F038D
4287U+F038E
4288U+F038F
4289U+F0390
4290U+F0391
4291U+F0392
4292U+F0393
4293U+F0394
4294U+F0395
4295U+F0396
4296U+F0397
4297U+F0398
4298U+F0399
4299U+F039A
4300U+F039B
4301U+F039C
4302U+F039D
4303U+F039E
4304U+F039F
4305U+F03A0
4306U+F03A1
4307U+F03A2
4308U+F03A3
4309U+F03A4
4310U+F03A5
4311U+F03A6
4312U+F03A7
4313U+F03A8
4314U+F03A9
4315U+F03AA
4316U+F03AB
4317U+F03AC
4318U+F03AD
4319U+F03AE
4320U+F03AF
4321U+F03B0
4322U+F03B1
4323U+F03B2
4324U+F03B3
4325U+F03B4
4326U+F03B5
4327U+F03B6
4328U+F03B7
4329U+F03B8
4330U+F03B9
4331U+F03BA
4332U+F03BB
4333U+F03BC
4334U+F03BD
4335U+F03BE
4336U+F03BF
4337U+F03C0
4338U+F03C1
4339U+F03C2
4340U+F03C3
4341U+F03C4
4342U+F03C5
4343U+F03C6
4344U+F03C7
4345U+F03C8
4346U+F03C9
4347U+F03CA
4348U+F03CB
4349U+F03CC
4350U+F03CD
4351U+F03CE
4352U+F03CF
4353U+F03D0
4354U+F03D1
4355U+F03D2
4356U+F03D3
4357U+F03D4
4358U+F03D5
4359U+F03D6
4360U+F03D7
4361U+F03D8
4362U+F03D9
4363U+F03DA
4364U+F03DB
4365U+F03DC
4366U+F03DD
4367U+F03DE
4368U+F03DF
4369U+F03E0
4370U+F03E1
4371U+F03E2
4372U+F03E3
4373U+F03E4
4374U+F03E5
4375U+F03E6
4376U+F03E7
4377U+F03E8
4378U+F03E9
4379U+F03EA
4380U+F03EB
4381U+F03EC
4382U+F03ED
4383U+F03EE
4384U+F03EF
4385U+F03F0
4386U+F03F1
4387U+F03F2
4388U+F03F3
4389U+F03F4
4390U+F03F5
4391U+F03F6
4392U+F03F7
4393U+F03F8
4394U+F03F9
4395U+F03FA
4396U+F03FB
4397U+F03FC
4398U+F03FD
4399U+F03FE
4400U+F03FF
4401U+F0400
4402U+F0401
4403U+F0402
4404U+F0403
4405U+F0404
4406U+F0405
4407U+F0406
4408U+F0407
4409U+F0408
4410U+F0409
4411U+F040A
4412U+F040B
4413U+F040C
4414U+F040D
4415U+F040E
4416U+F040F
4417U+F0410
4418U+F0411
4419U+F0412
4420U+F0413
4421U+F0414
4422U+F0415
4423U+F0416
4424U+F0417
4425U+F0418
4426U+F0419
4427U+F041A
4428U+F041B
4429U+F041C
4430U+F041D
4431U+F041E
4432U+F041F
4433U+F0420
4434U+F0421
4435U+F0422
4436U+F0423
4437U+F0424
4438U+F0425
4439U+F0426
4440U+F0427
4441U+F0428
4442U+F0429
4443U+F042A
4444U+F042B
4445U+F042C
4446U+F042D
4447U+F042E
4448U+F042F
4449U+F0430
4450U+F0431
4451U+F0432
4452U+F0433
4453U+F0434
4454U+F0435
4455U+F0436
4456U+F0437
4457U+F0438
4458U+F0439
4459U+F043A
4460U+F043B
4461U+F043C
4462U+F043D
4463U+F043E
4464U+F043F
4465U+F0440
4466U+F0441
4467U+F0442
4468U+F0443
4469U+F0444
4470U+F0445
4471U+F0446
4472U+F0447
4473U+F0448
4474U+F0449
4475U+F044A
4476U+F044B
4477U+F044C
4478U+F044D
4479U+F044E
4480U+F044F
4481U+F0450
4482U+F0451
4483U+F0452
4484U+F0453
4485U+F0454
4486U+F0455
4487U+F0456
4488U+F0457
4489U+F0458
4490U+F0459
4491U+F045A
4492U+F045B
4493U+F045C
4494U+F045D
4495U+F045E
4496U+F045F
4497U+F0460
4498U+F0461
4499U+F0462
4500U+F0463
4501U+F0464
4502U+F0465
4503U+F0466
4504U+F0467
4505U+F0468
4506U+F0469
4507U+F046A
4508U+F046B
4509U+F046C
4510U+F046D
4511U+F046E
4512U+F046F
4513U+F0470
4514U+F0471
4515U+F0472
4516U+F0473
4517U+F0474
4518U+F0475
4519U+F0476
4520U+F0477
4521U+F0478
4522U+F0479
4523U+F047A
4524U+F047B
4525U+F047C
4526U+F047D
4527U+F047E
4528U+F047F
4529U+F0480
4530U+F0481
4531U+F0482
4532U+F0483
4533U+F0484
4534U+F0485
4535U+F0486
4536U+F0487
4537U+F0488
4538U+F0489
4539U+F048A
4540U+F048B
4541U+F048C
4542U+F048D
4543U+F048E
4544U+F048F
4545U+F0490
4546U+F0491
4547U+F0492
4548U+F0493
4549U+F0494
4550U+F0495
4551U+F0496
4552U+F0497
4553U+F0498
4554U+F0499
4555U+F049A
4556U+F049B
4557U+F049C
4558U+F049D
4559U+F049E
4560U+F049F
4561U+F04A0
4562U+F04A1
4563U+F04A2
4564U+F04A3
4565U+F04A4
4566U+F04A5
4567U+F04A6
4568U+F04A7
4569U+F04A8
4570U+F04A9
4571U+F04AA
4572U+F04AB
4573U+F04AC
4574U+F04AD
4575U+F04AE
4576U+F04AF
4577U+F04B0
4578U+F04B1
4579U+F04B2
4580U+F04B3
4581U+F04B4
4582U+F04B5
4583U+F04B6
4584U+F04B7
4585U+F04B8
4586U+F04B9
4587U+F04BA
4588U+F04BB
4589U+F04BC
4590U+F04BD
4591U+F04BE
4592U+F04BF
4593U+F04C0
4594U+F04C1
4595U+F04C2
4596U+F04C3
4597U+F04C4
4598U+F04C5
4599U+F04C6
4600U+F04C7
4601U+F04C8
4602U+F04C9
4603U+F04CA
4604U+F04CB
4605U+F04CC
4606U+F04CD
4607U+F04CE
4608U+F04CF
4609U+F04D0
4610U+F04D1
4611U+F04D2
4612U+F04D3
4613U+F04D4
4614U+F04D5
4615U+F04D6
4616U+F04D7
4617U+F04D8
4618U+F04D9
4619U+F04DA
4620U+F04DB
4621U+F04DC
4622U+F04DD
4623U+F04DE
4624U+F04DF
4625U+F04E0
4626U+F04E1
4627U+F04E2
4628U+F04E3
4629U+F04E4
4630U+F04E5
4631U+F04E6
4632U+F04E7
4633U+F04E8
4634U+F04E9
4635U+F04EA
4636U+F04EB
4637U+F04EC
4638U+F04ED
4639U+F04EE
4640U+F04EF
4641U+F04F0
4642U+F04F1
4643U+F04F2
4644U+F04F3
4645U+F04F4
4646U+F04F5
4647U+F04F6
4648U+F04F7
4649U+F04F8
4650U+F04F9
4651U+F04FA
4652U+F04FB
4653U+F04FC
4654U+F04FD
4655U+F04FE
4656U+F04FF
4657U+F0500
4658U+F0501
4659U+F0502
4660U+F0503
4661U+F0504
4662U+F0505
4663U+F0506
4664U+F0507
4665U+F0508
4666U+F0509
4667U+F050A
4668U+F050B
4669U+F050C
4670U+F050D
4671U+F050E
4672U+F050F
4673U+F0510
4674U+F0511
4675U+F0512
4676U+F0513
4677U+F0514
4678U+F0515
4679U+F0516
4680U+F0517
4681U+F0518
4682U+F0519
4683U+F051A
4684U+F051B
4685U+F051C
4686U+F051D
4687U+F051E
4688U+F051F
4689U+F0520
4690U+F0521
4691U+F0522
4692U+F0523
4693U+F0524
4694U+F0525
4695U+F0526
4696U+F0527
4697U+F0528
4698U+F0529
4699U+F052A
4700U+F052B
4701U+F052C
4702U+F052D
4703U+F052E
4704U+F052F
4705U+F0530
4706U+F0531
4707U+F0532
4708U+F0533
4709U+F0534
4710U+F0535
4711U+F0536
4712U+F0537
4713U+F0538
4714U+F0539
4715U+F053A
4716U+F053B
4717U+F053C
4718U+F053D
4719U+F053E
4720U+F053F
4721U+F0540
4722U+F0541
4723U+F0542
4724U+F0543
4725U+F0544
4726U+F0545
4727U+F0546
4728U+F0547
4729U+F0548
4730U+F0549
4731U+F054A
4732U+F054B
4733U+F054C
4734U+F054D
4735U+F054E
4736U+F054F
4737U+F0550
4738U+F0551
4739U+F0552
4740U+F0553
4741U+F0554
4742U+F0555
4743U+F0556
4744U+F0557
4745U+F0558
4746U+F0559
4747U+F055A
4748U+F055B
4749U+F055C
4750U+F055D
4751U+F055E
4752U+F055F
4753U+F0560
4754U+F0561
4755U+F0562
4756U+F0563
4757U+F0564
4758U+F0565
4759U+F0566
4760U+F0567
4761U+F0568
4762U+F0569
4763U+F056A
4764U+F056B
4765U+F056C
4766U+F056D
4767U+F056E
4768U+F056F
4769U+F0570
4770U+F0571
4771U+F0572
4772U+F0573
4773U+F0574
4774U+F0575
4775U+F0576
4776U+F0577
4777U+F0578
4778U+F0579
4779U+F057A
4780U+F057B
4781U+F057C
4782U+F057D
4783U+F057E
4784U+F057F
4785U+F0580
4786U+F0581
4787U+F0582
4788U+F0583
4789U+F0584
4790U+F0585
4791U+F0586
4792U+F0587
4793U+F0588
4794U+F0589
4795U+F058A
4796U+F058B
4797U+F058C
4798U+F058D
4799U+F058E
4800U+F058F
4801U+F0590
4802U+F0591
4803U+F0592
4804U+F0593
4805U+F0594
4806U+F0595
4807U+F0596
4808U+F0597
4809U+F0598
4810U+F0599
4811U+F059A
4812U+F059B
4813U+F059C
4814U+F059D
4815U+F059E
4816U+F059F
4817U+F05A0
4818U+F05A1
4819U+F05A2
4820U+F05A3
4821U+F05A4
4822U+F05A5
4823U+F05A6
4824U+F05A7
4825U+F05A8
4826U+F05A9
4827U+F05AA
4828U+F05AB
4829U+F05AC
4830U+F05AD
4831U+F05AE
4832U+F05AF
4833U+F05B0
4834U+F05B1
4835U+F05B2
4836U+F05B3
4837U+F05B4
4838U+F05B5
4839U+F05B6
4840U+F05B7
4841U+F05B8
4842U+F05B9
4843U+F05BA
4844U+F05BB
4845U+F05BC
4846U+F05BD
4847U+F05BE
4848U+F05BF
4849U+F05C0
4850U+F05C1
4851U+F05C2
4852U+F05C3
4853U+F05C4
4854U+F05C5
4855U+F05C6
4856U+F05C7
4857U+F05C8
4858U+F05C9
4859U+F05CA
4860U+F05CB
4861U+F05CC
4862U+F05CD
4863U+F05CE
4864U+F05CF
4865U+F05D0
4866U+F05D1
4867U+F05D2
4868U+F05D3
4869U+F05D4
4870U+F05D5
4871U+F05D6
4872U+F05D7
4873U+F05D8
4874U+F05D9
4875U+F05DA
4876U+F05DB
4877U+F05DC
4878U+F05DD
4879U+F05DE
4880U+F05DF
4881U+F05E0
4882U+F05E1
4883U+F05E2
4884U+F05E3
4885U+F05E4
4886U+F05E5
4887U+F05E6
4888U+F05E7
4889U+F05E8
4890U+F05E9
4891U+F05EA
4892U+F05EB
4893U+F05EC
4894U+F05ED
4895U+F05EE
4896U+F05EF
4897U+F05F0
4898U+F05F1
4899U+F05F2
4900U+F05F3
4901U+F05F4
4902U+F05F5
4903U+F05F6
4904U+F05F7
4905U+F05F8
4906U+F05F9
4907U+F05FA
4908U+F05FB
4909U+F05FC
4910U+F05FD
4911U+F05FE
4912U+F05FF
4913U+F0600
4914U+F0601
4915U+F0602
4916U+F0603
4917U+F0604
4918U+F0605
4919U+F0606
4920U+F0607
4921U+F0608
4922U+F0609
4923U+F060A
4924U+F060B
4925U+F060C
4926U+F060D
4927U+F060E
4928U+F060F
4929U+F0610
4930U+F0611
4931U+F0612
4932U+F0613
4933U+F0614
4934U+F0615
4935U+F0616
4936U+F0617
4937U+F0618
4938U+F0619
4939U+F061A
4940U+F061B
4941U+F061C
4942U+F061D
4943U+F061E
4944U+F061F
4945U+F0620
4946U+F0621
4947U+F0622
4948U+F0623
4949U+F0624
4950U+F0625
4951U+F0626
4952U+F0627
4953U+F0628
4954U+F0629
4955U+F062A
4956U+F062B
4957U+F062C
4958U+F062D
4959U+F062E
4960U+F062F
4961U+F0630
4962U+F0631
4963U+F0632
4964U+F0633
4965U+F0634
4966U+F0635
4967U+F0636
4968U+F0637
4969U+F0638
4970U+F0639
4971U+F063A
4972U+F063B
4973U+F063C
4974U+F063D
4975U+F063E
4976U+F063F
4977U+F0640
4978U+F0641
4979U+F0642
4980U+F0643
4981U+F0644
4982U+F0645
4983U+F0646
4984U+F0647
4985U+F0648
4986U+F0649
4987U+F064A
4988U+F064B
4989U+F064C
4990U+F064D
4991U+F064E
4992U+F064F
4993U+F0650
4994U+F0651
4995U+F0652
4996U+F0653
4997U+F0654
4998U+F0655
4999U+F0656
5000U+F0657
5001U+F0658
5002U+F0659
5003U+F065A
5004U+F065B
5005U+F065C
5006U+F065D
5007U+F065E
5008U+F065F
5009U+F0660
5010U+F0661
5011U+F0662
5012U+F0663
5013U+F0664
5014U+F0665
5015U+F0666
5016U+F0667
5017U+F0668
5018U+F0669
5019U+F066A
5020U+F066B
5021U+F066C
5022U+F066D
5023U+F066E
5024U+F066F
5025U+F0670
5026U+F0671
5027U+F0672
5028U+F0673
5029U+F0674
5030U+F0675
5031U+F0676
5032U+F0677
5033U+F0678
5034U+F0679
5035U+F067A
5036U+F067B
5037U+F067C
5038U+F067D
5039U+F067E
5040U+F067F
5041U+F0680
5042U+F0681
5043U+F0682
5044U+F0683
5045U+F0684
5046U+F0685
5047U+F0686
5048U+F0687
5049U+F0688
5050U+F0689
5051U+F068A
5052U+F068B
5053U+F068C
5054U+F068D
5055U+F068E
5056U+F068F
5057U+F0690
5058U+F0691
5059U+F0692
5060U+F0693
5061U+F0694
5062U+F0695
5063U+F0696
5064U+F0697
5065U+F0698
5066U+F0699
5067U+F069A
5068U+F069B
5069U+F069C
5070U+F069D
5071U+F069E
5072U+F069F
5073U+F06A0
5074U+F06A1
5075U+F06A2
5076U+F06A3
5077U+F06A4
5078U+F06A5
5079U+F06A6
5080U+F06A7
5081U+F06A8
5082U+F06A9
5083U+F06AA
5084U+F06AB
5085U+F06AC
5086U+F06AD
5087U+F06AE
5088U+F06AF
5089U+F06B0
5090U+F06B1
5091U+F06B2
5092U+F06B3
5093U+F06B4
5094U+F06B5
5095U+F06B6
5096U+F06B7
5097U+F06B8
5098U+F06B9
5099U+F06BA
5100U+F06BB
5101U+F06BC
5102U+F06BD
5103U+F06BE
5104U+F06BF
5105U+F06C0
5106U+F06C1
5107U+F06C2
5108U+F06C3
5109U+F06C4
5110U+F06C5
5111U+F06C6
5112U+F06C7
5113U+F06C8
5114U+F06C9
5115U+F06CA
5116U+F06CB
5117U+F06CC
5118U+F06CD
5119U+F06CE
5120U+F06CF
5121U+F06D0
5122U+F06D1
5123U+F06D2
5124U+F06D3
5125U+F06D4
5126U+F06D5
5127U+F06D6
5128U+F06D7
5129U+F06D8
5130U+F06D9
5131U+F06DA
5132U+F06DB
5133U+F06DC
5134U+F06DD
5135U+F06DE
5136U+F06DF
5137U+F06E0
5138U+F06E1
5139U+F06E2
5140U+F06E3
5141U+F06E4
5142U+F06E5
5143U+F06E6
5144U+F06E7
5145U+F06E8
5146U+F06E9
5147U+F06EA
5148U+F06EB
5149U+F06EC
5150U+F06ED
5151U+F06EE
5152U+F06EF
5153U+F06F0
5154U+F06F1
5155U+F06F2
5156U+F06F3
5157U+F06F4
5158U+F06F5
5159U+F06F6
5160U+F06F7
5161U+F06F8
5162U+F06F9
5163U+F06FA
5164U+F06FB
5165U+F06FC
5166U+F06FD
5167U+F06FE
5168U+F06FF
5169U+F0700
5170U+F0701
5171U+F0702
5172U+F0703
5173U+F0704
5174U+F0705
5175U+F0706
5176U+F0707
5177U+F0708
5178U+F0709
5179U+F070A
5180U+F070B
5181U+F070C
5182U+F070D
5183U+F070E
5184U+F070F
5185U+F0710
5186U+F0711
5187U+F0712
5188U+F0713
5189U+F0714
5190U+F0715
5191U+F0716
5192U+F0717
5193U+F0718
5194U+F0719
5195U+F071A
5196U+F071B
5197U+F071C
5198U+F071D
5199U+F071E
5200U+F071F
5201U+F0720
5202U+F0721
5203U+F0722
5204U+F0723
5205U+F0724
5206U+F0725
5207U+F0726
5208U+F0727
5209U+F0728
5210U+F0729
5211U+F072A
5212U+F072B
5213U+F072C
5214U+F072D
5215U+F072E
5216U+F072F
5217U+F0730
5218U+F0731
5219U+F0732
5220U+F0733
5221U+F0734
5222U+F0735
5223U+F0736
5224U+F0737
5225U+F0738
5226U+F0739
5227U+F073A
5228U+F073B
5229U+F073C
5230U+F073D
5231U+F073E
5232U+F073F
5233U+F0740
5234U+F0741
5235U+F0742
5236U+F0743
5237U+F0744
5238U+F0745
5239U+F0746
5240U+F0747
5241U+F0748
5242U+F0749
5243U+F074A
5244U+F074B
5245U+F074C
5246U+F074D
5247U+F074E
5248U+F074F
5249U+F0750
5250U+F0751
5251U+F0752
5252U+F0753
5253U+F0754
5254U+F0755
5255U+F0756
5256U+F0757
5257U+F0758
5258U+F0759
5259U+F075A
5260U+F075B
5261U+F075C
5262U+F075D
5263U+F075E
5264U+F075F
5265U+F0760
5266U+F0761
5267U+F0762
5268U+F0763
5269U+F0764
5270U+F0765
5271U+F0766
5272U+F0767
5273U+F0768
5274U+F0769
5275U+F076A
5276U+F076B
5277U+F076C
5278U+F076D
5279U+F076E
5280U+F076F
5281U+F0770
5282U+F0771
5283U+F0772
5284U+F0773
5285U+F0774
5286U+F0775
5287U+F0776
5288U+F0777
5289U+F0778
5290U+F0779
5291U+F077A
5292U+F077B
5293U+F077C
5294U+F077D
5295U+F077E
5296U+F077F
5297U+F0780
5298U+F0781
5299U+F0782
5300U+F0783
5301U+F0784
5302U+F0785
5303U+F0786
5304U+F0787
5305U+F0788
5306U+F0789
5307U+F078A
5308U+F078B
5309U+F078C
5310U+F078D
5311U+F078E
5312U+F078F
5313U+F0790
5314U+F0791
5315U+F0792
5316U+F0793
5317U+F0794
5318U+F0795
5319U+F0796
5320U+F0797
5321U+F0798
5322U+F0799
5323U+F079A
5324U+F079B
5325U+F079C
5326U+F079D
5327U+F079E
5328U+F079F
5329U+F07A0
5330U+F07A1
5331U+F07A2
5332U+F07A3
5333U+F07A4
5334U+F07A5
5335U+F07A6
5336U+F07A7
5337U+F07A8
5338U+F07A9
5339U+F07AA
5340U+F07AB
5341U+F07AC
5342U+F07AD
5343U+F07AE
5344U+F07AF
5345U+F07B0
5346U+F07B1
5347U+F07B2
5348U+F07B3
5349U+F07B4
5350U+F07B5
5351U+F07B6
5352U+F07B7
5353U+F07B8
5354U+F07B9
5355U+F07BA
5356U+F07BB
5357U+F07BC
5358U+F07BD
5359U+F07BE
5360U+F07BF
5361U+F07C0
5362U+F07C1
5363U+F07C2
5364U+F07C3
5365U+F07C4
5366U+F07C5
5367U+F07C6
5368U+F07C7
5369U+F07C8
5370U+F07C9
5371U+F07CA
5372U+F07CB
5373U+F07CC
5374U+F07CD
5375U+F07CE
5376U+F07CF
5377U+F07D0
5378U+F07D1
5379U+F07D2
5380U+F07D3
5381U+F07D4
5382U+F07D5
5383U+F07D6
5384U+F07D7
5385U+F07D8
5386U+F07D9
5387U+F07DA
5388U+F07DB
5389U+F07DC
5390U+F07DD
5391U+F07DE
5392U+F07DF
5393U+F07E0
5394U+F07E1
5395U+F07E2
5396U+F07E3
5397U+F07E4
5398U+F07E5
5399U+F07E6
5400U+F07E7
5401U+F07E8
5402U+F07E9
5403U+F07EA
5404U+F07EB
5405U+F07EC
5406U+F07ED
5407U+F07EE
5408U+F07EF
5409U+F07F0
5410U+F07F1
5411U+F07F2
5412U+F07F3
5413U+F07F4
5414U+F07F5
5415U+F07F6
5416U+F07F7
5417U+F07F8
5418U+F07F9
5419U+F07FA
5420U+F07FB
5421U+F07FC
5422U+F07FD
5423U+F07FE
5424U+F07FF
5425U+F0800
5426U+F0801
5427U+F0802
5428U+F0803
5429U+F0804
5430U+F0805
5431U+F0806
5432U+F0807
5433U+F0808
5434U+F0809
5435U+F080A
5436U+F080B
5437U+F080C
5438U+F080D
5439U+F080E
5440U+F080F
5441U+F0810
5442U+F0811
5443U+F0812
5444U+F0813
5445U+F0814
5446U+F0815
5447U+F0816
5448U+F0817
5449U+F0818
5450U+F0819
5451U+F081A
5452U+F081B
5453U+F081C
5454U+F081D
5455U+F081E
5456U+F081F
5457U+F0820
5458U+F0821
5459U+F0822
5460U+F0823
5461U+F0824
5462U+F0825
5463U+F0826
5464U+F0827
5465U+F0828
5466U+F0829
5467U+F082A
5468U+F082B
5469U+F082C
5470U+F082D
5471U+F082E
5472U+F082F
5473U+F0830
5474U+F0831
5475U+F0832
5476U+F0833
5477U+F0834
5478U+F0835
5479U+F0836
5480U+F0837
5481U+F0838
5482U+F0839
5483U+F083A
5484U+F083B
5485U+F083C
5486U+F083D
5487U+F083E
5488U+F083F
5489U+F0840
5490U+F0841
5491U+F0842
5492U+F0843
5493U+F0844
5494U+F0845
5495U+F0846
5496U+F0847
5497U+F0848
5498U+F0849
5499U+F084A
5500U+F084B
5501U+F084C
5502U+F084D
5503U+F084E
5504U+F084F
5505U+F0850
5506U+F0851
5507U+F0852
5508U+F0853
5509U+F0854
5510U+F0855
5511U+F0856
5512U+F0857
5513U+F0858
5514U+F0859
5515U+F085A
5516U+F085B
5517U+F085C
5518U+F085D
5519U+F085E
5520U+F085F
5521U+F0860
5522U+F0861
5523U+F0862
5524U+F0863
5525U+F0864
5526U+F0865
5527U+F0866
5528U+F0867
5529U+F0868
5530U+F0869
5531U+F086A
5532U+F086B
5533U+F086C
5534U+F086D
5535U+F086E
5536U+F086F
5537U+F0870
5538U+F0871
5539U+F0872
5540U+F0873
5541U+F0874
5542U+F0875
5543U+F0876
5544U+F0877
5545U+F0878
5546U+F0879
5547U+F087A
5548U+F087B
5549U+F087C
5550U+F087D
5551U+F087E
5552U+F087F
5553U+F0880
5554U+F0881
5555U+F0882
5556U+F0883
5557U+F0884
5558U+F0885
5559U+F0886
5560U+F0887
5561U+F0888
5562U+F0889
5563U+F088A
5564U+F088B
5565U+F088C
5566U+F088D
5567U+F088E
5568U+F088F
5569U+F0890
5570U+F0891
5571U+F0892
5572U+F0893
5573U+F0894
5574U+F0895
5575U+F0896
5576U+F0897
5577U+F0898
5578U+F0899
5579U+F089A
5580U+F089B
5581U+F089C
5582U+F089D
5583U+F089E
5584U+F089F
5585U+F08A0
5586U+F08A1
5587U+F08A2
5588U+F08A3
5589U+F08A4
5590U+F08A5
5591U+F08A6
5592U+F08A7
5593U+F08A8
5594U+F08A9
5595U+F08AA
5596U+F08AB
5597U+F08AC
5598U+F08AD
5599U+F08AE
5600U+F08AF
5601U+F08B0
5602U+F08B1
5603U+F08B2
5604U+F08B3
5605U+F08B4
5606U+F08B5
5607U+F08B6
5608U+F08B7
5609U+F08B8
5610U+F08B9
5611U+F08BA
5612U+F08BB
5613U+F08BC
5614U+F08BD
5615U+F08BE
5616U+F08BF
5617U+F08C0
5618U+F08C1
5619U+F08C2
5620U+F08C3
5621U+F08C4
5622U+F08C5
5623U+F08C6
5624U+F08C7
5625U+F08C8
5626U+F08C9
5627U+F08CA
5628U+F08CB
5629U+F08CC
5630U+F08CD
5631U+F08CE
5632U+F08CF
5633U+F08D0
5634U+F08D1
5635U+F08D2
5636U+F08D3
5637U+F08D4
5638U+F08D5
5639U+F08D6
5640U+F08D7
5641U+F08D8
5642U+F08D9
5643U+F08DA
5644U+F08DB
5645U+F08DC
5646U+F08DD
5647U+F08DE
5648U+F08DF
5649U+F08E0
5650U+F08E1
5651U+F08E2
5652U+F08E3
5653U+F08E4
5654U+F08E5
5655U+F08E6
5656U+F08E7
5657U+F08E8
5658U+F08E9
5659U+F08EA
5660U+F08EB
5661U+F08EC
5662U+F08ED
5663U+F08EE
5664U+F08EF
5665U+F08F0
5666U+F08F1
5667U+F08F2
5668U+F08F3
5669U+F08F4
5670U+F08F5
5671U+F08F6
5672U+F08F7
5673U+F08F8
5674U+F08F9
5675U+F08FA
5676U+F08FB
5677U+F08FC
5678U+F08FD
5679U+F08FE
5680U+F08FF
5681U+F0900
5682U+F0901
5683U+F0902
5684U+F0903
5685U+F0904
5686U+F0905
5687U+F0906
5688U+F0907
5689U+F0908
5690U+F0909
5691U+F090A
5692U+F090B
5693U+F090C
5694U+F090D
5695U+F090E
5696U+F090F
5697U+F0910
5698U+F0911
5699U+F0912
5700U+F0913
5701U+F0914
5702U+F0915
5703U+F0916
5704U+F0917
5705U+F0918
5706U+F0919
5707U+F091A
5708U+F091B
5709U+F091C
5710U+F091D
5711U+F091E
5712U+F091F
5713U+F0920
5714U+F0921
5715U+F0922
5716U+F0923
5717U+F0924
5718U+F0925
5719U+F0926
5720U+F0927
5721U+F0928
5722U+F0929
5723U+F092A
5724U+F092B
5725U+F092C
5726U+F092D
5727U+F092E
5728U+F092F
5729U+F0930
5730U+F0931
5731U+F0932
5732U+F0933
5733U+F0934
5734U+F0935
5735U+F0936
5736U+F0937
5737U+F0938
5738U+F0939
5739U+F093A
5740U+F093B
5741U+F093C
5742U+F093D
5743U+F093E
5744U+F093F
5745U+F0940
5746U+F0941
5747U+F0942
5748U+F0943
5749U+F0944
5750U+F0945
5751U+F0946
5752U+F0947
5753U+F0948
5754U+F0949
5755U+F094A
5756U+F094B
5757U+F094C
5758U+F094D
5759U+F094E
5760U+F094F
5761U+F0950
5762U+F0951
5763U+F0952
5764U+F0953
5765U+F0954
5766U+F0955
5767U+F0956
5768U+F0957
5769U+F0958
5770U+F0959
5771U+F095A
5772U+F095B
5773U+F095C
5774U+F095D
5775U+F095E
5776U+F095F
5777U+F0960
5778U+F0961
5779U+F0962
5780U+F0963
5781U+F0964
5782U+F0965
5783U+F0966
5784U+F0967
5785U+F0968
5786U+F0969
5787U+F096A
5788U+F096B
5789U+F096C
5790U+F096D
5791U+F096E
5792U+F096F
5793U+F0970
5794U+F0971
5795U+F0972
5796U+F0973
5797U+F0974
5798U+F0975
5799U+F0976
5800U+F0977
5801U+F0978
5802U+F0979
5803U+F097A
5804U+F097B
5805U+F097C
5806U+F097D
5807U+F097E
5808U+F097F
5809U+F0980
5810U+F0981
5811U+F0982
5812U+F0983
5813U+F0984
5814U+F0985
5815U+F0986
5816U+F0987
5817U+F0988
5818U+F0989
5819U+F098A
5820U+F098B
5821U+F098C
5822U+F098D
5823U+F098E
5824U+F098F
5825U+F0990
5826U+F0991
5827U+F0992
5828U+F0993
5829U+F0994
5830U+F0995
5831U+F0996
5832U+F0997
5833U+F0998
5834U+F0999
5835U+F099A
5836U+F099B
5837U+F099C
5838U+F099D
5839U+F099E
5840U+F099F
5841U+F09A0
5842U+F09A1
5843U+F09A2
5844U+F09A3
5845U+F09A4
5846U+F09A5
5847U+F09A6
5848U+F09A7
5849U+F09A8
5850U+F09A9
5851U+F09AA
5852U+F09AB
5853U+F09AC
5854U+F09AD
5855U+F09AE
5856U+F09AF
5857U+F09B0
5858U+F09B1
5859U+F09B2
5860U+F09B3
5861U+F09B4
5862U+F09B5
5863U+F09B6
5864U+F09B7
5865U+F09B8
5866U+F09B9
5867U+F09BA
5868U+F09BB
5869U+F09BC
5870U+F09BD
5871U+F09BE
5872U+F09BF
5873U+F09C0
5874U+F09C1
5875U+F09C2
5876U+F09C3
5877U+F09C4
5878U+F09C5
5879U+F09C6
5880U+F09C7
5881U+F09C8
5882U+F09C9
5883U+F09CA
5884U+F09CB
5885U+F09CC
5886U+F09CD
5887U+F09CE
5888U+F09CF
5889U+F09D0
5890U+F09D1
5891U+F09D2
5892U+F09D3
5893U+F09D4
5894U+F09D5
5895U+F09D6
5896U+F09D7
5897U+F09D8
5898U+F09D9
5899U+F09DA
5900U+F09DB
5901U+F09DC
5902U+F09DD
5903U+F09DE
5904U+F09DF
5905U+F09E0
5906U+F09E1
5907U+F09E2
5908U+F09E3
5909U+F09E4
5910U+F09E5
5911U+F09E6
5912U+F09E7
5913U+F09E8
5914U+F09E9
5915U+F09EA
5916U+F09EB
5917U+F09EC
5918U+F09ED
5919U+F09EE
5920U+F09EF
5921U+F09F0
5922U+F09F1
5923U+F09F2
5924U+F09F3
5925U+F09F4
5926U+F09F5
5927U+F09F6
5928U+F09F7
5929U+F09F8
5930U+F09F9
5931U+F09FA
5932U+F09FB
5933U+F09FC
5934U+F09FD
5935U+F09FE
5936U+F09FF
5937U+F0A00
5938U+F0A01
5939U+F0A02
5940U+F0A03
5941U+F0A04
5942U+F0A05
5943U+F0A06
5944U+F0A07
5945U+F0A08
5946U+F0A09
5947U+F0A0A
5948U+F0A0B
5949U+F0A0C
5950U+F0A0D
5951U+F0A0E
5952U+F0A0F
5953U+F0A10
5954U+F0A11
5955U+F0A12
5956U+F0A13
5957U+F0A14
5958U+F0A15
5959U+F0A16
5960U+F0A17
5961U+F0A18
5962U+F0A19
5963U+F0A1A
5964U+F0A1B
5965U+F0A1C
5966U+F0A1D
5967U+F0A1E
5968U+F0A1F
5969U+F0A20
5970U+F0A21
5971U+F0A22
5972U+F0A23
5973U+F0A24
5974U+F0A25
5975U+F0A26
5976U+F0A27
5977U+F0A28
5978U+F0A29
5979U+F0A2A
5980U+F0A2B
5981U+F0A2C
5982U+F0A2D
5983U+F0A2E
5984U+F0A2F
5985U+F0A30
5986U+F0A31
5987U+F0A32
5988U+F0A33
5989U+F0A34
5990U+F0A35
5991U+F0A36
5992U+F0A37
5993U+F0A38
5994U+F0A39
5995U+F0A3A
5996U+F0A3B
5997U+F0A3C
5998U+F0A3D
5999U+F0A3E
6000U+F0A3F
6001U+F0A40
6002U+F0A41
6003U+F0A42
6004U+F0A43
6005U+F0A44
6006U+F0A45
6007U+F0A46
6008U+F0A47
6009U+F0A48
6010U+F0A49
6011U+F0A4A
6012U+F0A4B
6013U+F0A4C
6014U+F0A4D
6015U+F0A4E
6016U+F0A4F
6017U+F0A50
6018U+F0A51
6019U+F0A52
6020U+F0A53
6021U+F0A54
6022U+F0A55
6023U+F0A56
6024U+F0A57
6025U+F0A58
6026U+F0A59
6027U+F0A5A
6028U+F0A5B
6029U+F0A5C
6030U+F0A5D
6031U+F0A5E
6032U+F0A5F
6033U+F0A60
6034U+F0A61
6035U+F0A62
6036U+F0A63
6037U+F0A64
6038U+F0A65
6039U+F0A66
6040U+F0A67
6041U+F0A68
6042U+F0A69
6043U+F0A6A
6044U+F0A6B
6045U+F0A6C
6046U+F0A6D
6047U+F0A6E
6048U+F0A6F
6049U+F0A70
6050U+F0A71
6051U+F0A72
6052U+F0A73
6053U+F0A74
6054U+F0A75
6055U+F0A76
6056U+F0A77
6057U+F0A78
6058U+F0A79
6059U+F0A7A
6060U+F0A7B
6061U+F0A7C
6062U+F0A7D
6063U+F0A7E
6064U+F0A7F
6065U+F0A80
6066U+F0A81
6067U+F0A82
6068U+F0A83
6069U+F0A84
6070U+F0A85
6071U+F0A86
6072U+F0A87
6073U+F0A88
6074U+F0A89
6075U+F0A8A
6076U+F0A8B
6077U+F0A8C
6078U+F0A8D
6079U+F0A8E
6080U+F0A8F
6081U+F0A90
6082U+F0A91
6083U+F0A92
6084U+F0A93
6085U+F0A94
6086U+F0A95
6087U+F0A96
6088U+F0A97
6089U+F0A98
6090U+F0A99
6091U+F0A9A
6092U+F0A9B
6093U+F0A9C
6094U+F0A9D
6095U+F0A9E
6096U+F0A9F
6097U+F0AA0
6098U+F0AA1
6099U+F0AA2
6100U+F0AA3
6101U+F0AA4
6102U+F0AA5
6103U+F0AA6
6104U+F0AA7
6105U+F0AA8
6106U+F0AA9
6107U+F0AAA
6108U+F0AAB
6109U+F0AAC
6110U+F0AAD
6111U+F0AAE
6112U+F0AAF
6113U+F0AB0
6114U+F0AB1
6115U+F0AB2
6116U+F0AB3
6117U+F0AB4
6118U+F0AB5
6119U+F0AB6
6120U+F0AB7
6121U+F0AB8
6122U+F0AB9
6123U+F0ABA
6124U+F0ABB
6125U+F0ABC
6126U+F0ABD
6127U+F0ABE
6128U+F0ABF
6129U+F0AC0
6130U+F0AC1
6131U+F0AC2
6132U+F0AC3
6133U+F0AC4
6134U+F0AC5
6135U+F0AC6
6136U+F0AC7
6137U+F0AC8
6138U+F0AC9
6139U+F0ACA
6140U+F0ACB
6141U+F0ACC
6142U+F0ACD
6143U+F0ACE
6144U+F0ACF
6145U+F0AD0
6146U+F0AD1
6147U+F0AD2
6148U+F0AD3
6149U+F0AD4
6150U+F0AD5
6151U+F0AD6
6152U+F0AD7
6153U+F0AD8
6154U+F0AD9
6155U+F0ADA
6156U+F0ADB
6157U+F0ADC
6158U+F0ADD
6159U+F0ADE
6160U+F0ADF
6161U+F0AE0
6162U+F0AE1
6163U+F0AE2
6164U+F0AE3
6165U+F0AE4
6166U+F0AE5
6167U+F0AE6
6168U+F0AE7
6169U+F0AE8
6170U+F0AE9
6171U+F0AEA
6172U+F0AEB
6173U+F0AEC
6174U+F0AED
6175U+F0AEE
6176U+F0AEF
6177U+F0AF0
6178U+F0AF1
6179U+F0AF2
6180U+F0AF3
6181U+F0AF4
6182U+F0AF5
6183U+F0AF6
6184U+F0AF7
6185U+F0AF8
6186U+F0AF9
6187U+F0AFA
6188U+F0AFB
6189U+F0AFC
6190U+F0AFD
6191U+F0AFE
6192U+F0AFF
6193U+F0B00
6194U+F0B01
6195U+F0B02
6196U+F0B03
6197U+F0B04
6198U+F0B05
6199U+F0B06
6200U+F0B07
6201U+F0B08
6202U+F0B09
6203U+F0B0A
6204U+F0B0B
6205U+F0B0C
6206U+F0B0D
6207U+F0B0E
6208U+F0B0F
6209U+F0B10
6210U+F0B11
6211U+F0B12
6212U+F0B13
6213U+F0B14
6214U+F0B15
6215U+F0B16
6216U+F0B17
6217U+F0B18
6218U+F0B19
6219U+F0B1A
6220U+F0B1B
6221U+F0B1C
6222U+F0B1D
6223U+F0B1E
6224U+F0B1F
6225U+F0B20
6226U+F0B21
6227U+F0B22
6228U+F0B23
6229U+F0B24
6230U+F0B25
6231U+F0B26
6232U+F0B27
6233U+F0B28
6234U+F0B29
6235U+F0B2A
6236U+F0B2B
6237U+F0B2C
6238U+F0B2D
6239U+F0B2E
6240U+F0B2F
6241U+F0B30
6242U+F0B31
6243U+F0B32
6244U+F0B33
6245U+F0B34
6246U+F0B35
6247U+F0B36
6248U+F0B37
6249U+F0B38
6250U+F0B39
6251U+F0B3A
6252U+F0B3B
6253U+F0B3C
6254U+F0B3D
6255U+F0B3E
6256U+F0B3F
6257U+F0B40
6258U+F0B41
6259U+F0B42
6260U+F0B43
6261U+F0B44
6262U+F0B45
6263U+F0B46
6264U+F0B47
6265U+F0B48
6266U+F0B49
6267U+F0B4A
6268U+F0B4B
6269U+F0B4C
6270U+F0B4D
6271U+F0B4E
6272U+F0B4F
6273U+F0B50
6274U+F0B51
6275U+F0B52
6276U+F0B53
6277U+F0B54
6278U+F0B55
6279U+F0B56
6280U+F0B57
6281U+F0B58
6282U+F0B59
6283U+F0B5A
6284U+F0B5B
6285U+F0B5C
6286U+F0B5D
6287U+F0B5E
6288U+F0B5F
6289U+F0B60
6290U+F0B61
6291U+F0B62
6292U+F0B63
6293U+F0B64
6294U+F0B65
6295U+F0B66
6296U+F0B67
6297U+F0B68
6298U+F0B69
6299U+F0B6A
6300U+F0B6B
6301U+F0B6C
6302U+F0B6D
6303U+F0B6E
6304U+F0B6F
6305U+F0B70
6306U+F0B71
6307U+F0B72
6308U+F0B73
6309U+F0B74
6310U+F0B75
6311U+F0B76
6312U+F0B77
6313U+F0B78
6314U+F0B79
6315U+F0B7A
6316U+F0B7B
6317U+F0B7C
6318U+F0B7D
6319U+F0B7E
6320U+F0B7F
6321U+F0B80
6322U+F0B81
6323U+F0B82
6324U+F0B83
6325U+F0B84
6326U+F0B85
6327U+F0B86
6328U+F0B87
6329U+F0B88
6330U+F0B89
6331U+F0B8A
6332U+F0B8B
6333U+F0B8C
6334U+F0B8D
6335U+F0B8E
6336U+F0B8F
6337U+F0B90
6338U+F0B91
6339U+F0B92
6340U+F0B93
6341U+F0B94
6342U+F0B95
6343U+F0B96
6344U+F0B97
6345U+F0B98
6346U+F0B99
6347U+F0B9A
6348U+F0B9B
6349U+F0B9C
6350U+F0B9D
6351U+F0B9E
6352U+F0B9F
6353U+F0BA0
6354U+F0BA1
6355U+F0BA2
6356U+F0BA3
6357U+F0BA4
6358U+F0BA5
6359U+F0BA6
6360U+F0BA7
6361U+F0BA8
6362U+F0BA9
6363U+F0BAA
6364U+F0BAB
6365U+F0BAC
6366U+F0BAD
6367U+F0BAE
6368U+F0BAF
6369U+F0BB0
6370U+F0BB1
6371U+F0BB2
6372U+F0BB3
6373U+F0BB4
6374U+F0BB5
6375U+F0BB6
6376U+F0BB7
6377U+F0BB8
6378U+F0BB9
6379U+F0BBA
6380U+F0BBB
6381U+F0BBC
6382U+F0BBD
6383U+F0BBE
6384U+F0BBF
6385U+F0BC0
6386U+F0BC1
6387U+F0BC2
6388U+F0BC3
6389U+F0BC4
6390U+F0BC5
6391U+F0BC6
6392U+F0BC7
6393U+F0BC8
6394U+F0BC9
6395U+F0BCA
6396U+F0BCB
6397U+F0BCC
6398U+F0BCD
6399U+F0BCE
6400U+F0BCF
6401U+F0BD0
6402U+F0BD1
6403U+F0BD2
6404U+F0BD3
6405U+F0BD4
6406U+F0BD5
6407U+F0BD6
6408U+F0BD7
6409U+F0BD8
6410U+F0BD9
6411U+F0BDA
6412U+F0BDB
6413U+F0BDC
6414U+F0BDD
6415U+F0BDE
6416U+F0BDF
6417U+F0BE0
6418U+F0BE1
6419U+F0BE2
6420U+F0BE3
6421U+F0BE4
6422U+F0BE5
6423U+F0BE6
6424U+F0BE7
6425U+F0BE8
6426U+F0BE9
6427U+F0BEA
6428U+F0BEB
6429U+F0BEC
6430U+F0BED
6431U+F0BEE
6432U+F0BEF
6433U+F0BF0
6434U+F0BF1
6435U+F0BF2
6436U+F0BF3
6437U+F0BF4
6438U+F0BF5
6439U+F0BF6
6440U+F0BF7
6441U+F0BF8
6442U+F0BF9
6443U+F0BFA
6444U+F0BFB
6445U+F0BFC
6446U+F0BFD
6447U+F0BFE
6448U+F0BFF
6449U+F0C00
6450U+F0C01
6451U+F0C02
6452U+F0C03
6453U+F0C04
6454U+F0C05
6455U+F0C06
6456U+F0C07
6457U+F0C08
6458U+F0C09
6459U+F0C0A
6460U+F0C0B
6461U+F0C0C
6462U+F0C0D
6463U+F0C0E
6464U+F0C0F
6465U+F0C10
6466U+F0C11
6467U+F0C12
6468U+F0C13
6469U+F0C14
6470U+F0C15
6471U+F0C16
6472U+F0C17
6473U+F0C18
6474U+F0C19
6475U+F0C1A
6476U+F0C1B
6477U+F0C1C
6478U+F0C1D
6479U+F0C1E
6480U+F0C1F
6481U+F0C20
6482U+F0C21
6483U+F0C22
6484U+F0C23
6485U+F0C24
6486U+F0C25
6487U+F0C26
6488U+F0C27
6489U+F0C28
6490U+F0C29
6491U+F0C2A
6492U+F0C2B
6493U+F0C2C
6494U+F0C2D
6495U+F0C2E
6496U+F0C2F
6497U+F0C30
6498U+F0C31
6499U+F0C32
6500U+F0C33
6501U+F0C34
6502U+F0C35
6503U+F0C36
6504U+F0C37
6505U+F0C38
6506U+F0C39
6507U+F0C3A
6508U+F0C3B
6509U+F0C3C
6510U+F0C3D
6511U+F0C3E
6512U+F0C3F
6513U+F0C40
6514U+F0C41
6515U+F0C42
6516U+F0C43
6517U+F0C44
6518U+F0C45
6519U+F0C46
6520U+F0C47
6521U+F0C48
6522U+F0C49
6523U+F0C4A
6524U+F0C4B
6525U+F0C4C
6526U+F0C4D
6527U+F0C4E
6528U+F0C4F
6529U+F0C50
6530U+F0C51
6531U+F0C52
6532U+F0C53
6533U+F0C54
6534U+F0C55
6535U+F0C56
6536U+F0C57
6537U+F0C58
6538U+F0C59
6539U+F0C5A
6540U+F0C5B
6541U+F0C5C
6542U+F0C5D
6543U+F0C5E
6544U+F0C5F
6545U+F0C60
6546U+F0C61
6547U+F0C62
6548U+F0C63
6549U+F0C64
6550U+F0C65
6551U+F0C66
6552U+F0C67
6553U+F0C68
6554U+F0C69
6555U+F0C6A
6556U+F0C6B
6557U+F0C6C
6558U+F0C6D
6559U+F0C6E
6560U+F0C6F
6561U+F0C70
6562U+F0C71
6563U+F0C72
6564U+F0C73
6565U+F0C74
6566U+F0C75
6567U+F0C76
6568U+F0C77
6569U+F0C78
6570U+F0C79
6571U+F0C7A
6572U+F0C7B
6573U+F0C7C
6574U+F0C7D
6575U+F0C7E
6576U+F0C7F
6577U+F0C80
6578U+F0C81
6579U+F0C82
6580U+F0C83
6581U+F0C84
6582U+F0C85
6583U+F0C86
6584U+F0C87
6585U+F0C88
6586U+F0C89
6587U+F0C8A
6588U+F0C8B
6589U+F0C8C
6590U+F0C8D
6591U+F0C8E
6592U+F0C8F
6593U+F0C90
6594U+F0C91
6595U+F0C92
6596U+F0C93
6597U+F0C94
6598U+F0C95
6599U+F0C96
6600U+F0C97
6601U+F0C98
6602U+F0C99
6603U+F0C9A
6604U+F0C9B
6605U+F0C9C
6606U+F0C9D
6607U+F0C9E
6608U+F0C9F
6609U+F0CA0
6610U+F0CA1
6611U+F0CA2
6612U+F0CA3
6613U+F0CA4
6614U+F0CA5
6615U+F0CA6
6616U+F0CA7
6617U+F0CA8
6618U+F0CA9
6619U+F0CAA
6620U+F0CAB
6621U+F0CAC
6622U+F0CAD
6623U+F0CAE
6624U+F0CAF
6625U+F0CB0
6626U+F0CB1
6627U+F0CB2
6628U+F0CB3
6629U+F0CB4
6630U+F0CB5
6631U+F0CB6
6632U+F0CB7
6633U+F0CB8
6634U+F0CB9
6635U+F0CBA
6636U+F0CBB
6637U+F0CBC
6638U+F0CBD
6639U+F0CBE
6640U+F0CBF
6641U+F0CC0
6642U+F0CC1
6643U+F0CC2
6644U+F0CC3
6645U+F0CC4
6646U+F0CC5
6647U+F0CC6
6648U+F0CC7
6649U+F0CC8
6650U+F0CC9
6651U+F0CCA
6652U+F0CCB
6653U+F0CCC
6654U+F0CCD
6655U+F0CCE
6656U+F0CCF
6657U+F0CD0
6658U+F0CD1
6659U+F0CD2
6660U+F0CD3
6661U+F0CD4
6662U+F0CD5
6663U+F0CD6
6664U+F0CD7
6665U+F0CD8
6666U+F0CD9
6667U+F0CDA
6668U+F0CDB
6669U+F0CDC
6670U+F0CDD
6671U+F0CDE
6672U+F0CDF
6673U+F0CE0
6674U+F0CE1
6675U+F0CE2
6676U+F0CE3
6677U+F0CE4
6678U+F0CE5
6679U+F0CE6
6680U+F0CE7
6681U+F0CE8
6682U+F0CE9
6683U+F0CEA
6684U+F0CEB
6685U+F0CEC
6686U+F0CED
6687U+F0CEE
6688U+F0CEF
6689U+F0CF0
6690U+F0CF1
6691U+F0CF2
6692U+F0CF3
6693U+F0CF4
6694U+F0CF5
6695U+F0CF6
6696U+F0CF7
6697U+F0CF8
6698U+F0CF9
6699U+F0CFA
6700U+F0CFB
6701U+F0CFC
6702U+F0CFD
6703U+F0CFE
6704U+F0CFF
6705U+F0D00
6706U+F0D01
6707U+F0D02
6708U+F0D03
6709U+F0D04
6710U+F0D05
6711U+F0D06
6712U+F0D07
6713U+F0D08
6714U+F0D09
6715U+F0D0A
6716U+F0D0B
6717U+F0D0C
6718U+F0D0D
6719U+F0D0E
6720U+F0D0F
6721U+F0D10
6722U+F0D11
6723U+F0D12
6724U+F0D13
6725U+F0D14
6726U+F0D15
6727U+F0D16
6728U+F0D17
6729U+F0D18
6730U+F0D19
6731U+F0D1A
6732U+F0D1B
6733U+F0D1C
6734U+F0D1D
6735U+F0D1E
6736U+F0D1F
6737U+F0D20
6738U+F0D21
6739U+F0D22
6740U+F0D23
6741U+F0D24
6742U+F0D25
6743U+F0D26
6744U+F0D27
6745U+F0D28
6746U+F0D29
6747U+F0D2A
6748U+F0D2B
6749U+F0D2C
6750U+F0D2D
6751U+F0D2E
6752U+F0D2F
6753U+F0D30
6754U+F0D31
6755U+F0D32
6756U+F0D33
6757U+F0D34
6758U+F0D35
6759U+F0D36
6760U+F0D37
6761U+F0D38
6762U+F0D39
6763U+F0D3A
6764U+F0D3B
6765U+F0D3C
6766U+F0D3D
6767U+F0D3E
6768U+F0D3F
6769U+F0D40
6770U+F0D41
6771U+F0D42
6772U+F0D43
6773U+F0D44
6774U+F0D45
6775U+F0D46
6776U+F0D47
6777U+F0D48
6778U+F0D49
6779U+F0D4A
6780U+F0D4B
6781U+F0D4C
6782U+F0D4D
6783U+F0D4E
6784U+F0D4F
6785U+F0D50
6786U+F0D51
6787U+F0D52
6788U+F0D53
6789U+F0D54
6790U+F0D55
6791U+F0D56
6792U+F0D57
6793U+F0D58
6794U+F0D59
6795U+F0D5A
6796U+F0D5B
6797U+F0D5C
6798U+F0D5D
6799U+F0D5E
6800U+F0D5F
6801U+F0D60
6802U+F0D61
6803U+F0D62
6804U+F0D63
6805U+F0D64
6806U+F0D65
6807U+F0D66
6808U+F0D67
6809U+F0D68
6810U+F0D69
6811U+F0D6A
6812U+F0D6B
6813U+F0D6C
6814U+F0D6D
6815U+F0D6E
6816U+F0D6F
6817U+F0D70
6818U+F0D71
6819U+F0D72
6820U+F0D73
6821U+F0D74
6822U+F0D75
6823U+F0D76
6824U+F0D77
6825U+F0D78
6826U+F0D79
6827U+F0D7A
6828U+F0D7B
6829U+F0D7C
6830U+F0D7D
6831U+F0D7E
6832U+F0D7F
6833U+F0D80
6834U+F0D81
6835U+F0D82
6836U+F0D83
6837U+F0D84
6838U+F0D85
6839U+F0D86
6840U+F0D87
6841U+F0D88
6842U+F0D89
6843U+F0D8A
6844U+F0D8B
6845U+F0D8C
6846U+F0D8D
6847U+F0D8E
6848U+F0D8F
6849U+F0D90
6850U+F0D91
6851U+F0D92
6852U+F0D93
6853U+F0D94
6854U+F0D95
6855U+F0D96
6856U+F0D97
6857U+F0D98
6858U+F0D99
6859U+F0D9A
6860U+F0D9B
6861U+F0D9C
6862U+F0D9D
6863U+F0D9E
6864U+F0D9F
6865U+F0DA0
6866U+F0DA1
6867U+F0DA2
6868U+F0DA3
6869U+F0DA4
6870U+F0DA5
6871U+F0DA6
6872U+F0DA7
6873U+F0DA8
6874U+F0DA9
6875U+F0DAA
6876U+F0DAB
6877U+F0DAC
6878U+F0DAD
6879U+F0DAE
6880U+F0DAF
6881U+F0DB0
6882U+F0DB1
6883U+F0DB2
6884U+F0DB3
6885U+F0DB4
6886U+F0DB5
6887U+F0DB6
6888U+F0DB7
6889U+F0DB8
6890U+F0DB9
6891U+F0DBA
6892U+F0DBB
6893U+F0DBC
6894U+F0DBD
6895U+F0DBE
6896U+F0DBF
6897U+F0DC0
6898U+F0DC1
6899U+F0DC2
6900U+F0DC3
6901U+F0DC4
6902U+F0DC5
6903U+F0DC6
6904U+F0DC7
6905U+F0DC8
6906U+F0DC9
6907U+F0DCA
6908U+F0DCB
6909U+F0DCC
6910U+F0DCD
6911U+F0DCE
6912U+F0DCF
6913U+F0DD0
6914U+F0DD1
6915U+F0DD2
6916U+F0DD3
6917U+F0DD4
6918U+F0DD5
6919U+F0DD6
6920U+F0DD7
6921U+F0DD8
6922U+F0DD9
6923U+F0DDA
6924U+F0DDB
6925U+F0DDC
6926U+F0DDD
6927U+F0DDE
6928U+F0DDF
6929U+F0DE0
6930U+F0DE1
6931U+F0DE2
6932U+F0DE3
6933U+F0DE4
6934U+F0DE5
6935U+F0DE6
6936U+F0DE7
6937U+F0DE8
6938U+F0DE9
6939U+F0DEA
6940U+F0DEB
6941U+F0DEC
6942U+F0DED
6943U+F0DEE
6944U+F0DEF
6945U+F0DF0
6946U+F0DF1
6947U+F0DF2
6948U+F0DF3
6949U+F0DF4
6950U+F0DF5
6951U+F0DF6
6952U+F0DF7
6953U+F0DF8
6954U+F0DF9
6955U+F0DFA
6956U+F0DFB
6957U+F0DFC
6958U+F0DFD
6959U+F0DFE
6960U+F0DFF
6961U+F0E00
6962U+F0E01
6963U+F0E02
6964U+F0E03
6965U+F0E04
6966U+F0E05
6967U+F0E06
6968U+F0E07
6969U+F0E08
6970U+F0E09
6971U+F0E0A
6972U+F0E0B
6973U+F0E0C
6974U+F0E0D
6975U+F0E0E
6976U+F0E0F
6977U+F0E10
6978U+F0E11
6979U+F0E12
6980U+F0E13
6981U+F0E14
6982U+F0E15
6983U+F0E16
6984U+F0E17
6985U+F0E18
6986U+F0E19
6987U+F0E1A
6988U+F0E1B
6989U+F0E1C
6990U+F0E1D
6991U+F0E1E
6992U+F0E1F
6993U+F0E20
6994U+F0E21
6995U+F0E22
6996U+F0E23
6997U+F0E24
6998U+F0E25
6999U+F0E26
7000U+F0E27
7001U+F0E28
7002U+F0E29
7003U+F0E2A
7004U+F0E2B
7005U+F0E2C
7006U+F0E2D
7007U+F0E2E
7008U+F0E2F
7009U+F0E30
7010U+F0E31
7011U+F0E32
7012U+F0E33
7013U+F0E34
7014U+F0E35
7015U+F0E36
7016U+F0E37
7017U+F0E38
7018U+F0E39
7019U+F0E3A
7020U+F0E3B
7021U+F0E3C
7022U+F0E3D
7023U+F0E3E
7024U+F0E3F
7025U+F0E40
7026U+F0E41
7027U+F0E42
7028U+F0E43
7029U+F0E44
7030U+F0E45
7031U+F0E46
7032U+F0E47
7033U+F0E48
7034U+F0E49
7035U+F0E4A
7036U+F0E4B
7037U+F0E4C
7038U+F0E4D
7039U+F0E4E
7040U+F0E4F
7041U+F0E50
7042U+F0E51
7043U+F0E52
7044U+F0E53
7045U+F0E54
7046U+F0E55
7047U+F0E56
7048U+F0E57
7049U+F0E58
7050U+F0E59
7051U+F0E5A
7052U+F0E5B
7053U+F0E5C
7054U+F0E5D
7055U+F0E5E
7056U+F0E5F
7057U+F0E60
7058U+F0E61
7059U+F0E62
7060U+F0E63
7061U+F0E64
7062U+F0E65
7063U+F0E66
7064U+F0E67
7065U+F0E68
7066U+F0E69
7067U+F0E6A
7068U+F0E6B
7069U+F0E6C
7070U+F0E6D
7071U+F0E6E
7072U+F0E6F
7073U+F0E70
7074U+F0E71
7075U+F0E72
7076U+F0E73
7077U+F0E74
7078U+F0E75
7079U+F0E76
7080U+F0E77
7081U+F0E78
7082U+F0E79
7083U+F0E7A
7084U+F0E7B
7085U+F0E7C
7086U+F0E7D
7087U+F0E7E
7088U+F0E7F
7089U+F0E80
7090U+F0E81
7091U+F0E82
7092U+F0E83
7093U+F0E84
7094U+F0E85
7095U+F0E86
7096U+F0E87
7097U+F0E88
7098U+F0E89
7099U+F0E8A
7100U+F0E8B
7101U+F0E8C
7102U+F0E8D
7103U+F0E8E
7104U+F0E8F
7105U+F0E90
7106U+F0E91
7107U+F0E92
7108U+F0E93
7109U+F0E94
7110U+F0E95
7111U+F0E96
7112U+F0E97
7113U+F0E98
7114U+F0E99
7115U+F0E9A
7116U+F0E9B
7117U+F0E9C
7118U+F0E9D
7119U+F0E9E
7120U+F0E9F
7121U+F0EA0
7122U+F0EA1
7123U+F0EA2
7124U+F0EA3
7125U+F0EA4
7126U+F0EA5
7127U+F0EA6
7128U+F0EA7
7129U+F0EA8
7130U+F0EA9
7131U+F0EAA
7132U+F0EAB
7133U+F0EAC
7134U+F0EAD
7135U+F0EAE
7136U+F0EAF
7137U+F0EB0
7138U+F0EB1
7139U+F0EB2
7140U+F0EB3
7141U+F0EB4
7142U+F0EB5
7143U+F0EB6
7144U+F0EB7
7145U+F0EB8
7146U+F0EB9
7147U+F0EBA
7148U+F0EBB
7149U+F0EBC
7150U+F0EBD
7151U+F0EBE
7152U+F0EBF
7153U+F0EC0
7154U+F0EC1
7155U+F0EC2
7156U+F0EC3
7157U+F0EC4
7158U+F0EC5
7159U+F0EC6
7160U+F0EC7
7161U+F0EC8
7162U+F0EC9
7163U+F0ECA
7164U+F0ECB
7165U+F0ECC
7166U+F0ECD
7167U+F0ECE
7168U+F0ECF
7169U+F0ED0
7170U+F0ED1
7171U+F0ED2
7172U+F0ED3
7173U+F0ED4
7174U+F0ED5
7175U+F0ED6
7176U+F0ED7
7177U+F0ED8
7178U+F0ED9
7179U+F0EDA
7180U+F0EDB
7181U+F0EDC
7182U+F0EDD
7183U+F0EDE
7184U+F0EDF
7185U+F0EE0
7186U+F0EE1
7187U+F0EE2
7188U+F0EE3
7189U+F0EE4
7190U+F0EE5
7191U+F0EE6
7192U+F0EE7
7193U+F0EE8
7194U+F0EE9
7195U+F0EEA
7196U+F0EEB
7197U+F0EEC
7198U+F0EED
7199U+F0EEE
7200U+F0EEF
7201U+F0EF0
7202U+F0EF1
7203U+F0EF2
7204U+F0EF3
7205U+F0EF4
7206U+F0EF5
7207U+F0EF6
7208U+F0EF7
7209U+F0EF8
7210U+F0EF9
7211U+F0EFA
7212U+F0EFB
7213U+F0EFC
7214U+F0EFD
7215U+F0EFE
7216U+F0EFF
7217U+F0F00
7218U+F0F01
7219U+F0F02
7220U+F0F03
7221U+F0F04
7222U+F0F05
7223U+F0F06
7224U+F0F07
7225U+F0F08
7226U+F0F09
7227U+F0F0A
7228U+F0F0B
7229U+F0F0C
7230U+F0F0D
7231U+F0F0E
7232U+F0F0F
7233U+F0F10
7234U+F0F11
7235U+F0F12
7236U+F0F13
7237U+F0F14
7238U+F0F15
7239U+F0F16
7240U+F0F17
7241U+F0F18
7242U+F0F19
7243U+F0F1A
7244U+F0F1B
7245U+F0F1C
7246U+F0F1D
7247U+F0F1E
7248U+F0F1F
7249U+F0F20
7250U+F0F21
7251U+F0F22
7252U+F0F23
7253U+F0F24
7254U+F0F25
7255U+F0F26
7256U+F0F27
7257U+F0F28
7258U+F0F29
7259U+F0F2A
7260U+F0F2B
7261U+F0F2C
7262U+F0F2D
7263U+F0F2E
7264U+F0F2F
7265U+F0F30
7266U+F0F31
7267U+F0F32
7268U+F0F33
7269U+F0F34
7270U+F0F35
7271U+F0F36
7272U+F0F37
7273U+F0F38
7274U+F0F39
7275U+F0F3A
7276U+F0F3B
7277U+F0F3C
7278U+F0F3D
7279U+F0F3E
7280U+F0F3F
7281U+F0F40
7282U+F0F41
7283U+F0F42
7284U+F0F43
7285U+F0F44
7286U+F0F45
7287U+F0F46
7288U+F0F47
7289U+F0F48
7290U+F0F49
7291U+F0F4A
7292U+F0F4B
7293U+F0F4C
7294U+F0F4D
7295U+F0F4E
7296U+F0F4F
7297U+F0F50
7298U+F0F51
7299U+F0F52
7300U+F0F53
7301U+F0F54
7302U+F0F55
7303U+F0F56
7304U+F0F57
7305U+F0F58
7306U+F0F59
7307U+F0F5A
7308U+F0F5B
7309U+F0F5C
7310U+F0F5D
7311U+F0F5E
7312U+F0F5F
7313U+F0F60
7314U+F0F61
7315U+F0F62
7316U+F0F63
7317U+F0F64
7318U+F0F65
7319U+F0F66
7320U+F0F67
7321U+F0F68
7322U+F0F69
7323U+F0F6A
7324U+F0F6B
7325U+F0F6C
7326U+F0F6D
7327U+F0F6E
7328U+F0F6F
7329U+F0F70
7330U+F0F71
7331U+F0F72
7332U+F0F73
7333U+F0F74
7334U+F0F75
7335U+F0F76
7336U+F0F77
7337U+F0F78
7338U+F0F79
7339U+F0F7A
7340U+F0F7B
7341U+F0F7C
7342U+F0F7D
7343U+F0F7E
7344U+F0F7F
7345U+F0F80
7346U+F0F81
7347U+F0F82
7348U+F0F83
7349U+F0F84
7350U+F0F85
7351U+F0F86
7352U+F0F87
7353U+F0F88
7354U+F0F89
7355U+F0F8A
7356U+F0F8B
7357U+F0F8C
7358U+F0F8D
7359U+F0F8E
7360U+F0F8F
7361U+F0F90
7362U+F0F91
7363U+F0F92
7364U+F0F93
7365U+F0F94
7366U+F0F95
7367U+F0F96
7368U+F0F97
7369U+F0F98
7370U+F0F99
7371U+F0F9A
7372U+F0F9B
7373U+F0F9C
7374U+F0F9D
7375U+F0F9E
7376U+F0F9F
7377U+F0FA0
7378U+F0FA1
7379U+F0FA2
7380U+F0FA3
7381U+F0FA4
7382U+F0FA5
7383U+F0FA6
7384U+F0FA7
7385U+F0FA8
7386U+F0FA9
7387U+F0FAA
7388U+F0FAB
7389U+F0FAC
7390U+F0FAD
7391U+F0FAE
7392U+F0FAF
7393U+F0FB0
7394U+F0FB1
7395U+F0FB2
7396U+F0FB3
7397U+F0FB4
7398U+F0FB5
7399U+F0FB6
7400U+F0FB7
7401U+F0FB8
7402U+F0FB9
7403U+F0FBA
7404U+F0FBB
7405U+F0FBC
7406U+F0FBD
7407U+F0FBE
7408U+F0FBF
7409U+F0FC0
7410U+F0FC1
7411U+F0FC2
7412U+F0FC3
7413U+F0FC4
7414U+F0FC5
7415U+F0FC6
7416U+F0FC7
7417U+F0FC8
7418U+F0FC9
7419U+F0FCA
7420U+F0FCB
7421U+F0FCC
7422U+F0FCD
7423U+F0FCE
7424U+F0FCF
7425U+F0FD0
7426U+F0FD1
7427U+F0FD2
7428U+F0FD3
7429U+F0FD4
7430U+F0FD5
7431U+F0FD6
7432U+F0FD7
7433U+F0FD8
7434U+F0FD9
7435U+F0FDA
7436U+F0FDB
7437U+F0FDC
7438U+F0FDD
7439U+F0FDE
7440U+F0FDF
7441U+F0FE0
7442U+F0FE1
7443U+F0FE2
7444U+F0FE3
7445U+F0FE4
7446U+F0FE5
7447U+F0FE6
7448U+F0FE7
7449U+F0FE8
7450U+F0FE9
7451U+F0FEA
7452U+F0FEB
7453U+F0FEC
7454U+F0FED
7455U+F0FEE
7456U+F0FEF
7457U+F0FF0
7458U+F0FF1
7459U+F0FF2
7460U+F0FF3
7461U+F0FF4
7462U+F0FF5
7463U+F0FF6
7464U+F0FF7
7465U+F0FF8
7466U+F0FF9
7467U+F0FFA
7468U+F0FFB
7469U+F0FFC
7470U+F0FFD
7471U+F0FFE
7472U+F0FFF
7473U+F1000
7474U+F1001
7475U+F1002
7476U+F1003
7477U+F1004
7478U+F1005
7479U+F1006
7480U+F1007
7481U+F1008
7482U+F1009
7483U+F100A
7484U+F100B
7485U+F100C
7486U+F100D
7487U+F100E
7488U+F100F
7489U+F1010
7490U+F1011
7491U+F1012
7492U+F1013
7493U+F1014
7494U+F1015
7495U+F1016
7496U+F1017
7497U+F1018
7498U+F1019
7499U+F101A
7500U+F101B
7501U+F101C
7502U+F101D
7503U+F101E
7504U+F101F
7505U+F1020
7506U+F1021
7507U+F1022
7508U+F1023
7509U+F1024
7510U+F1025
7511U+F1026
7512U+F1027
7513U+F1028
7514U+F1029
7515U+F102A
7516U+F102B
7517U+F102C
7518U+F102D
7519U+F102E
7520U+F102F
7521U+F1030
7522U+F1031
7523U+F1032
7524U+F1033
7525U+F1034
7526U+F1035
7527U+F1036
7528U+F1037
7529U+F1038
7530U+F1039
7531U+F103A
7532U+F103B
7533U+F103C
7534U+F103D
7535U+F103E
7536U+F103F
7537U+F1040
7538U+F1041
7539U+F1042
7540U+F1043
7541U+F1044
7542U+F1045
7543U+F1046
7544U+F1047
7545U+F1048
7546U+F1049
7547U+F104A
7548U+F104B
7549U+F104C
7550U+F104D
7551U+F104E
7552U+F104F
7553U+F1050
7554U+F1051
7555U+F1052
7556U+F1053
7557U+F1054
7558U+F1055
7559U+F1056
7560U+F1057
7561U+F1058
7562U+F1059
7563U+F105A
7564U+F105B
7565U+F105C
7566U+F105D
7567U+F105E
7568U+F105F
7569U+F1060
7570U+F1061
7571U+F1062
7572U+F1063
7573U+F1064
7574U+F1065
7575U+F1066
7576U+F1067
7577U+F1068
7578U+F1069
7579U+F106A
7580U+F106B
7581U+F106C
7582U+F106D
7583U+F106E
7584U+F106F
7585U+F1070
7586U+F1071
7587U+F1072
7588U+F1073
7589U+F1074
7590U+F1075
7591U+F1076
7592U+F1077
7593U+F1078
7594U+F1079
7595U+F107A
7596U+F107B
7597U+F107C
7598U+F107D
7599U+F107E
7600U+F107F
7601U+F1080
7602U+F1081
7603U+F1082
7604U+F1083
7605U+F1084
7606U+F1085
7607U+F1086
7608U+F1087
7609U+F1088
7610U+F1089
7611U+F108A
7612U+F108B
7613U+F108C
7614U+F108D
7615U+F108E
7616U+F108F
7617U+F1090
7618U+F1091
7619U+F1092
7620U+F1093
7621U+F1094
7622U+F1095
7623U+F1096
7624U+F1097
7625U+F1098
7626U+F1099
7627U+F109A
7628U+F109B
7629U+F109C
7630U+F109D
7631U+F109E
7632U+F109F
7633U+F10A0
7634U+F10A1
7635U+F10A2
7636U+F10A3
7637U+F10A4
7638U+F10A5
7639U+F10A6
7640U+F10A7
7641U+F10A8
7642U+F10A9
7643U+F10AA
7644U+F10AB
7645U+F10AC
7646U+F10AD
7647U+F10AE
7648U+F10AF
7649U+F10B0
7650U+F10B1
7651U+F10B2
7652U+F10B3
7653U+F10B4
7654U+F10B5
7655U+F10B6
7656U+F10B7
7657U+F10B8
7658U+F10B9
7659U+F10BA
7660U+F10BB
7661U+F10BC
7662U+F10BD
7663U+F10BE
7664U+F10BF
7665U+F10C0
7666U+F10C1
7667U+F10C2
7668U+F10C3
7669U+F10C4
7670U+F10C5
7671U+F10C6
7672U+F10C7
7673U+F10C8
7674U+F10C9
7675U+F10CA
7676U+F10CB
7677U+F10CC
7678U+F10CD
7679U+F10CE
7680U+F10CF
7681U+F10D0
7682U+F10D1
7683U+F10D2
7684U+F10D3
7685U+F10D4
7686U+F10D5
7687U+F10D6
7688U+F10D7
7689U+F10D8
7690U+F10D9
7691U+F10DA
7692U+F10DB
7693U+F10DC
7694U+F10DD
7695U+F10DE
7696U+F10DF
7697U+F10E0
7698U+F10E1
7699U+F10E2
7700U+F10E3
7701U+F10E4
7702U+F10E5
7703U+F10E6
7704U+F10E7
7705U+F10E8
7706U+F10E9
7707U+F10EA
7708U+F10EB
7709U+F10EC
7710U+F10ED
7711U+F10EE
7712U+F10EF
7713U+F10F0
7714U+F10F1
7715U+F10F2
7716U+F10F3
7717U+F10F4
7718U+F10F5
7719U+F10F6
7720U+F10F7
7721U+F10F8
7722U+F10F9
7723U+F10FA
7724U+F10FB
7725U+F10FC
7726U+F10FD
7727U+F10FE
7728U+F10FF
7729U+F1100
7730U+F1101
7731U+F1102
7732U+F1103
7733U+F1104
7734U+F1105
7735U+F1106
7736U+F1107
7737U+F1108
7738U+F1109
7739U+F110A
7740U+F110B
7741U+F110C
7742U+F110D
7743U+F110E
7744U+F110F
7745U+F1110
7746U+F1111
7747U+F1112
7748U+F1113
7749U+F1114
7750U+F1115
7751U+F1116
7752U+F1117
7753U+F1118
7754U+F1119
7755U+F111A
7756U+F111B
7757U+F111C
7758U+F111D
7759U+F111E
7760U+F111F
7761U+F1120
7762U+F1121
7763U+F1122
7764U+F1123
7765U+F1124
7766U+F1125
7767U+F1126
7768U+F1127
7769U+F1128
7770U+F1129
7771U+F112A
7772U+F112B
7773U+F112C
7774U+F112D
7775U+F112E
7776U+F112F
7777U+F1130
7778U+F1131
7779U+F1132
7780U+F1133
7781U+F1134
7782U+F1135
7783U+F1136
7784U+F1137
7785U+F1138
7786U+F1139
7787U+F113A
7788U+F113B
7789U+F113C
7790U+F113D
7791U+F113E
7792U+F113F
7793U+F1140
7794U+F1141
7795U+F1142
7796U+F1143
7797U+F1144
7798U+F1145
7799U+F1146
7800U+F1147
7801U+F1148
7802U+F1149
7803U+F114A
7804U+F114B
7805U+F114C
7806U+F114D
7807U+F114E
7808U+F114F
7809U+F1150
7810U+F1151
7811U+F1152
7812U+F1153
7813U+F1154
7814U+F1155
7815U+F1156
7816U+F1157
7817U+F1158
7818U+F1159
7819U+F115A
7820U+F115B
7821U+F115C
7822U+F115D
7823U+F115E
7824U+F115F
7825U+F1160
7826U+F1161
7827U+F1162
7828U+F1163
7829U+F1164
7830U+F1165
7831U+F1166
7832U+F1167
7833U+F1168
7834U+F1169
7835U+F116A
7836U+F116B
7837U+F116C
7838U+F116D
7839U+F116E
7840U+F116F
7841U+F1170
7842U+F1171
7843U+F1172
7844U+F1173
7845U+F1174
7846U+F1175
7847U+F1176
7848U+F1177
7849U+F1178
7850U+F1179
7851U+F117A
7852U+F117B
7853U+F117C
7854U+F117D
7855U+F117E
7856U+F117F
7857U+F1180
7858U+F1181
7859U+F1182
7860U+F1183
7861U+F1184
7862U+F1185
7863U+F1186
7864U+F1187
7865U+F1188
7866U+F1189
7867U+F118A
7868U+F118B
7869U+F118C
7870U+F118D
7871U+F118E
7872U+F118F
7873U+F1190
7874U+F1191
7875U+F1192
7876U+F1193
7877U+F1194
7878U+F1195
7879U+F1196
7880U+F1197
7881U+F1198
7882U+F1199
7883U+F119A
7884U+F119B
7885U+F119C
7886U+F119D
7887U+F119E
7888U+F119F
7889U+F11A0
7890U+F11A1
7891U+F11A2
7892U+F11A3
7893U+F11A4
7894U+F11A5
7895U+F11A6
7896U+F11A7
7897U+F11A8
7898U+F11A9
7899U+F11AA
7900U+F11AB
7901U+F11AC
7902U+F11AD
7903U+F11AE
7904U+F11AF
7905U+F11B0
7906U+F11B1
7907U+F11B2
7908U+F11B3
7909U+F11B4
7910U+F11B5
7911U+F11B6
7912U+F11B7
7913U+F11B8
7914U+F11B9
7915U+F11BA
7916U+F11BB
7917U+F11BC
7918U+F11BD
7919U+F11BE
7920U+F11BF
7921U+F11C0
7922U+F11C1
7923U+F11C2
7924U+F11C3
7925U+F11C4
7926U+F11C5
7927U+F11C6
7928U+F11C7
7929U+F11C8
7930U+F11C9
7931U+F11CA
7932U+F11CB
7933U+F11CC
7934U+F11CD
7935U+F11CE
7936U+F11CF
7937U+F11D0
7938U+F11D1
7939U+F11D2
7940U+F11D3
7941U+F11D4
7942U+F11D5
7943U+F11D6
7944U+F11D7
7945U+F11D8
7946U+F11D9
7947U+F11DA
7948U+F11DB
7949U+F11DC
7950U+F11DD
7951U+F11DE
7952U+F11DF
7953U+F11E0
7954U+F11E1
7955U+F11E2
7956U+F11E3
7957U+F11E4
7958U+F11E5
7959U+F11E6
7960U+F11E7
7961U+F11E8
7962U+F11E9
7963U+F11EA
7964U+F11EB
7965U+F11EC
7966U+F11ED
7967U+F11EE
7968U+F11EF
7969U+F11F0
7970U+F11F1
7971U+F11F2
7972U+F11F3
7973U+F11F4
7974U+F11F5
7975U+F11F6
7976U+F11F7
7977U+F11F8
7978U+F11F9
7979U+F11FA
7980U+F11FB
7981U+F11FC
7982U+F11FD
7983U+F11FE
7984U+F11FF
7985U+F1200
7986U+F1201
7987U+F1202
7988U+F1203
7989U+F1204
7990U+F1205
7991U+F1206
7992U+F1207
7993U+F1208
7994U+F1209
7995U+F120A
7996U+F120B
7997U+F120C
7998U+F120D
7999U+F120E
8000U+F120F
8001U+F1210
8002U+F1211
8003U+F1212
8004U+F1213
8005U+F1214
8006U+F1215
8007U+F1216
8008U+F1217
8009U+F1218
8010U+F1219
8011U+F121A
8012U+F121B
8013U+F121C
8014U+F121D
8015U+F121E
8016U+F121F
8017U+F1220
8018U+F1221
8019U+F1222
8020U+F1223
8021U+F1224
8022U+F1225
8023U+F1226
8024U+F1227
8025U+F1228
8026U+F1229
8027U+F122A
8028U+F122B
8029U+F122C
8030U+F122D
8031U+F122E
8032U+F122F
8033U+F1230
8034U+F1231
8035U+F1232
8036U+F1233
8037U+F1234
8038U+F1235
8039U+F1236
8040U+F1237
8041U+F1238
8042U+F1239
8043U+F123A
8044U+F123B
8045U+F123C
8046U+F123D
8047U+F123E
8048U+F123F
8049U+F1240
8050U+F1241
8051U+F1242
8052U+F1243
8053U+F1244
8054U+F1245
8055U+F1246
8056U+F1247
8057U+F1248
8058U+F1249
8059U+F124A
8060U+F124B
8061U+F124C
8062U+F124D
8063U+F124E
8064U+F124F
8065U+F1250
8066U+F1251
8067U+F1252
8068U+F1253
8069U+F1254
8070U+F1255
8071U+F1256
8072U+F1257
8073U+F1258
8074U+F1259
8075U+F125A
8076U+F125B
8077U+F125C
8078U+F125D
8079U+F125E
8080U+F125F
8081U+F1260
8082U+F1261
8083U+F1262
8084U+F1263
8085U+F1264
8086U+F1265
8087U+F1266
8088U+F1267
8089U+F1268
8090U+F1269
8091U+F126A
8092U+F126B
8093U+F126C
8094U+F126D
8095U+F126E
8096U+F126F
8097U+F1270
8098U+F1271
8099U+F1272
8100U+F1273
8101U+F1274
8102U+F1275
8103U+F1276
8104U+F1277
8105U+F1278
8106U+F1279
8107U+F127A
8108U+F127B
8109U+F127C
8110U+F127D
8111U+F127E
8112U+F127F
8113U+F1280
8114U+F1281
8115U+F1282
8116U+F1283
8117U+F1284
8118U+F1285
8119U+F1286
8120U+F1287
8121U+F1288
8122U+F1289
8123U+F128A
8124U+F128B
8125U+F128C
8126U+F128D
8127U+F128E
8128U+F128F
8129U+F1290
8130U+F1291
8131U+F1292
8132U+F1293
8133U+F1294
8134U+F1295
8135U+F1296
8136U+F1297
8137U+F1298
8138U+F1299
8139U+F129A
8140U+F129B
8141U+F129C
8142U+F129D
8143U+F129E
8144U+F129F
8145U+F12A0
8146U+F12A1
8147U+F12A2
8148U+F12A3
8149U+F12A4
8150U+F12A5
8151U+F12A6
8152U+F12A7
8153U+F12A8
8154U+F12A9
8155U+F12AA
8156U+F12AB
8157U+F12AC
8158U+F12AD
8159U+F12AE
8160U+F12AF
8161U+F12B0
8162U+F12B1
8163U+F12B2
8164U+F12B3
8165U+F12B4
8166U+F12B5
8167U+F12B6
8168U+F12B7
8169U+F12B8
8170U+F12B9
8171U+F12BA
8172U+F12BB
8173U+F12BC
8174U+F12BD
8175U+F12BE
8176U+F12BF
8177U+F12C0
8178U+F12C1
8179U+F12C2
8180U+F12C3
8181U+F12C4
8182U+F12C5
8183U+F12C6
8184U+F12C7
8185U+F12C8
8186U+F12C9
8187U+F12CA
8188U+F12CB
8189U+F12CC
8190U+F12CD
8191U+F12CE
8192U+F12CF
8193U+F12D0
8194U+F12D1
8195U+F12D2
8196U+F12D3
8197U+F12D4
8198U+F12D5
8199U+F12D6
8200U+F12D7
8201U+F12D8
8202U+F12D9
8203U+F12DA
8204U+F12DB
8205U+F12DC
8206U+F12DD
8207U+F12DE
8208U+F12DF
8209U+F12E0
8210U+F12E1
8211U+F12E2
8212U+F12E3
8213U+F12E4
8214U+F12E5
8215U+F12E6
8216U+F12E7
8217U+F12E8
8218U+F12E9
8219U+F12EA
8220U+F12EB
8221U+F12EC
8222U+F12ED
8223U+F12EE
8224U+F12EF
8225U+F12F0
8226U+F12F1
8227U+F12F2
8228U+F12F3
8229U+F12F4
8230U+F12F5
8231U+F12F6
8232U+F12F7
8233U+F12F8
8234U+F12F9
8235U+F12FA
8236U+F12FB
8237U+F12FC
8238U+F12FD
8239U+F12FE
8240U+F12FF
8241U+F1300
8242U+F1301
8243U+F1302
8244U+F1303
8245U+F1304
8246U+F1305
8247U+F1306
8248U+F1307
8249U+F1308
8250U+F1309
8251U+F130A
8252U+F130B
8253U+F130C
8254U+F130D
8255U+F130E
8256U+F130F
8257U+F1310
8258U+F1311
8259U+F1312
8260U+F1313
8261U+F1314
8262U+F1315
8263U+F1316
8264U+F1317
8265U+F1318
8266U+F1319
8267U+F131A
8268U+F131B
8269U+F131C
8270U+F131D
8271U+F131E
8272U+F131F
8273U+F1320
8274U+F1321
8275U+F1322
8276U+F1323
8277U+F1324
8278U+F1325
8279U+F1326
8280U+F1327
8281U+F1328
8282U+F1329
8283U+F132A
8284U+F132B
8285U+F132C
8286U+F132D
8287U+F132E
8288U+F132F
8289U+F1330
8290U+F1331
8291U+F1332
8292U+F1333
8293U+F1334
8294U+F1335
8295U+F1336
8296U+F1337
8297U+F1338
8298U+F1339
8299U+F133A
8300U+F133B
8301U+F133C
8302U+F133D
8303U+F133E
8304U+F133F
8305U+F1340
8306U+F1341
8307U+F1342
8308U+F1343
8309U+F1344
8310U+F1345
8311U+F1346
8312U+F1347
8313U+F1348
8314U+F1349
8315U+F134A
8316U+F134B
8317U+F134C
8318U+F134D
8319U+F134E
8320U+F134F
8321U+F1350
8322U+F1351
8323U+F1352
8324U+F1353
8325U+F1354
8326U+F1355
8327U+F1356
8328U+F1357
8329U+F1358
8330U+F1359
8331U+F135A
8332U+F135B
8333U+F135C
8334U+F135D
8335U+F135E
8336U+F135F
8337U+F1360
8338U+F1361
8339U+F1362
8340U+F1363
8341U+F1364
8342U+F1365
8343U+F1366
8344U+F1367
8345U+F1368
8346U+F1369
8347U+F136A
8348U+F136B
8349U+F136C
8350U+F136D
8351U+F136E
8352U+F136F
8353U+F1370
8354U+F1371
8355U+F1372
8356U+F1373
8357U+F1374
8358U+F1375
8359U+F1376
8360U+F1377
8361U+F1378
8362U+F1379
8363U+F137A
8364U+F137B
8365U+F137C
8366U+F137D
8367U+F137E
8368U+F137F
8369U+F1380
8370U+F1381
8371U+F1382
8372U+F1383
8373U+F1384
8374U+F1385
8375U+F1386
8376U+F1387
8377U+F1388
8378U+F1389
8379U+F138A
8380U+F138B
8381U+F138C
8382U+F138D
8383U+F138E
8384U+F138F
8385U+F1390
8386U+F1391
8387U+F1392
8388U+F1393
8389U+F1394
8390U+F1395
8391U+F1396
8392U+F1397
8393U+F1398
8394U+F1399
8395U+F139A
8396U+F139B
8397U+F139C
8398U+F139D
8399U+F139E
8400U+F139F
8401U+F13A0
8402U+F13A1
8403U+F13A2
8404U+F13A3
8405U+F13A4
8406U+F13A5
8407U+F13A6
8408U+F13A7
8409U+F13A8
8410U+F13A9
8411U+F13AA
8412U+F13AB
8413U+F13AC
8414U+F13AD
8415U+F13AE
8416U+F13AF
8417U+F13B0
8418U+F13B1
8419U+F13B2
8420U+F13B3
8421U+F13B4
8422U+F13B5
8423U+F13B6
8424U+F13B7
8425U+F13B8
8426U+F13B9
8427U+F13BA
8428U+F13BB
8429U+F13BC
8430U+F13BD
8431U+F13BE
8432U+F13BF
8433U+F13C0
8434U+F13C1
8435U+F13C2
8436U+F13C3
8437U+F13C4
8438U+F13C5
8439U+F13C6
8440U+F13C7
8441U+F13C8
8442U+F13C9
8443U+F13CA
8444U+F13CB
8445U+F13CC
8446U+F13CD
8447U+F13CE
8448U+F13CF
8449U+F13D0
8450U+F13D1
8451U+F13D2
8452U+F13D3
8453U+F13D4
8454U+F13D5
8455U+F13D6
8456U+F13D7
8457U+F13D8
8458U+F13D9
8459U+F13DA
8460U+F13DB
8461U+F13DC
8462U+F13DD
8463U+F13DE
8464U+F13DF
8465U+F13E0
8466U+F13E1
8467U+F13E2
8468U+F13E3
8469U+F13E4
8470U+F13E5
8471U+F13E6
8472U+F13E7
8473U+F13E8
8474U+F13E9
8475U+F13EA
8476U+F13EB
8477U+F13EC
8478U+F13ED
8479U+F13EE
8480U+F13EF
8481U+F13F0
8482U+F13F1
8483U+F13F2
8484U+F13F3
8485U+F13F4
8486U+F13F5
8487U+F13F6
8488U+F13F7
8489U+F13F8
8490U+F13F9
8491U+F13FA
8492U+F13FB
8493U+F13FC
8494U+F13FD
8495U+F13FE
8496U+F13FF
8497U+F1400
8498U+F1401
8499U+F1402
8500U+F1403
8501U+F1404
8502U+F1405
8503U+F1406
8504U+F1407
8505U+F1408
8506U+F1409
8507U+F140A
8508U+F140B
8509U+F140C
8510U+F140D
8511U+F140E
8512U+F140F
8513U+F1410
8514U+F1411
8515U+F1412
8516U+F1413
8517U+F1414
8518U+F1415
8519U+F1416
8520U+F1417
8521U+F1418
8522U+F1419
8523U+F141A
8524U+F141B
8525U+F141C
8526U+F141D
8527U+F141E
8528U+F141F
8529U+F1420
8530U+F1421
8531U+F1422
8532U+F1423
8533U+F1424
8534U+F1425
8535U+F1426
8536U+F1427
8537U+F1428
8538U+F1429
8539U+F142A
8540U+F142B
8541U+F142C
8542U+F142D
8543U+F142E
8544U+F142F
8545U+F1430
8546U+F1431
8547U+F1432
8548U+F1433
8549U+F1434
8550U+F1435
8551U+F1436
8552U+F1437
8553U+F1438
8554U+F1439
8555U+F143A
8556U+F143B
8557U+F143C
8558U+F143D
8559U+F143E
8560U+F143F
8561U+F1440
8562U+F1441
8563U+F1442
8564U+F1443
8565U+F1444
8566U+F1445
8567U+F1446
8568U+F1447
8569U+F1448
8570U+F1449
8571U+F144A
8572U+F144B
8573U+F144C
8574U+F144D
8575U+F144E
8576U+F144F
8577U+F1450
8578U+F1451
8579U+F1452
8580U+F1453
8581U+F1454
8582U+F1455
8583U+F1456
8584U+F1457
8585U+F1458
8586U+F1459
8587U+F145A
8588U+F145B
8589U+F145C
8590U+F145D
8591U+F145E
8592U+F145F
8593U+F1460
8594U+F1461
8595U+F1462
8596U+F1463
8597U+F1464
8598U+F1465
8599U+F1466
8600U+F1467
8601U+F1468
8602U+F1469
8603U+F146A
8604U+F146B
8605U+F146C
8606U+F146D
8607U+F146E
8608U+F146F
8609U+F1470
8610U+F1471
8611U+F1472
8612U+F1473
8613U+F1474
8614U+F1475
8615U+F1476
8616U+F1477
8617U+F1478
8618U+F1479
8619U+F147A
8620U+F147B
8621U+F147C
8622U+F147D
8623U+F147E
8624U+F147F
8625U+F1480
8626U+F1481
8627U+F1482
8628U+F1483
8629U+F1484
8630U+F1485
8631U+F1486
8632U+F1487
8633U+F1488
8634U+F1489
8635U+F148A
8636U+F148B
8637U+F148C
8638U+F148D
8639U+F148E
8640U+F148F
8641U+F1490
8642U+F1491
8643U+F1492
8644U+F1493
8645U+F1494
8646U+F1495
8647U+F1496
8648U+F1497
8649U+F1498
8650U+F1499
8651U+F149A
8652U+F149B
8653U+F149C
8654U+F149D
8655U+F149E
8656U+F149F
8657U+F14A0
8658U+F14A1
8659U+F14A2
8660U+F14A3
8661U+F14A4
8662U+F14A5
8663U+F14A6
8664U+F14A7
8665U+F14A8
8666U+F14A9
8667U+F14AA
8668U+F14AB
8669U+F14AC
8670U+F14AD
8671U+F14AE
8672U+F14AF
8673U+F14B0
8674U+F14B1
8675U+F14B2
8676U+F14B3
8677U+F14B4
8678U+F14B5
8679U+F14B6
8680U+F14B7
8681U+F14B8
8682U+F14B9
8683U+F14BA
8684U+F14BB
8685U+F14BC
8686U+F14BD
8687U+F14BE
8688U+F14BF
8689U+F14C0
8690U+F14C1
8691U+F14C2
8692U+F14C3
8693U+F14C4
8694U+F14C5
8695U+F14C6
8696U+F14C7
8697U+F14C8
8698U+F14C9
8699U+F14CA
8700U+F14CB
8701U+F14CC
8702U+F14CD
8703U+F14CE
8704U+F14CF
8705U+F14D0
8706U+F14D1
8707U+F14D2
8708U+F14D3
8709U+F14D4
8710U+F14D5
8711U+F14D6
8712U+F14D7
8713U+F14D8
8714U+F14D9
8715U+F14DA
8716U+F14DB
8717U+F14DC
8718U+F14DD
8719U+F14DE
8720U+F14DF
8721U+F14E0
8722U+F14E1
8723U+F14E2
8724U+F14E3
8725U+F14E4
8726U+F14E5
8727U+F14E6
8728U+F14E7
8729U+F14E8
8730U+F14E9
8731U+F14EA
8732U+F14EB
8733U+F14EC
8734U+F14ED
8735U+F14EE
8736U+F14EF
8737U+F14F0
8738U+F14F1
8739U+F14F2
8740U+F14F3
8741U+F14F4
8742U+F14F5
8743U+F14F6
8744U+F14F7
8745U+F14F8
8746U+F14F9
8747U+F14FA
8748U+F14FB
8749U+F14FC
8750U+F14FD
8751U+F14FE
8752U+F14FF
8753U+F1500
8754U+F1501
8755U+F1502
8756U+F1503
8757U+F1504
8758U+F1505
8759U+F1506
8760U+F1507
8761U+F1508
8762U+F1509
8763U+F150A
8764U+F150B
8765U+F150C
8766U+F150D
8767U+F150E
8768U+F150F
8769U+F1510
8770U+F1511
8771U+F1512
8772U+F1513
8773U+F1514
8774U+F1515
8775U+F1516
8776U+F1517
8777U+F1518
8778U+F1519
8779U+F151A
8780U+F151B
8781U+F151C
8782U+F151D
8783U+F151E
8784U+F151F
8785U+F1520
8786U+F1521
8787U+F1522
8788U+F1523
8789U+F1524
8790U+F1525
8791U+F1526
8792U+F1527
8793U+F1528
8794U+F1529
8795U+F152A
8796U+F152B
8797U+F152C
8798U+F152D
8799U+F152E
8800U+F152F
8801U+F1530
8802U+F1531
8803U+F1532
8804U+F1533
8805U+F1534
8806U+F1535
8807U+F1536
8808U+F1537
8809U+F1538
8810U+F1539
8811U+F153A
8812U+F153B
8813U+F153C
8814U+F153D
8815U+F153E
8816U+F153F
8817U+F1540
8818U+F1541
8819U+F1542
8820U+F1543
8821U+F1544
8822U+F1545
8823U+F1546
8824U+F1547
8825U+F1548
8826U+F1549
8827U+F154A
8828U+F154B
8829U+F154C
8830U+F154D
8831U+F154E
8832U+F154F
8833U+F1550
8834U+F1551
8835U+F1552
8836U+F1553
8837U+F1554
8838U+F1555
8839U+F1556
8840U+F1557
8841U+F1558
8842U+F1559
8843U+F155A
8844U+F155B
8845U+F155C
8846U+F155D
8847U+F155E
8848U+F155F
8849U+F1560
8850U+F1561
8851U+F1562
8852U+F1563
8853U+F1564
8854U+F1565
8855U+F1566
8856U+F1567
8857U+F1568
8858U+F1569
8859U+F156A
8860U+F156B
8861U+F156C
8862U+F156D
8863U+F156E
8864U+F156F
8865U+F1570
8866U+F1571
8867U+F1572
8868U+F1573
8869U+F1574
8870U+F1575
8871U+F1576
8872U+F1577
8873U+F1578
8874U+F1579
8875U+F157A
8876U+F157B
8877U+F157C
8878U+F157D
8879U+F157E
8880U+F157F
8881U+F1580
8882U+F1581
8883U+F1582
8884U+F1583
8885U+F1584
8886U+F1585
8887U+F1586
8888U+F1587
8889U+F1588
8890U+F1589
8891U+F158A
8892U+F158B
8893U+F158C
8894U+F158D
8895U+F158E
8896U+F158F
8897U+F1590
8898U+F1591
8899U+F1592
8900U+F1593
8901U+F1594
8902U+F1595
8903U+F1596
8904U+F1597
8905U+F1598
8906U+F1599
8907U+F159A
8908U+F159B
8909U+F159C
8910U+F159D
8911U+F159E
8912U+F159F
8913U+F15A0
8914U+F15A1
8915U+F15A2
8916U+F15A3
8917U+F15A4
8918U+F15A5
8919U+F15A6
8920U+F15A7
8921U+F15A8
8922U+F15A9
8923U+F15AA
8924U+F15AB
8925U+F15AC
8926U+F15AD
8927U+F15AE
8928U+F15AF
8929U+F15B0
8930U+F15B1
8931U+F15B2
8932U+F15B3
8933U+F15B4
8934U+F15B5
8935U+F15B6
8936U+F15B7
8937U+F15B8
8938U+F15B9
8939U+F15BA
8940U+F15BB
8941U+F15BC
8942U+F15BD
8943U+F15BE
8944U+F15BF
8945U+F15C0
8946U+F15C1
8947U+F15C2
8948U+F15C3
8949U+F15C4
8950U+F15C5
8951U+F15C6
8952U+F15C7
8953U+F15C8
8954U+F15C9
8955U+F15CA
8956U+F15CB
8957U+F15CC
8958U+F15CD
8959U+F15CE
8960U+F15CF
8961U+F15D0
8962U+F15D1
8963U+F15D2
8964U+F15D3
8965U+F15D4
8966U+F15D5
8967U+F15D6
8968U+F15D7
8969U+F15D8
8970U+F15D9
8971U+F15DA
8972U+F15DB
8973U+F15DC
8974U+F15DD
8975U+F15DE
8976U+F15DF
8977U+F15E0
8978U+F15E1
8979U+F15E2
8980U+F15E3
8981U+F15E4
8982U+F15E5
8983U+F15E6
8984U+F15E7
8985U+F15E8
8986U+F15E9
8987U+F15EA
8988U+F15EB
8989U+F15EC
8990U+F15ED
8991U+F15EE
8992U+F15EF
8993U+F15F0
8994U+F15F1
8995U+F15F2
8996U+F15F3
8997U+F15F4
8998U+F15F5
8999U+F15F6
9000U+F15F7
9001U+F15F8
9002U+F15F9
9003U+F15FA
9004U+F15FB
9005U+F15FC
9006U+F15FD
9007U+F15FE
9008U+F15FF
9009U+F1600
9010U+F1601
9011U+F1602
9012U+F1603
9013U+F1604
9014U+F1605
9015U+F1606
9016U+F1607
9017U+F1608
9018U+F1609
9019U+F160A
9020U+F160B
9021U+F160C
9022U+F160D
9023U+F160E
9024U+F160F
9025U+F1610
9026U+F1611
9027U+F1612
9028U+F1613
9029U+F1614
9030U+F1615
9031U+F1616
9032U+F1617
9033U+F1618
9034U+F1619
9035U+F161A
9036U+F161B
9037U+F161C
9038U+F161D
9039U+F161E
9040U+F161F
9041U+F1620
9042U+F1621
9043U+F1622
9044U+F1623
9045U+F1624
9046U+F1625
9047U+F1626
9048U+F1627
9049U+F1628
9050U+F1629
9051U+F162A
9052U+F162B
9053U+F162C
9054U+F162D
9055U+F162E
9056U+F162F
9057U+F1630
9058U+F1631
9059U+F1632
9060U+F1633
9061U+F1634
9062U+F1635
9063U+F1636
9064U+F1637
9065U+F1638
9066U+F1639
9067U+F163A
9068U+F163B
9069U+F163C
9070U+F163D
9071U+F163E
9072U+F163F
9073U+F1640
9074U+F1641
9075U+F1642
9076U+F1643
9077U+F1644
9078U+F1645
9079U+F1646
9080U+F1647
9081U+F1648
9082U+F1649
9083U+F164A
9084U+F164B
9085U+F164C
9086U+F164D
9087U+F164E
9088U+F164F
9089U+F1650
9090U+F1651
9091U+F1652
9092U+F1653
9093U+F1654
9094U+F1655
9095U+F1656
9096U+F1657
9097U+F1658
9098U+F1659
9099U+F165A
9100U+F165B
9101U+F165C
9102U+F165D
9103U+F165E
9104U+F165F
9105U+F1660
9106U+F1661
9107U+F1662
9108U+F1663
9109U+F1664
9110U+F1665
9111U+F1666
9112U+F1667
9113U+F1668
9114U+F1669
9115U+F166A
9116U+F166B
9117U+F166C
9118U+F166D
9119U+F166E
9120U+F166F
9121U+F1670
9122U+F1671
9123U+F1672
9124U+F1673
9125U+F1674
9126U+F1675
9127U+F1676
9128U+F1677
9129U+F1678
9130U+F1679
9131U+F167A
9132U+F167B
9133U+F167C
9134U+F167D
9135U+F167E
9136U+F167F
9137U+F1680
9138U+F1681
9139U+F1682
9140U+F1683
9141U+F1684
9142U+F1685
9143U+F1686
9144U+F1687
9145U+F1688
9146U+F1689
9147U+F168A
9148U+F168B
9149U+F168C
9150U+F168D
9151U+F168E
9152U+F168F
9153U+F1690
9154U+F1691
9155U+F1692
9156U+F1693
9157U+F1694
9158U+F1695
9159U+F1696
9160U+F1697
9161U+F1698
9162U+F1699
9163U+F169A
9164U+F169B
9165U+F169C
9166U+F169D
9167U+F169E
9168U+F169F
9169U+F16A0
9170U+F16A1
9171U+F16A2
9172U+F16A3
9173U+F16A4
9174U+F16A5
9175U+F16A6
9176U+F16A7
9177U+F16A8
9178U+F16A9
9179U+F16AA
9180U+F16AB
9181U+F16AC
9182U+F16AD
9183U+F16AE
9184U+F16AF
9185U+F16B0
9186U+F16B1
9187U+F16B2
9188U+F16B3
9189U+F16B4
9190U+F16B5
9191U+F16B6
9192U+F16B7
9193U+F16B8
9194U+F16B9
9195U+F16BA
9196U+F16BB
9197U+F16BC
9198U+F16BD
9199U+F16BE
9200U+F16BF
9201U+F16C0
9202U+F16C1
9203U+F16C2
9204U+F16C3
9205U+F16C4
9206U+F16C5
9207U+F16C6
9208U+F16C7
9209U+F16C8
9210U+F16C9
9211U+F16CA
9212U+F16CB
9213U+F16CC
9214U+F16CD
9215U+F16CE
9216U+F16CF
9217U+F16D0
9218U+F16D1
9219U+F16D2
9220U+F16D3
9221U+F16D4
9222U+F16D5
9223U+F16D6
9224U+F16D7
9225U+F16D8
9226U+F16D9
9227U+F16DA
9228U+F16DB
9229U+F16DC
9230U+F16DD
9231U+F16DE
9232U+F16DF
9233U+F16E0
9234U+F16E1
9235U+F16E2
9236U+F16E3
9237U+F16E4
9238U+F16E5
9239U+F16E6
9240U+F16E7
9241U+F16E8
9242U+F16E9
9243U+F16EA
9244U+F16EB
9245U+F16EC
9246U+F16ED
9247U+F16EE
9248U+F16EF
9249U+F16F0
9250U+F16F1
9251U+F16F2
9252U+F16F3
9253U+F16F4
9254U+F16F5
9255U+F16F6
9256U+F16F7
9257U+F16F8
9258U+F16F9
9259U+F16FA
9260U+F16FB
9261U+F16FC
9262U+F16FD
9263U+F16FE
9264U+F16FF
9265U+F1700
9266U+F1701
9267U+F1702
9268U+F1703
9269U+F1704
9270U+F1705
9271U+F1706
9272U+F1707
9273U+F1708
9274U+F1709
9275U+F170A
9276U+F170B
9277U+F170C
9278U+F170D
9279U+F170E
9280U+F170F
9281U+F1710
9282U+F1711
9283U+F1712
9284U+F1713
9285U+F1714
9286U+F1715
9287U+F1716
9288U+F1717
9289U+F1718
9290U+F1719
9291U+F171A
9292U+F171B
9293U+F171C
9294U+F171D
9295U+F171E
9296U+F171F
9297U+F1720
9298U+F1721
9299U+F1722
9300U+F1723
9301U+F1724
9302U+F1725
9303U+F1726
9304U+F1727
9305U+F1728
9306U+F1729
9307U+F172A
9308U+F172B
9309U+F172C
9310U+F172D
9311U+F172E
9312U+F172F
9313U+F1730
9314U+F1731
9315U+F1732
9316U+F1733
9317U+F1734
9318U+F1735
9319U+F1736
9320U+F1737
9321U+F1738
9322U+F1739
9323U+F173A
9324U+F173B
9325U+F173C
9326U+F173D
9327U+F173E
9328U+F173F
9329U+F1740
9330U+F1741
9331U+F1742
9332U+F1743
9333U+F1744
9334U+F1745
9335U+F1746
9336U+F1747
9337U+F1748
9338U+F1749
9339U+F174A
9340U+F174B
9341U+F174C
9342U+F174D
9343U+F174E
9344U+F174F
9345U+F1750
9346U+F1751
9347U+F1752
9348U+F1753
9349U+F1754
9350U+F1755
9351U+F1756
9352U+F1757
9353U+F1758
9354U+F1759
9355U+F175A
9356U+F175B
9357U+F175C
9358U+F175D
9359U+F175E
9360U+F175F
9361U+F1760
9362U+F1761
9363U+F1762
9364U+F1763
9365U+F1764
9366U+F1765
9367U+F1766
9368U+F1767
9369U+F1768
9370U+F1769
9371U+F176A
9372U+F176B
9373U+F176C
9374U+F176D
9375U+F176E
9376U+F176F
9377U+F1770
9378U+F1771
9379U+F1772
9380U+F1773
9381U+F1774
9382U+F1775
9383U+F1776
9384U+F1777
9385U+F1778
9386U+F1779
9387U+F177A
9388U+F177B
9389U+F177C
9390U+F177D
9391U+F177E
9392U+F177F
9393U+F1780
9394U+F1781
9395U+F1782
9396U+F1783
9397U+F1784
9398U+F1785
9399U+F1786
9400U+F1787
9401U+F1788
9402U+F1789
9403U+F178A
9404U+F178B
9405U+F178C
9406U+F178D
9407U+F178E
9408U+F178F
9409U+F1790
9410U+F1791
9411U+F1792
9412U+F1793
9413U+F1794
9414U+F1795
9415U+F1796
9416U+F1797
9417U+F1798
9418U+F1799
9419U+F179A
9420U+F179B
9421U+F179C
9422U+F179D
9423U+F179E
9424U+F179F
9425U+F17A0
9426U+F17A1
9427U+F17A2
9428U+F17A3
9429U+F17A4
9430U+F17A5
9431U+F17A6
9432U+F17A7
9433U+F17A8
9434U+F17A9
9435U+F17AA
9436U+F17AB
9437U+F17AC
9438U+F17AD
9439U+F17AE
9440U+F17AF
9441U+F17B0
9442U+F17B1
9443U+F17B2
9444U+F17B3
9445U+F17B4
9446U+F17B5
9447U+F17B6
9448U+F17B7
9449U+F17B8
9450U+F17B9
9451U+F17BA
9452U+F17BB
9453U+F17BC
9454U+F17BD
9455U+F17BE
9456U+F17BF
9457U+F17C0
9458U+F17C1
9459U+F17C2
9460U+F17C3
9461U+F17C4
9462U+F17C5
9463U+F17C6
9464U+F17C7
9465U+F17C8
9466U+F17C9
9467U+F17CA
9468U+F17CB
9469U+F17CC
9470U+F17CD
9471U+F17CE
9472U+F17CF
9473U+F17D0
9474U+F17D1
9475U+F17D2
9476U+F17D3
9477U+F17D4
9478U+F17D5
9479U+F17D6
9480U+F17D7
9481U+F17D8
9482U+F17D9
9483U+F17DA
9484U+F17DB
9485U+F17DC
9486U+F17DD
9487U+F17DE
9488U+F17DF
9489U+F17E0
9490U+F17E1
9491U+F17E2
9492U+F17E3
9493U+F17E4
9494U+F17E5
9495U+F17E6
9496U+F17E7
9497U+F17E8
9498U+F17E9
9499U+F17EA
9500U+F17EB
9501U+F17EC
9502U+F17ED
9503U+F17EE
9504U+F17EF
9505U+F17F0
9506U+F17F1
9507U+F17F2
9508U+F17F3
9509U+F17F4
9510U+F17F5
9511U+F17F6
9512U+F17F7
9513U+F17F8
9514U+F17F9
9515U+F17FA
9516U+F17FB
9517U+F17FC
9518U+F17FD
9519U+F17FE
9520U+F17FF
9521U+F1800
9522U+F1801
9523U+F1802
9524U+F1803
9525U+F1804
9526U+F1805
9527U+F1806
9528U+F1807
9529U+F1808
9530U+F1809
9531U+F180A
9532U+F180B
9533U+F180C
9534U+F180D
9535U+F180E
9536U+F180F
9537U+F1810
9538U+F1811
9539U+F1812
9540U+F1813
9541U+F1814
9542U+F1815
9543U+F1816
9544U+F1817
9545U+F1818
9546U+F1819
9547U+F181A
9548U+F181B
9549U+F181C
9550U+F181D
9551U+F181E
9552U+F181F
9553U+F1820
9554U+F1821
9555U+F1822
9556U+F1823
9557U+F1824
9558U+F1825
9559U+F1826
9560U+F1827
9561U+F1828
9562U+F1829
9563U+F182A
9564U+F182B
9565U+F182C
9566U+F182D
9567U+F182E
9568U+F182F
9569U+F1830
9570U+F1831
9571U+F1832
9572U+F1833
9573U+F1834
9574U+F1835
9575U+F1836
9576U+F1837
9577U+F1838
9578U+F1839
9579U+F183A
9580U+F183B
9581U+F183C
9582U+F183D
9583U+F183E
9584U+F183F
9585U+F1840
9586U+F1841
9587U+F1842
9588U+F1843
9589U+F1844
9590U+F1845
9591U+F1846
9592U+F1847
9593U+F1848
9594U+F1849
9595U+F184A
9596U+F184B
9597U+F184C
9598U+F184D
9599U+F184E
9600U+F184F
9601U+F1850
9602U+F1851
9603U+F1852
9604U+F1853
9605U+F1854
9606U+F1855
9607U+F1856
9608U+F1857
9609U+F1858
9610U+F1859
9611U+F185A
9612U+F185B
9613U+F185C
9614U+F185D
9615U+F185E
9616U+F185F
9617U+F1860
9618U+F1861
9619U+F1862
9620U+F1863
9621U+F1864
9622U+F1865
9623U+F1866
9624U+F1867
9625U+F1868
9626U+F1869
9627U+F186A
9628U+F186B
9629U+F186C
9630U+F186D
9631U+F186E
9632U+F186F
9633U+F1870
9634U+F1871
9635U+F1872
9636U+F1873
9637U+F1874
9638U+F1875
9639U+F1876
9640U+F1877
9641U+F1878
9642U+F1879
9643U+F187A
9644U+F187B
9645U+F187C
9646U+F187D
9647U+F187E
9648U+F187F
9649U+F1880
9650U+F1881
9651U+F1882
9652U+F1883
9653U+F1884
9654U+F1885
9655U+F1886
9656U+F1887
9657U+F1888
9658U+F1889
9659U+F188A
9660U+F188B
9661U+F188C
9662U+F188D
9663U+F188E
9664U+F188F
9665U+F1890
9666U+F1891
9667U+F1892
9668U+F1893
9669U+F1894
9670U+F1895
9671U+F1896
9672U+F1897
9673U+F1898
9674U+F1899
9675U+F189A
9676U+F189B
9677U+F189C
9678U+F189D
9679U+F189E
9680U+F189F
9681U+F18A0
9682U+F18A1
9683U+F18A2
9684U+F18A3
9685U+F18A4
9686U+F18A5
9687U+F18A6
9688U+F18A7
9689U+F18A8
9690U+F18A9
9691U+F18AA
9692U+F18AB
9693U+F18AC
9694U+F18AD
9695U+F18AE
9696U+F18AF
9697U+F18B0
9698U+F18B1
9699U+F18B2
9700U+F18B3
9701U+F18B4
9702U+F18B5
9703U+F18B6
9704U+F18B7
9705U+F18B8
9706U+F18B9
9707U+F18BA
9708U+F18BB
9709U+F18BC
9710U+F18BD
9711U+F18BE
9712U+F18BF
9713U+F18C0
9714U+F18C1
9715U+F18C2
9716U+F18C3
9717U+F18C4
9718U+F18C5
9719U+F18C6
9720U+F18C7
9721U+F18C8
9722U+F18C9
9723U+F18CA
9724U+F18CB
9725U+F18CC
9726U+F18CD
9727U+F18CE
9728U+F18CF
9729U+F18D0
9730U+F18D1
9731U+F18D2
9732U+F18D3
9733U+F18D4
9734U+F18D5
9735U+F18D6
9736U+F18D7
9737U+F18D8
9738U+F18D9
9739U+F18DA
9740U+F18DB
9741U+F18DC
9742U+F18DD
9743U+F18DE
9744U+F18DF
9745U+F18E0
9746U+F18E1
9747U+F18E2
9748U+F18E3
9749U+F18E4
9750U+F18E5
9751U+F18E6
9752U+F18E7
9753U+F18E8
9754U+F18E9
9755U+F18EA
9756U+F18EB
9757U+F18EC
9758U+F18ED
9759U+F18EE
9760U+F18EF
9761U+F18F0
9762U+F18F1
9763U+F18F2
9764U+F18F3
9765U+F18F4
9766U+F18F5
9767U+F18F6
9768U+F18F7
9769U+F18F8
9770U+F18F9
9771U+F18FA
9772U+F18FB
9773U+F18FC
9774U+F18FD
9775U+F18FE
9776U+F18FF
9777U+F1900
9778U+F1901
9779U+F1902
9780U+F1903
9781U+F1904
9782U+F1905
9783U+F1906
9784U+F1907
9785U+F1908
9786U+F1909
9787U+F190A
9788U+F190B
9789U+F190C
9790U+F190D
9791U+F190E
9792U+F190F
9793U+F1910
9794U+F1911
9795U+F1912
9796U+F1913
9797U+F1914
9798U+F1915
9799U+F1916
9800U+F1917
9801U+F1918
9802U+F1919
9803U+F191A
9804U+F191B
9805U+F191C
9806U+F191D
9807U+F191E
9808U+F191F
9809U+F1920
9810U+F1921
9811U+F1922
9812U+F1923
9813U+F1924
9814U+F1925
9815U+F1926
9816U+F1927
9817U+F1928
9818U+F1929
9819U+F192A
9820U+F192B
9821U+F192C
9822U+F192D
9823U+F192E
9824U+F192F
9825U+F1930
9826U+F1931
9827U+F1932
9828U+F1933
9829U+F1934
9830U+F1935
9831U+F1936
9832U+F1937
9833U+F1938
9834U+F1939
9835U+F193A
9836U+F193B
9837U+F193C
9838U+F193D
9839U+F193E
9840U+F193F
9841U+F1940
9842U+F1941
9843U+F1942
9844U+F1943
9845U+F1944
9846U+F1945
9847U+F1946
9848U+F1947
9849U+F1948
9850U+F1949
9851U+F194A
9852U+F194B
9853U+F194C
9854U+F194D
9855U+F194E
9856U+F194F
9857U+F1950
9858U+F1951
9859U+F1952
9860U+F1953
9861U+F1954
9862U+F1955
9863U+F1956
9864U+F1957
9865U+F1958
9866U+F1959
9867U+F195A
9868U+F195B
9869U+F195C
9870U+F195D
9871U+F195E
9872U+F195F
9873U+F1960
9874U+F1961
9875U+F1962
9876U+F1963
9877U+F1964
9878U+F1965
9879U+F1966
9880U+F1967
9881U+F1968
9882U+F1969
9883U+F196A
9884U+F196B
9885U+F196C
9886U+F196D
9887U+F196E
9888U+F196F
9889U+F1970
9890U+F1971
9891U+F1972
9892U+F1973
9893U+F1974
9894U+F1975
9895U+F1976
9896U+F1977
9897U+F1978
9898U+F1979
9899U+F197A
9900U+F197B
9901U+F197C
9902U+F197D
9903U+F197E
9904U+F197F
9905U+F1980
9906U+F1981
9907U+F1982
9908U+F1983
9909U+F1984
9910U+F1985
9911U+F1986
9912U+F1987
9913U+F1988
9914U+F1989
9915U+F198A
9916U+F198B
9917U+F198C
9918U+F198D
9919U+F198E
9920U+F198F
9921U+F1990
9922U+F1991
9923U+F1992
9924U+F1993
9925U+F1994
9926U+F1995
9927U+F1996
9928U+F1997
9929U+F1998
9930U+F1999
9931U+F199A
9932U+F199B
9933U+F199C
9934U+F199D
9935U+F199E
9936U+F199F
9937U+F19A0
9938U+F19A1
9939U+F19A2
9940U+F19A3
9941U+F19A4
9942U+F19A5
9943U+F19A6
9944U+F19A7
9945U+F19A8
9946U+F19A9
9947U+F19AA
9948U+F19AB
9949U+F19AC
9950U+F19AD
9951U+F19AE
9952U+F19AF
9953U+F19B0
9954U+F19B1
9955U+F19B2
9956U+F19B3
9957U+F19B4
9958U+F19B5
9959U+F19B6
9960U+F19B7
9961U+F19B8
9962U+F19B9
9963U+F19BA
9964U+F19BB
9965U+F19BC
9966U+F19BD
9967U+F19BE
9968U+F19BF
9969U+F19C0
9970U+F19C1
9971U+F19C2
9972U+F19C3
9973U+F19C4
9974U+F19C5
9975U+F19C6
9976U+F19C7
9977U+F19C8
9978U+F19C9
9979U+F19CA
9980U+F19CB
9981U+F19CC
9982U+F19CD
9983U+F19CE
9984U+F19CF
9985U+F19D0
9986U+F19D1
9987U+F19D2
9988U+F19D3
9989U+F19D4
9990U+F19D5
9991U+F19D6
9992U+F19D7
9993U+F19D8
9994U+F19D9
9995U+F19DA
9996U+F19DB
9997U+F19DC
9998U+F19DD
9999U+F19DE
10000U+F19DF
10001U+F19E0
10002U+F19E1
10003U+F19E2
10004U+F19E3
10005U+F19E4
10006U+F19E5
10007U+F19E6
10008U+F19E7
10009U+F19E8
10010U+F19E9
10011U+F19EA
10012U+F19EB
10013U+F19EC
10014U+F19ED
10015U+F19EE
10016U+F19EF
10017U+F19F0
10018U+F19F1
10019U+F19F2
10020U+F19F3
10021U+F19F4
10022U+F19F5
10023U+F19F6
10024U+F19F7
10025U+F19F8
10026U+F19F9
10027U+F19FA
10028U+F19FB
10029U+F19FC
10030U+F19FD
10031U+F19FE
10032U+F19FF
10033U+F1A00
10034U+F1A01
10035U+F1A02
10036U+F1A03
10037U+F1A04
10038U+F1A05
10039U+F1A06
10040U+F1A07
10041U+F1A08
10042U+F1A09
10043U+F1A0A
10044U+F1A0B
10045U+F1A0C
10046U+F1A0D
10047U+F1A0E
10048U+F1A0F
10049U+F1A10
10050U+F1A11
10051U+F1A12
10052U+F1A13
10053U+F1A14
10054U+F1A15
10055U+F1A16
10056U+F1A17
10057U+F1A18
10058U+F1A19
10059U+F1A1A
10060U+F1A1B
10061U+F1A1C
10062U+F1A1D
10063U+F1A1E
10064U+F1A1F
10065U+F1A20
10066U+F1A21
10067U+F1A22
10068U+F1A23
10069U+F1A24
10070U+F1A25
10071U+F1A26
10072U+F1A27
10073U+F1A28
10074U+F1A29
10075U+F1A2A
10076U+F1A2B
10077U+F1A2C
10078U+F1A2D
10079U+F1A2E
10080U+F1A2F
10081U+F1A30
10082U+F1A31
10083U+F1A32
10084U+F1A33
10085U+F1A34
10086U+F1A35
10087U+F1A36
10088U+F1A37
10089U+F1A38
10090U+F1A39
10091U+F1A3A
10092U+F1A3B
10093U+F1A3C
10094U+F1A3D
10095U+F1A3E
10096U+F1A3F
10097U+F1A40
10098U+F1A41
10099U+F1A42
10100U+F1A43
10101U+F1A44
10102U+F1A45
10103U+F1A46
10104U+F1A47
10105U+F1A48
10106U+F1A49
10107U+F1A4A
10108U+F1A4B
10109U+F1A4C
10110U+F1A4D
10111U+F1A4E
10112U+F1A4F
10113U+F1A50
10114U+F1A51
10115U+F1A52
10116U+F1A53
10117U+F1A54
10118U+F1A55
10119U+F1A56
10120U+F1A57
10121U+F1A58
10122U+F1A59
10123U+F1A5A
10124U+F1A5B
10125U+F1A5C
10126U+F1A5D
10127U+F1A5E
10128U+F1A5F
10129U+F1A60
10130U+F1A61
10131U+F1A62
10132U+F1A63
10133U+F1A64
10134U+F1A65
10135U+F1A66
10136U+F1A67
10137U+F1A68
10138U+F1A69
10139U+F1A6A
10140U+F1A6B
10141U+F1A6C
10142U+F1A6D
10143U+F1A6E
10144U+F1A6F
10145U+F1A70
10146U+F1A71
10147U+F1A72
10148U+F1A73
10149U+F1A74
10150U+F1A75
10151U+F1A76
10152U+F1A77
10153U+F1A78
10154U+F1A79
10155U+F1A7A
10156U+F1A7B
10157U+F1A7C
10158U+F1A7D
10159U+F1A7E
10160U+F1A7F
10161U+F1A80
10162U+F1A81
10163U+F1A82
10164U+F1A83
10165U+F1A84
10166U+F1A85
10167U+F1A86
10168U+F1A87
10169U+F1A88
10170U+F1A89
10171U+F1A8A
10172U+F1A8B
10173U+F1A8C
10174U+F1A8D
10175U+F1A8E
10176U+F1A8F
10177U+F1A90
10178U+F1A91
10179U+F1A92
10180U+F1A93
10181U+F1A94
10182U+F1A95
10183U+F1A96
10184U+F1A97
10185U+F1A98
10186U+F1A99
10187U+F1A9A
10188U+F1A9B
10189U+F1A9C
10190U+F1A9D
10191U+F1A9E
10192U+F1A9F
10193U+F1AA0
10194U+F1AA1
10195U+F1AA2
10196U+F1AA3
10197U+F1AA4
10198U+F1AA5
10199U+F1AA6
10200U+F1AA7
10201U+F1AA8
10202U+F1AA9
10203U+F1AAA
10204U+F1AAB
10205U+F1AAC
10206U+F1AAD
10207U+F1AAE
10208U+F1AAF
10209U+F1AB0
10210U+F1AB1
10211U+F1AB2
10212U+F1AB3
10213U+F1AB4
10214U+F1AB5
10215U+F1AB6
10216U+F1AB7
10217U+F1AB8
10218U+F1AB9
10219U+F1ABA
10220U+F1ABB
10221U+F1ABC
10222U+F1ABD
10223U+F1ABE
10224U+F1ABF
10225U+F1AC0
10226U+F1AC1
10227U+F1AC2
10228U+F1AC3
10229U+F1AC4
10230U+F1AC5
10231U+F1AC6
10232U+F1AC7
10233U+F1AC8
10234U+F1AC9
10235U+F1ACA
10236U+F1ACB
10237U+F1ACC
10238U+F1ACD
10239U+F1ACE
10240U+F1ACF
10241U+F1AD0
10242U+F1AD1
10243U+F1AD2
10244U+F1AD3
10245U+F1AD4
10246U+F1AD5
10247U+F1AD6
10248U+F1AD7
10249U+F1AD8
10250U+F1AD9
10251U+F1ADA
10252U+F1ADB
10253U+F1ADC
10254U+F1ADD
10255U+F1ADE
10256U+F1ADF
10257U+F1AE0
10258U+F1AE1
10259U+F1AE2
10260U+F1AE3
10261U+F1AE4
10262U+F1AE5
10263U+F1AE6
10264U+F1AE7
10265U+F1AE8
10266U+F1AE9
10267U+F1AEA
10268U+F1AEB
10269U+F1AEC
10270U+F1AED
10271U+F1AEE
10272U+F1AEF
10273U+F1AF0
packages/terminal/fonts/__clo_terminal_primary_34_8_1-Italic.woff2 created
Binary files /dev/null and b/packages/terminal/fonts/__clo_terminal_primary_34_8_1-Italic.woff2 differ
packages/terminal/fonts/__clo_terminal_primary_34_8_1-Regular.woff2 created
Binary files /dev/null and b/packages/terminal/fonts/__clo_terminal_primary_34_8_1-Regular.woff2 differ
packages/terminal/fonts/__clo_terminal_primary_34_8_1-SemiBold.woff2 created
Binary files /dev/null and b/packages/terminal/fonts/__clo_terminal_primary_34_8_1-SemiBold.woff2 differ
packages/terminal/fonts/__clo_terminal_primary_34_8_1-SemiBoldItalic.woff2 created
Binary files /dev/null and b/packages/terminal/fonts/__clo_terminal_primary_34_8_1-SemiBoldItalic.woff2 differ
packages/terminal/fonts/__clo_terminal_symbols_3_4_0-Regular.woff2 created
Binary files /dev/null and b/packages/terminal/fonts/__clo_terminal_symbols_3_4_0-Regular.woff2 differ
packages/terminal/fonts/licenses/Apache-2.0.txt created+202
......@@ -0,0 +1,202 @@
1
2 Apache License
3 Version 2.0, January 2004
4 http://www.apache.org/licenses/
5
6 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
8 1. Definitions.
9
10 "License" shall mean the terms and conditions for use, reproduction,
11 and distribution as defined by Sections 1 through 9 of this document.
12
13 "Licensor" shall mean the copyright owner or entity authorized by
14 the copyright owner that is granting the License.
15
16 "Legal Entity" shall mean the union of the acting entity and all
17 other entities that control, are controlled by, or are under common
18 control with that entity. For the purposes of this definition,
19 "control" means (i) the power, direct or indirect, to cause the
20 direction or management of such entity, whether by contract or
21 otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 outstanding shares, or (iii) beneficial ownership of such entity.
23
24 "You" (or "Your") shall mean an individual or Legal Entity
25 exercising permissions granted by this License.
26
27 "Source" form shall mean the preferred form for making modifications,
28 including but not limited to software source code, documentation
29 source, and configuration files.
30
31 "Object" form shall mean any form resulting from mechanical
32 transformation or translation of a Source form, including but
33 not limited to compiled object code, generated documentation,
34 and conversions to other media types.
35
36 "Work" shall mean the work of authorship, whether in Source or
37 Object form, made available under the License, as indicated by a
38 copyright notice that is included in or attached to the work
39 (an example is provided in the Appendix below).
40
41 "Derivative Works" shall mean any work, whether in Source or Object
42 form, that is based on (or derived from) the Work and for which the
43 editorial revisions, annotations, elaborations, or other modifications
44 represent, as a whole, an original work of authorship. For the purposes
45 of this License, Derivative Works shall not include works that remain
46 separable from, or merely link (or bind by name) to the interfaces of,
47 the Work and Derivative Works thereof.
48
49 "Contribution" shall mean any work of authorship, including
50 the original version of the Work and any modifications or additions
51 to that Work or Derivative Works thereof, that is intentionally
52 submitted to Licensor for inclusion in the Work by the copyright owner
53 or by an individual or Legal Entity authorized to submit on behalf of
54 the copyright owner. For the purposes of this definition, "submitted"
55 means any form of electronic, verbal, or written communication sent
56 to the Licensor or its representatives, including but not limited to
57 communication on electronic mailing lists, source code control systems,
58 and issue tracking systems that are managed by, or on behalf of, the
59 Licensor for the purpose of discussing and improving the Work, but
60 excluding communication that is conspicuously marked or otherwise
61 designated in writing by the copyright owner as "Not a Contribution."
62
63 "Contributor" shall mean Licensor and any individual or Legal Entity
64 on behalf of whom a Contribution has been received by Licensor and
65 subsequently incorporated within the Work.
66
67 2. Grant of Copyright License. Subject to the terms and conditions of
68 this License, each Contributor hereby grants to You a perpetual,
69 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 copyright license to reproduce, prepare Derivative Works of,
71 publicly display, publicly perform, sublicense, and distribute the
72 Work and such Derivative Works in Source or Object form.
73
74 3. Grant of Patent License. Subject to the terms and conditions of
75 this License, each Contributor hereby grants to You a perpetual,
76 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 (except as stated in this section) patent license to make, have made,
78 use, offer to sell, sell, import, and otherwise transfer the Work,
79 where such license applies only to those patent claims licensable
80 by such Contributor that are necessarily infringed by their
81 Contribution(s) alone or by combination of their Contribution(s)
82 with the Work to which such Contribution(s) was submitted. If You
83 institute patent litigation against any entity (including a
84 cross-claim or counterclaim in a lawsuit) alleging that the Work
85 or a Contribution incorporated within the Work constitutes direct
86 or contributory patent infringement, then any patent licenses
87 granted to You under this License for that Work shall terminate
88 as of the date such litigation is filed.
89
90 4. Redistribution. You may reproduce and distribute copies of the
91 Work or Derivative Works thereof in any medium, with or without
92 modifications, and in Source or Object form, provided that You
93 meet the following conditions:
94
95 (a) You must give any other recipients of the Work or
96 Derivative Works a copy of this License; and
97
98 (b) You must cause any modified files to carry prominent notices
99 stating that You changed the files; and
100
101 (c) You must retain, in the Source form of any Derivative Works
102 that You distribute, all copyright, patent, trademark, and
103 attribution notices from the Source form of the Work,
104 excluding those notices that do not pertain to any part of
105 the Derivative Works; and
106
107 (d) If the Work includes a "NOTICE" text file as part of its
108 distribution, then any Derivative Works that You distribute must
109 include a readable copy of the attribution notices contained
110 within such NOTICE file, excluding those notices that do not
111 pertain to any part of the Derivative Works, in at least one
112 of the following places: within a NOTICE text file distributed
113 as part of the Derivative Works; within the Source form or
114 documentation, if provided along with the Derivative Works; or,
115 within a display generated by the Derivative Works, if and
116 wherever such third-party notices normally appear. The contents
117 of the NOTICE file are for informational purposes only and
118 do not modify the License. You may add Your own attribution
119 notices within Derivative Works that You distribute, alongside
120 or as an addendum to the NOTICE text from the Work, provided
121 that such additional attribution notices cannot be construed
122 as modifying the License.
123
124 You may add Your own copyright statement to Your modifications and
125 may provide additional or different license terms and conditions
126 for use, reproduction, or distribution of Your modifications, or
127 for any such Derivative Works as a whole, provided Your use,
128 reproduction, and distribution of the Work otherwise complies with
129 the conditions stated in this License.
130
131 5. Submission of Contributions. Unless You explicitly state otherwise,
132 any Contribution intentionally submitted for inclusion in the Work
133 by You to the Licensor shall be under the terms and conditions of
134 this License, without any additional terms or conditions.
135 Notwithstanding the above, nothing herein shall supersede or modify
136 the terms of any separate license agreement you may have executed
137 with Licensor regarding such Contributions.
138
139 6. Trademarks. This License does not grant permission to use the trade
140 names, trademarks, service marks, or product names of the Licensor,
141 except as required for reasonable and customary use in describing the
142 origin of the Work and reproducing the content of the NOTICE file.
143
144 7. Disclaimer of Warranty. Unless required by applicable law or
145 agreed to in writing, Licensor provides the Work (and each
146 Contributor provides its Contributions) on an "AS IS" BASIS,
147 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 implied, including, without limitation, any warranties or conditions
149 of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 PARTICULAR PURPOSE. You are solely responsible for determining the
151 appropriateness of using or redistributing the Work and assume any
152 risks associated with Your exercise of permissions under this License.
153
154 8. Limitation of Liability. In no event and under no legal theory,
155 whether in tort (including negligence), contract, or otherwise,
156 unless required by applicable law (such as deliberate and grossly
157 negligent acts) or agreed to in writing, shall any Contributor be
158 liable to You for damages, including any direct, indirect, special,
159 incidental, or consequential damages of any character arising as a
160 result of this License or out of the use or inability to use the
161 Work (including but not limited to damages for loss of goodwill,
162 work stoppage, computer failure or malfunction, or any and all
163 other commercial damages or losses), even if such Contributor
164 has been advised of the possibility of such damages.
165
166 9. Accepting Warranty or Additional Liability. While redistributing
167 the Work or Derivative Works thereof, You may choose to offer,
168 and charge a fee for, acceptance of support, warranty, indemnity,
169 or other liability obligations and/or rights consistent with this
170 License. However, in accepting such obligations, You may act only
171 on Your own behalf and on Your sole responsibility, not on behalf
172 of any other Contributor, and only if You agree to indemnify,
173 defend, and hold each Contributor harmless for any liability
174 incurred by, or claims asserted against, such Contributor by reason
175 of your accepting any such warranty or additional liability.
176
177 END OF TERMS AND CONDITIONS
178
179 APPENDIX: How to apply the Apache License to your work.
180
181 To apply the Apache License to your work, attach the following
182 boilerplate notice, with the fields enclosed by brackets "[]"
183 replaced with your own identifying information. (Don't include
184 the brackets!) The text should be enclosed in the appropriate
185 comment syntax for the file format. We also recommend that a
186 file or class name and description of purpose be included on the
187 same "printed page" as the copyright notice for easier
188 identification within third-party archives.
189
190 Copyright [yyyy] [name of copyright owner]
191
192 Licensed under the Apache License, Version 2.0 (the "License");
193 you may not use this file except in compliance with the License.
194 You may obtain a copy of the License at
195
196 http://www.apache.org/licenses/LICENSE-2.0
197
198 Unless required by applicable law or agreed to in writing, software
199 distributed under the License is distributed on an "AS IS" BASIS,
200 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 See the License for the specific language governing permissions and
202 limitations under the License.
packages/terminal/fonts/licenses/CC-BY-4.0.txt created+395
......@@ -0,0 +1,395 @@
1Attribution 4.0 International
2
3=======================================================================
4
5Creative Commons Corporation ("Creative Commons") is not a law firm and
6does not provide legal services or legal advice. Distribution of
7Creative Commons public licenses does not create a lawyer-client or
8other relationship. Creative Commons makes its licenses and related
9information available on an "as-is" basis. Creative Commons gives no
10warranties regarding its licenses, any material licensed under their
11terms and conditions, or any related information. Creative Commons
12disclaims all liability for damages resulting from their use to the
13fullest extent possible.
14
15Using Creative Commons Public Licenses
16
17Creative Commons public licenses provide a standard set of terms and
18conditions that creators and other rights holders may use to share
19original works of authorship and other material subject to copyright
20and certain other rights specified in the public license below. The
21following considerations are for informational purposes only, are not
22exhaustive, and do not form part of our licenses.
23
24 Considerations for licensors: Our public licenses are
25 intended for use by those authorized to give the public
26 permission to use material in ways otherwise restricted by
27 copyright and certain other rights. Our licenses are
28 irrevocable. Licensors should read and understand the terms
29 and conditions of the license they choose before applying it.
30 Licensors should also secure all rights necessary before
31 applying our licenses so that the public can reuse the
32 material as expected. Licensors should clearly mark any
33 material not subject to the license. This includes other CC-
34 licensed material, or material used under an exception or
35 limitation to copyright. More considerations for licensors:
36 wiki.creativecommons.org/Considerations_for_licensors
37
38 Considerations for the public: By using one of our public
39 licenses, a licensor grants the public permission to use the
40 licensed material under specified terms and conditions. If
41 the licensor's permission is not necessary for any reason--for
42 example, because of any applicable exception or limitation to
43 copyright--then that use is not regulated by the license. Our
44 licenses grant only permissions under copyright and certain
45 other rights that a licensor has authority to grant. Use of
46 the licensed material may still be restricted for other
47 reasons, including because others have copyright or other
48 rights in the material. A licensor may make special requests,
49 such as asking that all changes be marked or described.
50 Although not required by our licenses, you are encouraged to
51 respect those requests where reasonable. More_considerations
52 for the public:
53 wiki.creativecommons.org/Considerations_for_licensees
54
55=======================================================================
56
57Creative Commons Attribution 4.0 International Public License
58
59By exercising the Licensed Rights (defined below), You accept and agree
60to be bound by the terms and conditions of this Creative Commons
61Attribution 4.0 International Public License ("Public License"). To the
62extent this Public License may be interpreted as a contract, You are
63granted the Licensed Rights in consideration of Your acceptance of
64these terms and conditions, and the Licensor grants You such rights in
65consideration of benefits the Licensor receives from making the
66Licensed Material available under these terms and conditions.
67
68
69Section 1 -- Definitions.
70
71 a. Adapted Material means material subject to Copyright and Similar
72 Rights that is derived from or based upon the Licensed Material
73 and in which the Licensed Material is translated, altered,
74 arranged, transformed, or otherwise modified in a manner requiring
75 permission under the Copyright and Similar Rights held by the
76 Licensor. For purposes of this Public License, where the Licensed
77 Material is a musical work, performance, or sound recording,
78 Adapted Material is always produced where the Licensed Material is
79 synched in timed relation with a moving image.
80
81 b. Adapter's License means the license You apply to Your Copyright
82 and Similar Rights in Your contributions to Adapted Material in
83 accordance with the terms and conditions of this Public License.
84
85 c. Copyright and Similar Rights means copyright and/or similar rights
86 closely related to copyright including, without limitation,
87 performance, broadcast, sound recording, and Sui Generis Database
88 Rights, without regard to how the rights are labeled or
89 categorized. For purposes of this Public License, the rights
90 specified in Section 2(b)(1)-(2) are not Copyright and Similar
91 Rights.
92
93 d. Effective Technological Measures means those measures that, in the
94 absence of proper authority, may not be circumvented under laws
95 fulfilling obligations under Article 11 of the WIPO Copyright
96 Treaty adopted on December 20, 1996, and/or similar international
97 agreements.
98
99 e. Exceptions and Limitations means fair use, fair dealing, and/or
100 any other exception or limitation to Copyright and Similar Rights
101 that applies to Your use of the Licensed Material.
102
103 f. Licensed Material means the artistic or literary work, database,
104 or other material to which the Licensor applied this Public
105 License.
106
107 g. Licensed Rights means the rights granted to You subject to the
108 terms and conditions of this Public License, which are limited to
109 all Copyright and Similar Rights that apply to Your use of the
110 Licensed Material and that the Licensor has authority to license.
111
112 h. Licensor means the individual(s) or entity(ies) granting rights
113 under this Public License.
114
115 i. Share means to provide material to the public by any means or
116 process that requires permission under the Licensed Rights, such
117 as reproduction, public display, public performance, distribution,
118 dissemination, communication, or importation, and to make material
119 available to the public including in ways that members of the
120 public may access the material from a place and at a time
121 individually chosen by them.
122
123 j. Sui Generis Database Rights means rights other than copyright
124 resulting from Directive 96/9/EC of the European Parliament and of
125 the Council of 11 March 1996 on the legal protection of databases,
126 as amended and/or succeeded, as well as other essentially
127 equivalent rights anywhere in the world.
128
129 k. You means the individual or entity exercising the Licensed Rights
130 under this Public License. Your has a corresponding meaning.
131
132
133Section 2 -- Scope.
134
135 a. License grant.
136
137 1. Subject to the terms and conditions of this Public License,
138 the Licensor hereby grants You a worldwide, royalty-free,
139 non-sublicensable, non-exclusive, irrevocable license to
140 exercise the Licensed Rights in the Licensed Material to:
141
142 a. reproduce and Share the Licensed Material, in whole or
143 in part; and
144
145 b. produce, reproduce, and Share Adapted Material.
146
147 2. Exceptions and Limitations. For the avoidance of doubt, where
148 Exceptions and Limitations apply to Your use, this Public
149 License does not apply, and You do not need to comply with
150 its terms and conditions.
151
152 3. Term. The term of this Public License is specified in Section
153 6(a).
154
155 4. Media and formats; technical modifications allowed. The
156 Licensor authorizes You to exercise the Licensed Rights in
157 all media and formats whether now known or hereafter created,
158 and to make technical modifications necessary to do so. The
159 Licensor waives and/or agrees not to assert any right or
160 authority to forbid You from making technical modifications
161 necessary to exercise the Licensed Rights, including
162 technical modifications necessary to circumvent Effective
163 Technological Measures. For purposes of this Public License,
164 simply making modifications authorized by this Section 2(a)
165 (4) never produces Adapted Material.
166
167 5. Downstream recipients.
168
169 a. Offer from the Licensor -- Licensed Material. Every
170 recipient of the Licensed Material automatically
171 receives an offer from the Licensor to exercise the
172 Licensed Rights under the terms and conditions of this
173 Public License.
174
175 b. No downstream restrictions. You may not offer or impose
176 any additional or different terms or conditions on, or
177 apply any Effective Technological Measures to, the
178 Licensed Material if doing so restricts exercise of the
179 Licensed Rights by any recipient of the Licensed
180 Material.
181
182 6. No endorsement. Nothing in this Public License constitutes or
183 may be construed as permission to assert or imply that You
184 are, or that Your use of the Licensed Material is, connected
185 with, or sponsored, endorsed, or granted official status by,
186 the Licensor or others designated to receive attribution as
187 provided in Section 3(a)(1)(A)(i).
188
189 b. Other rights.
190
191 1. Moral rights, such as the right of integrity, are not
192 licensed under this Public License, nor are publicity,
193 privacy, and/or other similar personality rights; however, to
194 the extent possible, the Licensor waives and/or agrees not to
195 assert any such rights held by the Licensor to the limited
196 extent necessary to allow You to exercise the Licensed
197 Rights, but not otherwise.
198
199 2. Patent and trademark rights are not licensed under this
200 Public License.
201
202 3. To the extent possible, the Licensor waives any right to
203 collect royalties from You for the exercise of the Licensed
204 Rights, whether directly or through a collecting society
205 under any voluntary or waivable statutory or compulsory
206 licensing scheme. In all other cases the Licensor expressly
207 reserves any right to collect such royalties.
208
209
210Section 3 -- License Conditions.
211
212Your exercise of the Licensed Rights is expressly made subject to the
213following conditions.
214
215 a. Attribution.
216
217 1. If You Share the Licensed Material (including in modified
218 form), You must:
219
220 a. retain the following if it is supplied by the Licensor
221 with the Licensed Material:
222
223 i. identification of the creator(s) of the Licensed
224 Material and any others designated to receive
225 attribution, in any reasonable manner requested by
226 the Licensor (including by pseudonym if
227 designated);
228
229 ii. a copyright notice;
230
231 iii. a notice that refers to this Public License;
232
233 iv. a notice that refers to the disclaimer of
234 warranties;
235
236 v. a URI or hyperlink to the Licensed Material to the
237 extent reasonably practicable;
238
239 b. indicate if You modified the Licensed Material and
240 retain an indication of any previous modifications; and
241
242 c. indicate the Licensed Material is licensed under this
243 Public License, and include the text of, or the URI or
244 hyperlink to, this Public License.
245
246 2. You may satisfy the conditions in Section 3(a)(1) in any
247 reasonable manner based on the medium, means, and context in
248 which You Share the Licensed Material. For example, it may be
249 reasonable to satisfy the conditions by providing a URI or
250 hyperlink to a resource that includes the required
251 information.
252
253 3. If requested by the Licensor, You must remove any of the
254 information required by Section 3(a)(1)(A) to the extent
255 reasonably practicable.
256
257 4. If You Share Adapted Material You produce, the Adapter's
258 License You apply must not prevent recipients of the Adapted
259 Material from complying with this Public License.
260
261
262Section 4 -- Sui Generis Database Rights.
263
264Where the Licensed Rights include Sui Generis Database Rights that
265apply to Your use of the Licensed Material:
266
267 a. for the avoidance of doubt, Section 2(a)(1) grants You the right
268 to extract, reuse, reproduce, and Share all or a substantial
269 portion of the contents of the database;
270
271 b. if You include all or a substantial portion of the database
272 contents in a database in which You have Sui Generis Database
273 Rights, then the database in which You have Sui Generis Database
274 Rights (but not its individual contents) is Adapted Material; and
275
276 c. You must comply with the conditions in Section 3(a) if You Share
277 all or a substantial portion of the contents of the database.
278
279For the avoidance of doubt, this Section 4 supplements and does not
280replace Your obligations under this Public License where the Licensed
281Rights include other Copyright and Similar Rights.
282
283
284Section 5 -- Disclaimer of Warranties and Limitation of Liability.
285
286 a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
287 EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
288 AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
289 ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
290 IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
291 WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
292 PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
293 ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
294 KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
295 ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
296
297 b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
298 TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
299 NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
300 INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
301 COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
302 USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
303 ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
304 DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
305 IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
306
307 c. The disclaimer of warranties and limitation of liability provided
308 above shall be interpreted in a manner that, to the extent
309 possible, most closely approximates an absolute disclaimer and
310 waiver of all liability.
311
312
313Section 6 -- Term and Termination.
314
315 a. This Public License applies for the term of the Copyright and
316 Similar Rights licensed here. However, if You fail to comply with
317 this Public License, then Your rights under this Public License
318 terminate automatically.
319
320 b. Where Your right to use the Licensed Material has terminated under
321 Section 6(a), it reinstates:
322
323 1. automatically as of the date the violation is cured, provided
324 it is cured within 30 days of Your discovery of the
325 violation; or
326
327 2. upon express reinstatement by the Licensor.
328
329 For the avoidance of doubt, this Section 6(b) does not affect any
330 right the Licensor may have to seek remedies for Your violations
331 of this Public License.
332
333 c. For the avoidance of doubt, the Licensor may also offer the
334 Licensed Material under separate terms or conditions or stop
335 distributing the Licensed Material at any time; however, doing so
336 will not terminate this Public License.
337
338 d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
339 License.
340
341
342Section 7 -- Other Terms and Conditions.
343
344 a. The Licensor shall not be bound by any additional or different
345 terms or conditions communicated by You unless expressly agreed.
346
347 b. Any arrangements, understandings, or agreements regarding the
348 Licensed Material not stated herein are separate from and
349 independent of the terms and conditions of this Public License.
350
351
352Section 8 -- Interpretation.
353
354 a. For the avoidance of doubt, this Public License does not, and
355 shall not be interpreted to, reduce, limit, restrict, or impose
356 conditions on any use of the Licensed Material that could lawfully
357 be made without permission under this Public License.
358
359 b. To the extent possible, if any provision of this Public License is
360 deemed unenforceable, it shall be automatically reformed to the
361 minimum extent necessary to make it enforceable. If the provision
362 cannot be reformed, it shall be severed from this Public License
363 without affecting the enforceability of the remaining terms and
364 conditions.
365
366 c. No term or condition of this Public License will be waived and no
367 failure to comply consented to unless expressly agreed to by the
368 Licensor.
369
370 d. Nothing in this Public License constitutes or may be interpreted
371 as a limitation upon, or waiver of, any privileges and immunities
372 that apply to the Licensor or You, including from the legal
373 processes of any jurisdiction or authority.
374
375
376=======================================================================
377
378Creative Commons is not a party to its public
379licenses. Notwithstanding, Creative Commons may elect to apply one of
380its public licenses to material it publishes and in those instances
381will be considered the “Licensor.” The text of the Creative Commons
382public licenses is dedicated to the public domain under the CC0 Public
383Domain Dedication. Except for the limited purpose of indicating that
384material is shared under a Creative Commons public license or as
385otherwise permitted by the Creative Commons policies published at
386creativecommons.org/policies, Creative Commons does not authorize the
387use of the trademark "Creative Commons" or any other trademark or logo
388of Creative Commons without its prior written consent including,
389without limitation, in connection with any unauthorized modifications
390to any of its public licenses or any other arrangements,
391understandings, or agreements concerning use of licensed material. For
392the avoidance of doubt, this paragraph does not form part of the
393public licenses.
394
395Creative Commons may be contacted at creativecommons.org.
\ No newline at end of file
packages/terminal/fonts/licenses/Devicons-MIT.txt created+20
......@@ -0,0 +1,20 @@
1The MIT License (MIT)
2
3Copyright (c) 2015 konpa
4
5Permission is hereby granted, free of charge, to any person obtaining a copy of
6this software and associated documentation files (the "Software"), to deal in
7the Software without restriction, including without limitation the rights to
8use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9the Software, and to permit persons to whom the Software is furnished to do so,
10subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
packages/terminal/fonts/licenses/Font-Awesome-Extension-MIT.txt created+21
......@@ -0,0 +1,21 @@
1MIT License
2
3Copyright (c) 2017 André Luiz Gava
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
packages/terminal/fonts/licenses/Font-Awesome-LICENSE.txt created+165
......@@ -0,0 +1,165 @@
1Fonticons, Inc. (https://fontawesome.com)
2
3--------------------------------------------------------------------------------
4
5Font Awesome Free License
6
7Font Awesome Free is free, open source, and GPL friendly. You can use it for
8commercial projects, open source projects, or really almost whatever you want.
9Full Font Awesome Free license: https://fontawesome.com/license/free.
10
11--------------------------------------------------------------------------------
12
13# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
14
15The Font Awesome Free download is licensed under a Creative Commons
16Attribution 4.0 International License and applies to all icons packaged
17as SVG and JS file types.
18
19--------------------------------------------------------------------------------
20
21# Fonts: SIL OFL 1.1 License
22
23In the Font Awesome Free download, the SIL OFL license applies to all icons
24packaged as web and desktop font files.
25
26Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com)
27with Reserved Font Name: "Font Awesome".
28
29This Font Software is licensed under the SIL Open Font License, Version 1.1.
30This license is copied below, and is also available with a FAQ at:
31http://scripts.sil.org/OFL
32
33SIL OPEN FONT LICENSE
34Version 1.1 - 26 February 2007
35
36PREAMBLE
37The goals of the Open Font License (OFL) are to stimulate worldwide
38development of collaborative font projects, to support the font creation
39efforts of academic and linguistic communities, and to provide a free and
40open framework in which fonts may be shared and improved in partnership
41with others.
42
43The OFL allows the licensed fonts to be used, studied, modified and
44redistributed freely as long as they are not sold by themselves. The
45fonts, including any derivative works, can be bundled, embedded,
46redistributed and/or sold with any software provided that any reserved
47names are not used by derivative works. The fonts and derivatives,
48however, cannot be released under any other type of license. The
49requirement for fonts to remain under this license does not apply
50to any document created using the fonts or their derivatives.
51
52DEFINITIONS
53"Font Software" refers to the set of files released by the Copyright
54Holder(s) under this license and clearly marked as such. This may
55include source files, build scripts and documentation.
56
57"Reserved Font Name" refers to any names specified as such after the
58copyright statement(s).
59
60"Original Version" refers to the collection of Font Software components as
61distributed by the Copyright Holder(s).
62
63"Modified Version" refers to any derivative made by adding to, deleting,
64or substituting — in part or in whole — any of the components of the
65Original Version, by changing formats or by porting the Font Software to a
66new environment.
67
68"Author" refers to any designer, engineer, programmer, technical
69writer or other person who contributed to the Font Software.
70
71PERMISSION & CONDITIONS
72Permission is hereby granted, free of charge, to any person obtaining
73a copy of the Font Software, to use, study, copy, merge, embed, modify,
74redistribute, and sell modified and unmodified copies of the Font
75Software, subject to the following conditions:
76
771) Neither the Font Software nor any of its individual components,
78in Original or Modified Versions, may be sold by itself.
79
802) Original or Modified Versions of the Font Software may be bundled,
81redistributed and/or sold with any software, provided that each copy
82contains the above copyright notice and this license. These can be
83included either as stand-alone text files, human-readable headers or
84in the appropriate machine-readable metadata fields within text or
85binary files as long as those fields can be easily viewed by the user.
86
873) No Modified Version of the Font Software may use the Reserved Font
88Name(s) unless explicit written permission is granted by the corresponding
89Copyright Holder. This restriction only applies to the primary font name as
90presented to the users.
91
924) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
93Software shall not be used to promote, endorse or advertise any
94Modified Version, except to acknowledge the contribution(s) of the
95Copyright Holder(s) and the Author(s) or with their explicit written
96permission.
97
985) The Font Software, modified or unmodified, in part or in whole,
99must be distributed entirely under this license, and must not be
100distributed under any other license. The requirement for fonts to
101remain under this license does not apply to any document created
102using the Font Software.
103
104TERMINATION
105This license becomes null and void if any of the above conditions are
106not met.
107
108DISCLAIMER
109THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
110EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
111MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
112OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
113COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
114INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
115DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
116FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
117OTHER DEALINGS IN THE FONT SOFTWARE.
118
119--------------------------------------------------------------------------------
120
121# Code: MIT License (https://opensource.org/licenses/MIT)
122
123In the Font Awesome Free download, the MIT license applies to all non-font and
124non-icon files.
125
126Copyright 2022 Fonticons, Inc.
127
128Permission is hereby granted, free of charge, to any person obtaining a copy of
129this software and associated documentation files (the "Software"), to deal in the
130Software without restriction, including without limitation the rights to use, copy,
131modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
132and to permit persons to whom the Software is furnished to do so, subject to the
133following conditions:
134
135The above copyright notice and this permission notice shall be included in all
136copies or substantial portions of the Software.
137
138THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
139INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
140PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
141HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
142OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
143SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
144
145--------------------------------------------------------------------------------
146
147# Attribution
148
149Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
150Awesome Free files already contain embedded comments with sufficient
151attribution, so you shouldn't need to do anything additional when using these
152files normally.
153
154We've kept attribution comments terse, so we ask that you do not actively work
155to remove them from files, especially code. They're a great way for folks to
156learn about Font Awesome.
157
158--------------------------------------------------------------------------------
159
160# Brand Icons
161
162All brand icons are trademarks of their respective owners. The use of these
163trademarks does not indicate endorsement of the trademark holder by Font
164Awesome, nor vice versa. **Please do not use brand logos for any purpose except
165to represent the company, product, or service to which they refer.**
packages/terminal/fonts/licenses/Material-Design-Icons-NOTICE.txt created+20
......@@ -0,0 +1,20 @@
1Pictogrammers Free License
2--------------------------
3
4This icon collection is released as free, open source, and GPL friendly by
5the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
6for commercial projects, open source projects, or anything really.
7
8# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
9Some of the icons are redistributed under the Apache 2.0 license. All other
10icons are either redistributed under their respective licenses or are
11distributed under the Apache 2.0 license.
12
13# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
14All web and desktop fonts are distributed under the Apache 2.0 license. Web
15and desktop fonts contain some icons that are redistributed under the Apache
162.0 license. All other icons are either redistributed under their respective
17licenses or are distributed under the Apache 2.0 license.
18
19# Code: MIT (https://opensource.org/licenses/MIT)
20The MIT license applies to all non-font and non-icon files.
packages/terminal/fonts/licenses/NerdFonts-LICENSE.txt created+126
......@@ -0,0 +1,126 @@
1# Nerd Fonts Licensing
2
3There are various sources used under various licenses:
4
5* Nerd Fonts source fonts, patched fonts, and folders with explict OFL SIL files are licensed under SIL OPEN FONT LICENSE Version 1.1 (see below).
6* Nerd Fonts original source code files (such as `.sh`, `.py`, `font-patcher` and others) are licensed under the MIT License (MIT) (see below).
7* Many other licenses are present in this project for even more detailed breakdown see: [License Audit](https://github.com/ryanoasis/nerd-fonts/blob/-/license-audit.md).
8
9## Source files not in folders containing an explicit license are using the MIT License (MIT)
10
11The MIT License (MIT)
12
13Copyright (c) 2014 Ryan L McIntyre
14
15Permission is hereby granted, free of charge, to any person obtaining a copy
16of this software and associated documentation files (the "Software"), to deal
17in the Software without restriction, including without limitation the rights
18to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19copies of the Software, and to permit persons to whom the Software is
20furnished to do so, subject to the following conditions:
21
22The above copyright notice and this permission notice shall be included in all
23copies or substantial portions of the Software.
24
25THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31SOFTWARE.
32
33## Various Fonts, Patched Fonts, SVGs, Glyph Fonts, and any files in a folder with explicit SIL OFL 1.1 License
34
35Copyright (c) 2014, Ryan L McIntyre (https://ryanlmcintyre.com).
36
37This Font Software is licensed under the SIL Open Font License, Version 1.1.
38This license is copied below, and is also available with a FAQ at:
39http://scripts.sil.org/OFL
40
41-----------------------------------------------------------
42SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
43-----------------------------------------------------------
44
45PREAMBLE
46The goals of the Open Font License (OFL) are to stimulate worldwide
47development of collaborative font projects, to support the font creation
48efforts of academic and linguistic communities, and to provide a free and
49open framework in which fonts may be shared and improved in partnership
50with others.
51
52The OFL allows the licensed fonts to be used, studied, modified and
53redistributed freely as long as they are not sold by themselves. The
54fonts, including any derivative works, can be bundled, embedded,
55redistributed and/or sold with any software provided that any reserved
56names are not used by derivative works. The fonts and derivatives,
57however, cannot be released under any other type of license. The
58requirement for fonts to remain under this license does not apply
59to any document created using the fonts or their derivatives.
60
61DEFINITIONS
62"Font Software" refers to the set of files released by the Copyright
63Holder(s) under this license and clearly marked as such. This may
64include source files, build scripts and documentation.
65
66"Reserved Font Name" refers to any names specified as such after the
67copyright statement(s).
68
69"Original Version" refers to the collection of Font Software components as
70distributed by the Copyright Holder(s).
71
72"Modified Version" refers to any derivative made by adding to, deleting,
73or substituting -- in part or in whole -- any of the components of the
74Original Version, by changing formats or by porting the Font Software to a
75new environment.
76
77"Author" refers to any designer, engineer, programmer, technical
78writer or other person who contributed to the Font Software.
79
80PERMISSION & CONDITIONS
81Permission is hereby granted, free of charge, to any person obtaining
82a copy of the Font Software, to use, study, copy, merge, embed, modify,
83redistribute, and sell modified and unmodified copies of the Font
84Software, subject to the following conditions:
85
861) Neither the Font Software nor any of its individual components,
87in Original or Modified Versions, may be sold by itself.
88
892) Original or Modified Versions of the Font Software may be bundled,
90redistributed and/or sold with any software, provided that each copy
91contains the above copyright notice and this license. These can be
92included either as stand-alone text files, human-readable headers or
93in the appropriate machine-readable metadata fields within text or
94binary files as long as those fields can be easily viewed by the user.
95
963) No Modified Version of the Font Software may use the Reserved Font
97Name(s) unless explicit written permission is granted by the corresponding
98Copyright Holder. This restriction only applies to the primary font name as
99presented to the users.
100
1014) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
102Software shall not be used to promote, endorse or advertise any
103Modified Version, except to acknowledge the contribution(s) of the
104Copyright Holder(s) and the Author(s) or with their explicit written
105permission.
106
1075) The Font Software, modified or unmodified, in part or in whole,
108must be distributed entirely under this license, and must not be
109distributed under any other license. The requirement for fonts to
110remain under this license does not apply to any document created
111using the Font Software.
112
113TERMINATION
114This license becomes null and void if any of the above conditions are
115not met.
116
117DISCLAIMER
118THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
119EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
120MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
121OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
122COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
123INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
124DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
125FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
126OTHER DEALINGS IN THE FONT SOFTWARE.
packages/terminal/fonts/licenses/NerdFonts-Symbols-Only-MIT.txt created+21
......@@ -0,0 +1,21 @@
1The MIT License (MIT)
2
3Copyright (c) 2014 Ryan L McIntyre
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
packages/terminal/fonts/licenses/NerdFonts-license-audit.md created+126
......@@ -0,0 +1,126 @@
1# License audit
2
3## Licenses found and basic summary of each
4
5* MIT - all good do what you want basically
6* CC 4 - share, adapt and change in any format is okay
7* SIL OFL 1.1 - free to use, copy, merge, modify, redistribute with conditions: no selling, must contain this original license, must comply to the RFN (Reserved Font Names)
8* Apache 2.0 - Basically permissive like MIT but a few things to note. Must include original copyright, note any significant changes to original code, copy of NOTICE file. Modified code does not need to remain under Apache 2.0.
9* Creative Commons Attribution-ShareAlike 4.0 (CC BY-SA 4.0) - free to share and change. Must give proper attribution. Must distribute under same license. The font itself once patched indeed is.
10* Bitstream Vera License v1.00 - do what want, no selling, must rename font.
11* Do What The Fuck You Want To - the name summarizes the rules.
12* Go License - Redistribution and everything is allowed. Only limitation is use of Google Inc. to endorse/promote.
13* Vic Fieger License - free to do whatever you want
14* Monofur Free License - free do what you want just require license file
15* Bitstream License - do what you want, attribution required
16* Ubuntu Font License 1.0 - do what you want, attribution and license must be included. There may be some ambiguity on the naming, but I believe we comply with it:
17 * > (c) Modified Versions which are not Substantially Changed must be
18renamed to both (i) retain the name of the Original Version and (ii) add
19additional naming elements to distinguish the Modified Version from the
20Original Version. The name of such Modified Versions must be the name of
21the Original Version, with "derivative X" where X represents the name of
22the new work, appended to that name.
23
24## Original source works under Nerd Fonts repository
25
26All files created such as `font-patcher` and any `ph` or `sh` script/source files are released under an MIT license.
27
28## Glyph/icon sources
29
30| Project | License |
31| ------------------------------------------------ | ----------------|
32| Codicons | CC BY 4.0 |
33| Devicons | MIT |
34| Font Awesome | CC BY 4.0 |
35| Font Awesome Extension | MIT |
36| Font Logos | Unlicensed |
37| IEC Power Symbols | MIT |
38| Material Design Icons | Apache 2.0 |
39| Original Source (Seti-UI but modified) | MIT |
40| Octicons | MIT |
41| Pomicons | SIL OFL 1.1 |
42| Powerline Extra Symbols | MIT |
43| Powerline Symbols | Free License |
44| Weather Icons | SIL OFL 1.1 |
45
46### Additional notes
47
48* font awesome - we use the icons which are CC 4.0.
49* weather icons - project is unfortunately dead, but added the OFL license files to comply.
50
51## Fonts
52
53* All fonts have their appropriate license and/or release note information provided in the original unpatched version folders _and_ the patched version folders.
54* Machine readable license info is in the `fonts.json` file
55
56| Font | License SPDX |
57| ----------------------------- | ---------------------------- |
58| 0xProto | OFL-1.1-no-RFN |
59| IBM 3270 | BSD-3-Clause |
60| Agave | MIT |
61| Anonymous Pro | OFL-1.1-RFN |
62| Arimo | Apache-2.0 |
63| Aurulent Sans Mono | OFL-1.1-no-RFN |
64| BigBlue Terminal | CC-BY-SA-4.0 |
65| Bitstream Vera Sans Mono | Bitstream-Vera |
66| IBM Plex Mono | OFL-1.1-RFN |
67| Cascadia Code | OFL-1.1-RFN |
68| Cascadia Mono | OFL-1.1-RFN |
69| Code New Roman | OFL-1.1-no-RFN |
70| Comic Shanns Mono | MIT |
71| Commit Mono | OFL-1.1-no-RFN |
72| Cousine | Apache-2.0 |
73| D2Coding | OFL-1.1-no-RFN |
74| DaddyTimeMono | OFL-1.1-no-RFN |
75| DejaVu Sans Mono | Bitstream-Vera |
76| Droid Sans Mono | Apache-2.0 |
77| Envy Code R | OFL-1.1-RFN |
78| Fantasque Sans Mono | OFL-1.1-no-RFN |
79| Fira Code | OFL-1.1-no-RFN |
80| Fira | OFL-1.1-no-RFN |
81| Geist Mono | OFL-1.1-no-RFN |
82| Go Mono | BSD-3-Clause-Clear |
83| Gohu | WTFPL |
84| Hack | Bitstream-Vera AND MIT |
85| Hasklig | OFL-1.1-RFN |
86| Heavy Data | LicenseRef-VicFieger |
87| Hermit | OFL-1.1-RFN |
88| iA Writer | OFL-1.1-RFN |
89| Inconsolata | OFL-1.1-no-RFN |
90| InconsolataGo | OFL-1.1-no-RFN |
91| Inconsolata LGC | OFL-1.1-no-RFN |
92| Intel One Mono | OFL-1.1-RFN |
93| Iosevka | OFL-1.1-no-RFN |
94| Iosevka Term | OFL-1.1-no-RFN |
95| Iosevka Term Slab | OFL-1.1-no-RFN |
96| JetBrains Mono | OFL-1.1-no-RFN |
97| Lekton | OFL-1.1-no-RFN |
98| Liberation Mono | OFL-1.1-RFN |
99| Lilex | OFL-1.1-no-RFN |
100| MartianMono | OFL-1.1-no-RFN |
101| Meslo LG | Apache-2.0 |
102| Monaspace | OFL-1.1-RFN |
103| Monofur | LicenseRef-Monofur |
104| Monoid | MIT OR OFL-1.1-no-RFN |
105| Mononoki | OFL-1.1-RFN |
106| MPlus | OFL-1.1-no-RFN |
107| Noto | OFL-1.1-no-RFN |
108| OpenDyslexic | Bitstream-Vera |
109| Overpass | OFL-1.1-no-RFN or LGPL-2.1-only |
110| ProFont | MIT |
111| ProggyCleanTT | MIT |
112| Recursive Mono | OFL-1.1-no-RFN |
113| Roboto Mono | Apache-2.0 |
114| Share Tech Mono | OFL-1.1-RFN |
115| Source Code Pro | OFL-1.1-RFN |
116| Space Mono | OFL-1.1-no-RFN |
117| Symbols Only | MIT |
118| Terminus | OFL-1.1-RFN |
119| Tinos | Apache-2.0 |
120| Ubuntu | LicenseRef-UbuntuFont |
121| Ubuntu Mono | LicenseRef-UbuntuFont |
122| Ubuntu Sans | LicenseRef-UbuntuFont |
123| Victor Mono | OFL-1.1-no-RFN |
124| Zed Mono | OFL-1.1-no-RFN |
125
126_LicenseRef licenses have no SPDX value_
packages/terminal/fonts/licenses/Octicons-MIT.txt created+21
......@@ -0,0 +1,21 @@
1MIT License
2
3Copyright (c) 2023 GitHub Inc.
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
packages/terminal/fonts/licenses/Pomicons-OFL.txt created+94
......@@ -0,0 +1,94 @@
1Copyright (c) 2021, Gabriele Lana gabriele.lana@gmail.com
2with Reserved Font Name Pomicons.
3
4This Font Software is licensed under the SIL Open Font License, Version 1.1.
5This license is copied below, and is also available with a FAQ at:
6http://scripts.sil.org/OFL
7
8
9-----------------------------------------------------------
10SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
11-----------------------------------------------------------
12
13PREAMBLE
14The goals of the Open Font License (OFL) are to stimulate worldwide
15development of collaborative font projects, to support the font creation
16efforts of academic and linguistic communities, and to provide a free and
17open framework in which fonts may be shared and improved in partnership
18with others.
19
20The OFL allows the licensed fonts to be used, studied, modified and
21redistributed freely as long as they are not sold by themselves. The
22fonts, including any derivative works, can be bundled, embedded,
23redistributed and/or sold with any software provided that any reserved
24names are not used by derivative works. The fonts and derivatives,
25however, cannot be released under any other type of license. The
26requirement for fonts to remain under this license does not apply
27to any document created using the fonts or their derivatives.
28
29DEFINITIONS
30"Font Software" refers to the set of files released by the Copyright
31Holder(s) under this license and clearly marked as such. This may
32include source files, build scripts and documentation.
33
34"Reserved Font Name" refers to any names specified as such after the
35copyright statement(s).
36
37"Original Version" refers to the collection of Font Software components as
38distributed by the Copyright Holder(s).
39
40"Modified Version" refers to any derivative made by adding to, deleting,
41or substituting -- in part or in whole -- any of the components of the
42Original Version, by changing formats or by porting the Font Software to a
43new environment.
44
45"Author" refers to any designer, engineer, programmer, technical
46writer or other person who contributed to the Font Software.
47
48PERMISSION & CONDITIONS
49Permission is hereby granted, free of charge, to any person obtaining
50a copy of the Font Software, to use, study, copy, merge, embed, modify,
51redistribute, and sell modified and unmodified copies of the Font
52Software, subject to the following conditions:
53
541) Neither the Font Software nor any of its individual components,
55in Original or Modified Versions, may be sold by itself.
56
572) Original or Modified Versions of the Font Software may be bundled,
58redistributed and/or sold with any software, provided that each copy
59contains the above copyright notice and this license. These can be
60included either as stand-alone text files, human-readable headers or
61in the appropriate machine-readable metadata fields within text or
62binary files as long as those fields can be easily viewed by the user.
63
643) No Modified Version of the Font Software may use the Reserved Font
65Name(s) unless explicit written permission is granted by the corresponding
66Copyright Holder. This restriction only applies to the primary font name as
67presented to the users.
68
694) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
70Software shall not be used to promote, endorse or advertise any
71Modified Version, except to acknowledge the contribution(s) of the
72Copyright Holder(s) and the Author(s) or with their explicit written
73permission.
74
755) The Font Software, modified or unmodified, in part or in whole,
76must be distributed entirely under this license, and must not be
77distributed under any other license. The requirement for fonts to
78remain under this license does not apply to any document created
79using the Font Software.
80
81TERMINATION
82This license becomes null and void if any of the above conditions are
83not met.
84
85DISCLAIMER
86THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
87EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
88MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
89OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
90COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
91INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
92DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
94OTHER DEALINGS IN THE FONT SOFTWARE.
packages/terminal/fonts/licenses/Powerline-Extra-MIT.txt created+21
......@@ -0,0 +1,21 @@
1MIT License
2
3Copyright (c) 2016 Ryan L McIntyre
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
packages/terminal/fonts/licenses/Weather-Icons-OFL.txt created+97
......@@ -0,0 +1,97 @@
1Copyright (c) <dates>, <Erik Flowers> (<URL|email>),
2with Reserved Font Name <Reserved Font Name>.
3Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>),
4with Reserved Font Name <additional Reserved Font Name>.
5Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>).
6
7This Font Software is licensed under the SIL Open Font License, Version 1.1.
8This license is copied below, and is also available with a FAQ at:
9http://scripts.sil.org/OFL
10
11
12-----------------------------------------------------------
13SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
14-----------------------------------------------------------
15
16PREAMBLE
17The goals of the Open Font License (OFL) are to stimulate worldwide
18development of collaborative font projects, to support the font creation
19efforts of academic and linguistic communities, and to provide a free and
20open framework in which fonts may be shared and improved in partnership
21with others.
22
23The OFL allows the licensed fonts to be used, studied, modified and
24redistributed freely as long as they are not sold by themselves. The
25fonts, including any derivative works, can be bundled, embedded,
26redistributed and/or sold with any software provided that any reserved
27names are not used by derivative works. The fonts and derivatives,
28however, cannot be released under any other type of license. The
29requirement for fonts to remain under this license does not apply
30to any document created using the fonts or their derivatives.
31
32DEFINITIONS
33"Font Software" refers to the set of files released by the Copyright
34Holder(s) under this license and clearly marked as such. This may
35include source files, build scripts and documentation.
36
37"Reserved Font Name" refers to any names specified as such after the
38copyright statement(s).
39
40"Original Version" refers to the collection of Font Software components as
41distributed by the Copyright Holder(s).
42
43"Modified Version" refers to any derivative made by adding to, deleting,
44or substituting -- in part or in whole -- any of the components of the
45Original Version, by changing formats or by porting the Font Software to a
46new environment.
47
48"Author" refers to any designer, engineer, programmer, technical
49writer or other person who contributed to the Font Software.
50
51PERMISSION & CONDITIONS
52Permission is hereby granted, free of charge, to any person obtaining
53a copy of the Font Software, to use, study, copy, merge, embed, modify,
54redistribute, and sell modified and unmodified copies of the Font
55Software, subject to the following conditions:
56
571) Neither the Font Software nor any of its individual components,
58in Original or Modified Versions, may be sold by itself.
59
602) Original or Modified Versions of the Font Software may be bundled,
61redistributed and/or sold with any software, provided that each copy
62contains the above copyright notice and this license. These can be
63included either as stand-alone text files, human-readable headers or
64in the appropriate machine-readable metadata fields within text or
65binary files as long as those fields can be easily viewed by the user.
66
673) No Modified Version of the Font Software may use the Reserved Font
68Name(s) unless explicit written permission is granted by the corresponding
69Copyright Holder. This restriction only applies to the primary font name as
70presented to the users.
71
724) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
73Software shall not be used to promote, endorse or advertise any
74Modified Version, except to acknowledge the contribution(s) of the
75Copyright Holder(s) and the Author(s) or with their explicit written
76permission.
77
785) The Font Software, modified or unmodified, in part or in whole,
79must be distributed entirely under this license, and must not be
80distributed under any other license. The requirement for fonts to
81remain under this license does not apply to any document created
82using the Font Software.
83
84TERMINATION
85This license becomes null and void if any of the above conditions are
86not met.
87
88DISCLAIMER
89THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
90EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
91MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
92OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
93COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
94INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
95DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
96FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
97OTHER DEALINGS IN THE FONT SOFTWARE.
packages/terminal/fonts/subset-primary.py created+43
......@@ -0,0 +1,43 @@
1"""Emit the retained-codepoint manifest for the bundled primary font.
2
3The ranges are the terminal's own surface: every script and symbol block a cell
4can hold, plus Symbols for Legacy Computing, where the sextants and octants a
5program draws charts with live. What is dropped above them — mathematical
6alphanumerics, musical notation, emoji, and the historic scripts — is most of
7what makes an unsubset face 1.4 MB, and none of it reaches a terminal cell from
8a monospace primary.
9"""
10
11from pathlib import Path
12import sys
13
14from fontTools.ttLib import TTFont
15
16
17RETAINED_RANGES = (
18 (0x0000, 0x2BFF), # Latin through Miscellaneous Symbols and Arrows
19 (0x1FB00, 0x1FBFF), # Symbols for Legacy Computing
20)
21
22
23def main() -> None:
24 if len(sys.argv) != 3:
25 raise SystemExit("usage: subset-primary.py INPUT.ttf OUTPUT.txt")
26
27 source = TTFont(sys.argv[1])
28 cmap = source.getBestCmap()
29 if cmap is None:
30 raise SystemExit("input font has no Unicode cmap")
31
32 codepoints = sorted(
33 codepoint
34 for codepoint in cmap
35 if any(start <= codepoint <= end for start, end in RETAINED_RANGES)
36 )
37 Path(sys.argv[2]).write_text(
38 "".join(f"U+{codepoint:04X}\n" for codepoint in codepoints), encoding="ascii"
39 )
40
41
42if __name__ == "__main__":
43 main()
packages/terminal/fonts/subset-symbols.py created+49
......@@ -0,0 +1,49 @@
1from pathlib import Path
2import sys
3
4from fontTools.ttLib import TTFont
5
6
7RETAINED_RANGES = (
8 (0x23FB, 0x23FE), # IEC power symbols
9 (0x2630, 0x2630), # Powerline Extra
10 (0x2665, 0x2665), # Octicons
11 (0x26A1, 0x26A1), # Octicons
12 (0x276C, 0x2771), # Nerd Fonts custom heavy brackets
13 (0x2B58, 0x2B58), # IEC power symbols
14 (0xE000, 0xE00A), # Pomicons
15 (0xE0A3, 0xE0A3), # Powerline Extra; standard Powerline ends at U+E0A2
16 (0xE0B4, 0xE0D7), # Powerline Extra; standard Powerline ends at U+E0B3
17 (0xE200, 0xE2A9), # Font Awesome Extension
18 (0xE300, 0xE3E3), # Weather Icons
19 (0xE5FA, 0xE6B8), # Seti-UI and Nerd Fonts custom
20 (0xE700, 0xE8EF), # Devicons
21 (0xEA60, 0xEC1E), # Codicons
22 (0xED00, 0xEFCE), # Font Awesome and Nerd Fonts progress indicators
23 (0xF000, 0xF2FF), # Font Awesome compatibility codepoints
24 (0xF400, 0xF533), # Octicons
25 (0xF0001, 0xF1AF0), # Material Design Icons
26)
27
28
29def main() -> None:
30 if len(sys.argv) != 3:
31 raise SystemExit("usage: subset-symbols.py INPUT.ttf OUTPUT.txt")
32
33 source = TTFont(sys.argv[1])
34 cmap = source.getBestCmap()
35 if cmap is None:
36 raise SystemExit("input font has no Unicode cmap")
37
38 codepoints = sorted(
39 codepoint
40 for codepoint in cmap
41 if any(start <= codepoint <= end for start, end in RETAINED_RANGES)
42 )
43 Path(sys.argv[2]).write_text(
44 "".join(f"U+{codepoint:04X}\n" for codepoint in codepoints), encoding="ascii"
45 )
46
47
48if __name__ == "__main__":
49 main()
packages/terminal/fonts/symbols-requirements.txt created+2
......@@ -0,0 +1,2 @@
1fonttools==4.51.0
2brotli==1.1.0
packages/terminal/package.json created+95
......@@ -0,0 +1,95 @@
1{
2 "name": "@clo/terminal",
3 "version": "0.1.0",
4 "private": true,
5 "description": "browser terminal on libghostty-vt wasm with headless, DOM, React, and Solid APIs",
6 "keywords": [
7 "ghostty",
8 "react",
9 "solid",
10 "terminal",
11 "wasm"
12 ],
13 "license": "ISC",
14 "files": [
15 "src",
16 "fonts",
17 "vendor/ghostty-vt.wasm",
18 "vendor/GHOSTTY-LICENSE",
19 "vendor/COMMIT",
20 "vendor/SHA256",
21 "vendor/README.md",
22 "vendor/patches",
23 "LICENSE",
24 "README.md"
25 ],
26 "type": "module",
27 "sideEffects": [
28 "**/*.css"
29 ],
30 "main": "src/index.ts",
31 "exports": {
32 ".": "./src/index.ts",
33 "./shell": "./src/shell/index.ts",
34 "./react": "./src/react.tsx",
35 "./solid": "./src/solid.tsx",
36 "./terminal.css": "./src/renderer/terminal.css",
37 "./ghostty-vt.wasm": "./vendor/ghostty-vt.wasm",
38 "./fonts/__clo_terminal_primary_34_8_1-Regular.woff2": "./fonts/__clo_terminal_primary_34_8_1-Regular.woff2",
39 "./fonts/__clo_terminal_primary_34_8_1-SemiBold.woff2": "./fonts/__clo_terminal_primary_34_8_1-SemiBold.woff2",
40 "./fonts/__clo_terminal_primary_34_8_1-Italic.woff2": "./fonts/__clo_terminal_primary_34_8_1-Italic.woff2",
41 "./fonts/__clo_terminal_primary_34_8_1-SemiBoldItalic.woff2": "./fonts/__clo_terminal_primary_34_8_1-SemiBoldItalic.woff2",
42 "./fonts/__clo_terminal_symbols_3_4_0-Regular.woff2": "./fonts/__clo_terminal_symbols_3_4_0-Regular.woff2",
43 "./fonts/PRIMARY-CODEPOINTS.txt": "./fonts/PRIMARY-CODEPOINTS.txt",
44 "./fonts/subset-primary.py": "./fonts/subset-primary.py",
45 "./fonts/SYMBOLS-CODEPOINTS.txt": "./fonts/SYMBOLS-CODEPOINTS.txt",
46 "./fonts/subset-symbols.py": "./fonts/subset-symbols.py",
47 "./fonts/symbols-requirements.txt": "./fonts/symbols-requirements.txt",
48 "./fonts/OFL.txt": "./fonts/OFL.txt",
49 "./fonts/README.md": "./fonts/README.md",
50 "./fonts/NOTICES.md": "./fonts/NOTICES.md",
51 "./fonts/licenses/Apache-2.0.txt": "./fonts/licenses/Apache-2.0.txt",
52 "./fonts/licenses/CC-BY-4.0.txt": "./fonts/licenses/CC-BY-4.0.txt",
53 "./fonts/licenses/Devicons-MIT.txt": "./fonts/licenses/Devicons-MIT.txt",
54 "./fonts/licenses/Font-Awesome-Extension-MIT.txt": "./fonts/licenses/Font-Awesome-Extension-MIT.txt",
55 "./fonts/licenses/Font-Awesome-LICENSE.txt": "./fonts/licenses/Font-Awesome-LICENSE.txt",
56 "./fonts/licenses/Material-Design-Icons-NOTICE.txt": "./fonts/licenses/Material-Design-Icons-NOTICE.txt",
57 "./fonts/licenses/NerdFonts-LICENSE.txt": "./fonts/licenses/NerdFonts-LICENSE.txt",
58 "./fonts/licenses/NerdFonts-Symbols-Only-MIT.txt": "./fonts/licenses/NerdFonts-Symbols-Only-MIT.txt",
59 "./fonts/licenses/NerdFonts-license-audit.md": "./fonts/licenses/NerdFonts-license-audit.md",
60 "./fonts/licenses/Octicons-MIT.txt": "./fonts/licenses/Octicons-MIT.txt",
61 "./fonts/licenses/Pomicons-OFL.txt": "./fonts/licenses/Pomicons-OFL.txt",
62 "./fonts/licenses/Powerline-Extra-MIT.txt": "./fonts/licenses/Powerline-Extra-MIT.txt",
63 "./fonts/licenses/Weather-Icons-OFL.txt": "./fonts/licenses/Weather-Icons-OFL.txt"
64 },
65 "scripts": {
66 "typecheck": "tsc-rs -p tsconfig.json --noEmit",
67 "verify:vendor": "node vendor/verify.mjs"
68 },
69 "devDependencies": {
70 "@types/react": "~19.3.0",
71 "@types/react-dom": "~19.2.3",
72 "react": "19.2.6",
73 "react-dom": "19.2.6",
74 "solid-js": "^1.9.14",
75 "vite": "catalog:",
76 "vite-plus": "catalog:"
77 },
78 "peerDependencies": {
79 "react": ">=18 <20",
80 "solid-js": ">=1.8 <2"
81 },
82 "peerDependenciesMeta": {
83 "react": {
84 "optional": true
85 },
86 "solid-js": {
87 "optional": true
88 }
89 },
90 "vendoredFrom": {
91 "repository": "https://shale.paperclover.net/hexi.git",
92 "path": "sys/lib/terminal",
93 "commit": "166e71540b612a54732471cd986948e62e446b13"
94 }
95}
packages/terminal/src/ansi.ts created+283
......@@ -0,0 +1,283 @@
1const ESC = "\u001b";
2
3const ANSI_COLORS = [
4 "var(--clo-term-ansi-black, #000000)",
5 "var(--clo-term-ansi-red, #cd3131)",
6 "var(--clo-term-ansi-green, #0dbc79)",
7 "var(--clo-term-ansi-yellow, #e5e510)",
8 "var(--clo-term-ansi-blue, #2472c8)",
9 "var(--clo-term-ansi-magenta, #bc3fbc)",
10 "var(--clo-term-ansi-cyan, #11a8cd)",
11 "var(--clo-term-ansi-white, #e5e5e5)",
12 "var(--clo-term-ansi-bright-black, #666666)",
13 "var(--clo-term-ansi-bright-red, #f14c4c)",
14 "var(--clo-term-ansi-bright-green, #23d18b)",
15 "var(--clo-term-ansi-bright-yellow, #f5f543)",
16 "var(--clo-term-ansi-bright-blue, #3b8eea)",
17 "var(--clo-term-ansi-bright-magenta, #d670d6)",
18 "var(--clo-term-ansi-bright-cyan, #29b8db)",
19 "var(--clo-term-ansi-bright-white, #ffffff)",
20] as const;
21
22const UNDERLINES = ["", "solid", "double", "wavy", "dotted", "dashed"] as const;
23
24interface AnsiState {
25 bold: boolean;
26 faint: boolean;
27 italic: boolean;
28 underline: "" | "solid" | "double" | "wavy" | "dotted" | "dashed";
29 inverse: boolean;
30 invisible: boolean;
31 strikethrough: boolean;
32 overline: boolean;
33 foreground: string;
34 background: string;
35}
36
37function initialState(): AnsiState {
38 return {
39 bold: false,
40 faint: false,
41 italic: false,
42 underline: "",
43 inverse: false,
44 invisible: false,
45 strikethrough: false,
46 overline: false,
47 foreground: "",
48 background: "",
49 };
50}
51
52function byte(value: number): number {
53 return Math.max(0, Math.min(255, Number.isFinite(value) ? value : 0));
54}
55
56function colorByte(value: string | undefined): number | null {
57 if (value === undefined || !/^\d+$/.test(value)) return null;
58 const result = Number(value);
59 return result <= 255 ? result : null;
60}
61
62function paletteColor(index: number): string {
63 index = Math.trunc(byte(index));
64 const color = ANSI_COLORS[index];
65 if (color !== undefined) return color;
66 if (index >= 232) {
67 const value = byte(8 + (Math.min(index, 255) - 232) * 10);
68 return `rgb(${value} ${value} ${value})`;
69 }
70 const value = Math.max(0, Math.min(215, index - 16));
71 const levels = [0, 95, 135, 175, 215, 255] as const;
72 return `rgb(${levels[Math.floor(value / 36)]} ${levels[Math.floor((value % 36) / 6)]} ${levels[value % 6]})`;
73}
74
75function setColor(state: AnsiState, background: boolean, color: string): void {
76 if (background) state.background = color;
77 else state.foreground = color;
78}
79
80function applyCode(state: AnsiState, code: number): AnsiState {
81 if (code === 0) return initialState();
82 if (code === 1) state.bold = true;
83 else if (code === 2) state.faint = true;
84 else if (code === 3) state.italic = true;
85 else if (code === 4) state.underline = "solid";
86 else if (code === 7) state.inverse = true;
87 else if (code === 8) state.invisible = true;
88 else if (code === 9) state.strikethrough = true;
89 else if (code === 21) state.underline = "double";
90 else if (code === 22) {
91 state.bold = false;
92 state.faint = false;
93 } else if (code === 23) state.italic = false;
94 else if (code === 24) state.underline = "";
95 else if (code === 27) state.inverse = false;
96 else if (code === 28) state.invisible = false;
97 else if (code === 29) state.strikethrough = false;
98 else if (code === 39) state.foreground = "";
99 else if (code === 49) state.background = "";
100 else if (code === 53) state.overline = true;
101 else if (code === 55) state.overline = false;
102 else if (code >= 30 && code <= 37) state.foreground = paletteColor(code - 30);
103 else if (code >= 40 && code <= 47) state.background = paletteColor(code - 40);
104 else if (code >= 90 && code <= 97) state.foreground = paletteColor(code - 82);
105 else if (code >= 100 && code <= 107) state.background = paletteColor(code - 92);
106 return state;
107}
108
109function applySgr(state: AnsiState, parameters: string): AnsiState {
110 const groups = parameters === "" ? ["0"] : parameters.split(";");
111 for (let index = 0; index < groups.length; index++) {
112 const parts = groups[index]!.split(":");
113 const code = Number(parts[0] || 0);
114 if ((code === 38 || code === 48) && parts.length > 1) {
115 const mode = Number(parts[1]);
116 if (mode === 5 && parts.length === 3) {
117 const color = colorByte(parts[2]);
118 if (color !== null) setColor(state, code === 48, paletteColor(color));
119 } else if (
120 mode === 2 &&
121 (parts.length === 5 || (parts.length === 6 && (parts[2] === "" || /^\d+$/.test(parts[2]!))))
122 ) {
123 const rgb = parts.slice(-3).map(colorByte);
124 if (rgb.every((value) => value !== null))
125 setColor(state, code === 48, `rgb(${rgb[0]} ${rgb[1]} ${rgb[2]})`);
126 }
127 continue;
128 }
129 if (code === 4 && parts.length > 1) {
130 state.underline = UNDERLINES[Number(parts[1])] ?? "";
131 continue;
132 }
133 if ((code === 38 || code === 48) && groups[index + 1] === "5") {
134 const color = colorByte(groups[index + 2]);
135 if (color !== null) setColor(state, code === 48, paletteColor(color));
136 index += 2;
137 continue;
138 }
139 if ((code === 38 || code === 48) && groups[index + 1] === "2") {
140 const rgb = groups.slice(index + 2, index + 5).map(colorByte);
141 if (rgb.length === 3 && rgb.every((value) => value !== null))
142 setColor(state, code === 48, `rgb(${rgb[0]} ${rgb[1]} ${rgb[2]})`);
143 index += 4;
144 continue;
145 }
146 state = applyCode(state, code);
147 }
148 return state;
149}
150
151function styles(state: AnsiState): string {
152 const rules: string[] = [];
153 if (state.bold) rules.push("font-weight:700");
154 if (state.faint) rules.push("opacity:0.68");
155 if (state.italic) rules.push("font-style:italic");
156 const decorations = [
157 state.underline ? "underline" : "",
158 state.strikethrough ? "line-through" : "",
159 state.overline ? "overline" : "",
160 ].filter(Boolean);
161 if (decorations.length > 0) rules.push(`text-decoration-line:${decorations.join(" ")}`);
162 if (state.underline && state.underline !== "solid")
163 rules.push(`text-decoration-style:${state.underline}`);
164
165 let foreground = state.foreground;
166 let background = state.background;
167 if (state.inverse) {
168 [foreground, background] = [
169 background || "var(--clo-term-bg, #1e1e2e)",
170 foreground || "var(--clo-term-fg, #cdd6f4)",
171 ];
172 }
173 if (state.invisible) foreground = "transparent";
174 if (foreground) rules.push(`color:${foreground}`);
175 if (background) rules.push(`background-color:${background}`);
176 return rules.join(";");
177}
178
179function escapeHtml(text: string): string {
180 return text.replace(/[&<>"'\r]/g, (character) => {
181 if (character === "&") return "&amp;";
182 if (character === "<") return "&lt;";
183 if (character === ">") return "&gt;";
184 if (character === '"') return "&quot;";
185 if (character === "'") return "&#39;";
186 return "&#13;";
187 });
188}
189
190function stringEnd(text: string, start: number, bell: boolean): number {
191 for (let index = start; index < text.length; index++) {
192 const code = text.charCodeAt(index);
193 if (code === 0x18 || code === 0x1a || code === 0x9c || (bell && code === 0x07))
194 return index + 1;
195 if (text[index] === ESC && text[index + 1] === "\\") return index + 2;
196 if (text[index] === ESC) return index;
197 }
198 return text.length;
199}
200
201/**
202 * Convert styled ANSI text to escaped inline HTML. Terminal cursor, input,
203 * viewport, and scrolling behavior are intentionally absent; non-SGR control
204 * sequences are removed. Whitespace layout remains the caller's choice.
205 */
206export function ansiToHtml(text: string): string {
207 let state = initialState();
208 let html = "";
209 let plainStart = 0;
210 let index = 0;
211
212 const append = (value: string) => {
213 if (value === "") return;
214 const escaped = escapeHtml(value);
215 const style = styles(state);
216 html += style === "" ? escaped : `<span style="${style}">${escaped}</span>`;
217 };
218 const emit = (end: number) => append(text.slice(plainStart, end));
219
220 while (index < text.length) {
221 const code = text.charCodeAt(index);
222 const preservedWhitespace = code === 0x09 || code === 0x0a || code === 0x0d;
223 if (
224 code !== 0x1b &&
225 code !== 0x7f &&
226 !(code < 0x20 && !preservedWhitespace) &&
227 !(code >= 0x80 && code <= 0x9f)
228 ) {
229 index++;
230 continue;
231 }
232
233 emit(index);
234 const next = text[index + 1];
235 if ((code === 0x1b && next === "[") || code === 0x9b) {
236 let parameters = "";
237 let end = index + (code === 0x9b ? 1 : 2);
238 while (end < text.length) {
239 const final = text.charCodeAt(end);
240 if (final === 0x18 || final === 0x1a) {
241 end++;
242 break;
243 }
244 if (final === 0x1b || (final >= 0x80 && final <= 0x9f)) break;
245 if (final < 0x20 || final === 0x7f) {
246 if (final === 0x09 || final === 0x0a || final === 0x0d) append(text[end]!);
247 end++;
248 continue;
249 }
250 if (final >= 0x40 && final <= 0x7e) {
251 if (final === 0x6d && /^[0-9:;]*$/.test(parameters)) state = applySgr(state, parameters);
252 end++;
253 break;
254 }
255 parameters += text[end];
256 end++;
257 }
258 index = end;
259 } else if ((code === 0x1b && next === "]") || code === 0x9d) {
260 index = stringEnd(text, index + (code === 0x9d ? 1 : 2), true);
261 } else if (
262 (code === 0x1b && (next === "P" || next === "X" || next === "^" || next === "_")) ||
263 code === 0x90 ||
264 code === 0x98 ||
265 code === 0x9e ||
266 code === 0x9f
267 ) {
268 index = stringEnd(text, index + (code === 0x1b ? 2 : 1), false);
269 } else if (code === 0x1b) {
270 index++;
271 while (index < text.length) {
272 const intermediate = text.charCodeAt(index);
273 if (intermediate < 0x20 || intermediate > 0x2f) break;
274 index++;
275 }
276 const final = text.charCodeAt(index);
277 if (final >= 0x30 && final <= 0x7e) index++;
278 } else index++;
279 plainStart = index;
280 }
281 emit(text.length);
282 return html;
283}
packages/terminal/src/ghostty/constants.ts created+759
......@@ -0,0 +1,759 @@
1// GENERATED by vendor/gen-constants.mjs from the vendored libghostty-vt headers
2// (commit in vendor/COMMIT). Do not edit by hand; re-run the generator on bump.
3
4export const GhosttyBuildInfo = {
5 GHOSTTY_BUILD_INFO_INVALID: 0,
6 GHOSTTY_BUILD_INFO_SIMD: 1,
7 GHOSTTY_BUILD_INFO_KITTY_GRAPHICS: 2,
8 GHOSTTY_BUILD_INFO_TMUX_CONTROL_MODE: 3,
9 GHOSTTY_BUILD_INFO_OPTIMIZE: 4,
10 GHOSTTY_BUILD_INFO_VERSION_STRING: 5,
11 GHOSTTY_BUILD_INFO_VERSION_MAJOR: 6,
12 GHOSTTY_BUILD_INFO_VERSION_MINOR: 7,
13 GHOSTTY_BUILD_INFO_VERSION_PATCH: 8,
14 GHOSTTY_BUILD_INFO_VERSION_PRE: 9,
15 GHOSTTY_BUILD_INFO_VERSION_BUILD: 10,
16} as const;
17
18export const GhosttyCellContentTag = {
19 GHOSTTY_CELL_CONTENT_CODEPOINT: 0,
20 GHOSTTY_CELL_CONTENT_CODEPOINT_GRAPHEME: 1,
21 GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE: 2,
22 GHOSTTY_CELL_CONTENT_BG_COLOR_RGB: 3,
23} as const;
24
25export const GhosttyCellData = {
26 GHOSTTY_CELL_DATA_INVALID: 0,
27 GHOSTTY_CELL_DATA_CODEPOINT: 1,
28 GHOSTTY_CELL_DATA_CONTENT_TAG: 2,
29 GHOSTTY_CELL_DATA_WIDE: 3,
30 GHOSTTY_CELL_DATA_HAS_TEXT: 4,
31 GHOSTTY_CELL_DATA_HAS_STYLING: 5,
32 GHOSTTY_CELL_DATA_STYLE_ID: 6,
33 GHOSTTY_CELL_DATA_HAS_HYPERLINK: 7,
34 GHOSTTY_CELL_DATA_PROTECTED: 8,
35 GHOSTTY_CELL_DATA_SEMANTIC_CONTENT: 9,
36 GHOSTTY_CELL_DATA_COLOR_PALETTE: 10,
37 GHOSTTY_CELL_DATA_COLOR_RGB: 11,
38} as const;
39
40export const GhosttyCellSemanticContent = {
41 GHOSTTY_CELL_SEMANTIC_OUTPUT: 0,
42 GHOSTTY_CELL_SEMANTIC_INPUT: 1,
43 GHOSTTY_CELL_SEMANTIC_PROMPT: 2,
44} as const;
45
46export const GhosttyCellWide = {
47 GHOSTTY_CELL_WIDE_NARROW: 0,
48 GHOSTTY_CELL_WIDE_WIDE: 1,
49 GHOSTTY_CELL_WIDE_SPACER_TAIL: 2,
50 GHOSTTY_CELL_WIDE_SPACER_HEAD: 3,
51} as const;
52
53export const GhosttyClipboardLocation = {
54 GHOSTTY_CLIPBOARD_LOCATION_STANDARD: 0,
55 GHOSTTY_CLIPBOARD_LOCATION_SELECTION: 1,
56 GHOSTTY_CLIPBOARD_LOCATION_PRIMARY: 2,
57} as const;
58
59export const GhosttyClipboardWriteResult = {
60 GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS: 0,
61 GHOSTTY_CLIPBOARD_WRITE_RESULT_DENIED: 1,
62 GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED: 2,
63 GHOSTTY_CLIPBOARD_WRITE_RESULT_BUSY: 3,
64 GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA: 4,
65 GHOSTTY_CLIPBOARD_WRITE_RESULT_IO_ERROR: 5,
66} as const;
67
68export const GhosttyColorScheme = {
69 GHOSTTY_COLOR_SCHEME_LIGHT: 0,
70 GHOSTTY_COLOR_SCHEME_DARK: 1,
71} as const;
72
73export const GhosttyFocusEvent = {
74 GHOSTTY_FOCUS_GAINED: 0,
75 GHOSTTY_FOCUS_LOST: 1,
76} as const;
77
78export const GhosttyFormatterFormat = {
79 GHOSTTY_FORMATTER_FORMAT_PLAIN: 0,
80 GHOSTTY_FORMATTER_FORMAT_VT: 1,
81 GHOSTTY_FORMATTER_FORMAT_HTML: 2,
82} as const;
83
84export const GhosttyKey = {
85 GHOSTTY_KEY_UNIDENTIFIED: 0,
86 GHOSTTY_KEY_BACKQUOTE: 1,
87 GHOSTTY_KEY_BACKSLASH: 2,
88 GHOSTTY_KEY_BRACKET_LEFT: 3,
89 GHOSTTY_KEY_BRACKET_RIGHT: 4,
90 GHOSTTY_KEY_COMMA: 5,
91 GHOSTTY_KEY_DIGIT_0: 6,
92 GHOSTTY_KEY_DIGIT_1: 7,
93 GHOSTTY_KEY_DIGIT_2: 8,
94 GHOSTTY_KEY_DIGIT_3: 9,
95 GHOSTTY_KEY_DIGIT_4: 10,
96 GHOSTTY_KEY_DIGIT_5: 11,
97 GHOSTTY_KEY_DIGIT_6: 12,
98 GHOSTTY_KEY_DIGIT_7: 13,
99 GHOSTTY_KEY_DIGIT_8: 14,
100 GHOSTTY_KEY_DIGIT_9: 15,
101 GHOSTTY_KEY_EQUAL: 16,
102 GHOSTTY_KEY_INTL_BACKSLASH: 17,
103 GHOSTTY_KEY_INTL_RO: 18,
104 GHOSTTY_KEY_INTL_YEN: 19,
105 GHOSTTY_KEY_A: 20,
106 GHOSTTY_KEY_B: 21,
107 GHOSTTY_KEY_C: 22,
108 GHOSTTY_KEY_D: 23,
109 GHOSTTY_KEY_E: 24,
110 GHOSTTY_KEY_F: 25,
111 GHOSTTY_KEY_G: 26,
112 GHOSTTY_KEY_H: 27,
113 GHOSTTY_KEY_I: 28,
114 GHOSTTY_KEY_J: 29,
115 GHOSTTY_KEY_K: 30,
116 GHOSTTY_KEY_L: 31,
117 GHOSTTY_KEY_M: 32,
118 GHOSTTY_KEY_N: 33,
119 GHOSTTY_KEY_O: 34,
120 GHOSTTY_KEY_P: 35,
121 GHOSTTY_KEY_Q: 36,
122 GHOSTTY_KEY_R: 37,
123 GHOSTTY_KEY_S: 38,
124 GHOSTTY_KEY_T: 39,
125 GHOSTTY_KEY_U: 40,
126 GHOSTTY_KEY_V: 41,
127 GHOSTTY_KEY_W: 42,
128 GHOSTTY_KEY_X: 43,
129 GHOSTTY_KEY_Y: 44,
130 GHOSTTY_KEY_Z: 45,
131 GHOSTTY_KEY_MINUS: 46,
132 GHOSTTY_KEY_PERIOD: 47,
133 GHOSTTY_KEY_QUOTE: 48,
134 GHOSTTY_KEY_SEMICOLON: 49,
135 GHOSTTY_KEY_SLASH: 50,
136 GHOSTTY_KEY_ALT_LEFT: 51,
137 GHOSTTY_KEY_ALT_RIGHT: 52,
138 GHOSTTY_KEY_BACKSPACE: 53,
139 GHOSTTY_KEY_CAPS_LOCK: 54,
140 GHOSTTY_KEY_CONTEXT_MENU: 55,
141 GHOSTTY_KEY_CONTROL_LEFT: 56,
142 GHOSTTY_KEY_CONTROL_RIGHT: 57,
143 GHOSTTY_KEY_ENTER: 58,
144 GHOSTTY_KEY_META_LEFT: 59,
145 GHOSTTY_KEY_META_RIGHT: 60,
146 GHOSTTY_KEY_SHIFT_LEFT: 61,
147 GHOSTTY_KEY_SHIFT_RIGHT: 62,
148 GHOSTTY_KEY_SPACE: 63,
149 GHOSTTY_KEY_TAB: 64,
150 GHOSTTY_KEY_CONVERT: 65,
151 GHOSTTY_KEY_KANA_MODE: 66,
152 GHOSTTY_KEY_NON_CONVERT: 67,
153 GHOSTTY_KEY_DELETE: 68,
154 GHOSTTY_KEY_END: 69,
155 GHOSTTY_KEY_HELP: 70,
156 GHOSTTY_KEY_HOME: 71,
157 GHOSTTY_KEY_INSERT: 72,
158 GHOSTTY_KEY_PAGE_DOWN: 73,
159 GHOSTTY_KEY_PAGE_UP: 74,
160 GHOSTTY_KEY_ARROW_DOWN: 75,
161 GHOSTTY_KEY_ARROW_LEFT: 76,
162 GHOSTTY_KEY_ARROW_RIGHT: 77,
163 GHOSTTY_KEY_ARROW_UP: 78,
164 GHOSTTY_KEY_NUM_LOCK: 79,
165 GHOSTTY_KEY_NUMPAD_0: 80,
166 GHOSTTY_KEY_NUMPAD_1: 81,
167 GHOSTTY_KEY_NUMPAD_2: 82,
168 GHOSTTY_KEY_NUMPAD_3: 83,
169 GHOSTTY_KEY_NUMPAD_4: 84,
170 GHOSTTY_KEY_NUMPAD_5: 85,
171 GHOSTTY_KEY_NUMPAD_6: 86,
172 GHOSTTY_KEY_NUMPAD_7: 87,
173 GHOSTTY_KEY_NUMPAD_8: 88,
174 GHOSTTY_KEY_NUMPAD_9: 89,
175 GHOSTTY_KEY_NUMPAD_ADD: 90,
176 GHOSTTY_KEY_NUMPAD_BACKSPACE: 91,
177 GHOSTTY_KEY_NUMPAD_CLEAR: 92,
178 GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY: 93,
179 GHOSTTY_KEY_NUMPAD_COMMA: 94,
180 GHOSTTY_KEY_NUMPAD_DECIMAL: 95,
181 GHOSTTY_KEY_NUMPAD_DIVIDE: 96,
182 GHOSTTY_KEY_NUMPAD_ENTER: 97,
183 GHOSTTY_KEY_NUMPAD_EQUAL: 98,
184 GHOSTTY_KEY_NUMPAD_MEMORY_ADD: 99,
185 GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR: 100,
186 GHOSTTY_KEY_NUMPAD_MEMORY_RECALL: 101,
187 GHOSTTY_KEY_NUMPAD_MEMORY_STORE: 102,
188 GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT: 103,
189 GHOSTTY_KEY_NUMPAD_MULTIPLY: 104,
190 GHOSTTY_KEY_NUMPAD_PAREN_LEFT: 105,
191 GHOSTTY_KEY_NUMPAD_PAREN_RIGHT: 106,
192 GHOSTTY_KEY_NUMPAD_SUBTRACT: 107,
193 GHOSTTY_KEY_NUMPAD_SEPARATOR: 108,
194 GHOSTTY_KEY_NUMPAD_UP: 109,
195 GHOSTTY_KEY_NUMPAD_DOWN: 110,
196 GHOSTTY_KEY_NUMPAD_RIGHT: 111,
197 GHOSTTY_KEY_NUMPAD_LEFT: 112,
198 GHOSTTY_KEY_NUMPAD_BEGIN: 113,
199 GHOSTTY_KEY_NUMPAD_HOME: 114,
200 GHOSTTY_KEY_NUMPAD_END: 115,
201 GHOSTTY_KEY_NUMPAD_INSERT: 116,
202 GHOSTTY_KEY_NUMPAD_DELETE: 117,
203 GHOSTTY_KEY_NUMPAD_PAGE_UP: 118,
204 GHOSTTY_KEY_NUMPAD_PAGE_DOWN: 119,
205 GHOSTTY_KEY_ESCAPE: 120,
206 GHOSTTY_KEY_F1: 121,
207 GHOSTTY_KEY_F2: 122,
208 GHOSTTY_KEY_F3: 123,
209 GHOSTTY_KEY_F4: 124,
210 GHOSTTY_KEY_F5: 125,
211 GHOSTTY_KEY_F6: 126,
212 GHOSTTY_KEY_F7: 127,
213 GHOSTTY_KEY_F8: 128,
214 GHOSTTY_KEY_F9: 129,
215 GHOSTTY_KEY_F10: 130,
216 GHOSTTY_KEY_F11: 131,
217 GHOSTTY_KEY_F12: 132,
218 GHOSTTY_KEY_F13: 133,
219 GHOSTTY_KEY_F14: 134,
220 GHOSTTY_KEY_F15: 135,
221 GHOSTTY_KEY_F16: 136,
222 GHOSTTY_KEY_F17: 137,
223 GHOSTTY_KEY_F18: 138,
224 GHOSTTY_KEY_F19: 139,
225 GHOSTTY_KEY_F20: 140,
226 GHOSTTY_KEY_F21: 141,
227 GHOSTTY_KEY_F22: 142,
228 GHOSTTY_KEY_F23: 143,
229 GHOSTTY_KEY_F24: 144,
230 GHOSTTY_KEY_F25: 145,
231 GHOSTTY_KEY_FN: 146,
232 GHOSTTY_KEY_FN_LOCK: 147,
233 GHOSTTY_KEY_PRINT_SCREEN: 148,
234 GHOSTTY_KEY_SCROLL_LOCK: 149,
235 GHOSTTY_KEY_PAUSE: 150,
236 GHOSTTY_KEY_BROWSER_BACK: 151,
237 GHOSTTY_KEY_BROWSER_FAVORITES: 152,
238 GHOSTTY_KEY_BROWSER_FORWARD: 153,
239 GHOSTTY_KEY_BROWSER_HOME: 154,
240 GHOSTTY_KEY_BROWSER_REFRESH: 155,
241 GHOSTTY_KEY_BROWSER_SEARCH: 156,
242 GHOSTTY_KEY_BROWSER_STOP: 157,
243 GHOSTTY_KEY_EJECT: 158,
244 GHOSTTY_KEY_LAUNCH_APP_1: 159,
245 GHOSTTY_KEY_LAUNCH_APP_2: 160,
246 GHOSTTY_KEY_LAUNCH_MAIL: 161,
247 GHOSTTY_KEY_MEDIA_PLAY_PAUSE: 162,
248 GHOSTTY_KEY_MEDIA_SELECT: 163,
249 GHOSTTY_KEY_MEDIA_STOP: 164,
250 GHOSTTY_KEY_MEDIA_TRACK_NEXT: 165,
251 GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS: 166,
252 GHOSTTY_KEY_POWER: 167,
253 GHOSTTY_KEY_SLEEP: 168,
254 GHOSTTY_KEY_AUDIO_VOLUME_DOWN: 169,
255 GHOSTTY_KEY_AUDIO_VOLUME_MUTE: 170,
256 GHOSTTY_KEY_AUDIO_VOLUME_UP: 171,
257 GHOSTTY_KEY_WAKE_UP: 172,
258 GHOSTTY_KEY_COPY: 173,
259 GHOSTTY_KEY_CUT: 174,
260 GHOSTTY_KEY_PASTE: 175,
261} as const;
262
263export const GhosttyKeyAction = {
264 GHOSTTY_KEY_ACTION_RELEASE: 0,
265 GHOSTTY_KEY_ACTION_PRESS: 1,
266 GHOSTTY_KEY_ACTION_REPEAT: 2,
267} as const;
268
269export const GhosttyKeyEncoderOption = {
270 GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION: 0,
271 GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION: 1,
272 GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK: 2,
273 GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX: 3,
274 GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2: 4,
275 GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS: 5,
276 GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT: 6,
277 GHOSTTY_KEY_ENCODER_OPT_BACKARROW_KEY_MODE: 7,
278} as const;
279
280export const GhosttyKittyGraphicsData = {
281 GHOSTTY_KITTY_GRAPHICS_DATA_INVALID: 0,
282 GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR: 1,
283 GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION: 2,
284} as const;
285
286export const GhosttyKittyGraphicsImageData = {
287 GHOSTTY_KITTY_IMAGE_DATA_INVALID: 0,
288 GHOSTTY_KITTY_IMAGE_DATA_ID: 1,
289 GHOSTTY_KITTY_IMAGE_DATA_NUMBER: 2,
290 GHOSTTY_KITTY_IMAGE_DATA_WIDTH: 3,
291 GHOSTTY_KITTY_IMAGE_DATA_HEIGHT: 4,
292 GHOSTTY_KITTY_IMAGE_DATA_FORMAT: 5,
293 GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION: 6,
294 GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR: 7,
295 GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN: 8,
296 GHOSTTY_KITTY_IMAGE_DATA_GENERATION: 9,
297} as const;
298
299export const GhosttyKittyGraphicsPlacementData = {
300 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INVALID: 0,
301 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID: 1,
302 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID: 2,
303 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL: 3,
304 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET: 4,
305 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET: 5,
306 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X: 6,
307 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y: 7,
308 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH: 8,
309 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT: 9,
310 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS: 10,
311 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_ROWS: 11,
312 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z: 12,
313} as const;
314
315export const GhosttyKittyGraphicsPlacementIteratorOption = {
316 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER: 0,
317} as const;
318
319export const GhosttyKittyImageCompression = {
320 GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE: 0,
321 GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE: 1,
322} as const;
323
324export const GhosttyKittyImageFormat = {
325 GHOSTTY_KITTY_IMAGE_FORMAT_RGB: 0,
326 GHOSTTY_KITTY_IMAGE_FORMAT_RGBA: 1,
327 GHOSTTY_KITTY_IMAGE_FORMAT_PNG: 2,
328 GHOSTTY_KITTY_IMAGE_FORMAT_GRAY_ALPHA: 3,
329 GHOSTTY_KITTY_IMAGE_FORMAT_GRAY: 4,
330} as const;
331
332export const GhosttyKittyPlacementLayer = {
333 GHOSTTY_KITTY_PLACEMENT_LAYER_ALL: 0,
334 GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_BG: 1,
335 GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_TEXT: 2,
336 GHOSTTY_KITTY_PLACEMENT_LAYER_ABOVE_TEXT: 3,
337} as const;
338
339export const GhosttyModeReportState = {
340 GHOSTTY_MODE_REPORT_NOT_RECOGNIZED: 0,
341 GHOSTTY_MODE_REPORT_SET: 1,
342 GHOSTTY_MODE_REPORT_RESET: 2,
343 GHOSTTY_MODE_REPORT_PERMANENTLY_SET: 3,
344 GHOSTTY_MODE_REPORT_PERMANENTLY_RESET: 4,
345} as const;
346
347export const GhosttyMouseAction = {
348 GHOSTTY_MOUSE_ACTION_PRESS: 0,
349 GHOSTTY_MOUSE_ACTION_RELEASE: 1,
350 GHOSTTY_MOUSE_ACTION_MOTION: 2,
351} as const;
352
353export const GhosttyMouseButton = {
354 GHOSTTY_MOUSE_BUTTON_UNKNOWN: 0,
355 GHOSTTY_MOUSE_BUTTON_LEFT: 1,
356 GHOSTTY_MOUSE_BUTTON_RIGHT: 2,
357 GHOSTTY_MOUSE_BUTTON_MIDDLE: 3,
358 GHOSTTY_MOUSE_BUTTON_FOUR: 4,
359 GHOSTTY_MOUSE_BUTTON_FIVE: 5,
360 GHOSTTY_MOUSE_BUTTON_SIX: 6,
361 GHOSTTY_MOUSE_BUTTON_SEVEN: 7,
362 GHOSTTY_MOUSE_BUTTON_EIGHT: 8,
363 GHOSTTY_MOUSE_BUTTON_NINE: 9,
364 GHOSTTY_MOUSE_BUTTON_TEN: 10,
365 GHOSTTY_MOUSE_BUTTON_ELEVEN: 11,
366} as const;
367
368export const GhosttyMouseEncoderOption = {
369 GHOSTTY_MOUSE_ENCODER_OPT_EVENT: 0,
370 GHOSTTY_MOUSE_ENCODER_OPT_FORMAT: 1,
371 GHOSTTY_MOUSE_ENCODER_OPT_SIZE: 2,
372 GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED: 3,
373 GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL: 4,
374} as const;
375
376export const GhosttyMouseFormat = {
377 GHOSTTY_MOUSE_FORMAT_X10: 0,
378 GHOSTTY_MOUSE_FORMAT_UTF8: 1,
379 GHOSTTY_MOUSE_FORMAT_SGR: 2,
380 GHOSTTY_MOUSE_FORMAT_URXVT: 3,
381 GHOSTTY_MOUSE_FORMAT_SGR_PIXELS: 4,
382} as const;
383
384export const GhosttyMouseTrackingMode = {
385 GHOSTTY_MOUSE_TRACKING_NONE: 0,
386 GHOSTTY_MOUSE_TRACKING_X10: 1,
387 GHOSTTY_MOUSE_TRACKING_NORMAL: 2,
388 GHOSTTY_MOUSE_TRACKING_BUTTON: 3,
389 GHOSTTY_MOUSE_TRACKING_ANY: 4,
390} as const;
391
392export const GhosttyOptimizeMode = {
393 GHOSTTY_OPTIMIZE_DEBUG: 0,
394 GHOSTTY_OPTIMIZE_RELEASE_SAFE: 1,
395 GHOSTTY_OPTIMIZE_RELEASE_SMALL: 2,
396 GHOSTTY_OPTIMIZE_RELEASE_FAST: 3,
397} as const;
398
399export const GhosttyOptionAsAlt = {
400 GHOSTTY_OPTION_AS_ALT_FALSE: 0,
401 GHOSTTY_OPTION_AS_ALT_TRUE: 1,
402 GHOSTTY_OPTION_AS_ALT_LEFT: 2,
403 GHOSTTY_OPTION_AS_ALT_RIGHT: 3,
404} as const;
405
406export const GhosttyOscCommandData = {
407 GHOSTTY_OSC_DATA_INVALID: 0,
408 GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR: 1,
409} as const;
410
411export const GhosttyOscCommandType = {
412 GHOSTTY_OSC_COMMAND_INVALID: 0,
413 GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE: 1,
414 GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON: 2,
415 GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT: 3,
416 GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS: 4,
417 GHOSTTY_OSC_COMMAND_REPORT_PWD: 5,
418 GHOSTTY_OSC_COMMAND_MOUSE_SHAPE: 6,
419 GHOSTTY_OSC_COMMAND_COLOR_OPERATION: 7,
420 GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL: 8,
421 GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION: 9,
422 GHOSTTY_OSC_COMMAND_HYPERLINK_START: 10,
423 GHOSTTY_OSC_COMMAND_HYPERLINK_END: 11,
424 GHOSTTY_OSC_COMMAND_CONEMU_SLEEP: 12,
425 GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX: 13,
426 GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE: 14,
427 GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT: 15,
428 GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT: 16,
429 GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO: 17,
430 GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS: 18,
431 GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE: 19,
432 GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION: 20,
433 GHOSTTY_OSC_COMMAND_CONEMU_COMMENT: 21,
434 GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING: 22,
435} as const;
436
437export const GhosttyPointTag = {
438 GHOSTTY_POINT_TAG_ACTIVE: 0,
439 GHOSTTY_POINT_TAG_VIEWPORT: 1,
440 GHOSTTY_POINT_TAG_SCREEN: 2,
441 GHOSTTY_POINT_TAG_HISTORY: 3,
442} as const;
443
444export const GhosttyRenderStateCursorVisualStyle = {
445 GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR: 0,
446 GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK: 1,
447 GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE: 2,
448 GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW: 3,
449} as const;
450
451export const GhosttyRenderStateData = {
452 GHOSTTY_RENDER_STATE_DATA_INVALID: 0,
453 GHOSTTY_RENDER_STATE_DATA_COLS: 1,
454 GHOSTTY_RENDER_STATE_DATA_ROWS: 2,
455 GHOSTTY_RENDER_STATE_DATA_DIRTY: 3,
456 GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR: 4,
457 GHOSTTY_RENDER_STATE_DATA_COLOR_BACKGROUND: 5,
458 GHOSTTY_RENDER_STATE_DATA_COLOR_FOREGROUND: 6,
459 GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR: 7,
460 GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE: 8,
461 GHOSTTY_RENDER_STATE_DATA_COLOR_PALETTE: 9,
462 GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE: 10,
463 GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE: 11,
464 GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING: 12,
465 GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT: 13,
466 GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE: 14,
467 GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X: 15,
468 GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y: 16,
469 GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL: 17,
470} as const;
471
472export const GhosttyRenderStateDirty = {
473 GHOSTTY_RENDER_STATE_DIRTY_FALSE: 0,
474 GHOSTTY_RENDER_STATE_DIRTY_PARTIAL: 1,
475 GHOSTTY_RENDER_STATE_DIRTY_FULL: 2,
476} as const;
477
478export const GhosttyRenderStateOption = {
479 GHOSTTY_RENDER_STATE_OPTION_DIRTY: 0,
480} as const;
481
482export const GhosttyRenderStateRowCellsData = {
483 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_INVALID: 0,
484 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW: 1,
485 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE: 2,
486 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN: 3,
487 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF: 4,
488 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR: 5,
489 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR: 6,
490 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_SELECTED: 7,
491 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_HAS_STYLING: 8,
492 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8: 9,
493} as const;
494
495export const GhosttyRenderStateRowData = {
496 GHOSTTY_RENDER_STATE_ROW_DATA_INVALID: 0,
497 GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY: 1,
498 GHOSTTY_RENDER_STATE_ROW_DATA_RAW: 2,
499 GHOSTTY_RENDER_STATE_ROW_DATA_CELLS: 3,
500 GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION: 4,
501} as const;
502
503export const GhosttyRenderStateRowOption = {
504 GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY: 0,
505} as const;
506
507export const GhosttyResult = {
508 GHOSTTY_SUCCESS: 0,
509 GHOSTTY_OUT_OF_MEMORY: -1,
510 GHOSTTY_INVALID_VALUE: -2,
511 GHOSTTY_OUT_OF_SPACE: -3,
512 GHOSTTY_NO_VALUE: -4,
513} as const;
514
515export const GhosttyRowData = {
516 GHOSTTY_ROW_DATA_INVALID: 0,
517 GHOSTTY_ROW_DATA_WRAP: 1,
518 GHOSTTY_ROW_DATA_WRAP_CONTINUATION: 2,
519 GHOSTTY_ROW_DATA_GRAPHEME: 3,
520 GHOSTTY_ROW_DATA_STYLED: 4,
521 GHOSTTY_ROW_DATA_HYPERLINK: 5,
522 GHOSTTY_ROW_DATA_SEMANTIC_PROMPT: 6,
523 GHOSTTY_ROW_DATA_KITTY_VIRTUAL_PLACEHOLDER: 7,
524 GHOSTTY_ROW_DATA_DIRTY: 8,
525} as const;
526
527export const GhosttyRowSemanticPrompt = {
528 GHOSTTY_ROW_SEMANTIC_NONE: 0,
529 GHOSTTY_ROW_SEMANTIC_PROMPT: 1,
530 GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION: 2,
531} as const;
532
533export const GhosttySelectionAdjust = {
534 GHOSTTY_SELECTION_ADJUST_LEFT: 0,
535 GHOSTTY_SELECTION_ADJUST_RIGHT: 1,
536 GHOSTTY_SELECTION_ADJUST_UP: 2,
537 GHOSTTY_SELECTION_ADJUST_DOWN: 3,
538 GHOSTTY_SELECTION_ADJUST_HOME: 4,
539 GHOSTTY_SELECTION_ADJUST_END: 5,
540 GHOSTTY_SELECTION_ADJUST_PAGE_UP: 6,
541 GHOSTTY_SELECTION_ADJUST_PAGE_DOWN: 7,
542 GHOSTTY_SELECTION_ADJUST_BEGINNING_OF_LINE: 8,
543 GHOSTTY_SELECTION_ADJUST_END_OF_LINE: 9,
544} as const;
545
546export const GhosttySelectionGestureAutoscroll = {
547 GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_NONE: 0,
548 GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_UP: 1,
549 GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_DOWN: 2,
550} as const;
551
552export const GhosttySelectionGestureBehavior = {
553 GHOSTTY_SELECTION_GESTURE_BEHAVIOR_CELL: 0,
554 GHOSTTY_SELECTION_GESTURE_BEHAVIOR_WORD: 1,
555 GHOSTTY_SELECTION_GESTURE_BEHAVIOR_LINE: 2,
556 GHOSTTY_SELECTION_GESTURE_BEHAVIOR_OUTPUT: 3,
557} as const;
558
559export const GhosttySelectionGestureData = {
560 GHOSTTY_SELECTION_GESTURE_DATA_CLICK_COUNT: 0,
561 GHOSTTY_SELECTION_GESTURE_DATA_DRAGGED: 1,
562 GHOSTTY_SELECTION_GESTURE_DATA_AUTOSCROLL: 2,
563 GHOSTTY_SELECTION_GESTURE_DATA_BEHAVIOR: 3,
564 GHOSTTY_SELECTION_GESTURE_DATA_ANCHOR: 4,
565} as const;
566
567export const GhosttySelectionGestureEventOption = {
568 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF: 0,
569 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_POSITION: 1,
570 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_DISTANCE: 2,
571 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_TIME_NS: 3,
572 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_INTERVAL_NS: 4,
573 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS: 5,
574 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_BEHAVIORS: 6,
575 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_RECTANGLE: 7,
576 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY: 8,
577 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_VIEWPORT: 9,
578} as const;
579
580export const GhosttySelectionGestureEventType = {
581 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS: 0,
582 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE: 1,
583 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DRAG: 2,
584 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_AUTOSCROLL_TICK: 3,
585 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DEEP_PRESS: 4,
586} as const;
587
588export const GhosttySelectionOrder = {
589 GHOSTTY_SELECTION_ORDER_FORWARD: 0,
590 GHOSTTY_SELECTION_ORDER_REVERSE: 1,
591 GHOSTTY_SELECTION_ORDER_MIRRORED_FORWARD: 2,
592 GHOSTTY_SELECTION_ORDER_MIRRORED_REVERSE: 3,
593} as const;
594
595export const GhosttySgrAttributeTag = {
596 GHOSTTY_SGR_ATTR_UNSET: 0,
597 GHOSTTY_SGR_ATTR_UNKNOWN: 1,
598 GHOSTTY_SGR_ATTR_BOLD: 2,
599 GHOSTTY_SGR_ATTR_RESET_BOLD: 3,
600 GHOSTTY_SGR_ATTR_ITALIC: 4,
601 GHOSTTY_SGR_ATTR_RESET_ITALIC: 5,
602 GHOSTTY_SGR_ATTR_FAINT: 6,
603 GHOSTTY_SGR_ATTR_UNDERLINE: 7,
604 GHOSTTY_SGR_ATTR_UNDERLINE_COLOR: 8,
605 GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256: 9,
606 GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR: 10,
607 GHOSTTY_SGR_ATTR_OVERLINE: 11,
608 GHOSTTY_SGR_ATTR_RESET_OVERLINE: 12,
609 GHOSTTY_SGR_ATTR_BLINK: 13,
610 GHOSTTY_SGR_ATTR_RESET_BLINK: 14,
611 GHOSTTY_SGR_ATTR_INVERSE: 15,
612 GHOSTTY_SGR_ATTR_RESET_INVERSE: 16,
613 GHOSTTY_SGR_ATTR_INVISIBLE: 17,
614 GHOSTTY_SGR_ATTR_RESET_INVISIBLE: 18,
615 GHOSTTY_SGR_ATTR_STRIKETHROUGH: 19,
616 GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH: 20,
617 GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG: 21,
618 GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG: 22,
619 GHOSTTY_SGR_ATTR_BG_8: 23,
620 GHOSTTY_SGR_ATTR_FG_8: 24,
621 GHOSTTY_SGR_ATTR_RESET_FG: 25,
622 GHOSTTY_SGR_ATTR_RESET_BG: 26,
623 GHOSTTY_SGR_ATTR_BRIGHT_BG_8: 27,
624 GHOSTTY_SGR_ATTR_BRIGHT_FG_8: 28,
625 GHOSTTY_SGR_ATTR_BG_256: 29,
626 GHOSTTY_SGR_ATTR_FG_256: 30,
627} as const;
628
629export const GhosttySgrUnderline = {
630 GHOSTTY_SGR_UNDERLINE_NONE: 0,
631 GHOSTTY_SGR_UNDERLINE_SINGLE: 1,
632 GHOSTTY_SGR_UNDERLINE_DOUBLE: 2,
633 GHOSTTY_SGR_UNDERLINE_CURLY: 3,
634 GHOSTTY_SGR_UNDERLINE_DOTTED: 4,
635 GHOSTTY_SGR_UNDERLINE_DASHED: 5,
636} as const;
637
638export const GhosttySizeReportStyle = {
639 GHOSTTY_SIZE_REPORT_MODE_2048: 0,
640 GHOSTTY_SIZE_REPORT_CSI_14_T: 1,
641 GHOSTTY_SIZE_REPORT_CSI_16_T: 2,
642 GHOSTTY_SIZE_REPORT_CSI_18_T: 3,
643} as const;
644
645export const GhosttyStyleColorTag = {
646 GHOSTTY_STYLE_COLOR_NONE: 0,
647 GHOSTTY_STYLE_COLOR_PALETTE: 1,
648 GHOSTTY_STYLE_COLOR_RGB: 2,
649} as const;
650
651export const GhosttySysLogLevel = {
652 GHOSTTY_SYS_LOG_LEVEL_ERROR: 0,
653 GHOSTTY_SYS_LOG_LEVEL_WARNING: 1,
654 GHOSTTY_SYS_LOG_LEVEL_INFO: 2,
655 GHOSTTY_SYS_LOG_LEVEL_DEBUG: 3,
656} as const;
657
658export const GhosttySysOption = {
659 GHOSTTY_SYS_OPT_USERDATA: 0,
660 GHOSTTY_SYS_OPT_DECODE_PNG: 1,
661 GHOSTTY_SYS_OPT_LOG: 2,
662} as const;
663
664export const GhosttyTerminalCompressionMode = {
665 GHOSTTY_TERMINAL_COMPRESSION_MODE_INCREMENTAL: 0,
666 GHOSTTY_TERMINAL_COMPRESSION_MODE_FULL: 1,
667} as const;
668
669export const GhosttyTerminalCompressionResult = {
670 GHOSTTY_TERMINAL_COMPRESSION_RESULT_UNSUPPORTED: 0,
671 GHOSTTY_TERMINAL_COMPRESSION_RESULT_PENDING: 1,
672 GHOSTTY_TERMINAL_COMPRESSION_RESULT_COMPLETE: 2,
673} as const;
674
675export const GhosttyTerminalCursorStyle = {
676 GHOSTTY_TERMINAL_CURSOR_STYLE_BAR: 0,
677 GHOSTTY_TERMINAL_CURSOR_STYLE_BLOCK: 1,
678 GHOSTTY_TERMINAL_CURSOR_STYLE_UNDERLINE: 2,
679 GHOSTTY_TERMINAL_CURSOR_STYLE_BLOCK_HOLLOW: 3,
680} as const;
681
682export const GhosttyTerminalData = {
683 GHOSTTY_TERMINAL_DATA_INVALID: 0,
684 GHOSTTY_TERMINAL_DATA_COLS: 1,
685 GHOSTTY_TERMINAL_DATA_ROWS: 2,
686 GHOSTTY_TERMINAL_DATA_CURSOR_X: 3,
687 GHOSTTY_TERMINAL_DATA_CURSOR_Y: 4,
688 GHOSTTY_TERMINAL_DATA_CURSOR_PENDING_WRAP: 5,
689 GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN: 6,
690 GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE: 7,
691 GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS: 8,
692 GHOSTTY_TERMINAL_DATA_SCROLLBAR: 9,
693 GHOSTTY_TERMINAL_DATA_CURSOR_STYLE: 10,
694 GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING: 11,
695 GHOSTTY_TERMINAL_DATA_TITLE: 12,
696 GHOSTTY_TERMINAL_DATA_PWD: 13,
697 GHOSTTY_TERMINAL_DATA_TOTAL_ROWS: 14,
698 GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS: 15,
699 GHOSTTY_TERMINAL_DATA_WIDTH_PX: 16,
700 GHOSTTY_TERMINAL_DATA_HEIGHT_PX: 17,
701 GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND: 18,
702 GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND: 19,
703 GHOSTTY_TERMINAL_DATA_COLOR_CURSOR: 20,
704 GHOSTTY_TERMINAL_DATA_COLOR_PALETTE: 21,
705 GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT: 22,
706 GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT: 23,
707 GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT: 24,
708 GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT: 25,
709 GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_STORAGE_LIMIT: 26,
710 GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_FILE: 27,
711 GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE: 28,
712 GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM: 29,
713 GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS: 30,
714 GHOSTTY_TERMINAL_DATA_SELECTION: 31,
715 GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE: 32,
716 GHOSTTY_TERMINAL_DATA_VT_PROCESSING_ERROR: 33,
717} as const;
718
719export const GhosttyTerminalOption = {
720 GHOSTTY_TERMINAL_OPT_USERDATA: 0,
721 GHOSTTY_TERMINAL_OPT_WRITE_PTY: 1,
722 GHOSTTY_TERMINAL_OPT_BELL: 2,
723 GHOSTTY_TERMINAL_OPT_ENQUIRY: 3,
724 GHOSTTY_TERMINAL_OPT_XTVERSION: 4,
725 GHOSTTY_TERMINAL_OPT_TITLE_CHANGED: 5,
726 GHOSTTY_TERMINAL_OPT_SIZE: 6,
727 GHOSTTY_TERMINAL_OPT_COLOR_SCHEME: 7,
728 GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES: 8,
729 GHOSTTY_TERMINAL_OPT_TITLE: 9,
730 GHOSTTY_TERMINAL_OPT_PWD: 10,
731 GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND: 11,
732 GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND: 12,
733 GHOSTTY_TERMINAL_OPT_COLOR_CURSOR: 13,
734 GHOSTTY_TERMINAL_OPT_COLOR_PALETTE: 14,
735 GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT: 15,
736 GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE: 16,
737 GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE: 17,
738 GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM: 18,
739 GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES: 19,
740 GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES_KITTY: 20,
741 GHOSTTY_TERMINAL_OPT_SELECTION: 21,
742 GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_STYLE: 22,
743 GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_BLINK: 23,
744 GHOSTTY_TERMINAL_OPT_GLYPH_PROTOCOL: 24,
745 GHOSTTY_TERMINAL_OPT_PWD_CHANGED: 25,
746 GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE: 26,
747} as const;
748
749export const GhosttyTerminalScreen = {
750 GHOSTTY_TERMINAL_SCREEN_PRIMARY: 0,
751 GHOSTTY_TERMINAL_SCREEN_ALTERNATE: 1,
752} as const;
753
754export const GhosttyTerminalScrollViewportTag = {
755 GHOSTTY_SCROLL_VIEWPORT_TOP: 0,
756 GHOSTTY_SCROLL_VIEWPORT_BOTTOM: 1,
757 GHOSTTY_SCROLL_VIEWPORT_DELTA: 2,
758 GHOSTTY_SCROLL_VIEWPORT_ROW: 3,
759} as const;
packages/terminal/src/ghostty/module.ts created+266
......@@ -0,0 +1,266 @@
1// Loader and low-level memory helpers for the vendored libghostty-vt wasm module.
2// One GhosttyModule can host many terminals; all pointers are offsets into the
3// module's single linear memory. Never cache a DataView/TypedArray across an
4// exported call — memory.grow() detaches the buffer.
5
6export interface TypeField {
7 offset: number;
8 size: number;
9 type: string;
10}
11
12export interface TypeLayout {
13 [struct: string]: { size: number; align: number; fields: Record<string, TypeField> };
14}
15
16type Fn = (...args: Array<number | bigint>) => number;
17
18export interface WasmTrampoline {
19 readonly index: number;
20 free(): void;
21}
22
23export interface GhosttyExports {
24 memory: WebAssembly.Memory;
25 __indirect_function_table: WebAssembly.Table;
26 [name: string]: Fn | WebAssembly.Memory | WebAssembly.Table;
27}
28
29const encoder = new TextEncoder();
30const decoder = new TextDecoder();
31
32/**
33 * Byte layout of an n-arg trampoline module: (import "e" "cb") + (export "f")
34 * forwarding. The import and the export share one type, so declaring a result
35 * makes the call's value the exported function's result — which is what
36 * effects that answer the core (clipboard writes) need.
37 */
38function trampolineBytes(paramCount: number, resultCount: number): Uint8Array {
39 const params = Array.from({ length: paramCount }, () => 0x7f);
40 const results = resultCount > 0 ? [0x01, 0x7f] : [0x00];
41 const body: number[] = [0x00];
42 for (let i = 0; i < paramCount; i++) body.push(0x20, i);
43 body.push(0x10, 0x00, 0x0b);
44 const bytes = [
45 0x00,
46 0x61,
47 0x73,
48 0x6d,
49 0x01,
50 0x00,
51 0x00,
52 0x00,
53 0x01,
54 paramCount + 3 + results.length,
55 0x01,
56 0x60,
57 paramCount,
58 ...params,
59 ...results,
60 0x02,
61 0x08,
62 0x01,
63 0x01,
64 0x65,
65 0x02,
66 0x63,
67 0x62,
68 0x00,
69 0x00,
70 0x03,
71 0x02,
72 0x01,
73 0x00,
74 0x07,
75 0x05,
76 0x01,
77 0x01,
78 0x66,
79 0x00,
80 0x01,
81 0x0a,
82 body.length + 2,
83 0x01,
84 body.length,
85 ...body,
86 ];
87 return new Uint8Array(bytes);
88}
89
90export class GhosttyModule {
91 readonly exports: GhosttyExports;
92 readonly layout: TypeLayout;
93 private freeTableSlots: number[] = [];
94
95 private constructor(exports: GhosttyExports, layout: TypeLayout) {
96 this.exports = exports;
97 this.layout = layout;
98 }
99
100 /**
101 * Instantiate from wasm bytes or a precompiled module. Callers own fetching
102 * the artifact (vite: `new URL("@clo/terminal/ghostty-vt.wasm", import.meta.url)`;
103 * node: `fs.readFileSync`). Kept as a parameter so the package works in any
104 * bundler and in tests without a network seam.
105 */
106 static async load(source: BufferSource | WebAssembly.Module): Promise<GhosttyModule> {
107 const imports = {
108 env: {
109 log: (ptr: number, len: number) => {
110 const ex = instance.exports as unknown as GhosttyExports;
111 const text = decoder.decode(new Uint8Array(ex.memory.buffer, ptr, len));
112 console.debug("[ghostty-vt]", text);
113 },
114 },
115 };
116 let instance: WebAssembly.Instance;
117 if (source instanceof WebAssembly.Module) {
118 instance = await WebAssembly.instantiate(source, imports);
119 } else {
120 instance = (await WebAssembly.instantiate(source, imports)).instance;
121 }
122 const ex = instance.exports as unknown as GhosttyExports;
123 const jsonPtr = (ex.ghostty_type_json as Fn)();
124 const raw = new Uint8Array(ex.memory.buffer, jsonPtr);
125 const layout = JSON.parse(decoder.decode(raw.subarray(0, raw.indexOf(0)))) as TypeLayout;
126 return new GhosttyModule(ex, layout);
127 }
128
129 /** Fetch and instantiate a wasm artifact, rejecting non-success HTTP responses. */
130 static async fetch(source: RequestInfo | URL, init?: RequestInit): Promise<GhosttyModule> {
131 const response = await globalThis.fetch(source, init);
132 if (!response.ok) {
133 throw new Error(`ghostty wasm failed to load (${response.status})`);
134 }
135 return GhosttyModule.load(await response.arrayBuffer());
136 }
137
138 fn(name: string): Fn {
139 const f = this.exports[name];
140 if (typeof f !== "function") throw new Error(`missing wasm export: ${name}`);
141 return f;
142 }
143
144 view(): DataView {
145 return new DataView(this.exports.memory.buffer);
146 }
147
148 bytes(ptr: number, len: number): Uint8Array {
149 return new Uint8Array(this.exports.memory.buffer, ptr, len);
150 }
151
152 /** Copy of wasm memory — safe to hold across calls. */
153 copyBytes(ptr: number, len: number): Uint8Array {
154 return this.bytes(ptr, len).slice();
155 }
156
157 readCString(ptr: number, len: number): string {
158 return decoder.decode(this.bytes(ptr, len));
159 }
160
161 allocBytes(data: Uint8Array): { ptr: number; len: number; free: () => void } {
162 const allocation = this.allocScratch(data.length);
163 try {
164 this.bytes(allocation.ptr, data.length).set(data);
165 } catch (error) {
166 allocation.free();
167 throw error;
168 }
169 return { ptr: allocation.ptr, len: data.length, free: allocation.free };
170 }
171
172 allocScratch(len: number): { ptr: number; free: () => void } {
173 if (!Number.isInteger(len) || len < 0 || len > 0xffff_ffff) {
174 throw new RangeError(`ghostty wasm allocation length must be a uint32: ${len}`);
175 }
176 if (len === 0) return { ptr: 0, free: () => {} };
177 const ptr = this.fn("ghostty_wasm_alloc_u8_array")(len);
178 if (ptr === 0) throw new Error("ghostty wasm alloc failed");
179 let freed = false;
180 return {
181 ptr,
182 free: () => {
183 if (freed) return;
184 freed = true;
185 this.fn("ghostty_wasm_free_u8_array")(ptr, len);
186 },
187 };
188 }
189
190 encodeString(text: string) {
191 return this.allocBytes(encoder.encode(text));
192 }
193
194 /**
195 * Zeroed struct allocation with the sized-struct ABI convention applied:
196 * if the struct has a `size` field, it is pre-set to the struct's size.
197 */
198 allocStruct(name: string): { ptr: number; size: number; free: () => void } {
199 const info = this.layout[name];
200 if (!info) throw new Error(`unknown struct in type layout: ${name}`);
201 const scratch = this.allocScratch(info.size);
202 try {
203 this.bytes(scratch.ptr, info.size).fill(0);
204 const sizeField = info.fields["size"];
205 if (sizeField) this.view().setUint32(scratch.ptr + sizeField.offset, info.size, true);
206 return { ptr: scratch.ptr, size: info.size, free: scratch.free };
207 } catch (error) {
208 scratch.free();
209 throw error;
210 }
211 }
212
213 field(struct: string, field: string): TypeField {
214 const f = this.layout[struct]?.fields[field];
215 if (!f) throw new Error(`unknown field ${struct}.${field}`);
216 return f;
217 }
218
219 /** Call an out-param constructor: fn(allocator=0/default, outPtr, ...args) -> handle. */
220 create(name: string, ...args: number[]): number {
221 const out = this.fn("ghostty_wasm_alloc_opaque")();
222 if (out === 0) throw new Error("ghostty wasm alloc failed");
223 try {
224 const result = this.fn(name)(0, out, ...args);
225 if (result !== 0) throw new Error(`${name} failed: ${result}`);
226 const handle = this.view().getUint32(out, true);
227 if (handle === 0) throw new Error(`${name} returned a null handle`);
228 return handle;
229 } finally {
230 this.fn("ghostty_wasm_free_opaque")(out);
231 }
232 }
233
234 /**
235 * Register a JS callback as a wasm function-table entry (a C function
236 * pointer). The disposable slot is reused after release. libghostty-vt calls
237 * effects synchronously during vt_write; the JS function receives raw i32 args.
238 */
239 trampoline(
240 paramCount: number,
241 jsFn: (...args: number[]) => void | number,
242 resultCount = 0,
243 ): WasmTrampoline {
244 const mod = new WebAssembly.Module(trampolineBytes(paramCount, resultCount) as BufferSource);
245 const inst = new WebAssembly.Instance(mod, { e: { cb: jsFn } });
246 const table = this.exports.__indirect_function_table;
247 const reused = this.freeTableSlots.pop();
248 const index = reused ?? table.grow(1);
249 try {
250 table.set(index, inst.exports["f"]);
251 } catch (error) {
252 this.freeTableSlots.push(index);
253 throw error;
254 }
255 let freed = false;
256 return {
257 index,
258 free: () => {
259 if (freed) return;
260 freed = true;
261 table.set(index, null);
262 this.freeTableSlots.push(index);
263 },
264 };
265 }
266}
packages/terminal/src/ghostty/terminal.ts created+2244
......@@ -0,0 +1,2244 @@
1// The headless terminal: libghostty-vt bound to a bytes-in / cells-out surface.
2// Wire realities this layer absorbs: deltas arrive as JS strings that may split
3// escape sequences AND surrogate pairs at arbitrary boundaries (host windows are
4// UTF-16-code-unit sized), and a replayed scrollback head may open mid-sequence.
5// The parser is streaming (split sequences carry across writes); lone surrogates
6// at write boundaries are held back and re-joined here (`pendingSurrogate`).
7
8import { domCodeToGhosttyKey, domEventMods } from "../keys.ts";
9import { ShellCommands, type CommandAnchor, type ShellCommand } from "../shell/commands.ts";
10import { parseShellMark, type ShellMark } from "../shell/marks.ts";
11import { OscScanner } from "../shell/osc.ts";
12import {
13 GhosttyCellContentTag,
14 GhosttyCellData,
15 GhosttyCellSemanticContent,
16 GhosttyCellWide,
17 GhosttyClipboardLocation,
18 GhosttyClipboardWriteResult,
19 GhosttyKeyAction,
20 GhosttyMouseAction,
21 GhosttyMouseButton,
22 GhosttyMouseEncoderOption,
23 GhosttyMouseTrackingMode,
24 GhosttyPointTag,
25 GhosttyRowData,
26 GhosttyRenderStateData,
27 GhosttyRenderStateDirty,
28 GhosttyRenderStateOption,
29 GhosttyRenderStateRowCellsData,
30 GhosttyRenderStateRowData,
31 GhosttyRenderStateRowOption,
32 GhosttyResult,
33 GhosttyStyleColorTag,
34 GhosttyRowSemanticPrompt,
35 GhosttyTerminalScrollViewportTag,
36 GhosttyTerminalData,
37 GhosttyTerminalScreen,
38 GhosttyTerminalOption,
39} from "./constants.ts";
40import { GhosttyModule, type WasmTrampoline } from "./module.ts";
41
42export interface TerminalOptions {
43 cols: number;
44 rows: number;
45 /** Scrollback limit in BYTES of page memory (libghostty semantics), not lines. */
46 maxScrollbackBytes?: number;
47 /**
48 * Watch the byte stream for OSC 133/633 and maintain `terminal.shell`.
49 * Costs one JS pass over incoming bytes (ESC-skipping, so cheap on ordinary
50 * output) plus a tracked grid ref per command boundary. Default true.
51 */
52 shellIntegration?: boolean;
53 /** Retained commands; anchors beyond this are freed. Default 512. */
54 commandLimit?: number;
55}
56
57export interface TerminalEvents {
58 /** Query responses (DA/DSR/…) the host must forward to the pty as input. */
59 onOutput?: (bytes: Uint8Array) => void;
60 onBell?: () => void;
61 onTitleChanged?: (title: string) => void;
62 onPwdChanged?: (pwd: string) => void;
63 /** Every recognized shell-integration mark, before the command model folds it. */
64 onShellMark?: (mark: ShellMark) => void;
65 /** A command was submitted (OSC 133/633 `C`). */
66 onCommandStart?: (command: ShellCommand) => void;
67 /** A command finished (`D`), or was closed out by the next prompt. */
68 onCommandEnd?: (command: ShellCommand) => void;
69 /**
70 * A program asked to write the clipboard (OSC 52 / iTerm2 OSC 1337 Copy).
71 * ***Denied unless this handler returns true.*** Silent clipboard writes from
72 * whatever is running in a terminal — including something on the far end of
73 * an ssh session — are a real hazard, so the package never writes the
74 * clipboard itself and never opts you in. Reads (`OSC 52 ?`) are refused by
75 * the core and never surface here.
76 */
77 onClipboardWrite?: (write: ClipboardWrite) => boolean | void;
78}
79
80export interface ClipboardWrite {
81 /** Which selection the program targeted. */
82 location: "standard" | "selection" | "primary";
83 /** `text/plain` content, or "" when the program sent none. */
84 text: string;
85 /** Every representation offered, in order; commit atomically or not at all. */
86 entries: Array<{ mime: string; data: string }>;
87 /** True when the program asked to CLEAR the destination (no content). */
88 clear: boolean;
89}
90
91export interface CellSnapshot {
92 /** Grapheme text; "" for an empty cell. */
93 text: string;
94 /** 1 or 2 columns. Spacer heads own one column; spacer tails are omitted. */
95 width: number;
96 /** A soft-wrap spacer head owns a blank cell but contributes no grapheme. */
97 spacer: "none" | "head";
98 /** DECSCA protection; protected cells survive selective erase. */
99 protected: boolean;
100 /** OSC 133 semantic content. Output is Ghostty's zero/default value. */
101 semantic: "output" | "input" | "prompt";
102 /** Raw OSC 8 presence; true with a null URI only when URI bytes are malformed. */
103 hasOsc8: boolean;
104 /** Exact OSC 8 URI, or null when absent/malformed; use `hasOsc8` to distinguish. */
105 osc8Uri: string | null;
106 /** Resolved RGB as 0xRRGGBB, or -1 for "default". */
107 fg: number;
108 bg: number;
109 /** Resolved underline RGB as 0xRRGGBB, or -1 for the current foreground. */
110 underlineColor: number;
111 bold: boolean;
112 italic: boolean;
113 faint: boolean;
114 blink: boolean;
115 inverse: boolean;
116 invisible: boolean;
117 strikethrough: boolean;
118 overline: boolean;
119 /** GhosttyStyleUnderline value; 0 = none. */
120 underline: number;
121}
122
123/** True only when removing this trailing cell loses no canonical presentation state. */
124export function isCanonicalDefaultCell(cell: CellSnapshot): boolean {
125 return (
126 cell.text === "" &&
127 cell.width === 1 &&
128 cell.spacer === "none" &&
129 !cell.protected &&
130 cell.semantic === "output" &&
131 !cell.hasOsc8 &&
132 cell.osc8Uri === null &&
133 cell.fg === -1 &&
134 cell.bg === -1 &&
135 cell.underlineColor === -1 &&
136 !cell.bold &&
137 !cell.italic &&
138 !cell.faint &&
139 !cell.blink &&
140 !cell.inverse &&
141 !cell.invisible &&
142 !cell.strikethrough &&
143 !cell.overline &&
144 cell.underline === 0
145 );
146}
147
148export function trimCanonicalDefaultTail(cells: CellSnapshot[]): void {
149 let end = cells.length;
150 while (end > 0 && isCanonicalDefaultCell(cells[end - 1]!)) end--;
151 cells.length = end;
152}
153
154export interface RowSnapshot {
155 index: number;
156 wrap: boolean;
157 wrapContinuation: boolean;
158 semanticPrompt: "none" | "prompt" | "continuation";
159 hyperlink: boolean;
160 cells: CellSnapshot[];
161}
162
163type RowMetadata = Omit<RowSnapshot, "index" | "cells">;
164
165export interface CursorSnapshot {
166 x: number;
167 y: number;
168 style: "bar" | "block" | "underline" | "hollow";
169 blinking: boolean;
170 wide: boolean;
171 /** Explicit OSC 12 RGB as 0xRRGGBB, or -1 for the CSS theme fallback. */
172 color: number;
173}
174
175export interface FrameSnapshot {
176 cols: number;
177 rows: number;
178 dirty: "none" | "partial" | "full";
179 /** Only rows that need repainting (all rows when dirty === "full" or `full` was requested). */
180 dirtyRows: RowSnapshot[];
181 cursor: CursorSnapshot | null;
182 /** The program's OSC 10/11 override as 0xRRGGBB, or -1 when the theme decides. */
183 foreground: number;
184 background: number;
185}
186
187/** Exclusive consumer of dirty snapshots; dispose it before attaching another renderer. */
188export interface FrameReader {
189 snapshot(options?: { full?: boolean }): FrameSnapshot;
190 dispose(): void;
191}
192
193export interface Scrollbar {
194 total: number;
195 offset: number;
196 len: number;
197}
198
199const CURSOR_STYLES = ["bar", "block", "underline", "hollow"] as const;
200const encoder = new TextEncoder();
201// URI identity must not inherit TextDecoder's default leading-BOM removal or
202// replacement-character collisions for malformed UTF-8.
203const osc8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
204
205const CLIPBOARD_LOCATIONS: Record<number, "standard" | "selection" | "primary"> = {
206 [GhosttyClipboardLocation.GHOSTTY_CLIPBOARD_LOCATION_STANDARD]: "standard",
207 [GhosttyClipboardLocation.GHOSTTY_CLIPBOARD_LOCATION_SELECTION]: "selection",
208 [GhosttyClipboardLocation.GHOSTTY_CLIPBOARD_LOCATION_PRIMARY]: "primary",
209};
210
211const MOUSE_ACTIONS = {
212 press: GhosttyMouseAction.GHOSTTY_MOUSE_ACTION_PRESS,
213 release: GhosttyMouseAction.GHOSTTY_MOUSE_ACTION_RELEASE,
214 motion: GhosttyMouseAction.GHOSTTY_MOUSE_ACTION_MOTION,
215} as const;
216
217/**
218 * DOM `MouseEvent.button` uses 0/1/2 = left/middle/right; the VT numbering
219 * (and libghostty's) puts right before middle. Wheel arrives as buttons 4-7,
220 * which terminals report as presses of the corresponding button.
221 */
222function domButton(button: number): number | null {
223 switch (button) {
224 case 0:
225 return GhosttyMouseButton.GHOSTTY_MOUSE_BUTTON_LEFT;
226 case 1:
227 return GhosttyMouseButton.GHOSTTY_MOUSE_BUTTON_MIDDLE;
228 case 2:
229 return GhosttyMouseButton.GHOSTTY_MOUSE_BUTTON_RIGHT;
230 case 3:
231 return GhosttyMouseButton.GHOSTTY_MOUSE_BUTTON_FOUR;
232 case 4:
233 return GhosttyMouseButton.GHOSTTY_MOUSE_BUTTON_FIVE;
234 case 5:
235 return GhosttyMouseButton.GHOSTTY_MOUSE_BUTTON_SIX;
236 case 6:
237 return GhosttyMouseButton.GHOSTTY_MOUSE_BUTTON_SEVEN;
238 default:
239 return null;
240 }
241}
242
243const INITIAL_GRAPHEME_CAPACITY = 64;
244const INITIAL_HYPERLINK_URI_CAPACITY = 256;
245const MAX_RETAINED_INPUT_CAPACITY = 65_536;
246const SYNCHRONIZED_OUTPUT_MODE = 2026;
247const UINT16_MAX = 0xffff;
248const UINT32_MAX = 0xffff_ffff;
249const INT32_MIN = -0x8000_0000;
250const INT32_MAX = 0x7fff_ffff;
251const FLOAT32_MAX = 3.4028234663852886e38;
252
253interface HyperlinkScratch {
254 point: ReturnType<GhosttyModule["allocStruct"]>;
255 ref: ReturnType<GhosttyModule["allocStruct"]>;
256 outLen: ReturnType<GhosttyModule["allocScratch"]>;
257 uri: ReturnType<GhosttyModule["allocScratch"]>;
258 uriCapacity: number;
259}
260
261function codepointsToString(view: DataView, ptr: number, count: number): string {
262 const pieces: string[] = [];
263 for (let start = 0; start < count; start += 1024) {
264 const codepoints: number[] = [];
265 const end = Math.min(start + 1024, count);
266 for (let i = start; i < end; i++) codepoints.push(view.getUint32(ptr + i * 4, true));
267 pieces.push(String.fromCodePoint(...codepoints));
268 }
269 return pieces.join("");
270}
271
272function assertGhosttySuccess(result: number, operation: string): void {
273 if (result !== GhosttyResult.GHOSTTY_SUCCESS) throw new Error(`${operation} failed: ${result}`);
274}
275
276function integerInRange(name: string, value: number, min: number, max: number): number {
277 if (!Number.isInteger(value) || value < min || value > max) {
278 throw new RangeError(`${name} must be an integer from ${min} through ${max}: ${value}`);
279 }
280 return value;
281}
282
283/**
284 * A libghostty tracked grid ref behind the pure `CommandAnchor` seam. The
285 * handle stays valid after the terminal is freed (it then reports no value),
286 * so double-free is the only real hazard — `free()` is idempotent here.
287 */
288class TrackedAnchor implements CommandAnchor {
289 private mod: GhosttyModule;
290 private handle: number;
291
292 constructor(mod: GhosttyModule, handle: number) {
293 this.mod = mod;
294 this.handle = handle;
295 }
296
297 point(): { x: number; y: number } | null {
298 return this.read(GhosttyPointTag.GHOSTTY_POINT_TAG_VIEWPORT);
299 }
300
301 screenPoint(): { x: number; y: number } | null {
302 return this.read(GhosttyPointTag.GHOSTTY_POINT_TAG_SCREEN);
303 }
304
305 free(): void {
306 if (this.handle === 0) return;
307 this.mod.fn("ghostty_tracked_grid_ref_free")(this.handle);
308 this.handle = 0;
309 }
310
311 private read(tag: number): { x: number; y: number } | null {
312 if (this.handle === 0) return null;
313 const coord = this.mod.allocStruct("GhosttyPointCoordinate");
314 const result = this.mod.fn("ghostty_tracked_grid_ref_point")(this.handle, tag, coord.ptr);
315 let out: { x: number; y: number } | null = null;
316 if (result === GhosttyResult.GHOSTTY_SUCCESS) {
317 const view = this.mod.view();
318 out = {
319 x: view.getUint16(coord.ptr + this.mod.field("GhosttyPointCoordinate", "x").offset, true),
320 y: view.getUint32(coord.ptr + this.mod.field("GhosttyPointCoordinate", "y").offset, true),
321 };
322 }
323 coord.free();
324 return out;
325 }
326}
327
328export class Terminal {
329 readonly mod: GhosttyModule;
330 /** Shell-integration command model; empty unless the shell is instrumented. */
331 readonly shell: ShellCommands;
332 private scanner: OscScanner | null;
333 private term = 0;
334 private renderState = 0;
335 private rowIter = 0;
336 private rowCells = 0;
337 private rowMetadata: RowMetadata[] = [];
338 private hyperlinkScratch: HyperlinkScratch | null = null;
339 private asciiRowScratch: { ptr: number; capacity: number; free: () => void } | null = null;
340 private inputScratch: { ptr: number; capacity: number; free: () => void } | null = null;
341 private keyEncoder = 0;
342 private keyEvent = 0;
343 private mouseEncoder = 0;
344 private mouseEvent = 0;
345 private modeValue = 0;
346 private colsNow: number;
347 private rowsNow: number;
348 private viewportRevisionNow = 0;
349 private pendingSurrogate = "";
350 private events: TerminalEvents;
351 private disposed = false;
352 private disposePending = false;
353 private writing = false;
354 private callbackErrors: unknown[] | null = null;
355 private trampolines: WasmTrampoline[] = [];
356 private frameReaderOwner: object | null = null;
357
358 constructor(mod: GhosttyModule, options: TerminalOptions, events: TerminalEvents = {}) {
359 const cols = integerInRange("cols", options.cols, 1, UINT16_MAX);
360 const rows = integerInRange("rows", options.rows, 1, UINT16_MAX);
361 const maxScrollbackBytes = integerInRange(
362 "maxScrollbackBytes",
363 options.maxScrollbackBytes ?? 4_194_304,
364 0,
365 UINT32_MAX,
366 );
367 const commandLimit =
368 options.commandLimit === undefined
369 ? undefined
370 : integerInRange("commandLimit", options.commandLimit, 0, Number.MAX_SAFE_INTEGER);
371 this.mod = mod;
372 this.events = events;
373 this.colsNow = cols;
374 this.rowsNow = rows;
375 this.scanner = options.shellIntegration === false ? null : new OscScanner();
376 this.shell = new ShellCommands({
377 anchor: () => this.anchorAtCursor(),
378 readCommandLine: (from, to) => this.textBetween(from, to),
379 ...(commandLimit !== undefined ? { limit: commandLimit } : {}),
380 onCommandStart: (command) =>
381 this.invokeCallback(() => this.events.onCommandStart?.(command), undefined),
382 onCommandEnd: (command) =>
383 this.invokeCallback(() => this.events.onCommandEnd?.(command), undefined),
384 });
385
386 try {
387 const opts = mod.allocStruct("GhosttyTerminalOptions");
388 try {
389 const f = (name: string) => mod.field("GhosttyTerminalOptions", name);
390 mod.view().setUint16(opts.ptr + f("cols").offset, cols, true);
391 mod.view().setUint16(opts.ptr + f("rows").offset, rows, true);
392 mod.view().setUint32(opts.ptr + f("max_scrollback").offset, maxScrollbackBytes, true);
393 this.term = mod.create("ghostty_terminal_new", opts.ptr);
394 } finally {
395 opts.free();
396 }
397
398 this.renderState = mod.create("ghostty_render_state_new");
399 this.rowIter = mod.create("ghostty_render_state_row_iterator_new");
400 this.rowCells = mod.create("ghostty_render_state_row_cells_new");
401 this.keyEncoder = mod.create("ghostty_key_encoder_new");
402 this.keyEvent = mod.create("ghostty_key_event_new");
403 this.mouseEncoder = mod.create("ghostty_mouse_encoder_new");
404 this.mouseEvent = mod.create("ghostty_mouse_event_new");
405 this.modeValue = mod.fn("ghostty_wasm_alloc_u8")();
406 if (this.modeValue === 0) throw new Error("ghostty wasm alloc failed");
407
408 // ***Verified inert at the pinned vendor commit.*** Keep the renderer's
409 // cell dedup until a vendor bump proves this encoder option effective.
410 const trackLastCell = mod.fn("ghostty_wasm_alloc_u8")();
411 if (trackLastCell === 0) throw new Error("ghostty wasm alloc failed");
412 try {
413 mod.view().setUint8(trackLastCell, 1);
414 mod.fn("ghostty_mouse_encoder_setopt")(
415 this.mouseEncoder,
416 GhosttyMouseEncoderOption.GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL,
417 trackLastCell,
418 );
419 } finally {
420 mod.fn("ghostty_wasm_free_u8")(trackLastCell);
421 }
422
423 const setEffect = (option: number, trampoline: WasmTrampoline) => {
424 this.trampolines.push(trampoline);
425 assertGhosttySuccess(
426 mod.fn("ghostty_terminal_set")(this.term, option, trampoline.index),
427 "terminal_set(effect)",
428 );
429 };
430 setEffect(
431 GhosttyTerminalOption.GHOSTTY_TERMINAL_OPT_WRITE_PTY,
432 mod.trampoline(4, (_t, _u, data, len) =>
433 this.invokeCallback(() => this.events.onOutput?.(mod.copyBytes(data, len)), undefined),
434 ),
435 );
436 setEffect(
437 GhosttyTerminalOption.GHOSTTY_TERMINAL_OPT_BELL,
438 mod.trampoline(2, () => this.invokeCallback(() => this.events.onBell?.(), undefined)),
439 );
440 setEffect(
441 GhosttyTerminalOption.GHOSTTY_TERMINAL_OPT_TITLE_CHANGED,
442 mod.trampoline(2, () =>
443 this.invokeCallback(() => {
444 const title = this.title();
445 if (title !== null) this.events.onTitleChanged?.(title);
446 }, undefined),
447 ),
448 );
449 setEffect(
450 GhosttyTerminalOption.GHOSTTY_TERMINAL_OPT_PWD_CHANGED,
451 mod.trampoline(2, () =>
452 this.invokeCallback(() => {
453 const pwd = this.pwd();
454 if (pwd !== null) this.events.onPwdChanged?.(pwd);
455 }, undefined),
456 ),
457 );
458 setEffect(
459 GhosttyTerminalOption.GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE,
460 mod.trampoline(
461 3,
462 (_t, _u, writePtr) =>
463 this.invokeCallback(
464 () => this.onClipboardWrite(writePtr),
465 GhosttyClipboardWriteResult.GHOSTTY_CLIPBOARD_WRITE_RESULT_IO_ERROR,
466 ),
467 1,
468 ),
469 );
470
471 // Grapheme clustering (mode 2027) matches the Ghostty app.
472 assertGhosttySuccess(
473 mod.fn("ghostty_terminal_mode_set")(this.term, 2027, 1),
474 "terminal_mode_set(grapheme clustering)",
475 );
476 } catch (error) {
477 this.releaseWasmResources();
478 this.shell.dispose();
479 this.disposed = true;
480 throw error;
481 }
482 }
483
484 get cols(): number {
485 return this.colsNow;
486 }
487
488 get rows(): number {
489 return this.rowsNow;
490 }
491
492 /** Changes when public geometry or scroll methods can invalidate a view's projection. */
493 get viewportRevision(): number {
494 return this.viewportRevisionNow;
495 }
496
497 /** DEC mode 2026 suppresses presentation until the application closes the update. */
498 get synchronizedOutput(): boolean {
499 this.assertLive();
500 this.mod.view().setUint8(this.modeValue, 0);
501 assertGhosttySuccess(
502 this.mod.fn("ghostty_terminal_mode_get")(this.term, SYNCHRONIZED_OUTPUT_MODE, this.modeValue),
503 "terminal_mode_get(synchronized_output)",
504 );
505 return this.mod.view().getUint8(this.modeValue) !== 0;
506 }
507
508 /** Release an unterminated synchronized update so untrusted output cannot freeze presentation. */
509 resetSynchronizedOutput(): void {
510 this.assertLive();
511 assertGhosttySuccess(
512 this.mod.fn("ghostty_terminal_mode_set")(this.term, SYNCHRONIZED_OUTPUT_MODE, 0),
513 "terminal_mode_set(synchronized_output)",
514 );
515 }
516
517 /** Feed pty output. Strings may split surrogate pairs at chunk boundaries. */
518 write(data: string | Uint8Array): void {
519 this.assertLive();
520 if (this.writing) throw new Error("terminal writes are not reentrant");
521 this.writing = true;
522 const callbackErrors: unknown[] = [];
523 this.callbackErrors = callbackErrors;
524 try {
525 let bytes: Uint8Array;
526 if (typeof data === "string") {
527 let text = this.pendingSurrogate + data;
528 this.pendingSurrogate = "";
529 const last = text.charCodeAt(text.length - 1);
530 if (last >= 0xd800 && last <= 0xdbff) {
531 this.pendingSurrogate = text.slice(-1);
532 text = text.slice(0, -1);
533 }
534 if (text.length === 0) return;
535 bytes = encoder.encode(text);
536 } else {
537 if (this.pendingSurrogate === "") bytes = data;
538 else {
539 const prefix = encoder.encode(this.pendingSurrogate);
540 this.pendingSurrogate = "";
541 bytes = new Uint8Array(prefix.length + data.length);
542 bytes.set(prefix);
543 bytes.set(data, prefix.length);
544 }
545 }
546 if (bytes.length === 0) return;
547 this.processBytes(bytes);
548 if (callbackErrors.length === 1) throw callbackErrors[0];
549 if (callbackErrors.length > 1) {
550 throw new AggregateError(callbackErrors, "terminal callbacks failed");
551 }
552 } finally {
553 this.callbackErrors = null;
554 this.writing = false;
555 if (this.disposePending) {
556 this.disposePending = false;
557 this.free();
558 }
559 }
560 }
561
562 private processBytes(bytes: Uint8Array): void {
563 const scanner = this.scanner;
564 if (scanner === null) {
565 this.feed(bytes);
566 return;
567 }
568 // Marks are anchored to the cursor, so each terminator is fed before its
569 // anchor is taken. Callback failures are retained until the whole chunk lands.
570 const spans = scanner.scan(bytes);
571 if (spans.length === 0) {
572 this.feed(bytes);
573 return;
574 }
575 let from = 0;
576 for (const span of spans) {
577 this.feed(bytes.subarray(from, span.end));
578 from = span.end;
579 const mark = parseShellMark(span.payload);
580 if (mark === null) continue;
581 this.invokeCallback(() => this.events.onShellMark?.(mark), undefined);
582 this.shell.handle(mark);
583 }
584 if (from < bytes.length) this.feed(bytes.subarray(from));
585 }
586
587 private invokeCallback<T>(callback: () => T, fallback: T): T {
588 try {
589 return callback();
590 } catch (error) {
591 if (this.callbackErrors === null) throw error;
592 this.callbackErrors.push(error);
593 return fallback;
594 }
595 }
596
597 private feed(bytes: Uint8Array): void {
598 if (bytes.length === 0) return;
599 if (bytes.length <= MAX_RETAINED_INPUT_CAPACITY) {
600 if (this.inputScratch === null || this.inputScratch.capacity < bytes.length) {
601 const capacity = 2 ** Math.ceil(Math.log2(bytes.length));
602 const next = this.mod.allocScratch(capacity);
603 this.inputScratch?.free();
604 this.inputScratch = { ...next, capacity };
605 }
606 this.mod.bytes(this.inputScratch.ptr, bytes.length).set(bytes);
607 this.mod.fn("ghostty_terminal_vt_write")(this.term, this.inputScratch.ptr, bytes.length);
608 return;
609 }
610 const buf = this.mod.allocBytes(bytes);
611 try {
612 this.mod.fn("ghostty_terminal_vt_write")(this.term, buf.ptr, buf.len);
613 } finally {
614 buf.free();
615 }
616 }
617
618 /**
619 * Anchor a point so it survives scrolling, scrollback pruning and reflow
620 * (libghostty tracked grid ref). Each live anchor adds bookkeeping to every
621 * terminal mutation — keep them few and free them.
622 */
623 trackPoint(
624 x: number,
625 y: number,
626 space: "active" | "viewport" | "screen" = "active",
627 ): CommandAnchor | null {
628 this.assertLive();
629 integerInRange("x", x, 0, UINT16_MAX);
630 integerInRange("y", y, 0, UINT32_MAX);
631 const mod = this.mod;
632 const tag =
633 space === "viewport"
634 ? GhosttyPointTag.GHOSTTY_POINT_TAG_VIEWPORT
635 : space === "screen"
636 ? GhosttyPointTag.GHOSTTY_POINT_TAG_SCREEN
637 : GhosttyPointTag.GHOSTTY_POINT_TAG_ACTIVE;
638 const point = this.allocPoint(tag, x, y);
639 try {
640 const out = mod.allocScratch(4);
641 try {
642 const result = mod.fn("ghostty_terminal_grid_ref_track")(this.term, point.ptr, out.ptr);
643 const handle = mod.view().getUint32(out.ptr, true);
644 if (result !== GhosttyResult.GHOSTTY_SUCCESS || handle === 0) return null;
645 return new TrackedAnchor(mod, handle);
646 } finally {
647 out.free();
648 }
649 } finally {
650 point.free();
651 }
652 }
653
654 private anchorAtCursor(): CommandAnchor | null {
655 const scratch = this.mod.allocScratch(8);
656 try {
657 const read = (what: number) => {
658 this.mod.view().setUint32(scratch.ptr, 0, true);
659 const result = this.mod.fn("ghostty_terminal_get")(this.term, what, scratch.ptr);
660 return result === GhosttyResult.GHOSTTY_SUCCESS
661 ? this.mod.view().getUint16(scratch.ptr, true)
662 : -1;
663 };
664 const x = read(GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_CURSOR_X);
665 const y = read(GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_CURSOR_Y);
666 if (x < 0 || y < 0) return null;
667 return this.trackPoint(x, y, "active");
668 } finally {
669 scratch.free();
670 }
671 }
672
673 private allocPoint(tag: number, x: number, y: number) {
674 const point = this.mod.allocStruct("GhosttyPoint");
675 try {
676 const f = (name: string) => this.mod.field("GhosttyPoint", name).offset;
677 const coord = point.ptr + f("value");
678 this.mod.view().setUint32(point.ptr + f("tag"), tag, true);
679 this.mod
680 .view()
681 .setUint16(coord + this.mod.field("GhosttyPointCoordinate", "x").offset, x, true);
682 this.mod
683 .view()
684 .setUint32(coord + this.mod.field("GhosttyPointCoordinate", "y").offset, y, true);
685 return point;
686 } catch (error) {
687 point.free();
688 throw error;
689 }
690 }
691
692 /**
693 * Plain text between two anchors (`from` inclusive, `to` exclusive), reading
694 * the grid directly. Soft-wrapped rows are joined without a newline. Returns
695 * null when either anchor has been discarded.
696 *
697 * This is the command-line fallback for shells that emit OSC 133 without
698 * VS Code's `633;E`, and the basis for copy-output.
699 */
700 textBetween(from: CommandAnchor, to: CommandAnchor): string | null {
701 this.assertLive();
702 const start = from.screenPoint();
703 const end = to.screenPoint();
704 if (start === null || end === null) return null;
705 if (end.y < start.y || (end.y === start.y && end.x <= start.x)) return "";
706
707 const mod = this.mod;
708 const allocations: Array<{ free(): void }> = [];
709 const own = <T extends { free(): void }>(allocation: T): T => {
710 allocations.push(allocation);
711 return allocation;
712 };
713 let text = "";
714 try {
715 const ref = own(mod.allocStruct("GhosttyGridRef"));
716 const xOffset = mod.field("GhosttyGridRef", "x").offset;
717 let graphemeCapacity = INITIAL_GRAPHEME_CAPACITY;
718 let graphemes = own(mod.allocScratch(graphemeCapacity * 4));
719 const outLen = own(mod.allocScratch(4));
720 const cellOut = own(mod.allocScratch(8));
721 // GhosttyRow, like GhosttyCell, is a packed u64 value rather than a pointer.
722 const rowOut = own(mod.allocScratch(8));
723 const scratch = own(mod.allocScratch(8));
724 for (let y = start.y; y <= end.y; y++) {
725 const point = this.allocPoint(GhosttyPointTag.GHOSTTY_POINT_TAG_SCREEN, 0, y);
726 let resolved: number;
727 try {
728 resolved = mod.fn("ghostty_terminal_grid_ref")(this.term, point.ptr, ref.ptr);
729 } finally {
730 point.free();
731 }
732 if (resolved !== GhosttyResult.GHOSTTY_SUCCESS) break;
733
734 const firstX = y === start.y ? start.x : 0;
735 const lastX = y === end.y ? end.x : this.colsNow;
736 // The end anchor usually sits at column 0 of the row *after* the last
737 // line of content (the cursor after a newline). That row contributes
738 // nothing, and emitting it would add a phantom trailing line.
739 if (lastX <= firstX) break;
740 let line = "";
741 for (let x = firstX; x < lastX; x++) {
742 // The ref is a resolved position in one page node; x is the column
743 // within it, so a row is walked by bumping x rather than re-resolving
744 // (screen-tag lookups can traverse the whole page list).
745 mod.view().setUint16(ref.ptr + xOffset, x, true);
746 mod.view().setBigUint64(cellOut.ptr, 0n, true);
747 if (
748 mod.fn("ghostty_grid_ref_cell")(ref.ptr, cellOut.ptr) === GhosttyResult.GHOSTTY_SUCCESS
749 ) {
750 const raw = mod.view().getBigUint64(cellOut.ptr, true);
751 mod.view().setUint32(scratch.ptr, 0, true);
752 mod.fn("ghostty_cell_get")(raw, GhosttyCellData.GHOSTTY_CELL_DATA_WIDE, scratch.ptr);
753 const wide = mod.view().getUint32(scratch.ptr, true);
754 // Spacers carry no text of their own; emitting them would double
755 // the space after every wide glyph.
756 if (
757 wide === GhosttyCellWide.GHOSTTY_CELL_WIDE_SPACER_TAIL ||
758 wide === GhosttyCellWide.GHOSTTY_CELL_WIDE_SPACER_HEAD
759 ) {
760 continue;
761 }
762 }
763 mod.view().setUint32(outLen.ptr, 0, true);
764 let g = mod.fn("ghostty_grid_ref_graphemes")(
765 ref.ptr,
766 graphemes.ptr,
767 graphemeCapacity,
768 outLen.ptr,
769 );
770 if (g === GhosttyResult.GHOSTTY_OUT_OF_SPACE) {
771 const required = mod.view().getUint32(outLen.ptr, true);
772 const nextCapacity = 2 ** Math.ceil(Math.log2(required));
773 const next = own(mod.allocScratch(nextCapacity * 4));
774 graphemes.free();
775 graphemeCapacity = nextCapacity;
776 graphemes = next;
777 g = mod.fn("ghostty_grid_ref_graphemes")(
778 ref.ptr,
779 graphemes.ptr,
780 graphemeCapacity,
781 outLen.ptr,
782 );
783 }
784 if (g !== GhosttyResult.GHOSTTY_SUCCESS) continue;
785 const count = mod.view().getUint32(outLen.ptr, true);
786 if (count === 0) {
787 line += " ";
788 continue;
789 }
790 line += codepointsToString(mod.view(), graphemes.ptr, count);
791 }
792
793 // Soft-wrapped rows continue the same logical line.
794 mod.view().setUint16(ref.ptr + xOffset, 0, true);
795 let wrapped = false;
796 mod.view().setBigUint64(rowOut.ptr, 0n, true);
797 if (mod.fn("ghostty_grid_ref_row")(ref.ptr, rowOut.ptr) === GhosttyResult.GHOSTTY_SUCCESS) {
798 const row = mod.view().getBigUint64(rowOut.ptr, true);
799 mod.view().setUint8(scratch.ptr, 0);
800 if (
801 mod.fn("ghostty_row_get")(row, GhosttyRowData.GHOSTTY_ROW_DATA_WRAP, scratch.ptr) ===
802 GhosttyResult.GHOSTTY_SUCCESS
803 ) {
804 wrapped = mod.view().getUint8(scratch.ptr) !== 0;
805 }
806 }
807 text += wrapped ? line : `${line.replace(/\s+$/, "")}\n`;
808 }
809 } finally {
810 for (let index = allocations.length - 1; index >= 0; index--) allocations[index]!.free();
811 }
812 return text.replace(/\n$/, "");
813 }
814
815 /** The output a finished command produced, or null if it can't be recovered. */
816 outputText(command: ShellCommand): string | null {
817 if (command.outputStart === null || command.outputEnd === null) return null;
818 return this.textBetween(command.outputStart, command.outputEnd);
819 }
820
821 resize(cols: number, rows: number): void {
822 this.assertLive();
823 integerInRange("cols", cols, 1, UINT16_MAX);
824 integerInRange("rows", rows, 1, UINT16_MAX);
825 if (cols === this.colsNow && rows === this.rowsNow) return;
826 const result = this.mod.fn("ghostty_terminal_resize")(this.term, cols, rows);
827 if (result !== GhosttyResult.GHOSTTY_SUCCESS) throw new Error(`resize failed: ${result}`);
828 this.colsNow = cols;
829 this.rowsNow = rows;
830 this.viewportRevisionNow++;
831 }
832
833 scrollDelta(rows: number): void {
834 this.scroll(GhosttyTerminalScrollViewportTag.GHOSTTY_SCROLL_VIEWPORT_DELTA, rows);
835 }
836
837 scrollToBottom(): void {
838 this.scroll(GhosttyTerminalScrollViewportTag.GHOSTTY_SCROLL_VIEWPORT_BOTTOM, 0);
839 }
840
841 scrollToTop(): void {
842 this.scroll(GhosttyTerminalScrollViewportTag.GHOSTTY_SCROLL_VIEWPORT_TOP, 0);
843 }
844
845 scrollToRow(row: number): void {
846 this.scroll(GhosttyTerminalScrollViewportTag.GHOSTTY_SCROLL_VIEWPORT_ROW, row);
847 }
848
849 private scroll(tag: number, value: number): void {
850 this.assertLive();
851 if (tag === GhosttyTerminalScrollViewportTag.GHOSTTY_SCROLL_VIEWPORT_DELTA) {
852 integerInRange("scroll delta", value, INT32_MIN, INT32_MAX);
853 } else if (tag === GhosttyTerminalScrollViewportTag.GHOSTTY_SCROLL_VIEWPORT_ROW) {
854 integerInRange("scroll row", value, 0, UINT32_MAX);
855 }
856 const s = this.mod.allocStruct("GhosttyTerminalScrollViewport");
857 const tagField = this.mod.field("GhosttyTerminalScrollViewport", "tag");
858 const valueField = this.mod.field("GhosttyTerminalScrollViewport", "value");
859 this.mod.view().setUint32(s.ptr + tagField.offset, tag, true);
860 if (tag === GhosttyTerminalScrollViewportTag.GHOSTTY_SCROLL_VIEWPORT_DELTA) {
861 this.mod.view().setInt32(s.ptr + valueField.offset, value, true);
862 } else {
863 this.mod.view().setUint32(s.ptr + valueField.offset, value, true);
864 }
865 this.mod.fn("ghostty_terminal_scroll_viewport")(this.term, s.ptr);
866 s.free();
867 this.viewportRevisionNow++;
868 }
869
870 /**
871 * The theme's own colors, so the core can answer a program's `OSC 10/11/12`.
872 *
873 * ***This is a report, not a paint instruction.*** Painting stays
874 * CSS-custom-property-only: a snapshot color is the program's OSC OVERRIDE or
875 * `-1`, and setting a default here never makes one appear. What it buys is the
876 * query — codex asks the background before it draws, and a terminal that
877 * cannot answer gets a composer with no background at all rather than a
878 * fallback one.
879 *
880 * A `null` clears that default and takes the answer away again.
881 */
882 setDefaultColors(colors: {
883 foreground?: number | null;
884 background?: number | null;
885 cursor?: number | null;
886 }): void {
887 this.assertLive();
888 const mod = this.mod;
889 const set = (name: string, option: number, color: number | null | undefined) => {
890 if (color === undefined) return;
891 if (color === null) {
892 assertGhosttySuccess(
893 mod.fn("ghostty_terminal_set")(this.term, option, 0),
894 `terminal_set(${name})`,
895 );
896 return;
897 }
898 const rgb = integerInRange(name, color, 0, 0xffffff);
899 const struct = mod.allocStruct("GhosttyColorRgb");
900 try {
901 const view = mod.view();
902 view.setUint8(struct.ptr + mod.field("GhosttyColorRgb", "r").offset, (rgb >> 16) & 0xff);
903 view.setUint8(struct.ptr + mod.field("GhosttyColorRgb", "g").offset, (rgb >> 8) & 0xff);
904 view.setUint8(struct.ptr + mod.field("GhosttyColorRgb", "b").offset, rgb & 0xff);
905 assertGhosttySuccess(
906 mod.fn("ghostty_terminal_set")(this.term, option, struct.ptr),
907 `terminal_set(${name})`,
908 );
909 } finally {
910 struct.free();
911 }
912 };
913 set(
914 "foreground",
915 GhosttyTerminalOption.GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND,
916 colors.foreground,
917 );
918 set(
919 "background",
920 GhosttyTerminalOption.GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND,
921 colors.background,
922 );
923 set("cursor", GhosttyTerminalOption.GHOSTTY_TERMINAL_OPT_COLOR_CURSOR, colors.cursor);
924 }
925
926 /**
927 * The theme's 16 ANSI colors, as the palette a cell's color index resolves through.
928 *
929 * ***Unlike the default fore/background this is paint***: the core resolves an
930 * indexed color before the snapshot leaves it, so an unset palette is not the
931 * theme's and not neutral either — it is ghostty's own xterm defaults, dark
932 * terminal and all, under every theme. Entries 16-255 keep their defaults, a
933 * `null` slot keeps its own, and a program's `OSC 4` override outranks all of
934 * it.
935 *
936 * **_A palette the core already holds is never re-set_**: the set dirties the
937 * screen, and a host republishes its theme on every metric refresh — a full
938 * repaint per font load, per probe resize, over colors nobody changed.
939 */
940 setDefaultPalette(ansi: readonly (number | null)[]): void {
941 this.assertLive();
942 if (ansi.every((color) => color === null)) return;
943 const mod = this.mod;
944 const rgb = mod.layout["GhosttyColorRgb"]!;
945 const palette = mod.allocScratch(rgb.size * 256);
946 try {
947 assertGhosttySuccess(
948 mod.fn("ghostty_terminal_get")(
949 this.term,
950 GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT,
951 palette.ptr,
952 ),
953 "terminal_get(color_palette_default)",
954 );
955 const view = mod.view();
956 let changed = false;
957 ansi.slice(0, 16).forEach((color, index) => {
958 if (color === null) return;
959 const value = integerInRange(`ansi[${index}]`, color, 0, 0xffffff);
960 const entry = palette.ptr + index * rgb.size;
961 const channels = [(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff];
962 for (const [offset, channel] of ["r", "g", "b"].entries()) {
963 const at = entry + rgb.fields[channel]!.offset;
964 if (view.getUint8(at) === channels[offset]!) continue;
965 view.setUint8(at, channels[offset]!);
966 changed = true;
967 }
968 });
969 if (!changed) return;
970 assertGhosttySuccess(
971 mod.fn("ghostty_terminal_set")(
972 this.term,
973 GhosttyTerminalOption.GHOSTTY_TERMINAL_OPT_COLOR_PALETTE,
974 palette.ptr,
975 ),
976 "terminal_set(color_palette)",
977 );
978 } finally {
979 palette.free();
980 }
981 }
982
983 scrollbar(): Scrollbar {
984 this.assertLive();
985 const s = this.mod.allocStruct("GhosttyTerminalScrollbar");
986 const result = this.mod.fn("ghostty_terminal_get")(
987 this.term,
988 GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_SCROLLBAR,
989 s.ptr,
990 );
991 if (result !== GhosttyResult.GHOSTTY_SUCCESS) {
992 s.free();
993 return { total: this.rowsNow, offset: 0, len: this.rowsNow };
994 }
995 const g = (name: string) =>
996 Number(
997 this.mod
998 .view()
999 .getBigUint64(s.ptr + this.mod.field("GhosttyTerminalScrollbar", name).offset, true),
1000 );
1001 const out = { total: g("total"), offset: g("offset"), len: g("len") };
1002 s.free();
1003 return out;
1004 }
1005
1006 title(): string | null {
1007 return this.getString(GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_TITLE);
1008 }
1009
1010 pwd(): string | null {
1011 return this.getString(GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_PWD);
1012 }
1013
1014 private getString(data: number): string | null {
1015 this.assertLive();
1016 const s = this.mod.allocStruct("GhosttyString");
1017 const result = this.mod.fn("ghostty_terminal_get")(this.term, data, s.ptr);
1018 if (result !== GhosttyResult.GHOSTTY_SUCCESS) {
1019 s.free();
1020 return null;
1021 }
1022 const ptr = this.mod
1023 .view()
1024 .getUint32(s.ptr + this.mod.field("GhosttyString", "ptr").offset, true);
1025 const len = this.mod
1026 .view()
1027 .getUint32(s.ptr + this.mod.field("GhosttyString", "len").offset, true);
1028 const text = len > 0 ? this.mod.readCString(ptr, len) : "";
1029 s.free();
1030 return text;
1031 }
1032
1033 /** Encode a DOM keydown into pty bytes. Returns null when the key isn't terminal input. */
1034 encodeKey(event: {
1035 code: string;
1036 key: string;
1037 ctrlKey: boolean;
1038 altKey: boolean;
1039 metaKey: boolean;
1040 shiftKey: boolean;
1041 repeat?: boolean;
1042 }): Uint8Array | null {
1043 this.assertLive();
1044 const keyValue = domCodeToGhosttyKey(event.code);
1045 if (keyValue === null) return null;
1046 const mod = this.mod;
1047 const setoptFromTerminal = mod.fn("ghostty_key_encoder_setopt_from_terminal");
1048 setoptFromTerminal(this.keyEncoder, this.term);
1049
1050 const ev = this.keyEvent;
1051 mod.fn("ghostty_key_event_set_action")(
1052 ev,
1053 event.repeat
1054 ? GhosttyKeyAction.GHOSTTY_KEY_ACTION_REPEAT
1055 : GhosttyKeyAction.GHOSTTY_KEY_ACTION_PRESS,
1056 );
1057 mod.fn("ghostty_key_event_set_key")(ev, keyValue);
1058 mod.fn("ghostty_key_event_set_mods")(ev, domEventMods(event));
1059
1060 const printable = event.key.length === 1 && !event.ctrlKey && !event.metaKey;
1061 let utf8Buf: { ptr: number; len: number; free: () => void } | null = null;
1062 let out: ReturnType<GhosttyModule["allocScratch"]> | null = null;
1063 let written: ReturnType<GhosttyModule["allocScratch"]> | null = null;
1064 try {
1065 if (printable) {
1066 utf8Buf = mod.encodeString(event.key);
1067 mod.fn("ghostty_key_event_set_utf8")(ev, utf8Buf.ptr, utf8Buf.len);
1068 // Shift/alt that produced the character are consumed by translation.
1069 mod.fn("ghostty_key_event_set_consumed_mods")(ev, domEventMods(event) & 0b101);
1070 const cp = event.key.codePointAt(0) ?? 0;
1071 mod.fn("ghostty_key_event_set_unshifted_codepoint")(
1072 ev,
1073 cp >= 0x41 && cp <= 0x5a ? cp + 0x20 : cp,
1074 );
1075 } else {
1076 mod.fn("ghostty_key_event_set_utf8")(ev, 0, 0);
1077 mod.fn("ghostty_key_event_set_consumed_mods")(ev, 0);
1078 mod.fn("ghostty_key_event_set_unshifted_codepoint")(ev, 0);
1079 }
1080
1081 out = mod.allocScratch(128);
1082 written = mod.allocScratch(4);
1083 const result = mod.fn("ghostty_key_encoder_encode")(
1084 this.keyEncoder,
1085 ev,
1086 out.ptr,
1087 128,
1088 written.ptr,
1089 );
1090 if (result !== GhosttyResult.GHOSTTY_SUCCESS) return null;
1091 const len = mod.view().getUint32(written.ptr, true);
1092 return len > 0 ? mod.copyBytes(out.ptr, len) : null;
1093 } finally {
1094 written?.free();
1095 out?.free();
1096 utf8Buf?.free();
1097 }
1098 }
1099
1100 /**
1101 * Decode a borrowed clipboard-write request and ask the embedder. Everything
1102 * the pointer refers to is valid only for this call, so strings are copied
1103 * before the handler runs.
1104 */
1105 private onClipboardWrite(writePtr: number): number {
1106 const mod = this.mod;
1107 const RESULT = GhosttyClipboardWriteResult;
1108 if (this.events.onClipboardWrite === undefined) {
1109 return RESULT.GHOSTTY_CLIPBOARD_WRITE_RESULT_DENIED;
1110 }
1111 const field = (name: string) => mod.field("GhosttyClipboardWrite", name).offset;
1112 const view = mod.view();
1113 const location = view.getUint32(writePtr + field("location"), true);
1114 const contents = view.getUint32(writePtr + field("contents"), true);
1115 const count = view.getUint32(writePtr + field("contents_len"), true);
1116
1117 const stringAt = (ptr: number): string => {
1118 const v = mod.view();
1119 const dataPtr = v.getUint32(ptr + mod.field("GhosttyString", "ptr").offset, true);
1120 const len = v.getUint32(ptr + mod.field("GhosttyString", "len").offset, true);
1121 return len > 0 ? mod.readCString(dataPtr, len) : "";
1122 };
1123
1124 const entrySize = mod.layout["GhosttyClipboardContent"]?.size ?? 16;
1125 const mimeOffset = mod.field("GhosttyClipboardContent", "mime").offset;
1126 const dataOffset = mod.field("GhosttyClipboardContent", "data").offset;
1127 const entries: Array<{ mime: string; data: string }> = [];
1128 for (let i = 0; i < count; i++) {
1129 const entry = contents + i * entrySize;
1130 entries.push({ mime: stringAt(entry + mimeOffset), data: stringAt(entry + dataOffset) });
1131 }
1132
1133 const write: ClipboardWrite = {
1134 location: CLIPBOARD_LOCATIONS[location] ?? "standard",
1135 text: entries.find((e) => e.mime === "text/plain" || e.mime === "")?.data ?? "",
1136 entries,
1137 // Zero representations means "clear", which is distinct from an entry
1138 // whose data happens to be empty.
1139 clear: count === 0,
1140 };
1141 const accepted = this.events.onClipboardWrite(write) === true;
1142 return accepted
1143 ? RESULT.GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS
1144 : RESULT.GHOSTTY_CLIPBOARD_WRITE_RESULT_DENIED;
1145 }
1146
1147 get alternateScreen(): boolean {
1148 this.assertLive();
1149 const scratch = this.mod.allocScratch(4);
1150 try {
1151 assertGhosttySuccess(
1152 this.mod.fn("ghostty_terminal_get")(
1153 this.term,
1154 GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN,
1155 scratch.ptr,
1156 ),
1157 "terminal_get(active_screen)",
1158 );
1159 return (
1160 this.mod.view().getUint32(scratch.ptr, true) ===
1161 GhosttyTerminalScreen.GHOSTTY_TERMINAL_SCREEN_ALTERNATE
1162 );
1163 } finally {
1164 scratch.free();
1165 }
1166 }
1167
1168 /**
1169 * Whether the running program asked for mouse events. False means the
1170 * renderer should leave the mouse to the browser — that is what keeps native
1171 * text selection working outside full-screen apps.
1172 */
1173 mouseTracking(): boolean {
1174 this.assertLive();
1175 const scratch = this.mod.allocScratch(8);
1176 this.mod.view().setUint32(scratch.ptr, 0, true);
1177 const result = this.mod.fn("ghostty_terminal_get")(
1178 this.term,
1179 GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING,
1180 scratch.ptr,
1181 );
1182 const mode = this.mod.view().getUint32(scratch.ptr, true);
1183 scratch.free();
1184 return (
1185 result === GhosttyResult.GHOSTTY_SUCCESS &&
1186 mode !== GhosttyMouseTrackingMode.GHOSTTY_MOUSE_TRACKING_NONE
1187 );
1188 }
1189
1190 /**
1191 * Tell the mouse encoder the pixel geometry it needs to turn a surface-space
1192 * position into a cell. Call on resize and whenever metrics change; encoding
1193 * before this is set reports cell 0,0 for everything.
1194 */
1195 setMouseGeometry(geometry: {
1196 width: number;
1197 height: number;
1198 cellWidth: number;
1199 cellHeight: number;
1200 paddingTop?: number;
1201 paddingBottom?: number;
1202 paddingLeft?: number;
1203 paddingRight?: number;
1204 }): void {
1205 this.assertLive();
1206 const mod = this.mod;
1207 const value = (field: string, input: number, min = 0) =>
1208 integerInRange(`mouse geometry ${field}`, Math.round(input), min, UINT32_MAX);
1209 const values: Record<string, number> = {
1210 screen_width: value("screen_width", geometry.width),
1211 screen_height: value("screen_height", geometry.height),
1212 cell_width: value("cell_width", geometry.cellWidth, 1),
1213 cell_height: value("cell_height", geometry.cellHeight, 1),
1214 padding_top: value("padding_top", geometry.paddingTop ?? 0),
1215 padding_bottom: value("padding_bottom", geometry.paddingBottom ?? 0),
1216 padding_left: value("padding_left", geometry.paddingLeft ?? 0),
1217 padding_right: value("padding_right", geometry.paddingRight ?? 0),
1218 };
1219 const size = mod.allocStruct("GhosttyMouseEncoderSize");
1220 try {
1221 for (const [field, fieldValue] of Object.entries(values)) {
1222 mod
1223 .view()
1224 .setUint32(
1225 size.ptr + mod.field("GhosttyMouseEncoderSize", field).offset,
1226 fieldValue,
1227 true,
1228 );
1229 }
1230 mod.fn("ghostty_mouse_encoder_setopt")(
1231 this.mouseEncoder,
1232 GhosttyMouseEncoderOption.GHOSTTY_MOUSE_ENCODER_OPT_SIZE,
1233 size.ptr,
1234 );
1235 } finally {
1236 size.free();
1237 }
1238 }
1239
1240 /**
1241 * Encode a mouse event for the running program. `x`/`y` are surface-space
1242 * pixels (offsets within the terminal element), not cells — libghostty does
1243 * the cell math, including the pixel-precise SGR formats.
1244 *
1245 * Returns null when the event produces no output, which is the common case:
1246 * motion without tracking, a button the mode does not report, or a duplicate
1247 * cell in cell-granular formats.
1248 */
1249 encodeMouse(event: {
1250 action: "press" | "release" | "motion";
1251 /** DOM button number, or null for motion with no button held. */
1252 button: number | null;
1253 x: number;
1254 y: number;
1255 ctrlKey?: boolean;
1256 altKey?: boolean;
1257 metaKey?: boolean;
1258 shiftKey?: boolean;
1259 }): Uint8Array | null {
1260 this.assertLive();
1261 if (
1262 !Number.isFinite(event.x) ||
1263 !Number.isFinite(event.y) ||
1264 Math.abs(event.x) > FLOAT32_MAX ||
1265 Math.abs(event.y) > FLOAT32_MAX
1266 ) {
1267 throw new RangeError(`mouse position must fit float32: ${event.x}, ${event.y}`);
1268 }
1269 const mod = this.mod;
1270 mod.fn("ghostty_mouse_encoder_setopt_from_terminal")(this.mouseEncoder, this.term);
1271
1272 const ev = this.mouseEvent;
1273 mod.fn("ghostty_mouse_event_set_action")(ev, MOUSE_ACTIONS[event.action]);
1274 const button = event.button === null ? null : domButton(event.button);
1275 if (button === null) mod.fn("ghostty_mouse_event_clear_button")(ev);
1276 else mod.fn("ghostty_mouse_event_set_button")(ev, button);
1277 mod.fn("ghostty_mouse_event_set_mods")(
1278 ev,
1279 domEventMods({
1280 ctrlKey: event.ctrlKey === true,
1281 altKey: event.altKey === true,
1282 metaKey: event.metaKey === true,
1283 shiftKey: event.shiftKey === true,
1284 }),
1285 );
1286
1287 const position = mod.allocStruct("GhosttyMousePosition");
1288 try {
1289 const view = mod.view();
1290 view.setFloat32(position.ptr + mod.field("GhosttyMousePosition", "x").offset, event.x, true);
1291 view.setFloat32(position.ptr + mod.field("GhosttyMousePosition", "y").offset, event.y, true);
1292 mod.fn("ghostty_mouse_event_set_position")(ev, position.ptr);
1293 } finally {
1294 position.free();
1295 }
1296
1297 const out = mod.allocScratch(64);
1298 let written: ReturnType<GhosttyModule["allocScratch"]> | null = null;
1299 try {
1300 written = mod.allocScratch(4);
1301 const result = mod.fn("ghostty_mouse_encoder_encode")(
1302 this.mouseEncoder,
1303 ev,
1304 out.ptr,
1305 64,
1306 written.ptr,
1307 );
1308 if (result !== GhosttyResult.GHOSTTY_SUCCESS) return null;
1309 const len = mod.view().getUint32(written.ptr, true);
1310 return len > 0 ? mod.copyBytes(out.ptr, len) : null;
1311 } finally {
1312 written?.free();
1313 out.free();
1314 }
1315 }
1316
1317 /** Encode a paste with bracketed-paste handling driven by terminal mode 2004. */
1318 encodePaste(text: string): Uint8Array {
1319 this.assertLive();
1320 const mod = this.mod;
1321 // GHOSTTY_MODE_BRACKETED_PASTE = ghostty_mode_new(2004, ansi=false) = 2004.
1322 const flag = mod.allocScratch(1);
1323 let bracketed: number;
1324 try {
1325 mod.view().setUint8(flag.ptr, 0);
1326 mod.fn("ghostty_terminal_mode_get")(this.term, 2004, flag.ptr);
1327 bracketed = mod.view().getUint8(flag.ptr) !== 0 ? 1 : 0;
1328 } finally {
1329 flag.free();
1330 }
1331 const input = mod.encodeString(text);
1332 let out: ReturnType<GhosttyModule["allocScratch"]> | null = null;
1333 let written: ReturnType<GhosttyModule["allocScratch"]> | null = null;
1334 try {
1335 const outLen = input.len + 16;
1336 out = mod.allocScratch(outLen);
1337 written = mod.allocScratch(4);
1338 const result = mod.fn("ghostty_paste_encode")(
1339 input.ptr,
1340 input.len,
1341 bracketed,
1342 out.ptr,
1343 outLen,
1344 written.ptr,
1345 );
1346 return result === GhosttyResult.GHOSTTY_SUCCESS
1347 ? mod.copyBytes(out.ptr, mod.view().getUint32(written.ptr, true))
1348 : encoder.encode(text);
1349 } finally {
1350 written?.free();
1351 out?.free();
1352 input.free();
1353 }
1354 }
1355
1356 /** Claim the dirty snapshot stream for one renderer until its reader is disposed. */
1357 acquireFrameReader(): FrameReader {
1358 this.assertLive();
1359 if (this.frameReaderOwner !== null) {
1360 throw new Error("terminal frame snapshots already have a reader");
1361 }
1362 const owner = {};
1363 this.frameReaderOwner = owner;
1364 let released = false;
1365 return {
1366 snapshot: (options = {}) => {
1367 this.assertLive();
1368 if (released || this.frameReaderOwner !== owner) {
1369 throw new Error("terminal frame reader is disposed");
1370 }
1371 return this.readSnapshot(options);
1372 },
1373 dispose: () => {
1374 if (released) return;
1375 released = true;
1376 if (this.frameReaderOwner === owner) this.frameReaderOwner = null;
1377 },
1378 };
1379 }
1380
1381 /**
1382 * Read headless state when no renderer owns the dirty snapshot stream. With
1383 * `full`, every viewport row is returned. Every read consumes dirty flags.
1384 */
1385 snapshot(options: { full?: boolean } = {}): FrameSnapshot {
1386 this.assertLive();
1387 if (this.frameReaderOwner !== null) {
1388 throw new Error("terminal frame snapshots are owned by an attached reader");
1389 }
1390 return this.readSnapshot(options);
1391 }
1392
1393 private readSnapshot(options: { full?: boolean }): FrameSnapshot {
1394 const mod = this.mod;
1395 const ex = {
1396 update: mod.fn("ghostty_render_state_update"),
1397 get: mod.fn("ghostty_render_state_get"),
1398 set: mod.fn("ghostty_render_state_set"),
1399 rowNext: mod.fn("ghostty_render_state_row_iterator_next"),
1400 rowGet: mod.fn("ghostty_render_state_row_get"),
1401 rowSet: mod.fn("ghostty_render_state_row_set"),
1402 cellsNext: mod.fn("ghostty_render_state_row_cells_next"),
1403 cellsGet: mod.fn("ghostty_render_state_row_cells_get"),
1404 cellGet: mod.fn("ghostty_cell_get"),
1405 };
1406 const result = ex.update(this.renderState, this.term);
1407 if (result !== GhosttyResult.GHOSTTY_SUCCESS)
1408 throw new Error(`render_state_update failed: ${result}`);
1409
1410 const D = GhosttyRenderStateData;
1411 const scratch = mod.allocScratch(16);
1412 try {
1413 const readU16 = (what: number) => {
1414 assertGhosttySuccess(ex.get(this.renderState, what, scratch.ptr), "render_state_get(u16)");
1415 return mod.view().getUint16(scratch.ptr, true);
1416 };
1417 const readU32 = (what: number) => {
1418 assertGhosttySuccess(ex.get(this.renderState, what, scratch.ptr), "render_state_get(u32)");
1419 return mod.view().getUint32(scratch.ptr, true);
1420 };
1421 const readBool = (what: number) => {
1422 mod.view().setUint8(scratch.ptr, 0);
1423 assertGhosttySuccess(
1424 ex.get(this.renderState, what, scratch.ptr),
1425 "render_state_get(boolean)",
1426 );
1427 return mod.view().getUint8(scratch.ptr) !== 0;
1428 };
1429 const colorFields = {
1430 r: mod.field("GhosttyColorRgb", "r").offset,
1431 g: mod.field("GhosttyColorRgb", "g").offset,
1432 b: mod.field("GhosttyColorRgb", "b").offset,
1433 };
1434 let palette: number[] | null = null;
1435 let paletteRead = false;
1436 const readPaletteColor = (index: number) => {
1437 if (!paletteRead) {
1438 paletteRead = true;
1439 const rgb = mod.layout["GhosttyColorRgb"]!;
1440 const out = mod.allocScratch(rgb.size * 256);
1441 try {
1442 assertGhosttySuccess(
1443 ex.get(this.renderState, D.GHOSTTY_RENDER_STATE_DATA_COLOR_PALETTE, out.ptr),
1444 "render_state_get(color_palette)",
1445 );
1446 const view = mod.view();
1447 palette = Array.from({ length: 256 }, (_, paletteIndex) => {
1448 const ptr = out.ptr + paletteIndex * rgb.size;
1449 return (
1450 (view.getUint8(ptr + colorFields.r) << 16) |
1451 (view.getUint8(ptr + colorFields.g) << 8) |
1452 view.getUint8(ptr + colorFields.b)
1453 );
1454 });
1455 } finally {
1456 out.free();
1457 }
1458 }
1459 return palette?.[index] ?? -1;
1460 };
1461 const readRenderColor = (what: number) => {
1462 const before = mod.view();
1463 before.setUint8(scratch.ptr + colorFields.r, 0);
1464 before.setUint8(scratch.ptr + colorFields.g, 0);
1465 before.setUint8(scratch.ptr + colorFields.b, 0);
1466 assertGhosttySuccess(
1467 ex.get(this.renderState, what, scratch.ptr),
1468 "render_state_get(color)",
1469 );
1470 const after = mod.view();
1471 return (
1472 (after.getUint8(scratch.ptr + colorFields.r) << 16) |
1473 (after.getUint8(scratch.ptr + colorFields.g) << 8) |
1474 after.getUint8(scratch.ptr + colorFields.b)
1475 );
1476 };
1477
1478 const cols = readU16(D.GHOSTTY_RENDER_STATE_DATA_COLS);
1479 const rows = readU16(D.GHOSTTY_RENDER_STATE_DATA_ROWS);
1480 const dirtyRaw = readU32(D.GHOSTTY_RENDER_STATE_DATA_DIRTY);
1481 let dirty: FrameSnapshot["dirty"] =
1482 dirtyRaw === GhosttyRenderStateDirty.GHOSTTY_RENDER_STATE_DIRTY_FULL
1483 ? "full"
1484 : dirtyRaw === GhosttyRenderStateDirty.GHOSTTY_RENDER_STATE_DIRTY_PARTIAL
1485 ? "partial"
1486 : "none";
1487
1488 // Colors: the program's OSC OVERRIDE, or -1 meaning "the CSS theme
1489 // decides". ***The effective value is not the answer***: `setDefaultColors`
1490 // puts the theme's own colors in the core so it can answer `OSC 10/11/12`,
1491 // and reporting those back would pin `--clo-term-effective-*` to the theme
1492 // and freeze it there — a later theme change would repaint nothing. An
1493 // override that happens to equal the default reads as no override, which
1494 // is the same pixels either way.
1495 const terminalGet = mod.fn("ghostty_terminal_get");
1496 const readColor = (what: number) => {
1497 const v = mod.view();
1498 v.setUint8(scratch.ptr, 0);
1499 v.setUint8(scratch.ptr + 1, 0);
1500 v.setUint8(scratch.ptr + 2, 0);
1501 const r = terminalGet(this.term, what, scratch.ptr);
1502 if (r !== GhosttyResult.GHOSTTY_SUCCESS) return -1;
1503 const v2 = mod.view();
1504 return (
1505 (v2.getUint8(scratch.ptr) << 16) |
1506 (v2.getUint8(scratch.ptr + 1) << 8) |
1507 v2.getUint8(scratch.ptr + 2)
1508 );
1509 };
1510 const overrideColor = (effective: number, fallback: number) => {
1511 const value = readColor(effective);
1512 return value === readColor(fallback) ? -1 : value;
1513 };
1514 const foreground = overrideColor(
1515 GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND,
1516 GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT,
1517 );
1518 const background = overrideColor(
1519 GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND,
1520 GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT,
1521 );
1522
1523 // Cursor: the render state carries the effective color, so the same
1524 // override rule applies to it as to foreground and background.
1525 const cursorDefault = readColor(
1526 GhosttyTerminalData.GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT,
1527 );
1528 const cursorEffective = readBool(D.GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE)
1529 ? readRenderColor(D.GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR)
1530 : -1;
1531 const cursorColor = cursorEffective === cursorDefault ? -1 : cursorEffective;
1532 let cursor: CursorSnapshot | null = null;
1533 if (
1534 readBool(D.GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE) &&
1535 readBool(D.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE)
1536 ) {
1537 const x = readU16(D.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X);
1538 const y = readU16(D.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y);
1539 const styleRaw = readU32(D.GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE);
1540 const blinking = readBool(D.GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING);
1541 const wide = readBool(D.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL);
1542 cursor = {
1543 x,
1544 y,
1545 style: CURSOR_STYLES[styleRaw] ?? "block",
1546 blinking,
1547 wide,
1548 color: cursorColor,
1549 };
1550 }
1551
1552 // Rows
1553 const wantAll = options.full === true || dirty === "full";
1554 const acquireRows = (renderState = this.renderState) => {
1555 const iterOut = mod.allocScratch(4);
1556 try {
1557 mod.view().setUint32(iterOut.ptr, this.rowIter, true);
1558 assertGhosttySuccess(
1559 ex.get(renderState, D.GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, iterOut.ptr),
1560 "render_state_get(row_iterator)",
1561 );
1562 } finally {
1563 iterOut.free();
1564 }
1565 };
1566 acquireRows();
1567
1568 const clean = mod.allocScratch(1);
1569 let cleanState: ReturnType<GhosttyModule["allocScratch"]>;
1570 try {
1571 cleanState = mod.allocScratch(4);
1572 } catch (error) {
1573 clean.free();
1574 throw error;
1575 }
1576 // Incremental render state can retain stale raw row summaries when Ghostty
1577 // changes a non-painted boundary flag without dirtying that row. Preserve
1578 // and clear its dirty set, then decode through a fresh render state.
1579 let originalRowDirty: boolean[] | null = null;
1580 const refreshRawRows = options.full === true || dirty !== "none";
1581 let canonicalRenderState = 0;
1582 const dirtyRows: RowSnapshot[] = [];
1583 try {
1584 mod.view().setUint8(clean.ptr, 0);
1585 if (refreshRawRows) {
1586 originalRowDirty = [];
1587 while (ex.rowNext(this.rowIter) !== 0) {
1588 mod.view().setUint8(scratch.ptr, 0);
1589 assertGhosttySuccess(
1590 ex.rowGet(
1591 this.rowIter,
1592 GhosttyRenderStateRowData.GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY,
1593 scratch.ptr,
1594 ),
1595 "render_state_row_get(dirty)",
1596 );
1597 const rowDirty = mod.view().getUint8(scratch.ptr) !== 0;
1598 originalRowDirty.push(rowDirty);
1599 }
1600 canonicalRenderState = mod.create("ghostty_render_state_new");
1601 const refresh = ex.update(canonicalRenderState, this.term);
1602 if (refresh !== GhosttyResult.GHOSTTY_SUCCESS) {
1603 throw new Error(`render_state_update failed while refreshing rows: ${refresh}`);
1604 }
1605 acquireRows(canonicalRenderState);
1606 }
1607
1608 const rowGet = mod.fn("ghostty_row_get");
1609 const rowBool = (rawRow: bigint, what: number) => {
1610 mod.view().setUint8(scratch.ptr, 0);
1611 assertGhosttySuccess(rowGet(rawRow, what, scratch.ptr), "row_get(boolean)");
1612 return mod.view().getUint8(scratch.ptr) !== 0;
1613 };
1614 let rowIndex = 0;
1615 const nextRowMetadata: RowMetadata[] = [];
1616 while (refreshRawRows && ex.rowNext(this.rowIter) !== 0) {
1617 const rowDirty = originalRowDirty?.[rowIndex] ?? false;
1618 mod.view().setBigUint64(scratch.ptr, 0n, true);
1619 assertGhosttySuccess(
1620 ex.rowGet(
1621 this.rowIter,
1622 GhosttyRenderStateRowData.GHOSTTY_RENDER_STATE_ROW_DATA_RAW,
1623 scratch.ptr,
1624 ),
1625 "render_state_row_get(raw)",
1626 );
1627 const rawRow = mod.view().getBigUint64(scratch.ptr, true);
1628 mod.view().setUint32(scratch.ptr, 0, true);
1629 const semanticResult = rowGet(
1630 rawRow,
1631 GhosttyRowData.GHOSTTY_ROW_DATA_SEMANTIC_PROMPT,
1632 scratch.ptr,
1633 );
1634 assertGhosttySuccess(semanticResult, "row_get(semantic_prompt)");
1635 const semanticRaw = mod.view().getUint32(scratch.ptr, true);
1636 const metadata: RowMetadata = {
1637 wrap: rowBool(rawRow, GhosttyRowData.GHOSTTY_ROW_DATA_WRAP),
1638 wrapContinuation: rowBool(rawRow, GhosttyRowData.GHOSTTY_ROW_DATA_WRAP_CONTINUATION),
1639 semanticPrompt:
1640 semanticRaw === GhosttyRowSemanticPrompt.GHOSTTY_ROW_SEMANTIC_PROMPT
1641 ? "prompt"
1642 : semanticRaw === GhosttyRowSemanticPrompt.GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION
1643 ? "continuation"
1644 : "none",
1645 hyperlink: rowBool(rawRow, GhosttyRowData.GHOSTTY_ROW_DATA_HYPERLINK),
1646 };
1647 const previous = this.rowMetadata[rowIndex];
1648 const metadataChanged =
1649 previous === undefined ||
1650 previous.wrap !== metadata.wrap ||
1651 previous.wrapContinuation !== metadata.wrapContinuation ||
1652 previous.semanticPrompt !== metadata.semanticPrompt ||
1653 previous.hyperlink !== metadata.hyperlink;
1654 nextRowMetadata[rowIndex] = metadata;
1655 // Replacing a continuation row can remove the predecessor's spacer
1656 // head without dirtying that predecessor in Ghostty. A wrapped row
1657 // therefore shares the continuation row's dirty boundary.
1658 const continuationDirty = metadata.wrap && (originalRowDirty?.[rowIndex + 1] ?? false);
1659 if (rowDirty || continuationDirty || wantAll || metadataChanged) {
1660 if (!rowDirty && !wantAll && dirty === "none") dirty = "partial";
1661 dirtyRows.push({
1662 index: rowIndex,
1663 ...metadata,
1664 cells: this.readRowCells(rowIndex, metadata.hyperlink, scratch.ptr, readPaletteColor),
1665 });
1666 }
1667 rowIndex++;
1668 }
1669 if (refreshRawRows) {
1670 // Promote before consuming row bits so any cleanup failure retries a
1671 // complete frame rather than losing rows already marked clean.
1672 mod
1673 .view()
1674 .setUint32(
1675 cleanState.ptr,
1676 GhosttyRenderStateDirty.GHOSTTY_RENDER_STATE_DIRTY_FULL,
1677 true,
1678 );
1679 assertGhosttySuccess(
1680 ex.set(
1681 this.renderState,
1682 GhosttyRenderStateOption.GHOSTTY_RENDER_STATE_OPTION_DIRTY,
1683 cleanState.ptr,
1684 ),
1685 "render_state_set(full)",
1686 );
1687 acquireRows();
1688 let dirtyIndex = 0;
1689 while (ex.rowNext(this.rowIter) !== 0) {
1690 if (originalRowDirty?.[dirtyIndex] === true) {
1691 assertGhosttySuccess(
1692 ex.rowSet(
1693 this.rowIter,
1694 GhosttyRenderStateRowOption.GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY,
1695 clean.ptr,
1696 ),
1697 "render_state_row_set(dirty)",
1698 );
1699 }
1700 dirtyIndex++;
1701 }
1702 this.rowMetadata = nextRowMetadata;
1703 }
1704 } catch (error) {
1705 cleanState.free();
1706 throw error;
1707 } finally {
1708 clean.free();
1709 if (canonicalRenderState !== 0) {
1710 mod.fn("ghostty_render_state_free")(canonicalRenderState);
1711 }
1712 }
1713
1714 mod
1715 .view()
1716 .setUint32(cleanState.ptr, GhosttyRenderStateDirty.GHOSTTY_RENDER_STATE_DIRTY_FALSE, true);
1717 try {
1718 assertGhosttySuccess(
1719 ex.set(
1720 this.renderState,
1721 GhosttyRenderStateOption.GHOSTTY_RENDER_STATE_OPTION_DIRTY,
1722 cleanState.ptr,
1723 ),
1724 "render_state_set(dirty)",
1725 );
1726 } finally {
1727 cleanState.free();
1728 }
1729
1730 return { cols, rows, dirty, dirtyRows, cursor, foreground, background };
1731 } finally {
1732 scratch.free();
1733 }
1734 }
1735
1736 private readRowCells(
1737 rowIndex: number,
1738 rowHasHyperlink: boolean,
1739 scratch: number,
1740 readPaletteColor: (index: number) => number,
1741 ): CellSnapshot[] {
1742 const mod = this.mod;
1743 const cellsGet = mod.fn("ghostty_render_state_row_cells_get");
1744 const cellsGetMulti = mod.fn("ghostty_render_state_row_cells_get_multi");
1745 const cellsNext = mod.fn("ghostty_render_state_row_cells_next");
1746 const cellGetMulti = mod.fn("ghostty_cell_get_multi");
1747 const C = GhosttyRenderStateRowCellsData;
1748 const asciiCell = mod.layout["GhosttyRenderStateAsciiCell"];
1749 if (asciiCell === undefined) throw new Error("missing GhosttyRenderStateAsciiCell layout");
1750 const asciiCodepoint = mod.field("GhosttyRenderStateAsciiCell", "codepoint").offset;
1751 const asciiForeground = mod.field("GhosttyRenderStateAsciiCell", "foreground").offset;
1752
1753 const cellsOut = mod.allocScratch(4);
1754 try {
1755 mod.view().setUint32(cellsOut.ptr, this.rowCells, true);
1756 assertGhosttySuccess(
1757 mod.fn("ghostty_render_state_row_get")(
1758 this.rowIter,
1759 GhosttyRenderStateRowData.GHOSTTY_RENDER_STATE_ROW_DATA_CELLS,
1760 cellsOut.ptr,
1761 ),
1762 "render_state_row_get(cells)",
1763 );
1764 } finally {
1765 cellsOut.free();
1766 }
1767
1768 if (this.asciiRowScratch === null || this.asciiRowScratch.capacity < this.colsNow) {
1769 const capacity = 2 ** Math.ceil(Math.log2(this.colsNow));
1770 const next = mod.allocScratch(capacity * asciiCell.size);
1771 this.asciiRowScratch?.free();
1772 this.asciiRowScratch = { ...next, capacity };
1773 }
1774 const asciiResult = mod.fn("ghostty_render_state_row_cells_ascii")(
1775 this.rowCells,
1776 this.asciiRowScratch.ptr,
1777 this.asciiRowScratch.capacity,
1778 scratch,
1779 );
1780 if (asciiResult === GhosttyResult.GHOSTTY_SUCCESS) {
1781 const length = mod.view().getUint32(scratch, true);
1782 const cells: CellSnapshot[] = [];
1783 for (let index = 0; index < length; index++) {
1784 const ptr = this.asciiRowScratch.ptr + index * asciiCell.size;
1785 const codepoint = mod.view().getUint32(ptr + asciiCodepoint, true);
1786 const foreground = mod.view().getUint32(ptr + asciiForeground, true);
1787 cells.push({
1788 text: codepoint === 0 ? "" : String.fromCharCode(codepoint),
1789 width: 1,
1790 spacer: "none",
1791 protected: false,
1792 semantic: "output",
1793 hasOsc8: false,
1794 osc8Uri: null,
1795 fg: foreground === 0xffffffff ? -1 : foreground,
1796 bg: -1,
1797 underlineColor: -1,
1798 bold: false,
1799 italic: false,
1800 faint: false,
1801 blink: false,
1802 inverse: false,
1803 invisible: false,
1804 strikethrough: false,
1805 overline: false,
1806 underline: 0,
1807 });
1808 }
1809 return cells;
1810 }
1811 if (asciiResult !== GhosttyResult.GHOSTTY_NO_VALUE) {
1812 throw new Error(`render_state_row_cells_ascii failed: ${asciiResult}`);
1813 }
1814
1815 const allocations: Array<{ free(): void }> = [];
1816 const own = <T extends { free(): void }>(allocation: T): T => {
1817 allocations.push(allocation);
1818 return allocation;
1819 };
1820 try {
1821 const style = own(mod.allocStruct("GhosttyStyle"));
1822 const rowKeys = own(mod.allocScratch(16));
1823 const rowValues = own(mod.allocScratch(16));
1824 const cellKeys = own(mod.allocScratch(20));
1825 const cellValues = own(mod.allocScratch(20));
1826 const rowQuery = [
1827 C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW,
1828 C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE,
1829 C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN,
1830 C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_HAS_STYLING,
1831 ];
1832 const cellQuery = [
1833 GhosttyCellData.GHOSTTY_CELL_DATA_WIDE,
1834 GhosttyCellData.GHOSTTY_CELL_DATA_PROTECTED,
1835 GhosttyCellData.GHOSTTY_CELL_DATA_SEMANTIC_CONTENT,
1836 GhosttyCellData.GHOSTTY_CELL_DATA_HAS_HYPERLINK,
1837 GhosttyCellData.GHOSTTY_CELL_DATA_CONTENT_TAG,
1838 ];
1839 const queryView = mod.view();
1840 rowQuery.forEach((key, index) => queryView.setUint32(rowKeys.ptr + index * 4, key, true));
1841 [scratch, style.ptr, scratch + 8, scratch + 12].forEach((ptr, index) =>
1842 queryView.setUint32(rowValues.ptr + index * 4, ptr, true),
1843 );
1844 cellQuery.forEach((key, index) => queryView.setUint32(cellKeys.ptr + index * 4, key, true));
1845 [scratch, scratch + 8, scratch + 4, scratch + 9, scratch + 12].forEach((ptr, index) =>
1846 queryView.setUint32(cellValues.ptr + index * 4, ptr, true),
1847 );
1848 const styleFields = {
1849 bold: mod.field("GhosttyStyle", "bold").offset,
1850 italic: mod.field("GhosttyStyle", "italic").offset,
1851 faint: mod.field("GhosttyStyle", "faint").offset,
1852 blink: mod.field("GhosttyStyle", "blink").offset,
1853 inverse: mod.field("GhosttyStyle", "inverse").offset,
1854 invisible: mod.field("GhosttyStyle", "invisible").offset,
1855 strikethrough: mod.field("GhosttyStyle", "strikethrough").offset,
1856 overline: mod.field("GhosttyStyle", "overline").offset,
1857 underline: mod.field("GhosttyStyle", "underline").offset,
1858 fgTag: mod.field("GhosttyStyle", "fg_color").offset,
1859 bgTag: mod.field("GhosttyStyle", "bg_color").offset,
1860 underlineColor: mod.field("GhosttyStyle", "underline_color").offset,
1861 colorTag: mod.field("GhosttyStyleColor", "tag").offset,
1862 colorValue: mod.field("GhosttyStyleColor", "value").offset,
1863 };
1864 const colorFields = {
1865 r: mod.field("GhosttyColorRgb", "r").offset,
1866 g: mod.field("GhosttyColorRgb", "g").offset,
1867 b: mod.field("GhosttyColorRgb", "b").offset,
1868 };
1869 let graphemeCapacity = INITIAL_GRAPHEME_CAPACITY;
1870 let graphemeBuf = own(mod.allocScratch(graphemeCapacity * 4));
1871 const readResolvedColor = (what: number) => {
1872 const view = mod.view();
1873 view.setUint8(scratch + colorFields.r, 0);
1874 view.setUint8(scratch + colorFields.g, 0);
1875 view.setUint8(scratch + colorFields.b, 0);
1876 assertGhosttySuccess(cellsGet(this.rowCells, what, scratch), "row_cells_get(color)");
1877 const resolved = mod.view();
1878 return (
1879 (resolved.getUint8(scratch + colorFields.r) << 16) |
1880 (resolved.getUint8(scratch + colorFields.g) << 8) |
1881 resolved.getUint8(scratch + colorFields.b)
1882 );
1883 };
1884
1885 const cells: CellSnapshot[] = [];
1886 const hyperlink = rowHasHyperlink ? this.resolveHyperlinkRow(rowIndex) : null;
1887 let physicalColumn = 0;
1888 while (cellsNext(this.rowCells) !== 0) {
1889 const cellColumn = physicalColumn++;
1890 assertGhosttySuccess(
1891 cellsGetMulti(this.rowCells, rowQuery.length, rowKeys.ptr, rowValues.ptr, 0),
1892 "row_cells_get_multi",
1893 );
1894 const rawCell = mod.view().getBigUint64(scratch, true);
1895 const graphemeLen = mod.view().getUint32(scratch + 8, true);
1896 const hasStyling = mod.view().getUint8(scratch + 12) !== 0;
1897 let width = 1;
1898 let spacer: CellSnapshot["spacer"] = "none";
1899 let protectedCell = false;
1900 let semantic: CellSnapshot["semantic"] = "output";
1901 let hasOsc8 = false;
1902 let osc8Uri: string | null = null;
1903 let contentTag: number = GhosttyCellContentTag.GHOSTTY_CELL_CONTENT_CODEPOINT;
1904 if (rawCell !== 0n) {
1905 // GhosttyCell is a packed 64-bit value, not a pointer: pass it as i64.
1906 assertGhosttySuccess(
1907 cellGetMulti(rawCell, cellQuery.length, cellKeys.ptr, cellValues.ptr, 0),
1908 "cell_get_multi",
1909 );
1910 const wide = mod.view().getUint32(scratch, true);
1911 spacer = wide === GhosttyCellWide.GHOSTTY_CELL_WIDE_SPACER_HEAD ? "head" : "none";
1912 width = wide === GhosttyCellWide.GHOSTTY_CELL_WIDE_WIDE ? 2 : 1;
1913 const semanticRaw = mod.view().getUint32(scratch + 4, true);
1914 protectedCell = mod.view().getUint8(scratch + 8) !== 0;
1915 contentTag = mod.view().getUint32(scratch + 12, true);
1916 semantic =
1917 semanticRaw === GhosttyCellSemanticContent.GHOSTTY_CELL_SEMANTIC_INPUT
1918 ? "input"
1919 : semanticRaw === GhosttyCellSemanticContent.GHOSTTY_CELL_SEMANTIC_PROMPT
1920 ? "prompt"
1921 : "output";
1922
1923 if (hyperlink !== null) {
1924 if (mod.view().getUint8(scratch + 9) !== 0) {
1925 hasOsc8 = true;
1926 osc8Uri = this.readHyperlinkUri(hyperlink, cellColumn);
1927 }
1928 }
1929 if (wide === GhosttyCellWide.GHOSTTY_CELL_WIDE_SPACER_TAIL) continue;
1930 }
1931
1932 let text = "";
1933 if (graphemeLen > 0) {
1934 if (graphemeLen > graphemeCapacity) {
1935 graphemeBuf.free();
1936 graphemeCapacity = 2 ** Math.ceil(Math.log2(graphemeLen));
1937 graphemeBuf = own(mod.allocScratch(graphemeCapacity * 4));
1938 }
1939 assertGhosttySuccess(
1940 cellsGet(
1941 this.rowCells,
1942 C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF,
1943 graphemeBuf.ptr,
1944 ),
1945 "row_cells_get(graphemes_buf)",
1946 );
1947 text = codepointsToString(mod.view(), graphemeBuf.ptr, graphemeLen);
1948 }
1949
1950 let cell: CellSnapshot;
1951 if (hasStyling) {
1952 const readColor = (tagOffset: number, resolved: number) => {
1953 return mod.view().getUint32(style.ptr + tagOffset + styleFields.colorTag, true) === 0
1954 ? -1
1955 : readResolvedColor(resolved);
1956 };
1957 const fg = readColor(styleFields.fgTag, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR);
1958 const bg = readColor(styleFields.bgTag, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR);
1959 const underlineColorPtr = style.ptr + styleFields.underlineColor + styleFields.colorValue;
1960 const underlineColorTag = mod
1961 .view()
1962 .getUint32(style.ptr + styleFields.underlineColor + styleFields.colorTag, true);
1963 let underlineColor = -1;
1964 if (underlineColorTag === GhosttyStyleColorTag.GHOSTTY_STYLE_COLOR_RGB) {
1965 const color = mod.view();
1966 underlineColor =
1967 (color.getUint8(underlineColorPtr + colorFields.r) << 16) |
1968 (color.getUint8(underlineColorPtr + colorFields.g) << 8) |
1969 color.getUint8(underlineColorPtr + colorFields.b);
1970 } else if (underlineColorTag === GhosttyStyleColorTag.GHOSTTY_STYLE_COLOR_PALETTE) {
1971 underlineColor = readPaletteColor(mod.view().getUint8(underlineColorPtr));
1972 }
1973 const v = mod.view();
1974 cell = {
1975 text,
1976 width,
1977 spacer,
1978 protected: protectedCell,
1979 semantic,
1980 hasOsc8,
1981 osc8Uri,
1982 fg,
1983 bg,
1984 underlineColor,
1985 bold: v.getUint8(style.ptr + styleFields.bold) !== 0,
1986 italic: v.getUint8(style.ptr + styleFields.italic) !== 0,
1987 faint: v.getUint8(style.ptr + styleFields.faint) !== 0,
1988 blink: v.getUint8(style.ptr + styleFields.blink) !== 0,
1989 inverse: v.getUint8(style.ptr + styleFields.inverse) !== 0,
1990 invisible: v.getUint8(style.ptr + styleFields.invisible) !== 0,
1991 strikethrough: v.getUint8(style.ptr + styleFields.strikethrough) !== 0,
1992 overline: v.getUint8(style.ptr + styleFields.overline) !== 0,
1993 underline: v.getInt32(style.ptr + styleFields.underline, true),
1994 };
1995 } else {
1996 let bg = -1;
1997 if (rawCell !== 0n && text === "") {
1998 if (
1999 contentTag === GhosttyCellContentTag.GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE ||
2000 contentTag === GhosttyCellContentTag.GHOSTTY_CELL_CONTENT_BG_COLOR_RGB
2001 ) {
2002 bg = readResolvedColor(C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR);
2003 }
2004 }
2005 cell = {
2006 text,
2007 width,
2008 spacer,
2009 protected: protectedCell,
2010 semantic,
2011 hasOsc8,
2012 osc8Uri,
2013 fg: -1,
2014 bg,
2015 underlineColor: -1,
2016 bold: false,
2017 italic: false,
2018 faint: false,
2019 blink: false,
2020 inverse: false,
2021 invisible: false,
2022 strikethrough: false,
2023 overline: false,
2024 underline: 0,
2025 };
2026 }
2027 cells.push(cell);
2028 }
2029
2030 // Trim only cells with no canonical presentation state so runs/DOM stay small.
2031 trimCanonicalDefaultTail(cells);
2032 return cells;
2033 } finally {
2034 for (let index = allocations.length - 1; index >= 0; index--) allocations[index]!.free();
2035 }
2036 }
2037
2038 private resolveHyperlinkRow(rowIndex: number): HyperlinkScratch {
2039 const mod = this.mod;
2040 let hyperlink = this.hyperlinkScratch;
2041 if (hyperlink === null) {
2042 let point: HyperlinkScratch["point"] | null = null;
2043 let ref: HyperlinkScratch["ref"] | null = null;
2044 let outLen: HyperlinkScratch["outLen"] | null = null;
2045 try {
2046 point = mod.allocStruct("GhosttyPoint");
2047 ref = mod.allocStruct("GhosttyGridRef");
2048 outLen = mod.allocScratch(4);
2049 hyperlink = {
2050 point,
2051 ref,
2052 outLen,
2053 uri: mod.allocScratch(INITIAL_HYPERLINK_URI_CAPACITY),
2054 uriCapacity: INITIAL_HYPERLINK_URI_CAPACITY,
2055 };
2056 } catch (error) {
2057 outLen?.free();
2058 ref?.free();
2059 point?.free();
2060 throw error;
2061 }
2062 this.hyperlinkScratch = hyperlink;
2063 }
2064
2065 const pointTag = mod.field("GhosttyPoint", "tag").offset;
2066 const pointValue = mod.field("GhosttyPoint", "value").offset;
2067 const coordinateX = mod.field("GhosttyPointCoordinate", "x").offset;
2068 const coordinateY = mod.field("GhosttyPointCoordinate", "y").offset;
2069 mod
2070 .view()
2071 .setUint32(hyperlink.point.ptr + pointTag, GhosttyPointTag.GHOSTTY_POINT_TAG_VIEWPORT, true);
2072 mod.view().setUint16(hyperlink.point.ptr + pointValue + coordinateX, 0, true);
2073 mod.view().setUint32(hyperlink.point.ptr + pointValue + coordinateY, rowIndex, true);
2074 assertGhosttySuccess(
2075 mod.fn("ghostty_terminal_grid_ref")(this.term, hyperlink.point.ptr, hyperlink.ref.ptr),
2076 "terminal_grid_ref(hyperlink row)",
2077 );
2078 return hyperlink;
2079 }
2080
2081 private readHyperlinkUri(hyperlink: HyperlinkScratch, physicalColumn: number): string | null {
2082 const mod = this.mod;
2083 mod
2084 .view()
2085 .setUint16(hyperlink.ref.ptr + mod.field("GhosttyGridRef", "x").offset, physicalColumn, true);
2086 mod.view().setUint32(hyperlink.outLen.ptr, 0, true);
2087 let result = mod.fn("ghostty_grid_ref_hyperlink_uri")(
2088 hyperlink.ref.ptr,
2089 hyperlink.uri.ptr,
2090 hyperlink.uriCapacity,
2091 hyperlink.outLen.ptr,
2092 );
2093 if (result === GhosttyResult.GHOSTTY_OUT_OF_SPACE) {
2094 const required = mod.view().getUint32(hyperlink.outLen.ptr, true);
2095 if (required <= 0) throw new Error("grid_ref_hyperlink_uri returned an invalid size");
2096 const next = mod.allocScratch(required);
2097 hyperlink.uri.free();
2098 hyperlink.uri = next;
2099 hyperlink.uriCapacity = required;
2100 mod.view().setUint32(hyperlink.outLen.ptr, 0, true);
2101 result = mod.fn("ghostty_grid_ref_hyperlink_uri")(
2102 hyperlink.ref.ptr,
2103 hyperlink.uri.ptr,
2104 hyperlink.uriCapacity,
2105 hyperlink.outLen.ptr,
2106 );
2107 }
2108 assertGhosttySuccess(result, "grid_ref_hyperlink_uri");
2109 const length = mod.view().getUint32(hyperlink.outLen.ptr, true);
2110 if (length > hyperlink.uriCapacity) {
2111 throw new Error(`grid_ref_hyperlink_uri wrote invalid length: ${length}`);
2112 }
2113 const bytes = mod.bytes(hyperlink.uri.ptr, length);
2114 try {
2115 return osc8Decoder.decode(bytes);
2116 } catch {
2117 return null;
2118 }
2119 }
2120
2121 /** Plain-text dump of the active screen (tests, copy-all). */
2122 text(): string {
2123 this.assertLive();
2124 const mod = this.mod;
2125 const opts = mod.allocStruct("GhosttyFormatterTerminalOptions");
2126 let fmt: number;
2127 try {
2128 const trim = mod.field("GhosttyFormatterTerminalOptions", "trim");
2129 mod.view().setUint8(opts.ptr + trim.offset, 1);
2130 const extraOff = mod.field("GhosttyFormatterTerminalOptions", "extra").offset;
2131 const extra = mod.layout["GhosttyFormatterTerminalExtra"]!;
2132 mod.view().setUint32(opts.ptr + extraOff + extra.fields["size"]!.offset, extra.size, true);
2133 const screenOff = extra.fields["screen"]!.offset;
2134 const screen = mod.layout["GhosttyFormatterScreenExtra"]!;
2135 mod
2136 .view()
2137 .setUint32(
2138 opts.ptr + extraOff + screenOff + screen.fields["size"]!.offset,
2139 screen.size,
2140 true,
2141 );
2142 fmt = mod.create("ghostty_formatter_terminal_new", this.term, opts.ptr);
2143 } finally {
2144 opts.free();
2145 }
2146
2147 let outPtrPtr: ReturnType<GhosttyModule["allocScratch"]> | null = null;
2148 let outLenPtr: ReturnType<GhosttyModule["allocScratch"]> | null = null;
2149 try {
2150 outPtrPtr = mod.allocScratch(4);
2151 outLenPtr = mod.allocScratch(4);
2152 mod.view().setUint32(outPtrPtr.ptr, 0, true);
2153 mod.view().setUint32(outLenPtr.ptr, 0, true);
2154 const result = mod.fn("ghostty_formatter_format_alloc")(fmt, 0, outPtrPtr.ptr, outLenPtr.ptr);
2155 const ptr = mod.view().getUint32(outPtrPtr.ptr, true);
2156 const len = mod.view().getUint32(outLenPtr.ptr, true);
2157 if (ptr === 0) {
2158 if (result === GhosttyResult.GHOSTTY_SUCCESS && len !== 0) {
2159 throw new Error("ghostty_formatter_format_alloc returned a null buffer");
2160 }
2161 return "";
2162 }
2163 try {
2164 return result === GhosttyResult.GHOSTTY_SUCCESS ? mod.readCString(ptr, len) : "";
2165 } finally {
2166 mod.fn("ghostty_free")(0, ptr, len);
2167 }
2168 } finally {
2169 outLenPtr?.free();
2170 outPtrPtr?.free();
2171 mod.fn("ghostty_formatter_free")(fmt);
2172 }
2173 }
2174
2175 free(): void {
2176 if (this.disposed || this.disposePending) return;
2177 if (this.writing) {
2178 this.disposePending = true;
2179 return;
2180 }
2181 this.disposed = true;
2182 this.frameReaderOwner = null;
2183 // Anchors first: tracked refs outlive the terminal, so freeing them after
2184 // it would leak the handles rather than crash.
2185 this.shell.dispose();
2186 this.releaseWasmResources();
2187 }
2188
2189 private releaseWasmResources(): void {
2190 const mod = this.mod;
2191 this.asciiRowScratch?.free();
2192 this.asciiRowScratch = null;
2193 this.inputScratch?.free();
2194 this.inputScratch = null;
2195 if (this.hyperlinkScratch !== null) {
2196 this.hyperlinkScratch.uri.free();
2197 this.hyperlinkScratch.outLen.free();
2198 this.hyperlinkScratch.ref.free();
2199 this.hyperlinkScratch.point.free();
2200 this.hyperlinkScratch = null;
2201 }
2202 if (this.mouseEvent !== 0) {
2203 mod.fn("ghostty_mouse_event_free")(this.mouseEvent);
2204 this.mouseEvent = 0;
2205 }
2206 if (this.mouseEncoder !== 0) {
2207 mod.fn("ghostty_mouse_encoder_free")(this.mouseEncoder);
2208 this.mouseEncoder = 0;
2209 }
2210 if (this.keyEvent !== 0) {
2211 mod.fn("ghostty_key_event_free")(this.keyEvent);
2212 this.keyEvent = 0;
2213 }
2214 if (this.keyEncoder !== 0) {
2215 mod.fn("ghostty_key_encoder_free")(this.keyEncoder);
2216 this.keyEncoder = 0;
2217 }
2218 if (this.rowCells !== 0) {
2219 mod.fn("ghostty_render_state_row_cells_free")(this.rowCells);
2220 this.rowCells = 0;
2221 }
2222 if (this.rowIter !== 0) {
2223 mod.fn("ghostty_render_state_row_iterator_free")(this.rowIter);
2224 this.rowIter = 0;
2225 }
2226 if (this.renderState !== 0) {
2227 mod.fn("ghostty_render_state_free")(this.renderState);
2228 this.renderState = 0;
2229 }
2230 if (this.modeValue !== 0) {
2231 mod.fn("ghostty_wasm_free_u8")(this.modeValue);
2232 this.modeValue = 0;
2233 }
2234 if (this.term !== 0) {
2235 mod.fn("ghostty_terminal_free")(this.term);
2236 this.term = 0;
2237 }
2238 for (const trampoline of this.trampolines.splice(0).reverse()) trampoline.free();
2239 }
2240
2241 private assertLive(): void {
2242 if (this.disposed) throw new Error("terminal is disposed");
2243 }
2244}
packages/terminal/src/index.ts created+58
......@@ -0,0 +1,58 @@
1export { GhosttyModule } from "./ghostty/module.ts";
2export { ansiToHtml } from "./ansi.ts";
3export { mountTerminal, type MountTerminalOptions, type TerminalHandle } from "./mount.ts";
4export {
5 Terminal,
6 type CellSnapshot,
7 type ClipboardWrite,
8 type CursorSnapshot,
9 type FrameReader,
10 type FrameSnapshot,
11 type RowSnapshot,
12 type Scrollbar,
13 type TerminalEvents,
14 type TerminalOptions,
15} from "./ghostty/terminal.ts";
16export {
17 TerminalView,
18 type TerminalLinkOptions,
19 type TerminalViewOptions,
20} from "./renderer/dom.ts";
21export { cellsToRuns, cellText, linkCells, rowText, type CellRun, type RowText } from "./runs.ts";
22export {
23 columnTexts,
24 DEFAULT_PREDICT_OPTIONS,
25 isPredictableText,
26 PredictionEngine,
27 resolvePredictOptions,
28 type PredictionMode,
29 type PredictionSite,
30 type PredictOptions,
31 type ReconcileFrame,
32 type ResolvedPredictOptions,
33} from "./predict/index.ts";
34export { detectLinks, type DetectedLink, type DetectLinksOptions, type LinkKind } from "./links.ts";
35export {
36 OscScanner,
37 parseShellMark,
38 ShellCommands,
39 stickyCommand,
40 stickyTarget,
41 unescapeMarkValue,
42 type CommandAnchor,
43 type CommandState,
44 type MarkSource,
45 type OscSpan,
46 type ShellCommand,
47 type ShellCommandsOptions,
48 type ShellMark,
49} from "./shell/index.ts";
50export {
51 domCodeToGhosttyKey,
52 domEventMods,
53 MODS_ALT,
54 MODS_CTRL,
55 MODS_SHIFT,
56 MODS_SUPER,
57 type ModState,
58} from "./keys.ts";
packages/terminal/src/keys.ts created+60
......@@ -0,0 +1,60 @@
1// DOM KeyboardEvent -> libghostty key model. Pure: no wasm, no DOM types.
2// Ghostty's key enum is named after W3C KeyboardEvent.code values, so the
3// mapping is a mechanical name transform plus a small irregular table.
4
5import { GhosttyKey } from "./ghostty/constants.ts";
6
7// Bit values from vendor/include/ghostty/vt/key/event.h (#defines, invisible
8// to the enum generator — verified on vendor bump by test/keys.test.ts).
9export const MODS_SHIFT = 1 << 0;
10export const MODS_CTRL = 1 << 1;
11export const MODS_ALT = 1 << 2;
12export const MODS_SUPER = 1 << 3;
13export const MODS_CAPS_LOCK = 1 << 4;
14export const MODS_NUM_LOCK = 1 << 5;
15
16const IRREGULAR: Record<string, string> = {
17 MetaLeft: "GHOSTTY_KEY_META_LEFT",
18 MetaRight: "GHOSTTY_KEY_META_RIGHT",
19 OSLeft: "GHOSTTY_KEY_META_LEFT",
20 OSRight: "GHOSTTY_KEY_META_RIGHT",
21};
22
23/** "KeyA" -> GHOSTTY_KEY_A, "ArrowDown" -> GHOSTTY_KEY_ARROW_DOWN, "F1" -> GHOSTTY_KEY_F1, "Digit1" -> GHOSTTY_KEY_DIGIT_1. */
24export function domCodeToGhosttyKey(code: string): number | null {
25 if (code === "") return null;
26 let name = IRREGULAR[code];
27 if (name === undefined) {
28 const letter = /^Key([A-Z])$/.exec(code);
29 if (letter !== null) {
30 name = `GHOSTTY_KEY_${letter[1]}`;
31 } else if (/^F\d+$/.test(code)) {
32 name = `GHOSTTY_KEY_${code}`;
33 } else {
34 const snake = code
35 .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
36 .replace(/([A-Za-z])(\d)/g, "$1_$2")
37 .toUpperCase();
38 name = `GHOSTTY_KEY_${snake}`;
39 }
40 }
41 const value = (GhosttyKey as Record<string, number>)[name];
42 // GHOSTTY_KEY_UNIDENTIFIED (0) means "no key" — report it as unmapped.
43 return value === undefined || value === GhosttyKey.GHOSTTY_KEY_UNIDENTIFIED ? null : value;
44}
45
46export interface ModState {
47 ctrlKey: boolean;
48 altKey: boolean;
49 metaKey: boolean;
50 shiftKey: boolean;
51}
52
53export function domEventMods(event: ModState): number {
54 let mods = 0;
55 if (event.shiftKey) mods |= MODS_SHIFT;
56 if (event.ctrlKey) mods |= MODS_CTRL;
57 if (event.altKey) mods |= MODS_ALT;
58 if (event.metaKey) mods |= MODS_SUPER;
59 return mods;
60}
packages/terminal/src/links.ts created+102
......@@ -0,0 +1,102 @@
1// Link detection over a row's text. Pure: no DOM, no wasm, no workspace
2// knowledge. The renderer turns the returned ranges into spans; the embedder
3// decides what a path means (see `validate`) and what activating one does.
4//
5// Regex detection produces URLs and file paths with the `:line:col` suffix
6// emitted by compilers, linters, and test runners. The renderer adds canonical
7// OSC 8 identity from cells without asking this text-only detector to infer it.
8
9export type LinkKind = "url" | "path" | "osc8";
10
11export interface DetectedLink {
12 kind: LinkKind;
13 /** Offsets into the row text (JS string indices, not columns). */
14 start: number;
15 end: number;
16 /** Visible link text; regex matches have trailing punctuation removed. */
17 text: string;
18 /** Path without its position suffix, visible URL, or exact OSC 8 URI. */
19 target: string;
20 line: number | null;
21 column: number | null;
22}
23
24export interface DetectLinksOptions {
25 /**
26 * Decide whether a path-shaped match is really a link — the workbench passes
27 * a workspace probe. ***Required for path detection: without it only URLs are
28 * returned.*** Path shape alone is not evidence (`e.g.` and `1.2.3` match the
29 * same grammar as `a/b.ts`), and a terminal that underlines prose is worse
30 * than one that underlines nothing.
31 */
32 validate?: (target: string) => boolean;
33 /** Cap on matches per row — a pathological line should not stall a paint. */
34 limit?: number;
35}
36
37const URL_RE = /\b(?:https?|file|ftp):\/\/[^\s<>"'`]+/gi;
38
39const PATH_RE =
40 /(?:~\/|\.{1,2}[\\/]|[A-Za-z]:[\\/]|\\\\|\/)[^\s"'`<>]+|(?:[\w.@+-]+[\\/])+[\w.@+-]+(?::\d+){0,2}|[\w.@+-]+\.[A-Za-z]\w{0,9}(?::\d+){0,2}/g;
41
42/** Characters a URL or path may end with in prose but that are not part of it. */
43const TRAILING = /[.,:;!?)\]}'"]+$/;
44
45const DEFAULT_LIMIT = 64;
46
47export function detectLinks(text: string, options: DetectLinksOptions = {}): DetectedLink[] {
48 const limit = options.limit ?? DEFAULT_LIMIT;
49 const links: DetectedLink[] = [];
50 const taken: Array<[number, number]> = [];
51
52 const overlaps = (start: number, end: number) => taken.some(([s, e]) => start < e && end > s);
53
54 URL_RE.lastIndex = 0;
55 for (let m = URL_RE.exec(text); m !== null; m = URL_RE.exec(text)) {
56 if (links.length >= limit) break;
57 const raw = m[0];
58 const trimmed = raw.replace(TRAILING, "");
59 if (trimmed.length === 0) continue;
60 const start = m.index;
61 const end = start + trimmed.length;
62 taken.push([start, end]);
63 links.push({
64 kind: "url",
65 start,
66 end,
67 text: trimmed,
68 target: trimmed,
69 line: null,
70 column: null,
71 });
72 }
73
74 const validate = options.validate;
75 if (validate === undefined) return links;
76
77 PATH_RE.lastIndex = 0;
78 for (let m = PATH_RE.exec(text); m !== null; m = PATH_RE.exec(text)) {
79 if (links.length >= limit) break;
80 let raw = m[0];
81 // Strip a trailing position-less punctuation tail, then re-parse position:
82 // `src/a.ts:12:3.` -> `src/a.ts:12:3`.
83 raw = raw.replace(TRAILING, "");
84 if (raw.length === 0) continue;
85 const start = m.index;
86 const end = start + raw.length;
87 if (overlaps(start, end)) continue;
88
89 const position = /^(.*?):(\d+)(?::(\d+))?$/.exec(raw);
90 const target = position ? position[1]! : raw;
91 const line = position ? Number(position[2]) : null;
92 const column = position?.[3] !== undefined ? Number(position[3]) : null;
93 if (target.length === 0) continue;
94 if (!validate(target)) continue;
95
96 taken.push([start, end]);
97 links.push({ kind: "path", start, end, text: raw, target, line, column });
98 }
99
100 links.sort((a, b) => a.start - b.start);
101 return links;
102}
packages/terminal/src/mount.ts created+109
......@@ -0,0 +1,109 @@
1import type { GhosttyModule } from "./ghostty/module.ts";
2import { Terminal, type TerminalEvents, type TerminalOptions } from "./ghostty/terminal.ts";
3import { TerminalView, type TerminalViewOptions } from "./renderer/dom.ts";
4
5export interface MountTerminalOptions
6 extends
7 Partial<TerminalOptions>,
8 Omit<TerminalEvents, "onOutput">,
9 Omit<TerminalViewOptions, "onData"> {
10 /** Keystrokes, paste, mouse reports, and terminal query responses. Forward to the pty. */
11 onData?: (bytes: Uint8Array) => void;
12 /** Called after a fitted container changes the terminal's cell geometry. */
13 onResize?: (size: { cols: number; rows: number }) => void;
14}
15
16export interface TerminalHandle {
17 readonly terminal: Terminal;
18 readonly view: TerminalView;
19 /** Initial font loading, measurement, fit, and resize reporting have settled. */
20 readonly ready: Promise<void>;
21 /** Feed pty output into the emulator and schedule its DOM frame. */
22 write(data: string | Uint8Array): void;
23 /** Release the renderer lease and every native terminal resource. Idempotent. */
24 dispose(): void;
25}
26
27/**
28 * Mount the complete DOM terminal. Transport stays caller-owned: pass pty output
29 * to `write` and forward `onData` bytes back to the pty.
30 */
31export function mountTerminal(
32 container: HTMLElement,
33 module: GhosttyModule,
34 options: MountTerminalOptions = {},
35): TerminalHandle {
36 const terminal = new Terminal(
37 module,
38 {
39 cols: options.cols ?? 80,
40 rows: options.rows ?? 24,
41 ...(options.maxScrollbackBytes !== undefined
42 ? { maxScrollbackBytes: options.maxScrollbackBytes }
43 : {}),
44 ...(options.shellIntegration !== undefined
45 ? { shellIntegration: options.shellIntegration }
46 : {}),
47 ...(options.commandLimit !== undefined ? { commandLimit: options.commandLimit } : {}),
48 },
49 {
50 onOutput: (bytes) => options.onData?.(bytes),
51 onBell: () => options.onBell?.(),
52 onTitleChanged: (title) => options.onTitleChanged?.(title),
53 onPwdChanged: (pwd) => options.onPwdChanged?.(pwd),
54 onShellMark: (mark) => options.onShellMark?.(mark),
55 onCommandStart: (command) => options.onCommandStart?.(command),
56 onCommandEnd: (command) => options.onCommandEnd?.(command),
57 onClipboardWrite: (write) => options.onClipboardWrite?.(write),
58 },
59 );
60 let view: TerminalView | null = null;
61 try {
62 view = new TerminalView(container, terminal, {
63 onData: (bytes) => options.onData?.(bytes),
64 ...(options.onBeforeKey ? { onBeforeKey: options.onBeforeKey } : {}),
65 ...(options.visible !== undefined ? { visible: options.visible } : {}),
66 ...(options.scrollback !== undefined ? { scrollback: options.scrollback } : {}),
67 ...(options.scrollbar !== undefined ? { scrollbar: options.scrollbar } : {}),
68 ...(options.links !== undefined ? { links: options.links } : {}),
69 ...(options.mouse !== undefined ? { mouse: options.mouse } : {}),
70 ...(options.stickyScroll !== undefined ? { stickyScroll: options.stickyScroll } : {}),
71 ...(options.predict !== undefined ? { predict: options.predict } : {}),
72 });
73 view.observeResize((size) => options.onResize?.(size));
74 } catch (error) {
75 view?.dispose();
76 terminal.free();
77 throw error;
78 }
79
80 const mountedView = view;
81 let disposed = false;
82 return {
83 terminal,
84 view: mountedView,
85 ready: mountedView.ready,
86 write: (data) => {
87 let failed = false;
88 let writeError: unknown;
89 try {
90 terminal.write(data);
91 } catch (error) {
92 failed = true;
93 writeError = error;
94 }
95 try {
96 if (!disposed) mountedView.scheduleFrame();
97 } catch (error) {
98 if (!failed) throw error;
99 }
100 if (failed) throw writeError;
101 },
102 dispose: () => {
103 if (disposed) return;
104 disposed = true;
105 mountedView.dispose();
106 terminal.free();
107 },
108 };
109}
packages/terminal/src/predict/engine.ts created+385
......@@ -0,0 +1,385 @@
1// Predictive local echo: speculative glyphs painted at the moment a key is
2// pressed, before the pty has echoed anything back.
3//
4// The idea is mosh's, but the soundness gate is not. Mosh runs against a bare
5// byte stream and has to *guess* whether the far end is in a line-editing mode
6// that will echo what you typed; it guesses wrong in full-screen applications
7// and pays for it with visible retractions. We have shell integration, so we
8// can prove it instead: OSC 133/633 tells us a prompt has started accepting
9// input (`B`) and has not yet executed (`C`), which is exactly the readline
10// region where a typed character echoes at the cursor and nowhere else.
11//
12// Everything outside that proof is refused rather than guessed. The engine
13// predicts single-column printable ASCII and backspaces over its own earlier
14// predictions; wide characters, combining marks, wraps, control keys, and paste
15// all drop the whole speculation instead of extending it. That is a deliberate
16// scope: a wrong prediction is worse than a late one, and JavaScript never
17// re-segments cells (Ghostty owns grapheme boundaries and width).
18//
19// Pure by construction: no DOM, no wasm, no clock. Time arrives as a `now`
20// argument and screen state as plain data, so the whole thing is property
21// testable without a terminal.
22
23/** `off` disables prediction entirely; `prompt` predicts only inside a proven readline region. */
24export type PredictionMode = "off" | "prompt";
25
26export interface PredictOptions {
27 mode?: PredictionMode;
28 /**
29 * Show predictions only once the measured round trip reaches this. On a fast
30 * link the echo beats the next frame anyway, so predicting only adds a chance
31 * to be wrong. 0 displays from the first keystroke.
32 */
33 minLatencyMs?: number;
34 /** Drop the whole speculation when a prediction goes this long unconfirmed. */
35 timeoutMs?: number;
36 /** Underline unconfirmed predictions so speculation is legible as speculation. */
37 underline?: boolean;
38 /** Hard cap on outstanding predictions; exceeding it drops the speculation. */
39 maxPredictions?: number;
40}
41
42export interface ResolvedPredictOptions {
43 readonly mode: PredictionMode;
44 readonly minLatencyMs: number;
45 readonly timeoutMs: number;
46 readonly underline: boolean;
47 readonly maxPredictions: number;
48}
49
50export const DEFAULT_PREDICT_OPTIONS: ResolvedPredictOptions = {
51 mode: "off",
52 minLatencyMs: 25,
53 timeoutMs: 1000,
54 underline: true,
55 maxPredictions: 128,
56};
57
58const positive = (value: number | undefined, fallback: number): number =>
59 value === undefined || !Number.isFinite(value) || value < 0 ? fallback : value;
60
61export function resolvePredictOptions(
62 options?: PredictOptions | PredictionMode | boolean,
63): ResolvedPredictOptions {
64 if (options === undefined) return DEFAULT_PREDICT_OPTIONS;
65 if (typeof options === "boolean") {
66 return { ...DEFAULT_PREDICT_OPTIONS, mode: options ? "prompt" : "off" };
67 }
68 if (typeof options === "string") return { ...DEFAULT_PREDICT_OPTIONS, mode: options };
69 return {
70 mode: options.mode ?? DEFAULT_PREDICT_OPTIONS.mode,
71 minLatencyMs: positive(options.minLatencyMs, DEFAULT_PREDICT_OPTIONS.minLatencyMs),
72 timeoutMs: positive(options.timeoutMs, DEFAULT_PREDICT_OPTIONS.timeoutMs) || Infinity,
73 underline: options.underline ?? DEFAULT_PREDICT_OPTIONS.underline,
74 maxPredictions:
75 Math.floor(positive(options.maxPredictions, DEFAULT_PREDICT_OPTIONS.maxPredictions)) || 1,
76 };
77}
78
79/** Where a keystroke would land, as the renderer sees it at press time. */
80export interface PredictionSite {
81 /** Absolute screen row of the cursor (scrollback included). */
82 row: number;
83 /** Cursor column. */
84 col: number;
85 /** Terminal width, for the wrap refusal. */
86 cols: number;
87 /**
88 * Proof we are inside the shell's line-editing region: OSC 133/633 `B` has
89 * arrived and `C` has not. False for every uninstrumented shell and every
90 * full-screen application, which is precisely the point.
91 */
92 editing: boolean;
93}
94
95/** The emulator's real state, narrowed to what reconciliation needs. */
96export interface ReconcileFrame {
97 /** Absolute row -> the canonical per-column text the emulator now shows. */
98 rows: ReadonlyMap<number, readonly string[]>;
99 /** Absolute cursor position, or null when the cursor is hidden. */
100 cursor: { row: number; col: number } | null;
101 /** Still inside the line-editing region. */
102 editing: boolean;
103}
104
105/** One speculative single-column glyph. */
106interface Prediction {
107 row: number;
108 col: number;
109 text: string;
110 sentAt: number;
111}
112
113/** True only for characters we can place in exactly one column without re-segmenting. */
114export function isPredictableText(text: string): boolean {
115 if (text.length !== 1) return false;
116 const code = text.charCodeAt(0);
117 return code >= 0x20 && code <= 0x7e;
118}
119
120const BLANK = " ";
121
122/**
123 * One cell's glyph, with Ghostty's empty cell and a written space treated as
124 * the same thing. Without this, typing a space could never confirm — the core
125 * reports a blank cell as `""` whether or not a space was written into it — and
126 * every space would eventually retract the whole line.
127 */
128const glyph = (text: string): string => (text === "" ? BLANK : text);
129
130/** Smoothing factor from RFC 6298 / mosh: a new sample moves the estimate by an eighth. */
131const SRTT_ALPHA = 1 / 8;
132const MAX_RTT_SAMPLE_MS = 10_000;
133/** Never call a prediction wrong before the echo has had a plausible chance to arrive. */
134const MIN_GRACE_MS = 50;
135
136export class PredictionEngine {
137 private readonly options: ResolvedPredictOptions;
138 private list: Prediction[] = [];
139 private cursorNow: { row: number; col: number } | null = null;
140 private srtt: number | null = null;
141
142 constructor(options: ResolvedPredictOptions = DEFAULT_PREDICT_OPTIONS) {
143 this.options = options;
144 }
145
146 get enabled(): boolean {
147 return this.options.mode !== "off";
148 }
149
150 /** Smoothed round trip in milliseconds, or null before the first confirmation. */
151 get latencyMs(): number | null {
152 return this.srtt;
153 }
154
155 get underline(): boolean {
156 return this.options.underline;
157 }
158
159 /**
160 * Whether predictions should be painted at all. Deliberately false until a
161 * round trip has been measured: the first keystrokes of a session pay for the
162 * measurement, and on a fast link nothing is ever shown.
163 */
164 get displaying(): boolean {
165 if (!this.enabled) return false;
166 if (this.options.minLatencyMs <= 0) return true;
167 return this.srtt !== null && this.srtt >= this.options.minLatencyMs;
168 }
169
170 /** The speculative cursor position, or null when nothing is outstanding. */
171 get cursor(): { row: number; col: number } | null {
172 if (!this.displaying || this.list.length === 0 || this.cursorNow === null) return null;
173 return { ...this.cursorNow };
174 }
175
176 /** Columns to superimpose on `row`, or undefined when that row carries no overlay. */
177 overlayFor(row: number): ReadonlyMap<number, string> | undefined {
178 if (!this.displaying) return undefined;
179 let cells: Map<number, string> | undefined;
180 for (const prediction of this.list) {
181 if (prediction.row !== row) continue;
182 cells ??= new Map();
183 cells.set(prediction.col, prediction.text);
184 }
185 return cells;
186 }
187
188 /** Every row currently carrying an overlay. */
189 overlaidRows(): number[] {
190 if (!this.displaying) return [];
191 return [...new Set(this.list.map((prediction) => prediction.row))];
192 }
193
194 /** Record locally typed text. Returns the rows the renderer must repaint. */
195 type(text: string, site: PredictionSite, now: number): readonly number[] {
196 if (!this.enabled) return [];
197 const before = this.snapshot();
198 this.applyType(text, site, now);
199 return this.changedRows(before);
200 }
201
202 /** Record a backspace. Returns the rows the renderer must repaint. */
203 backspace(site: PredictionSite, now: number): readonly number[] {
204 if (!this.enabled) return [];
205 const before = this.snapshot();
206 this.applyBackspace(site, now);
207 return this.changedRows(before);
208 }
209
210 /**
211 * Any input we refuse to model — Enter, arrows, chords, paste, mouse. The
212 * outstanding speculation is dropped rather than left to rot: whatever the
213 * program does next, our glyphs are no longer a claim we can defend.
214 */
215 interrupt(): readonly number[] {
216 if (!this.enabled) return [];
217 const before = this.snapshot();
218 this.clear();
219 return this.changedRows(before);
220 }
221
222 /** Drop everything, including the latency estimate. For resize and disposal. */
223 reset(): readonly number[] {
224 const before = this.snapshot();
225 this.clear();
226 this.srtt = null;
227 return this.changedRows(before);
228 }
229
230 /**
231 * Reconcile against real emulator state. Confirmed predictions are retired
232 * (the row now genuinely shows them) and any contradiction drops the entire
233 * speculation, not just the offending glyph — a wrong prediction means our
234 * model of the far end was wrong, so every prediction built on it is suspect.
235 */
236 reconcile(frame: ReconcileFrame, now: number): readonly number[] {
237 if (!this.enabled || this.list.length === 0) return [];
238 const before = this.snapshot();
239 this.applyReconcile(frame, now);
240 return this.changedRows(before);
241 }
242
243 private applyType(text: string, site: PredictionSite, now: number): void {
244 if (!site.editing || !isPredictableText(text)) {
245 this.clear();
246 return;
247 }
248 const base = this.cursorNow ?? { row: site.row, col: site.col };
249 // A wrap changes row identity and can reflow; we cannot place a glyph past
250 // the last column without claiming to know how the core would wrap it.
251 if (base.col >= site.cols - 1 || base.col < 0) {
252 this.clear();
253 return;
254 }
255 if (this.list.length >= this.options.maxPredictions) {
256 this.clear();
257 return;
258 }
259 this.list.push({ row: base.row, col: base.col, text, sentAt: now });
260 this.cursorNow = { row: base.row, col: base.col + 1 };
261 }
262
263 private applyBackspace(site: PredictionSite, _now: number): void {
264 // We only ever un-type our own speculation. Backspacing over real content
265 // would be a claim about what the shell's line editor does with it, and at
266 // the left edge it is a claim about the previous row.
267 const cursor = this.cursorNow;
268 if (!site.editing || cursor === null || this.list.length === 0) {
269 this.clear();
270 return;
271 }
272 const last = this.list[this.list.length - 1]!;
273 if (last.row !== cursor.row || last.col !== cursor.col - 1) {
274 this.clear();
275 return;
276 }
277 this.list.pop();
278 this.cursorNow = { row: last.row, col: last.col };
279 if (this.list.length === 0) this.cursorNow = null;
280 }
281
282 private applyReconcile(frame: ReconcileFrame, now: number): void {
283 // Leaving the readline region invalidates the premise the predictions rest
284 // on, whatever the screen happens to show.
285 if (!frame.editing || frame.cursor === null) {
286 this.clear();
287 return;
288 }
289 const kept: Prediction[] = [];
290 for (const prediction of this.list) {
291 const cells = frame.rows.get(prediction.row);
292 const actual = glyph(cells?.[prediction.col] ?? "");
293 const expected = glyph(prediction.text);
294 if (cells !== undefined && actual === expected) {
295 // A blank column matching a predicted space is already visually true,
296 // but it is not evidence that a round trip completed, so it must not
297 // pull the latency estimate down.
298 if (actual !== BLANK) this.sample(now - prediction.sentAt);
299 continue;
300 }
301 if (now - prediction.sentAt >= this.options.timeoutMs) {
302 this.clear();
303 return;
304 }
305 if (cells !== undefined) {
306 // Some other glyph already occupies the column. The echo did not fail
307 // to arrive — it arrived as something else, and no amount of waiting
308 // makes our claim true again.
309 if (actual !== BLANK) {
310 this.clear();
311 return;
312 }
313 // Still blank past the point the echo should have landed.
314 if (now - prediction.sentAt >= this.graceMs()) {
315 this.clear();
316 return;
317 }
318 }
319 kept.push(prediction);
320 }
321 // A confirmation retires the oldest predictions first; anything still
322 // outstanding must remain a contiguous tail, or our chain has a hole in it.
323 if (kept.length > 0 && kept.length !== this.list.length) {
324 const head = this.list.length - kept.length;
325 if (this.list.slice(head).some((prediction, index) => prediction !== kept[index])) {
326 this.clear();
327 return;
328 }
329 }
330 // The real cursor must still be on the row we are predicting into. If it
331 // moved, the program did something we did not model.
332 if (kept.length > 0 && frame.cursor.row !== kept[0]!.row) {
333 this.clear();
334 return;
335 }
336 this.list = kept;
337 if (kept.length === 0) this.cursorNow = null;
338 }
339
340 /**
341 * How long an unconfirmed prediction stays innocent: about one round trip,
342 * floored so a fast link cannot retract an echo that is merely in flight.
343 * Before the first measurement the floor is all we have.
344 */
345 private graceMs(): number {
346 const estimate = this.srtt === null ? MIN_GRACE_MS : this.srtt * 2;
347 return Math.max(MIN_GRACE_MS, Math.min(estimate, this.options.timeoutMs));
348 }
349
350 private sample(rtt: number): void {
351 if (!Number.isFinite(rtt) || rtt < 0) return;
352 const bounded = Math.min(rtt, MAX_RTT_SAMPLE_MS);
353 this.srtt = this.srtt === null ? bounded : this.srtt + (bounded - this.srtt) * SRTT_ALPHA;
354 }
355
356 private clear(): void {
357 this.list = [];
358 this.cursorNow = null;
359 }
360
361 /** Overlay identity as a comparable string, for computing the repaint set. */
362 private snapshot(): Map<number, string> {
363 const rows = new Map<number, string>();
364 if (!this.displaying) return rows;
365 for (const prediction of this.list) {
366 rows.set(
367 prediction.row,
368 `${rows.get(prediction.row) ?? ""}|${prediction.col}:${prediction.text}`,
369 );
370 }
371 if (this.cursorNow !== null && this.list.length > 0) {
372 const row = this.cursorNow.row;
373 rows.set(row, `${rows.get(row) ?? ""}#${this.cursorNow.col}`);
374 }
375 return rows;
376 }
377
378 private changedRows(before: Map<number, string>): readonly number[] {
379 const after = this.snapshot();
380 const changed = new Set<number>();
381 for (const [row, value] of before) if (after.get(row) !== value) changed.add(row);
382 for (const [row, value] of after) if (before.get(row) !== value) changed.add(row);
383 return [...changed];
384 }
385}
packages/terminal/src/predict/frame.ts created+28
......@@ -0,0 +1,28 @@
1// Adapting canonical row snapshots into the flat, column-indexed shape the
2// prediction engine reconciles against.
3//
4// This exists because `RowSnapshot.cells` is not column-indexed: spacer tails
5// are omitted, so a wide glyph advances two columns from one array entry, and
6// the canonical default tail is trimmed away entirely. Predictions are stated
7// in columns, so the translation has to happen somewhere — and it belongs here,
8// tested once, rather than inline at the reconciliation site where an off-by-one
9// would silently read the wrong cell and retract a correct prediction.
10
11import type { CellSnapshot } from "../ghostty/terminal.ts";
12
13/**
14 * Column-indexed text for one row. Empty and trimmed columns read as `""`,
15 * which is also what a prediction compares against when its glyph has not
16 * arrived yet.
17 */
18export function columnTexts(cells: readonly CellSnapshot[], cols: number): string[] {
19 const texts = Array.from<string>({ length: Math.max(0, cols) }).fill("");
20 let column = 0;
21 for (const cell of cells) {
22 if (column >= texts.length) break;
23 // A spacer head owns its column but carries no grapheme of its own.
24 texts[column] = cell.spacer === "head" ? "" : cell.text;
25 column += Math.max(1, cell.width);
26 }
27 return texts;
28}
packages/terminal/src/predict/index.ts created+12
......@@ -0,0 +1,12 @@
1export {
2 DEFAULT_PREDICT_OPTIONS,
3 isPredictableText,
4 PredictionEngine,
5 resolvePredictOptions,
6 type PredictionMode,
7 type PredictionSite,
8 type PredictOptions,
9 type ReconcileFrame,
10 type ResolvedPredictOptions,
11} from "./engine.ts";
12export { columnTexts } from "./frame.ts";
packages/terminal/src/react.tsx created+81
......@@ -0,0 +1,81 @@
1// Thin React binding over Terminal + TerminalView. Behavior lives in the
2// headless modules; this file only manages lifecycle.
3
4import { useEffect, useRef, type CSSProperties, type HTMLAttributes } from "react";
5
6import { ansiToHtml } from "./ansi.ts";
7import type { GhosttyModule } from "./ghostty/module.ts";
8import { mountTerminal, type MountTerminalOptions, type TerminalHandle } from "./mount.ts";
9
10export interface AnsiTextProps extends Omit<
11 HTMLAttributes<HTMLSpanElement>,
12 "children" | "dangerouslySetInnerHTML"
13> {
14 text: string;
15}
16
17/** Static ANSI text with no terminal viewport, input, or scroll behavior. */
18export function AnsiText({ text, ...props }: AnsiTextProps) {
19 return <span {...props} dangerouslySetInnerHTML={{ __html: ansiToHtml(text) }} />;
20}
21
22export interface TerminalPaneProps extends MountTerminalOptions {
23 module: GhosttyModule;
24 /** @deprecated Use `scrollbar`; retained for existing consumers. */
25 initialScrollbar?: boolean;
26 /** Receives the mounted handle and may return caller cleanup for unmount/remount. */
27 onReady?: (handle: TerminalHandle) => void | (() => void);
28 className?: string;
29 style?: CSSProperties;
30}
31
32export function TerminalPane(props: TerminalPaneProps) {
33 const hostRef = useRef<HTMLDivElement | null>(null);
34 const propsRef = useRef(props);
35 propsRef.current = props;
36
37 useEffect(() => {
38 const host = hostRef.current;
39 if (host === null) return;
40 const p = propsRef.current;
41 const handle = mountTerminal(host, p.module, {
42 ...p,
43 onData: (bytes) => propsRef.current.onData?.(bytes),
44 onBeforeKey: (event) => propsRef.current.onBeforeKey?.(event) ?? true,
45 onResize: (size) => propsRef.current.onResize?.(size),
46 onTitleChanged: (title) => propsRef.current.onTitleChanged?.(title),
47 onPwdChanged: (pwd) => propsRef.current.onPwdChanged?.(pwd),
48 onBell: () => propsRef.current.onBell?.(),
49 onShellMark: (mark) => propsRef.current.onShellMark?.(mark),
50 onCommandStart: (command) => propsRef.current.onCommandStart?.(command),
51 onCommandEnd: (command) => propsRef.current.onCommandEnd?.(command),
52 onClipboardWrite: (write) => propsRef.current.onClipboardWrite?.(write),
53 ...(p.scrollbar !== undefined || p.initialScrollbar !== undefined
54 ? { scrollbar: p.scrollbar ?? p.initialScrollbar }
55 : {}),
56 });
57 let cleanup: void | (() => void);
58 try {
59 cleanup = p.onReady?.(handle);
60 } catch (error) {
61 handle.dispose();
62 throw error;
63 }
64 return () => {
65 try {
66 cleanup?.();
67 } finally {
68 handle.dispose();
69 }
70 };
71 }, [props.module]);
72
73 return (
74 <div
75 className={`clo-term-mount${props.className === undefined ? "" : ` ${props.className}`}`}
76 style={props.style}
77 >
78 <div ref={hostRef} style={{ width: "100%", height: "100%" }} />
79 </div>
80 );
81}
packages/terminal/src/renderer/dom.ts created+2120
......@@ -0,0 +1,2120 @@
1// DOM renderer: dirty rows repaint as span runs, once per animation frame.
2// Deliberately DOM (not canvas/GL): terminal output keeps native selection,
3// browser find, and accessibility; throughput lands on the wasm parser, which
4// absorbs bytes at full speed while this layer paints at most once per frame.
5
6import type {
7 CellSnapshot,
8 FrameReader,
9 FrameSnapshot,
10 RowSnapshot,
11 Scrollbar,
12 Terminal,
13} from "../ghostty/terminal.ts";
14import { detectLinks, type DetectedLink, type DetectLinksOptions } from "../links.ts";
15import {
16 isPredictableText,
17 PredictionEngine,
18 resolvePredictOptions,
19 type PredictionMode,
20 type PredictionSite,
21 type PredictOptions,
22} from "../predict/engine.ts";
23import { columnTexts } from "../predict/frame.ts";
24import { cellsToRuns, cellText, linkCells, rowText } from "../runs.ts";
25import { stickyCommand, stickyTarget } from "../shell/sticky.ts";
26
27type GlyphFitMode = "wide" | "symbol" | "powerline";
28
29interface FittedGlyph {
30 element: HTMLElement;
31 key: string;
32 cache: boolean;
33 mode: GlyphFitMode;
34 target: number;
35}
36
37interface GlyphFit {
38 scaleX: number;
39 scaleY: number;
40 marginLeft: number;
41}
42
43const GLYPH_FIT_CACHE_LIMIT = 512;
44const GLYPH_FIT_CACHE_TEXT_LIMIT = 64;
45// Matches terminal.css: 10px track + 3px right inset.
46const SCROLLBAR_GUTTER = 13;
47const FONT_LOADING_CLASS = "clo-term-font-loading";
48const FONT_ERROR_EVENT = "clo-terminal-font-error";
49const SYNCHRONIZED_OUTPUT_TIMEOUT_MS = 1000;
50const SYMBOL_FONT_FAMILY = "__clo_terminal_symbols_3_4_0";
51const ROOT_CLASSES = [
52 "clo-term",
53 "clo-term-has-scrollbar",
54 "clo-term-has-blink",
55 FONT_LOADING_CLASS,
56] as const;
57const ROOT_STYLE_PROPERTIES = [
58 "--clo-term-effective-fg",
59 "--clo-term-effective-bg",
60 "--clo-term-effective-cursor",
61] as const;
62/** The palette's first 16 slots, in index order: the eight hues, then the same eight bright. */
63const ANSI_HUES = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"];
64const ANSI_TOKENS = [
65 ...ANSI_HUES.map((hue) => `--clo-term-ansi-${hue}`),
66 ...ANSI_HUES.map((hue) => `--clo-term-ansi-bright-${hue}`),
67];
68const PRIMARY_FONT_SAMPLE = "W";
69const SYMBOL_FONT_SAMPLE = "\uE0A3";
70const POWERLINE_SPRITES = new Map<number, string>([
71 [0xe0b0, "right-arrow"],
72 [0xe0b2, "left-arrow"],
73 [0xe0b4, "right-round"],
74 [0xe0b6, "left-round"],
75 [0xe0b8, "lower-left"],
76 [0xe0ba, "lower-right"],
77 [0xe0bc, "upper-left"],
78 [0xe0be, "upper-right"],
79]);
80
81function privateUseBaseCodepoint(text: string): number | null {
82 const codepoint = text.codePointAt(0);
83 if (codepoint === undefined) return null;
84 return (codepoint >= 0xe000 && codepoint <= 0xf8ff) ||
85 (codepoint >= 0xf0000 && codepoint <= 0xffffd) ||
86 (codepoint >= 0x100000 && codepoint <= 0x10fffd)
87 ? codepoint
88 : null;
89}
90
91function applyGlyphFit(element: HTMLElement, fit: GlyphFit): void {
92 element.style.marginLeft = `${fit.marginLeft}px`;
93 element.style.transform = `scale(${fit.scaleX}, ${fit.scaleY})`;
94}
95
96interface FontCompletionEvent extends Event {
97 readonly fontfaces: readonly FontFace[];
98}
99
100interface FontCompletionSource {
101 addEventListener(type: "loadingdone", listener: EventListener): void;
102 removeEventListener(type: "loadingdone", listener: EventListener): void;
103}
104
105function parseFontFamilies(value: string): string[] {
106 const families: string[] = [];
107 let start = 0;
108 let quote = "";
109 let escaped = false;
110 for (let index = 0; index < value.length; index++) {
111 const char = value[index]!;
112 if (escaped) escaped = false;
113 else if (char === "\\") escaped = true;
114 else if (quote !== "") {
115 if (char === quote) quote = "";
116 } else if (char === '"' || char === "'") quote = char;
117 else if (char === ",") {
118 families.push(value.slice(start, index).trim());
119 start = index + 1;
120 }
121 }
122 families.push(value.slice(start).trim());
123 return families.filter((family) => family !== "");
124}
125
126function normalizeFontFamily(value: string): string {
127 const family = value.trim();
128 const quote = family[0];
129 const unquoted =
130 family.length >= 2 && (quote === '"' || quote === "'") && family.at(-1) === quote
131 ? family.slice(1, -1)
132 : family;
133 return unquoted.replace(/\\(.)/g, "$1").toLowerCase();
134}
135
136function sameCells(left: readonly CellSnapshot[] | undefined, right: readonly CellSnapshot[]) {
137 return (
138 left !== undefined &&
139 left.length === right.length &&
140 left.every((cell, index) => {
141 const other = right[index]!;
142 return (
143 cell.text === other.text &&
144 cell.width === other.width &&
145 cell.spacer === other.spacer &&
146 cell.protected === other.protected &&
147 cell.semantic === other.semantic &&
148 cell.hasOsc8 === other.hasOsc8 &&
149 cell.osc8Uri === other.osc8Uri &&
150 cell.fg === other.fg &&
151 cell.bg === other.bg &&
152 cell.underlineColor === other.underlineColor &&
153 cell.bold === other.bold &&
154 cell.italic === other.italic &&
155 cell.faint === other.faint &&
156 cell.blink === other.blink &&
157 cell.inverse === other.inverse &&
158 cell.invisible === other.invisible &&
159 cell.strikethrough === other.strikethrough &&
160 cell.overline === other.overline &&
161 cell.underline === other.underline
162 );
163 })
164 );
165}
166
167function sameRowMetadata(left: RowSnapshot | undefined, right: RowSnapshot): boolean {
168 return (
169 left !== undefined &&
170 left.wrap === right.wrap &&
171 left.wrapContinuation === right.wrapContinuation &&
172 left.semanticPrompt === right.semanticPrompt &&
173 left.hyperlink === right.hyperlink
174 );
175}
176
177export interface TerminalLinkOptions extends DetectLinksOptions {
178 /**
179 * Called on click (or ctrl/cmd-click, per `requireModifier`). Without it,
180 * links are detected and styled but inert — the embedder decides what a
181 * path, URL, or OSC 8 URI means.
182 */
183 onActivate?: (link: DetectedLink, event: MouseEvent) => void;
184 /** Require ctrl/cmd like an editor, rather than plain click. Default false. */
185 requireModifier?: boolean;
186}
187
188export interface TerminalViewOptions {
189 /** Let the host claim application shortcuts before VT encoding. */
190 onBeforeKey?: (event: KeyboardEvent) => boolean;
191 /** Hidden terminals keep their grid and dirty rows until shown. */
192 visible?: boolean;
193 /** All terminal-bound input: keystrokes, paste, and query responses. */
194 onData?: (bytes: Uint8Array) => void;
195 /** Wheel scrollback is enabled unless the alternate screen has the viewport. */
196 scrollback?: boolean;
197 /** Render the terminal-owned scrollbar and reserve its gutter. Default true. */
198 scrollbar?: boolean;
199 /** Render OSC 8 links and detect URLs/`path:line:col`. Off when omitted. */
200 links?: TerminalLinkOptions;
201 /**
202 * Forward mouse events to programs that ask for them (vim, htop, lazygit).
203 * Only active while the program has tracking on, so ordinary output keeps
204 * native selection. Default true.
205 */
206 mouse?: boolean;
207 /**
208 * Pin the running command's line at the top while scrolled inside its
209 * output. Needs shell integration to have anything to pin; inert otherwise.
210 * Default true.
211 */
212 stickyScroll?: boolean;
213 /**
214 * Paint typed characters speculatively, before the pty echoes them, to hide
215 * round-trip latency on a remote host. Off by default, and inert without
216 * shell integration: prediction only runs inside a proven readline region.
217 * See [`../predict/engine.ts`](../predict/engine.ts).
218 */
219 predict?: PredictOptions | PredictionMode | boolean;
220}
221
222const num = (v: number) => `#${v.toString(16).padStart(6, "0")}`;
223
224/**
225 * A row as it would look with speculative glyphs applied, plus the indices of
226 * the cells that are speculative. Only overlaid columns allocate a derived
227 * cell; every other entry stays the canonical reference the run grouper expects.
228 *
229 * The row is extended with blank cells when a prediction lands past the
230 * canonical default tail, which is the normal case: you type into empty space.
231 */
232function applyOverlay(
233 row: RowSnapshot,
234 overlay: ReadonlyMap<number, string>,
235): { row: RowSnapshot; predicted: Set<number> } {
236 const cells = [...row.cells];
237 const predicted = new Set<number>();
238 // Cell indices are not columns — wide glyphs advance two — so walk the row
239 // once and record where each column's cell lives.
240 const cellAtColumn = new Map<number, number>();
241 let column = 0;
242 for (let index = 0; index < cells.length; index++) {
243 cellAtColumn.set(column, index);
244 column += Math.max(1, cells[index]!.width);
245 }
246 const blank = (): CellSnapshot => ({
247 text: "",
248 width: 1,
249 spacer: "none",
250 protected: false,
251 semantic: "input",
252 hasOsc8: false,
253 osc8Uri: null,
254 fg: -1,
255 bg: -1,
256 underlineColor: -1,
257 bold: false,
258 italic: false,
259 faint: false,
260 blink: false,
261 inverse: false,
262 invisible: false,
263 strikethrough: false,
264 overline: false,
265 underline: 0,
266 });
267 for (const [col, text] of [...overlay].sort((a, b) => a[0] - b[0])) {
268 let index = cellAtColumn.get(col);
269 if (index === undefined) {
270 // Past the trimmed tail: pad with blanks up to the predicted column.
271 while (column <= col) {
272 cellAtColumn.set(column, cells.length);
273 cells.push(blank());
274 column++;
275 }
276 index = cellAtColumn.get(col);
277 if (index === undefined) continue;
278 }
279 const base = cells[index]!;
280 // Inherit the canonical cell's presentation so a prediction adopts the
281 // prompt's colors rather than inventing its own.
282 cells[index] = { ...base, text, width: 1, spacer: "none" };
283 predicted.add(index);
284 }
285 return { row: { ...row, cells }, predicted };
286}
287
288export class TerminalView {
289 readonly element: HTMLElement;
290 readonly input: HTMLTextAreaElement;
291 /** Resolves after the first font-ready or fallback geometry transaction. */
292 readonly ready: Promise<void>;
293 private terminal: Terminal;
294 private frameReader: FrameReader;
295 private options: TerminalViewOptions;
296 private viewportEl: HTMLElement;
297 private rowsHost: HTMLElement;
298 private cursorEl: HTMLElement;
299 private metricProbe: HTMLElement;
300 private scrollbarEl: HTMLElement | null = null;
301 private scrollbarThumb: HTMLElement | null = null;
302 private stickyEl: HTMLElement | null = null;
303 /** Painted rows keyed in Ghostty's absolute scrollbar/screen row space. */
304 private rowEls = new Map<number, HTMLElement>();
305 private rowSnapshots = new Map<number, RowSnapshot>();
306 private rowLinks = new Map<number, DetectedLink[]>();
307 /** Last cell a motion event was reported for; suppresses per-pixel reports. */
308 private lastMouseCell = "";
309 private predictor: PredictionEngine;
310 /**
311 * Rows currently carrying a speculative glyph. Load-bearing: `rowSnapshots`
312 * stays canonical, so the next real frame's `sameCells` check would consider
313 * an overlaid row unchanged and leave a retracted prediction on screen. These
314 * rows repaint unconditionally.
315 */
316 private overlaidRows = new Set<number>();
317 /** The last cursor the emulator reported, in absolute row space. */
318 private realCursor: { row: number; col: number; wide: boolean; className: string } | null = null;
319 private cursorUnderline = false;
320 private firstRowNow = 0;
321 /** Which command the sticky header currently shows; avoids per-frame DOM writes. */
322 private stickyCommandId: number | null = null;
323 private frameQueued = false;
324 private frameToken = 0;
325 private automaticMetricRefreshPending = false;
326 private contentFramePending = false;
327 private synchronizedOutputTimeout: ReturnType<typeof setTimeout> | null = null;
328 private needFull = true;
329 private frameForeground = Number.NaN;
330 private frameBackground = Number.NaN;
331 private frameCursorColor = Number.NaN;
332 private cellW = 8;
333 private cellH = 17;
334 /** Visual viewport top. Ghostty itself remains on an adjacent integer row. */
335 private scrollPosition: number | null = null;
336 private scrollMax = 0;
337 private terminalViewportRevision: number;
338 private scrollbarDrag: { pointerId: number; y: number; offset: number } | null = null;
339 private scrollbarTrackHeight: number | null = null;
340 private glyphFits = new Map<string, GlyphFit>();
341 private detach: Array<() => void> = [];
342 private observer: ResizeObserver | null = null;
343 private metricObserver: ResizeObserver | null = null;
344 private completedMetricResize: { width: number; height: number } | null = null;
345 private resizeCallback: ((size: { cols: number; rows: number }) => void) | null = null;
346 private initialized = false;
347 private visible: boolean;
348 private selectingOutput = false;
349 private disposedView = false;
350 private initialTabIndex: string | null;
351 private initialClasses: Set<string>;
352 private initialRootStyles: Array<{ name: string; value: string; priority: string }>;
353
354 constructor(container: HTMLElement, terminal: Terminal, options: TerminalViewOptions = {}) {
355 this.terminal = terminal;
356 this.frameReader = terminal.acquireFrameReader();
357 this.terminalViewportRevision = terminal.viewportRevision;
358 this.options = options;
359 this.visible = options.visible !== false;
360 this.predictor = new PredictionEngine(resolvePredictOptions(options.predict));
361 this.element = container;
362 this.initialTabIndex = container.getAttribute("tabindex");
363 this.initialClasses = new Set(
364 ROOT_CLASSES.filter((name) => container.classList.contains(name)),
365 );
366 this.initialRootStyles = ROOT_STYLE_PROPERTIES.map((name) => ({
367 name,
368 value: container.style.getPropertyValue(name),
369 priority: container.style.getPropertyPriority(name),
370 }));
371 try {
372 this.input = container.ownerDocument.createElement("textarea");
373 this.viewportEl = container.ownerDocument.createElement("div");
374 this.rowsHost = container.ownerDocument.createElement("div");
375 this.cursorEl = container.ownerDocument.createElement("div");
376 this.metricProbe = container.ownerDocument.createElement("span");
377 } catch (error) {
378 this.frameReader.dispose();
379 throw error;
380 }
381
382 try {
383 container.classList.add("clo-term");
384 container.classList.add(FONT_LOADING_CLASS);
385 container.classList.toggle("clo-term-has-scrollbar", options.scrollbar !== false);
386 container.tabIndex = 0;
387 this.input.className = "clo-term-input";
388 this.input.tabIndex = -1;
389 this.input.setAttribute("aria-label", "Terminal input");
390 this.input.autocomplete = "off";
391 this.input.spellcheck = false;
392 container.appendChild(this.input);
393
394 this.viewportEl.className = "clo-term-viewport";
395 this.viewportEl.style.touchAction = "pan-x pinch-zoom";
396 container.appendChild(this.viewportEl);
397
398 this.rowsHost.className = "clo-term-rows";
399 this.viewportEl.appendChild(this.rowsHost);
400
401 this.cursorEl.className = "clo-term-cursor";
402 this.rowsHost.appendChild(this.cursorEl);
403
404 this.metricProbe.className = "clo-term-measure";
405 this.metricProbe.textContent = "W".repeat(32);
406 container.appendChild(this.metricProbe);
407
408 if (options.scrollbar !== false) {
409 this.scrollbarEl = container.ownerDocument.createElement("div");
410 this.scrollbarEl.className = "clo-term-scrollbar";
411 this.scrollbarEl.role = "scrollbar";
412 this.scrollbarEl.tabIndex = 0;
413 this.scrollbarEl.setAttribute("aria-orientation", "vertical");
414 this.scrollbarEl.setAttribute("aria-label", "Terminal scrollback");
415 this.scrollbarThumb = container.ownerDocument.createElement("div");
416 this.scrollbarThumb.className = "clo-term-scrollbar-thumb";
417 this.scrollbarEl.appendChild(this.scrollbarThumb);
418 container.appendChild(this.scrollbarEl);
419 this.bindScrollbar();
420 }
421
422 if (options.stickyScroll !== false) {
423 this.stickyEl = container.ownerDocument.createElement("div");
424 this.stickyEl.className = "clo-term-sticky";
425 this.stickyEl.style.display = "none";
426 // Clicking the pinned header jumps back to where the command started —
427 // the affordance that makes it navigation rather than decoration.
428 this.stickyEl.addEventListener("click", () => this.jumpToSticky());
429 container.appendChild(this.stickyEl);
430 }
431
432 if (typeof ResizeObserver !== "undefined") {
433 this.metricObserver = new ResizeObserver((entries) => {
434 if (!this.initialized) return;
435 const rect = entries.find((entry) => entry.target === this.metricProbe)?.contentRect;
436 if (
437 rect !== undefined &&
438 rect.width === this.completedMetricResize?.width &&
439 rect.height === this.completedMetricResize.height
440 ) {
441 this.completedMetricResize = null;
442 return;
443 }
444 this.completedMetricResize = null;
445 this.queueAutomaticMetricRefresh();
446 });
447 this.metricObserver.observe(this.metricProbe);
448 this.observer = new ResizeObserver(() => {
449 if (!this.initialized) return;
450 const resized = this.resizeToFit();
451 this.syncViewportGeometry();
452 this.syncMouseGeometry();
453 if (resized) this.needFull = true;
454 this.queueFrame();
455 });
456 this.observer.observe(this.element);
457 }
458 this.bindInput();
459 // ***Before the font gate, not inside it***: a program asks for the
460 // background in its first bytes — codex does — and an answer that waits on
461 // a webfont arrives after it has already decided how to draw.
462 this.publishThemeColors();
463 this.scheduleFrame();
464
465 const fonts = container.ownerDocument.fonts as FontFaceSet | undefined;
466 if (
467 fonts === undefined ||
468 typeof fonts.load !== "function" ||
469 typeof fonts.check !== "function"
470 ) {
471 this.initialize();
472 this.ready = Promise.resolve();
473 } else {
474 this.ready = this.loadInitialFonts(fonts)
475 .then(
476 () => this.initialize(),
477 (error: unknown) => this.initializeFallback(error),
478 )
479 .then(() => this.bindFontCompletion(fonts));
480 }
481 } catch (error) {
482 this.dispose();
483 throw error;
484 }
485 }
486
487 private async loadInitialFonts(fonts: FontFaceSet): Promise<void> {
488 const style = getComputedStyle(this.element);
489 const families = parseFontFamilies(style.fontFamily);
490 const primary = families[0];
491 if (primary === undefined) throw new Error("terminal primary font family is unavailable");
492 const requests = [
493 { descriptor: `normal 400 ${style.fontSize} ${primary}`, sample: PRIMARY_FONT_SAMPLE },
494 { descriptor: `normal 600 ${style.fontSize} ${primary}`, sample: PRIMARY_FONT_SAMPLE },
495 { descriptor: `italic 400 ${style.fontSize} ${primary}`, sample: PRIMARY_FONT_SAMPLE },
496 { descriptor: `italic 600 ${style.fontSize} ${primary}`, sample: PRIMARY_FONT_SAMPLE },
497 ];
498 const symbol = families.find(
499 (family) => normalizeFontFamily(family) === SYMBOL_FONT_FAMILY.toLowerCase(),
500 );
501 if (symbol !== undefined) {
502 requests.push({
503 descriptor: `normal 400 ${style.fontSize} ${symbol}`,
504 sample: SYMBOL_FONT_SAMPLE,
505 });
506 }
507 const loaded = await Promise.allSettled(
508 requests.map(({ descriptor, sample }) => fonts.load(descriptor, sample)),
509 );
510 const unavailable = requests.filter(({ descriptor, sample }, index) => {
511 const result = loaded[index]!;
512 return (
513 result.status === "rejected" ||
514 result.value.length === 0 ||
515 !fonts.check(descriptor, sample)
516 );
517 });
518 if (unavailable.length > 0) {
519 const rejection = loaded.find((result) => result.status === "rejected");
520 if (rejection !== undefined) throw rejection.reason;
521 throw new Error(
522 `terminal font faces unavailable: ${unavailable.map(({ descriptor }) => descriptor).join(", ")}`,
523 );
524 }
525 }
526
527 private initializeFallback(error: unknown): void {
528 if (this.disposedView) return;
529 this.element.dispatchEvent(
530 new CustomEvent(FONT_ERROR_EVENT, {
531 bubbles: true,
532 detail: { error },
533 }),
534 );
535 this.initialize();
536 }
537
538 private initialize(): void {
539 if (this.initialized || this.disposedView) return;
540 this.measure();
541 this.resizeToFit(false);
542 this.syncViewportGeometry();
543 if (this.visible) this.resizeCallback?.({ cols: this.terminal.cols, rows: this.terminal.rows });
544 if (this.disposedView) return;
545 this.syncMouseGeometry();
546 this.initialized = true;
547 this.needFull = true;
548 this.element.classList.remove(FONT_LOADING_CLASS);
549 this.queueFrame();
550 }
551
552 private bindFontCompletion(fonts: FontFaceSet): void {
553 if (this.disposedView) return;
554 // Some DOM libraries have not yet specialized FontFaceSet's EventTarget
555 // overload even though browsers expose FontFaceSetLoadEvent.fontfaces.
556 const source = fonts as unknown as FontCompletionSource;
557 if (
558 typeof source.addEventListener !== "function" ||
559 typeof source.removeEventListener !== "function"
560 ) {
561 return;
562 }
563 const onLoadingDone: EventListener = (event) => {
564 if (!this.initialized || this.disposedView) return;
565 const completed = (event as FontCompletionEvent).fontfaces;
566 if (completed === undefined) return;
567 const families = new Set(
568 parseFontFamilies(getComputedStyle(this.element).fontFamily).map(normalizeFontFamily),
569 );
570 if (!completed.some((face) => families.has(normalizeFontFamily(face.family)))) return;
571 this.queueAutomaticMetricRefresh();
572 };
573 source.addEventListener("loadingdone", onLoadingDone);
574 this.detach.push(() => source.removeEventListener("loadingdone", onLoadingDone));
575 }
576
577 /** Call after every terminal.write() batch; coalesces to one paint per frame. */
578 scheduleFrame(): void {
579 this.contentFramePending = true;
580 if (!this.visible) return;
581 if (this.suspendSynchronizedOutput()) return;
582 this.queueFrame();
583 }
584
585 private suspendSynchronizedOutput(): boolean {
586 if (!this.terminal.synchronizedOutput) return false;
587 this.synchronizedOutputTimeout ??= setTimeout(() => {
588 this.synchronizedOutputTimeout = null;
589 if (this.disposedView || !this.terminal.synchronizedOutput) return;
590 this.terminal.resetSynchronizedOutput();
591 this.scheduleFrame();
592 }, SYNCHRONIZED_OUTPUT_TIMEOUT_MS);
593 return true;
594 }
595
596 private queueFrame(): void {
597 if (!this.initialized || this.disposedView || !this.visible || this.frameQueued) return;
598 this.frameQueued = true;
599 const raf =
600 typeof requestAnimationFrame === "function"
601 ? requestAnimationFrame
602 : (fn: () => void) => setTimeout(fn, 16);
603 const token = ++this.frameToken;
604 raf(() => {
605 if (token !== this.frameToken) return;
606 this.frameQueued = false;
607 this.paint();
608 });
609 }
610
611 private queueAutomaticMetricRefresh(): void {
612 this.automaticMetricRefreshPending = true;
613 this.scheduleFrame();
614 }
615
616 /** Compute the cols/rows that fit the container at current metrics. */
617 private availableSize(): { width: number; height: number } {
618 const rect = this.element.getBoundingClientRect();
619 const style = getComputedStyle(this.element);
620 const px = (value: string) => Number.parseFloat(value) || 0;
621 const paddingX = px(style.paddingLeft) + px(style.paddingRight);
622 const paddingY = px(style.paddingTop) + px(style.paddingBottom);
623 const borderX = px(style.borderLeftWidth) + px(style.borderRightWidth);
624 const borderY = px(style.borderTopWidth) + px(style.borderBottomWidth);
625 const width =
626 this.element.clientWidth > 0
627 ? this.element.clientWidth - paddingX
628 : rect.width - paddingX - borderX;
629 const height =
630 this.element.clientHeight > 0
631 ? this.element.clientHeight - paddingY
632 : rect.height - paddingY - borderY;
633 return { width, height };
634 }
635
636 /** Compute the cols/rows that fit the container at current metrics. */
637 fit(): { cols: number; rows: number } {
638 if (!this.visible) return { cols: this.terminal.cols, rows: this.terminal.rows };
639 const { width, height } = this.availableSize();
640 const availableWidth = Math.max(0, width - (this.scrollbarEl === null ? 0 : SCROLLBAR_GUTTER));
641 const cols =
642 width > 0 ? Math.max(2, Math.floor(availableWidth / this.cellW)) : this.terminal.cols;
643 const rows = height > 0 ? Math.max(1, Math.floor(height / this.cellH)) : this.terminal.rows;
644 return { cols, rows };
645 }
646
647 /** Receive geometry changes (wire to pty.resize); fitting itself is intrinsic. */
648 observeResize(onResize: (size: { cols: number; rows: number }) => void): void {
649 this.resizeCallback = onResize;
650 if (this.initialized && this.visible)
651 onResize({ cols: this.terminal.cols, rows: this.terminal.rows });
652 }
653
654 private resizeToFit(report = true): boolean {
655 if (!this.visible) return false;
656 const size = this.fit();
657 if (size.cols === this.terminal.cols && size.rows === this.terminal.rows) return false;
658 this.settleFractionalScroll();
659 // Reflow moves rows and columns underneath every outstanding prediction,
660 // and the latency estimate belongs to a geometry that no longer exists.
661 this.predictor.reset();
662 this.overlaidRows.clear();
663 this.terminal.resize(size.cols, size.rows);
664 if (report) this.resizeCallback?.(size);
665 return true;
666 }
667
668 private syncViewportGeometry(): void {
669 const { width, height } = this.availableSize();
670 const availableWidth = Math.max(0, width - (this.scrollbarEl === null ? 0 : SCROLLBAR_GUTTER));
671 this.viewportEl.style.width = `${
672 width > 0
673 ? Math.min(this.terminal.cols * this.cellW, availableWidth)
674 : this.terminal.cols * this.cellW
675 }px`;
676 this.viewportEl.style.height = `${
677 height > 0
678 ? Math.min(this.terminal.rows * this.cellH, height)
679 : this.terminal.rows * this.cellH
680 }px`;
681 }
682
683 paint(): void {
684 if (!this.visible) return;
685 if (!this.initialized || this.disposedView) return;
686 if (this.frameQueued) {
687 this.frameQueued = false;
688 this.frameToken++;
689 }
690 if (this.suspendSynchronizedOutput()) return;
691 if (this.synchronizedOutputTimeout !== null) {
692 clearTimeout(this.synchronizedOutputTimeout);
693 this.synchronizedOutputTimeout = null;
694 }
695 if (this.automaticMetricRefreshPending) {
696 this.automaticMetricRefreshPending = false;
697 if (this.refreshMetricTransaction(false)) {
698 this.completedMetricResize = { width: this.cellW * 32, height: this.cellH };
699 }
700 }
701 const contentFrame = this.contentFramePending;
702 const snapshot = this.frameReader.snapshot({ full: this.needFull });
703 this.needFull = false;
704 this.applyFrame(snapshot);
705 if (contentFrame) this.refreshFractionalBoundary();
706 this.contentFramePending = false;
707 }
708
709 /**
710 * The predictive-echo engine, for diagnostics and tests. Read-only in
711 * practice: input drives it, and reaching in to mutate it desynchronizes the
712 * renderer's overlaid-row bookkeeping.
713 */
714 get predictions(): PredictionEngine {
715 return this.predictor;
716 }
717
718 /** Read renderer-owned state for diagnostics, forcing the next paint to rebuild its row cache. */
719 inspectFrame(options: { full?: boolean } = { full: true }): FrameSnapshot {
720 if (this.disposedView) throw new Error("terminal view is disposed");
721 if (this.terminal.synchronizedOutput) {
722 throw new Error("terminal frame is suppressed by synchronized output");
723 }
724 const frame = this.frameReader.snapshot(options);
725 this.needFull = true;
726 return frame;
727 }
728
729 /** Refresh the one visible row outside Ghostty's adjacent integer viewport. */
730 private refreshFractionalBoundary(): void {
731 const position = this.scrollPosition;
732 if (position === null || Number.isInteger(position)) return;
733 const current = this.terminal.scrollbar().offset;
734 const firstRow = Math.floor(position);
735 const other = current === firstRow ? firstRow + 1 : firstRow;
736 const boundary = current === firstRow ? firstRow + this.terminal.rows : firstRow;
737
738 this.terminal.scrollToRow(other);
739 const frame = this.frameReader.snapshot({ full: true });
740 const offset = this.terminal.scrollbar().offset;
741 const row = frame.dirtyRows.find((candidate) => offset + candidate.index === boundary);
742 if (row !== undefined) {
743 let el = this.rowEls.get(boundary);
744 if (el === undefined) {
745 el = this.element.ownerDocument.createElement("div");
746 el.className = "clo-term-row";
747 el.dataset["screenRow"] = String(boundary);
748 this.rowEls.set(boundary, el);
749 const next = this.rowEls.get(boundary + 1);
750 if (next === undefined) this.rowsHost.appendChild(el);
751 else next.before(el);
752 }
753 const previous = this.rowSnapshots.get(boundary);
754 const cellsChanged = !sameCells(previous?.cells, row.cells);
755 const metadataChanged = !sameRowMetadata(previous, row);
756 this.rowSnapshots.set(boundary, row);
757 if (metadataChanged) this.applyRowMetadata(el, row);
758 if (cellsChanged) {
759 this.fitGlyphs(
760 this.paintRow(row, frame, boundary, el, this.predictor.overlayFor(boundary)),
761 );
762 }
763 }
764 this.terminal.scrollToRow(current);
765 }
766
767 private applyFrame(frame: FrameSnapshot): void {
768 const doc = this.element.ownerDocument;
769 const rootStyle = this.element.style;
770 const cursorColor = frame.cursor?.color ?? -1;
771 const colorsChanged =
772 frame.foreground !== this.frameForeground || frame.background !== this.frameBackground;
773 if (frame.foreground !== this.frameForeground) {
774 if (frame.foreground === -1) rootStyle.removeProperty("--clo-term-effective-fg");
775 else rootStyle.setProperty("--clo-term-effective-fg", num(frame.foreground));
776 }
777 if (frame.background !== this.frameBackground) {
778 if (frame.background === -1) rootStyle.removeProperty("--clo-term-effective-bg");
779 else rootStyle.setProperty("--clo-term-effective-bg", num(frame.background));
780 }
781 if (cursorColor !== this.frameCursorColor) {
782 if (cursorColor === -1) rootStyle.removeProperty("--clo-term-effective-cursor");
783 else rootStyle.setProperty("--clo-term-effective-cursor", num(cursorColor));
784 }
785 const scrollbar = this.terminal.scrollbar();
786 this.terminalViewportRevision = this.terminal.viewportRevision;
787 const wasAtBottom = this.scrollPosition !== null && this.scrollPosition === this.scrollMax;
788 if (
789 this.scrollPosition === null ||
790 Number.isInteger(this.scrollPosition) ||
791 (wasAtBottom && scrollbar.offset === scrollbar.total - scrollbar.len)
792 ) {
793 this.scrollPosition = scrollbar.offset;
794 }
795 this.scrollMax = Math.max(0, scrollbar.total - scrollbar.len);
796
797 const position = Math.max(0, Math.min(this.scrollMax, this.scrollPosition));
798 this.scrollPosition = position;
799 const firstRow = Math.floor(position);
800 this.firstRowNow = firstRow;
801 const fraction = position - firstRow;
802 const lastRow = firstRow + frame.rows - 1 + (fraction > 0 ? 1 : 0);
803
804 // Reconcile before painting so this frame renders the overlay the emulator
805 // state actually justifies, rather than one frame of stale speculation.
806 const wasOverlaid = new Set(this.overlaidRows);
807 this.reconcilePredictions(frame, scrollbar.offset);
808
809 const previousRows = new Map<number, RowSnapshot | undefined>();
810 for (const row of frame.dirtyRows) {
811 const absoluteRow = scrollbar.offset + row.index;
812 previousRows.set(absoluteRow, this.rowSnapshots.get(absoluteRow));
813 this.rowSnapshots.set(absoluteRow, row);
814 }
815 const fittedGlyphs: FittedGlyph[] = [];
816 const paintedRows = new Set<number>();
817 for (const row of frame.dirtyRows) {
818 const absoluteRow = scrollbar.offset + row.index;
819 let el = this.rowEls.get(absoluteRow);
820 const created = el === undefined;
821 if (el === undefined) {
822 el = doc.createElement("div");
823 el.className = "clo-term-row";
824 el.dataset["screenRow"] = String(absoluteRow);
825 this.rowEls.set(absoluteRow, el);
826 }
827 const previous = previousRows.get(absoluteRow);
828 const cellsChanged = !sameCells(previous?.cells, row.cells);
829 const metadataChanged = !sameRowMetadata(previous, row);
830 if (created || metadataChanged) this.applyRowMetadata(el, row);
831 const overlay = this.predictor.overlayFor(absoluteRow);
832 // An overlaid row must repaint even when its canonical cells are
833 // unchanged: `rowSnapshots` never carries the speculation, so `sameCells`
834 // cannot see a prediction being added or retracted.
835 const overlayChanged = overlay !== undefined || wasOverlaid.has(absoluteRow);
836 if (created || cellsChanged || colorsChanged || overlayChanged) {
837 fittedGlyphs.push(...this.paintRow(row, frame, absoluteRow, el, overlay));
838 paintedRows.add(absoluteRow);
839 }
840 }
841 // Rows that carried speculation but are not in this frame still have to
842 // lose it; nothing else would ever repaint them.
843 for (const absoluteRow of wasOverlaid) {
844 if (paintedRows.has(absoluteRow)) continue;
845 const snapshot = this.rowSnapshots.get(absoluteRow);
846 const el = this.rowEls.get(absoluteRow);
847 if (snapshot === undefined || el === undefined) continue;
848 fittedGlyphs.push(
849 ...this.paintRow(snapshot, frame, absoluteRow, el, this.predictor.overlayFor(absoluteRow)),
850 );
851 paintedRows.add(absoluteRow);
852 }
853 this.overlaidRows = new Set(this.predictor.overlaidRows());
854 if (colorsChanged) {
855 for (const [absoluteRow, row] of this.rowSnapshots) {
856 if (absoluteRow < firstRow || absoluteRow > lastRow || paintedRows.has(absoluteRow))
857 continue;
858 const el = this.rowEls.get(absoluteRow);
859 if (el !== undefined) {
860 fittedGlyphs.push(
861 ...this.paintRow(row, frame, absoluteRow, el, this.predictor.overlayFor(absoluteRow)),
862 );
863 }
864 }
865 }
866 this.frameForeground = frame.foreground;
867 this.frameBackground = frame.background;
868 this.frameCursorColor = cursorColor;
869
870 for (const [screenRow, el] of this.rowEls) {
871 if (screenRow < firstRow || screenRow > lastRow) {
872 el.remove();
873 this.rowEls.delete(screenRow);
874 this.rowSnapshots.delete(screenRow);
875 this.rowLinks.delete(screenRow);
876 }
877 }
878 this.element.classList.toggle(
879 "clo-term-has-blink",
880 frame.cursor?.blinking === true ||
881 [...this.rowSnapshots.values()].some((row) => row.cells.some((cell) => cell.blink)),
882 );
883 let previous = this.cursorEl;
884 for (let screenRow = firstRow; screenRow <= lastRow; screenRow++) {
885 const el = this.rowEls.get(screenRow);
886 if (el !== undefined) {
887 if (previous.nextElementSibling !== el) previous.after(el);
888 previous = el;
889 }
890 }
891 this.rowsHost.style.transform = fraction === 0 ? "" : `translateY(${-fraction * this.cellH}px)`;
892
893 this.fitGlyphs(fittedGlyphs);
894 this.paintCursor(frame, scrollbar.offset, firstRow);
895 this.paintSticky(position);
896 this.paintScrollbar(scrollbar, position);
897 }
898
899 private now(): number {
900 const clock = globalThis.performance;
901 return clock !== undefined && typeof clock.now === "function" ? clock.now() : Date.now();
902 }
903
904 /** True only while the shell says a prompt is accepting input and has not executed. */
905 private editing(): boolean {
906 return this.terminal.shell.current?.state === "editing";
907 }
908
909 /** Fold this frame's real rows into the engine, retiring or dropping speculation. */
910 private reconcilePredictions(frame: FrameSnapshot, viewportTop: number): void {
911 if (!this.predictor.enabled) return;
912 const rows = new Map<number, readonly string[]>();
913 for (const row of frame.dirtyRows) {
914 rows.set(viewportTop + row.index, columnTexts(row.cells, frame.cols));
915 }
916 this.predictor.reconcile(
917 {
918 rows,
919 cursor:
920 frame.cursor === null ? null : { row: viewportTop + frame.cursor.y, col: frame.cursor.x },
921 editing: this.editing(),
922 },
923 this.now(),
924 );
925 }
926
927 /** Where a keystroke would land right now, as the last painted frame saw it. */
928 private predictionSite(): PredictionSite | null {
929 const cursor = this.realCursor;
930 if (cursor === null) return null;
931 return {
932 row: cursor.row,
933 col: cursor.col,
934 cols: this.terminal.cols,
935 editing: this.editing(),
936 };
937 }
938
939 /**
940 * Repaint rows whose speculation changed, without a frame.
941 *
942 * This is the one paint path that must never touch the frame reader: dirty
943 * snapshots are destructive, so consuming one here would swallow real pty
944 * output that had not been painted yet. It repaints from the canonical
945 * `rowSnapshots` cache instead, which is exactly what the last frame left.
946 */
947 private repaintPredictedRows(rows: readonly number[]): void {
948 if (rows.length === 0 || !this.initialized || this.disposedView) return;
949 const colors = { foreground: this.frameForeground, background: this.frameBackground };
950 const fittedGlyphs: FittedGlyph[] = [];
951 for (const absoluteRow of rows) {
952 const snapshot = this.rowSnapshots.get(absoluteRow);
953 const el = this.rowEls.get(absoluteRow);
954 if (snapshot === undefined || el === undefined) continue;
955 const overlay = this.predictor.overlayFor(absoluteRow);
956 if (overlay === undefined) this.overlaidRows.delete(absoluteRow);
957 else this.overlaidRows.add(absoluteRow);
958 fittedGlyphs.push(...this.paintRow(snapshot, colors, absoluteRow, el, overlay));
959 }
960 this.fitGlyphs(fittedGlyphs);
961 this.placeCursor();
962 }
963
964 /** Fold a keystroke into the engine and paint whatever it changed. */
965 private predictKey(event: KeyboardEvent): void {
966 if (!this.predictor.enabled) return;
967 const site = this.predictionSite();
968 if (site === null) return;
969 const now = this.now();
970 const bare = !event.ctrlKey && !event.metaKey && !event.altKey;
971 const rows =
972 bare && event.key === "Backspace"
973 ? this.predictor.backspace(site, now)
974 : bare && isPredictableText(event.key)
975 ? this.predictor.type(event.key, site, now)
976 : this.predictor.interrupt();
977 this.repaintPredictedRows(rows);
978 }
979
980 /** Drop speculation that no longer has a defensible basis. */
981 private dropPredictions(): void {
982 if (!this.predictor.enabled) return;
983 this.repaintPredictedRows(this.predictor.interrupt());
984 }
985
986 /**
987 * Pin the command whose output the viewport is inside. Recomputed per frame
988 * rather than cached: scrolling does not dirty rows, so there is no cheaper
989 * signal, and the work is one pass over a bounded command list.
990 */
991 private paintSticky(viewportTop: number): void {
992 const el = this.stickyEl;
993 if (el === null) return;
994 const command = stickyCommand(this.terminal.shell.commands, viewportTop);
995 if (command === null || command.commandLine === null) {
996 el.style.display = "none";
997 this.stickyCommandId = null;
998 return;
999 }
1000 if (this.stickyCommandId !== command.id) {
1001 this.stickyCommandId = command.id;
1002 el.textContent = command.commandLine;
1003 }
1004 el.className =
1005 command.exitCode !== null && command.exitCode !== 0
1006 ? "clo-term-sticky failed"
1007 : "clo-term-sticky";
1008 el.style.display = "block";
1009 el.style.height = `${this.cellH}px`;
1010 }
1011
1012 private jumpToSticky(): void {
1013 const command = stickyCommand(
1014 this.terminal.shell.commands,
1015 this.scrollPosition ?? this.terminal.scrollbar().offset,
1016 );
1017 if (command === null) return;
1018 const row = stickyTarget(command);
1019 if (row === null) return;
1020 this.scrollToRow(row);
1021 }
1022
1023 private paintScrollbar(scrollbar: Scrollbar, position = scrollbar.offset): void {
1024 const track = this.scrollbarEl;
1025 const thumb = this.scrollbarThumb;
1026 if (track === null || thumb === null) return;
1027 const maxOffset = Math.max(0, scrollbar.total - scrollbar.len);
1028 track.hidden = maxOffset === 0;
1029 track.setAttribute("aria-valuemin", "0");
1030 track.setAttribute("aria-valuemax", String(maxOffset));
1031 track.setAttribute("aria-valuenow", String(position));
1032 if (maxOffset === 0) {
1033 thumb.removeAttribute("style");
1034 return;
1035 }
1036
1037 const trackHeight =
1038 this.scrollbarTrackHeight ?? Math.max(0, this.terminal.rows * this.cellH - 6);
1039 const thumbHeight = Math.min(
1040 trackHeight,
1041 Math.max(24, trackHeight * (scrollbar.len / scrollbar.total)),
1042 );
1043 const travel = trackHeight - thumbHeight;
1044 thumb.style.height = `${thumbHeight}px`;
1045 thumb.style.transform = `translateY(${travel * (position / maxOffset)}px)`;
1046 }
1047
1048 private scrollBy(rows: number, fractional: boolean): boolean {
1049 if (rows === 0) return false;
1050 const scrollbar = this.terminal.scrollbar();
1051 const maxOffset = Math.max(0, scrollbar.total - scrollbar.len);
1052 if (maxOffset === 0) {
1053 this.resetFractionalScroll();
1054 return false;
1055 }
1056
1057 const current = this.scrollPosition ?? scrollbar.offset;
1058 let target = Math.max(0, Math.min(maxOffset, current + rows));
1059 if (!fractional) target = Math.max(0, Math.min(maxOffset, Math.round(target)));
1060 const nearest = Math.round(target);
1061 if (Math.abs(target - nearest) < 1e-9) target = nearest;
1062 if (target === current) return true;
1063
1064 this.scrollPosition = target;
1065 this.scrollMax = maxOffset;
1066 const firstRow = Math.floor(target);
1067 const fraction = target - firstRow;
1068 const anchor = fraction === 0 ? firstRow : rows > 0 ? firstRow + 1 : firstRow;
1069 const missingBoundary = rows > 0 ? firstRow : firstRow + scrollbar.len;
1070 if (fraction !== 0 && !this.rowEls.has(missingBoundary)) {
1071 const prime = rows > 0 ? firstRow : firstRow + 1;
1072 this.terminal.scrollToRow(prime);
1073 this.applyFrame(this.frameReader.snapshot({ full: true }));
1074 }
1075 const beforeAnchor = this.terminal.scrollbar().offset;
1076 this.terminal.scrollToRow(anchor);
1077 if (anchor !== beforeAnchor) this.needFull = true;
1078 this.queueFrame();
1079 return true;
1080 }
1081
1082 private wheelRows(event: WheelEvent): { rows: number; fractional: boolean } {
1083 if (event.deltaMode === 1) return { rows: event.deltaY, fractional: false };
1084 if (event.deltaMode === 2) {
1085 return { rows: event.deltaY * this.terminal.scrollbar().len, fractional: false };
1086 }
1087 return { rows: event.deltaY / this.cellH, fractional: true };
1088 }
1089
1090 private resetFractionalScroll(): void {
1091 this.scrollPosition = null;
1092 this.rowsHost.style.transform = "";
1093 }
1094
1095 /** Input follows the live prompt, but an already-live viewport needs no paint. */
1096 private scrollToBottomForInput(): boolean {
1097 if (
1098 this.terminalViewportRevision === this.terminal.viewportRevision &&
1099 this.scrollPosition !== null &&
1100 this.scrollPosition === this.scrollMax
1101 ) {
1102 return false;
1103 }
1104 const scrollbar = this.terminal.scrollbar();
1105 const bottom = Math.max(0, scrollbar.total - scrollbar.len);
1106 if (scrollbar.offset === bottom && (this.scrollPosition ?? scrollbar.offset) === bottom) {
1107 this.terminalViewportRevision = this.terminal.viewportRevision;
1108 return false;
1109 }
1110 this.resetFractionalScroll();
1111 this.terminal.scrollToBottom();
1112 this.terminalViewportRevision = this.terminal.viewportRevision;
1113 this.needFull = true;
1114 return true;
1115 }
1116
1117 private settleFractionalScroll(paintNow = false): void {
1118 const position = this.scrollPosition;
1119 if (position === null || Number.isInteger(position)) return;
1120 const target = Math.round(position);
1121 this.scrollPosition = target;
1122 this.terminal.scrollToRow(target);
1123 this.rowsHost.style.transform = "";
1124 this.needFull = true;
1125 if (paintNow) this.paint();
1126 }
1127
1128 private scrollToRow(row: number): void {
1129 const target = Math.max(0, Math.round(row));
1130 this.scrollPosition = target;
1131 this.terminal.scrollToRow(target);
1132 this.needFull = true;
1133 this.scheduleFrame();
1134 }
1135
1136 private scrollToPosition(row: number): void {
1137 const scrollbar = this.terminal.scrollbar();
1138 const maxOffset = Math.max(0, scrollbar.total - scrollbar.len);
1139 const current = this.scrollPosition ?? scrollbar.offset;
1140 const target = Math.max(0, Math.min(maxOffset, row));
1141 this.scrollBy(target - current, true);
1142 }
1143
1144 private scrollbarGeometry(): { maxOffset: number; travel: number; thumbHeight: number } | null {
1145 const track = this.scrollbarEl;
1146 if (track === null) return null;
1147 const scrollbar = this.terminal.scrollbar();
1148 const maxOffset = Math.max(0, scrollbar.total - scrollbar.len);
1149 const trackHeight =
1150 this.scrollbarTrackHeight ?? Math.max(0, this.terminal.rows * this.cellH - 6);
1151 const thumbHeight = Math.min(
1152 trackHeight,
1153 Math.max(24, trackHeight * (scrollbar.len / scrollbar.total)),
1154 );
1155 return { maxOffset, travel: trackHeight - thumbHeight, thumbHeight };
1156 }
1157
1158 private bindScrollbar(): void {
1159 const track = this.scrollbarEl!;
1160 const thumb = this.scrollbarThumb!;
1161 const stopMouse = (event: Event) => event.stopPropagation();
1162 const onPointerDown = (event: PointerEvent) => {
1163 event.preventDefault();
1164 event.stopPropagation();
1165 const geometry = this.scrollbarGeometry();
1166 if (geometry === null || geometry.maxOffset === 0) return;
1167 const rect = track.getBoundingClientRect();
1168 if (event.target === thumb) {
1169 this.scrollbarDrag = {
1170 pointerId: event.pointerId,
1171 y: event.clientY,
1172 offset: this.scrollPosition ?? this.terminal.scrollbar().offset,
1173 };
1174 track.setPointerCapture?.(event.pointerId);
1175 return;
1176 }
1177 const top = Math.max(
1178 0,
1179 Math.min(geometry.travel, event.clientY - rect.top - geometry.thumbHeight / 2),
1180 );
1181 this.scrollToPosition(
1182 geometry.travel === 0 ? 0 : (top / geometry.travel) * geometry.maxOffset,
1183 );
1184 };
1185 const onPointerMove = (event: PointerEvent) => {
1186 const drag = this.scrollbarDrag;
1187 if (drag === null || drag.pointerId !== event.pointerId) return;
1188 event.preventDefault();
1189 event.stopPropagation();
1190 const geometry = this.scrollbarGeometry();
1191 if (geometry === null || geometry.travel === 0) return;
1192 const rows = ((event.clientY - drag.y) / geometry.travel) * geometry.maxOffset;
1193 this.scrollToPosition(drag.offset + rows);
1194 };
1195 const endDrag = (event: PointerEvent) => {
1196 if (this.scrollbarDrag?.pointerId !== event.pointerId) return;
1197 event.stopPropagation();
1198 this.scrollbarDrag = null;
1199 track.releasePointerCapture?.(event.pointerId);
1200 };
1201 const onKeyDown = (event: KeyboardEvent) => {
1202 const scrollbar = this.terminal.scrollbar();
1203 const maxOffset = Math.max(0, scrollbar.total - scrollbar.len);
1204 const position = this.scrollPosition ?? scrollbar.offset;
1205 let target: number | null = null;
1206 if (event.key === "ArrowUp") target = position - 1;
1207 if (event.key === "ArrowDown") target = position + 1;
1208 if (event.key === "PageUp") target = position - scrollbar.len;
1209 if (event.key === "PageDown") target = position + scrollbar.len;
1210 if (event.key === "Home") target = 0;
1211 if (event.key === "End") target = maxOffset;
1212 if (target === null) return;
1213 event.preventDefault();
1214 event.stopPropagation();
1215 this.scrollToRow(target);
1216 };
1217 const onWheel = (event: WheelEvent) => {
1218 event.stopPropagation();
1219 const wheel = this.wheelRows(event);
1220 if (this.scrollBy(wheel.rows, wheel.fractional)) event.preventDefault();
1221 };
1222
1223 track.addEventListener("pointerdown", onPointerDown);
1224 track.addEventListener("pointermove", onPointerMove);
1225 track.addEventListener("pointerup", endDrag);
1226 track.addEventListener("pointercancel", endDrag);
1227 track.addEventListener("lostpointercapture", endDrag);
1228 track.addEventListener("keydown", onKeyDown);
1229 track.addEventListener("wheel", onWheel, { passive: false });
1230 track.addEventListener("mousedown", stopMouse);
1231 track.addEventListener("mouseup", stopMouse);
1232 track.addEventListener("mousemove", stopMouse);
1233 track.addEventListener("contextmenu", stopMouse);
1234 this.detach.push(() => {
1235 track.removeEventListener("pointerdown", onPointerDown);
1236 track.removeEventListener("pointermove", onPointerMove);
1237 track.removeEventListener("pointerup", endDrag);
1238 track.removeEventListener("pointercancel", endDrag);
1239 track.removeEventListener("lostpointercapture", endDrag);
1240 track.removeEventListener("keydown", onKeyDown);
1241 track.removeEventListener("wheel", onWheel);
1242 track.removeEventListener("mousedown", stopMouse);
1243 track.removeEventListener("mouseup", stopMouse);
1244 track.removeEventListener("mousemove", stopMouse);
1245 track.removeEventListener("contextmenu", stopMouse);
1246 });
1247 }
1248
1249 private detectRowLinks(row: RowSnapshot, absoluteRow: number): DetectedLink[] {
1250 const rows = [{ row, absoluteRow }];
1251 while (rows.length < 64 && rows[0]!.row.wrapContinuation) {
1252 const index = rows[0]!.absoluteRow - 1;
1253 const previous = this.rowSnapshots.get(index);
1254 if (!previous?.wrap) break;
1255 rows.unshift({ row: previous, absoluteRow: index });
1256 }
1257 while (rows.length < 64 && rows.at(-1)!.row.wrap) {
1258 const index = rows.at(-1)!.absoluteRow + 1;
1259 const next = this.rowSnapshots.get(index);
1260 if (!next?.wrapContinuation) break;
1261 rows.push({ row: next, absoluteRow: index });
1262 }
1263 const texts = rows.map(({ row }) => rowText(row.cells).text);
1264 const index = rows.findIndex((entry) => entry.absoluteRow === absoluteRow);
1265 const start = texts.slice(0, index).join("").length;
1266 const end = start + texts[index]!.length;
1267 return detectLinks(texts.join(""), this.options.links).flatMap((link) => {
1268 if (link.start >= end || link.end <= start) return [];
1269 return [
1270 { ...link, start: Math.max(0, link.start - start), end: Math.min(end, link.end) - start },
1271 ];
1272 });
1273 }
1274
1275 private paintRow(
1276 canonical: RowSnapshot,
1277 frame: Pick<FrameSnapshot, "foreground" | "background">,
1278 absoluteRow: number,
1279 el: HTMLElement,
1280 overlay?: ReadonlyMap<number, string>,
1281 ): FittedGlyph[] {
1282 const doc = this.element.ownerDocument;
1283 const fragment = doc.createDocumentFragment();
1284 const fittedGlyphs: FittedGlyph[] = [];
1285 // Speculation never reaches the canonical snapshot: it is applied to a
1286 // derived copy here and nowhere else, so `rowSnapshots` keeps describing
1287 // what the emulator actually holds.
1288 const overlaid = overlay === undefined ? null : applyOverlay(canonical, overlay);
1289 const row = overlaid?.row ?? canonical;
1290 const predictedCells = overlaid?.predicted;
1291
1292 // Links are resolved per row and folded into run grouping, so a run never
1293 // straddles a link boundary and the DOM stays one span list.
1294 let links: DetectedLink[] = [];
1295 let cellLinks: number[] | undefined;
1296 if (this.options.links) {
1297 const { cellAt } = rowText(row.cells);
1298 links = this.detectRowLinks(row, absoluteRow);
1299 cellLinks = linkCells(row.cells, links, cellAt);
1300 let textOffset = 0;
1301 let oscLink = -1;
1302 let previousUri: string | null = null;
1303 for (let index = 0; index < row.cells.length; index++) {
1304 const cell = row.cells[index]!;
1305 const cellText = cell.spacer === "head" ? "" : cell.text === "" ? " " : cell.text;
1306 if (cell.spacer === "head") {
1307 cellLinks[index] = -1;
1308 oscLink = -1;
1309 previousUri = null;
1310 continue;
1311 }
1312 if (cell.hasOsc8 && cell.osc8Uri !== null) {
1313 if (cell.osc8Uri !== previousUri) {
1314 oscLink = links.length;
1315 links.push({
1316 kind: "osc8",
1317 start: textOffset,
1318 end: textOffset + cellText.length,
1319 text: cellText,
1320 target: cell.osc8Uri,
1321 line: null,
1322 column: null,
1323 });
1324 } else {
1325 const link = links[oscLink]!;
1326 link.end += cellText.length;
1327 link.text += cellText;
1328 }
1329 cellLinks[index] = oscLink;
1330 } else {
1331 oscLink = -1;
1332 if (cell.hasOsc8) cellLinks[index] = -1;
1333 }
1334 previousUri = cell.osc8Uri;
1335 textOffset += cellText.length;
1336 }
1337 if (links.length === 0) cellLinks = undefined;
1338 }
1339 this.rowLinks.set(absoluteRow, links);
1340
1341 for (const run of cellsToRuns(row.cells, cellLinks, predictedCells)) {
1342 const span = doc.createElement("span");
1343 const style = run.cells[0]!;
1344 let cls = "clo-term-run";
1345 if (run.predicted) cls += this.predictor.underline ? " predicted underlined" : " predicted";
1346 if (style.bold) cls += " bold";
1347 if (style.italic) cls += " italic";
1348 if (style.faint) cls += " faint";
1349 if (style.blink) cls += " blink";
1350 if (style.invisible) cls += " invisible";
1351 if (style.strikethrough) cls += " strikethrough";
1352 if (style.overline) cls += " overline";
1353 if (style.underline > 0) cls += ` underline-${Math.min(style.underline, 5)}`;
1354 let fg = style.fg;
1355 let bg = style.bg;
1356 if (style.inverse) {
1357 // OSC-effective and explicit sides swap inline; sides without either
1358 // use the inverse CSS chain for host theme and built-in fallbacks.
1359 cls += " inverse";
1360 const swappedFg = bg === -1 ? (frame.background === -1 ? -1 : frame.background) : bg;
1361 const swappedBg = fg === -1 ? (frame.foreground === -1 ? -1 : frame.foreground) : fg;
1362 fg = swappedFg;
1363 bg = swappedBg;
1364 }
1365 if (run.link >= 0) {
1366 cls += " clo-term-link";
1367 span.dataset["link"] = String(run.link);
1368 span.dataset["row"] = String(absoluteRow);
1369 span.dataset["linkKind"] = links[run.link]!.kind;
1370 }
1371 if (style.invisible) span.style.color = "transparent";
1372 else if (fg !== -1) span.style.color = num(fg);
1373 if (style.underlineColor !== -1) {
1374 const color = num(style.underlineColor);
1375 span.style.setProperty(
1376 "--clo-term-decoration-color",
1377 style.faint ? `color-mix(in srgb, ${color} 55%, transparent)` : color,
1378 );
1379 }
1380 const compact =
1381 style.underlineColor === -1 &&
1382 !style.bold &&
1383 !style.italic &&
1384 !style.faint &&
1385 !style.blink &&
1386 !style.inverse &&
1387 !style.invisible &&
1388 !style.strikethrough &&
1389 !style.overline &&
1390 style.underline === 0 &&
1391 run.link < 0 &&
1392 run.cells.every((cell) => {
1393 const text = cellText(cell);
1394 return (
1395 cell.width === 1 &&
1396 cell.spacer === "none" &&
1397 !cell.protected &&
1398 cell.semantic === "output" &&
1399 !cell.hasOsc8 &&
1400 cell.osc8Uri === null &&
1401 text.length === 1 &&
1402 text.charCodeAt(0) >= 0x20 &&
1403 text.charCodeAt(0) <= 0x7e
1404 );
1405 });
1406 if (compact) {
1407 span.className = "clo-term-run clo-term-cell clo-term-compact";
1408 if (bg !== -1) span.style.backgroundColor = num(bg);
1409 if (run.columns > 1) {
1410 span.dataset["columns"] = String(run.columns);
1411 span.style.setProperty("--clo-term-cell-columns", String(run.columns));
1412 }
1413 span.textContent = run.text;
1414 fragment.appendChild(span);
1415 continue;
1416 }
1417 span.className = cls;
1418 for (const runCell of run.cells) {
1419 const cell = doc.createElement("span");
1420 cell.className = "clo-term-cell";
1421 cell.dataset["columns"] = String(runCell.width);
1422 cell.dataset["spacer"] = runCell.spacer;
1423 cell.dataset["protected"] = String(runCell.protected);
1424 cell.dataset["semantic"] = runCell.semantic;
1425 if (runCell.spacer === "head") {
1426 cell.classList.add("clo-term-spacer-head");
1427 cell.setAttribute("aria-hidden", "true");
1428 }
1429 cell.style.setProperty("--clo-term-cell-columns", String(runCell.width));
1430 if (bg !== -1) cell.style.backgroundColor = num(bg);
1431 const glyph = doc.createElement("span");
1432 glyph.className = "clo-term-glyph";
1433 const ink = doc.createElement("span");
1434 ink.className = "clo-term-ink";
1435 const text = cellText(runCell);
1436 ink.textContent = text;
1437 glyph.appendChild(ink);
1438 cell.appendChild(glyph);
1439 const codepoint = runCell.width === 1 ? privateUseBaseCodepoint(text) : null;
1440 const sprite =
1441 codepoint === null || text !== String.fromCodePoint(codepoint)
1442 ? undefined
1443 : POWERLINE_SPRITES.get(codepoint);
1444 if (sprite !== undefined) {
1445 cell.classList.add("clo-term-powerline-cell");
1446 glyph.classList.add("clo-term-powerline", `clo-term-powerline-${sprite}`);
1447 } else if (runCell.width === 2 || codepoint !== null) {
1448 const mode: GlyphFitMode =
1449 runCell.width === 2
1450 ? "wide"
1451 : (codepoint! >= 0xe0b0 && codepoint! <= 0xe0bf) ||
1452 codepoint === 0xe0d2 ||
1453 codepoint === 0xe0d4
1454 ? "powerline"
1455 : "symbol";
1456 cell.classList.add("clo-term-fitted-cell");
1457 glyph.classList.add(mode === "wide" ? "clo-term-wide-glyph" : `clo-term-${mode}-glyph`);
1458 const key = `${mode}\0${style.bold ? 1 : 0}${style.italic ? 1 : 0}\0${text}`;
1459 const cache = text.length <= GLYPH_FIT_CACHE_TEXT_LIMIT;
1460 const fit = cache ? this.glyphFits.get(key) : undefined;
1461 if (fit === undefined) {
1462 fittedGlyphs.push({
1463 element: glyph,
1464 key,
1465 cache,
1466 mode,
1467 target: this.cellW * runCell.width,
1468 });
1469 } else applyGlyphFit(glyph, fit);
1470 }
1471 span.appendChild(cell);
1472 }
1473 fragment.appendChild(span);
1474 }
1475 el.replaceChildren(fragment);
1476 return fittedGlyphs;
1477 }
1478
1479 private applyRowMetadata(el: HTMLElement, row: RowSnapshot): void {
1480 el.dataset["wrap"] = String(row.wrap);
1481 el.dataset["wrapContinuation"] = String(row.wrapContinuation);
1482 el.dataset["semanticPrompt"] = row.semanticPrompt;
1483 el.dataset["hyperlink"] = String(row.hyperlink);
1484 }
1485
1486 private fitGlyphs(glyphs: readonly FittedGlyph[]): void {
1487 if (glyphs.length === 0) return;
1488 const groups = new Map<string, FittedGlyph[]>();
1489 for (const glyph of glyphs) {
1490 const group = groups.get(glyph.key);
1491 if (group === undefined) groups.set(glyph.key, [glyph]);
1492 else group.push(glyph);
1493 }
1494 const unique = [...groups.values()];
1495 // Read one representative per grapheme/style before writing transforms, so
1496 // a cold dirty viewport pays one layout and no duplicate measurements.
1497 const widths = unique.map((group) => group[0]!.element.getBoundingClientRect().width);
1498 for (let index = 0; index < unique.length; index++) {
1499 const width = widths[index]!;
1500 if (width > 0) {
1501 const group = unique[index]!;
1502 const representative = group[0]!;
1503 const scaleX =
1504 representative.mode === "symbol"
1505 ? Math.min(1, representative.target / width)
1506 : representative.target / width;
1507 const fit = {
1508 scaleX,
1509 scaleY: representative.mode === "powerline" ? 1 : scaleX,
1510 marginLeft: (representative.target - width) / 2,
1511 };
1512 if (group[0]!.cache) {
1513 if (this.glyphFits.size >= GLYPH_FIT_CACHE_LIMIT) {
1514 const oldest = this.glyphFits.keys().next().value as string | undefined;
1515 if (oldest !== undefined) this.glyphFits.delete(oldest);
1516 }
1517 this.glyphFits.set(group[0]!.key, fit);
1518 }
1519 for (const glyph of group) applyGlyphFit(glyph.element, fit);
1520 }
1521 }
1522 }
1523
1524 private paintCursor(frame: FrameSnapshot, viewportTop: number, firstRow: number): void {
1525 const cursor = frame.cursor;
1526 if (cursor === null) {
1527 this.realCursor = null;
1528 this.placeCursor();
1529 return;
1530 }
1531 // Ghostty reports a wide cursor at its spacer tail; paint from the glyph head.
1532 this.realCursor = {
1533 row: viewportTop + cursor.y,
1534 col: cursor.wide ? Math.max(0, cursor.x - 1) : cursor.x,
1535 wide: cursor.wide,
1536 className: `clo-term-cursor style-${cursor.style}${cursor.blinking ? " blinking" : ""}`,
1537 };
1538 this.cursorUnderline = cursor.style === "underline";
1539 this.placeCursor(firstRow);
1540 }
1541
1542 /**
1543 * Position the cursor element. The speculative cursor wins while predictions
1544 * are outstanding — without it the caret sits behind the glyphs the user just
1545 * typed, which reads as more broken than not predicting at all.
1546 */
1547 private placeCursor(firstRow = this.firstRowNow): void {
1548 const real = this.realCursor;
1549 if (real === null) {
1550 this.cursorEl.className = "clo-term-cursor";
1551 this.cursorEl.removeAttribute("style");
1552 this.cursorEl.style.display = "none";
1553 return;
1554 }
1555 const predicted = this.predictor.cursor;
1556 const at = predicted === null ? real : { row: predicted.row, col: predicted.col };
1557 const wide = predicted === null && real.wide;
1558 const style = this.cursorEl.style;
1559 style.display = "block";
1560 style.left = `${at.col * this.cellW}px`;
1561 style.top = `${
1562 this.cursorUnderline
1563 ? (at.row - firstRow + 1) * this.cellH - 2
1564 : (at.row - firstRow) * this.cellH
1565 }px`;
1566 this.input.style.left = style.left;
1567 this.input.style.top = `${Math.max(0, Math.min(this.terminal.rows - 1, at.row - firstRow)) * this.cellH}px`;
1568 style.width = `${(wide ? 2 : 1) * this.cellW}px`;
1569 style.height = `${this.cellH}px`;
1570 this.cursorEl.className = `${real.className}${
1571 this.element.contains(this.element.ownerDocument.activeElement) ? "" : " blurred"
1572 }`;
1573 }
1574
1575 /**
1576 * The mouse encoder works in surface pixels, so it needs our cell metrics to
1577 * resolve a click to a cell. Re-sent whenever geometry changes; without it
1578 * every event would report cell 0,0.
1579 */
1580 private syncMouseGeometry(): void {
1581 const rect = this.viewportEl.getBoundingClientRect();
1582 this.scrollbarTrackHeight = Math.max(0, rect.height - 6);
1583 if (this.options.mouse === false) return;
1584 this.terminal.setMouseGeometry({
1585 width: rect.width || this.terminal.cols * this.cellW,
1586 height: rect.height || this.terminal.rows * this.cellH,
1587 cellWidth: this.cellW,
1588 cellHeight: this.cellH,
1589 });
1590 }
1591
1592 /**
1593 * Hand the core the theme's own colors: fore/background and cursor so it can
1594 * answer `OSC 10/11/12`, and the 16 ANSI slots it resolves indexed colors
1595 * through.
1596 *
1597 * **The browser resolves them, this does not parse CSS**: a token holds
1598 * anything CSS accepts — a `var()` chain into the product's palette,
1599 * `color-mix()`, a keyword — so the value is spent on the metric probe's
1600 * `color` and read back. An unset token answers nothing, and neither does a
1601 * transparent one: a see-through terminal has no background to report. The
1602 * theme token is read rather than the painted background, which already
1603 * carries a program's override and would be handed straight back as the
1604 * default.
1605 *
1606 * Three spellings come back: `rgb()` from a browser, the authored hex from
1607 * happy-dom, and whatever space a `color-mix()` was mixed in, which is what
1608 * the canvas is here to read.
1609 */
1610 private publishThemeColors(): void {
1611 const probe = this.metricProbe;
1612 const tokens = getComputedStyle(this.element);
1613 const context =
1614 probe.ownerDocument
1615 .createElement("canvas")
1616 .getContext?.("2d", { willReadFrequently: true }) ?? null;
1617 const resolve = (token: string): number | null => {
1618 // The computed value of a custom property is its substituted token stream,
1619 // so a `var()` chain into the product's palette is already followed here;
1620 // what is left may still be `color-mix()` or a keyword, which is what the
1621 // probe is for.
1622 const declared = tokens.getPropertyValue(token).trim();
1623 if (declared === "") return null;
1624 probe.style.color = "";
1625 probe.style.color = declared;
1626 const value = getComputedStyle(probe).color.trim();
1627 probe.style.color = "";
1628 const rgb = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)$/.exec(value);
1629 if (rgb !== null) {
1630 if (rgb[4] !== undefined && Number(rgb[4]) === 0) return null;
1631 return (Number(rgb[1]) << 16) | (Number(rgb[2]) << 8) | Number(rgb[3]);
1632 }
1633 const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value);
1634 if (hex !== null) {
1635 const digits = hex[1]!;
1636 const full =
1637 digits.length === 3
1638 ? digits
1639 .split("")
1640 .map((digit) => digit + digit)
1641 .join("")
1642 : digits;
1643 return Number.parseInt(full, 16);
1644 }
1645 // ***A computed color is not always `rgb()`***: `color-mix()` keeps the
1646 // space it was mixed in, and a host's palette is written in one. Canvas is
1647 // the only parser that agrees with the pixels, and it is the one notation
1648 // reader here that never has to learn a color space.
1649 if (context === null) return null;
1650 context.clearRect(0, 0, 1, 1);
1651 context.fillStyle = value;
1652 context.fillRect(0, 0, 1, 1);
1653 const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data;
1654 if (alpha === 0) return null;
1655 return (red! << 16) | (green! << 8) | blue!;
1656 };
1657 this.terminal.setDefaultColors({
1658 foreground: resolve("--clo-term-fg"),
1659 background: resolve("--clo-term-bg"),
1660 cursor: resolve("--clo-term-cursor"),
1661 });
1662 this.terminal.setDefaultPalette(ANSI_TOKENS.map(resolve));
1663 }
1664
1665 private measure(): boolean {
1666 const rect = this.metricProbe.getBoundingClientRect();
1667 let cellW = this.cellW;
1668 let cellH = this.cellH;
1669 if (rect.width > 0) {
1670 // Ghostty's mouse geometry uses integer pixels; paint the same grid it encodes.
1671 cellW = Math.max(1, Math.round(rect.width / 32));
1672 cellH = Math.max(1, Math.round(rect.height));
1673 }
1674 const changed = cellW !== this.cellW || cellH !== this.cellH;
1675 if (changed) {
1676 this.cellW = cellW;
1677 this.cellH = cellH;
1678 this.resetFractionalScroll();
1679 this.glyphFits.clear();
1680 }
1681 this.rowsHost.style.setProperty("--clo-term-cell-w", `${this.cellW}px`);
1682 this.rowsHost.style.setProperty("--clo-term-cell-h", `${this.cellH}px`);
1683 return changed;
1684 }
1685
1686 setVisible(visible: boolean): void {
1687 if (this.visible === visible) return;
1688 this.visible = visible;
1689 if (visible) this.refreshMetrics();
1690 else if (this.frameQueued) {
1691 this.frameToken++;
1692 this.frameQueued = false;
1693 }
1694 }
1695
1696 /** Re-read font metrics after changing metric-affecting terminal CSS. */
1697 refreshMetrics(): void {
1698 this.refreshMetricTransaction();
1699 }
1700
1701 private refreshMetricTransaction(schedule = true): boolean {
1702 if (this.disposedView) return false;
1703 if (!this.initialized) {
1704 this.scheduleFrame();
1705 return false;
1706 }
1707 this.publishThemeColors();
1708 const changed = this.measure();
1709 // Fallback CJK/emoji fonts can change while primary `W` metrics remain
1710 // identical, so every explicit/font-ready refresh invalidates ink fitting.
1711 this.glyphFits.clear();
1712 this.rowSnapshots.clear();
1713 this.resizeToFit();
1714 this.syncViewportGeometry();
1715 this.syncMouseGeometry();
1716 this.needFull = true;
1717 if (schedule) this.scheduleFrame();
1718 return changed;
1719 }
1720
1721 /** Textarea focus clears native output ranges; selection gestures keep the root focused. */
1722 focus(): void {
1723 if (this.disposedView) return;
1724 const selection = this.element.ownerDocument.getSelection();
1725 const outputSelected =
1726 selection !== null &&
1727 !selection.isCollapsed &&
1728 (this.rowsHost.contains(selection.anchorNode) || this.rowsHost.contains(selection.focusNode));
1729 const target = this.selectingOutput || outputSelected ? this.element : this.input;
1730 target.focus({ preventScroll: true });
1731 }
1732
1733 private bindInput(): void {
1734 const el = this.element;
1735 const emit = (bytes: Uint8Array | null) => {
1736 if (bytes !== null && bytes.length > 0) this.options.onData?.(bytes);
1737 };
1738
1739 let composing = false;
1740 let compositionInputPending = false;
1741 const onCompositionStart = () => {
1742 this.input.classList.add("composing");
1743 composing = true;
1744 compositionInputPending = false;
1745 };
1746 const onCompositionEnd = (event: CompositionEvent) => {
1747 this.input.classList.remove("composing");
1748 composing = false;
1749 compositionInputPending = true;
1750 this.input.value = "";
1751 if (!this.input.readOnly && event.data !== "") emit(new TextEncoder().encode(event.data));
1752 };
1753 const onInput = () => {
1754 if (composing) return;
1755 const text = this.input.value;
1756 this.input.value = "";
1757 if (compositionInputPending) {
1758 compositionInputPending = false;
1759 return;
1760 }
1761 if (!this.input.readOnly && text !== "") emit(new TextEncoder().encode(text));
1762 };
1763 const onKeyDown = (event: KeyboardEvent) => {
1764 if (composing || this.input.readOnly || event.defaultPrevented) return;
1765 if (this.options.onBeforeKey?.(event) === false) return;
1766 if (
1767 event.isComposing ||
1768 event.keyCode === 229 ||
1769 event.key === "Process" ||
1770 event.key === "Dead"
1771 ) {
1772 this.input.focus({ preventScroll: true });
1773 return;
1774 }
1775 compositionInputPending = false;
1776 const bytes = this.terminal.encodeKey(event);
1777 if (bytes !== null) {
1778 this.input.focus({ preventScroll: true });
1779 event.preventDefault();
1780 const viewportMoved = this.scrollToBottomForInput();
1781 emit(bytes);
1782 if (viewportMoved) this.scheduleFrame();
1783 this.predictKey(event);
1784 } else if (
1785 event.key.toLowerCase() === "v" &&
1786 (event.metaKey || (event.ctrlKey && event.shiftKey))
1787 ) {
1788 this.input.focus({ preventScroll: true });
1789 }
1790 };
1791 const onPaste = (event: ClipboardEvent) => {
1792 if (this.input.readOnly) return;
1793 const text = event.clipboardData?.getData("text");
1794 if (text !== undefined && text !== "") {
1795 this.input.focus({ preventScroll: true });
1796 event.preventDefault();
1797 const viewportMoved = this.scrollToBottomForInput();
1798 emit(this.terminal.encodePaste(text));
1799 if (viewportMoved) this.scheduleFrame();
1800 // Bracketed paste is one opaque blob to the line editor; we have no
1801 // model of what it does with it.
1802 this.dropPredictions();
1803 }
1804 };
1805 // Mouse reporting is claimed by the program, not by us: shift is the
1806 // universal terminal escape hatch for "let me select anyway", and we honor
1807 // it so a full-screen app can never take selection away entirely.
1808 const mouseClaimed = (event: MouseEvent | WheelEvent) =>
1809 this.options.mouse !== false && !event.shiftKey && this.terminal.mouseTracking();
1810 let mouseGestureClaimed = false;
1811
1812 const position = (event: MouseEvent | WheelEvent) => {
1813 const rect = this.viewportEl.getBoundingClientRect();
1814 return {
1815 x: (Number.isFinite(event.clientX) ? event.clientX : rect.left) - rect.left,
1816 y: (Number.isFinite(event.clientY) ? event.clientY : rect.top) - rect.top,
1817 };
1818 };
1819
1820 const forward = (
1821 event: MouseEvent,
1822 action: "press" | "release" | "motion",
1823 button: number | null,
1824 ) => {
1825 this.settleFractionalScroll(true);
1826 const bytes = this.terminal.encodeMouse({
1827 action,
1828 button,
1829 ...position(event),
1830 ctrlKey: event.ctrlKey,
1831 altKey: event.altKey,
1832 metaKey: event.metaKey,
1833 shiftKey: event.shiftKey,
1834 });
1835 if (bytes !== null) {
1836 event.preventDefault();
1837 emit(bytes);
1838 // A program taking mouse input is not a line editor echoing keystrokes.
1839 this.dropPredictions();
1840 }
1841 };
1842
1843 const onMouseDown = (event: MouseEvent) => {
1844 mouseGestureClaimed = mouseClaimed(event);
1845 if (mouseGestureClaimed) {
1846 this.selectingOutput = false;
1847 this.input.focus({ preventScroll: true });
1848 this.lastMouseCell = "";
1849 forward(event, "press", event.button);
1850 return;
1851 }
1852 if (event.button !== 0 || event.target === this.input) return;
1853 this.selectingOutput = true;
1854 this.element.focus({ preventScroll: true });
1855 };
1856 const onMouseUp = (event: MouseEvent) => {
1857 if (this.selectingOutput || (!mouseGestureClaimed && !mouseClaimed(event))) return;
1858 this.lastMouseCell = "";
1859 forward(event, "release", event.button);
1860 };
1861 const finishOutputSelection = (event: MouseEvent) => {
1862 if (event.button !== 0 || !this.selectingOutput) return;
1863 this.selectingOutput = false;
1864 if (el.ownerDocument.activeElement === el) this.focus();
1865 };
1866 const cancelOutputSelection = (event: Event) => {
1867 if (event.type === "blur" && event.target !== event.currentTarget) return;
1868 this.selectingOutput = false;
1869 };
1870 const onMouseMove = (event: MouseEvent) => {
1871 if (this.selectingOutput || !mouseClaimed(event)) return;
1872 this.settleFractionalScroll(true);
1873 // Report once per cell crossed, not once per pixel of travel. libghostty
1874 // has an option for this that is inert at the pinned vendor commit, so
1875 // the guard lives here; without it a slow drag writes to the pty on
1876 // every mousemove.
1877 const at = position(event);
1878 const cell = `${Math.floor(at.x / this.cellW)},${Math.floor(at.y / this.cellH)}`;
1879 if (cell === this.lastMouseCell) return;
1880 this.lastMouseCell = cell;
1881 // buttons is a bitmask of held buttons; 0 means a bare hover, which only
1882 // "any" tracking reports. The encoder decides — we just report honestly.
1883 const held = event.buttons & 1 ? 0 : event.buttons & 4 ? 1 : event.buttons & 2 ? 2 : null;
1884 forward(event, "motion", held);
1885 };
1886 const onContextMenu = (event: MouseEvent) => {
1887 // Without this a right-click reported to the program also opens the
1888 // browser menu over it.
1889 if (mouseClaimed(event)) event.preventDefault();
1890 };
1891
1892 const onWheel = (event: WheelEvent) => {
1893 if (mouseClaimed(event)) {
1894 this.settleFractionalScroll(true);
1895 const vertical = Math.abs(event.deltaY) >= Math.abs(event.deltaX);
1896 const button = vertical
1897 ? event.deltaY < 0
1898 ? 3
1899 : event.deltaY > 0
1900 ? 4
1901 : null
1902 : event.deltaX < 0
1903 ? 5
1904 : event.deltaX > 0
1905 ? 6
1906 : null;
1907 if (button !== null) {
1908 // Programs receive wheel axes as VT button 4-7 presses.
1909 const bytes = this.terminal.encodeMouse({
1910 action: "press",
1911 button,
1912 ...position(event),
1913 ctrlKey: event.ctrlKey,
1914 altKey: event.altKey,
1915 metaKey: event.metaKey,
1916 shiftKey: event.shiftKey,
1917 });
1918 if (bytes !== null) {
1919 event.preventDefault();
1920 emit(bytes);
1921 return;
1922 }
1923 }
1924 }
1925 if (this.options.scrollback === false) return;
1926 const wheel = this.wheelRows(event);
1927 if (this.scrollBy(wheel.rows, wheel.fractional)) event.preventDefault();
1928 };
1929 const onFocus = () => {
1930 this.focus();
1931 this.scheduleFrame();
1932 };
1933 const onFocusChange = () => this.scheduleFrame();
1934 const activateLink = (event: MouseEvent, target: EventTarget | null) => {
1935 const links = this.options.links;
1936 if (!links?.onActivate) return;
1937 const claimed = mouseGestureClaimed || mouseClaimed(event);
1938 mouseGestureClaimed = false;
1939 if (claimed) return;
1940 if (links.requireModifier === true && !event.ctrlKey && !event.metaKey) return;
1941 const linkElement =
1942 target instanceof Element ? target.closest<HTMLElement>(".clo-term-link") : null;
1943 const index = linkElement?.dataset["link"];
1944 const rowIndex = linkElement?.dataset["row"];
1945 if (index === undefined || rowIndex === undefined) return;
1946 let link = this.rowLinks.get(Number(rowIndex))?.[Number(index)];
1947 if (link === undefined) return;
1948 const row = this.rowSnapshots.get(Number(rowIndex));
1949 if (row && link.kind !== "osc8") {
1950 const displayed = link;
1951 link = this.detectRowLinks(row, Number(rowIndex)).find(
1952 (candidate) => candidate.start <= displayed.start && candidate.end >= displayed.end,
1953 );
1954 if (!link) return;
1955 }
1956 event.preventDefault();
1957 links.onActivate(link, event);
1958 };
1959
1960 const onClick = (event: MouseEvent) => {
1961 if ("pointerType" in event && event.pointerType === "touch") return;
1962 activateLink(event, event.target);
1963 };
1964 let touch: {
1965 pointerId: number;
1966 startX: number;
1967 startY: number;
1968 lastY: number;
1969 remainder: number;
1970 moved: boolean;
1971 target: EventTarget | null;
1972 } | null = null;
1973 const onTouchStart = (event: PointerEvent) => {
1974 if (event.pointerType !== "touch") return;
1975 event.preventDefault();
1976 if (!event.isPrimary || touch !== null) return;
1977 this.selectingOutput = false;
1978 touch = {
1979 pointerId: event.pointerId,
1980 startX: event.clientX,
1981 startY: event.clientY,
1982 lastY: event.clientY,
1983 remainder: 0,
1984 moved: false,
1985 target: event.target,
1986 };
1987 this.viewportEl.setPointerCapture?.(event.pointerId);
1988 };
1989 const onTouchMove = (event: PointerEvent) => {
1990 if (touch?.pointerId !== event.pointerId || event.pointerType !== "touch") return;
1991 event.preventDefault();
1992 touch.moved ||= Math.hypot(event.clientX - touch.startX, event.clientY - touch.startY) > 4;
1993 if (!touch.moved) return;
1994 const delta = (touch.lastY - event.clientY) / this.cellH;
1995 touch.lastY = event.clientY;
1996 if (!mouseClaimed(event) && !this.terminal.alternateScreen) {
1997 if (this.options.scrollback !== false) this.scrollBy(delta, true);
1998 return;
1999 }
2000 const total = touch.remainder + delta;
2001 const rows = Math.trunc(total);
2002 touch.remainder = total - rows;
2003 for (let index = 0; index < Math.abs(rows); index++) {
2004 if (mouseClaimed(event)) forward(event, "press", rows < 0 ? 3 : 4);
2005 else {
2006 const key = rows < 0 ? "ArrowUp" : "ArrowDown";
2007 emit(
2008 this.terminal.encodeKey({
2009 code: key,
2010 key,
2011 ctrlKey: event.ctrlKey,
2012 altKey: event.altKey,
2013 metaKey: event.metaKey,
2014 shiftKey: event.shiftKey,
2015 }),
2016 );
2017 }
2018 }
2019 };
2020 const onTouchEnd = (event: PointerEvent) => {
2021 if (touch?.pointerId !== event.pointerId || event.pointerType !== "touch") return;
2022 const gesture = touch;
2023 touch = null;
2024 event.preventDefault();
2025 if (this.viewportEl.hasPointerCapture?.(event.pointerId)) {
2026 this.viewportEl.releasePointerCapture(event.pointerId);
2027 }
2028 if (event.type !== "pointerup" || gesture.moved) return;
2029 if (mouseClaimed(event)) {
2030 forward(event, "press", 0);
2031 forward(event, "release", 0);
2032 } else {
2033 activateLink(event, gesture.target);
2034 this.element.ownerDocument.getSelection()?.removeAllRanges();
2035 }
2036 this.focus();
2037 };
2038
2039 this.detach.push(() => {
2040 this.viewportEl.removeEventListener("pointerdown", onTouchStart);
2041 this.viewportEl.removeEventListener("pointermove", onTouchMove);
2042 this.viewportEl.removeEventListener("pointerup", onTouchEnd);
2043 this.viewportEl.removeEventListener("pointercancel", onTouchEnd);
2044 this.viewportEl.removeEventListener("lostpointercapture", onTouchEnd);
2045 el.removeEventListener("click", onClick);
2046 el.removeEventListener("mousedown", onMouseDown);
2047 el.ownerDocument.removeEventListener("mouseup", finishOutputSelection);
2048 el.ownerDocument.removeEventListener("pointercancel", cancelOutputSelection);
2049 el.ownerDocument.defaultView?.removeEventListener("blur", cancelOutputSelection);
2050 el.removeEventListener("mouseup", onMouseUp);
2051 el.removeEventListener("mousemove", onMouseMove);
2052 el.removeEventListener("contextmenu", onContextMenu);
2053 el.removeEventListener("keydown", onKeyDown);
2054 el.removeEventListener("paste", onPaste);
2055 el.removeEventListener("wheel", onWheel);
2056 el.removeEventListener("focus", onFocus);
2057 this.input.removeEventListener("compositionstart", onCompositionStart);
2058 this.input.removeEventListener("compositionend", onCompositionEnd);
2059 this.input.removeEventListener("input", onInput);
2060 this.input.removeEventListener("focus", onFocusChange);
2061 this.input.removeEventListener("blur", onFocusChange);
2062 el.removeEventListener("blur", onFocusChange);
2063 });
2064 this.viewportEl.addEventListener("pointerdown", onTouchStart);
2065 this.viewportEl.addEventListener("pointermove", onTouchMove);
2066 this.viewportEl.addEventListener("pointerup", onTouchEnd);
2067 this.viewportEl.addEventListener("pointercancel", onTouchEnd);
2068 this.viewportEl.addEventListener("lostpointercapture", onTouchEnd);
2069 el.addEventListener("click", onClick);
2070 el.addEventListener("mousedown", onMouseDown);
2071 el.ownerDocument.addEventListener("mouseup", finishOutputSelection);
2072 el.ownerDocument.addEventListener("pointercancel", cancelOutputSelection);
2073 el.ownerDocument.defaultView?.addEventListener("blur", cancelOutputSelection);
2074 el.addEventListener("mouseup", onMouseUp);
2075 el.addEventListener("mousemove", onMouseMove);
2076 el.addEventListener("contextmenu", onContextMenu);
2077 el.addEventListener("keydown", onKeyDown);
2078 el.addEventListener("paste", onPaste);
2079 el.addEventListener("wheel", onWheel, { passive: false });
2080 el.addEventListener("focus", onFocus);
2081 this.input.addEventListener("compositionstart", onCompositionStart);
2082 this.input.addEventListener("compositionend", onCompositionEnd);
2083 this.input.addEventListener("input", onInput);
2084 this.input.addEventListener("focus", onFocusChange);
2085 this.input.addEventListener("blur", onFocusChange);
2086 el.addEventListener("blur", onFocusChange);
2087 }
2088
2089 dispose(): void {
2090 if (this.disposedView) return;
2091 this.disposedView = true;
2092 this.predictor.reset();
2093 this.overlaidRows.clear();
2094 this.frameReader.dispose();
2095 if (this.synchronizedOutputTimeout !== null) {
2096 clearTimeout(this.synchronizedOutputTimeout);
2097 this.synchronizedOutputTimeout = null;
2098 }
2099 this.observer?.disconnect();
2100 this.metricObserver?.disconnect();
2101 for (const fn of this.detach) fn();
2102 this.rowsHost.remove();
2103 this.viewportEl.remove();
2104 this.cursorEl.remove();
2105 this.stickyEl?.remove();
2106 this.scrollbarEl?.remove();
2107 this.metricProbe.remove();
2108 this.input.remove();
2109 for (const name of ROOT_CLASSES) {
2110 if (this.initialClasses.has(name)) this.element.classList.add(name);
2111 else this.element.classList.remove(name);
2112 }
2113 for (const { name, value, priority } of this.initialRootStyles) {
2114 if (value === "") this.element.style.removeProperty(name);
2115 else this.element.style.setProperty(name, value, priority);
2116 }
2117 if (this.initialTabIndex === null) this.element.removeAttribute("tabindex");
2118 else this.element.setAttribute("tabindex", this.initialTabIndex);
2119 }
2120}
packages/terminal/src/renderer/terminal.css created+482
......@@ -0,0 +1,482 @@
1/* @clo/terminal DOM renderer. Effective VT defaults occupy renderer-owned
2 custom properties; host theme properties remain the fallback seams. */
3
4@font-face {
5 font-family: "__clo_terminal_primary_34_8_1";
6 src: url("../../fonts/__clo_terminal_primary_34_8_1-Regular.woff2") format("woff2");
7 font-style: normal;
8 font-weight: 400;
9}
10
11@font-face {
12 font-family: "__clo_terminal_primary_34_8_1";
13 src: url("../../fonts/__clo_terminal_primary_34_8_1-SemiBold.woff2") format("woff2");
14 font-style: normal;
15 font-weight: 600;
16}
17
18@font-face {
19 font-family: "__clo_terminal_primary_34_8_1";
20 src: url("../../fonts/__clo_terminal_primary_34_8_1-Italic.woff2") format("woff2");
21 font-style: italic;
22 font-weight: 400;
23}
24
25@font-face {
26 font-family: "__clo_terminal_primary_34_8_1";
27 src: url("../../fonts/__clo_terminal_primary_34_8_1-SemiBoldItalic.woff2") format("woff2");
28 font-style: italic;
29 font-weight: 600;
30}
31
32@font-face {
33 font-family: "__clo_terminal_symbols_3_4_0";
34 src: url("../../fonts/__clo_terminal_symbols_3_4_0-Regular.woff2") format("woff2");
35 font-style: normal;
36 font-weight: 400;
37}
38
39@font-face {
40 font-family: "__clo_terminal_symbols_3_4_0";
41 src: url("../../fonts/__clo_terminal_symbols_3_4_0-Regular.woff2") format("woff2");
42 font-style: normal;
43 font-weight: 600;
44}
45
46@font-face {
47 font-family: "__clo_terminal_symbols_3_4_0";
48 src: url("../../fonts/__clo_terminal_symbols_3_4_0-Regular.woff2") format("woff2");
49 font-style: italic;
50 font-weight: 400;
51}
52
53@font-face {
54 font-family: "__clo_terminal_symbols_3_4_0";
55 src: url("../../fonts/__clo_terminal_symbols_3_4_0-Regular.woff2") format("woff2");
56 font-style: italic;
57 font-weight: 600;
58}
59
60.clo-term-mount,
61.clo-term {
62 /* `--clo-term-font` replaces the TEXT face and nothing else. It resolves to an
63 undefined family when unset, which a stack skips, so the bundled faces stay
64 behind whatever a consumer names — a product font with no box drawing in it
65 still draws boxes. */
66 font-family:
67 var(--clo-term-font, __clo_terminal_consumer_unset), "__clo_terminal_primary_34_8_1",
68 "__clo_terminal_symbols_3_4_0", ui-monospace, "SF Mono", Menlo, monospace;
69 font-size: var(--clo-term-font-size, 13px);
70 line-height: var(--clo-term-line-height, 1.3);
71}
72
73.clo-term {
74 position: relative;
75 overflow: hidden;
76 background: var(--clo-term-effective-bg, var(--clo-term-bg, #1e1e2e));
77 color: var(--clo-term-effective-fg, var(--clo-term-fg, #cdd6f4));
78 font-synthesis: none;
79 user-select: text;
80 cursor: text;
81}
82
83.clo-term:focus {
84 outline: none;
85}
86
87.clo-term.clo-term-font-loading > :not(.clo-term-input) {
88 visibility: hidden;
89}
90
91.clo-term-viewport {
92 position: relative;
93 overflow: hidden;
94}
95
96.clo-term-rows {
97 position: absolute;
98 inset: 0 auto auto 0;
99 white-space: pre;
100 will-change: transform;
101}
102
103.clo-term-row {
104 height: var(--clo-term-cell-h, 1.3em);
105 direction: ltr;
106}
107
108.clo-term-cell {
109 display: inline-block;
110 width: calc(var(--clo-term-cell-columns, 1) * var(--clo-term-cell-w, 1ch));
111 height: var(--clo-term-cell-h, 1.3em);
112 box-sizing: border-box;
113 overflow: hidden;
114 font-kerning: none;
115 font-variant-ligatures: none;
116 line-height: var(--clo-term-cell-h, 1.3em);
117 vertical-align: top;
118}
119
120.clo-term-spacer-head {
121 user-select: none;
122}
123
124/* Browser fallback fonts size CJK and color emoji at one em, which is often
125 only ~1.5 primary-monospace cells. Ghostty says these graphemes occupy two
126 columns, so size their square em to the exact two-cell advance. A compact run
127 carries the same class with `data-columns` counting its ASCII cells, not one
128 grapheme's width, so a two-column run must never be resized. */
129.clo-term-cell[data-columns="2"]:not(.clo-term-compact) {
130 font-size: min(calc(var(--clo-term-cell-w, 1ch) * 2), var(--clo-term-cell-h, 1.3em));
131}
132
133.clo-term-glyph {
134 display: inline-block;
135 transform-origin: center;
136}
137
138/* Filled Powerline separators own the exact cell rectangle instead of using a
139 square fallback-font advance. The text node remains for selection and a11y. */
140.clo-term-powerline {
141 position: relative;
142 width: 100%;
143 height: 100%;
144 vertical-align: top;
145}
146
147.clo-term-powerline > .clo-term-ink {
148 position: absolute;
149 inset: 0;
150 overflow: hidden;
151 text-decoration-color: inherit;
152 -webkit-text-fill-color: transparent;
153}
154
155.clo-term-powerline::before {
156 position: absolute;
157 inset: -0.5px;
158 background: currentColor;
159 content: "";
160}
161
162.clo-term-run.overline > .clo-term-cell > .clo-term-powerline::after,
163.clo-term-run.strikethrough > .clo-term-cell > .clo-term-powerline > .clo-term-ink::before,
164.clo-term-run[class*="underline-"] > .clo-term-cell > .clo-term-powerline > .clo-term-ink::after,
165.clo-term-link:hover > .clo-term-cell > .clo-term-powerline > .clo-term-ink::after {
166 position: absolute;
167 right: 0;
168 left: 0;
169 content: "";
170}
171
172.clo-term-run.overline > .clo-term-cell > .clo-term-powerline::after {
173 top: 0;
174 height: 1px;
175 background: currentColor;
176}
177
178.clo-term-run.strikethrough > .clo-term-cell > .clo-term-powerline > .clo-term-ink::before {
179 top: 50%;
180 height: 1px;
181 background: currentColor;
182}
183
184.clo-term-link:hover > .clo-term-cell > .clo-term-powerline > .clo-term-ink::after {
185 bottom: 0;
186 height: 1px;
187 background: currentColor;
188}
189
190.clo-term-run[class*="underline-"] > .clo-term-cell > .clo-term-powerline > .clo-term-ink::after {
191 bottom: 0;
192 height: 1px;
193 background: var(--clo-term-decoration-color, currentColor);
194}
195
196.clo-term-run.underline-2 > .clo-term-cell > .clo-term-powerline > .clo-term-ink::after {
197 height: 3px;
198 background: linear-gradient(
199 to bottom,
200 var(--clo-term-decoration-color, currentColor) 0 1px,
201 transparent 1px 2px,
202 var(--clo-term-decoration-color, currentColor) 2px 3px
203 );
204}
205
206.clo-term-run.underline-3 > .clo-term-cell > .clo-term-powerline > .clo-term-ink::after {
207 height: 3px;
208 background: var(--clo-term-decoration-color, currentColor);
209 mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='3'%3E%3Cpath d='M0 2 Q1.5 0 3 2 T6 2' fill='none' stroke='black' stroke-width='1'/%3E%3C/svg%3E")
210 repeat-x bottom / 6px 3px;
211}
212
213.clo-term-run.underline-4 > .clo-term-cell > .clo-term-powerline > .clo-term-ink::after {
214 background: repeating-linear-gradient(
215 to right,
216 var(--clo-term-decoration-color, currentColor) 0 1px,
217 transparent 1px 3px
218 );
219}
220
221.clo-term-run.underline-5 > .clo-term-cell > .clo-term-powerline > .clo-term-ink::after {
222 background: repeating-linear-gradient(
223 to right,
224 var(--clo-term-decoration-color, currentColor) 0 3px,
225 transparent 3px 5px
226 );
227}
228
229.clo-term-powerline-right-arrow::before {
230 clip-path: polygon(0 0, 100% 50%, 0 100%);
231}
232
233.clo-term-powerline-left-arrow::before {
234 clip-path: polygon(100% 0, 0 50%, 100% 100%);
235}
236
237.clo-term-powerline-right-round::before {
238 border-radius: 0 999px 999px 0;
239}
240
241.clo-term-powerline-left-round::before {
242 border-radius: 999px 0 0 999px;
243}
244
245.clo-term-powerline-lower-left::before {
246 clip-path: polygon(0 0, 100% 100%, 0 100%);
247}
248
249.clo-term-powerline-lower-right::before {
250 clip-path: polygon(100% 0, 100% 100%, 0 100%);
251}
252
253.clo-term-powerline-upper-left::before {
254 clip-path: polygon(0 0, 100% 0, 0 100%);
255}
256
257.clo-term-powerline-upper-right::before {
258 clip-path: polygon(0 0, 100% 0, 100% 100%);
259}
260
261/*
262 * Predictive local echo. A prediction is a claim about what the far end will
263 * echo, so it is drawn as a claim: dimmed, and underlined unless the embedder
264 * turns that off. Theming stays custom-property-only like every other color
265 * here, so a host can make speculation invisible without patching the renderer.
266 */
267.clo-term-run.predicted > .clo-term-cell > .clo-term-glyph {
268 opacity: var(--clo-term-predicted-opacity, 0.75);
269}
270.clo-term-run.predicted.underlined > .clo-term-cell > .clo-term-glyph > .clo-term-ink {
271 text-decoration-line: underline;
272 text-decoration-style: dotted;
273 text-decoration-color: var(--clo-term-predicted-underline, currentColor);
274}
275
276.clo-term-run.bold {
277 font-weight: 600;
278}
279.clo-term-run.italic {
280 font-style: italic;
281}
282.clo-term-run.faint > .clo-term-cell > .clo-term-glyph {
283 color: color-mix(in srgb, currentColor 55%, transparent);
284}
285.clo-term-run.invisible {
286 --clo-term-decoration-color: transparent !important;
287 color: transparent;
288}
289.clo-term-run.inverse {
290 color: var(--clo-term-effective-bg, var(--clo-term-bg, #1e1e2e));
291}
292.clo-term-run.inverse > .clo-term-cell {
293 background: var(--clo-term-effective-fg, var(--clo-term-fg, #cdd6f4));
294}
295.clo-term-run.strikethrough {
296 --clo-term-decoration-strike: line-through;
297}
298.clo-term-run.overline {
299 --clo-term-decoration-overline: overline;
300}
301.clo-term-run.strikethrough > .clo-term-cell > .clo-term-glyph,
302.clo-term-run.overline > .clo-term-cell > .clo-term-glyph {
303 text-decoration-line: var(--clo-term-decoration-overline,) var(--clo-term-decoration-strike,);
304 text-decoration-color: currentColor;
305}
306.clo-term-run[class*="underline-"] > .clo-term-cell > .clo-term-glyph > .clo-term-ink {
307 text-decoration-line: underline;
308 text-decoration-color: var(--clo-term-decoration-color, currentColor);
309}
310.clo-term-run.underline-3 > .clo-term-cell > .clo-term-glyph > .clo-term-ink {
311 text-decoration-style: wavy;
312}
313.clo-term-run.underline-4 > .clo-term-cell > .clo-term-glyph > .clo-term-ink {
314 text-decoration-style: dotted;
315}
316.clo-term-run.underline-5 > .clo-term-cell > .clo-term-glyph > .clo-term-ink {
317 text-decoration-style: dashed;
318}
319.clo-term-run.underline-2 > .clo-term-cell > .clo-term-glyph > .clo-term-ink {
320 text-decoration-style: double;
321}
322
323/* One terminal blink clock: ink is visible for the first half-second and
324 hidden for the second. Cell boxes never participate, so backgrounds hold. */
325@keyframes clo-term-blink {
326 0%,
327 49.999% {
328 --clo-term-blink-visibility: visible;
329 }
330 50%,
331 99.999% {
332 --clo-term-blink-visibility: hidden;
333 }
334 100% {
335 --clo-term-blink-visibility: visible;
336 }
337}
338
339.clo-term.clo-term-has-blink {
340 animation: clo-term-blink 1000ms linear infinite;
341}
342
343.clo-term-run.blink > .clo-term-cell > .clo-term-glyph,
344.clo-term-cursor.blinking:not(.blurred) {
345 visibility: var(--clo-term-blink-visibility, visible);
346}
347
348/* Sticky scroll: the command whose output you are scrolled inside, pinned at
349 the top. It overlays row 0 — the standard trade, same as VS Code's. */
350.clo-term-sticky {
351 position: absolute;
352 top: 0;
353 left: 0;
354 right: 0;
355 z-index: 2;
356 white-space: pre;
357 overflow: hidden;
358 text-overflow: ellipsis;
359 cursor: pointer;
360 padding: 0 4px;
361 background: var(--clo-term-sticky-bg, var(--clo-term-bg, #1e1e2e));
362 color: var(--clo-term-sticky-fg, var(--clo-term-fg, #cdd6f4));
363 box-shadow: 0 1px 0 var(--clo-term-sticky-border, rgba(205, 214, 244, 0.18));
364 opacity: 0.95;
365}
366
367.clo-term-has-scrollbar > .clo-term-sticky {
368 right: 13px;
369}
370
371.clo-term-sticky.failed {
372 color: var(--clo-term-sticky-failed, #f38ba8);
373}
374
375/* Links are detected per row; the underline only appears on hover so ordinary
376 output does not read as a wall of links. */
377.clo-term-link:hover > .clo-term-cell > .clo-term-glyph > .clo-term-ink {
378 text-decoration-line: underline;
379 cursor: pointer;
380}
381.clo-term-cursor {
382 position: absolute;
383 z-index: 1;
384 pointer-events: none;
385 background: var(--clo-term-effective-cursor, var(--clo-term-cursor, #f5e0dc));
386 mix-blend-mode: difference;
387}
388
389.clo-term-cursor.style-bar {
390 width: 2px !important;
391}
392
393.clo-term-cursor.style-underline {
394 height: 2px !important;
395}
396
397.clo-term-cursor.style-hollow {
398 background: transparent;
399 box-shadow: inset 0 0 0 1px var(--clo-term-effective-cursor, var(--clo-term-cursor, #f5e0dc));
400}
401
402.clo-term-cursor.blurred {
403 background: transparent;
404 box-shadow: inset 0 0 0 1px var(--clo-term-effective-cursor, var(--clo-term-cursor, #f5e0dc));
405}
406
407@media (prefers-reduced-motion: reduce) {
408 .clo-term.clo-term-has-blink {
409 --clo-term-blink-visibility: visible;
410 animation: none;
411 }
412}
413
414.clo-term-measure {
415 position: absolute;
416 visibility: hidden;
417 top: -9999px;
418 white-space: pre;
419}
420
421.clo-term-scrollbar {
422 position: absolute;
423 top: 3px;
424 right: 3px;
425 bottom: 3px;
426 z-index: 3;
427 width: 10px;
428 border-radius: 999px;
429 background: var(--clo-term-scrollbar-track, transparent);
430 opacity: 0;
431 transition: opacity 120ms ease-out;
432 touch-action: none;
433}
434
435.clo-term:hover > .clo-term-scrollbar,
436.clo-term-scrollbar:focus,
437.clo-term-scrollbar:focus-within {
438 opacity: 1;
439}
440
441.clo-term-scrollbar[hidden] {
442 display: none;
443}
444
445.clo-term-scrollbar-thumb {
446 width: 100%;
447 min-height: 24px;
448 border-radius: inherit;
449 background: var(--clo-term-scrollbar-thumb, rgb(205 214 244 / 28%));
450 transition: background-color 100ms ease-out;
451 will-change: transform;
452}
453
454.clo-term-scrollbar:hover .clo-term-scrollbar-thumb,
455.clo-term-scrollbar:focus .clo-term-scrollbar-thumb {
456 background: var(--clo-term-scrollbar-thumb-hover, rgb(205 214 244 / 48%));
457}
458
459.clo-term-input {
460 position: absolute;
461 width: 1px;
462 height: 1em;
463 padding: 0;
464 border: 0;
465 opacity: 0;
466 font: inherit;
467 resize: none;
468 overflow: hidden;
469}
470
471.clo-term ::selection {
472 background: var(--clo-term-selection, Highlight);
473}
474
475.clo-term-input.composing {
476 width: 20ch;
477 max-width: 100%;
478 opacity: 1;
479 color: inherit;
480 background: var(--clo-term-effective-bg, var(--clo-term-bg, #1e1e2e));
481 caret-color: var(--clo-term-cursor, currentColor);
482}
packages/terminal/src/runs.ts created+119
......@@ -0,0 +1,119 @@
1// Pure cell-run grouping: consecutive cells with identical presentation
2// collapse into one run so a DOM row is a handful of spans, not cols spans.
3// Links participate in the grouping (rather than splitting runs afterwards)
4// so a run never straddles a link boundary and column math stays exact.
5
6import type { CellSnapshot } from "./ghostty/terminal.ts";
7import type { DetectedLink } from "./links.ts";
8
9export interface CellRun {
10 text: string;
11 /** Total columns covered (wide cells count 2). */
12 columns: number;
13 /** Canonical references; run grouping must not allocate a second cell model. */
14 cells: CellSnapshot[];
15 /** Index into the row's link list, or -1. */
16 link: number;
17 /** Speculative local echo, not yet confirmed by the pty. */
18 predicted: boolean;
19}
20
21export interface RowText {
22 text: string;
23 /** `cellAt[offset]` identifies the canonical cell that owns that UTF-16 code unit. */
24 cellAt: number[];
25}
26
27function sameStyle(a: CellSnapshot, b: CellSnapshot): boolean {
28 return (
29 a.fg === b.fg &&
30 a.hasOsc8 === b.hasOsc8 &&
31 a.osc8Uri === b.osc8Uri &&
32 a.bg === b.bg &&
33 a.underlineColor === b.underlineColor &&
34 a.bold === b.bold &&
35 a.italic === b.italic &&
36 a.faint === b.faint &&
37 a.blink === b.blink &&
38 a.inverse === b.inverse &&
39 a.invisible === b.invisible &&
40 a.strikethrough === b.strikethrough &&
41 a.overline === b.overline &&
42 a.underline === b.underline
43 );
44}
45
46/** Program text for one cell. Spacer heads own geometry but carry no text. */
47export function cellText(cell: CellSnapshot): string {
48 if (cell.spacer === "head") return "";
49 return cell.text === "" ? " " : cell.text;
50}
51
52/** Concatenated row text, plus the cell index each character came from. */
53export function rowText(cells: readonly CellSnapshot[]): RowText {
54 let text = "";
55 const cellAt: number[] = [];
56 for (let i = 0; i < cells.length; i++) {
57 const piece = cellText(cells[i]!);
58 text += piece;
59 for (let c = 0; c < piece.length; c++) cellAt.push(i);
60 }
61 return { text, cellAt };
62}
63
64/**
65 * Map detected links (text offsets) onto cells: `out[cellIndex]` is the link's
66 * index, or -1. A cell belongs to the link that covers its first character.
67 */
68export function linkCells(
69 cells: readonly CellSnapshot[],
70 links: readonly DetectedLink[],
71 cellAt: readonly number[],
72): number[] {
73 const out: number[] = Array.from({ length: cells.length }, () => -1);
74 for (let l = 0; l < links.length; l++) {
75 const link = links[l]!;
76 for (let i = link.start; i < link.end && i < cellAt.length; i++) {
77 const cell = cellAt[i]!;
78 if (out[cell] === -1) out[cell] = l;
79 }
80 }
81 return out;
82}
83
84export function cellsToRuns(
85 cells: readonly CellSnapshot[],
86 links?: readonly number[],
87 predicted?: ReadonlySet<number>,
88): CellRun[] {
89 const runs: CellRun[] = [];
90 let current: CellRun | null = null;
91 let anchor: CellSnapshot | null = null;
92 for (let i = 0; i < cells.length; i++) {
93 const cell = cells[i]!;
94 const link = links?.[i] ?? -1;
95 const speculative = predicted?.has(i) ?? false;
96 if (
97 current !== null &&
98 anchor !== null &&
99 sameStyle(anchor, cell) &&
100 current.link === link &&
101 current.predicted === speculative
102 ) {
103 current.text += cellText(cell);
104 current.columns += cell.width;
105 current.cells.push(cell);
106 continue;
107 }
108 current = {
109 text: cellText(cell),
110 columns: cell.width,
111 cells: [cell],
112 link,
113 predicted: speculative,
114 };
115 anchor = cell;
116 runs.push(current);
117 }
118 return runs;
119}
packages/terminal/src/shell/commands.ts created+243
......@@ -0,0 +1,243 @@
1// The shell-integration command model: marks in, a list of commands out.
2//
3// This is the layer that makes a terminal more than a screen — command
4// boundaries, exit codes, cwd, and durations are exactly the "what did the
5// agent run, and did it fail" question, and they are emulator-independent.
6//
7// Pure by construction: screen anchoring arrives through the `anchor` seam
8// (the wasm layer hands back tracked grid refs; tests hand back counters), and
9// time through `now`. Nothing here touches wasm or the DOM.
10
11import type { ShellMark } from "./marks.ts";
12
13/**
14 * A position on the screen that survives scrolling, scrollback pruning and
15 * reflow. `point()` returns null once the anchored row is gone.
16 */
17export interface CommandAnchor {
18 /** Viewport coordinates for painting decorations; null when scrolled out. */
19 point(): { x: number; y: number } | null;
20 /** Screen coordinates (scrollback included) — stable, for ordering and reads. */
21 screenPoint(): { x: number; y: number } | null;
22 free(): void;
23}
24
25export type CommandState = "prompt" | "editing" | "running" | "finished";
26
27export interface ShellCommand {
28 readonly id: number;
29 /** From `633;E`, or read off the screen at execute time when the seam is wired. */
30 commandLine: string | null;
31 cwd: string | null;
32 /** Null while running, and after a `D` that carried no status. */
33 exitCode: number | null;
34 state: CommandState;
35 /** Milliseconds from the `now` seam; null until the phase is reached. */
36 executedAt: number | null;
37 finishedAt: number | null;
38 promptStart: CommandAnchor | null;
39 commandStart: CommandAnchor | null;
40 outputStart: CommandAnchor | null;
41 outputEnd: CommandAnchor | null;
42}
43
44export interface ShellCommandsOptions {
45 /**
46 * Anchor the current cursor position. Returning null (unanchorable point,
47 * alternate screen) degrades the command to a text-only record.
48 */
49 anchor?: () => CommandAnchor | null;
50 now?: () => number;
51 /**
52 * Read the command line off the screen between two anchors, for shells that
53 * emit OSC 133 without VS Code's `633;E`. Called once, at execute time.
54 */
55 readCommandLine?: (from: CommandAnchor, to: CommandAnchor) => string | null;
56 /**
57 * Retained command records. Anchors are freed on eviction — each tracked
58 * anchor costs bookkeeping on every terminal mutation, so this cap is a
59 * performance contract, not just memory hygiene.
60 */
61 limit?: number;
62 onCommandStart?: (command: ShellCommand) => void;
63 onCommandEnd?: (command: ShellCommand) => void;
64 /** Any change to the list, including cwd/command-line fills. */
65 onChange?: () => void;
66}
67
68const DEFAULT_LIMIT = 512;
69
70export class ShellCommands {
71 private options: ShellCommandsOptions;
72 private list: ShellCommand[] = [];
73 private open: ShellCommand | null = null;
74 private nextId = 1;
75 private cwdNow: string | null = null;
76
77 constructor(options: ShellCommandsOptions = {}) {
78 this.options = options;
79 }
80
81 /** Commands oldest-first. The last entry may still be running. */
82 get commands(): readonly ShellCommand[] {
83 return this.list;
84 }
85
86 /** The command currently at the prompt or running, if any. */
87 get current(): ShellCommand | null {
88 return this.open;
89 }
90
91 /** Last known cwd, tracked independently of command boundaries. */
92 get cwd(): string | null {
93 return this.cwdNow;
94 }
95
96 /** True once any recognized mark has arrived — i.e. the shell is instrumented. */
97 get active(): boolean {
98 return this.list.length > 0;
99 }
100
101 handle(mark: ShellMark): void {
102 switch (mark.kind) {
103 case "prompt-start": {
104 // A new prompt implies the previous command ended, even if its `D`
105 // never arrived (SIGKILL, a shell that lost the status, a torn replay).
106 this.closeOpen(null);
107 const command = this.begin();
108 command.promptStart = this.anchor();
109 this.changed();
110 break;
111 }
112
113 case "command-start": {
114 const command = this.open ?? this.begin();
115 command.commandStart = this.anchor();
116 command.state = "editing";
117 this.changed();
118 break;
119 }
120
121 case "command-executed": {
122 const command = this.open ?? this.begin();
123 // A command joins the list when it actually runs. Before `C` it is
124 // just a prompt the user may never submit.
125 if (!this.list.includes(command)) {
126 this.list.push(command);
127 this.evict();
128 }
129 command.outputStart = this.anchor();
130 command.state = "running";
131 command.executedAt = this.now();
132 if (command.commandLine === null && command.commandStart && command.outputStart) {
133 command.commandLine =
134 this.options.readCommandLine?.(command.commandStart, command.outputStart) ?? null;
135 }
136 this.options.onCommandStart?.(command);
137 this.changed();
138 break;
139 }
140
141 case "command-end":
142 this.closeOpen(mark.exitCode);
143 this.changed();
144 break;
145
146 case "command-line": {
147 const command = this.open ?? this.begin();
148 command.commandLine = mark.text;
149 this.changed();
150 break;
151 }
152
153 case "property":
154 if (mark.key === "Cwd") {
155 this.cwdNow = mark.value;
156 if (this.open) this.open.cwd = mark.value;
157 this.changed();
158 }
159 break;
160
161 case "set-mark":
162 break;
163 }
164 }
165
166 /** Free every anchor. Call before freeing the terminal. */
167 dispose(): void {
168 for (const command of this.list) freeAnchors(command);
169 this.list = [];
170 this.open = null;
171 }
172
173 private begin(): ShellCommand {
174 const command: ShellCommand = {
175 id: this.nextId++,
176 commandLine: null,
177 cwd: this.cwdNow,
178 exitCode: null,
179 state: "prompt",
180 executedAt: null,
181 finishedAt: null,
182 promptStart: null,
183 commandStart: null,
184 outputStart: null,
185 outputEnd: null,
186 };
187 this.open = command;
188 return command;
189 }
190
191 private closeOpen(exitCode: number | null): void {
192 const command = this.open;
193 if (command === null) return;
194 this.open = null;
195 // A command that never reached `C` never ran (an empty prompt, ^C at the
196 // prompt): it was never listed, so just release its anchors.
197 if (command.state === "prompt" || command.state === "editing") {
198 freeAnchors(command);
199 return;
200 }
201 command.state = "finished";
202 command.exitCode = exitCode;
203 command.finishedAt = this.now();
204 command.outputEnd = this.anchor();
205 this.options.onCommandEnd?.(command);
206 }
207
208 private evict(): void {
209 const limit = this.options.limit ?? DEFAULT_LIMIT;
210 // Never evict the open command — it is still accumulating.
211 while (this.list.length > limit && this.list[0] !== this.open) {
212 const dropped = this.list.shift();
213 if (dropped) freeAnchors(dropped);
214 }
215 }
216
217 private anchor(): CommandAnchor | null {
218 return this.options.anchor?.() ?? null;
219 }
220
221 private now(): number {
222 return this.options.now?.() ?? Date.now();
223 }
224
225 private changed(): void {
226 this.options.onChange?.();
227 }
228}
229
230function freeAnchors(command: ShellCommand): void {
231 for (const anchor of [
232 command.promptStart,
233 command.commandStart,
234 command.outputStart,
235 command.outputEnd,
236 ]) {
237 anchor?.free();
238 }
239 command.promptStart = null;
240 command.commandStart = null;
241 command.outputStart = null;
242 command.outputEnd = null;
243}
packages/terminal/src/shell/index.ts created+10
......@@ -0,0 +1,10 @@
1export { OscScanner, type OscSpan } from "./osc.ts";
2export { parseShellMark, unescapeMarkValue, type MarkSource, type ShellMark } from "./marks.ts";
3export { stickyCommand, stickyTarget } from "./sticky.ts";
4export {
5 ShellCommands,
6 type CommandAnchor,
7 type CommandState,
8 type ShellCommand,
9 type ShellCommandsOptions,
10} from "./commands.ts";
packages/terminal/src/shell/marks.ts created+120
......@@ -0,0 +1,120 @@
1// OSC payload -> shell-integration mark.
2//
3// Two dialects, same shape. OSC 133 is the FinalTerm/iTerm2 semantic-prompt
4// convention every shell-integration script emits; OSC 633 is VS Code's
5// superset, which adds the command line itself (`E`) and properties (`P`) —
6// the reason we prefer it when both are present. Everything here is a pure
7// string->value mapping; the state machine lives in commands.ts.
8//
9// Unrecognized payloads return null by design: a scrollback replay can open
10// mid-sequence and hand this layer a torn prefix, and shells emit plenty of
11// OSC we do not model.
12
13export type MarkSource = 133 | 633;
14
15export type ShellMark =
16 /** `A` — prompt begins. */
17 | { kind: "prompt-start"; source: MarkSource }
18 /** `B` — prompt ends, the user's command line begins. */
19 | { kind: "command-start"; source: MarkSource }
20 /** `C` — the command was submitted; output begins. */
21 | { kind: "command-executed"; source: MarkSource }
22 /** `D[;exit]` — the command finished. */
23 | { kind: "command-end"; exitCode: number | null; source: MarkSource }
24 /** `633;E` — the command line, reported rather than read off the screen. */
25 | { kind: "command-line"; text: string; source: MarkSource }
26 /** `P;Key=Value` — Cwd and friends. */
27 | { kind: "property"; key: string; value: string; source: MarkSource }
28 /** `633;SetMark` — a user bookmark, unrelated to command boundaries. */
29 | { kind: "set-mark"; source: MarkSource };
30
31/**
32 * Undo the VS Code shell-integration escaping (`\xAB` hex, `\\` literal).
33 * Applied to `633;E` command lines and `633;P` values, which would otherwise
34 * be unable to carry `;` or newlines.
35 */
36export function unescapeMarkValue(text: string): string {
37 if (!text.includes("\\")) return text;
38 let out = "";
39 for (let i = 0; i < text.length; i++) {
40 const ch = text[i]!;
41 if (ch !== "\\") {
42 out += ch;
43 continue;
44 }
45 const next = text[i + 1];
46 if (next === "\\") {
47 out += "\\";
48 i++;
49 } else if (next === "x" || next === "X") {
50 const hex = text.slice(i + 2, i + 4);
51 if (/^[0-9a-fA-F]{2}$/.test(hex)) {
52 out += String.fromCharCode(Number.parseInt(hex, 16));
53 i += 3;
54 } else {
55 out += ch;
56 }
57 } else {
58 out += ch;
59 }
60 }
61 return out;
62}
63
64/** Parse one OSC payload (the bytes between `ESC ]` and the terminator). */
65export function parseShellMark(payload: string): ShellMark | null {
66 const semi = payload.indexOf(";");
67 if (semi <= 0) return null;
68 const code = payload.slice(0, semi);
69 if (code !== "133" && code !== "633") return null;
70 const source: MarkSource = code === "633" ? 633 : 133;
71 const body = payload.slice(semi + 1);
72 const parts = body.split(";");
73 const command = parts[0] ?? "";
74
75 switch (command) {
76 case "A":
77 return { kind: "prompt-start", source };
78 case "B":
79 return { kind: "command-start", source };
80 case "C":
81 return { kind: "command-executed", source };
82 case "D": {
83 const raw = parts[1];
84 // `D` with no argument means "finished, exit status unknown" — a shell
85 // that lost the status, or a command interrupted before it reported one.
86 const exitCode =
87 raw === undefined || raw === "" ? null : Number.isNaN(Number(raw)) ? null : Number(raw);
88 return { kind: "command-end", exitCode, source };
89 }
90 case "E": {
91 // `633;E;<commandline>[;nonce]`. The command line may itself contain
92 // escaped separators, so rejoin everything but a trailing nonce.
93 if (source !== 633) return null;
94 const rest = parts.slice(1);
95 if (rest.length === 0) return null;
96 // The nonce (when present) is the last field and never contains an
97 // escape; treat a trailing field as a nonce only if more than one field
98 // survived the split.
99 const text = unescapeMarkValue(rest.length > 1 ? rest.slice(0, -1).join(";") : rest[0]!);
100 return { kind: "command-line", text, source };
101 }
102 case "P": {
103 const pair = parts.slice(1).join(";");
104 const eq = pair.indexOf("=");
105 if (eq <= 0) return null;
106 return {
107 kind: "property",
108 key: pair.slice(0, eq),
109 value: unescapeMarkValue(pair.slice(eq + 1)),
110 source,
111 };
112 }
113 case "SetMark":
114 return { kind: "set-mark", source };
115 default:
116 // F/G (continuation), H/I (right prompt) and anything newer: recognized
117 // as "not ours" rather than misread.
118 return null;
119 }
120}
packages/terminal/src/shell/osc.ts created+142
......@@ -0,0 +1,142 @@
1// Streaming OSC scanner over the raw pty byte stream.
2//
3// Why a second parser at all: libghostty-vt consumes OSC internally and only
4// surfaces the handful it models (title, pwd, clipboard). The shell-integration
5// command model needs OSC 133 and VS Code's OSC 633, which the core does not
6// hand back. This scanner watches the same bytes on the way in.
7//
8// It is deliberately *not* a VT parser. It recognizes exactly one construct —
9// `ESC ]` … terminator — which is enough to lift OSC payloads out, and is
10// resilient to the wire's realities: sequences split across writes (state
11// carries), a replayed scrollback head that opens mid-sequence (garbage
12// payloads fail to parse one layer up), and unbounded OSC floods (capped).
13
14/** Payload cap: longer OSC bodies are dropped, not buffered (ghostty-web #186). */
15const MAX_PAYLOAD = 32 * 1024;
16
17const BEL = 0x07;
18const ESC = 0x1b;
19const CAN = 0x18;
20const SUB = 0x1a;
21const BACKSLASH = 0x5c;
22
23export interface OscSpan {
24 /**
25 * Byte offset in the current chunk one past the sequence terminator. Writes
26 * split here so a mark's anchor sees the cursor exactly where the shell put it.
27 */
28 end: number;
29 /** Payload between `ESC ]` and the terminator, decoded as UTF-8. */
30 payload: string;
31}
32
33// A plain const rather than `const enum`: erasable-syntax-only toolchains
34// (node --experimental-strip-types among them) reject const enums outright.
35const State = {
36 Ground: 0,
37 Esc: 1,
38 Osc: 2,
39 OscEsc: 3,
40} as const;
41
42const decoder = new TextDecoder();
43
44type StateValue = (typeof State)[keyof typeof State];
45
46export class OscScanner {
47 private state: StateValue = State.Ground;
48 private buf: number[] = [];
49 /** Payload blew the cap: consume to the terminator, then emit nothing. */
50 private overflowed = false;
51
52 /**
53 * Feed a chunk; returns the OSC sequences that *ended* in it, in order.
54 * State carries across calls, so a sequence opened in an earlier chunk is
55 * reported against the chunk that closed it.
56 */
57 scan(bytes: Uint8Array): OscSpan[] {
58 const spans: OscSpan[] = [];
59 for (let i = 0; i < bytes.length; i++) {
60 const byte = bytes[i]!;
61 switch (this.state) {
62 case State.Ground: {
63 // Ordinary output is the overwhelming majority of the stream and
64 // contains no ESC. Skip to the next one natively rather than
65 // switch-dispatching per byte.
66 if (byte === ESC) {
67 this.state = State.Esc;
68 break;
69 }
70 const next = bytes.indexOf(ESC, i + 1);
71 if (next === -1) return spans;
72 i = next - 1;
73 break;
74 }
75
76 case State.Esc:
77 // `ESC ]` opens an OSC; anything else is some other escape we ignore
78 // (but a second ESC keeps us in Esc — ESC ESC ] is still an opener).
79 if (byte === 0x5d) {
80 this.state = State.Osc;
81 this.buf = [];
82 this.overflowed = false;
83 } else if (byte !== ESC) {
84 this.state = State.Ground;
85 }
86 break;
87
88 case State.Osc:
89 if (byte === BEL) {
90 this.emit(spans, i + 1);
91 } else if (byte === ESC) {
92 this.state = State.OscEsc;
93 } else if (byte === CAN || byte === SUB) {
94 // xterm's abort characters: the sequence is cancelled, not emitted.
95 this.reset();
96 } else if (!this.overflowed) {
97 this.buf.push(byte);
98 if (this.buf.length > MAX_PAYLOAD) {
99 this.buf = [];
100 this.overflowed = true;
101 }
102 }
103 break;
104
105 case State.OscEsc:
106 if (byte === BACKSLASH) {
107 // ST terminator.
108 this.emit(spans, i + 1);
109 } else {
110 // A bare ESC inside the payload: the sequence is malformed. Drop it
111 // and re-dispatch this byte from ground so a following `ESC ]` is
112 // not swallowed.
113 this.reset();
114 i--;
115 }
116 break;
117 }
118 }
119 return spans;
120 }
121
122 /** True when a sequence is mid-flight and its bytes are still buffered. */
123 get pending(): boolean {
124 return this.state !== State.Ground;
125 }
126
127 /** Drop carried state — e.g. before replaying a scrollback snapshot. */
128 reset(): void {
129 this.state = State.Ground;
130 this.buf = [];
131 this.overflowed = false;
132 }
133
134 private emit(spans: OscSpan[], end: number): void {
135 if (!this.overflowed) spans.push({ end, payload: decodePayload(this.buf) });
136 this.reset();
137 }
138}
139
140function decodePayload(buf: number[]): string {
141 return decoder.decode(new Uint8Array(buf));
142}
packages/terminal/src/shell/sticky.ts created+49
......@@ -0,0 +1,49 @@
1// Sticky scroll: which command's header belongs pinned at the top of the
2// viewport right now.
3//
4// The question only has an answer because command boundaries are anchored in
5// screen space and survive scrolling (`commands.ts`). Scroll into the middle of
6// a long build and the command that produced it has left the screen; pinning it
7// is what turns scrollback into something you can read without losing your
8// place.
9//
10// Pure: takes the viewport's top row and the command list, returns the command
11// (or null). No DOM, no wasm — the renderer supplies `viewportTop` from the
12// scrollbar, whose `offset` is the viewport's top row in the same screen
13// coordinate space anchors report.
14
15import type { ShellCommand } from "./commands.ts";
16
17/**
18 * The command whose output the viewport is currently inside, if that command's
19 * own line has scrolled off the top.
20 *
21 * Returns null when the command's line is still visible (pinning it would show
22 * it twice), when the viewport is above every command, or when the shell is
23 * uninstrumented.
24 */
25export function stickyCommand(
26 commands: readonly ShellCommand[],
27 viewportTop: number,
28): ShellCommand | null {
29 let sticky: ShellCommand | null = null;
30 for (const command of commands) {
31 const start = (command.commandStart ?? command.promptStart)?.screenPoint();
32 if (start === null || start === undefined) continue;
33 // Commands are in order, so the first one starting at or below the
34 // viewport top ends the search.
35 if (start.y >= viewportTop) break;
36 // A finished command whose output ended above the viewport is behind us.
37 // A running command has no end yet and extends to the bottom.
38 const end = command.outputEnd?.screenPoint();
39 if (end !== null && end !== undefined && end.y < viewportTop) continue;
40 sticky = command;
41 }
42 return sticky;
43}
44
45/** The row to scroll to when the pinned header is activated. */
46export function stickyTarget(command: ShellCommand): number | null {
47 const start = (command.promptStart ?? command.commandStart)?.screenPoint();
48 return start?.y ?? null;
49}
packages/terminal/src/solid.tsx created+83
......@@ -0,0 +1,83 @@
1/** @jsxImportSource solid-js */
2
3import { createEffect, onCleanup, splitProps, untrack, type JSX } from "solid-js";
4
5import { ansiToHtml } from "./ansi.ts";
6import type { GhosttyModule } from "./ghostty/module.ts";
7import { mountTerminal, type MountTerminalOptions, type TerminalHandle } from "./mount.ts";
8
9export type AnsiTextProps = { text: string } & Omit<
10 JSX.HTMLAttributes<HTMLSpanElement>,
11 "children" | "innerHTML"
12>;
13
14/** Static ANSI text with no terminal viewport, input, or scroll behavior. */
15export function AnsiText(props: AnsiTextProps): JSX.Element {
16 const [local, attributes] = splitProps(props, ["text"]);
17 return <span {...attributes} innerHTML={ansiToHtml(local.text)} />;
18}
19
20export interface TerminalPaneProps extends MountTerminalOptions {
21 module: GhosttyModule;
22 /** Receives the mounted handle and may return caller cleanup for unmount/remount. */
23 onReady?: (handle: TerminalHandle) => void | (() => void);
24 class?: string;
25 classList?: Record<string, boolean | undefined>;
26 style?: JSX.CSSProperties | string;
27}
28
29/** Solid lifecycle adapter over `mountTerminal`. Creation options are read when `module` mounts. */
30export function TerminalPane(props: TerminalPaneProps): JSX.Element {
31 let host: HTMLDivElement | undefined;
32
33 createEffect(() => {
34 const module = props.module;
35 const container = host;
36 if (container === undefined) throw new Error("solid terminal host did not mount");
37 const options = untrack<MountTerminalOptions>(() => ({
38 ...props,
39 onData: (bytes) => props.onData?.(bytes),
40 onBeforeKey: (event) => props.onBeforeKey?.(event) ?? true,
41 onResize: (size) => props.onResize?.(size),
42 onTitleChanged: (title) => props.onTitleChanged?.(title),
43 onPwdChanged: (pwd) => props.onPwdChanged?.(pwd),
44 onBell: () => props.onBell?.(),
45 onShellMark: (mark) => props.onShellMark?.(mark),
46 onCommandStart: (command) => props.onCommandStart?.(command),
47 onCommandEnd: (command) => props.onCommandEnd?.(command),
48 onClipboardWrite: (write) => props.onClipboardWrite?.(write),
49 }));
50 const handle = mountTerminal(container, module, options);
51 let cleanup: void | (() => void);
52 onCleanup(() => {
53 try {
54 cleanup?.();
55 } finally {
56 handle.dispose();
57 }
58 });
59 try {
60 cleanup = untrack(() => props.onReady?.(handle));
61 } catch (error) {
62 handle.dispose();
63 throw error;
64 }
65 });
66
67 return (
68 <div
69 class={[
70 "clo-term-mount",
71 props.class,
72 ...Object.entries(props.classList ?? {})
73 .filter(([name, enabled]) => name !== "clo-term-mount" && enabled)
74 .map(([name]) => name),
75 ]
76 .filter(Boolean)
77 .join(" ")}
78 style={props.style}
79 >
80 <div ref={(element) => (host = element)} style={{ width: "100%", height: "100%" }} />
81 </div>
82 );
83}
packages/terminal/test/dom.test.ts created+217
......@@ -0,0 +1,217 @@
1// @vitest-environment jsdom
2import * as NodeFS from "node:fs";
3import * as NodeURL from "node:url";
4import { afterEach, beforeAll, beforeEach, expect, it, vi } from "vite-plus/test";
5
6import { GhosttyModule, mountTerminal, type TerminalHandle } from "../src/index.ts";
7
8let module: GhosttyModule;
9let cellWidth = 7.8;
10let cellHeight = 15.6;
11const handles = new Set<TerminalHandle>();
12const decoder = new TextDecoder();
13
14beforeAll(async () => {
15 module = await GhosttyModule.load(
16 new Uint8Array(
17 NodeFS.readFileSync(
18 NodeURL.fileURLToPath(import.meta.url).replace(
19 /test\/dom\.test\.ts$/,
20 "vendor/ghostty-vt.wasm",
21 ),
22 ),
23 ),
24 );
25});
26
27beforeEach(() => {
28 cellWidth = 7.8;
29 cellHeight = 15.6;
30 vi.stubGlobal("requestAnimationFrame", () => 0);
31 vi.stubGlobal("cancelAnimationFrame", () => {});
32 vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null);
33 vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function (
34 this: HTMLElement,
35 ) {
36 if (this.classList.contains("clo-term-measure")) {
37 return new DOMRect(0, 0, cellWidth * 32, cellHeight);
38 }
39 if (this.classList.contains("clo-term-viewport")) {
40 return new DOMRect(
41 80,
42 120,
43 Number.parseFloat(this.style.width),
44 Number.parseFloat(this.style.height),
45 );
46 }
47 return new DOMRect(72, 108, 800, 800);
48 });
49});
50
51afterEach(() => {
52 handles.forEach((handle) => handle.dispose());
53 handles.clear();
54 document.body.replaceChildren();
55 vi.restoreAllMocks();
56 vi.unstubAllGlobals();
57});
58
59async function mount() {
60 const host = document.createElement("div");
61 document.body.append(host);
62 const reports: string[] = [];
63 const handle = mountTerminal(host, module, {
64 scrollbar: false,
65 stickyScroll: false,
66 onData: (bytes) => reports.push(decoder.decode(bytes)),
67 });
68 handles.add(handle);
69 await handle.ready;
70 handle.write("\x1b[?1049h\x1b[?1003h\x1b[?1006h");
71 handle.view.paint();
72 const viewport = host.querySelector<HTMLElement>(".clo-term-viewport")!;
73 const rows = host.querySelector<HTMLElement>(".clo-term-rows")!;
74 return { handle, reports, viewport, rows };
75}
76
77it("reports the rendered cell across a grid with fractional font metrics and after a font change", async () => {
78 const { handle, reports, viewport, rows } = await mount();
79
80 for (const metrics of [
81 [7.8, 15.6],
82 [6.5, 14.3],
83 ]) {
84 [cellWidth, cellHeight] = metrics;
85 handle.view.refreshMetrics();
86 handle.view.paint();
87 const renderedWidth = Number.parseFloat(rows.style.getPropertyValue("--clo-term-cell-w"));
88 const renderedHeight = Number.parseFloat(rows.style.getPropertyValue("--clo-term-cell-h"));
89
90 for (const col of [0, 1, 40, handle.terminal.cols - 1]) {
91 for (const row of [0, 1, 30, handle.terminal.rows - 1]) {
92 for (const fraction of [0.01, 0.5, 0.99]) {
93 const at = {
94 bubbles: true,
95 cancelable: true,
96 clientX: 80 + (col + fraction) * renderedWidth,
97 clientY: 120 + (row + fraction) * renderedHeight,
98 };
99 viewport.dispatchEvent(new MouseEvent("mousedown", at));
100 expect(reports.pop()).toBe(`\x1b[<0;${col + 1};${row + 1}M`);
101 viewport.dispatchEvent(new MouseEvent("mouseup", at));
102 expect(reports.pop()).toBe(`\x1b[<0;${col + 1};${row + 1}m`);
103 }
104 }
105 }
106 }
107});
108
109it("keeps wheel, drag and pixel reports in the same viewport coordinate system", async () => {
110 const { handle, reports, viewport, rows } = await mount();
111 const renderedWidth = Number.parseFloat(rows.style.getPropertyValue("--clo-term-cell-w"));
112 const renderedHeight = Number.parseFloat(rows.style.getPropertyValue("--clo-term-cell-h"));
113 const at = {
114 bubbles: true,
115 cancelable: true,
116 clientX: 80 + 70.5 * renderedWidth,
117 clientY: 120 + 40.5 * renderedHeight,
118 };
119 viewport.dispatchEvent(new MouseEvent("mousemove", { ...at, buttons: 1 }));
120 expect(reports.pop()).toBe("\x1b[<32;71;41M");
121 viewport.dispatchEvent(new WheelEvent("wheel", { ...at, deltaY: 10 }));
122 expect(reports.pop()).toBe("\x1b[<65;71;41M");
123
124 handle.write("\x1b[?1016h");
125 viewport.dispatchEvent(new MouseEvent("mousedown", { ...at, clientX: 311.5, clientY: 582.25 }));
126 expect(reports.pop()).toBe("\x1b[<0;232;462M");
127});
128
129function touchPointer(
130 target: HTMLElement,
131 type: string,
132 clientY: number,
133 options: { pointerId?: number; isPrimary?: boolean; clientX?: number } = {},
134) {
135 const event = new MouseEvent(type, {
136 bubbles: true,
137 cancelable: true,
138 clientX: options.clientX ?? 100,
139 clientY,
140 });
141 Object.defineProperties(event, {
142 pointerType: { value: "touch" },
143 pointerId: { value: options.pointerId ?? 1 },
144 isPrimary: { value: options.isPrimary ?? true },
145 });
146 target.dispatchEvent(event);
147 return event;
148}
149
150it("reports one tap at the touched cell and focuses the terminal input", async () => {
151 const { handle, reports, viewport } = await mount();
152 reports.length = 0;
153 touchPointer(viewport, "pointerdown", 120 + 2.5 * cellHeight, {
154 clientX: 80 + 3.5 * cellWidth,
155 });
156 expect(reports).toEqual([]);
157 touchPointer(viewport, "pointerup", 120 + 2.5 * cellHeight, {
158 clientX: 80 + 3.5 * cellWidth,
159 });
160 touchPointer(viewport, "click", 120 + 2.5 * cellHeight);
161 expect(reports).toEqual(["\x1b[<0;4;3M", "\x1b[<0;4;3m"]);
162 expect(document.activeElement).toBe(handle.view.input);
163});
164
165it("turns touch swipes into mouse wheel rows without also clicking", async () => {
166 const { reports, viewport } = await mount();
167 reports.length = 0;
168 touchPointer(viewport, "pointerdown", 240);
169 touchPointer(viewport, "pointermove", 240 - 2.5 * cellHeight);
170 touchPointer(viewport, "pointermove", 240 - 3.5 * cellHeight);
171 touchPointer(viewport, "pointerup", 240 - 3.5 * cellHeight);
172 expect(reports).toHaveLength(3);
173 expect(reports.every((report) => report.startsWith("\x1b[<65;"))).toBe(true);
174});
175
176it("scrolls normal output on touch without sending terminal input", async () => {
177 const { handle, reports, viewport } = await mount();
178 handle.write("\x1b[?1003l\x1b[?1049l");
179 handle.write(Array.from({ length: 100 }, (_, i) => `line ${i}\r\n`).join(""));
180 handle.view.paint();
181 reports.length = 0;
182 const before = handle.terminal.scrollbar().offset;
183 touchPointer(viewport, "pointerdown", 240);
184 touchPointer(viewport, "pointermove", 240 + 3 * cellHeight);
185 touchPointer(viewport, "pointerup", 240 + 3 * cellHeight);
186 expect(handle.terminal.scrollbar().offset).toBeLessThan(before);
187 expect(reports).toEqual([]);
188});
189
190it.each([
191 { application: false, expected: "\x1b[A" },
192 { application: true, expected: "\x1bOA" },
193])(
194 "scrolls a non-mouse TUI using its cursor-key mode: $application",
195 async ({ application, expected }) => {
196 const { handle, reports, viewport } = await mount();
197 handle.write(`\x1b[?1003l\x1b[?1${application ? "h" : "l"}`);
198 reports.length = 0;
199 touchPointer(viewport, "pointerdown", 240);
200 touchPointer(viewport, "pointermove", 240 + 2.5 * cellHeight);
201 touchPointer(viewport, "pointerup", 240 + 2.5 * cellHeight);
202 expect(reports).toEqual([expected, expected]);
203 },
204);
205
206it("ignores secondary touches and cancelled taps", async () => {
207 const { reports, viewport } = await mount();
208 reports.length = 0;
209 touchPointer(viewport, "pointerdown", 240, { pointerId: 2, isPrimary: false });
210 touchPointer(viewport, "pointerup", 240, { pointerId: 2, isPrimary: false });
211 touchPointer(viewport, "pointerdown", 240);
212 touchPointer(viewport, "pointerdown", 220, { pointerId: 2, isPrimary: false });
213 touchPointer(viewport, "pointermove", 180, { pointerId: 2, isPrimary: false });
214 touchPointer(viewport, "pointercancel", 240);
215 touchPointer(viewport, "pointerup", 240);
216 expect(reports).toEqual([]);
217});
packages/terminal/test/links.test.ts created+49
......@@ -0,0 +1,49 @@
1import { expect, it, vi } from "vite-plus/test";
2import { detectLinks } from "../src/links.ts";
3
4it.each([
5 String.raw`C:\repo\src\file.ts`,
6 "C:/repo/src/file.ts",
7 String.raw`\\server\share\file.ts`,
8 "~/src/file.ts",
9 "/repo/src/file.ts",
10 "../src/file.ts",
11 "src/file.ts",
12 String.raw`src\file.ts`,
13])("detects the full host path %s with its source position", (path) => {
14 const validate = vi.fn(() => true);
15 const text = `error (${path}:12:3):`;
16 expect(detectLinks(text, { validate })).toEqual([
17 {
18 kind: "path",
19 start: 7,
20 end: 7 + path.length + 5,
21 text: `${path}:12:3`,
22 target: path,
23 line: 12,
24 column: 3,
25 },
26 ]);
27 expect(validate).toHaveBeenCalledExactlyOnceWith(path);
28});
29
30it("validates the complete target and never falls back to an allowed basename", () => {
31 const validate = vi.fn((target: string) => target === "file.ts");
32 expect(detectLinks(String.raw`C:\repo\src\file.ts:12:3 ~/src/file.ts:7`, { validate })).toEqual(
33 [],
34 );
35 expect(validate.mock.calls.map(([target]) => target)).toEqual([
36 String.raw`C:\repo\src\file.ts`,
37 "~/src/file.ts",
38 ]);
39});
40
41it("keeps URL matches distinct from filesystem paths", () => {
42 const links = detectLinks("https://example.test/src/file.ts:12 ~/src/file.ts:3", {
43 validate: () => true,
44 });
45 expect(links.map(({ kind, target, line }) => ({ kind, target, line }))).toEqual([
46 { kind: "url", target: "https://example.test/src/file.ts:12", line: null },
47 { kind: "path", target: "~/src/file.ts", line: 3 },
48 ]);
49});
packages/terminal/test/terminal.test.ts created+66
......@@ -0,0 +1,66 @@
1import * as NodeFS from "node:fs";
2import * as NodeURL from "node:url";
3import { afterEach, beforeAll, expect, it } from "vite-plus/test";
4
5import { GhosttyModule, Terminal } from "../src/index.ts";
6
7let module: GhosttyModule;
8const terminals = new Set<Terminal>();
9
10beforeAll(async () => {
11 const path = NodeURL.fileURLToPath(import.meta.url).replace(
12 /test\/terminal\.test\.ts$/,
13 "vendor/ghostty-vt.wasm",
14 );
15 module = await GhosttyModule.load(new Uint8Array(NodeFS.readFileSync(path)));
16});
17
18afterEach(() => {
19 terminals.forEach((terminal) => terminal.free());
20 terminals.clear();
21});
22
23function create() {
24 const terminal = new Terminal(module, { cols: 20, rows: 4, maxScrollbackBytes: 1024 * 1024 });
25 terminals.add(terminal);
26 return terminal;
27}
28
29it("preserves Unicode, wide cells and ANSI under arbitrary byte chunking", () => {
30 const whole = create();
31 const split = create();
32 const bytes = new TextEncoder().encode("hello 👩‍💻 日本\r\n\x1b[31mred\x1b[0m");
33 whole.write(bytes);
34 for (let index = 0; index < bytes.length; index += 3) split.write(bytes.slice(index, index + 3));
35 expect(split.snapshot({ full: true })).toEqual(whole.snapshot({ full: true }));
36 expect(split.text()).toContain("hello 👩‍💻 日本");
37 expect(split.text()).toContain("red");
38});
39
40it("retains scrollback and reflows the same terminal through a resize", () => {
41 const terminal = create();
42 terminal.write("one\r\ntwo\r\nthree\r\nfour\r\nfive");
43 const before = terminal.scrollbar();
44 expect(before.total).toBeGreaterThan(before.len);
45 terminal.scrollToTop();
46 expect(terminal.scrollbar().offset).toBe(0);
47 terminal.resize(10, 3);
48 expect(terminal.cols).toBe(10);
49 expect(terminal.rows).toBe(3);
50 terminal.scrollToBottom();
51 expect(terminal.text()).toContain("five");
52});
53
54it("keeps a single destructive frame reader until disposal", () => {
55 const terminal = create();
56 const reader = terminal.acquireFrameReader();
57 terminal.write("hello");
58 expect(() => terminal.snapshot()).toThrow();
59 expect(
60 reader
61 .snapshot({ full: true })
62 .dirtyRows.some((row) => row.cells.some((cell) => cell.text === "h")),
63 ).toBe(true);
64 reader.dispose();
65 expect(terminal.snapshot({ full: true }).cols).toBe(20);
66});
packages/terminal/tsconfig.json created+22
......@@ -0,0 +1,22 @@
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "lib": ["ES2023", "DOM", "DOM.Iterable"],
5 "module": "Preserve",
6 "moduleResolution": "Bundler",
7 "jsx": "react-jsx",
8 "types": ["vite/client"],
9 "plugins": [
10 {
11 "name": "@effect/language-service",
12 "diagnosticSeverity": {
13 "globalDate": "off",
14 "globalConsole": "off",
15 "globalTimers": "off",
16 "globalFetch": "off"
17 }
18 }
19 ]
20 },
21 "include": ["src", "types"]
22}
packages/terminal/types/assets.d.ts created+6
......@@ -0,0 +1,6 @@
1declare module "*?url" {
2 const url: string;
3 export default url;
4}
5
6declare module "*.css";
packages/terminal/vendor/COMMIT created+1
......@@ -0,0 +1 @@
14c725242b7dbe8c77c6e227ef1f9540c5ef17921
packages/terminal/vendor/GHOSTTY-LICENSE created+21
......@@ -0,0 +1,21 @@
1MIT License
2
3Copyright (c) 2024 Mitchell Hashimoto, Ghostty contributors
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
packages/terminal/vendor/README.md created+35
......@@ -0,0 +1,35 @@
1# vendor/
2
3`ghostty-vt.wasm` is libghostty-vt built from the Ghostty source tree (MIT, `GHOSTTY-LICENSE`) at the commit pinned in `COMMIT`. `include/` is the matching C API header set, vendored for reference and as the source of truth for `gen-constants.mjs`.
4
5The vendored artifact is **pinned to the commit in `COMMIT`**. Do not update without running the full bump checklist — libghostty-vt has no tagged release, and every bump is a potential silent ABI break.
6
7## Rebuild recipe
8
9```sh
10git clone https://github.com/ghostty-org/ghostty.git # mirror; upstream is migrating forges
11cd ghostty && git checkout $(cat ../COMMIT)
12patch -p1 < <terminal-package>/vendor/patches/wasm-batched-vt.patch
13patch -p1 < <terminal-package>/vendor/patches/wasm-ascii-row.patch
14zig build -Demit-lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall
15cp zig-out/bin/ghostty-vt.wasm <here>
16node <terminal-package>/vendor/verify.mjs <ghostty-source>
17```
18
19The local patch keeps Ghostty's existing scalar `print_slice` batching enabled
20for wasm. Upstream's `simd=false` disables native SIMD dependencies, but the
21unpatched stream also falls back to one parser action per byte even though the
22batched path has a scalar decoder.
23
24The ASCII-row patch adds a fail-closed bulk export for narrow printable cells
25with default presentation or an explicit foreground. Unsupported rows return
26`GHOSTTY_NO_VALUE` and continue through the canonical per-cell API.
27
28Zig version must match `.minimum_zig_version` in `build.zig.zon` (0.16.0 at the pinned commit). If zig's fetcher fails with `TlsInitializationFailed` against `deps.files.ghostty.org`, curl the tarball it names and `zig fetch <file>` manually, then retry.
29
30## Bump checklist
31
321. New commit into `COMMIT`, rebuild wasm, re-vendor `include/`, and replace `SHA256` with the rebuilt wasm's lowercase SHA-256.
332. `node vendor/gen-constants.mjs` — regenerates `src/ghostty/constants.ts`; a changed enum value or struct layout is an API break to review, not noise.
343. `node vendor/verify.mjs <ghostty-source>` verifies the checked-in wasm hash, required exports and type metadata, patched source metadata, and the upstream wasm-allocator page-zeroing guard. The source argument is mandatory during a bump; `pnpm verify:vendor` performs the artifact-only checks on every package check.
354. Run the package test suite; `test/lifecycle.vi.ts` re-proves the grapheme create/free cycle against the new artifact.
packages/terminal/vendor/SHA256 created+1
......@@ -0,0 +1 @@
1d87ad59912c12ed2ca8f33da01137e6ea489a4de5dd181802d0f50d46c75c14d
packages/terminal/vendor/gen-constants.mjs created+62
......@@ -0,0 +1,62 @@
1// Regenerates src/ghostty/constants.ts from the vendored C headers.
2// Parses `typedef enum ... } Name;` blocks, tracking implicit +1 values.
3// Run from the package root: node vendor/gen-constants.mjs
4import { readFileSync, readdirSync, writeFileSync } from "node:fs";
5import { join, dirname } from "node:path";
6import { fileURLToPath } from "node:url";
7
8const root = dirname(fileURLToPath(import.meta.url));
9const headerDir = join(root, "include/ghostty/vt");
10const headers = readdirSync(headerDir, { recursive: true })
11 .filter((f) => String(f).endsWith(".h"))
12 .map((f) => readFileSync(join(headerDir, String(f)), "utf8"));
13
14const enums = new Map();
15const enumRe = /typedef enum[^{]*\{([\s\S]*?)\}\s*(\w+)\s*;/g;
16for (const src of headers) {
17 for (const m of src.matchAll(enumRe)) {
18 const [, body, name] = m;
19 const members = [];
20 let next = 0;
21 const stripped = body.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
22 for (const entry of stripped.split(",")) {
23 const em = entry.match(/([A-Z][A-Z0-9_]+)\s*(?:=\s*([^,\s]+))?/);
24 if (!em) continue;
25 const [, ident, valueExpr] = em;
26 if (ident.endsWith("_MAX_VALUE")) continue;
27 let value = next;
28 if (valueExpr !== undefined) {
29 if (/^-?\d+$/.test(valueExpr)) value = Number(valueExpr);
30 else if (/^0x[0-9a-fA-F]+$/.test(valueExpr)) value = Number(valueExpr);
31 else continue; // symbolic (GHOSTTY_ENUM_MAX_VALUE etc.)
32 }
33 members.push([ident, value]);
34 next = value + 1;
35 }
36 if (members.length > 0 && name.startsWith("Ghostty")) enums.set(name, members);
37 }
38}
39
40let out = `// GENERATED by vendor/gen-constants.mjs from the vendored libghostty-vt headers\n`;
41out += `// (commit in vendor/COMMIT). Do not edit by hand; re-run the generator on bump.\n\n`;
42for (const [name, members] of [...enums.entries()].sort(([a], [b]) => a.localeCompare(b))) {
43 out += `export const ${name} = {\n`;
44 for (const [ident, value] of members) out += ` ${ident}: ${value},\n`;
45 out += `} as const;\n\n`;
46}
47out = `${out.trimEnd()}\n`;
48const outPath = join(root, "../src/ghostty/constants.ts");
49if (process.argv.includes("--check")) {
50 const current = readFileSync(outPath, "utf8");
51 if (current !== out) {
52 let mismatch = 0;
53 while (current[mismatch] === out[mismatch]) mismatch++;
54 throw new Error(
55 `${outPath} is stale at byte ${mismatch}: expected ${JSON.stringify(out.slice(mismatch, mismatch + 80))}, got ${JSON.stringify(current.slice(mismatch, mismatch + 80))}; run node vendor/gen-constants.mjs`,
56 );
57 }
58 console.log(`verified ${outPath}: ${enums.size} enums`);
59} else {
60 writeFileSync(outPath, out);
61 console.log(`wrote ${outPath}: ${enums.size} enums`);
62}
packages/terminal/vendor/ghostty-vt.wasm created
Binary files /dev/null and b/packages/terminal/vendor/ghostty-vt.wasm differ
packages/terminal/vendor/include/ghostty.h created+1209
......@@ -0,0 +1,1209 @@
1// Ghostty embedding API. The documentation for the embedding API is
2// only within the Zig source files that define the implementations. This
3// isn't meant to be a general purpose embedding API (yet) so there hasn't
4// been documentation or example work beyond that.
5//
6// The only consumer of this API is the macOS app, but the API is built to
7// be more general purpose.
8#ifndef GHOSTTY_H
9#define GHOSTTY_H
10
11#ifdef __cplusplus
12extern "C" {
13#endif
14
15#include <stdbool.h>
16#include <stddef.h>
17#include <stdint.h>
18
19#ifdef _MSC_VER
20#include <BaseTsd.h>
21typedef SSIZE_T ssize_t;
22#else
23#include <sys/types.h>
24#endif
25
26//-------------------------------------------------------------------
27// Macros
28
29#define GHOSTTY_SUCCESS 0
30
31// Symbol visibility for shared library builds. On Windows, functions
32// are exported from the DLL when building and imported when consuming.
33// On other platforms with GCC/Clang, functions are marked with default
34// visibility so they remain accessible when the library is built with
35// -fvisibility=hidden. For static library builds, define GHOSTTY_STATIC
36// before including this header to make this a no-op.
37#ifndef GHOSTTY_API
38#if defined(GHOSTTY_STATIC)
39 #define GHOSTTY_API
40#elif defined(_WIN32) || defined(_WIN64)
41 #ifdef GHOSTTY_BUILD_SHARED
42 #define GHOSTTY_API __declspec(dllexport)
43 #else
44 #define GHOSTTY_API __declspec(dllimport)
45 #endif
46#elif defined(__GNUC__) && __GNUC__ >= 4
47 #define GHOSTTY_API __attribute__((visibility("default")))
48#else
49 #define GHOSTTY_API
50#endif
51#endif
52
53//-------------------------------------------------------------------
54// Types
55
56// Opaque types
57typedef void* ghostty_app_t;
58typedef void* ghostty_config_t;
59typedef void* ghostty_surface_t;
60typedef void* ghostty_inspector_t;
61
62// All the types below are fully defined and must be kept in sync with
63// their Zig counterparts. Any changes to these types MUST have an associated
64// Zig change.
65typedef enum {
66 GHOSTTY_PLATFORM_INVALID,
67 GHOSTTY_PLATFORM_MACOS,
68 GHOSTTY_PLATFORM_IOS,
69} ghostty_platform_e;
70
71typedef enum {
72 GHOSTTY_CLIPBOARD_STANDARD,
73 GHOSTTY_CLIPBOARD_SELECTION,
74} ghostty_clipboard_e;
75
76typedef struct {
77 const char *mime;
78 const char *data;
79} ghostty_clipboard_content_s;
80
81typedef enum {
82 GHOSTTY_CLIPBOARD_REQUEST_PASTE,
83 GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
84 GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
85} ghostty_clipboard_request_e;
86
87typedef enum {
88 GHOSTTY_MOUSE_RELEASE,
89 GHOSTTY_MOUSE_PRESS,
90} ghostty_input_mouse_state_e;
91
92typedef enum {
93 GHOSTTY_MOUSE_UNKNOWN,
94 GHOSTTY_MOUSE_LEFT,
95 GHOSTTY_MOUSE_RIGHT,
96 GHOSTTY_MOUSE_MIDDLE,
97 GHOSTTY_MOUSE_FOUR,
98 GHOSTTY_MOUSE_FIVE,
99 GHOSTTY_MOUSE_SIX,
100 GHOSTTY_MOUSE_SEVEN,
101 GHOSTTY_MOUSE_EIGHT,
102 GHOSTTY_MOUSE_NINE,
103 GHOSTTY_MOUSE_TEN,
104 GHOSTTY_MOUSE_ELEVEN,
105} ghostty_input_mouse_button_e;
106
107typedef enum {
108 GHOSTTY_MOUSE_MOMENTUM_NONE,
109 GHOSTTY_MOUSE_MOMENTUM_BEGAN,
110 GHOSTTY_MOUSE_MOMENTUM_STATIONARY,
111 GHOSTTY_MOUSE_MOMENTUM_CHANGED,
112 GHOSTTY_MOUSE_MOMENTUM_ENDED,
113 GHOSTTY_MOUSE_MOMENTUM_CANCELLED,
114 GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
115} ghostty_input_mouse_momentum_e;
116
117typedef enum {
118 GHOSTTY_COLOR_SCHEME_LIGHT = 0,
119 GHOSTTY_COLOR_SCHEME_DARK = 1,
120} ghostty_color_scheme_e;
121
122// This is a packed struct (see src/input/mouse.zig) but the C standard
123// afaik doesn't let us reliably define packed structs so we build it up
124// from scratch.
125typedef int ghostty_input_scroll_mods_t;
126
127typedef enum {
128 GHOSTTY_MODS_NONE = 0,
129 GHOSTTY_MODS_SHIFT = 1 << 0,
130 GHOSTTY_MODS_CTRL = 1 << 1,
131 GHOSTTY_MODS_ALT = 1 << 2,
132 GHOSTTY_MODS_SUPER = 1 << 3,
133 GHOSTTY_MODS_CAPS = 1 << 4,
134 GHOSTTY_MODS_NUM = 1 << 5,
135 GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6,
136 GHOSTTY_MODS_CTRL_RIGHT = 1 << 7,
137 GHOSTTY_MODS_ALT_RIGHT = 1 << 8,
138 GHOSTTY_MODS_SUPER_RIGHT = 1 << 9,
139} ghostty_input_mods_e;
140
141typedef enum {
142 GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0,
143 GHOSTTY_BINDING_FLAGS_ALL = 1 << 1,
144 GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2,
145 GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3,
146} ghostty_binding_flags_e;
147
148typedef enum {
149 GHOSTTY_ACTION_RELEASE,
150 GHOSTTY_ACTION_PRESS,
151 GHOSTTY_ACTION_REPEAT,
152} ghostty_input_action_e;
153
154// Based on: https://www.w3.org/TR/uievents-code/
155typedef enum {
156 GHOSTTY_KEY_UNIDENTIFIED,
157
158 // "Writing System Keys" § 3.1.1
159 GHOSTTY_KEY_BACKQUOTE,
160 GHOSTTY_KEY_BACKSLASH,
161 GHOSTTY_KEY_BRACKET_LEFT,
162 GHOSTTY_KEY_BRACKET_RIGHT,
163 GHOSTTY_KEY_COMMA,
164 GHOSTTY_KEY_DIGIT_0,
165 GHOSTTY_KEY_DIGIT_1,
166 GHOSTTY_KEY_DIGIT_2,
167 GHOSTTY_KEY_DIGIT_3,
168 GHOSTTY_KEY_DIGIT_4,
169 GHOSTTY_KEY_DIGIT_5,
170 GHOSTTY_KEY_DIGIT_6,
171 GHOSTTY_KEY_DIGIT_7,
172 GHOSTTY_KEY_DIGIT_8,
173 GHOSTTY_KEY_DIGIT_9,
174 GHOSTTY_KEY_EQUAL,
175 GHOSTTY_KEY_INTL_BACKSLASH,
176 GHOSTTY_KEY_INTL_RO,
177 GHOSTTY_KEY_INTL_YEN,
178 GHOSTTY_KEY_A,
179 GHOSTTY_KEY_B,
180 GHOSTTY_KEY_C,
181 GHOSTTY_KEY_D,
182 GHOSTTY_KEY_E,
183 GHOSTTY_KEY_F,
184 GHOSTTY_KEY_G,
185 GHOSTTY_KEY_H,
186 GHOSTTY_KEY_I,
187 GHOSTTY_KEY_J,
188 GHOSTTY_KEY_K,
189 GHOSTTY_KEY_L,
190 GHOSTTY_KEY_M,
191 GHOSTTY_KEY_N,
192 GHOSTTY_KEY_O,
193 GHOSTTY_KEY_P,
194 GHOSTTY_KEY_Q,
195 GHOSTTY_KEY_R,
196 GHOSTTY_KEY_S,
197 GHOSTTY_KEY_T,
198 GHOSTTY_KEY_U,
199 GHOSTTY_KEY_V,
200 GHOSTTY_KEY_W,
201 GHOSTTY_KEY_X,
202 GHOSTTY_KEY_Y,
203 GHOSTTY_KEY_Z,
204 GHOSTTY_KEY_MINUS,
205 GHOSTTY_KEY_PERIOD,
206 GHOSTTY_KEY_QUOTE,
207 GHOSTTY_KEY_SEMICOLON,
208 GHOSTTY_KEY_SLASH,
209
210 // "Functional Keys" § 3.1.2
211 GHOSTTY_KEY_ALT_LEFT,
212 GHOSTTY_KEY_ALT_RIGHT,
213 GHOSTTY_KEY_BACKSPACE,
214 GHOSTTY_KEY_CAPS_LOCK,
215 GHOSTTY_KEY_CONTEXT_MENU,
216 GHOSTTY_KEY_CONTROL_LEFT,
217 GHOSTTY_KEY_CONTROL_RIGHT,
218 GHOSTTY_KEY_ENTER,
219 GHOSTTY_KEY_META_LEFT,
220 GHOSTTY_KEY_META_RIGHT,
221 GHOSTTY_KEY_SHIFT_LEFT,
222 GHOSTTY_KEY_SHIFT_RIGHT,
223 GHOSTTY_KEY_SPACE,
224 GHOSTTY_KEY_TAB,
225 GHOSTTY_KEY_CONVERT,
226 GHOSTTY_KEY_KANA_MODE,
227 GHOSTTY_KEY_NON_CONVERT,
228
229 // "Control Pad Section" § 3.2
230 GHOSTTY_KEY_DELETE,
231 GHOSTTY_KEY_END,
232 GHOSTTY_KEY_HELP,
233 GHOSTTY_KEY_HOME,
234 GHOSTTY_KEY_INSERT,
235 GHOSTTY_KEY_PAGE_DOWN,
236 GHOSTTY_KEY_PAGE_UP,
237
238 // "Arrow Pad Section" § 3.3
239 GHOSTTY_KEY_ARROW_DOWN,
240 GHOSTTY_KEY_ARROW_LEFT,
241 GHOSTTY_KEY_ARROW_RIGHT,
242 GHOSTTY_KEY_ARROW_UP,
243
244 // "Numpad Section" § 3.4
245 GHOSTTY_KEY_NUM_LOCK,
246 GHOSTTY_KEY_NUMPAD_0,
247 GHOSTTY_KEY_NUMPAD_1,
248 GHOSTTY_KEY_NUMPAD_2,
249 GHOSTTY_KEY_NUMPAD_3,
250 GHOSTTY_KEY_NUMPAD_4,
251 GHOSTTY_KEY_NUMPAD_5,
252 GHOSTTY_KEY_NUMPAD_6,
253 GHOSTTY_KEY_NUMPAD_7,
254 GHOSTTY_KEY_NUMPAD_8,
255 GHOSTTY_KEY_NUMPAD_9,
256 GHOSTTY_KEY_NUMPAD_ADD,
257 GHOSTTY_KEY_NUMPAD_BACKSPACE,
258 GHOSTTY_KEY_NUMPAD_CLEAR,
259 GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
260 GHOSTTY_KEY_NUMPAD_COMMA,
261 GHOSTTY_KEY_NUMPAD_DECIMAL,
262 GHOSTTY_KEY_NUMPAD_DIVIDE,
263 GHOSTTY_KEY_NUMPAD_ENTER,
264 GHOSTTY_KEY_NUMPAD_EQUAL,
265 GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
266 GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
267 GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
268 GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
269 GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
270 GHOSTTY_KEY_NUMPAD_MULTIPLY,
271 GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
272 GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
273 GHOSTTY_KEY_NUMPAD_SUBTRACT,
274 GHOSTTY_KEY_NUMPAD_SEPARATOR,
275 GHOSTTY_KEY_NUMPAD_UP,
276 GHOSTTY_KEY_NUMPAD_DOWN,
277 GHOSTTY_KEY_NUMPAD_RIGHT,
278 GHOSTTY_KEY_NUMPAD_LEFT,
279 GHOSTTY_KEY_NUMPAD_BEGIN,
280 GHOSTTY_KEY_NUMPAD_HOME,
281 GHOSTTY_KEY_NUMPAD_END,
282 GHOSTTY_KEY_NUMPAD_INSERT,
283 GHOSTTY_KEY_NUMPAD_DELETE,
284 GHOSTTY_KEY_NUMPAD_PAGE_UP,
285 GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
286
287 // "Function Section" § 3.5
288 GHOSTTY_KEY_ESCAPE,
289 GHOSTTY_KEY_F1,
290 GHOSTTY_KEY_F2,
291 GHOSTTY_KEY_F3,
292 GHOSTTY_KEY_F4,
293 GHOSTTY_KEY_F5,
294 GHOSTTY_KEY_F6,
295 GHOSTTY_KEY_F7,
296 GHOSTTY_KEY_F8,
297 GHOSTTY_KEY_F9,
298 GHOSTTY_KEY_F10,
299 GHOSTTY_KEY_F11,
300 GHOSTTY_KEY_F12,
301 GHOSTTY_KEY_F13,
302 GHOSTTY_KEY_F14,
303 GHOSTTY_KEY_F15,
304 GHOSTTY_KEY_F16,
305 GHOSTTY_KEY_F17,
306 GHOSTTY_KEY_F18,
307 GHOSTTY_KEY_F19,
308 GHOSTTY_KEY_F20,
309 GHOSTTY_KEY_F21,
310 GHOSTTY_KEY_F22,
311 GHOSTTY_KEY_F23,
312 GHOSTTY_KEY_F24,
313 GHOSTTY_KEY_F25,
314 GHOSTTY_KEY_FN,
315 GHOSTTY_KEY_FN_LOCK,
316 GHOSTTY_KEY_PRINT_SCREEN,
317 GHOSTTY_KEY_SCROLL_LOCK,
318 GHOSTTY_KEY_PAUSE,
319
320 // "Media Keys" § 3.6
321 GHOSTTY_KEY_BROWSER_BACK,
322 GHOSTTY_KEY_BROWSER_FAVORITES,
323 GHOSTTY_KEY_BROWSER_FORWARD,
324 GHOSTTY_KEY_BROWSER_HOME,
325 GHOSTTY_KEY_BROWSER_REFRESH,
326 GHOSTTY_KEY_BROWSER_SEARCH,
327 GHOSTTY_KEY_BROWSER_STOP,
328 GHOSTTY_KEY_EJECT,
329 GHOSTTY_KEY_LAUNCH_APP_1,
330 GHOSTTY_KEY_LAUNCH_APP_2,
331 GHOSTTY_KEY_LAUNCH_MAIL,
332 GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
333 GHOSTTY_KEY_MEDIA_SELECT,
334 GHOSTTY_KEY_MEDIA_STOP,
335 GHOSTTY_KEY_MEDIA_TRACK_NEXT,
336 GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
337 GHOSTTY_KEY_POWER,
338 GHOSTTY_KEY_SLEEP,
339 GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
340 GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
341 GHOSTTY_KEY_AUDIO_VOLUME_UP,
342 GHOSTTY_KEY_WAKE_UP,
343
344 // "Legacy, Non-standard, and Special Keys" § 3.7
345 GHOSTTY_KEY_COPY,
346 GHOSTTY_KEY_CUT,
347 GHOSTTY_KEY_PASTE,
348} ghostty_input_key_e;
349
350typedef struct {
351 ghostty_input_action_e action;
352 ghostty_input_mods_e mods;
353 ghostty_input_mods_e consumed_mods;
354 uint32_t keycode;
355 const char* text;
356 uint32_t unshifted_codepoint;
357 bool composing;
358} ghostty_input_key_s;
359
360typedef enum {
361 GHOSTTY_TRIGGER_PHYSICAL,
362 GHOSTTY_TRIGGER_UNICODE,
363 GHOSTTY_TRIGGER_CATCH_ALL,
364} ghostty_input_trigger_tag_e;
365
366typedef union {
367 ghostty_input_key_e physical;
368 uint32_t unicode;
369 // catch_all has no payload
370} ghostty_input_trigger_key_u;
371
372typedef struct {
373 ghostty_input_trigger_tag_e tag;
374 ghostty_input_trigger_key_u key;
375 ghostty_input_mods_e mods;
376} ghostty_input_trigger_s;
377
378typedef struct {
379 const char* action_key;
380 const char* action;
381 const char* title;
382 const char* description;
383} ghostty_command_s;
384
385typedef enum {
386 GHOSTTY_BUILD_MODE_DEBUG,
387 GHOSTTY_BUILD_MODE_RELEASE_SAFE,
388 GHOSTTY_BUILD_MODE_RELEASE_FAST,
389 GHOSTTY_BUILD_MODE_RELEASE_SMALL,
390} ghostty_build_mode_e;
391
392typedef struct {
393 ghostty_build_mode_e build_mode;
394 const char* version;
395 uintptr_t version_len;
396} ghostty_info_s;
397
398typedef struct {
399 const char* message;
400} ghostty_diagnostic_s;
401
402typedef struct {
403 const char* ptr;
404 uintptr_t len;
405 bool sentinel;
406} ghostty_string_s;
407
408typedef struct {
409 double tl_px_x;
410 double tl_px_y;
411 uint32_t offset_start;
412 uint32_t offset_len;
413 const char* text;
414 uintptr_t text_len;
415} ghostty_text_s;
416
417typedef enum {
418 GHOSTTY_POINT_ACTIVE,
419 GHOSTTY_POINT_VIEWPORT,
420 GHOSTTY_POINT_SCREEN,
421 GHOSTTY_POINT_SURFACE,
422} ghostty_point_tag_e;
423
424typedef enum {
425 GHOSTTY_POINT_COORD_EXACT,
426 GHOSTTY_POINT_COORD_TOP_LEFT,
427 GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
428} ghostty_point_coord_e;
429
430typedef struct {
431 ghostty_point_tag_e tag;
432 ghostty_point_coord_e coord;
433 uint32_t x;
434 uint32_t y;
435} ghostty_point_s;
436
437typedef struct {
438 ghostty_point_s top_left;
439 ghostty_point_s bottom_right;
440 bool rectangle;
441} ghostty_selection_s;
442
443typedef struct {
444 const char* key;
445 const char* value;
446} ghostty_env_var_s;
447
448typedef struct {
449 void* nsview;
450} ghostty_platform_macos_s;
451
452typedef struct {
453 void* uiview;
454} ghostty_platform_ios_s;
455
456typedef union {
457 ghostty_platform_macos_s macos;
458 ghostty_platform_ios_s ios;
459} ghostty_platform_u;
460
461typedef enum {
462 GHOSTTY_SURFACE_CONTEXT_WINDOW = 0,
463 GHOSTTY_SURFACE_CONTEXT_TAB = 1,
464 GHOSTTY_SURFACE_CONTEXT_SPLIT = 2,
465} ghostty_surface_context_e;
466
467typedef struct {
468 ghostty_platform_e platform_tag;
469 ghostty_platform_u platform;
470 void* userdata;
471 double scale_factor;
472 float font_size;
473 const char* working_directory;
474 const char* command;
475 ghostty_env_var_s* env_vars;
476 size_t env_var_count;
477 const char* initial_input;
478 bool wait_after_command;
479 ghostty_surface_context_e context;
480} ghostty_surface_config_s;
481
482typedef struct {
483 uint16_t columns;
484 uint16_t rows;
485 uint32_t width_px;
486 uint32_t height_px;
487 uint32_t cell_width_px;
488 uint32_t cell_height_px;
489} ghostty_surface_size_s;
490
491// Config types
492
493// config.Path
494typedef struct {
495 const char* path;
496 bool optional;
497} ghostty_config_path_s;
498
499// config.Color
500typedef struct {
501 uint8_t r;
502 uint8_t g;
503 uint8_t b;
504} ghostty_config_color_s;
505
506// config.ColorList
507typedef struct {
508 const ghostty_config_color_s* colors;
509 size_t len;
510} ghostty_config_color_list_s;
511
512// config.RepeatableCommand
513typedef struct {
514 const ghostty_command_s* commands;
515 size_t len;
516} ghostty_config_command_list_s;
517
518// config.Palette
519typedef struct {
520 ghostty_config_color_s colors[256];
521} ghostty_config_palette_s;
522
523// config.QuickTerminalSize
524typedef enum {
525 GHOSTTY_QUICK_TERMINAL_SIZE_NONE,
526 GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE,
527 GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS,
528} ghostty_quick_terminal_size_tag_e;
529
530typedef union {
531 float percentage;
532 uint32_t pixels;
533} ghostty_quick_terminal_size_value_u;
534
535typedef struct {
536 ghostty_quick_terminal_size_tag_e tag;
537 ghostty_quick_terminal_size_value_u value;
538} ghostty_quick_terminal_size_s;
539
540typedef struct {
541 ghostty_quick_terminal_size_s primary;
542 ghostty_quick_terminal_size_s secondary;
543} ghostty_config_quick_terminal_size_s;
544
545// config.Fullscreen
546typedef enum {
547 GHOSTTY_CONFIG_FULLSCREEN_FALSE,
548 GHOSTTY_CONFIG_FULLSCREEN_TRUE,
549 GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE,
550 GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
551 GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
552} ghostty_config_fullscreen_e;
553
554// apprt.Target.Key
555typedef enum {
556 GHOSTTY_TARGET_APP,
557 GHOSTTY_TARGET_SURFACE,
558} ghostty_target_tag_e;
559
560typedef union {
561 ghostty_surface_t surface;
562} ghostty_target_u;
563
564typedef struct {
565 ghostty_target_tag_e tag;
566 ghostty_target_u target;
567} ghostty_target_s;
568
569// apprt.action.SplitDirection
570typedef enum {
571 GHOSTTY_SPLIT_DIRECTION_RIGHT,
572 GHOSTTY_SPLIT_DIRECTION_DOWN,
573 GHOSTTY_SPLIT_DIRECTION_LEFT,
574 GHOSTTY_SPLIT_DIRECTION_UP,
575} ghostty_action_split_direction_e;
576
577// apprt.action.GotoSplit
578typedef enum {
579 GHOSTTY_GOTO_SPLIT_PREVIOUS,
580 GHOSTTY_GOTO_SPLIT_NEXT,
581 GHOSTTY_GOTO_SPLIT_UP,
582 GHOSTTY_GOTO_SPLIT_LEFT,
583 GHOSTTY_GOTO_SPLIT_DOWN,
584 GHOSTTY_GOTO_SPLIT_RIGHT,
585} ghostty_action_goto_split_e;
586
587// apprt.action.GotoWindow
588typedef enum {
589 GHOSTTY_GOTO_WINDOW_PREVIOUS,
590 GHOSTTY_GOTO_WINDOW_NEXT,
591} ghostty_action_goto_window_e;
592
593// apprt.action.ResizeSplit.Direction
594typedef enum {
595 GHOSTTY_RESIZE_SPLIT_UP,
596 GHOSTTY_RESIZE_SPLIT_DOWN,
597 GHOSTTY_RESIZE_SPLIT_LEFT,
598 GHOSTTY_RESIZE_SPLIT_RIGHT,
599} ghostty_action_resize_split_direction_e;
600
601// apprt.action.ResizeSplit
602typedef struct {
603 uint16_t amount;
604 ghostty_action_resize_split_direction_e direction;
605} ghostty_action_resize_split_s;
606
607// apprt.action.MoveTab
608typedef struct {
609 ssize_t amount;
610} ghostty_action_move_tab_s;
611
612// apprt.action.GotoTab
613typedef enum {
614 GHOSTTY_GOTO_TAB_PREVIOUS = -1,
615 GHOSTTY_GOTO_TAB_NEXT = -2,
616 GHOSTTY_GOTO_TAB_LAST = -3,
617} ghostty_action_goto_tab_e;
618
619// apprt.action.Fullscreen
620typedef enum {
621 GHOSTTY_FULLSCREEN_NATIVE,
622 GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE,
623 GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU,
624 GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH,
625} ghostty_action_fullscreen_e;
626
627// apprt.action.FloatWindow
628typedef enum {
629 GHOSTTY_FLOAT_WINDOW_ON,
630 GHOSTTY_FLOAT_WINDOW_OFF,
631 GHOSTTY_FLOAT_WINDOW_TOGGLE,
632} ghostty_action_float_window_e;
633
634// apprt.action.SecureInput
635typedef enum {
636 GHOSTTY_SECURE_INPUT_ON,
637 GHOSTTY_SECURE_INPUT_OFF,
638 GHOSTTY_SECURE_INPUT_TOGGLE,
639} ghostty_action_secure_input_e;
640
641// apprt.action.Inspector
642typedef enum {
643 GHOSTTY_INSPECTOR_TOGGLE,
644 GHOSTTY_INSPECTOR_SHOW,
645 GHOSTTY_INSPECTOR_HIDE,
646} ghostty_action_inspector_e;
647
648// apprt.action.QuitTimer
649typedef enum {
650 GHOSTTY_QUIT_TIMER_START,
651 GHOSTTY_QUIT_TIMER_STOP,
652} ghostty_action_quit_timer_e;
653
654// apprt.action.Readonly
655typedef enum {
656 GHOSTTY_READONLY_OFF,
657 GHOSTTY_READONLY_ON,
658} ghostty_action_readonly_e;
659
660// apprt.action.DesktopNotification.C
661typedef struct {
662 const char* title;
663 const char* body;
664} ghostty_action_desktop_notification_s;
665
666// apprt.action.SetTitle.C
667typedef struct {
668 const char* title;
669} ghostty_action_set_title_s;
670
671// apprt.action.PromptTitle
672typedef enum {
673 GHOSTTY_PROMPT_TITLE_SURFACE,
674 GHOSTTY_PROMPT_TITLE_TAB,
675} ghostty_action_prompt_title_e;
676
677// apprt.action.Pwd.C
678typedef struct {
679 const char* pwd;
680} ghostty_action_pwd_s;
681
682// terminal.MouseShape
683typedef enum {
684 GHOSTTY_MOUSE_SHAPE_DEFAULT,
685 GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU,
686 GHOSTTY_MOUSE_SHAPE_HELP,
687 GHOSTTY_MOUSE_SHAPE_POINTER,
688 GHOSTTY_MOUSE_SHAPE_PROGRESS,
689 GHOSTTY_MOUSE_SHAPE_WAIT,
690 GHOSTTY_MOUSE_SHAPE_CELL,
691 GHOSTTY_MOUSE_SHAPE_CROSSHAIR,
692 GHOSTTY_MOUSE_SHAPE_TEXT,
693 GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT,
694 GHOSTTY_MOUSE_SHAPE_ALIAS,
695 GHOSTTY_MOUSE_SHAPE_COPY,
696 GHOSTTY_MOUSE_SHAPE_MOVE,
697 GHOSTTY_MOUSE_SHAPE_NO_DROP,
698 GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED,
699 GHOSTTY_MOUSE_SHAPE_GRAB,
700 GHOSTTY_MOUSE_SHAPE_GRABBING,
701 GHOSTTY_MOUSE_SHAPE_ALL_SCROLL,
702 GHOSTTY_MOUSE_SHAPE_COL_RESIZE,
703 GHOSTTY_MOUSE_SHAPE_ROW_RESIZE,
704 GHOSTTY_MOUSE_SHAPE_N_RESIZE,
705 GHOSTTY_MOUSE_SHAPE_E_RESIZE,
706 GHOSTTY_MOUSE_SHAPE_S_RESIZE,
707 GHOSTTY_MOUSE_SHAPE_W_RESIZE,
708 GHOSTTY_MOUSE_SHAPE_NE_RESIZE,
709 GHOSTTY_MOUSE_SHAPE_NW_RESIZE,
710 GHOSTTY_MOUSE_SHAPE_SE_RESIZE,
711 GHOSTTY_MOUSE_SHAPE_SW_RESIZE,
712 GHOSTTY_MOUSE_SHAPE_EW_RESIZE,
713 GHOSTTY_MOUSE_SHAPE_NS_RESIZE,
714 GHOSTTY_MOUSE_SHAPE_NESW_RESIZE,
715 GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE,
716 GHOSTTY_MOUSE_SHAPE_ZOOM_IN,
717 GHOSTTY_MOUSE_SHAPE_ZOOM_OUT,
718} ghostty_action_mouse_shape_e;
719
720// apprt.action.MouseVisibility
721typedef enum {
722 GHOSTTY_MOUSE_VISIBLE,
723 GHOSTTY_MOUSE_HIDDEN,
724} ghostty_action_mouse_visibility_e;
725
726// apprt.action.MouseOverLink
727typedef struct {
728 const char* url;
729 size_t len;
730} ghostty_action_mouse_over_link_s;
731
732// apprt.action.SizeLimit
733typedef struct {
734 uint32_t min_width;
735 uint32_t min_height;
736 uint32_t max_width;
737 uint32_t max_height;
738} ghostty_action_size_limit_s;
739
740// apprt.action.InitialSize
741typedef struct {
742 uint32_t width;
743 uint32_t height;
744} ghostty_action_initial_size_s;
745
746// apprt.action.CellSize
747typedef struct {
748 uint32_t width;
749 uint32_t height;
750} ghostty_action_cell_size_s;
751
752// renderer.Health
753typedef enum {
754 GHOSTTY_RENDERER_HEALTH_HEALTHY,
755 GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
756} ghostty_action_renderer_health_e;
757
758// apprt.action.KeySequence
759typedef struct {
760 bool active;
761 ghostty_input_trigger_s trigger;
762} ghostty_action_key_sequence_s;
763
764// apprt.action.KeyTable.Tag
765typedef enum {
766 GHOSTTY_KEY_TABLE_ACTIVATE,
767 GHOSTTY_KEY_TABLE_DEACTIVATE,
768 GHOSTTY_KEY_TABLE_DEACTIVATE_ALL,
769} ghostty_action_key_table_tag_e;
770
771// apprt.action.KeyTable.CValue
772typedef union {
773 struct {
774 const char *name;
775 size_t len;
776 } activate;
777} ghostty_action_key_table_u;
778
779// apprt.action.KeyTable.C
780typedef struct {
781 ghostty_action_key_table_tag_e tag;
782 ghostty_action_key_table_u value;
783} ghostty_action_key_table_s;
784
785// apprt.action.ColorKind
786typedef enum {
787 GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1,
788 GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2,
789 GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3,
790} ghostty_action_color_kind_e;
791
792// apprt.action.ColorChange
793typedef struct {
794 ghostty_action_color_kind_e kind;
795 uint8_t r;
796 uint8_t g;
797 uint8_t b;
798} ghostty_action_color_change_s;
799
800// apprt.action.ConfigChange
801typedef struct {
802 ghostty_config_t config;
803} ghostty_action_config_change_s;
804
805// apprt.action.ReloadConfig
806typedef struct {
807 bool soft;
808} ghostty_action_reload_config_s;
809
810// apprt.action.OpenUrlKind
811typedef enum {
812 GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
813 GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
814 GHOSTTY_ACTION_OPEN_URL_KIND_HTML,
815} ghostty_action_open_url_kind_e;
816
817// apprt.action.OpenUrl.C
818typedef struct {
819 ghostty_action_open_url_kind_e kind;
820 const char* url;
821 uintptr_t len;
822} ghostty_action_open_url_s;
823
824// apprt.action.CloseTabMode
825typedef enum {
826 GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
827 GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
828 GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
829} ghostty_action_close_tab_mode_e;
830
831// apprt.surface.Message.ChildExited
832typedef struct {
833 uint32_t exit_code;
834 uint64_t timetime_ms;
835} ghostty_surface_message_childexited_s;
836
837// terminal.osc.Command.ProgressReport.State
838typedef enum {
839 GHOSTTY_PROGRESS_STATE_REMOVE,
840 GHOSTTY_PROGRESS_STATE_SET,
841 GHOSTTY_PROGRESS_STATE_ERROR,
842 GHOSTTY_PROGRESS_STATE_INDETERMINATE,
843 GHOSTTY_PROGRESS_STATE_PAUSE,
844} ghostty_action_progress_report_state_e;
845
846// terminal.osc.Command.ProgressReport.C
847typedef struct {
848 ghostty_action_progress_report_state_e state;
849 // -1 if no progress was reported, otherwise 0-100 indicating percent
850 // completeness.
851 int8_t progress;
852} ghostty_action_progress_report_s;
853
854// apprt.action.CommandFinished.C
855typedef struct {
856 // -1 if no exit code was reported, otherwise 0-255
857 int16_t exit_code;
858 // number of nanoseconds that command was running for
859 uint64_t duration;
860} ghostty_action_command_finished_s;
861
862// apprt.action.StartSearch.C
863typedef struct {
864 const char* needle;
865} ghostty_action_start_search_s;
866
867// apprt.action.SearchTotal
868typedef struct {
869 ssize_t total;
870} ghostty_action_search_total_s;
871
872// apprt.action.SearchSelected
873typedef struct {
874 ssize_t selected;
875} ghostty_action_search_selected_s;
876
877// terminal.Scrollbar
878typedef struct {
879 uint64_t total;
880 uint64_t offset;
881 uint64_t len;
882} ghostty_action_scrollbar_s;
883
884// apprt.Action.Key
885typedef enum {
886 GHOSTTY_ACTION_QUIT,
887 GHOSTTY_ACTION_NEW_WINDOW,
888 GHOSTTY_ACTION_NEW_TAB,
889 GHOSTTY_ACTION_CLOSE_TAB,
890 GHOSTTY_ACTION_NEW_SPLIT,
891 GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
892 GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
893 GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
894 GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
895 GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
896 GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
897 GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE,
898 GHOSTTY_ACTION_TOGGLE_VISIBILITY,
899 GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY,
900 GHOSTTY_ACTION_MOVE_TAB,
901 GHOSTTY_ACTION_GOTO_TAB,
902 GHOSTTY_ACTION_GOTO_SPLIT,
903 GHOSTTY_ACTION_GOTO_WINDOW,
904 GHOSTTY_ACTION_RESIZE_SPLIT,
905 GHOSTTY_ACTION_EQUALIZE_SPLITS,
906 GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
907 GHOSTTY_ACTION_PRESENT_TERMINAL,
908 GHOSTTY_ACTION_SIZE_LIMIT,
909 GHOSTTY_ACTION_RESET_WINDOW_SIZE,
910 GHOSTTY_ACTION_INITIAL_SIZE,
911 GHOSTTY_ACTION_CELL_SIZE,
912 GHOSTTY_ACTION_SCROLLBAR,
913 GHOSTTY_ACTION_RENDER,
914 GHOSTTY_ACTION_INSPECTOR,
915 GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
916 GHOSTTY_ACTION_RENDER_INSPECTOR,
917 GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
918 GHOSTTY_ACTION_SET_TITLE,
919 GHOSTTY_ACTION_SET_TAB_TITLE,
920 GHOSTTY_ACTION_PROMPT_TITLE,
921 GHOSTTY_ACTION_PWD,
922 GHOSTTY_ACTION_MOUSE_SHAPE,
923 GHOSTTY_ACTION_MOUSE_VISIBILITY,
924 GHOSTTY_ACTION_MOUSE_OVER_LINK,
925 GHOSTTY_ACTION_RENDERER_HEALTH,
926 GHOSTTY_ACTION_OPEN_CONFIG,
927 GHOSTTY_ACTION_QUIT_TIMER,
928 GHOSTTY_ACTION_FLOAT_WINDOW,
929 GHOSTTY_ACTION_SECURE_INPUT,
930 GHOSTTY_ACTION_KEY_SEQUENCE,
931 GHOSTTY_ACTION_KEY_TABLE,
932 GHOSTTY_ACTION_COLOR_CHANGE,
933 GHOSTTY_ACTION_RELOAD_CONFIG,
934 GHOSTTY_ACTION_CONFIG_CHANGE,
935 GHOSTTY_ACTION_CLOSE_WINDOW,
936 GHOSTTY_ACTION_RING_BELL,
937 GHOSTTY_ACTION_SELECTION_CHANGED,
938 GHOSTTY_ACTION_UNDO,
939 GHOSTTY_ACTION_REDO,
940 GHOSTTY_ACTION_CHECK_FOR_UPDATES,
941 GHOSTTY_ACTION_OPEN_URL,
942 GHOSTTY_ACTION_SHOW_CHILD_EXITED,
943 GHOSTTY_ACTION_PROGRESS_REPORT,
944 GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
945 GHOSTTY_ACTION_COMMAND_FINISHED,
946 GHOSTTY_ACTION_START_SEARCH,
947 GHOSTTY_ACTION_END_SEARCH,
948 GHOSTTY_ACTION_SEARCH_TOTAL,
949 GHOSTTY_ACTION_SEARCH_SELECTED,
950 GHOSTTY_ACTION_READONLY,
951 GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD,
952} ghostty_action_tag_e;
953
954typedef union {
955 ghostty_action_split_direction_e new_split;
956 ghostty_action_fullscreen_e toggle_fullscreen;
957 ghostty_action_move_tab_s move_tab;
958 ghostty_action_goto_tab_e goto_tab;
959 ghostty_action_goto_split_e goto_split;
960 ghostty_action_goto_window_e goto_window;
961 ghostty_action_resize_split_s resize_split;
962 ghostty_action_size_limit_s size_limit;
963 ghostty_action_initial_size_s initial_size;
964 ghostty_action_cell_size_s cell_size;
965 ghostty_action_scrollbar_s scrollbar;
966 ghostty_action_inspector_e inspector;
967 ghostty_action_desktop_notification_s desktop_notification;
968 ghostty_action_set_title_s set_title;
969 ghostty_action_set_title_s set_tab_title;
970 ghostty_action_prompt_title_e prompt_title;
971 ghostty_action_pwd_s pwd;
972 ghostty_action_mouse_shape_e mouse_shape;
973 ghostty_action_mouse_visibility_e mouse_visibility;
974 ghostty_action_mouse_over_link_s mouse_over_link;
975 ghostty_action_renderer_health_e renderer_health;
976 ghostty_action_quit_timer_e quit_timer;
977 ghostty_action_float_window_e float_window;
978 ghostty_action_secure_input_e secure_input;
979 ghostty_action_key_sequence_s key_sequence;
980 ghostty_action_key_table_s key_table;
981 ghostty_action_color_change_s color_change;
982 ghostty_action_reload_config_s reload_config;
983 ghostty_action_config_change_s config_change;
984 ghostty_action_open_url_s open_url;
985 ghostty_action_close_tab_mode_e close_tab_mode;
986 ghostty_surface_message_childexited_s child_exited;
987 ghostty_action_progress_report_s progress_report;
988 ghostty_action_command_finished_s command_finished;
989 ghostty_action_start_search_s start_search;
990 ghostty_action_search_total_s search_total;
991 ghostty_action_search_selected_s search_selected;
992 ghostty_action_readonly_e readonly;
993} ghostty_action_u;
994
995typedef struct {
996 ghostty_action_tag_e tag;
997 ghostty_action_u action;
998} ghostty_action_s;
999
1000typedef void (*ghostty_runtime_wakeup_cb)(void*);
1001typedef bool (*ghostty_runtime_read_clipboard_cb)(void*,
1002 ghostty_clipboard_e,
1003 void*);
1004typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
1005 void*,
1006 const char*,
1007 void*,
1008 ghostty_clipboard_request_e);
1009typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
1010 ghostty_clipboard_e,
1011 const ghostty_clipboard_content_s*,
1012 size_t,
1013 bool);
1014typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
1015typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
1016 ghostty_target_s,
1017 ghostty_action_s);
1018
1019typedef struct {
1020 void* userdata;
1021 bool supports_selection_clipboard;
1022 ghostty_runtime_wakeup_cb wakeup_cb;
1023 ghostty_runtime_action_cb action_cb;
1024 ghostty_runtime_read_clipboard_cb read_clipboard_cb;
1025 ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb;
1026 ghostty_runtime_write_clipboard_cb write_clipboard_cb;
1027 ghostty_runtime_close_surface_cb close_surface_cb;
1028} ghostty_runtime_config_s;
1029
1030// apprt.ipc.Target.Key
1031typedef enum {
1032 GHOSTTY_IPC_TARGET_CLASS,
1033 GHOSTTY_IPC_TARGET_DETECT,
1034} ghostty_ipc_target_tag_e;
1035
1036typedef union {
1037 char *klass;
1038} ghostty_ipc_target_u;
1039
1040typedef struct {
1041 ghostty_ipc_target_tag_e tag;
1042 ghostty_ipc_target_u target;
1043} chostty_ipc_target_s;
1044
1045// apprt.ipc.Action.NewWindow
1046typedef struct {
1047 // This should be a null terminated list of strings.
1048 const char **arguments;
1049} ghostty_ipc_action_new_window_s;
1050
1051typedef union {
1052 ghostty_ipc_action_new_window_s new_window;
1053} ghostty_ipc_action_u;
1054
1055// apprt.ipc.Action.Key
1056typedef enum {
1057 GHOSTTY_IPC_ACTION_NEW_WINDOW,
1058 GHOSTTY_IPC_ACTION_TOGGLE_QUICK_TERMINAL,
1059} ghostty_ipc_action_tag_e;
1060
1061//-------------------------------------------------------------------
1062// Published API
1063
1064GHOSTTY_API int ghostty_init(uintptr_t, char**);
1065GHOSTTY_API void ghostty_cli_try_action(void);
1066GHOSTTY_API ghostty_info_s ghostty_info(void);
1067GHOSTTY_API const char* ghostty_translate(const char*);
1068GHOSTTY_API void ghostty_string_free(ghostty_string_s);
1069
1070GHOSTTY_API ghostty_config_t ghostty_config_new();
1071GHOSTTY_API void ghostty_config_free(ghostty_config_t);
1072GHOSTTY_API ghostty_config_t ghostty_config_clone(ghostty_config_t);
1073GHOSTTY_API void ghostty_config_load_cli_args(ghostty_config_t);
1074GHOSTTY_API void ghostty_config_load_file(ghostty_config_t, const char*);
1075GHOSTTY_API void ghostty_config_load_default_files(ghostty_config_t);
1076GHOSTTY_API void ghostty_config_load_recursive_files(ghostty_config_t);
1077GHOSTTY_API void ghostty_config_finalize(ghostty_config_t);
1078GHOSTTY_API bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
1079GHOSTTY_API ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
1080 const char*,
1081 uintptr_t);
1082GHOSTTY_API bool ghostty_config_key_is_binding(ghostty_config_t, ghostty_input_key_s);
1083GHOSTTY_API uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
1084GHOSTTY_API ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
1085GHOSTTY_API ghostty_string_s ghostty_config_open_path(void);
1086
1087GHOSTTY_API ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
1088 ghostty_config_t);
1089GHOSTTY_API void ghostty_app_free(ghostty_app_t);
1090GHOSTTY_API void ghostty_app_tick(ghostty_app_t);
1091GHOSTTY_API void* ghostty_app_userdata(ghostty_app_t);
1092GHOSTTY_API void ghostty_app_set_focus(ghostty_app_t, bool);
1093GHOSTTY_API bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
1094GHOSTTY_API void ghostty_app_keyboard_changed(ghostty_app_t);
1095GHOSTTY_API void ghostty_app_open_config(ghostty_app_t);
1096GHOSTTY_API void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
1097GHOSTTY_API bool ghostty_app_needs_confirm_quit(ghostty_app_t);
1098GHOSTTY_API bool ghostty_app_has_global_keybinds(ghostty_app_t);
1099GHOSTTY_API void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
1100
1101GHOSTTY_API ghostty_surface_config_s ghostty_surface_config_new();
1102
1103GHOSTTY_API ghostty_surface_t ghostty_surface_new(ghostty_app_t,
1104 const ghostty_surface_config_s*);
1105GHOSTTY_API void ghostty_surface_free(ghostty_surface_t);
1106GHOSTTY_API void* ghostty_surface_userdata(ghostty_surface_t);
1107GHOSTTY_API ghostty_app_t ghostty_surface_app(ghostty_surface_t);
1108GHOSTTY_API ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e);
1109GHOSTTY_API void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
1110GHOSTTY_API bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
1111GHOSTTY_API bool ghostty_surface_process_exited(ghostty_surface_t);
1112GHOSTTY_API void ghostty_surface_refresh(ghostty_surface_t);
1113GHOSTTY_API void ghostty_surface_draw(ghostty_surface_t);
1114GHOSTTY_API void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
1115GHOSTTY_API void ghostty_surface_set_focus(ghostty_surface_t, bool);
1116GHOSTTY_API void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
1117GHOSTTY_API void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
1118GHOSTTY_API ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
1119GHOSTTY_API uint64_t ghostty_surface_foreground_pid(ghostty_surface_t);
1120GHOSTTY_API ghostty_string_s ghostty_surface_tty_name(ghostty_surface_t);
1121GHOSTTY_API void ghostty_surface_set_color_scheme(ghostty_surface_t,
1122 ghostty_color_scheme_e);
1123GHOSTTY_API ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
1124 ghostty_input_mods_e);
1125GHOSTTY_API bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
1126GHOSTTY_API bool ghostty_surface_key_is_binding(ghostty_surface_t,
1127 ghostty_input_key_s,
1128 ghostty_binding_flags_e*);
1129GHOSTTY_API void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
1130GHOSTTY_API void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
1131GHOSTTY_API bool ghostty_surface_mouse_captured(ghostty_surface_t);
1132GHOSTTY_API bool ghostty_surface_mouse_button(ghostty_surface_t,
1133 ghostty_input_mouse_state_e,
1134 ghostty_input_mouse_button_e,
1135 ghostty_input_mods_e);
1136GHOSTTY_API void ghostty_surface_mouse_pos(ghostty_surface_t,
1137 double,
1138 double,
1139 ghostty_input_mods_e);
1140GHOSTTY_API void ghostty_surface_mouse_scroll(ghostty_surface_t,
1141 double,
1142 double,
1143 ghostty_input_scroll_mods_t);
1144GHOSTTY_API void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
1145GHOSTTY_API void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
1146GHOSTTY_API void ghostty_surface_request_close(ghostty_surface_t);
1147GHOSTTY_API void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
1148GHOSTTY_API void ghostty_surface_split_focus(ghostty_surface_t,
1149 ghostty_action_goto_split_e);
1150GHOSTTY_API void ghostty_surface_split_resize(ghostty_surface_t,
1151 ghostty_action_resize_split_direction_e,
1152 uint16_t);
1153GHOSTTY_API void ghostty_surface_split_equalize(ghostty_surface_t);
1154GHOSTTY_API bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
1155GHOSTTY_API void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
1156 const char*,
1157 void*,
1158 bool);
1159GHOSTTY_API bool ghostty_surface_has_selection(ghostty_surface_t);
1160GHOSTTY_API bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
1161GHOSTTY_API bool ghostty_surface_read_text(ghostty_surface_t,
1162 ghostty_selection_s,
1163 ghostty_text_s*);
1164GHOSTTY_API void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);
1165
1166#ifdef __APPLE__
1167GHOSTTY_API void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
1168GHOSTTY_API void* ghostty_surface_quicklook_font(ghostty_surface_t);
1169GHOSTTY_API bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*);
1170#endif
1171
1172GHOSTTY_API ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
1173GHOSTTY_API void ghostty_inspector_free(ghostty_surface_t);
1174GHOSTTY_API void ghostty_inspector_set_focus(ghostty_inspector_t, bool);
1175GHOSTTY_API void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
1176GHOSTTY_API void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
1177GHOSTTY_API void ghostty_inspector_mouse_button(ghostty_inspector_t,
1178 ghostty_input_mouse_state_e,
1179 ghostty_input_mouse_button_e,
1180 ghostty_input_mods_e);
1181GHOSTTY_API void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double);
1182GHOSTTY_API void ghostty_inspector_mouse_scroll(ghostty_inspector_t,
1183 double,
1184 double,
1185 ghostty_input_scroll_mods_t);
1186GHOSTTY_API void ghostty_inspector_key(ghostty_inspector_t,
1187 ghostty_input_action_e,
1188 ghostty_input_key_e,
1189 ghostty_input_mods_e);
1190GHOSTTY_API void ghostty_inspector_text(ghostty_inspector_t, const char*);
1191
1192#ifdef __APPLE__
1193GHOSTTY_API bool ghostty_inspector_metal_init(ghostty_inspector_t, void*);
1194GHOSTTY_API void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*);
1195GHOSTTY_API bool ghostty_inspector_metal_shutdown(ghostty_inspector_t);
1196#endif
1197
1198// APIs I'd like to get rid of eventually but are still needed for now.
1199// Don't use these unless you know what you're doing.
1200GHOSTTY_API void ghostty_set_window_background_blur(ghostty_app_t, void*);
1201
1202// Benchmark API, if available.
1203GHOSTTY_API bool ghostty_benchmark_cli(const char*, const char*);
1204
1205#ifdef __cplusplus
1206}
1207#endif
1208
1209#endif /* GHOSTTY_H */
packages/terminal/vendor/include/ghostty/vt.h created+163
......@@ -0,0 +1,163 @@
1/**
2 * @file vt.h
3 *
4 * libghostty-vt - Virtual terminal emulator library
5 *
6 * This library provides functionality for parsing and handling terminal
7 * escape sequences as well as maintaining terminal state such as styles,
8 * cursor position, screen, scrollback, and more.
9 *
10 * WARNING: This is an incomplete, work-in-progress API. It is not yet
11 * stable and is definitely going to change.
12 */
13
14/**
15 * @mainpage libghostty-vt - Virtual Terminal Emulator Library
16 *
17 * libghostty-vt is a C library which implements a modern terminal emulator,
18 * extracted from the [Ghostty](https://ghostty.org) terminal emulator.
19 *
20 * libghostty-vt contains the logic for handling the core parts of a terminal
21 * emulator: parsing terminal escape sequences, maintaining terminal state,
22 * encoding input events, etc. It can handle scrollback, line wrapping,
23 * reflow on resize, and more.
24 *
25 * @warning This library is currently in development and the API is not yet stable.
26 * Breaking changes are expected in future versions. Use with caution in production code.
27 *
28 * @section groups_sec API Reference
29 *
30 * The API is organized into the following groups:
31 * - @ref terminal "Terminal" - Complete terminal emulator state and rendering
32 * - @ref render "Render State" - Incremental render state updates for custom renderers
33 * - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML
34 * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
35 * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
36 * - @ref paste "Paste Utilities" - Validate paste data safety
37 * - @ref unicode "Unicode Utilities" - Codepoint properties for text layout
38 * - @ref build_info "Build Info" - Query compile-time build configuration
39 * - @ref allocator "Memory Management" - Memory management and custom allocators
40 * - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions
41 *
42 * Encoding related APIs:
43 * - @ref focus "Focus Encoding" - Encode focus in/out events into terminal sequences
44 * - @ref key "Key Encoding" - Encode key events into terminal sequences
45 * - @ref mouse "Mouse Encoding" - Encode mouse events into terminal sequences
46 *
47 * @section examples_sec Examples
48 *
49 * Complete working examples:
50 * - @ref c-vt-build-info/src/main.c - Build info query example
51 * - @ref c-vt/src/main.c - OSC parser example
52 * - @ref c-vt-encode-key/src/main.c - Key encoding example
53 * - @ref c-vt-encode-mouse/src/main.c - Mouse encoding example
54 * - @ref c-vt-paste/src/main.c - Paste safety check example
55 * - @ref c-vt-sgr/src/main.c - SGR parser example
56 * - @ref c-vt-formatter/src/main.c - Terminal formatter example
57 * - @ref c-vt-grid-traverse/src/main.c - Grid traversal example using grid refs
58 * - @ref c-vt-grid-ref-tracked/src/main.c - Tracked grid ref example
59 * - @ref c-vt-compression/src/main.c - Idle scrollback compression example
60 *
61 */
62
63/** @example c-vt-build-info/src/main.c
64 * This example demonstrates how to query compile-time build configuration
65 * such as SIMD support, Kitty graphics, and tmux control mode availability.
66 */
67
68/** @example c-vt/src/main.c
69 * This example demonstrates how to use the OSC parser to parse an OSC sequence,
70 * extract command information, and retrieve command-specific data like window titles.
71 */
72
73/** @example c-vt-encode-key/src/main.c
74 * This example demonstrates how to use the key encoder to convert key events
75 * into terminal escape sequences using the Kitty keyboard protocol.
76 */
77
78/** @example c-vt-encode-mouse/src/main.c
79 * This example demonstrates how to use the mouse encoder to convert mouse events
80 * into terminal escape sequences using the SGR mouse format.
81 */
82
83/** @example c-vt-paste/src/main.c
84 * This example demonstrates how to use the paste utilities to check if
85 * paste data is safe before sending it to the terminal.
86 */
87
88/** @example c-vt-sgr/src/main.c
89 * This example demonstrates how to use the SGR parser to parse terminal
90 * styling sequences and extract text attributes like colors and underline styles.
91 */
92
93/** @example c-vt-formatter/src/main.c
94 * This example demonstrates how to use the terminal and formatter APIs to
95 * create a terminal, write VT-encoded content into it, and format the screen
96 * contents as plain text.
97 */
98
99/** @example c-vt-grid-traverse/src/main.c
100 * This example demonstrates how to traverse the entire terminal grid using
101 * grid refs to inspect cell codepoints, row wrap state, and cell styles.
102 */
103
104/** @example c-vt-grid-ref-tracked/src/main.c
105 * This example demonstrates how to track a grid ref as the terminal scrolls,
106 * detect when it loses its value, and move it to a new point.
107 */
108
109/** @example c-vt-compression/src/main.c
110 * This example demonstrates how to schedule incremental scrollback compression
111 * after compression-relevant terminal activity becomes idle.
112 */
113
114/** @example c-vt-selection-gesture/src/main.c
115 * This example demonstrates how to use synthetic selection gesture events to
116 * derive drag and deep-press selection snapshots.
117 */
118
119/** @example c-vt-kitty-graphics/src/main.c
120 * This example demonstrates how to use the system interface to install a
121 * PNG decoder callback and send a Kitty Graphics Protocol image.
122 */
123
124#ifndef GHOSTTY_VT_H
125#define GHOSTTY_VT_H
126
127#ifdef __cplusplus
128extern "C" {
129#endif
130
131#include <ghostty/vt/types.h>
132#include <ghostty/vt/allocator.h>
133#include <ghostty/vt/build_info.h>
134#include <ghostty/vt/color.h>
135#include <ghostty/vt/color_scheme.h>
136#include <ghostty/vt/device.h>
137#include <ghostty/vt/focus.h>
138#include <ghostty/vt/formatter.h>
139#include <ghostty/vt/render.h>
140#include <ghostty/vt/terminal.h>
141#include <ghostty/vt/grid_ref.h>
142#include <ghostty/vt/grid_ref_tracked.h>
143#include <ghostty/vt/osc.h>
144#include <ghostty/vt/sgr.h>
145#include <ghostty/vt/style.h>
146#include <ghostty/vt/sys.h>
147#include <ghostty/vt/key.h>
148#include <ghostty/vt/kitty_graphics.h>
149#include <ghostty/vt/modes.h>
150#include <ghostty/vt/mouse.h>
151#include <ghostty/vt/paste.h>
152#include <ghostty/vt/point.h>
153#include <ghostty/vt/screen.h>
154#include <ghostty/vt/selection.h>
155#include <ghostty/vt/size_report.h>
156#include <ghostty/vt/unicode.h>
157#include <ghostty/vt/wasm.h>
158
159#ifdef __cplusplus
160}
161#endif
162
163#endif /* GHOSTTY_VT_H */
packages/terminal/vendor/include/ghostty/vt/allocator.h created+255
......@@ -0,0 +1,255 @@
1/**
2 * @file allocator.h
3 *
4 * Memory management interface for libghostty-vt.
5 */
6
7#ifndef GHOSTTY_VT_ALLOCATOR_H
8#define GHOSTTY_VT_ALLOCATOR_H
9
10#include <stdbool.h>
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/types.h>
14
15/** @defgroup allocator Memory Management
16 *
17 * libghostty-vt does require memory allocation for various operations,
18 * but is resilient to allocation failures and will gracefully handle
19 * out-of-memory situations by returning error codes.
20 *
21 * The exact memory management semantics are documented in the relevant
22 * functions and data structures.
23 *
24 * libghostty-vt uses explicit memory allocation via an allocator
25 * interface provided by GhosttyAllocator. The interface is based on the
26 * [Zig](https://ziglang.org) allocator interface, since this has been
27 * shown to be a flexible and powerful interface in practice and enables
28 * a wide variety of allocation strategies.
29 *
30 * **For the common case, you can pass NULL as the allocator for any
31 * function that accepts one,** and libghostty will use a default allocator.
32 * The default allocator will be libc malloc/free if libc is linked.
33 * Otherwise, a custom allocator is used (currently Zig's SMP allocator)
34 * that doesn't require any external dependencies.
35 *
36 * ## Basic Usage
37 *
38 * For simple use cases, you can ignore this interface entirely by passing NULL
39 * as the allocator parameter to functions that accept one. This will use the
40 * default allocator (typically libc malloc/free, if libc is linked, but
41 * we provide our own default allocator if libc isn't linked).
42 *
43 * To use a custom allocator:
44 * 1. Implement the GhosttyAllocatorVtable function pointers
45 * 2. Create a GhosttyAllocator struct with your vtable and context
46 * 3. Pass the allocator to functions that accept one
47 *
48 * ## Alloc/Free Helpers
49 *
50 * ghostty_alloc() and ghostty_free() provide a simple malloc/free-style
51 * interface for allocating and freeing byte buffers through the library's
52 * allocator. These are useful when:
53 *
54 * - You need to allocate a buffer to pass into a libghostty-vt function
55 * (e.g. preparing input data for ghostty_terminal_vt_write()).
56 * - You need to free a buffer returned by a libghostty-vt function
57 * (e.g. the output of ghostty_formatter_format_alloc()).
58 * - You are on a platform where the library's internal allocator differs
59 * from the consumer's C runtime (e.g. Windows, where Zig's libc and
60 * MSVC's CRT maintain separate heaps), so calling the standard C
61 * free() on library-allocated memory would be undefined behavior.
62 *
63 * Always use the same allocator (or NULL) for both the allocation and
64 * the corresponding free.
65 *
66 * @{
67 */
68
69/**
70 * Function table for custom memory allocator operations.
71 *
72 * This vtable defines the interface for a custom memory allocator. All
73 * function pointers must be valid and non-NULL.
74 *
75 * @ingroup allocator
76 *
77 * If you're not going to use a custom allocator, you can ignore all of
78 * this. All functions that take an allocator pointer allow NULL to use a
79 * default allocator.
80 *
81 * The interface is based on the Zig allocator interface. I'll say up front
82 * that it is easy to look at this interface and think "wow, this is really
83 * overcomplicated". The reason for this complexity is well thought out by
84 * the Zig folks, and it enables a diverse set of allocation strategies
85 * as shown by the Zig ecosystem. As a consolation, please note that many
86 * of the arguments are only needed for advanced use cases and can be
87 * safely ignored in simple implementations. For example, if you look at
88 * the Zig implementation of the libc allocator in `lib/std/heap.zig`
89 * (search for CAllocator), you'll see it is very simple.
90 *
91 * We chose to align with the Zig allocator interface because:
92 *
93 * 1. It is a proven interface that serves a wide variety of use cases
94 * in the real world via the Zig ecosystem. It's shown to work.
95 *
96 * 2. Our core implementation itself is Zig, and this lets us very
97 * cheaply and easily convert between C and Zig allocators.
98 *
99 * NOTE(mitchellh): In the future, we can have default implementations of
100 * resize/remap and allow those to be null.
101 */
102typedef struct {
103 /**
104 * Return a pointer to `len` bytes with specified `alignment`, or return
105 * `NULL` indicating the allocation failed.
106 *
107 * @param ctx The allocator context
108 * @param len Number of bytes to allocate
109 * @param alignment Required alignment for the allocation. Guaranteed to
110 * be a power of two between 1 and 16 inclusive.
111 * @param ret_addr First return address of the allocation call stack (0 if not provided)
112 * @return Pointer to allocated memory, or NULL if allocation failed
113 */
114 void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr);
115
116 /**
117 * Attempt to expand or shrink memory in place.
118 *
119 * `memory_len` must equal the length requested from the most recent
120 * successful call to `alloc`, `resize`, or `remap`. `alignment` must
121 * equal the same value that was passed as the `alignment` parameter to
122 * the original `alloc` call.
123 *
124 * `new_len` must be greater than zero.
125 *
126 * @param ctx The allocator context
127 * @param memory Pointer to the memory block to resize
128 * @param memory_len Current size of the memory block
129 * @param alignment Alignment (must match original allocation)
130 * @param new_len New requested size
131 * @param ret_addr First return address of the allocation call stack (0 if not provided)
132 * @return true if resize was successful in-place, false if relocation would be required
133 */
134 bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
135
136 /**
137 * Attempt to expand or shrink memory, allowing relocation.
138 *
139 * `memory_len` must equal the length requested from the most recent
140 * successful call to `alloc`, `resize`, or `remap`. `alignment` must
141 * equal the same value that was passed as the `alignment` parameter to
142 * the original `alloc` call.
143 *
144 * A non-`NULL` return value indicates the resize was successful. The
145 * allocation may have same address, or may have been relocated. In either
146 * case, the allocation now has size of `new_len`. A `NULL` return value
147 * indicates that the resize would be equivalent to allocating new memory,
148 * copying the bytes from the old memory, and then freeing the old memory.
149 * In such case, it is more efficient for the caller to perform the copy.
150 *
151 * `new_len` must be greater than zero.
152 *
153 * @param ctx The allocator context
154 * @param memory Pointer to the memory block to remap
155 * @param memory_len Current size of the memory block
156 * @param alignment Alignment (must match original allocation)
157 * @param new_len New requested size
158 * @param ret_addr First return address of the allocation call stack (0 if not provided)
159 * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed
160 */
161 void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
162
163 /**
164 * Free and invalidate a region of memory.
165 *
166 * `memory_len` must equal the length requested from the most recent
167 * successful call to `alloc`, `resize`, or `remap`. `alignment` must
168 * equal the same value that was passed as the `alignment` parameter to
169 * the original `alloc` call.
170 *
171 * @param ctx The allocator context
172 * @param memory Pointer to the memory block to free
173 * @param memory_len Size of the memory block
174 * @param alignment Alignment (must match original allocation)
175 * @param ret_addr First return address of the allocation call stack (0 if not provided)
176 */
177 void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr);
178} GhosttyAllocatorVtable;
179
180/**
181 * Custom memory allocator.
182 *
183 * For functions that take an allocator pointer, a NULL pointer indicates
184 * that the default allocator should be used. The default allocator will
185 * be libc malloc/free if we're linking to libc. If libc isn't linked,
186 * a custom allocator is used (currently Zig's SMP allocator).
187 *
188 * @ingroup allocator
189 *
190 * Usage example:
191 * @code
192 * GhosttyAllocator allocator = {
193 * .vtable = &my_allocator_vtable,
194 * .ctx = my_allocator_state
195 * };
196 * @endcode
197 */
198typedef struct GhosttyAllocator {
199 /**
200 * Opaque context pointer passed to all vtable functions.
201 * This allows the allocator implementation to maintain state
202 * or reference external resources needed for memory management.
203 */
204 void *ctx;
205
206 /**
207 * Pointer to the allocator's vtable containing function pointers
208 * for memory operations (alloc, resize, remap, free).
209 */
210 const GhosttyAllocatorVtable *vtable;
211} GhosttyAllocator;
212
213/**
214 * Allocate a buffer of `len` bytes.
215 *
216 * Uses the provided allocator, or the default allocator if NULL is passed.
217 * The returned buffer must be freed with ghostty_free() using the same
218 * allocator.
219 *
220 * @param allocator Pointer to the allocator to use, or NULL for the default
221 * @param len Number of bytes to allocate
222 * @return Pointer to the allocated buffer, or NULL if allocation failed
223 *
224 * @ingroup allocator
225 */
226GHOSTTY_API uint8_t* ghostty_alloc(const GhosttyAllocator* allocator, size_t len);
227
228/**
229 * Free memory that was allocated by a libghostty-vt function.
230 *
231 * Use this to free buffers returned by functions such as
232 * ghostty_formatter_format_alloc(). Pass the same allocator that was
233 * used for the allocation, or NULL if the default allocator was used.
234 *
235 * On platforms where the library's internal allocator differs from the
236 * consumer's C runtime (e.g. Windows, where Zig's libc and MSVC's CRT
237 * maintain separate heaps), calling the standard C free() on memory
238 * allocated by the library causes undefined behavior. This function
239 * guarantees the correct allocator is used regardless of platform.
240 *
241 * It is safe to pass a NULL pointer; the call is a no-op in that case.
242 *
243 * @param allocator Pointer to the allocator that was used to allocate the
244 * memory, or NULL if the default allocator was used
245 * @param ptr Pointer to the memory to free (may be NULL)
246 * @param len Length of the allocation in bytes (must match the original
247 * allocation size)
248 *
249 * @ingroup allocator
250 */
251GHOSTTY_API void ghostty_free(const GhosttyAllocator* allocator, uint8_t* ptr, size_t len);
252
253/** @} */
254
255#endif /* GHOSTTY_VT_ALLOCATOR_H */
packages/terminal/vendor/include/ghostty/vt/build_info.h created+150
......@@ -0,0 +1,150 @@
1/**
2 * @file build_info.h
3 *
4 * Build info - query compile-time build configuration of libghostty-vt.
5 */
6
7#ifndef GHOSTTY_VT_BUILD_INFO_H
8#define GHOSTTY_VT_BUILD_INFO_H
9
10/** @defgroup build_info Build Info
11 *
12 * Query compile-time build configuration of libghostty-vt.
13 *
14 * These values reflect the options the library was built with and are
15 * constant for the lifetime of the process.
16 *
17 * ## Basic Usage
18 *
19 * Use ghostty_build_info() to query individual build options:
20 *
21 * @snippet c-vt-build-info/src/main.c build-info-query
22 *
23 * @{
24 */
25
26#include <stddef.h>
27#include <stdbool.h>
28
29#include <ghostty/vt/types.h>
30
31#ifdef __cplusplus
32extern "C" {
33#endif
34
35/**
36 * Build optimization mode.
37 */
38typedef enum GHOSTTY_ENUM_TYPED {
39 GHOSTTY_OPTIMIZE_DEBUG = 0,
40 GHOSTTY_OPTIMIZE_RELEASE_SAFE = 1,
41 GHOSTTY_OPTIMIZE_RELEASE_SMALL = 2,
42 GHOSTTY_OPTIMIZE_RELEASE_FAST = 3,
43 GHOSTTY_OPTIMIZE_MODE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
44} GhosttyOptimizeMode;
45
46/**
47 * Build info data types that can be queried.
48 *
49 * Each variant documents the expected output pointer type.
50 */
51typedef enum GHOSTTY_ENUM_TYPED {
52 /** Invalid data type. Never results in any data extraction. */
53 GHOSTTY_BUILD_INFO_INVALID = 0,
54
55 /**
56 * Whether SIMD-accelerated code paths are enabled.
57 *
58 * Output type: bool *
59 */
60 GHOSTTY_BUILD_INFO_SIMD = 1,
61
62 /**
63 * Whether Kitty graphics protocol support is available.
64 *
65 * Output type: bool *
66 */
67 GHOSTTY_BUILD_INFO_KITTY_GRAPHICS = 2,
68
69 /**
70 * Whether tmux control mode support is available.
71 *
72 * Output type: bool *
73 */
74 GHOSTTY_BUILD_INFO_TMUX_CONTROL_MODE = 3,
75
76 /**
77 * The optimization mode the library was built with.
78 *
79 * Output type: GhosttyOptimizeMode *
80 */
81 GHOSTTY_BUILD_INFO_OPTIMIZE = 4,
82
83 /**
84 * The full version string (e.g. "1.2.3" or "1.2.3-dev+abcdef").
85 *
86 * Output type: GhosttyString *
87 */
88 GHOSTTY_BUILD_INFO_VERSION_STRING = 5,
89
90 /**
91 * The major version number.
92 *
93 * Output type: size_t *
94 */
95 GHOSTTY_BUILD_INFO_VERSION_MAJOR = 6,
96
97 /**
98 * The minor version number.
99 *
100 * Output type: size_t *
101 */
102 GHOSTTY_BUILD_INFO_VERSION_MINOR = 7,
103
104 /**
105 * The patch version number.
106 *
107 * Output type: size_t *
108 */
109 GHOSTTY_BUILD_INFO_VERSION_PATCH = 8,
110
111 /**
112 * The pre metadata string (e.g. "alpha", "beta", "dev"). Has zero length if
113 * no pre metadata is present.
114 *
115 * Output type: GhosttyString *
116 */
117 GHOSTTY_BUILD_INFO_VERSION_PRE = 9,
118
119 /**
120 * The build metadata string (e.g. commit hash). Has zero length if
121 * no build metadata is present.
122 *
123 * Output type: GhosttyString *
124 */
125 GHOSTTY_BUILD_INFO_VERSION_BUILD = 10,
126 GHOSTTY_BUILD_INFO_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
127} GhosttyBuildInfo;
128
129/**
130 * Query a compile-time build configuration value.
131 *
132 * The caller must pass a pointer to the correct output type for the
133 * requested data (see GhosttyBuildInfo variants for types).
134 *
135 * @param data The build info field to query
136 * @param out Pointer to store the result (type depends on data parameter)
137 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
138 * data type is invalid
139 *
140 * @ingroup build_info
141 */
142GHOSTTY_API GhosttyResult ghostty_build_info(GhosttyBuildInfo data, void *out);
143
144#ifdef __cplusplus
145}
146#endif
147
148/** @} */
149
150#endif /* GHOSTTY_VT_BUILD_INFO_H */
packages/terminal/vendor/include/ghostty/vt/color.h created+479
......@@ -0,0 +1,479 @@
1/**
2 * @file color.h
3 *
4 * Color types and utilities.
5 */
6
7#ifndef GHOSTTY_VT_COLOR_H
8#define GHOSTTY_VT_COLOR_H
9
10/** @defgroup color Color Utilities
11 *
12 * Color parsing, palette generation, color math, and X11 color name
13 * utilities shared by libghostty-vt.
14 *
15 * These APIs expose Ghostty's color semantics directly to embedders. Use
16 * them when an application needs to parse the same color strings as Ghostty
17 * config and theme files, generate the same 256-color palette used by the
18 * terminal, list supported X11 color names, or make UI decisions from
19 * luminance and contrast values.
20 *
21 * ## Parsing Colors
22 *
23 * ghostty_color_parse() accepts the flexible syntax used by Ghostty for
24 * terminal colors:
25 *
26 * - X11 color names, matched ASCII case-insensitively.
27 * - 3- or 6-digit hex colors, with or without a leading `#`.
28 * - 9- or 12-digit hex colors, with a leading `#`.
29 * - XParseColor-style `rgb:<red>/<green>/<blue>` values.
30 * - XParseColor-style `rgbi:<red>/<green>/<blue>` values.
31 *
32 * Leading and trailing spaces and tabs are ignored. Use
33 * ghostty_color_parse_x11() when only X11 names should be accepted.
34 *
35 * @code{.c}
36 * GhosttyColorRgb color;
37 *
38 * if (ghostty_color_parse(
39 * "ForestGreen",
40 * sizeof("ForestGreen") - 1,
41 * &color) != GHOSTTY_SUCCESS) {
42 * // Handle invalid color input.
43 * }
44 *
45 * ghostty_color_parse("#abc", sizeof("#abc") - 1, &color);
46 * ghostty_color_parse("rgb:12/34/56", sizeof("rgb:12/34/56") - 1, &color);
47 * @endcode
48 *
49 * ## Palette Entries
50 *
51 * ghostty_color_parse_palette_entry() parses a single Ghostty palette
52 * override in `INDEX=COLOR` form. The index may be decimal or use a `0x`,
53 * `0o`, or `0b` prefix. The color side uses ghostty_color_parse().
54 *
55 * @code{.c}
56 * GhosttyColorRgb palette[256];
57 * ghostty_color_palette_default(palette);
58 *
59 * uint8_t index;
60 * GhosttyColorRgb rgb;
61 *
62 * if (ghostty_color_parse_palette_entry(
63 * "0x10=#282c34",
64 * sizeof("0x10=#282c34") - 1,
65 * &index,
66 * &rgb) == GHOSTTY_SUCCESS) {
67 * palette[index] = rgb;
68 * }
69 * @endcode
70 *
71 * ## Palette Generation
72 *
73 * ghostty_color_palette_generate() derives the 216-color cube and grayscale
74 * ramp from a base palette, background, and foreground. Set bits in
75 * GhosttyColorPaletteMask preserve specific indices from the base palette.
76 * The output may alias the base input.
77 *
78 * @code{.c}
79 * GhosttyColorRgb palette[256];
80 * ghostty_color_palette_default(palette);
81 *
82 * GhosttyColorPaletteMask skip = {0};
83 * GHOSTTY_COLOR_PALETTE_MASK_SET(&skip, 16);
84 *
85 * GhosttyColorRgb background = {40, 44, 52};
86 * GhosttyColorRgb foreground = {220, 223, 228};
87 *
88 * ghostty_color_palette_generate(
89 * palette,
90 * &skip,
91 * &background,
92 * &foreground,
93 * true,
94 * palette);
95 * @endcode
96 *
97 * ## X11 Color Names
98 *
99 * The X11 name table is static program-lifetime memory. Entries are in
100 * rgb.txt order and are terminated by an entry with `name == NULL`.
101 * ghostty_color_x11_name_count() returns the number of non-terminator
102 * entries.
103 *
104 * @code{.c}
105 * const GhosttyColorX11Entry* names = ghostty_color_x11_names();
106 * size_t count = ghostty_color_x11_name_count();
107 *
108 * for (size_t i = 0; i < count; i++) {
109 * // names[i].name and names[i].color are valid here.
110 * }
111 * @endcode
112 *
113 * @{
114 */
115
116#include <stdbool.h>
117#include <stddef.h>
118#include <stdint.h>
119#include <ghostty/vt/types.h>
120
121#ifdef __cplusplus
122extern "C" {
123#endif
124
125/**
126 * RGB color value.
127 *
128 * @ingroup color
129 */
130typedef struct {
131 uint8_t r; /**< Red component (0-255) */
132 uint8_t g; /**< Green component (0-255) */
133 uint8_t b; /**< Blue component (0-255) */
134} GhosttyColorRgb;
135
136/**
137 * Palette color index (0-255).
138 *
139 * @ingroup color
140 */
141typedef uint8_t GhosttyColorPaletteIndex;
142
143/**
144 * A 256-bit mask of palette indices.
145 *
146 * Index i is set iff `(bits[i >> 6] >> (i & 63)) & 1` is 1.
147 * The mask is typically initialized to zero and then populated with
148 * GHOSTTY_COLOR_PALETTE_MASK_SET().
149 *
150 * @code{.c}
151 * GhosttyColorPaletteMask mask = {0};
152 * GHOSTTY_COLOR_PALETTE_MASK_SET(&mask, 20);
153 * if (GHOSTTY_COLOR_PALETTE_MASK_IS_SET(&mask, 20)) {
154 * // Index 20 will be preserved.
155 * }
156 * @endcode
157 *
158 * @ingroup color
159 */
160typedef struct {
161 uint64_t bits[4];
162} GhosttyColorPaletteMask;
163
164/**
165 * An entry in Ghostty's X11 color name table.
166 *
167 * @ingroup color
168 */
169typedef struct {
170 /** Null-terminated color name. NULL marks the end of the table. */
171 const char* name;
172 /** The RGB value of the color. */
173 GhosttyColorRgb color;
174} GhosttyColorX11Entry;
175
176/**
177 * Return the storage word for a palette mask index.
178 *
179 * @param index The palette index (0-255)
180 *
181 * @ingroup color
182 */
183#define GHOSTTY_COLOR_PALETTE_MASK_WORD(index) ((index) >> 6)
184
185/**
186 * Return the storage bit for a palette mask index.
187 *
188 * @param index The palette index (0-255)
189 *
190 * @ingroup color
191 */
192#define GHOSTTY_COLOR_PALETTE_MASK_BIT(index) (UINT64_C(1) << ((index) & 63))
193
194/**
195 * Set a palette mask index.
196 *
197 * @param mask Pointer to a GhosttyColorPaletteMask
198 * @param index The palette index (0-255)
199 *
200 * @ingroup color
201 */
202#define GHOSTTY_COLOR_PALETTE_MASK_SET(mask, index) \
203 ((mask)->bits[GHOSTTY_COLOR_PALETTE_MASK_WORD(index)] |= GHOSTTY_COLOR_PALETTE_MASK_BIT(index))
204
205/**
206 * Clear a palette mask index.
207 *
208 * @param mask Pointer to a GhosttyColorPaletteMask
209 * @param index The palette index (0-255)
210 *
211 * @ingroup color
212 */
213#define GHOSTTY_COLOR_PALETTE_MASK_UNSET(mask, index) \
214 ((mask)->bits[GHOSTTY_COLOR_PALETTE_MASK_WORD(index)] &= ~GHOSTTY_COLOR_PALETTE_MASK_BIT(index))
215
216/**
217 * Test whether a palette mask index is set.
218 *
219 * @param mask Pointer to a GhosttyColorPaletteMask
220 * @param index The palette index (0-255)
221 * @return true if the palette index is set, false otherwise
222 *
223 * @ingroup color
224 */
225#define GHOSTTY_COLOR_PALETTE_MASK_IS_SET(mask, index) \
226 (((mask)->bits[GHOSTTY_COLOR_PALETTE_MASK_WORD(index)] & GHOSTTY_COLOR_PALETTE_MASK_BIT(index)) != 0)
227
228/** Black color (0) @ingroup color */
229#define GHOSTTY_COLOR_NAMED_BLACK 0
230/** Red color (1) @ingroup color */
231#define GHOSTTY_COLOR_NAMED_RED 1
232/** Green color (2) @ingroup color */
233#define GHOSTTY_COLOR_NAMED_GREEN 2
234/** Yellow color (3) @ingroup color */
235#define GHOSTTY_COLOR_NAMED_YELLOW 3
236/** Blue color (4) @ingroup color */
237#define GHOSTTY_COLOR_NAMED_BLUE 4
238/** Magenta color (5) @ingroup color */
239#define GHOSTTY_COLOR_NAMED_MAGENTA 5
240/** Cyan color (6) @ingroup color */
241#define GHOSTTY_COLOR_NAMED_CYAN 6
242/** White color (7) @ingroup color */
243#define GHOSTTY_COLOR_NAMED_WHITE 7
244/** Bright black color (8) @ingroup color */
245#define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8
246/** Bright red color (9) @ingroup color */
247#define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9
248/** Bright green color (10) @ingroup color */
249#define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10
250/** Bright yellow color (11) @ingroup color */
251#define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11
252/** Bright blue color (12) @ingroup color */
253#define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12
254/** Bright magenta color (13) @ingroup color */
255#define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13
256/** Bright cyan color (14) @ingroup color */
257#define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14
258/** Bright white color (15) @ingroup color */
259#define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15
260
261/**
262 * Get the RGB color components.
263 *
264 * This function extracts the individual red, green, and blue components
265 * from a GhosttyColorRgb value. Primarily useful in WebAssembly environments
266 * where accessing struct fields directly is difficult.
267 *
268 * @param color Pointer to the RGB color value
269 * @param r Pointer to store the red component (0-255)
270 * @param g Pointer to store the green component (0-255)
271 * @param b Pointer to store the blue component (0-255)
272 *
273 * @ingroup color
274 */
275GHOSTTY_API void ghostty_color_rgb_get(const GhosttyColorRgb* color,
276 uint8_t* r,
277 uint8_t* g,
278 uint8_t* b);
279
280/**
281 * Parse an X11 color name.
282 *
283 * The color name is resolved from Ghostty's embedded rgb.txt table.
284 * Leading and trailing spaces and tabs are trimmed, and matching is
285 * ASCII case-insensitive. Hex values are not accepted by this function.
286 *
287 * @param name The color name bytes (must not be NULL)
288 * @param len The length of @p name in bytes
289 * @param[out] out The parsed RGB color
290 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if no color
291 * matches or @p name is NULL
292 *
293 * @ingroup color
294 */
295GHOSTTY_API GhosttyResult ghostty_color_parse_x11(
296 const char* name,
297 size_t len,
298 GhosttyColorRgb* out);
299
300/**
301 * Parse a flexible Ghostty color value.
302 *
303 * Accepts Ghostty's terminal color syntax: X11 color names, hex colors
304 * in 3-, 6-, 9-, or 12-digit form (the leading # is optional for 3- and
305 * 6-digit values), and rgb:<red>/<green>/<blue> or
306 * rgbi:<red>/<green>/<blue> specifications. Leading and trailing spaces
307 * and tabs are trimmed.
308 *
309 * @param value The color value bytes (must not be NULL)
310 * @param len The length of @p value in bytes
311 * @param[out] out The parsed RGB color
312 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if parsing
313 * fails or @p value is NULL
314 *
315 * @ingroup color
316 */
317GHOSTTY_API GhosttyResult ghostty_color_parse(
318 const char* value,
319 size_t len,
320 GhosttyColorRgb* out);
321
322/**
323 * Parse a Ghostty palette entry.
324 *
325 * Accepts Ghostty palette config syntax: N=COLOR. N is a palette index
326 * from 0 to 255 in decimal or in 0x, 0o, or 0b-prefixed form. Spaces and
327 * tabs around N and COLOR are ignored. COLOR accepts the same syntax as
328 * ghostty_color_parse().
329 *
330 * @param value The palette entry bytes (must not be NULL)
331 * @param len The length of @p value in bytes
332 * @param[out] out_index The parsed palette index
333 * @param[out] out_rgb The parsed RGB color
334 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE on any
335 * failure, including index overflow
336 *
337 * @ingroup color
338 */
339GHOSTTY_API GhosttyResult ghostty_color_parse_palette_entry(
340 const char* value,
341 size_t len,
342 uint8_t* out_index,
343 GhosttyColorRgb* out_rgb);
344
345/**
346 * Get Ghostty's built-in default 256-color palette.
347 *
348 * Writes exactly 256 entries: Ghostty's base16 defaults, the xterm
349 * 6x6x6 color cube, and the grayscale ramp.
350 *
351 * @param[out] out The output palette, an array of exactly 256
352 * GhosttyColorRgb values
353 *
354 * @ingroup color
355 */
356GHOSTTY_API void ghostty_color_palette_default(GhosttyColorRgb* out);
357
358/**
359 * Generate a 256-color palette from base colors.
360 *
361 * The base palette supplies indices 0-15, which are always preserved.
362 * If @p base is NULL, Ghostty's default palette is used. If @p skip is
363 * NULL, no extra indices are skipped. Set bits in @p skip preserve those
364 * indices from @p base. The 216-color cube at indices 16-231 is generated
365 * with trilinear CIELAB interpolation, and the grayscale ramp at indices
366 * 232-255 is interpolated from the background to the foreground.
367 *
368 * For light themes, @p harmonious controls whether the generated palette
369 * keeps the background-to-foreground orientation. When false, Ghostty
370 * swaps the light background and dark foreground so the cube and ramp run
371 * dark-to-light. The output palette may be the same pointer as @p base.
372 *
373 * @param base The base palette, an array of exactly 256 GhosttyColorRgb
374 * values, or NULL to use Ghostty's default palette
375 * @param skip The palette indices to preserve from @p base, or NULL for
376 * an empty mask
377 * @param bg The terminal background color (must not be NULL)
378 * @param fg The terminal foreground color (must not be NULL)
379 * @param harmonious Whether light themes keep background-to-foreground
380 * orientation
381 * @param[out] out The output palette, an array of exactly 256
382 * GhosttyColorRgb values
383 *
384 * @ingroup color
385 */
386GHOSTTY_API void ghostty_color_palette_generate(
387 const GhosttyColorRgb* base,
388 const GhosttyColorPaletteMask* skip,
389 const GhosttyColorRgb* bg,
390 const GhosttyColorRgb* fg,
391 bool harmonious,
392 GhosttyColorRgb* out);
393
394/**
395 * Calculate W3C relative luminance for an RGB color.
396 *
397 * Returns a normalized value from 0.0 for black to 1.0 for white.
398 * See https://www.w3.org/TR/WCAG20/#relativeluminancedef.
399 *
400 * @param color The RGB color (must not be NULL)
401 * @return Relative luminance in the range 0.0 to 1.0
402 *
403 * @ingroup color
404 */
405GHOSTTY_API double ghostty_color_luminance(const GhosttyColorRgb* color);
406
407/**
408 * Calculate perceived luminance for an RGB color.
409 *
410 * Returns a normalized value from 0.0 for black to 1.0 for white.
411 * Ghostty treats a background color as light when this exceeds 0.5.
412 * This is not the metric used internally by
413 * ghostty_color_palette_generate(), which uses CIELAB lightness.
414 *
415 * @param color The RGB color (must not be NULL)
416 * @return Perceived luminance in the range 0.0 to 1.0
417 *
418 * @ingroup color
419 */
420GHOSTTY_API double ghostty_color_perceived_luminance(const GhosttyColorRgb* color);
421
422/**
423 * Calculate the WCAG contrast ratio between two RGB colors.
424 *
425 * The contrast ratio is symmetric and ranges from 1.0 for identical
426 * colors to 21.0 for black and white.
427 *
428 * @param a The first RGB color (must not be NULL)
429 * @param b The second RGB color (must not be NULL)
430 * @return WCAG contrast ratio in the range 1.0 to 21.0
431 *
432 * @ingroup color
433 */
434GHOSTTY_API double ghostty_color_contrast(const GhosttyColorRgb* a,
435 const GhosttyColorRgb* b);
436
437/**
438 * Get Ghostty's X11 color name table.
439 *
440 * The returned pointer references static memory valid for the program
441 * lifetime and is never NULL. Entries are in rgb.txt order and are
442 * terminated by an entry with name == NULL. Aliases are separate entries,
443 * such as "medium spring green" and "MediumSpringGreen". Names are the
444 * exact supported spellings from rgb.txt; ghostty_color_parse_x11() also
445 * matches them case-insensitively.
446 *
447 * @code{.c}
448 * for (const GhosttyColorX11Entry* e = ghostty_color_x11_names();
449 * e->name != NULL;
450 * e++) {
451 * // e->name and e->color are valid here.
452 * }
453 * @endcode
454 *
455 * @return Pointer to the first X11 color entry
456 *
457 * @ingroup color
458 */
459GHOSTTY_API const GhosttyColorX11Entry* ghostty_color_x11_names(void);
460
461/**
462 * Get the number of X11 color name entries.
463 *
464 * The returned count excludes the NULL terminator and is provided so
465 * bindings can preallocate storage before reading ghostty_color_x11_names().
466 *
467 * @return Number of X11 color name entries
468 *
469 * @ingroup color
470 */
471GHOSTTY_API size_t ghostty_color_x11_name_count(void);
472
473#ifdef __cplusplus
474}
475#endif
476
477/** @} */
478
479#endif /* GHOSTTY_VT_COLOR_H */
packages/terminal/vendor/include/ghostty/vt/color_scheme.h created+73
......@@ -0,0 +1,73 @@
1/**
2 * @file color_scheme.h
3 *
4 * Color scheme report encoding - encode terminal color scheme reports into
5 * escape sequences.
6 */
7
8#ifndef GHOSTTY_VT_COLOR_SCHEME_H
9#define GHOSTTY_VT_COLOR_SCHEME_H
10
11/** @defgroup color_scheme Color Scheme Report Encoding
12 *
13 * Utilities for encoding color scheme reports into terminal escape
14 * sequences for color scheme reporting mode (mode 2031).
15 *
16 * ## Basic Usage
17 *
18 * Use ghostty_color_scheme_report_encode() to encode a color scheme report
19 * into a caller-provided buffer. If the buffer is too small, the function
20 * returns GHOSTTY_OUT_OF_SPACE and sets the required size in the output
21 * parameter.
22 *
23 * ## Example
24 *
25 * @snippet c-vt-color-scheme/src/main.c color-scheme-report-encode
26 *
27 * @{
28 */
29
30#include <stddef.h>
31#include <ghostty/vt/types.h>
32#include <ghostty/vt/device.h>
33
34#ifdef __cplusplus
35extern "C" {
36#endif
37
38/**
39 * Encode a color scheme report into an escape sequence.
40 *
41 * Encodes a color scheme report into the provided buffer. Dark color schemes
42 * emit ESC [ ? 997 ; 1 n, and light color schemes emit ESC [ ? 997 ; 2 n.
43 * The encoded bytes are identical to the terminal's internal CSI ? 996 n
44 * query response.
45 *
46 * Hosts should gate unsolicited sends on GHOSTTY_MODE_COLOR_SCHEME_REPORT
47 * (mode 2031) being set, which can be checked via the mode getters.
48 *
49 * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE
50 * and writes the required buffer size to @p out_written. The caller can
51 * then retry with a sufficiently sized buffer.
52 *
53 * @param scheme The color scheme to encode
54 * @param buf Output buffer to write the encoded sequence into (may be NULL)
55 * @param buf_len Size of the output buffer in bytes
56 * @param[out] out_written On success, the number of bytes written. On
57 * GHOSTTY_OUT_OF_SPACE, the required buffer size.
58 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer
59 * is too small
60 */
61GHOSTTY_API GhosttyResult ghostty_color_scheme_report_encode(
62 GhosttyColorScheme scheme,
63 char* buf,
64 size_t buf_len,
65 size_t* out_written);
66
67#ifdef __cplusplus
68}
69#endif
70
71/** @} */
72
73#endif /* GHOSTTY_VT_COLOR_SCHEME_H */
packages/terminal/vendor/include/ghostty/vt/device.h created+151
......@@ -0,0 +1,151 @@
1/**
2 * @file device.h
3 *
4 * Device types used by the terminal for device status and device attribute
5 * queries.
6 */
7
8#ifndef GHOSTTY_VT_DEVICE_H
9#define GHOSTTY_VT_DEVICE_H
10
11#include <stddef.h>
12#include <stdint.h>
13
14/* DA1 conformance levels (Pp parameter). */
15#define GHOSTTY_DA_CONFORMANCE_VT100 1
16#define GHOSTTY_DA_CONFORMANCE_VT101 1
17#define GHOSTTY_DA_CONFORMANCE_VT102 6
18#define GHOSTTY_DA_CONFORMANCE_VT125 12
19#define GHOSTTY_DA_CONFORMANCE_VT131 7
20#define GHOSTTY_DA_CONFORMANCE_VT132 4
21#define GHOSTTY_DA_CONFORMANCE_VT220 62
22#define GHOSTTY_DA_CONFORMANCE_VT240 62
23#define GHOSTTY_DA_CONFORMANCE_VT320 63
24#define GHOSTTY_DA_CONFORMANCE_VT340 63
25#define GHOSTTY_DA_CONFORMANCE_VT420 64
26#define GHOSTTY_DA_CONFORMANCE_VT510 65
27#define GHOSTTY_DA_CONFORMANCE_VT520 65
28#define GHOSTTY_DA_CONFORMANCE_VT525 65
29#define GHOSTTY_DA_CONFORMANCE_LEVEL_2 62
30#define GHOSTTY_DA_CONFORMANCE_LEVEL_3 63
31#define GHOSTTY_DA_CONFORMANCE_LEVEL_4 64
32#define GHOSTTY_DA_CONFORMANCE_LEVEL_5 65
33
34/* DA1 feature codes (Ps parameters). */
35#define GHOSTTY_DA_FEATURE_COLUMNS_132 1
36#define GHOSTTY_DA_FEATURE_PRINTER 2
37#define GHOSTTY_DA_FEATURE_REGIS 3
38#define GHOSTTY_DA_FEATURE_SIXEL 4
39#define GHOSTTY_DA_FEATURE_SELECTIVE_ERASE 6
40#define GHOSTTY_DA_FEATURE_USER_DEFINED_KEYS 8
41#define GHOSTTY_DA_FEATURE_NATIONAL_REPLACEMENT 9
42#define GHOSTTY_DA_FEATURE_TECHNICAL_CHARACTERS 15
43#define GHOSTTY_DA_FEATURE_LOCATOR 16
44#define GHOSTTY_DA_FEATURE_TERMINAL_STATE 17
45#define GHOSTTY_DA_FEATURE_WINDOWING 18
46#define GHOSTTY_DA_FEATURE_HORIZONTAL_SCROLLING 21
47#define GHOSTTY_DA_FEATURE_ANSI_COLOR 22
48#define GHOSTTY_DA_FEATURE_RECTANGULAR_EDITING 28
49#define GHOSTTY_DA_FEATURE_ANSI_TEXT_LOCATOR 29
50#define GHOSTTY_DA_FEATURE_CLIPBOARD 52
51
52/* DA2 device type identifiers (Pp parameter). */
53#define GHOSTTY_DA_DEVICE_TYPE_VT100 0
54#define GHOSTTY_DA_DEVICE_TYPE_VT220 1
55#define GHOSTTY_DA_DEVICE_TYPE_VT240 2
56#define GHOSTTY_DA_DEVICE_TYPE_VT330 18
57#define GHOSTTY_DA_DEVICE_TYPE_VT340 19
58#define GHOSTTY_DA_DEVICE_TYPE_VT320 24
59#define GHOSTTY_DA_DEVICE_TYPE_VT382 32
60#define GHOSTTY_DA_DEVICE_TYPE_VT420 41
61#define GHOSTTY_DA_DEVICE_TYPE_VT510 61
62#define GHOSTTY_DA_DEVICE_TYPE_VT520 64
63#define GHOSTTY_DA_DEVICE_TYPE_VT525 65
64
65#ifdef __cplusplus
66extern "C" {
67#endif
68
69/**
70 * Color scheme reported in response to a CSI ? 996 n query.
71 *
72 * @ingroup terminal
73 */
74typedef enum GHOSTTY_ENUM_TYPED {
75 GHOSTTY_COLOR_SCHEME_LIGHT = 0,
76 GHOSTTY_COLOR_SCHEME_DARK = 1,
77 GHOSTTY_COLOR_SCHEME_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
78} GhosttyColorScheme;
79
80/**
81 * Primary device attributes (DA1) response data.
82 *
83 * Returned as part of GhosttyDeviceAttributes in response to a CSI c query.
84 * The conformance_level is the Pp parameter and features contains the Ps
85 * feature codes.
86 *
87 * @ingroup terminal
88 */
89typedef struct {
90 /** Conformance level (Pp parameter). E.g. 62 for VT220. */
91 uint16_t conformance_level;
92
93 /** DA1 feature codes. Only the first num_features entries are valid. */
94 uint16_t features[64];
95
96 /** Number of valid entries in the features array. */
97 size_t num_features;
98} GhosttyDeviceAttributesPrimary;
99
100/**
101 * Secondary device attributes (DA2) response data.
102 *
103 * Returned as part of GhosttyDeviceAttributes in response to a CSI > c query.
104 * Response format: CSI > Pp ; Pv ; Pc c
105 *
106 * @ingroup terminal
107 */
108typedef struct {
109 /** Terminal type identifier (Pp). E.g. 1 for VT220. */
110 uint16_t device_type;
111
112 /** Firmware/patch version number (Pv). */
113 uint16_t firmware_version;
114
115 /** ROM cartridge registration number (Pc). Always 0 for emulators. */
116 uint16_t rom_cartridge;
117} GhosttyDeviceAttributesSecondary;
118
119/**
120 * Tertiary device attributes (DA3) response data.
121 *
122 * Returned as part of GhosttyDeviceAttributes in response to a CSI = c query.
123 * Response format: DCS ! | D...D ST (DECRPTUI).
124 *
125 * @ingroup terminal
126 */
127typedef struct {
128 /** Unit ID encoded as 8 uppercase hex digits in the response. */
129 uint32_t unit_id;
130} GhosttyDeviceAttributesTertiary;
131
132/**
133 * Device attributes response data for all three DA levels.
134 *
135 * Filled by the device_attributes callback in response to CSI c,
136 * CSI > c, or CSI = c queries. The terminal uses whichever sub-struct
137 * matches the request type.
138 *
139 * @ingroup terminal
140 */
141typedef struct {
142 GhosttyDeviceAttributesPrimary primary;
143 GhosttyDeviceAttributesSecondary secondary;
144 GhosttyDeviceAttributesTertiary tertiary;
145} GhosttyDeviceAttributes;
146
147#ifdef __cplusplus
148}
149#endif
150
151#endif /* GHOSTTY_VT_DEVICE_H */
packages/terminal/vendor/include/ghostty/vt/focus.h created+76
......@@ -0,0 +1,76 @@
1/**
2 * @file focus.h
3 *
4 * Focus encoding - encode focus in/out events into terminal escape sequences.
5 */
6
7#ifndef GHOSTTY_VT_FOCUS_H
8#define GHOSTTY_VT_FOCUS_H
9
10/** @defgroup focus Focus Encoding
11 *
12 * Utilities for encoding focus gained/lost events into terminal escape
13 * sequences (CSI I / CSI O) for focus reporting mode (mode 1004).
14 *
15 * ## Basic Usage
16 *
17 * Use ghostty_focus_encode() to encode a focus event into a caller-provided
18 * buffer. If the buffer is too small, the function returns
19 * GHOSTTY_OUT_OF_SPACE and sets the required size in the output parameter.
20 *
21 * ## Example
22 *
23 * @snippet c-vt-encode-focus/src/main.c focus-encode
24 *
25 * @{
26 */
27
28#include <stddef.h>
29#include <ghostty/vt/types.h>
30
31#ifdef __cplusplus
32extern "C" {
33#endif
34
35/**
36 * Focus event types for focus reporting mode (mode 1004).
37 */
38typedef enum GHOSTTY_ENUM_TYPED {
39 /** Terminal window gained focus */
40 GHOSTTY_FOCUS_GAINED = 0,
41 /** Terminal window lost focus */
42 GHOSTTY_FOCUS_LOST = 1,
43 GHOSTTY_FOCUS_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
44} GhosttyFocusEvent;
45
46/**
47 * Encode a focus event into a terminal escape sequence.
48 *
49 * Encodes a focus gained (CSI I) or focus lost (CSI O) report into the
50 * provided buffer.
51 *
52 * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE
53 * and writes the required buffer size to @p out_written. The caller can
54 * then retry with a sufficiently sized buffer.
55 *
56 * @param event The focus event to encode
57 * @param buf Output buffer to write the encoded sequence into (may be NULL)
58 * @param buf_len Size of the output buffer in bytes
59 * @param[out] out_written On success, the number of bytes written. On
60 * GHOSTTY_OUT_OF_SPACE, the required buffer size.
61 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer
62 * is too small
63 */
64GHOSTTY_API GhosttyResult ghostty_focus_encode(
65 GhosttyFocusEvent event,
66 char* buf,
67 size_t buf_len,
68 size_t* out_written);
69
70#ifdef __cplusplus
71}
72#endif
73
74/** @} */
75
76#endif /* GHOSTTY_VT_FOCUS_H */
packages/terminal/vendor/include/ghostty/vt/formatter.h created+207
......@@ -0,0 +1,207 @@
1/**
2 * @file formatter.h
3 *
4 * Format terminal content as plain text, VT sequences, or HTML.
5 */
6
7#ifndef GHOSTTY_VT_FORMATTER_H
8#define GHOSTTY_VT_FORMATTER_H
9
10#include <stdbool.h>
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/allocator.h>
14#include <ghostty/vt/selection.h>
15#include <ghostty/vt/types.h>
16#include <ghostty/vt/terminal.h>
17
18#ifdef __cplusplus
19extern "C" {
20#endif
21
22/** @defgroup formatter Formatter
23 *
24 * Format terminal content as plain text, VT sequences, or HTML.
25 *
26 * A formatter captures a reference to a terminal and formatting options.
27 * It can be used repeatedly to produce output that reflects the current
28 * terminal state at the time of each format call.
29 *
30 * The terminal must outlive the formatter.
31 *
32 * @{
33 */
34
35/**
36 * Extra screen state to include in styled output.
37 *
38 * @ingroup formatter
39 */
40typedef struct {
41 /** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterScreenExtra). */
42 size_t size;
43
44 /** Emit cursor position using CUP (CSI H). */
45 bool cursor;
46
47 /** Emit current SGR style state based on the cursor's active style_id. */
48 bool style;
49
50 /** Emit current hyperlink state using OSC 8 sequences. */
51 bool hyperlink;
52
53 /** Emit character protection mode using DECSCA. */
54 bool protection;
55
56 /** Emit Kitty keyboard protocol state using CSI > u and CSI = sequences. */
57 bool kitty_keyboard;
58
59 /** Emit character set designations and invocations. */
60 bool charsets;
61} GhosttyFormatterScreenExtra;
62
63/**
64 * Extra terminal state to include in styled output.
65 *
66 * @ingroup formatter
67 */
68typedef struct {
69 /** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterTerminalExtra). */
70 size_t size;
71
72 /** Emit the palette using OSC 4 sequences. */
73 bool palette;
74
75 /** Emit terminal modes that differ from their defaults using CSI h/l. */
76 bool modes;
77
78 /** Emit scrolling region state using DECSTBM and DECSLRM sequences. */
79 bool scrolling_region;
80
81 /** Emit tabstop positions by clearing all tabs and setting each one. */
82 bool tabstops;
83
84 /** Emit the present working directory using OSC 7. */
85 bool pwd;
86
87 /** Emit keyboard modes such as ModifyOtherKeys. */
88 bool keyboard;
89
90 /** Screen-level extras. */
91 GhosttyFormatterScreenExtra screen;
92} GhosttyFormatterTerminalExtra;
93
94/**
95 * Options for creating a terminal formatter.
96 *
97 * @ingroup formatter
98 */
99typedef struct {
100 /** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterTerminalOptions). */
101 size_t size;
102
103 /** Output format to emit. */
104 GhosttyFormatterFormat emit;
105
106 /** Whether to unwrap soft-wrapped lines. */
107 bool unwrap;
108
109 /** Whether to trim trailing whitespace on non-blank lines. */
110 bool trim;
111
112 /** Extra terminal state to include in styled output. */
113 GhosttyFormatterTerminalExtra extra;
114
115 /** Optional selection to restrict output to a range.
116 * If NULL, the entire screen is formatted. */
117 const GhosttySelection *selection;
118} GhosttyFormatterTerminalOptions;
119
120/**
121 * Create a formatter for a terminal's active screen.
122 *
123 * The terminal must outlive the formatter. The formatter stores a borrowed
124 * reference to the terminal and reads its current state on each format call.
125 *
126 * @param allocator Pointer to allocator, or NULL to use the default allocator
127 * @param formatter Pointer to store the created formatter handle
128 * @param terminal The terminal to format (must not be NULL)
129 * @param options Formatting options
130 * @return GHOSTTY_SUCCESS on success, or an error code on failure
131 *
132 * @ingroup formatter
133 */
134GHOSTTY_API GhosttyResult ghostty_formatter_terminal_new(
135 const GhosttyAllocator* allocator,
136 GhosttyFormatter* formatter,
137 GhosttyTerminal terminal,
138 GhosttyFormatterTerminalOptions options);
139
140/**
141 * Run the formatter and produce output into the caller-provided buffer.
142 *
143 * Each call formats the current terminal state. Pass NULL for buf to
144 * query the required buffer size without writing any output; in that case
145 * out_written receives the required size and the return value is
146 * GHOSTTY_OUT_OF_SPACE.
147 *
148 * If the buffer is too small, returns GHOSTTY_OUT_OF_SPACE and sets
149 * out_written to the required size. The caller can then retry with a
150 * larger buffer.
151 *
152 * @param formatter The formatter handle (must not be NULL)
153 * @param buf Pointer to the output buffer, or NULL to query size
154 * @param buf_len Length of the output buffer in bytes
155 * @param out_written Pointer to receive the number of bytes written,
156 * or the required size on failure
157 * @return GHOSTTY_SUCCESS on success, or an error code on failure
158 *
159 * @ingroup formatter
160 */
161GHOSTTY_API GhosttyResult ghostty_formatter_format_buf(GhosttyFormatter formatter,
162 uint8_t* buf,
163 size_t buf_len,
164 size_t* out_written);
165
166/**
167 * Run the formatter and return an allocated buffer with the output.
168 *
169 * Each call formats the current terminal state. The buffer is allocated
170 * using the provided allocator (or the default allocator if NULL).
171 * The caller is responsible for freeing the returned buffer with
172 * ghostty_free(), passing the same allocator (or NULL for the default)
173 * that was used for the allocation.
174 *
175 * @param formatter The formatter handle (must not be NULL)
176 * @param allocator Pointer to allocator, or NULL to use the default allocator
177 * @param out_ptr Pointer to receive the allocated buffer
178 * @param out_len Pointer to receive the length of the output in bytes
179 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
180 * failure
181 *
182 * @ingroup formatter
183 */
184GHOSTTY_API GhosttyResult ghostty_formatter_format_alloc(GhosttyFormatter formatter,
185 const GhosttyAllocator* allocator,
186 uint8_t** out_ptr,
187 size_t* out_len);
188
189/**
190 * Free a formatter instance.
191 *
192 * Releases all resources associated with the formatter. After this call,
193 * the formatter handle becomes invalid.
194 *
195 * @param formatter The formatter handle to free (may be NULL)
196 *
197 * @ingroup formatter
198 */
199GHOSTTY_API void ghostty_formatter_free(GhosttyFormatter formatter);
200
201/** @} */
202
203#ifdef __cplusplus
204}
205#endif
206
207#endif /* GHOSTTY_VT_FORMATTER_H */
packages/terminal/vendor/include/ghostty/vt/grid_ref.h created+212
......@@ -0,0 +1,212 @@
1/**
2 * @file grid_ref.h
3 *
4 * Terminal grid reference type for referencing a resolved position in the
5 * terminal grid.
6 */
7
8#ifndef GHOSTTY_VT_GRID_REF_H
9#define GHOSTTY_VT_GRID_REF_H
10
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/types.h>
14#include <ghostty/vt/screen.h>
15#include <ghostty/vt/style.h>
16
17#ifdef __cplusplus
18extern "C" {
19#endif
20
21/** @defgroup grid_ref Grid Reference
22 *
23 * A grid reference is a reference to a specific cell position in the
24 * terminal. Obtain a grid reference from `ghostty_terminal_grid_ref`
25 * for untracked or `ghostty_terminal_grid_ref_track` for tracked. Untracked
26 * vs tracked is explained next.
27 *
28 * Important: The grid reference APIs are not meant to be used as the core of a render
29 * loop. They are not built to sustain the framerates needed for rendering large
30 * screens. Use the render state API for that.
31 *
32 * ## Untracked vs Tracked References
33 *
34 * ### Untracked Reference
35 *
36 * An untracked grid reference is a value type that snapshots a specific
37 * cell. It is only valid until the next update to the terminal instance.
38 * There is no guarantee that it will remain valid after any operation,
39 * even if a seemingly unrelated part of the grid is changed. These are meant
40 * to be read and have their values cached immediately after obtaining it.
41 *
42 * An untracked grid reference has a performance cost in its initial lookup,
43 * but doesn't affect the ongoing performance of the terminal in any way,
44 * since it is a one-time snapshot.
45 *
46 * ### Tracked Reference
47 *
48 * A tracked grid reference follows its cell across normal screen operations.
49 * For example scrolling, scrollback pruning, resize/reflow, and other
50 * terminal mutations update the tracked reference automatically.
51 *
52 * A tracked reference can still lose its original semantic location. This can
53 * happen when the underlying grid is reset, pruned, or otherwise discarded in a
54 * way that cannot be mapped to a meaningful new cell. In that state,
55 * ghostty_tracked_grid_ref_has_value() returns false and
56 * ghostty_tracked_grid_ref_snapshot() / ghostty_tracked_grid_ref_point() return
57 * GHOSTTY_NO_VALUE. The handle remains valid, and callers may move it to a new
58 * point with ghostty_tracked_grid_ref_set().
59 *
60 * To read cell data from a tracked reference, first snapshot it with
61 * ghostty_tracked_grid_ref_snapshot(). The returned `GhosttyGridRef` is again
62 * an untracked reference and follows the same short lifetime rules as any other
63 * untracked grid reference.
64 *
65 * A tracked reference belongs to the terminal screen/page-list that was active
66 * when it was created or last set. Converting it to a point uses that owning
67 * screen/page-list, even if the terminal has since switched between primary and
68 * alternate screens. Calling ghostty_tracked_grid_ref_set() resolves the new
69 * point against the terminal's currently active screen/page-list and may move
70 * the tracked reference between screens.
71 *
72 * Tracked references are owned by the caller and must be freed with
73 * ghostty_tracked_grid_ref_free(). If the terminal that created a tracked
74 * reference is freed first, the handle remains valid only for tracked-grid-ref
75 * APIs: it reports no value and can still be freed.
76 *
77 * Each tracked reference adds bookkeeping to terminal mutations. Use them
78 * sparingly for long-lived anchors such as selections, search state, marks,
79 * or application-side bookmarks.
80 *
81 * ## Lifetime
82 *
83 * An untracked reference is a snapshot. It doesn't need to be freed.
84 * The safety of accessing the value is documented explicitly above: it
85 * is only safe to access any data until the next terminal mutating
86 * operation (including free).
87 *
88 * A tracked reference is allocated and must be freed when it is no
89 * longer needed. A tracked reference may outlive the terminal that created it;
90 * after terminal free, it reports no value and can still be freed.
91 *
92 * ## Examples
93 *
94 * @snippet c-vt-grid-traverse/src/main.c grid-ref-traverse
95 * @snippet c-vt-grid-ref-tracked/src/main.c grid-ref-tracked
96 *
97 * @{
98 */
99
100/**
101 * A resolved reference to a terminal cell position.
102 *
103 * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
104 *
105 * @ingroup grid_ref
106 */
107typedef struct {
108 size_t size;
109 void *node;
110 uint16_t x;
111 uint16_t y;
112} GhosttyGridRef;
113
114/**
115 * Get the cell from a grid reference.
116 *
117 * @param ref Pointer to the grid reference
118 * @param[out] out_cell On success, set to the cell at the ref's position (may be NULL)
119 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
120 * node is NULL
121 *
122 * @ingroup grid_ref
123 */
124GHOSTTY_API GhosttyResult ghostty_grid_ref_cell(const GhosttyGridRef *ref,
125 GhosttyCell *out_cell);
126
127/**
128 * Get the row from a grid reference.
129 *
130 * @param ref Pointer to the grid reference
131 * @param[out] out_row On success, set to the row at the ref's position (may be NULL)
132 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
133 * node is NULL
134 *
135 * @ingroup grid_ref
136 */
137GHOSTTY_API GhosttyResult ghostty_grid_ref_row(const GhosttyGridRef *ref,
138 GhosttyRow *out_row);
139
140/**
141 * Get the grapheme cluster codepoints for the cell at the grid reference's
142 * position.
143 *
144 * Writes the full grapheme cluster (the cell's primary codepoint followed by
145 * any combining codepoints) into the provided buffer. If the cell has no text,
146 * out_len is set to 0 and GHOSTTY_SUCCESS is returned.
147 *
148 * If the buffer is too small (or NULL), the function returns
149 * GHOSTTY_OUT_OF_SPACE and writes the required number of codepoints to
150 * out_len. The caller can then retry with a sufficiently sized buffer.
151 *
152 * @param ref Pointer to the grid reference
153 * @param buf Output buffer of uint32_t codepoints (may be NULL)
154 * @param buf_len Number of uint32_t elements in the buffer
155 * @param[out] out_len On success, the number of codepoints written. On
156 * GHOSTTY_OUT_OF_SPACE, the required buffer size in codepoints.
157 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
158 * node is NULL, GHOSTTY_OUT_OF_SPACE if the buffer is too small
159 *
160 * @ingroup grid_ref
161 */
162GHOSTTY_API GhosttyResult ghostty_grid_ref_graphemes(const GhosttyGridRef *ref,
163 uint32_t *buf,
164 size_t buf_len,
165 size_t *out_len);
166
167/**
168 * Get the hyperlink URI for the cell at the grid reference's position.
169 *
170 * Writes the URI bytes into the provided buffer. If the cell has no
171 * hyperlink, out_len is set to 0 and GHOSTTY_SUCCESS is returned.
172 *
173 * If the buffer is too small (or NULL), the function returns
174 * GHOSTTY_OUT_OF_SPACE and writes the required number of bytes to
175 * out_len. The caller can then retry with a sufficiently sized buffer.
176 *
177 * @param ref Pointer to the grid reference
178 * @param buf Output buffer for the URI bytes (may be NULL)
179 * @param buf_len Size of the output buffer in bytes
180 * @param[out] out_len On success, the number of bytes written. On
181 * GHOSTTY_OUT_OF_SPACE, the required buffer size in bytes.
182 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
183 * node is NULL, GHOSTTY_OUT_OF_SPACE if the buffer is too small
184 *
185 * @ingroup grid_ref
186 */
187GHOSTTY_API GhosttyResult ghostty_grid_ref_hyperlink_uri(
188 const GhosttyGridRef *ref,
189 uint8_t *buf,
190 size_t buf_len,
191 size_t *out_len);
192
193/**
194 * Get the style of the cell at the grid reference's position.
195 *
196 * @param ref Pointer to the grid reference
197 * @param[out] out_style On success, set to the cell's style (may be NULL)
198 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
199 * node is NULL
200 *
201 * @ingroup grid_ref
202 */
203GHOSTTY_API GhosttyResult ghostty_grid_ref_style(const GhosttyGridRef *ref,
204 GhosttyStyle *out_style);
205
206/** @} */
207
208#ifdef __cplusplus
209}
210#endif
211
212#endif /* GHOSTTY_VT_GRID_REF_H */
packages/terminal/vendor/include/ghostty/vt/grid_ref_tracked.h created+139
......@@ -0,0 +1,139 @@
1/**
2 * @file grid_ref_tracked.h
3 *
4 * Tracked terminal grid references.
5 */
6
7#ifndef GHOSTTY_VT_GRID_REF_TRACKED_H
8#define GHOSTTY_VT_GRID_REF_TRACKED_H
9
10#include <stdbool.h>
11#include <ghostty/vt/types.h>
12#include <ghostty/vt/grid_ref.h>
13#include <ghostty/vt/point.h>
14
15#ifdef __cplusplus
16extern "C" {
17#endif
18
19/**
20 * Tracked grid references are owned grid references that move with the
21 * terminal. See @ref grid_ref for the full overview of tracked and untracked
22 * grid reference behavior.
23 *
24 * @ingroup grid_ref
25 */
26
27/**
28 * Free a tracked grid reference.
29 *
30 * Passing NULL is allowed and has no effect. A tracked reference may be freed
31 * after the terminal that created it is freed.
32 *
33 * @param ref Tracked grid reference to free.
34 *
35 * @ingroup grid_ref
36 */
37GHOSTTY_API void ghostty_tracked_grid_ref_free(GhosttyTrackedGridRef ref);
38
39/**
40 * Return whether a tracked grid reference currently has a meaningful value.
41 *
42 * If the terminal that created the tracked reference has been freed, this
43 * returns false.
44 *
45 * @param ref Tracked grid reference.
46 * @return true if the reference currently has a meaningful value.
47 *
48 * @ingroup grid_ref
49 */
50GHOSTTY_API bool ghostty_tracked_grid_ref_has_value(
51 GhosttyTrackedGridRef ref);
52
53/**
54 * Convert a tracked grid reference to a point in the requested coordinate
55 * space.
56 *
57 * This is the tracked equivalent of ghostty_terminal_point_from_grid_ref().
58 * Unlike snapshotting, this does not expose an intermediate untracked
59 * GhosttyGridRef.
60 *
61 * A tracked reference is resolved against the terminal screen/page-list that
62 * currently owns the reference. If the terminal has switched between primary
63 * and alternate screens since the reference was created or last set, this may
64 * be different from the terminal's currently active screen.
65 *
66 * If the tracked reference no longer has a meaningful value, this returns
67 * GHOSTTY_NO_VALUE. GHOSTTY_NO_VALUE is also returned when the reference cannot
68 * be represented in the requested coordinate space, including after the
69 * terminal that created the tracked reference has been freed.
70 *
71 * @param ref Tracked grid reference.
72 * @param tag Coordinate space to convert into.
73 * @param[out] out_point On success, receives the coordinate. May be NULL.
74 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if ref is invalid,
75 * or GHOSTTY_NO_VALUE if there is no representable value.
76 *
77 * @ingroup grid_ref
78 */
79GHOSTTY_API GhosttyResult ghostty_tracked_grid_ref_point(
80 GhosttyTrackedGridRef ref,
81 GhosttyPointTag tag,
82 GhosttyPointCoordinate *out_point);
83
84/**
85 * Move an existing tracked grid reference to a new terminal point.
86 *
87 * On success, the tracked reference begins tracking the new point and any prior
88 * "no value" state is cleared. On GHOSTTY_OUT_OF_MEMORY, the original tracked
89 * reference is left unchanged.
90 *
91 * The terminal must be the same terminal that created the tracked reference.
92 * The point is resolved against the terminal screen/page-list that is active at
93 * the time this function is called. If the terminal has switched between
94 * primary and alternate screens, this may move the tracked reference from one
95 * screen/page-list to the other.
96 *
97 * @param ref Tracked grid reference.
98 * @param terminal Terminal instance that owns the reference.
99 * @param point New point to track.
100 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if ref, terminal,
101 * or point is invalid, or GHOSTTY_OUT_OF_MEMORY if allocation fails.
102 *
103 * @ingroup grid_ref
104 */
105GHOSTTY_API GhosttyResult ghostty_tracked_grid_ref_set(
106 GhosttyTrackedGridRef ref,
107 GhosttyTerminal terminal,
108 GhosttyPoint point);
109
110/**
111 * Snapshot a tracked grid reference into a regular GhosttyGridRef.
112 *
113 * The returned GhosttyGridRef is an untracked snapshot and has the same
114 * lifetime rules as ghostty_terminal_grid_ref(): it is only valid until the
115 * next terminal update. Snapshot immediately before calling
116 * ghostty_grid_ref_cell(), ghostty_grid_ref_row(),
117 * ghostty_grid_ref_graphemes(), ghostty_grid_ref_hyperlink_uri(), or
118 * ghostty_grid_ref_style().
119 *
120 * If the tracked reference no longer has a meaningful value, this returns
121 * GHOSTTY_NO_VALUE. This includes references whose owning terminal has been
122 * freed.
123 *
124 * @param ref Tracked grid reference.
125 * @param[out] out_ref On success, receives an untracked snapshot. May be NULL.
126 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if ref is invalid,
127 * or GHOSTTY_NO_VALUE if the tracked location was discarded.
128 *
129 * @ingroup grid_ref
130 */
131GHOSTTY_API GhosttyResult ghostty_tracked_grid_ref_snapshot(
132 GhosttyTrackedGridRef ref,
133 GhosttyGridRef *out_ref);
134
135#ifdef __cplusplus
136}
137#endif
138
139#endif /* GHOSTTY_VT_GRID_REF_TRACKED_H */
packages/terminal/vendor/include/ghostty/vt/key.h created+73
......@@ -0,0 +1,73 @@
1/**
2 * @file key.h
3 *
4 * Key encoding module - encode key events into terminal escape sequences.
5 */
6
7#ifndef GHOSTTY_VT_KEY_H
8#define GHOSTTY_VT_KEY_H
9
10/** @defgroup key Key Encoding
11 *
12 * Utilities for encoding key events into terminal escape sequences,
13 * supporting both legacy encoding as well as Kitty Keyboard Protocol.
14 *
15 * ## Basic Usage
16 *
17 * 1. Create an encoder instance with ghostty_key_encoder_new()
18 * 2. Configure encoder options with ghostty_key_encoder_setopt()
19 * or ghostty_key_encoder_setopt_from_terminal() if you have a
20 * GhosttyTerminal.
21 * 3. For each key event:
22 * - Create a key event with ghostty_key_event_new()
23 * - Set event properties (action, key, modifiers, etc.)
24 * - Encode with ghostty_key_encoder_encode()
25 * - Free the event with ghostty_key_event_free()
26 * - Note: You can also reuse the same key event multiple times by
27 * changing its properties.
28 * 4. Free the encoder with ghostty_key_encoder_free() when done
29 *
30 * For a complete working example, see example/c-vt-encode-key in the
31 * repository.
32 *
33 * ## Example
34 *
35 * @snippet c-vt-encode-key/src/main.c key-encode
36 *
37 * ## Example: Encoding with Terminal State
38 *
39 * When you have a GhosttyTerminal, you can sync its modes (cursor key
40 * application, Kitty flags, etc.) into the encoder automatically:
41 *
42 * @code{.c}
43 * // Create a terminal and feed it some VT data that changes modes
44 * GhosttyTerminal terminal;
45 * ghostty_terminal_new(NULL, &terminal,
46 * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0});
47 *
48 * // Application might write data that enables Kitty keyboard protocol, etc.
49 * ghostty_terminal_vt_write(terminal, vt_data, vt_len);
50 *
51 * // Create an encoder and sync its options from the terminal
52 * GhosttyKeyEncoder encoder;
53 * ghostty_key_encoder_new(NULL, &encoder);
54 * ghostty_key_encoder_setopt_from_terminal(encoder, terminal);
55 *
56 * // Encode a key event using the terminal-derived options
57 * char buf[128];
58 * size_t written = 0;
59 * ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
60 *
61 * ghostty_key_encoder_free(encoder);
62 * ghostty_terminal_free(terminal);
63 * @endcode
64 *
65 * @{
66 */
67
68#include <ghostty/vt/key/event.h>
69#include <ghostty/vt/key/encoder.h>
70
71/** @} */
72
73#endif /* GHOSTTY_VT_KEY_H */
packages/terminal/vendor/include/ghostty/vt/key/encoder.h created+255
......@@ -0,0 +1,255 @@
1/**
2 * @file encoder.h
3 *
4 * Key event encoding to terminal escape sequences.
5 */
6
7#ifndef GHOSTTY_VT_KEY_ENCODER_H
8#define GHOSTTY_VT_KEY_ENCODER_H
9
10#include <stddef.h>
11#include <stdint.h>
12#include <ghostty/vt/types.h>
13#include <ghostty/vt/allocator.h>
14#include <ghostty/vt/terminal.h>
15#include <ghostty/vt/key/event.h>
16
17/**
18 * Opaque handle to a key encoder instance.
19 *
20 * This handle represents a key encoder that converts key events into terminal
21 * escape sequences.
22 *
23 * @ingroup key
24 */
25typedef struct GhosttyKeyEncoderImpl *GhosttyKeyEncoder;
26
27/**
28 * Kitty keyboard protocol flags.
29 *
30 * Bitflags representing the various modes of the Kitty keyboard protocol.
31 * These can be combined using bitwise OR operations. Valid values all
32 * start with `GHOSTTY_KITTY_KEY_`.
33 *
34 * @ingroup key
35 */
36typedef uint8_t GhosttyKittyKeyFlags;
37
38/** Kitty keyboard protocol disabled (all flags off) */
39#define GHOSTTY_KITTY_KEY_DISABLED 0
40
41/** Disambiguate escape codes */
42#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0)
43
44/** Report key press and release events */
45#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1)
46
47/** Report alternate key codes */
48#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2)
49
50/** Report all key events including those normally handled by the terminal */
51#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3)
52
53/** Report associated text with key events */
54#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4)
55
56/** All Kitty keyboard protocol flags enabled */
57#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED)
58
59/**
60 * macOS option key behavior.
61 *
62 * Determines whether the "option" key on macOS is treated as "alt" or not.
63 * See the Ghostty `macos-option-as-alt` configuration option for more details.
64 *
65 * @ingroup key
66 */
67typedef enum GHOSTTY_ENUM_TYPED {
68 /** Option key is not treated as alt */
69 GHOSTTY_OPTION_AS_ALT_FALSE = 0,
70 /** Option key is treated as alt */
71 GHOSTTY_OPTION_AS_ALT_TRUE = 1,
72 /** Only left option key is treated as alt */
73 GHOSTTY_OPTION_AS_ALT_LEFT = 2,
74 /** Only right option key is treated as alt */
75 GHOSTTY_OPTION_AS_ALT_RIGHT = 3,
76 GHOSTTY_OPTION_AS_ALT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
77} GhosttyOptionAsAlt;
78
79/**
80 * Key encoder option identifiers.
81 *
82 * These values are used with ghostty_key_encoder_setopt() to configure
83 * the behavior of the key encoder.
84 *
85 * @ingroup key
86 */
87typedef enum GHOSTTY_ENUM_TYPED {
88 /** Terminal DEC mode 1: cursor key application mode (value: bool) */
89 GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0,
90
91 /** Terminal DEC mode 66: keypad key application mode (value: bool) */
92 GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1,
93
94 /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */
95 GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2,
96
97 /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */
98 GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3,
99
100 /** xterm modifyOtherKeys mode 2 (value: bool) */
101 GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4,
102
103 /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */
104 GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5,
105
106 /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */
107 GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6,
108
109 /** Backarrow key mode (value: bool)
110 * See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170
111 * If `false` (the default), `backspace` emits 0x7f
112 * If `true`, `backspace` emits 0x08
113 */
114 GHOSTTY_KEY_ENCODER_OPT_BACKARROW_KEY_MODE = 7,
115
116 GHOSTTY_KEY_ENCODER_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
117} GhosttyKeyEncoderOption;
118
119/**
120 * Create a new key encoder instance.
121 *
122 * Creates a new key encoder with default options. The encoder can be configured
123 * using ghostty_key_encoder_setopt() and must be freed using
124 * ghostty_key_encoder_free() when no longer needed.
125 *
126 * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
127 * @param encoder Pointer to store the created encoder handle
128 * @return GHOSTTY_SUCCESS on success, or an error code on failure
129 *
130 * @ingroup key
131 */
132GHOSTTY_API GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder);
133
134/**
135 * Free a key encoder instance.
136 *
137 * Releases all resources associated with the key encoder. After this call,
138 * the encoder handle becomes invalid and must not be used.
139 *
140 * @param encoder The encoder handle to free (may be NULL)
141 *
142 * @ingroup key
143 */
144GHOSTTY_API void ghostty_key_encoder_free(GhosttyKeyEncoder encoder);
145
146/**
147 * Set an option on the key encoder.
148 *
149 * Configures the behavior of the key encoder. Options control various aspects
150 * of encoding such as terminal modes (cursor key application mode, keypad mode),
151 * protocol selection (Kitty keyboard protocol flags), and platform-specific
152 * behaviors (macOS option-as-alt).
153 *
154 * If you are using a terminal instance, you can set the key encoding
155 * options based on the active terminal state (e.g. legacy vs Kitty mode
156 * and associated flags) with ghostty_key_encoder_setopt_from_terminal().
157 *
158 * A null pointer value does nothing. It does not reset the value to the
159 * default. The setopt call will do nothing.
160 *
161 * @param encoder The encoder handle, must not be NULL
162 * @param option The option to set
163 * @param value Pointer to the value to set (type depends on the option)
164 *
165 * @ingroup key
166 */
167GHOSTTY_API void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value);
168
169/**
170 * Set encoder options from a terminal's current state.
171 *
172 * Reads the terminal's current modes and flags and applies them to the
173 * encoder's options. This sets cursor key application mode, keypad mode,
174 * alt escape prefix, modifyOtherKeys state, and Kitty keyboard protocol
175 * flags from the terminal state.
176 *
177 * Note that the `macos_option_as_alt` option cannot be determined from
178 * terminal state and is reset to `GHOSTTY_OPTION_AS_ALT_FALSE` by this
179 * call. Use ghostty_key_encoder_setopt() to set it afterward if needed.
180 *
181 * @param encoder The encoder handle, must not be NULL
182 * @param terminal The terminal handle, must not be NULL
183 *
184 * @ingroup key
185 */
186GHOSTTY_API void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder encoder, GhosttyTerminal terminal);
187
188/**
189 * Encode a key event into a terminal escape sequence.
190 *
191 * Converts a key event into the appropriate terminal escape sequence based on
192 * the encoder's current options. The sequence is written to the provided buffer.
193 *
194 * Not all key events produce output. For example, unmodified modifier keys
195 * typically don't generate escape sequences. Check the out_len parameter to
196 * determine if any data was written.
197 *
198 * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_SPACE
199 * and out_len will contain the required buffer size. The caller can then
200 * allocate a larger buffer and call the function again.
201 *
202 * @param encoder The encoder handle, must not be NULL
203 * @param event The key event to encode, must not be NULL
204 * @param out_buf Buffer to write the encoded sequence to
205 * @param out_buf_size Size of the output buffer in bytes
206 * @param out_len Pointer to store the number of bytes written (may be NULL)
207 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if buffer too small, or other error code
208 *
209 * ## Example: Calculate required buffer size
210 *
211 * @code{.c}
212 * // Query the required size with a NULL buffer (always returns OUT_OF_SPACE)
213 * size_t required = 0;
214 * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required);
215 * assert(result == GHOSTTY_OUT_OF_SPACE);
216 *
217 * // Allocate buffer of required size
218 * char *buf = malloc(required);
219 *
220 * // Encode with properly sized buffer
221 * size_t written = 0;
222 * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written);
223 * assert(result == GHOSTTY_SUCCESS);
224 *
225 * // Use the encoded sequence...
226 *
227 * free(buf);
228 * @endcode
229 *
230 * ## Example: Direct encoding with static buffer
231 *
232 * @code{.c}
233 * // Most escape sequences are short, so a static buffer often suffices
234 * char buf[128];
235 * size_t written = 0;
236 * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
237 *
238 * if (result == GHOSTTY_SUCCESS) {
239 * // Write the encoded sequence to the terminal
240 * write(pty_fd, buf, written);
241 * } else if (result == GHOSTTY_OUT_OF_SPACE) {
242 * // Buffer too small, written contains required size
243 * char *dynamic_buf = malloc(written);
244 * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written);
245 * assert(result == GHOSTTY_SUCCESS);
246 * write(pty_fd, dynamic_buf, written);
247 * free(dynamic_buf);
248 * }
249 * @endcode
250 *
251 * @ingroup key
252 */
253GHOSTTY_API GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len);
254
255#endif /* GHOSTTY_VT_KEY_ENCODER_H */
packages/terminal/vendor/include/ghostty/vt/key/event.h created+482
......@@ -0,0 +1,482 @@
1/**
2 * @file event.h
3 *
4 * Key event representation and manipulation.
5 */
6
7#ifndef GHOSTTY_VT_KEY_EVENT_H
8#define GHOSTTY_VT_KEY_EVENT_H
9
10#include <stdbool.h>
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/types.h>
14#include <ghostty/vt/allocator.h>
15
16/**
17 * Opaque handle to a key event.
18 *
19 * This handle represents a keyboard input event containing information about
20 * the physical key pressed, modifiers, and generated text.
21 *
22 * @ingroup key
23 */
24typedef struct GhosttyKeyEventImpl *GhosttyKeyEvent;
25
26/**
27 * Keyboard input event types.
28 *
29 * @ingroup key
30 */
31typedef enum GHOSTTY_ENUM_TYPED {
32 /** Key was released */
33 GHOSTTY_KEY_ACTION_RELEASE = 0,
34 /** Key was pressed */
35 GHOSTTY_KEY_ACTION_PRESS = 1,
36 /** Key is being repeated (held down) */
37 GHOSTTY_KEY_ACTION_REPEAT = 2,
38 GHOSTTY_KEY_ACTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
39} GhosttyKeyAction;
40
41/**
42 * Keyboard modifier keys bitmask.
43 *
44 * A bitmask representing all keyboard modifiers. This tracks which modifier keys
45 * are pressed and, where supported by the platform, which side (left or right)
46 * of each modifier is active.
47 *
48 * Use the GHOSTTY_MODS_* constants to test and set individual modifiers.
49 *
50 * Modifier side bits are only meaningful when the corresponding modifier bit is set.
51 * Not all platforms support distinguishing between left and right modifier
52 * keys and Ghostty is built to expect that some platforms may not provide this
53 * information.
54 *
55 * @ingroup key
56 */
57typedef uint16_t GhosttyMods;
58
59/** Shift key is pressed */
60#define GHOSTTY_MODS_SHIFT (1 << 0)
61/** Control key is pressed */
62#define GHOSTTY_MODS_CTRL (1 << 1)
63/** Alt/Option key is pressed */
64#define GHOSTTY_MODS_ALT (1 << 2)
65/** Super/Command/Windows key is pressed */
66#define GHOSTTY_MODS_SUPER (1 << 3)
67/** Caps Lock is active */
68#define GHOSTTY_MODS_CAPS_LOCK (1 << 4)
69/** Num Lock is active */
70#define GHOSTTY_MODS_NUM_LOCK (1 << 5)
71
72/**
73 * Right shift is pressed (0 = left, 1 = right).
74 * Only meaningful when GHOSTTY_MODS_SHIFT is set.
75 */
76#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6)
77/**
78 * Right ctrl is pressed (0 = left, 1 = right).
79 * Only meaningful when GHOSTTY_MODS_CTRL is set.
80 */
81#define GHOSTTY_MODS_CTRL_SIDE (1 << 7)
82/**
83 * Right alt is pressed (0 = left, 1 = right).
84 * Only meaningful when GHOSTTY_MODS_ALT is set.
85 */
86#define GHOSTTY_MODS_ALT_SIDE (1 << 8)
87/**
88 * Right super is pressed (0 = left, 1 = right).
89 * Only meaningful when GHOSTTY_MODS_SUPER is set.
90 */
91#define GHOSTTY_MODS_SUPER_SIDE (1 << 9)
92
93/**
94 * Physical key codes.
95 *
96 * The set of key codes that Ghostty is aware of. These represent physical keys
97 * on the keyboard and are layout-independent. For example, the "a" key on a US
98 * keyboard is the same as the "ф" key on a Russian keyboard, but both will
99 * report the same key_a value.
100 *
101 * Layout-dependent strings are provided separately as UTF-8 text and are produced
102 * by the platform. These values are based on the W3C UI Events KeyboardEvent code
103 * standard. See: https://www.w3.org/TR/uievents-code
104 *
105 * @ingroup key
106 */
107typedef enum GHOSTTY_ENUM_TYPED {
108 GHOSTTY_KEY_UNIDENTIFIED = 0,
109
110 // Writing System Keys (W3C § 3.1.1)
111 GHOSTTY_KEY_BACKQUOTE,
112 GHOSTTY_KEY_BACKSLASH,
113 GHOSTTY_KEY_BRACKET_LEFT,
114 GHOSTTY_KEY_BRACKET_RIGHT,
115 GHOSTTY_KEY_COMMA,
116 GHOSTTY_KEY_DIGIT_0,
117 GHOSTTY_KEY_DIGIT_1,
118 GHOSTTY_KEY_DIGIT_2,
119 GHOSTTY_KEY_DIGIT_3,
120 GHOSTTY_KEY_DIGIT_4,
121 GHOSTTY_KEY_DIGIT_5,
122 GHOSTTY_KEY_DIGIT_6,
123 GHOSTTY_KEY_DIGIT_7,
124 GHOSTTY_KEY_DIGIT_8,
125 GHOSTTY_KEY_DIGIT_9,
126 GHOSTTY_KEY_EQUAL,
127 GHOSTTY_KEY_INTL_BACKSLASH,
128 GHOSTTY_KEY_INTL_RO,
129 GHOSTTY_KEY_INTL_YEN,
130 GHOSTTY_KEY_A,
131 GHOSTTY_KEY_B,
132 GHOSTTY_KEY_C,
133 GHOSTTY_KEY_D,
134 GHOSTTY_KEY_E,
135 GHOSTTY_KEY_F,
136 GHOSTTY_KEY_G,
137 GHOSTTY_KEY_H,
138 GHOSTTY_KEY_I,
139 GHOSTTY_KEY_J,
140 GHOSTTY_KEY_K,
141 GHOSTTY_KEY_L,
142 GHOSTTY_KEY_M,
143 GHOSTTY_KEY_N,
144 GHOSTTY_KEY_O,
145 GHOSTTY_KEY_P,
146 GHOSTTY_KEY_Q,
147 GHOSTTY_KEY_R,
148 GHOSTTY_KEY_S,
149 GHOSTTY_KEY_T,
150 GHOSTTY_KEY_U,
151 GHOSTTY_KEY_V,
152 GHOSTTY_KEY_W,
153 GHOSTTY_KEY_X,
154 GHOSTTY_KEY_Y,
155 GHOSTTY_KEY_Z,
156 GHOSTTY_KEY_MINUS,
157 GHOSTTY_KEY_PERIOD,
158 GHOSTTY_KEY_QUOTE,
159 GHOSTTY_KEY_SEMICOLON,
160 GHOSTTY_KEY_SLASH,
161
162 // Functional Keys (W3C § 3.1.2)
163 GHOSTTY_KEY_ALT_LEFT,
164 GHOSTTY_KEY_ALT_RIGHT,
165 GHOSTTY_KEY_BACKSPACE,
166 GHOSTTY_KEY_CAPS_LOCK,
167 GHOSTTY_KEY_CONTEXT_MENU,
168 GHOSTTY_KEY_CONTROL_LEFT,
169 GHOSTTY_KEY_CONTROL_RIGHT,
170 GHOSTTY_KEY_ENTER,
171 GHOSTTY_KEY_META_LEFT,
172 GHOSTTY_KEY_META_RIGHT,
173 GHOSTTY_KEY_SHIFT_LEFT,
174 GHOSTTY_KEY_SHIFT_RIGHT,
175 GHOSTTY_KEY_SPACE,
176 GHOSTTY_KEY_TAB,
177 GHOSTTY_KEY_CONVERT,
178 GHOSTTY_KEY_KANA_MODE,
179 GHOSTTY_KEY_NON_CONVERT,
180
181 // Control Pad Section (W3C § 3.2)
182 GHOSTTY_KEY_DELETE,
183 GHOSTTY_KEY_END,
184 GHOSTTY_KEY_HELP,
185 GHOSTTY_KEY_HOME,
186 GHOSTTY_KEY_INSERT,
187 GHOSTTY_KEY_PAGE_DOWN,
188 GHOSTTY_KEY_PAGE_UP,
189
190 // Arrow Pad Section (W3C § 3.3)
191 GHOSTTY_KEY_ARROW_DOWN,
192 GHOSTTY_KEY_ARROW_LEFT,
193 GHOSTTY_KEY_ARROW_RIGHT,
194 GHOSTTY_KEY_ARROW_UP,
195
196 // Numpad Section (W3C § 3.4)
197 GHOSTTY_KEY_NUM_LOCK,
198 GHOSTTY_KEY_NUMPAD_0,
199 GHOSTTY_KEY_NUMPAD_1,
200 GHOSTTY_KEY_NUMPAD_2,
201 GHOSTTY_KEY_NUMPAD_3,
202 GHOSTTY_KEY_NUMPAD_4,
203 GHOSTTY_KEY_NUMPAD_5,
204 GHOSTTY_KEY_NUMPAD_6,
205 GHOSTTY_KEY_NUMPAD_7,
206 GHOSTTY_KEY_NUMPAD_8,
207 GHOSTTY_KEY_NUMPAD_9,
208 GHOSTTY_KEY_NUMPAD_ADD,
209 GHOSTTY_KEY_NUMPAD_BACKSPACE,
210 GHOSTTY_KEY_NUMPAD_CLEAR,
211 GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
212 GHOSTTY_KEY_NUMPAD_COMMA,
213 GHOSTTY_KEY_NUMPAD_DECIMAL,
214 GHOSTTY_KEY_NUMPAD_DIVIDE,
215 GHOSTTY_KEY_NUMPAD_ENTER,
216 GHOSTTY_KEY_NUMPAD_EQUAL,
217 GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
218 GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
219 GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
220 GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
221 GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
222 GHOSTTY_KEY_NUMPAD_MULTIPLY,
223 GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
224 GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
225 GHOSTTY_KEY_NUMPAD_SUBTRACT,
226 GHOSTTY_KEY_NUMPAD_SEPARATOR,
227 GHOSTTY_KEY_NUMPAD_UP,
228 GHOSTTY_KEY_NUMPAD_DOWN,
229 GHOSTTY_KEY_NUMPAD_RIGHT,
230 GHOSTTY_KEY_NUMPAD_LEFT,
231 GHOSTTY_KEY_NUMPAD_BEGIN,
232 GHOSTTY_KEY_NUMPAD_HOME,
233 GHOSTTY_KEY_NUMPAD_END,
234 GHOSTTY_KEY_NUMPAD_INSERT,
235 GHOSTTY_KEY_NUMPAD_DELETE,
236 GHOSTTY_KEY_NUMPAD_PAGE_UP,
237 GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
238
239 // Function Section (W3C § 3.5)
240 GHOSTTY_KEY_ESCAPE,
241 GHOSTTY_KEY_F1,
242 GHOSTTY_KEY_F2,
243 GHOSTTY_KEY_F3,
244 GHOSTTY_KEY_F4,
245 GHOSTTY_KEY_F5,
246 GHOSTTY_KEY_F6,
247 GHOSTTY_KEY_F7,
248 GHOSTTY_KEY_F8,
249 GHOSTTY_KEY_F9,
250 GHOSTTY_KEY_F10,
251 GHOSTTY_KEY_F11,
252 GHOSTTY_KEY_F12,
253 GHOSTTY_KEY_F13,
254 GHOSTTY_KEY_F14,
255 GHOSTTY_KEY_F15,
256 GHOSTTY_KEY_F16,
257 GHOSTTY_KEY_F17,
258 GHOSTTY_KEY_F18,
259 GHOSTTY_KEY_F19,
260 GHOSTTY_KEY_F20,
261 GHOSTTY_KEY_F21,
262 GHOSTTY_KEY_F22,
263 GHOSTTY_KEY_F23,
264 GHOSTTY_KEY_F24,
265 GHOSTTY_KEY_F25,
266 GHOSTTY_KEY_FN,
267 GHOSTTY_KEY_FN_LOCK,
268 GHOSTTY_KEY_PRINT_SCREEN,
269 GHOSTTY_KEY_SCROLL_LOCK,
270 GHOSTTY_KEY_PAUSE,
271
272 // Media Keys (W3C § 3.6)
273 GHOSTTY_KEY_BROWSER_BACK,
274 GHOSTTY_KEY_BROWSER_FAVORITES,
275 GHOSTTY_KEY_BROWSER_FORWARD,
276 GHOSTTY_KEY_BROWSER_HOME,
277 GHOSTTY_KEY_BROWSER_REFRESH,
278 GHOSTTY_KEY_BROWSER_SEARCH,
279 GHOSTTY_KEY_BROWSER_STOP,
280 GHOSTTY_KEY_EJECT,
281 GHOSTTY_KEY_LAUNCH_APP_1,
282 GHOSTTY_KEY_LAUNCH_APP_2,
283 GHOSTTY_KEY_LAUNCH_MAIL,
284 GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
285 GHOSTTY_KEY_MEDIA_SELECT,
286 GHOSTTY_KEY_MEDIA_STOP,
287 GHOSTTY_KEY_MEDIA_TRACK_NEXT,
288 GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
289 GHOSTTY_KEY_POWER,
290 GHOSTTY_KEY_SLEEP,
291 GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
292 GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
293 GHOSTTY_KEY_AUDIO_VOLUME_UP,
294 GHOSTTY_KEY_WAKE_UP,
295
296 // Legacy, Non-standard, and Special Keys (W3C § 3.7)
297 GHOSTTY_KEY_COPY,
298 GHOSTTY_KEY_CUT,
299 GHOSTTY_KEY_PASTE,
300 GHOSTTY_KEY_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
301} GhosttyKey;
302
303/**
304 * Create a new key event instance.
305 *
306 * Creates a new key event with default values. The event must be freed using
307 * ghostty_key_event_free() when no longer needed.
308 *
309 * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
310 * @param event Pointer to store the created key event handle
311 * @return GHOSTTY_SUCCESS on success, or an error code on failure
312 *
313 * @ingroup key
314 */
315GHOSTTY_API GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event);
316
317/**
318 * Free a key event instance.
319 *
320 * Releases all resources associated with the key event. After this call,
321 * the event handle becomes invalid and must not be used.
322 *
323 * @param event The key event handle to free (may be NULL)
324 *
325 * @ingroup key
326 */
327GHOSTTY_API void ghostty_key_event_free(GhosttyKeyEvent event);
328
329/**
330 * Set the key action (press, release, repeat).
331 *
332 * @param event The key event handle, must not be NULL
333 * @param action The action to set
334 *
335 * @ingroup key
336 */
337GHOSTTY_API void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action);
338
339/**
340 * Get the key action (press, release, repeat).
341 *
342 * @param event The key event handle, must not be NULL
343 * @return The key action
344 *
345 * @ingroup key
346 */
347GHOSTTY_API GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event);
348
349/**
350 * Set the physical key code.
351 *
352 * @param event The key event handle, must not be NULL
353 * @param key The physical key code to set
354 *
355 * @ingroup key
356 */
357GHOSTTY_API void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key);
358
359/**
360 * Get the physical key code.
361 *
362 * @param event The key event handle, must not be NULL
363 * @return The physical key code
364 *
365 * @ingroup key
366 */
367GHOSTTY_API GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event);
368
369/**
370 * Set the modifier keys bitmask.
371 *
372 * @param event The key event handle, must not be NULL
373 * @param mods The modifier keys bitmask to set
374 *
375 * @ingroup key
376 */
377GHOSTTY_API void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods);
378
379/**
380 * Get the modifier keys bitmask.
381 *
382 * @param event The key event handle, must not be NULL
383 * @return The modifier keys bitmask
384 *
385 * @ingroup key
386 */
387GHOSTTY_API GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event);
388
389/**
390 * Set the consumed modifiers bitmask.
391 *
392 * @param event The key event handle, must not be NULL
393 * @param consumed_mods The consumed modifiers bitmask to set
394 *
395 * @ingroup key
396 */
397GHOSTTY_API void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods);
398
399/**
400 * Get the consumed modifiers bitmask.
401 *
402 * @param event The key event handle, must not be NULL
403 * @return The consumed modifiers bitmask
404 *
405 * @ingroup key
406 */
407GHOSTTY_API GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event);
408
409/**
410 * Set whether the key event is part of a composition sequence.
411 *
412 * @param event The key event handle, must not be NULL
413 * @param composing Whether the key event is part of a composition sequence
414 *
415 * @ingroup key
416 */
417GHOSTTY_API void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing);
418
419/**
420 * Get whether the key event is part of a composition sequence.
421 *
422 * @param event The key event handle, must not be NULL
423 * @return Whether the key event is part of a composition sequence
424 *
425 * @ingroup key
426 */
427GHOSTTY_API bool ghostty_key_event_get_composing(GhosttyKeyEvent event);
428
429/**
430 * Set the UTF-8 text generated by the key for the current keyboard layout.
431 *
432 * Must contain the unmodified character before any Ctrl/Meta transformations.
433 * The encoder derives modifier sequences from the logical key and mods
434 * bitmask, not from this text. Do not pass C0 control characters
435 * (U+0000-U+001F, U+007F) or platform function key codes (e.g. macOS PUA
436 * U+F700-U+F8FF); pass NULL instead and let the encoder use the logical key.
437 *
438 * The key event does NOT take ownership of the text pointer. The caller
439 * must ensure the string remains valid for the lifetime needed by the event.
440 *
441 * @param event The key event handle, must not be NULL
442 * @param utf8 The UTF-8 text to set (or NULL for empty)
443 * @param len Length of the UTF-8 text in bytes
444 *
445 * @ingroup key
446 */
447GHOSTTY_API void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len);
448
449/**
450 * Get the UTF-8 text generated by the key event.
451 *
452 * The returned pointer is valid until the event is freed or the UTF-8 text is modified.
453 *
454 * @param event The key event handle, must not be NULL
455 * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL)
456 * @return The UTF-8 text (or NULL for empty)
457 *
458 * @ingroup key
459 */
460GHOSTTY_API const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len);
461
462/**
463 * Set the unshifted Unicode codepoint.
464 *
465 * @param event The key event handle, must not be NULL
466 * @param codepoint The unshifted Unicode codepoint to set
467 *
468 * @ingroup key
469 */
470GHOSTTY_API void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint);
471
472/**
473 * Get the unshifted Unicode codepoint.
474 *
475 * @param event The key event handle, must not be NULL
476 * @return The unshifted Unicode codepoint
477 *
478 * @ingroup key
479 */
480GHOSTTY_API uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event);
481
482#endif /* GHOSTTY_VT_KEY_EVENT_H */
packages/terminal/vendor/include/ghostty/vt/kitty_graphics.h created+859
......@@ -0,0 +1,859 @@
1/**
2 * @file kitty_graphics.h
3 *
4 * Kitty graphics protocol
5 *
6 * See @ref kitty_graphics for a full usage guide.
7 */
8
9#ifndef GHOSTTY_VT_KITTY_GRAPHICS_H
10#define GHOSTTY_VT_KITTY_GRAPHICS_H
11
12#include <stdbool.h>
13#include <stdint.h>
14#include <ghostty/vt/allocator.h>
15#include <ghostty/vt/selection.h>
16#include <ghostty/vt/types.h>
17
18#ifdef __cplusplus
19extern "C" {
20#endif
21
22/** @defgroup kitty_graphics Kitty Graphics
23 *
24 * API for inspecting images and placements stored via the
25 * [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/).
26 *
27 * The central object is @ref GhosttyKittyGraphics, an opaque handle to
28 * the image storage associated with a terminal's active screen. From it
29 * you can iterate over placements and look up individual images.
30 *
31 * ## Obtaining a KittyGraphics Handle
32 *
33 * A @ref GhosttyKittyGraphics handle is obtained from a terminal via
34 * ghostty_terminal_get() with @ref GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS.
35 * The handle is borrowed from the terminal and remains valid until the
36 * next mutating terminal call (e.g. ghostty_terminal_vt_write() or
37 * ghostty_terminal_reset()).
38 *
39 * Before images can be stored, Kitty graphics must be enabled on the
40 * terminal by setting a non-zero storage limit with
41 * @ref GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT, and a PNG
42 * decoder callback must be installed via ghostty_sys_set() with
43 * @ref GHOSTTY_SYS_OPT_DECODE_PNG.
44 *
45 * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-decode-png
46 *
47 * ## Iterating Placements
48 *
49 * Placements are inspected through a @ref GhosttyKittyGraphicsPlacementIterator.
50 * The typical workflow is:
51 *
52 * 1. Create an iterator with ghostty_kitty_graphics_placement_iterator_new().
53 * 2. Populate it from the storage with ghostty_kitty_graphics_get() using
54 * @ref GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR.
55 * 3. Optionally filter by z-layer with
56 * ghostty_kitty_graphics_placement_iterator_set().
57 * 4. Advance with ghostty_kitty_graphics_placement_next() and read
58 * per-placement data with ghostty_kitty_graphics_placement_get().
59 * 5. For each placement, look up its image with
60 * ghostty_kitty_graphics_image() to access pixel data and dimensions.
61 * 6. Free the iterator with ghostty_kitty_graphics_placement_iterator_free().
62 *
63 * ## Looking Up Images
64 *
65 * Given an image ID (obtained from a placement via
66 * @ref GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID), call
67 * ghostty_kitty_graphics_image() to get a @ref GhosttyKittyGraphicsImage
68 * handle. From this handle, ghostty_kitty_graphics_image_get() provides
69 * the image dimensions, pixel format, compression, and a borrowed pointer
70 * to the raw pixel data.
71 *
72 * ## Rendering Helpers
73 *
74 * Several functions assist with rendering a placement:
75 *
76 * - ghostty_kitty_graphics_placement_pixel_size() — rendered pixel
77 * dimensions accounting for source rect and aspect ratio.
78 * - ghostty_kitty_graphics_placement_grid_size() — number of grid
79 * columns and rows the placement occupies.
80 * - ghostty_kitty_graphics_placement_viewport_pos() — viewport-relative
81 * grid position (may be negative for partially scrolled placements).
82 * - ghostty_kitty_graphics_placement_source_rect() — resolved source
83 * rectangle in pixels, clamped to image bounds.
84 * - ghostty_kitty_graphics_placement_rect() — bounding rectangle as a
85 * @ref GhosttySelection.
86 *
87 * ## Change Detection
88 *
89 * Generation stamps allow renderers to cheaply detect whether Kitty
90 * graphics state changed between frames:
91 *
92 * - @ref GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION is a storage-wide stamp
93 * updated on any transmit, placement, or delete. If unchanged, the
94 * placement set and all image data are identical and both placement
95 * snapshots and per-image staleness checks can be skipped. Placement
96 * geometry can still change independently (scrolling moves
97 * placements), so ghostty_kitty_graphics_placement_render_info()
98 * should still be recomputed on dirty frames.
99 * - @ref GHOSTTY_KITTY_IMAGE_DATA_GENERATION is a per-image stamp
100 * changed on every add/replace of that image ID. Texture caches
101 * should treat a cached texture as stale when this differs from the
102 * cached value; dimension/length heuristics cannot detect a
103 * same-sized retransmission.
104 *
105 * Stamps are unique and monotonically increasing process-wide, so
106 * caches keyed on a generation value never alias across screens
107 * (main/alternate), resets, or terminals.
108 *
109 * ## Lifetime and Thread Safety
110 *
111 * All handles borrowed from the terminal (GhosttyKittyGraphics,
112 * GhosttyKittyGraphicsImage) are invalidated by any mutating terminal
113 * call. The placement iterator is independently owned and must be freed
114 * by the caller, but the data it yields is only valid while the
115 * underlying terminal is not mutated.
116 *
117 * ## Example
118 *
119 * The following example creates a terminal, sends a Kitty graphics
120 * image, then iterates placements and prints image metadata:
121 *
122 * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-main
123 *
124 * @{
125 */
126
127/**
128 * Queryable data kinds for ghostty_kitty_graphics_get().
129 *
130 * @ingroup kitty_graphics
131 */
132typedef enum GHOSTTY_ENUM_TYPED {
133 /** Invalid / sentinel value. */
134 GHOSTTY_KITTY_GRAPHICS_DATA_INVALID = 0,
135
136 /**
137 * Populate a pre-allocated placement iterator with placement data from
138 * the storage. Iterator data is only valid as long as the underlying
139 * terminal is not mutated.
140 *
141 * Output type: GhosttyKittyGraphicsPlacementIterator *
142 */
143 GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR = 1,
144
145 /**
146 * Generation stamp of the last content mutation to this storage:
147 * any image transmit/replace, placement add, or delete. Zero means
148 * the storage has never been mutated (and is therefore empty).
149 *
150 * If the generation is unchanged since a previous query, the set of
151 * placements and all image data are identical, so placement iteration
152 * and image staleness checks can be skipped entirely. Note that
153 * placement *geometry* may still have changed (scrolling and resizing
154 * move placements without changing the storage contents), so rendering
155 * geometry such as ghostty_kitty_graphics_placement_render_info()
156 * must still be recomputed for frames marked dirty.
157 *
158 * Stamps are unique and monotonically increasing process-wide: a
159 * value observed from any storage never recurs for different content,
160 * even across screen switches (main vs. alternate screen have
161 * independent storages) or terminal resets. It is therefore safe to
162 * key caches on this value alone.
163 *
164 * Output type: uint64_t *
165 */
166 GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION = 2,
167 GHOSTTY_KITTY_GRAPHICS_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
168} GhosttyKittyGraphicsData;
169
170/**
171 * Queryable data kinds for ghostty_kitty_graphics_placement_get().
172 *
173 * @ingroup kitty_graphics
174 */
175typedef enum GHOSTTY_ENUM_TYPED {
176 /** Invalid / sentinel value. */
177 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INVALID = 0,
178
179 /**
180 * The image ID this placement belongs to.
181 *
182 * Output type: uint32_t *
183 */
184 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID = 1,
185
186 /**
187 * The placement ID.
188 *
189 * Output type: uint32_t *
190 */
191 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID = 2,
192
193 /**
194 * Whether this is a virtual placement (unicode placeholder).
195 *
196 * Output type: bool *
197 */
198 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL = 3,
199
200 /**
201 * Pixel offset from the left edge of the cell.
202 *
203 * Output type: uint32_t *
204 */
205 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET = 4,
206
207 /**
208 * Pixel offset from the top edge of the cell.
209 *
210 * Output type: uint32_t *
211 */
212 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET = 5,
213
214 /**
215 * Source rectangle x origin in pixels.
216 *
217 * Output type: uint32_t *
218 */
219 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X = 6,
220
221 /**
222 * Source rectangle y origin in pixels.
223 *
224 * Output type: uint32_t *
225 */
226 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y = 7,
227
228 /**
229 * Source rectangle width in pixels (0 = full image width).
230 *
231 * Output type: uint32_t *
232 */
233 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH = 8,
234
235 /**
236 * Source rectangle height in pixels (0 = full image height).
237 *
238 * Output type: uint32_t *
239 */
240 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT = 9,
241
242 /**
243 * Number of columns this placement occupies.
244 *
245 * Output type: uint32_t *
246 */
247 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS = 10,
248
249 /**
250 * Number of rows this placement occupies.
251 *
252 * Output type: uint32_t *
253 */
254 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_ROWS = 11,
255
256 /**
257 * Z-index for this placement.
258 *
259 * Output type: int32_t *
260 */
261 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z = 12,
262
263 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
264} GhosttyKittyGraphicsPlacementData;
265
266/**
267 * Z-layer classification for kitty graphics placements.
268 *
269 * Based on the kitty protocol z-index conventions:
270 * - BELOW_BG: z < INT32_MIN/2 (drawn below cell background)
271 * - BELOW_TEXT: INT32_MIN/2 <= z < 0 (above background, below text)
272 * - ABOVE_TEXT: z >= 0 (above text)
273 * - ALL: no filtering (current behavior)
274 *
275 * @ingroup kitty_graphics
276 */
277typedef enum GHOSTTY_ENUM_TYPED {
278 GHOSTTY_KITTY_PLACEMENT_LAYER_ALL = 0,
279 GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_BG = 1,
280 GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_TEXT = 2,
281 GHOSTTY_KITTY_PLACEMENT_LAYER_ABOVE_TEXT = 3,
282 GHOSTTY_KITTY_PLACEMENT_LAYER_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
283} GhosttyKittyPlacementLayer;
284
285/**
286 * Settable options for ghostty_kitty_graphics_placement_iterator_set().
287 *
288 * @ingroup kitty_graphics
289 */
290typedef enum GHOSTTY_ENUM_TYPED {
291 /**
292 * Set the z-layer filter for the iterator.
293 *
294 * Input type: GhosttyKittyPlacementLayer *
295 */
296 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER = 0,
297 GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
298} GhosttyKittyGraphicsPlacementIteratorOption;
299
300/**
301 * Pixel format of a Kitty graphics image.
302 *
303 * Note that stored images are always fully decoded:
304 * GHOSTTY_KITTY_IMAGE_FORMAT_PNG is never returned by
305 * ghostty_kitty_graphics_image_get() because PNG payloads are decoded
306 * to GHOSTTY_KITTY_IMAGE_FORMAT_RGBA before storage. The PNG value
307 * exists only for protocol-level completeness.
308 *
309 * @ingroup kitty_graphics
310 */
311typedef enum GHOSTTY_ENUM_TYPED {
312 GHOSTTY_KITTY_IMAGE_FORMAT_RGB = 0,
313 GHOSTTY_KITTY_IMAGE_FORMAT_RGBA = 1,
314 GHOSTTY_KITTY_IMAGE_FORMAT_PNG = 2,
315 GHOSTTY_KITTY_IMAGE_FORMAT_GRAY_ALPHA = 3,
316 GHOSTTY_KITTY_IMAGE_FORMAT_GRAY = 4,
317 GHOSTTY_KITTY_IMAGE_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
318} GhosttyKittyImageFormat;
319
320/**
321 * Compression of a Kitty graphics image.
322 *
323 * Note that stored images are always decompressed:
324 * GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE payloads are inflated
325 * before storage, so ghostty_kitty_graphics_image_get() always reports
326 * GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE. Consumers never need to
327 * inflate image data themselves.
328 *
329 * @ingroup kitty_graphics
330 */
331typedef enum GHOSTTY_ENUM_TYPED {
332 GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE = 0,
333 GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE = 1,
334 GHOSTTY_KITTY_IMAGE_COMPRESSION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
335} GhosttyKittyImageCompression;
336
337/**
338 * Queryable data kinds for ghostty_kitty_graphics_image_get().
339 *
340 * @ingroup kitty_graphics
341 */
342typedef enum GHOSTTY_ENUM_TYPED {
343 /** Invalid / sentinel value. */
344 GHOSTTY_KITTY_IMAGE_DATA_INVALID = 0,
345
346 /**
347 * The image ID.
348 *
349 * Output type: uint32_t *
350 */
351 GHOSTTY_KITTY_IMAGE_DATA_ID = 1,
352
353 /**
354 * The image number.
355 *
356 * Output type: uint32_t *
357 */
358 GHOSTTY_KITTY_IMAGE_DATA_NUMBER = 2,
359
360 /**
361 * Image width in pixels.
362 *
363 * Output type: uint32_t *
364 */
365 GHOSTTY_KITTY_IMAGE_DATA_WIDTH = 3,
366
367 /**
368 * Image height in pixels.
369 *
370 * Output type: uint32_t *
371 */
372 GHOSTTY_KITTY_IMAGE_DATA_HEIGHT = 4,
373
374 /**
375 * Pixel format of the image. Never GHOSTTY_KITTY_IMAGE_FORMAT_PNG;
376 * PNG payloads are decoded to RGBA before storage.
377 *
378 * Output type: GhosttyKittyImageFormat *
379 */
380 GHOSTTY_KITTY_IMAGE_DATA_FORMAT = 5,
381
382 /**
383 * Compression of the image. Always
384 * GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE; compressed payloads are
385 * inflated before storage.
386 *
387 * Output type: GhosttyKittyImageCompression *
388 */
389 GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION = 6,
390
391 /**
392 * Borrowed pointer to the raw pixel data. Valid as long as the
393 * underlying terminal is not mutated.
394 *
395 * The data is always fully decoded, uncompressed pixels in the
396 * format reported by GHOSTTY_KITTY_IMAGE_DATA_FORMAT: zlib payloads
397 * are inflated and PNG payloads are decoded to RGBA at transmission
398 * time, before the image is stored. Consumers can upload this
399 * directly to the GPU without any decode step.
400 *
401 * Output type: const uint8_t **
402 */
403 GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR = 7,
404
405 /**
406 * Length of the raw pixel data in bytes. Always equal to
407 * width * height * bytes-per-pixel for the reported format.
408 *
409 * Output type: size_t *
410 */
411 GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN = 8,
412
413 /**
414 * Generation stamp assigned when this image was added to (or
415 * replaced in) the storage. A changed generation for a given image
416 * ID means the pixel contents may have changed even when the
417 * dimensions, format, and data length are identical (e.g. a
418 * retransmission of the same image ID), so texture caches must key
419 * staleness on this value rather than on size heuristics.
420 *
421 * Stamps are unique and monotonically increasing process-wide and
422 * are drawn from the same sequence as
423 * GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION. Never zero for a stored
424 * image, so zero can be used as an "empty" sentinel by callers.
425 *
426 * Output type: uint64_t *
427 */
428 GHOSTTY_KITTY_IMAGE_DATA_GENERATION = 9,
429
430 GHOSTTY_KITTY_IMAGE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
431} GhosttyKittyGraphicsImageData;
432
433/**
434 * Combined rendering geometry for a placement in a single sized struct.
435 *
436 * Combines the results of ghostty_kitty_graphics_placement_pixel_size(),
437 * ghostty_kitty_graphics_placement_grid_size(),
438 * ghostty_kitty_graphics_placement_viewport_pos(), and
439 * ghostty_kitty_graphics_placement_source_rect() into one call. This is
440 * an optimization over calling those four functions individually,
441 * particularly useful in environments with high per-call overhead such
442 * as FFI or Cgo.
443 *
444 * This struct uses the sized-struct ABI pattern. Initialize with
445 * GHOSTTY_INIT_SIZED(GhosttyKittyGraphicsPlacementRenderInfo) before calling
446 * ghostty_kitty_graphics_placement_render_info().
447 *
448 * @ingroup kitty_graphics
449 */
450typedef struct {
451 /** Size of this struct in bytes. Must be set to sizeof(GhosttyKittyGraphicsPlacementRenderInfo). */
452 size_t size;
453 /** Rendered width in pixels. */
454 uint32_t pixel_width;
455 /** Rendered height in pixels. */
456 uint32_t pixel_height;
457 /** Number of grid columns the placement occupies. */
458 uint32_t grid_cols;
459 /** Number of grid rows the placement occupies. */
460 uint32_t grid_rows;
461 /** Viewport-relative column (may be negative for partially visible placements). */
462 int32_t viewport_col;
463 /** Viewport-relative row (may be negative for partially visible placements). */
464 int32_t viewport_row;
465 /** False when the placement is fully off-screen or virtual. */
466 bool viewport_visible;
467 /** Resolved source rectangle x origin in pixels. */
468 uint32_t source_x;
469 /** Resolved source rectangle y origin in pixels. */
470 uint32_t source_y;
471 /** Resolved source rectangle width in pixels. */
472 uint32_t source_width;
473 /** Resolved source rectangle height in pixels. */
474 uint32_t source_height;
475} GhosttyKittyGraphicsPlacementRenderInfo;
476
477/**
478 * Get data from a kitty graphics storage instance.
479 *
480 * The output pointer must be of the appropriate type for the requested
481 * data kind.
482 *
483 * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
484 *
485 * @param graphics The kitty graphics handle
486 * @param data The type of data to extract
487 * @param[out] out Pointer to store the extracted data
488 * @return GHOSTTY_SUCCESS on success
489 *
490 * @ingroup kitty_graphics
491 */
492GHOSTTY_API GhosttyResult ghostty_kitty_graphics_get(
493 GhosttyKittyGraphics graphics,
494 GhosttyKittyGraphicsData data,
495 void* out);
496
497/**
498 * Look up a Kitty graphics image by its image ID.
499 *
500 * Returns NULL if no image with the given ID exists or if Kitty graphics
501 * are disabled at build time.
502 *
503 * @param graphics The kitty graphics handle
504 * @param image_id The image ID to look up
505 * @return An opaque image handle, or NULL if not found
506 *
507 * @ingroup kitty_graphics
508 */
509GHOSTTY_API GhosttyKittyGraphicsImage ghostty_kitty_graphics_image(
510 GhosttyKittyGraphics graphics,
511 uint32_t image_id);
512
513/**
514 * Get data from a Kitty graphics image.
515 *
516 * The output pointer must be of the appropriate type for the requested
517 * data kind.
518 *
519 * @param image The image handle (NULL returns GHOSTTY_INVALID_VALUE)
520 * @param data The data kind to query
521 * @param[out] out Pointer to receive the queried value
522 * @return GHOSTTY_SUCCESS on success
523 *
524 * @ingroup kitty_graphics
525 */
526GHOSTTY_API GhosttyResult ghostty_kitty_graphics_image_get(
527 GhosttyKittyGraphicsImage image,
528 GhosttyKittyGraphicsImageData data,
529 void* out);
530
531/**
532 * Get multiple data fields from a Kitty graphics image in a single call.
533 *
534 * This is an optimization over calling ghostty_kitty_graphics_image_get()
535 * repeatedly, particularly useful in environments with high per-call
536 * overhead such as FFI or Cgo.
537 *
538 * Each element in the keys array specifies a data kind, and the
539 * corresponding element in the values array receives the result.
540 * The type of each values[i] pointer must match the output type
541 * documented for keys[i].
542 *
543 * Processing stops at the first error; on success out_written
544 * is set to count, on error it is set to the index of the
545 * failing key (i.e. the number of values successfully written).
546 *
547 * @param image The image handle (NULL returns GHOSTTY_INVALID_VALUE)
548 * @param count Number of key/value pairs
549 * @param keys Array of data kinds to query
550 * @param values Array of output pointers (types must match each key's
551 * documented output type)
552 * @param[out] out_written On return, receives the number of values
553 * successfully written (may be NULL)
554 * @return GHOSTTY_SUCCESS if all queries succeed
555 *
556 * @ingroup kitty_graphics
557 */
558GHOSTTY_API GhosttyResult ghostty_kitty_graphics_image_get_multi(
559 GhosttyKittyGraphicsImage image,
560 size_t count,
561 const GhosttyKittyGraphicsImageData* keys,
562 void** values,
563 size_t* out_written);
564
565/**
566 * Create a new placement iterator instance.
567 *
568 * All fields except the allocator are left undefined until populated
569 * via ghostty_kitty_graphics_get() with
570 * GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR.
571 *
572 * @param allocator Pointer to allocator, or NULL to use the default allocator
573 * @param[out] out_iterator On success, receives the created iterator handle
574 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
575 * failure
576 *
577 * @ingroup kitty_graphics
578 */
579GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_new(
580 const GhosttyAllocator* allocator,
581 GhosttyKittyGraphicsPlacementIterator* out_iterator);
582
583/**
584 * Free a placement iterator.
585 *
586 * @param iterator The iterator handle to free (may be NULL)
587 *
588 * @ingroup kitty_graphics
589 */
590GHOSTTY_API void ghostty_kitty_graphics_placement_iterator_free(
591 GhosttyKittyGraphicsPlacementIterator iterator);
592
593/**
594 * Set an option on a placement iterator.
595 *
596 * Use GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER with a
597 * GhosttyKittyPlacementLayer value to filter placements by z-layer.
598 * The filter is applied during iteration: ghostty_kitty_graphics_placement_next()
599 * will skip placements that do not match the configured layer.
600 *
601 * The default layer is GHOSTTY_KITTY_PLACEMENT_LAYER_ALL (no filtering).
602 *
603 * @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE)
604 * @param option The option to set
605 * @param value Pointer to the value (type depends on option; NULL returns
606 * GHOSTTY_INVALID_VALUE)
607 * @return GHOSTTY_SUCCESS on success
608 *
609 * @ingroup kitty_graphics
610 */
611GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_set(
612 GhosttyKittyGraphicsPlacementIterator iterator,
613 GhosttyKittyGraphicsPlacementIteratorOption option,
614 const void* value);
615
616/**
617 * Advance the placement iterator to the next placement.
618 *
619 * If a layer filter has been set via
620 * ghostty_kitty_graphics_placement_iterator_set(), only placements
621 * matching that layer are returned.
622 *
623 * @param iterator The iterator handle (may be NULL)
624 * @return true if advanced to the next placement, false if at the end
625 *
626 * @ingroup kitty_graphics
627 */
628GHOSTTY_API bool ghostty_kitty_graphics_placement_next(
629 GhosttyKittyGraphicsPlacementIterator iterator);
630
631/**
632 * Get data from the current placement in a placement iterator.
633 *
634 * Call ghostty_kitty_graphics_placement_next() at least once before
635 * calling this function.
636 *
637 * @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE)
638 * @param data The data kind to query
639 * @param[out] out Pointer to receive the queried value
640 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
641 * iterator is NULL or not positioned on a placement
642 *
643 * @ingroup kitty_graphics
644 */
645GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_get(
646 GhosttyKittyGraphicsPlacementIterator iterator,
647 GhosttyKittyGraphicsPlacementData data,
648 void* out);
649
650/**
651 * Get multiple data fields from the current placement in a single call.
652 *
653 * This is an optimization over calling ghostty_kitty_graphics_placement_get()
654 * repeatedly, particularly useful in environments with high per-call
655 * overhead such as FFI or Cgo.
656 *
657 * Each element in the keys array specifies a data kind, and the
658 * corresponding element in the values array receives the result.
659 * The type of each values[i] pointer must match the output type
660 * documented for keys[i].
661 *
662 * Processing stops at the first error; on success out_written
663 * is set to count, on error it is set to the index of the
664 * failing key (i.e. the number of values successfully written).
665 *
666 * @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE)
667 * @param count Number of key/value pairs
668 * @param keys Array of data kinds to query
669 * @param values Array of output pointers (types must match each key's
670 * documented output type)
671 * @param[out] out_written On return, receives the number of values
672 * successfully written (may be NULL)
673 * @return GHOSTTY_SUCCESS if all queries succeed
674 *
675 * @ingroup kitty_graphics
676 */
677GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_get_multi(
678 GhosttyKittyGraphicsPlacementIterator iterator,
679 size_t count,
680 const GhosttyKittyGraphicsPlacementData* keys,
681 void** values,
682 size_t* out_written);
683
684/**
685 * Compute the grid rectangle occupied by the current placement.
686 *
687 * Uses the placement's pin, the image dimensions, and the terminal's
688 * cell/pixel geometry to calculate the bounding rectangle. Virtual
689 * placements (unicode placeholders) return GHOSTTY_NO_VALUE.
690 *
691 * @param terminal The terminal handle
692 * @param image The image handle for this placement's image
693 * @param iterator The placement iterator positioned on a placement
694 * @param[out] out_selection On success, receives the bounding rectangle
695 * as a selection with rectangle=true
696 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
697 * is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE for
698 * virtual placements or when Kitty graphics are disabled
699 *
700 * @ingroup kitty_graphics
701 */
702GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_rect(
703 GhosttyKittyGraphicsPlacementIterator iterator,
704 GhosttyKittyGraphicsImage image,
705 GhosttyTerminal terminal,
706 GhosttySelection* out_selection);
707
708/**
709 * Compute the rendered pixel size of the current placement.
710 *
711 * Takes into account the placement's source rectangle, specified
712 * columns/rows, and aspect ratio to calculate the final rendered
713 * pixel dimensions.
714 *
715 * @param iterator The placement iterator positioned on a placement
716 * @param image The image handle for this placement's image
717 * @param terminal The terminal handle
718 * @param[out] out_width On success, receives the width in pixels
719 * @param[out] out_height On success, receives the height in pixels
720 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
721 * is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when
722 * Kitty graphics are disabled
723 *
724 * @ingroup kitty_graphics
725 */
726GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_pixel_size(
727 GhosttyKittyGraphicsPlacementIterator iterator,
728 GhosttyKittyGraphicsImage image,
729 GhosttyTerminal terminal,
730 uint32_t* out_width,
731 uint32_t* out_height);
732
733/**
734 * Compute the grid cell size of the current placement.
735 *
736 * Returns the number of columns and rows that the placement occupies
737 * in the terminal grid. If the placement specifies explicit columns
738 * and rows, those are returned directly; otherwise they are calculated
739 * from the pixel size and cell dimensions.
740 *
741 * @param iterator The placement iterator positioned on a placement
742 * @param image The image handle for this placement's image
743 * @param terminal The terminal handle
744 * @param[out] out_cols On success, receives the number of columns
745 * @param[out] out_rows On success, receives the number of rows
746 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
747 * is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when
748 * Kitty graphics are disabled
749 *
750 * @ingroup kitty_graphics
751 */
752GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_grid_size(
753 GhosttyKittyGraphicsPlacementIterator iterator,
754 GhosttyKittyGraphicsImage image,
755 GhosttyTerminal terminal,
756 uint32_t* out_cols,
757 uint32_t* out_rows);
758
759/**
760 * Get the viewport-relative grid position of the current placement.
761 *
762 * Converts the placement's internal pin to viewport-relative column and
763 * row coordinates. The returned coordinates represent the top-left
764 * corner of the placement in the viewport's grid coordinate space.
765 *
766 * The row value can be negative when the placement's origin has
767 * scrolled above the top of the viewport. For example, a 4-row
768 * image that has scrolled up by 2 rows returns row=-2, meaning
769 * its top 2 rows are above the visible area but its bottom 2 rows
770 * are still on screen. Embedders should use these coordinates
771 * directly when computing the destination rectangle for rendering;
772 * the embedder is responsible for clipping the portion of the image
773 * that falls outside the viewport.
774 *
775 * Returns GHOSTTY_SUCCESS for any placement that is at least
776 * partially visible in the viewport. Returns GHOSTTY_NO_VALUE when
777 * the placement is completely outside the viewport (its bottom edge
778 * is above the viewport or its top edge is at or below the last
779 * viewport row), or when the placement is a virtual (unicode
780 * placeholder) placement.
781 *
782 * @param iterator The placement iterator positioned on a placement
783 * @param image The image handle for this placement's image
784 * @param terminal The terminal handle
785 * @param[out] out_col On success, receives the viewport-relative column
786 * @param[out] out_row On success, receives the viewport-relative row
787 * (may be negative for partially visible placements)
788 * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if fully
789 * off-screen or virtual, GHOSTTY_INVALID_VALUE if any handle
790 * is NULL or the iterator is not positioned
791 *
792 * @ingroup kitty_graphics
793 */
794GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_viewport_pos(
795 GhosttyKittyGraphicsPlacementIterator iterator,
796 GhosttyKittyGraphicsImage image,
797 GhosttyTerminal terminal,
798 int32_t* out_col,
799 int32_t* out_row);
800
801/**
802 * Get the resolved source rectangle for the current placement.
803 *
804 * Applies kitty protocol semantics: a width or height of 0 in the
805 * placement means "use the full image dimension", and the resulting
806 * rectangle is clamped to the actual image bounds. The returned
807 * values are in pixels and are ready to use for texture sampling.
808 *
809 * @param iterator The placement iterator positioned on a placement
810 * @param image The image handle for this placement's image
811 * @param[out] out_x Source rect x origin in pixels
812 * @param[out] out_y Source rect y origin in pixels
813 * @param[out] out_width Source rect width in pixels
814 * @param[out] out_height Source rect height in pixels
815 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any
816 * handle is NULL or the iterator is not positioned
817 *
818 * @ingroup kitty_graphics
819 */
820GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_source_rect(
821 GhosttyKittyGraphicsPlacementIterator iterator,
822 GhosttyKittyGraphicsImage image,
823 uint32_t* out_x,
824 uint32_t* out_y,
825 uint32_t* out_width,
826 uint32_t* out_height);
827
828/**
829 * Get all rendering geometry for a placement in a single call.
830 *
831 * Combines pixel size, grid size, viewport position, and source
832 * rectangle into one struct. Initialize with
833 * GHOSTTY_INIT_SIZED(GhosttyKittyGraphicsPlacementRenderInfo).
834 *
835 * When viewport_visible is false, the placement is fully off-screen
836 * or is a virtual placement; viewport_col and viewport_row may
837 * contain meaningless values in that case.
838 *
839 * @param iterator The iterator positioned on a placement
840 * @param image The image handle for this placement's image
841 * @param terminal The terminal handle
842 * @param[out] out_info Pointer to receive the rendering geometry
843 * @return GHOSTTY_SUCCESS on success
844 *
845 * @ingroup kitty_graphics
846 */
847GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_render_info(
848 GhosttyKittyGraphicsPlacementIterator iterator,
849 GhosttyKittyGraphicsImage image,
850 GhosttyTerminal terminal,
851 GhosttyKittyGraphicsPlacementRenderInfo* out_info);
852
853/** @} */
854
855#ifdef __cplusplus
856}
857#endif
858
859#endif /* GHOSTTY_VT_KITTY_GRAPHICS_H */
packages/terminal/vendor/include/ghostty/vt/modes.h created+198
......@@ -0,0 +1,198 @@
1/**
2 * @file modes.h
3 *
4 * Terminal mode utilities - pack and unpack ANSI/DEC mode identifiers.
5 */
6
7#ifndef GHOSTTY_VT_MODES_H
8#define GHOSTTY_VT_MODES_H
9
10/** @defgroup modes Mode Utilities
11 *
12 * Utilities for working with terminal modes. A mode is a compact
13 * 16-bit representation of a terminal mode identifier that encodes both
14 * the numeric mode value (up to 15 bits) and whether the mode is an ANSI
15 * mode or a DEC private mode (?-prefixed).
16 *
17 * The packed layout (least-significant bit first) is:
18 * - Bits 0–14: mode value (u15)
19 * - Bit 15: ANSI flag (0 = DEC private mode, 1 = ANSI mode)
20 *
21 * ## Example
22 *
23 * @snippet c-vt-modes/src/main.c modes-pack-unpack
24 *
25 * ## DECRPM Report Encoding
26 *
27 * Use ghostty_mode_report_encode() to encode a DECRPM response into a
28 * caller-provided buffer:
29 *
30 * @snippet c-vt-modes/src/main.c modes-decrpm
31 *
32 * @{
33 */
34
35#include <stdbool.h>
36#include <stddef.h>
37#include <stdint.h>
38
39#include <ghostty/vt/types.h>
40
41#ifdef __cplusplus
42extern "C" {
43#endif
44
45/** @name ANSI Modes
46 * Modes for standard ANSI modes.
47 * @{
48 */
49#define GHOSTTY_MODE_KAM (ghostty_mode_new(2, true)) /**< Keyboard action (disable keyboard) */
50#define GHOSTTY_MODE_INSERT (ghostty_mode_new(4, true)) /**< Insert mode */
51#define GHOSTTY_MODE_SRM (ghostty_mode_new(12, true)) /**< Send/receive mode */
52#define GHOSTTY_MODE_LINEFEED (ghostty_mode_new(20, true)) /**< Linefeed/new line mode */
53/** @} */
54
55/** @name DEC Private Modes
56 * Modes for DEC private modes (?-prefixed).
57 * @{
58 */
59#define GHOSTTY_MODE_DECCKM (ghostty_mode_new(1, false)) /**< Cursor keys */
60#define GHOSTTY_MODE_132_COLUMN (ghostty_mode_new(3, false)) /**< 132/80 column mode */
61#define GHOSTTY_MODE_SLOW_SCROLL (ghostty_mode_new(4, false)) /**< Slow scroll */
62#define GHOSTTY_MODE_REVERSE_COLORS (ghostty_mode_new(5, false)) /**< Reverse video */
63#define GHOSTTY_MODE_ORIGIN (ghostty_mode_new(6, false)) /**< Origin mode */
64#define GHOSTTY_MODE_WRAPAROUND (ghostty_mode_new(7, false)) /**< Auto-wrap mode */
65#define GHOSTTY_MODE_AUTOREPEAT (ghostty_mode_new(8, false)) /**< Auto-repeat keys */
66#define GHOSTTY_MODE_X10_MOUSE (ghostty_mode_new(9, false)) /**< X10 mouse reporting */
67#define GHOSTTY_MODE_CURSOR_BLINKING (ghostty_mode_new(12, false)) /**< Cursor blink */
68#define GHOSTTY_MODE_CURSOR_VISIBLE (ghostty_mode_new(25, false)) /**< Cursor visible (DECTCEM) */
69#define GHOSTTY_MODE_ENABLE_MODE_3 (ghostty_mode_new(40, false)) /**< Allow 132 column mode */
70#define GHOSTTY_MODE_REVERSE_WRAP (ghostty_mode_new(45, false)) /**< Reverse wrap */
71#define GHOSTTY_MODE_ALT_SCREEN_LEGACY (ghostty_mode_new(47, false)) /**< Alternate screen (legacy) */
72#define GHOSTTY_MODE_KEYPAD_KEYS (ghostty_mode_new(66, false)) /**< Application keypad */
73#define GHOSTTY_MODE_BACKARROW_KEY_MODE (ghostty_mode_new(67, false)) /**< Backarrow key mode (DECBKM) */
74#define GHOSTTY_MODE_LEFT_RIGHT_MARGIN (ghostty_mode_new(69, false)) /**< Left/right margin mode */
75#define GHOSTTY_MODE_NORMAL_MOUSE (ghostty_mode_new(1000, false)) /**< Normal mouse tracking */
76#define GHOSTTY_MODE_BUTTON_MOUSE (ghostty_mode_new(1002, false)) /**< Button-event mouse tracking */
77#define GHOSTTY_MODE_ANY_MOUSE (ghostty_mode_new(1003, false)) /**< Any-event mouse tracking */
78#define GHOSTTY_MODE_FOCUS_EVENT (ghostty_mode_new(1004, false)) /**< Focus in/out events */
79#define GHOSTTY_MODE_UTF8_MOUSE (ghostty_mode_new(1005, false)) /**< UTF-8 mouse format */
80#define GHOSTTY_MODE_SGR_MOUSE (ghostty_mode_new(1006, false)) /**< SGR mouse format */
81#define GHOSTTY_MODE_ALT_SCROLL (ghostty_mode_new(1007, false)) /**< Alternate scroll mode */
82#define GHOSTTY_MODE_URXVT_MOUSE (ghostty_mode_new(1015, false)) /**< URxvt mouse format */
83#define GHOSTTY_MODE_SGR_PIXELS_MOUSE (ghostty_mode_new(1016, false)) /**< SGR-Pixels mouse format */
84#define GHOSTTY_MODE_NUMLOCK_KEYPAD (ghostty_mode_new(1035, false)) /**< Ignore keypad with NumLock */
85#define GHOSTTY_MODE_ALT_ESC_PREFIX (ghostty_mode_new(1036, false)) /**< Alt key sends ESC prefix */
86#define GHOSTTY_MODE_ALT_SENDS_ESC (ghostty_mode_new(1039, false)) /**< Alt sends escape */
87#define GHOSTTY_MODE_REVERSE_WRAP_EXT (ghostty_mode_new(1045, false)) /**< Extended reverse wrap */
88#define GHOSTTY_MODE_ALT_SCREEN (ghostty_mode_new(1047, false)) /**< Alternate screen */
89#define GHOSTTY_MODE_SAVE_CURSOR (ghostty_mode_new(1048, false)) /**< Save cursor (DECSC) */
90#define GHOSTTY_MODE_ALT_SCREEN_SAVE (ghostty_mode_new(1049, false)) /**< Alt screen + save cursor + clear */
91#define GHOSTTY_MODE_BRACKETED_PASTE (ghostty_mode_new(2004, false)) /**< Bracketed paste mode */
92#define GHOSTTY_MODE_SYNC_OUTPUT (ghostty_mode_new(2026, false)) /**< Synchronized output */
93#define GHOSTTY_MODE_GRAPHEME_CLUSTER (ghostty_mode_new(2027, false)) /**< Grapheme cluster mode */
94#define GHOSTTY_MODE_COLOR_SCHEME_REPORT (ghostty_mode_new(2031, false)) /**< Report color scheme */
95#define GHOSTTY_MODE_IN_BAND_RESIZE (ghostty_mode_new(2048, false)) /**< In-band size reports */
96/** @} */
97
98/**
99 * A packed 16-bit terminal mode.
100 *
101 * Encodes a mode value (bits 0–14) and an ANSI flag (bit 15) into a
102 * single 16-bit integer. Use the inline helper functions to construct
103 * and inspect modes rather than manipulating bits directly.
104 */
105typedef uint16_t GhosttyMode;
106
107/**
108 * Create a mode from a mode value and ANSI flag.
109 *
110 * @param value The numeric mode value (0–32767)
111 * @param ansi true for an ANSI mode, false for a DEC private mode
112 * @return The packed mode
113 *
114 * @ingroup modes
115 */
116static inline GhosttyMode ghostty_mode_new(uint16_t value, bool ansi) {
117 return (GhosttyMode)((value & 0x7FFF) | ((uint16_t)ansi << 15));
118}
119
120/**
121 * Extract the numeric mode value from a mode.
122 *
123 * @param mode The mode
124 * @return The mode value (0–32767)
125 *
126 * @ingroup modes
127 */
128static inline uint16_t ghostty_mode_value(GhosttyMode mode) {
129 return mode & 0x7FFF;
130}
131
132/**
133 * Check whether a mode represents an ANSI mode.
134 *
135 * @param mode The mode
136 * @return true if this is an ANSI mode, false if it is a DEC private mode
137 *
138 * @ingroup modes
139 */
140static inline bool ghostty_mode_ansi(GhosttyMode mode) {
141 return (mode >> 15) != 0;
142}
143
144/**
145 * DECRPM report state values.
146 *
147 * These correspond to the Ps2 parameter in a DECRPM response
148 * sequence (CSI ? Ps1 ; Ps2 $ y).
149 */
150typedef enum GHOSTTY_ENUM_TYPED {
151 /** Mode is not recognized */
152 GHOSTTY_MODE_REPORT_NOT_RECOGNIZED = 0,
153 /** Mode is set (enabled) */
154 GHOSTTY_MODE_REPORT_SET = 1,
155 /** Mode is reset (disabled) */
156 GHOSTTY_MODE_REPORT_RESET = 2,
157 /** Mode is permanently set */
158 GHOSTTY_MODE_REPORT_PERMANENTLY_SET = 3,
159 /** Mode is permanently reset */
160 GHOSTTY_MODE_REPORT_PERMANENTLY_RESET = 4,
161 GHOSTTY_MODE_REPORT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
162} GhosttyModeReportState;
163
164/**
165 * Encode a DECRPM (DEC Private Mode Report) response sequence.
166 *
167 * Writes a mode report escape sequence into the provided buffer.
168 * The generated sequence has the form:
169 * - DEC private mode: CSI ? Ps1 ; Ps2 $ y
170 * - ANSI mode: CSI Ps1 ; Ps2 $ y
171 *
172 * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE
173 * and writes the required buffer size to @p out_written. The caller can
174 * then retry with a sufficiently sized buffer.
175 *
176 * @param mode The mode identifying the mode to report on
177 * @param state The report state for this mode
178 * @param buf Output buffer to write the encoded sequence into (may be NULL)
179 * @param buf_len Size of the output buffer in bytes
180 * @param[out] out_written On success, the number of bytes written. On
181 * GHOSTTY_OUT_OF_SPACE, the required buffer size.
182 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer
183 * is too small
184 */
185GHOSTTY_API GhosttyResult ghostty_mode_report_encode(
186 GhosttyMode mode,
187 GhosttyModeReportState state,
188 char* buf,
189 size_t buf_len,
190 size_t* out_written);
191
192#ifdef __cplusplus
193}
194#endif
195
196/** @} */
197
198#endif /* GHOSTTY_VT_MODES_H */
packages/terminal/vendor/include/ghostty/vt/mouse.h created+70
......@@ -0,0 +1,70 @@
1/**
2 * @file mouse.h
3 *
4 * Mouse encoding module - encode mouse events into terminal escape sequences.
5 */
6
7#ifndef GHOSTTY_VT_MOUSE_H
8#define GHOSTTY_VT_MOUSE_H
9
10/** @defgroup mouse Mouse Encoding
11 *
12 * Utilities for encoding mouse events into terminal escape sequences,
13 * supporting X10, UTF-8, SGR, URxvt, and SGR-Pixels mouse protocols.
14 *
15 * ## Basic Usage
16 *
17 * 1. Create an encoder instance with ghostty_mouse_encoder_new().
18 * 2. Configure encoder options with ghostty_mouse_encoder_setopt() or
19 * ghostty_mouse_encoder_setopt_from_terminal().
20 * 3. For each mouse event:
21 * - Create a mouse event with ghostty_mouse_event_new().
22 * - Set event properties (action, button, modifiers, position).
23 * - Encode with ghostty_mouse_encoder_encode().
24 * - Free the event with ghostty_mouse_event_free() or reuse it.
25 * 4. Free the encoder with ghostty_mouse_encoder_free() when done.
26 *
27 * For a complete working example, see example/c-vt-encode-mouse in the
28 * repository.
29 *
30 * ## Example
31 *
32 * @snippet c-vt-encode-mouse/src/main.c mouse-encode
33 *
34 * ## Example: Encoding with Terminal State
35 *
36 * When you have a GhosttyTerminal, you can sync its tracking mode and
37 * output format into the encoder automatically:
38 *
39 * @code{.c}
40 * // Create a terminal and feed it some VT data that enables mouse tracking
41 * GhosttyTerminal terminal;
42 * ghostty_terminal_new(NULL, &terminal,
43 * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0});
44 *
45 * // Application might write data that enables mouse reporting, etc.
46 * ghostty_terminal_vt_write(terminal, vt_data, vt_len);
47 *
48 * // Create an encoder and sync its options from the terminal
49 * GhosttyMouseEncoder encoder;
50 * ghostty_mouse_encoder_new(NULL, &encoder);
51 * ghostty_mouse_encoder_setopt_from_terminal(encoder, terminal);
52 *
53 * // Encode a mouse event using the terminal-derived options
54 * char buf[128];
55 * size_t written = 0;
56 * ghostty_mouse_encoder_encode(encoder, event, buf, sizeof(buf), &written);
57 *
58 * ghostty_mouse_encoder_free(encoder);
59 * ghostty_terminal_free(terminal);
60 * @endcode
61 *
62 * @{
63 */
64
65#include <ghostty/vt/mouse/event.h>
66#include <ghostty/vt/mouse/encoder.h>
67
68/** @} */
69
70#endif /* GHOSTTY_VT_MOUSE_H */
packages/terminal/vendor/include/ghostty/vt/mouse/encoder.h created+214
......@@ -0,0 +1,214 @@
1/**
2 * @file encoder.h
3 *
4 * Mouse event encoding to terminal escape sequences.
5 */
6
7#ifndef GHOSTTY_VT_MOUSE_ENCODER_H
8#define GHOSTTY_VT_MOUSE_ENCODER_H
9
10#include <stdbool.h>
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/allocator.h>
14#include <ghostty/vt/mouse/event.h>
15#include <ghostty/vt/terminal.h>
16#include <ghostty/vt/types.h>
17
18/**
19 * Opaque handle to a mouse encoder instance.
20 *
21 * This handle represents a mouse encoder that converts normalized
22 * mouse events into terminal escape sequences.
23 *
24 * @ingroup mouse
25 */
26typedef struct GhosttyMouseEncoderImpl *GhosttyMouseEncoder;
27
28/**
29 * Mouse tracking mode.
30 *
31 * @ingroup mouse
32 */
33typedef enum GHOSTTY_ENUM_TYPED {
34 /** Mouse reporting disabled. */
35 GHOSTTY_MOUSE_TRACKING_NONE = 0,
36
37 /** X10 mouse mode. */
38 GHOSTTY_MOUSE_TRACKING_X10 = 1,
39
40 /** Normal mouse mode (button press/release only). */
41 GHOSTTY_MOUSE_TRACKING_NORMAL = 2,
42
43 /** Button-event tracking mode. */
44 GHOSTTY_MOUSE_TRACKING_BUTTON = 3,
45
46 /** Any-event tracking mode. */
47 GHOSTTY_MOUSE_TRACKING_ANY = 4,
48 GHOSTTY_MOUSE_TRACKING_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
49} GhosttyMouseTrackingMode;
50
51/**
52 * Mouse output format.
53 *
54 * @ingroup mouse
55 */
56typedef enum GHOSTTY_ENUM_TYPED {
57 GHOSTTY_MOUSE_FORMAT_X10 = 0,
58 GHOSTTY_MOUSE_FORMAT_UTF8 = 1,
59 GHOSTTY_MOUSE_FORMAT_SGR = 2,
60 GHOSTTY_MOUSE_FORMAT_URXVT = 3,
61 GHOSTTY_MOUSE_FORMAT_SGR_PIXELS = 4,
62 GHOSTTY_MOUSE_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
63} GhosttyMouseFormat;
64
65/**
66 * Mouse encoder size and geometry context.
67 *
68 * This describes the rendered terminal geometry used to convert
69 * surface-space positions into encoded coordinates.
70 *
71 * @ingroup mouse
72 */
73typedef struct {
74 /** Size of this struct in bytes. Must be set to sizeof(GhosttyMouseEncoderSize). */
75 size_t size;
76
77 /** Full screen width in pixels. */
78 uint32_t screen_width;
79
80 /** Full screen height in pixels. */
81 uint32_t screen_height;
82
83 /** Cell width in pixels. Must be non-zero. */
84 uint32_t cell_width;
85
86 /** Cell height in pixels. Must be non-zero. */
87 uint32_t cell_height;
88
89 /** Top padding in pixels. */
90 uint32_t padding_top;
91
92 /** Bottom padding in pixels. */
93 uint32_t padding_bottom;
94
95 /** Right padding in pixels. */
96 uint32_t padding_right;
97
98 /** Left padding in pixels. */
99 uint32_t padding_left;
100} GhosttyMouseEncoderSize;
101
102/**
103 * Mouse encoder option identifiers.
104 *
105 * These values are used with ghostty_mouse_encoder_setopt() to configure
106 * the behavior of the mouse encoder.
107 *
108 * @ingroup mouse
109 */
110typedef enum GHOSTTY_ENUM_TYPED {
111 /** Mouse tracking mode (value: GhosttyMouseTrackingMode). */
112 GHOSTTY_MOUSE_ENCODER_OPT_EVENT = 0,
113
114 /** Mouse output format (value: GhosttyMouseFormat). */
115 GHOSTTY_MOUSE_ENCODER_OPT_FORMAT = 1,
116
117 /** Renderer size context (value: GhosttyMouseEncoderSize). */
118 GHOSTTY_MOUSE_ENCODER_OPT_SIZE = 2,
119
120 /** Whether any mouse button is currently pressed (value: bool). */
121 GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED = 3,
122
123 /** Whether to enable motion deduplication by last cell (value: bool). */
124 GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL = 4,
125 GHOSTTY_MOUSE_ENCODER_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
126} GhosttyMouseEncoderOption;
127
128/**
129 * Create a new mouse encoder instance.
130 *
131 * @param allocator Pointer to allocator, or NULL to use the default allocator
132 * @param encoder Pointer to store the created encoder handle
133 * @return GHOSTTY_SUCCESS on success, or an error code on failure
134 *
135 * @ingroup mouse
136 */
137GHOSTTY_API GhosttyResult ghostty_mouse_encoder_new(const GhosttyAllocator *allocator,
138 GhosttyMouseEncoder *encoder);
139
140/**
141 * Free a mouse encoder instance.
142 *
143 * @param encoder The encoder handle to free (may be NULL)
144 *
145 * @ingroup mouse
146 */
147GHOSTTY_API void ghostty_mouse_encoder_free(GhosttyMouseEncoder encoder);
148
149/**
150 * Set an option on the mouse encoder.
151 *
152 * A null pointer value does nothing. It does not reset to defaults.
153 *
154 * @param encoder The encoder handle, must not be NULL
155 * @param option The option to set
156 * @param value Pointer to option value (type depends on option)
157 *
158 * @ingroup mouse
159 */
160GHOSTTY_API void ghostty_mouse_encoder_setopt(GhosttyMouseEncoder encoder,
161 GhosttyMouseEncoderOption option,
162 const void *value);
163
164/**
165 * Set encoder options from a terminal's current state.
166 *
167 * This sets tracking mode and output format from terminal state.
168 * It does not modify size or any-button state.
169 *
170 * @param encoder The encoder handle, must not be NULL
171 * @param terminal The terminal handle, must not be NULL
172 *
173 * @ingroup mouse
174 */
175GHOSTTY_API void ghostty_mouse_encoder_setopt_from_terminal(GhosttyMouseEncoder encoder,
176 GhosttyTerminal terminal);
177
178/**
179 * Reset internal encoder state.
180 *
181 * This clears motion deduplication state (last tracked cell).
182 *
183 * @param encoder The encoder handle (may be NULL)
184 *
185 * @ingroup mouse
186 */
187GHOSTTY_API void ghostty_mouse_encoder_reset(GhosttyMouseEncoder encoder);
188
189/**
190 * Encode a mouse event into a terminal escape sequence.
191 *
192 * Not all mouse events produce output. In such cases this returns
193 * GHOSTTY_SUCCESS with out_len set to 0.
194 *
195 * If the output buffer is too small, this returns GHOSTTY_OUT_OF_SPACE
196 * and out_len contains the required size.
197 *
198 * @param encoder The encoder handle, must not be NULL
199 * @param event The mouse event to encode, must not be NULL
200 * @param out_buf Buffer to write encoded bytes to, or NULL to query required size
201 * @param out_buf_size Size of out_buf in bytes
202 * @param out_len Pointer to store bytes written (or required bytes on failure)
203 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if buffer is too small,
204 * or another error code
205 *
206 * @ingroup mouse
207 */
208GHOSTTY_API GhosttyResult ghostty_mouse_encoder_encode(GhosttyMouseEncoder encoder,
209 GhosttyMouseEvent event,
210 char *out_buf,
211 size_t out_buf_size,
212 size_t *out_len);
213
214#endif /* GHOSTTY_VT_MOUSE_ENCODER_H */
packages/terminal/vendor/include/ghostty/vt/mouse/event.h created+195
......@@ -0,0 +1,195 @@
1/**
2 * @file event.h
3 *
4 * Mouse event representation and manipulation.
5 */
6
7#ifndef GHOSTTY_VT_MOUSE_EVENT_H
8#define GHOSTTY_VT_MOUSE_EVENT_H
9
10#include <stdbool.h>
11#include <ghostty/vt/allocator.h>
12#include <ghostty/vt/key/event.h>
13#include <ghostty/vt/types.h>
14
15/**
16 * Opaque handle to a mouse event.
17 *
18 * This handle represents a normalized mouse input event containing
19 * action, button, modifiers, and surface-space position.
20 *
21 * @ingroup mouse
22 */
23typedef struct GhosttyMouseEventImpl *GhosttyMouseEvent;
24
25/**
26 * Mouse event action type.
27 *
28 * @ingroup mouse
29 */
30typedef enum GHOSTTY_ENUM_TYPED {
31 /** Mouse button was pressed. */
32 GHOSTTY_MOUSE_ACTION_PRESS = 0,
33
34 /** Mouse button was released. */
35 GHOSTTY_MOUSE_ACTION_RELEASE = 1,
36
37 /** Mouse moved. */
38 GHOSTTY_MOUSE_ACTION_MOTION = 2,
39 GHOSTTY_MOUSE_ACTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
40} GhosttyMouseAction;
41
42/**
43 * Mouse button identity.
44 *
45 * @ingroup mouse
46 */
47typedef enum GHOSTTY_ENUM_TYPED {
48 GHOSTTY_MOUSE_BUTTON_UNKNOWN = 0,
49 GHOSTTY_MOUSE_BUTTON_LEFT = 1,
50 GHOSTTY_MOUSE_BUTTON_RIGHT = 2,
51 GHOSTTY_MOUSE_BUTTON_MIDDLE = 3,
52 GHOSTTY_MOUSE_BUTTON_FOUR = 4,
53 GHOSTTY_MOUSE_BUTTON_FIVE = 5,
54 GHOSTTY_MOUSE_BUTTON_SIX = 6,
55 GHOSTTY_MOUSE_BUTTON_SEVEN = 7,
56 GHOSTTY_MOUSE_BUTTON_EIGHT = 8,
57 GHOSTTY_MOUSE_BUTTON_NINE = 9,
58 GHOSTTY_MOUSE_BUTTON_TEN = 10,
59 GHOSTTY_MOUSE_BUTTON_ELEVEN = 11,
60 GHOSTTY_MOUSE_BUTTON_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
61} GhosttyMouseButton;
62
63/**
64 * Mouse position in surface-space pixels.
65 *
66 * @ingroup mouse
67 */
68typedef struct {
69 float x;
70 float y;
71} GhosttyMousePosition;
72
73/**
74 * Create a new mouse event instance.
75 *
76 * @param allocator Pointer to allocator, or NULL to use the default allocator
77 * @param event Pointer to store the created event handle
78 * @return GHOSTTY_SUCCESS on success, or an error code on failure
79 *
80 * @ingroup mouse
81 */
82GHOSTTY_API GhosttyResult ghostty_mouse_event_new(const GhosttyAllocator *allocator,
83 GhosttyMouseEvent *event);
84
85/**
86 * Free a mouse event instance.
87 *
88 * @param event The mouse event handle to free (may be NULL)
89 *
90 * @ingroup mouse
91 */
92GHOSTTY_API void ghostty_mouse_event_free(GhosttyMouseEvent event);
93
94/**
95 * Set the event action.
96 *
97 * @param event The event handle, must not be NULL
98 * @param action The action to set
99 *
100 * @ingroup mouse
101 */
102GHOSTTY_API void ghostty_mouse_event_set_action(GhosttyMouseEvent event,
103 GhosttyMouseAction action);
104
105/**
106 * Get the event action.
107 *
108 * @param event The event handle, must not be NULL
109 * @return The event action
110 *
111 * @ingroup mouse
112 */
113GHOSTTY_API GhosttyMouseAction ghostty_mouse_event_get_action(GhosttyMouseEvent event);
114
115/**
116 * Set the event button.
117 *
118 * This sets a concrete button identity for the event.
119 * To represent "no button" (for motion events), use
120 * ghostty_mouse_event_clear_button().
121 *
122 * @param event The event handle, must not be NULL
123 * @param button The button to set
124 *
125 * @ingroup mouse
126 */
127GHOSTTY_API void ghostty_mouse_event_set_button(GhosttyMouseEvent event,
128 GhosttyMouseButton button);
129
130/**
131 * Clear the event button.
132 *
133 * This sets the event button to "none".
134 *
135 * @param event The event handle, must not be NULL
136 *
137 * @ingroup mouse
138 */
139GHOSTTY_API void ghostty_mouse_event_clear_button(GhosttyMouseEvent event);
140
141/**
142 * Get the event button.
143 *
144 * @param event The event handle, must not be NULL
145 * @param out_button Output pointer for the button value (may be NULL)
146 * @return true if a button is set, false if no button is set
147 *
148 * @ingroup mouse
149 */
150GHOSTTY_API bool ghostty_mouse_event_get_button(GhosttyMouseEvent event,
151 GhosttyMouseButton *out_button);
152
153/**
154 * Set keyboard modifiers held during the event.
155 *
156 * @param event The event handle, must not be NULL
157 * @param mods Modifier bitmask
158 *
159 * @ingroup mouse
160 */
161GHOSTTY_API void ghostty_mouse_event_set_mods(GhosttyMouseEvent event,
162 GhosttyMods mods);
163
164/**
165 * Get keyboard modifiers held during the event.
166 *
167 * @param event The event handle, must not be NULL
168 * @return Modifier bitmask
169 *
170 * @ingroup mouse
171 */
172GHOSTTY_API GhosttyMods ghostty_mouse_event_get_mods(GhosttyMouseEvent event);
173
174/**
175 * Set the event position in surface-space pixels.
176 *
177 * @param event The event handle, must not be NULL
178 * @param position The position to set
179 *
180 * @ingroup mouse
181 */
182GHOSTTY_API void ghostty_mouse_event_set_position(GhosttyMouseEvent event,
183 GhosttyMousePosition position);
184
185/**
186 * Get the event position in surface-space pixels.
187 *
188 * @param event The event handle, must not be NULL
189 * @return The current event position
190 *
191 * @ingroup mouse
192 */
193GHOSTTY_API GhosttyMousePosition ghostty_mouse_event_get_position(GhosttyMouseEvent event);
194
195#endif /* GHOSTTY_VT_MOUSE_EVENT_H */
packages/terminal/vendor/include/ghostty/vt/osc.h created+215
......@@ -0,0 +1,215 @@
1/**
2 * @file osc.h
3 *
4 * OSC (Operating System Command) sequence parser and command handling.
5 */
6
7#ifndef GHOSTTY_VT_OSC_H
8#define GHOSTTY_VT_OSC_H
9
10#include <stdbool.h>
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/types.h>
14#include <ghostty/vt/allocator.h>
15
16/** @defgroup osc OSC Parser
17 *
18 * OSC (Operating System Command) sequence parser and command handling.
19 *
20 * The parser operates in a streaming fashion, processing input byte-by-byte
21 * to handle OSC sequences that may arrive in fragments across multiple reads.
22 * This interface makes it easy to integrate into most environments and avoids
23 * over-allocating buffers.
24 *
25 * ## Basic Usage
26 *
27 * 1. Create a parser instance with ghostty_osc_new()
28 * 2. Feed bytes to the parser using ghostty_osc_next()
29 * 3. Finalize parsing with ghostty_osc_end() to get the command
30 * 4. Query command type and extract data using ghostty_osc_command_type()
31 * and ghostty_osc_command_data()
32 * 5. Free the parser with ghostty_osc_free() when done
33 *
34 * @{
35 */
36
37/**
38 * OSC command types.
39 *
40 * @ingroup osc
41 */
42typedef enum GHOSTTY_ENUM_TYPED {
43 GHOSTTY_OSC_COMMAND_INVALID = 0,
44 GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1,
45 GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2,
46 GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3,
47 GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4,
48 GHOSTTY_OSC_COMMAND_REPORT_PWD = 5,
49 GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6,
50 GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7,
51 GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8,
52 GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9,
53 GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10,
54 GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11,
55 GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12,
56 GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13,
57 GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14,
58 GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15,
59 GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16,
60 GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17,
61 GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18,
62 GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19,
63 GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20,
64 GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21,
65 GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22,
66 GHOSTTY_OSC_COMMAND_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
67} GhosttyOscCommandType;
68
69/**
70 * OSC command data types.
71 *
72 * These values specify what type of data to extract from an OSC command
73 * using `ghostty_osc_command_data`.
74 *
75 * @ingroup osc
76 */
77typedef enum GHOSTTY_ENUM_TYPED {
78 /** Invalid data type. Never results in any data extraction. */
79 GHOSTTY_OSC_DATA_INVALID = 0,
80
81 /**
82 * Window title string data.
83 *
84 * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE
85 *
86 * Output type: const char ** (pointer to null-terminated string)
87 *
88 * Lifetime: Valid until the next call to any ghostty_osc_* function with
89 * the same parser instance. Memory is owned by the parser.
90 */
91 GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1,
92 GHOSTTY_OSC_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
93} GhosttyOscCommandData;
94
95/**
96 * Create a new OSC parser instance.
97 *
98 * Creates a new OSC (Operating System Command) parser using the provided
99 * allocator. The parser must be freed using ghostty_vt_osc_free() when
100 * no longer needed.
101 *
102 * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
103 * @param parser Pointer to store the created parser handle
104 * @return GHOSTTY_SUCCESS on success, or an error code on failure
105 *
106 * @ingroup osc
107 */
108GHOSTTY_API GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser);
109
110/**
111 * Free an OSC parser instance.
112 *
113 * Releases all resources associated with the OSC parser. After this call,
114 * the parser handle becomes invalid and must not be used.
115 *
116 * @param parser The parser handle to free (may be NULL)
117 *
118 * @ingroup osc
119 */
120GHOSTTY_API void ghostty_osc_free(GhosttyOscParser parser);
121
122/**
123 * Reset an OSC parser instance to its initial state.
124 *
125 * Resets the parser state, clearing any partially parsed OSC sequences
126 * and returning the parser to its initial state. This is useful for
127 * reusing a parser instance or recovering from parse errors.
128 *
129 * @param parser The parser handle to reset, must not be null.
130 *
131 * @ingroup osc
132 */
133GHOSTTY_API void ghostty_osc_reset(GhosttyOscParser parser);
134
135/**
136 * Parse the next byte in an OSC sequence.
137 *
138 * Processes a single byte as part of an OSC sequence. The parser maintains
139 * internal state to track the progress through the sequence. Call this
140 * function for each byte in the sequence data.
141 *
142 * When finished pumping the parser with bytes, call ghostty_osc_end
143 * to get the final result.
144 *
145 * @param parser The parser handle, must not be null.
146 * @param byte The next byte to parse
147 *
148 * @ingroup osc
149 */
150GHOSTTY_API void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte);
151
152/**
153 * Finalize OSC parsing and retrieve the parsed command.
154 *
155 * Call this function after feeding all bytes of an OSC sequence to the parser
156 * using ghostty_osc_next() with the exception of the terminating character
157 * (ESC or ST). This function finalizes the parsing process and returns the
158 * parsed OSC command.
159 *
160 * The return value is never NULL. Invalid commands will return a command
161 * with type GHOSTTY_OSC_COMMAND_INVALID.
162 *
163 * The terminator parameter specifies the byte that terminated the OSC sequence
164 * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is
165 * preserved in the parsed command so that responses can use the same terminator
166 * format for better compatibility with the calling program. For commands that
167 * do not require a response, this parameter is ignored and the resulting
168 * command will not retain the terminator information.
169 *
170 * The returned command handle is valid until the next call to any
171 * `ghostty_osc_*` function with the same parser instance with the exception
172 * of command introspection functions such as `ghostty_osc_command_type`.
173 *
174 * @param parser The parser handle, must not be null.
175 * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST)
176 * @return Handle to the parsed OSC command
177 *
178 * @ingroup osc
179 */
180GHOSTTY_API GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator);
181
182/**
183 * Get the type of an OSC command.
184 *
185 * Returns the type identifier for the given OSC command. This can be used
186 * to determine what kind of command was parsed and what data might be
187 * available from it.
188 *
189 * @param command The OSC command handle to query (may be NULL)
190 * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL
191 *
192 * @ingroup osc
193 */
194GHOSTTY_API GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command);
195
196/**
197 * Extract data from an OSC command.
198 *
199 * Extracts typed data from the given OSC command based on the specified
200 * data type. The output pointer must be of the appropriate type for the
201 * requested data kind. Valid command types, output types, and memory
202 * safety information are documented in the `GhosttyOscCommandData` enum.
203 *
204 * @param command The OSC command handle to query (may be NULL)
205 * @param data The type of data to extract
206 * @param out Pointer to store the extracted data (type depends on data parameter)
207 * @return true if data extraction was successful, false otherwise
208 *
209 * @ingroup osc
210 */
211GHOSTTY_API bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out);
212
213/** @} */
214
215#endif /* GHOSTTY_VT_OSC_H */
packages/terminal/vendor/include/ghostty/vt/paste.h created+101
......@@ -0,0 +1,101 @@
1/**
2 * @file paste.h
3 *
4 * Paste utilities - validate and encode paste data for terminal input.
5 */
6
7#ifndef GHOSTTY_VT_PASTE_H
8#define GHOSTTY_VT_PASTE_H
9
10/** @defgroup paste Paste Utilities
11 *
12 * Utilities for validating and encoding paste data for terminal input.
13 *
14 * ## Basic Usage
15 *
16 * Use ghostty_paste_is_safe() to check if paste data contains potentially
17 * dangerous sequences before sending it to the terminal.
18 *
19 * Use ghostty_paste_encode() to encode paste data for writing to the pty,
20 * including bracketed paste wrapping and unsafe byte stripping.
21 *
22 * ## Examples
23 *
24 * ### Safety Check
25 *
26 * @snippet c-vt-paste/src/main.c paste-safety
27 *
28 * ### Encoding
29 *
30 * @snippet c-vt-paste/src/main.c paste-encode
31 *
32 * @{
33 */
34
35#include <stdbool.h>
36#include <stddef.h>
37#include <ghostty/vt/types.h>
38
39#ifdef __cplusplus
40extern "C" {
41#endif
42
43/**
44 * Check if paste data is safe to paste into the terminal.
45 *
46 * Data is considered unsafe if it contains:
47 * - Newlines (`\n`) which can inject commands
48 * - The bracketed paste end sequence (`\x1b[201~`) which can be used
49 * to exit bracketed paste mode and inject commands
50 *
51 * This check is conservative and considers data unsafe regardless of
52 * current terminal state.
53 *
54 * @param data The paste data to check (must not be NULL)
55 * @param len The length of the data in bytes
56 * @return true if the data is safe to paste, false otherwise
57 */
58GHOSTTY_API bool ghostty_paste_is_safe(const char* data, size_t len);
59
60/**
61 * Encode paste data for writing to the terminal pty.
62 *
63 * This function prepares paste data for terminal input by:
64 * - Stripping unsafe control bytes (NUL, ESC, DEL, etc.) by replacing
65 * them with spaces
66 * - Wrapping the data in bracketed paste sequences if @p bracketed is true
67 * - Replacing newlines with carriage returns if @p bracketed is false
68 *
69 * The input @p data buffer is modified in place during encoding. The
70 * encoded result (potentially with bracketed paste prefix/suffix) is
71 * written to the output buffer.
72 *
73 * If the output buffer is too small, the function returns
74 * GHOSTTY_OUT_OF_SPACE and sets the required size in @p out_written.
75 * The caller can then retry with a sufficiently sized buffer.
76 *
77 * @param data The paste data to encode (modified in place, may be NULL)
78 * @param data_len The length of the input data in bytes
79 * @param bracketed Whether bracketed paste mode is active
80 * @param buf Output buffer to write the encoded result into (may be NULL)
81 * @param buf_len Size of the output buffer in bytes
82 * @param[out] out_written On success, the number of bytes written. On
83 * GHOSTTY_OUT_OF_SPACE, the required buffer size.
84 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer
85 * is too small
86 */
87GHOSTTY_API GhosttyResult ghostty_paste_encode(
88 char* data,
89 size_t data_len,
90 bool bracketed,
91 char* buf,
92 size_t buf_len,
93 size_t* out_written);
94
95#ifdef __cplusplus
96}
97#endif
98
99/** @} */
100
101#endif /* GHOSTTY_VT_PASTE_H */
packages/terminal/vendor/include/ghostty/vt/point.h created+89
......@@ -0,0 +1,89 @@
1/**
2 * @file point.h
3 *
4 * Terminal point types for referencing locations in the terminal grid.
5 */
6
7#ifndef GHOSTTY_VT_POINT_H
8#define GHOSTTY_VT_POINT_H
9
10#include <stdint.h>
11
12#ifdef __cplusplus
13extern "C" {
14#endif
15
16/** @defgroup point Point
17 *
18 * Types for referencing x/y positions in the terminal grid under
19 * different coordinate systems (active area, viewport, full screen,
20 * scrollback history).
21 *
22 * @{
23 */
24
25/**
26 * A coordinate in the terminal grid.
27 *
28 * @ingroup point
29 */
30typedef struct {
31 /** Column (0-indexed). */
32 uint16_t x;
33
34 /** Row (0-indexed). May exceed page size for screen/history tags. */
35 uint32_t y;
36} GhosttyPointCoordinate;
37
38/**
39 * Point reference tag.
40 *
41 * Determines which coordinate system a point uses.
42 *
43 * @ingroup point
44 */
45typedef enum GHOSTTY_ENUM_TYPED {
46 /** Active area where the cursor can move. */
47 GHOSTTY_POINT_TAG_ACTIVE = 0,
48
49 /** Visible viewport (changes when scrolled). */
50 GHOSTTY_POINT_TAG_VIEWPORT = 1,
51
52 /** Full screen including scrollback. */
53 GHOSTTY_POINT_TAG_SCREEN = 2,
54
55 /** Scrollback history only (before active area). */
56 GHOSTTY_POINT_TAG_HISTORY = 3,
57 GHOSTTY_POINT_TAG_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
58 } GhosttyPointTag;
59
60/**
61 * Point value union.
62 *
63 * @ingroup point
64 */
65typedef union {
66 /** Coordinate (used for all tag variants). */
67 GhosttyPointCoordinate coordinate;
68
69 /** Padding for ABI compatibility. Do not use. */
70 uint64_t _padding[2];
71} GhosttyPointValue;
72
73/**
74 * Tagged union for a point in the terminal grid.
75 *
76 * @ingroup point
77 */
78typedef struct {
79 GhosttyPointTag tag;
80 GhosttyPointValue value;
81} GhosttyPoint;
82
83/** @} */
84
85#ifdef __cplusplus
86}
87#endif
88
89#endif /* GHOSTTY_VT_POINT_H */
packages/terminal/vendor/include/ghostty/vt/render.h created+811
......@@ -0,0 +1,811 @@
1/**
2 * @file render.h
3 *
4 * Render state for creating high performance renderers.
5 */
6
7#ifndef GHOSTTY_VT_RENDER_H
8#define GHOSTTY_VT_RENDER_H
9
10#include <stdbool.h>
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/allocator.h>
14#include <ghostty/vt/color.h>
15#include <ghostty/vt/terminal.h>
16#include <ghostty/vt/types.h>
17
18#ifdef __cplusplus
19extern "C" {
20#endif
21
22/** @defgroup render Render State
23 *
24 * Represents the state required to render a visible screen (a viewport)
25 * of a terminal instance. This is stateful and optimized for repeated
26 * updates from a single terminal instance and only updating dirty regions
27 * of the screen.
28 *
29 * The key design principle of this API is that it only needs read/write
30 * access to the terminal instance during the update call. This allows
31 * the render state to minimally impact terminal IO performance and also
32 * allows the renderer to be safely multi-threaded (as long as a lock is
33 * held during the update call to ensure exclusive access to the terminal
34 * instance).
35 *
36 * The basic usage of this API is:
37 *
38 * 1. Create an empty render state
39 * 2. Update it from a terminal instance whenever you need.
40 * 3. Read from the render state to get the data needed to draw your frame.
41 *
42 * ## Two-Phase Updates
43 *
44 * For callers that synchronize terminal access (e.g. a renderer thread
45 * sharing a lock with an IO thread), the update can be split into two
46 * phases to minimize the time the terminal must be held exclusively:
47 * ghostty_render_state_begin_update requires terminal access, while
48 * ghostty_render_state_end_update completes any deferred work using only
49 * memory owned by the render state. A typical renderer would lock, begin
50 * the update, unlock, and then end the update while the IO thread is free
51 * to continue modifying the terminal. ghostty_render_state_update is a
52 * convenience that performs both phases in one call.
53 *
54 * ## Dirty Tracking
55 *
56 * Dirty tracking is a key feature of the render state that allows renderers
57 * to efficiently determine what parts of the screen have changed and only
58 * redraw changed regions.
59 *
60 * The render state API keeps track of dirty state at two independent layers:
61 * a global dirty state that indicates whether the entire frame is clean,
62 * partially dirty, or fully dirty, and a per-row dirty state that allows
63 * tracking which rows in a partially dirty frame have changed.
64 *
65 * The user of the render state API is expected to unset both of these.
66 * The `update` call does not unset dirty state, it only updates it.
67 *
68 * An extremely important detail: setting one dirty state doesn't unset
69 * the other. For example, setting the global dirty state to false does not
70 * reset the row-level dirty flags. So, the caller of the render state API must
71 * be careful to manage both layers of dirty state correctly.
72 *
73 * ## Examples
74 *
75 * ### Creating and updating render state
76 * @snippet c-vt-render/src/main.c render-state-update
77 *
78 * ### Checking dirty state
79 * @snippet c-vt-render/src/main.c render-dirty-check
80 *
81 * ### Reading colors
82 * @snippet c-vt-render/src/main.c render-colors
83 *
84 * ### Reading cursor state
85 * @snippet c-vt-render/src/main.c render-cursor
86 *
87 * ### Iterating rows and cells
88 * @snippet c-vt-render/src/main.c render-row-iterate
89 *
90 * ### Resetting dirty state after rendering
91 * @snippet c-vt-render/src/main.c render-dirty-reset
92 *
93 * @{
94 */
95
96/**
97 * Dirty state of a render state after update.
98 *
99 * @ingroup render
100 */
101typedef enum GHOSTTY_ENUM_TYPED {
102 /** Not dirty at all; rendering can be skipped. */
103 GHOSTTY_RENDER_STATE_DIRTY_FALSE = 0,
104
105 /** Some rows changed; renderer can redraw incrementally. */
106 GHOSTTY_RENDER_STATE_DIRTY_PARTIAL = 1,
107
108 /** Global state changed; renderer should redraw everything. */
109 GHOSTTY_RENDER_STATE_DIRTY_FULL = 2,
110 GHOSTTY_RENDER_STATE_DIRTY_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
111} GhosttyRenderStateDirty;
112
113/**
114 * Visual style of the cursor.
115 *
116 * @ingroup render
117 */
118typedef enum GHOSTTY_ENUM_TYPED {
119 /** Bar cursor (DECSCUSR 5, 6). */
120 GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR = 0,
121
122 /** Block cursor (DECSCUSR 1, 2). */
123 GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK = 1,
124
125 /** Underline cursor (DECSCUSR 3, 4). */
126 GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE = 2,
127
128 /** Hollow block cursor. */
129 GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW = 3,
130 GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
131} GhosttyRenderStateCursorVisualStyle;
132
133/**
134 * Queryable data kinds for ghostty_render_state_get().
135 *
136 * @ingroup render
137 */
138typedef enum GHOSTTY_ENUM_TYPED {
139 /** Invalid / sentinel value. */
140 GHOSTTY_RENDER_STATE_DATA_INVALID = 0,
141
142 /** Viewport width in cells (uint16_t). */
143 GHOSTTY_RENDER_STATE_DATA_COLS = 1,
144
145 /** Viewport height in cells (uint16_t). */
146 GHOSTTY_RENDER_STATE_DATA_ROWS = 2,
147
148 /** Current dirty state (GhosttyRenderStateDirty). */
149 GHOSTTY_RENDER_STATE_DATA_DIRTY = 3,
150
151 /** Populate a pre-allocated GhosttyRenderStateRowIterator with row data
152 * from the render state (GhosttyRenderStateRowIterator). Row data is
153 * only valid as long as the underlying render state is not updated.
154 * It is unsafe to use row data after updating the render state.
155 * */
156 GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR = 4,
157
158 /** Default/current background color (GhosttyColorRgb). */
159 GHOSTTY_RENDER_STATE_DATA_COLOR_BACKGROUND = 5,
160
161 /** Default/current foreground color (GhosttyColorRgb). */
162 GHOSTTY_RENDER_STATE_DATA_COLOR_FOREGROUND = 6,
163
164 /** Cursor color when explicitly set by terminal state (GhosttyColorRgb).
165 * Returns GHOSTTY_INVALID_VALUE if no explicit cursor color is set;
166 * use COLOR_CURSOR_HAS_VALUE to check first. */
167 GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR = 7,
168
169 /** Whether an explicit cursor color is set (bool). */
170 GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE = 8,
171
172 /** The active 256-color palette (GhosttyColorRgb[256]). */
173 GHOSTTY_RENDER_STATE_DATA_COLOR_PALETTE = 9,
174
175 /** The visual style of the cursor (GhosttyRenderStateCursorVisualStyle). */
176 GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE = 10,
177
178 /** Whether the cursor is visible based on terminal modes (bool). */
179 GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE = 11,
180
181 /** Whether the cursor should blink based on terminal modes (bool). */
182 GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING = 12,
183
184 /** Whether the cursor is at a password input field (bool). */
185 GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT = 13,
186
187 /** Whether the cursor is visible within the viewport (bool).
188 * If false, the cursor viewport position values are undefined. */
189 GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE = 14,
190
191 /** Cursor viewport x position in cells (uint16_t).
192 * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */
193 GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X = 15,
194
195 /** Cursor viewport y position in cells (uint16_t).
196 * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */
197 GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y = 16,
198
199 /** Whether the cursor is on the tail of a wide character (bool).
200 * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */
201 GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL = 17,
202 GHOSTTY_RENDER_STATE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
203} GhosttyRenderStateData;
204
205/**
206 * Settable options for ghostty_render_state_set().
207 *
208 * @ingroup render
209 */
210typedef enum GHOSTTY_ENUM_TYPED {
211 /** Set dirty state (GhosttyRenderStateDirty). */
212 GHOSTTY_RENDER_STATE_OPTION_DIRTY = 0,
213 GHOSTTY_RENDER_STATE_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
214} GhosttyRenderStateOption;
215
216/**
217 * Queryable data kinds for ghostty_render_state_row_get().
218 *
219 * @ingroup render
220 */
221typedef enum GHOSTTY_ENUM_TYPED {
222 /** Invalid / sentinel value. */
223 GHOSTTY_RENDER_STATE_ROW_DATA_INVALID = 0,
224
225 /** Whether the current row is dirty (bool). */
226 GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY = 1,
227
228 /** The raw row value (GhosttyRow). */
229 GHOSTTY_RENDER_STATE_ROW_DATA_RAW = 2,
230
231 /** Populate a pre-allocated GhosttyRenderStateRowCells with cell data for
232 * the current row (GhosttyRenderStateRowCells). Cell data is only
233 * valid as long as the underlying render state is not updated.
234 * It is unsafe to use cell data after updating the render state. */
235 GHOSTTY_RENDER_STATE_ROW_DATA_CELLS = 3,
236
237 /** Row-local selected cell range (GhosttyRenderStateRowSelection). */
238 GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION = 4,
239 GHOSTTY_RENDER_STATE_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
240} GhosttyRenderStateRowData;
241
242/**
243 * Settable options for ghostty_render_state_row_set().
244 *
245 * @ingroup render
246 */
247typedef enum GHOSTTY_ENUM_TYPED {
248 /** Set dirty state for the current row (bool). */
249 GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY = 0,
250 GHOSTTY_RENDER_STATE_ROW_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
251} GhosttyRenderStateRowOption;
252
253/**
254 * Row-local selection range.
255 *
256 * This struct uses the sized-struct ABI pattern. Initialize with
257 * GHOSTTY_INIT_SIZED(GhosttyRenderStateRowSelection) before querying
258 * GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION.
259 *
260 * Querying GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION returns GHOSTTY_NO_VALUE
261 * if the current row does not intersect the current selection.
262 *
263 * @ingroup render
264 */
265typedef struct {
266 /** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateRowSelection). */
267 size_t size;
268
269 /** Start column of the row-local selection range, inclusive. */
270 uint16_t start_x;
271
272 /** End column of the row-local selection range, inclusive. */
273 uint16_t end_x;
274} GhosttyRenderStateRowSelection;
275
276/** Narrow ASCII cell returned by the optional whole-row render fast path. */
277typedef struct {
278 uint32_t codepoint;
279 uint32_t foreground;
280} GhosttyRenderStateAsciiCell;
281
282/**
283 * Render-state color information.
284 *
285 * This struct uses the sized-struct ABI pattern. Initialize with
286 * GHOSTTY_INIT_SIZED(GhosttyRenderStateColors) before calling
287 * ghostty_render_state_colors_get().
288 *
289 * Example:
290 * @code
291 * GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors);
292 * GhosttyResult result = ghostty_render_state_colors_get(state, &colors);
293 * @endcode
294 *
295 * @ingroup render
296 */
297typedef struct {
298 /** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateColors). */
299 size_t size;
300
301 /** The default/current background color for the render state. */
302 GhosttyColorRgb background;
303
304 /** The default/current foreground color for the render state. */
305 GhosttyColorRgb foreground;
306
307 /** The cursor color when explicitly set by terminal state. */
308 GhosttyColorRgb cursor;
309
310 /**
311 * True when cursor contains a valid explicit cursor color value.
312 * If this is false, the cursor color should be ignored; it will
313 * contain undefined data.
314 * */
315 bool cursor_has_value;
316
317 /** The active 256-color palette for this render state. */
318 GhosttyColorRgb palette[256];
319} GhosttyRenderStateColors;
320
321/**
322 * Create a new render state instance.
323 *
324 * @param allocator Pointer to allocator, or NULL to use the default allocator
325 * @param state Pointer to store the created render state handle
326 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
327 * failure
328 *
329 * @ingroup render
330 */
331GHOSTTY_API GhosttyResult ghostty_render_state_new(const GhosttyAllocator* allocator,
332 GhosttyRenderState* state);
333
334/**
335 * Free a render state instance.
336 *
337 * Releases all resources associated with the render state. After this call,
338 * the render state handle becomes invalid.
339 *
340 * @param state The render state handle to free (may be NULL)
341 *
342 * @ingroup render
343 */
344GHOSTTY_API void ghostty_render_state_free(GhosttyRenderState state);
345
346/**
347 * Update a render state instance from a terminal.
348 *
349 * This consumes terminal/screen dirty state in the same way as the internal
350 * render state update path.
351 *
352 * This is a convenience function that performs a full update in one call,
353 * equivalent to ghostty_render_state_begin_update immediately followed by
354 * ghostty_render_state_end_update. Callers that hold a lock over the
355 * terminal state should prefer calling the two phases directly so that the
356 * lock is only held for the begin phase.
357 *
358 * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
359 * @param terminal The terminal handle to read from (NULL returns GHOSTTY_INVALID_VALUE)
360 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
361 * `terminal` is NULL, GHOSTTY_OUT_OF_MEMORY if updating the state requires
362 * allocation and that allocation fails
363 *
364 * @ingroup render
365 */
366GHOSTTY_API GhosttyResult ghostty_render_state_update(GhosttyRenderState state,
367 GhosttyTerminal terminal);
368
369/**
370 * Begin an update of a render state instance from a terminal.
371 *
372 * Every begin must be completed with a ghostty_render_state_end_update call
373 * before the render state is read.
374 *
375 * This two-phase structure exists for callers that synchronize access to the
376 * terminal state (e.g. with a lock shared with an IO thread): only this
377 * function requires terminal access, so a caller can hold its lock for this
378 * call only and then call ghostty_render_state_end_update after releasing
379 * it. The end phase exclusively reads and writes memory owned by the render
380 * state, so it is safe to call while the terminal is being modified.
381 *
382 * Work that doesn't require terminal access may be deferred to the end phase
383 * to keep this call (and therefore lock hold time) as short as possible.
384 * Callers must treat the render state as incomplete until
385 * ghostty_render_state_end_update is called.
386 *
387 * This consumes terminal/screen dirty state in the same way as the internal
388 * render state update path.
389 *
390 * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
391 * @param terminal The terminal handle to read from (NULL returns GHOSTTY_INVALID_VALUE)
392 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
393 * `terminal` is NULL, GHOSTTY_OUT_OF_MEMORY if updating the state requires
394 * allocation and that allocation fails
395 *
396 * @ingroup render
397 */
398GHOSTTY_API GhosttyResult ghostty_render_state_begin_update(GhosttyRenderState state,
399 GhosttyTerminal terminal);
400
401/**
402 * Complete a prior ghostty_render_state_begin_update call by performing any
403 * deferred work.
404 *
405 * This only reads and writes memory owned by the render state, so it is safe
406 * to call while the terminal is being modified (no terminal synchronization
407 * is required). Calling this without a prior begin is a safe no-op.
408 *
409 * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
410 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is
411 * NULL
412 *
413 * @ingroup render
414 */
415GHOSTTY_API GhosttyResult ghostty_render_state_end_update(GhosttyRenderState state);
416
417/**
418 * Get a value from a render state.
419 *
420 * The `out` pointer must point to a value of the type corresponding to the
421 * requested data kind (see GhosttyRenderStateData).
422 *
423 * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
424 * @param data The data kind to query
425 * @param[out] out Pointer to receive the queried value
426 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is
427 * NULL or `data` is not a recognized enum value
428 *
429 * @ingroup render
430 */
431GHOSTTY_API GhosttyResult ghostty_render_state_get(GhosttyRenderState state,
432 GhosttyRenderStateData data,
433 void* out);
434
435/**
436 * Get multiple data fields from a render state in a single call.
437 *
438 * Each element in the keys array specifies a data kind, and the
439 * corresponding element in the values array receives the result.
440 *
441 * Processing stops at the first error; on success out_written
442 * is set to count, on error it is set to the index of the
443 * failing key (i.e. the number of values successfully written).
444 *
445 * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
446 * @param count Number of key/value pairs
447 * @param keys Array of data kinds to query
448 * @param values Array of output pointers (types must match each key's
449 * documented output type)
450 * @param[out] out_written On return, receives the number of values
451 * successfully written (may be NULL)
452 * @return GHOSTTY_SUCCESS if all queries succeed
453 *
454 * @ingroup render
455 */
456GHOSTTY_API GhosttyResult ghostty_render_state_get_multi(
457 GhosttyRenderState state,
458 size_t count,
459 const GhosttyRenderStateData* keys,
460 void** values,
461 size_t* out_written);
462
463/**
464 * Set an option on a render state.
465 *
466 * The `value` pointer must point to a value of the type corresponding to the
467 * requested option kind (see GhosttyRenderStateOption).
468 *
469 * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
470 * @param option The option to set
471 * @param[in] value Pointer to the value to set (NULL returns
472 * GHOSTTY_INVALID_VALUE)
473 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
474 * `value` is NULL
475 *
476 * @ingroup render
477 */
478GHOSTTY_API GhosttyResult ghostty_render_state_set(GhosttyRenderState state,
479 GhosttyRenderStateOption option,
480 const void* value);
481
482/**
483 * Get the current color information from a render state.
484 *
485 * This writes as many fields as fit in the caller-provided sized struct.
486 * `out_colors->size` must be set by the caller (typically via
487 * GHOSTTY_INIT_SIZED(GhosttyRenderStateColors)).
488 *
489 * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
490 * @param[out] out_colors Sized output struct to receive render-state colors
491 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
492 * `out_colors` is NULL, or if `out_colors->size` is smaller than
493 * `sizeof(size_t)`
494 *
495 * @ingroup render
496 */
497GHOSTTY_API GhosttyResult ghostty_render_state_colors_get(GhosttyRenderState state,
498 GhosttyRenderStateColors* out_colors);
499
500/**
501 * Create a new row iterator instance.
502 *
503 * All fields except the allocator are left undefined until populated
504 * via ghostty_render_state_get() with
505 * GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR.
506 *
507 * @param allocator Pointer to allocator, or NULL to use the default allocator
508 * @param[out] out_iterator On success, receives the created iterator handle
509 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
510 * failure
511 *
512 * @ingroup render
513 */
514GHOSTTY_API GhosttyResult ghostty_render_state_row_iterator_new(
515 const GhosttyAllocator* allocator,
516 GhosttyRenderStateRowIterator* out_iterator);
517
518/**
519 * Free a render-state row iterator.
520 *
521 * @param iterator The iterator handle to free (may be NULL)
522 *
523 * @ingroup render
524 */
525GHOSTTY_API void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIterator iterator);
526
527/**
528 * Move a render-state row iterator to the next row.
529 *
530 * Returns true if the iterator moved successfully and row data is
531 * available to read at the new position.
532 *
533 * @param iterator The iterator handle to advance (may be NULL)
534 * @return true if advanced to the next row, false if `iterator` is
535 * NULL or if the iterator has reached the end
536 *
537 * @ingroup render
538 */
539GHOSTTY_API bool ghostty_render_state_row_iterator_next(GhosttyRenderStateRowIterator iterator);
540
541/**
542 * Get a value from the current row in a render-state row iterator.
543 *
544 * The `out` pointer must point to a value of the type corresponding to the
545 * requested data kind (see GhosttyRenderStateRowData).
546 * Call ghostty_render_state_row_iterator_next() at least once before
547 * calling this function.
548 *
549 * @param iterator The iterator handle to query (NULL returns GHOSTTY_INVALID_VALUE)
550 * @param data The data kind to query
551 * @param[out] out Pointer to receive the queried value
552 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if
553 * `iterator` is NULL or the iterator is not positioned on a row
554 *
555 * @ingroup render
556 */
557GHOSTTY_API GhosttyResult ghostty_render_state_row_get(
558 GhosttyRenderStateRowIterator iterator,
559 GhosttyRenderStateRowData data,
560 void* out);
561
562/**
563 * Get multiple data fields from the current row in a single call.
564 *
565 * Each element in the keys array specifies a data kind, and the
566 * corresponding element in the values array receives the result.
567 *
568 * Processing stops at the first error; on success out_written
569 * is set to count, on error it is set to the index of the
570 * failing key (i.e. the number of values successfully written).
571 *
572 * @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE)
573 * @param count Number of key/value pairs
574 * @param keys Array of data kinds to query
575 * @param values Array of output pointers (types must match each key's
576 * documented output type)
577 * @param[out] out_written On return, receives the number of values
578 * successfully written (may be NULL)
579 * @return GHOSTTY_SUCCESS if all queries succeed
580 *
581 * @ingroup render
582 */
583GHOSTTY_API GhosttyResult ghostty_render_state_row_get_multi(
584 GhosttyRenderStateRowIterator iterator,
585 size_t count,
586 const GhosttyRenderStateRowData* keys,
587 void** values,
588 size_t* out_written);
589
590/**
591 * Set an option on the current row in a render-state row iterator.
592 *
593 * The `value` pointer must point to a value of the type corresponding to the
594 * requested option kind (see GhosttyRenderStateRowOption).
595 * Call ghostty_render_state_row_iterator_next() at least once before
596 * calling this function.
597 *
598 * @param iterator The iterator handle to update (NULL returns GHOSTTY_INVALID_VALUE)
599 * @param option The option to set
600 * @param[in] value Pointer to the value to set (NULL returns
601 * GHOSTTY_INVALID_VALUE)
602 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if
603 * `iterator` is NULL or the iterator is not positioned on a row
604 *
605 * @ingroup render
606 */
607GHOSTTY_API GhosttyResult ghostty_render_state_row_set(
608 GhosttyRenderStateRowIterator iterator,
609 GhosttyRenderStateRowOption option,
610 const void* value);
611
612/**
613 * Create a new row cells instance.
614 *
615 * All fields except the allocator are left undefined until populated
616 * via ghostty_render_state_row_get() with
617 * GHOSTTY_RENDER_STATE_ROW_DATA_CELLS.
618 *
619 * You can reuse this value repeatedly with ghostty_render_state_row_get() to
620 * avoid allocating a new cells container for every row.
621 *
622 * @param allocator Pointer to allocator, or NULL to use the default allocator
623 * @param[out] out_cells On success, receives the created row cells handle
624 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
625 * failure
626 *
627 * @ingroup render
628 */
629GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_new(
630 const GhosttyAllocator* allocator,
631 GhosttyRenderStateRowCells* out_cells);
632
633/**
634 * Queryable data kinds for ghostty_render_state_row_cells_get().
635 *
636 * @ingroup render
637 */
638typedef enum GHOSTTY_ENUM_TYPED {
639 /** Invalid / sentinel value. */
640 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_INVALID = 0,
641
642 /** The raw cell value (GhosttyCell). */
643 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW = 1,
644
645 /** The style for the current cell (GhosttyStyle). */
646 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE = 2,
647
648 /** The total number of grapheme codepoints including the base codepoint
649 * (uint32_t). Returns 0 if the cell has no text. */
650 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN = 3,
651
652 /** Write grapheme codepoints into a caller-provided buffer (uint32_t*).
653 * The buffer must be at least graphemes_len elements. The base codepoint
654 * is written first, followed by any extra codepoints. */
655 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4,
656
657 /** The resolved background color of the cell (GhosttyColorRgb).
658 * Flattens the three possible sources: content-tag bg_color_rgb,
659 * content-tag bg_color_palette (looked up in the palette), or the
660 * style's bg_color. Returns GHOSTTY_INVALID_VALUE if the cell has
661 * no background color, in which case the caller should use whatever
662 * default background color it wants (e.g. the terminal background). */
663 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR = 5,
664
665 /** The resolved foreground color of the cell (GhosttyColorRgb).
666 * Resolves palette indices through the palette. Bold color handling
667 * is not applied; the caller should handle bold styling separately.
668 * Returns GHOSTTY_INVALID_VALUE if the cell has no explicit foreground
669 * color, in which case the caller should use whatever default foreground
670 * color it wants (e.g. the terminal foreground). */
671 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR = 6,
672
673 /** Whether the cell is contained within the current selection (bool).
674 * This returns true when the cell's column is within the current row's
675 * row-local selection range, and false otherwise. Rendering policy for
676 * selected cells (colors, inversion, etc.) is left to the caller.
677 *
678 * Renderers that can draw cells in spans may be more efficient querying
679 * GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION once per row and applying that
680 * range directly, avoiding one C API call per cell for selection state. */
681 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_SELECTED = 7,
682
683 /** Whether the cell has any explicit styling (bool).
684 * This is equivalent to querying the raw cell's
685 * GHOSTTY_CELL_DATA_HAS_STYLING value, but avoids materializing the raw
686 * GhosttyCell for renderers that only need to know whether fetching the
687 * full style is necessary. */
688 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_HAS_STYLING = 8,
689
690 /**
691 * Encode the current cell's full grapheme cluster as UTF-8 into a
692 * caller-provided buffer (GhosttyBuffer).
693 *
694 * The base codepoint is encoded first, followed by any extra grapheme
695 * codepoints. Returns GHOSTTY_SUCCESS with len=0 when the cell has no text.
696 *
697 * If ptr is NULL or cap is too small for a non-empty cell, returns
698 * GHOSTTY_OUT_OF_SPACE without writing any bytes and sets len to the required
699 * buffer size in bytes.
700 */
701 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8 = 9,
702 GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
703} GhosttyRenderStateRowCellsData;
704
705/**
706 * Move a render-state row cells iterator to the next cell.
707 *
708 * Returns true if the iterator moved successfully and cell data is
709 * available to read at the new position.
710 *
711 * @param cells The row cells handle to advance (may be NULL)
712 * @return true if advanced to the next cell, false if `cells` is
713 * NULL or if the iterator has reached the end
714 *
715 * @ingroup render
716 */
717GHOSTTY_API bool ghostty_render_state_row_cells_next(GhosttyRenderStateRowCells cells);
718
719/**
720 * Move a render-state row cells iterator to a specific column.
721 *
722 * Positions the iterator at the given x (column) index so that
723 * subsequent reads return data for that cell.
724 *
725 * @param cells The row cells handle to reposition (NULL returns
726 * GHOSTTY_INVALID_VALUE)
727 * @param x The zero-based column index to select
728 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `cells`
729 * is NULL or `x` is out of range
730 *
731 * @ingroup render
732 */
733GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_select(
734 GhosttyRenderStateRowCells cells, uint16_t x);
735
736/**
737 * Get a value from the current cell in a render-state row cells iterator.
738 *
739 * The `out` pointer must point to a value of the type corresponding to the
740 * requested data kind (see GhosttyRenderStateRowCellsData).
741 * Call ghostty_render_state_row_cells_next() or
742 * ghostty_render_state_row_cells_select() at least once before
743 * calling this function.
744 *
745 * @param cells The row cells handle to query (NULL returns GHOSTTY_INVALID_VALUE)
746 * @param data The data kind to query
747 * @param[out] out Pointer to receive the queried value
748 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if
749 * `cells` is NULL or the iterator is not positioned on a cell
750 *
751 * @ingroup render
752 */
753GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_get(
754 GhosttyRenderStateRowCells cells,
755 GhosttyRenderStateRowCellsData data,
756 void* out);
757
758/**
759 * Get multiple data fields from the current cell in a single call.
760 *
761 * Each element in the keys array specifies a data kind, and the
762 * corresponding element in the values array receives the result.
763 *
764 * Processing stops at the first error; on success out_written
765 * is set to count, on error it is set to the index of the
766 * failing key (i.e. the number of values successfully written).
767 *
768 * @param cells The row cells handle (NULL returns GHOSTTY_INVALID_VALUE)
769 * @param count Number of key/value pairs
770 * @param keys Array of data kinds to query
771 * @param values Array of output pointers (types must match each key's
772 * documented output type)
773 * @param[out] out_written On return, receives the number of values
774 * successfully written (may be NULL)
775 * @return GHOSTTY_SUCCESS if all queries succeed
776 *
777 * @ingroup render
778 */
779GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_get_multi(
780 GhosttyRenderStateRowCells cells,
781 size_t count,
782 const GhosttyRenderStateRowCellsData* keys,
783 void** values,
784 size_t* out_written);
785
786/**
787 * Export the row when every retained cell is narrow printable ASCII with only
788 * an optional foreground color. GHOSTTY_NO_VALUE selects the canonical API.
789 */
790GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_ascii(
791 GhosttyRenderStateRowCells cells,
792 GhosttyRenderStateAsciiCell* out,
793 size_t capacity,
794 size_t* out_len);
795
796/**
797 * Free a row cells instance.
798 *
799 * @param cells The row cells handle to free (may be NULL)
800 *
801 * @ingroup render
802 */
803GHOSTTY_API void ghostty_render_state_row_cells_free(GhosttyRenderStateRowCells cells);
804
805/** @} */
806
807#ifdef __cplusplus
808}
809#endif
810
811#endif /* GHOSTTY_VT_RENDER_H */
packages/terminal/vendor/include/ghostty/vt/screen.h created+400
......@@ -0,0 +1,400 @@
1/**
2 * @file screen.h
3 *
4 * Terminal screen cell and row types.
5 */
6
7#ifndef GHOSTTY_VT_SCREEN_H
8#define GHOSTTY_VT_SCREEN_H
9
10#include <stdbool.h>
11#include <stdint.h>
12#include <ghostty/vt/color.h>
13#include <ghostty/vt/types.h>
14
15#ifdef __cplusplus
16extern "C" {
17#endif
18
19/** @defgroup screen Screen
20 *
21 * Terminal screen cell and row types.
22 *
23 * These types represent the contents of a terminal screen. A GhosttyCell
24 * is a single grid cell and a GhosttyRow is a single row. Both are opaque
25 * values whose fields are accessed via ghostty_cell_get() and
26 * ghostty_row_get() respectively.
27 *
28 * @{
29 */
30
31/**
32 * Opaque cell value.
33 *
34 * Represents a single terminal cell. The internal layout is opaque and
35 * must be queried via ghostty_cell_get(). Obtain cell values from
36 * terminal query APIs.
37 *
38 * @ingroup screen
39 */
40typedef uint64_t GhosttyCell;
41
42/**
43 * Opaque row value.
44 *
45 * Represents a single terminal row. The internal layout is opaque and
46 * must be queried via ghostty_row_get(). Obtain row values from
47 * terminal query APIs.
48 *
49 * @ingroup screen
50 */
51typedef uint64_t GhosttyRow;
52
53/**
54 * Cell content tag.
55 *
56 * Describes what kind of content a cell holds.
57 *
58 * @ingroup screen
59 */
60typedef enum GHOSTTY_ENUM_TYPED {
61 /** A single codepoint (may be zero for empty). */
62 GHOSTTY_CELL_CONTENT_CODEPOINT = 0,
63
64 /** A codepoint that is part of a multi-codepoint grapheme cluster. */
65 GHOSTTY_CELL_CONTENT_CODEPOINT_GRAPHEME = 1,
66
67 /** No text; background color from palette. */
68 GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE = 2,
69
70 /** No text; background color as RGB. */
71 GHOSTTY_CELL_CONTENT_BG_COLOR_RGB = 3,
72 GHOSTTY_CELL_CONTENT_TAG_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
73} GhosttyCellContentTag;
74
75/**
76 * Cell wide property.
77 *
78 * Describes the width behavior of a cell.
79 *
80 * @ingroup screen
81 */
82typedef enum GHOSTTY_ENUM_TYPED {
83 /** Not a wide character, cell width 1. */
84 GHOSTTY_CELL_WIDE_NARROW = 0,
85
86 /** Wide character, cell width 2. */
87 GHOSTTY_CELL_WIDE_WIDE = 1,
88
89 /** Spacer after wide character. Do not render. */
90 GHOSTTY_CELL_WIDE_SPACER_TAIL = 2,
91
92 /** Spacer at end of soft-wrapped line for a wide character. */
93 GHOSTTY_CELL_WIDE_SPACER_HEAD = 3,
94 GHOSTTY_CELL_WIDE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
95} GhosttyCellWide;
96
97/**
98 * Semantic content type of a cell.
99 *
100 * Set by semantic prompt sequences (OSC 133) to distinguish between
101 * command output, user input, and shell prompt text.
102 *
103 * @ingroup screen
104 */
105typedef enum GHOSTTY_ENUM_TYPED {
106 /** Regular output content, such as command output. */
107 GHOSTTY_CELL_SEMANTIC_OUTPUT = 0,
108
109 /** Content that is part of user input. */
110 GHOSTTY_CELL_SEMANTIC_INPUT = 1,
111
112 /** Content that is part of a shell prompt. */
113 GHOSTTY_CELL_SEMANTIC_PROMPT = 2,
114 GHOSTTY_CELL_SEMANTIC_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
115} GhosttyCellSemanticContent;
116
117/**
118 * Cell data types.
119 *
120 * These values specify what type of data to extract from a cell
121 * using `ghostty_cell_get`.
122 *
123 * @ingroup screen
124 */
125typedef enum GHOSTTY_ENUM_TYPED {
126 /** Invalid data type. Never results in any data extraction. */
127 GHOSTTY_CELL_DATA_INVALID = 0,
128
129 /**
130 * The codepoint of the cell (0 if empty or bg-color-only).
131 *
132 * Output type: uint32_t *
133 */
134 GHOSTTY_CELL_DATA_CODEPOINT = 1,
135
136 /**
137 * The content tag describing what kind of content is in the cell.
138 *
139 * Output type: GhosttyCellContentTag *
140 */
141 GHOSTTY_CELL_DATA_CONTENT_TAG = 2,
142
143 /**
144 * The wide property of the cell.
145 *
146 * Output type: GhosttyCellWide *
147 */
148 GHOSTTY_CELL_DATA_WIDE = 3,
149
150 /**
151 * Whether the cell has text to render.
152 *
153 * Output type: bool *
154 */
155 GHOSTTY_CELL_DATA_HAS_TEXT = 4,
156
157 /**
158 * Whether the cell has non-default styling.
159 *
160 * Output type: bool *
161 */
162 GHOSTTY_CELL_DATA_HAS_STYLING = 5,
163
164 /**
165 * The style ID for the cell (for use with style lookups).
166 *
167 * Output type: uint16_t *
168 */
169 GHOSTTY_CELL_DATA_STYLE_ID = 6,
170
171 /**
172 * Whether the cell has a hyperlink.
173 *
174 * Output type: bool *
175 */
176 GHOSTTY_CELL_DATA_HAS_HYPERLINK = 7,
177
178 /**
179 * Whether the cell is protected.
180 *
181 * Output type: bool *
182 */
183 GHOSTTY_CELL_DATA_PROTECTED = 8,
184
185 /**
186 * The semantic content type of the cell (from OSC 133).
187 *
188 * Output type: GhosttyCellSemanticContent *
189 */
190 GHOSTTY_CELL_DATA_SEMANTIC_CONTENT = 9,
191
192 /**
193 * The palette index for the cell's background color.
194 * Only valid when content_tag is GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE.
195 *
196 * Output type: GhosttyColorPaletteIndex *
197 */
198 GHOSTTY_CELL_DATA_COLOR_PALETTE = 10,
199
200 /**
201 * The RGB value for the cell's background color.
202 * Only valid when content_tag is GHOSTTY_CELL_CONTENT_BG_COLOR_RGB.
203 *
204 * Output type: GhosttyColorRgb *
205 */
206 GHOSTTY_CELL_DATA_COLOR_RGB = 11,
207 GHOSTTY_CELL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
208} GhosttyCellData;
209
210/**
211 * Row semantic prompt state.
212 *
213 * Indicates whether any cells in a row are part of a shell prompt,
214 * as reported by OSC 133 sequences.
215 *
216 * @ingroup screen
217 */
218typedef enum GHOSTTY_ENUM_TYPED {
219 /** No prompt cells in this row. */
220 GHOSTTY_ROW_SEMANTIC_NONE = 0,
221
222 /** Prompt cells exist and this is a primary prompt line. */
223 GHOSTTY_ROW_SEMANTIC_PROMPT = 1,
224
225 /** Prompt cells exist and this is a continuation line. */
226 GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION = 2,
227 GHOSTTY_ROW_SEMANTIC_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
228} GhosttyRowSemanticPrompt;
229
230/**
231 * Row data types.
232 *
233 * These values specify what type of data to extract from a row
234 * using `ghostty_row_get`.
235 *
236 * @ingroup screen
237 */
238typedef enum GHOSTTY_ENUM_TYPED {
239 /** Invalid data type. Never results in any data extraction. */
240 GHOSTTY_ROW_DATA_INVALID = 0,
241
242 /**
243 * Whether this row is soft-wrapped.
244 *
245 * Output type: bool *
246 */
247 GHOSTTY_ROW_DATA_WRAP = 1,
248
249 /**
250 * Whether this row is a continuation of a soft-wrapped row.
251 *
252 * Output type: bool *
253 */
254 GHOSTTY_ROW_DATA_WRAP_CONTINUATION = 2,
255
256 /**
257 * Whether any cells in this row have grapheme clusters.
258 *
259 * Output type: bool *
260 */
261 GHOSTTY_ROW_DATA_GRAPHEME = 3,
262
263 /**
264 * Whether any cells in this row have styling (may have false positives).
265 *
266 * Output type: bool *
267 */
268 GHOSTTY_ROW_DATA_STYLED = 4,
269
270 /**
271 * Whether any cells in this row have hyperlinks (may have false positives).
272 *
273 * Output type: bool *
274 */
275 GHOSTTY_ROW_DATA_HYPERLINK = 5,
276
277 /**
278 * The semantic prompt state of this row.
279 *
280 * Output type: GhosttyRowSemanticPrompt *
281 */
282 GHOSTTY_ROW_DATA_SEMANTIC_PROMPT = 6,
283
284 /**
285 * Whether this row contains a Kitty virtual placeholder.
286 *
287 * Output type: bool *
288 */
289 GHOSTTY_ROW_DATA_KITTY_VIRTUAL_PLACEHOLDER = 7,
290
291 /**
292 * Whether this row is dirty and requires a redraw.
293 *
294 * Output type: bool *
295 */
296 GHOSTTY_ROW_DATA_DIRTY = 8,
297 GHOSTTY_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
298} GhosttyRowData;
299
300/**
301 * Get data from a cell.
302 *
303 * Extracts typed data from the given cell based on the specified
304 * data type. The output pointer must be of the appropriate type for the
305 * requested data kind. Valid data types and output types are documented
306 * in the `GhosttyCellData` enum.
307 *
308 * @param cell The cell value
309 * @param data The type of data to extract
310 * @param out Pointer to store the extracted data (type depends on data parameter)
311 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
312 * data type is invalid
313 *
314 * @ingroup screen
315 */
316GHOSTTY_API GhosttyResult ghostty_cell_get(GhosttyCell cell,
317 GhosttyCellData data,
318 void *out);
319
320/**
321 * Get multiple data fields from a cell in a single call.
322 *
323 * Each element in the keys array specifies a data kind, and the
324 * corresponding element in the values array receives the result.
325 *
326 * Processing stops at the first error; on success out_written
327 * is set to count, on error it is set to the index of the
328 * failing key (i.e. the number of values successfully written).
329 *
330 * @param cell The cell value
331 * @param count Number of key/value pairs
332 * @param keys Array of data kinds to query
333 * @param values Array of output pointers (types must match each key's
334 * documented output type)
335 * @param[out] out_written On return, receives the number of values
336 * successfully written (may be NULL)
337 * @return GHOSTTY_SUCCESS if all queries succeed
338 *
339 * @ingroup screen
340 */
341GHOSTTY_API GhosttyResult ghostty_cell_get_multi(GhosttyCell cell,
342 size_t count,
343 const GhosttyCellData* keys,
344 void** values,
345 size_t* out_written);
346
347/**
348 * Get data from a row.
349 *
350 * Extracts typed data from the given row based on the specified
351 * data type. The output pointer must be of the appropriate type for the
352 * requested data kind. Valid data types and output types are documented
353 * in the `GhosttyRowData` enum.
354 *
355 * @param row The row value
356 * @param data The type of data to extract
357 * @param out Pointer to store the extracted data (type depends on data parameter)
358 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
359 * data type is invalid
360 *
361 * @ingroup screen
362 */
363GHOSTTY_API GhosttyResult ghostty_row_get(GhosttyRow row,
364 GhosttyRowData data,
365 void *out);
366
367/**
368 * Get multiple data fields from a row in a single call.
369 *
370 * Each element in the keys array specifies a data kind, and the
371 * corresponding element in the values array receives the result.
372 *
373 * Processing stops at the first error; on success out_written
374 * is set to count, on error it is set to the index of the
375 * failing key (i.e. the number of values successfully written).
376 *
377 * @param row The row value
378 * @param count Number of key/value pairs
379 * @param keys Array of data kinds to query
380 * @param values Array of output pointers (types must match each key's
381 * documented output type)
382 * @param[out] out_written On return, receives the number of values
383 * successfully written (may be NULL)
384 * @return GHOSTTY_SUCCESS if all queries succeed
385 *
386 * @ingroup screen
387 */
388GHOSTTY_API GhosttyResult ghostty_row_get_multi(GhosttyRow row,
389 size_t count,
390 const GhosttyRowData* keys,
391 void** values,
392 size_t* out_written);
393
394/** @} */
395
396#ifdef __cplusplus
397}
398#endif
399
400#endif /* GHOSTTY_VT_SCREEN_H */
packages/terminal/vendor/include/ghostty/vt/selection.h created+1061
......@@ -0,0 +1,1061 @@
1/**
2 * @file selection.h
3 *
4 * Selection range type for specifying a region of terminal content.
5 */
6
7#ifndef GHOSTTY_VT_SELECTION_H
8#define GHOSTTY_VT_SELECTION_H
9
10#include <stdbool.h>
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/allocator.h>
14#include <ghostty/vt/grid_ref.h>
15#include <ghostty/vt/point.h>
16#include <ghostty/vt/types.h>
17
18#ifdef __cplusplus
19extern "C" {
20#endif
21
22/** @defgroup selection Selection
23 *
24 * A snapshot selection range defined by two grid references that identifies
25 * a contiguous or rectangular region of terminal content.
26 *
27 * The start and end values are GhosttyGridRef values. They are therefore
28 * untracked grid references and inherit the same lifetime rules: they are
29 * only safe to use until the next mutating operation on the terminal that
30 * produced them, including freeing the terminal. To keep a selection valid
31 * across terminal mutations, callers must maintain tracked grid references
32 * for the endpoints and reconstruct a GhosttySelection from fresh snapshots
33 * when needed.
34 *
35 * Selection gestures provide a reusable state machine for turning UI pointer
36 * interactions into selection snapshots. A caller creates one
37 * GhosttySelectionGesture per active gesture stream, reuses typed
38 * GhosttySelectionGestureEvent objects for synthetic press, drag, release,
39 * autoscroll tick, and deep-press events, and applies each event with
40 * ghostty_selection_gesture_event(). The returned GhosttySelection is a
41 * snapshot; the embedder decides whether to render it, format/copy it, or
42 * install it as the terminal's active selection.
43 *
44 * ## Examples
45 *
46 * @snippet c-vt-selection/src/main.c selection-main
47 * @snippet c-vt-selection-gesture/src/main.c selection-gesture-main
48 *
49 * @{
50 */
51
52/**
53 * Opaque handle to state for interpreting terminal selection gestures.
54 *
55 * The gesture owns only the state required to interpret pointer events. Calls
56 * that use a gesture are not concurrency-safe and must be serialized with
57 * terminal mutations.
58 *
59 * @ingroup selection
60 */
61typedef struct GhosttySelectionGestureImpl* GhosttySelectionGesture;
62
63/**
64 * Opaque handle to reusable input data for selection gesture operations.
65 *
66 * Event options are set with ghostty_selection_gesture_event_set(). Individual
67 * gesture operations document which options are required or optional.
68 *
69 * @ingroup selection
70 */
71typedef struct GhosttySelectionGestureEventImpl* GhosttySelectionGestureEvent;
72
73/**
74 * A snapshot selection range defined by two grid references.
75 *
76 * Both endpoints are inclusive. The endpoints preserve selection direction
77 * and may be reversed; callers must not assume that start is the top-left
78 * endpoint or that end is the bottom-right endpoint.
79 *
80 * When rectangle is false, the endpoints describe a linear selection. When
81 * rectangle is true, the same endpoints are interpreted as opposite corners
82 * of a rectangular/block selection.
83 *
84 * The start and end values are untracked GhosttyGridRef snapshots and are
85 * only valid until the next mutating operation on the terminal that produced
86 * them unless the selection is reconstructed from tracked references.
87 *
88 * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
89 *
90 * @ingroup selection
91 */
92typedef struct {
93 /** Size of this struct in bytes. Must be set to sizeof(GhosttySelection). */
94 size_t size;
95
96 /**
97 * Start of the selection range (inclusive).
98 *
99 * This may be after end in terminal order. It is an untracked
100 * GhosttyGridRef snapshot and follows untracked grid-ref lifetime rules.
101 */
102 GhosttyGridRef start;
103
104 /**
105 * End of the selection range (inclusive).
106 *
107 * This may be before start in terminal order. It is an untracked
108 * GhosttyGridRef snapshot and follows untracked grid-ref lifetime rules.
109 */
110 GhosttyGridRef end;
111
112 /**
113 * Whether the endpoints are interpreted as a rectangular/block selection
114 * rather than a linear selection.
115 */
116 bool rectangle;
117} GhosttySelection;
118
119/**
120 * Options for deriving a word selection from a terminal grid reference.
121 *
122 * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
123 * If boundary_codepoints is NULL and boundary_codepoints_len is 0, Ghostty's
124 * default word-boundary codepoints are used. If boundary_codepoints_len is
125 * non-zero, boundary_codepoints must not be NULL.
126 *
127 * @ingroup selection
128 */
129typedef struct {
130 /** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectWordOptions). */
131 size_t size;
132
133 /** Grid reference under which to derive the word selection. */
134 GhosttyGridRef ref;
135
136 /** Optional word-boundary codepoints as uint32_t scalar values. */
137 const uint32_t* boundary_codepoints;
138
139 /** Number of entries in boundary_codepoints. */
140 size_t boundary_codepoints_len;
141} GhosttyTerminalSelectWordOptions;
142
143/**
144 * Options for deriving the nearest word selection between two grid references.
145 *
146 * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
147 * If boundary_codepoints is NULL and boundary_codepoints_len is 0, Ghostty's
148 * default word-boundary codepoints are used. If boundary_codepoints_len is
149 * non-zero, boundary_codepoints must not be NULL.
150 *
151 * @ingroup selection
152 */
153typedef struct {
154 /** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectWordBetweenOptions). */
155 size_t size;
156
157 /** Starting grid reference for the inclusive search range. */
158 GhosttyGridRef start;
159
160 /** Ending grid reference for the inclusive search range. */
161 GhosttyGridRef end;
162
163 /** Optional word-boundary codepoints as uint32_t scalar values. */
164 const uint32_t* boundary_codepoints;
165
166 /** Number of entries in boundary_codepoints. */
167 size_t boundary_codepoints_len;
168} GhosttyTerminalSelectWordBetweenOptions;
169
170/**
171 * Options for deriving a line selection from a terminal grid reference.
172 *
173 * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
174 * If whitespace is NULL and whitespace_len is 0, Ghostty's default line-trim
175 * whitespace codepoints are used. If whitespace_len is non-zero, whitespace
176 * must not be NULL.
177 *
178 * @ingroup selection
179 */
180typedef struct {
181 /** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectLineOptions). */
182 size_t size;
183
184 /** Grid reference under which to derive the line selection. */
185 GhosttyGridRef ref;
186
187 /** Optional codepoints to trim from the start and end of the line. */
188 const uint32_t* whitespace;
189
190 /** Number of entries in whitespace. */
191 size_t whitespace_len;
192
193 /** Whether semantic prompt state changes should bound the line selection. */
194 bool semantic_prompt_boundary;
195} GhosttyTerminalSelectLineOptions;
196
197/**
198 * Options for one-shot formatting of a terminal selection.
199 *
200 * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
201 *
202 * If selection is NULL, the terminal's current active selection is used.
203 * If selection is non-NULL, that caller-provided snapshot selection is used.
204 *
205 * The selection is formatted from the terminal's active screen using the same
206 * formatting semantics as GhosttyFormatter. For copy/clipboard behavior
207 * matching Ghostty's Screen.selectionString(), use plain output with unwrap
208 * and trim both set to true.
209 *
210 * @ingroup selection
211 */
212typedef struct {
213 /** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectionFormatOptions). */
214 size_t size;
215
216 /** Output format to emit. */
217 GhosttyFormatterFormat emit;
218
219 /** Whether to unwrap soft-wrapped lines. */
220 bool unwrap;
221
222 /** Whether to trim trailing whitespace on non-blank lines. */
223 bool trim;
224
225 /**
226 * Optional selection to format.
227 *
228 * If NULL, the terminal's current active selection is used. If the terminal
229 * has no active selection, formatting returns GHOSTTY_NO_VALUE.
230 *
231 * If non-NULL, the pointed-to selection must be a valid snapshot selection
232 * for this terminal and must obey GhosttySelection lifetime rules.
233 */
234 const GhosttySelection *selection;
235} GhosttyTerminalSelectionFormatOptions;
236
237/**
238 * Ordering of a selection's endpoints in terminal coordinates.
239 *
240 * Mirrored orders are only produced by rectangular selections whose start
241 * and end endpoints are on opposite diagonal corners that are not simple
242 * top-left-to-bottom-right or bottom-right-to-top-left orderings.
243 *
244 * @ingroup selection
245 */
246typedef enum GHOSTTY_ENUM_TYPED {
247 /** Start is before end in top-left to bottom-right order. */
248 GHOSTTY_SELECTION_ORDER_FORWARD = 0,
249
250 /** End is before start in top-left to bottom-right order. */
251 GHOSTTY_SELECTION_ORDER_REVERSE = 1,
252
253 /** Rectangular selection from top-right to bottom-left. */
254 GHOSTTY_SELECTION_ORDER_MIRRORED_FORWARD = 2,
255
256 /** Rectangular selection from bottom-left to top-right. */
257 GHOSTTY_SELECTION_ORDER_MIRRORED_REVERSE = 3,
258
259 GHOSTTY_SELECTION_ORDER_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
260} GhosttySelectionOrder;
261
262/**
263 * Operation used to adjust a selection endpoint.
264 *
265 * Adjustment mutates the selection's logical end endpoint, not whichever
266 * endpoint is visually bottom/right. This preserves keyboard and drag
267 * behavior for both forward and reversed selections.
268 *
269 * @ingroup selection
270 */
271typedef enum GHOSTTY_ENUM_TYPED {
272 /** Move left to the previous non-empty cell, wrapping upward. */
273 GHOSTTY_SELECTION_ADJUST_LEFT = 0,
274
275 /** Move right to the next non-empty cell, wrapping downward. */
276 GHOSTTY_SELECTION_ADJUST_RIGHT = 1,
277
278 /**
279 * Move up one row at the current column, or to the beginning of the
280 * line if already at the top.
281 */
282 GHOSTTY_SELECTION_ADJUST_UP = 2,
283
284 /**
285 * Move down to the next non-blank row at the current column, or to the
286 * end of the line if none exists.
287 */
288 GHOSTTY_SELECTION_ADJUST_DOWN = 3,
289
290 /** Move to the top-left cell of the screen. */
291 GHOSTTY_SELECTION_ADJUST_HOME = 4,
292
293 /** Move to the right edge of the last non-blank row on the screen. */
294 GHOSTTY_SELECTION_ADJUST_END = 5,
295
296 /**
297 * Move up by one terminal page height, or to home if that would move
298 * past the top.
299 */
300 GHOSTTY_SELECTION_ADJUST_PAGE_UP = 6,
301
302 /**
303 * Move down by one terminal page height, or to end if that would move
304 * past the bottom.
305 */
306 GHOSTTY_SELECTION_ADJUST_PAGE_DOWN = 7,
307
308 /** Move to the left edge of the current line. */
309 GHOSTTY_SELECTION_ADJUST_BEGINNING_OF_LINE = 8,
310
311 /** Move to the right edge of the current line. */
312 GHOSTTY_SELECTION_ADJUST_END_OF_LINE = 9,
313
314 GHOSTTY_SELECTION_ADJUST_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
315} GhosttySelectionAdjust;
316
317/**
318 * Selection behavior chosen for a gesture's click sequence.
319 *
320 * @ingroup selection
321 */
322typedef enum GHOSTTY_ENUM_TYPED {
323 /** Cell-granular drag selection. */
324 GHOSTTY_SELECTION_GESTURE_BEHAVIOR_CELL = 0,
325
326 /** Word selection on press and word-granular drag selection. */
327 GHOSTTY_SELECTION_GESTURE_BEHAVIOR_WORD = 1,
328
329 /** Line selection on press and line-granular drag selection. */
330 GHOSTTY_SELECTION_GESTURE_BEHAVIOR_LINE = 2,
331
332 /** Semantic command output selection on press and drag. */
333 GHOSTTY_SELECTION_GESTURE_BEHAVIOR_OUTPUT = 3,
334
335 GHOSTTY_SELECTION_GESTURE_BEHAVIOR_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
336} GhosttySelectionGestureBehavior;
337
338/**
339 * Selection behaviors for single-, double-, and triple-click gestures.
340 *
341 * @ingroup selection
342 */
343typedef struct {
344 /** Behavior for single-click selection gestures. */
345 GhosttySelectionGestureBehavior single_click;
346
347 /** Behavior for double-click selection gestures. */
348 GhosttySelectionGestureBehavior double_click;
349
350 /** Behavior for triple-click selection gestures. */
351 GhosttySelectionGestureBehavior triple_click;
352} GhosttySelectionGestureBehaviors;
353
354/**
355 * Display geometry used to interpret selection gesture drag events.
356 *
357 * @ingroup selection
358 */
359typedef struct {
360 /** Number of columns in the rendered terminal grid. Must be non-zero. */
361 uint32_t columns;
362
363 /** Width of one terminal cell in surface pixels. Must be non-zero. */
364 uint32_t cell_width;
365
366 /** Left padding before the terminal grid begins in surface pixels. */
367 uint32_t padding_left;
368
369 /** Height of the rendered terminal surface in surface pixels. Must be non-zero. */
370 uint32_t screen_height;
371} GhosttySelectionGestureGeometry;
372
373/**
374 * Current autoscroll direction for an active selection drag gesture.
375 *
376 * @ingroup selection
377 */
378typedef enum GHOSTTY_ENUM_TYPED {
379 /** No selection autoscroll is requested. */
380 GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_NONE = 0,
381
382 /** Selection dragging should autoscroll the viewport upward. */
383 GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_UP = 1,
384
385 /** Selection dragging should autoscroll the viewport downward. */
386 GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_DOWN = 2,
387
388 GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
389} GhosttySelectionGestureAutoscroll;
390
391/**
392 * Data fields readable from a selection gesture with
393 * ghostty_selection_gesture_get().
394 *
395 * @ingroup selection
396 */
397typedef enum GHOSTTY_ENUM_TYPED {
398 /** Current click count: uint8_t*. 0 means inactive. */
399 GHOSTTY_SELECTION_GESTURE_DATA_CLICK_COUNT = 0,
400
401 /** Whether the current/last left-click gesture has dragged: bool*. */
402 GHOSTTY_SELECTION_GESTURE_DATA_DRAGGED = 1,
403
404 /** Current autoscroll request: GhosttySelectionGestureAutoscroll*. */
405 GHOSTTY_SELECTION_GESTURE_DATA_AUTOSCROLL = 2,
406
407 /** Current gesture behavior: GhosttySelectionGestureBehavior*. */
408 GHOSTTY_SELECTION_GESTURE_DATA_BEHAVIOR = 3,
409
410 /**
411 * Current left-click anchor: GhosttyGridRef*.
412 *
413 * Returns GHOSTTY_NO_VALUE if there is no valid active anchor. On success,
414 * writes an untracked GhosttyGridRef snapshot with normal GhosttyGridRef
415 * lifetime rules.
416 */
417 GHOSTTY_SELECTION_GESTURE_DATA_ANCHOR = 4,
418
419 GHOSTTY_SELECTION_GESTURE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
420} GhosttySelectionGestureData;
421
422/**
423 * Selection gesture event type.
424 *
425 * The event type is fixed when the event is created. Each event type documents
426 * which options are valid and which options are required by gesture operations.
427 *
428 * @ingroup selection
429 */
430typedef enum GHOSTTY_ENUM_TYPED {
431 /** Press event for ghostty_selection_gesture_event(). */
432 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS = 0,
433
434 /** Release event for ghostty_selection_gesture_event(). */
435 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE = 1,
436
437 /** Drag event for ghostty_selection_gesture_event(). */
438 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DRAG = 2,
439
440 /** Autoscroll tick event for ghostty_selection_gesture_event(). */
441 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_AUTOSCROLL_TICK = 3,
442
443 /** Deep press event for ghostty_selection_gesture_event(). */
444 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DEEP_PRESS = 4,
445
446 GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
447} GhosttySelectionGestureEventType;
448
449/**
450 * Options stored on a reusable selection gesture event.
451 *
452 * Passing NULL as the value to ghostty_selection_gesture_event_set() clears the
453 * corresponding option.
454 *
455 * @ingroup selection
456 */
457typedef enum GHOSTTY_ENUM_TYPED {
458 /**
459 * Grid reference under the pointer: GhosttyGridRef*.
460 *
461 * Required for PRESS and DRAG events. Optional for RELEASE events; when unset
462 * or cleared, release records that the pointer did not map to a valid cell.
463 */
464 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF = 0,
465
466 /**
467 * Surface-space pointer position: GhosttySurfacePosition*.
468 *
469 * Valid for PRESS, DRAG, and AUTOSCROLL_TICK.
470 */
471 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_POSITION = 1,
472
473 /** Maximum repeat-click distance in pixels: double*. */
474 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_DISTANCE = 2,
475
476 /**
477 * Optional monotonic event time in nanoseconds: uint64_t*.
478 *
479 * If unset, press treats the event as untimed and only single-click behavior
480 * is available.
481 */
482 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_TIME_NS = 3,
483
484 /** Maximum interval between repeat clicks in nanoseconds: uint64_t*. */
485 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_INTERVAL_NS = 4,
486
487 /**
488 * Word-boundary codepoints: GhosttyCodepoints*.
489 *
490 * The codepoints are copied into event-owned storage when set. If unset,
491 * operations that need word boundaries use Ghostty's defaults.
492 *
493 * Valid for PRESS, DRAG, AUTOSCROLL_TICK, and DEEP_PRESS.
494 */
495 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS = 5,
496
497 /**
498 * Selection behavior table: GhosttySelectionGestureBehaviors*.
499 *
500 * If unset, press uses the default behavior table: cell, word, line.
501 */
502 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_BEHAVIORS = 6,
503
504 /** Whether a drag or autoscroll tick should produce a rectangular selection: bool*. */
505 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_RECTANGLE = 7,
506
507 /** Drag display geometry: GhosttySelectionGestureGeometry*. Required for DRAG and AUTOSCROLL_TICK. */
508 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY = 8,
509
510 /** Viewport coordinate for an autoscroll tick: GhosttyPointCoordinate*. Required for AUTOSCROLL_TICK. */
511 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_VIEWPORT = 9,
512
513 GHOSTTY_SELECTION_GESTURE_EVENT_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
514} GhosttySelectionGestureEventOption;
515
516/**
517 * Create a reusable selection gesture event object.
518 *
519 * @param allocator Allocator, or NULL for the default allocator
520 * @param out_event Receives the created event handle
521 * @param type Event type. This is fixed for the lifetime of the event.
522 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if out_event is
523 * NULL or type is invalid, or GHOSTTY_OUT_OF_MEMORY if allocation fails
524 *
525 * @ingroup selection
526 */
527GHOSTTY_API GhosttyResult ghostty_selection_gesture_event_new(
528 const GhosttyAllocator* allocator,
529 GhosttySelectionGestureEvent* out_event,
530 GhosttySelectionGestureEventType type);
531
532/**
533 * Free a selection gesture event object.
534 *
535 * Passing NULL is allowed and is a no-op.
536 *
537 * @param event Selection gesture event handle to free
538 *
539 * @ingroup selection
540 */
541GHOSTTY_API void ghostty_selection_gesture_event_free(
542 GhosttySelectionGestureEvent event);
543
544/**
545 * Set or clear an option on a selection gesture event.
546 *
547 * The value type depends on option and is documented by
548 * GhosttySelectionGestureEventOption. Passing NULL for value clears the option.
549 *
550 * @param event Selection gesture event handle (NULL returns GHOSTTY_INVALID_VALUE)
551 * @param option Event option to set or clear
552 * @param value Pointer to the input value for option, or NULL to clear
553 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if copying
554 * event-owned data fails, or GHOSTTY_INVALID_VALUE if event, option, or
555 * value is invalid
556 *
557 * @ingroup selection
558 */
559GHOSTTY_API GhosttyResult ghostty_selection_gesture_event_set(
560 GhosttySelectionGestureEvent event,
561 GhosttySelectionGestureEventOption option,
562 const void* value);
563
564/**
565 * Apply a selection gesture event and return the resulting selection snapshot.
566 *
567 * This dispatches to the gesture operation matching the event's fixed type.
568 * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS, the event must have
569 * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF set before calling this function.
570 * All other press options use their initialized defaults when unset or cleared.
571 *
572 * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE, only
573 * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF is valid. It is optional; if unset or
574 * cleared, release records that the pointer did not map to a valid cell. Release
575 * events update gesture state but do not produce a selection, so this function
576 * returns GHOSTTY_NO_VALUE after applying them.
577 *
578 * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DRAG,
579 * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF and
580 * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY are required. Position,
581 * rectangle, and word-boundary codepoints are optional and use initialized
582 * defaults when unset or cleared.
583 *
584 * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_AUTOSCROLL_TICK,
585 * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_VIEWPORT and
586 * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY are required. Position,
587 * rectangle, and word-boundary codepoints are optional and use initialized
588 * defaults when unset or cleared.
589 *
590 * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DEEP_PRESS, only
591 * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS is valid. It is
592 * optional and uses initialized defaults when unset or cleared.
593 *
594 * The returned selection is not installed as the terminal's current selection.
595 * It is a snapshot with the same lifetime rules as GhosttySelection.
596 *
597 * @param gesture Selection gesture handle (NULL returns GHOSTTY_INVALID_VALUE)
598 * @param terminal Terminal used to interpret and update gesture state
599 * @param event Selection gesture event handle (NULL returns GHOSTTY_INVALID_VALUE)
600 * @param[out] out_selection On success, receives the resulting selection. May
601 * be NULL to apply the event and discard the selection result.
602 * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the event does not
603 * currently produce a selection, GHOSTTY_OUT_OF_MEMORY if tracking
604 * gesture state fails, or GHOSTTY_INVALID_VALUE if gesture, terminal,
605 * event, or required event data is invalid
606 *
607 * @ingroup selection
608 */
609GHOSTTY_API GhosttyResult ghostty_selection_gesture_event(
610 GhosttySelectionGesture gesture,
611 GhosttyTerminal terminal,
612 GhosttySelectionGestureEvent event,
613 GhosttySelection* out_selection);
614
615/**
616 * Create a selection gesture object.
617 *
618 * The gesture stores mutable state for terminal text selection gestures. The
619 * gesture is not bound to a terminal at creation time; terminal-dependent APIs
620 * take the terminal explicitly.
621 *
622 * @param allocator Allocator, or NULL for the default allocator
623 * @param out_gesture Receives the created gesture handle
624 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if out_gesture is
625 * NULL, or GHOSTTY_OUT_OF_MEMORY if allocation fails
626 *
627 * @ingroup selection
628 */
629GHOSTTY_API GhosttyResult ghostty_selection_gesture_new(
630 const GhosttyAllocator* allocator,
631 GhosttySelectionGesture* out_gesture);
632
633/**
634 * Free a selection gesture object.
635 *
636 * This releases any tracked terminal references owned by the gesture using the
637 * provided terminal, then frees the gesture object. Passing NULL for gesture is
638 * allowed and is a no-op.
639 *
640 * If the terminal is still alive, pass the terminal most recently used with the
641 * gesture so any tracked terminal references can be released correctly. If the
642 * terminal has already been freed, pass NULL for terminal; the terminal's page
643 * storage has already released the underlying tracked references, so the
644 * gesture wrapper can be safely discarded without touching the stale terminal
645 * state.
646 *
647 * @param gesture Selection gesture handle to free
648 * @param terminal Terminal used to release tracked gesture state, or NULL if
649 * the terminal has already been freed
650 *
651 * @ingroup selection
652 */
653GHOSTTY_API void ghostty_selection_gesture_free(
654 GhosttySelectionGesture gesture,
655 GhosttyTerminal terminal);
656
657/**
658 * Reset any active selection gesture state.
659 *
660 * This cancels the active click sequence and releases any tracked terminal
661 * references owned by the gesture without freeing the gesture object.
662 * Passing NULL is allowed and is a no-op.
663 *
664 * @param gesture Selection gesture handle to reset
665 * @param terminal Terminal used to release tracked gesture state
666 *
667 * @ingroup selection
668 */
669GHOSTTY_API void ghostty_selection_gesture_reset(
670 GhosttySelectionGesture gesture,
671 GhosttyTerminal terminal);
672
673/**
674 * Read data from a selection gesture.
675 *
676 * The type of value depends on data and is documented by
677 * GhosttySelectionGestureData. For GHOSTTY_SELECTION_GESTURE_DATA_ANCHOR,
678 * the returned GhosttyGridRef is an untracked snapshot with normal grid-ref
679 * lifetime rules.
680 *
681 * @param gesture Selection gesture handle (NULL returns GHOSTTY_INVALID_VALUE)
682 * @param terminal Terminal used to validate terminal-backed gesture state
683 * @param data Data field to read
684 * @param value Output pointer whose type depends on data
685 * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the requested data
686 * has no value, or GHOSTTY_INVALID_VALUE if gesture, terminal, data, or
687 * value is invalid
688 *
689 * @ingroup selection
690 */
691GHOSTTY_API GhosttyResult ghostty_selection_gesture_get(
692 GhosttySelectionGesture gesture,
693 GhosttyTerminal terminal,
694 GhosttySelectionGestureData data,
695 void* value);
696
697/**
698 * Read multiple data fields from a selection gesture in a single call.
699 *
700 * This is an optimization over calling ghostty_selection_gesture_get() multiple
701 * times. Each entry in values must point to storage of the type documented by
702 * the corresponding GhosttySelectionGestureData key.
703 *
704 * If any individual read fails, the function returns that error and writes the
705 * index of the failing key to out_written when out_written is non-NULL. On
706 * success, out_written receives count when non-NULL.
707 *
708 * @param gesture Selection gesture handle (NULL returns GHOSTTY_INVALID_VALUE)
709 * @param terminal Terminal used to validate terminal-backed gesture state
710 * @param count Number of data fields to read
711 * @param keys Data fields to read (must not be NULL)
712 * @param values Output pointers corresponding to keys (must not be NULL)
713 * @param out_written Optional number of fields read, or failing index on error
714 * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if a requested data
715 * field has no value, or GHOSTTY_INVALID_VALUE if gesture, terminal,
716 * keys, values, or a value pointer is invalid
717 *
718 * @ingroup selection
719 */
720GHOSTTY_API GhosttyResult ghostty_selection_gesture_get_multi(
721 GhosttySelectionGesture gesture,
722 GhosttyTerminal terminal,
723 size_t count,
724 const GhosttySelectionGestureData* keys,
725 void** values,
726 size_t* out_written);
727
728/**
729 * Derive a word selection snapshot from a terminal grid reference.
730 *
731 * The returned selection is not installed as the terminal's current
732 * selection. It is a snapshot with the same lifetime rules as GhosttySelection.
733 *
734 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
735 * @param options Word-selection options
736 * @param[out] out_selection On success, receives the derived selection
737 * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref has
738 * no selectable word content, or GHOSTTY_INVALID_VALUE if the
739 * terminal, options, ref, codepoint pointer, or output pointer are
740 * invalid.
741 *
742 * @ingroup selection
743 */
744GHOSTTY_API GhosttyResult ghostty_terminal_select_word(
745 GhosttyTerminal terminal,
746 const GhosttyTerminalSelectWordOptions* options,
747 GhosttySelection* out_selection);
748
749/**
750 * Derive the nearest word selection snapshot between two terminal grid refs.
751 *
752 * Starting at options->start, this searches toward options->end (inclusive)
753 * and returns the first selectable word found using Ghostty's word-selection
754 * rules.
755 *
756 * This is useful for implementing double-click-and-drag selection in a UI. If
757 * a user double-clicks one word and drags across spaces or punctuation toward
758 * another word, selecting only the word directly under the current pointer can
759 * flicker or collapse when the pointer is between words. Instead, ask for the
760 * nearest word between the original click and the drag point, ask again in the
761 * reverse direction, and combine the two word bounds into the drag selection.
762 *
763 * @snippet c-vt-selection/src/main.c selection-word-between
764 *
765 * The returned selection is not installed as the terminal's current
766 * selection. It is a snapshot with the same lifetime rules as GhosttySelection.
767 *
768 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
769 * @param options Word-between-selection options
770 * @param[out] out_selection On success, receives the derived selection
771 * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if there is no
772 * selectable word content between the valid refs, or
773 * GHOSTTY_INVALID_VALUE if the terminal, options, refs, codepoint
774 * pointer, or output pointer are invalid.
775 *
776 * @ingroup selection
777 */
778GHOSTTY_API GhosttyResult ghostty_terminal_select_word_between(
779 GhosttyTerminal terminal,
780 const GhosttyTerminalSelectWordBetweenOptions* options,
781 GhosttySelection* out_selection);
782
783/**
784 * Derive a line selection snapshot from a terminal grid reference.
785 *
786 * The returned selection is not installed as the terminal's current
787 * selection. It is a snapshot with the same lifetime rules as GhosttySelection.
788 *
789 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
790 * @param options Line-selection options
791 * @param[out] out_selection On success, receives the derived selection
792 * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref has
793 * no selectable line content, or GHOSTTY_INVALID_VALUE if the
794 * terminal, options, ref, codepoint pointer, or output pointer are
795 * invalid.
796 *
797 * @ingroup selection
798 */
799GHOSTTY_API GhosttyResult ghostty_terminal_select_line(
800 GhosttyTerminal terminal,
801 const GhosttyTerminalSelectLineOptions* options,
802 GhosttySelection* out_selection);
803
804/**
805 * Derive a selection snapshot covering all selectable terminal content.
806 *
807 * The returned selection is not installed as the terminal's current
808 * selection. It is a snapshot with the same lifetime rules as GhosttySelection.
809 *
810 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
811 * @param[out] out_selection On success, receives the derived selection
812 * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if there is no
813 * selectable content, or GHOSTTY_INVALID_VALUE if the terminal or
814 * output pointer is invalid.
815 *
816 * @ingroup selection
817 */
818GHOSTTY_API GhosttyResult ghostty_terminal_select_all(
819 GhosttyTerminal terminal,
820 GhosttySelection* out_selection);
821
822/**
823 * Derive a command-output selection snapshot from a terminal grid reference.
824 *
825 * The returned selection is not installed as the terminal's current
826 * selection. It is a snapshot with the same lifetime rules as GhosttySelection.
827 *
828 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
829 * @param ref Grid reference within command output to select
830 * @param[out] out_selection On success, receives the derived selection
831 * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref is
832 * not selectable command output, or GHOSTTY_INVALID_VALUE if the
833 * terminal, ref, or output pointer is invalid.
834 *
835 * @ingroup selection
836 */
837GHOSTTY_API GhosttyResult ghostty_terminal_select_output(
838 GhosttyTerminal terminal,
839 GhosttyGridRef ref,
840 GhosttySelection* out_selection);
841
842/**
843 * Format a terminal selection into a caller-provided buffer.
844 *
845 * This is a one-shot convenience API for formatting either the terminal's
846 * active selection or a caller-provided GhosttySelection without explicitly
847 * creating a GhosttyFormatter.
848 *
849 * Pass NULL for buf to query the required output size. In that case,
850 * out_written receives the required size and the function returns
851 * GHOSTTY_OUT_OF_SPACE.
852 *
853 * If buf is too small, the function returns GHOSTTY_OUT_OF_SPACE and writes
854 * the required size to out_written. The caller can then retry with a larger
855 * buffer.
856 *
857 * If options.selection is NULL and the terminal has no active selection, the
858 * function returns GHOSTTY_NO_VALUE.
859 *
860 * @param terminal The terminal to read from (must not be NULL)
861 * @param options Selection formatting options
862 * @param buf Output buffer, or NULL to query required size
863 * @param buf_len Length of buf in bytes
864 * @param out_written Number of bytes written, or required size on
865 * GHOSTTY_OUT_OF_SPACE (must not be NULL)
866 * @return GHOSTTY_SUCCESS on success, or an error code on failure
867 *
868 * @ingroup selection
869 */
870GHOSTTY_API GhosttyResult ghostty_terminal_selection_format_buf(
871 GhosttyTerminal terminal,
872 GhosttyTerminalSelectionFormatOptions options,
873 uint8_t* buf,
874 size_t buf_len,
875 size_t* out_written);
876
877/**
878 * Format a terminal selection into an allocated buffer.
879 *
880 * This is a one-shot convenience API for formatting either the terminal's
881 * active selection or a caller-provided GhosttySelection without explicitly
882 * creating a GhosttyFormatter.
883 *
884 * The returned buffer is allocated using allocator, or the default allocator
885 * if NULL is passed. The caller owns the returned buffer and must free it with
886 * ghostty_free(), passing the same allocator and returned length.
887 *
888 * The returned bytes are not NUL-terminated. This supports plain text, VT, and
889 * HTML uniformly as byte output.
890 *
891 * If options.selection is NULL and the terminal has no active selection, the
892 * function returns GHOSTTY_NO_VALUE and leaves out_ptr as NULL and out_len as 0.
893 *
894 * @param terminal The terminal to read from (must not be NULL)
895 * @param allocator Allocator used for the returned buffer, or NULL for the default allocator
896 * @param options Selection formatting options
897 * @param out_ptr Receives the allocated output buffer (must not be NULL)
898 * @param out_len Receives the output length in bytes (must not be NULL)
899 * @return GHOSTTY_SUCCESS on success, or an error code on failure
900 *
901 * @ingroup selection
902 */
903GHOSTTY_API GhosttyResult ghostty_terminal_selection_format_alloc(
904 GhosttyTerminal terminal,
905 const GhosttyAllocator* allocator,
906 GhosttyTerminalSelectionFormatOptions options,
907 uint8_t** out_ptr,
908 size_t* out_len);
909
910/**
911 * Adjust a selection snapshot using terminal selection semantics.
912 *
913 * This mutates the caller-provided GhosttySelection in place. The logical end
914 * endpoint is always moved, regardless of whether the selection is forward or
915 * reversed visually. The input selection remains a snapshot: after adjustment,
916 * call ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_SELECTION to install it
917 * as the terminal-owned selection if desired.
918 *
919 * The selection's start and end grid refs must both be valid untracked
920 * snapshots for the given terminal's currently active screen. In practice,
921 * they must come from that terminal and screen, and no mutating terminal call
922 * may have occurred since the refs were produced or reconstructed from
923 * tracked refs. Passing refs from another terminal, another screen, or stale
924 * refs violates this precondition.
925 *
926 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
927 * @param selection Selection snapshot to adjust in place
928 * @param adjustment The adjustment operation to apply
929 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
930 * selection, or adjustment are invalid. Selection reference validity
931 * is a precondition and is not checked.
932 *
933 * @ingroup selection
934 */
935GHOSTTY_API GhosttyResult ghostty_terminal_selection_adjust(
936 GhosttyTerminal terminal,
937 GhosttySelection* selection,
938 GhosttySelectionAdjust adjustment);
939
940/**
941 * Get the current endpoint ordering of a selection snapshot.
942 *
943 * The selection's start and end grid refs must both be valid untracked
944 * snapshots for the given terminal's currently active screen. In practice,
945 * they must come from that terminal and screen, and no mutating terminal call
946 * may have occurred since the refs were produced or reconstructed from
947 * tracked refs. Passing refs from another terminal, another screen, or stale
948 * refs violates this precondition.
949 *
950 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
951 * @param selection Selection snapshot to inspect
952 * @param[out] out_order On success, receives the selection order
953 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
954 * selection, or output pointer are invalid. Selection reference
955 * validity is a precondition and is not checked.
956 *
957 * @ingroup selection
958 */
959GHOSTTY_API GhosttyResult ghostty_terminal_selection_order(
960 GhosttyTerminal terminal,
961 const GhosttySelection* selection,
962 GhosttySelectionOrder* out_order);
963
964/**
965 * Return a selection snapshot with endpoints ordered as requested.
966 *
967 * Use GHOSTTY_SELECTION_ORDER_FORWARD to get top-left to bottom-right bounds,
968 * and GHOSTTY_SELECTION_ORDER_REVERSE to get bottom-right to top-left bounds.
969 * Mirrored desired orders are accepted but normalized the same as forward.
970 * The output selection is a fresh untracked snapshot and is not installed as
971 * the terminal's current selection.
972 *
973 * The selection's start and end grid refs must both be valid untracked
974 * snapshots for the given terminal's currently active screen. In practice,
975 * they must come from that terminal and screen, and no mutating terminal call
976 * may have occurred since the refs were produced or reconstructed from
977 * tracked refs. Passing refs from another terminal, another screen, or stale
978 * refs violates this precondition.
979 *
980 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
981 * @param selection Selection snapshot to order
982 * @param desired Desired endpoint order
983 * @param[out] out_selection On success, receives the ordered selection
984 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
985 * selection, desired order, or output pointer are invalid. Selection
986 * reference validity is a precondition and is not checked.
987 *
988 * @ingroup selection
989 */
990GHOSTTY_API GhosttyResult ghostty_terminal_selection_ordered(
991 GhosttyTerminal terminal,
992 const GhosttySelection* selection,
993 GhosttySelectionOrder desired,
994 GhosttySelection* out_selection);
995
996/**
997 * Test whether a terminal point is inside a selection snapshot.
998 *
999 * This uses the same selection semantics as the terminal, including
1000 * rectangular/block selections and linear selections spanning multiple rows.
1001 *
1002 * The selection's start and end grid refs must both be valid untracked
1003 * snapshots for the given terminal's currently active screen. In practice,
1004 * they must come from that terminal and screen, and no mutating terminal call
1005 * may have occurred since the refs were produced or reconstructed from
1006 * tracked refs. Passing refs from another terminal, another screen, or stale
1007 * refs violates this precondition.
1008 *
1009 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
1010 * @param selection Selection snapshot to inspect
1011 * @param point Point to test for containment
1012 * @param[out] out_contains On success, receives whether point is inside selection
1013 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
1014 * selection, point, or output pointer are invalid. Selection reference
1015 * validity is a precondition and is not checked.
1016 *
1017 * @ingroup selection
1018 */
1019GHOSTTY_API GhosttyResult ghostty_terminal_selection_contains(
1020 GhosttyTerminal terminal,
1021 const GhosttySelection* selection,
1022 GhosttyPoint point,
1023 bool* out_contains);
1024
1025/**
1026 * Test whether two selection snapshots are equal.
1027 *
1028 * Equality uses the terminal's internal selection semantics: both endpoint
1029 * pins must match and both selections must have the same rectangular/block
1030 * state. This avoids requiring callers to compare raw GhosttyGridRef internals.
1031 *
1032 * Both selections' start and end grid refs must be valid untracked snapshots
1033 * for the given terminal's currently active screen. In practice, they must
1034 * come from that terminal and screen, and no mutating terminal call may have
1035 * occurred since the refs were produced or reconstructed from tracked refs.
1036 * Passing refs from another terminal, another screen, or stale refs violates
1037 * this precondition.
1038 *
1039 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
1040 * @param a First selection snapshot to compare
1041 * @param b Second selection snapshot to compare
1042 * @param[out] out_equal On success, receives whether the selections are equal
1043 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
1044 * selections, or output pointer are invalid. Selection reference
1045 * validity is a precondition and is not checked.
1046 *
1047 * @ingroup selection
1048 */
1049GHOSTTY_API GhosttyResult ghostty_terminal_selection_equal(
1050 GhosttyTerminal terminal,
1051 const GhosttySelection* a,
1052 const GhosttySelection* b,
1053 bool* out_equal);
1054
1055/** @} */
1056
1057#ifdef __cplusplus
1058}
1059#endif
1060
1061#endif /* GHOSTTY_VT_SELECTION_H */
packages/terminal/vendor/include/ghostty/vt/sgr.h created+353
......@@ -0,0 +1,353 @@
1/**
2 * @file sgr.h
3 *
4 * SGR (Select Graphic Rendition) attribute parsing and handling.
5 */
6
7#ifndef GHOSTTY_VT_SGR_H
8#define GHOSTTY_VT_SGR_H
9
10/** @defgroup sgr SGR Parser
11 *
12 * SGR (Select Graphic Rendition) attribute parser.
13 *
14 * SGR sequences are the syntax used to set styling attributes such as
15 * bold, italic, underline, and colors for text in terminal emulators.
16 * For example, you may be familiar with sequences like `ESC[1;31m`. The
17 * `1;31` is the SGR attribute list.
18 *
19 * The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`)
20 * and returns individual text attributes like bold, italic, colors, etc.
21 * It supports both semicolon (`;`) and colon (`:`) separators, possibly mixed,
22 * and handles SGR color attributes including 8-color, 16-color, 256-color,
23 * direct RGB, underline color, and reset forms. Color values are returned
24 * using the shared @ref color types; applications that need to parse Ghostty
25 * config/theme color strings, generate palettes, inspect X11 color names, or
26 * calculate luminance and contrast should use the @ref color APIs directly.
27 *
28 * ## Basic Usage
29 *
30 * 1. Create a parser instance with ghostty_sgr_new()
31 * 2. Set SGR parameters with ghostty_sgr_set_params()
32 * 3. Iterate through attributes using ghostty_sgr_next()
33 * 4. Free the parser with ghostty_sgr_free() when done
34 *
35 * ## Example
36 *
37 * @snippet c-vt-sgr/src/main.c sgr-basic
38 *
39 * @{
40 */
41
42#include <ghostty/vt/allocator.h>
43#include <ghostty/vt/color.h>
44#include <ghostty/vt/types.h>
45#include <stdbool.h>
46#include <stddef.h>
47#include <stdint.h>
48
49#ifdef __cplusplus
50extern "C" {
51#endif
52
53/**
54 * SGR attribute tags.
55 *
56 * These values identify the type of an SGR attribute in a tagged union.
57 * Use the tag to determine which field in the attribute value union to access.
58 *
59 * @ingroup sgr
60 */
61typedef enum GHOSTTY_ENUM_TYPED {
62 GHOSTTY_SGR_ATTR_UNSET = 0,
63 GHOSTTY_SGR_ATTR_UNKNOWN = 1,
64 GHOSTTY_SGR_ATTR_BOLD = 2,
65 GHOSTTY_SGR_ATTR_RESET_BOLD = 3,
66 GHOSTTY_SGR_ATTR_ITALIC = 4,
67 GHOSTTY_SGR_ATTR_RESET_ITALIC = 5,
68 GHOSTTY_SGR_ATTR_FAINT = 6,
69 GHOSTTY_SGR_ATTR_UNDERLINE = 7,
70 GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 8,
71 GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 9,
72 GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 10,
73 GHOSTTY_SGR_ATTR_OVERLINE = 11,
74 GHOSTTY_SGR_ATTR_RESET_OVERLINE = 12,
75 GHOSTTY_SGR_ATTR_BLINK = 13,
76 GHOSTTY_SGR_ATTR_RESET_BLINK = 14,
77 GHOSTTY_SGR_ATTR_INVERSE = 15,
78 GHOSTTY_SGR_ATTR_RESET_INVERSE = 16,
79 GHOSTTY_SGR_ATTR_INVISIBLE = 17,
80 GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 18,
81 GHOSTTY_SGR_ATTR_STRIKETHROUGH = 19,
82 GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 20,
83 GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 21,
84 GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 22,
85 GHOSTTY_SGR_ATTR_BG_8 = 23,
86 GHOSTTY_SGR_ATTR_FG_8 = 24,
87 GHOSTTY_SGR_ATTR_RESET_FG = 25,
88 GHOSTTY_SGR_ATTR_RESET_BG = 26,
89 GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 27,
90 GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 28,
91 GHOSTTY_SGR_ATTR_BG_256 = 29,
92 GHOSTTY_SGR_ATTR_FG_256 = 30,
93 GHOSTTY_SGR_ATTR_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
94} GhosttySgrAttributeTag;
95
96/**
97 * Underline style types.
98 *
99 * @ingroup sgr
100 */
101typedef enum GHOSTTY_ENUM_TYPED {
102 GHOSTTY_SGR_UNDERLINE_NONE = 0,
103 GHOSTTY_SGR_UNDERLINE_SINGLE = 1,
104 GHOSTTY_SGR_UNDERLINE_DOUBLE = 2,
105 GHOSTTY_SGR_UNDERLINE_CURLY = 3,
106 GHOSTTY_SGR_UNDERLINE_DOTTED = 4,
107 GHOSTTY_SGR_UNDERLINE_DASHED = 5,
108 GHOSTTY_SGR_UNDERLINE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
109} GhosttySgrUnderline;
110
111/**
112 * Unknown SGR attribute data.
113 *
114 * Contains the full parameter list and the partial list where parsing
115 * encountered an unknown or invalid sequence.
116 *
117 * @ingroup sgr
118 */
119typedef struct {
120 const uint16_t* full_ptr;
121 size_t full_len;
122 const uint16_t* partial_ptr;
123 size_t partial_len;
124} GhosttySgrUnknown;
125
126/**
127 * SGR attribute value union.
128 *
129 * This union contains all possible attribute values. Use the tag field
130 * to determine which union member is active. Attributes without associated
131 * data (like bold, italic) don't use the union value.
132 *
133 * @ingroup sgr
134 */
135typedef union {
136 GhosttySgrUnknown unknown;
137 GhosttySgrUnderline underline;
138 GhosttyColorRgb underline_color;
139 GhosttyColorPaletteIndex underline_color_256;
140 GhosttyColorRgb direct_color_fg;
141 GhosttyColorRgb direct_color_bg;
142 GhosttyColorPaletteIndex bg_8;
143 GhosttyColorPaletteIndex fg_8;
144 GhosttyColorPaletteIndex bright_bg_8;
145 GhosttyColorPaletteIndex bright_fg_8;
146 GhosttyColorPaletteIndex bg_256;
147 GhosttyColorPaletteIndex fg_256;
148 uint64_t _padding[8];
149} GhosttySgrAttributeValue;
150
151/**
152 * SGR attribute (tagged union).
153 *
154 * A complete SGR attribute with both its type tag and associated value.
155 * Always check the tag field to determine which value union member is valid.
156 *
157 * Attributes without associated data (e.g., GHOSTTY_SGR_ATTR_BOLD) can be
158 * identified by tag alone; the value union is not used for these and
159 * the memory in the value field is undefined.
160 *
161 * @ingroup sgr
162 */
163typedef struct {
164 GhosttySgrAttributeTag tag;
165 GhosttySgrAttributeValue value;
166} GhosttySgrAttribute;
167
168/**
169 * Create a new SGR parser instance.
170 *
171 * Creates a new SGR (Select Graphic Rendition) parser using the provided
172 * allocator. The parser must be freed using ghostty_sgr_free() when
173 * no longer needed.
174 *
175 * @param allocator Pointer to the allocator to use for memory management, or
176 * NULL to use the default allocator
177 * @param parser Pointer to store the created parser handle
178 * @return GHOSTTY_SUCCESS on success, or an error code on failure
179 *
180 * @ingroup sgr
181 */
182GHOSTTY_API GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator,
183 GhosttySgrParser* parser);
184
185/**
186 * Free an SGR parser instance.
187 *
188 * Releases all resources associated with the SGR parser. After this call,
189 * the parser handle becomes invalid and must not be used. This includes
190 * any attributes previously returned by ghostty_sgr_next().
191 *
192 * @param parser The parser handle to free (may be NULL)
193 *
194 * @ingroup sgr
195 */
196GHOSTTY_API void ghostty_sgr_free(GhosttySgrParser parser);
197
198/**
199 * Reset an SGR parser instance to the beginning of the parameter list.
200 *
201 * Resets the parser's iteration state without clearing the parameters.
202 * After calling this, ghostty_sgr_next() will start from the beginning
203 * of the parameter list again.
204 *
205 * @param parser The parser handle to reset, must not be NULL
206 *
207 * @ingroup sgr
208 */
209GHOSTTY_API void ghostty_sgr_reset(GhosttySgrParser parser);
210
211/**
212 * Set SGR parameters for parsing.
213 *
214 * Sets the SGR parameter list to parse. Parameters are the numeric values
215 * from a CSI SGR sequence (e.g., for `ESC[1;31m`, params would be {1, 31}).
216 *
217 * The separators array optionally specifies the separator type for each
218 * parameter position. Each byte should be either ';' for semicolon or ':'
219 * for colon. This is needed for certain color formats that use colon
220 * separators (e.g., `ESC[4:3m` for curly underline). Any invalid separator
221 * values are treated as semicolons. The separators array must have the same
222 * length as the params array, if it is not NULL.
223 *
224 * If separators is NULL, all parameters are assumed to be semicolon-separated.
225 *
226 * This function makes an internal copy of the parameter and separator data,
227 * so the caller can safely free or modify the input arrays after this call.
228 *
229 * After calling this function, the parser is automatically reset and ready
230 * to iterate from the beginning.
231 *
232 * @param parser The parser handle, must not be NULL
233 * @param params Array of SGR parameter values
234 * @param separators Optional array of separator characters (';' or ':'), or
235 * NULL
236 * @param len Number of parameters (and separators if provided)
237 * @return GHOSTTY_SUCCESS on success, or an error code on failure
238 *
239 * @ingroup sgr
240 */
241GHOSTTY_API GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser,
242 const uint16_t* params,
243 const char* separators,
244 size_t len);
245
246/**
247 * Get the next SGR attribute.
248 *
249 * Parses and returns the next attribute from the parameter list.
250 * Call this function repeatedly until it returns false to process
251 * all attributes in the sequence.
252 *
253 * @param parser The parser handle, must not be NULL
254 * @param attr Pointer to store the next attribute
255 * @return true if an attribute was returned, false if no more attributes
256 *
257 * @ingroup sgr
258 */
259GHOSTTY_API bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr);
260
261/**
262 * Get the full parameter list from an unknown SGR attribute.
263 *
264 * This function retrieves the full parameter list that was provided to the
265 * parser when an unknown attribute was encountered. Primarily useful in
266 * WebAssembly environments where accessing struct fields directly is difficult.
267 *
268 * @param unknown The unknown attribute data
269 * @param ptr Pointer to store the pointer to the parameter array (may be NULL)
270 * @return The length of the full parameter array
271 *
272 * @ingroup sgr
273 */
274GHOSTTY_API size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown,
275 const uint16_t** ptr);
276
277/**
278 * Get the partial parameter list from an unknown SGR attribute.
279 *
280 * This function retrieves the partial parameter list where parsing stopped
281 * when an unknown attribute was encountered. Primarily useful in WebAssembly
282 * environments where accessing struct fields directly is difficult.
283 *
284 * @param unknown The unknown attribute data
285 * @param ptr Pointer to store the pointer to the parameter array (may be NULL)
286 * @return The length of the partial parameter array
287 *
288 * @ingroup sgr
289 */
290GHOSTTY_API size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown,
291 const uint16_t** ptr);
292
293/**
294 * Get the tag from an SGR attribute.
295 *
296 * This function extracts the tag that identifies which type of attribute
297 * this is. Primarily useful in WebAssembly environments where accessing
298 * struct fields directly is difficult.
299 *
300 * @param attr The SGR attribute
301 * @return The attribute tag
302 *
303 * @ingroup sgr
304 */
305GHOSTTY_API GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr);
306
307/**
308 * Get the value from an SGR attribute.
309 *
310 * This function returns a pointer to the value union from an SGR attribute. Use
311 * the tag to determine which field of the union is valid. Primarily useful in
312 * WebAssembly environments where accessing struct fields directly is difficult.
313 *
314 * @param attr Pointer to the SGR attribute
315 * @return Pointer to the attribute value union
316 *
317 * @ingroup sgr
318 */
319GHOSTTY_API GhosttySgrAttributeValue* ghostty_sgr_attribute_value(
320 GhosttySgrAttribute* attr);
321
322#ifdef __wasm__
323/**
324 * Allocate memory for an SGR attribute (WebAssembly only).
325 *
326 * This is a convenience function for WebAssembly environments to allocate
327 * memory for an SGR attribute structure that can be passed to ghostty_sgr_next.
328 *
329 * @return Pointer to the allocated attribute structure
330 *
331 * @ingroup wasm
332 */
333GHOSTTY_API GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void);
334
335/**
336 * Free memory for an SGR attribute (WebAssembly only).
337 *
338 * Frees memory allocated by ghostty_wasm_alloc_sgr_attribute.
339 *
340 * @param attr Pointer to the attribute structure to free
341 *
342 * @ingroup wasm
343 */
344GHOSTTY_API void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr);
345#endif
346
347#ifdef __cplusplus
348}
349#endif
350
351/** @} */
352
353#endif /* GHOSTTY_VT_SGR_H */
packages/terminal/vendor/include/ghostty/vt/size_report.h created+101
......@@ -0,0 +1,101 @@
1/**
2 * @file size_report.h
3 *
4 * Size report encoding - encode terminal size reports into escape sequences.
5 */
6
7#ifndef GHOSTTY_VT_SIZE_REPORT_H
8#define GHOSTTY_VT_SIZE_REPORT_H
9
10/** @defgroup size_report Size Report Encoding
11 *
12 * Utilities for encoding terminal size reports into escape sequences,
13 * supporting in-band size reports (mode 2048) and XTWINOPS responses
14 * (CSI 14 t, CSI 16 t, CSI 18 t).
15 *
16 * ## Basic Usage
17 *
18 * Use ghostty_size_report_encode() to encode a size report into a
19 * caller-provided buffer. If the buffer is too small, the function
20 * returns GHOSTTY_OUT_OF_SPACE and sets the required size in the
21 * output parameter.
22 *
23 * ## Example
24 *
25 * @snippet c-vt-size-report/src/main.c size-report-encode
26 *
27 * @{
28 */
29
30#include <stddef.h>
31#include <stdint.h>
32#include <ghostty/vt/types.h>
33
34#ifdef __cplusplus
35extern "C" {
36#endif
37
38/**
39 * Size report style.
40 *
41 * Determines the output format for the terminal size report.
42 */
43typedef enum GHOSTTY_ENUM_TYPED {
44 /** In-band size report (mode 2048): ESC [ 48 ; rows ; cols ; height ; width t */
45 GHOSTTY_SIZE_REPORT_MODE_2048 = 0,
46 /** XTWINOPS text area size in pixels: ESC [ 4 ; height ; width t */
47 GHOSTTY_SIZE_REPORT_CSI_14_T = 1,
48 /** XTWINOPS cell size in pixels: ESC [ 6 ; height ; width t */
49 GHOSTTY_SIZE_REPORT_CSI_16_T = 2,
50 /** XTWINOPS text area size in characters: ESC [ 8 ; rows ; cols t */
51 GHOSTTY_SIZE_REPORT_CSI_18_T = 3,
52 GHOSTTY_SIZE_REPORT_STYLE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
53} GhosttySizeReportStyle;
54
55/**
56 * Terminal size information for encoding size reports.
57 */
58typedef struct {
59 /** Terminal row count in cells. */
60 uint16_t rows;
61 /** Terminal column count in cells. */
62 uint16_t columns;
63 /** Width of a single terminal cell in pixels. */
64 uint32_t cell_width;
65 /** Height of a single terminal cell in pixels. */
66 uint32_t cell_height;
67} GhosttySizeReportSize;
68
69/**
70 * Encode a terminal size report into an escape sequence.
71 *
72 * Encodes a size report in the format specified by @p style into the
73 * provided buffer.
74 *
75 * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE
76 * and writes the required buffer size to @p out_written. The caller can
77 * then retry with a sufficiently sized buffer.
78 *
79 * @param style The size report format to encode
80 * @param size Terminal size information
81 * @param buf Output buffer to write the encoded sequence into (may be NULL)
82 * @param buf_len Size of the output buffer in bytes
83 * @param[out] out_written On success, the number of bytes written. On
84 * GHOSTTY_OUT_OF_SPACE, the required buffer size.
85 * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer
86 * is too small
87 */
88GHOSTTY_API GhosttyResult ghostty_size_report_encode(
89 GhosttySizeReportStyle style,
90 GhosttySizeReportSize size,
91 char* buf,
92 size_t buf_len,
93 size_t* out_written);
94
95#ifdef __cplusplus
96}
97#endif
98
99/** @} */
100
101#endif /* GHOSTTY_VT_SIZE_REPORT_H */
packages/terminal/vendor/include/ghostty/vt/style.h created+139
......@@ -0,0 +1,139 @@
1/**
2 * @file style.h
3 *
4 * Terminal cell style types.
5 */
6
7#ifndef GHOSTTY_VT_STYLE_H
8#define GHOSTTY_VT_STYLE_H
9
10#include <ghostty/vt/color.h>
11#include <ghostty/vt/types.h>
12#include <stdbool.h>
13#include <stddef.h>
14#include <stdint.h>
15
16#ifdef __cplusplus
17extern "C" {
18#endif
19
20/** @defgroup style Style
21 *
22 * Terminal cell style attributes.
23 *
24 * A style describes the visual attributes of a terminal cell, including
25 * foreground, background, and underline colors, as well as flags for
26 * bold, italic, underline, and other text decorations.
27 *
28 * @{
29 */
30
31/**
32 * Style identifier type.
33 *
34 * Used to look up the full style from a grid reference.
35 * Obtain this from a cell via GHOSTTY_CELL_DATA_STYLE_ID.
36 *
37 * @ingroup style
38 */
39typedef uint16_t GhosttyStyleId;
40
41/**
42 * Style color tags.
43 *
44 * These values identify the type of color in a style color.
45 * Use the tag to determine which field in the color value union to access.
46 *
47 * @ingroup style
48 */
49typedef enum GHOSTTY_ENUM_TYPED {
50 GHOSTTY_STYLE_COLOR_NONE = 0,
51 GHOSTTY_STYLE_COLOR_PALETTE = 1,
52 GHOSTTY_STYLE_COLOR_RGB = 2,
53 GHOSTTY_STYLE_COLOR_TAG_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
54 } GhosttyStyleColorTag;
55
56/**
57 * Style color value union.
58 *
59 * Use the tag to determine which field is active.
60 *
61 * @ingroup style
62 */
63typedef union {
64 GhosttyColorPaletteIndex palette;
65 GhosttyColorRgb rgb;
66 uint64_t _padding;
67} GhosttyStyleColorValue;
68
69/**
70 * Style color (tagged union).
71 *
72 * A color used in a style attribute. Can be unset (none), a palette
73 * index, or a direct RGB value.
74 *
75 * @ingroup style
76 */
77typedef struct {
78 GhosttyStyleColorTag tag;
79 GhosttyStyleColorValue value;
80} GhosttyStyleColor;
81
82/**
83 * Terminal cell style.
84 *
85 * Describes the complete visual style for a terminal cell, including
86 * foreground, background, and underline colors, as well as text
87 * decoration flags. The underline field uses the same values as
88 * GhosttySgrUnderline.
89 *
90 * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
91 *
92 * @ingroup style
93 */
94typedef struct {
95 size_t size;
96 GhosttyStyleColor fg_color;
97 GhosttyStyleColor bg_color;
98 GhosttyStyleColor underline_color;
99 bool bold;
100 bool italic;
101 bool faint;
102 bool blink;
103 bool inverse;
104 bool invisible;
105 bool strikethrough;
106 bool overline;
107 int underline; /**< One of GHOSTTY_SGR_UNDERLINE_* values */
108} GhosttyStyle;
109
110/**
111 * Get the default style.
112 *
113 * Initializes the style to the default values (no colors, no flags).
114 *
115 * @param style Pointer to the style to initialize
116 *
117 * @ingroup style
118 */
119GHOSTTY_API void ghostty_style_default(GhosttyStyle* style);
120
121/**
122 * Check if a style is the default style.
123 *
124 * Returns true if all colors are unset and all flags are off.
125 *
126 * @param style Pointer to the style to check
127 * @return true if the style is the default style
128 *
129 * @ingroup style
130 */
131GHOSTTY_API bool ghostty_style_is_default(const GhosttyStyle* style);
132
133#ifdef __cplusplus
134}
135#endif
136
137/** @} */
138
139#endif /* GHOSTTY_VT_STYLE_H */
packages/terminal/vendor/include/ghostty/vt/sys.h created+210
......@@ -0,0 +1,210 @@
1/**
2 * @file sys.h
3 *
4 * System interface - runtime-swappable implementations for external dependencies.
5 */
6
7#ifndef GHOSTTY_VT_SYS_H
8#define GHOSTTY_VT_SYS_H
9
10#include <stdbool.h>
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/types.h>
14#include <ghostty/vt/allocator.h>
15
16/** @defgroup sys System Interface
17 *
18 * Runtime-swappable function pointers for operations that depend on
19 * external implementations (e.g. image decoding).
20 *
21 * These are process-global settings that must be configured at startup
22 * before any terminal functionality that depends on them is used.
23 * Setting these enables various optional features of the terminal. For
24 * example, setting a PNG decoder enables PNG image support in the Kitty
25 * Graphics Protocol.
26 *
27 * Use ghostty_sys_set() with a `GhosttySysOption` to install or clear
28 * an implementation. Passing NULL as the value clears the implementation
29 * and disables the corresponding feature.
30 *
31 * ## Example
32 *
33 * ### Defining a PNG decode callback
34 * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-decode-png
35 *
36 * ### Installing the callback and sending a PNG image
37 * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-main
38 *
39 * @{
40 */
41
42#ifdef __cplusplus
43extern "C" {
44#endif
45
46/**
47 * Result of decoding an image.
48 *
49 * The `data` buffer must be allocated through the allocator provided to
50 * the decode callback. The library takes ownership and will free it
51 * with the same allocator.
52 */
53typedef struct {
54 /** Image width in pixels. */
55 uint32_t width;
56
57 /** Image height in pixels. */
58 uint32_t height;
59
60 /** Pointer to the decoded RGBA pixel data. */
61 uint8_t* data;
62
63 /** Length of the pixel data in bytes. */
64 size_t data_len;
65} GhosttySysImage;
66
67/**
68 * Log severity levels for the log callback.
69 */
70typedef enum GHOSTTY_ENUM_TYPED {
71 GHOSTTY_SYS_LOG_LEVEL_ERROR = 0,
72 GHOSTTY_SYS_LOG_LEVEL_WARNING = 1,
73 GHOSTTY_SYS_LOG_LEVEL_INFO = 2,
74 GHOSTTY_SYS_LOG_LEVEL_DEBUG = 3,
75 GHOSTTY_SYS_LOG_LEVEL_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
76} GhosttySysLogLevel;
77
78/**
79 * Callback type for logging.
80 *
81 * When installed, internal library log messages are delivered through
82 * this callback instead of being discarded. The embedder is responsible
83 * for formatting and routing log output.
84 *
85 * @p scope is the log scope name as UTF-8 bytes (e.g. "osc", "kitty").
86 * When the log is unscoped (default scope), @p scope_len is 0.
87 *
88 * All pointer arguments are only valid for the duration of the callback.
89 * The callback must be safe to call from any thread.
90 *
91 * @param userdata The userdata pointer set via GHOSTTY_SYS_OPT_USERDATA
92 * @param level The severity level of the log message
93 * @param scope Pointer to the scope name bytes
94 * @param scope_len Length of the scope name in bytes
95 * @param message Pointer to the log message bytes
96 * @param message_len Length of the log message in bytes
97 */
98typedef void (*GhosttySysLogFn)(
99 void* userdata,
100 GhosttySysLogLevel level,
101 const uint8_t* scope,
102 size_t scope_len,
103 const uint8_t* message,
104 size_t message_len);
105
106/**
107 * Callback type for PNG decoding.
108 *
109 * Decodes raw PNG data into RGBA pixels. The output pixel data must be
110 * allocated through the provided allocator. The library takes ownership
111 * of the buffer and will free it with the same allocator.
112 *
113 * @param userdata The userdata pointer set via GHOSTTY_SYS_OPT_USERDATA
114 * @param allocator The allocator to use for the output pixel buffer
115 * @param data Pointer to the raw PNG data
116 * @param data_len Length of the raw PNG data in bytes
117 * @param[out] out On success, filled with the decoded image
118 * @return true on success, false on failure
119 */
120typedef bool (*GhosttySysDecodePngFn)(
121 void* userdata,
122 const GhosttyAllocator* allocator,
123 const uint8_t* data,
124 size_t data_len,
125 GhosttySysImage* out);
126
127/**
128 * System option identifiers for ghostty_sys_set().
129 */
130typedef enum GHOSTTY_ENUM_TYPED {
131 /**
132 * Set the userdata pointer passed to all sys callbacks.
133 *
134 * Input type: void* (or NULL)
135 */
136 GHOSTTY_SYS_OPT_USERDATA = 0,
137
138 /**
139 * Set the PNG decode function.
140 *
141 * When set, the terminal can accept PNG images via the Kitty
142 * Graphics Protocol. When cleared (NULL value), PNG decoding is
143 * unsupported and PNG image data will be rejected.
144 *
145 * Input type: GhosttySysDecodePngFn (function pointer, or NULL)
146 */
147 GHOSTTY_SYS_OPT_DECODE_PNG = 1,
148
149 /**
150 * Set the log callback.
151 *
152 * When set, internal library log messages are delivered to this
153 * callback. When cleared (NULL value), log messages are silently
154 * discarded.
155 *
156 * Use ghostty_sys_log_stderr as a convenience callback that
157 * writes formatted messages to stderr.
158 *
159 * Which log levels are emitted depends on the build mode of the
160 * library and is not configurable at runtime. Debug builds emit
161 * all levels (debug and above). Release builds emit info and
162 * above; debug-level messages are compiled out entirely and will
163 * never reach the callback.
164 *
165 * Input type: GhosttySysLogFn (function pointer, or NULL)
166 */
167 GHOSTTY_SYS_OPT_LOG = 2,
168 GHOSTTY_SYS_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
169} GhosttySysOption;
170
171/**
172 * Set a system-level option.
173 *
174 * Configures a process-global implementation function. These should be
175 * set once at startup before using any terminal functionality that
176 * depends on them.
177 *
178 * @param option The option to set
179 * @param value Pointer to the value (type depends on the option),
180 * or NULL to clear it
181 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
182 * option is not recognized
183 */
184GHOSTTY_API GhosttyResult ghostty_sys_set(GhosttySysOption option,
185 const void* value);
186
187/**
188 * Built-in log callback that writes to stderr.
189 *
190 * Formats each message as "[level](scope): message\n".
191 * Can be passed directly to ghostty_sys_set():
192 *
193 * @code
194 * ghostty_sys_set(GHOSTTY_SYS_OPT_LOG, &ghostty_sys_log_stderr);
195 * @endcode
196 */
197GHOSTTY_API void ghostty_sys_log_stderr(void* userdata,
198 GhosttySysLogLevel level,
199 const uint8_t* scope,
200 size_t scope_len,
201 const uint8_t* message,
202 size_t message_len);
203
204#ifdef __cplusplus
205}
206#endif
207
208/** @} */
209
210#endif /* GHOSTTY_VT_SYS_H */
packages/terminal/vendor/include/ghostty/vt/terminal.h created+1598
......@@ -0,0 +1,1598 @@
1/**
2 * @file terminal.h
3 *
4 * Complete terminal emulator state and rendering.
5 */
6
7#ifndef GHOSTTY_VT_TERMINAL_H
8#define GHOSTTY_VT_TERMINAL_H
9
10#include <stdbool.h>
11#include <stddef.h>
12#include <stdint.h>
13#include <ghostty/vt/types.h>
14#include <ghostty/vt/allocator.h>
15#include <ghostty/vt/device.h>
16#include <ghostty/vt/modes.h>
17#include <ghostty/vt/size_report.h>
18#include <ghostty/vt/grid_ref.h>
19#include <ghostty/vt/kitty_graphics.h>
20#include <ghostty/vt/screen.h>
21#include <ghostty/vt/point.h>
22#include <ghostty/vt/selection.h>
23#include <ghostty/vt/style.h>
24
25#ifdef __cplusplus
26extern "C" {
27#endif
28
29/** @defgroup terminal Terminal
30 *
31 * Complete terminal emulator state and rendering.
32 *
33 * A terminal instance manages the full emulator state including the screen,
34 * scrollback, cursor, styles, modes, and VT stream processing.
35 *
36 * Once a terminal session is up and running, you can configure a key encoder
37 * to write keyboard input via ghostty_key_encoder_setopt_from_terminal().
38 *
39 * ### Example: VT stream processing
40 * @snippet c-vt-stream/src/main.c vt-stream-init
41 * @snippet c-vt-stream/src/main.c vt-stream-write
42 *
43 * ## Scrollback Compression
44 *
45 * Scrollback compression is caller-driven. The terminal exposes an opaque
46 * activity token so an embedding application can restart an idle timer only
47 * when compression-relevant state changes. Once idle, call incremental
48 * compression until it no longer reports pending work. libghostty-vt does not
49 * create a timer or background thread.
50 *
51 * @snippet c-vt-compression/src/main.c compression-activity
52 * @snippet c-vt-compression/src/main.c compression-idle-step
53 *
54 * ## Effects
55 *
56 * By default, the terminal sequence processing with ghostty_terminal_vt_write()
57 * only process sequences that directly affect terminal state and
58 * ignores sequences that have side effect behavior or require responses.
59 * These sequences include things like bell characters, title changes, device
60 * attributes queries, and more. To handle these sequences, the embedder
61 * must configure "effects."
62 *
63 * Effects are callbacks that the terminal invokes in response to VT
64 * sequences processed during ghostty_terminal_vt_write(). They let the
65 * embedding application react to terminal-initiated events such as bell
66 * characters, title changes, device status report responses, and more.
67 *
68 * Each effect is registered with ghostty_terminal_set() using the
69 * corresponding `GhosttyTerminalOption` identifier. A `NULL` value
70 * pointer clears the callback and disables the effect.
71 *
72 * A userdata pointer can be attached via `GHOSTTY_TERMINAL_OPT_USERDATA`
73 * and is passed to every callback, allowing callers to route events
74 * back to their own application state without global variables.
75 * You cannot specify different userdata for different callbacks.
76 *
77 * All callbacks are invoked synchronously during
78 * ghostty_terminal_vt_write(). Callbacks **must not** call
79 * ghostty_terminal_vt_write() on the same terminal (no reentrancy).
80 * And callbacks must be very careful to not block for too long or perform
81 * expensive operations, since they are blocking further IO processing.
82 *
83 * The available effects are:
84 *
85 * | Option | Callback Type | Trigger |
86 * |-----------------------------------------|-----------------------------------|-------------------------------------------|
87 * | `GHOSTTY_TERMINAL_OPT_WRITE_PTY` | `GhosttyTerminalWritePtyFn` | Query responses written back to the pty |
88 * | `GHOSTTY_TERMINAL_OPT_BELL` | `GhosttyTerminalBellFn` | BEL character (0x07) |
89 * | `GHOSTTY_TERMINAL_OPT_TITLE_CHANGED` | `GhosttyTerminalTitleChangedFn` | Title change via OSC 0 / OSC 2 |
90 * | `GHOSTTY_TERMINAL_OPT_PWD_CHANGED` | `GhosttyTerminalPwdChangedFn` | Pwd change via OSC 7 / OSC 9 / OSC 1337 |
91 * | `GHOSTTY_TERMINAL_OPT_ENQUIRY` | `GhosttyTerminalEnquiryFn` | ENQ character (0x05) |
92 * | `GHOSTTY_TERMINAL_OPT_XTVERSION` | `GhosttyTerminalXtversionFn` | XTVERSION query (CSI > q) |
93 * | `GHOSTTY_TERMINAL_OPT_SIZE` | `GhosttyTerminalSizeFn` | XTWINOPS size query (CSI 14/16/18 t) |
94 * | `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME` | `GhosttyTerminalColorSchemeFn` | Color scheme query (CSI ? 996 n) |
95 * | `GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES`| `GhosttyTerminalDeviceAttributesFn`| Device attributes query (CSI c / > c / = c)|
96 * | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE` | `GhosttyTerminalClipboardWriteFn` | Clipboard write via OSC 52 / OSC 1337 |
97 *
98 * ### Defining a write_pty callback
99 * @snippet c-vt-effects/src/main.c effects-write-pty
100 *
101 * ### Defining a bell callback
102 * @snippet c-vt-effects/src/main.c effects-bell
103 *
104 * ### Defining a title_changed callback
105 * @snippet c-vt-effects/src/main.c effects-title-changed
106 *
107 * ### Defining a clipboard_write callback
108 * @snippet c-vt-effects/src/main.c effects-clipboard-write
109 *
110 * ### Registering effects and processing VT data
111 * @snippet c-vt-effects/src/main.c effects-register
112 *
113 * ## Color Theme
114 *
115 * The terminal maintains a set of colors used for rendering: a foreground
116 * color, a background color, a cursor color, and a 256-color palette. Each
117 * of these has two layers: a **default** value set by the embedder, and an
118 * **override** value that programs running in the terminal can set via OSC
119 * escape sequences (e.g. OSC 10/11/12 for foreground/background/cursor,
120 * OSC 4 for individual palette entries).
121 *
122 * ### Default Colors
123 *
124 * Use ghostty_terminal_set() with the color options to configure the
125 * default colors. These represent the theme or configuration chosen by
126 * the embedder. Passing `NULL` clears the default, leaving the color
127 * unset.
128 *
129 * | Option | Input Type | Description |
130 * |-----------------------------------------|-------------------------|--------------------------------------|
131 * | `GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND` | `GhosttyColorRgb*` | Default foreground color |
132 * | `GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND` | `GhosttyColorRgb*` | Default background color |
133 * | `GHOSTTY_TERMINAL_OPT_COLOR_CURSOR` | `GhosttyColorRgb*` | Default cursor color |
134 * | `GHOSTTY_TERMINAL_OPT_COLOR_PALETTE` | `GhosttyColorRgb[256]*` | Default 256-color palette |
135 *
136 * For the palette, passing `NULL` resets to the built-in default palette.
137 * The palette set operation preserves any per-index OSC overrides that
138 * programs have applied; only unmodified indices are updated.
139 *
140 * ### Reading colors
141 *
142 * Use ghostty_terminal_get() to read colors. There are two variants for
143 * each color: the **effective** value (which returns the OSC override if
144 * one is active, otherwise the default) and the **default** value (which
145 * ignores any OSC overrides).
146 *
147 * | Data | Output Type | Description |
148 * |---------------------------------------------------|-------------------------|------------------------------------------------|
149 * | `GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND` | `GhosttyColorRgb*` | Effective foreground (override or default) |
150 * | `GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND` | `GhosttyColorRgb*` | Effective background (override or default) |
151 * | `GHOSTTY_TERMINAL_DATA_COLOR_CURSOR` | `GhosttyColorRgb*` | Effective cursor (override or default) |
152 * | `GHOSTTY_TERMINAL_DATA_COLOR_PALETTE` | `GhosttyColorRgb[256]*` | Current palette (with any OSC overrides) |
153 * | `GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT` | `GhosttyColorRgb*` | Default foreground only (ignores OSC override) |
154 * | `GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT` | `GhosttyColorRgb*` | Default background only (ignores OSC override) |
155 * | `GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT` | `GhosttyColorRgb*` | Default cursor only (ignores OSC override) |
156 * | `GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT` | `GhosttyColorRgb[256]*` | Default palette only (ignores OSC overrides) |
157 *
158 * For foreground, background, and cursor colors, the getters return
159 * `GHOSTTY_NO_VALUE` if no color is configured (neither a default nor an
160 * OSC override). The palette getters always succeed since the palette
161 * always has a value (the built-in default if nothing else is set).
162 *
163 * ### Setting a color theme
164 * @snippet c-vt-colors/src/main.c colors-set-defaults
165 *
166 * ### Reading effective and default colors
167 * @snippet c-vt-colors/src/main.c colors-read
168 *
169 * ### Full example with OSC overrides
170 * @snippet c-vt-colors/src/main.c colors-main
171 *
172 * @{
173 */
174
175/**
176 * Terminal initialization options.
177 *
178 * @ingroup terminal
179 */
180typedef struct {
181 /** Terminal width in cells. Must be greater than zero. */
182 uint16_t cols;
183
184 /** Terminal height in cells. Must be greater than zero. */
185 uint16_t rows;
186
187 /** Maximum number of lines to keep in scrollback history. */
188 size_t max_scrollback;
189
190 // TODO: Consider ABI compatibility implications of this struct.
191 // We may want to artificially pad it significantly to support
192 // future options.
193} GhosttyTerminalOptions;
194
195/**
196 * Amount of compression work to perform before returning.
197 *
198 * @ingroup terminal
199 */
200typedef enum GHOSTTY_ENUM_TYPED {
201 /** Perform one bounded compression step suitable for idle scheduling. */
202 GHOSTTY_TERMINAL_COMPRESSION_MODE_INCREMENTAL = 0,
203
204 /** Synchronously inspect every currently eligible page. */
205 GHOSTTY_TERMINAL_COMPRESSION_MODE_FULL = 1,
206 GHOSTTY_TERMINAL_COMPRESSION_MODE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
207} GhosttyTerminalCompressionMode;
208
209/**
210 * Scheduling result from terminal compression.
211 *
212 * @ingroup terminal
213 */
214typedef enum GHOSTTY_ENUM_TYPED {
215 /** Retained-mapping reclamation is unavailable on this target. */
216 GHOSTTY_TERMINAL_COMPRESSION_RESULT_UNSUPPORTED = 0,
217
218 /** More incremental compression work remains. */
219 GHOSTTY_TERMINAL_COMPRESSION_RESULT_PENDING = 1,
220
221 /** The pass has no continuation to schedule. */
222 GHOSTTY_TERMINAL_COMPRESSION_RESULT_COMPLETE = 2,
223 GHOSTTY_TERMINAL_COMPRESSION_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
224} GhosttyTerminalCompressionResult;
225
226/**
227 * Scroll viewport behavior tag.
228 *
229 * @ingroup terminal
230 */
231typedef enum GHOSTTY_ENUM_TYPED {
232 /** Scroll to the top of the scrollback. */
233 GHOSTTY_SCROLL_VIEWPORT_TOP,
234
235 /** Scroll to the bottom (active area). */
236 GHOSTTY_SCROLL_VIEWPORT_BOTTOM,
237
238 /** Scroll by a delta amount (up is negative). */
239 GHOSTTY_SCROLL_VIEWPORT_DELTA,
240
241 /**
242 * Scroll to an absolute row offset from the top of the scrollable
243 * area. Row 0 is the top of the scrollback and the requested row
244 * becomes the first visible row of the viewport. The value is
245 * clamped so the viewport never scrolls beyond the top of the
246 * active area. If the terminal has no scrollback (e.g. the
247 * alternate screen is active), the viewport always remains on the
248 * active area.
249 *
250 * This is the same row space as the offset field of
251 * GhosttyTerminalScrollbar, so a scrollbar position obtained from
252 * GHOSTTY_TERMINAL_DATA_SCROLLBAR round-trips cleanly.
253 */
254 GHOSTTY_SCROLL_VIEWPORT_ROW,
255 GHOSTTY_SCROLL_VIEWPORT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
256} GhosttyTerminalScrollViewportTag;
257
258/**
259 * Scroll viewport value.
260 *
261 * @ingroup terminal
262 */
263typedef union {
264 /** Scroll delta (only used with GHOSTTY_SCROLL_VIEWPORT_DELTA). Up is negative. */
265 intptr_t delta;
266
267 /** Absolute row offset (only used with GHOSTTY_SCROLL_VIEWPORT_ROW). */
268 size_t row;
269
270 /** Padding for ABI compatibility. Do not use. */
271 uint64_t _padding[2];
272} GhosttyTerminalScrollViewportValue;
273
274/**
275 * Tagged union for scroll viewport behavior.
276 *
277 * @ingroup terminal
278 */
279typedef struct {
280 GhosttyTerminalScrollViewportTag tag;
281 GhosttyTerminalScrollViewportValue value;
282} GhosttyTerminalScrollViewport;
283
284/**
285 * Terminal screen identifier.
286 *
287 * Identifies which screen buffer is active in the terminal.
288 *
289 * @ingroup terminal
290 */
291typedef enum GHOSTTY_ENUM_TYPED {
292 /** The primary (normal) screen. */
293 GHOSTTY_TERMINAL_SCREEN_PRIMARY = 0,
294
295 /** The alternate screen. */
296 GHOSTTY_TERMINAL_SCREEN_ALTERNATE = 1,
297 GHOSTTY_TERMINAL_SCREEN_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
298} GhosttyTerminalScreen;
299
300/**
301 * Visual style of the terminal cursor.
302 *
303 * @ingroup terminal
304 */
305typedef enum GHOSTTY_ENUM_TYPED {
306 /** Bar cursor (DECSCUSR 5, 6). */
307 GHOSTTY_TERMINAL_CURSOR_STYLE_BAR = 0,
308
309 /** Block cursor (DECSCUSR 1, 2). */
310 GHOSTTY_TERMINAL_CURSOR_STYLE_BLOCK = 1,
311
312 /** Underline cursor (DECSCUSR 3, 4). */
313 GHOSTTY_TERMINAL_CURSOR_STYLE_UNDERLINE = 2,
314
315 /** Hollow block cursor. */
316 GHOSTTY_TERMINAL_CURSOR_STYLE_BLOCK_HOLLOW = 3,
317 GHOSTTY_TERMINAL_CURSOR_STYLE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
318} GhosttyTerminalCursorStyle;
319
320/**
321 * Scrollbar state for the terminal viewport.
322 *
323 * Represents the scrollable area dimensions needed to render a scrollbar.
324 *
325 * @ingroup terminal
326 */
327typedef struct {
328 /** Total size of the scrollable area in rows. */
329 uint64_t total;
330
331 /** Offset into the total area that the viewport is at. */
332 uint64_t offset;
333
334 /** Length of the visible area in rows. */
335 uint64_t len;
336} GhosttyTerminalScrollbar;
337
338/**
339 * Callback function type for bell.
340 *
341 * Called when the terminal receives a BEL character (0x07).
342 *
343 * @param terminal The terminal handle
344 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
345 *
346 * @ingroup terminal
347 */
348typedef void (*GhosttyTerminalBellFn)(GhosttyTerminal terminal,
349 void* userdata);
350
351/**
352 * Clipboard destination for a clipboard write.
353 *
354 * Protocol-specific destination identifiers are normalized to these values
355 * before the clipboard write callback is invoked.
356 *
357 * @ingroup terminal
358 */
359typedef enum GHOSTTY_ENUM_TYPED {
360 /** The standard system clipboard. */
361 GHOSTTY_CLIPBOARD_LOCATION_STANDARD = 0,
362
363 /** The selection clipboard. */
364 GHOSTTY_CLIPBOARD_LOCATION_SELECTION = 1,
365
366 /** The primary selection clipboard. */
367 GHOSTTY_CLIPBOARD_LOCATION_PRIMARY = 2,
368 GHOSTTY_CLIPBOARD_LOCATION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
369} GhosttyClipboardLocation;
370
371/**
372 * One MIME representation in a clipboard write.
373 *
374 * Both strings are borrowed and valid only for the duration of the callback.
375 * The data is binary-safe and has already been decoded from any protocol-level
376 * encoding. A zero-length data string is an explicit empty representation; it
377 * does not clear the clipboard.
378 *
379 * This struct has a frozen layout and will not gain fields in future versions.
380 *
381 * @ingroup terminal
382 */
383typedef struct {
384 /** MIME type of the representation. */
385 GhosttyString mime;
386
387 /** Decoded, binary-safe representation data. */
388 GhosttyString data;
389} GhosttyClipboardContent;
390
391/**
392 * A semantic, atomic clipboard write.
393 *
394 * This is a sized struct. The callback must only access fields present in the
395 * size reported by `size`. The request, contents array, MIME strings, and
396 * data strings are all borrowed and valid only for the callback duration.
397 *
398 * All entries in `contents` are representations of the same logical value
399 * and must be committed atomically. A `contents_len` of zero requests that
400 * the destination be cleared. This is distinct from a content entry whose data
401 * has zero length.
402 *
403 * @ingroup terminal
404 */
405typedef struct {
406 /** Size of this struct in bytes. */
407 size_t size;
408
409 /** Clipboard destination. */
410 GhosttyClipboardLocation location;
411
412 /** Borrowed array of MIME representations. */
413 const GhosttyClipboardContent* contents;
414
415 /** Number of entries in contents; zero means clear the destination. */
416 size_t contents_len;
417} GhosttyClipboardWrite;
418
419/**
420 * Result of a clipboard write callback.
421 *
422 * Protocols without write acknowledgements, including OSC 52 and iTerm2
423 * OSC 1337 Copy, ignore this result.
424 *
425 * @ingroup terminal
426 */
427typedef enum GHOSTTY_ENUM_TYPED {
428 /** The clipboard write completed successfully. */
429 GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS = 0,
430
431 /** The clipboard write was denied by policy or the user. */
432 GHOSTTY_CLIPBOARD_WRITE_RESULT_DENIED = 1,
433
434 /** The destination or one or more representations are unsupported. */
435 GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED = 2,
436
437 /** The clipboard is temporarily unavailable. */
438 GHOSTTY_CLIPBOARD_WRITE_RESULT_BUSY = 3,
439
440 /** One or more representations contain invalid data. */
441 GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA = 4,
442
443 /** The clipboard write failed due to an I/O error. */
444 GHOSTTY_CLIPBOARD_WRITE_RESULT_IO_ERROR = 5,
445 GHOSTTY_CLIPBOARD_WRITE_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
446} GhosttyClipboardWriteResult;
447
448/**
449 * Callback function type for clipboard_write.
450 *
451 * Called synchronously for a complete logical clipboard write. Protocol
452 * details such as OSC 52 selectors, base64 encoding, multipart chunks,
453 * aliases, and terminators are normalized before this callback is invoked.
454 * OSC 52 and iTerm2 OSC 1337 Copy writes therefore use the same callback
455 * shape. OSC 52 clipboard read requests ("?") are always ignored and never
456 * forwarded to this callback.
457 *
458 * @param terminal The terminal handle
459 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
460 * @param write Borrowed atomic clipboard write request
461 * @return The result of attempting the clipboard write
462 *
463 * @ingroup terminal
464 */
465typedef GhosttyClipboardWriteResult (*GhosttyTerminalClipboardWriteFn)(
466 GhosttyTerminal terminal,
467 void* userdata,
468 const GhosttyClipboardWrite* write);
469
470/**
471 * Callback function type for color scheme queries (CSI ? 996 n).
472 *
473 * Called when the terminal receives a color scheme device status report
474 * query. Return true and fill *out_scheme with the current color scheme,
475 * or return false to silently ignore the query.
476 *
477 * @param terminal The terminal handle
478 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
479 * @param[out] out_scheme Pointer to store the current color scheme
480 * @return true if the color scheme was filled, false to ignore the query
481 *
482 * @ingroup terminal
483 */
484typedef bool (*GhosttyTerminalColorSchemeFn)(GhosttyTerminal terminal,
485 void* userdata,
486 GhosttyColorScheme* out_scheme);
487
488/**
489 * Callback function type for device attributes queries (DA1/DA2/DA3).
490 *
491 * Called when the terminal receives a device attributes query (CSI c,
492 * CSI > c, or CSI = c). Return true and fill *out_attrs with the
493 * response data, or return false to silently ignore the query.
494 *
495 * The terminal uses whichever sub-struct (primary, secondary, tertiary)
496 * matches the request type, but all three should be filled for simplicity.
497 *
498 * @param terminal The terminal handle
499 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
500 * @param[out] out_attrs Pointer to store the device attributes response
501 * @return true if attributes were filled, false to ignore the query
502 *
503 * @ingroup terminal
504 */
505typedef bool (*GhosttyTerminalDeviceAttributesFn)(GhosttyTerminal terminal,
506 void* userdata,
507 GhosttyDeviceAttributes* out_attrs);
508
509/**
510 * Callback function type for enquiry (ENQ, 0x05).
511 *
512 * Called when the terminal receives an ENQ character. Return the
513 * response bytes as a GhosttyString. The memory must remain valid
514 * until the callback returns. Return a zero-length string to send
515 * no response.
516 *
517 * @param terminal The terminal handle
518 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
519 * @return The response bytes to write back to the pty
520 *
521 * @ingroup terminal
522 */
523typedef GhosttyString (*GhosttyTerminalEnquiryFn)(GhosttyTerminal terminal,
524 void* userdata);
525
526/**
527 * Callback function type for size queries (XTWINOPS).
528 *
529 * Called in response to XTWINOPS size queries (CSI 14/16/18 t).
530 * Return true and fill *out_size with the current terminal geometry,
531 * or return false to silently ignore the query.
532 *
533 * @param terminal The terminal handle
534 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
535 * @param[out] out_size Pointer to store the terminal size information
536 * @return true if size was filled, false to ignore the query
537 *
538 * @ingroup terminal
539 */
540typedef bool (*GhosttyTerminalSizeFn)(GhosttyTerminal terminal,
541 void* userdata,
542 GhosttySizeReportSize* out_size);
543
544/**
545 * Callback function type for title_changed.
546 *
547 * Called when the terminal title changes via escape sequences
548 * (e.g. OSC 0 or OSC 2). The new title can be queried from the
549 * terminal after the callback returns.
550 *
551 * @param terminal The terminal handle
552 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
553 *
554 * @ingroup terminal
555 */
556typedef void (*GhosttyTerminalTitleChangedFn)(GhosttyTerminal terminal,
557 void* userdata);
558
559/**
560 * Callback function type for pwd_changed.
561 *
562 * Called when the terminal pwd (current working directory) changes via
563 * escape sequences: OSC 7 (file:// URI), OSC 9 (ConEmu CurrentDir), or
564 * OSC 1337 CurrentDir (iTerm2). Use ghostty_terminal_get() with
565 * GHOSTTY_TERMINAL_DATA_PWD inside the callback to read the new value.
566 *
567 * The terminal stores whatever bytes the shell emitted, without parsing.
568 * That means for OSC 7 the value is the raw URI (typically file://...);
569 * for OSC 9/OSC 1337 it is typically a bare path. The embedder is
570 * responsible for decoding any URI scheme or host if it cares about them.
571 *
572 * The callback also fires when the shell clears the pwd (e.g. an empty
573 * OSC 7). In that case GHOSTTY_TERMINAL_DATA_PWD returns a zero-length
574 * string.
575 *
576 * @param terminal The terminal handle
577 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
578 *
579 * @ingroup terminal
580 */
581typedef void (*GhosttyTerminalPwdChangedFn)(GhosttyTerminal terminal,
582 void* userdata);
583
584/**
585 * Callback function type for write_pty.
586 *
587 * Called when the terminal needs to write data back to the pty, for
588 * example in response to a device status report or mode query. The
589 * data is only valid for the duration of the call; callers must copy
590 * it if it needs to persist.
591 *
592 * @param terminal The terminal handle
593 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
594 * @param data Pointer to the response bytes
595 * @param len Length of the response in bytes
596 *
597 * @ingroup terminal
598 */
599typedef void (*GhosttyTerminalWritePtyFn)(GhosttyTerminal terminal,
600 void* userdata,
601 const uint8_t* data,
602 size_t len);
603
604/**
605 * Callback function type for XTVERSION.
606 *
607 * Called when the terminal receives an XTVERSION query (CSI > q).
608 * Return the version string (e.g. "myterm 1.0") as a GhosttyString.
609 * The memory must remain valid until the callback returns. Return a
610 * zero-length string to report the default "libghostty" version.
611 *
612 * @param terminal The terminal handle
613 * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
614 * @return The version string to report
615 *
616 * @ingroup terminal
617 */
618typedef GhosttyString (*GhosttyTerminalXtversionFn)(GhosttyTerminal terminal,
619 void* userdata);
620
621/**
622 * Terminal option identifiers.
623 *
624 * These values are used with ghostty_terminal_set() to configure
625 * terminal callbacks and associated state.
626 *
627 * @ingroup terminal
628 */
629typedef enum GHOSTTY_ENUM_TYPED {
630 /**
631 * Opaque userdata pointer passed to all callbacks.
632 *
633 * Input type: void*
634 */
635 GHOSTTY_TERMINAL_OPT_USERDATA = 0,
636
637 /**
638 * Callback invoked when the terminal needs to write data back
639 * to the pty (e.g. in response to a DECRQM query or device
640 * status report). Set to NULL to ignore such sequences.
641 *
642 * Input type: GhosttyTerminalWritePtyFn
643 */
644 GHOSTTY_TERMINAL_OPT_WRITE_PTY = 1,
645
646 /**
647 * Callback invoked when the terminal receives a BEL character
648 * (0x07). Set to NULL to ignore bell events.
649 *
650 * Input type: GhosttyTerminalBellFn
651 */
652 GHOSTTY_TERMINAL_OPT_BELL = 2,
653
654 /**
655 * Callback invoked when the terminal receives an ENQ character
656 * (0x05). Set to NULL to send no response.
657 *
658 * Input type: GhosttyTerminalEnquiryFn
659 */
660 GHOSTTY_TERMINAL_OPT_ENQUIRY = 3,
661
662 /**
663 * Callback invoked when the terminal receives an XTVERSION query
664 * (CSI > q). Set to NULL to report the default "libghostty" string.
665 *
666 * Input type: GhosttyTerminalXtversionFn
667 */
668 GHOSTTY_TERMINAL_OPT_XTVERSION = 4,
669
670 /**
671 * Callback invoked when the terminal title changes via escape
672 * sequences (e.g. OSC 0 or OSC 2). Set to NULL to ignore title
673 * change events.
674 *
675 * Input type: GhosttyTerminalTitleChangedFn
676 */
677 GHOSTTY_TERMINAL_OPT_TITLE_CHANGED = 5,
678
679 /**
680 * Callback invoked in response to XTWINOPS size queries
681 * (CSI 14/16/18 t). Set to NULL to silently ignore size queries.
682 *
683 * Input type: GhosttyTerminalSizeFn
684 */
685 GHOSTTY_TERMINAL_OPT_SIZE = 6,
686
687 /**
688 * Callback invoked in response to a color scheme device status
689 * report query (CSI ? 996 n). Return true and fill the out pointer
690 * to report the current scheme, or return false to silently ignore.
691 * Set to NULL to ignore color scheme queries.
692 *
693 * Input type: GhosttyTerminalColorSchemeFn
694 */
695 GHOSTTY_TERMINAL_OPT_COLOR_SCHEME = 7,
696
697 /**
698 * Callback invoked in response to a device attributes query
699 * (CSI c, CSI > c, or CSI = c). Return true and fill the out
700 * pointer with response data, or return false to silently ignore.
701 * Set to NULL to ignore device attributes queries.
702 *
703 * Input type: GhosttyTerminalDeviceAttributesFn
704 */
705 GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES = 8,
706
707 /**
708 * Set the terminal title manually.
709 *
710 * The string data is copied into the terminal. A NULL value pointer
711 * clears the title (equivalent to setting an empty string).
712 *
713 * Input type: GhosttyString*
714 */
715 GHOSTTY_TERMINAL_OPT_TITLE = 9,
716
717 /**
718 * Set the terminal working directory manually.
719 *
720 * The string data is copied into the terminal. A NULL value pointer
721 * clears the pwd (equivalent to setting an empty string).
722 *
723 * Input type: GhosttyString*
724 */
725 GHOSTTY_TERMINAL_OPT_PWD = 10,
726
727 /**
728 * Set the default foreground color.
729 *
730 * A NULL value pointer clears the default (unset).
731 *
732 * Input type: GhosttyColorRgb*
733 */
734 GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND = 11,
735
736 /**
737 * Set the default background color.
738 *
739 * A NULL value pointer clears the default (unset).
740 *
741 * Input type: GhosttyColorRgb*
742 */
743 GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND = 12,
744
745 /**
746 * Set the default cursor color.
747 *
748 * A NULL value pointer clears the default (unset).
749 *
750 * Input type: GhosttyColorRgb*
751 */
752 GHOSTTY_TERMINAL_OPT_COLOR_CURSOR = 13,
753
754 /**
755 * Set the default 256-color palette.
756 *
757 * The value must point to an array of exactly 256 GhosttyColorRgb values.
758 * A NULL value pointer resets to the built-in default palette.
759 *
760 * Input type: GhosttyColorRgb[256]*
761 */
762 GHOSTTY_TERMINAL_OPT_COLOR_PALETTE = 14,
763
764 /**
765 * Set the Kitty image storage limit in bytes.
766 *
767 * Applied to all initialized screens (primary and alternate).
768 * A value of zero disables the Kitty graphics protocol entirely,
769 * deleting all stored images and placements. A NULL value pointer
770 * is equivalent to zero (disables). Has no effect when Kitty graphics
771 * are disabled at build time.
772 *
773 * Input type: uint64_t*
774 */
775 GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT = 15,
776
777 /**
778 * Enable or disable Kitty image loading via the file medium.
779 *
780 * A NULL value pointer is a no-op. Has no effect when Kitty graphics
781 * are disabled at build time.
782 *
783 * Input type: bool*
784 */
785 GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE = 16,
786
787 /**
788 * Enable Kitty image loading via the temporary file medium, restricted to
789 * the provided directory. The string data is copied into the terminal.
790 *
791 * A NULL value pointer disables the temporary file medium. Has no effect
792 * when Kitty graphics are disabled at build time.
793 *
794 * Input type: GhosttyString*
795 */
796 GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE = 17,
797
798 /**
799 * Enable or disable Kitty image loading via the shared memory medium.
800 *
801 * A NULL value pointer is a no-op. Has no effect when Kitty graphics
802 * are disabled at build time.
803 *
804 * Input type: bool*
805 */
806 GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM = 18,
807
808 /**
809 * Set the maximum bytes the APC handler will buffer for all protocols.
810 * This prevents malicious input from causing unbounded memory allocation.
811 * A NULL value pointer removes all overrides, reverting to the built-in
812 * defaults.
813 *
814 * Input type: size_t*
815 */
816 GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES = 19,
817
818 /**
819 * Set the maximum bytes the APC handler will buffer for Kitty graphics
820 * protocol data. A NULL value pointer removes the override, reverting
821 * to the built-in default.
822 *
823 * Input type: size_t*
824 */
825 GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES_KITTY = 20,
826
827 /**
828 * Set the active screen selection.
829 *
830 * The value must point to a GhosttySelection whose grid references are
831 * valid for this terminal's active screen at the time of the call. The
832 * terminal copies the selection immediately and converts it to
833 * terminal-owned tracked state, so the GhosttySelection struct and its
834 * untracked grid references do not need to outlive this call.
835 *
836 * Passing NULL clears the active screen selection.
837 *
838 * Input type: GhosttySelection*
839 */
840 GHOSTTY_TERMINAL_OPT_SELECTION = 21,
841
842 /**
843 * Set the default cursor style used by DECSCUSR reset (CSI 0 q).
844 *
845 * A NULL value pointer resets to the built-in default block cursor.
846 *
847 * Input type: GhosttyTerminalCursorStyle*
848 */
849 GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_STYLE = 22,
850
851 /**
852 * Set whether the default cursor should blink when reset by DECSCUSR
853 * (CSI 0 q).
854 *
855 * A NULL value pointer resets to the built-in default of not blinking.
856 *
857 * Input type: bool*
858 */
859 GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_BLINK = 23,
860
861 /**
862 * Enable or disable Glyph Protocol APC handling.
863 *
864 * When disabled, Glyph Protocol APC sequences are ignored and no
865 * support/query/register/clear responses are emitted. Disabling also clears
866 * the terminal session's glyph glossary. A NULL value pointer is a no-op.
867 *
868 * Input type: bool*
869 */
870 GHOSTTY_TERMINAL_OPT_GLYPH_PROTOCOL = 24,
871
872 /**
873 * Callback invoked when the terminal pwd changes via escape
874 * sequences (OSC 7, OSC 9, or OSC 1337 CurrentDir). Set to NULL
875 * to ignore pwd change events.
876 *
877 * Input type: GhosttyTerminalPwdChangedFn
878 */
879 GHOSTTY_TERMINAL_OPT_PWD_CHANGED = 25,
880
881 /**
882 * Callback invoked when the running program performs a clipboard write.
883 * OSC 52 and iTerm2 OSC 1337 Copy writes are normalized to an atomic set
884 * of decoded MIME representations. Set to NULL to ignore clipboard writes.
885 * Clipboard read requests are always ignored; see
886 * GhosttyTerminalClipboardWriteFn.
887 *
888 * Input type: GhosttyTerminalClipboardWriteFn
889 */
890 GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE = 26,
891 GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
892} GhosttyTerminalOption;
893
894/**
895 * Terminal data types.
896 *
897 * These values specify what type of data to extract from a terminal
898 * using `ghostty_terminal_get`.
899 *
900 * @ingroup terminal
901 */
902typedef enum GHOSTTY_ENUM_TYPED {
903 /** Invalid data type. Never results in any data extraction. */
904 GHOSTTY_TERMINAL_DATA_INVALID = 0,
905
906 /**
907 * Terminal width in cells.
908 *
909 * Output type: uint16_t *
910 */
911 GHOSTTY_TERMINAL_DATA_COLS = 1,
912
913 /**
914 * Terminal height in cells.
915 *
916 * Output type: uint16_t *
917 */
918 GHOSTTY_TERMINAL_DATA_ROWS = 2,
919
920 /**
921 * Cursor column position (0-indexed).
922 *
923 * Output type: uint16_t *
924 */
925 GHOSTTY_TERMINAL_DATA_CURSOR_X = 3,
926
927 /**
928 * Cursor row position within the active area (0-indexed).
929 *
930 * Output type: uint16_t *
931 */
932 GHOSTTY_TERMINAL_DATA_CURSOR_Y = 4,
933
934 /**
935 * Whether the cursor has a pending wrap (next print will soft-wrap).
936 *
937 * Output type: bool *
938 */
939 GHOSTTY_TERMINAL_DATA_CURSOR_PENDING_WRAP = 5,
940
941 /**
942 * The currently active screen.
943 *
944 * Output type: GhosttyTerminalScreen *
945 */
946 GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN = 6,
947
948 /**
949 * Whether the cursor is visible (DEC mode 25).
950 *
951 * Output type: bool *
952 */
953 GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE = 7,
954
955 /**
956 * Current Kitty keyboard protocol flags.
957 *
958 * Output type: GhosttyKittyKeyFlags * (uint8_t *)
959 */
960 GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS = 8,
961
962 /**
963 * Scrollbar state for the terminal viewport.
964 *
965 * This is amortized O(1): the total is maintained incrementally as
966 * the terminal is modified and the viewport offset is cached. The
967 * first read after the viewport moves to an arbitrary position that
968 * isn't an absolute row (e.g. scrolling to a selection) may cost
969 * O(pages) to compute the offset, after which it is cached again.
970 *
971 * There is intentionally no change notification for scroll state.
972 * Callers building scrollbars should poll this once per frame or
973 * per write batch and diff the result to detect changes; this is
974 * what Ghostty's own renderer does.
975 *
976 * Output type: GhosttyTerminalScrollbar *
977 */
978 GHOSTTY_TERMINAL_DATA_SCROLLBAR = 9,
979
980 /**
981 * The current SGR style of the cursor.
982 *
983 * This is the style that will be applied to newly printed characters.
984 *
985 * Output type: GhosttyStyle *
986 */
987 GHOSTTY_TERMINAL_DATA_CURSOR_STYLE = 10,
988
989 /**
990 * Whether any mouse tracking mode is active.
991 *
992 * Returns true if any of the mouse tracking modes (X10, normal, button,
993 * or any-event) are enabled.
994 *
995 * Output type: bool *
996 */
997 GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING = 11,
998
999 /**
1000 * The terminal title as set by escape sequences (e.g. OSC 0/2).
1001 *
1002 * Returns a borrowed string. The pointer is valid until the next call
1003 * to ghostty_terminal_vt_write() or ghostty_terminal_reset(). An empty
1004 * string (len=0) is returned when no title has been set.
1005 *
1006 * Output type: GhosttyString *
1007 */
1008 GHOSTTY_TERMINAL_DATA_TITLE = 12,
1009
1010 /**
1011 * The terminal's current working directory as set by escape sequences
1012 * (e.g. OSC 7).
1013 *
1014 * Returns a borrowed string. The pointer is valid until the next call
1015 * to ghostty_terminal_vt_write() or ghostty_terminal_reset(). An empty
1016 * string (len=0) is returned when no pwd has been set.
1017 *
1018 * Output type: GhosttyString *
1019 */
1020 GHOSTTY_TERMINAL_DATA_PWD = 13,
1021
1022 /**
1023 * The total number of rows in the active screen including scrollback.
1024 *
1025 * Output type: size_t *
1026 */
1027 GHOSTTY_TERMINAL_DATA_TOTAL_ROWS = 14,
1028
1029 /**
1030 * The number of scrollback rows (total rows minus viewport rows).
1031 *
1032 * Output type: size_t *
1033 */
1034 GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS = 15,
1035
1036 /**
1037 * The total width of the terminal in pixels.
1038 *
1039 * This is cols * cell_width_px as set by ghostty_terminal_resize().
1040 *
1041 * Output type: uint32_t *
1042 */
1043 GHOSTTY_TERMINAL_DATA_WIDTH_PX = 16,
1044
1045 /**
1046 * The total height of the terminal in pixels.
1047 *
1048 * This is rows * cell_height_px as set by ghostty_terminal_resize().
1049 *
1050 * Output type: uint32_t *
1051 */
1052 GHOSTTY_TERMINAL_DATA_HEIGHT_PX = 17,
1053
1054 /**
1055 * The effective foreground color (override or default).
1056 *
1057 * Returns GHOSTTY_NO_VALUE if no foreground color is set.
1058 *
1059 * Output type: GhosttyColorRgb *
1060 */
1061 GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND = 18,
1062
1063 /**
1064 * The effective background color (override or default).
1065 *
1066 * Returns GHOSTTY_NO_VALUE if no background color is set.
1067 *
1068 * Output type: GhosttyColorRgb *
1069 */
1070 GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND = 19,
1071
1072 /**
1073 * The effective cursor color (override or default).
1074 *
1075 * Returns GHOSTTY_NO_VALUE if no cursor color is set.
1076 *
1077 * Output type: GhosttyColorRgb *
1078 */
1079 GHOSTTY_TERMINAL_DATA_COLOR_CURSOR = 20,
1080
1081 /**
1082 * The current 256-color palette.
1083 *
1084 * Output type: GhosttyColorRgb[256] *
1085 */
1086 GHOSTTY_TERMINAL_DATA_COLOR_PALETTE = 21,
1087
1088 /**
1089 * The default foreground color (ignoring any OSC override).
1090 *
1091 * Returns GHOSTTY_NO_VALUE if no default foreground color is set.
1092 *
1093 * Output type: GhosttyColorRgb *
1094 */
1095 GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT = 22,
1096
1097 /**
1098 * The default background color (ignoring any OSC override).
1099 *
1100 * Returns GHOSTTY_NO_VALUE if no default background color is set.
1101 *
1102 * Output type: GhosttyColorRgb *
1103 */
1104 GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT = 23,
1105
1106 /**
1107 * The default cursor color (ignoring any OSC override).
1108 *
1109 * Returns GHOSTTY_NO_VALUE if no default cursor color is set.
1110 *
1111 * Output type: GhosttyColorRgb *
1112 */
1113 GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT = 24,
1114
1115 /**
1116 * The default 256-color palette (ignoring any OSC overrides).
1117 *
1118 * Output type: GhosttyColorRgb[256] *
1119 */
1120 GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT = 25,
1121
1122 /**
1123 * The Kitty image storage limit in bytes for the active screen.
1124 *
1125 * A value of zero means the Kitty graphics protocol is disabled.
1126 * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
1127 *
1128 * Output type: uint64_t *
1129 */
1130 GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_STORAGE_LIMIT = 26,
1131
1132 /**
1133 * Whether the file medium is enabled for Kitty image loading on the
1134 * active screen.
1135 *
1136 * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
1137 *
1138 * Output type: bool *
1139 */
1140 GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_FILE = 27,
1141
1142 /**
1143 * The directory allowed for Kitty image loading via the temporary file
1144 * medium on the active screen. The string is empty when the medium is
1145 * disabled.
1146 *
1147 * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
1148 *
1149 * Output type: GhosttyString *
1150 */
1151 GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE = 28,
1152
1153 /**
1154 * Whether the shared memory medium is enabled for Kitty image loading
1155 * on the active screen.
1156 *
1157 * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
1158 *
1159 * Output type: bool *
1160 */
1161 GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM = 29,
1162
1163 /**
1164 * The Kitty graphics image storage for the active screen.
1165 *
1166 * Returns a borrowed pointer to the image storage. The pointer is valid
1167 * until the next mutating terminal call (e.g. ghostty_terminal_vt_write()
1168 * or ghostty_terminal_reset()).
1169 *
1170 * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
1171 *
1172 * Output type: GhosttyKittyGraphics *
1173 */
1174 GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS = 30,
1175
1176 /**
1177 * The active screen's current selection.
1178 *
1179 * On success, writes an untracked snapshot of the terminal-owned selection
1180 * to the caller-provided GhosttySelection. The GhosttySelection struct is
1181 * caller-owned and may be kept, but the grid references inside it are
1182 * untracked borrowed references into the active screen. They are only valid
1183 * until the next mutating terminal call, such as ghostty_terminal_set(),
1184 * ghostty_terminal_vt_write(), ghostty_terminal_resize(), or
1185 * ghostty_terminal_reset().
1186 *
1187 * Returns GHOSTTY_NO_VALUE when there is no active selection.
1188 *
1189 * Output type: GhosttySelection *
1190 */
1191 GHOSTTY_TERMINAL_DATA_SELECTION = 31,
1192
1193 /**
1194 * Whether the viewport is currently pinned to the active area.
1195 *
1196 * This is true when the viewport is following the active terminal area,
1197 * and false when the user has scrolled into history.
1198 *
1199 * Output type: bool *
1200 */
1201 GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE = 32,
1202
1203 /**
1204 * Whether VT processing encountered a non-gracefully handled error that may
1205 * have prevented a terminal-owned semantic update.
1206 *
1207 * Processing remains best-effort, and ghostty_terminal_reset() does not
1208 * clear it. Gracefully handled protocol failures, configured limits,
1209 * malformed or unsupported input, and failures limited to external effects
1210 * or query responses do not set it.
1211 *
1212 * This can't currently be unset. This is purely informational to consumers
1213 * if there was some error that happened at some point during VT processing.
1214 *
1215 * Output type: bool *
1216 */
1217 GHOSTTY_TERMINAL_DATA_VT_PROCESSING_ERROR = 33,
1218 GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
1219} GhosttyTerminalData;
1220
1221/**
1222 * Create a new terminal instance.
1223 *
1224 * @param allocator Pointer to allocator, or NULL to use the default allocator
1225 * @param terminal Pointer to store the created terminal handle
1226 * @param options Terminal initialization options
1227 * @return GHOSTTY_SUCCESS on success, or an error code on failure
1228 *
1229 * @ingroup terminal
1230 */
1231GHOSTTY_API GhosttyResult ghostty_terminal_new(const GhosttyAllocator* allocator,
1232 GhosttyTerminal* terminal,
1233 GhosttyTerminalOptions options);
1234
1235/**
1236 * Free a terminal instance.
1237 *
1238 * Releases all resources associated with the terminal. After this call,
1239 * the terminal handle becomes invalid and must not be used.
1240 *
1241 * @param terminal The terminal handle to free (may be NULL)
1242 *
1243 * @ingroup terminal
1244 */
1245GHOSTTY_API void ghostty_terminal_free(GhosttyTerminal terminal);
1246
1247/**
1248 * Perform a full reset of the terminal (RIS).
1249 *
1250 * Resets all terminal state back to its initial configuration, including
1251 * modes, scrollback, scrolling region, and screen contents. The terminal
1252 * dimensions are preserved.
1253 *
1254 * @param terminal The terminal handle (may be NULL, in which case this is a no-op)
1255 *
1256 * @ingroup terminal
1257 */
1258GHOSTTY_API void ghostty_terminal_reset(GhosttyTerminal terminal);
1259
1260/**
1261 * Resize the terminal to the given dimensions.
1262 *
1263 * Changes the number of columns and rows in the terminal. The primary
1264 * screen will reflow content if wraparound mode is enabled; the alternate
1265 * screen does not reflow. If the dimensions are unchanged, this is a no-op.
1266 *
1267 * This also updates the terminal's pixel dimensions (used for image
1268 * protocols and size reports), disables synchronized output mode (allowed
1269 * by the spec so that resize results are shown immediately), and sends an
1270 * in-band size report if mode 2048 is enabled.
1271 *
1272 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
1273 * @param cols New width in cells (must be greater than zero)
1274 * @param rows New height in cells (must be greater than zero)
1275 * @param cell_width_px Width of a single cell in pixels
1276 * @param cell_height_px Height of a single cell in pixels
1277 * @return GHOSTTY_SUCCESS on success, or an error code on failure
1278 *
1279 * @ingroup terminal
1280 */
1281GHOSTTY_API GhosttyResult ghostty_terminal_resize(GhosttyTerminal terminal,
1282 uint16_t cols,
1283 uint16_t rows,
1284 uint32_t cell_width_px,
1285 uint32_t cell_height_px);
1286
1287/**
1288 * Set an option on the terminal.
1289 *
1290 * Configures terminal callbacks and associated state such as the
1291 * write_pty callback and userdata pointer. The value is passed
1292 * directly for pointer types (callbacks, userdata) or as a pointer
1293 * to the value for non-pointer types (e.g. GhosttyString*).
1294 * NULL clears the option to its default.
1295 *
1296 * Callbacks are invoked synchronously during ghostty_terminal_vt_write().
1297 * Callbacks must not call ghostty_terminal_vt_write() on the same
1298 * terminal (no reentrancy).
1299 *
1300 * @param terminal The terminal handle (may be NULL, in which case this is a no-op)
1301 * @param option The option to set
1302 * @param value Pointer to the value to set (type depends on the option),
1303 * or NULL to clear the option
1304 *
1305 * @ingroup terminal
1306 */
1307GHOSTTY_API GhosttyResult ghostty_terminal_set(GhosttyTerminal terminal,
1308 GhosttyTerminalOption option,
1309 const void* value);
1310
1311/**
1312 * Write VT-encoded data to the terminal for processing.
1313 *
1314 * Feeds raw bytes through the terminal's VT stream parser, updating
1315 * terminal state accordingly. By default, sequences that require output
1316 * (queries, device status reports) are silently ignored. Use
1317 * ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_WRITE_PTY to install
1318 * a callback that receives response data.
1319 *
1320 * This never fails. Any erroneous input or errors in processing the
1321 * input are logged internally but do not cause this function to fail
1322 * because this input is assumed to be untrusted and from an external
1323 * source; so the primary goal is to keep the terminal state consistent and
1324 * not allow malformed input to corrupt or crash.
1325 *
1326 * @param terminal The terminal handle
1327 * @param data Pointer to the data to write
1328 * @param len Length of the data in bytes
1329 *
1330 * @ingroup terminal
1331 */
1332GHOSTTY_API void ghostty_terminal_vt_write(GhosttyTerminal terminal,
1333 const uint8_t* data,
1334 size_t len);
1335
1336/**
1337 * Scroll the terminal viewport.
1338 *
1339 * Scrolls the terminal's viewport according to the given behavior.
1340 * When using GHOSTTY_SCROLL_VIEWPORT_DELTA, set the delta field in
1341 * the value union to specify the number of rows to scroll (negative
1342 * for up, positive for down). When using GHOSTTY_SCROLL_VIEWPORT_ROW,
1343 * set the row field to the absolute row offset from the top of the
1344 * scrollable area (the same row space as the offset field of
1345 * GhosttyTerminalScrollbar). For other behaviors, the value is ignored.
1346 *
1347 * @param terminal The terminal handle (may be NULL, in which case this is a no-op)
1348 * @param behavior The scroll behavior as a tagged union
1349 *
1350 * @ingroup terminal
1351 */
1352GHOSTTY_API void ghostty_terminal_scroll_viewport(GhosttyTerminal terminal,
1353 GhosttyTerminalScrollViewport behavior);
1354
1355/**
1356 * Return the current compression activity token.
1357 *
1358 * The token is opaque and only equality comparisons are meaningful. An
1359 * embedding application should cache it and restart its compression idle
1360 * delay whenever the value changes. The value may wrap and changes in either
1361 * direction have the same meaning.
1362 *
1363 * This function only observes terminal state. It does not perform or schedule
1364 * compression.
1365 *
1366 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
1367 * @param[out] out_activity Receives the current activity token
1368 * @return GHOSTTY_SUCCESS on success, or GHOSTTY_INVALID_VALUE if an argument
1369 * is NULL
1370 *
1371 * @ingroup terminal
1372 */
1373GHOSTTY_API GhosttyResult ghostty_terminal_compression_activity(
1374 GhosttyTerminal terminal,
1375 uint64_t* out_activity);
1376
1377/**
1378 * Compress eligible terminal scrollback.
1379 *
1380 * Incremental mode performs bounded work suitable for an idle callback. A
1381 * pending result means the application should invoke another step while the
1382 * terminal remains idle. A complete result means no continuation is needed
1383 * until ghostty_terminal_compression_activity() changes. Full mode performs
1384 * one synchronous scan and can stall on large scrollback buffers.
1385 *
1386 * Compression is opportunistic. Complete means the pass has finished, not
1387 * that every page was compressed: pages may be unprofitable or encounter an
1388 * allocation or reclamation failure. Compression changes only the terminal's
1389 * storage representation and never its logical contents or scrollback limit.
1390 * Accessing compressed history restores it transparently.
1391 *
1392 * This function is not thread-safe with other operations on the same
1393 * terminal. The caller must serialize it with writes, rendering, searches,
1394 * and other terminal access.
1395 *
1396 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
1397 * @param mode The amount of compression work to perform
1398 * @param[out] out_result Receives the compression scheduling result
1399 * @return GHOSTTY_SUCCESS on success, or GHOSTTY_INVALID_VALUE if an argument
1400 * or mode is invalid
1401 *
1402 * @ingroup terminal
1403 */
1404GHOSTTY_API GhosttyResult ghostty_terminal_compress(
1405 GhosttyTerminal terminal,
1406 GhosttyTerminalCompressionMode mode,
1407 GhosttyTerminalCompressionResult* out_result);
1408
1409/**
1410 * Get the current value of a terminal mode.
1411 *
1412 * Returns the value of the mode identified by the given mode.
1413 *
1414 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
1415 * @param mode The mode identifying the mode to query
1416 * @param[out] out_value On success, set to true if the mode is set, false
1417 * if it is reset
1418 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
1419 * is NULL or the mode does not correspond to a known mode
1420 *
1421 * @ingroup terminal
1422 */
1423GHOSTTY_API GhosttyResult ghostty_terminal_mode_get(GhosttyTerminal terminal,
1424 GhosttyMode mode,
1425 bool* out_value);
1426
1427/**
1428 * Set the value of a terminal mode.
1429 *
1430 * Sets the mode identified by the given mode to the specified value.
1431 *
1432 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
1433 * @param mode The mode identifying the mode to set
1434 * @param value true to set the mode, false to reset it
1435 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
1436 * is NULL or the mode does not correspond to a known mode
1437 *
1438 * @ingroup terminal
1439 */
1440GHOSTTY_API GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal,
1441 GhosttyMode mode,
1442 bool value);
1443
1444/**
1445 * Get data from a terminal instance.
1446 *
1447 * Extracts typed data from the given terminal based on the specified
1448 * data type. The output pointer must be of the appropriate type for the
1449 * requested data kind. Valid data types and output types are documented
1450 * in the `GhosttyTerminalData` enum.
1451 *
1452 * @param terminal The terminal handle (may be NULL)
1453 * @param data The type of data to extract
1454 * @param out Pointer to store the extracted data (type depends on data parameter)
1455 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
1456 * is NULL or the data type is invalid
1457 *
1458 * @ingroup terminal
1459 */
1460GHOSTTY_API GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal,
1461 GhosttyTerminalData data,
1462 void *out);
1463
1464/**
1465 * Get multiple data fields from a terminal in a single call.
1466 *
1467 * This is an optimization over calling ghostty_terminal_get()
1468 * repeatedly, particularly useful in environments with high per-call
1469 * overhead such as FFI or Cgo.
1470 *
1471 * Each element in the keys array specifies a data kind, and the
1472 * corresponding element in the values array receives the result.
1473 * The type of each values[i] pointer must match the output type
1474 * documented for keys[i].
1475 *
1476 * Processing stops at the first error; on success out_written
1477 * is set to count, on error it is set to the index of the
1478 * failing key (i.e. the number of values successfully written).
1479 *
1480 * @param terminal The terminal handle (may be NULL)
1481 * @param count Number of key/value pairs
1482 * @param keys Array of data kinds to query
1483 * @param values Array of output pointers (types must match each key's
1484 * documented output type)
1485 * @param[out] out_written On return, receives the number of values
1486 * successfully written (may be NULL)
1487 * @return GHOSTTY_SUCCESS if all queries succeed
1488 *
1489 * @ingroup terminal
1490 */
1491GHOSTTY_API GhosttyResult ghostty_terminal_get_multi(GhosttyTerminal terminal,
1492 size_t count,
1493 const GhosttyTerminalData* keys,
1494 void** values,
1495 size_t* out_written);
1496
1497/**
1498 * Resolve a point in the terminal grid to a grid reference.
1499 *
1500 * Resolves the given point (which can be in active, viewport, screen,
1501 * or history coordinates) to a grid reference for that location. Use
1502 * ghostty_grid_ref_cell() and ghostty_grid_ref_row() to extract the cell
1503 * and row.
1504 *
1505 * Lookups using the `active` and `viewport` tags are fast. The `screen`
1506 * and `history` tags may require traversing the full scrollback page list
1507 * to resolve the y coordinate, so they can be expensive for large
1508 * scrollback buffers.
1509 *
1510 * This function isn't meant to be used as the core of render loop. It
1511 * isn't built to sustain the framerates needed for rendering large screens.
1512 * Use the render state API for that. This API is instead meant for less
1513 * strictly performance-sensitive use cases.
1514 *
1515 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
1516 * @param point The point specifying which cell to look up
1517 * @param[out] out_ref On success, set to the grid reference at the given point (may be NULL)
1518 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
1519 * is NULL or the point is out of bounds
1520 *
1521 * @ingroup terminal
1522 */
1523GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal,
1524 GhosttyPoint point,
1525 GhosttyGridRef *out_ref);
1526
1527/**
1528 * Create an owned tracked grid reference for a terminal point.
1529 *
1530 * This is the tracked variant of ghostty_terminal_grid_ref(). The returned
1531 * handle follows the referenced cell as the terminal's page list is modified:
1532 * scrolling, pruning, resize/reflow, and other page-list operations update the
1533 * tracked reference automatically.
1534 *
1535 * The reference is attached to the terminal screen/page-list that is active at
1536 * creation time.
1537 *
1538 * If the point is outside the requested coordinate space, this returns
1539 * GHOSTTY_INVALID_VALUE and writes NULL to out_ref.
1540 *
1541 * The returned handle must be freed with ghostty_tracked_grid_ref_free(). If
1542 * the terminal is freed first, the handle remains valid only for
1543 * tracked-grid-ref APIs: it reports no value and can still be freed.
1544 *
1545 * @param terminal Terminal instance.
1546 * @param point Point to track.
1547 * @param[out] out_ref On success, receives the tracked reference handle.
1548 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if terminal,
1549 * point, or out_ref is invalid, or GHOSTTY_OUT_OF_MEMORY if allocation
1550 * fails.
1551 *
1552 * @ingroup terminal
1553 */
1554GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref_track(
1555 GhosttyTerminal terminal,
1556 GhosttyPoint point,
1557 GhosttyTrackedGridRef *out_ref);
1558
1559/**
1560 * Convert a grid reference back to a point in the given coordinate system.
1561 *
1562 * This is the inverse of ghostty_terminal_grid_ref(): given a grid reference,
1563 * it returns the x/y coordinates in the requested coordinate system (active,
1564 * viewport, screen, or history).
1565 *
1566 * The grid reference must have been obtained from the same terminal instance.
1567 * Like all grid references, it is only valid until the next mutating terminal
1568 * call.
1569 *
1570 * Not every grid reference is representable in every coordinate system. For
1571 * example, a cell in scrollback history cannot be expressed in active
1572 * coordinates, and a cell that has scrolled off the visible area cannot be
1573 * expressed in viewport coordinates. In these cases, the function returns
1574 * GHOSTTY_NO_VALUE.
1575 *
1576 * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
1577 * @param ref Pointer to the grid reference to convert
1578 * @param tag The target coordinate system
1579 * @param[out] out On success, set to the coordinate in the requested system (may be NULL)
1580 * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
1581 * or ref is NULL/invalid, GHOSTTY_NO_VALUE if the ref falls outside
1582 * the requested coordinate system
1583 *
1584 * @ingroup terminal
1585 */
1586GHOSTTY_API GhosttyResult ghostty_terminal_point_from_grid_ref(
1587 GhosttyTerminal terminal,
1588 const GhosttyGridRef *ref,
1589 GhosttyPointTag tag,
1590 GhosttyPointCoordinate *out);
1591
1592/** @} */
1593
1594#ifdef __cplusplus
1595}
1596#endif
1597
1598#endif /* GHOSTTY_VT_TERMINAL_H */
packages/terminal/vendor/include/ghostty/vt/types.h created+335
......@@ -0,0 +1,335 @@
1/**
2 * @file types.h
3 *
4 * Common types, macros, and utilities for libghostty-vt.
5 */
6
7#ifndef GHOSTTY_VT_TYPES_H
8#define GHOSTTY_VT_TYPES_H
9
10#include <limits.h>
11#include <stddef.h>
12#include <stdint.h>
13
14// Symbol visibility for shared library builds. On Windows, functions
15// are exported from the DLL when building and imported when consuming.
16// On other platforms with GCC/Clang, functions are marked with default
17// visibility so they remain accessible when the library is built with
18// -fvisibility=hidden. For static library builds, define GHOSTTY_STATIC
19// before including this header to make this a no-op.
20#ifndef GHOSTTY_API
21#if defined(GHOSTTY_STATIC)
22 #define GHOSTTY_API
23#elif defined(_WIN32) || defined(_WIN64)
24 #ifdef GHOSTTY_BUILD_SHARED
25 #define GHOSTTY_API __declspec(dllexport)
26 #else
27 #define GHOSTTY_API __declspec(dllimport)
28 #endif
29#elif defined(__GNUC__) && __GNUC__ >= 4
30 #define GHOSTTY_API __attribute__((visibility("default")))
31#else
32 #define GHOSTTY_API
33#endif
34#endif
35
36/**
37 * Enum int-sizing helpers.
38 *
39 * The Zig side backs all C enums with c_int, so the C declarations
40 * must use int as their underlying type to maintain ABI compatibility.
41 *
42 * C23 (detected via __STDC_VERSION__ >= 202311L) supports explicit
43 * enum underlying types with `enum : int { ... }`. For pre-C23
44 * compilers, which are free to choose any type that can represent
45 * all values (C11 §6.7.2.2), we add an INT_MAX sentinel as the last
46 * entry to force the compiler to use int.
47 *
48 * INT_MAX is used rather than a fixed constant like 0xFFFFFFFF
49 * because enum constants must have type int (which is signed).
50 * Values above INT_MAX overflow signed int and are a constraint
51 * violation in standard C; compilers that accept them interpret them
52 * as negative values via two's complement, which can collide with
53 * legitimate negative enum values.
54 *
55 * Usage:
56 * @code
57 * typedef enum GHOSTTY_ENUM_TYPED {
58 * FOO_A = 0,
59 * FOO_B = 1,
60 * FOO_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
61 * } Foo;
62 * @endcode
63 */
64#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
65#define GHOSTTY_ENUM_TYPED : int
66#else
67#define GHOSTTY_ENUM_TYPED
68#endif
69#define GHOSTTY_ENUM_MAX_VALUE INT_MAX
70
71/**
72 * Result codes for libghostty-vt operations.
73 */
74typedef enum GHOSTTY_ENUM_TYPED {
75 /** Operation completed successfully */
76 GHOSTTY_SUCCESS = 0,
77 /** Operation failed due to failed allocation */
78 GHOSTTY_OUT_OF_MEMORY = -1,
79 /** Operation failed due to invalid value */
80 GHOSTTY_INVALID_VALUE = -2,
81 /** Operation failed because the provided buffer was too small */
82 GHOSTTY_OUT_OF_SPACE = -3,
83 /** The requested value has no value */
84 GHOSTTY_NO_VALUE = -4,
85 GHOSTTY_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
86} GhosttyResult;
87
88/* ---- Opaque handles ---- */
89
90/**
91 * Opaque handle to a terminal instance.
92 *
93 * @ingroup terminal
94 */
95typedef struct GhosttyTerminalImpl* GhosttyTerminal;
96
97/**
98 * Opaque handle to a tracked grid reference.
99 *
100 * A tracked grid reference is owned by the caller and must be freed with
101 * ghostty_tracked_grid_ref_free(). If the terminal that created it is freed
102 * first, the handle remains valid only for tracked-grid-ref APIs: it reports no
103 * value and can still be freed.
104 *
105 * @ingroup grid_ref
106 */
107typedef struct GhosttyTrackedGridRefImpl* GhosttyTrackedGridRef;
108
109/**
110 * Opaque handle to a Kitty graphics image storage.
111 *
112 * Obtained via ghostty_terminal_get() with
113 * GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS. The pointer is borrowed from
114 * the terminal and remains valid until the next mutating terminal call
115 * (e.g. ghostty_terminal_vt_write() or ghostty_terminal_reset()).
116 *
117 * @ingroup kitty_graphics
118 */
119typedef struct GhosttyKittyGraphicsImpl* GhosttyKittyGraphics;
120
121/**
122 * Opaque handle to a Kitty graphics image.
123 *
124 * Obtained via ghostty_kitty_graphics_image() with an image ID. The
125 * pointer is borrowed from the storage and remains valid until the next
126 * mutating terminal call.
127 *
128 * @ingroup kitty_graphics
129 */
130typedef const struct GhosttyKittyGraphicsImageImpl* GhosttyKittyGraphicsImage;
131
132/**
133 * Opaque handle to a Kitty graphics placement iterator.
134 *
135 * @ingroup kitty_graphics
136 */
137typedef struct GhosttyKittyGraphicsPlacementIteratorImpl* GhosttyKittyGraphicsPlacementIterator;
138
139/**
140 * Opaque handle to a render state instance.
141 *
142 * @ingroup render
143 */
144typedef struct GhosttyRenderStateImpl* GhosttyRenderState;
145
146/**
147 * Opaque handle to a render-state row iterator.
148 *
149 * @ingroup render
150 */
151typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator;
152
153/**
154 * Opaque handle to render-state row cells.
155 *
156 * @ingroup render
157 */
158typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells;
159
160/**
161 * Opaque handle to an SGR parser instance.
162 *
163 * This handle represents an SGR (Select Graphic Rendition) parser that can
164 * be used to parse SGR sequences and extract individual text attributes.
165 *
166 * @ingroup sgr
167 */
168typedef struct GhosttySgrParserImpl* GhosttySgrParser;
169
170/**
171 * Opaque handle to a formatter instance.
172 *
173 * @ingroup formatter
174 */
175typedef struct GhosttyFormatterImpl* GhosttyFormatter;
176
177/**
178 * Opaque handle to an OSC parser instance.
179 *
180 * This handle represents an OSC (Operating System Command) parser that can
181 * be used to parse the contents of OSC sequences.
182 *
183 * @ingroup osc
184 */
185typedef struct GhosttyOscParserImpl* GhosttyOscParser;
186
187/**
188 * Opaque handle to a single OSC command.
189 *
190 * This handle represents a parsed OSC (Operating System Command) command.
191 * The command can be queried for its type and associated data.
192 *
193 * @ingroup osc
194 */
195typedef struct GhosttyOscCommandImpl* GhosttyOscCommand;
196
197/* ---- Common value types ---- */
198
199/**
200 * Terminal content output format.
201 *
202 * @ingroup formatter
203 */
204typedef enum GHOSTTY_ENUM_TYPED {
205 /** Plain text (no escape sequences). */
206 GHOSTTY_FORMATTER_FORMAT_PLAIN,
207
208 /** VT sequences preserving colors, styles, URLs, etc. */
209 GHOSTTY_FORMATTER_FORMAT_VT,
210
211 /** HTML with inline styles. */
212 GHOSTTY_FORMATTER_FORMAT_HTML,
213 GHOSTTY_FORMATTER_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
214} GhosttyFormatterFormat;
215
216/**
217 * A borrowed byte string (pointer + length).
218 *
219 * The memory is not owned by this struct. The pointer is only valid
220 * for the lifetime documented by the API that produces or consumes it.
221 */
222typedef struct {
223 /** Pointer to the string bytes. */
224 const uint8_t* ptr;
225
226 /** Length of the string in bytes. */
227 size_t len;
228} GhosttyString;
229
230/**
231 * A caller-provided byte buffer.
232 *
233 * APIs that write to this type use `len` for the number of bytes written on
234 * GHOSTTY_SUCCESS and the required byte capacity on GHOSTTY_OUT_OF_SPACE.
235 */
236typedef struct {
237 /** Destination buffer for bytes. May be NULL when cap is 0 to query required size. */
238 uint8_t* ptr;
239
240 /** Capacity of ptr in bytes. */
241 size_t cap;
242
243 /** Bytes written on success, or required byte capacity on GHOSTTY_OUT_OF_SPACE. */
244 size_t len;
245} GhosttyBuffer;
246
247/**
248 * A surface-space position in pixels.
249 *
250 * This is not a terminal grid coordinate. It represents an x/y position in the
251 * rendered surface coordinate space, with (0, 0) at the top-left of the
252 * surface.
253 */
254typedef struct {
255 /** X position in surface pixels. */
256 double x;
257
258 /** Y position in surface pixels. */
259 double y;
260} GhosttySurfacePosition;
261
262/**
263 * A borrowed list of Unicode scalar values.
264 *
265 * Values are encoded as uint32_t scalar values. The memory is not owned by this
266 * struct. The pointer is only valid for the lifetime documented by the API that
267 * consumes or produces it.
268 *
269 * APIs may document special handling for NULL + len 0, such as “use defaults”.
270 */
271typedef struct {
272 /** Pointer to Unicode scalar values. */
273 const uint32_t* ptr;
274
275 /** Number of entries in ptr. */
276 size_t len;
277} GhosttyCodepoints;
278
279/**
280 * Initialize a sized struct to zero and set its size field.
281 *
282 * Sized structs use a `size` field as the first member for ABI
283 * compatibility. This macro zero-initializes the struct and sets the
284 * size field to `sizeof(type)`, which allows the library to detect
285 * which version of the struct the caller was compiled against.
286 *
287 * @param type The struct type to initialize
288 * @return A zero-initialized struct with the size field set
289 *
290 * Example:
291 * @code
292 * GhosttyFormatterTerminalOptions opts = GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions);
293 * opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN;
294 * opts.trim = true;
295 * @endcode
296 */
297#define GHOSTTY_INIT_SIZED(type) \
298 ((type){ .size = sizeof(type) })
299
300/**
301 * Return a pointer to a null-terminated JSON string describing the
302 * layout of every C API struct for the current target.
303 *
304 * This is primarily useful for language bindings that can't easily
305 * set C struct fields and need to do so via byte offsets. For example,
306 * WebAssembly modules can't share struct definitions with the host.
307 *
308 * Example (abbreviated):
309 * @code{.json}
310 * {
311 * "GhosttyMouseEncoderSize": {
312 * "size": 40,
313 * "align": 8,
314 * "fields": {
315 * "size": { "offset": 0, "size": 8, "type": "u64" },
316 * "screen_width": { "offset": 8, "size": 4, "type": "u32" },
317 * "screen_height": { "offset": 12, "size": 4, "type": "u32" },
318 * "cell_width": { "offset": 16, "size": 4, "type": "u32" },
319 * "cell_height": { "offset": 20, "size": 4, "type": "u32" },
320 * "padding_top": { "offset": 24, "size": 4, "type": "u32" },
321 * "padding_bottom": { "offset": 28, "size": 4, "type": "u32" },
322 * "padding_right": { "offset": 32, "size": 4, "type": "u32" },
323 * "padding_left": { "offset": 36, "size": 4, "type": "u32" }
324 * }
325 * }
326 * }
327 * @endcode
328 *
329 * The returned pointer is valid for the lifetime of the process.
330 *
331 * @return Pointer to the null-terminated JSON string.
332 */
333GHOSTTY_API const char *ghostty_type_json(void);
334
335#endif /* GHOSTTY_VT_TYPES_H */
packages/terminal/vendor/include/ghostty/vt/unicode.h created+154
......@@ -0,0 +1,154 @@
1/**
2 * @file unicode.h
3 *
4 * Unicode utilities - codepoint properties matching the terminal's
5 * text layout semantics.
6 */
7
8#ifndef GHOSTTY_VT_UNICODE_H
9#define GHOSTTY_VT_UNICODE_H
10
11/** @defgroup unicode Unicode Utilities
12 *
13 * Unicode codepoint properties matching the terminal's text layout
14 * semantics.
15 *
16 * ## Basic Usage
17 *
18 * Use ghostty_unicode_codepoint_width() to determine how many terminal
19 * grid cells a codepoint occupies, using the exact same width table the
20 * terminal itself uses when laying out printed text. Use
21 * ghostty_unicode_grapheme_width() to segment and measure full grapheme
22 * clusters with the same rules the terminal uses when mode 2027 is
23 * enabled. These functions are useful for predicting column layout of
24 * text that has not yet been written to the terminal, such as IME
25 * preedit (composition) overlays.
26 *
27 * @{
28 */
29
30#include <stddef.h>
31#include <stdint.h>
32#include <ghostty/vt/types.h>
33
34#ifdef __cplusplus
35extern "C" {
36#endif
37
38/**
39 * Returns the terminal display width of a Unicode codepoint in
40 * terminal grid cells: 0, 1, or 2.
41 *
42 * This is the same width table the terminal itself uses when laying
43 * out printed text, so callers can predict column layout (e.g. IME
44 * preedit overlays) that exactly matches what the terminal will do
45 * when the text is actually written to it.
46 *
47 * Semantics:
48 * - Returns 0 for zero-width codepoints: C0/C1 control characters,
49 * nonspacing and enclosing combining marks, default-ignorable
50 * codepoints (ZWJ, ZWNJ, variation selectors, etc.), and
51 * surrogate codepoints.
52 * - Returns 2 for wide codepoints: East Asian Wide/Fullwidth
53 * (including emoji with default emoji presentation) and regional
54 * indicators. Width is clamped to 2 (e.g. the three-em dash).
55 * - Returns 1 for everything else, including invalid codepoints
56 * beyond U+10FFFF (this function is total; it never fails).
57 *
58 * This operates on a single codepoint only and therefore cannot account
59 * for grapheme-cluster-level width rules (VS16 emoji presentation,
60 * combining sequences, etc.). For cluster-accurate widths, use
61 * ghostty_unicode_grapheme_width(). Summing per-codepoint widths is only
62 * correct when mode 2027 (grapheme clustering) is disabled.
63 *
64 * This function is pure, allocates nothing, and is thread-safe.
65 *
66 * @param cp The Unicode codepoint to measure
67 * @return Display width in cells: 0, 1, or 2
68 */
69GHOSTTY_API uint8_t ghostty_unicode_codepoint_width(uint32_t cp);
70
71/**
72 * Measures the terminal display width of the first grapheme cluster in a
73 * sequence of Unicode codepoints.
74 *
75 * This uses the exact same grapheme segmentation and cluster width rules
76 * the terminal itself uses when printing text with grapheme clustering
77 * enabled (mode 2027), so callers can predict column layout (e.g. IME
78 * preedit overlays) that exactly matches what the terminal will do when
79 * the text is actually written to it. Unlike
80 * ghostty_unicode_codepoint_width(), this accounts for cluster-level
81 * rules: emoji variation selectors, ZWJ sequences, combining marks, and
82 * skin tone modifiers.
83 *
84 * Reads codepoints from cps until the terminal would consider the
85 * grapheme cluster complete, stores the cluster's total width in cells
86 * (0, 1, or 2) into width (which may be NULL if only segmentation is
87 * desired), and returns the number of codepoints consumed. Returns 0 if
88 * and only if len is 0; otherwise consumes at least one codepoint. Measure
89 * a whole string by calling in a loop:
90 *
91 * @code
92 * size_t total = 0;
93 * for (size_t i = 0; i < len;) {
94 * uint8_t width;
95 * i += ghostty_unicode_grapheme_width(cps + i, len - i, &width);
96 * total += width;
97 * }
98 * @endcode
99 *
100 * This is not a streaming API. The provided sequence must contain a
101 * complete first grapheme cluster, or the logical end of the string. If
102 * input arrives in chunks, keep buffering while this function consumes all
103 * available codepoints (return value == len) and the stream may still
104 * continue; a later codepoint could still extend the cluster and change
105 * its width.
106 *
107 * Width semantics, matching the terminal with mode 2027 enabled:
108 * - The cluster starts at the width of its first codepoint, as returned by
109 * ghostty_unicode_codepoint_width().
110 * - VS16 (U+FE0F) forces the cluster wide (2) and VS15 (U+FE0E) forces it
111 * narrow (1), but only when the immediately preceding codepoint in the
112 * cluster is a valid emoji variation sequence base (per Unicode
113 * emoji-variation-sequences.txt). Invalid variation selectors are
114 * ignored entirely.
115 * - Any other continuation codepoint that contributes to grapheme width
116 * forces the cluster wide (2). Note this means cluster width is NOT the
117 * maximum of per-codepoint widths: some continuation marks have narrow
118 * codepoint width yet still widen the cluster.
119 *
120 * Mode dependence: this models mode 2027 (grapheme clustering) enabled,
121 * which is Ghostty's recommended configuration. When mode 2027 is
122 * disabled, clusters never combine and variation selectors never change
123 * width; predict layout in that case by summing
124 * ghostty_unicode_codepoint_width() over each codepoint instead.
125 *
126 * Edge cases:
127 * - Codepoints beyond U+10FFFF consume one codepoint, have width 1, and
128 * are always cluster boundaries. This function is total; it never fails.
129 * - Control characters (C0/C1, CR, LF) are never printed through the
130 * terminal's text path; passing them here returns an unspecified (but
131 * stable and bounded) result.
132 * - A cluster whose first codepoint is zero-width (e.g. a lone combining
133 * mark) is malformed at a cell start; the terminal may attach it to
134 * earlier screen content. This function reports the fold result for the
135 * sequence in isolation (typically 0).
136 *
137 * This function is pure, allocates nothing, and is thread-safe.
138 *
139 * @param cps Pointer to codepoints (may be NULL only when len is 0)
140 * @param len Number of codepoints available
141 * @param width Out: cluster display width in cells (0-2); may be NULL
142 * @return Number of codepoints in the first grapheme cluster
143 */
144GHOSTTY_API size_t ghostty_unicode_grapheme_width(const uint32_t *cps,
145 size_t len,
146 uint8_t *width);
147
148#ifdef __cplusplus
149}
150#endif
151
152/** @} */
153
154#endif /* GHOSTTY_VT_UNICODE_H */
packages/terminal/vendor/include/ghostty/vt/wasm.h created+160
......@@ -0,0 +1,160 @@
1/**
2 * @file wasm.h
3 *
4 * WebAssembly utility functions for libghostty-vt.
5 */
6
7#ifndef GHOSTTY_VT_WASM_H
8#define GHOSTTY_VT_WASM_H
9
10#ifdef __wasm__
11
12#include <stddef.h>
13#include <stdint.h>
14#include <ghostty/vt/types.h>
15
16/** @defgroup wasm WebAssembly Utilities
17 *
18 * Convenience functions for allocating various types in WebAssembly builds.
19 * **These are only available the libghostty-vt wasm module.**
20 *
21 * Ghostty relies on pointers to various types for ABI compatibility, and
22 * creating those pointers in Wasm can be tedious. These functions provide
23 * a purely additive set of utilities that simplify memory management in
24 * Wasm environments without changing the core C library API.
25 *
26 * @note These functions always use the default allocator. If you need
27 * custom allocation strategies, you should allocate types manually using
28 * your custom allocator. This is a very rare use case in the WebAssembly
29 * world so these are optimized for simplicity.
30 *
31 * ## Example Usage
32 *
33 * Here's a simple example of using the Wasm utilities with the key encoder:
34 *
35 * @code
36 * const { exports } = wasmInstance;
37 * const view = new DataView(wasmMemory.buffer);
38 *
39 * // Create key encoder
40 * const encoderPtr = exports.ghostty_wasm_alloc_opaque();
41 * exports.ghostty_key_encoder_new(null, encoderPtr);
42 * const encoder = view.getUint32(encoder, true);
43 *
44 * // Configure encoder with Kitty protocol flags
45 * const flagsPtr = exports.ghostty_wasm_alloc_u8();
46 * view.setUint8(flagsPtr, 0x1F);
47 * exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr);
48 *
49 * // Allocate output buffer and size pointer
50 * const bufferSize = 32;
51 * const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize);
52 * const writtenPtr = exports.ghostty_wasm_alloc_usize();
53 *
54 * // Encode the key event
55 * exports.ghostty_key_encoder_encode(
56 * encoder, eventPtr, bufPtr, bufferSize, writtenPtr
57 * );
58 *
59 * // Read encoded output
60 * const bytesWritten = view.getUint32(writtenPtr, true);
61 * const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten);
62 * @endcode
63 *
64 * @remark The code above is pretty ugly! This is the lowest level interface
65 * to the libghostty-vt Wasm module. In practice, this should be wrapped
66 * in a higher-level API that abstracts away all this.
67 *
68 * @{
69 */
70
71/**
72 * Allocate an opaque pointer. This can be used for any opaque pointer
73 * types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc.
74 *
75 * @return Pointer to allocated opaque pointer, or NULL if allocation failed
76 * @ingroup wasm
77 */
78GHOSTTY_API void** ghostty_wasm_alloc_opaque(void);
79
80/**
81 * Free an opaque pointer allocated by ghostty_wasm_alloc_opaque().
82 *
83 * @param ptr Pointer to free, or NULL (NULL is safely ignored)
84 * @ingroup wasm
85 */
86GHOSTTY_API void ghostty_wasm_free_opaque(void **ptr);
87
88/**
89 * Allocate an array of uint8_t values.
90 *
91 * @param len Number of uint8_t elements to allocate
92 * @return Pointer to allocated array, or NULL if allocation failed
93 * @ingroup wasm
94 */
95GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8_array(size_t len);
96
97/**
98 * Free an array allocated by ghostty_wasm_alloc_u8_array().
99 *
100 * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
101 * @param len Length of the array (must match the length passed to alloc)
102 * @ingroup wasm
103 */
104GHOSTTY_API void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len);
105
106/**
107 * Allocate an array of uint16_t values.
108 *
109 * @param len Number of uint16_t elements to allocate
110 * @return Pointer to allocated array, or NULL if allocation failed
111 * @ingroup wasm
112 */
113GHOSTTY_API uint16_t* ghostty_wasm_alloc_u16_array(size_t len);
114
115/**
116 * Free an array allocated by ghostty_wasm_alloc_u16_array().
117 *
118 * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
119 * @param len Length of the array (must match the length passed to alloc)
120 * @ingroup wasm
121 */
122GHOSTTY_API void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len);
123
124/**
125 * Allocate a single uint8_t value.
126 *
127 * @return Pointer to allocated uint8_t, or NULL if allocation failed
128 * @ingroup wasm
129 */
130GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8(void);
131
132/**
133 * Free a uint8_t allocated by ghostty_wasm_alloc_u8().
134 *
135 * @param ptr Pointer to free, or NULL (NULL is safely ignored)
136 * @ingroup wasm
137 */
138GHOSTTY_API void ghostty_wasm_free_u8(uint8_t *ptr);
139
140/**
141 * Allocate a single size_t value.
142 *
143 * @return Pointer to allocated size_t, or NULL if allocation failed
144 * @ingroup wasm
145 */
146GHOSTTY_API size_t* ghostty_wasm_alloc_usize(void);
147
148/**
149 * Free a size_t allocated by ghostty_wasm_alloc_usize().
150 *
151 * @param ptr Pointer to free, or NULL (NULL is safely ignored)
152 * @ingroup wasm
153 */
154GHOSTTY_API void ghostty_wasm_free_usize(size_t *ptr);
155
156/** @} */
157
158#endif /* __wasm__ */
159
160#endif /* GHOSTTY_VT_WASM_H */
packages/terminal/vendor/include/module.modulemap created+7
......@@ -0,0 +1,7 @@
1// This makes Ghostty available to the XCode build for the macOS app.
2// We append "Kit" to it not to be cute, but because targets have to have
3// unique names and we use Ghostty for other things.
4module GhosttyKit {
5 umbrella header "ghostty.h"
6 export *
7}
packages/terminal/vendor/patches/wasm-ascii-row.patch created+145
......@@ -0,0 +1,145 @@
1--- a/src/terminal/c/render.zig
2+++ b/src/terminal/c/render.zig
3@@ -59,6 +59,12 @@
4
5 /// C: GhosttyRenderStateRowCells
6 pub const RowCells = ?*RowCellsWrapper;
7+
8+/// C: GhosttyRenderStateAsciiCell
9+pub const AsciiCell = extern struct {
10+ codepoint: u32,
11+ foreground: u32,
12+};
13
14 /// C: GhosttyRenderStateDirty
15 pub const Dirty = renderpkg.RenderState.Dirty;
16@@ -475,6 +481,67 @@
17 alloc.destroy(cells);
18 }
19
20+/// Export one row when every retained cell is narrow ASCII with only an
21+/// optional foreground color. Unsupported rows use the canonical cell API.
22+pub fn row_cells_ascii(
23+ cells_: RowCells,
24+ out_: ?[*]AsciiCell,
25+ capacity: usize,
26+ out_len_: ?*usize,
27+) callconv(lib.calling_conv) Result {
28+ const cells = cells_ orelse return .invalid_value;
29+ const out = out_ orelse return .invalid_value;
30+ const out_len = out_len_ orelse return .invalid_value;
31+ out_len.* = 0;
32+
33+ var end = cells.raws.len;
34+ while (end > 0 and @as(u64, @bitCast(cells.raws[end - 1])) == 0) end -= 1;
35+ if (capacity < end) {
36+ out_len.* = end;
37+ return .out_of_space;
38+ }
39+
40+ for (cells.raws[0..end], cells.styles[0..end], 0..) |cell, style_value, index| {
41+ if (@as(u64, @bitCast(cell)) == 0) {
42+ out[index] = .{ .codepoint = 0, .foreground = std.math.maxInt(u32) };
43+ continue;
44+ }
45+ if (cell.content_tag != .codepoint or
46+ cell.wide != .narrow or
47+ cell.protected or
48+ cell.semantic_content != .output or
49+ cell.hyperlink)
50+ {
51+ return .no_value;
52+ }
53+
54+ const codepoint = cell.codepoint();
55+ if (codepoint < 0x20 or codepoint > 0x7E) return .no_value;
56+ const s: Style = if (cell.hasStyling()) style_value else .{};
57+ if (!s.bg_color.eql(.none) or
58+ !s.underline_color.eql(.none) or
59+ s.flags != @TypeOf(s.flags){})
60+ {
61+ return .no_value;
62+ }
63+ const foreground: u32 = switch (s.fg_color) {
64+ .none => std.math.maxInt(u32),
65+ .palette => |palette_index| rgb: {
66+ const color = cells.palette[palette_index];
67+ break :rgb (@as(u32, color.r) << 16) |
68+ (@as(u32, color.g) << 8) |
69+ color.b;
70+ },
71+ .rgb => |color| (@as(u32, color.r) << 16) |
72+ (@as(u32, color.g) << 8) |
73+ color.b,
74+ };
75+ out[index] = .{ .codepoint = codepoint, .foreground = foreground };
76+ }
77+ out_len.* = end;
78+ return .success;
79+}
80+
81 /// C: GhosttyRenderStateRowCellsData
82 pub const RowCellsData = enum(c_int) {
83 invalid = 0,
84--- a/src/terminal/c/types.zig
85+++ b/src/terminal/c/types.zig
86@@ -60,6 +60,7 @@
87 .{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
88 .{ "GhosttyPoint", StructInfo.init(point.Point.C) },
89 .{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
90+ .{ "GhosttyRenderStateAsciiCell", StructInfo.init(render.AsciiCell) },
91 .{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
92 .{ "GhosttySelectionGestureBehaviors", StructInfo.init(selection_gesture.Behaviors) },
93 .{ "GhosttySelectionGestureGeometry", StructInfo.init(selection_gesture.Geometry) },
94--- a/src/terminal/c/main.zig
95+++ b/src/terminal/c/main.zig
96@@ -100,6 +100,7 @@
97 pub const render_state_row_cells_select = render.row_cells_select;
98 pub const render_state_row_cells_get = render.row_cells_get;
99 pub const render_state_row_cells_get_multi = render.row_cells_get_multi;
100+pub const render_state_row_cells_ascii = render.row_cells_ascii;
101 pub const render_state_row_cells_free = render.row_cells_free;
102
103 pub const sgr_new = sgr.new;
104--- a/src/lib_vt.zig
105+++ b/src/lib_vt.zig
106@@ -258,6 +258,7 @@
107 @export(&c.render_state_row_cells_select, .{ .name = "ghostty_render_state_row_cells_select" });
108 @export(&c.render_state_row_cells_get, .{ .name = "ghostty_render_state_row_cells_get" });
109 @export(&c.render_state_row_cells_get_multi, .{ .name = "ghostty_render_state_row_cells_get_multi" });
110+ @export(&c.render_state_row_cells_ascii, .{ .name = "ghostty_render_state_row_cells_ascii" });
111 @export(&c.render_state_row_cells_free, .{ .name = "ghostty_render_state_row_cells_free" });
112 @export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
113 @export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });
114--- a/include/ghostty/vt/render.h
115+++ b/include/ghostty/vt/render.h
116@@ -272,6 +272,12 @@
117 /** End column of the row-local selection range, inclusive. */
118 uint16_t end_x;
119 } GhosttyRenderStateRowSelection;
120+
121+/** Narrow ASCII cell returned by the optional whole-row render fast path. */
122+typedef struct {
123+ uint32_t codepoint;
124+ uint32_t foreground;
125+} GhosttyRenderStateAsciiCell;
126
127 /**
128 * Render-state color information.
129@@ -776,6 +782,16 @@
130 const GhosttyRenderStateRowCellsData* keys,
131 void** values,
132 size_t* out_written);
133+
134+/**
135+ * Export the row when every retained cell is narrow printable ASCII with only
136+ * an optional foreground color. GHOSTTY_NO_VALUE selects the canonical API.
137+ */
138+GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_ascii(
139+ GhosttyRenderStateRowCells cells,
140+ GhosttyRenderStateAsciiCell* out,
141+ size_t capacity,
142+ size_t* out_len);
143
144 /**
145 * Free a row cells instance.
packages/terminal/vendor/patches/wasm-batched-vt.patch created+12
......@@ -0,0 +1,12 @@
1--- a/src/terminal/stream.zig
2+++ b/src/terminal/stream.zig
3@@ -514,8 +514,7 @@
4 pub inline fn nextSlice(self: *Self, input: []const u8) void {
5- // Disable SIMD optimizations if build requests it or if our
6- // manual debug mode is on.
7- if (comptime debug or !build_options.simd) {
8+ // Manual debug mode preserves one action per codepoint.
9+ if (comptime debug) {
10 for (input) |c| self.next(c);
11 return;
12 }
packages/terminal/vendor/verify.mjs created+199
......@@ -0,0 +1,199 @@
1import { execFileSync } from "node:child_process";
2import { createHash } from "node:crypto";
3import { readdir, readFile } from "node:fs/promises";
4import { resolve } from "node:path";
5import { fileURLToPath } from "node:url";
6
7const vendorDir = new URL("./", import.meta.url);
8const pinnedCommit = (await readFile(new URL("COMMIT", vendorDir), "utf8")).trim();
9const wasm = await readFile(new URL("ghostty-vt.wasm", vendorDir));
10const expectedHash = (await readFile(new URL("SHA256", vendorDir), "utf8")).trim();
11const actualHash = createHash("sha256").update(wasm).digest("hex");
12if (actualHash !== expectedHash) {
13 throw new Error(`ghostty-vt.wasm SHA-256 mismatch: expected ${expectedHash}, got ${actualHash}`);
14}
15
16const instance = await WebAssembly.instantiate(wasm, { env: { log() {} } });
17const exports = instance.instance.exports;
18if (!(exports.memory instanceof WebAssembly.Memory)) throw new Error("wasm memory export missing");
19if (!(exports.__indirect_function_table instanceof WebAssembly.Table)) {
20 throw new Error("wasm function table export missing");
21}
22
23const runtimeSources = await Promise.all(
24 ["module.ts", "terminal.ts"].map((file) =>
25 readFile(new URL(`../src/ghostty/${file}`, vendorDir), "utf8"),
26 ),
27);
28const requiredExports = new Set(["ghostty_type_json"]);
29const requiredLayouts = new Map();
30const requireLayout = (name, field) => {
31 let fields = requiredLayouts.get(name);
32 if (fields === undefined) {
33 fields = new Set();
34 requiredLayouts.set(name, fields);
35 }
36 if (field !== undefined) fields.add(field);
37};
38for (const source of runtimeSources) {
39 for (const match of source.matchAll(/\.(?:fn|create)\("([^"]+)"\)/g)) {
40 requiredExports.add(match[1]);
41 }
42 for (const match of source.matchAll(/\.allocStruct\("([^"]+)"\)/g)) {
43 requireLayout(match[1]);
44 }
45 for (const match of source.matchAll(/\.layout\["([^"]+)"\]/g)) {
46 requireLayout(match[1]);
47 }
48 for (const match of source.matchAll(/\.field\("([^"]+)",\s*"([^"]+)"\)/g)) {
49 requireLayout(match[1], match[2]);
50 }
51}
52for (const [name, fields] of Object.entries({
53 GhosttyClipboardWrite: ["location", "contents", "contents_len"],
54 GhosttyFormatterScreenExtra: ["size"],
55 GhosttyFormatterTerminalExtra: ["size", "screen"],
56 GhosttyMouseEncoderSize: [
57 "screen_width",
58 "screen_height",
59 "cell_width",
60 "cell_height",
61 "padding_top",
62 "padding_bottom",
63 "padding_left",
64 "padding_right",
65 ],
66 GhosttyPoint: ["tag", "value"],
67 GhosttyTerminalOptions: ["cols", "rows", "max_scrollback"],
68 GhosttyTerminalScrollbar: ["total", "offset", "len"],
69})) {
70 for (const field of fields) requireLayout(name, field);
71}
72for (const name of requiredExports) {
73 if (typeof exports[name] !== "function") throw new Error(`required wasm export missing: ${name}`);
74}
75const jsonPtr = exports.ghostty_type_json();
76const bytes = new Uint8Array(exports.memory.buffer, jsonPtr);
77const end = bytes.indexOf(0);
78if (end < 0) throw new Error("type metadata is not null-terminated");
79const layout = JSON.parse(new TextDecoder().decode(bytes.subarray(0, end)));
80for (const [name, requiredFields] of requiredLayouts) {
81 const info = layout[name];
82 if (
83 !info ||
84 !Number.isInteger(info.size) ||
85 info.size <= 0 ||
86 !Number.isInteger(info.align) ||
87 info.align <= 0
88 ) {
89 throw new Error(`required type layout missing or invalid: ${name}`);
90 }
91 for (const field of requiredFields) {
92 const value = info.fields?.[field];
93 if (
94 !value ||
95 !Number.isInteger(value.offset) ||
96 value.offset < 0 ||
97 !Number.isInteger(value.size) ||
98 value.size <= 0 ||
99 value.offset + value.size > info.size
100 ) {
101 throw new Error(`required field layout missing or invalid: ${name}.${field}`);
102 }
103 }
104}
105const ascii = layout.GhosttyRenderStateAsciiCell;
106if (
107 ascii?.size !== 8 ||
108 ascii.align !== 4 ||
109 ascii.fields?.codepoint?.type !== "u32" ||
110 ascii.fields.codepoint.offset !== 0 ||
111 ascii.fields.codepoint.size !== 4 ||
112 ascii.fields?.foreground?.type !== "u32" ||
113 ascii.fields.foreground.offset !== 4 ||
114 ascii.fields.foreground.size !== 4
115) {
116 throw new Error("invalid GhosttyRenderStateAsciiCell layout");
117}
118
119try {
120 execFileSync(
121 process.execPath,
122 [fileURLToPath(new URL("gen-constants.mjs", vendorDir)), "--check"],
123 { stdio: "pipe" },
124 );
125} catch (error) {
126 const detail = error instanceof Error && "stderr" in error ? String(error.stderr).trim() : "";
127 throw new Error(`generated constants verification failed${detail === "" ? "" : `: ${detail}`}`, {
128 cause: error,
129 });
130}
131
132const sourceArg = process.argv[2];
133if (sourceArg !== undefined) {
134 const source = resolve(sourceArg);
135 let sourceCommit;
136 try {
137 sourceCommit = execFileSync("git", ["-C", source, "rev-parse", "--verify", "HEAD^{commit}"], {
138 encoding: "utf8",
139 stdio: ["ignore", "pipe", "pipe"],
140 }).trim();
141 } catch (error) {
142 const detail = error instanceof Error && "stderr" in error ? String(error.stderr).trim() : "";
143 throw new Error(`unable to read source checkout commit${detail === "" ? "" : `: ${detail}`}`, {
144 cause: error,
145 });
146 }
147 if (sourceCommit !== pinnedCommit) {
148 throw new Error(
149 `source checkout commit mismatch: expected ${pinnedCommit}, got ${sourceCommit}`,
150 );
151 }
152 const patches = (await readdir(new URL("patches/", vendorDir)))
153 .filter((name) => name.endsWith(".patch"))
154 .sort();
155 for (const patch of patches) {
156 try {
157 execFileSync(
158 "git",
159 [
160 "-C",
161 source,
162 "apply",
163 "--reverse",
164 "--check",
165 fileURLToPath(new URL(`patches/${patch}`, vendorDir)),
166 ],
167 { stdio: ["ignore", "pipe", "pipe"] },
168 );
169 } catch (error) {
170 const detail = error instanceof Error && "stderr" in error ? String(error.stderr).trim() : "";
171 throw new Error(
172 `source checkout does not contain exact patch ${patch}${detail === "" ? "" : `: ${detail}`}`,
173 { cause: error },
174 );
175 }
176 }
177 const pageList = await readFile(resolve(source, "src/terminal/PageList.zig"), "utf8");
178 if (!/WasmAllocator reuses freed slots[\s\S]{0,500}@memset\(page_buf, 0\)/.test(pageList)) {
179 throw new Error("upstream wasm page-zeroing guard missing");
180 }
181 const types = await readFile(resolve(source, "src/terminal/c/types.zig"), "utf8");
182 if (!types.includes('"GhosttyRenderStateAsciiCell", StructInfo.init(render.AsciiCell)')) {
183 throw new Error("patched ASCII cell type metadata missing");
184 }
185 const stream = await readFile(resolve(source, "src/terminal/stream.zig"), "utf8");
186 if (
187 !stream.includes("if (comptime debug) {") ||
188 stream.includes("if (comptime debug or !build_options.simd) {")
189 ) {
190 throw new Error("patched wasm batched VT source missing");
191 }
192 const sourceRenderHeader = await readFile(resolve(source, "include/ghostty/vt/render.h"));
193 const vendoredRenderHeader = await readFile(new URL("include/ghostty/vt/render.h", vendorDir));
194 if (!sourceRenderHeader.equals(vendoredRenderHeader)) {
195 throw new Error("patched source render header does not match vendored render header");
196 }
197}
198
199console.log(`ghostty vendor verified: commit ${pinnedCommit}, sha256 ${actualHash}`);
pnpm-lock.yaml+48-11
......@@ -760,6 +760,9 @@ importers:
760760 '@clerk/react':
761761 specifier: 6.16.1
762762 version: 6.16.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
763 '@clo/terminal':
764 specifier: workspace:*
765 version: link:../../packages/terminal
763766 '@daypicker/react':
764767 specifier: ^10.0.1
765768 version: 10.0.1(@types/react@19.3.0)(react@19.2.6)
......@@ -1399,7 +1402,7 @@ importers:
13991402 version: 1.15.13
14001403 '@opencode/client':
14011404 specifier: 2.0.28
1402 version: 2.0.28(effect@4.0.4)
1405 version: 2.0.28(effect@4.0.4)(solid-js@1.9.17)
14031406 '@opencode/protocol':
14041407 specifier: 2.0.28
14051408 version: 2.0.28
......@@ -1832,6 +1835,30 @@ importers:
18321835 specifier: 1.0.0
18331836 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
18341837
1838 packages/terminal:
1839 devDependencies:
1840 '@types/react':
1841 specifier: ~19.3.0
1842 version: 19.3.0
1843 '@types/react-dom':
1844 specifier: ~19.2.3
1845 version: 19.2.3(@types/react@19.3.0)
1846 react:
1847 specifier: 19.2.6
1848 version: 19.2.6
1849 react-dom:
1850 specifier: 19.2.6
1851 version: 19.2.6(react@19.2.6)
1852 solid-js:
1853 specifier: ^1.9.14
1854 version: 1.9.17
1855 vite:
1856 specifier: npm:@voidzero-dev/vite-plus-core@1.0.0
1857 version: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)'
1858 vite-plus:
1859 specifier: 1.0.0
1860 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
1861
18351862 scripts:
18361863 dependencies:
18371864 '@effect/platform-node':
......@@ -11203,14 +11230,14 @@ packages:
1120311230 resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
1120411231 engines: {node: '>=10'}
1120511232
11206 seroval-plugins@1.5.4:
11207 resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==}
11233 seroval-plugins@1.6.9:
11234 resolution: {integrity: sha512-kiDK/Tb8IWZ61WEU8pA2PDmAuw37gKph9Y77OZlD56mg0Y2g0Bg7gZ1HeHhs5d40w+nRoUq/wj08V26ilbez9g==}
1120811235 engines: {node: '>=10'}
1120911236 peerDependencies:
1121011237 seroval: ^1.0
1121111238
11212 seroval@1.5.4:
11213 resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==}
11239 seroval@1.6.9:
11240 resolution: {integrity: sha512-UeEzHwAK+yl0YLbsvROV1t5yMYEwZZ0CfKYxDvUgS2fO9Vxdf8djqVIB+OuVpuawRNPqw68LE1Du/GMsW2ZPqA==}
1121411241 engines: {node: '>=10'}
1121511242
1121611243 serve-static@1.16.3:
......@@ -11333,6 +11360,9 @@ packages:
1133311360 resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
1133411361 engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
1133511362
11363 solid-js@1.9.17:
11364 resolution: {integrity: sha512-4GAmLOWPfNV8H9Jfn2LSFTpNEG6jiOKPiwd5lnLGQtehHaX6l3TOLzJqQ7mqwB/3k1Yy3zyumFVopmc/tm34ow==}
11365
1133611366 source-map-js@1.2.1:
1133711367 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1133811368 engines: {node: '>=0.10.0'}
......@@ -15369,12 +15399,13 @@ snapshots:
1536915399 dependencies:
1537015400 cross-spawn: 7.0.6
1537115401
15372 '@opencode/client@2.0.28(effect@4.0.4)':
15402 '@opencode/client@2.0.28(effect@4.0.4)(solid-js@1.9.17)':
1537315403 dependencies:
1537415404 '@opencode/protocol': 2.0.28
1537515405 '@opencode/schema': 2.0.28
1537615406 optionalDependencies:
1537715407 effect: 4.0.4
15408 solid-js: 1.9.17
1537815409
1537915410 '@opencode/protocol@2.0.28':
1538015411 dependencies:
......@@ -16520,8 +16551,8 @@ snapshots:
1652016551 dependencies:
1652116552 '@tanstack/history': 1.162.0
1652216553 cookie-es: 3.1.1
16523 seroval: 1.5.4
16524 seroval-plugins: 1.5.4(seroval@1.5.4)
16554 seroval: 1.6.9
16555 seroval-plugins: 1.6.9(seroval@1.6.9)
1652516556
1652616557 '@tanstack/router-generator@1.167.12':
1652716558 dependencies:
......@@ -22611,11 +22642,11 @@ snapshots:
2261122642 type-fest: 0.13.1
2261222643 optional: true
2261322644
22614 seroval-plugins@1.5.4(seroval@1.5.4):
22645 seroval-plugins@1.6.9(seroval@1.6.9):
2261522646 dependencies:
22616 seroval: 1.5.4
22647 seroval: 1.6.9
2261722648
22618 seroval@1.5.4: {}
22649 seroval@1.6.9: {}
2261922650
2262022651 serve-static@1.16.3:
2262122652 dependencies:
......@@ -22788,6 +22819,12 @@ snapshots:
2278822819 smart-buffer: 4.2.0
2278922820 optional: true
2279022821
22822 solid-js@1.9.17:
22823 dependencies:
22824 csstype: 3.2.3
22825 seroval: 1.6.9
22826 seroval-plugins: 1.6.9(seroval@1.6.9)
22827
2279122828 source-map-js@1.2.1: {}
2279222829
2279322830 source-map-support@0.5.21:
third-party-licenses.config.json+25-3
......@@ -101,9 +101,31 @@
101101 },
102102 {
103103 "bundles": ["assets", "web"],
104 "license": "MIT",
105 "name": "Symbols Nerd Font Mono",
106 "noticeFile": "apps/web/src/terminal/ghostty/fonts/LICENSE",
104 "license": "OFL-1.1",
105 "name": "Iosevka Fixed Extended (terminal subset)",
106 "noticeFile": "packages/terminal/fonts/OFL.txt",
107 "sourceUrl": "https://github.com/be5invis/Iosevka"
108 },
109 {
110 "bundles": ["assets", "web"],
111 "license": "MIT AND OFL-1.1 AND CC-BY-4.0 AND Apache-2.0",
112 "name": "Nerd Fonts Symbols (terminal subset)",
113 "noticeFiles": [
114 "packages/terminal/fonts/NOTICES.md",
115 "packages/terminal/fonts/licenses/Apache-2.0.txt",
116 "packages/terminal/fonts/licenses/CC-BY-4.0.txt",
117 "packages/terminal/fonts/licenses/Devicons-MIT.txt",
118 "packages/terminal/fonts/licenses/Font-Awesome-Extension-MIT.txt",
119 "packages/terminal/fonts/licenses/Font-Awesome-LICENSE.txt",
120 "packages/terminal/fonts/licenses/Material-Design-Icons-NOTICE.txt",
121 "packages/terminal/fonts/licenses/NerdFonts-LICENSE.txt",
122 "packages/terminal/fonts/licenses/NerdFonts-Symbols-Only-MIT.txt",
123 "packages/terminal/fonts/licenses/NerdFonts-license-audit.md",
124 "packages/terminal/fonts/licenses/Octicons-MIT.txt",
125 "packages/terminal/fonts/licenses/Pomicons-OFL.txt",
126 "packages/terminal/fonts/licenses/Powerline-Extra-MIT.txt",
127 "packages/terminal/fonts/licenses/Weather-Icons-OFL.txt"
128 ],
107129 "sourceUrl": "https://github.com/ryanoasis/nerd-fonts"
108130 },
109131 {