| author | |
| committer | |
| log | 06b388143eb05c2ed94bc04901560d243bb58823 |
| tree | c02e3bb385f484720ac9b6458fecc19b016798ef |
| parent | 134f2f0765bf03e98da10fc3f7c1614dbe99a9fe |
| signature | Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ (~clover) |
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-sol137 files changed, 38402 insertions(+), 8258 deletions(-)
apps/web/package.json+1-1| ... | ... | @@ -4,7 +4,6 @@ |
| 4 | 4 | "private": true, |
| 5 | 5 | "type": "module", |
| 6 | 6 | "scripts": { |
| 7 | "build:ghostty-wasm": "bash scripts/build-libghostty-wasm.sh", | |
| 8 | 7 | "dev": "vp dev", |
| 9 | 8 | "build": "vp build", |
| 10 | 9 | "preview": "vp preview", |
| ... | ... | @@ -15,6 +14,7 @@ |
| 15 | 14 | "@base-ui/react": "^1.4.1", |
| 16 | 15 | "@clerk/electron": "catalog:", |
| 17 | 16 | "@clerk/react": "catalog:", |
| 17 | "@clo/terminal": "workspace:*", | |
| 18 | 18 | "@daypicker/react": "^10.0.1", |
| 19 | 19 | "@dnd-kit/core": "^6.3.1", |
| 20 | 20 | "@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 | ||
| 3 | set -euo pipefail | |
| 4 | ||
| 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| 6 | WEB_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" | |
| 7 | REPO_DIR="$(cd "${WEB_DIR}/../.." && pwd)" | |
| 8 | CANONICAL_VENDOR_DIR="${REPO_DIR}/native/libghostty-vt" | |
| 9 | VENDOR_DIR="${WEB_DIR}/src/terminal/ghostty/vendor" | |
| 10 | ||
| 11 | GHOSTTY_REVISION="$(tr -d '[:space:]' < "${CANONICAL_VENDOR_DIR}/VERSION")" | |
| 12 | GHOSTTY_SOURCE_DIR="${GHOSTTY_SOURCE_DIR:-${HOME}/.cache/t3code/ghostty-${GHOSTTY_REVISION:0:8}}" | |
| 13 | GHOSTTY_ZIG_VERSION="${GHOSTTY_ZIG_VERSION:-0.15.2}" | |
| 14 | GHOSTTY_ZIG="${GHOSTTY_ZIG:-}" | |
| 15 | ||
| 16 | log() { | |
| 17 | printf '[libghostty-vt-wasm] %s\n' "$*" | |
| 18 | } | |
| 19 | ||
| 20 | die() { | |
| 21 | printf '[libghostty-vt-wasm] error: %s\n' "$*" >&2 | |
| 22 | exit 1 | |
| 23 | } | |
| 24 | ||
| 25 | require_cmd() { | |
| 26 | command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" | |
| 27 | } | |
| 28 | ||
| 29 | ensure_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 | ||
| 68 | ensure_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 | ||
| 92 | ensure_zig | |
| 93 | ensure_ghostty_source | |
| 94 | ||
| 95 | build_root="$(mktemp -d)" | |
| 96 | trap 'rm -rf "${build_root}"' EXIT | |
| 97 | ||
| 98 | log "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 | ||
| 113 | mkdir -p "${VENDOR_DIR}" | |
| 114 | cp "${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" | |
| 122 | chmod 0644 "${VENDOR_DIR}/ghostty-write-pty.wasm" | |
| 123 | log "wrote ${VENDOR_DIR}/ghostty-vt.wasm" |
apps/web/src/components/ChatView.tsx+97-58| ... | ... | @@ -566,6 +566,7 @@ import { |
| 566 | 566 | shouldRefocusComposerOnWindowFocus, |
| 567 | 567 | } from "./ChatView.logic"; |
| 568 | 568 | import { useLocalStorage } from "~/hooks/useLocalStorage"; |
| 569 | import { useTerminalPlacement } from "~/hooks/useTerminalPlacement"; | |
| 569 | 570 | import { useComposerHandleContext } from "../composerHandleContext"; |
| 570 | 571 | import { |
| 571 | 572 | awaitAttachmentUploads, |
| ... | ... | @@ -1953,6 +1954,7 @@ export default function ChatView(props: ChatViewProps) { |
| 1953 | 1954 | // When set, the thread-change reset effect will open the sidebar instead of closing it. |
| 1954 | 1955 | // Used by "Implement in a new thread" to carry the sidebar-open intent across navigation. |
| 1955 | 1956 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); |
| 1957 | const [terminalPlacement] = useTerminalPlacement(); | |
| 1956 | 1958 | const [pullRequestDialogState, setPullRequestDialogState] = |
| 1957 | 1959 | useState<PullRequestDialogState | null>(null); |
| 1958 | 1960 | const [terminalUiLaunchContext, setTerminalUiLaunchContext] = |
| ... | ... | @@ -2326,6 +2328,9 @@ export default function ChatView(props: ChatViewProps) { |
| 2326 | 2328 | const activeRightPanelSurface = useRightPanelStore((state) => |
| 2327 | 2329 | selectActiveRightPanelSurface(state.byThreadKey, activeThreadRef), |
| 2328 | 2330 | ); |
| 2331 | const terminalVisible = | |
| 2332 | terminalUiState.terminalOpen || | |
| 2333 | (rightPanelState.isOpen && activeRightPanelSurface?.kind === "terminal"); | |
| 2329 | 2334 | const activePreviewState = useThreadPreviewState(activeThreadRef); |
| 2330 | 2335 | const activePreviewServerEpoch = activePreviewState.serverEpoch; |
| 2331 | 2336 | const previewSessionsReady = !activeEnvironmentServerBrowser || activePreviewState.listLoaded; |
| ... | ... | @@ -4805,8 +4810,69 @@ export default function ChatView(props: ChatViewProps) { |
| 4805 | 4810 | }, |
| 4806 | 4811 | [activeThreadRef, storeSetTerminalOpen], |
| 4807 | 4812 | ); |
| 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 | ]); | |
| 4808 | 4842 | const toggleTerminalVisibility = useCallback(() => { |
| 4809 | 4843 | 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 | } | |
| 4810 | 4876 | const nextOpen = !terminalUiState.terminalOpen; |
| 4811 | 4877 | if ( |
| 4812 | 4878 | nextOpen && |
| ... | ... | @@ -4850,6 +4916,8 @@ export default function ChatView(props: ChatViewProps) { |
| 4850 | 4916 | } |
| 4851 | 4917 | setTerminalOpen(nextOpen); |
| 4852 | 4918 | }, [ |
| 4919 | addTerminalSurface, | |
| 4920 | terminalPlacement, | |
| 4853 | 4921 | activeServerOrderedTerminalIds, |
| 4854 | 4922 | activeProject, |
| 4855 | 4923 | activeThreadId, |
| ... | ... | @@ -5026,7 +5094,6 @@ export default function ChatView(props: ChatViewProps) { |
| 5026 | 5094 | cwd: targetCwd, |
| 5027 | 5095 | worktreePath: targetWorktreePath, |
| 5028 | 5096 | }); |
| 5029 | setTerminalOpen(true); | |
| 5030 | 5097 | if (!activeThreadRef) { |
| 5031 | 5098 | return; |
| 5032 | 5099 | } |
| ... | ... | @@ -5058,10 +5125,18 @@ export default function ChatView(props: ChatViewProps) { |
| 5058 | 5125 | env: runtimeEnv, |
| 5059 | 5126 | }; |
| 5060 | 5127 | |
| 5061 | if (shouldCreateNewTerminal) { | |
| 5062 | storeNewTerminal(activeThreadRef, targetTerminalId); | |
| 5128 | if ( | |
| 5129 | terminalPlacement === "right" && | |
| 5130 | !terminalUiState.terminalIds.includes(targetTerminalId) | |
| 5131 | ) { | |
| 5132 | useRightPanelStore.getState().openTerminal(activeThreadRef, targetTerminalId); | |
| 5063 | 5133 | } else { |
| 5064 | storeSetActiveTerminal(activeThreadRef, targetTerminalId); | |
| 5134 | setTerminalOpen(true); | |
| 5135 | if (shouldCreateNewTerminal) { | |
| 5136 | storeNewTerminal(activeThreadRef, targetTerminalId); | |
| 5137 | } else { | |
| 5138 | storeSetActiveTerminal(activeThreadRef, targetTerminalId); | |
| 5139 | } | |
| 5065 | 5140 | } |
| 5066 | 5141 | |
| 5067 | 5142 | const openResult = await openTerminal({ environmentId, input: openTerminalInput }); |
| ... | ... | @@ -5132,6 +5207,8 @@ export default function ChatView(props: ChatViewProps) { |
| 5132 | 5207 | canReuseTerminal, |
| 5133 | 5208 | allocateTerminalId, |
| 5134 | 5209 | runningTerminalIds, |
| 5210 | terminalPlacement, | |
| 5211 | terminalUiState.terminalIds, | |
| 5135 | 5212 | terminalUiState.activeTerminalId, |
| 5136 | 5213 | writeTerminal, |
| 5137 | 5214 | ], |
| ... | ... | @@ -5928,35 +6005,6 @@ export default function ChatView(props: ChatViewProps) { |
| 5928 | 6005 | createBrowserSurface, |
| 5929 | 6006 | previewPanelOpen, |
| 5930 | 6007 | ]); |
| 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 | ]); | |
| 5960 | 6008 | const splitPanelTerminal = useCallback( |
| 5961 | 6009 | (direction: "horizontal" | "vertical" = "horizontal") => { |
| 5962 | 6010 | if ( |
| ... | ... | @@ -6042,27 +6090,25 @@ export default function ChatView(props: ChatViewProps) { |
| 6042 | 6090 | ); |
| 6043 | 6091 | const requestCloseTerminal = useCallback( |
| 6044 | 6092 | (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) => { | |
| 6048 | 6095 | if (confirmed && readEnvironmentScope(environmentId, AuthTerminalOperateScope)) { |
| 6049 | 6096 | closeTerminal(terminalId); |
| 6050 | 6097 | } |
| 6051 | 6098 | }); |
| 6052 | 6099 | }, |
| 6053 | [hasTerminalWriteAccess, activeTerminalLabelsById, closeTerminal, environmentId], | |
| 6100 | [hasTerminalWriteAccess, activeThreadRef, closeTerminal, environmentId], | |
| 6054 | 6101 | ); |
| 6055 | 6102 | const requestClosePanelTerminal = useCallback( |
| 6056 | 6103 | (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) => { | |
| 6060 | 6106 | if (confirmed && readEnvironmentScope(environmentId, AuthTerminalOperateScope)) { |
| 6061 | 6107 | closePanelTerminal(terminalId); |
| 6062 | 6108 | } |
| 6063 | 6109 | }); |
| 6064 | 6110 | }, |
| 6065 | [hasTerminalWriteAccess, activeTerminalLabelsById, closePanelTerminal, environmentId], | |
| 6111 | [hasTerminalWriteAccess, activeThreadRef, closePanelTerminal, environmentId], | |
| 6066 | 6112 | ); |
| 6067 | 6113 | const activateRightPanelSurface = useCallback( |
| 6068 | 6114 | (surface: RightPanelSurface) => { |
| ... | ... | @@ -6206,26 +6252,19 @@ export default function ChatView(props: ChatViewProps) { |
| 6206 | 6252 | finishClose(); |
| 6207 | 6253 | return; |
| 6208 | 6254 | } |
| 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) => { | |
| 6218 | 6262 | if (confirmed) { |
| 6219 | 6263 | finishClose(); |
| 6220 | 6264 | } |
| 6221 | 6265 | }); |
| 6222 | 6266 | }, |
| 6223 | [ | |
| 6224 | activeThreadRef, | |
| 6225 | activeTerminalLabelsById, | |
| 6226 | closeAfterAgentBrowserConfirmation, | |
| 6227 | finishRightPanelSurfaceClose, | |
| 6228 | ], | |
| 6267 | [activeThreadRef, closeAfterAgentBrowserConfirmation, finishRightPanelSurfaceClose], | |
| 6229 | 6268 | ); |
| 6230 | 6269 | const closeOtherRightPanelSurfaces = useCallback( |
| 6231 | 6270 | (surface: RightPanelSurface) => { |
| ... | ... | @@ -11340,7 +11379,7 @@ export default function ChatView(props: ChatViewProps) { |
| 11340 | 11379 | }; |
| 11341 | 11380 | const panelToggleControlProps = { |
| 11342 | 11381 | terminalAvailable: activeProject !== null, |
| 11343 | terminalOpen: terminalUiState.terminalOpen, | |
| 11382 | terminalOpen: terminalVisible, | |
| 11344 | 11383 | terminalShortcutLabel: shortcutLabelForCommand(keybindings, "terminal.toggle"), |
| 11345 | 11384 | threadPanelOpen, |
| 11346 | 11385 | threadPanelPresentation, |
| ... | ... | @@ -11874,7 +11913,7 @@ export default function ChatView(props: ChatViewProps) { |
| 11874 | 11913 | resolvedTheme={resolvedTheme} |
| 11875 | 11914 | settings={settings} |
| 11876 | 11915 | keybindings={keybindings} |
| 11877 | terminalOpen={Boolean(terminalUiState.terminalOpen)} | |
| 11916 | terminalOpen={terminalVisible} | |
| 11878 | 11917 | gitCwd={gitCwd} |
| 11879 | 11918 | pullRequestProjectId={ |
| 11880 | 11919 | supportsPullRequests ? (activeProject?.id ?? null) : null |
apps/web/src/components/ThreadTerminalDrawer.permissions.test.tsx+39-17| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | import { |
| 2 | AuthOrchestrationOperateScope, | |
| 2 | 3 | AuthTerminalOperateScope, |
| 3 | 4 | EnvironmentId, |
| 4 | 5 | ThreadId, |
| ... | ... | @@ -9,23 +10,16 @@ import { nextTerminalAttachSeedState } from "@t3tools/client-runtime/state/termi |
| 9 | 10 | import { AsyncResult } from "effect/reactivity"; |
| 10 | 11 | import { act, create, type ReactTestRenderer } from "react-test-renderer"; |
| 11 | 12 | import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; |
| 12 | import type { | |
| 13 | GhosttyTerminalSurface, | |
| 14 | GhosttyTerminalSurfaceOptions, | |
| 15 | } from "~/terminal/ghostty/surface"; | |
| 13 | import type { TerminalSurface, TerminalSurfaceOptions } from "~/terminal/TerminalSurface"; | |
| 16 | 14 | |
| 17 | 15 | const state = vi.hoisted(() => ({ |
| 18 | 16 | allowed: true, |
| 17 | editorAllowed: true, | |
| 19 | 18 | listeners: new Set<() => void>(), |
| 20 | 19 | resize: vi.fn(), |
| 21 | 20 | otherCommand: vi.fn(), |
| 22 | 21 | createSurface: |
| 23 | vi.fn< | |
| 24 | ( | |
| 25 | mount: HTMLElement, | |
| 26 | options: GhosttyTerminalSurfaceOptions, | |
| 27 | ) => Promise<GhosttyTerminalSurface> | |
| 28 | >(), | |
| 22 | vi.fn<(mount: HTMLElement, options: TerminalSurfaceOptions) => Promise<TerminalSurface>>(), | |
| 29 | 23 | })); |
| 30 | 24 | |
| 31 | 25 | vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null })); |
| ... | ... | @@ -47,14 +41,20 @@ vi.mock("../state/use-atom-command", () => ({ |
| 47 | 41 | vi.mock("../state/terminalSessions", () => ({ |
| 48 | 42 | useAttachedTerminalSession: () => session, |
| 49 | 43 | })); |
| 50 | vi.mock("~/terminal/ghostty/surface", () => ({ | |
| 51 | GhosttyTerminalSurface: { create: state.createSurface }, | |
| 44 | vi.mock("~/terminal/TerminalSurface", () => ({ | |
| 45 | TerminalSurface: { create: state.createSurface }, | |
| 52 | 46 | })); |
| 53 | 47 | vi.mock("../state/session", async () => { |
| 54 | 48 | 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 | }; | |
| 58 | 58 | return { |
| 59 | 59 | readEnvironmentScope, |
| 60 | 60 | useEnvironmentScope: (id: EnvironmentId | null, scope: AuthEnvironmentScope) => |
| ... | ... | @@ -79,6 +79,7 @@ let renderer: ReactTestRenderer | undefined; |
| 79 | 79 | |
| 80 | 80 | beforeEach(() => { |
| 81 | 81 | state.allowed = true; |
| 82 | state.editorAllowed = true; | |
| 82 | 83 | state.listeners.clear(); |
| 83 | 84 | state.resize.mockReset().mockResolvedValue(AsyncResult.success(undefined)); |
| 84 | 85 | state.otherCommand.mockReset().mockResolvedValue(AsyncResult.success(undefined)); |
| ... | ... | @@ -106,7 +107,7 @@ afterEach(async () => { |
| 106 | 107 | vi.unstubAllGlobals(); |
| 107 | 108 | }); |
| 108 | 109 | |
| 109 | it("rechecks the target terminal grant when a retained surface callback reports a resize", async () => { | |
| 110 | async function renderViewport(cwd = "/repo") { | |
| 110 | 111 | await act(async () => { |
| 111 | 112 | renderer = create( |
| 112 | 113 | <TerminalViewport |
| ... | ... | @@ -115,7 +116,7 @@ it("rechecks the target terminal grant when a retained surface callback reports |
| 115 | 116 | threadId={threadRef.threadId} |
| 116 | 117 | terminalId="terminal-1" |
| 117 | 118 | terminalLabel="Terminal 1" |
| 118 | cwd="/repo" | |
| 119 | cwd={cwd} | |
| 119 | 120 | onSessionExited={state.otherCommand} |
| 120 | 121 | focusRequestId={0} |
| 121 | 122 | autoFocus={false} |
| ... | ... | @@ -127,6 +128,10 @@ it("rechecks the target terminal grant when a retained surface callback reports |
| 127 | 128 | { createNodeMock: () => ({ closest: () => null, contains: () => false }) }, |
| 128 | 129 | ); |
| 129 | 130 | }); |
| 131 | } | |
| 132 | ||
| 133 | it("rechecks the target terminal grant when a retained surface callback reports a resize", async () => { | |
| 134 | await renderViewport(); | |
| 130 | 135 | expect(state.createSurface).toHaveBeenCalledOnce(); |
| 131 | 136 | const onResize = state.createSurface.mock.calls[0]![1].onResize; |
| 132 | 137 | 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 |
| 150 | 155 | environmentId: threadRef.environmentId, |
| 151 | 156 | input: { threadId: threadRef.threadId, terminalId: "terminal-1", cols: 100, rows: 30 }, |
| 152 | 157 | }); |
| 158 | ||
| 159 | await act(async () => renderer!.unmount()); | |
| 160 | renderer = undefined; | |
| 161 | onResize(120, 40); | |
| 162 | expect(state.resize).toHaveBeenCalledTimes(2); | |
| 163 | }); | |
| 164 | ||
| 165 | it("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); | |
| 153 | 175 | }); |
apps/web/src/components/ThreadTerminalDrawer.test.ts+5-71| ... | ... | @@ -8,18 +8,15 @@ import { |
| 8 | 8 | } from "@t3tools/client-runtime/state/terminal"; |
| 9 | 9 | |
| 10 | 10 | import { |
| 11 | shouldClearTerminalSelectionAction, | |
| 12 | 11 | shouldHandleTerminalExit, |
| 13 | 12 | terminalContextMenuItems, |
| 14 | 13 | synchronizeTerminalOutput, |
| 15 | 14 | terminalSelectionLineRange, |
| 16 | terminalSelectionMenuItems, | |
| 17 | 15 | terminalThemeFromApp, |
| 18 | 16 | } from "./ThreadTerminalDrawer"; |
| 19 | 17 | |
| 20 | 18 | describe("terminal selection menus", () => { |
| 21 | 19 | it("omits Add to chat when the terminal has no chat target", () => { |
| 22 | expect(terminalSelectionMenuItems().map(({ id }) => id)).toEqual(["add-to-chat", "copy"]); | |
| 23 | 20 | expect(terminalContextMenuItems({ hasSelection: true }).map(({ id }) => id)).toEqual([ |
| 24 | 21 | "add-to-chat", |
| 25 | 22 | "copy", |
| ... | ... | @@ -28,9 +25,6 @@ describe("terminal selection menus", () => { |
| 28 | 25 | "scroll-to-bottom", |
| 29 | 26 | ]); |
| 30 | 27 | |
| 31 | expect(terminalSelectionMenuItems({ canAddToChat: false }).map(({ id }) => id)).toEqual([ | |
| 32 | "copy", | |
| 33 | ]); | |
| 34 | 28 | expect( |
| 35 | 29 | terminalContextMenuItems({ hasSelection: true, canAddToChat: false }).map(({ id }) => id), |
| 36 | 30 | ).toEqual(["copy", "paste", "select-all", "scroll-to-bottom"]); |
| ... | ... | @@ -59,34 +53,8 @@ describe("terminalThemeFromApp", () => { |
| 59 | 53 | const root = { classList: { contains: () => false } }; |
| 60 | 54 | const body = {}; |
| 61 | 55 | 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 | ||
| 90 | 58 | vi.stubGlobal("getComputedStyle", (element: object) => { |
| 91 | 59 | const local = element === drawer; |
| 92 | 60 | const values = local |
| ... | ... | @@ -110,47 +78,13 @@ describe("terminalThemeFromApp", () => { |
| 110 | 78 | |
| 111 | 79 | const theme = terminalThemeFromApp(); |
| 112 | 80 | |
| 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"); | |
| 116 | 84 | }); |
| 117 | 85 | }); |
| 118 | 86 | |
| 119 | 87 | describe("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 | ||
| 154 | 88 | it("uses Ghostty's physical screen range for visually wrapped selections", () => { |
| 155 | 89 | expect( |
| 156 | 90 | terminalSelectionLineRange({ |
apps/web/src/components/ThreadTerminalDrawer.tsx+60-226| ... | ... | @@ -50,17 +50,18 @@ import { readTextFromClipboard, writeTextToClipboard } from "~/hooks/useCopyToCl |
| 50 | 50 | import { cn } from "~/lib/utils"; |
| 51 | 51 | import { type TerminalContextSelection } from "~/lib/terminalContext"; |
| 52 | 52 | import { |
| 53 | observeSelectionActions, | |
| 54 | resolveSelectionActionPosition, | |
| 55 | type SelectionActionPoint, | |
| 56 | } from "~/lib/selectionActions"; | |
| 57 | import { | |
| 58 | GhosttyTerminalSurface, | |
| 59 | type GhosttyTerminalSurfaceOptions, | |
| 60 | } from "~/terminal/ghostty/surface"; | |
| 61 | import { type GhosttyColor, type GhosttyTheme } from "~/terminal/ghostty/core"; | |
| 53 | TerminalSurface, | |
| 54 | type TerminalSurfaceOptions, | |
| 55 | type TerminalTheme, | |
| 56 | } from "~/terminal/TerminalSurface"; | |
| 62 | 57 | import { useOpenInPreferredEditor } from "../editorPreferences"; |
| 63 | import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; | |
| 58 | import { | |
| 59 | formatFilePathPosition, | |
| 60 | parseFileUrlHref, | |
| 61 | resolvePathLinkTarget, | |
| 62 | splitFilePathPosition, | |
| 63 | } from "@t3tools/shared/fileLinks"; | |
| 64 | import { safeDecodeURIComponent } from "@t3tools/shared/markdownLinks"; | |
| 64 | 65 | import { isTerminalUrl } from "../terminal-links"; |
| 65 | 66 | import { |
| 66 | 67 | isDiffToggleShortcut, |
| ... | ... | @@ -109,12 +110,12 @@ function clampDrawerHeight(height: number, maxHeight = maxDrawerHeight()): numbe |
| 109 | 110 | return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight); |
| 110 | 111 | } |
| 111 | 112 | |
| 112 | function writeSystemMessage(terminal: GhosttyTerminalSurface, message: string): void { | |
| 113 | function writeSystemMessage(terminal: TerminalSurface, message: string): void { | |
| 113 | 114 | terminal.write(`\r\n[terminal] ${message}\r\n`); |
| 114 | 115 | } |
| 115 | 116 | |
| 116 | 117 | export function writeTerminalOutputUpdate( |
| 117 | terminal: Pick<GhosttyTerminalSurface, "resetAndWrite" | "write">, | |
| 118 | terminal: Pick<TerminalSurface, "resetAndWrite" | "write">, | |
| 118 | 119 | update: TerminalOutputUpdate, |
| 119 | 120 | ): void { |
| 120 | 121 | if (update.type === "reset") { |
| ... | ... | @@ -125,44 +126,16 @@ export function writeTerminalOutputUpdate( |
| 125 | 126 | } |
| 126 | 127 | |
| 127 | 128 | export function synchronizeTerminalOutput( |
| 128 | terminal: Pick<GhosttyTerminalSurface, "resetAndWrite" | "write" | "clearSelection">, | |
| 129 | terminal: Pick<TerminalSurface, "resetAndWrite" | "write">, | |
| 129 | 130 | session: Pick<TerminalSessionState, "output" | "version">, |
| 130 | 131 | cursor: TerminalOutputCursor, |
| 131 | 132 | ): TerminalOutputCursor { |
| 132 | 133 | if (session.version === 0) return cursor; |
| 133 | 134 | const update = readTerminalOutputUpdate(session.output, cursor); |
| 134 | 135 | writeTerminalOutputUpdate(terminal, update); |
| 135 | terminal.clearSelection(); | |
| 136 | 136 | return update.cursor; |
| 137 | 137 | } |
| 138 | 138 | |
| 139 | function 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 | ||
| 161 | function 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 | ||
| 166 | 139 | // VS Code's default ANSI colors. |
| 167 | 140 | const TERMINAL_PALETTES = { |
| 168 | 141 | light: [ |
| ... | ... | @@ -182,7 +155,7 @@ const TERMINAL_PALETTES = { |
| 182 | 155 | "#bc05bc", |
| 183 | 156 | "#0598bc", |
| 184 | 157 | "#a5a5a5", |
| 185 | ].map(hexToGhosttyColor), | |
| 158 | ], | |
| 186 | 159 | dark: [ |
| 187 | 160 | "#000000", |
| 188 | 161 | "#cd3131", |
| ... | ... | @@ -200,7 +173,7 @@ const TERMINAL_PALETTES = { |
| 200 | 173 | "#d670d6", |
| 201 | 174 | "#29b8db", |
| 202 | 175 | "#e5e5e5", |
| 203 | ].map(hexToGhosttyColor), | |
| 176 | ], | |
| 204 | 177 | }; |
| 205 | 178 | |
| 206 | 179 | function runtimeEnvSignature(runtimeEnv: Record<string, string> | undefined): string { |
| ... | ... | @@ -235,7 +208,7 @@ function terminalFontOptions(family: string, size: number): { family?: string; s |
| 235 | 208 | return trimmed.length > 0 ? { family: trimmed, size } : { size }; |
| 236 | 209 | } |
| 237 | 210 | |
| 238 | export function terminalThemeFromApp(mountElement?: HTMLElement | null): GhosttyTheme { | |
| 211 | export function terminalThemeFromApp(mountElement?: HTMLElement | null): TerminalTheme { | |
| 239 | 212 | const drawerSurface = |
| 240 | 213 | mountElement?.closest("[data-thread-terminal-drawer]") ?? |
| 241 | 214 | document.querySelector("[data-thread-terminal-drawer]") ?? |
| ... | ... | @@ -282,18 +255,9 @@ export function terminalThemeFromApp(mountElement?: HTMLElement | null): Ghostty |
| 282 | 255 | isDark ? "rgba(180, 203, 255, 0.25)" : "rgba(37, 63, 99, 0.2)", |
| 283 | 256 | ); |
| 284 | 257 | 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, | |
| 297 | 261 | palette: TERMINAL_PALETTES[isDark ? "dark" : "light"], |
| 298 | 262 | selectionBackground: terminalSelection, |
| 299 | 263 | }; |
| ... | ... | @@ -317,24 +281,6 @@ export type TerminalContextMenuAction = |
| 317 | 281 | | "select-all" |
| 318 | 282 | | "scroll-to-bottom"; |
| 319 | 283 | |
| 320 | /** Post-selection popup: available selection actions, always enabled. */ | |
| 321 | export 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 | */ | |
| 338 | 284 | export function terminalContextMenuItems(options: { |
| 339 | 285 | hasSelection: boolean; |
| 340 | 286 | canAddToChat?: boolean; |
| ... | ... | @@ -342,31 +288,16 @@ export function terminalContextMenuItems(options: { |
| 342 | 288 | }): ContextMenuItem<TerminalContextMenuAction>[] { |
| 343 | 289 | const { hasSelection, canAddToChat = true } = options; |
| 344 | 290 | 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 }, | |
| 349 | 295 | { id: "paste", label: "Paste", ...(options.readOnly ? { disabled: true } : {}) }, |
| 350 | 296 | { id: "select-all", label: "Select all" }, |
| 351 | 297 | { id: "scroll-to-bottom", label: "Jump to latest" }, |
| 352 | 298 | ]; |
| 353 | 299 | } |
| 354 | 300 | |
| 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 | */ | |
| 362 | export 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 | ||
| 370 | 301 | export function shouldHandleTerminalExit( |
| 371 | 302 | current: TerminalSessionState["status"], |
| 372 | 303 | synchronized: TerminalSessionState["status"], |
| ... | ... | @@ -427,14 +358,13 @@ export function TerminalViewport({ |
| 427 | 358 | keybindings, |
| 428 | 359 | }: TerminalViewportProps) { |
| 429 | 360 | const containerRef = useRef<HTMLDivElement>(null); |
| 430 | const terminalRef = useRef<GhosttyTerminalSurface | null>(null); | |
| 361 | const terminalRef = useRef<TerminalSurface | null>(null); | |
| 431 | 362 | const visibleRef = useRef(visible); |
| 432 | 363 | const environmentId = threadRef.environmentId; |
| 433 | 364 | const canOperateTerminal = useEnvironmentScope(environmentId, AuthTerminalOperateScope); |
| 434 | 365 | const hasTerminalWriteAccess = useEffectEvent(() => |
| 435 | 366 | readEnvironmentScope(environmentId, AuthTerminalOperateScope), |
| 436 | 367 | ); |
| 437 | const canOpenHostEditor = useEnvironmentScope(environmentId, AuthOrchestrationOperateScope); | |
| 438 | 368 | const canActivateTerminalLink = useEffectEvent( |
| 439 | 369 | (text: string) => |
| 440 | 370 | isTerminalUrl(text) || readEnvironmentScope(environmentId, AuthOrchestrationOperateScope), |
| ... | ... | @@ -455,11 +385,7 @@ export function TerminalViewport({ |
| 455 | 385 | reportFailure: false, |
| 456 | 386 | }); |
| 457 | 387 | 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); | |
| 463 | 389 | const keybindingsRef = useRef(keybindings); |
| 464 | 390 | const runtimeEnvKey = useMemo(() => runtimeEnvSignature(runtimeEnv), [runtimeEnv]); |
| 465 | 391 | const handleSessionExited = useEffectEvent(() => { |
| ... | ... | @@ -518,7 +444,7 @@ export function TerminalViewport({ |
| 518 | 444 | const outputCursorRef = useRef<TerminalOutputCursor>(INITIAL_TERMINAL_OUTPUT_CURSOR); |
| 519 | 445 | const synchronizedStatusRef = useRef<TerminalSessionState["status"]>("closed"); |
| 520 | 446 | const synchronizeTerminalStatus = useEffectEvent( |
| 521 | (terminal: GhosttyTerminalSurface, status: TerminalSessionState["status"], version: number) => { | |
| 447 | (terminal: TerminalSurface, status: TerminalSessionState["status"], version: number) => { | |
| 522 | 448 | const synchronized = synchronizedStatusRef.current; |
| 523 | 449 | if (version > 0 && status === "running") { |
| 524 | 450 | hasHandledExitRef.current = false; |
| ... | ... | @@ -559,11 +485,6 @@ export function TerminalViewport({ |
| 559 | 485 | if (terminalRef.current) terminalRef.current.input.readOnly = !canOperateTerminal; |
| 560 | 486 | }, [canOperateTerminal]); |
| 561 | 487 | |
| 562 | // A grant can change while the pointer remains over a link. | |
| 563 | useEffect(() => { | |
| 564 | terminalRef.current?.refreshLinkActivation(); | |
| 565 | }, [canOpenHostEditor]); | |
| 566 | ||
| 567 | 488 | useEffect(() => { |
| 568 | 489 | if (resizeSessionGeneration === null) return; |
| 569 | 490 | // The first fit can finish before authorization or the attach snapshot. |
| ... | ... | @@ -590,32 +511,32 @@ export function TerminalViewport({ |
| 590 | 511 | const localApi = readLocalApi(); |
| 591 | 512 | let cancelled = false; |
| 592 | 513 | let teardown: (() => void) | null = null; |
| 593 | let setupTerminal: GhosttyTerminalSurface | null = null; | |
| 514 | let setupTerminal: TerminalSurface | null = null; | |
| 594 | 515 | let setupCleanups: Array<() => void> = []; |
| 595 | let selectionActions: ReturnType<typeof observeSelectionActions> | null = null; | |
| 596 | 516 | |
| 597 | 517 | const setup = async (): Promise<(() => void) | null> => { |
| 598 | 518 | const setupFont = terminalFontRef.current; |
| 599 | const terminalOptions: GhosttyTerminalSurfaceOptions = { | |
| 519 | const terminalOptions: TerminalSurfaceOptions = { | |
| 600 | 520 | theme: terminalThemeFromApp(mount), |
| 601 | 521 | font: terminalFontOptions(setupFont.family, setupFont.size), |
| 602 | 522 | get visible() { |
| 603 | 523 | return visibleRef.current; |
| 604 | 524 | }, |
| 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 | }, | |
| 608 | 531 | beforeKey: (event) => handleBeforeKey(event), |
| 609 | onLinkActivate: (text, event) => handleLinkActivate(text, event), | |
| 532 | onLinkActivate: (text) => handleLinkActivate(text), | |
| 610 | 533 | 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)), | |
| 614 | 535 | onContextMenu: (event) => { |
| 615 | 536 | if (terminalRef.current) void showTerminalContextMenu(event); |
| 616 | 537 | }, |
| 617 | 538 | }; |
| 618 | const terminal = await GhosttyTerminalSurface.create(mount, terminalOptions); | |
| 539 | const terminal = await TerminalSurface.create(mount, terminalOptions); | |
| 619 | 540 | if (cancelled) { |
| 620 | 541 | terminal.dispose(); |
| 621 | 542 | return null; |
| ... | ... | @@ -660,23 +581,7 @@ export function TerminalViewport({ |
| 660 | 581 | terminal.focus(); |
| 661 | 582 | } |
| 662 | 583 | |
| 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 = (): { | |
| 680 | 585 | clipboardText: string; |
| 681 | 586 | selection: TerminalContextSelection; |
| 682 | 587 | } | null => { |
| ... | ... | @@ -692,15 +597,7 @@ export function TerminalViewport({ |
| 692 | 597 | return null; |
| 693 | 598 | } |
| 694 | 599 | 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 | }); | |
| 702 | 600 | return { |
| 703 | position, | |
| 704 | 601 | clipboardText: selectionText, |
| 705 | 602 | selection: { |
| 706 | 603 | terminalId, |
| ... | ... | @@ -718,10 +615,9 @@ export function TerminalViewport({ |
| 718 | 615 | terminalRef.current?.focus(); |
| 719 | 616 | }; |
| 720 | 617 | |
| 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. | |
| 723 | 619 | const reportIfCurrent = (requestId: number, error: unknown, fallback: string) => { |
| 724 | if (requestId !== selectionActionRequestIdRef.current) return; | |
| 620 | if (requestId !== contextMenuRequestIdRef.current) return; | |
| 725 | 621 | const activeTerminal = terminalRef.current; |
| 726 | 622 | if (activeTerminal) { |
| 727 | 623 | writeSystemMessage(activeTerminal, error instanceof Error ? error.message : fallback); |
| ... | ... | @@ -729,7 +625,7 @@ export function TerminalViewport({ |
| 729 | 625 | }; |
| 730 | 626 | |
| 731 | 627 | const focusIfCurrent = (requestId: number) => { |
| 732 | if (requestId === selectionActionRequestIdRef.current) { | |
| 628 | if (requestId === contextMenuRequestIdRef.current) { | |
| 733 | 629 | terminalRef.current?.focus(); |
| 734 | 630 | } |
| 735 | 631 | }; |
| ... | ... | @@ -748,13 +644,9 @@ export function TerminalViewport({ |
| 748 | 644 | const activeTerminal = terminalRef.current; |
| 749 | 645 | if (!activeTerminal) return; |
| 750 | 646 | 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); | |
| 758 | 650 | } catch (error) { |
| 759 | 651 | reportIfCurrent(requestId, error, "Unable to read the clipboard"); |
| 760 | 652 | return; |
| ... | ... | @@ -764,14 +656,9 @@ export function TerminalViewport({ |
| 764 | 656 | |
| 765 | 657 | const showTerminalContextMenu = async (event: MouseEvent) => { |
| 766 | 658 | 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. | |
| 770 | 659 | event.preventDefault(); |
| 771 | // A right-click supersedes a selection popup that is pending or open. | |
| 772 | clearSelectionAction(); | |
| 660 | const requestId = ++contextMenuRequestIdRef.current; | |
| 773 | 661 | const selectionAction = readSelectionAction(); |
| 774 | const requestId = selectionActionRequestIdRef.current; | |
| 775 | 662 | let clicked: TerminalContextMenuAction | null; |
| 776 | 663 | try { |
| 777 | 664 | clicked = await localApi.contextMenu.show( |
| ... | ... | @@ -787,7 +674,7 @@ export function TerminalViewport({ |
| 787 | 674 | focusIfCurrent(requestId); |
| 788 | 675 | return; |
| 789 | 676 | } |
| 790 | if (requestId !== selectionActionRequestIdRef.current || clicked === null) { | |
| 677 | if (requestId !== contextMenuRequestIdRef.current || clicked === null) { | |
| 791 | 678 | return; |
| 792 | 679 | } |
| 793 | 680 | switch (clicked) { |
| ... | ... | @@ -813,44 +700,6 @@ export function TerminalViewport({ |
| 813 | 700 | } |
| 814 | 701 | }; |
| 815 | 702 | |
| 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 | ||
| 854 | 703 | const sendTerminalInput = async (data: string, fallbackError: string) => { |
| 855 | 704 | if (!hasTerminalWriteAccess()) return; |
| 856 | 705 | const activeTerminal = terminalRef.current; |
| ... | ... | @@ -904,7 +753,7 @@ export function TerminalViewport({ |
| 904 | 753 | return false; |
| 905 | 754 | } |
| 906 | 755 | |
| 907 | function handleLinkActivate(text: string, event: MouseEvent): void { | |
| 756 | function handleLinkActivate(text: string): void { | |
| 908 | 757 | if (!canActivateTerminalLink(text)) return; |
| 909 | 758 | const latestTerminal = terminalRef.current; |
| 910 | 759 | if (!latestTerminal) return; |
| ... | ... | @@ -930,7 +779,7 @@ export function TerminalViewport({ |
| 930 | 779 | threadRef, |
| 931 | 780 | openPreview, |
| 932 | 781 | fallbackToBrowser, |
| 933 | forceBrowser: event.metaKey || event.ctrlKey, | |
| 782 | forceBrowser: false, | |
| 934 | 783 | }).catch((error: unknown) => { |
| 935 | 784 | toastManager.add( |
| 936 | 785 | stackedThreadToast({ |
| ... | ... | @@ -942,7 +791,13 @@ export function TerminalViewport({ |
| 942 | 791 | }); |
| 943 | 792 | return; |
| 944 | 793 | } |
| 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); | |
| 946 | 801 | void (async () => { |
| 947 | 802 | const result = await openTerminalPath(target); |
| 948 | 803 | if (result._tag === "Success" || isAtomCommandInterrupted(result)) { |
| ... | ... | @@ -969,28 +824,6 @@ export function TerminalViewport({ |
| 969 | 824 | })(); |
| 970 | 825 | } |
| 971 | 826 | |
| 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 | ||
| 994 | 827 | const themeObserver = new MutationObserver(() => { |
| 995 | 828 | const activeTerminal = terminalRef.current; |
| 996 | 829 | if (!activeTerminal) return; |
| ... | ... | @@ -1017,6 +850,7 @@ export function TerminalViewport({ |
| 1017 | 850 | setupCleanups = []; |
| 1018 | 851 | setupTerminal = null; |
| 1019 | 852 | return () => { |
| 853 | contextMenuRequestIdRef.current += 1; | |
| 1020 | 854 | for (const cleanup of cleanups.toReversed()) cleanup(); |
| 1021 | 855 | if (terminalRef.current === terminal) terminalRef.current = null; |
| 1022 | 856 | terminal.dispose(); |
| ... | ... | @@ -1109,6 +943,7 @@ export function TerminalViewport({ |
| 1109 | 943 | <div |
| 1110 | 944 | ref={containerRef} |
| 1111 | 945 | tabIndex={-1} |
| 946 | aria-label={terminalLabel} | |
| 1112 | 947 | className="relative h-full w-full overflow-hidden bg-(--terminal-background)" |
| 1113 | 948 | /> |
| 1114 | 949 | ); |
| ... | ... | @@ -1419,14 +1254,13 @@ export default function ThreadTerminalDrawer({ |
| 1419 | 1254 | const confirmCloseTerminal = useCallback( |
| 1420 | 1255 | (terminalId: string) => { |
| 1421 | 1256 | if (!canOperateTerminal) return; |
| 1422 | const label = terminalLabelById.get(terminalId) ?? getTerminalLabel(terminalId); | |
| 1423 | void confirmTerminalClose([label]).then((confirmed) => { | |
| 1257 | void confirmTerminalClose(threadRef, [terminalId]).then((confirmed) => { | |
| 1424 | 1258 | if (confirmed && readEnvironmentScope(threadRef.environmentId, AuthTerminalOperateScope)) { |
| 1425 | 1259 | onCloseTerminal(terminalId); |
| 1426 | 1260 | } |
| 1427 | 1261 | }); |
| 1428 | 1262 | }, |
| 1429 | [canOperateTerminal, onCloseTerminal, terminalLabelById, threadRef.environmentId], | |
| 1263 | [canOperateTerminal, onCloseTerminal, threadRef], | |
| 1430 | 1264 | ); |
| 1431 | 1265 | |
| 1432 | 1266 | useEffect(() => { |
apps/web/src/components/chat/PanelLayoutControls.tsx+3-3| ... | ... | @@ -90,7 +90,7 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ |
| 90 | 90 | className="shrink-0 [-webkit-app-region:no-drag]" |
| 91 | 91 | pressed={terminalOpen} |
| 92 | 92 | onPressedChange={onToggleTerminal} |
| 93 | aria-label="Toggle terminal drawer" | |
| 93 | aria-label="Toggle terminal" | |
| 94 | 94 | variant="ghost" |
| 95 | 95 | size="sm" |
| 96 | 96 | disabled={!terminalAvailable} |
| ... | ... | @@ -100,8 +100,8 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ |
| 100 | 100 | </TooltipTrigger> |
| 101 | 101 | <TooltipPopup side="bottom"> |
| 102 | 102 | {terminalAvailable |
| 103 | ? `Toggle terminal drawer${terminalShortcutLabel ? ` (${terminalShortcutLabel})` : ""}` | |
| 104 | : "Terminal drawer is unavailable"} | |
| 103 | ? `Toggle terminal${terminalShortcutLabel ? ` (${terminalShortcutLabel})` : ""}` | |
| 104 | : "Terminal is unavailable"} | |
| 105 | 105 | </TooltipPopup> |
| 106 | 106 | </Tooltip> |
| 107 | 107 | ) : null} |
apps/web/src/components/settings/ProviderAuthTerminal.test.tsx created+87| ... | ... | @@ -0,0 +1,87 @@ |
| 1 | import { act, create, type ReactTestRenderer } from "react-test-renderer"; | |
| 2 | import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; | |
| 3 | ||
| 4 | const state = vi.hoisted(() => ({ | |
| 5 | create: vi.fn(), | |
| 6 | write: vi.fn(), | |
| 7 | resetAndWrite: vi.fn(), | |
| 8 | dispose: vi.fn(), | |
| 9 | })); | |
| 10 | vi.mock("../../terminal/TerminalSurface", () => ({ TerminalSurface: { create: state.create } })); | |
| 11 | vi.mock("../ThreadTerminalDrawer", () => ({ terminalThemeFromApp: () => ({}) })); | |
| 12 | ||
| 13 | import ProviderAuthTerminal from "./ProviderAuthTerminal"; | |
| 14 | ||
| 15 | let renderer: ReactTestRenderer | undefined; | |
| 16 | ||
| 17 | beforeEach(() => { | |
| 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 | ||
| 25 | afterEach(async () => { | |
| 26 | await act(async () => renderer?.unmount()); | |
| 27 | renderer = undefined; | |
| 28 | vi.unstubAllGlobals(); | |
| 29 | }); | |
| 30 | ||
| 31 | it("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 | ||
| 69 | it("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 @@ |
| 1 | 1 | import type { ProviderAuthResponse } from "@t3tools/contracts"; |
| 2 | 2 | import { useEffect, useRef, useState } from "react"; |
| 3 | import { GhosttyTerminalSurface } from "../../terminal/ghostty/surface"; | |
| 3 | import { TerminalSurface } from "../../terminal/TerminalSurface"; | |
| 4 | 4 | import { ensureLocalApi } from "../../localApi"; |
| 5 | 5 | import { terminalThemeFromApp } from "../ThreadTerminalDrawer"; |
| 6 | 6 | |
| ... | ... | @@ -15,7 +15,7 @@ export default function ProviderAuthTerminal({ |
| 15 | 15 | readonly onResponse: (response: Extract<ProviderAuthResponse, { type: "terminal" }>) => void; |
| 16 | 16 | }) { |
| 17 | 17 | const mount = useRef<HTMLDivElement>(null); |
| 18 | const surface = useRef<GhosttyTerminalSurface | null>(null); | |
| 18 | const surface = useRef<TerminalSurface | null>(null); | |
| 19 | 19 | const latest = useRef({ output, offset: outputOffset ?? output.length, onResponse }); |
| 20 | 20 | const written = useRef(0); |
| 21 | 21 | const [error, setError] = useState<string | null>(null); |
| ... | ... | @@ -32,13 +32,12 @@ export default function ProviderAuthTerminal({ |
| 32 | 32 | const element = mount.current; |
| 33 | 33 | if (!element) return; |
| 34 | 34 | let disposed = false; |
| 35 | void GhosttyTerminalSurface.create(element, { | |
| 35 | void TerminalSurface.create(element, { | |
| 36 | 36 | theme: terminalThemeFromApp(element), |
| 37 | 37 | font: { size: 13 }, |
| 38 | 38 | onData: (data) => latest.current.onResponse({ type: "terminal", data }), |
| 39 | 39 | onResize: (cols, rows) => |
| 40 | 40 | latest.current.onResponse({ type: "terminal", data: "", size: { cols, rows } }), |
| 41 | onSelectionChange: () => {}, | |
| 42 | 41 | beforeKey: (event) => event.key !== "Tab", |
| 43 | 42 | onLinkActivate: (url) => { |
| 44 | 43 | if (/^https?:\/\//i.test(url)) |
| ... | ... | @@ -53,7 +52,7 @@ export default function ProviderAuthTerminal({ |
| 53 | 52 | return; |
| 54 | 53 | } |
| 55 | 54 | surface.current = terminal; |
| 56 | terminal.write(latest.current.output); | |
| 55 | terminal.resetAndWrite(latest.current.output); | |
| 57 | 56 | written.current = latest.current.offset; |
| 58 | 57 | }) |
| 59 | 58 | .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"; |
| 7 | 7 | import { DISCONNECTED_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder"; |
| 8 | 8 | import { resolveDiffThemeName, type DiffThemeName } from "../../lib/diffRendering"; |
| 9 | 9 | import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting"; |
| 10 | import { 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. | |
| 10 | import { TerminalSurface } from "~/terminal/TerminalSurface"; | |
| 17 | 11 | |
| 18 | 12 | const EMPTY_SKILLS: ReadonlyArray<never> = []; |
| 19 | 13 | |
| ... | ... | @@ -22,7 +16,7 @@ const EMPTY_SKILLS: ReadonlyArray<never> = []; |
| 22 | 16 | // text and pills exactly as the real composer draws them. |
| 23 | 17 | const PROMPT_PREVIEW_TEXT = |
| 24 | 18 | "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 " + | |
| 26 | 20 | "[SettingsPanels.tsx](apps/web/src/components/settings/SettingsPanels.tsx) before shipping."; |
| 27 | 21 | |
| 28 | 22 | function noop() {} |
| ... | ... | @@ -174,14 +168,12 @@ function previewTerminalFont(family: string, size: number): { family?: string; s |
| 174 | 168 | } |
| 175 | 169 | |
| 176 | 170 | /** |
| 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. | |
| 181 | 173 | */ |
| 182 | 174 | export function TerminalFontPreview({ family, size }: { family: string; size: number }) { |
| 183 | 175 | const mountRef = useRef<HTMLDivElement>(null); |
| 184 | const surfaceRef = useRef<GhosttyTerminalSurface | null>(null); | |
| 176 | const surfaceRef = useRef<TerminalSurface | null>(null); | |
| 185 | 177 | const fontRef = useRef({ family, size }); |
| 186 | 178 | const { theme, resolvedTheme } = useTheme(); |
| 187 | 179 | |
| ... | ... | @@ -234,12 +226,11 @@ export function TerminalFontPreview({ family, size }: { family: string; size: nu |
| 234 | 226 | lineLength += printable.length; |
| 235 | 227 | }; |
| 236 | 228 | |
| 237 | void GhosttyTerminalSurface.create(mount, { | |
| 229 | void TerminalSurface.create(mount, { | |
| 238 | 230 | theme: terminalThemeFromApp(mount), |
| 239 | 231 | font: previewTerminalFont(fontRef.current.family, fontRef.current.size), |
| 240 | 232 | onData: echo, |
| 241 | 233 | onResize: noop, |
| 242 | onSelectionChange: noop, | |
| 243 | 234 | // Tab keeps walking the settings page instead of feeding the echo loop. |
| 244 | 235 | beforeKey: (event) => event.key !== "Tab", |
| 245 | 236 | onLinkActivate: noop, |
apps/web/src/components/settings/SettingsPanels.tsx+35| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | import { performDesktopUpdate } from "../desktopUpdate.actions"; |
| 2 | import { useTerminalPlacement } from "~/hooks/useTerminalPlacement"; | |
| 2 | 3 | import { SettingsGroup } from "./SettingsGroup"; |
| 3 | 4 | import { useScopedSettingsWriteAllowed } from "./useScopedSettings"; |
| 4 | 5 | import { Spinner } from "~/components/ui/spinner"; |
| ... | ... | @@ -2123,6 +2124,7 @@ function LegacyFeaturesSection() { |
| 2123 | 2124 | } |
| 2124 | 2125 | |
| 2125 | 2126 | export function GeneralSettingsPanel() { |
| 2127 | const [terminalPlacement, setTerminalPlacement] = useTerminalPlacement(); | |
| 2126 | 2128 | const modifierLabel = isMacPlatform(navigator.platform) ? "⌘" : "Ctrl"; |
| 2127 | 2129 | const sendShortcutOptions = [ |
| 2128 | 2130 | { value: "enter", label: "Enter" }, |
| ... | ... | @@ -2660,6 +2662,39 @@ export function GeneralSettingsPanel() { |
| 2660 | 2662 | } |
| 2661 | 2663 | /> |
| 2662 | 2664 | |
| 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 | ||
| 2663 | 2698 | <SettingsRow |
| 2664 | 2699 | {...searchableSetting("proactive-panels")} |
| 2665 | 2700 | 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 = [ |
| 429 | 429 | to: "/settings/general", |
| 430 | 430 | searchTerms: ["stacked split side by side unified inline view"], |
| 431 | 431 | }, |
| 432 | { | |
| 433 | id: "terminal-position", | |
| 434 | title: "Terminal position", | |
| 435 | to: "/settings/general", | |
| 436 | searchTerms: ["right bottom dock placement panel shell"], | |
| 437 | }, | |
| 432 | 438 | { |
| 433 | 439 | id: "proactive-panels", |
| 434 | 440 | title: "Proactive panels", |
apps/web/src/hooks/useTerminalPlacement.ts created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | import * as Schema from "effect/Schema"; | |
| 2 | ||
| 3 | import { getLocalStorageItem, useLocalStorage } from "./useLocalStorage"; | |
| 4 | ||
| 5 | const TERMINAL_PLACEMENT_STORAGE_KEY = "t3code.terminalPlacement"; | |
| 6 | const TerminalPlacement = Schema.Literals(["right", "bottom"]); | |
| 7 | ||
| 8 | export function useTerminalPlacement() { | |
| 9 | return useLocalStorage(TERMINAL_PLACEMENT_STORAGE_KEY, "right", TerminalPlacement); | |
| 10 | } | |
| 11 | ||
| 12 | export 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 @@ |
| 1 | 1 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 2 | 2 | import { EnvironmentId, ThreadId } from "@t3tools/contracts"; |
| 3 | import { beforeEach, expect, it } from "vite-plus/test"; | |
| 3 | import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; | |
| 4 | 4 | |
| 5 | 5 | import { selectThreadRightPanelState, useRightPanelStore } from "../rightPanelStore"; |
| 6 | 6 | import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; |
| ... | ... | @@ -9,7 +9,13 @@ import { revealThreadTerminal } from "./revealThreadTerminal"; |
| 9 | 9 | const threadRef = scopeThreadRef(EnvironmentId.make("local"), ThreadId.make("settled-thread")); |
| 10 | 10 | const remoteRef = scopeThreadRef(EnvironmentId.make("remote"), threadRef.threadId); |
| 11 | 11 | |
| 12 | const preferences = new Map<string, string>(); | |
| 13 | ||
| 12 | 14 | beforeEach(() => { |
| 15 | preferences.clear(); | |
| 16 | vi.stubGlobal("window", { | |
| 17 | localStorage: { getItem: (key: string) => preferences.get(key) ?? null }, | |
| 18 | }); | |
| 13 | 19 | useTerminalUiStateStore.setState({ |
| 14 | 20 | terminalUiStateByThreadKey: {}, |
| 15 | 21 | suppressedTerminalIdsByThreadKey: {}, |
| ... | ... | @@ -17,7 +23,26 @@ beforeEach(() => { |
| 17 | 23 | useRightPanelStore.setState({ byThreadKey: {} }); |
| 18 | 24 | }); |
| 19 | 25 | |
| 20 | it("reveals a server-created setup terminal without adding a default shell", () => { | |
| 26 | afterEach(() => vi.unstubAllGlobals()); | |
| 27 | ||
| 28 | it("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 | ||
| 44 | it("honors an explicit bottom preference for an unowned terminal", () => { | |
| 45 | preferences.set("t3code.terminalPlacement", '"bottom"'); | |
| 21 | 46 | revealThreadTerminal(threadRef, "setup-install"); |
| 22 | 47 | expect( |
| 23 | 48 | selectThreadTerminalUiState( |
| ... | ... | @@ -27,8 +52,24 @@ it("reveals a server-created setup terminal without adding a default shell", () |
| 27 | 52 | ).toMatchObject({ |
| 28 | 53 | terminalOpen: true, |
| 29 | 54 | terminalIds: ["setup-install"], |
| 30 | activeTerminalId: "setup-install", | |
| 31 | 55 | }); |
| 56 | expect( | |
| 57 | selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces, | |
| 58 | ).toEqual([]); | |
| 59 | }); | |
| 60 | ||
| 61 | it("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([]); | |
| 32 | 73 | }); |
| 33 | 74 | |
| 34 | 75 | it("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", () |
| 64 | 105 | }); |
| 65 | 106 | |
| 66 | 107 | it("reveals terminals in their own environment without changing another environment's drawer", () => { |
| 108 | useTerminalUiStateStore.getState().ensureTerminal(threadRef, "default", { open: false }); | |
| 67 | 109 | revealThreadTerminal(threadRef, "default"); |
| 68 | 110 | revealThreadTerminal(remoteRef, "setup-install"); |
| 69 | 111 | const state = useTerminalUiStateStore.getState().terminalUiStateByThreadKey; |
| 70 | 112 | 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 | ||
| 119 | it("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([]); | |
| 72 | 135 | }); |
apps/web/src/lib/revealThreadTerminal.ts+9-2| ... | ... | @@ -1,7 +1,8 @@ |
| 1 | import { getTerminalPlacement } from "~/hooks/useTerminalPlacement"; | |
| 1 | 2 | import type { ScopedThreadRef } from "@t3tools/contracts"; |
| 2 | 3 | |
| 3 | 4 | import { selectThreadRightPanelState, useRightPanelStore } from "../rightPanelStore"; |
| 4 | import { useTerminalUiStateStore } from "../terminalUiStateStore"; | |
| 5 | import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; | |
| 5 | 6 | |
| 6 | 7 | /** Reveal an existing terminal in the panel that owns it, without opening another PTY. */ |
| 7 | 8 | export function revealThreadTerminal(threadRef: ScopedThreadRef, terminalId: string) { |
| ... | ... | @@ -14,7 +15,13 @@ export function revealThreadTerminal(threadRef: ScopedThreadRef, terminalId: str |
| 14 | 15 | panels.activateSurface(threadRef, surface.id); |
| 15 | 16 | return; |
| 16 | 17 | } |
| 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, { | |
| 18 | 25 | open: true, |
| 19 | 26 | active: true, |
| 20 | 27 | }); |
apps/web/src/lib/terminalCloseConfirm.test.ts+152-39| ... | ... | @@ -1,65 +1,145 @@ |
| 1 | import { EnvironmentId, ThreadId, type TerminalSummary } from "@t3tools/contracts"; | |
| 1 | 2 | import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 2 | 3 | |
| 3 | const { 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 | }); | |
| 4 | const { 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 | })); | |
| 14 | 12 | |
| 15 | vi.mock("~/localApi", () => ({ | |
| 16 | readLocalApi: () => readLocalApiMock(), | |
| 13 | vi.mock("~/localApi", () => ({ readLocalApi: readLocalApiMock })); | |
| 14 | vi.mock("~/rpc/atomRegistry", () => ({ appAtomRegistry: registry })); | |
| 15 | vi.mock("~/state/terminal", () => ({ | |
| 16 | terminalEnvironment: { metadataSnapshot: metadataSnapshotMock }, | |
| 17 | })); | |
| 18 | vi.mock("@t3tools/client-runtime/state/runtime", () => ({ | |
| 19 | executeAtomQuery: executeAtomQueryMock, | |
| 17 | 20 | })); |
| 18 | 21 | |
| 19 | 22 | import { confirmTerminalClose, isTerminalCloseConfirmPending } from "./terminalCloseConfirm"; |
| 20 | 23 | |
| 24 | const threadRef = { | |
| 25 | environmentId: EnvironmentId.make("secondary"), | |
| 26 | threadId: ThreadId.make("thread"), | |
| 27 | }; | |
| 28 | const 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 | ||
| 42 | function metadata(terminals: ReadonlyArray<TerminalSummary>) { | |
| 43 | return { _tag: "Success", value: terminals }; | |
| 44 | } | |
| 45 | ||
| 21 | 46 | describe("terminal close confirmation", () => { |
| 22 | 47 | beforeEach(() => { |
| 23 | confirmMock.mockReset(); | |
| 24 | readLocalApiMock.mockReset(); | |
| 48 | vi.clearAllMocks(); | |
| 25 | 49 | readLocalApiMock.mockReturnValue({ dialogs: { confirm: confirmMock } }); |
| 50 | executeAtomQueryMock.mockResolvedValue(metadata([terminal])); | |
| 51 | confirmMock.mockResolvedValue(true); | |
| 26 | 52 | }); |
| 27 | 53 | |
| 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 | ); | |
| 31 | 58 | |
| 59 | await expect(confirmTerminalClose(threadRef, ["term-1"])).resolves.toBe(true); | |
| 60 | ||
| 61 | expect(confirmMock).not.toHaveBeenCalled(); | |
| 32 | 62 | expect(isTerminalCloseConfirmPending()).toBe(false); |
| 63 | }); | |
| 33 | 64 | |
| 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 | ); | |
| 36 | 69 | |
| 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(); | |
| 40 | 72 | }); |
| 41 | 73 | |
| 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" }, | |
| 49 | 96 | ); |
| 97 | }); | |
| 50 | 98 | |
| 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 | ); | |
| 53 | 103 | |
| 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(); | |
| 57 | 106 | }); |
| 58 | 107 | |
| 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); | |
| 61 | 113 | |
| 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); | |
| 63 | 143 | expect(confirmMock).toHaveBeenCalledWith( |
| 64 | 144 | [ |
| 65 | 145 | "Close 2 terminals?", |
| ... | ... | @@ -69,10 +149,43 @@ describe("terminal close confirmation", () => { |
| 69 | 149 | ); |
| 70 | 150 | }); |
| 71 | 151 | |
| 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 | ||
| 72 | 184 | it("closes without prompting when no local API is available", async () => { |
| 73 | 185 | readLocalApiMock.mockReturnValue(undefined); |
| 74 | 186 | |
| 75 | await expect(confirmTerminalClose(["Terminal 1"])).resolves.toBe(true); | |
| 187 | await expect(confirmTerminalClose(threadRef, ["term-1"])).resolves.toBe(true); | |
| 76 | 188 | expect(confirmMock).not.toHaveBeenCalled(); |
| 189 | expect(executeAtomQueryMock).not.toHaveBeenCalled(); | |
| 77 | 190 | }); |
| 78 | 191 | }); |
apps/web/src/lib/terminalCloseConfirm.ts+28-8| ... | ... | @@ -1,25 +1,45 @@ |
| 1 | import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; | |
| 2 | import type { ScopedThreadRef } from "@t3tools/contracts"; | |
| 3 | import { resolveTerminalSessionLabel } from "@t3tools/shared/terminalLabels"; | |
| 4 | ||
| 1 | 5 | import { readLocalApi } from "~/localApi"; |
| 6 | import { appAtomRegistry } from "~/rpc/atomRegistry"; | |
| 7 | import { terminalEnvironment } from "~/state/terminal"; | |
| 2 | 8 | |
| 3 | 9 | let pendingConfirmations = 0; |
| 4 | 10 | |
| 5 | /** Whether a terminal-close confirmation is currently waiting on the user. */ | |
| 6 | 11 | export function isTerminalCloseConfirmPending(): boolean { |
| 7 | 12 | return pendingConfirmations > 0; |
| 8 | 13 | } |
| 9 | 14 | |
| 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 | */ | |
| 16 | 15 | export async function confirmTerminalClose( |
| 17 | labels: readonly [string, ...string[]], | |
| 16 | threadRef: ScopedThreadRef, | |
| 17 | terminalIds: readonly [string, ...string[]], | |
| 18 | 18 | ): Promise<boolean> { |
| 19 | 19 | const localApi = readLocalApi(); |
| 20 | 20 | if (!localApi) return true; |
| 21 | 21 | pendingConfirmations += 1; |
| 22 | 22 | 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 | ); | |
| 23 | 43 | return await localApi.dialogs.confirm( |
| 24 | 44 | labels.length === 1 |
| 25 | 45 | ? [ |
apps/web/src/terminal-links.test.ts+3-192| ... | ... | @@ -1,203 +1,14 @@ |
| 1 | import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; | |
| 2 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 2 | |
| 4 | import { | |
| 5 | collectWrappedTerminalLinkLine, | |
| 6 | extractTerminalLinks, | |
| 7 | isTerminalLinkActivation, | |
| 8 | isTerminalUrl, | |
| 9 | type TerminalBufferLineLike, | |
| 10 | } from "./terminal-links"; | |
| 3 | import { isTerminalLinkActivation, isTerminalUrl } from "./terminal-links"; | |
| 11 | 4 | |
| 12 | function createBufferLine(text: string, isWrapped = false): TerminalBufferLineLike { | |
| 13 | return { | |
| 14 | isWrapped, | |
| 15 | translateToString: (trimRight = false) => (trimRight ? text.replace(/\s+$/u, "") : text), | |
| 16 | }; | |
| 17 | } | |
| 18 | ||
| 19 | describe("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", () => { | |
| 5 | describe("isTerminalUrl", () => { | |
| 6 | it("recognizes HTTP schemes without treating workspace paths as URLs", () => { | |
| 40 | 7 | expect(isTerminalUrl("HTTPS://example.com/docs")).toBe(true); |
| 41 | 8 | expect(isTerminalUrl("Http://example.com")).toBe(true); |
| 42 | 9 | expect(isTerminalUrl("src/components/main.ts")).toBe(false); |
| 43 | 10 | expect(isTerminalUrl("httpsdocs/readme.md")).toBe(false); |
| 44 | 11 | }); |
| 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 | ||
| 120 | describe("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 | ||
| 173 | describe("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 | }); | |
| 201 | 12 | }); |
| 202 | 13 | |
| 203 | 14 | describe("isTerminalLinkActivation", () => { |
apps/web/src/terminal-links.ts-146| ... | ... | @@ -1,155 +1,9 @@ |
| 1 | 1 | import { isMacPlatform } from "./lib/utils"; |
| 2 | 2 | |
| 3 | export type TerminalLinkKind = "url" | "path"; | |
| 4 | ||
| 5 | export interface TerminalLinkMatch { | |
| 6 | kind: TerminalLinkKind; | |
| 7 | text: string; | |
| 8 | start: number; | |
| 9 | end: number; | |
| 10 | } | |
| 11 | ||
| 12 | export interface TerminalBufferLineLike { | |
| 13 | readonly isWrapped?: boolean; | |
| 14 | translateToString(trimRight?: boolean): string; | |
| 15 | } | |
| 16 | ||
| 17 | export interface WrappedTerminalLinkLineSegment { | |
| 18 | bufferLineNumber: number; | |
| 19 | text: string; | |
| 20 | startIndex: number; | |
| 21 | endIndex: number; | |
| 22 | } | |
| 23 | ||
| 24 | export interface WrappedTerminalLinkLine { | |
| 25 | text: string; | |
| 26 | segments: ReadonlyArray<WrappedTerminalLinkLineSegment>; | |
| 27 | } | |
| 28 | ||
| 29 | const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/giu; | |
| 30 | const FILE_PATH_PATTERN = | |
| 31 | /(?:~\/|\.{1,2}\/|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}/g; | |
| 32 | const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/; | |
| 33 | // Paths also drop a trailing colon: compilers end `file:line:col:` with one. | |
| 34 | const TRAILING_PATH_PUNCTUATION_PATTERN = /[.,;:!?]+$/; | |
| 35 | ||
| 36 | function 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 | ||
| 58 | function overlaps(a: { start: number; end: number }, b: { start: number; end: number }): boolean { | |
| 59 | return a.start < b.end && b.start < a.end; | |
| 60 | } | |
| 61 | ||
| 62 | function 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 | ||
| 96 | export 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 | ||
| 102 | 3 | export function isTerminalUrl(value: string): boolean { |
| 103 | 4 | return /^https?:\/\//iu.test(value); |
| 104 | 5 | } |
| 105 | 6 | |
| 106 | export 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 | ||
| 153 | 7 | export function isTerminalLinkActivation( |
| 154 | 8 | event: Pick<MouseEvent, "metaKey" | "ctrlKey">, |
| 155 | 9 | 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. | |
| 3 | import * as NodeFS from "node:fs"; | |
| 4 | import * as NodePath from "node:path"; | |
| 5 | import * as NodeURL from "node:url"; | |
| 6 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 7 | ||
| 8 | import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; | |
| 9 | ||
| 10 | import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; | |
| 11 | import { TerminalSurface, type TerminalSurfaceOptions } from "./TerminalSurface"; | |
| 12 | ||
| 13 | vi.mock("~/hooks/useCopyToClipboard", () => ({ writeTextToClipboard: vi.fn(async () => {}) })); | |
| 14 | ||
| 15 | const 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 | ); | |
| 23 | const surfaces = new Set<TerminalSurface>(); | |
| 24 | const frames = new Map<number, FrameRequestCallback>(); | |
| 25 | let nextFrame = 0; | |
| 26 | let width = 640; | |
| 27 | let height = 192; | |
| 28 | let platform = "MacIntel"; | |
| 29 | ||
| 30 | function paint() { | |
| 31 | const pending = [...frames.values()]; | |
| 32 | frames.clear(); | |
| 33 | pending.forEach((callback) => callback(performance.now())); | |
| 34 | } | |
| 35 | ||
| 36 | async 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 | ||
| 66 | function 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. | |
| 93 | function 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 | ||
| 103 | function 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 | ||
| 115 | beforeEach(() => { | |
| 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 | ||
| 143 | afterEach(() => { | |
| 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 | ||
| 153 | describe("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 @@ |
| 1 | import { GhosttyModule, mountTerminal, type TerminalHandle } from "@clo/terminal"; | |
| 2 | import wasmUrl from "@clo/terminal/ghostty-vt.wasm?url"; | |
| 3 | import "@clo/terminal/terminal.css"; | |
| 4 | ||
| 5 | import { isTerminalLinkActivation } from "~/terminal-links"; | |
| 6 | import { readLocalApi } from "~/localApi"; | |
| 7 | import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; | |
| 8 | ||
| 9 | export interface TerminalTheme { | |
| 10 | foreground: string; | |
| 11 | background: string; | |
| 12 | cursor: string; | |
| 13 | palette: readonly string[]; | |
| 14 | selectionBackground: string; | |
| 15 | } | |
| 16 | ||
| 17 | export 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 | ||
| 30 | let modulePromise: Promise<GhosttyModule> | null = null; | |
| 31 | const ANSI_NAMES = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]; | |
| 32 | const ANSI_TOKENS = [...ANSI_NAMES, ...ANSI_NAMES.map((name) => `bright-${name}`)]; | |
| 33 | const decoder = new TextDecoder(); | |
| 34 | ||
| 35 | function 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 | ||
| 46 | export 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 | ||
| 3 | This directory is the browser adapter for the same official `libghostty-vt` C ABI used by Android. | |
| 4 | It 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 | ||
| 18 | Keep browser behavior here and terminal transport in the existing client runtime. Do not add React | |
| 19 | state 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 @@ |
| 1 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 2 | import { | |
| 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 | ||
| 12 | import { writeTerminalOutputUpdate } from "../../components/ThreadTerminalDrawer"; | |
| 13 | import { GHOSTTY_CELL_WIDE, GhosttyTerminalCore, ghosttyCellText } from "./core"; | |
| 14 | import { loadGhosttyRuntime } from "./runtime"; | |
| 15 | ||
| 16 | vi.mock("./vendor/ghostty-vt.wasm?url", async () => ({ | |
| 17 | default: (await import("./vendor/ghostty-vt.wasm?inline")).default, | |
| 18 | })); | |
| 19 | vi.mock("./vendor/ghostty-write-pty.wasm?url&no-inline", async () => ({ | |
| 20 | default: (await import("./vendor/ghostty-write-pty.wasm?inline")).default, | |
| 21 | })); | |
| 22 | ||
| 23 | function 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 | ||
| 29 | describe("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 | ||
| 60 | describe("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 @@ |
| 1 | import { | |
| 2 | type GhosttyKeyboardLayoutMap, | |
| 3 | ghosttyConsumedMods, | |
| 4 | ghosttyKeyForCode, | |
| 5 | ghosttyUnshiftedCodepoint, | |
| 6 | loadGhosttyKeyboardLayoutMap, | |
| 7 | } from "./keyCodes"; | |
| 8 | import { GhosttyRuntime, loadGhosttyRuntime } from "./runtime"; | |
| 9 | ||
| 10 | const GHOSTTY_SUCCESS = 0; | |
| 11 | const GHOSTTY_OUT_OF_SPACE = -3; | |
| 12 | const MAX_SCROLLBACK_ROWS = 10_000; | |
| 13 | // wasm32 C ABI layout for GhosttyTerminalSelectionFormatOptions at the | |
| 14 | // libghostty-vt revision pinned alongside this module. | |
| 15 | const SELECTION_FORMAT_OPTIONS_SIZE = 16; | |
| 16 | const PALETTE_BYTES = 256 * 3; | |
| 17 | ||
| 18 | const 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 | ||
| 35 | const ROW_DATA = { | |
| 36 | dirty: 1, | |
| 37 | raw: 2, | |
| 38 | cells: 3, | |
| 39 | } as const; | |
| 40 | ||
| 41 | const 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. | |
| 53 | const 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 | ||
| 64 | const RAW_CELL_DATA = { | |
| 65 | wide: 3, | |
| 66 | } as const; | |
| 67 | ||
| 68 | export const GHOSTTY_CELL_WIDE = { | |
| 69 | narrow: 0, | |
| 70 | wide: 1, | |
| 71 | spacerTail: 2, | |
| 72 | spacerHead: 3, | |
| 73 | } as const; | |
| 74 | ||
| 75 | export interface GhosttyColor { | |
| 76 | readonly r: number; | |
| 77 | readonly g: number; | |
| 78 | readonly b: number; | |
| 79 | } | |
| 80 | ||
| 81 | export 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 | ||
| 90 | export 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 | ||
| 104 | export 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 | ||
| 112 | export 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 | ||
| 127 | export 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 | ||
| 138 | export 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. */ | |
| 145 | export interface GhosttyPointInput { | |
| 146 | readonly x: number; | |
| 147 | readonly y: number; | |
| 148 | readonly tag?: 1 | 2; | |
| 149 | } | |
| 150 | ||
| 151 | export 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 | ||
| 168 | const decoder = new TextDecoder(); | |
| 169 | const encoder = new TextEncoder(); | |
| 170 | ||
| 171 | function 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 | ||
| 180 | function 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 | */ | |
| 191 | export 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 | ||
| 206 | export 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 | ||
| 1288 | export 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 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2014 Ryan L McIntyre | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. |
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 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { ghosttyConsumedMods, ghosttyKeyForCode, ghosttyUnshiftedCodepoint } from "./keyCodes"; | |
| 4 | ||
| 5 | describe("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 | ||
| 14 | describe("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 | ||
| 26 | describe("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. | |
| 4 | const 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 | ||
| 183 | const codeToGhosttyKey = new Map<string, number>( | |
| 184 | ghosttyKeyboardCodes.map((code, index) => [code, index]), | |
| 185 | ); | |
| 186 | ||
| 187 | export function ghosttyKeyForCode(code: string): number { | |
| 188 | return codeToGhosttyKey.get(code) ?? 0; | |
| 189 | } | |
| 190 | ||
| 191 | export interface GhosttyKeyboardLayoutMap { | |
| 192 | get(code: string): string | undefined; | |
| 193 | } | |
| 194 | ||
| 195 | const 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 | ||
| 219 | let keyboardLayoutMapPromise: Promise<GhosttyKeyboardLayoutMap | undefined> | undefined; | |
| 220 | ||
| 221 | export 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. | |
| 238 | export 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 | ||
| 245 | export 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 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { GHOSTTY_CELL_WIDE, type GhosttyCell, type GhosttySnapshot } from "./core"; | |
| 4 | import { | |
| 5 | ghosttyTextRunEnd, | |
| 6 | measureGhosttyCell, | |
| 7 | renderGhosttySnapshot, | |
| 8 | terminalGridSize, | |
| 9 | } from "./renderer"; | |
| 10 | ||
| 11 | const 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 | ||
| 25 | describe("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 | ||
| 41 | describe("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 | ||
| 60 | describe("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 | ||
| 73 | describe("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 @@ |
| 1 | import { | |
| 2 | GHOSTTY_CELL_WIDE, | |
| 3 | ghosttyColorsEqual, | |
| 4 | type GhosttyCell, | |
| 5 | type GhosttyColor, | |
| 6 | type GhosttySnapshot, | |
| 7 | } from "./core"; | |
| 8 | ||
| 9 | export interface GhosttyCellMetrics { | |
| 10 | readonly width: number; | |
| 11 | readonly height: number; | |
| 12 | readonly baseline: number; | |
| 13 | } | |
| 14 | ||
| 15 | export interface GhosttyCellRange { | |
| 16 | readonly start: { readonly x: number; readonly y: number }; | |
| 17 | readonly end: { readonly x: number; readonly y: number }; | |
| 18 | } | |
| 19 | ||
| 20 | const DEFAULT_SELECTION_BACKGROUND = "rgba(72, 122, 191, 0.35)"; | |
| 21 | ||
| 22 | function cssColor(color: GhosttyColor): string { | |
| 23 | return `rgb(${color.r}, ${color.g}, ${color.b})`; | |
| 24 | } | |
| 25 | ||
| 26 | function 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 | ||
| 38 | export 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 | ||
| 57 | function 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 | ||
| 63 | export 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 | ||
| 82 | export 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 | ||
| 94 | export 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 @@ |
| 1 | import ghosttyWasmUrl from "./vendor/ghostty-vt.wasm?url"; | |
| 2 | import ghosttyWritePtyWasmUrl from "./vendor/ghostty-write-pty.wasm?url&no-inline"; | |
| 3 | ||
| 4 | type WasmFunction = (...args: Array<number | bigint>) => number; | |
| 5 | ||
| 6 | interface TypeField { | |
| 7 | readonly offset: number; | |
| 8 | readonly size: number; | |
| 9 | readonly type: string; | |
| 10 | } | |
| 11 | ||
| 12 | interface TypeLayout { | |
| 13 | readonly size: number; | |
| 14 | readonly align: number; | |
| 15 | readonly fields: Readonly<Record<string, TypeField>>; | |
| 16 | } | |
| 17 | ||
| 18 | type TypeLayouts = Readonly<Record<string, TypeLayout>>; | |
| 19 | ||
| 20 | const textDecoder = new TextDecoder(); | |
| 21 | ||
| 22 | export 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 | ||
| 225 | let runtimePromise: Promise<GhosttyRuntime> | null = null; | |
| 226 | ||
| 227 | export 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 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import wasmDataUrl from "./vendor/ghostty-vt.wasm?inline"; | |
| 4 | import writePtyWasmDataUrl from "./vendor/ghostty-write-pty.wasm?inline"; | |
| 5 | import pinnedVersion from "../../../../../native/libghostty-vt/VERSION?raw"; | |
| 6 | import { ghosttyKeyForCode } from "./keyCodes"; | |
| 7 | ||
| 8 | type WasmFunction = (...args: number[]) => number; | |
| 9 | ||
| 10 | function 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 | ||
| 16 | describe("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 @@ |
| 1 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { GhosttyTerminalCore, type GhosttyCell, type GhosttyRow } from "./core"; | |
| 4 | import { | |
| 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 | ||
| 35 | vi.mock("./vendor/ghostty-vt.wasm?url", async () => ({ | |
| 36 | default: (await import("./vendor/ghostty-vt.wasm?inline")).default, | |
| 37 | })); | |
| 38 | vi.mock("./vendor/ghostty-write-pty.wasm?url&no-inline", async () => ({ | |
| 39 | default: (await import("./vendor/ghostty-write-pty.wasm?inline")).default, | |
| 40 | })); | |
| 41 | ||
| 42 | describe("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 | ||
| 936 | const 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 | ||
| 950 | describe("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 | ||
| 967 | describe("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 | ||
| 993 | describe("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 | ||
| 1016 | describe("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 | ||
| 1115 | describe("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 | ||
| 1157 | describe("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 | ||
| 1202 | describe("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 | ||
| 1230 | describe("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 | ||
| 1268 | describe("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 | ||
| 1280 | describe("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 | ||
| 1302 | describe("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 | ||
| 1358 | describe("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 | ||
| 1415 | describe("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 | ||
| 1440 | describe("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 | ||
| 1466 | describe("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 | ||
| 1475 | describe("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 | ||
| 1522 | describe("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 @@ |
| 1 | import { isMacPlatform } from "../../lib/utils"; | |
| 2 | import { SELECTION_MULTI_CLICK_INTERVAL_MS } from "../../lib/selectionActions"; | |
| 3 | import { collectWrappedTerminalLinkLine, extractTerminalLinks } from "../../terminal-links"; | |
| 4 | import { | |
| 5 | GhosttyTerminalCore, | |
| 6 | type GhosttyScrollbar, | |
| 7 | type GhosttySnapshot, | |
| 8 | type GhosttyTheme, | |
| 9 | } from "./core"; | |
| 10 | import { | |
| 11 | measureGhosttyCell, | |
| 12 | renderGhosttySnapshot, | |
| 13 | terminalGridSize, | |
| 14 | type GhosttyCellRange, | |
| 15 | type GhosttyCellMetrics, | |
| 16 | } from "./renderer"; | |
| 17 | import symbolsFontUrl from "./fonts/SymbolsNerdFontMono-Regular.woff2?url"; | |
| 18 | import { isMonospaceFamily } from "../../appearanceFonts"; | |
| 19 | import { observeResize } from "../../lib/observeResize"; | |
| 20 | ||
| 21 | export const DEFAULT_TERMINAL_FONT_SIZE = 12; | |
| 22 | const MIN_TERMINAL_FONT_SIZE = 6; | |
| 23 | const 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. | |
| 28 | const 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. | |
| 35 | export const DEFAULT_TERMINAL_FONT_FAMILY = | |
| 36 | '"SF Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", ' + TERMINAL_GLYPH_FALLBACKS; | |
| 37 | const CONTENT_PADDING = 4; | |
| 38 | const MIN_SCROLLBAR_THUMB_HEIGHT = 18; | |
| 39 | /** Half a blink cycle: the visible and hidden phases are equally long. */ | |
| 40 | const CURSOR_BLINK_INTERVAL_MS = 500; | |
| 41 | const TERMINAL_FONT_LOAD_TEXT = "iMW0@# ."; | |
| 42 | const 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. */ | |
| 50 | export interface GhosttyTerminalFont { | |
| 51 | readonly family?: string; | |
| 52 | readonly size?: number; | |
| 53 | } | |
| 54 | ||
| 55 | let 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 | */ | |
| 63 | function 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 | ||
| 76 | function 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 | ||
| 90 | function 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 | ||
| 97 | export 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. */ | |
| 111 | export 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 | ||
| 134 | export 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 | */ | |
| 144 | export 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 | */ | |
| 160 | export 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 | ||
| 172 | export interface TerminalScrollbarGeometry { | |
| 173 | readonly thumbHeight: number; | |
| 174 | readonly thumbTop: number; | |
| 175 | readonly maxOffset: number; | |
| 176 | } | |
| 177 | ||
| 178 | export 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 | ||
| 199 | export 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 | ||
| 213 | export 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 | ||
| 235 | function 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 | ||
| 240 | function 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 | ||
| 248 | export interface TerminalLinkWithRange { | |
| 249 | readonly text: string; | |
| 250 | readonly range: GhosttyCellRange; | |
| 251 | } | |
| 252 | ||
| 253 | function 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 | ||
| 266 | function 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 | ||
| 274 | export 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 | ||
| 334 | export 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 | */ | |
| 351 | export 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 | ||
| 360 | export 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 | */ | |
| 376 | export 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 | ||
| 387 | export 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 | */ | |
| 404 | function isMiddleClickPastePlatform(): boolean { | |
| 405 | return /linux|bsd/i.test(navigator.platform); | |
| 406 | } | |
| 407 | ||
| 408 | export 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. */ | |
| 417 | export 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 | ||
| 424 | export function isTerminalAltGraphText( | |
| 425 | event: Pick<KeyboardEvent, "getModifierState" | "key">, | |
| 426 | ): boolean { | |
| 427 | return event.getModifierState("AltGraph") && [...event.key].length === 1; | |
| 428 | } | |
| 429 | ||
| 430 | export 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 | ||
| 437 | type TerminalMouseAction = "press" | "release" | "motion"; | |
| 438 | ||
| 439 | export 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 | ||
| 451 | export 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 | ||
| 462 | export 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 | ||
| 480 | export 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 | ||
| 493 | export 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 | ||
| 510 | export interface TerminalSelectionClickSequence { | |
| 511 | readonly count: number; | |
| 512 | readonly time: number; | |
| 513 | readonly x: number; | |
| 514 | readonly y: number; | |
| 515 | } | |
| 516 | ||
| 517 | const TERMINAL_LINK_DRAG_THRESHOLD_PX = 4; | |
| 518 | ||
| 519 | export 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 | ||
| 536 | export interface GhosttySelectionPosition { | |
| 537 | readonly start: { readonly x: number; readonly y: number }; | |
| 538 | readonly end: { readonly x: number; readonly y: number }; | |
| 539 | } | |
| 540 | ||
| 541 | export 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 | ||
| 560 | export 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. |
| 36 | 36 | |
| 37 | 37 | ## Renderer ownership |
| 38 | 38 | |
| 39 | Android and web use the same `libghostty-vt` C ABI for terminal behavior. Platform | |
| 40 | adapters own drawing and input integration, and React stays out of terminal frames. | |
| 41 | The web adapter shares one WebAssembly instance per browser tab while each terminal | |
| 42 | owns and frees its own handles. The canonical upstream pin is | |
| 43 | [`native/libghostty-vt/VERSION`](../../native/libghostty-vt/VERSION); both native and | |
| 44 | web artifacts must be rebuilt when it changes. Web embeds the revision in its build | |
| 45 | info so the ABI check can detect drift without a second pin. | |
| 39 | Android 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 | |
| 42 | frames and native selection; the T3 adapter owns environment transport and local | |
| 43 | interaction policy. One WebAssembly instance serves each browser tab, with separate | |
| 44 | terminal handles freed on disposal. The Android pin remains independent at | |
| 45 | [`native/libghostty-vt/VERSION`](../../native/libghostty-vt/VERSION). | |
| 46 | 46 | |
| 47 | 47 | Restoring scrollback must not send terminal replies to the current shell. Historical |
| 48 | 48 | device queries can otherwise provoke fresh replies that appear as junk at the |
| 49 | 49 | prompt. 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 | |
| 51 | during replay. Preserve both protections when changing retention or renderer code. | |
| 50 | [web adapter](../../apps/web/src/terminal/TerminalSurface.ts) suppresses its PTY data | |
| 51 | sink during replay. Preserve both protections when changing retention or renderer code. |
packages/terminal/LICENSE created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | ISC License | |
| 2 | ||
| 3 | Copyright (c) 2026 clover caruso | |
| 4 | ||
| 5 | Permission to use, copy, modify, and/or distribute this software for any | |
| 6 | purpose with or without fee is hereby granted, provided that the above | |
| 7 | copyright notice and this permission notice appear in all copies. | |
| 8 | ||
| 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
| 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |
| 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
| 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | |
| 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR | |
| 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | |
| 15 | PERFORMANCE OF THIS SOFTWARE. |
packages/terminal/README.md created+176| ... | ... | @@ -0,0 +1,176 @@ |
| 1 | # @clo/terminal | |
| 2 | ||
| 3 | Browser-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 | |
| 8 | pnpm add @clo/terminal | |
| 9 | ``` | |
| 10 | ||
| 11 | Add `react` or `solid-js` only when using that adapter. Import the renderer stylesheet once: | |
| 12 | ||
| 13 | ```ts | |
| 14 | import "@clo/terminal/terminal.css"; | |
| 15 | ``` | |
| 16 | ||
| 17 | Vite and compatible bundlers can resolve the exported wasm as a URL: | |
| 18 | ||
| 19 | ```ts | |
| 20 | import { GhosttyModule } from "@clo/terminal"; | |
| 21 | import wasmUrl from "@clo/terminal/ghostty-vt.wasm?url"; | |
| 22 | ||
| 23 | const 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 | |
| 31 | import { mountTerminal } from "@clo/terminal"; | |
| 32 | ||
| 33 | const terminal = mountTerminal(document.querySelector("#terminal")!, module, { | |
| 34 | onData: (bytes) => socket.send(bytes), | |
| 35 | onResize: ({ cols, rows }) => resizePty(cols, rows), | |
| 36 | }); | |
| 37 | ||
| 38 | socket.binaryType = "arraybuffer"; | |
| 39 | const receive = ({ data }: MessageEvent<string | ArrayBuffer>) => | |
| 40 | terminal.write(typeof data === "string" ? data : new Uint8Array(data)); | |
| 41 | socket.addEventListener("message", receive); | |
| 42 | await terminal.ready; | |
| 43 | ||
| 44 | // Later: | |
| 45 | socket.removeEventListener("message", receive); | |
| 46 | terminal.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 | |
| 54 | import { 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 | |
| 73 | import { GhosttyModule } from "@clo/terminal"; | |
| 74 | import { Show, createSignal } from "solid-js"; | |
| 75 | import { TerminalPane } from "@clo/terminal/solid"; | |
| 76 | ||
| 77 | const [module, setModule] = createSignal<GhosttyModule>(); | |
| 78 | void 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 | ||
| 98 | Both 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 | ||
| 122 | The 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 | ||
| 130 | Over 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 | |
| 133 | mountTerminal(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 | ||
| 141 | Unlike 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 | ||
| 143 | Display 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 | ||
| 153 | Speculative 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 | ||
| 159 | Set 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 | ||
| 172 | Call `handle.view.refreshMetrics()` after changing a loaded font family or metric-affecting CSS variable. | |
| 173 | ||
| 174 | ## Security | |
| 175 | ||
| 176 | Links 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 | ||
| 3 | This file covers `__clo_terminal_symbols_3_4_0-Regular.woff2`, derived from | |
| 4 | Symbols Nerd Font Mono v3.4.0 at Nerd Fonts commit | |
| 5 | `fa7b859994228a9c8759f99c55a8d31ee92a1b5e`. | |
| 6 | ||
| 7 | ## Modifications | |
| 8 | ||
| 9 | Nerd Fonts assembled and normalized the component glyphs into its monospaced | |
| 10 | Symbols Only font. Clover selected the exact cmap listed in | |
| 11 | `SYMBOLS-CODEPOINTS.txt`, removed standard Powerline and unlicensed Font Logos, | |
| 12 | and converted the result to WOFF2 with fonttools. No retained outline was | |
| 13 | intentionally edited. This notice records those modifications for CC BY 4.0 | |
| 14 | and 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 | |
| 34 | audit, and is not retained. Standard Powerline `U+E0A0-E0A2` and | |
| 35 | `U+E0B0-E0B3` is also not retained. Accordingly, its separate Free License is | |
| 36 | not 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 | ||
| 54 | The source Weather Icons TTF is byte-identical to upstream git blob | |
| 55 | `948f0a5d26e899539e7821aa5a500932f76ba582`; its metadata names Erik Flowers | |
| 56 | and Lukas Bischoff. The retained Nerd Fonts source README identifies Devicons | |
| 57 | as `2.16.0.custom` and Codicons as `0.0.35`. These facts close attribution that | |
| 58 | is abbreviated in the component license text. |
packages/terminal/fonts/OFL.txt created+110| ... | ... | @@ -0,0 +1,110 @@ |
| 1 | Copyright (c) 2015-2026, Renzhi Li (aka. Belleve Invis, belleve@typeof.net) | |
| 2 | ||
| 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | ||
| 5 | This license is copied below, and is also available with a FAQ at: | |
| 6 | http://scripts.sil.org/OFL | |
| 7 | ||
| 8 | -------------------------- | |
| 9 | ||
| 10 | ||
| 11 | SIL Open Font License v1.1 | |
| 12 | ==================================================== | |
| 13 | ||
| 14 | ||
| 15 | Preamble | |
| 16 | ---------- | |
| 17 | ||
| 18 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 19 | development of collaborative font projects, to support the font creation | |
| 20 | efforts of academic and linguistic communities, and to provide a free and | |
| 21 | open framework in which fonts may be shared and improved in partnership | |
| 22 | with others. | |
| 23 | ||
| 24 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 25 | redistributed freely as long as they are not sold by themselves. The | |
| 26 | fonts, including any derivative works, can be bundled, embedded, | |
| 27 | redistributed and/or sold with any software provided that any reserved | |
| 28 | names are not used by derivative works. The fonts and derivatives, | |
| 29 | however, cannot be released under any other type of license. The | |
| 30 | requirement for fonts to remain under this license does not apply | |
| 31 | to any document created using the fonts or their derivatives. | |
| 32 | ||
| 33 | ||
| 34 | Definitions | |
| 35 | ------------- | |
| 36 | ||
| 37 | `"Font Software"` refers to the set of files released by the Copyright | |
| 38 | Holder(s) under this license and clearly marked as such. This may | |
| 39 | include source files, build scripts and documentation. | |
| 40 | ||
| 41 | `"Reserved Font Name"` refers to any names specified as such after the | |
| 42 | copyright statement(s). | |
| 43 | ||
| 44 | `"Original Version"` refers to the collection of Font Software components as | |
| 45 | distributed by the Copyright Holder(s). | |
| 46 | ||
| 47 | `"Modified Version"` refers to any derivative made by adding to, deleting, | |
| 48 | or substituting -- in part or in whole -- any of the components of the | |
| 49 | Original Version, by changing formats or by porting the Font Software to a | |
| 50 | new environment. | |
| 51 | ||
| 52 | `"Author"` refers to any designer, engineer, programmer, technical | |
| 53 | writer or other person who contributed to the Font Software. | |
| 54 | ||
| 55 | ||
| 56 | Permission & Conditions | |
| 57 | ------------------------ | |
| 58 | ||
| 59 | Permission is hereby granted, free of charge, to any person obtaining | |
| 60 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 61 | redistribute, and sell modified and unmodified copies of the Font | |
| 62 | Software, subject to the following conditions: | |
| 63 | ||
| 64 | 1. Neither the Font Software nor any of its individual components, | |
| 65 | in Original or Modified Versions, may be sold by itself. | |
| 66 | ||
| 67 | 2. 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 | ||
| 74 | 3. 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 | ||
| 79 | 4. 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 | ||
| 85 | 5. 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 | ||
| 93 | Termination | |
| 94 | ----------- | |
| 95 | ||
| 96 | This license becomes null and void if any of the above conditions are | |
| 97 | not 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 @@ |
| 1 | U+000D | |
| 2 | U+0020 | |
| 3 | U+0021 | |
| 4 | U+0022 | |
| 5 | U+0023 | |
| 6 | U+0024 | |
| 7 | U+0025 | |
| 8 | U+0026 | |
| 9 | U+0027 | |
| 10 | U+0028 | |
| 11 | U+0029 | |
| 12 | U+002A | |
| 13 | U+002B | |
| 14 | U+002C | |
| 15 | U+002D | |
| 16 | U+002E | |
| 17 | U+002F | |
| 18 | U+0030 | |
| 19 | U+0031 | |
| 20 | U+0032 | |
| 21 | U+0033 | |
| 22 | U+0034 | |
| 23 | U+0035 | |
| 24 | U+0036 | |
| 25 | U+0037 | |
| 26 | U+0038 | |
| 27 | U+0039 | |
| 28 | U+003A | |
| 29 | U+003B | |
| 30 | U+003C | |
| 31 | U+003D | |
| 32 | U+003E | |
| 33 | U+003F | |
| 34 | U+0040 | |
| 35 | U+0041 | |
| 36 | U+0042 | |
| 37 | U+0043 | |
| 38 | U+0044 | |
| 39 | U+0045 | |
| 40 | U+0046 | |
| 41 | U+0047 | |
| 42 | U+0048 | |
| 43 | U+0049 | |
| 44 | U+004A | |
| 45 | U+004B | |
| 46 | U+004C | |
| 47 | U+004D | |
| 48 | U+004E | |
| 49 | U+004F | |
| 50 | U+0050 | |
| 51 | U+0051 | |
| 52 | U+0052 | |
| 53 | U+0053 | |
| 54 | U+0054 | |
| 55 | U+0055 | |
| 56 | U+0056 | |
| 57 | U+0057 | |
| 58 | U+0058 | |
| 59 | U+0059 | |
| 60 | U+005A | |
| 61 | U+005B | |
| 62 | U+005C | |
| 63 | U+005D | |
| 64 | U+005E | |
| 65 | U+005F | |
| 66 | U+0060 | |
| 67 | U+0061 | |
| 68 | U+0062 | |
| 69 | U+0063 | |
| 70 | U+0064 | |
| 71 | U+0065 | |
| 72 | U+0066 | |
| 73 | U+0067 | |
| 74 | U+0068 | |
| 75 | U+0069 | |
| 76 | U+006A | |
| 77 | U+006B | |
| 78 | U+006C | |
| 79 | U+006D | |
| 80 | U+006E | |
| 81 | U+006F | |
| 82 | U+0070 | |
| 83 | U+0071 | |
| 84 | U+0072 | |
| 85 | U+0073 | |
| 86 | U+0074 | |
| 87 | U+0075 | |
| 88 | U+0076 | |
| 89 | U+0077 | |
| 90 | U+0078 | |
| 91 | U+0079 | |
| 92 | U+007A | |
| 93 | U+007B | |
| 94 | U+007C | |
| 95 | U+007D | |
| 96 | U+007E | |
| 97 | U+00A0 | |
| 98 | U+00A1 | |
| 99 | U+00A2 | |
| 100 | U+00A3 | |
| 101 | U+00A4 | |
| 102 | U+00A5 | |
| 103 | U+00A6 | |
| 104 | U+00A7 | |
| 105 | U+00A8 | |
| 106 | U+00A9 | |
| 107 | U+00AA | |
| 108 | U+00AB | |
| 109 | U+00AC | |
| 110 | U+00AD | |
| 111 | U+00AE | |
| 112 | U+00AF | |
| 113 | U+00B0 | |
| 114 | U+00B1 | |
| 115 | U+00B2 | |
| 116 | U+00B3 | |
| 117 | U+00B4 | |
| 118 | U+00B5 | |
| 119 | U+00B6 | |
| 120 | U+00B7 | |
| 121 | U+00B8 | |
| 122 | U+00B9 | |
| 123 | U+00BA | |
| 124 | U+00BB | |
| 125 | U+00BC | |
| 126 | U+00BD | |
| 127 | U+00BE | |
| 128 | U+00BF | |
| 129 | U+00C0 | |
| 130 | U+00C1 | |
| 131 | U+00C2 | |
| 132 | U+00C3 | |
| 133 | U+00C4 | |
| 134 | U+00C5 | |
| 135 | U+00C6 | |
| 136 | U+00C7 | |
| 137 | U+00C8 | |
| 138 | U+00C9 | |
| 139 | U+00CA | |
| 140 | U+00CB | |
| 141 | U+00CC | |
| 142 | U+00CD | |
| 143 | U+00CE | |
| 144 | U+00CF | |
| 145 | U+00D0 | |
| 146 | U+00D1 | |
| 147 | U+00D2 | |
| 148 | U+00D3 | |
| 149 | U+00D4 | |
| 150 | U+00D5 | |
| 151 | U+00D6 | |
| 152 | U+00D7 | |
| 153 | U+00D8 | |
| 154 | U+00D9 | |
| 155 | U+00DA | |
| 156 | U+00DB | |
| 157 | U+00DC | |
| 158 | U+00DD | |
| 159 | U+00DE | |
| 160 | U+00DF | |
| 161 | U+00E0 | |
| 162 | U+00E1 | |
| 163 | U+00E2 | |
| 164 | U+00E3 | |
| 165 | U+00E4 | |
| 166 | U+00E5 | |
| 167 | U+00E6 | |
| 168 | U+00E7 | |
| 169 | U+00E8 | |
| 170 | U+00E9 | |
| 171 | U+00EA | |
| 172 | U+00EB | |
| 173 | U+00EC | |
| 174 | U+00ED | |
| 175 | U+00EE | |
| 176 | U+00EF | |
| 177 | U+00F0 | |
| 178 | U+00F1 | |
| 179 | U+00F2 | |
| 180 | U+00F3 | |
| 181 | U+00F4 | |
| 182 | U+00F5 | |
| 183 | U+00F6 | |
| 184 | U+00F7 | |
| 185 | U+00F8 | |
| 186 | U+00F9 | |
| 187 | U+00FA | |
| 188 | U+00FB | |
| 189 | U+00FC | |
| 190 | U+00FD | |
| 191 | U+00FE | |
| 192 | U+00FF | |
| 193 | U+0100 | |
| 194 | U+0101 | |
| 195 | U+0102 | |
| 196 | U+0103 | |
| 197 | U+0104 | |
| 198 | U+0105 | |
| 199 | U+0106 | |
| 200 | U+0107 | |
| 201 | U+0108 | |
| 202 | U+0109 | |
| 203 | U+010A | |
| 204 | U+010B | |
| 205 | U+010C | |
| 206 | U+010D | |
| 207 | U+010E | |
| 208 | U+010F | |
| 209 | U+0110 | |
| 210 | U+0111 | |
| 211 | U+0112 | |
| 212 | U+0113 | |
| 213 | U+0114 | |
| 214 | U+0115 | |
| 215 | U+0116 | |
| 216 | U+0117 | |
| 217 | U+0118 | |
| 218 | U+0119 | |
| 219 | U+011A | |
| 220 | U+011B | |
| 221 | U+011C | |
| 222 | U+011D | |
| 223 | U+011E | |
| 224 | U+011F | |
| 225 | U+0120 | |
| 226 | U+0121 | |
| 227 | U+0122 | |
| 228 | U+0123 | |
| 229 | U+0124 | |
| 230 | U+0125 | |
| 231 | U+0126 | |
| 232 | U+0127 | |
| 233 | U+0128 | |
| 234 | U+0129 | |
| 235 | U+012A | |
| 236 | U+012B | |
| 237 | U+012C | |
| 238 | U+012D | |
| 239 | U+012E | |
| 240 | U+012F | |
| 241 | U+0130 | |
| 242 | U+0131 | |
| 243 | U+0132 | |
| 244 | U+0133 | |
| 245 | U+0134 | |
| 246 | U+0135 | |
| 247 | U+0136 | |
| 248 | U+0137 | |
| 249 | U+0138 | |
| 250 | U+0139 | |
| 251 | U+013A | |
| 252 | U+013B | |
| 253 | U+013C | |
| 254 | U+013D | |
| 255 | U+013E | |
| 256 | U+013F | |
| 257 | U+0140 | |
| 258 | U+0141 | |
| 259 | U+0142 | |
| 260 | U+0143 | |
| 261 | U+0144 | |
| 262 | U+0145 | |
| 263 | U+0146 | |
| 264 | U+0147 | |
| 265 | U+0148 | |
| 266 | U+0149 | |
| 267 | U+014A | |
| 268 | U+014B | |
| 269 | U+014C | |
| 270 | U+014D | |
| 271 | U+014E | |
| 272 | U+014F | |
| 273 | U+0150 | |
| 274 | U+0151 | |
| 275 | U+0152 | |
| 276 | U+0153 | |
| 277 | U+0154 | |
| 278 | U+0155 | |
| 279 | U+0156 | |
| 280 | U+0157 | |
| 281 | U+0158 | |
| 282 | U+0159 | |
| 283 | U+015A | |
| 284 | U+015B | |
| 285 | U+015C | |
| 286 | U+015D | |
| 287 | U+015E | |
| 288 | U+015F | |
| 289 | U+0160 | |
| 290 | U+0161 | |
| 291 | U+0162 | |
| 292 | U+0163 | |
| 293 | U+0164 | |
| 294 | U+0165 | |
| 295 | U+0166 | |
| 296 | U+0167 | |
| 297 | U+0168 | |
| 298 | U+0169 | |
| 299 | U+016A | |
| 300 | U+016B | |
| 301 | U+016C | |
| 302 | U+016D | |
| 303 | U+016E | |
| 304 | U+016F | |
| 305 | U+0170 | |
| 306 | U+0171 | |
| 307 | U+0172 | |
| 308 | U+0173 | |
| 309 | U+0174 | |
| 310 | U+0175 | |
| 311 | U+0176 | |
| 312 | U+0177 | |
| 313 | U+0178 | |
| 314 | U+0179 | |
| 315 | U+017A | |
| 316 | U+017B | |
| 317 | U+017C | |
| 318 | U+017D | |
| 319 | U+017E | |
| 320 | U+017F | |
| 321 | U+0180 | |
| 322 | U+0181 | |
| 323 | U+0182 | |
| 324 | U+0183 | |
| 325 | U+0184 | |
| 326 | U+0185 | |
| 327 | U+0186 | |
| 328 | U+0187 | |
| 329 | U+0188 | |
| 330 | U+0189 | |
| 331 | U+018A | |
| 332 | U+018B | |
| 333 | U+018C | |
| 334 | U+018D | |
| 335 | U+018E | |
| 336 | U+018F | |
| 337 | U+0190 | |
| 338 | U+0191 | |
| 339 | U+0192 | |
| 340 | U+0193 | |
| 341 | U+0194 | |
| 342 | U+0195 | |
| 343 | U+0196 | |
| 344 | U+0197 | |
| 345 | U+0198 | |
| 346 | U+0199 | |
| 347 | U+019A | |
| 348 | U+019B | |
| 349 | U+019C | |
| 350 | U+019D | |
| 351 | U+019E | |
| 352 | U+019F | |
| 353 | U+01A0 | |
| 354 | U+01A1 | |
| 355 | U+01A2 | |
| 356 | U+01A3 | |
| 357 | U+01A4 | |
| 358 | U+01A5 | |
| 359 | U+01A6 | |
| 360 | U+01A7 | |
| 361 | U+01A8 | |
| 362 | U+01A9 | |
| 363 | U+01AA | |
| 364 | U+01AB | |
| 365 | U+01AC | |
| 366 | U+01AD | |
| 367 | U+01AE | |
| 368 | U+01AF | |
| 369 | U+01B0 | |
| 370 | U+01B1 | |
| 371 | U+01B2 | |
| 372 | U+01B3 | |
| 373 | U+01B4 | |
| 374 | U+01B5 | |
| 375 | U+01B6 | |
| 376 | U+01B7 | |
| 377 | U+01B8 | |
| 378 | U+01B9 | |
| 379 | U+01BA | |
| 380 | U+01BB | |
| 381 | U+01BC | |
| 382 | U+01BD | |
| 383 | U+01BE | |
| 384 | U+01BF | |
| 385 | U+01C0 | |
| 386 | U+01C1 | |
| 387 | U+01C2 | |
| 388 | U+01C3 | |
| 389 | U+01C4 | |
| 390 | U+01C5 | |
| 391 | U+01C6 | |
| 392 | U+01C7 | |
| 393 | U+01C8 | |
| 394 | U+01C9 | |
| 395 | U+01CA | |
| 396 | U+01CB | |
| 397 | U+01CC | |
| 398 | U+01CD | |
| 399 | U+01CE | |
| 400 | U+01CF | |
| 401 | U+01D0 | |
| 402 | U+01D1 | |
| 403 | U+01D2 | |
| 404 | U+01D3 | |
| 405 | U+01D4 | |
| 406 | U+01D5 | |
| 407 | U+01D6 | |
| 408 | U+01D7 | |
| 409 | U+01D8 | |
| 410 | U+01D9 | |
| 411 | U+01DA | |
| 412 | U+01DB | |
| 413 | U+01DC | |
| 414 | U+01DD | |
| 415 | U+01DE | |
| 416 | U+01DF | |
| 417 | U+01E0 | |
| 418 | U+01E1 | |
| 419 | U+01E2 | |
| 420 | U+01E3 | |
| 421 | U+01E4 | |
| 422 | U+01E5 | |
| 423 | U+01E6 | |
| 424 | U+01E7 | |
| 425 | U+01E8 | |
| 426 | U+01E9 | |
| 427 | U+01EA | |
| 428 | U+01EB | |
| 429 | U+01EC | |
| 430 | U+01ED | |
| 431 | U+01EE | |
| 432 | U+01EF | |
| 433 | U+01F0 | |
| 434 | U+01F1 | |
| 435 | U+01F2 | |
| 436 | U+01F3 | |
| 437 | U+01F4 | |
| 438 | U+01F5 | |
| 439 | U+01F6 | |
| 440 | U+01F7 | |
| 441 | U+01F8 | |
| 442 | U+01F9 | |
| 443 | U+01FA | |
| 444 | U+01FB | |
| 445 | U+01FC | |
| 446 | U+01FD | |
| 447 | U+01FE | |
| 448 | U+01FF | |
| 449 | U+0200 | |
| 450 | U+0201 | |
| 451 | U+0202 | |
| 452 | U+0203 | |
| 453 | U+0204 | |
| 454 | U+0205 | |
| 455 | U+0206 | |
| 456 | U+0207 | |
| 457 | U+0208 | |
| 458 | U+0209 | |
| 459 | U+020A | |
| 460 | U+020B | |
| 461 | U+020C | |
| 462 | U+020D | |
| 463 | U+020E | |
| 464 | U+020F | |
| 465 | U+0210 | |
| 466 | U+0211 | |
| 467 | U+0212 | |
| 468 | U+0213 | |
| 469 | U+0214 | |
| 470 | U+0215 | |
| 471 | U+0216 | |
| 472 | U+0217 | |
| 473 | U+0218 | |
| 474 | U+0219 | |
| 475 | U+021A | |
| 476 | U+021B | |
| 477 | U+021C | |
| 478 | U+021D | |
| 479 | U+021E | |
| 480 | U+021F | |
| 481 | U+0220 | |
| 482 | U+0221 | |
| 483 | U+0222 | |
| 484 | U+0223 | |
| 485 | U+0224 | |
| 486 | U+0225 | |
| 487 | U+0226 | |
| 488 | U+0227 | |
| 489 | U+0228 | |
| 490 | U+0229 | |
| 491 | U+022A | |
| 492 | U+022B | |
| 493 | U+022C | |
| 494 | U+022D | |
| 495 | U+022E | |
| 496 | U+022F | |
| 497 | U+0230 | |
| 498 | U+0231 | |
| 499 | U+0232 | |
| 500 | U+0233 | |
| 501 | U+0234 | |
| 502 | U+0235 | |
| 503 | U+0236 | |
| 504 | U+0237 | |
| 505 | U+0238 | |
| 506 | U+0239 | |
| 507 | U+023A | |
| 508 | U+023B | |
| 509 | U+023C | |
| 510 | U+023D | |
| 511 | U+023E | |
| 512 | U+023F | |
| 513 | U+0240 | |
| 514 | U+0241 | |
| 515 | U+0242 | |
| 516 | U+0243 | |
| 517 | U+0244 | |
| 518 | U+0245 | |
| 519 | U+0246 | |
| 520 | U+0247 | |
| 521 | U+0248 | |
| 522 | U+0249 | |
| 523 | U+024A | |
| 524 | U+024B | |
| 525 | U+024C | |
| 526 | U+024D | |
| 527 | U+024E | |
| 528 | U+024F | |
| 529 | U+0250 | |
| 530 | U+0251 | |
| 531 | U+0252 | |
| 532 | U+0253 | |
| 533 | U+0254 | |
| 534 | U+0255 | |
| 535 | U+0256 | |
| 536 | U+0257 | |
| 537 | U+0258 | |
| 538 | U+0259 | |
| 539 | U+025A | |
| 540 | U+025B | |
| 541 | U+025C | |
| 542 | U+025D | |
| 543 | U+025E | |
| 544 | U+025F | |
| 545 | U+0260 | |
| 546 | U+0261 | |
| 547 | U+0262 | |
| 548 | U+0263 | |
| 549 | U+0264 | |
| 550 | U+0265 | |
| 551 | U+0266 | |
| 552 | U+0267 | |
| 553 | U+0268 | |
| 554 | U+0269 | |
| 555 | U+026A | |
| 556 | U+026B | |
| 557 | U+026C | |
| 558 | U+026D | |
| 559 | U+026E | |
| 560 | U+026F | |
| 561 | U+0270 | |
| 562 | U+0271 | |
| 563 | U+0272 | |
| 564 | U+0273 | |
| 565 | U+0274 | |
| 566 | U+0275 | |
| 567 | U+0276 | |
| 568 | U+0277 | |
| 569 | U+0278 | |
| 570 | U+0279 | |
| 571 | U+027A | |
| 572 | U+027B | |
| 573 | U+027C | |
| 574 | U+027D | |
| 575 | U+027E | |
| 576 | U+027F | |
| 577 | U+0280 | |
| 578 | U+0281 | |
| 579 | U+0282 | |
| 580 | U+0283 | |
| 581 | U+0284 | |
| 582 | U+0285 | |
| 583 | U+0286 | |
| 584 | U+0287 | |
| 585 | U+0288 | |
| 586 | U+0289 | |
| 587 | U+028A | |
| 588 | U+028B | |
| 589 | U+028C | |
| 590 | U+028D | |
| 591 | U+028E | |
| 592 | U+028F | |
| 593 | U+0290 | |
| 594 | U+0291 | |
| 595 | U+0292 | |
| 596 | U+0293 | |
| 597 | U+0294 | |
| 598 | U+0295 | |
| 599 | U+0296 | |
| 600 | U+0297 | |
| 601 | U+0298 | |
| 602 | U+0299 | |
| 603 | U+029A | |
| 604 | U+029B | |
| 605 | U+029C | |
| 606 | U+029D | |
| 607 | U+029E | |
| 608 | U+029F | |
| 609 | U+02A0 | |
| 610 | U+02A1 | |
| 611 | U+02A2 | |
| 612 | U+02A3 | |
| 613 | U+02A4 | |
| 614 | U+02A5 | |
| 615 | U+02A6 | |
| 616 | U+02A7 | |
| 617 | U+02A8 | |
| 618 | U+02A9 | |
| 619 | U+02AA | |
| 620 | U+02AB | |
| 621 | U+02AC | |
| 622 | U+02AD | |
| 623 | U+02AE | |
| 624 | U+02AF | |
| 625 | U+02B0 | |
| 626 | U+02B1 | |
| 627 | U+02B2 | |
| 628 | U+02B3 | |
| 629 | U+02B4 | |
| 630 | U+02B5 | |
| 631 | U+02B6 | |
| 632 | U+02B7 | |
| 633 | U+02B8 | |
| 634 | U+02B9 | |
| 635 | U+02BA | |
| 636 | U+02BB | |
| 637 | U+02BC | |
| 638 | U+02BD | |
| 639 | U+02BE | |
| 640 | U+02BF | |
| 641 | U+02C0 | |
| 642 | U+02C1 | |
| 643 | U+02C2 | |
| 644 | U+02C3 | |
| 645 | U+02C4 | |
| 646 | U+02C5 | |
| 647 | U+02C6 | |
| 648 | U+02C7 | |
| 649 | U+02C8 | |
| 650 | U+02C9 | |
| 651 | U+02CA | |
| 652 | U+02CB | |
| 653 | U+02CC | |
| 654 | U+02CD | |
| 655 | U+02CE | |
| 656 | U+02CF | |
| 657 | U+02D0 | |
| 658 | U+02D1 | |
| 659 | U+02D2 | |
| 660 | U+02D3 | |
| 661 | U+02D4 | |
| 662 | U+02D5 | |
| 663 | U+02D6 | |
| 664 | U+02D7 | |
| 665 | U+02D8 | |
| 666 | U+02D9 | |
| 667 | U+02DA | |
| 668 | U+02DB | |
| 669 | U+02DC | |
| 670 | U+02DD | |
| 671 | U+02DE | |
| 672 | U+02DF | |
| 673 | U+02E0 | |
| 674 | U+02E1 | |
| 675 | U+02E2 | |
| 676 | U+02E3 | |
| 677 | U+02E4 | |
| 678 | U+02E5 | |
| 679 | U+02E6 | |
| 680 | U+02E7 | |
| 681 | U+02E8 | |
| 682 | U+02E9 | |
| 683 | U+02EA | |
| 684 | U+02EB | |
| 685 | U+02EC | |
| 686 | U+02ED | |
| 687 | U+02EE | |
| 688 | U+02EF | |
| 689 | U+02F0 | |
| 690 | U+02F1 | |
| 691 | U+02F2 | |
| 692 | U+02F3 | |
| 693 | U+02F4 | |
| 694 | U+02F5 | |
| 695 | U+02F6 | |
| 696 | U+02F7 | |
| 697 | U+02F8 | |
| 698 | U+02F9 | |
| 699 | U+02FA | |
| 700 | U+02FB | |
| 701 | U+02FC | |
| 702 | U+02FD | |
| 703 | U+02FE | |
| 704 | U+02FF | |
| 705 | U+0300 | |
| 706 | U+0301 | |
| 707 | U+0302 | |
| 708 | U+0303 | |
| 709 | U+0304 | |
| 710 | U+0305 | |
| 711 | U+0306 | |
| 712 | U+0307 | |
| 713 | U+0308 | |
| 714 | U+0309 | |
| 715 | U+030A | |
| 716 | U+030B | |
| 717 | U+030C | |
| 718 | U+030D | |
| 719 | U+030E | |
| 720 | U+030F | |
| 721 | U+0310 | |
| 722 | U+0311 | |
| 723 | U+0312 | |
| 724 | U+0313 | |
| 725 | U+0314 | |
| 726 | U+0315 | |
| 727 | U+0316 | |
| 728 | U+0317 | |
| 729 | U+0318 | |
| 730 | U+0319 | |
| 731 | U+031A | |
| 732 | U+031B | |
| 733 | U+031C | |
| 734 | U+031D | |
| 735 | U+031E | |
| 736 | U+031F | |
| 737 | U+0320 | |
| 738 | U+0321 | |
| 739 | U+0322 | |
| 740 | U+0323 | |
| 741 | U+0324 | |
| 742 | U+0325 | |
| 743 | U+0326 | |
| 744 | U+0327 | |
| 745 | U+0328 | |
| 746 | U+0329 | |
| 747 | U+032A | |
| 748 | U+032B | |
| 749 | U+032C | |
| 750 | U+032D | |
| 751 | U+032E | |
| 752 | U+032F | |
| 753 | U+0330 | |
| 754 | U+0331 | |
| 755 | U+0332 | |
| 756 | U+0333 | |
| 757 | U+0334 | |
| 758 | U+0335 | |
| 759 | U+0336 | |
| 760 | U+0337 | |
| 761 | U+0338 | |
| 762 | U+0339 | |
| 763 | U+033A | |
| 764 | U+033B | |
| 765 | U+033C | |
| 766 | U+033D | |
| 767 | U+033E | |
| 768 | U+033F | |
| 769 | U+0340 | |
| 770 | U+0341 | |
| 771 | U+0342 | |
| 772 | U+0343 | |
| 773 | U+0344 | |
| 774 | U+0345 | |
| 775 | U+0346 | |
| 776 | U+0347 | |
| 777 | U+0348 | |
| 778 | U+0349 | |
| 779 | U+034A | |
| 780 | U+034B | |
| 781 | U+034C | |
| 782 | U+034D | |
| 783 | U+034E | |
| 784 | U+034F | |
| 785 | U+0350 | |
| 786 | U+0351 | |
| 787 | U+0352 | |
| 788 | U+0353 | |
| 789 | U+0354 | |
| 790 | U+0355 | |
| 791 | U+0356 | |
| 792 | U+0357 | |
| 793 | U+0358 | |
| 794 | U+0359 | |
| 795 | U+035A | |
| 796 | U+035B | |
| 797 | U+035C | |
| 798 | U+035D | |
| 799 | U+035E | |
| 800 | U+035F | |
| 801 | U+0360 | |
| 802 | U+0361 | |
| 803 | U+0362 | |
| 804 | U+0363 | |
| 805 | U+0364 | |
| 806 | U+0365 | |
| 807 | U+0366 | |
| 808 | U+0367 | |
| 809 | U+0368 | |
| 810 | U+0369 | |
| 811 | U+036A | |
| 812 | U+036B | |
| 813 | U+036C | |
| 814 | U+036D | |
| 815 | U+036E | |
| 816 | U+036F | |
| 817 | U+0370 | |
| 818 | U+0371 | |
| 819 | U+0372 | |
| 820 | U+0373 | |
| 821 | U+0374 | |
| 822 | U+0375 | |
| 823 | U+0376 | |
| 824 | U+0377 | |
| 825 | U+037A | |
| 826 | U+037B | |
| 827 | U+037C | |
| 828 | U+037D | |
| 829 | U+037E | |
| 830 | U+037F | |
| 831 | U+0384 | |
| 832 | U+0385 | |
| 833 | U+0386 | |
| 834 | U+0387 | |
| 835 | U+0388 | |
| 836 | U+0389 | |
| 837 | U+038A | |
| 838 | U+038C | |
| 839 | U+038E | |
| 840 | U+038F | |
| 841 | U+0390 | |
| 842 | U+0391 | |
| 843 | U+0392 | |
| 844 | U+0393 | |
| 845 | U+0394 | |
| 846 | U+0395 | |
| 847 | U+0396 | |
| 848 | U+0397 | |
| 849 | U+0398 | |
| 850 | U+0399 | |
| 851 | U+039A | |
| 852 | U+039B | |
| 853 | U+039C | |
| 854 | U+039D | |
| 855 | U+039E | |
| 856 | U+039F | |
| 857 | U+03A0 | |
| 858 | U+03A1 | |
| 859 | U+03A3 | |
| 860 | U+03A4 | |
| 861 | U+03A5 | |
| 862 | U+03A6 | |
| 863 | U+03A7 | |
| 864 | U+03A8 | |
| 865 | U+03A9 | |
| 866 | U+03AA | |
| 867 | U+03AB | |
| 868 | U+03AC | |
| 869 | U+03AD | |
| 870 | U+03AE | |
| 871 | U+03AF | |
| 872 | U+03B0 | |
| 873 | U+03B1 | |
| 874 | U+03B2 | |
| 875 | U+03B3 | |
| 876 | U+03B4 | |
| 877 | U+03B5 | |
| 878 | U+03B6 | |
| 879 | U+03B7 | |
| 880 | U+03B8 | |
| 881 | U+03B9 | |
| 882 | U+03BA | |
| 883 | U+03BB | |
| 884 | U+03BC | |
| 885 | U+03BD | |
| 886 | U+03BE | |
| 887 | U+03BF | |
| 888 | U+03C0 | |
| 889 | U+03C1 | |
| 890 | U+03C2 | |
| 891 | U+03C3 | |
| 892 | U+03C4 | |
| 893 | U+03C5 | |
| 894 | U+03C6 | |
| 895 | U+03C7 | |
| 896 | U+03C8 | |
| 897 | U+03C9 | |
| 898 | U+03CA | |
| 899 | U+03CB | |
| 900 | U+03CC | |
| 901 | U+03CD | |
| 902 | U+03CE | |
| 903 | U+03CF | |
| 904 | U+03D0 | |
| 905 | U+03D1 | |
| 906 | U+03D2 | |
| 907 | U+03D3 | |
| 908 | U+03D4 | |
| 909 | U+03D5 | |
| 910 | U+03D6 | |
| 911 | U+03D7 | |
| 912 | U+03D8 | |
| 913 | U+03D9 | |
| 914 | U+03DA | |
| 915 | U+03DB | |
| 916 | U+03DC | |
| 917 | U+03DD | |
| 918 | U+03DE | |
| 919 | U+03DF | |
| 920 | U+03E0 | |
| 921 | U+03E1 | |
| 922 | U+03F0 | |
| 923 | U+03F1 | |
| 924 | U+03F2 | |
| 925 | U+03F3 | |
| 926 | U+03F4 | |
| 927 | U+03F5 | |
| 928 | U+03F6 | |
| 929 | U+03F7 | |
| 930 | U+03F8 | |
| 931 | U+03F9 | |
| 932 | U+03FA | |
| 933 | U+03FB | |
| 934 | U+03FC | |
| 935 | U+03FD | |
| 936 | U+03FE | |
| 937 | U+03FF | |
| 938 | U+0400 | |
| 939 | U+0401 | |
| 940 | U+0402 | |
| 941 | U+0403 | |
| 942 | U+0404 | |
| 943 | U+0405 | |
| 944 | U+0406 | |
| 945 | U+0407 | |
| 946 | U+0408 | |
| 947 | U+0409 | |
| 948 | U+040A | |
| 949 | U+040B | |
| 950 | U+040C | |
| 951 | U+040D | |
| 952 | U+040E | |
| 953 | U+040F | |
| 954 | U+0410 | |
| 955 | U+0411 | |
| 956 | U+0412 | |
| 957 | U+0413 | |
| 958 | U+0414 | |
| 959 | U+0415 | |
| 960 | U+0416 | |
| 961 | U+0417 | |
| 962 | U+0418 | |
| 963 | U+0419 | |
| 964 | U+041A | |
| 965 | U+041B | |
| 966 | U+041C | |
| 967 | U+041D | |
| 968 | U+041E | |
| 969 | U+041F | |
| 970 | U+0420 | |
| 971 | U+0421 | |
| 972 | U+0422 | |
| 973 | U+0423 | |
| 974 | U+0424 | |
| 975 | U+0425 | |
| 976 | U+0426 | |
| 977 | U+0427 | |
| 978 | U+0428 | |
| 979 | U+0429 | |
| 980 | U+042A | |
| 981 | U+042B | |
| 982 | U+042C | |
| 983 | U+042D | |
| 984 | U+042E | |
| 985 | U+042F | |
| 986 | U+0430 | |
| 987 | U+0431 | |
| 988 | U+0432 | |
| 989 | U+0433 | |
| 990 | U+0434 | |
| 991 | U+0435 | |
| 992 | U+0436 | |
| 993 | U+0437 | |
| 994 | U+0438 | |
| 995 | U+0439 | |
| 996 | U+043A | |
| 997 | U+043B | |
| 998 | U+043C | |
| 999 | U+043D | |
| 1000 | U+043E | |
| 1001 | U+043F | |
| 1002 | U+0440 | |
| 1003 | U+0441 | |
| 1004 | U+0442 | |
| 1005 | U+0443 | |
| 1006 | U+0444 | |
| 1007 | U+0445 | |
| 1008 | U+0446 | |
| 1009 | U+0447 | |
| 1010 | U+0448 | |
| 1011 | U+0449 | |
| 1012 | U+044A | |
| 1013 | U+044B | |
| 1014 | U+044C | |
| 1015 | U+044D | |
| 1016 | U+044E | |
| 1017 | U+044F | |
| 1018 | U+0450 | |
| 1019 | U+0451 | |
| 1020 | U+0452 | |
| 1021 | U+0453 | |
| 1022 | U+0454 | |
| 1023 | U+0455 | |
| 1024 | U+0456 | |
| 1025 | U+0457 | |
| 1026 | U+0458 | |
| 1027 | U+0459 | |
| 1028 | U+045A | |
| 1029 | U+045B | |
| 1030 | U+045C | |
| 1031 | U+045D | |
| 1032 | U+045E | |
| 1033 | U+045F | |
| 1034 | U+0460 | |
| 1035 | U+0461 | |
| 1036 | U+0462 | |
| 1037 | U+0463 | |
| 1038 | U+0464 | |
| 1039 | U+0465 | |
| 1040 | U+0466 | |
| 1041 | U+0467 | |
| 1042 | U+0468 | |
| 1043 | U+0469 | |
| 1044 | U+046A | |
| 1045 | U+046B | |
| 1046 | U+046C | |
| 1047 | U+046D | |
| 1048 | U+046E | |
| 1049 | U+046F | |
| 1050 | U+0470 | |
| 1051 | U+0471 | |
| 1052 | U+0472 | |
| 1053 | U+0473 | |
| 1054 | U+0474 | |
| 1055 | U+0475 | |
| 1056 | U+0476 | |
| 1057 | U+0477 | |
| 1058 | U+0478 | |
| 1059 | U+0479 | |
| 1060 | U+047A | |
| 1061 | U+047B | |
| 1062 | U+047C | |
| 1063 | U+047D | |
| 1064 | U+047E | |
| 1065 | U+047F | |
| 1066 | U+0480 | |
| 1067 | U+0481 | |
| 1068 | U+0482 | |
| 1069 | U+0483 | |
| 1070 | U+0484 | |
| 1071 | U+0485 | |
| 1072 | U+0486 | |
| 1073 | U+0487 | |
| 1074 | U+0488 | |
| 1075 | U+0489 | |
| 1076 | U+048A | |
| 1077 | U+048B | |
| 1078 | U+048C | |
| 1079 | U+048D | |
| 1080 | U+048E | |
| 1081 | U+048F | |
| 1082 | U+0490 | |
| 1083 | U+0491 | |
| 1084 | U+0492 | |
| 1085 | U+0493 | |
| 1086 | U+0494 | |
| 1087 | U+0495 | |
| 1088 | U+0496 | |
| 1089 | U+0497 | |
| 1090 | U+0498 | |
| 1091 | U+0499 | |
| 1092 | U+049A | |
| 1093 | U+049B | |
| 1094 | U+049C | |
| 1095 | U+049D | |
| 1096 | U+049E | |
| 1097 | U+049F | |
| 1098 | U+04A0 | |
| 1099 | U+04A1 | |
| 1100 | U+04A2 | |
| 1101 | U+04A3 | |
| 1102 | U+04A4 | |
| 1103 | U+04A5 | |
| 1104 | U+04A6 | |
| 1105 | U+04A7 | |
| 1106 | U+04A8 | |
| 1107 | U+04A9 | |
| 1108 | U+04AA | |
| 1109 | U+04AB | |
| 1110 | U+04AC | |
| 1111 | U+04AD | |
| 1112 | U+04AE | |
| 1113 | U+04AF | |
| 1114 | U+04B0 | |
| 1115 | U+04B1 | |
| 1116 | U+04B2 | |
| 1117 | U+04B3 | |
| 1118 | U+04B4 | |
| 1119 | U+04B5 | |
| 1120 | U+04B6 | |
| 1121 | U+04B7 | |
| 1122 | U+04B8 | |
| 1123 | U+04B9 | |
| 1124 | U+04BA | |
| 1125 | U+04BB | |
| 1126 | U+04BC | |
| 1127 | U+04BD | |
| 1128 | U+04BE | |
| 1129 | U+04BF | |
| 1130 | U+04C0 | |
| 1131 | U+04C1 | |
| 1132 | U+04C2 | |
| 1133 | U+04C3 | |
| 1134 | U+04C4 | |
| 1135 | U+04C5 | |
| 1136 | U+04C6 | |
| 1137 | U+04C7 | |
| 1138 | U+04C8 | |
| 1139 | U+04C9 | |
| 1140 | U+04CA | |
| 1141 | U+04CB | |
| 1142 | U+04CC | |
| 1143 | U+04CD | |
| 1144 | U+04CE | |
| 1145 | U+04CF | |
| 1146 | U+04D0 | |
| 1147 | U+04D1 | |
| 1148 | U+04D2 | |
| 1149 | U+04D3 | |
| 1150 | U+04D4 | |
| 1151 | U+04D5 | |
| 1152 | U+04D6 | |
| 1153 | U+04D7 | |
| 1154 | U+04D8 | |
| 1155 | U+04D9 | |
| 1156 | U+04DA | |
| 1157 | U+04DB | |
| 1158 | U+04DC | |
| 1159 | U+04DD | |
| 1160 | U+04DE | |
| 1161 | U+04DF | |
| 1162 | U+04E0 | |
| 1163 | U+04E1 | |
| 1164 | U+04E2 | |
| 1165 | U+04E3 | |
| 1166 | U+04E4 | |
| 1167 | U+04E5 | |
| 1168 | U+04E6 | |
| 1169 | U+04E7 | |
| 1170 | U+04E8 | |
| 1171 | U+04E9 | |
| 1172 | U+04EA | |
| 1173 | U+04EB | |
| 1174 | U+04EC | |
| 1175 | U+04ED | |
| 1176 | U+04EE | |
| 1177 | U+04EF | |
| 1178 | U+04F0 | |
| 1179 | U+04F1 | |
| 1180 | U+04F2 | |
| 1181 | U+04F3 | |
| 1182 | U+04F4 | |
| 1183 | U+04F5 | |
| 1184 | U+04F6 | |
| 1185 | U+04F7 | |
| 1186 | U+04F8 | |
| 1187 | U+04F9 | |
| 1188 | U+04FA | |
| 1189 | U+04FB | |
| 1190 | U+04FC | |
| 1191 | U+04FD | |
| 1192 | U+04FE | |
| 1193 | U+04FF | |
| 1194 | U+0500 | |
| 1195 | U+0501 | |
| 1196 | U+0502 | |
| 1197 | U+0503 | |
| 1198 | U+0504 | |
| 1199 | U+0505 | |
| 1200 | U+0506 | |
| 1201 | U+0507 | |
| 1202 | U+0508 | |
| 1203 | U+0509 | |
| 1204 | U+050A | |
| 1205 | U+050B | |
| 1206 | U+050C | |
| 1207 | U+050D | |
| 1208 | U+050E | |
| 1209 | U+050F | |
| 1210 | U+0510 | |
| 1211 | U+0511 | |
| 1212 | U+0512 | |
| 1213 | U+0513 | |
| 1214 | U+0514 | |
| 1215 | U+0515 | |
| 1216 | U+0516 | |
| 1217 | U+0517 | |
| 1218 | U+0518 | |
| 1219 | U+0519 | |
| 1220 | U+051A | |
| 1221 | U+051B | |
| 1222 | U+051C | |
| 1223 | U+051D | |
| 1224 | U+051E | |
| 1225 | U+051F | |
| 1226 | U+0520 | |
| 1227 | U+0521 | |
| 1228 | U+0522 | |
| 1229 | U+0523 | |
| 1230 | U+0524 | |
| 1231 | U+0525 | |
| 1232 | U+0526 | |
| 1233 | U+0527 | |
| 1234 | U+0528 | |
| 1235 | U+0529 | |
| 1236 | U+052A | |
| 1237 | U+052B | |
| 1238 | U+052C | |
| 1239 | U+052D | |
| 1240 | U+052E | |
| 1241 | U+052F | |
| 1242 | U+0531 | |
| 1243 | U+0532 | |
| 1244 | U+0533 | |
| 1245 | U+0534 | |
| 1246 | U+0535 | |
| 1247 | U+0536 | |
| 1248 | U+0537 | |
| 1249 | U+0538 | |
| 1250 | U+0539 | |
| 1251 | U+053A | |
| 1252 | U+053B | |
| 1253 | U+053C | |
| 1254 | U+053D | |
| 1255 | U+053E | |
| 1256 | U+053F | |
| 1257 | U+0540 | |
| 1258 | U+0541 | |
| 1259 | U+0542 | |
| 1260 | U+0543 | |
| 1261 | U+0544 | |
| 1262 | U+0545 | |
| 1263 | U+0546 | |
| 1264 | U+0547 | |
| 1265 | U+0548 | |
| 1266 | U+0549 | |
| 1267 | U+054A | |
| 1268 | U+054B | |
| 1269 | U+054C | |
| 1270 | U+054D | |
| 1271 | U+054E | |
| 1272 | U+054F | |
| 1273 | U+0550 | |
| 1274 | U+0551 | |
| 1275 | U+0552 | |
| 1276 | U+0553 | |
| 1277 | U+0554 | |
| 1278 | U+0555 | |
| 1279 | U+0556 | |
| 1280 | U+0559 | |
| 1281 | U+055A | |
| 1282 | U+055B | |
| 1283 | U+055C | |
| 1284 | U+055D | |
| 1285 | U+055E | |
| 1286 | U+055F | |
| 1287 | U+0560 | |
| 1288 | U+0561 | |
| 1289 | U+0562 | |
| 1290 | U+0563 | |
| 1291 | U+0564 | |
| 1292 | U+0565 | |
| 1293 | U+0566 | |
| 1294 | U+0567 | |
| 1295 | U+0568 | |
| 1296 | U+0569 | |
| 1297 | U+056A | |
| 1298 | U+056B | |
| 1299 | U+056C | |
| 1300 | U+056D | |
| 1301 | U+056E | |
| 1302 | U+056F | |
| 1303 | U+0570 | |
| 1304 | U+0571 | |
| 1305 | U+0572 | |
| 1306 | U+0573 | |
| 1307 | U+0574 | |
| 1308 | U+0575 | |
| 1309 | U+0576 | |
| 1310 | U+0577 | |
| 1311 | U+0578 | |
| 1312 | U+0579 | |
| 1313 | U+057A | |
| 1314 | U+057B | |
| 1315 | U+057C | |
| 1316 | U+057D | |
| 1317 | U+057E | |
| 1318 | U+057F | |
| 1319 | U+0580 | |
| 1320 | U+0581 | |
| 1321 | U+0582 | |
| 1322 | U+0583 | |
| 1323 | U+0584 | |
| 1324 | U+0585 | |
| 1325 | U+0586 | |
| 1326 | U+0587 | |
| 1327 | U+0588 | |
| 1328 | U+0589 | |
| 1329 | U+058A | |
| 1330 | U+058D | |
| 1331 | U+058E | |
| 1332 | U+058F | |
| 1333 | U+0E3F | |
| 1334 | U+10FB | |
| 1335 | U+1AB0 | |
| 1336 | U+1AB1 | |
| 1337 | U+1AB2 | |
| 1338 | U+1AB3 | |
| 1339 | U+1AB4 | |
| 1340 | U+1AB5 | |
| 1341 | U+1AB7 | |
| 1342 | U+1AB8 | |
| 1343 | U+1ABB | |
| 1344 | U+1ABC | |
| 1345 | U+1ABD | |
| 1346 | U+1ABF | |
| 1347 | U+1AC0 | |
| 1348 | U+1AC1 | |
| 1349 | U+1AC2 | |
| 1350 | U+1AC3 | |
| 1351 | U+1AC4 | |
| 1352 | U+1AC5 | |
| 1353 | U+1AC6 | |
| 1354 | U+1AC7 | |
| 1355 | U+1AC8 | |
| 1356 | U+1AC9 | |
| 1357 | U+1ACA | |
| 1358 | U+1ACB | |
| 1359 | U+1ACC | |
| 1360 | U+1ACD | |
| 1361 | U+1ACE | |
| 1362 | U+1ACF | |
| 1363 | U+1AD9 | |
| 1364 | U+1ADA | |
| 1365 | U+1ADB | |
| 1366 | U+1ADC | |
| 1367 | U+1ADD | |
| 1368 | U+1AE0 | |
| 1369 | U+1AE1 | |
| 1370 | U+1AE2 | |
| 1371 | U+1AE3 | |
| 1372 | U+1AE4 | |
| 1373 | U+1AE5 | |
| 1374 | U+1AE6 | |
| 1375 | U+1AE7 | |
| 1376 | U+1AE8 | |
| 1377 | U+1AE9 | |
| 1378 | U+1AEA | |
| 1379 | U+1AEB | |
| 1380 | U+1C80 | |
| 1381 | U+1C81 | |
| 1382 | U+1C82 | |
| 1383 | U+1C83 | |
| 1384 | U+1C84 | |
| 1385 | U+1C85 | |
| 1386 | U+1C86 | |
| 1387 | U+1C87 | |
| 1388 | U+1C88 | |
| 1389 | U+1C89 | |
| 1390 | U+1C8A | |
| 1391 | U+1D00 | |
| 1392 | U+1D01 | |
| 1393 | U+1D02 | |
| 1394 | U+1D03 | |
| 1395 | U+1D04 | |
| 1396 | U+1D05 | |
| 1397 | U+1D06 | |
| 1398 | U+1D07 | |
| 1399 | U+1D08 | |
| 1400 | U+1D09 | |
| 1401 | U+1D0A | |
| 1402 | U+1D0B | |
| 1403 | U+1D0C | |
| 1404 | U+1D0D | |
| 1405 | U+1D0E | |
| 1406 | U+1D0F | |
| 1407 | U+1D10 | |
| 1408 | U+1D11 | |
| 1409 | U+1D12 | |
| 1410 | U+1D13 | |
| 1411 | U+1D14 | |
| 1412 | U+1D15 | |
| 1413 | U+1D16 | |
| 1414 | U+1D17 | |
| 1415 | U+1D18 | |
| 1416 | U+1D19 | |
| 1417 | U+1D1A | |
| 1418 | U+1D1B | |
| 1419 | U+1D1C | |
| 1420 | U+1D1D | |
| 1421 | U+1D1E | |
| 1422 | U+1D1F | |
| 1423 | U+1D20 | |
| 1424 | U+1D21 | |
| 1425 | U+1D22 | |
| 1426 | U+1D23 | |
| 1427 | U+1D24 | |
| 1428 | U+1D25 | |
| 1429 | U+1D26 | |
| 1430 | U+1D27 | |
| 1431 | U+1D28 | |
| 1432 | U+1D29 | |
| 1433 | U+1D2A | |
| 1434 | U+1D2B | |
| 1435 | U+1D2C | |
| 1436 | U+1D2D | |
| 1437 | U+1D2E | |
| 1438 | U+1D2F | |
| 1439 | U+1D30 | |
| 1440 | U+1D31 | |
| 1441 | U+1D32 | |
| 1442 | U+1D33 | |
| 1443 | U+1D34 | |
| 1444 | U+1D35 | |
| 1445 | U+1D36 | |
| 1446 | U+1D37 | |
| 1447 | U+1D38 | |
| 1448 | U+1D39 | |
| 1449 | U+1D3A | |
| 1450 | U+1D3B | |
| 1451 | U+1D3C | |
| 1452 | U+1D3D | |
| 1453 | U+1D3E | |
| 1454 | U+1D3F | |
| 1455 | U+1D40 | |
| 1456 | U+1D41 | |
| 1457 | U+1D42 | |
| 1458 | U+1D43 | |
| 1459 | U+1D44 | |
| 1460 | U+1D45 | |
| 1461 | U+1D46 | |
| 1462 | U+1D47 | |
| 1463 | U+1D48 | |
| 1464 | U+1D49 | |
| 1465 | U+1D4A | |
| 1466 | U+1D4B | |
| 1467 | U+1D4C | |
| 1468 | U+1D4D | |
| 1469 | U+1D4E | |
| 1470 | U+1D4F | |
| 1471 | U+1D50 | |
| 1472 | U+1D51 | |
| 1473 | U+1D52 | |
| 1474 | U+1D53 | |
| 1475 | U+1D54 | |
| 1476 | U+1D55 | |
| 1477 | U+1D56 | |
| 1478 | U+1D57 | |
| 1479 | U+1D58 | |
| 1480 | U+1D59 | |
| 1481 | U+1D5A | |
| 1482 | U+1D5B | |
| 1483 | U+1D5C | |
| 1484 | U+1D5D | |
| 1485 | U+1D5E | |
| 1486 | U+1D5F | |
| 1487 | U+1D60 | |
| 1488 | U+1D61 | |
| 1489 | U+1D62 | |
| 1490 | U+1D63 | |
| 1491 | U+1D64 | |
| 1492 | U+1D65 | |
| 1493 | U+1D66 | |
| 1494 | U+1D67 | |
| 1495 | U+1D68 | |
| 1496 | U+1D69 | |
| 1497 | U+1D6A | |
| 1498 | U+1D6B | |
| 1499 | U+1D6C | |
| 1500 | U+1D6D | |
| 1501 | U+1D6E | |
| 1502 | U+1D6F | |
| 1503 | U+1D70 | |
| 1504 | U+1D71 | |
| 1505 | U+1D72 | |
| 1506 | U+1D73 | |
| 1507 | U+1D74 | |
| 1508 | U+1D75 | |
| 1509 | U+1D76 | |
| 1510 | U+1D77 | |
| 1511 | U+1D78 | |
| 1512 | U+1D79 | |
| 1513 | U+1D7A | |
| 1514 | U+1D7B | |
| 1515 | U+1D7C | |
| 1516 | U+1D7D | |
| 1517 | U+1D7E | |
| 1518 | U+1D7F | |
| 1519 | U+1D80 | |
| 1520 | U+1D81 | |
| 1521 | U+1D82 | |
| 1522 | U+1D83 | |
| 1523 | U+1D84 | |
| 1524 | U+1D85 | |
| 1525 | U+1D86 | |
| 1526 | U+1D87 | |
| 1527 | U+1D88 | |
| 1528 | U+1D89 | |
| 1529 | U+1D8A | |
| 1530 | U+1D8B | |
| 1531 | U+1D8C | |
| 1532 | U+1D8D | |
| 1533 | U+1D8E | |
| 1534 | U+1D8F | |
| 1535 | U+1D90 | |
| 1536 | U+1D91 | |
| 1537 | U+1D92 | |
| 1538 | U+1D93 | |
| 1539 | U+1D94 | |
| 1540 | U+1D95 | |
| 1541 | U+1D96 | |
| 1542 | U+1D97 | |
| 1543 | U+1D98 | |
| 1544 | U+1D99 | |
| 1545 | U+1D9A | |
| 1546 | U+1D9B | |
| 1547 | U+1D9C | |
| 1548 | U+1D9D | |
| 1549 | U+1D9E | |
| 1550 | U+1D9F | |
| 1551 | U+1DA0 | |
| 1552 | U+1DA1 | |
| 1553 | U+1DA2 | |
| 1554 | U+1DA3 | |
| 1555 | U+1DA4 | |
| 1556 | U+1DA5 | |
| 1557 | U+1DA6 | |
| 1558 | U+1DA7 | |
| 1559 | U+1DA8 | |
| 1560 | U+1DA9 | |
| 1561 | U+1DAA | |
| 1562 | U+1DAB | |
| 1563 | U+1DAC | |
| 1564 | U+1DAD | |
| 1565 | U+1DAE | |
| 1566 | U+1DAF | |
| 1567 | U+1DB0 | |
| 1568 | U+1DB1 | |
| 1569 | U+1DB2 | |
| 1570 | U+1DB3 | |
| 1571 | U+1DB4 | |
| 1572 | U+1DB5 | |
| 1573 | U+1DB6 | |
| 1574 | U+1DB7 | |
| 1575 | U+1DB8 | |
| 1576 | U+1DB9 | |
| 1577 | U+1DBA | |
| 1578 | U+1DBB | |
| 1579 | U+1DBC | |
| 1580 | U+1DBD | |
| 1581 | U+1DBE | |
| 1582 | U+1DBF | |
| 1583 | U+1DC3 | |
| 1584 | U+1DC4 | |
| 1585 | U+1DC5 | |
| 1586 | U+1DC6 | |
| 1587 | U+1DC7 | |
| 1588 | U+1DCA | |
| 1589 | U+1DCB | |
| 1590 | U+1DCC | |
| 1591 | U+1DCD | |
| 1592 | U+1DCE | |
| 1593 | U+1DCF | |
| 1594 | U+1DD3 | |
| 1595 | U+1DD4 | |
| 1596 | U+1DD5 | |
| 1597 | U+1DD7 | |
| 1598 | U+1DD8 | |
| 1599 | U+1DD9 | |
| 1600 | U+1DDA | |
| 1601 | U+1DDB | |
| 1602 | U+1DDC | |
| 1603 | U+1DDD | |
| 1604 | U+1DDE | |
| 1605 | U+1DDF | |
| 1606 | U+1DE0 | |
| 1607 | U+1DE1 | |
| 1608 | U+1DE2 | |
| 1609 | U+1DE3 | |
| 1610 | U+1DE4 | |
| 1611 | U+1DE5 | |
| 1612 | U+1DE6 | |
| 1613 | U+1DE7 | |
| 1614 | U+1DE8 | |
| 1615 | U+1DE9 | |
| 1616 | U+1DEA | |
| 1617 | U+1DEB | |
| 1618 | U+1DEC | |
| 1619 | U+1DED | |
| 1620 | U+1DEE | |
| 1621 | U+1DEF | |
| 1622 | U+1DF0 | |
| 1623 | U+1DF1 | |
| 1624 | U+1DF2 | |
| 1625 | U+1DF3 | |
| 1626 | U+1DF4 | |
| 1627 | U+1DF5 | |
| 1628 | U+1DF6 | |
| 1629 | U+1DF7 | |
| 1630 | U+1DF8 | |
| 1631 | U+1DF9 | |
| 1632 | U+1DFA | |
| 1633 | U+1DFB | |
| 1634 | U+1DFC | |
| 1635 | U+1DFD | |
| 1636 | U+1DFE | |
| 1637 | U+1DFF | |
| 1638 | U+1E00 | |
| 1639 | U+1E01 | |
| 1640 | U+1E02 | |
| 1641 | U+1E03 | |
| 1642 | U+1E04 | |
| 1643 | U+1E05 | |
| 1644 | U+1E06 | |
| 1645 | U+1E07 | |
| 1646 | U+1E08 | |
| 1647 | U+1E09 | |
| 1648 | U+1E0A | |
| 1649 | U+1E0B | |
| 1650 | U+1E0C | |
| 1651 | U+1E0D | |
| 1652 | U+1E0E | |
| 1653 | U+1E0F | |
| 1654 | U+1E10 | |
| 1655 | U+1E11 | |
| 1656 | U+1E12 | |
| 1657 | U+1E13 | |
| 1658 | U+1E14 | |
| 1659 | U+1E15 | |
| 1660 | U+1E16 | |
| 1661 | U+1E17 | |
| 1662 | U+1E18 | |
| 1663 | U+1E19 | |
| 1664 | U+1E1A | |
| 1665 | U+1E1B | |
| 1666 | U+1E1C | |
| 1667 | U+1E1D | |
| 1668 | U+1E1E | |
| 1669 | U+1E1F | |
| 1670 | U+1E20 | |
| 1671 | U+1E21 | |
| 1672 | U+1E22 | |
| 1673 | U+1E23 | |
| 1674 | U+1E24 | |
| 1675 | U+1E25 | |
| 1676 | U+1E26 | |
| 1677 | U+1E27 | |
| 1678 | U+1E28 | |
| 1679 | U+1E29 | |
| 1680 | U+1E2A | |
| 1681 | U+1E2B | |
| 1682 | U+1E2C | |
| 1683 | U+1E2D | |
| 1684 | U+1E2E | |
| 1685 | U+1E2F | |
| 1686 | U+1E30 | |
| 1687 | U+1E31 | |
| 1688 | U+1E32 | |
| 1689 | U+1E33 | |
| 1690 | U+1E34 | |
| 1691 | U+1E35 | |
| 1692 | U+1E36 | |
| 1693 | U+1E37 | |
| 1694 | U+1E38 | |
| 1695 | U+1E39 | |
| 1696 | U+1E3A | |
| 1697 | U+1E3B | |
| 1698 | U+1E3C | |
| 1699 | U+1E3D | |
| 1700 | U+1E3E | |
| 1701 | U+1E3F | |
| 1702 | U+1E40 | |
| 1703 | U+1E41 | |
| 1704 | U+1E42 | |
| 1705 | U+1E43 | |
| 1706 | U+1E44 | |
| 1707 | U+1E45 | |
| 1708 | U+1E46 | |
| 1709 | U+1E47 | |
| 1710 | U+1E48 | |
| 1711 | U+1E49 | |
| 1712 | U+1E4A | |
| 1713 | U+1E4B | |
| 1714 | U+1E4C | |
| 1715 | U+1E4D | |
| 1716 | U+1E4E | |
| 1717 | U+1E4F | |
| 1718 | U+1E50 | |
| 1719 | U+1E51 | |
| 1720 | U+1E52 | |
| 1721 | U+1E53 | |
| 1722 | U+1E54 | |
| 1723 | U+1E55 | |
| 1724 | U+1E56 | |
| 1725 | U+1E57 | |
| 1726 | U+1E58 | |
| 1727 | U+1E59 | |
| 1728 | U+1E5A | |
| 1729 | U+1E5B | |
| 1730 | U+1E5C | |
| 1731 | U+1E5D | |
| 1732 | U+1E5E | |
| 1733 | U+1E5F | |
| 1734 | U+1E60 | |
| 1735 | U+1E61 | |
| 1736 | U+1E62 | |
| 1737 | U+1E63 | |
| 1738 | U+1E64 | |
| 1739 | U+1E65 | |
| 1740 | U+1E66 | |
| 1741 | U+1E67 | |
| 1742 | U+1E68 | |
| 1743 | U+1E69 | |
| 1744 | U+1E6A | |
| 1745 | U+1E6B | |
| 1746 | U+1E6C | |
| 1747 | U+1E6D | |
| 1748 | U+1E6E | |
| 1749 | U+1E6F | |
| 1750 | U+1E70 | |
| 1751 | U+1E71 | |
| 1752 | U+1E72 | |
| 1753 | U+1E73 | |
| 1754 | U+1E74 | |
| 1755 | U+1E75 | |
| 1756 | U+1E76 | |
| 1757 | U+1E77 | |
| 1758 | U+1E78 | |
| 1759 | U+1E79 | |
| 1760 | U+1E7A | |
| 1761 | U+1E7B | |
| 1762 | U+1E7C | |
| 1763 | U+1E7D | |
| 1764 | U+1E7E | |
| 1765 | U+1E7F | |
| 1766 | U+1E80 | |
| 1767 | U+1E81 | |
| 1768 | U+1E82 | |
| 1769 | U+1E83 | |
| 1770 | U+1E84 | |
| 1771 | U+1E85 | |
| 1772 | U+1E86 | |
| 1773 | U+1E87 | |
| 1774 | U+1E88 | |
| 1775 | U+1E89 | |
| 1776 | U+1E8A | |
| 1777 | U+1E8B | |
| 1778 | U+1E8C | |
| 1779 | U+1E8D | |
| 1780 | U+1E8E | |
| 1781 | U+1E8F | |
| 1782 | U+1E90 | |
| 1783 | U+1E91 | |
| 1784 | U+1E92 | |
| 1785 | U+1E93 | |
| 1786 | U+1E94 | |
| 1787 | U+1E95 | |
| 1788 | U+1E96 | |
| 1789 | U+1E97 | |
| 1790 | U+1E98 | |
| 1791 | U+1E99 | |
| 1792 | U+1E9A | |
| 1793 | U+1E9B | |
| 1794 | U+1E9C | |
| 1795 | U+1E9D | |
| 1796 | U+1E9E | |
| 1797 | U+1E9F | |
| 1798 | U+1EA0 | |
| 1799 | U+1EA1 | |
| 1800 | U+1EA2 | |
| 1801 | U+1EA3 | |
| 1802 | U+1EA4 | |
| 1803 | U+1EA5 | |
| 1804 | U+1EA6 | |
| 1805 | U+1EA7 | |
| 1806 | U+1EA8 | |
| 1807 | U+1EA9 | |
| 1808 | U+1EAA | |
| 1809 | U+1EAB | |
| 1810 | U+1EAC | |
| 1811 | U+1EAD | |
| 1812 | U+1EAE | |
| 1813 | U+1EAF | |
| 1814 | U+1EB0 | |
| 1815 | U+1EB1 | |
| 1816 | U+1EB2 | |
| 1817 | U+1EB3 | |
| 1818 | U+1EB4 | |
| 1819 | U+1EB5 | |
| 1820 | U+1EB6 | |
| 1821 | U+1EB7 | |
| 1822 | U+1EB8 | |
| 1823 | U+1EB9 | |
| 1824 | U+1EBA | |
| 1825 | U+1EBB | |
| 1826 | U+1EBC | |
| 1827 | U+1EBD | |
| 1828 | U+1EBE | |
| 1829 | U+1EBF | |
| 1830 | U+1EC0 | |
| 1831 | U+1EC1 | |
| 1832 | U+1EC2 | |
| 1833 | U+1EC3 | |
| 1834 | U+1EC4 | |
| 1835 | U+1EC5 | |
| 1836 | U+1EC6 | |
| 1837 | U+1EC7 | |
| 1838 | U+1EC8 | |
| 1839 | U+1EC9 | |
| 1840 | U+1ECA | |
| 1841 | U+1ECB | |
| 1842 | U+1ECC | |
| 1843 | U+1ECD | |
| 1844 | U+1ECE | |
| 1845 | U+1ECF | |
| 1846 | U+1ED0 | |
| 1847 | U+1ED1 | |
| 1848 | U+1ED2 | |
| 1849 | U+1ED3 | |
| 1850 | U+1ED4 | |
| 1851 | U+1ED5 | |
| 1852 | U+1ED6 | |
| 1853 | U+1ED7 | |
| 1854 | U+1ED8 | |
| 1855 | U+1ED9 | |
| 1856 | U+1EDA | |
| 1857 | U+1EDB | |
| 1858 | U+1EDC | |
| 1859 | U+1EDD | |
| 1860 | U+1EDE | |
| 1861 | U+1EDF | |
| 1862 | U+1EE0 | |
| 1863 | U+1EE1 | |
| 1864 | U+1EE2 | |
| 1865 | U+1EE3 | |
| 1866 | U+1EE4 | |
| 1867 | U+1EE5 | |
| 1868 | U+1EE6 | |
| 1869 | U+1EE7 | |
| 1870 | U+1EE8 | |
| 1871 | U+1EE9 | |
| 1872 | U+1EEA | |
| 1873 | U+1EEB | |
| 1874 | U+1EEC | |
| 1875 | U+1EED | |
| 1876 | U+1EEE | |
| 1877 | U+1EEF | |
| 1878 | U+1EF0 | |
| 1879 | U+1EF1 | |
| 1880 | U+1EF2 | |
| 1881 | U+1EF3 | |
| 1882 | U+1EF4 | |
| 1883 | U+1EF5 | |
| 1884 | U+1EF6 | |
| 1885 | U+1EF7 | |
| 1886 | U+1EF8 | |
| 1887 | U+1EF9 | |
| 1888 | U+1EFA | |
| 1889 | U+1EFB | |
| 1890 | U+1EFC | |
| 1891 | U+1EFD | |
| 1892 | U+1EFE | |
| 1893 | U+1EFF | |
| 1894 | U+1F00 | |
| 1895 | U+1F01 | |
| 1896 | U+1F02 | |
| 1897 | U+1F03 | |
| 1898 | U+1F04 | |
| 1899 | U+1F05 | |
| 1900 | U+1F06 | |
| 1901 | U+1F07 | |
| 1902 | U+1F08 | |
| 1903 | U+1F09 | |
| 1904 | U+1F0A | |
| 1905 | U+1F0B | |
| 1906 | U+1F0C | |
| 1907 | U+1F0D | |
| 1908 | U+1F0E | |
| 1909 | U+1F0F | |
| 1910 | U+1F10 | |
| 1911 | U+1F11 | |
| 1912 | U+1F12 | |
| 1913 | U+1F13 | |
| 1914 | U+1F14 | |
| 1915 | U+1F15 | |
| 1916 | U+1F18 | |
| 1917 | U+1F19 | |
| 1918 | U+1F1A | |
| 1919 | U+1F1B | |
| 1920 | U+1F1C | |
| 1921 | U+1F1D | |
| 1922 | U+1F20 | |
| 1923 | U+1F21 | |
| 1924 | U+1F22 | |
| 1925 | U+1F23 | |
| 1926 | U+1F24 | |
| 1927 | U+1F25 | |
| 1928 | U+1F26 | |
| 1929 | U+1F27 | |
| 1930 | U+1F28 | |
| 1931 | U+1F29 | |
| 1932 | U+1F2A | |
| 1933 | U+1F2B | |
| 1934 | U+1F2C | |
| 1935 | U+1F2D | |
| 1936 | U+1F2E | |
| 1937 | U+1F2F | |
| 1938 | U+1F30 | |
| 1939 | U+1F31 | |
| 1940 | U+1F32 | |
| 1941 | U+1F33 | |
| 1942 | U+1F34 | |
| 1943 | U+1F35 | |
| 1944 | U+1F36 | |
| 1945 | U+1F37 | |
| 1946 | U+1F38 | |
| 1947 | U+1F39 | |
| 1948 | U+1F3A | |
| 1949 | U+1F3B | |
| 1950 | U+1F3C | |
| 1951 | U+1F3D | |
| 1952 | U+1F3E | |
| 1953 | U+1F3F | |
| 1954 | U+1F40 | |
| 1955 | U+1F41 | |
| 1956 | U+1F42 | |
| 1957 | U+1F43 | |
| 1958 | U+1F44 | |
| 1959 | U+1F45 | |
| 1960 | U+1F48 | |
| 1961 | U+1F49 | |
| 1962 | U+1F4A | |
| 1963 | U+1F4B | |
| 1964 | U+1F4C | |
| 1965 | U+1F4D | |
| 1966 | U+1F50 | |
| 1967 | U+1F51 | |
| 1968 | U+1F52 | |
| 1969 | U+1F53 | |
| 1970 | U+1F54 | |
| 1971 | U+1F55 | |
| 1972 | U+1F56 | |
| 1973 | U+1F57 | |
| 1974 | U+1F59 | |
| 1975 | U+1F5B | |
| 1976 | U+1F5D | |
| 1977 | U+1F5F | |
| 1978 | U+1F60 | |
| 1979 | U+1F61 | |
| 1980 | U+1F62 | |
| 1981 | U+1F63 | |
| 1982 | U+1F64 | |
| 1983 | U+1F65 | |
| 1984 | U+1F66 | |
| 1985 | U+1F67 | |
| 1986 | U+1F68 | |
| 1987 | U+1F69 | |
| 1988 | U+1F6A | |
| 1989 | U+1F6B | |
| 1990 | U+1F6C | |
| 1991 | U+1F6D | |
| 1992 | U+1F6E | |
| 1993 | U+1F6F | |
| 1994 | U+1F70 | |
| 1995 | U+1F71 | |
| 1996 | U+1F72 | |
| 1997 | U+1F73 | |
| 1998 | U+1F74 | |
| 1999 | U+1F75 | |
| 2000 | U+1F76 | |
| 2001 | U+1F77 | |
| 2002 | U+1F78 | |
| 2003 | U+1F79 | |
| 2004 | U+1F7A | |
| 2005 | U+1F7B | |
| 2006 | U+1F7C | |
| 2007 | U+1F7D | |
| 2008 | U+1F80 | |
| 2009 | U+1F81 | |
| 2010 | U+1F82 | |
| 2011 | U+1F83 | |
| 2012 | U+1F84 | |
| 2013 | U+1F85 | |
| 2014 | U+1F86 | |
| 2015 | U+1F87 | |
| 2016 | U+1F88 | |
| 2017 | U+1F89 | |
| 2018 | U+1F8A | |
| 2019 | U+1F8B | |
| 2020 | U+1F8C | |
| 2021 | U+1F8D | |
| 2022 | U+1F8E | |
| 2023 | U+1F8F | |
| 2024 | U+1F90 | |
| 2025 | U+1F91 | |
| 2026 | U+1F92 | |
| 2027 | U+1F93 | |
| 2028 | U+1F94 | |
| 2029 | U+1F95 | |
| 2030 | U+1F96 | |
| 2031 | U+1F97 | |
| 2032 | U+1F98 | |
| 2033 | U+1F99 | |
| 2034 | U+1F9A | |
| 2035 | U+1F9B | |
| 2036 | U+1F9C | |
| 2037 | U+1F9D | |
| 2038 | U+1F9E | |
| 2039 | U+1F9F | |
| 2040 | U+1FA0 | |
| 2041 | U+1FA1 | |
| 2042 | U+1FA2 | |
| 2043 | U+1FA3 | |
| 2044 | U+1FA4 | |
| 2045 | U+1FA5 | |
| 2046 | U+1FA6 | |
| 2047 | U+1FA7 | |
| 2048 | U+1FA8 | |
| 2049 | U+1FA9 | |
| 2050 | U+1FAA | |
| 2051 | U+1FAB | |
| 2052 | U+1FAC | |
| 2053 | U+1FAD | |
| 2054 | U+1FAE | |
| 2055 | U+1FAF | |
| 2056 | U+1FB0 | |
| 2057 | U+1FB1 | |
| 2058 | U+1FB2 | |
| 2059 | U+1FB3 | |
| 2060 | U+1FB4 | |
| 2061 | U+1FB6 | |
| 2062 | U+1FB7 | |
| 2063 | U+1FB8 | |
| 2064 | U+1FB9 | |
| 2065 | U+1FBA | |
| 2066 | U+1FBB | |
| 2067 | U+1FBC | |
| 2068 | U+1FBD | |
| 2069 | U+1FBE | |
| 2070 | U+1FBF | |
| 2071 | U+1FC0 | |
| 2072 | U+1FC1 | |
| 2073 | U+1FC2 | |
| 2074 | U+1FC3 | |
| 2075 | U+1FC4 | |
| 2076 | U+1FC6 | |
| 2077 | U+1FC7 | |
| 2078 | U+1FC8 | |
| 2079 | U+1FC9 | |
| 2080 | U+1FCA | |
| 2081 | U+1FCB | |
| 2082 | U+1FCC | |
| 2083 | U+1FCD | |
| 2084 | U+1FCE | |
| 2085 | U+1FCF | |
| 2086 | U+1FD0 | |
| 2087 | U+1FD1 | |
| 2088 | U+1FD2 | |
| 2089 | U+1FD3 | |
| 2090 | U+1FD6 | |
| 2091 | U+1FD7 | |
| 2092 | U+1FD8 | |
| 2093 | U+1FD9 | |
| 2094 | U+1FDA | |
| 2095 | U+1FDB | |
| 2096 | U+1FDD | |
| 2097 | U+1FDE | |
| 2098 | U+1FDF | |
| 2099 | U+1FE0 | |
| 2100 | U+1FE1 | |
| 2101 | U+1FE2 | |
| 2102 | U+1FE3 | |
| 2103 | U+1FE4 | |
| 2104 | U+1FE5 | |
| 2105 | U+1FE6 | |
| 2106 | U+1FE7 | |
| 2107 | U+1FE8 | |
| 2108 | U+1FE9 | |
| 2109 | U+1FEA | |
| 2110 | U+1FEB | |
| 2111 | U+1FEC | |
| 2112 | U+1FED | |
| 2113 | U+1FEE | |
| 2114 | U+1FEF | |
| 2115 | U+1FF2 | |
| 2116 | U+1FF3 | |
| 2117 | U+1FF4 | |
| 2118 | U+1FF6 | |
| 2119 | U+1FF7 | |
| 2120 | U+1FF8 | |
| 2121 | U+1FF9 | |
| 2122 | U+1FFA | |
| 2123 | U+1FFB | |
| 2124 | U+1FFC | |
| 2125 | U+1FFD | |
| 2126 | U+1FFE | |
| 2127 | U+2000 | |
| 2128 | U+2001 | |
| 2129 | U+2002 | |
| 2130 | U+2003 | |
| 2131 | U+2004 | |
| 2132 | U+2005 | |
| 2133 | U+2006 | |
| 2134 | U+2007 | |
| 2135 | U+2008 | |
| 2136 | U+2009 | |
| 2137 | U+200A | |
| 2138 | U+200B | |
| 2139 | U+200C | |
| 2140 | U+200D | |
| 2141 | U+2010 | |
| 2142 | U+2011 | |
| 2143 | U+2012 | |
| 2144 | U+2013 | |
| 2145 | U+2014 | |
| 2146 | U+2015 | |
| 2147 | U+2016 | |
| 2148 | U+2017 | |
| 2149 | U+2018 | |
| 2150 | U+2019 | |
| 2151 | U+201A | |
| 2152 | U+201B | |
| 2153 | U+201C | |
| 2154 | U+201D | |
| 2155 | U+201E | |
| 2156 | U+201F | |
| 2157 | U+2020 | |
| 2158 | U+2021 | |
| 2159 | U+2022 | |
| 2160 | U+2023 | |
| 2161 | U+2024 | |
| 2162 | U+2025 | |
| 2163 | U+2026 | |
| 2164 | U+2027 | |
| 2165 | U+2028 | |
| 2166 | U+2029 | |
| 2167 | U+202F | |
| 2168 | U+2030 | |
| 2169 | U+2031 | |
| 2170 | U+2032 | |
| 2171 | U+2033 | |
| 2172 | U+2034 | |
| 2173 | U+2035 | |
| 2174 | U+2036 | |
| 2175 | U+2037 | |
| 2176 | U+2038 | |
| 2177 | U+2039 | |
| 2178 | U+203A | |
| 2179 | U+203B | |
| 2180 | U+203C | |
| 2181 | U+203D | |
| 2182 | U+203E | |
| 2183 | U+203F | |
| 2184 | U+2040 | |
| 2185 | U+2041 | |
| 2186 | U+2042 | |
| 2187 | U+2043 | |
| 2188 | U+2044 | |
| 2189 | U+2045 | |
| 2190 | U+2046 | |
| 2191 | U+2047 | |
| 2192 | U+2048 | |
| 2193 | U+2049 | |
| 2194 | U+204A | |
| 2195 | U+204B | |
| 2196 | U+204C | |
| 2197 | U+204D | |
| 2198 | U+204E | |
| 2199 | U+204F | |
| 2200 | U+2050 | |
| 2201 | U+2051 | |
| 2202 | U+2052 | |
| 2203 | U+2053 | |
| 2204 | U+2054 | |
| 2205 | U+2055 | |
| 2206 | U+2056 | |
| 2207 | U+2057 | |
| 2208 | U+2058 | |
| 2209 | U+2059 | |
| 2210 | U+205A | |
| 2211 | U+205B | |
| 2212 | U+205C | |
| 2213 | U+205D | |
| 2214 | U+205E | |
| 2215 | U+205F | |
| 2216 | U+2060 | |
| 2217 | U+2061 | |
| 2218 | U+2062 | |
| 2219 | U+2063 | |
| 2220 | U+2064 | |
| 2221 | U+2070 | |
| 2222 | U+2071 | |
| 2223 | U+2074 | |
| 2224 | U+2075 | |
| 2225 | U+2076 | |
| 2226 | U+2077 | |
| 2227 | U+2078 | |
| 2228 | U+2079 | |
| 2229 | U+207A | |
| 2230 | U+207B | |
| 2231 | U+207C | |
| 2232 | U+207D | |
| 2233 | U+207E | |
| 2234 | U+207F | |
| 2235 | U+2080 | |
| 2236 | U+2081 | |
| 2237 | U+2082 | |
| 2238 | U+2083 | |
| 2239 | U+2084 | |
| 2240 | U+2085 | |
| 2241 | U+2086 | |
| 2242 | U+2087 | |
| 2243 | U+2088 | |
| 2244 | U+2089 | |
| 2245 | U+208A | |
| 2246 | U+208B | |
| 2247 | U+208C | |
| 2248 | U+208D | |
| 2249 | U+208E | |
| 2250 | U+2090 | |
| 2251 | U+2091 | |
| 2252 | U+2092 | |
| 2253 | U+2093 | |
| 2254 | U+2094 | |
| 2255 | U+2095 | |
| 2256 | U+2096 | |
| 2257 | U+2097 | |
| 2258 | U+2098 | |
| 2259 | U+2099 | |
| 2260 | U+209A | |
| 2261 | U+209B | |
| 2262 | U+209C | |
| 2263 | U+20A1 | |
| 2264 | U+20A3 | |
| 2265 | U+20A4 | |
| 2266 | U+20A5 | |
| 2267 | U+20A6 | |
| 2268 | U+20A7 | |
| 2269 | U+20A8 | |
| 2270 | U+20A9 | |
| 2271 | U+20AA | |
| 2272 | U+20AB | |
| 2273 | U+20AC | |
| 2274 | U+20AD | |
| 2275 | U+20AE | |
| 2276 | U+20AF | |
| 2277 | U+20B1 | |
| 2278 | U+20B2 | |
| 2279 | U+20B3 | |
| 2280 | U+20B4 | |
| 2281 | U+20B5 | |
| 2282 | U+20B8 | |
| 2283 | U+20B9 | |
| 2284 | U+20BA | |
| 2285 | U+20BC | |
| 2286 | U+20BD | |
| 2287 | U+20BE | |
| 2288 | U+20BF | |
| 2289 | U+20C0 | |
| 2290 | U+20D0 | |
| 2291 | U+20D1 | |
| 2292 | U+20D2 | |
| 2293 | U+20D3 | |
| 2294 | U+20D6 | |
| 2295 | U+20D7 | |
| 2296 | U+20D8 | |
| 2297 | U+20DB | |
| 2298 | U+20DC | |
| 2299 | U+20DD | |
| 2300 | U+20E1 | |
| 2301 | U+20E5 | |
| 2302 | U+20E6 | |
| 2303 | U+20E8 | |
| 2304 | U+20E9 | |
| 2305 | U+20EB | |
| 2306 | U+20EC | |
| 2307 | U+20ED | |
| 2308 | U+20EE | |
| 2309 | U+20EF | |
| 2310 | U+20F0 | |
| 2311 | U+2100 | |
| 2312 | U+2101 | |
| 2313 | U+2102 | |
| 2314 | U+2103 | |
| 2315 | U+2105 | |
| 2316 | U+2106 | |
| 2317 | U+2107 | |
| 2318 | U+2108 | |
| 2319 | U+2109 | |
| 2320 | U+210C | |
| 2321 | U+210D | |
| 2322 | U+210E | |
| 2323 | U+210F | |
| 2324 | U+2111 | |
| 2325 | U+2113 | |
| 2326 | U+2115 | |
| 2327 | U+2116 | |
| 2328 | U+2117 | |
| 2329 | U+2118 | |
| 2330 | U+2119 | |
| 2331 | U+211A | |
| 2332 | U+211C | |
| 2333 | U+211D | |
| 2334 | U+2120 | |
| 2335 | U+2121 | |
| 2336 | U+2122 | |
| 2337 | U+2124 | |
| 2338 | U+2126 | |
| 2339 | U+2127 | |
| 2340 | U+2128 | |
| 2341 | U+2129 | |
| 2342 | U+212A | |
| 2343 | U+212B | |
| 2344 | U+212D | |
| 2345 | U+212E | |
| 2346 | U+2132 | |
| 2347 | U+2135 | |
| 2348 | U+2136 | |
| 2349 | U+2137 | |
| 2350 | U+2138 | |
| 2351 | U+2139 | |
| 2352 | U+213A | |
| 2353 | U+213B | |
| 2354 | U+213C | |
| 2355 | U+213D | |
| 2356 | U+213E | |
| 2357 | U+213F | |
| 2358 | U+2140 | |
| 2359 | U+2141 | |
| 2360 | U+2142 | |
| 2361 | U+2143 | |
| 2362 | U+2144 | |
| 2363 | U+2145 | |
| 2364 | U+2146 | |
| 2365 | U+2147 | |
| 2366 | U+2148 | |
| 2367 | U+2149 | |
| 2368 | U+214B | |
| 2369 | U+214D | |
| 2370 | U+214E | |
| 2371 | U+214F | |
| 2372 | U+2150 | |
| 2373 | U+2151 | |
| 2374 | U+2152 | |
| 2375 | U+2153 | |
| 2376 | U+2154 | |
| 2377 | U+2155 | |
| 2378 | U+2156 | |
| 2379 | U+2157 | |
| 2380 | U+2158 | |
| 2381 | U+2159 | |
| 2382 | U+215A | |
| 2383 | U+215B | |
| 2384 | U+215C | |
| 2385 | U+215D | |
| 2386 | U+215E | |
| 2387 | U+215F | |
| 2388 | U+2160 | |
| 2389 | U+2161 | |
| 2390 | U+2162 | |
| 2391 | U+2163 | |
| 2392 | U+2164 | |
| 2393 | U+2165 | |
| 2394 | U+2166 | |
| 2395 | U+2167 | |
| 2396 | U+2168 | |
| 2397 | U+2169 | |
| 2398 | U+216A | |
| 2399 | U+216B | |
| 2400 | U+216C | |
| 2401 | U+216D | |
| 2402 | U+216E | |
| 2403 | U+216F | |
| 2404 | U+2170 | |
| 2405 | U+2171 | |
| 2406 | U+2172 | |
| 2407 | U+2173 | |
| 2408 | U+2174 | |
| 2409 | U+2175 | |
| 2410 | U+2176 | |
| 2411 | U+2177 | |
| 2412 | U+2178 | |
| 2413 | U+2179 | |
| 2414 | U+217A | |
| 2415 | U+217B | |
| 2416 | U+217C | |
| 2417 | U+217D | |
| 2418 | U+217E | |
| 2419 | U+217F | |
| 2420 | U+2180 | |
| 2421 | U+2181 | |
| 2422 | U+2182 | |
| 2423 | U+2183 | |
| 2424 | U+2184 | |
| 2425 | U+2185 | |
| 2426 | U+2186 | |
| 2427 | U+2187 | |
| 2428 | U+2188 | |
| 2429 | U+2189 | |
| 2430 | U+218A | |
| 2431 | U+218B | |
| 2432 | U+2190 | |
| 2433 | U+2191 | |
| 2434 | U+2192 | |
| 2435 | U+2193 | |
| 2436 | U+2194 | |
| 2437 | U+2195 | |
| 2438 | U+2196 | |
| 2439 | U+2197 | |
| 2440 | U+2198 | |
| 2441 | U+2199 | |
| 2442 | U+219A | |
| 2443 | U+219B | |
| 2444 | U+219C | |
| 2445 | U+219D | |
| 2446 | U+219E | |
| 2447 | U+219F | |
| 2448 | U+21A0 | |
| 2449 | U+21A1 | |
| 2450 | U+21A2 | |
| 2451 | U+21A3 | |
| 2452 | U+21A4 | |
| 2453 | U+21A5 | |
| 2454 | U+21A6 | |
| 2455 | U+21A7 | |
| 2456 | U+21A8 | |
| 2457 | U+21A9 | |
| 2458 | U+21AA | |
| 2459 | U+21AB | |
| 2460 | U+21AC | |
| 2461 | U+21AD | |
| 2462 | U+21AE | |
| 2463 | U+21AF | |
| 2464 | U+21B0 | |
| 2465 | U+21B1 | |
| 2466 | U+21B2 | |
| 2467 | U+21B3 | |
| 2468 | U+21B4 | |
| 2469 | U+21B5 | |
| 2470 | U+21B6 | |
| 2471 | U+21B7 | |
| 2472 | U+21B8 | |
| 2473 | U+21B9 | |
| 2474 | U+21BA | |
| 2475 | U+21BB | |
| 2476 | U+21BC | |
| 2477 | U+21BD | |
| 2478 | U+21BE | |
| 2479 | U+21BF | |
| 2480 | U+21C0 | |
| 2481 | U+21C1 | |
| 2482 | U+21C2 | |
| 2483 | U+21C3 | |
| 2484 | U+21C4 | |
| 2485 | U+21C5 | |
| 2486 | U+21C6 | |
| 2487 | U+21C7 | |
| 2488 | U+21C8 | |
| 2489 | U+21C9 | |
| 2490 | U+21CA | |
| 2491 | U+21CB | |
| 2492 | U+21CC | |
| 2493 | U+21CD | |
| 2494 | U+21CE | |
| 2495 | U+21CF | |
| 2496 | U+21D0 | |
| 2497 | U+21D1 | |
| 2498 | U+21D2 | |
| 2499 | U+21D3 | |
| 2500 | U+21D4 | |
| 2501 | U+21D5 | |
| 2502 | U+21D6 | |
| 2503 | U+21D7 | |
| 2504 | U+21D8 | |
| 2505 | U+21D9 | |
| 2506 | U+21DA | |
| 2507 | U+21DB | |
| 2508 | U+21DC | |
| 2509 | U+21DD | |
| 2510 | U+21DE | |
| 2511 | U+21DF | |
| 2512 | U+21E0 | |
| 2513 | U+21E1 | |
| 2514 | U+21E2 | |
| 2515 | U+21E3 | |
| 2516 | U+21E4 | |
| 2517 | U+21E5 | |
| 2518 | U+21E6 | |
| 2519 | U+21E7 | |
| 2520 | U+21E8 | |
| 2521 | U+21E9 | |
| 2522 | U+21EA | |
| 2523 | U+21EB | |
| 2524 | U+21EC | |
| 2525 | U+21ED | |
| 2526 | U+21EE | |
| 2527 | U+21EF | |
| 2528 | U+21F0 | |
| 2529 | U+21F1 | |
| 2530 | U+21F2 | |
| 2531 | U+21F3 | |
| 2532 | U+21F4 | |
| 2533 | U+21F5 | |
| 2534 | U+21F6 | |
| 2535 | U+21F7 | |
| 2536 | U+21F8 | |
| 2537 | U+21F9 | |
| 2538 | U+21FA | |
| 2539 | U+21FB | |
| 2540 | U+21FC | |
| 2541 | U+21FD | |
| 2542 | U+21FE | |
| 2543 | U+21FF | |
| 2544 | U+2200 | |
| 2545 | U+2201 | |
| 2546 | U+2202 | |
| 2547 | U+2203 | |
| 2548 | U+2204 | |
| 2549 | U+2205 | |
| 2550 | U+2206 | |
| 2551 | U+2207 | |
| 2552 | U+2208 | |
| 2553 | U+2209 | |
| 2554 | U+220A | |
| 2555 | U+220B | |
| 2556 | U+220C | |
| 2557 | U+220D | |
| 2558 | U+220E | |
| 2559 | U+220F | |
| 2560 | U+2210 | |
| 2561 | U+2211 | |
| 2562 | U+2212 | |
| 2563 | U+2213 | |
| 2564 | U+2214 | |
| 2565 | U+2215 | |
| 2566 | U+2216 | |
| 2567 | U+2217 | |
| 2568 | U+2218 | |
| 2569 | U+2219 | |
| 2570 | U+221A | |
| 2571 | U+221B | |
| 2572 | U+221C | |
| 2573 | U+221D | |
| 2574 | U+221E | |
| 2575 | U+221F | |
| 2576 | U+2220 | |
| 2577 | U+2221 | |
| 2578 | U+2222 | |
| 2579 | U+2223 | |
| 2580 | U+2224 | |
| 2581 | U+2225 | |
| 2582 | U+2226 | |
| 2583 | U+2227 | |
| 2584 | U+2228 | |
| 2585 | U+2229 | |
| 2586 | U+222A | |
| 2587 | U+222B | |
| 2588 | U+222C | |
| 2589 | U+222D | |
| 2590 | U+222E | |
| 2591 | U+222F | |
| 2592 | U+2230 | |
| 2593 | U+2231 | |
| 2594 | U+2232 | |
| 2595 | U+2233 | |
| 2596 | U+2234 | |
| 2597 | U+2235 | |
| 2598 | U+2236 | |
| 2599 | U+2237 | |
| 2600 | U+2238 | |
| 2601 | U+2239 | |
| 2602 | U+223A | |
| 2603 | U+223B | |
| 2604 | U+223C | |
| 2605 | U+223D | |
| 2606 | U+223E | |
| 2607 | U+223F | |
| 2608 | U+2240 | |
| 2609 | U+2241 | |
| 2610 | U+2242 | |
| 2611 | U+2243 | |
| 2612 | U+2244 | |
| 2613 | U+2245 | |
| 2614 | U+2246 | |
| 2615 | U+2247 | |
| 2616 | U+2248 | |
| 2617 | U+2249 | |
| 2618 | U+224A | |
| 2619 | U+224B | |
| 2620 | U+224C | |
| 2621 | U+224D | |
| 2622 | U+224E | |
| 2623 | U+224F | |
| 2624 | U+2250 | |
| 2625 | U+2251 | |
| 2626 | U+2252 | |
| 2627 | U+2253 | |
| 2628 | U+2254 | |
| 2629 | U+2255 | |
| 2630 | U+2256 | |
| 2631 | U+2257 | |
| 2632 | U+2258 | |
| 2633 | U+2259 | |
| 2634 | U+225A | |
| 2635 | U+225B | |
| 2636 | U+225C | |
| 2637 | U+225D | |
| 2638 | U+225E | |
| 2639 | U+225F | |
| 2640 | U+2260 | |
| 2641 | U+2261 | |
| 2642 | U+2262 | |
| 2643 | U+2263 | |
| 2644 | U+2264 | |
| 2645 | U+2265 | |
| 2646 | U+2266 | |
| 2647 | U+2267 | |
| 2648 | U+2268 | |
| 2649 | U+2269 | |
| 2650 | U+226A | |
| 2651 | U+226B | |
| 2652 | U+226C | |
| 2653 | U+226D | |
| 2654 | U+226E | |
| 2655 | U+226F | |
| 2656 | U+2270 | |
| 2657 | U+2271 | |
| 2658 | U+2272 | |
| 2659 | U+2273 | |
| 2660 | U+2274 | |
| 2661 | U+2275 | |
| 2662 | U+2276 | |
| 2663 | U+2277 | |
| 2664 | U+2278 | |
| 2665 | U+2279 | |
| 2666 | U+227A | |
| 2667 | U+227B | |
| 2668 | U+227C | |
| 2669 | U+227D | |
| 2670 | U+227E | |
| 2671 | U+227F | |
| 2672 | U+2280 | |
| 2673 | U+2281 | |
| 2674 | U+2282 | |
| 2675 | U+2283 | |
| 2676 | U+2284 | |
| 2677 | U+2285 | |
| 2678 | U+2286 | |
| 2679 | U+2287 | |
| 2680 | U+2288 | |
| 2681 | U+2289 | |
| 2682 | U+228A | |
| 2683 | U+228B | |
| 2684 | U+228C | |
| 2685 | U+228D | |
| 2686 | U+228E | |
| 2687 | U+228F | |
| 2688 | U+2290 | |
| 2689 | U+2291 | |
| 2690 | U+2292 | |
| 2691 | U+2293 | |
| 2692 | U+2294 | |
| 2693 | U+2295 | |
| 2694 | U+2296 | |
| 2695 | U+2297 | |
| 2696 | U+2298 | |
| 2697 | U+2299 | |
| 2698 | U+229A | |
| 2699 | U+229B | |
| 2700 | U+229C | |
| 2701 | U+229D | |
| 2702 | U+229E | |
| 2703 | U+229F | |
| 2704 | U+22A0 | |
| 2705 | U+22A1 | |
| 2706 | U+22A2 | |
| 2707 | U+22A3 | |
| 2708 | U+22A4 | |
| 2709 | U+22A5 | |
| 2710 | U+22A6 | |
| 2711 | U+22A7 | |
| 2712 | U+22A8 | |
| 2713 | U+22A9 | |
| 2714 | U+22AA | |
| 2715 | U+22AB | |
| 2716 | U+22AC | |
| 2717 | U+22AD | |
| 2718 | U+22AE | |
| 2719 | U+22AF | |
| 2720 | U+22B0 | |
| 2721 | U+22B1 | |
| 2722 | U+22B2 | |
| 2723 | U+22B3 | |
| 2724 | U+22B4 | |
| 2725 | U+22B5 | |
| 2726 | U+22B6 | |
| 2727 | U+22B7 | |
| 2728 | U+22B8 | |
| 2729 | U+22B9 | |
| 2730 | U+22BA | |
| 2731 | U+22BB | |
| 2732 | U+22BC | |
| 2733 | U+22BD | |
| 2734 | U+22BE | |
| 2735 | U+22BF | |
| 2736 | U+22C0 | |
| 2737 | U+22C1 | |
| 2738 | U+22C2 | |
| 2739 | U+22C3 | |
| 2740 | U+22C4 | |
| 2741 | U+22C5 | |
| 2742 | U+22C6 | |
| 2743 | U+22C7 | |
| 2744 | U+22C8 | |
| 2745 | U+22C9 | |
| 2746 | U+22CA | |
| 2747 | U+22CB | |
| 2748 | U+22CC | |
| 2749 | U+22CD | |
| 2750 | U+22CE | |
| 2751 | U+22CF | |
| 2752 | U+22D0 | |
| 2753 | U+22D1 | |
| 2754 | U+22D2 | |
| 2755 | U+22D3 | |
| 2756 | U+22D4 | |
| 2757 | U+22D5 | |
| 2758 | U+22D6 | |
| 2759 | U+22D7 | |
| 2760 | U+22D8 | |
| 2761 | U+22D9 | |
| 2762 | U+22DA | |
| 2763 | U+22DB | |
| 2764 | U+22DC | |
| 2765 | U+22DD | |
| 2766 | U+22DE | |
| 2767 | U+22DF | |
| 2768 | U+22E0 | |
| 2769 | U+22E1 | |
| 2770 | U+22E2 | |
| 2771 | U+22E3 | |
| 2772 | U+22E4 | |
| 2773 | U+22E5 | |
| 2774 | U+22E6 | |
| 2775 | U+22E7 | |
| 2776 | U+22E8 | |
| 2777 | U+22E9 | |
| 2778 | U+22EA | |
| 2779 | U+22EB | |
| 2780 | U+22EC | |
| 2781 | U+22ED | |
| 2782 | U+22EE | |
| 2783 | U+22EF | |
| 2784 | U+22F0 | |
| 2785 | U+22F1 | |
| 2786 | U+22F2 | |
| 2787 | U+22F3 | |
| 2788 | U+22F4 | |
| 2789 | U+22F5 | |
| 2790 | U+22F6 | |
| 2791 | U+22F7 | |
| 2792 | U+22F8 | |
| 2793 | U+22F9 | |
| 2794 | U+22FA | |
| 2795 | U+22FB | |
| 2796 | U+22FC | |
| 2797 | U+22FD | |
| 2798 | U+22FE | |
| 2799 | U+22FF | |
| 2800 | U+2300 | |
| 2801 | U+2301 | |
| 2802 | U+2302 | |
| 2803 | U+2303 | |
| 2804 | U+2304 | |
| 2805 | U+2305 | |
| 2806 | U+2306 | |
| 2807 | U+2307 | |
| 2808 | U+2308 | |
| 2809 | U+2309 | |
| 2810 | U+230A | |
| 2811 | U+230B | |
| 2812 | U+230C | |
| 2813 | U+230D | |
| 2814 | U+230E | |
| 2815 | U+230F | |
| 2816 | U+2310 | |
| 2817 | U+2311 | |
| 2818 | U+2312 | |
| 2819 | U+2313 | |
| 2820 | U+2314 | |
| 2821 | U+2315 | |
| 2822 | U+2316 | |
| 2823 | U+2317 | |
| 2824 | U+2318 | |
| 2825 | U+2319 | |
| 2826 | U+231A | |
| 2827 | U+231B | |
| 2828 | U+231C | |
| 2829 | U+231D | |
| 2830 | U+231E | |
| 2831 | U+231F | |
| 2832 | U+2320 | |
| 2833 | U+2321 | |
| 2834 | U+2322 | |
| 2835 | U+2323 | |
| 2836 | U+2324 | |
| 2837 | U+2325 | |
| 2838 | U+2326 | |
| 2839 | U+2327 | |
| 2840 | U+2328 | |
| 2841 | U+2329 | |
| 2842 | U+232A | |
| 2843 | U+232B | |
| 2844 | U+232D | |
| 2845 | U+232E | |
| 2846 | U+232F | |
| 2847 | U+2330 | |
| 2848 | U+2331 | |
| 2849 | U+2332 | |
| 2850 | U+2333 | |
| 2851 | U+2334 | |
| 2852 | U+2335 | |
| 2853 | U+2336 | |
| 2854 | U+2337 | |
| 2855 | U+2338 | |
| 2856 | U+2339 | |
| 2857 | U+233A | |
| 2858 | U+233B | |
| 2859 | U+233C | |
| 2860 | U+233D | |
| 2861 | U+233E | |
| 2862 | U+233F | |
| 2863 | U+2340 | |
| 2864 | U+2341 | |
| 2865 | U+2342 | |
| 2866 | U+2343 | |
| 2867 | U+2344 | |
| 2868 | U+2345 | |
| 2869 | U+2346 | |
| 2870 | U+2347 | |
| 2871 | U+2348 | |
| 2872 | U+2349 | |
| 2873 | U+234A | |
| 2874 | U+234B | |
| 2875 | U+234C | |
| 2876 | U+234D | |
| 2877 | U+234E | |
| 2878 | U+234F | |
| 2879 | U+2350 | |
| 2880 | U+2351 | |
| 2881 | U+2352 | |
| 2882 | U+2353 | |
| 2883 | U+2354 | |
| 2884 | U+2355 | |
| 2885 | U+2356 | |
| 2886 | U+2357 | |
| 2887 | U+2358 | |
| 2888 | U+2359 | |
| 2889 | U+235A | |
| 2890 | U+235B | |
| 2891 | U+235C | |
| 2892 | U+235D | |
| 2893 | U+235E | |
| 2894 | U+235F | |
| 2895 | U+2360 | |
| 2896 | U+2361 | |
| 2897 | U+2362 | |
| 2898 | U+2363 | |
| 2899 | U+2364 | |
| 2900 | U+2365 | |
| 2901 | U+2366 | |
| 2902 | U+2367 | |
| 2903 | U+2368 | |
| 2904 | U+2369 | |
| 2905 | U+236A | |
| 2906 | U+236B | |
| 2907 | U+236C | |
| 2908 | U+236D | |
| 2909 | U+236E | |
| 2910 | U+236F | |
| 2911 | U+2370 | |
| 2912 | U+2371 | |
| 2913 | U+2372 | |
| 2914 | U+2373 | |
| 2915 | U+2374 | |
| 2916 | U+2375 | |
| 2917 | U+2376 | |
| 2918 | U+2377 | |
| 2919 | U+2378 | |
| 2920 | U+2379 | |
| 2921 | U+237A | |
| 2922 | U+237B | |
| 2923 | U+237D | |
| 2924 | U+237E | |
| 2925 | U+237F | |
| 2926 | U+2380 | |
| 2927 | U+2381 | |
| 2928 | U+2382 | |
| 2929 | U+2383 | |
| 2930 | U+2384 | |
| 2931 | U+2385 | |
| 2932 | U+2386 | |
| 2933 | U+2387 | |
| 2934 | U+2388 | |
| 2935 | U+2389 | |
| 2936 | U+238A | |
| 2937 | U+238B | |
| 2938 | U+238D | |
| 2939 | U+2394 | |
| 2940 | U+2395 | |
| 2941 | U+239B | |
| 2942 | U+239C | |
| 2943 | U+239D | |
| 2944 | U+239E | |
| 2945 | U+239F | |
| 2946 | U+23A0 | |
| 2947 | U+23A1 | |
| 2948 | U+23A2 | |
| 2949 | U+23A3 | |
| 2950 | U+23A4 | |
| 2951 | U+23A5 | |
| 2952 | U+23A6 | |
| 2953 | U+23A7 | |
| 2954 | U+23A8 | |
| 2955 | U+23A9 | |
| 2956 | U+23AA | |
| 2957 | U+23AB | |
| 2958 | U+23AC | |
| 2959 | U+23AD | |
| 2960 | U+23AE | |
| 2961 | U+23B0 | |
| 2962 | U+23B1 | |
| 2963 | U+23B2 | |
| 2964 | U+23B3 | |
| 2965 | U+23B8 | |
| 2966 | U+23B9 | |
| 2967 | U+23BA | |
| 2968 | U+23BB | |
| 2969 | U+23BC | |
| 2970 | U+23BD | |
| 2971 | U+23BE | |
| 2972 | U+23BF | |
| 2973 | U+23C0 | |
| 2974 | U+23C1 | |
| 2975 | U+23C2 | |
| 2976 | U+23C3 | |
| 2977 | U+23C4 | |
| 2978 | U+23C5 | |
| 2979 | U+23C6 | |
| 2980 | U+23C7 | |
| 2981 | U+23C8 | |
| 2982 | U+23C9 | |
| 2983 | U+23CA | |
| 2984 | U+23CB | |
| 2985 | U+23CC | |
| 2986 | U+23CE | |
| 2987 | U+23CF | |
| 2988 | U+23DA | |
| 2989 | U+23DB | |
| 2990 | U+23E2 | |
| 2991 | U+23E4 | |
| 2992 | U+23E5 | |
| 2993 | U+23E8 | |
| 2994 | U+23E9 | |
| 2995 | U+23EA | |
| 2996 | U+23EB | |
| 2997 | U+23EC | |
| 2998 | U+23ED | |
| 2999 | U+23EE | |
| 3000 | U+23EF | |
| 3001 | U+23F0 | |
| 3002 | U+23F1 | |
| 3003 | U+23F2 | |
| 3004 | U+23F3 | |
| 3005 | U+23F4 | |
| 3006 | U+23F5 | |
| 3007 | U+23F6 | |
| 3008 | U+23F7 | |
| 3009 | U+23F8 | |
| 3010 | U+23F9 | |
| 3011 | U+23FA | |
| 3012 | U+23FB | |
| 3013 | U+23FC | |
| 3014 | U+23FD | |
| 3015 | U+23FE | |
| 3016 | U+23FF | |
| 3017 | U+2400 | |
| 3018 | U+2401 | |
| 3019 | U+2402 | |
| 3020 | U+2403 | |
| 3021 | U+2404 | |
| 3022 | U+2405 | |
| 3023 | U+2406 | |
| 3024 | U+2407 | |
| 3025 | U+2408 | |
| 3026 | U+2409 | |
| 3027 | U+240A | |
| 3028 | U+240B | |
| 3029 | U+240C | |
| 3030 | U+240D | |
| 3031 | U+240E | |
| 3032 | U+240F | |
| 3033 | U+2410 | |
| 3034 | U+2411 | |
| 3035 | U+2412 | |
| 3036 | U+2413 | |
| 3037 | U+2414 | |
| 3038 | U+2415 | |
| 3039 | U+2416 | |
| 3040 | U+2417 | |
| 3041 | U+2418 | |
| 3042 | U+2419 | |
| 3043 | U+241A | |
| 3044 | U+241B | |
| 3045 | U+241C | |
| 3046 | U+241D | |
| 3047 | U+241E | |
| 3048 | U+241F | |
| 3049 | U+2420 | |
| 3050 | U+2421 | |
| 3051 | U+2422 | |
| 3052 | U+2423 | |
| 3053 | U+2424 | |
| 3054 | U+2425 | |
| 3055 | U+2426 | |
| 3056 | U+2427 | |
| 3057 | U+2428 | |
| 3058 | U+2429 | |
| 3059 | U+2440 | |
| 3060 | U+2441 | |
| 3061 | U+2442 | |
| 3062 | U+2443 | |
| 3063 | U+2444 | |
| 3064 | U+2445 | |
| 3065 | U+2460 | |
| 3066 | U+2461 | |
| 3067 | U+2462 | |
| 3068 | U+2463 | |
| 3069 | U+2464 | |
| 3070 | U+2465 | |
| 3071 | U+2466 | |
| 3072 | U+2467 | |
| 3073 | U+2468 | |
| 3074 | U+2469 | |
| 3075 | U+246A | |
| 3076 | U+246B | |
| 3077 | U+246C | |
| 3078 | U+246D | |
| 3079 | U+246E | |
| 3080 | U+246F | |
| 3081 | U+2470 | |
| 3082 | U+2471 | |
| 3083 | U+2472 | |
| 3084 | U+2473 | |
| 3085 | U+2474 | |
| 3086 | U+2475 | |
| 3087 | U+2476 | |
| 3088 | U+2477 | |
| 3089 | U+2478 | |
| 3090 | U+2479 | |
| 3091 | U+247A | |
| 3092 | U+247B | |
| 3093 | U+247C | |
| 3094 | U+247D | |
| 3095 | U+247E | |
| 3096 | U+247F | |
| 3097 | U+2480 | |
| 3098 | U+2481 | |
| 3099 | U+2482 | |
| 3100 | U+2483 | |
| 3101 | U+2484 | |
| 3102 | U+2485 | |
| 3103 | U+2486 | |
| 3104 | U+2487 | |
| 3105 | U+2488 | |
| 3106 | U+2489 | |
| 3107 | U+248A | |
| 3108 | U+248B | |
| 3109 | U+248C | |
| 3110 | U+248D | |
| 3111 | U+248E | |
| 3112 | U+248F | |
| 3113 | U+2490 | |
| 3114 | U+2491 | |
| 3115 | U+2492 | |
| 3116 | U+2493 | |
| 3117 | U+2494 | |
| 3118 | U+2495 | |
| 3119 | U+2496 | |
| 3120 | U+2497 | |
| 3121 | U+2498 | |
| 3122 | U+2499 | |
| 3123 | U+249A | |
| 3124 | U+249B | |
| 3125 | U+249C | |
| 3126 | U+249D | |
| 3127 | U+249E | |
| 3128 | U+249F | |
| 3129 | U+24A0 | |
| 3130 | U+24A1 | |
| 3131 | U+24A2 | |
| 3132 | U+24A3 | |
| 3133 | U+24A4 | |
| 3134 | U+24A5 | |
| 3135 | U+24A6 | |
| 3136 | U+24A7 | |
| 3137 | U+24A8 | |
| 3138 | U+24A9 | |
| 3139 | U+24AA | |
| 3140 | U+24AB | |
| 3141 | U+24AC | |
| 3142 | U+24AD | |
| 3143 | U+24AE | |
| 3144 | U+24AF | |
| 3145 | U+24B0 | |
| 3146 | U+24B1 | |
| 3147 | U+24B2 | |
| 3148 | U+24B3 | |
| 3149 | U+24B4 | |
| 3150 | U+24B5 | |
| 3151 | U+24B6 | |
| 3152 | U+24B7 | |
| 3153 | U+24B8 | |
| 3154 | U+24B9 | |
| 3155 | U+24BA | |
| 3156 | U+24BB | |
| 3157 | U+24BC | |
| 3158 | U+24BD | |
| 3159 | U+24BE | |
| 3160 | U+24BF | |
| 3161 | U+24C0 | |
| 3162 | U+24C1 | |
| 3163 | U+24C2 | |
| 3164 | U+24C3 | |
| 3165 | U+24C4 | |
| 3166 | U+24C5 | |
| 3167 | U+24C6 | |
| 3168 | U+24C7 | |
| 3169 | U+24C8 | |
| 3170 | U+24C9 | |
| 3171 | U+24CA | |
| 3172 | U+24CB | |
| 3173 | U+24CC | |
| 3174 | U+24CD | |
| 3175 | U+24CE | |
| 3176 | U+24CF | |
| 3177 | U+24D0 | |
| 3178 | U+24D1 | |
| 3179 | U+24D2 | |
| 3180 | U+24D3 | |
| 3181 | U+24D4 | |
| 3182 | U+24D5 | |
| 3183 | U+24D6 | |
| 3184 | U+24D7 | |
| 3185 | U+24D8 | |
| 3186 | U+24D9 | |
| 3187 | U+24DA | |
| 3188 | U+24DB | |
| 3189 | U+24DC | |
| 3190 | U+24DD | |
| 3191 | U+24DE | |
| 3192 | U+24DF | |
| 3193 | U+24E0 | |
| 3194 | U+24E1 | |
| 3195 | U+24E2 | |
| 3196 | U+24E3 | |
| 3197 | U+24E4 | |
| 3198 | U+24E5 | |
| 3199 | U+24E6 | |
| 3200 | U+24E7 | |
| 3201 | U+24E8 | |
| 3202 | U+24E9 | |
| 3203 | U+24EA | |
| 3204 | U+24EB | |
| 3205 | U+24EC | |
| 3206 | U+24ED | |
| 3207 | U+24EE | |
| 3208 | U+24EF | |
| 3209 | U+24F0 | |
| 3210 | U+24F1 | |
| 3211 | U+24F2 | |
| 3212 | U+24F3 | |
| 3213 | U+24F4 | |
| 3214 | U+24F5 | |
| 3215 | U+24F6 | |
| 3216 | U+24F7 | |
| 3217 | U+24F8 | |
| 3218 | U+24F9 | |
| 3219 | U+24FA | |
| 3220 | U+24FB | |
| 3221 | U+24FC | |
| 3222 | U+24FD | |
| 3223 | U+24FE | |
| 3224 | U+24FF | |
| 3225 | U+2500 | |
| 3226 | U+2501 | |
| 3227 | U+2502 | |
| 3228 | U+2503 | |
| 3229 | U+2504 | |
| 3230 | U+2505 | |
| 3231 | U+2506 | |
| 3232 | U+2507 | |
| 3233 | U+2508 | |
| 3234 | U+2509 | |
| 3235 | U+250A | |
| 3236 | U+250B | |
| 3237 | U+250C | |
| 3238 | U+250D | |
| 3239 | U+250E | |
| 3240 | U+250F | |
| 3241 | U+2510 | |
| 3242 | U+2511 | |
| 3243 | U+2512 | |
| 3244 | U+2513 | |
| 3245 | U+2514 | |
| 3246 | U+2515 | |
| 3247 | U+2516 | |
| 3248 | U+2517 | |
| 3249 | U+2518 | |
| 3250 | U+2519 | |
| 3251 | U+251A | |
| 3252 | U+251B | |
| 3253 | U+251C | |
| 3254 | U+251D | |
| 3255 | U+251E | |
| 3256 | U+251F | |
| 3257 | U+2520 | |
| 3258 | U+2521 | |
| 3259 | U+2522 | |
| 3260 | U+2523 | |
| 3261 | U+2524 | |
| 3262 | U+2525 | |
| 3263 | U+2526 | |
| 3264 | U+2527 | |
| 3265 | U+2528 | |
| 3266 | U+2529 | |
| 3267 | U+252A | |
| 3268 | U+252B | |
| 3269 | U+252C | |
| 3270 | U+252D | |
| 3271 | U+252E | |
| 3272 | U+252F | |
| 3273 | U+2530 | |
| 3274 | U+2531 | |
| 3275 | U+2532 | |
| 3276 | U+2533 | |
| 3277 | U+2534 | |
| 3278 | U+2535 | |
| 3279 | U+2536 | |
| 3280 | U+2537 | |
| 3281 | U+2538 | |
| 3282 | U+2539 | |
| 3283 | U+253A | |
| 3284 | U+253B | |
| 3285 | U+253C | |
| 3286 | U+253D | |
| 3287 | U+253E | |
| 3288 | U+253F | |
| 3289 | U+2540 | |
| 3290 | U+2541 | |
| 3291 | U+2542 | |
| 3292 | U+2543 | |
| 3293 | U+2544 | |
| 3294 | U+2545 | |
| 3295 | U+2546 | |
| 3296 | U+2547 | |
| 3297 | U+2548 | |
| 3298 | U+2549 | |
| 3299 | U+254A | |
| 3300 | U+254B | |
| 3301 | U+254C | |
| 3302 | U+254D | |
| 3303 | U+254E | |
| 3304 | U+254F | |
| 3305 | U+2550 | |
| 3306 | U+2551 | |
| 3307 | U+2552 | |
| 3308 | U+2553 | |
| 3309 | U+2554 | |
| 3310 | U+2555 | |
| 3311 | U+2556 | |
| 3312 | U+2557 | |
| 3313 | U+2558 | |
| 3314 | U+2559 | |
| 3315 | U+255A | |
| 3316 | U+255B | |
| 3317 | U+255C | |
| 3318 | U+255D | |
| 3319 | U+255E | |
| 3320 | U+255F | |
| 3321 | U+2560 | |
| 3322 | U+2561 | |
| 3323 | U+2562 | |
| 3324 | U+2563 | |
| 3325 | U+2564 | |
| 3326 | U+2565 | |
| 3327 | U+2566 | |
| 3328 | U+2567 | |
| 3329 | U+2568 | |
| 3330 | U+2569 | |
| 3331 | U+256A | |
| 3332 | U+256B | |
| 3333 | U+256C | |
| 3334 | U+256D | |
| 3335 | U+256E | |
| 3336 | U+256F | |
| 3337 | U+2570 | |
| 3338 | U+2571 | |
| 3339 | U+2572 | |
| 3340 | U+2573 | |
| 3341 | U+2574 | |
| 3342 | U+2575 | |
| 3343 | U+2576 | |
| 3344 | U+2577 | |
| 3345 | U+2578 | |
| 3346 | U+2579 | |
| 3347 | U+257A | |
| 3348 | U+257B | |
| 3349 | U+257C | |
| 3350 | U+257D | |
| 3351 | U+257E | |
| 3352 | U+257F | |
| 3353 | U+2580 | |
| 3354 | U+2581 | |
| 3355 | U+2582 | |
| 3356 | U+2583 | |
| 3357 | U+2584 | |
| 3358 | U+2585 | |
| 3359 | U+2586 | |
| 3360 | U+2587 | |
| 3361 | U+2588 | |
| 3362 | U+2589 | |
| 3363 | U+258A | |
| 3364 | U+258B | |
| 3365 | U+258C | |
| 3366 | U+258D | |
| 3367 | U+258E | |
| 3368 | U+258F | |
| 3369 | U+2590 | |
| 3370 | U+2591 | |
| 3371 | U+2592 | |
| 3372 | U+2593 | |
| 3373 | U+2594 | |
| 3374 | U+2595 | |
| 3375 | U+2596 | |
| 3376 | U+2597 | |
| 3377 | U+2598 | |
| 3378 | U+2599 | |
| 3379 | U+259A | |
| 3380 | U+259B | |
| 3381 | U+259C | |
| 3382 | U+259D | |
| 3383 | U+259E | |
| 3384 | U+259F | |
| 3385 | U+25A0 | |
| 3386 | U+25A1 | |
| 3387 | U+25A2 | |
| 3388 | U+25A3 | |
| 3389 | U+25A4 | |
| 3390 | U+25A5 | |
| 3391 | U+25A6 | |
| 3392 | U+25A7 | |
| 3393 | U+25A8 | |
| 3394 | U+25A9 | |
| 3395 | U+25AA | |
| 3396 | U+25AB | |
| 3397 | U+25AC | |
| 3398 | U+25AD | |
| 3399 | U+25AE | |
| 3400 | U+25AF | |
| 3401 | U+25B0 | |
| 3402 | U+25B1 | |
| 3403 | U+25B2 | |
| 3404 | U+25B3 | |
| 3405 | U+25B4 | |
| 3406 | U+25B5 | |
| 3407 | U+25B6 | |
| 3408 | U+25B7 | |
| 3409 | U+25B8 | |
| 3410 | U+25B9 | |
| 3411 | U+25BA | |
| 3412 | U+25BB | |
| 3413 | U+25BC | |
| 3414 | U+25BD | |
| 3415 | U+25BE | |
| 3416 | U+25BF | |
| 3417 | U+25C0 | |
| 3418 | U+25C1 | |
| 3419 | U+25C2 | |
| 3420 | U+25C3 | |
| 3421 | U+25C4 | |
| 3422 | U+25C5 | |
| 3423 | U+25C6 | |
| 3424 | U+25C7 | |
| 3425 | U+25C8 | |
| 3426 | U+25C9 | |
| 3427 | U+25CA | |
| 3428 | U+25CB | |
| 3429 | U+25CC | |
| 3430 | U+25CD | |
| 3431 | U+25CE | |
| 3432 | U+25CF | |
| 3433 | U+25D0 | |
| 3434 | U+25D1 | |
| 3435 | U+25D2 | |
| 3436 | U+25D3 | |
| 3437 | U+25D4 | |
| 3438 | U+25D5 | |
| 3439 | U+25D6 | |
| 3440 | U+25D7 | |
| 3441 | U+25D8 | |
| 3442 | U+25D9 | |
| 3443 | U+25DA | |
| 3444 | U+25DB | |
| 3445 | U+25DC | |
| 3446 | U+25DD | |
| 3447 | U+25DE | |
| 3448 | U+25DF | |
| 3449 | U+25E0 | |
| 3450 | U+25E1 | |
| 3451 | U+25E2 | |
| 3452 | U+25E3 | |
| 3453 | U+25E4 | |
| 3454 | U+25E5 | |
| 3455 | U+25E6 | |
| 3456 | U+25E7 | |
| 3457 | U+25E8 | |
| 3458 | U+25E9 | |
| 3459 | U+25EA | |
| 3460 | U+25EB | |
| 3461 | U+25EC | |
| 3462 | U+25ED | |
| 3463 | U+25EE | |
| 3464 | U+25EF | |
| 3465 | U+25F0 | |
| 3466 | U+25F1 | |
| 3467 | U+25F2 | |
| 3468 | U+25F3 | |
| 3469 | U+25F4 | |
| 3470 | U+25F5 | |
| 3471 | U+25F6 | |
| 3472 | U+25F7 | |
| 3473 | U+25F8 | |
| 3474 | U+25F9 | |
| 3475 | U+25FA | |
| 3476 | U+25FB | |
| 3477 | U+25FC | |
| 3478 | U+25FD | |
| 3479 | U+25FE | |
| 3480 | U+25FF | |
| 3481 | U+2600 | |
| 3482 | U+2605 | |
| 3483 | U+2606 | |
| 3484 | U+2607 | |
| 3485 | U+2609 | |
| 3486 | U+2610 | |
| 3487 | U+2611 | |
| 3488 | U+2612 | |
| 3489 | U+2613 | |
| 3490 | U+261C | |
| 3491 | U+261D | |
| 3492 | U+261E | |
| 3493 | U+261F | |
| 3494 | U+2621 | |
| 3495 | U+2626 | |
| 3496 | U+2628 | |
| 3497 | U+2630 | |
| 3498 | U+2631 | |
| 3499 | U+2632 | |
| 3500 | U+2633 | |
| 3501 | U+2634 | |
| 3502 | U+2635 | |
| 3503 | U+2636 | |
| 3504 | U+2637 | |
| 3505 | U+2639 | |
| 3506 | U+263A | |
| 3507 | U+263B | |
| 3508 | U+263C | |
| 3509 | U+263F | |
| 3510 | U+2640 | |
| 3511 | U+2641 | |
| 3512 | U+2642 | |
| 3513 | U+2660 | |
| 3514 | U+2661 | |
| 3515 | U+2662 | |
| 3516 | U+2663 | |
| 3517 | U+2664 | |
| 3518 | U+2665 | |
| 3519 | U+2666 | |
| 3520 | U+2667 | |
| 3521 | U+2669 | |
| 3522 | U+266A | |
| 3523 | U+266B | |
| 3524 | U+266C | |
| 3525 | U+266D | |
| 3526 | U+266E | |
| 3527 | U+266F | |
| 3528 | U+267E | |
| 3529 | U+2680 | |
| 3530 | U+2681 | |
| 3531 | U+2682 | |
| 3532 | U+2683 | |
| 3533 | U+2684 | |
| 3534 | U+2685 | |
| 3535 | U+2686 | |
| 3536 | U+2687 | |
| 3537 | U+2688 | |
| 3538 | U+2689 | |
| 3539 | U+268A | |
| 3540 | U+268B | |
| 3541 | U+268C | |
| 3542 | U+268D | |
| 3543 | U+268E | |
| 3544 | U+268F | |
| 3545 | U+2690 | |
| 3546 | U+2691 | |
| 3547 | U+2699 | |
| 3548 | U+269B | |
| 3549 | U+26A0 | |
| 3550 | U+26A1 | |
| 3551 | U+26AA | |
| 3552 | U+26AB | |
| 3553 | U+26AC | |
| 3554 | U+26AD | |
| 3555 | U+26AE | |
| 3556 | U+26AF | |
| 3557 | U+26B2 | |
| 3558 | U+26CB | |
| 3559 | U+26DA | |
| 3560 | U+26DD | |
| 3561 | U+26DE | |
| 3562 | U+26ED | |
| 3563 | U+26EE | |
| 3564 | U+26EF | |
| 3565 | U+26F6 | |
| 3566 | U+2705 | |
| 3567 | U+2713 | |
| 3568 | U+2714 | |
| 3569 | U+2715 | |
| 3570 | U+2716 | |
| 3571 | U+2717 | |
| 3572 | U+2718 | |
| 3573 | U+271A | |
| 3574 | U+271B | |
| 3575 | U+271C | |
| 3576 | U+271D | |
| 3577 | U+2720 | |
| 3578 | U+2721 | |
| 3579 | U+2722 | |
| 3580 | U+2723 | |
| 3581 | U+2724 | |
| 3582 | U+2725 | |
| 3583 | U+2726 | |
| 3584 | U+2727 | |
| 3585 | U+2731 | |
| 3586 | U+2732 | |
| 3587 | U+2733 | |
| 3588 | U+2734 | |
| 3589 | U+2736 | |
| 3590 | U+2737 | |
| 3591 | U+2738 | |
| 3592 | U+2739 | |
| 3593 | U+273A | |
| 3594 | U+273B | |
| 3595 | U+273C | |
| 3596 | U+273D | |
| 3597 | U+273E | |
| 3598 | U+273F | |
| 3599 | U+2740 | |
| 3600 | U+2741 | |
| 3601 | U+2742 | |
| 3602 | U+2743 | |
| 3603 | U+2744 | |
| 3604 | U+2745 | |
| 3605 | U+2746 | |
| 3606 | U+2747 | |
| 3607 | U+2748 | |
| 3608 | U+2749 | |
| 3609 | U+274A | |
| 3610 | U+274B | |
| 3611 | U+274C | |
| 3612 | U+274E | |
| 3613 | U+2756 | |
| 3614 | U+2758 | |
| 3615 | U+2759 | |
| 3616 | U+275A | |
| 3617 | U+2761 | |
| 3618 | U+2764 | |
| 3619 | U+276C | |
| 3620 | U+276D | |
| 3621 | U+276E | |
| 3622 | U+276F | |
| 3623 | U+2770 | |
| 3624 | U+2771 | |
| 3625 | U+2776 | |
| 3626 | U+2777 | |
| 3627 | U+2778 | |
| 3628 | U+2779 | |
| 3629 | U+277A | |
| 3630 | U+277B | |
| 3631 | U+277C | |
| 3632 | U+277D | |
| 3633 | U+277E | |
| 3634 | U+277F | |
| 3635 | U+2780 | |
| 3636 | U+2781 | |
| 3637 | U+2782 | |
| 3638 | U+2783 | |
| 3639 | U+2784 | |
| 3640 | U+2785 | |
| 3641 | U+2786 | |
| 3642 | U+2787 | |
| 3643 | U+2788 | |
| 3644 | U+2789 | |
| 3645 | U+278A | |
| 3646 | U+278B | |
| 3647 | U+278C | |
| 3648 | U+278D | |
| 3649 | U+278E | |
| 3650 | U+278F | |
| 3651 | U+2790 | |
| 3652 | U+2791 | |
| 3653 | U+2792 | |
| 3654 | U+2793 | |
| 3655 | U+2794 | |
| 3656 | U+279C | |
| 3657 | U+279D | |
| 3658 | U+279E | |
| 3659 | U+27A1 | |
| 3660 | U+27A2 | |
| 3661 | U+27A3 | |
| 3662 | U+27A4 | |
| 3663 | U+27A5 | |
| 3664 | U+27A6 | |
| 3665 | U+27A7 | |
| 3666 | U+27B0 | |
| 3667 | U+27BF | |
| 3668 | U+27C2 | |
| 3669 | U+27CA | |
| 3670 | U+27CB | |
| 3671 | U+27CD | |
| 3672 | U+27D2 | |
| 3673 | U+27D3 | |
| 3674 | U+27D4 | |
| 3675 | U+27D5 | |
| 3676 | U+27D6 | |
| 3677 | U+27D7 | |
| 3678 | U+27D8 | |
| 3679 | U+27D9 | |
| 3680 | U+27DA | |
| 3681 | U+27DB | |
| 3682 | U+27DC | |
| 3683 | U+27DF | |
| 3684 | U+27E0 | |
| 3685 | U+27E1 | |
| 3686 | U+27E6 | |
| 3687 | U+27E7 | |
| 3688 | U+27E8 | |
| 3689 | U+27E9 | |
| 3690 | U+27EA | |
| 3691 | U+27EB | |
| 3692 | U+27EE | |
| 3693 | U+27EF | |
| 3694 | U+27F0 | |
| 3695 | U+27F1 | |
| 3696 | U+27F2 | |
| 3697 | U+27F3 | |
| 3698 | U+27F4 | |
| 3699 | U+2800 | |
| 3700 | U+2801 | |
| 3701 | U+2802 | |
| 3702 | U+2803 | |
| 3703 | U+2804 | |
| 3704 | U+2805 | |
| 3705 | U+2806 | |
| 3706 | U+2807 | |
| 3707 | U+2808 | |
| 3708 | U+2809 | |
| 3709 | U+280A | |
| 3710 | U+280B | |
| 3711 | U+280C | |
| 3712 | U+280D | |
| 3713 | U+280E | |
| 3714 | U+280F | |
| 3715 | U+2810 | |
| 3716 | U+2811 | |
| 3717 | U+2812 | |
| 3718 | U+2813 | |
| 3719 | U+2814 | |
| 3720 | U+2815 | |
| 3721 | U+2816 | |
| 3722 | U+2817 | |
| 3723 | U+2818 | |
| 3724 | U+2819 | |
| 3725 | U+281A | |
| 3726 | U+281B | |
| 3727 | U+281C | |
| 3728 | U+281D | |
| 3729 | U+281E | |
| 3730 | U+281F | |
| 3731 | U+2820 | |
| 3732 | U+2821 | |
| 3733 | U+2822 | |
| 3734 | U+2823 | |
| 3735 | U+2824 | |
| 3736 | U+2825 | |
| 3737 | U+2826 | |
| 3738 | U+2827 | |
| 3739 | U+2828 | |
| 3740 | U+2829 | |
| 3741 | U+282A | |
| 3742 | U+282B | |
| 3743 | U+282C | |
| 3744 | U+282D | |
| 3745 | U+282E | |
| 3746 | U+282F | |
| 3747 | U+2830 | |
| 3748 | U+2831 | |
| 3749 | U+2832 | |
| 3750 | U+2833 | |
| 3751 | U+2834 | |
| 3752 | U+2835 | |
| 3753 | U+2836 | |
| 3754 | U+2837 | |
| 3755 | U+2838 | |
| 3756 | U+2839 | |
| 3757 | U+283A | |
| 3758 | U+283B | |
| 3759 | U+283C | |
| 3760 | U+283D | |
| 3761 | U+283E | |
| 3762 | U+283F | |
| 3763 | U+2840 | |
| 3764 | U+2841 | |
| 3765 | U+2842 | |
| 3766 | U+2843 | |
| 3767 | U+2844 | |
| 3768 | U+2845 | |
| 3769 | U+2846 | |
| 3770 | U+2847 | |
| 3771 | U+2848 | |
| 3772 | U+2849 | |
| 3773 | U+284A | |
| 3774 | U+284B | |
| 3775 | U+284C | |
| 3776 | U+284D | |
| 3777 | U+284E | |
| 3778 | U+284F | |
| 3779 | U+2850 | |
| 3780 | U+2851 | |
| 3781 | U+2852 | |
| 3782 | U+2853 | |
| 3783 | U+2854 | |
| 3784 | U+2855 | |
| 3785 | U+2856 | |
| 3786 | U+2857 | |
| 3787 | U+2858 | |
| 3788 | U+2859 | |
| 3789 | U+285A | |
| 3790 | U+285B | |
| 3791 | U+285C | |
| 3792 | U+285D | |
| 3793 | U+285E | |
| 3794 | U+285F | |
| 3795 | U+2860 | |
| 3796 | U+2861 | |
| 3797 | U+2862 | |
| 3798 | U+2863 | |
| 3799 | U+2864 | |
| 3800 | U+2865 | |
| 3801 | U+2866 | |
| 3802 | U+2867 | |
| 3803 | U+2868 | |
| 3804 | U+2869 | |
| 3805 | U+286A | |
| 3806 | U+286B | |
| 3807 | U+286C | |
| 3808 | U+286D | |
| 3809 | U+286E | |
| 3810 | U+286F | |
| 3811 | U+2870 | |
| 3812 | U+2871 | |
| 3813 | U+2872 | |
| 3814 | U+2873 | |
| 3815 | U+2874 | |
| 3816 | U+2875 | |
| 3817 | U+2876 | |
| 3818 | U+2877 | |
| 3819 | U+2878 | |
| 3820 | U+2879 | |
| 3821 | U+287A | |
| 3822 | U+287B | |
| 3823 | U+287C | |
| 3824 | U+287D | |
| 3825 | U+287E | |
| 3826 | U+287F | |
| 3827 | U+2880 | |
| 3828 | U+2881 | |
| 3829 | U+2882 | |
| 3830 | U+2883 | |
| 3831 | U+2884 | |
| 3832 | U+2885 | |
| 3833 | U+2886 | |
| 3834 | U+2887 | |
| 3835 | U+2888 | |
| 3836 | U+2889 | |
| 3837 | U+288A | |
| 3838 | U+288B | |
| 3839 | U+288C | |
| 3840 | U+288D | |
| 3841 | U+288E | |
| 3842 | U+288F | |
| 3843 | U+2890 | |
| 3844 | U+2891 | |
| 3845 | U+2892 | |
| 3846 | U+2893 | |
| 3847 | U+2894 | |
| 3848 | U+2895 | |
| 3849 | U+2896 | |
| 3850 | U+2897 | |
| 3851 | U+2898 | |
| 3852 | U+2899 | |
| 3853 | U+289A | |
| 3854 | U+289B | |
| 3855 | U+289C | |
| 3856 | U+289D | |
| 3857 | U+289E | |
| 3858 | U+289F | |
| 3859 | U+28A0 | |
| 3860 | U+28A1 | |
| 3861 | U+28A2 | |
| 3862 | U+28A3 | |
| 3863 | U+28A4 | |
| 3864 | U+28A5 | |
| 3865 | U+28A6 | |
| 3866 | U+28A7 | |
| 3867 | U+28A8 | |
| 3868 | U+28A9 | |
| 3869 | U+28AA | |
| 3870 | U+28AB | |
| 3871 | U+28AC | |
| 3872 | U+28AD | |
| 3873 | U+28AE | |
| 3874 | U+28AF | |
| 3875 | U+28B0 | |
| 3876 | U+28B1 | |
| 3877 | U+28B2 | |
| 3878 | U+28B3 | |
| 3879 | U+28B4 | |
| 3880 | U+28B5 | |
| 3881 | U+28B6 | |
| 3882 | U+28B7 | |
| 3883 | U+28B8 | |
| 3884 | U+28B9 | |
| 3885 | U+28BA | |
| 3886 | U+28BB | |
| 3887 | U+28BC | |
| 3888 | U+28BD | |
| 3889 | U+28BE | |
| 3890 | U+28BF | |
| 3891 | U+28C0 | |
| 3892 | U+28C1 | |
| 3893 | U+28C2 | |
| 3894 | U+28C3 | |
| 3895 | U+28C4 | |
| 3896 | U+28C5 | |
| 3897 | U+28C6 | |
| 3898 | U+28C7 | |
| 3899 | U+28C8 | |
| 3900 | U+28C9 | |
| 3901 | U+28CA | |
| 3902 | U+28CB | |
| 3903 | U+28CC | |
| 3904 | U+28CD | |
| 3905 | U+28CE | |
| 3906 | U+28CF | |
| 3907 | U+28D0 | |
| 3908 | U+28D1 | |
| 3909 | U+28D2 | |
| 3910 | U+28D3 | |
| 3911 | U+28D4 | |
| 3912 | U+28D5 | |
| 3913 | U+28D6 | |
| 3914 | U+28D7 | |
| 3915 | U+28D8 | |
| 3916 | U+28D9 | |
| 3917 | U+28DA | |
| 3918 | U+28DB | |
| 3919 | U+28DC | |
| 3920 | U+28DD | |
| 3921 | U+28DE | |
| 3922 | U+28DF | |
| 3923 | U+28E0 | |
| 3924 | U+28E1 | |
| 3925 | U+28E2 | |
| 3926 | U+28E3 | |
| 3927 | U+28E4 | |
| 3928 | U+28E5 | |
| 3929 | U+28E6 | |
| 3930 | U+28E7 | |
| 3931 | U+28E8 | |
| 3932 | U+28E9 | |
| 3933 | U+28EA | |
| 3934 | U+28EB | |
| 3935 | U+28EC | |
| 3936 | U+28ED | |
| 3937 | U+28EE | |
| 3938 | U+28EF | |
| 3939 | U+28F0 | |
| 3940 | U+28F1 | |
| 3941 | U+28F2 | |
| 3942 | U+28F3 | |
| 3943 | U+28F4 | |
| 3944 | U+28F5 | |
| 3945 | U+28F6 | |
| 3946 | U+28F7 | |
| 3947 | U+28F8 | |
| 3948 | U+28F9 | |
| 3949 | U+28FA | |
| 3950 | U+28FB | |
| 3951 | U+28FC | |
| 3952 | U+28FD | |
| 3953 | U+28FE | |
| 3954 | U+28FF | |
| 3955 | U+2900 | |
| 3956 | U+2901 | |
| 3957 | U+2902 | |
| 3958 | U+2903 | |
| 3959 | U+2904 | |
| 3960 | U+2905 | |
| 3961 | U+2906 | |
| 3962 | U+2907 | |
| 3963 | U+2908 | |
| 3964 | U+2909 | |
| 3965 | U+290A | |
| 3966 | U+290B | |
| 3967 | U+290C | |
| 3968 | U+290D | |
| 3969 | U+290E | |
| 3970 | U+290F | |
| 3971 | U+2910 | |
| 3972 | U+2911 | |
| 3973 | U+2912 | |
| 3974 | U+2913 | |
| 3975 | U+2914 | |
| 3976 | U+2915 | |
| 3977 | U+2916 | |
| 3978 | U+2917 | |
| 3979 | U+2918 | |
| 3980 | U+2919 | |
| 3981 | U+291A | |
| 3982 | U+291B | |
| 3983 | U+291C | |
| 3984 | U+291D | |
| 3985 | U+291E | |
| 3986 | U+291F | |
| 3987 | U+2920 | |
| 3988 | U+2921 | |
| 3989 | U+2922 | |
| 3990 | U+2923 | |
| 3991 | U+2924 | |
| 3992 | U+2925 | |
| 3993 | U+2926 | |
| 3994 | U+2933 | |
| 3995 | U+2940 | |
| 3996 | U+2941 | |
| 3997 | U+2947 | |
| 3998 | U+2948 | |
| 3999 | U+294A | |
| 4000 | U+294B | |
| 4001 | U+294C | |
| 4002 | U+294D | |
| 4003 | U+294E | |
| 4004 | U+294F | |
| 4005 | U+2950 | |
| 4006 | U+2951 | |
| 4007 | U+2952 | |
| 4008 | U+2953 | |
| 4009 | U+2954 | |
| 4010 | U+2955 | |
| 4011 | U+2956 | |
| 4012 | U+2957 | |
| 4013 | U+2958 | |
| 4014 | U+2959 | |
| 4015 | U+295A | |
| 4016 | U+295B | |
| 4017 | U+295C | |
| 4018 | U+295D | |
| 4019 | U+295E | |
| 4020 | U+295F | |
| 4021 | U+2960 | |
| 4022 | U+2961 | |
| 4023 | U+2962 | |
| 4024 | U+2963 | |
| 4025 | U+2964 | |
| 4026 | U+2965 | |
| 4027 | U+296E | |
| 4028 | U+296F | |
| 4029 | U+2980 | |
| 4030 | U+2981 | |
| 4031 | U+2982 | |
| 4032 | U+2983 | |
| 4033 | U+2984 | |
| 4034 | U+2987 | |
| 4035 | U+2988 | |
| 4036 | U+2989 | |
| 4037 | U+298A | |
| 4038 | U+2999 | |
| 4039 | U+299A | |
| 4040 | U+29A2 | |
| 4041 | U+29A3 | |
| 4042 | U+29B0 | |
| 4043 | U+29B1 | |
| 4044 | U+29B2 | |
| 4045 | U+29B3 | |
| 4046 | U+29B4 | |
| 4047 | U+29B5 | |
| 4048 | U+29B6 | |
| 4049 | U+29B7 | |
| 4050 | U+29B8 | |
| 4051 | U+29B9 | |
| 4052 | U+29BA | |
| 4053 | U+29BB | |
| 4054 | U+29BC | |
| 4055 | U+29BD | |
| 4056 | U+29BE | |
| 4057 | U+29BF | |
| 4058 | U+29C4 | |
| 4059 | U+29C5 | |
| 4060 | U+29C6 | |
| 4061 | U+29C7 | |
| 4062 | U+29C8 | |
| 4063 | U+29C9 | |
| 4064 | U+29CA | |
| 4065 | U+29CB | |
| 4066 | U+29CD | |
| 4067 | U+29CE | |
| 4068 | U+29D1 | |
| 4069 | U+29D2 | |
| 4070 | U+29D3 | |
| 4071 | U+29D4 | |
| 4072 | U+29D5 | |
| 4073 | U+29D6 | |
| 4074 | U+29D7 | |
| 4075 | U+29D8 | |
| 4076 | U+29D9 | |
| 4077 | U+29DA | |
| 4078 | U+29DB | |
| 4079 | U+29DF | |
| 4080 | U+29E2 | |
| 4081 | U+29E3 | |
| 4082 | U+29E4 | |
| 4083 | U+29E5 | |
| 4084 | U+29E6 | |
| 4085 | U+29E7 | |
| 4086 | U+29E8 | |
| 4087 | U+29E9 | |
| 4088 | U+29EB | |
| 4089 | U+29F4 | |
| 4090 | U+29F5 | |
| 4091 | U+29F6 | |
| 4092 | U+29F7 | |
| 4093 | U+29F8 | |
| 4094 | U+29F9 | |
| 4095 | U+29FA | |
| 4096 | U+29FB | |
| 4097 | U+2A00 | |
| 4098 | U+2A01 | |
| 4099 | U+2A02 | |
| 4100 | U+2A05 | |
| 4101 | U+2A06 | |
| 4102 | U+2A09 | |
| 4103 | U+2A11 | |
| 4104 | U+2A12 | |
| 4105 | U+2A13 | |
| 4106 | U+2A14 | |
| 4107 | U+2A15 | |
| 4108 | U+2A16 | |
| 4109 | U+2A1D | |
| 4110 | U+2A1E | |
| 4111 | U+2A1F | |
| 4112 | U+2A20 | |
| 4113 | U+2A21 | |
| 4114 | U+2A22 | |
| 4115 | U+2A23 | |
| 4116 | U+2A24 | |
| 4117 | U+2A25 | |
| 4118 | U+2A26 | |
| 4119 | U+2A2A | |
| 4120 | U+2A2B | |
| 4121 | U+2A2C | |
| 4122 | U+2A2D | |
| 4123 | U+2A2E | |
| 4124 | U+2A2F | |
| 4125 | U+2A30 | |
| 4126 | U+2A31 | |
| 4127 | U+2A32 | |
| 4128 | U+2A34 | |
| 4129 | U+2A35 | |
| 4130 | U+2A36 | |
| 4131 | U+2A38 | |
| 4132 | U+2A3C | |
| 4133 | U+2A3D | |
| 4134 | U+2A3E | |
| 4135 | U+2A3F | |
| 4136 | U+2A40 | |
| 4137 | U+2A41 | |
| 4138 | U+2A42 | |
| 4139 | U+2A43 | |
| 4140 | U+2A4C | |
| 4141 | U+2A4D | |
| 4142 | U+2A4E | |
| 4143 | U+2A4F | |
| 4144 | U+2A53 | |
| 4145 | U+2A54 | |
| 4146 | U+2A57 | |
| 4147 | U+2A58 | |
| 4148 | U+2A5A | |
| 4149 | U+2A5B | |
| 4150 | U+2A5C | |
| 4151 | U+2A5D | |
| 4152 | U+2A5E | |
| 4153 | U+2A5F | |
| 4154 | U+2A60 | |
| 4155 | U+2A61 | |
| 4156 | U+2A62 | |
| 4157 | U+2A63 | |
| 4158 | U+2A64 | |
| 4159 | U+2A65 | |
| 4160 | U+2A66 | |
| 4161 | U+2A67 | |
| 4162 | U+2A68 | |
| 4163 | U+2A69 | |
| 4164 | U+2A6A | |
| 4165 | U+2A6B | |
| 4166 | U+2A6C | |
| 4167 | U+2A6D | |
| 4168 | U+2A6E | |
| 4169 | U+2A6F | |
| 4170 | U+2A70 | |
| 4171 | U+2A71 | |
| 4172 | U+2A72 | |
| 4173 | U+2A73 | |
| 4174 | U+2A7D | |
| 4175 | U+2A7E | |
| 4176 | U+2A7F | |
| 4177 | U+2A80 | |
| 4178 | U+2A85 | |
| 4179 | U+2A86 | |
| 4180 | U+2A87 | |
| 4181 | U+2A88 | |
| 4182 | U+2A89 | |
| 4183 | U+2A8A | |
| 4184 | U+2A8D | |
| 4185 | U+2A8E | |
| 4186 | U+2A95 | |
| 4187 | U+2A96 | |
| 4188 | U+2A97 | |
| 4189 | U+2A98 | |
| 4190 | U+2A99 | |
| 4191 | U+2A9A | |
| 4192 | U+2A9D | |
| 4193 | U+2A9E | |
| 4194 | U+2AAA | |
| 4195 | U+2AAB | |
| 4196 | U+2AAC | |
| 4197 | U+2AAD | |
| 4198 | U+2AAE | |
| 4199 | U+2AAF | |
| 4200 | U+2AB0 | |
| 4201 | U+2AB1 | |
| 4202 | U+2AB2 | |
| 4203 | U+2AB3 | |
| 4204 | U+2AB4 | |
| 4205 | U+2AB5 | |
| 4206 | U+2AB6 | |
| 4207 | U+2AB7 | |
| 4208 | U+2AB8 | |
| 4209 | U+2AB9 | |
| 4210 | U+2ABA | |
| 4211 | U+2ABD | |
| 4212 | U+2ABE | |
| 4213 | U+2ACF | |
| 4214 | U+2AD0 | |
| 4215 | U+2AD1 | |
| 4216 | U+2AD2 | |
| 4217 | U+2AD9 | |
| 4218 | U+2ADA | |
| 4219 | U+2ADB | |
| 4220 | U+2ADC | |
| 4221 | U+2ADD | |
| 4222 | U+2ADE | |
| 4223 | U+2ADF | |
| 4224 | U+2AE0 | |
| 4225 | U+2AE2 | |
| 4226 | U+2AE3 | |
| 4227 | U+2AE4 | |
| 4228 | U+2AE5 | |
| 4229 | U+2AE6 | |
| 4230 | U+2AE7 | |
| 4231 | U+2AE8 | |
| 4232 | U+2AE9 | |
| 4233 | U+2AEA | |
| 4234 | U+2AEB | |
| 4235 | U+2AEC | |
| 4236 | U+2AED | |
| 4237 | U+2AEE | |
| 4238 | U+2AEF | |
| 4239 | U+2AF0 | |
| 4240 | U+2AF1 | |
| 4241 | U+2AF2 | |
| 4242 | U+2AF3 | |
| 4243 | U+2AF4 | |
| 4244 | U+2AF5 | |
| 4245 | U+2AF6 | |
| 4246 | U+2AFC | |
| 4247 | U+2AFD | |
| 4248 | U+2B00 | |
| 4249 | U+2B01 | |
| 4250 | U+2B02 | |
| 4251 | U+2B03 | |
| 4252 | U+2B04 | |
| 4253 | U+2B05 | |
| 4254 | U+2B06 | |
| 4255 | U+2B07 | |
| 4256 | U+2B08 | |
| 4257 | U+2B09 | |
| 4258 | U+2B0A | |
| 4259 | U+2B0B | |
| 4260 | U+2B0C | |
| 4261 | U+2B0D | |
| 4262 | U+2B0E | |
| 4263 | U+2B0F | |
| 4264 | U+2B10 | |
| 4265 | U+2B11 | |
| 4266 | U+2B12 | |
| 4267 | U+2B13 | |
| 4268 | U+2B14 | |
| 4269 | U+2B15 | |
| 4270 | U+2B16 | |
| 4271 | U+2B17 | |
| 4272 | U+2B18 | |
| 4273 | U+2B19 | |
| 4274 | U+2B1A | |
| 4275 | U+2B1B | |
| 4276 | U+2B1C | |
| 4277 | U+2B1D | |
| 4278 | U+2B1E | |
| 4279 | U+2B1F | |
| 4280 | U+2B20 | |
| 4281 | U+2B21 | |
| 4282 | U+2B22 | |
| 4283 | U+2B23 | |
| 4284 | U+2B24 | |
| 4285 | U+2B25 | |
| 4286 | U+2B26 | |
| 4287 | U+2B27 | |
| 4288 | U+2B28 | |
| 4289 | U+2B29 | |
| 4290 | U+2B2A | |
| 4291 | U+2B2B | |
| 4292 | U+2B2C | |
| 4293 | U+2B2D | |
| 4294 | U+2B2E | |
| 4295 | U+2B2F | |
| 4296 | U+2B30 | |
| 4297 | U+2B31 | |
| 4298 | U+2B32 | |
| 4299 | U+2B34 | |
| 4300 | U+2B35 | |
| 4301 | U+2B36 | |
| 4302 | U+2B37 | |
| 4303 | U+2B38 | |
| 4304 | U+2B39 | |
| 4305 | U+2B3A | |
| 4306 | U+2B3B | |
| 4307 | U+2B3C | |
| 4308 | U+2B3D | |
| 4309 | U+2B3E | |
| 4310 | U+2B3F | |
| 4311 | U+2B45 | |
| 4312 | U+2B46 | |
| 4313 | U+2B4D | |
| 4314 | U+2B50 | |
| 4315 | U+2B51 | |
| 4316 | U+2B52 | |
| 4317 | U+2B53 | |
| 4318 | U+2B54 | |
| 4319 | U+2B55 | |
| 4320 | U+2B58 | |
| 4321 | U+2B59 | |
| 4322 | U+2B60 | |
| 4323 | U+2B61 | |
| 4324 | U+2B62 | |
| 4325 | U+2B63 | |
| 4326 | U+2B64 | |
| 4327 | U+2B65 | |
| 4328 | U+2B66 | |
| 4329 | U+2B67 | |
| 4330 | U+2B68 | |
| 4331 | U+2B69 | |
| 4332 | U+2B6A | |
| 4333 | U+2B6B | |
| 4334 | U+2B6C | |
| 4335 | U+2B6D | |
| 4336 | U+2B6E | |
| 4337 | U+2B6F | |
| 4338 | U+2B70 | |
| 4339 | U+2B71 | |
| 4340 | U+2B72 | |
| 4341 | U+2B73 | |
| 4342 | U+2B7A | |
| 4343 | U+2B7B | |
| 4344 | U+2B7C | |
| 4345 | U+2B7D | |
| 4346 | U+2B7E | |
| 4347 | U+2B7F | |
| 4348 | U+2B80 | |
| 4349 | U+2B81 | |
| 4350 | U+2B82 | |
| 4351 | U+2B83 | |
| 4352 | U+2B84 | |
| 4353 | U+2B85 | |
| 4354 | U+2B86 | |
| 4355 | U+2B87 | |
| 4356 | U+2B8C | |
| 4357 | U+2B8D | |
| 4358 | U+2B8E | |
| 4359 | U+2B8F | |
| 4360 | U+2B90 | |
| 4361 | U+2B91 | |
| 4362 | U+2B92 | |
| 4363 | U+2B93 | |
| 4364 | U+2B94 | |
| 4365 | U+2B95 | |
| 4366 | U+2BA0 | |
| 4367 | U+2BA1 | |
| 4368 | U+2BA2 | |
| 4369 | U+2BA3 | |
| 4370 | U+2BA4 | |
| 4371 | U+2BA5 | |
| 4372 | U+2BA6 | |
| 4373 | U+2BA7 | |
| 4374 | U+2BB8 | |
| 4375 | U+2BB9 | |
| 4376 | U+2BBA | |
| 4377 | U+2BBB | |
| 4378 | U+2BBC | |
| 4379 | U+2BBD | |
| 4380 | U+2BBE | |
| 4381 | U+2BBF | |
| 4382 | U+2BC0 | |
| 4383 | U+2BC1 | |
| 4384 | U+2BC2 | |
| 4385 | U+2BC3 | |
| 4386 | U+2BC4 | |
| 4387 | U+2BC5 | |
| 4388 | U+2BC6 | |
| 4389 | U+2BC7 | |
| 4390 | U+2BC8 | |
| 4391 | U+2BCA | |
| 4392 | U+2BCB | |
| 4393 | U+2BCC | |
| 4394 | U+2BCD | |
| 4395 | U+2BCE | |
| 4396 | U+2BCF | |
| 4397 | U+2BD0 | |
| 4398 | U+2BD1 | |
| 4399 | U+2BD2 | |
| 4400 | U+2BE8 | |
| 4401 | U+2BE9 | |
| 4402 | U+2BEA | |
| 4403 | U+2BEB | |
| 4404 | U+2BEC | |
| 4405 | U+2BED | |
| 4406 | U+2BEE | |
| 4407 | U+2BEF | |
| 4408 | U+2BFA | |
| 4409 | U+2BFB | |
| 4410 | U+2BFC | |
| 4411 | U+2BFD | |
| 4412 | U+2BFE | |
| 4413 | U+2BFF | |
| 4414 | U+1FB00 | |
| 4415 | U+1FB01 | |
| 4416 | U+1FB02 | |
| 4417 | U+1FB03 | |
| 4418 | U+1FB04 | |
| 4419 | U+1FB05 | |
| 4420 | U+1FB06 | |
| 4421 | U+1FB07 | |
| 4422 | U+1FB08 | |
| 4423 | U+1FB09 | |
| 4424 | U+1FB0A | |
| 4425 | U+1FB0B | |
| 4426 | U+1FB0C | |
| 4427 | U+1FB0D | |
| 4428 | U+1FB0E | |
| 4429 | U+1FB0F | |
| 4430 | U+1FB10 | |
| 4431 | U+1FB11 | |
| 4432 | U+1FB12 | |
| 4433 | U+1FB13 | |
| 4434 | U+1FB14 | |
| 4435 | U+1FB15 | |
| 4436 | U+1FB16 | |
| 4437 | U+1FB17 | |
| 4438 | U+1FB18 | |
| 4439 | U+1FB19 | |
| 4440 | U+1FB1A | |
| 4441 | U+1FB1B | |
| 4442 | U+1FB1C | |
| 4443 | U+1FB1D | |
| 4444 | U+1FB1E | |
| 4445 | U+1FB1F | |
| 4446 | U+1FB20 | |
| 4447 | U+1FB21 | |
| 4448 | U+1FB22 | |
| 4449 | U+1FB23 | |
| 4450 | U+1FB24 | |
| 4451 | U+1FB25 | |
| 4452 | U+1FB26 | |
| 4453 | U+1FB27 | |
| 4454 | U+1FB28 | |
| 4455 | U+1FB29 | |
| 4456 | U+1FB2A | |
| 4457 | U+1FB2B | |
| 4458 | U+1FB2C | |
| 4459 | U+1FB2D | |
| 4460 | U+1FB2E | |
| 4461 | U+1FB2F | |
| 4462 | U+1FB30 | |
| 4463 | U+1FB31 | |
| 4464 | U+1FB32 | |
| 4465 | U+1FB33 | |
| 4466 | U+1FB34 | |
| 4467 | U+1FB35 | |
| 4468 | U+1FB36 | |
| 4469 | U+1FB37 | |
| 4470 | U+1FB38 | |
| 4471 | U+1FB39 | |
| 4472 | U+1FB3A | |
| 4473 | U+1FB3B | |
| 4474 | U+1FB3C | |
| 4475 | U+1FB3D | |
| 4476 | U+1FB3E | |
| 4477 | U+1FB3F | |
| 4478 | U+1FB40 | |
| 4479 | U+1FB41 | |
| 4480 | U+1FB42 | |
| 4481 | U+1FB43 | |
| 4482 | U+1FB44 | |
| 4483 | U+1FB45 | |
| 4484 | U+1FB46 | |
| 4485 | U+1FB47 | |
| 4486 | U+1FB48 | |
| 4487 | U+1FB49 | |
| 4488 | U+1FB4A | |
| 4489 | U+1FB4B | |
| 4490 | U+1FB4C | |
| 4491 | U+1FB4D | |
| 4492 | U+1FB4E | |
| 4493 | U+1FB4F | |
| 4494 | U+1FB50 | |
| 4495 | U+1FB51 | |
| 4496 | U+1FB52 | |
| 4497 | U+1FB53 | |
| 4498 | U+1FB54 | |
| 4499 | U+1FB55 | |
| 4500 | U+1FB56 | |
| 4501 | U+1FB57 | |
| 4502 | U+1FB58 | |
| 4503 | U+1FB59 | |
| 4504 | U+1FB5A | |
| 4505 | U+1FB5B | |
| 4506 | U+1FB5C | |
| 4507 | U+1FB5D | |
| 4508 | U+1FB5E | |
| 4509 | U+1FB5F | |
| 4510 | U+1FB60 | |
| 4511 | U+1FB61 | |
| 4512 | U+1FB62 | |
| 4513 | U+1FB63 | |
| 4514 | U+1FB64 | |
| 4515 | U+1FB65 | |
| 4516 | U+1FB66 | |
| 4517 | U+1FB67 | |
| 4518 | U+1FB68 | |
| 4519 | U+1FB69 | |
| 4520 | U+1FB6A | |
| 4521 | U+1FB6B | |
| 4522 | U+1FB6C | |
| 4523 | U+1FB6D | |
| 4524 | U+1FB6E | |
| 4525 | U+1FB6F | |
| 4526 | U+1FB70 | |
| 4527 | U+1FB71 | |
| 4528 | U+1FB72 | |
| 4529 | U+1FB73 | |
| 4530 | U+1FB74 | |
| 4531 | U+1FB75 | |
| 4532 | U+1FB76 | |
| 4533 | U+1FB77 | |
| 4534 | U+1FB78 | |
| 4535 | U+1FB79 | |
| 4536 | U+1FB7A | |
| 4537 | U+1FB7B | |
| 4538 | U+1FB7C | |
| 4539 | U+1FB7D | |
| 4540 | U+1FB7E | |
| 4541 | U+1FB7F | |
| 4542 | U+1FB80 | |
| 4543 | U+1FB81 | |
| 4544 | U+1FB82 | |
| 4545 | U+1FB83 | |
| 4546 | U+1FB84 | |
| 4547 | U+1FB85 | |
| 4548 | U+1FB86 | |
| 4549 | U+1FB87 | |
| 4550 | U+1FB88 | |
| 4551 | U+1FB89 | |
| 4552 | U+1FB8A | |
| 4553 | U+1FB8B | |
| 4554 | U+1FB8C | |
| 4555 | U+1FB8D | |
| 4556 | U+1FB8E | |
| 4557 | U+1FB8F | |
| 4558 | U+1FB90 | |
| 4559 | U+1FB91 | |
| 4560 | U+1FB92 | |
| 4561 | U+1FB94 | |
| 4562 | U+1FB95 | |
| 4563 | U+1FB96 | |
| 4564 | U+1FB97 | |
| 4565 | U+1FB98 | |
| 4566 | U+1FB99 | |
| 4567 | U+1FB9A | |
| 4568 | U+1FB9B | |
| 4569 | U+1FB9C | |
| 4570 | U+1FB9D | |
| 4571 | U+1FB9E | |
| 4572 | U+1FB9F | |
| 4573 | U+1FBA0 | |
| 4574 | U+1FBA1 | |
| 4575 | U+1FBA2 | |
| 4576 | U+1FBA3 | |
| 4577 | U+1FBA4 | |
| 4578 | U+1FBA5 | |
| 4579 | U+1FBA6 | |
| 4580 | U+1FBA7 | |
| 4581 | U+1FBA8 | |
| 4582 | U+1FBA9 | |
| 4583 | U+1FBAA | |
| 4584 | U+1FBAB | |
| 4585 | U+1FBAC | |
| 4586 | U+1FBAD | |
| 4587 | U+1FBAE | |
| 4588 | U+1FBAF | |
| 4589 | U+1FBB0 | |
| 4590 | U+1FBB1 | |
| 4591 | U+1FBB2 | |
| 4592 | U+1FBB3 | |
| 4593 | U+1FBB4 | |
| 4594 | U+1FBB5 | |
| 4595 | U+1FBB6 | |
| 4596 | U+1FBB7 | |
| 4597 | U+1FBB8 | |
| 4598 | U+1FBB9 | |
| 4599 | U+1FBBA | |
| 4600 | U+1FBBB | |
| 4601 | U+1FBBC | |
| 4602 | U+1FBBD | |
| 4603 | U+1FBBE | |
| 4604 | U+1FBBF | |
| 4605 | U+1FBC0 | |
| 4606 | U+1FBC1 | |
| 4607 | U+1FBC2 | |
| 4608 | U+1FBC3 | |
| 4609 | U+1FBC4 | |
| 4610 | U+1FBC5 | |
| 4611 | U+1FBC6 | |
| 4612 | U+1FBC7 | |
| 4613 | U+1FBC8 | |
| 4614 | U+1FBC9 | |
| 4615 | U+1FBCA | |
| 4616 | U+1FBCC | |
| 4617 | U+1FBCE | |
| 4618 | U+1FBCF | |
| 4619 | U+1FBD0 | |
| 4620 | U+1FBD1 | |
| 4621 | U+1FBD2 | |
| 4622 | U+1FBD3 | |
| 4623 | U+1FBD4 | |
| 4624 | U+1FBD5 | |
| 4625 | U+1FBD6 | |
| 4626 | U+1FBD7 | |
| 4627 | U+1FBD8 | |
| 4628 | U+1FBD9 | |
| 4629 | U+1FBDA | |
| 4630 | U+1FBDB | |
| 4631 | U+1FBDC | |
| 4632 | U+1FBDD | |
| 4633 | U+1FBDE | |
| 4634 | U+1FBDF | |
| 4635 | U+1FBE0 | |
| 4636 | U+1FBE1 | |
| 4637 | U+1FBE2 | |
| 4638 | U+1FBE3 | |
| 4639 | U+1FBE4 | |
| 4640 | U+1FBE5 | |
| 4641 | U+1FBE6 | |
| 4642 | U+1FBE7 | |
| 4643 | U+1FBE8 | |
| 4644 | U+1FBE9 | |
| 4645 | U+1FBEA | |
| 4646 | U+1FBEB | |
| 4647 | U+1FBEC | |
| 4648 | U+1FBED | |
| 4649 | U+1FBEE | |
| 4650 | U+1FBEF | |
| 4651 | U+1FBF0 | |
| 4652 | U+1FBF1 | |
| 4653 | U+1FBF2 | |
| 4654 | U+1FBF3 | |
| 4655 | U+1FBF4 | |
| 4656 | U+1FBF5 | |
| 4657 | U+1FBF6 | |
| 4658 | U+1FBF7 | |
| 4659 | U+1FBF8 | |
| 4660 | U+1FBF9 |
packages/terminal/fonts/README.md created+143| ... | ... | @@ -0,0 +1,143 @@ |
| 1 | # terminal fonts | |
| 2 | ||
| 3 | ## Primary font | |
| 4 | ||
| 5 | The four WOFF2 files are a reproducible subset of **Iosevka Fixed Extended | |
| 6 | v34.8.1** (Renzhi Li / Belleve Invis), taken from the release's own WebFont | |
| 7 | package: | |
| 8 | ||
| 9 | `https://github.com/be5invis/Iosevka/releases/download/v34.8.1/PkgWebFont-IosevkaFixed-34.8.1.zip` | |
| 10 | ||
| 11 | The 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 | ||
| 22 | The zip is acquisition material and must remain outside the repository. | |
| 23 | ||
| 24 | **Fixed Extended, and neither half of that is cosmetic.** _Extended_ is the | |
| 25 | 0.600 em advance — the same one JetBrains Mono v2.304 shipped before it, so the | |
| 26 | cell geometry did not move; Iosevka's default width is 0.500 em and reads | |
| 27 | compressed at terminal sizes. _Fixed_ is the variant with no glyph wider than | |
| 28 | one 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 | |
| 30 | renderer 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 | ||
| 39 | Copyright (c) 2015-2026, Renzhi Li (aka. Belleve Invis, belleve@typeof.net). | |
| 40 | Distributed under the SIL Open Font License 1.1 in [`OFL.txt`](OFL.txt). | |
| 41 | Iosevka reserves no font name, so the subset carries no naming obligation; the | |
| 42 | private `__clo_terminal_primary_34_8_1` family name is this package's, not a | |
| 43 | claim about the upstream face. | |
| 44 | ||
| 45 | The checked-in manifest [`PRIMARY-CODEPOINTS.txt`](PRIMARY-CODEPOINTS.txt) is | |
| 46 | the sorted intersection of the source cmap with the retained ranges in | |
| 47 | [`subset-primary.py`](subset-primary.py). It contains 4,660 codepoints, is | |
| 48 | 32,867 bytes, and has SHA-256 | |
| 49 | `ebe06abbcb42d87fe2d2f37f805ab0413b69c8dbb438dded94e4a8f4c2832672`. | |
| 50 | The ranges are `U+0000-2BFF` and `U+1FB00-1FBFF`: every script and symbol block | |
| 51 | a cell can hold, plus Symbols for Legacy Computing. That keeps braille, | |
| 52 | technical, dingbats and the sextants the previous primary had none of, and drops | |
| 53 | mathematical alphanumerics, musical notation, emoji and the historic scripts — | |
| 54 | 2,899 of the source font's 7,559 codepoints, and most of its bytes. | |
| 55 | ||
| 56 | Regenerate 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 | |
| 60 | python fonts/subset-primary.py "$TTF_DIR/IosevkaFixed-Extended.ttf" \ | |
| 61 | "$CLEAN_DIR/PRIMARY-CODEPOINTS.txt" | |
| 62 | for 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 | |
| 75 | done | |
| 76 | ``` | |
| 77 | ||
| 78 | Two builds from separate clean directories produced byte-identical manifests | |
| 79 | and 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 | |
| 82 | and require byte equality plus the hashes above before replacing checked-in | |
| 83 | outputs. | |
| 84 | ||
| 85 | ## Terminal symbols | |
| 86 | ||
| 87 | `__clo_terminal_symbols_3_4_0-Regular.woff2` is a subset of Symbols Nerd Font | |
| 88 | Mono 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 | ||
| 94 | The downloaded TTF is 2,507,556 bytes with SHA-256 | |
| 95 | `f0f624d9b474bea1662cf7e862d44aebe1ae1f6c7f9cb7a0ca5d0e5ac9561c60`. | |
| 96 | Its 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 | |
| 98 | must remain outside the repository. | |
| 99 | ||
| 100 | The checked-in manifest [`SYMBOLS-CODEPOINTS.txt`](SYMBOLS-CODEPOINTS.txt) is | |
| 101 | the sorted intersection of the source cmap with the retained ranges in | |
| 102 | [`subset-symbols.py`](subset-symbols.py). It contains 10,273 codepoints, is | |
| 103 | 78,807 bytes, and has SHA-256 | |
| 104 | `128c0e17659bd21ea91edf73006304b368fb714d38f12c0d7b005d713052eac1`. | |
| 105 | The range policy excludes primary-owned box/block `U+2500-U+259F`, standard | |
| 106 | Powerline `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 | |
| 108 | source cmap; the other exclusions remove 7 and 130 source codepoints. | |
| 109 | ||
| 110 | The output WOFF2 is 1,083,700 bytes with SHA-256 | |
| 111 | `a41598870ee7b32226cc76fdaab59edca51ba04e30d2c9a0bfa453615e3cca8c`. | |
| 112 | Two builds from separate clean directories produced byte-identical manifests | |
| 113 | and 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` | |
| 115 | and `brotli==1.1.0`. | |
| 116 | ||
| 117 | Given an input TTF in approved temporary storage, regenerate the manifest and | |
| 118 | font with these exact commands from `sys/lib/terminal`: | |
| 119 | ||
| 120 | ```sh | |
| 121 | python fonts/subset-symbols.py "$INPUT_TTF" "$CLEAN_DIR/SYMBOLS-CODEPOINTS.txt" | |
| 122 | pyftsubset "$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 | ||
| 135 | Run both commands twice with distinct empty `CLEAN_DIR` values and require | |
| 136 | byte equality plus the hashes above before replacing checked-in outputs. | |
| 137 | ||
| 138 | Nerd Fonts marks Symbols Only as MIT, while its own audit assigns retained | |
| 139 | glyph components additional MIT, CC BY 4.0, Apache 2.0, and SIL OFL 1.1 | |
| 140 | terms. The subset is distributed with every applicable component term rather | |
| 141 | than treated as MIT-only. [`NOTICES.md`](NOTICES.md) maps retained ranges to | |
| 142 | the exact notices under [`licenses/`](licenses/). Font Logos is absent because | |
| 143 | the pinned audit records that component as unlicensed. |
packages/terminal/fonts/SYMBOLS-CODEPOINTS.txt created+10273| ... | ... | @@ -0,0 +1,10273 @@ |
| 1 | U+23FB | |
| 2 | U+23FC | |
| 3 | U+23FD | |
| 4 | U+23FE | |
| 5 | U+2630 | |
| 6 | U+2665 | |
| 7 | U+26A1 | |
| 8 | U+276C | |
| 9 | U+276D | |
| 10 | U+276E | |
| 11 | U+276F | |
| 12 | U+2770 | |
| 13 | U+2771 | |
| 14 | U+2B58 | |
| 15 | U+E000 | |
| 16 | U+E001 | |
| 17 | U+E002 | |
| 18 | U+E003 | |
| 19 | U+E004 | |
| 20 | U+E005 | |
| 21 | U+E006 | |
| 22 | U+E007 | |
| 23 | U+E008 | |
| 24 | U+E009 | |
| 25 | U+E00A | |
| 26 | U+E0A3 | |
| 27 | U+E0B4 | |
| 28 | U+E0B5 | |
| 29 | U+E0B6 | |
| 30 | U+E0B7 | |
| 31 | U+E0B8 | |
| 32 | U+E0B9 | |
| 33 | U+E0BA | |
| 34 | U+E0BB | |
| 35 | U+E0BC | |
| 36 | U+E0BD | |
| 37 | U+E0BE | |
| 38 | U+E0BF | |
| 39 | U+E0C0 | |
| 40 | U+E0C1 | |
| 41 | U+E0C2 | |
| 42 | U+E0C3 | |
| 43 | U+E0C4 | |
| 44 | U+E0C5 | |
| 45 | U+E0C6 | |
| 46 | U+E0C7 | |
| 47 | U+E0C8 | |
| 48 | U+E0CA | |
| 49 | U+E0CC | |
| 50 | U+E0CD | |
| 51 | U+E0CE | |
| 52 | U+E0CF | |
| 53 | U+E0D0 | |
| 54 | U+E0D1 | |
| 55 | U+E0D2 | |
| 56 | U+E0D4 | |
| 57 | U+E0D6 | |
| 58 | U+E0D7 | |
| 59 | U+E200 | |
| 60 | U+E201 | |
| 61 | U+E202 | |
| 62 | U+E203 | |
| 63 | U+E204 | |
| 64 | U+E205 | |
| 65 | U+E206 | |
| 66 | U+E207 | |
| 67 | U+E208 | |
| 68 | U+E209 | |
| 69 | U+E20A | |
| 70 | U+E20B | |
| 71 | U+E20C | |
| 72 | U+E20D | |
| 73 | U+E20E | |
| 74 | U+E20F | |
| 75 | U+E210 | |
| 76 | U+E211 | |
| 77 | U+E212 | |
| 78 | U+E213 | |
| 79 | U+E214 | |
| 80 | U+E215 | |
| 81 | U+E216 | |
| 82 | U+E217 | |
| 83 | U+E218 | |
| 84 | U+E219 | |
| 85 | U+E21A | |
| 86 | U+E21B | |
| 87 | U+E21C | |
| 88 | U+E21D | |
| 89 | U+E21E | |
| 90 | U+E21F | |
| 91 | U+E220 | |
| 92 | U+E221 | |
| 93 | U+E222 | |
| 94 | U+E223 | |
| 95 | U+E224 | |
| 96 | U+E225 | |
| 97 | U+E226 | |
| 98 | U+E227 | |
| 99 | U+E228 | |
| 100 | U+E229 | |
| 101 | U+E22A | |
| 102 | U+E22B | |
| 103 | U+E22C | |
| 104 | U+E22D | |
| 105 | U+E22E | |
| 106 | U+E22F | |
| 107 | U+E230 | |
| 108 | U+E231 | |
| 109 | U+E232 | |
| 110 | U+E233 | |
| 111 | U+E234 | |
| 112 | U+E235 | |
| 113 | U+E236 | |
| 114 | U+E237 | |
| 115 | U+E238 | |
| 116 | U+E239 | |
| 117 | U+E23A | |
| 118 | U+E23B | |
| 119 | U+E23C | |
| 120 | U+E23D | |
| 121 | U+E23E | |
| 122 | U+E23F | |
| 123 | U+E240 | |
| 124 | U+E241 | |
| 125 | U+E242 | |
| 126 | U+E243 | |
| 127 | U+E244 | |
| 128 | U+E245 | |
| 129 | U+E246 | |
| 130 | U+E247 | |
| 131 | U+E248 | |
| 132 | U+E249 | |
| 133 | U+E24A | |
| 134 | U+E24B | |
| 135 | U+E24C | |
| 136 | U+E24D | |
| 137 | U+E24E | |
| 138 | U+E24F | |
| 139 | U+E250 | |
| 140 | U+E251 | |
| 141 | U+E252 | |
| 142 | U+E253 | |
| 143 | U+E254 | |
| 144 | U+E255 | |
| 145 | U+E256 | |
| 146 | U+E257 | |
| 147 | U+E258 | |
| 148 | U+E259 | |
| 149 | U+E25A | |
| 150 | U+E25B | |
| 151 | U+E25C | |
| 152 | U+E25D | |
| 153 | U+E25E | |
| 154 | U+E25F | |
| 155 | U+E260 | |
| 156 | U+E261 | |
| 157 | U+E262 | |
| 158 | U+E263 | |
| 159 | U+E264 | |
| 160 | U+E265 | |
| 161 | U+E266 | |
| 162 | U+E267 | |
| 163 | U+E268 | |
| 164 | U+E269 | |
| 165 | U+E26A | |
| 166 | U+E26B | |
| 167 | U+E26C | |
| 168 | U+E26D | |
| 169 | U+E26E | |
| 170 | U+E26F | |
| 171 | U+E270 | |
| 172 | U+E271 | |
| 173 | U+E272 | |
| 174 | U+E273 | |
| 175 | U+E274 | |
| 176 | U+E275 | |
| 177 | U+E276 | |
| 178 | U+E277 | |
| 179 | U+E278 | |
| 180 | U+E279 | |
| 181 | U+E27A | |
| 182 | U+E27B | |
| 183 | U+E27C | |
| 184 | U+E27D | |
| 185 | U+E27E | |
| 186 | U+E27F | |
| 187 | U+E280 | |
| 188 | U+E281 | |
| 189 | U+E282 | |
| 190 | U+E283 | |
| 191 | U+E284 | |
| 192 | U+E285 | |
| 193 | U+E286 | |
| 194 | U+E287 | |
| 195 | U+E288 | |
| 196 | U+E289 | |
| 197 | U+E28A | |
| 198 | U+E28B | |
| 199 | U+E28C | |
| 200 | U+E28D | |
| 201 | U+E28E | |
| 202 | U+E28F | |
| 203 | U+E290 | |
| 204 | U+E291 | |
| 205 | U+E292 | |
| 206 | U+E293 | |
| 207 | U+E294 | |
| 208 | U+E295 | |
| 209 | U+E296 | |
| 210 | U+E297 | |
| 211 | U+E298 | |
| 212 | U+E299 | |
| 213 | U+E29A | |
| 214 | U+E29B | |
| 215 | U+E29C | |
| 216 | U+E29D | |
| 217 | U+E29E | |
| 218 | U+E29F | |
| 219 | U+E2A0 | |
| 220 | U+E2A1 | |
| 221 | U+E2A2 | |
| 222 | U+E2A3 | |
| 223 | U+E2A4 | |
| 224 | U+E2A5 | |
| 225 | U+E2A6 | |
| 226 | U+E2A7 | |
| 227 | U+E2A8 | |
| 228 | U+E2A9 | |
| 229 | U+E300 | |
| 230 | U+E301 | |
| 231 | U+E302 | |
| 232 | U+E303 | |
| 233 | U+E304 | |
| 234 | U+E305 | |
| 235 | U+E306 | |
| 236 | U+E307 | |
| 237 | U+E308 | |
| 238 | U+E309 | |
| 239 | U+E30A | |
| 240 | U+E30B | |
| 241 | U+E30C | |
| 242 | U+E30D | |
| 243 | U+E30E | |
| 244 | U+E30F | |
| 245 | U+E310 | |
| 246 | U+E311 | |
| 247 | U+E312 | |
| 248 | U+E313 | |
| 249 | U+E314 | |
| 250 | U+E315 | |
| 251 | U+E316 | |
| 252 | U+E317 | |
| 253 | U+E318 | |
| 254 | U+E319 | |
| 255 | U+E31A | |
| 256 | U+E31B | |
| 257 | U+E31C | |
| 258 | U+E31D | |
| 259 | U+E31E | |
| 260 | U+E31F | |
| 261 | U+E320 | |
| 262 | U+E321 | |
| 263 | U+E322 | |
| 264 | U+E323 | |
| 265 | U+E324 | |
| 266 | U+E325 | |
| 267 | U+E326 | |
| 268 | U+E327 | |
| 269 | U+E328 | |
| 270 | U+E329 | |
| 271 | U+E32A | |
| 272 | U+E32B | |
| 273 | U+E32C | |
| 274 | U+E32D | |
| 275 | U+E32E | |
| 276 | U+E32F | |
| 277 | U+E330 | |
| 278 | U+E331 | |
| 279 | U+E332 | |
| 280 | U+E333 | |
| 281 | U+E334 | |
| 282 | U+E335 | |
| 283 | U+E336 | |
| 284 | U+E337 | |
| 285 | U+E338 | |
| 286 | U+E339 | |
| 287 | U+E33A | |
| 288 | U+E33B | |
| 289 | U+E33C | |
| 290 | U+E33D | |
| 291 | U+E33E | |
| 292 | U+E33F | |
| 293 | U+E340 | |
| 294 | U+E341 | |
| 295 | U+E342 | |
| 296 | U+E343 | |
| 297 | U+E344 | |
| 298 | U+E345 | |
| 299 | U+E346 | |
| 300 | U+E347 | |
| 301 | U+E348 | |
| 302 | U+E349 | |
| 303 | U+E34A | |
| 304 | U+E34B | |
| 305 | U+E34C | |
| 306 | U+E34D | |
| 307 | U+E34E | |
| 308 | U+E34F | |
| 309 | U+E350 | |
| 310 | U+E351 | |
| 311 | U+E352 | |
| 312 | U+E353 | |
| 313 | U+E354 | |
| 314 | U+E355 | |
| 315 | U+E356 | |
| 316 | U+E357 | |
| 317 | U+E358 | |
| 318 | U+E359 | |
| 319 | U+E35A | |
| 320 | U+E35B | |
| 321 | U+E35C | |
| 322 | U+E35D | |
| 323 | U+E35E | |
| 324 | U+E35F | |
| 325 | U+E360 | |
| 326 | U+E361 | |
| 327 | U+E362 | |
| 328 | U+E363 | |
| 329 | U+E364 | |
| 330 | U+E365 | |
| 331 | U+E366 | |
| 332 | U+E367 | |
| 333 | U+E368 | |
| 334 | U+E369 | |
| 335 | U+E36A | |
| 336 | U+E36B | |
| 337 | U+E36C | |
| 338 | U+E36D | |
| 339 | U+E36E | |
| 340 | U+E36F | |
| 341 | U+E370 | |
| 342 | U+E371 | |
| 343 | U+E372 | |
| 344 | U+E373 | |
| 345 | U+E374 | |
| 346 | U+E375 | |
| 347 | U+E376 | |
| 348 | U+E377 | |
| 349 | U+E378 | |
| 350 | U+E379 | |
| 351 | U+E37A | |
| 352 | U+E37B | |
| 353 | U+E37C | |
| 354 | U+E37D | |
| 355 | U+E37E | |
| 356 | U+E37F | |
| 357 | U+E380 | |
| 358 | U+E381 | |
| 359 | U+E382 | |
| 360 | U+E383 | |
| 361 | U+E384 | |
| 362 | U+E385 | |
| 363 | U+E386 | |
| 364 | U+E387 | |
| 365 | U+E388 | |
| 366 | U+E389 | |
| 367 | U+E38A | |
| 368 | U+E38B | |
| 369 | U+E38C | |
| 370 | U+E38D | |
| 371 | U+E38E | |
| 372 | U+E38F | |
| 373 | U+E390 | |
| 374 | U+E391 | |
| 375 | U+E392 | |
| 376 | U+E393 | |
| 377 | U+E394 | |
| 378 | U+E395 | |
| 379 | U+E396 | |
| 380 | U+E397 | |
| 381 | U+E398 | |
| 382 | U+E399 | |
| 383 | U+E39A | |
| 384 | U+E39B | |
| 385 | U+E39C | |
| 386 | U+E39D | |
| 387 | U+E39E | |
| 388 | U+E39F | |
| 389 | U+E3A0 | |
| 390 | U+E3A1 | |
| 391 | U+E3A2 | |
| 392 | U+E3A3 | |
| 393 | U+E3A4 | |
| 394 | U+E3A5 | |
| 395 | U+E3A6 | |
| 396 | U+E3A7 | |
| 397 | U+E3A8 | |
| 398 | U+E3A9 | |
| 399 | U+E3AA | |
| 400 | U+E3AB | |
| 401 | U+E3AC | |
| 402 | U+E3AD | |
| 403 | U+E3AE | |
| 404 | U+E3AF | |
| 405 | U+E3B0 | |
| 406 | U+E3B1 | |
| 407 | U+E3B2 | |
| 408 | U+E3B3 | |
| 409 | U+E3B4 | |
| 410 | U+E3B5 | |
| 411 | U+E3B6 | |
| 412 | U+E3B7 | |
| 413 | U+E3B8 | |
| 414 | U+E3B9 | |
| 415 | U+E3BA | |
| 416 | U+E3BB | |
| 417 | U+E3BC | |
| 418 | U+E3BD | |
| 419 | U+E3BE | |
| 420 | U+E3BF | |
| 421 | U+E3C0 | |
| 422 | U+E3C1 | |
| 423 | U+E3C2 | |
| 424 | U+E3C3 | |
| 425 | U+E3C4 | |
| 426 | U+E3C5 | |
| 427 | U+E3C6 | |
| 428 | U+E3C7 | |
| 429 | U+E3C8 | |
| 430 | U+E3C9 | |
| 431 | U+E3CA | |
| 432 | U+E3CB | |
| 433 | U+E3CC | |
| 434 | U+E3CD | |
| 435 | U+E3CE | |
| 436 | U+E3CF | |
| 437 | U+E3D0 | |
| 438 | U+E3D1 | |
| 439 | U+E3D2 | |
| 440 | U+E3D3 | |
| 441 | U+E3D4 | |
| 442 | U+E3D5 | |
| 443 | U+E3D6 | |
| 444 | U+E3D7 | |
| 445 | U+E3D8 | |
| 446 | U+E3D9 | |
| 447 | U+E3DA | |
| 448 | U+E3DB | |
| 449 | U+E3DC | |
| 450 | U+E3DD | |
| 451 | U+E3DE | |
| 452 | U+E3DF | |
| 453 | U+E3E0 | |
| 454 | U+E3E1 | |
| 455 | U+E3E2 | |
| 456 | U+E3E3 | |
| 457 | U+E5FA | |
| 458 | U+E5FB | |
| 459 | U+E5FC | |
| 460 | U+E5FD | |
| 461 | U+E5FE | |
| 462 | U+E5FF | |
| 463 | U+E600 | |
| 464 | U+E601 | |
| 465 | U+E602 | |
| 466 | U+E603 | |
| 467 | U+E604 | |
| 468 | U+E605 | |
| 469 | U+E606 | |
| 470 | U+E607 | |
| 471 | U+E608 | |
| 472 | U+E609 | |
| 473 | U+E60A | |
| 474 | U+E60B | |
| 475 | U+E60C | |
| 476 | U+E60D | |
| 477 | U+E60E | |
| 478 | U+E60F | |
| 479 | U+E610 | |
| 480 | U+E611 | |
| 481 | U+E612 | |
| 482 | U+E613 | |
| 483 | U+E614 | |
| 484 | U+E615 | |
| 485 | U+E616 | |
| 486 | U+E617 | |
| 487 | U+E618 | |
| 488 | U+E619 | |
| 489 | U+E61A | |
| 490 | U+E61B | |
| 491 | U+E61C | |
| 492 | U+E61D | |
| 493 | U+E61E | |
| 494 | U+E61F | |
| 495 | U+E620 | |
| 496 | U+E621 | |
| 497 | U+E622 | |
| 498 | U+E623 | |
| 499 | U+E624 | |
| 500 | U+E625 | |
| 501 | U+E626 | |
| 502 | U+E627 | |
| 503 | U+E628 | |
| 504 | U+E629 | |
| 505 | U+E62A | |
| 506 | U+E62B | |
| 507 | U+E62C | |
| 508 | U+E62D | |
| 509 | U+E62E | |
| 510 | U+E62F | |
| 511 | U+E630 | |
| 512 | U+E631 | |
| 513 | U+E632 | |
| 514 | U+E633 | |
| 515 | U+E634 | |
| 516 | U+E635 | |
| 517 | U+E636 | |
| 518 | U+E637 | |
| 519 | U+E638 | |
| 520 | U+E639 | |
| 521 | U+E63A | |
| 522 | U+E63B | |
| 523 | U+E63C | |
| 524 | U+E63D | |
| 525 | U+E63E | |
| 526 | U+E63F | |
| 527 | U+E640 | |
| 528 | U+E641 | |
| 529 | U+E642 | |
| 530 | U+E643 | |
| 531 | U+E644 | |
| 532 | U+E645 | |
| 533 | U+E646 | |
| 534 | U+E647 | |
| 535 | U+E648 | |
| 536 | U+E649 | |
| 537 | U+E64A | |
| 538 | U+E64B | |
| 539 | U+E64C | |
| 540 | U+E64D | |
| 541 | U+E64E | |
| 542 | U+E64F | |
| 543 | U+E650 | |
| 544 | U+E651 | |
| 545 | U+E652 | |
| 546 | U+E653 | |
| 547 | U+E654 | |
| 548 | U+E655 | |
| 549 | U+E656 | |
| 550 | U+E657 | |
| 551 | U+E658 | |
| 552 | U+E659 | |
| 553 | U+E65A | |
| 554 | U+E65B | |
| 555 | U+E65C | |
| 556 | U+E65D | |
| 557 | U+E65E | |
| 558 | U+E65F | |
| 559 | U+E660 | |
| 560 | U+E661 | |
| 561 | U+E662 | |
| 562 | U+E663 | |
| 563 | U+E664 | |
| 564 | U+E665 | |
| 565 | U+E666 | |
| 566 | U+E667 | |
| 567 | U+E668 | |
| 568 | U+E669 | |
| 569 | U+E66A | |
| 570 | U+E66B | |
| 571 | U+E66C | |
| 572 | U+E66D | |
| 573 | U+E66E | |
| 574 | U+E66F | |
| 575 | U+E670 | |
| 576 | U+E671 | |
| 577 | U+E672 | |
| 578 | U+E673 | |
| 579 | U+E674 | |
| 580 | U+E675 | |
| 581 | U+E676 | |
| 582 | U+E677 | |
| 583 | U+E678 | |
| 584 | U+E679 | |
| 585 | U+E67A | |
| 586 | U+E67B | |
| 587 | U+E67C | |
| 588 | U+E67D | |
| 589 | U+E67E | |
| 590 | U+E67F | |
| 591 | U+E680 | |
| 592 | U+E681 | |
| 593 | U+E682 | |
| 594 | U+E683 | |
| 595 | U+E684 | |
| 596 | U+E685 | |
| 597 | U+E686 | |
| 598 | U+E687 | |
| 599 | U+E688 | |
| 600 | U+E689 | |
| 601 | U+E68A | |
| 602 | U+E68B | |
| 603 | U+E68C | |
| 604 | U+E68D | |
| 605 | U+E68E | |
| 606 | U+E68F | |
| 607 | U+E690 | |
| 608 | U+E691 | |
| 609 | U+E692 | |
| 610 | U+E693 | |
| 611 | U+E694 | |
| 612 | U+E695 | |
| 613 | U+E696 | |
| 614 | U+E697 | |
| 615 | U+E698 | |
| 616 | U+E699 | |
| 617 | U+E69A | |
| 618 | U+E69B | |
| 619 | U+E69C | |
| 620 | U+E69D | |
| 621 | U+E69E | |
| 622 | U+E69F | |
| 623 | U+E6A0 | |
| 624 | U+E6A1 | |
| 625 | U+E6A2 | |
| 626 | U+E6A3 | |
| 627 | U+E6A4 | |
| 628 | U+E6A5 | |
| 629 | U+E6A6 | |
| 630 | U+E6A7 | |
| 631 | U+E6A8 | |
| 632 | U+E6A9 | |
| 633 | U+E6AA | |
| 634 | U+E6AB | |
| 635 | U+E6AC | |
| 636 | U+E6AD | |
| 637 | U+E6AE | |
| 638 | U+E6AF | |
| 639 | U+E6B0 | |
| 640 | U+E6B1 | |
| 641 | U+E6B2 | |
| 642 | U+E6B3 | |
| 643 | U+E6B4 | |
| 644 | U+E6B5 | |
| 645 | U+E6B6 | |
| 646 | U+E6B7 | |
| 647 | U+E6B8 | |
| 648 | U+E700 | |
| 649 | U+E701 | |
| 650 | U+E702 | |
| 651 | U+E703 | |
| 652 | U+E704 | |
| 653 | U+E705 | |
| 654 | U+E706 | |
| 655 | U+E707 | |
| 656 | U+E708 | |
| 657 | U+E709 | |
| 658 | U+E70A | |
| 659 | U+E70B | |
| 660 | U+E70C | |
| 661 | U+E70D | |
| 662 | U+E70E | |
| 663 | U+E70F | |
| 664 | U+E710 | |
| 665 | U+E711 | |
| 666 | U+E712 | |
| 667 | U+E713 | |
| 668 | U+E714 | |
| 669 | U+E715 | |
| 670 | U+E716 | |
| 671 | U+E717 | |
| 672 | U+E718 | |
| 673 | U+E719 | |
| 674 | U+E71A | |
| 675 | U+E71B | |
| 676 | U+E71C | |
| 677 | U+E71D | |
| 678 | U+E71E | |
| 679 | U+E71F | |
| 680 | U+E720 | |
| 681 | U+E721 | |
| 682 | U+E722 | |
| 683 | U+E723 | |
| 684 | U+E724 | |
| 685 | U+E725 | |
| 686 | U+E726 | |
| 687 | U+E727 | |
| 688 | U+E728 | |
| 689 | U+E729 | |
| 690 | U+E72A | |
| 691 | U+E72B | |
| 692 | U+E72C | |
| 693 | U+E72D | |
| 694 | U+E72E | |
| 695 | U+E72F | |
| 696 | U+E730 | |
| 697 | U+E731 | |
| 698 | U+E732 | |
| 699 | U+E733 | |
| 700 | U+E734 | |
| 701 | U+E735 | |
| 702 | U+E736 | |
| 703 | U+E737 | |
| 704 | U+E738 | |
| 705 | U+E739 | |
| 706 | U+E73A | |
| 707 | U+E73B | |
| 708 | U+E73C | |
| 709 | U+E73D | |
| 710 | U+E73E | |
| 711 | U+E73F | |
| 712 | U+E740 | |
| 713 | U+E741 | |
| 714 | U+E742 | |
| 715 | U+E743 | |
| 716 | U+E744 | |
| 717 | U+E745 | |
| 718 | U+E746 | |
| 719 | U+E747 | |
| 720 | U+E748 | |
| 721 | U+E749 | |
| 722 | U+E74A | |
| 723 | U+E74B | |
| 724 | U+E74C | |
| 725 | U+E74D | |
| 726 | U+E74E | |
| 727 | U+E74F | |
| 728 | U+E750 | |
| 729 | U+E751 | |
| 730 | U+E752 | |
| 731 | U+E753 | |
| 732 | U+E754 | |
| 733 | U+E755 | |
| 734 | U+E756 | |
| 735 | U+E757 | |
| 736 | U+E758 | |
| 737 | U+E759 | |
| 738 | U+E75A | |
| 739 | U+E75B | |
| 740 | U+E75C | |
| 741 | U+E75D | |
| 742 | U+E75E | |
| 743 | U+E75F | |
| 744 | U+E760 | |
| 745 | U+E761 | |
| 746 | U+E762 | |
| 747 | U+E763 | |
| 748 | U+E764 | |
| 749 | U+E765 | |
| 750 | U+E766 | |
| 751 | U+E767 | |
| 752 | U+E768 | |
| 753 | U+E769 | |
| 754 | U+E76A | |
| 755 | U+E76B | |
| 756 | U+E76C | |
| 757 | U+E76D | |
| 758 | U+E76E | |
| 759 | U+E76F | |
| 760 | U+E770 | |
| 761 | U+E771 | |
| 762 | U+E772 | |
| 763 | U+E773 | |
| 764 | U+E774 | |
| 765 | U+E775 | |
| 766 | U+E776 | |
| 767 | U+E777 | |
| 768 | U+E778 | |
| 769 | U+E779 | |
| 770 | U+E77A | |
| 771 | U+E77B | |
| 772 | U+E77C | |
| 773 | U+E77D | |
| 774 | U+E77E | |
| 775 | U+E77F | |
| 776 | U+E780 | |
| 777 | U+E781 | |
| 778 | U+E782 | |
| 779 | U+E783 | |
| 780 | U+E784 | |
| 781 | U+E785 | |
| 782 | U+E786 | |
| 783 | U+E787 | |
| 784 | U+E788 | |
| 785 | U+E789 | |
| 786 | U+E78A | |
| 787 | U+E78B | |
| 788 | U+E78C | |
| 789 | U+E78D | |
| 790 | U+E78E | |
| 791 | U+E78F | |
| 792 | U+E790 | |
| 793 | U+E791 | |
| 794 | U+E792 | |
| 795 | U+E793 | |
| 796 | U+E794 | |
| 797 | U+E795 | |
| 798 | U+E796 | |
| 799 | U+E797 | |
| 800 | U+E798 | |
| 801 | U+E799 | |
| 802 | U+E79A | |
| 803 | U+E79B | |
| 804 | U+E79C | |
| 805 | U+E79D | |
| 806 | U+E79E | |
| 807 | U+E79F | |
| 808 | U+E7A0 | |
| 809 | U+E7A1 | |
| 810 | U+E7A2 | |
| 811 | U+E7A3 | |
| 812 | U+E7A4 | |
| 813 | U+E7A5 | |
| 814 | U+E7A6 | |
| 815 | U+E7A7 | |
| 816 | U+E7A8 | |
| 817 | U+E7A9 | |
| 818 | U+E7AA | |
| 819 | U+E7AB | |
| 820 | U+E7AC | |
| 821 | U+E7AD | |
| 822 | U+E7AE | |
| 823 | U+E7AF | |
| 824 | U+E7B0 | |
| 825 | U+E7B1 | |
| 826 | U+E7B2 | |
| 827 | U+E7B3 | |
| 828 | U+E7B4 | |
| 829 | U+E7B5 | |
| 830 | U+E7B6 | |
| 831 | U+E7B7 | |
| 832 | U+E7B8 | |
| 833 | U+E7B9 | |
| 834 | U+E7BA | |
| 835 | U+E7BB | |
| 836 | U+E7BC | |
| 837 | U+E7BD | |
| 838 | U+E7BE | |
| 839 | U+E7BF | |
| 840 | U+E7C0 | |
| 841 | U+E7C1 | |
| 842 | U+E7C2 | |
| 843 | U+E7C3 | |
| 844 | U+E7C4 | |
| 845 | U+E7C5 | |
| 846 | U+E7C6 | |
| 847 | U+E7C7 | |
| 848 | U+E7C8 | |
| 849 | U+E7C9 | |
| 850 | U+E7CA | |
| 851 | U+E7CB | |
| 852 | U+E7CC | |
| 853 | U+E7CD | |
| 854 | U+E7CE | |
| 855 | U+E7CF | |
| 856 | U+E7D0 | |
| 857 | U+E7D1 | |
| 858 | U+E7D2 | |
| 859 | U+E7D3 | |
| 860 | U+E7D4 | |
| 861 | U+E7D5 | |
| 862 | U+E7D6 | |
| 863 | U+E7D7 | |
| 864 | U+E7D8 | |
| 865 | U+E7D9 | |
| 866 | U+E7DA | |
| 867 | U+E7DB | |
| 868 | U+E7DC | |
| 869 | U+E7DD | |
| 870 | U+E7DE | |
| 871 | U+E7DF | |
| 872 | U+E7E0 | |
| 873 | U+E7E1 | |
| 874 | U+E7E2 | |
| 875 | U+E7E3 | |
| 876 | U+E7E4 | |
| 877 | U+E7E5 | |
| 878 | U+E7E6 | |
| 879 | U+E7E7 | |
| 880 | U+E7E8 | |
| 881 | U+E7E9 | |
| 882 | U+E7EA | |
| 883 | U+E7EB | |
| 884 | U+E7EC | |
| 885 | U+E7ED | |
| 886 | U+E7EE | |
| 887 | U+E7EF | |
| 888 | U+E7F0 | |
| 889 | U+E7F1 | |
| 890 | U+E7F2 | |
| 891 | U+E7F3 | |
| 892 | U+E7F4 | |
| 893 | U+E7F5 | |
| 894 | U+E7F6 | |
| 895 | U+E7F7 | |
| 896 | U+E7F8 | |
| 897 | U+E7F9 | |
| 898 | U+E7FA | |
| 899 | U+E7FB | |
| 900 | U+E7FC | |
| 901 | U+E7FD | |
| 902 | U+E7FE | |
| 903 | U+E7FF | |
| 904 | U+E800 | |
| 905 | U+E801 | |
| 906 | U+E802 | |
| 907 | U+E803 | |
| 908 | U+E804 | |
| 909 | U+E805 | |
| 910 | U+E806 | |
| 911 | U+E807 | |
| 912 | U+E808 | |
| 913 | U+E809 | |
| 914 | U+E80A | |
| 915 | U+E80B | |
| 916 | U+E80C | |
| 917 | U+E80D | |
| 918 | U+E80E | |
| 919 | U+E80F | |
| 920 | U+E810 | |
| 921 | U+E811 | |
| 922 | U+E812 | |
| 923 | U+E813 | |
| 924 | U+E814 | |
| 925 | U+E815 | |
| 926 | U+E816 | |
| 927 | U+E817 | |
| 928 | U+E818 | |
| 929 | U+E819 | |
| 930 | U+E81A | |
| 931 | U+E81B | |
| 932 | U+E81C | |
| 933 | U+E81D | |
| 934 | U+E81E | |
| 935 | U+E81F | |
| 936 | U+E820 | |
| 937 | U+E821 | |
| 938 | U+E822 | |
| 939 | U+E823 | |
| 940 | U+E824 | |
| 941 | U+E825 | |
| 942 | U+E826 | |
| 943 | U+E827 | |
| 944 | U+E828 | |
| 945 | U+E829 | |
| 946 | U+E82A | |
| 947 | U+E82B | |
| 948 | U+E82C | |
| 949 | U+E82D | |
| 950 | U+E82E | |
| 951 | U+E82F | |
| 952 | U+E830 | |
| 953 | U+E831 | |
| 954 | U+E832 | |
| 955 | U+E833 | |
| 956 | U+E834 | |
| 957 | U+E835 | |
| 958 | U+E836 | |
| 959 | U+E837 | |
| 960 | U+E838 | |
| 961 | U+E839 | |
| 962 | U+E83A | |
| 963 | U+E83B | |
| 964 | U+E83C | |
| 965 | U+E83D | |
| 966 | U+E83E | |
| 967 | U+E83F | |
| 968 | U+E840 | |
| 969 | U+E841 | |
| 970 | U+E842 | |
| 971 | U+E843 | |
| 972 | U+E844 | |
| 973 | U+E845 | |
| 974 | U+E846 | |
| 975 | U+E847 | |
| 976 | U+E848 | |
| 977 | U+E849 | |
| 978 | U+E84A | |
| 979 | U+E84B | |
| 980 | U+E84C | |
| 981 | U+E84D | |
| 982 | U+E84E | |
| 983 | U+E84F | |
| 984 | U+E850 | |
| 985 | U+E851 | |
| 986 | U+E852 | |
| 987 | U+E853 | |
| 988 | U+E854 | |
| 989 | U+E855 | |
| 990 | U+E856 | |
| 991 | U+E857 | |
| 992 | U+E858 | |
| 993 | U+E859 | |
| 994 | U+E85A | |
| 995 | U+E85B | |
| 996 | U+E85C | |
| 997 | U+E85D | |
| 998 | U+E85E | |
| 999 | U+E85F | |
| 1000 | U+E860 | |
| 1001 | U+E861 | |
| 1002 | U+E862 | |
| 1003 | U+E863 | |
| 1004 | U+E864 | |
| 1005 | U+E865 | |
| 1006 | U+E866 | |
| 1007 | U+E867 | |
| 1008 | U+E868 | |
| 1009 | U+E869 | |
| 1010 | U+E86A | |
| 1011 | U+E86B | |
| 1012 | U+E86C | |
| 1013 | U+E86D | |
| 1014 | U+E86E | |
| 1015 | U+E86F | |
| 1016 | U+E870 | |
| 1017 | U+E871 | |
| 1018 | U+E872 | |
| 1019 | U+E873 | |
| 1020 | U+E874 | |
| 1021 | U+E875 | |
| 1022 | U+E876 | |
| 1023 | U+E877 | |
| 1024 | U+E878 | |
| 1025 | U+E879 | |
| 1026 | U+E87A | |
| 1027 | U+E87B | |
| 1028 | U+E87C | |
| 1029 | U+E87D | |
| 1030 | U+E87E | |
| 1031 | U+E87F | |
| 1032 | U+E880 | |
| 1033 | U+E881 | |
| 1034 | U+E882 | |
| 1035 | U+E883 | |
| 1036 | U+E884 | |
| 1037 | U+E885 | |
| 1038 | U+E886 | |
| 1039 | U+E887 | |
| 1040 | U+E888 | |
| 1041 | U+E889 | |
| 1042 | U+E88A | |
| 1043 | U+E88B | |
| 1044 | U+E88C | |
| 1045 | U+E88D | |
| 1046 | U+E88E | |
| 1047 | U+E88F | |
| 1048 | U+E890 | |
| 1049 | U+E891 | |
| 1050 | U+E892 | |
| 1051 | U+E893 | |
| 1052 | U+E894 | |
| 1053 | U+E895 | |
| 1054 | U+E896 | |
| 1055 | U+E897 | |
| 1056 | U+E898 | |
| 1057 | U+E899 | |
| 1058 | U+E89A | |
| 1059 | U+E89B | |
| 1060 | U+E89C | |
| 1061 | U+E89D | |
| 1062 | U+E89E | |
| 1063 | U+E89F | |
| 1064 | U+E8A0 | |
| 1065 | U+E8A1 | |
| 1066 | U+E8A2 | |
| 1067 | U+E8A3 | |
| 1068 | U+E8A4 | |
| 1069 | U+E8A5 | |
| 1070 | U+E8A6 | |
| 1071 | U+E8A7 | |
| 1072 | U+E8A8 | |
| 1073 | U+E8A9 | |
| 1074 | U+E8AA | |
| 1075 | U+E8AB | |
| 1076 | U+E8AC | |
| 1077 | U+E8AD | |
| 1078 | U+E8AE | |
| 1079 | U+E8AF | |
| 1080 | U+E8B0 | |
| 1081 | U+E8B1 | |
| 1082 | U+E8B2 | |
| 1083 | U+E8B3 | |
| 1084 | U+E8B4 | |
| 1085 | U+E8B5 | |
| 1086 | U+E8B6 | |
| 1087 | U+E8B7 | |
| 1088 | U+E8B8 | |
| 1089 | U+E8B9 | |
| 1090 | U+E8BA | |
| 1091 | U+E8BB | |
| 1092 | U+E8BC | |
| 1093 | U+E8BD | |
| 1094 | U+E8BE | |
| 1095 | U+E8BF | |
| 1096 | U+E8C0 | |
| 1097 | U+E8C1 | |
| 1098 | U+E8C2 | |
| 1099 | U+E8C3 | |
| 1100 | U+E8C4 | |
| 1101 | U+E8C5 | |
| 1102 | U+E8C6 | |
| 1103 | U+E8C7 | |
| 1104 | U+E8C8 | |
| 1105 | U+E8C9 | |
| 1106 | U+E8CA | |
| 1107 | U+E8CB | |
| 1108 | U+E8CC | |
| 1109 | U+E8CD | |
| 1110 | U+E8CE | |
| 1111 | U+E8CF | |
| 1112 | U+E8D0 | |
| 1113 | U+E8D1 | |
| 1114 | U+E8D2 | |
| 1115 | U+E8D3 | |
| 1116 | U+E8D4 | |
| 1117 | U+E8D5 | |
| 1118 | U+E8D6 | |
| 1119 | U+E8D7 | |
| 1120 | U+E8D8 | |
| 1121 | U+E8D9 | |
| 1122 | U+E8DA | |
| 1123 | U+E8DB | |
| 1124 | U+E8DC | |
| 1125 | U+E8DD | |
| 1126 | U+E8DE | |
| 1127 | U+E8DF | |
| 1128 | U+E8E0 | |
| 1129 | U+E8E1 | |
| 1130 | U+E8E2 | |
| 1131 | U+E8E3 | |
| 1132 | U+E8E4 | |
| 1133 | U+E8E5 | |
| 1134 | U+E8E6 | |
| 1135 | U+E8E7 | |
| 1136 | U+E8E8 | |
| 1137 | U+E8E9 | |
| 1138 | U+E8EA | |
| 1139 | U+E8EB | |
| 1140 | U+E8EC | |
| 1141 | U+E8ED | |
| 1142 | U+E8EE | |
| 1143 | U+E8EF | |
| 1144 | U+EA60 | |
| 1145 | U+EA61 | |
| 1146 | U+EA62 | |
| 1147 | U+EA63 | |
| 1148 | U+EA64 | |
| 1149 | U+EA65 | |
| 1150 | U+EA66 | |
| 1151 | U+EA67 | |
| 1152 | U+EA68 | |
| 1153 | U+EA69 | |
| 1154 | U+EA6A | |
| 1155 | U+EA6B | |
| 1156 | U+EA6C | |
| 1157 | U+EA6D | |
| 1158 | U+EA6E | |
| 1159 | U+EA6F | |
| 1160 | U+EA70 | |
| 1161 | U+EA71 | |
| 1162 | U+EA72 | |
| 1163 | U+EA73 | |
| 1164 | U+EA74 | |
| 1165 | U+EA75 | |
| 1166 | U+EA76 | |
| 1167 | U+EA77 | |
| 1168 | U+EA78 | |
| 1169 | U+EA79 | |
| 1170 | U+EA7A | |
| 1171 | U+EA7B | |
| 1172 | U+EA7C | |
| 1173 | U+EA7D | |
| 1174 | U+EA7E | |
| 1175 | U+EA7F | |
| 1176 | U+EA80 | |
| 1177 | U+EA81 | |
| 1178 | U+EA82 | |
| 1179 | U+EA83 | |
| 1180 | U+EA84 | |
| 1181 | U+EA85 | |
| 1182 | U+EA86 | |
| 1183 | U+EA87 | |
| 1184 | U+EA88 | |
| 1185 | U+EA8A | |
| 1186 | U+EA8B | |
| 1187 | U+EA8C | |
| 1188 | U+EA8F | |
| 1189 | U+EA90 | |
| 1190 | U+EA91 | |
| 1191 | U+EA92 | |
| 1192 | U+EA93 | |
| 1193 | U+EA94 | |
| 1194 | U+EA95 | |
| 1195 | U+EA96 | |
| 1196 | U+EA97 | |
| 1197 | U+EA98 | |
| 1198 | U+EA99 | |
| 1199 | U+EA9A | |
| 1200 | U+EA9B | |
| 1201 | U+EA9C | |
| 1202 | U+EA9D | |
| 1203 | U+EA9E | |
| 1204 | U+EA9F | |
| 1205 | U+EAA0 | |
| 1206 | U+EAA1 | |
| 1207 | U+EAA2 | |
| 1208 | U+EAA3 | |
| 1209 | U+EAA4 | |
| 1210 | U+EAA5 | |
| 1211 | U+EAA6 | |
| 1212 | U+EAA7 | |
| 1213 | U+EAA8 | |
| 1214 | U+EAA9 | |
| 1215 | U+EAAA | |
| 1216 | U+EAAB | |
| 1217 | U+EAAC | |
| 1218 | U+EAAD | |
| 1219 | U+EAAE | |
| 1220 | U+EAAF | |
| 1221 | U+EAB0 | |
| 1222 | U+EAB1 | |
| 1223 | U+EAB2 | |
| 1224 | U+EAB3 | |
| 1225 | U+EAB4 | |
| 1226 | U+EAB5 | |
| 1227 | U+EAB6 | |
| 1228 | U+EAB7 | |
| 1229 | U+EAB8 | |
| 1230 | U+EAB9 | |
| 1231 | U+EABA | |
| 1232 | U+EABB | |
| 1233 | U+EABC | |
| 1234 | U+EABD | |
| 1235 | U+EABE | |
| 1236 | U+EABF | |
| 1237 | U+EAC0 | |
| 1238 | U+EAC1 | |
| 1239 | U+EAC2 | |
| 1240 | U+EAC3 | |
| 1241 | U+EAC4 | |
| 1242 | U+EAC5 | |
| 1243 | U+EAC6 | |
| 1244 | U+EAC7 | |
| 1245 | U+EAC9 | |
| 1246 | U+EACC | |
| 1247 | U+EACD | |
| 1248 | U+EACE | |
| 1249 | U+EACF | |
| 1250 | U+EAD0 | |
| 1251 | U+EAD1 | |
| 1252 | U+EAD2 | |
| 1253 | U+EAD3 | |
| 1254 | U+EAD4 | |
| 1255 | U+EAD5 | |
| 1256 | U+EAD6 | |
| 1257 | U+EAD7 | |
| 1258 | U+EAD8 | |
| 1259 | U+EAD9 | |
| 1260 | U+EADA | |
| 1261 | U+EADB | |
| 1262 | U+EADC | |
| 1263 | U+EADD | |
| 1264 | U+EADE | |
| 1265 | U+EADF | |
| 1266 | U+EAE0 | |
| 1267 | U+EAE1 | |
| 1268 | U+EAE2 | |
| 1269 | U+EAE3 | |
| 1270 | U+EAE4 | |
| 1271 | U+EAE5 | |
| 1272 | U+EAE6 | |
| 1273 | U+EAE7 | |
| 1274 | U+EAE8 | |
| 1275 | U+EAE9 | |
| 1276 | U+EAEA | |
| 1277 | U+EAEB | |
| 1278 | U+EAEC | |
| 1279 | U+EAED | |
| 1280 | U+EAEE | |
| 1281 | U+EAEF | |
| 1282 | U+EAF0 | |
| 1283 | U+EAF1 | |
| 1284 | U+EAF2 | |
| 1285 | U+EAF3 | |
| 1286 | U+EAF4 | |
| 1287 | U+EAF5 | |
| 1288 | U+EAF6 | |
| 1289 | U+EAF7 | |
| 1290 | U+EAF8 | |
| 1291 | U+EAF9 | |
| 1292 | U+EAFA | |
| 1293 | U+EAFB | |
| 1294 | U+EAFC | |
| 1295 | U+EAFD | |
| 1296 | U+EAFE | |
| 1297 | U+EAFF | |
| 1298 | U+EB00 | |
| 1299 | U+EB01 | |
| 1300 | U+EB02 | |
| 1301 | U+EB03 | |
| 1302 | U+EB04 | |
| 1303 | U+EB05 | |
| 1304 | U+EB06 | |
| 1305 | U+EB07 | |
| 1306 | U+EB08 | |
| 1307 | U+EB09 | |
| 1308 | U+EB0B | |
| 1309 | U+EB0C | |
| 1310 | U+EB0D | |
| 1311 | U+EB0E | |
| 1312 | U+EB0F | |
| 1313 | U+EB10 | |
| 1314 | U+EB11 | |
| 1315 | U+EB12 | |
| 1316 | U+EB13 | |
| 1317 | U+EB14 | |
| 1318 | U+EB15 | |
| 1319 | U+EB16 | |
| 1320 | U+EB17 | |
| 1321 | U+EB18 | |
| 1322 | U+EB19 | |
| 1323 | U+EB1A | |
| 1324 | U+EB1B | |
| 1325 | U+EB1C | |
| 1326 | U+EB1D | |
| 1327 | U+EB1E | |
| 1328 | U+EB1F | |
| 1329 | U+EB20 | |
| 1330 | U+EB21 | |
| 1331 | U+EB22 | |
| 1332 | U+EB23 | |
| 1333 | U+EB24 | |
| 1334 | U+EB25 | |
| 1335 | U+EB26 | |
| 1336 | U+EB27 | |
| 1337 | U+EB28 | |
| 1338 | U+EB29 | |
| 1339 | U+EB2A | |
| 1340 | U+EB2B | |
| 1341 | U+EB2C | |
| 1342 | U+EB2D | |
| 1343 | U+EB2E | |
| 1344 | U+EB2F | |
| 1345 | U+EB30 | |
| 1346 | U+EB31 | |
| 1347 | U+EB32 | |
| 1348 | U+EB33 | |
| 1349 | U+EB34 | |
| 1350 | U+EB35 | |
| 1351 | U+EB36 | |
| 1352 | U+EB37 | |
| 1353 | U+EB38 | |
| 1354 | U+EB39 | |
| 1355 | U+EB3A | |
| 1356 | U+EB3B | |
| 1357 | U+EB3C | |
| 1358 | U+EB3D | |
| 1359 | U+EB3E | |
| 1360 | U+EB3F | |
| 1361 | U+EB40 | |
| 1362 | U+EB41 | |
| 1363 | U+EB42 | |
| 1364 | U+EB43 | |
| 1365 | U+EB44 | |
| 1366 | U+EB45 | |
| 1367 | U+EB46 | |
| 1368 | U+EB47 | |
| 1369 | U+EB48 | |
| 1370 | U+EB49 | |
| 1371 | U+EB4A | |
| 1372 | U+EB4B | |
| 1373 | U+EB4C | |
| 1374 | U+EB4D | |
| 1375 | U+EB4E | |
| 1376 | U+EB50 | |
| 1377 | U+EB51 | |
| 1378 | U+EB52 | |
| 1379 | U+EB53 | |
| 1380 | U+EB54 | |
| 1381 | U+EB55 | |
| 1382 | U+EB56 | |
| 1383 | U+EB57 | |
| 1384 | U+EB58 | |
| 1385 | U+EB59 | |
| 1386 | U+EB5A | |
| 1387 | U+EB5B | |
| 1388 | U+EB5C | |
| 1389 | U+EB5D | |
| 1390 | U+EB5E | |
| 1391 | U+EB5F | |
| 1392 | U+EB60 | |
| 1393 | U+EB61 | |
| 1394 | U+EB62 | |
| 1395 | U+EB63 | |
| 1396 | U+EB64 | |
| 1397 | U+EB65 | |
| 1398 | U+EB66 | |
| 1399 | U+EB67 | |
| 1400 | U+EB68 | |
| 1401 | U+EB69 | |
| 1402 | U+EB6A | |
| 1403 | U+EB6B | |
| 1404 | U+EB6C | |
| 1405 | U+EB6D | |
| 1406 | U+EB6E | |
| 1407 | U+EB6F | |
| 1408 | U+EB70 | |
| 1409 | U+EB71 | |
| 1410 | U+EB72 | |
| 1411 | U+EB73 | |
| 1412 | U+EB74 | |
| 1413 | U+EB75 | |
| 1414 | U+EB76 | |
| 1415 | U+EB77 | |
| 1416 | U+EB78 | |
| 1417 | U+EB79 | |
| 1418 | U+EB7A | |
| 1419 | U+EB7B | |
| 1420 | U+EB7C | |
| 1421 | U+EB7D | |
| 1422 | U+EB7E | |
| 1423 | U+EB7F | |
| 1424 | U+EB80 | |
| 1425 | U+EB81 | |
| 1426 | U+EB82 | |
| 1427 | U+EB83 | |
| 1428 | U+EB84 | |
| 1429 | U+EB85 | |
| 1430 | U+EB86 | |
| 1431 | U+EB87 | |
| 1432 | U+EB88 | |
| 1433 | U+EB89 | |
| 1434 | U+EB8A | |
| 1435 | U+EB8B | |
| 1436 | U+EB8C | |
| 1437 | U+EB8D | |
| 1438 | U+EB8E | |
| 1439 | U+EB8F | |
| 1440 | U+EB90 | |
| 1441 | U+EB91 | |
| 1442 | U+EB92 | |
| 1443 | U+EB93 | |
| 1444 | U+EB94 | |
| 1445 | U+EB95 | |
| 1446 | U+EB96 | |
| 1447 | U+EB97 | |
| 1448 | U+EB98 | |
| 1449 | U+EB99 | |
| 1450 | U+EB9A | |
| 1451 | U+EB9B | |
| 1452 | U+EB9C | |
| 1453 | U+EB9D | |
| 1454 | U+EB9E | |
| 1455 | U+EB9F | |
| 1456 | U+EBA0 | |
| 1457 | U+EBA1 | |
| 1458 | U+EBA2 | |
| 1459 | U+EBA3 | |
| 1460 | U+EBA4 | |
| 1461 | U+EBA5 | |
| 1462 | U+EBA6 | |
| 1463 | U+EBA7 | |
| 1464 | U+EBA8 | |
| 1465 | U+EBA9 | |
| 1466 | U+EBAA | |
| 1467 | U+EBAB | |
| 1468 | U+EBAC | |
| 1469 | U+EBAD | |
| 1470 | U+EBAE | |
| 1471 | U+EBAF | |
| 1472 | U+EBB0 | |
| 1473 | U+EBB1 | |
| 1474 | U+EBB2 | |
| 1475 | U+EBB3 | |
| 1476 | U+EBB4 | |
| 1477 | U+EBB5 | |
| 1478 | U+EBB6 | |
| 1479 | U+EBB7 | |
| 1480 | U+EBB8 | |
| 1481 | U+EBB9 | |
| 1482 | U+EBBA | |
| 1483 | U+EBBB | |
| 1484 | U+EBBC | |
| 1485 | U+EBBD | |
| 1486 | U+EBBE | |
| 1487 | U+EBBF | |
| 1488 | U+EBC0 | |
| 1489 | U+EBC1 | |
| 1490 | U+EBC2 | |
| 1491 | U+EBC3 | |
| 1492 | U+EBC4 | |
| 1493 | U+EBC5 | |
| 1494 | U+EBC6 | |
| 1495 | U+EBC7 | |
| 1496 | U+EBC8 | |
| 1497 | U+EBC9 | |
| 1498 | U+EBCA | |
| 1499 | U+EBCB | |
| 1500 | U+EBCC | |
| 1501 | U+EBCD | |
| 1502 | U+EBCE | |
| 1503 | U+EBCF | |
| 1504 | U+EBD0 | |
| 1505 | U+EBD1 | |
| 1506 | U+EBD2 | |
| 1507 | U+EBD3 | |
| 1508 | U+EBD4 | |
| 1509 | U+EBD5 | |
| 1510 | U+EBD6 | |
| 1511 | U+EBD7 | |
| 1512 | U+EBD8 | |
| 1513 | U+EBD9 | |
| 1514 | U+EBDA | |
| 1515 | U+EBDB | |
| 1516 | U+EBDC | |
| 1517 | U+EBDD | |
| 1518 | U+EBDE | |
| 1519 | U+EBDF | |
| 1520 | U+EBE0 | |
| 1521 | U+EBE1 | |
| 1522 | U+EBE2 | |
| 1523 | U+EBE3 | |
| 1524 | U+EBE4 | |
| 1525 | U+EBE5 | |
| 1526 | U+EBE6 | |
| 1527 | U+EBE7 | |
| 1528 | U+EBE8 | |
| 1529 | U+EBE9 | |
| 1530 | U+EBEA | |
| 1531 | U+EBEB | |
| 1532 | U+EBEC | |
| 1533 | U+EBED | |
| 1534 | U+EBEE | |
| 1535 | U+EBEF | |
| 1536 | U+EBF0 | |
| 1537 | U+EBF1 | |
| 1538 | U+EBF2 | |
| 1539 | U+EBF3 | |
| 1540 | U+EBF4 | |
| 1541 | U+EBF5 | |
| 1542 | U+EBF6 | |
| 1543 | U+EBF7 | |
| 1544 | U+EBF8 | |
| 1545 | U+EBF9 | |
| 1546 | U+EBFA | |
| 1547 | U+EBFB | |
| 1548 | U+EBFC | |
| 1549 | U+EBFD | |
| 1550 | U+EBFE | |
| 1551 | U+EBFF | |
| 1552 | U+EC00 | |
| 1553 | U+EC01 | |
| 1554 | U+EC02 | |
| 1555 | U+EC03 | |
| 1556 | U+EC04 | |
| 1557 | U+EC05 | |
| 1558 | U+EC06 | |
| 1559 | U+EC07 | |
| 1560 | U+EC08 | |
| 1561 | U+EC09 | |
| 1562 | U+EC0A | |
| 1563 | U+EC0B | |
| 1564 | U+EC0C | |
| 1565 | U+EC0D | |
| 1566 | U+EC0E | |
| 1567 | U+EC0F | |
| 1568 | U+EC10 | |
| 1569 | U+EC11 | |
| 1570 | U+EC12 | |
| 1571 | U+EC13 | |
| 1572 | U+EC14 | |
| 1573 | U+EC15 | |
| 1574 | U+EC16 | |
| 1575 | U+EC17 | |
| 1576 | U+EC18 | |
| 1577 | U+EC19 | |
| 1578 | U+EC1A | |
| 1579 | U+EC1B | |
| 1580 | U+EC1C | |
| 1581 | U+EC1D | |
| 1582 | U+EC1E | |
| 1583 | U+ED00 | |
| 1584 | U+ED01 | |
| 1585 | U+ED02 | |
| 1586 | U+ED03 | |
| 1587 | U+ED04 | |
| 1588 | U+ED05 | |
| 1589 | U+ED06 | |
| 1590 | U+ED07 | |
| 1591 | U+ED08 | |
| 1592 | U+ED09 | |
| 1593 | U+ED0A | |
| 1594 | U+ED0B | |
| 1595 | U+ED0C | |
| 1596 | U+ED0D | |
| 1597 | U+ED0E | |
| 1598 | U+ED0F | |
| 1599 | U+ED10 | |
| 1600 | U+ED11 | |
| 1601 | U+ED12 | |
| 1602 | U+ED13 | |
| 1603 | U+ED14 | |
| 1604 | U+ED15 | |
| 1605 | U+ED16 | |
| 1606 | U+ED17 | |
| 1607 | U+ED18 | |
| 1608 | U+ED19 | |
| 1609 | U+ED1A | |
| 1610 | U+ED1B | |
| 1611 | U+ED1C | |
| 1612 | U+ED1D | |
| 1613 | U+ED1E | |
| 1614 | U+ED1F | |
| 1615 | U+ED20 | |
| 1616 | U+ED21 | |
| 1617 | U+ED22 | |
| 1618 | U+ED23 | |
| 1619 | U+ED24 | |
| 1620 | U+ED25 | |
| 1621 | U+ED26 | |
| 1622 | U+ED27 | |
| 1623 | U+ED28 | |
| 1624 | U+ED29 | |
| 1625 | U+ED2A | |
| 1626 | U+ED2B | |
| 1627 | U+ED2C | |
| 1628 | U+ED2D | |
| 1629 | U+ED2E | |
| 1630 | U+ED2F | |
| 1631 | U+ED30 | |
| 1632 | U+ED31 | |
| 1633 | U+ED32 | |
| 1634 | U+ED33 | |
| 1635 | U+ED34 | |
| 1636 | U+ED35 | |
| 1637 | U+ED36 | |
| 1638 | U+ED37 | |
| 1639 | U+ED38 | |
| 1640 | U+ED39 | |
| 1641 | U+ED3A | |
| 1642 | U+ED3B | |
| 1643 | U+ED3C | |
| 1644 | U+ED3D | |
| 1645 | U+ED3E | |
| 1646 | U+ED3F | |
| 1647 | U+ED40 | |
| 1648 | U+ED41 | |
| 1649 | U+ED42 | |
| 1650 | U+ED43 | |
| 1651 | U+ED44 | |
| 1652 | U+ED45 | |
| 1653 | U+ED46 | |
| 1654 | U+ED47 | |
| 1655 | U+ED48 | |
| 1656 | U+ED49 | |
| 1657 | U+ED4A | |
| 1658 | U+ED4B | |
| 1659 | U+ED4C | |
| 1660 | U+ED4D | |
| 1661 | U+ED4E | |
| 1662 | U+ED4F | |
| 1663 | U+ED50 | |
| 1664 | U+ED51 | |
| 1665 | U+ED52 | |
| 1666 | U+ED53 | |
| 1667 | U+ED54 | |
| 1668 | U+ED55 | |
| 1669 | U+ED56 | |
| 1670 | U+ED57 | |
| 1671 | U+ED58 | |
| 1672 | U+ED59 | |
| 1673 | U+ED5A | |
| 1674 | U+ED5B | |
| 1675 | U+ED5C | |
| 1676 | U+ED5D | |
| 1677 | U+ED5E | |
| 1678 | U+ED5F | |
| 1679 | U+ED60 | |
| 1680 | U+ED61 | |
| 1681 | U+ED62 | |
| 1682 | U+ED63 | |
| 1683 | U+ED64 | |
| 1684 | U+ED65 | |
| 1685 | U+ED66 | |
| 1686 | U+ED67 | |
| 1687 | U+ED68 | |
| 1688 | U+ED69 | |
| 1689 | U+ED6A | |
| 1690 | U+ED6B | |
| 1691 | U+ED6C | |
| 1692 | U+ED6D | |
| 1693 | U+ED6E | |
| 1694 | U+ED6F | |
| 1695 | U+ED70 | |
| 1696 | U+ED71 | |
| 1697 | U+ED72 | |
| 1698 | U+ED73 | |
| 1699 | U+ED74 | |
| 1700 | U+ED75 | |
| 1701 | U+ED76 | |
| 1702 | U+ED77 | |
| 1703 | U+ED78 | |
| 1704 | U+ED79 | |
| 1705 | U+ED7A | |
| 1706 | U+ED7B | |
| 1707 | U+ED7C | |
| 1708 | U+ED7D | |
| 1709 | U+ED7E | |
| 1710 | U+ED7F | |
| 1711 | U+ED80 | |
| 1712 | U+ED81 | |
| 1713 | U+ED82 | |
| 1714 | U+ED83 | |
| 1715 | U+ED84 | |
| 1716 | U+ED85 | |
| 1717 | U+ED86 | |
| 1718 | U+ED87 | |
| 1719 | U+ED88 | |
| 1720 | U+ED89 | |
| 1721 | U+ED8A | |
| 1722 | U+ED8B | |
| 1723 | U+ED8C | |
| 1724 | U+ED8D | |
| 1725 | U+ED8E | |
| 1726 | U+ED8F | |
| 1727 | U+ED90 | |
| 1728 | U+ED91 | |
| 1729 | U+ED92 | |
| 1730 | U+ED93 | |
| 1731 | U+ED94 | |
| 1732 | U+ED95 | |
| 1733 | U+ED96 | |
| 1734 | U+ED97 | |
| 1735 | U+ED98 | |
| 1736 | U+ED99 | |
| 1737 | U+ED9A | |
| 1738 | U+ED9B | |
| 1739 | U+ED9C | |
| 1740 | U+ED9D | |
| 1741 | U+ED9E | |
| 1742 | U+ED9F | |
| 1743 | U+EDA0 | |
| 1744 | U+EDA1 | |
| 1745 | U+EDA2 | |
| 1746 | U+EDA3 | |
| 1747 | U+EDA4 | |
| 1748 | U+EDA5 | |
| 1749 | U+EDA6 | |
| 1750 | U+EDA7 | |
| 1751 | U+EDA8 | |
| 1752 | U+EDA9 | |
| 1753 | U+EDAA | |
| 1754 | U+EDAB | |
| 1755 | U+EDAC | |
| 1756 | U+EDAD | |
| 1757 | U+EDAE | |
| 1758 | U+EDAF | |
| 1759 | U+EDB0 | |
| 1760 | U+EDB1 | |
| 1761 | U+EDB2 | |
| 1762 | U+EDB3 | |
| 1763 | U+EDB4 | |
| 1764 | U+EDB5 | |
| 1765 | U+EDB6 | |
| 1766 | U+EDB7 | |
| 1767 | U+EDB8 | |
| 1768 | U+EDB9 | |
| 1769 | U+EDBA | |
| 1770 | U+EDBB | |
| 1771 | U+EDBC | |
| 1772 | U+EDBD | |
| 1773 | U+EDBE | |
| 1774 | U+EDBF | |
| 1775 | U+EDC0 | |
| 1776 | U+EDC1 | |
| 1777 | U+EDC2 | |
| 1778 | U+EDC3 | |
| 1779 | U+EDC4 | |
| 1780 | U+EDC5 | |
| 1781 | U+EDC6 | |
| 1782 | U+EDC7 | |
| 1783 | U+EDC8 | |
| 1784 | U+EDC9 | |
| 1785 | U+EDCA | |
| 1786 | U+EDCB | |
| 1787 | U+EDCC | |
| 1788 | U+EDCD | |
| 1789 | U+EDCE | |
| 1790 | U+EDCF | |
| 1791 | U+EDD0 | |
| 1792 | U+EDD1 | |
| 1793 | U+EDD2 | |
| 1794 | U+EDD3 | |
| 1795 | U+EDD4 | |
| 1796 | U+EDD5 | |
| 1797 | U+EDD6 | |
| 1798 | U+EDD7 | |
| 1799 | U+EDD8 | |
| 1800 | U+EDD9 | |
| 1801 | U+EDDA | |
| 1802 | U+EDDB | |
| 1803 | U+EDDC | |
| 1804 | U+EDDD | |
| 1805 | U+EDDE | |
| 1806 | U+EDDF | |
| 1807 | U+EDE0 | |
| 1808 | U+EDE1 | |
| 1809 | U+EDE2 | |
| 1810 | U+EDE3 | |
| 1811 | U+EDE4 | |
| 1812 | U+EDE5 | |
| 1813 | U+EDE6 | |
| 1814 | U+EDE7 | |
| 1815 | U+EDE8 | |
| 1816 | U+EDE9 | |
| 1817 | U+EDEA | |
| 1818 | U+EDEB | |
| 1819 | U+EDEC | |
| 1820 | U+EDED | |
| 1821 | U+EDEE | |
| 1822 | U+EDEF | |
| 1823 | U+EDF0 | |
| 1824 | U+EDF1 | |
| 1825 | U+EDF2 | |
| 1826 | U+EDF3 | |
| 1827 | U+EDF4 | |
| 1828 | U+EDF5 | |
| 1829 | U+EDF6 | |
| 1830 | U+EDF7 | |
| 1831 | U+EDF8 | |
| 1832 | U+EDF9 | |
| 1833 | U+EDFA | |
| 1834 | U+EDFB | |
| 1835 | U+EDFC | |
| 1836 | U+EDFD | |
| 1837 | U+EDFE | |
| 1838 | U+EDFF | |
| 1839 | U+EE00 | |
| 1840 | U+EE01 | |
| 1841 | U+EE02 | |
| 1842 | U+EE03 | |
| 1843 | U+EE04 | |
| 1844 | U+EE05 | |
| 1845 | U+EE06 | |
| 1846 | U+EE07 | |
| 1847 | U+EE08 | |
| 1848 | U+EE09 | |
| 1849 | U+EE0A | |
| 1850 | U+EE0B | |
| 1851 | U+EE0C | |
| 1852 | U+EE0D | |
| 1853 | U+EE0E | |
| 1854 | U+EE0F | |
| 1855 | U+EE10 | |
| 1856 | U+EE11 | |
| 1857 | U+EE12 | |
| 1858 | U+EE13 | |
| 1859 | U+EE14 | |
| 1860 | U+EE15 | |
| 1861 | U+EE16 | |
| 1862 | U+EE17 | |
| 1863 | U+EE18 | |
| 1864 | U+EE19 | |
| 1865 | U+EE1A | |
| 1866 | U+EE1B | |
| 1867 | U+EE1C | |
| 1868 | U+EE1D | |
| 1869 | U+EE1E | |
| 1870 | U+EE1F | |
| 1871 | U+EE20 | |
| 1872 | U+EE21 | |
| 1873 | U+EE22 | |
| 1874 | U+EE23 | |
| 1875 | U+EE24 | |
| 1876 | U+EE25 | |
| 1877 | U+EE26 | |
| 1878 | U+EE27 | |
| 1879 | U+EE28 | |
| 1880 | U+EE29 | |
| 1881 | U+EE2A | |
| 1882 | U+EE2B | |
| 1883 | U+EE2C | |
| 1884 | U+EE2D | |
| 1885 | U+EE2E | |
| 1886 | U+EE2F | |
| 1887 | U+EE30 | |
| 1888 | U+EE31 | |
| 1889 | U+EE32 | |
| 1890 | U+EE33 | |
| 1891 | U+EE34 | |
| 1892 | U+EE35 | |
| 1893 | U+EE36 | |
| 1894 | U+EE37 | |
| 1895 | U+EE38 | |
| 1896 | U+EE39 | |
| 1897 | U+EE3A | |
| 1898 | U+EE3B | |
| 1899 | U+EE3C | |
| 1900 | U+EE3D | |
| 1901 | U+EE3E | |
| 1902 | U+EE3F | |
| 1903 | U+EE40 | |
| 1904 | U+EE41 | |
| 1905 | U+EE42 | |
| 1906 | U+EE43 | |
| 1907 | U+EE44 | |
| 1908 | U+EE45 | |
| 1909 | U+EE46 | |
| 1910 | U+EE47 | |
| 1911 | U+EE48 | |
| 1912 | U+EE49 | |
| 1913 | U+EE4A | |
| 1914 | U+EE4B | |
| 1915 | U+EE4C | |
| 1916 | U+EE4D | |
| 1917 | U+EE4E | |
| 1918 | U+EE4F | |
| 1919 | U+EE50 | |
| 1920 | U+EE51 | |
| 1921 | U+EE52 | |
| 1922 | U+EE53 | |
| 1923 | U+EE54 | |
| 1924 | U+EE55 | |
| 1925 | U+EE56 | |
| 1926 | U+EE57 | |
| 1927 | U+EE58 | |
| 1928 | U+EE59 | |
| 1929 | U+EE5A | |
| 1930 | U+EE5B | |
| 1931 | U+EE5C | |
| 1932 | U+EE5D | |
| 1933 | U+EE5E | |
| 1934 | U+EE5F | |
| 1935 | U+EE60 | |
| 1936 | U+EE61 | |
| 1937 | U+EE62 | |
| 1938 | U+EE63 | |
| 1939 | U+EE64 | |
| 1940 | U+EE65 | |
| 1941 | U+EE66 | |
| 1942 | U+EE67 | |
| 1943 | U+EE68 | |
| 1944 | U+EE69 | |
| 1945 | U+EE6A | |
| 1946 | U+EE6B | |
| 1947 | U+EE6C | |
| 1948 | U+EE6D | |
| 1949 | U+EE6E | |
| 1950 | U+EE6F | |
| 1951 | U+EE70 | |
| 1952 | U+EE71 | |
| 1953 | U+EE72 | |
| 1954 | U+EE73 | |
| 1955 | U+EE74 | |
| 1956 | U+EE75 | |
| 1957 | U+EE76 | |
| 1958 | U+EE77 | |
| 1959 | U+EE78 | |
| 1960 | U+EE79 | |
| 1961 | U+EE7A | |
| 1962 | U+EE7B | |
| 1963 | U+EE7C | |
| 1964 | U+EE7D | |
| 1965 | U+EE7E | |
| 1966 | U+EE7F | |
| 1967 | U+EE80 | |
| 1968 | U+EE81 | |
| 1969 | U+EE82 | |
| 1970 | U+EE83 | |
| 1971 | U+EE84 | |
| 1972 | U+EE85 | |
| 1973 | U+EE86 | |
| 1974 | U+EE87 | |
| 1975 | U+EE88 | |
| 1976 | U+EE89 | |
| 1977 | U+EE8A | |
| 1978 | U+EE8B | |
| 1979 | U+EE8C | |
| 1980 | U+EE8D | |
| 1981 | U+EE8E | |
| 1982 | U+EE8F | |
| 1983 | U+EE90 | |
| 1984 | U+EE91 | |
| 1985 | U+EE92 | |
| 1986 | U+EE93 | |
| 1987 | U+EE94 | |
| 1988 | U+EE95 | |
| 1989 | U+EE96 | |
| 1990 | U+EE97 | |
| 1991 | U+EE98 | |
| 1992 | U+EE99 | |
| 1993 | U+EE9A | |
| 1994 | U+EE9B | |
| 1995 | U+EE9C | |
| 1996 | U+EE9D | |
| 1997 | U+EE9E | |
| 1998 | U+EE9F | |
| 1999 | U+EEA0 | |
| 2000 | U+EEA1 | |
| 2001 | U+EEA2 | |
| 2002 | U+EEA3 | |
| 2003 | U+EEA4 | |
| 2004 | U+EEA5 | |
| 2005 | U+EEA6 | |
| 2006 | U+EEA7 | |
| 2007 | U+EEA8 | |
| 2008 | U+EEA9 | |
| 2009 | U+EEAA | |
| 2010 | U+EEAB | |
| 2011 | U+EEAC | |
| 2012 | U+EEAD | |
| 2013 | U+EEAE | |
| 2014 | U+EEAF | |
| 2015 | U+EEB0 | |
| 2016 | U+EEB1 | |
| 2017 | U+EEB2 | |
| 2018 | U+EEB3 | |
| 2019 | U+EEB4 | |
| 2020 | U+EEB5 | |
| 2021 | U+EEB6 | |
| 2022 | U+EEB7 | |
| 2023 | U+EEB8 | |
| 2024 | U+EEB9 | |
| 2025 | U+EEBA | |
| 2026 | U+EEBB | |
| 2027 | U+EEBC | |
| 2028 | U+EEBD | |
| 2029 | U+EEBE | |
| 2030 | U+EEBF | |
| 2031 | U+EEC0 | |
| 2032 | U+EEC1 | |
| 2033 | U+EEC2 | |
| 2034 | U+EEC3 | |
| 2035 | U+EEC4 | |
| 2036 | U+EEC5 | |
| 2037 | U+EEC6 | |
| 2038 | U+EEC7 | |
| 2039 | U+EEC8 | |
| 2040 | U+EEC9 | |
| 2041 | U+EECA | |
| 2042 | U+EECB | |
| 2043 | U+EECC | |
| 2044 | U+EECD | |
| 2045 | U+EECE | |
| 2046 | U+EECF | |
| 2047 | U+EED0 | |
| 2048 | U+EED1 | |
| 2049 | U+EED2 | |
| 2050 | U+EED3 | |
| 2051 | U+EED4 | |
| 2052 | U+EED5 | |
| 2053 | U+EED6 | |
| 2054 | U+EED7 | |
| 2055 | U+EED8 | |
| 2056 | U+EED9 | |
| 2057 | U+EEDA | |
| 2058 | U+EEDB | |
| 2059 | U+EEDC | |
| 2060 | U+EEDD | |
| 2061 | U+EEDE | |
| 2062 | U+EEDF | |
| 2063 | U+EEE0 | |
| 2064 | U+EEE1 | |
| 2065 | U+EEE2 | |
| 2066 | U+EEE3 | |
| 2067 | U+EEE4 | |
| 2068 | U+EEE5 | |
| 2069 | U+EEE6 | |
| 2070 | U+EEE7 | |
| 2071 | U+EEE8 | |
| 2072 | U+EEE9 | |
| 2073 | U+EEEA | |
| 2074 | U+EEEB | |
| 2075 | U+EEEC | |
| 2076 | U+EEED | |
| 2077 | U+EEEE | |
| 2078 | U+EEEF | |
| 2079 | U+EEF0 | |
| 2080 | U+EEF1 | |
| 2081 | U+EEF2 | |
| 2082 | U+EEF3 | |
| 2083 | U+EEF4 | |
| 2084 | U+EEF5 | |
| 2085 | U+EEF6 | |
| 2086 | U+EEF7 | |
| 2087 | U+EEF8 | |
| 2088 | U+EEF9 | |
| 2089 | U+EEFA | |
| 2090 | U+EEFB | |
| 2091 | U+EEFC | |
| 2092 | U+EEFD | |
| 2093 | U+EEFE | |
| 2094 | U+EEFF | |
| 2095 | U+EF00 | |
| 2096 | U+EF01 | |
| 2097 | U+EF02 | |
| 2098 | U+EF03 | |
| 2099 | U+EF04 | |
| 2100 | U+EF05 | |
| 2101 | U+EF06 | |
| 2102 | U+EF07 | |
| 2103 | U+EF08 | |
| 2104 | U+EF09 | |
| 2105 | U+EF0A | |
| 2106 | U+EF0B | |
| 2107 | U+EF0C | |
| 2108 | U+EF0D | |
| 2109 | U+EF0E | |
| 2110 | U+EF0F | |
| 2111 | U+EF10 | |
| 2112 | U+EF11 | |
| 2113 | U+EF12 | |
| 2114 | U+EF13 | |
| 2115 | U+EF14 | |
| 2116 | U+EF15 | |
| 2117 | U+EF16 | |
| 2118 | U+EF17 | |
| 2119 | U+EF18 | |
| 2120 | U+EF19 | |
| 2121 | U+EF1A | |
| 2122 | U+EF1B | |
| 2123 | U+EF1C | |
| 2124 | U+EF1D | |
| 2125 | U+EF1E | |
| 2126 | U+EF1F | |
| 2127 | U+EF20 | |
| 2128 | U+EF21 | |
| 2129 | U+EF22 | |
| 2130 | U+EF23 | |
| 2131 | U+EF24 | |
| 2132 | U+EF25 | |
| 2133 | U+EF26 | |
| 2134 | U+EF27 | |
| 2135 | U+EF28 | |
| 2136 | U+EF29 | |
| 2137 | U+EF2A | |
| 2138 | U+EF2B | |
| 2139 | U+EF2C | |
| 2140 | U+EF2D | |
| 2141 | U+EF2E | |
| 2142 | U+EF2F | |
| 2143 | U+EF30 | |
| 2144 | U+EF31 | |
| 2145 | U+EF32 | |
| 2146 | U+EF33 | |
| 2147 | U+EF34 | |
| 2148 | U+EF35 | |
| 2149 | U+EF36 | |
| 2150 | U+EF37 | |
| 2151 | U+EF38 | |
| 2152 | U+EF39 | |
| 2153 | U+EF3A | |
| 2154 | U+EF3B | |
| 2155 | U+EF3C | |
| 2156 | U+EF3D | |
| 2157 | U+EF3E | |
| 2158 | U+EF3F | |
| 2159 | U+EF40 | |
| 2160 | U+EF41 | |
| 2161 | U+EF42 | |
| 2162 | U+EF43 | |
| 2163 | U+EF44 | |
| 2164 | U+EF45 | |
| 2165 | U+EF46 | |
| 2166 | U+EF47 | |
| 2167 | U+EF48 | |
| 2168 | U+EF49 | |
| 2169 | U+EF4A | |
| 2170 | U+EF4B | |
| 2171 | U+EF4C | |
| 2172 | U+EF4D | |
| 2173 | U+EF4E | |
| 2174 | U+EF4F | |
| 2175 | U+EF50 | |
| 2176 | U+EF51 | |
| 2177 | U+EF52 | |
| 2178 | U+EF53 | |
| 2179 | U+EF54 | |
| 2180 | U+EF55 | |
| 2181 | U+EF56 | |
| 2182 | U+EF57 | |
| 2183 | U+EF58 | |
| 2184 | U+EF59 | |
| 2185 | U+EF5A | |
| 2186 | U+EF5B | |
| 2187 | U+EF5C | |
| 2188 | U+EF5D | |
| 2189 | U+EF5E | |
| 2190 | U+EF5F | |
| 2191 | U+EF60 | |
| 2192 | U+EF61 | |
| 2193 | U+EF62 | |
| 2194 | U+EF63 | |
| 2195 | U+EF64 | |
| 2196 | U+EF65 | |
| 2197 | U+EF66 | |
| 2198 | U+EF67 | |
| 2199 | U+EF68 | |
| 2200 | U+EF69 | |
| 2201 | U+EF6A | |
| 2202 | U+EF6B | |
| 2203 | U+EF6C | |
| 2204 | U+EF6D | |
| 2205 | U+EF6E | |
| 2206 | U+EF6F | |
| 2207 | U+EF70 | |
| 2208 | U+EF71 | |
| 2209 | U+EF72 | |
| 2210 | U+EF73 | |
| 2211 | U+EF74 | |
| 2212 | U+EF75 | |
| 2213 | U+EF76 | |
| 2214 | U+EF77 | |
| 2215 | U+EF78 | |
| 2216 | U+EF79 | |
| 2217 | U+EF7A | |
| 2218 | U+EF7B | |
| 2219 | U+EF7C | |
| 2220 | U+EF7D | |
| 2221 | U+EF7E | |
| 2222 | U+EF7F | |
| 2223 | U+EF80 | |
| 2224 | U+EF81 | |
| 2225 | U+EF82 | |
| 2226 | U+EF83 | |
| 2227 | U+EF84 | |
| 2228 | U+EF85 | |
| 2229 | U+EF86 | |
| 2230 | U+EF87 | |
| 2231 | U+EF88 | |
| 2232 | U+EF89 | |
| 2233 | U+EF8A | |
| 2234 | U+EF8B | |
| 2235 | U+EF8C | |
| 2236 | U+EF8D | |
| 2237 | U+EF8E | |
| 2238 | U+EF8F | |
| 2239 | U+EF90 | |
| 2240 | U+EF91 | |
| 2241 | U+EF92 | |
| 2242 | U+EF93 | |
| 2243 | U+EF94 | |
| 2244 | U+EF95 | |
| 2245 | U+EF96 | |
| 2246 | U+EF97 | |
| 2247 | U+EF98 | |
| 2248 | U+EF99 | |
| 2249 | U+EF9A | |
| 2250 | U+EF9B | |
| 2251 | U+EF9C | |
| 2252 | U+EF9D | |
| 2253 | U+EF9E | |
| 2254 | U+EF9F | |
| 2255 | U+EFA0 | |
| 2256 | U+EFA1 | |
| 2257 | U+EFA2 | |
| 2258 | U+EFA3 | |
| 2259 | U+EFA4 | |
| 2260 | U+EFA5 | |
| 2261 | U+EFA6 | |
| 2262 | U+EFA7 | |
| 2263 | U+EFA8 | |
| 2264 | U+EFA9 | |
| 2265 | U+EFAA | |
| 2266 | U+EFAB | |
| 2267 | U+EFAC | |
| 2268 | U+EFAD | |
| 2269 | U+EFAE | |
| 2270 | U+EFAF | |
| 2271 | U+EFB0 | |
| 2272 | U+EFB1 | |
| 2273 | U+EFB2 | |
| 2274 | U+EFB3 | |
| 2275 | U+EFB4 | |
| 2276 | U+EFB5 | |
| 2277 | U+EFB6 | |
| 2278 | U+EFB7 | |
| 2279 | U+EFB8 | |
| 2280 | U+EFB9 | |
| 2281 | U+EFBA | |
| 2282 | U+EFBB | |
| 2283 | U+EFBC | |
| 2284 | U+EFBD | |
| 2285 | U+EFBE | |
| 2286 | U+EFBF | |
| 2287 | U+EFC0 | |
| 2288 | U+EFC1 | |
| 2289 | U+EFC2 | |
| 2290 | U+EFC3 | |
| 2291 | U+EFC4 | |
| 2292 | U+EFC5 | |
| 2293 | U+EFC6 | |
| 2294 | U+EFC7 | |
| 2295 | U+EFC8 | |
| 2296 | U+EFC9 | |
| 2297 | U+EFCA | |
| 2298 | U+EFCB | |
| 2299 | U+EFCC | |
| 2300 | U+EFCD | |
| 2301 | U+EFCE | |
| 2302 | U+F000 | |
| 2303 | U+F001 | |
| 2304 | U+F002 | |
| 2305 | U+F003 | |
| 2306 | U+F004 | |
| 2307 | U+F005 | |
| 2308 | U+F006 | |
| 2309 | U+F007 | |
| 2310 | U+F008 | |
| 2311 | U+F009 | |
| 2312 | U+F00A | |
| 2313 | U+F00B | |
| 2314 | U+F00C | |
| 2315 | U+F00D | |
| 2316 | U+F00E | |
| 2317 | U+F00F | |
| 2318 | U+F010 | |
| 2319 | U+F011 | |
| 2320 | U+F012 | |
| 2321 | U+F013 | |
| 2322 | U+F014 | |
| 2323 | U+F015 | |
| 2324 | U+F016 | |
| 2325 | U+F017 | |
| 2326 | U+F018 | |
| 2327 | U+F019 | |
| 2328 | U+F01A | |
| 2329 | U+F01B | |
| 2330 | U+F01C | |
| 2331 | U+F01D | |
| 2332 | U+F01E | |
| 2333 | U+F01F | |
| 2334 | U+F020 | |
| 2335 | U+F021 | |
| 2336 | U+F022 | |
| 2337 | U+F023 | |
| 2338 | U+F024 | |
| 2339 | U+F025 | |
| 2340 | U+F026 | |
| 2341 | U+F027 | |
| 2342 | U+F028 | |
| 2343 | U+F029 | |
| 2344 | U+F02A | |
| 2345 | U+F02B | |
| 2346 | U+F02C | |
| 2347 | U+F02D | |
| 2348 | U+F02E | |
| 2349 | U+F02F | |
| 2350 | U+F030 | |
| 2351 | U+F031 | |
| 2352 | U+F032 | |
| 2353 | U+F033 | |
| 2354 | U+F034 | |
| 2355 | U+F035 | |
| 2356 | U+F036 | |
| 2357 | U+F037 | |
| 2358 | U+F038 | |
| 2359 | U+F039 | |
| 2360 | U+F03A | |
| 2361 | U+F03B | |
| 2362 | U+F03C | |
| 2363 | U+F03D | |
| 2364 | U+F03E | |
| 2365 | U+F03F | |
| 2366 | U+F040 | |
| 2367 | U+F041 | |
| 2368 | U+F042 | |
| 2369 | U+F043 | |
| 2370 | U+F044 | |
| 2371 | U+F045 | |
| 2372 | U+F046 | |
| 2373 | U+F047 | |
| 2374 | U+F048 | |
| 2375 | U+F049 | |
| 2376 | U+F04A | |
| 2377 | U+F04B | |
| 2378 | U+F04C | |
| 2379 | U+F04D | |
| 2380 | U+F04E | |
| 2381 | U+F04F | |
| 2382 | U+F050 | |
| 2383 | U+F051 | |
| 2384 | U+F052 | |
| 2385 | U+F053 | |
| 2386 | U+F054 | |
| 2387 | U+F055 | |
| 2388 | U+F056 | |
| 2389 | U+F057 | |
| 2390 | U+F058 | |
| 2391 | U+F059 | |
| 2392 | U+F05A | |
| 2393 | U+F05B | |
| 2394 | U+F05C | |
| 2395 | U+F05D | |
| 2396 | U+F05E | |
| 2397 | U+F05F | |
| 2398 | U+F060 | |
| 2399 | U+F061 | |
| 2400 | U+F062 | |
| 2401 | U+F063 | |
| 2402 | U+F064 | |
| 2403 | U+F065 | |
| 2404 | U+F066 | |
| 2405 | U+F067 | |
| 2406 | U+F068 | |
| 2407 | U+F069 | |
| 2408 | U+F06A | |
| 2409 | U+F06B | |
| 2410 | U+F06C | |
| 2411 | U+F06D | |
| 2412 | U+F06E | |
| 2413 | U+F06F | |
| 2414 | U+F070 | |
| 2415 | U+F071 | |
| 2416 | U+F072 | |
| 2417 | U+F073 | |
| 2418 | U+F074 | |
| 2419 | U+F075 | |
| 2420 | U+F076 | |
| 2421 | U+F077 | |
| 2422 | U+F078 | |
| 2423 | U+F079 | |
| 2424 | U+F07A | |
| 2425 | U+F07B | |
| 2426 | U+F07C | |
| 2427 | U+F07D | |
| 2428 | U+F07E | |
| 2429 | U+F07F | |
| 2430 | U+F080 | |
| 2431 | U+F081 | |
| 2432 | U+F082 | |
| 2433 | U+F083 | |
| 2434 | U+F084 | |
| 2435 | U+F085 | |
| 2436 | U+F086 | |
| 2437 | U+F087 | |
| 2438 | U+F088 | |
| 2439 | U+F089 | |
| 2440 | U+F08A | |
| 2441 | U+F08B | |
| 2442 | U+F08C | |
| 2443 | U+F08D | |
| 2444 | U+F08E | |
| 2445 | U+F08F | |
| 2446 | U+F090 | |
| 2447 | U+F091 | |
| 2448 | U+F092 | |
| 2449 | U+F093 | |
| 2450 | U+F094 | |
| 2451 | U+F095 | |
| 2452 | U+F096 | |
| 2453 | U+F097 | |
| 2454 | U+F098 | |
| 2455 | U+F099 | |
| 2456 | U+F09A | |
| 2457 | U+F09B | |
| 2458 | U+F09C | |
| 2459 | U+F09D | |
| 2460 | U+F09E | |
| 2461 | U+F09F | |
| 2462 | U+F0A0 | |
| 2463 | U+F0A1 | |
| 2464 | U+F0A2 | |
| 2465 | U+F0A3 | |
| 2466 | U+F0A4 | |
| 2467 | U+F0A5 | |
| 2468 | U+F0A6 | |
| 2469 | U+F0A7 | |
| 2470 | U+F0A8 | |
| 2471 | U+F0A9 | |
| 2472 | U+F0AA | |
| 2473 | U+F0AB | |
| 2474 | U+F0AC | |
| 2475 | U+F0AD | |
| 2476 | U+F0AE | |
| 2477 | U+F0AF | |
| 2478 | U+F0B0 | |
| 2479 | U+F0B1 | |
| 2480 | U+F0B2 | |
| 2481 | U+F0B3 | |
| 2482 | U+F0B4 | |
| 2483 | U+F0B5 | |
| 2484 | U+F0B6 | |
| 2485 | U+F0B7 | |
| 2486 | U+F0B8 | |
| 2487 | U+F0B9 | |
| 2488 | U+F0BA | |
| 2489 | U+F0BB | |
| 2490 | U+F0BC | |
| 2491 | U+F0BD | |
| 2492 | U+F0BE | |
| 2493 | U+F0BF | |
| 2494 | U+F0C0 | |
| 2495 | U+F0C1 | |
| 2496 | U+F0C2 | |
| 2497 | U+F0C3 | |
| 2498 | U+F0C4 | |
| 2499 | U+F0C5 | |
| 2500 | U+F0C6 | |
| 2501 | U+F0C7 | |
| 2502 | U+F0C8 | |
| 2503 | U+F0C9 | |
| 2504 | U+F0CA | |
| 2505 | U+F0CB | |
| 2506 | U+F0CC | |
| 2507 | U+F0CD | |
| 2508 | U+F0CE | |
| 2509 | U+F0CF | |
| 2510 | U+F0D0 | |
| 2511 | U+F0D1 | |
| 2512 | U+F0D2 | |
| 2513 | U+F0D3 | |
| 2514 | U+F0D4 | |
| 2515 | U+F0D5 | |
| 2516 | U+F0D6 | |
| 2517 | U+F0D7 | |
| 2518 | U+F0D8 | |
| 2519 | U+F0D9 | |
| 2520 | U+F0DA | |
| 2521 | U+F0DB | |
| 2522 | U+F0DC | |
| 2523 | U+F0DD | |
| 2524 | U+F0DE | |
| 2525 | U+F0DF | |
| 2526 | U+F0E0 | |
| 2527 | U+F0E1 | |
| 2528 | U+F0E2 | |
| 2529 | U+F0E3 | |
| 2530 | U+F0E4 | |
| 2531 | U+F0E5 | |
| 2532 | U+F0E6 | |
| 2533 | U+F0E7 | |
| 2534 | U+F0E8 | |
| 2535 | U+F0E9 | |
| 2536 | U+F0EA | |
| 2537 | U+F0EB | |
| 2538 | U+F0EC | |
| 2539 | U+F0ED | |
| 2540 | U+F0EE | |
| 2541 | U+F0EF | |
| 2542 | U+F0F0 | |
| 2543 | U+F0F1 | |
| 2544 | U+F0F2 | |
| 2545 | U+F0F3 | |
| 2546 | U+F0F4 | |
| 2547 | U+F0F5 | |
| 2548 | U+F0F6 | |
| 2549 | U+F0F7 | |
| 2550 | U+F0F8 | |
| 2551 | U+F0F9 | |
| 2552 | U+F0FA | |
| 2553 | U+F0FB | |
| 2554 | U+F0FC | |
| 2555 | U+F0FD | |
| 2556 | U+F0FE | |
| 2557 | U+F0FF | |
| 2558 | U+F100 | |
| 2559 | U+F101 | |
| 2560 | U+F102 | |
| 2561 | U+F103 | |
| 2562 | U+F104 | |
| 2563 | U+F105 | |
| 2564 | U+F106 | |
| 2565 | U+F107 | |
| 2566 | U+F108 | |
| 2567 | U+F109 | |
| 2568 | U+F10A | |
| 2569 | U+F10B | |
| 2570 | U+F10C | |
| 2571 | U+F10D | |
| 2572 | U+F10E | |
| 2573 | U+F10F | |
| 2574 | U+F110 | |
| 2575 | U+F111 | |
| 2576 | U+F112 | |
| 2577 | U+F113 | |
| 2578 | U+F114 | |
| 2579 | U+F115 | |
| 2580 | U+F116 | |
| 2581 | U+F117 | |
| 2582 | U+F118 | |
| 2583 | U+F119 | |
| 2584 | U+F11A | |
| 2585 | U+F11B | |
| 2586 | U+F11C | |
| 2587 | U+F11D | |
| 2588 | U+F11E | |
| 2589 | U+F11F | |
| 2590 | U+F120 | |
| 2591 | U+F121 | |
| 2592 | U+F122 | |
| 2593 | U+F123 | |
| 2594 | U+F124 | |
| 2595 | U+F125 | |
| 2596 | U+F126 | |
| 2597 | U+F127 | |
| 2598 | U+F128 | |
| 2599 | U+F129 | |
| 2600 | U+F12A | |
| 2601 | U+F12B | |
| 2602 | U+F12C | |
| 2603 | U+F12D | |
| 2604 | U+F12E | |
| 2605 | U+F12F | |
| 2606 | U+F130 | |
| 2607 | U+F131 | |
| 2608 | U+F132 | |
| 2609 | U+F133 | |
| 2610 | U+F134 | |
| 2611 | U+F135 | |
| 2612 | U+F136 | |
| 2613 | U+F137 | |
| 2614 | U+F138 | |
| 2615 | U+F139 | |
| 2616 | U+F13A | |
| 2617 | U+F13B | |
| 2618 | U+F13C | |
| 2619 | U+F13D | |
| 2620 | U+F13E | |
| 2621 | U+F13F | |
| 2622 | U+F140 | |
| 2623 | U+F141 | |
| 2624 | U+F142 | |
| 2625 | U+F143 | |
| 2626 | U+F144 | |
| 2627 | U+F145 | |
| 2628 | U+F146 | |
| 2629 | U+F147 | |
| 2630 | U+F148 | |
| 2631 | U+F149 | |
| 2632 | U+F14A | |
| 2633 | U+F14B | |
| 2634 | U+F14C | |
| 2635 | U+F14D | |
| 2636 | U+F14E | |
| 2637 | U+F14F | |
| 2638 | U+F150 | |
| 2639 | U+F151 | |
| 2640 | U+F152 | |
| 2641 | U+F153 | |
| 2642 | U+F154 | |
| 2643 | U+F155 | |
| 2644 | U+F156 | |
| 2645 | U+F157 | |
| 2646 | U+F158 | |
| 2647 | U+F159 | |
| 2648 | U+F15A | |
| 2649 | U+F15B | |
| 2650 | U+F15C | |
| 2651 | U+F15D | |
| 2652 | U+F15E | |
| 2653 | U+F15F | |
| 2654 | U+F160 | |
| 2655 | U+F161 | |
| 2656 | U+F162 | |
| 2657 | U+F163 | |
| 2658 | U+F164 | |
| 2659 | U+F165 | |
| 2660 | U+F166 | |
| 2661 | U+F167 | |
| 2662 | U+F168 | |
| 2663 | U+F169 | |
| 2664 | U+F16A | |
| 2665 | U+F16B | |
| 2666 | U+F16C | |
| 2667 | U+F16D | |
| 2668 | U+F16E | |
| 2669 | U+F16F | |
| 2670 | U+F170 | |
| 2671 | U+F171 | |
| 2672 | U+F172 | |
| 2673 | U+F173 | |
| 2674 | U+F174 | |
| 2675 | U+F175 | |
| 2676 | U+F176 | |
| 2677 | U+F177 | |
| 2678 | U+F178 | |
| 2679 | U+F179 | |
| 2680 | U+F17A | |
| 2681 | U+F17B | |
| 2682 | U+F17C | |
| 2683 | U+F17D | |
| 2684 | U+F17E | |
| 2685 | U+F17F | |
| 2686 | U+F180 | |
| 2687 | U+F181 | |
| 2688 | U+F182 | |
| 2689 | U+F183 | |
| 2690 | U+F184 | |
| 2691 | U+F185 | |
| 2692 | U+F186 | |
| 2693 | U+F187 | |
| 2694 | U+F188 | |
| 2695 | U+F189 | |
| 2696 | U+F18A | |
| 2697 | U+F18B | |
| 2698 | U+F18C | |
| 2699 | U+F18D | |
| 2700 | U+F18E | |
| 2701 | U+F18F | |
| 2702 | U+F190 | |
| 2703 | U+F191 | |
| 2704 | U+F192 | |
| 2705 | U+F193 | |
| 2706 | U+F194 | |
| 2707 | U+F195 | |
| 2708 | U+F196 | |
| 2709 | U+F197 | |
| 2710 | U+F198 | |
| 2711 | U+F199 | |
| 2712 | U+F19A | |
| 2713 | U+F19B | |
| 2714 | U+F19C | |
| 2715 | U+F19D | |
| 2716 | U+F19E | |
| 2717 | U+F19F | |
| 2718 | U+F1A0 | |
| 2719 | U+F1A1 | |
| 2720 | U+F1A2 | |
| 2721 | U+F1A3 | |
| 2722 | U+F1A4 | |
| 2723 | U+F1A5 | |
| 2724 | U+F1A6 | |
| 2725 | U+F1A7 | |
| 2726 | U+F1A8 | |
| 2727 | U+F1A9 | |
| 2728 | U+F1AA | |
| 2729 | U+F1AB | |
| 2730 | U+F1AC | |
| 2731 | U+F1AD | |
| 2732 | U+F1AE | |
| 2733 | U+F1AF | |
| 2734 | U+F1B0 | |
| 2735 | U+F1B1 | |
| 2736 | U+F1B2 | |
| 2737 | U+F1B3 | |
| 2738 | U+F1B4 | |
| 2739 | U+F1B5 | |
| 2740 | U+F1B6 | |
| 2741 | U+F1B7 | |
| 2742 | U+F1B8 | |
| 2743 | U+F1B9 | |
| 2744 | U+F1BA | |
| 2745 | U+F1BB | |
| 2746 | U+F1BC | |
| 2747 | U+F1BD | |
| 2748 | U+F1BE | |
| 2749 | U+F1BF | |
| 2750 | U+F1C0 | |
| 2751 | U+F1C1 | |
| 2752 | U+F1C2 | |
| 2753 | U+F1C3 | |
| 2754 | U+F1C4 | |
| 2755 | U+F1C5 | |
| 2756 | U+F1C6 | |
| 2757 | U+F1C7 | |
| 2758 | U+F1C8 | |
| 2759 | U+F1C9 | |
| 2760 | U+F1CA | |
| 2761 | U+F1CB | |
| 2762 | U+F1CC | |
| 2763 | U+F1CD | |
| 2764 | U+F1CE | |
| 2765 | U+F1CF | |
| 2766 | U+F1D0 | |
| 2767 | U+F1D1 | |
| 2768 | U+F1D2 | |
| 2769 | U+F1D3 | |
| 2770 | U+F1D4 | |
| 2771 | U+F1D5 | |
| 2772 | U+F1D6 | |
| 2773 | U+F1D7 | |
| 2774 | U+F1D8 | |
| 2775 | U+F1D9 | |
| 2776 | U+F1DA | |
| 2777 | U+F1DB | |
| 2778 | U+F1DC | |
| 2779 | U+F1DD | |
| 2780 | U+F1DE | |
| 2781 | U+F1DF | |
| 2782 | U+F1E0 | |
| 2783 | U+F1E1 | |
| 2784 | U+F1E2 | |
| 2785 | U+F1E3 | |
| 2786 | U+F1E4 | |
| 2787 | U+F1E5 | |
| 2788 | U+F1E6 | |
| 2789 | U+F1E7 | |
| 2790 | U+F1E8 | |
| 2791 | U+F1E9 | |
| 2792 | U+F1EA | |
| 2793 | U+F1EB | |
| 2794 | U+F1EC | |
| 2795 | U+F1ED | |
| 2796 | U+F1EE | |
| 2797 | U+F1EF | |
| 2798 | U+F1F0 | |
| 2799 | U+F1F1 | |
| 2800 | U+F1F2 | |
| 2801 | U+F1F3 | |
| 2802 | U+F1F4 | |
| 2803 | U+F1F5 | |
| 2804 | U+F1F6 | |
| 2805 | U+F1F7 | |
| 2806 | U+F1F8 | |
| 2807 | U+F1F9 | |
| 2808 | U+F1FA | |
| 2809 | U+F1FB | |
| 2810 | U+F1FC | |
| 2811 | U+F1FD | |
| 2812 | U+F1FE | |
| 2813 | U+F1FF | |
| 2814 | U+F200 | |
| 2815 | U+F201 | |
| 2816 | U+F202 | |
| 2817 | U+F203 | |
| 2818 | U+F204 | |
| 2819 | U+F205 | |
| 2820 | U+F206 | |
| 2821 | U+F207 | |
| 2822 | U+F208 | |
| 2823 | U+F209 | |
| 2824 | U+F20A | |
| 2825 | U+F20B | |
| 2826 | U+F20C | |
| 2827 | U+F20D | |
| 2828 | U+F20E | |
| 2829 | U+F20F | |
| 2830 | U+F210 | |
| 2831 | U+F211 | |
| 2832 | U+F212 | |
| 2833 | U+F213 | |
| 2834 | U+F214 | |
| 2835 | U+F215 | |
| 2836 | U+F216 | |
| 2837 | U+F217 | |
| 2838 | U+F218 | |
| 2839 | U+F219 | |
| 2840 | U+F21A | |
| 2841 | U+F21B | |
| 2842 | U+F21C | |
| 2843 | U+F21D | |
| 2844 | U+F21E | |
| 2845 | U+F21F | |
| 2846 | U+F220 | |
| 2847 | U+F221 | |
| 2848 | U+F222 | |
| 2849 | U+F223 | |
| 2850 | U+F224 | |
| 2851 | U+F225 | |
| 2852 | U+F226 | |
| 2853 | U+F227 | |
| 2854 | U+F228 | |
| 2855 | U+F229 | |
| 2856 | U+F22A | |
| 2857 | U+F22B | |
| 2858 | U+F22C | |
| 2859 | U+F22D | |
| 2860 | U+F22E | |
| 2861 | U+F22F | |
| 2862 | U+F230 | |
| 2863 | U+F231 | |
| 2864 | U+F232 | |
| 2865 | U+F233 | |
| 2866 | U+F234 | |
| 2867 | U+F235 | |
| 2868 | U+F236 | |
| 2869 | U+F237 | |
| 2870 | U+F238 | |
| 2871 | U+F239 | |
| 2872 | U+F23A | |
| 2873 | U+F23B | |
| 2874 | U+F23C | |
| 2875 | U+F23D | |
| 2876 | U+F23E | |
| 2877 | U+F23F | |
| 2878 | U+F240 | |
| 2879 | U+F241 | |
| 2880 | U+F242 | |
| 2881 | U+F243 | |
| 2882 | U+F244 | |
| 2883 | U+F245 | |
| 2884 | U+F246 | |
| 2885 | U+F247 | |
| 2886 | U+F248 | |
| 2887 | U+F249 | |
| 2888 | U+F24A | |
| 2889 | U+F24B | |
| 2890 | U+F24C | |
| 2891 | U+F24D | |
| 2892 | U+F24E | |
| 2893 | U+F24F | |
| 2894 | U+F250 | |
| 2895 | U+F251 | |
| 2896 | U+F252 | |
| 2897 | U+F253 | |
| 2898 | U+F254 | |
| 2899 | U+F255 | |
| 2900 | U+F256 | |
| 2901 | U+F257 | |
| 2902 | U+F258 | |
| 2903 | U+F259 | |
| 2904 | U+F25A | |
| 2905 | U+F25B | |
| 2906 | U+F25C | |
| 2907 | U+F25D | |
| 2908 | U+F25E | |
| 2909 | U+F25F | |
| 2910 | U+F260 | |
| 2911 | U+F261 | |
| 2912 | U+F262 | |
| 2913 | U+F263 | |
| 2914 | U+F264 | |
| 2915 | U+F265 | |
| 2916 | U+F266 | |
| 2917 | U+F267 | |
| 2918 | U+F268 | |
| 2919 | U+F269 | |
| 2920 | U+F26A | |
| 2921 | U+F26B | |
| 2922 | U+F26C | |
| 2923 | U+F26D | |
| 2924 | U+F26E | |
| 2925 | U+F26F | |
| 2926 | U+F270 | |
| 2927 | U+F271 | |
| 2928 | U+F272 | |
| 2929 | U+F273 | |
| 2930 | U+F274 | |
| 2931 | U+F275 | |
| 2932 | U+F276 | |
| 2933 | U+F277 | |
| 2934 | U+F278 | |
| 2935 | U+F279 | |
| 2936 | U+F27A | |
| 2937 | U+F27B | |
| 2938 | U+F27C | |
| 2939 | U+F27D | |
| 2940 | U+F27E | |
| 2941 | U+F27F | |
| 2942 | U+F280 | |
| 2943 | U+F281 | |
| 2944 | U+F282 | |
| 2945 | U+F283 | |
| 2946 | U+F284 | |
| 2947 | U+F285 | |
| 2948 | U+F286 | |
| 2949 | U+F287 | |
| 2950 | U+F288 | |
| 2951 | U+F289 | |
| 2952 | U+F28A | |
| 2953 | U+F28B | |
| 2954 | U+F28C | |
| 2955 | U+F28D | |
| 2956 | U+F28E | |
| 2957 | U+F28F | |
| 2958 | U+F290 | |
| 2959 | U+F291 | |
| 2960 | U+F292 | |
| 2961 | U+F293 | |
| 2962 | U+F294 | |
| 2963 | U+F295 | |
| 2964 | U+F296 | |
| 2965 | U+F297 | |
| 2966 | U+F298 | |
| 2967 | U+F299 | |
| 2968 | U+F29A | |
| 2969 | U+F29B | |
| 2970 | U+F29C | |
| 2971 | U+F29D | |
| 2972 | U+F29E | |
| 2973 | U+F29F | |
| 2974 | U+F2A0 | |
| 2975 | U+F2A1 | |
| 2976 | U+F2A2 | |
| 2977 | U+F2A3 | |
| 2978 | U+F2A4 | |
| 2979 | U+F2A5 | |
| 2980 | U+F2A6 | |
| 2981 | U+F2A7 | |
| 2982 | U+F2A8 | |
| 2983 | U+F2A9 | |
| 2984 | U+F2AA | |
| 2985 | U+F2AB | |
| 2986 | U+F2AC | |
| 2987 | U+F2AD | |
| 2988 | U+F2AE | |
| 2989 | U+F2AF | |
| 2990 | U+F2B0 | |
| 2991 | U+F2B1 | |
| 2992 | U+F2B2 | |
| 2993 | U+F2B3 | |
| 2994 | U+F2B4 | |
| 2995 | U+F2B5 | |
| 2996 | U+F2B6 | |
| 2997 | U+F2B7 | |
| 2998 | U+F2B8 | |
| 2999 | U+F2B9 | |
| 3000 | U+F2BA | |
| 3001 | U+F2BB | |
| 3002 | U+F2BC | |
| 3003 | U+F2BD | |
| 3004 | U+F2BE | |
| 3005 | U+F2BF | |
| 3006 | U+F2C0 | |
| 3007 | U+F2C1 | |
| 3008 | U+F2C2 | |
| 3009 | U+F2C3 | |
| 3010 | U+F2C4 | |
| 3011 | U+F2C5 | |
| 3012 | U+F2C6 | |
| 3013 | U+F2C7 | |
| 3014 | U+F2C8 | |
| 3015 | U+F2C9 | |
| 3016 | U+F2CA | |
| 3017 | U+F2CB | |
| 3018 | U+F2CC | |
| 3019 | U+F2CD | |
| 3020 | U+F2CE | |
| 3021 | U+F2CF | |
| 3022 | U+F2D0 | |
| 3023 | U+F2D1 | |
| 3024 | U+F2D2 | |
| 3025 | U+F2D3 | |
| 3026 | U+F2D4 | |
| 3027 | U+F2D5 | |
| 3028 | U+F2D6 | |
| 3029 | U+F2D7 | |
| 3030 | U+F2D8 | |
| 3031 | U+F2D9 | |
| 3032 | U+F2DA | |
| 3033 | U+F2DB | |
| 3034 | U+F2DC | |
| 3035 | U+F2DD | |
| 3036 | U+F2DE | |
| 3037 | U+F2DF | |
| 3038 | U+F2E0 | |
| 3039 | U+F2E1 | |
| 3040 | U+F2E2 | |
| 3041 | U+F2E3 | |
| 3042 | U+F2E4 | |
| 3043 | U+F2E5 | |
| 3044 | U+F2E6 | |
| 3045 | U+F2E7 | |
| 3046 | U+F2E8 | |
| 3047 | U+F2E9 | |
| 3048 | U+F2EA | |
| 3049 | U+F2EB | |
| 3050 | U+F2EC | |
| 3051 | U+F2ED | |
| 3052 | U+F2EE | |
| 3053 | U+F2EF | |
| 3054 | U+F2F0 | |
| 3055 | U+F2F1 | |
| 3056 | U+F2F2 | |
| 3057 | U+F2F3 | |
| 3058 | U+F2F4 | |
| 3059 | U+F2F5 | |
| 3060 | U+F2F6 | |
| 3061 | U+F2F7 | |
| 3062 | U+F2F8 | |
| 3063 | U+F2F9 | |
| 3064 | U+F2FA | |
| 3065 | U+F2FB | |
| 3066 | U+F2FC | |
| 3067 | U+F2FD | |
| 3068 | U+F2FE | |
| 3069 | U+F2FF | |
| 3070 | U+F400 | |
| 3071 | U+F401 | |
| 3072 | U+F402 | |
| 3073 | U+F403 | |
| 3074 | U+F404 | |
| 3075 | U+F405 | |
| 3076 | U+F406 | |
| 3077 | U+F407 | |
| 3078 | U+F408 | |
| 3079 | U+F409 | |
| 3080 | U+F40A | |
| 3081 | U+F40B | |
| 3082 | U+F40C | |
| 3083 | U+F40D | |
| 3084 | U+F40E | |
| 3085 | U+F40F | |
| 3086 | U+F410 | |
| 3087 | U+F411 | |
| 3088 | U+F412 | |
| 3089 | U+F413 | |
| 3090 | U+F414 | |
| 3091 | U+F415 | |
| 3092 | U+F416 | |
| 3093 | U+F417 | |
| 3094 | U+F418 | |
| 3095 | U+F419 | |
| 3096 | U+F41A | |
| 3097 | U+F41B | |
| 3098 | U+F41C | |
| 3099 | U+F41D | |
| 3100 | U+F41E | |
| 3101 | U+F41F | |
| 3102 | U+F420 | |
| 3103 | U+F421 | |
| 3104 | U+F422 | |
| 3105 | U+F423 | |
| 3106 | U+F424 | |
| 3107 | U+F425 | |
| 3108 | U+F426 | |
| 3109 | U+F427 | |
| 3110 | U+F428 | |
| 3111 | U+F429 | |
| 3112 | U+F42A | |
| 3113 | U+F42B | |
| 3114 | U+F42C | |
| 3115 | U+F42D | |
| 3116 | U+F42E | |
| 3117 | U+F42F | |
| 3118 | U+F430 | |
| 3119 | U+F431 | |
| 3120 | U+F432 | |
| 3121 | U+F433 | |
| 3122 | U+F434 | |
| 3123 | U+F435 | |
| 3124 | U+F436 | |
| 3125 | U+F437 | |
| 3126 | U+F438 | |
| 3127 | U+F439 | |
| 3128 | U+F43A | |
| 3129 | U+F43B | |
| 3130 | U+F43C | |
| 3131 | U+F43D | |
| 3132 | U+F43E | |
| 3133 | U+F43F | |
| 3134 | U+F440 | |
| 3135 | U+F441 | |
| 3136 | U+F442 | |
| 3137 | U+F443 | |
| 3138 | U+F444 | |
| 3139 | U+F445 | |
| 3140 | U+F446 | |
| 3141 | U+F447 | |
| 3142 | U+F448 | |
| 3143 | U+F449 | |
| 3144 | U+F44A | |
| 3145 | U+F44B | |
| 3146 | U+F44C | |
| 3147 | U+F44D | |
| 3148 | U+F44E | |
| 3149 | U+F44F | |
| 3150 | U+F450 | |
| 3151 | U+F451 | |
| 3152 | U+F452 | |
| 3153 | U+F453 | |
| 3154 | U+F454 | |
| 3155 | U+F455 | |
| 3156 | U+F456 | |
| 3157 | U+F457 | |
| 3158 | U+F458 | |
| 3159 | U+F459 | |
| 3160 | U+F45A | |
| 3161 | U+F45B | |
| 3162 | U+F45C | |
| 3163 | U+F45D | |
| 3164 | U+F45E | |
| 3165 | U+F45F | |
| 3166 | U+F460 | |
| 3167 | U+F461 | |
| 3168 | U+F462 | |
| 3169 | U+F463 | |
| 3170 | U+F464 | |
| 3171 | U+F465 | |
| 3172 | U+F466 | |
| 3173 | U+F467 | |
| 3174 | U+F468 | |
| 3175 | U+F469 | |
| 3176 | U+F46A | |
| 3177 | U+F46B | |
| 3178 | U+F46C | |
| 3179 | U+F46D | |
| 3180 | U+F46E | |
| 3181 | U+F46F | |
| 3182 | U+F470 | |
| 3183 | U+F471 | |
| 3184 | U+F472 | |
| 3185 | U+F473 | |
| 3186 | U+F474 | |
| 3187 | U+F475 | |
| 3188 | U+F476 | |
| 3189 | U+F477 | |
| 3190 | U+F478 | |
| 3191 | U+F479 | |
| 3192 | U+F47A | |
| 3193 | U+F47B | |
| 3194 | U+F47C | |
| 3195 | U+F47D | |
| 3196 | U+F47E | |
| 3197 | U+F47F | |
| 3198 | U+F480 | |
| 3199 | U+F481 | |
| 3200 | U+F482 | |
| 3201 | U+F483 | |
| 3202 | U+F484 | |
| 3203 | U+F485 | |
| 3204 | U+F486 | |
| 3205 | U+F487 | |
| 3206 | U+F488 | |
| 3207 | U+F489 | |
| 3208 | U+F48A | |
| 3209 | U+F48B | |
| 3210 | U+F48C | |
| 3211 | U+F48D | |
| 3212 | U+F48E | |
| 3213 | U+F48F | |
| 3214 | U+F490 | |
| 3215 | U+F491 | |
| 3216 | U+F492 | |
| 3217 | U+F493 | |
| 3218 | U+F494 | |
| 3219 | U+F495 | |
| 3220 | U+F496 | |
| 3221 | U+F497 | |
| 3222 | U+F498 | |
| 3223 | U+F499 | |
| 3224 | U+F49A | |
| 3225 | U+F49B | |
| 3226 | U+F49C | |
| 3227 | U+F49D | |
| 3228 | U+F49E | |
| 3229 | U+F49F | |
| 3230 | U+F4A0 | |
| 3231 | U+F4A1 | |
| 3232 | U+F4A2 | |
| 3233 | U+F4A3 | |
| 3234 | U+F4A4 | |
| 3235 | U+F4A5 | |
| 3236 | U+F4A6 | |
| 3237 | U+F4A7 | |
| 3238 | U+F4A8 | |
| 3239 | U+F4A9 | |
| 3240 | U+F4AA | |
| 3241 | U+F4AB | |
| 3242 | U+F4AC | |
| 3243 | U+F4AD | |
| 3244 | U+F4AE | |
| 3245 | U+F4AF | |
| 3246 | U+F4B0 | |
| 3247 | U+F4B1 | |
| 3248 | U+F4B2 | |
| 3249 | U+F4B3 | |
| 3250 | U+F4B4 | |
| 3251 | U+F4B5 | |
| 3252 | U+F4B6 | |
| 3253 | U+F4B7 | |
| 3254 | U+F4B8 | |
| 3255 | U+F4B9 | |
| 3256 | U+F4BA | |
| 3257 | U+F4BB | |
| 3258 | U+F4BC | |
| 3259 | U+F4BD | |
| 3260 | U+F4BE | |
| 3261 | U+F4BF | |
| 3262 | U+F4C0 | |
| 3263 | U+F4C1 | |
| 3264 | U+F4C2 | |
| 3265 | U+F4C3 | |
| 3266 | U+F4C4 | |
| 3267 | U+F4C5 | |
| 3268 | U+F4C6 | |
| 3269 | U+F4C7 | |
| 3270 | U+F4C8 | |
| 3271 | U+F4C9 | |
| 3272 | U+F4CA | |
| 3273 | U+F4CB | |
| 3274 | U+F4CC | |
| 3275 | U+F4CD | |
| 3276 | U+F4CE | |
| 3277 | U+F4CF | |
| 3278 | U+F4D0 | |
| 3279 | U+F4D1 | |
| 3280 | U+F4D2 | |
| 3281 | U+F4D3 | |
| 3282 | U+F4D4 | |
| 3283 | U+F4D5 | |
| 3284 | U+F4D6 | |
| 3285 | U+F4D7 | |
| 3286 | U+F4D8 | |
| 3287 | U+F4D9 | |
| 3288 | U+F4DA | |
| 3289 | U+F4DB | |
| 3290 | U+F4DC | |
| 3291 | U+F4DD | |
| 3292 | U+F4DE | |
| 3293 | U+F4DF | |
| 3294 | U+F4E0 | |
| 3295 | U+F4E1 | |
| 3296 | U+F4E2 | |
| 3297 | U+F4E3 | |
| 3298 | U+F4E4 | |
| 3299 | U+F4E5 | |
| 3300 | U+F4E6 | |
| 3301 | U+F4E7 | |
| 3302 | U+F4E8 | |
| 3303 | U+F4E9 | |
| 3304 | U+F4EA | |
| 3305 | U+F4EB | |
| 3306 | U+F4EC | |
| 3307 | U+F4ED | |
| 3308 | U+F4EE | |
| 3309 | U+F4EF | |
| 3310 | U+F4F0 | |
| 3311 | U+F4F1 | |
| 3312 | U+F4F2 | |
| 3313 | U+F4F3 | |
| 3314 | U+F4F4 | |
| 3315 | U+F4F5 | |
| 3316 | U+F4F6 | |
| 3317 | U+F4F7 | |
| 3318 | U+F4F8 | |
| 3319 | U+F4F9 | |
| 3320 | U+F4FA | |
| 3321 | U+F4FB | |
| 3322 | U+F4FC | |
| 3323 | U+F4FD | |
| 3324 | U+F4FE | |
| 3325 | U+F4FF | |
| 3326 | U+F500 | |
| 3327 | U+F501 | |
| 3328 | U+F502 | |
| 3329 | U+F503 | |
| 3330 | U+F504 | |
| 3331 | U+F505 | |
| 3332 | U+F506 | |
| 3333 | U+F507 | |
| 3334 | U+F508 | |
| 3335 | U+F509 | |
| 3336 | U+F50A | |
| 3337 | U+F50B | |
| 3338 | U+F50C | |
| 3339 | U+F50D | |
| 3340 | U+F50E | |
| 3341 | U+F50F | |
| 3342 | U+F510 | |
| 3343 | U+F511 | |
| 3344 | U+F512 | |
| 3345 | U+F513 | |
| 3346 | U+F514 | |
| 3347 | U+F515 | |
| 3348 | U+F516 | |
| 3349 | U+F517 | |
| 3350 | U+F518 | |
| 3351 | U+F519 | |
| 3352 | U+F51A | |
| 3353 | U+F51B | |
| 3354 | U+F51C | |
| 3355 | U+F51D | |
| 3356 | U+F51E | |
| 3357 | U+F51F | |
| 3358 | U+F520 | |
| 3359 | U+F521 | |
| 3360 | U+F522 | |
| 3361 | U+F523 | |
| 3362 | U+F524 | |
| 3363 | U+F525 | |
| 3364 | U+F526 | |
| 3365 | U+F527 | |
| 3366 | U+F528 | |
| 3367 | U+F529 | |
| 3368 | U+F52A | |
| 3369 | U+F52B | |
| 3370 | U+F52C | |
| 3371 | U+F52D | |
| 3372 | U+F52E | |
| 3373 | U+F52F | |
| 3374 | U+F530 | |
| 3375 | U+F531 | |
| 3376 | U+F532 | |
| 3377 | U+F533 | |
| 3378 | U+F0001 | |
| 3379 | U+F0002 | |
| 3380 | U+F0003 | |
| 3381 | U+F0004 | |
| 3382 | U+F0005 | |
| 3383 | U+F0006 | |
| 3384 | U+F0007 | |
| 3385 | U+F0008 | |
| 3386 | U+F0009 | |
| 3387 | U+F000A | |
| 3388 | U+F000B | |
| 3389 | U+F000C | |
| 3390 | U+F000D | |
| 3391 | U+F000E | |
| 3392 | U+F000F | |
| 3393 | U+F0010 | |
| 3394 | U+F0011 | |
| 3395 | U+F0012 | |
| 3396 | U+F0013 | |
| 3397 | U+F0014 | |
| 3398 | U+F0015 | |
| 3399 | U+F0016 | |
| 3400 | U+F0017 | |
| 3401 | U+F0018 | |
| 3402 | U+F0019 | |
| 3403 | U+F001A | |
| 3404 | U+F001B | |
| 3405 | U+F001C | |
| 3406 | U+F001D | |
| 3407 | U+F001E | |
| 3408 | U+F001F | |
| 3409 | U+F0020 | |
| 3410 | U+F0021 | |
| 3411 | U+F0022 | |
| 3412 | U+F0023 | |
| 3413 | U+F0024 | |
| 3414 | U+F0025 | |
| 3415 | U+F0026 | |
| 3416 | U+F0027 | |
| 3417 | U+F0028 | |
| 3418 | U+F0029 | |
| 3419 | U+F002A | |
| 3420 | U+F002B | |
| 3421 | U+F002C | |
| 3422 | U+F002D | |
| 3423 | U+F002E | |
| 3424 | U+F002F | |
| 3425 | U+F0030 | |
| 3426 | U+F0031 | |
| 3427 | U+F0032 | |
| 3428 | U+F0033 | |
| 3429 | U+F0034 | |
| 3430 | U+F0035 | |
| 3431 | U+F0036 | |
| 3432 | U+F0037 | |
| 3433 | U+F0038 | |
| 3434 | U+F0039 | |
| 3435 | U+F003A | |
| 3436 | U+F003B | |
| 3437 | U+F003C | |
| 3438 | U+F003D | |
| 3439 | U+F003E | |
| 3440 | U+F003F | |
| 3441 | U+F0040 | |
| 3442 | U+F0041 | |
| 3443 | U+F0042 | |
| 3444 | U+F0043 | |
| 3445 | U+F0044 | |
| 3446 | U+F0045 | |
| 3447 | U+F0046 | |
| 3448 | U+F0047 | |
| 3449 | U+F0048 | |
| 3450 | U+F0049 | |
| 3451 | U+F004A | |
| 3452 | U+F004B | |
| 3453 | U+F004C | |
| 3454 | U+F004D | |
| 3455 | U+F004E | |
| 3456 | U+F004F | |
| 3457 | U+F0050 | |
| 3458 | U+F0051 | |
| 3459 | U+F0052 | |
| 3460 | U+F0053 | |
| 3461 | U+F0054 | |
| 3462 | U+F0055 | |
| 3463 | U+F0056 | |
| 3464 | U+F0057 | |
| 3465 | U+F0058 | |
| 3466 | U+F0059 | |
| 3467 | U+F005A | |
| 3468 | U+F005B | |
| 3469 | U+F005C | |
| 3470 | U+F005D | |
| 3471 | U+F005E | |
| 3472 | U+F005F | |
| 3473 | U+F0060 | |
| 3474 | U+F0061 | |
| 3475 | U+F0062 | |
| 3476 | U+F0063 | |
| 3477 | U+F0064 | |
| 3478 | U+F0065 | |
| 3479 | U+F0066 | |
| 3480 | U+F0067 | |
| 3481 | U+F0068 | |
| 3482 | U+F0069 | |
| 3483 | U+F006A | |
| 3484 | U+F006B | |
| 3485 | U+F006C | |
| 3486 | U+F006D | |
| 3487 | U+F006E | |
| 3488 | U+F006F | |
| 3489 | U+F0070 | |
| 3490 | U+F0071 | |
| 3491 | U+F0072 | |
| 3492 | U+F0073 | |
| 3493 | U+F0074 | |
| 3494 | U+F0075 | |
| 3495 | U+F0076 | |
| 3496 | U+F0077 | |
| 3497 | U+F0078 | |
| 3498 | U+F0079 | |
| 3499 | U+F007A | |
| 3500 | U+F007B | |
| 3501 | U+F007C | |
| 3502 | U+F007D | |
| 3503 | U+F007E | |
| 3504 | U+F007F | |
| 3505 | U+F0080 | |
| 3506 | U+F0081 | |
| 3507 | U+F0082 | |
| 3508 | U+F0083 | |
| 3509 | U+F0084 | |
| 3510 | U+F0085 | |
| 3511 | U+F0086 | |
| 3512 | U+F0087 | |
| 3513 | U+F0088 | |
| 3514 | U+F0089 | |
| 3515 | U+F008A | |
| 3516 | U+F008B | |
| 3517 | U+F008C | |
| 3518 | U+F008D | |
| 3519 | U+F008E | |
| 3520 | U+F008F | |
| 3521 | U+F0090 | |
| 3522 | U+F0091 | |
| 3523 | U+F0092 | |
| 3524 | U+F0093 | |
| 3525 | U+F0094 | |
| 3526 | U+F0095 | |
| 3527 | U+F0096 | |
| 3528 | U+F0097 | |
| 3529 | U+F0098 | |
| 3530 | U+F0099 | |
| 3531 | U+F009A | |
| 3532 | U+F009B | |
| 3533 | U+F009C | |
| 3534 | U+F009D | |
| 3535 | U+F009E | |
| 3536 | U+F009F | |
| 3537 | U+F00A0 | |
| 3538 | U+F00A1 | |
| 3539 | U+F00A2 | |
| 3540 | U+F00A3 | |
| 3541 | U+F00A4 | |
| 3542 | U+F00A5 | |
| 3543 | U+F00A6 | |
| 3544 | U+F00A7 | |
| 3545 | U+F00A8 | |
| 3546 | U+F00A9 | |
| 3547 | U+F00AA | |
| 3548 | U+F00AB | |
| 3549 | U+F00AC | |
| 3550 | U+F00AD | |
| 3551 | U+F00AE | |
| 3552 | U+F00AF | |
| 3553 | U+F00B0 | |
| 3554 | U+F00B1 | |
| 3555 | U+F00B2 | |
| 3556 | U+F00B3 | |
| 3557 | U+F00B4 | |
| 3558 | U+F00B5 | |
| 3559 | U+F00B6 | |
| 3560 | U+F00B7 | |
| 3561 | U+F00B8 | |
| 3562 | U+F00B9 | |
| 3563 | U+F00BA | |
| 3564 | U+F00BB | |
| 3565 | U+F00BC | |
| 3566 | U+F00BD | |
| 3567 | U+F00BE | |
| 3568 | U+F00BF | |
| 3569 | U+F00C0 | |
| 3570 | U+F00C1 | |
| 3571 | U+F00C2 | |
| 3572 | U+F00C3 | |
| 3573 | U+F00C4 | |
| 3574 | U+F00C5 | |
| 3575 | U+F00C6 | |
| 3576 | U+F00C7 | |
| 3577 | U+F00C8 | |
| 3578 | U+F00C9 | |
| 3579 | U+F00CA | |
| 3580 | U+F00CB | |
| 3581 | U+F00CC | |
| 3582 | U+F00CD | |
| 3583 | U+F00CE | |
| 3584 | U+F00CF | |
| 3585 | U+F00D0 | |
| 3586 | U+F00D1 | |
| 3587 | U+F00D2 | |
| 3588 | U+F00D3 | |
| 3589 | U+F00D4 | |
| 3590 | U+F00D5 | |
| 3591 | U+F00D6 | |
| 3592 | U+F00D7 | |
| 3593 | U+F00D8 | |
| 3594 | U+F00D9 | |
| 3595 | U+F00DA | |
| 3596 | U+F00DB | |
| 3597 | U+F00DC | |
| 3598 | U+F00DD | |
| 3599 | U+F00DE | |
| 3600 | U+F00DF | |
| 3601 | U+F00E0 | |
| 3602 | U+F00E1 | |
| 3603 | U+F00E2 | |
| 3604 | U+F00E3 | |
| 3605 | U+F00E4 | |
| 3606 | U+F00E5 | |
| 3607 | U+F00E6 | |
| 3608 | U+F00E7 | |
| 3609 | U+F00E8 | |
| 3610 | U+F00E9 | |
| 3611 | U+F00EA | |
| 3612 | U+F00EB | |
| 3613 | U+F00EC | |
| 3614 | U+F00ED | |
| 3615 | U+F00EE | |
| 3616 | U+F00EF | |
| 3617 | U+F00F0 | |
| 3618 | U+F00F1 | |
| 3619 | U+F00F2 | |
| 3620 | U+F00F3 | |
| 3621 | U+F00F4 | |
| 3622 | U+F00F5 | |
| 3623 | U+F00F6 | |
| 3624 | U+F00F7 | |
| 3625 | U+F00F8 | |
| 3626 | U+F00F9 | |
| 3627 | U+F00FA | |
| 3628 | U+F00FB | |
| 3629 | U+F00FC | |
| 3630 | U+F00FD | |
| 3631 | U+F00FE | |
| 3632 | U+F00FF | |
| 3633 | U+F0100 | |
| 3634 | U+F0101 | |
| 3635 | U+F0102 | |
| 3636 | U+F0103 | |
| 3637 | U+F0104 | |
| 3638 | U+F0105 | |
| 3639 | U+F0106 | |
| 3640 | U+F0107 | |
| 3641 | U+F0108 | |
| 3642 | U+F0109 | |
| 3643 | U+F010A | |
| 3644 | U+F010B | |
| 3645 | U+F010C | |
| 3646 | U+F010D | |
| 3647 | U+F010E | |
| 3648 | U+F010F | |
| 3649 | U+F0110 | |
| 3650 | U+F0111 | |
| 3651 | U+F0112 | |
| 3652 | U+F0113 | |
| 3653 | U+F0114 | |
| 3654 | U+F0115 | |
| 3655 | U+F0116 | |
| 3656 | U+F0117 | |
| 3657 | U+F0118 | |
| 3658 | U+F0119 | |
| 3659 | U+F011A | |
| 3660 | U+F011B | |
| 3661 | U+F011C | |
| 3662 | U+F011D | |
| 3663 | U+F011E | |
| 3664 | U+F011F | |
| 3665 | U+F0120 | |
| 3666 | U+F0121 | |
| 3667 | U+F0122 | |
| 3668 | U+F0123 | |
| 3669 | U+F0124 | |
| 3670 | U+F0125 | |
| 3671 | U+F0126 | |
| 3672 | U+F0127 | |
| 3673 | U+F0128 | |
| 3674 | U+F0129 | |
| 3675 | U+F012A | |
| 3676 | U+F012B | |
| 3677 | U+F012C | |
| 3678 | U+F012D | |
| 3679 | U+F012E | |
| 3680 | U+F012F | |
| 3681 | U+F0130 | |
| 3682 | U+F0131 | |
| 3683 | U+F0132 | |
| 3684 | U+F0133 | |
| 3685 | U+F0134 | |
| 3686 | U+F0135 | |
| 3687 | U+F0136 | |
| 3688 | U+F0137 | |
| 3689 | U+F0138 | |
| 3690 | U+F0139 | |
| 3691 | U+F013A | |
| 3692 | U+F013B | |
| 3693 | U+F013C | |
| 3694 | U+F013D | |
| 3695 | U+F013E | |
| 3696 | U+F013F | |
| 3697 | U+F0140 | |
| 3698 | U+F0141 | |
| 3699 | U+F0142 | |
| 3700 | U+F0143 | |
| 3701 | U+F0144 | |
| 3702 | U+F0145 | |
| 3703 | U+F0146 | |
| 3704 | U+F0147 | |
| 3705 | U+F0148 | |
| 3706 | U+F0149 | |
| 3707 | U+F014A | |
| 3708 | U+F014B | |
| 3709 | U+F014C | |
| 3710 | U+F014D | |
| 3711 | U+F014E | |
| 3712 | U+F014F | |
| 3713 | U+F0150 | |
| 3714 | U+F0151 | |
| 3715 | U+F0152 | |
| 3716 | U+F0153 | |
| 3717 | U+F0154 | |
| 3718 | U+F0155 | |
| 3719 | U+F0156 | |
| 3720 | U+F0157 | |
| 3721 | U+F0158 | |
| 3722 | U+F0159 | |
| 3723 | U+F015A | |
| 3724 | U+F015B | |
| 3725 | U+F015C | |
| 3726 | U+F015D | |
| 3727 | U+F015E | |
| 3728 | U+F015F | |
| 3729 | U+F0160 | |
| 3730 | U+F0161 | |
| 3731 | U+F0162 | |
| 3732 | U+F0163 | |
| 3733 | U+F0164 | |
| 3734 | U+F0165 | |
| 3735 | U+F0166 | |
| 3736 | U+F0167 | |
| 3737 | U+F0168 | |
| 3738 | U+F0169 | |
| 3739 | U+F016A | |
| 3740 | U+F016B | |
| 3741 | U+F016C | |
| 3742 | U+F016D | |
| 3743 | U+F016E | |
| 3744 | U+F016F | |
| 3745 | U+F0170 | |
| 3746 | U+F0171 | |
| 3747 | U+F0172 | |
| 3748 | U+F0173 | |
| 3749 | U+F0174 | |
| 3750 | U+F0175 | |
| 3751 | U+F0176 | |
| 3752 | U+F0177 | |
| 3753 | U+F0178 | |
| 3754 | U+F0179 | |
| 3755 | U+F017A | |
| 3756 | U+F017B | |
| 3757 | U+F017C | |
| 3758 | U+F017D | |
| 3759 | U+F017E | |
| 3760 | U+F017F | |
| 3761 | U+F0180 | |
| 3762 | U+F0181 | |
| 3763 | U+F0182 | |
| 3764 | U+F0183 | |
| 3765 | U+F0184 | |
| 3766 | U+F0185 | |
| 3767 | U+F0186 | |
| 3768 | U+F0187 | |
| 3769 | U+F0188 | |
| 3770 | U+F0189 | |
| 3771 | U+F018A | |
| 3772 | U+F018B | |
| 3773 | U+F018C | |
| 3774 | U+F018D | |
| 3775 | U+F018E | |
| 3776 | U+F018F | |
| 3777 | U+F0190 | |
| 3778 | U+F0191 | |
| 3779 | U+F0192 | |
| 3780 | U+F0193 | |
| 3781 | U+F0194 | |
| 3782 | U+F0195 | |
| 3783 | U+F0196 | |
| 3784 | U+F0197 | |
| 3785 | U+F0198 | |
| 3786 | U+F0199 | |
| 3787 | U+F019A | |
| 3788 | U+F019B | |
| 3789 | U+F019C | |
| 3790 | U+F019D | |
| 3791 | U+F019E | |
| 3792 | U+F019F | |
| 3793 | U+F01A0 | |
| 3794 | U+F01A1 | |
| 3795 | U+F01A2 | |
| 3796 | U+F01A3 | |
| 3797 | U+F01A4 | |
| 3798 | U+F01A5 | |
| 3799 | U+F01A6 | |
| 3800 | U+F01A7 | |
| 3801 | U+F01A8 | |
| 3802 | U+F01A9 | |
| 3803 | U+F01AA | |
| 3804 | U+F01AB | |
| 3805 | U+F01AC | |
| 3806 | U+F01AD | |
| 3807 | U+F01AE | |
| 3808 | U+F01AF | |
| 3809 | U+F01B0 | |
| 3810 | U+F01B1 | |
| 3811 | U+F01B2 | |
| 3812 | U+F01B3 | |
| 3813 | U+F01B4 | |
| 3814 | U+F01B5 | |
| 3815 | U+F01B6 | |
| 3816 | U+F01B7 | |
| 3817 | U+F01B8 | |
| 3818 | U+F01B9 | |
| 3819 | U+F01BA | |
| 3820 | U+F01BB | |
| 3821 | U+F01BC | |
| 3822 | U+F01BD | |
| 3823 | U+F01BE | |
| 3824 | U+F01BF | |
| 3825 | U+F01C0 | |
| 3826 | U+F01C1 | |
| 3827 | U+F01C2 | |
| 3828 | U+F01C3 | |
| 3829 | U+F01C4 | |
| 3830 | U+F01C5 | |
| 3831 | U+F01C6 | |
| 3832 | U+F01C7 | |
| 3833 | U+F01C8 | |
| 3834 | U+F01C9 | |
| 3835 | U+F01CA | |
| 3836 | U+F01CB | |
| 3837 | U+F01CC | |
| 3838 | U+F01CD | |
| 3839 | U+F01CE | |
| 3840 | U+F01CF | |
| 3841 | U+F01D0 | |
| 3842 | U+F01D1 | |
| 3843 | U+F01D2 | |
| 3844 | U+F01D3 | |
| 3845 | U+F01D4 | |
| 3846 | U+F01D5 | |
| 3847 | U+F01D6 | |
| 3848 | U+F01D7 | |
| 3849 | U+F01D8 | |
| 3850 | U+F01D9 | |
| 3851 | U+F01DA | |
| 3852 | U+F01DB | |
| 3853 | U+F01DC | |
| 3854 | U+F01DD | |
| 3855 | U+F01DE | |
| 3856 | U+F01DF | |
| 3857 | U+F01E0 | |
| 3858 | U+F01E1 | |
| 3859 | U+F01E2 | |
| 3860 | U+F01E3 | |
| 3861 | U+F01E4 | |
| 3862 | U+F01E5 | |
| 3863 | U+F01E6 | |
| 3864 | U+F01E7 | |
| 3865 | U+F01E8 | |
| 3866 | U+F01E9 | |
| 3867 | U+F01EA | |
| 3868 | U+F01EB | |
| 3869 | U+F01EC | |
| 3870 | U+F01ED | |
| 3871 | U+F01EE | |
| 3872 | U+F01EF | |
| 3873 | U+F01F0 | |
| 3874 | U+F01F1 | |
| 3875 | U+F01F2 | |
| 3876 | U+F01F3 | |
| 3877 | U+F01F4 | |
| 3878 | U+F01F5 | |
| 3879 | U+F01F6 | |
| 3880 | U+F01F7 | |
| 3881 | U+F01F8 | |
| 3882 | U+F01F9 | |
| 3883 | U+F01FA | |
| 3884 | U+F01FB | |
| 3885 | U+F01FC | |
| 3886 | U+F01FD | |
| 3887 | U+F01FE | |
| 3888 | U+F01FF | |
| 3889 | U+F0200 | |
| 3890 | U+F0201 | |
| 3891 | U+F0202 | |
| 3892 | U+F0203 | |
| 3893 | U+F0204 | |
| 3894 | U+F0205 | |
| 3895 | U+F0206 | |
| 3896 | U+F0207 | |
| 3897 | U+F0208 | |
| 3898 | U+F0209 | |
| 3899 | U+F020A | |
| 3900 | U+F020B | |
| 3901 | U+F020C | |
| 3902 | U+F020D | |
| 3903 | U+F020E | |
| 3904 | U+F020F | |
| 3905 | U+F0210 | |
| 3906 | U+F0211 | |
| 3907 | U+F0212 | |
| 3908 | U+F0213 | |
| 3909 | U+F0214 | |
| 3910 | U+F0215 | |
| 3911 | U+F0216 | |
| 3912 | U+F0217 | |
| 3913 | U+F0218 | |
| 3914 | U+F0219 | |
| 3915 | U+F021A | |
| 3916 | U+F021B | |
| 3917 | U+F021C | |
| 3918 | U+F021D | |
| 3919 | U+F021E | |
| 3920 | U+F021F | |
| 3921 | U+F0220 | |
| 3922 | U+F0221 | |
| 3923 | U+F0222 | |
| 3924 | U+F0223 | |
| 3925 | U+F0224 | |
| 3926 | U+F0225 | |
| 3927 | U+F0226 | |
| 3928 | U+F0227 | |
| 3929 | U+F0228 | |
| 3930 | U+F0229 | |
| 3931 | U+F022A | |
| 3932 | U+F022B | |
| 3933 | U+F022C | |
| 3934 | U+F022D | |
| 3935 | U+F022E | |
| 3936 | U+F022F | |
| 3937 | U+F0230 | |
| 3938 | U+F0231 | |
| 3939 | U+F0232 | |
| 3940 | U+F0233 | |
| 3941 | U+F0234 | |
| 3942 | U+F0235 | |
| 3943 | U+F0236 | |
| 3944 | U+F0237 | |
| 3945 | U+F0238 | |
| 3946 | U+F0239 | |
| 3947 | U+F023A | |
| 3948 | U+F023B | |
| 3949 | U+F023C | |
| 3950 | U+F023D | |
| 3951 | U+F023E | |
| 3952 | U+F023F | |
| 3953 | U+F0240 | |
| 3954 | U+F0241 | |
| 3955 | U+F0242 | |
| 3956 | U+F0243 | |
| 3957 | U+F0244 | |
| 3958 | U+F0245 | |
| 3959 | U+F0246 | |
| 3960 | U+F0247 | |
| 3961 | U+F0248 | |
| 3962 | U+F0249 | |
| 3963 | U+F024A | |
| 3964 | U+F024B | |
| 3965 | U+F024C | |
| 3966 | U+F024D | |
| 3967 | U+F024E | |
| 3968 | U+F024F | |
| 3969 | U+F0250 | |
| 3970 | U+F0251 | |
| 3971 | U+F0252 | |
| 3972 | U+F0253 | |
| 3973 | U+F0254 | |
| 3974 | U+F0255 | |
| 3975 | U+F0256 | |
| 3976 | U+F0257 | |
| 3977 | U+F0258 | |
| 3978 | U+F0259 | |
| 3979 | U+F025A | |
| 3980 | U+F025B | |
| 3981 | U+F025C | |
| 3982 | U+F025D | |
| 3983 | U+F025E | |
| 3984 | U+F025F | |
| 3985 | U+F0260 | |
| 3986 | U+F0261 | |
| 3987 | U+F0262 | |
| 3988 | U+F0263 | |
| 3989 | U+F0264 | |
| 3990 | U+F0265 | |
| 3991 | U+F0266 | |
| 3992 | U+F0267 | |
| 3993 | U+F0268 | |
| 3994 | U+F0269 | |
| 3995 | U+F026A | |
| 3996 | U+F026B | |
| 3997 | U+F026C | |
| 3998 | U+F026D | |
| 3999 | U+F026E | |
| 4000 | U+F026F | |
| 4001 | U+F0270 | |
| 4002 | U+F0271 | |
| 4003 | U+F0272 | |
| 4004 | U+F0273 | |
| 4005 | U+F0274 | |
| 4006 | U+F0275 | |
| 4007 | U+F0276 | |
| 4008 | U+F0277 | |
| 4009 | U+F0278 | |
| 4010 | U+F0279 | |
| 4011 | U+F027A | |
| 4012 | U+F027B | |
| 4013 | U+F027C | |
| 4014 | U+F027D | |
| 4015 | U+F027E | |
| 4016 | U+F027F | |
| 4017 | U+F0280 | |
| 4018 | U+F0281 | |
| 4019 | U+F0282 | |
| 4020 | U+F0283 | |
| 4021 | U+F0284 | |
| 4022 | U+F0285 | |
| 4023 | U+F0286 | |
| 4024 | U+F0287 | |
| 4025 | U+F0288 | |
| 4026 | U+F0289 | |
| 4027 | U+F028A | |
| 4028 | U+F028B | |
| 4029 | U+F028C | |
| 4030 | U+F028D | |
| 4031 | U+F028E | |
| 4032 | U+F028F | |
| 4033 | U+F0290 | |
| 4034 | U+F0291 | |
| 4035 | U+F0292 | |
| 4036 | U+F0293 | |
| 4037 | U+F0294 | |
| 4038 | U+F0295 | |
| 4039 | U+F0296 | |
| 4040 | U+F0297 | |
| 4041 | U+F0298 | |
| 4042 | U+F0299 | |
| 4043 | U+F029A | |
| 4044 | U+F029B | |
| 4045 | U+F029C | |
| 4046 | U+F029D | |
| 4047 | U+F029E | |
| 4048 | U+F029F | |
| 4049 | U+F02A0 | |
| 4050 | U+F02A1 | |
| 4051 | U+F02A2 | |
| 4052 | U+F02A3 | |
| 4053 | U+F02A4 | |
| 4054 | U+F02A5 | |
| 4055 | U+F02A6 | |
| 4056 | U+F02A7 | |
| 4057 | U+F02A8 | |
| 4058 | U+F02A9 | |
| 4059 | U+F02AA | |
| 4060 | U+F02AB | |
| 4061 | U+F02AC | |
| 4062 | U+F02AD | |
| 4063 | U+F02AE | |
| 4064 | U+F02AF | |
| 4065 | U+F02B0 | |
| 4066 | U+F02B1 | |
| 4067 | U+F02B2 | |
| 4068 | U+F02B3 | |
| 4069 | U+F02B4 | |
| 4070 | U+F02B5 | |
| 4071 | U+F02B6 | |
| 4072 | U+F02B7 | |
| 4073 | U+F02B8 | |
| 4074 | U+F02B9 | |
| 4075 | U+F02BA | |
| 4076 | U+F02BB | |
| 4077 | U+F02BC | |
| 4078 | U+F02BD | |
| 4079 | U+F02BE | |
| 4080 | U+F02BF | |
| 4081 | U+F02C0 | |
| 4082 | U+F02C1 | |
| 4083 | U+F02C2 | |
| 4084 | U+F02C3 | |
| 4085 | U+F02C4 | |
| 4086 | U+F02C5 | |
| 4087 | U+F02C6 | |
| 4088 | U+F02C7 | |
| 4089 | U+F02C8 | |
| 4090 | U+F02C9 | |
| 4091 | U+F02CA | |
| 4092 | U+F02CB | |
| 4093 | U+F02CC | |
| 4094 | U+F02CD | |
| 4095 | U+F02CE | |
| 4096 | U+F02CF | |
| 4097 | U+F02D0 | |
| 4098 | U+F02D1 | |
| 4099 | U+F02D2 | |
| 4100 | U+F02D3 | |
| 4101 | U+F02D4 | |
| 4102 | U+F02D5 | |
| 4103 | U+F02D6 | |
| 4104 | U+F02D7 | |
| 4105 | U+F02D8 | |
| 4106 | U+F02D9 | |
| 4107 | U+F02DA | |
| 4108 | U+F02DB | |
| 4109 | U+F02DC | |
| 4110 | U+F02DD | |
| 4111 | U+F02DE | |
| 4112 | U+F02DF | |
| 4113 | U+F02E0 | |
| 4114 | U+F02E1 | |
| 4115 | U+F02E2 | |
| 4116 | U+F02E3 | |
| 4117 | U+F02E4 | |
| 4118 | U+F02E5 | |
| 4119 | U+F02E6 | |
| 4120 | U+F02E7 | |
| 4121 | U+F02E8 | |
| 4122 | U+F02E9 | |
| 4123 | U+F02EA | |
| 4124 | U+F02EB | |
| 4125 | U+F02EC | |
| 4126 | U+F02ED | |
| 4127 | U+F02EE | |
| 4128 | U+F02EF | |
| 4129 | U+F02F0 | |
| 4130 | U+F02F1 | |
| 4131 | U+F02F2 | |
| 4132 | U+F02F3 | |
| 4133 | U+F02F4 | |
| 4134 | U+F02F5 | |
| 4135 | U+F02F6 | |
| 4136 | U+F02F7 | |
| 4137 | U+F02F8 | |
| 4138 | U+F02F9 | |
| 4139 | U+F02FA | |
| 4140 | U+F02FB | |
| 4141 | U+F02FC | |
| 4142 | U+F02FD | |
| 4143 | U+F02FE | |
| 4144 | U+F02FF | |
| 4145 | U+F0300 | |
| 4146 | U+F0301 | |
| 4147 | U+F0302 | |
| 4148 | U+F0303 | |
| 4149 | U+F0304 | |
| 4150 | U+F0305 | |
| 4151 | U+F0306 | |
| 4152 | U+F0307 | |
| 4153 | U+F0308 | |
| 4154 | U+F0309 | |
| 4155 | U+F030A | |
| 4156 | U+F030B | |
| 4157 | U+F030C | |
| 4158 | U+F030D | |
| 4159 | U+F030E | |
| 4160 | U+F030F | |
| 4161 | U+F0310 | |
| 4162 | U+F0311 | |
| 4163 | U+F0312 | |
| 4164 | U+F0313 | |
| 4165 | U+F0314 | |
| 4166 | U+F0315 | |
| 4167 | U+F0316 | |
| 4168 | U+F0317 | |
| 4169 | U+F0318 | |
| 4170 | U+F0319 | |
| 4171 | U+F031A | |
| 4172 | U+F031B | |
| 4173 | U+F031C | |
| 4174 | U+F031D | |
| 4175 | U+F031E | |
| 4176 | U+F031F | |
| 4177 | U+F0320 | |
| 4178 | U+F0321 | |
| 4179 | U+F0322 | |
| 4180 | U+F0323 | |
| 4181 | U+F0324 | |
| 4182 | U+F0325 | |
| 4183 | U+F0326 | |
| 4184 | U+F0327 | |
| 4185 | U+F0328 | |
| 4186 | U+F0329 | |
| 4187 | U+F032A | |
| 4188 | U+F032B | |
| 4189 | U+F032C | |
| 4190 | U+F032D | |
| 4191 | U+F032E | |
| 4192 | U+F032F | |
| 4193 | U+F0330 | |
| 4194 | U+F0331 | |
| 4195 | U+F0332 | |
| 4196 | U+F0333 | |
| 4197 | U+F0334 | |
| 4198 | U+F0335 | |
| 4199 | U+F0336 | |
| 4200 | U+F0337 | |
| 4201 | U+F0338 | |
| 4202 | U+F0339 | |
| 4203 | U+F033A | |
| 4204 | U+F033B | |
| 4205 | U+F033C | |
| 4206 | U+F033D | |
| 4207 | U+F033E | |
| 4208 | U+F033F | |
| 4209 | U+F0340 | |
| 4210 | U+F0341 | |
| 4211 | U+F0342 | |
| 4212 | U+F0343 | |
| 4213 | U+F0344 | |
| 4214 | U+F0345 | |
| 4215 | U+F0346 | |
| 4216 | U+F0347 | |
| 4217 | U+F0348 | |
| 4218 | U+F0349 | |
| 4219 | U+F034A | |
| 4220 | U+F034B | |
| 4221 | U+F034C | |
| 4222 | U+F034D | |
| 4223 | U+F034E | |
| 4224 | U+F034F | |
| 4225 | U+F0350 | |
| 4226 | U+F0351 | |
| 4227 | U+F0352 | |
| 4228 | U+F0353 | |
| 4229 | U+F0354 | |
| 4230 | U+F0355 | |
| 4231 | U+F0356 | |
| 4232 | U+F0357 | |
| 4233 | U+F0358 | |
| 4234 | U+F0359 | |
| 4235 | U+F035A | |
| 4236 | U+F035B | |
| 4237 | U+F035C | |
| 4238 | U+F035D | |
| 4239 | U+F035E | |
| 4240 | U+F035F | |
| 4241 | U+F0360 | |
| 4242 | U+F0361 | |
| 4243 | U+F0362 | |
| 4244 | U+F0363 | |
| 4245 | U+F0364 | |
| 4246 | U+F0365 | |
| 4247 | U+F0366 | |
| 4248 | U+F0367 | |
| 4249 | U+F0368 | |
| 4250 | U+F0369 | |
| 4251 | U+F036A | |
| 4252 | U+F036B | |
| 4253 | U+F036C | |
| 4254 | U+F036D | |
| 4255 | U+F036E | |
| 4256 | U+F036F | |
| 4257 | U+F0370 | |
| 4258 | U+F0371 | |
| 4259 | U+F0372 | |
| 4260 | U+F0373 | |
| 4261 | U+F0374 | |
| 4262 | U+F0375 | |
| 4263 | U+F0376 | |
| 4264 | U+F0377 | |
| 4265 | U+F0378 | |
| 4266 | U+F0379 | |
| 4267 | U+F037A | |
| 4268 | U+F037B | |
| 4269 | U+F037C | |
| 4270 | U+F037D | |
| 4271 | U+F037E | |
| 4272 | U+F037F | |
| 4273 | U+F0380 | |
| 4274 | U+F0381 | |
| 4275 | U+F0382 | |
| 4276 | U+F0383 | |
| 4277 | U+F0384 | |
| 4278 | U+F0385 | |
| 4279 | U+F0386 | |
| 4280 | U+F0387 | |
| 4281 | U+F0388 | |
| 4282 | U+F0389 | |
| 4283 | U+F038A | |
| 4284 | U+F038B | |
| 4285 | U+F038C | |
| 4286 | U+F038D | |
| 4287 | U+F038E | |
| 4288 | U+F038F | |
| 4289 | U+F0390 | |
| 4290 | U+F0391 | |
| 4291 | U+F0392 | |
| 4292 | U+F0393 | |
| 4293 | U+F0394 | |
| 4294 | U+F0395 | |
| 4295 | U+F0396 | |
| 4296 | U+F0397 | |
| 4297 | U+F0398 | |
| 4298 | U+F0399 | |
| 4299 | U+F039A | |
| 4300 | U+F039B | |
| 4301 | U+F039C | |
| 4302 | U+F039D | |
| 4303 | U+F039E | |
| 4304 | U+F039F | |
| 4305 | U+F03A0 | |
| 4306 | U+F03A1 | |
| 4307 | U+F03A2 | |
| 4308 | U+F03A3 | |
| 4309 | U+F03A4 | |
| 4310 | U+F03A5 | |
| 4311 | U+F03A6 | |
| 4312 | U+F03A7 | |
| 4313 | U+F03A8 | |
| 4314 | U+F03A9 | |
| 4315 | U+F03AA | |
| 4316 | U+F03AB | |
| 4317 | U+F03AC | |
| 4318 | U+F03AD | |
| 4319 | U+F03AE | |
| 4320 | U+F03AF | |
| 4321 | U+F03B0 | |
| 4322 | U+F03B1 | |
| 4323 | U+F03B2 | |
| 4324 | U+F03B3 | |
| 4325 | U+F03B4 | |
| 4326 | U+F03B5 | |
| 4327 | U+F03B6 | |
| 4328 | U+F03B7 | |
| 4329 | U+F03B8 | |
| 4330 | U+F03B9 | |
| 4331 | U+F03BA | |
| 4332 | U+F03BB | |
| 4333 | U+F03BC | |
| 4334 | U+F03BD | |
| 4335 | U+F03BE | |
| 4336 | U+F03BF | |
| 4337 | U+F03C0 | |
| 4338 | U+F03C1 | |
| 4339 | U+F03C2 | |
| 4340 | U+F03C3 | |
| 4341 | U+F03C4 | |
| 4342 | U+F03C5 | |
| 4343 | U+F03C6 | |
| 4344 | U+F03C7 | |
| 4345 | U+F03C8 | |
| 4346 | U+F03C9 | |
| 4347 | U+F03CA | |
| 4348 | U+F03CB | |
| 4349 | U+F03CC | |
| 4350 | U+F03CD | |
| 4351 | U+F03CE | |
| 4352 | U+F03CF | |
| 4353 | U+F03D0 | |
| 4354 | U+F03D1 | |
| 4355 | U+F03D2 | |
| 4356 | U+F03D3 | |
| 4357 | U+F03D4 | |
| 4358 | U+F03D5 | |
| 4359 | U+F03D6 | |
| 4360 | U+F03D7 | |
| 4361 | U+F03D8 | |
| 4362 | U+F03D9 | |
| 4363 | U+F03DA | |
| 4364 | U+F03DB | |
| 4365 | U+F03DC | |
| 4366 | U+F03DD | |
| 4367 | U+F03DE | |
| 4368 | U+F03DF | |
| 4369 | U+F03E0 | |
| 4370 | U+F03E1 | |
| 4371 | U+F03E2 | |
| 4372 | U+F03E3 | |
| 4373 | U+F03E4 | |
| 4374 | U+F03E5 | |
| 4375 | U+F03E6 | |
| 4376 | U+F03E7 | |
| 4377 | U+F03E8 | |
| 4378 | U+F03E9 | |
| 4379 | U+F03EA | |
| 4380 | U+F03EB | |
| 4381 | U+F03EC | |
| 4382 | U+F03ED | |
| 4383 | U+F03EE | |
| 4384 | U+F03EF | |
| 4385 | U+F03F0 | |
| 4386 | U+F03F1 | |
| 4387 | U+F03F2 | |
| 4388 | U+F03F3 | |
| 4389 | U+F03F4 | |
| 4390 | U+F03F5 | |
| 4391 | U+F03F6 | |
| 4392 | U+F03F7 | |
| 4393 | U+F03F8 | |
| 4394 | U+F03F9 | |
| 4395 | U+F03FA | |
| 4396 | U+F03FB | |
| 4397 | U+F03FC | |
| 4398 | U+F03FD | |
| 4399 | U+F03FE | |
| 4400 | U+F03FF | |
| 4401 | U+F0400 | |
| 4402 | U+F0401 | |
| 4403 | U+F0402 | |
| 4404 | U+F0403 | |
| 4405 | U+F0404 | |
| 4406 | U+F0405 | |
| 4407 | U+F0406 | |
| 4408 | U+F0407 | |
| 4409 | U+F0408 | |
| 4410 | U+F0409 | |
| 4411 | U+F040A | |
| 4412 | U+F040B | |
| 4413 | U+F040C | |
| 4414 | U+F040D | |
| 4415 | U+F040E | |
| 4416 | U+F040F | |
| 4417 | U+F0410 | |
| 4418 | U+F0411 | |
| 4419 | U+F0412 | |
| 4420 | U+F0413 | |
| 4421 | U+F0414 | |
| 4422 | U+F0415 | |
| 4423 | U+F0416 | |
| 4424 | U+F0417 | |
| 4425 | U+F0418 | |
| 4426 | U+F0419 | |
| 4427 | U+F041A | |
| 4428 | U+F041B | |
| 4429 | U+F041C | |
| 4430 | U+F041D | |
| 4431 | U+F041E | |
| 4432 | U+F041F | |
| 4433 | U+F0420 | |
| 4434 | U+F0421 | |
| 4435 | U+F0422 | |
| 4436 | U+F0423 | |
| 4437 | U+F0424 | |
| 4438 | U+F0425 | |
| 4439 | U+F0426 | |
| 4440 | U+F0427 | |
| 4441 | U+F0428 | |
| 4442 | U+F0429 | |
| 4443 | U+F042A | |
| 4444 | U+F042B | |
| 4445 | U+F042C | |
| 4446 | U+F042D | |
| 4447 | U+F042E | |
| 4448 | U+F042F | |
| 4449 | U+F0430 | |
| 4450 | U+F0431 | |
| 4451 | U+F0432 | |
| 4452 | U+F0433 | |
| 4453 | U+F0434 | |
| 4454 | U+F0435 | |
| 4455 | U+F0436 | |
| 4456 | U+F0437 | |
| 4457 | U+F0438 | |
| 4458 | U+F0439 | |
| 4459 | U+F043A | |
| 4460 | U+F043B | |
| 4461 | U+F043C | |
| 4462 | U+F043D | |
| 4463 | U+F043E | |
| 4464 | U+F043F | |
| 4465 | U+F0440 | |
| 4466 | U+F0441 | |
| 4467 | U+F0442 | |
| 4468 | U+F0443 | |
| 4469 | U+F0444 | |
| 4470 | U+F0445 | |
| 4471 | U+F0446 | |
| 4472 | U+F0447 | |
| 4473 | U+F0448 | |
| 4474 | U+F0449 | |
| 4475 | U+F044A | |
| 4476 | U+F044B | |
| 4477 | U+F044C | |
| 4478 | U+F044D | |
| 4479 | U+F044E | |
| 4480 | U+F044F | |
| 4481 | U+F0450 | |
| 4482 | U+F0451 | |
| 4483 | U+F0452 | |
| 4484 | U+F0453 | |
| 4485 | U+F0454 | |
| 4486 | U+F0455 | |
| 4487 | U+F0456 | |
| 4488 | U+F0457 | |
| 4489 | U+F0458 | |
| 4490 | U+F0459 | |
| 4491 | U+F045A | |
| 4492 | U+F045B | |
| 4493 | U+F045C | |
| 4494 | U+F045D | |
| 4495 | U+F045E | |
| 4496 | U+F045F | |
| 4497 | U+F0460 | |
| 4498 | U+F0461 | |
| 4499 | U+F0462 | |
| 4500 | U+F0463 | |
| 4501 | U+F0464 | |
| 4502 | U+F0465 | |
| 4503 | U+F0466 | |
| 4504 | U+F0467 | |
| 4505 | U+F0468 | |
| 4506 | U+F0469 | |
| 4507 | U+F046A | |
| 4508 | U+F046B | |
| 4509 | U+F046C | |
| 4510 | U+F046D | |
| 4511 | U+F046E | |
| 4512 | U+F046F | |
| 4513 | U+F0470 | |
| 4514 | U+F0471 | |
| 4515 | U+F0472 | |
| 4516 | U+F0473 | |
| 4517 | U+F0474 | |
| 4518 | U+F0475 | |
| 4519 | U+F0476 | |
| 4520 | U+F0477 | |
| 4521 | U+F0478 | |
| 4522 | U+F0479 | |
| 4523 | U+F047A | |
| 4524 | U+F047B | |
| 4525 | U+F047C | |
| 4526 | U+F047D | |
| 4527 | U+F047E | |
| 4528 | U+F047F | |
| 4529 | U+F0480 | |
| 4530 | U+F0481 | |
| 4531 | U+F0482 | |
| 4532 | U+F0483 | |
| 4533 | U+F0484 | |
| 4534 | U+F0485 | |
| 4535 | U+F0486 | |
| 4536 | U+F0487 | |
| 4537 | U+F0488 | |
| 4538 | U+F0489 | |
| 4539 | U+F048A | |
| 4540 | U+F048B | |
| 4541 | U+F048C | |
| 4542 | U+F048D | |
| 4543 | U+F048E | |
| 4544 | U+F048F | |
| 4545 | U+F0490 | |
| 4546 | U+F0491 | |
| 4547 | U+F0492 | |
| 4548 | U+F0493 | |
| 4549 | U+F0494 | |
| 4550 | U+F0495 | |
| 4551 | U+F0496 | |
| 4552 | U+F0497 | |
| 4553 | U+F0498 | |
| 4554 | U+F0499 | |
| 4555 | U+F049A | |
| 4556 | U+F049B | |
| 4557 | U+F049C | |
| 4558 | U+F049D | |
| 4559 | U+F049E | |
| 4560 | U+F049F | |
| 4561 | U+F04A0 | |
| 4562 | U+F04A1 | |
| 4563 | U+F04A2 | |
| 4564 | U+F04A3 | |
| 4565 | U+F04A4 | |
| 4566 | U+F04A5 | |
| 4567 | U+F04A6 | |
| 4568 | U+F04A7 | |
| 4569 | U+F04A8 | |
| 4570 | U+F04A9 | |
| 4571 | U+F04AA | |
| 4572 | U+F04AB | |
| 4573 | U+F04AC | |
| 4574 | U+F04AD | |
| 4575 | U+F04AE | |
| 4576 | U+F04AF | |
| 4577 | U+F04B0 | |
| 4578 | U+F04B1 | |
| 4579 | U+F04B2 | |
| 4580 | U+F04B3 | |
| 4581 | U+F04B4 | |
| 4582 | U+F04B5 | |
| 4583 | U+F04B6 | |
| 4584 | U+F04B7 | |
| 4585 | U+F04B8 | |
| 4586 | U+F04B9 | |
| 4587 | U+F04BA | |
| 4588 | U+F04BB | |
| 4589 | U+F04BC | |
| 4590 | U+F04BD | |
| 4591 | U+F04BE | |
| 4592 | U+F04BF | |
| 4593 | U+F04C0 | |
| 4594 | U+F04C1 | |
| 4595 | U+F04C2 | |
| 4596 | U+F04C3 | |
| 4597 | U+F04C4 | |
| 4598 | U+F04C5 | |
| 4599 | U+F04C6 | |
| 4600 | U+F04C7 | |
| 4601 | U+F04C8 | |
| 4602 | U+F04C9 | |
| 4603 | U+F04CA | |
| 4604 | U+F04CB | |
| 4605 | U+F04CC | |
| 4606 | U+F04CD | |
| 4607 | U+F04CE | |
| 4608 | U+F04CF | |
| 4609 | U+F04D0 | |
| 4610 | U+F04D1 | |
| 4611 | U+F04D2 | |
| 4612 | U+F04D3 | |
| 4613 | U+F04D4 | |
| 4614 | U+F04D5 | |
| 4615 | U+F04D6 | |
| 4616 | U+F04D7 | |
| 4617 | U+F04D8 | |
| 4618 | U+F04D9 | |
| 4619 | U+F04DA | |
| 4620 | U+F04DB | |
| 4621 | U+F04DC | |
| 4622 | U+F04DD | |
| 4623 | U+F04DE | |
| 4624 | U+F04DF | |
| 4625 | U+F04E0 | |
| 4626 | U+F04E1 | |
| 4627 | U+F04E2 | |
| 4628 | U+F04E3 | |
| 4629 | U+F04E4 | |
| 4630 | U+F04E5 | |
| 4631 | U+F04E6 | |
| 4632 | U+F04E7 | |
| 4633 | U+F04E8 | |
| 4634 | U+F04E9 | |
| 4635 | U+F04EA | |
| 4636 | U+F04EB | |
| 4637 | U+F04EC | |
| 4638 | U+F04ED | |
| 4639 | U+F04EE | |
| 4640 | U+F04EF | |
| 4641 | U+F04F0 | |
| 4642 | U+F04F1 | |
| 4643 | U+F04F2 | |
| 4644 | U+F04F3 | |
| 4645 | U+F04F4 | |
| 4646 | U+F04F5 | |
| 4647 | U+F04F6 | |
| 4648 | U+F04F7 | |
| 4649 | U+F04F8 | |
| 4650 | U+F04F9 | |
| 4651 | U+F04FA | |
| 4652 | U+F04FB | |
| 4653 | U+F04FC | |
| 4654 | U+F04FD | |
| 4655 | U+F04FE | |
| 4656 | U+F04FF | |
| 4657 | U+F0500 | |
| 4658 | U+F0501 | |
| 4659 | U+F0502 | |
| 4660 | U+F0503 | |
| 4661 | U+F0504 | |
| 4662 | U+F0505 | |
| 4663 | U+F0506 | |
| 4664 | U+F0507 | |
| 4665 | U+F0508 | |
| 4666 | U+F0509 | |
| 4667 | U+F050A | |
| 4668 | U+F050B | |
| 4669 | U+F050C | |
| 4670 | U+F050D | |
| 4671 | U+F050E | |
| 4672 | U+F050F | |
| 4673 | U+F0510 | |
| 4674 | U+F0511 | |
| 4675 | U+F0512 | |
| 4676 | U+F0513 | |
| 4677 | U+F0514 | |
| 4678 | U+F0515 | |
| 4679 | U+F0516 | |
| 4680 | U+F0517 | |
| 4681 | U+F0518 | |
| 4682 | U+F0519 | |
| 4683 | U+F051A | |
| 4684 | U+F051B | |
| 4685 | U+F051C | |
| 4686 | U+F051D | |
| 4687 | U+F051E | |
| 4688 | U+F051F | |
| 4689 | U+F0520 | |
| 4690 | U+F0521 | |
| 4691 | U+F0522 | |
| 4692 | U+F0523 | |
| 4693 | U+F0524 | |
| 4694 | U+F0525 | |
| 4695 | U+F0526 | |
| 4696 | U+F0527 | |
| 4697 | U+F0528 | |
| 4698 | U+F0529 | |
| 4699 | U+F052A | |
| 4700 | U+F052B | |
| 4701 | U+F052C | |
| 4702 | U+F052D | |
| 4703 | U+F052E | |
| 4704 | U+F052F | |
| 4705 | U+F0530 | |
| 4706 | U+F0531 | |
| 4707 | U+F0532 | |
| 4708 | U+F0533 | |
| 4709 | U+F0534 | |
| 4710 | U+F0535 | |
| 4711 | U+F0536 | |
| 4712 | U+F0537 | |
| 4713 | U+F0538 | |
| 4714 | U+F0539 | |
| 4715 | U+F053A | |
| 4716 | U+F053B | |
| 4717 | U+F053C | |
| 4718 | U+F053D | |
| 4719 | U+F053E | |
| 4720 | U+F053F | |
| 4721 | U+F0540 | |
| 4722 | U+F0541 | |
| 4723 | U+F0542 | |
| 4724 | U+F0543 | |
| 4725 | U+F0544 | |
| 4726 | U+F0545 | |
| 4727 | U+F0546 | |
| 4728 | U+F0547 | |
| 4729 | U+F0548 | |
| 4730 | U+F0549 | |
| 4731 | U+F054A | |
| 4732 | U+F054B | |
| 4733 | U+F054C | |
| 4734 | U+F054D | |
| 4735 | U+F054E | |
| 4736 | U+F054F | |
| 4737 | U+F0550 | |
| 4738 | U+F0551 | |
| 4739 | U+F0552 | |
| 4740 | U+F0553 | |
| 4741 | U+F0554 | |
| 4742 | U+F0555 | |
| 4743 | U+F0556 | |
| 4744 | U+F0557 | |
| 4745 | U+F0558 | |
| 4746 | U+F0559 | |
| 4747 | U+F055A | |
| 4748 | U+F055B | |
| 4749 | U+F055C | |
| 4750 | U+F055D | |
| 4751 | U+F055E | |
| 4752 | U+F055F | |
| 4753 | U+F0560 | |
| 4754 | U+F0561 | |
| 4755 | U+F0562 | |
| 4756 | U+F0563 | |
| 4757 | U+F0564 | |
| 4758 | U+F0565 | |
| 4759 | U+F0566 | |
| 4760 | U+F0567 | |
| 4761 | U+F0568 | |
| 4762 | U+F0569 | |
| 4763 | U+F056A | |
| 4764 | U+F056B | |
| 4765 | U+F056C | |
| 4766 | U+F056D | |
| 4767 | U+F056E | |
| 4768 | U+F056F | |
| 4769 | U+F0570 | |
| 4770 | U+F0571 | |
| 4771 | U+F0572 | |
| 4772 | U+F0573 | |
| 4773 | U+F0574 | |
| 4774 | U+F0575 | |
| 4775 | U+F0576 | |
| 4776 | U+F0577 | |
| 4777 | U+F0578 | |
| 4778 | U+F0579 | |
| 4779 | U+F057A | |
| 4780 | U+F057B | |
| 4781 | U+F057C | |
| 4782 | U+F057D | |
| 4783 | U+F057E | |
| 4784 | U+F057F | |
| 4785 | U+F0580 | |
| 4786 | U+F0581 | |
| 4787 | U+F0582 | |
| 4788 | U+F0583 | |
| 4789 | U+F0584 | |
| 4790 | U+F0585 | |
| 4791 | U+F0586 | |
| 4792 | U+F0587 | |
| 4793 | U+F0588 | |
| 4794 | U+F0589 | |
| 4795 | U+F058A | |
| 4796 | U+F058B | |
| 4797 | U+F058C | |
| 4798 | U+F058D | |
| 4799 | U+F058E | |
| 4800 | U+F058F | |
| 4801 | U+F0590 | |
| 4802 | U+F0591 | |
| 4803 | U+F0592 | |
| 4804 | U+F0593 | |
| 4805 | U+F0594 | |
| 4806 | U+F0595 | |
| 4807 | U+F0596 | |
| 4808 | U+F0597 | |
| 4809 | U+F0598 | |
| 4810 | U+F0599 | |
| 4811 | U+F059A | |
| 4812 | U+F059B | |
| 4813 | U+F059C | |
| 4814 | U+F059D | |
| 4815 | U+F059E | |
| 4816 | U+F059F | |
| 4817 | U+F05A0 | |
| 4818 | U+F05A1 | |
| 4819 | U+F05A2 | |
| 4820 | U+F05A3 | |
| 4821 | U+F05A4 | |
| 4822 | U+F05A5 | |
| 4823 | U+F05A6 | |
| 4824 | U+F05A7 | |
| 4825 | U+F05A8 | |
| 4826 | U+F05A9 | |
| 4827 | U+F05AA | |
| 4828 | U+F05AB | |
| 4829 | U+F05AC | |
| 4830 | U+F05AD | |
| 4831 | U+F05AE | |
| 4832 | U+F05AF | |
| 4833 | U+F05B0 | |
| 4834 | U+F05B1 | |
| 4835 | U+F05B2 | |
| 4836 | U+F05B3 | |
| 4837 | U+F05B4 | |
| 4838 | U+F05B5 | |
| 4839 | U+F05B6 | |
| 4840 | U+F05B7 | |
| 4841 | U+F05B8 | |
| 4842 | U+F05B9 | |
| 4843 | U+F05BA | |
| 4844 | U+F05BB | |
| 4845 | U+F05BC | |
| 4846 | U+F05BD | |
| 4847 | U+F05BE | |
| 4848 | U+F05BF | |
| 4849 | U+F05C0 | |
| 4850 | U+F05C1 | |
| 4851 | U+F05C2 | |
| 4852 | U+F05C3 | |
| 4853 | U+F05C4 | |
| 4854 | U+F05C5 | |
| 4855 | U+F05C6 | |
| 4856 | U+F05C7 | |
| 4857 | U+F05C8 | |
| 4858 | U+F05C9 | |
| 4859 | U+F05CA | |
| 4860 | U+F05CB | |
| 4861 | U+F05CC | |
| 4862 | U+F05CD | |
| 4863 | U+F05CE | |
| 4864 | U+F05CF | |
| 4865 | U+F05D0 | |
| 4866 | U+F05D1 | |
| 4867 | U+F05D2 | |
| 4868 | U+F05D3 | |
| 4869 | U+F05D4 | |
| 4870 | U+F05D5 | |
| 4871 | U+F05D6 | |
| 4872 | U+F05D7 | |
| 4873 | U+F05D8 | |
| 4874 | U+F05D9 | |
| 4875 | U+F05DA | |
| 4876 | U+F05DB | |
| 4877 | U+F05DC | |
| 4878 | U+F05DD | |
| 4879 | U+F05DE | |
| 4880 | U+F05DF | |
| 4881 | U+F05E0 | |
| 4882 | U+F05E1 | |
| 4883 | U+F05E2 | |
| 4884 | U+F05E3 | |
| 4885 | U+F05E4 | |
| 4886 | U+F05E5 | |
| 4887 | U+F05E6 | |
| 4888 | U+F05E7 | |
| 4889 | U+F05E8 | |
| 4890 | U+F05E9 | |
| 4891 | U+F05EA | |
| 4892 | U+F05EB | |
| 4893 | U+F05EC | |
| 4894 | U+F05ED | |
| 4895 | U+F05EE | |
| 4896 | U+F05EF | |
| 4897 | U+F05F0 | |
| 4898 | U+F05F1 | |
| 4899 | U+F05F2 | |
| 4900 | U+F05F3 | |
| 4901 | U+F05F4 | |
| 4902 | U+F05F5 | |
| 4903 | U+F05F6 | |
| 4904 | U+F05F7 | |
| 4905 | U+F05F8 | |
| 4906 | U+F05F9 | |
| 4907 | U+F05FA | |
| 4908 | U+F05FB | |
| 4909 | U+F05FC | |
| 4910 | U+F05FD | |
| 4911 | U+F05FE | |
| 4912 | U+F05FF | |
| 4913 | U+F0600 | |
| 4914 | U+F0601 | |
| 4915 | U+F0602 | |
| 4916 | U+F0603 | |
| 4917 | U+F0604 | |
| 4918 | U+F0605 | |
| 4919 | U+F0606 | |
| 4920 | U+F0607 | |
| 4921 | U+F0608 | |
| 4922 | U+F0609 | |
| 4923 | U+F060A | |
| 4924 | U+F060B | |
| 4925 | U+F060C | |
| 4926 | U+F060D | |
| 4927 | U+F060E | |
| 4928 | U+F060F | |
| 4929 | U+F0610 | |
| 4930 | U+F0611 | |
| 4931 | U+F0612 | |
| 4932 | U+F0613 | |
| 4933 | U+F0614 | |
| 4934 | U+F0615 | |
| 4935 | U+F0616 | |
| 4936 | U+F0617 | |
| 4937 | U+F0618 | |
| 4938 | U+F0619 | |
| 4939 | U+F061A | |
| 4940 | U+F061B | |
| 4941 | U+F061C | |
| 4942 | U+F061D | |
| 4943 | U+F061E | |
| 4944 | U+F061F | |
| 4945 | U+F0620 | |
| 4946 | U+F0621 | |
| 4947 | U+F0622 | |
| 4948 | U+F0623 | |
| 4949 | U+F0624 | |
| 4950 | U+F0625 | |
| 4951 | U+F0626 | |
| 4952 | U+F0627 | |
| 4953 | U+F0628 | |
| 4954 | U+F0629 | |
| 4955 | U+F062A | |
| 4956 | U+F062B | |
| 4957 | U+F062C | |
| 4958 | U+F062D | |
| 4959 | U+F062E | |
| 4960 | U+F062F | |
| 4961 | U+F0630 | |
| 4962 | U+F0631 | |
| 4963 | U+F0632 | |
| 4964 | U+F0633 | |
| 4965 | U+F0634 | |
| 4966 | U+F0635 | |
| 4967 | U+F0636 | |
| 4968 | U+F0637 | |
| 4969 | U+F0638 | |
| 4970 | U+F0639 | |
| 4971 | U+F063A | |
| 4972 | U+F063B | |
| 4973 | U+F063C | |
| 4974 | U+F063D | |
| 4975 | U+F063E | |
| 4976 | U+F063F | |
| 4977 | U+F0640 | |
| 4978 | U+F0641 | |
| 4979 | U+F0642 | |
| 4980 | U+F0643 | |
| 4981 | U+F0644 | |
| 4982 | U+F0645 | |
| 4983 | U+F0646 | |
| 4984 | U+F0647 | |
| 4985 | U+F0648 | |
| 4986 | U+F0649 | |
| 4987 | U+F064A | |
| 4988 | U+F064B | |
| 4989 | U+F064C | |
| 4990 | U+F064D | |
| 4991 | U+F064E | |
| 4992 | U+F064F | |
| 4993 | U+F0650 | |
| 4994 | U+F0651 | |
| 4995 | U+F0652 | |
| 4996 | U+F0653 | |
| 4997 | U+F0654 | |
| 4998 | U+F0655 | |
| 4999 | U+F0656 | |
| 5000 | U+F0657 | |
| 5001 | U+F0658 | |
| 5002 | U+F0659 | |
| 5003 | U+F065A | |
| 5004 | U+F065B | |
| 5005 | U+F065C | |
| 5006 | U+F065D | |
| 5007 | U+F065E | |
| 5008 | U+F065F | |
| 5009 | U+F0660 | |
| 5010 | U+F0661 | |
| 5011 | U+F0662 | |
| 5012 | U+F0663 | |
| 5013 | U+F0664 | |
| 5014 | U+F0665 | |
| 5015 | U+F0666 | |
| 5016 | U+F0667 | |
| 5017 | U+F0668 | |
| 5018 | U+F0669 | |
| 5019 | U+F066A | |
| 5020 | U+F066B | |
| 5021 | U+F066C | |
| 5022 | U+F066D | |
| 5023 | U+F066E | |
| 5024 | U+F066F | |
| 5025 | U+F0670 | |
| 5026 | U+F0671 | |
| 5027 | U+F0672 | |
| 5028 | U+F0673 | |
| 5029 | U+F0674 | |
| 5030 | U+F0675 | |
| 5031 | U+F0676 | |
| 5032 | U+F0677 | |
| 5033 | U+F0678 | |
| 5034 | U+F0679 | |
| 5035 | U+F067A | |
| 5036 | U+F067B | |
| 5037 | U+F067C | |
| 5038 | U+F067D | |
| 5039 | U+F067E | |
| 5040 | U+F067F | |
| 5041 | U+F0680 | |
| 5042 | U+F0681 | |
| 5043 | U+F0682 | |
| 5044 | U+F0683 | |
| 5045 | U+F0684 | |
| 5046 | U+F0685 | |
| 5047 | U+F0686 | |
| 5048 | U+F0687 | |
| 5049 | U+F0688 | |
| 5050 | U+F0689 | |
| 5051 | U+F068A | |
| 5052 | U+F068B | |
| 5053 | U+F068C | |
| 5054 | U+F068D | |
| 5055 | U+F068E | |
| 5056 | U+F068F | |
| 5057 | U+F0690 | |
| 5058 | U+F0691 | |
| 5059 | U+F0692 | |
| 5060 | U+F0693 | |
| 5061 | U+F0694 | |
| 5062 | U+F0695 | |
| 5063 | U+F0696 | |
| 5064 | U+F0697 | |
| 5065 | U+F0698 | |
| 5066 | U+F0699 | |
| 5067 | U+F069A | |
| 5068 | U+F069B | |
| 5069 | U+F069C | |
| 5070 | U+F069D | |
| 5071 | U+F069E | |
| 5072 | U+F069F | |
| 5073 | U+F06A0 | |
| 5074 | U+F06A1 | |
| 5075 | U+F06A2 | |
| 5076 | U+F06A3 | |
| 5077 | U+F06A4 | |
| 5078 | U+F06A5 | |
| 5079 | U+F06A6 | |
| 5080 | U+F06A7 | |
| 5081 | U+F06A8 | |
| 5082 | U+F06A9 | |
| 5083 | U+F06AA | |
| 5084 | U+F06AB | |
| 5085 | U+F06AC | |
| 5086 | U+F06AD | |
| 5087 | U+F06AE | |
| 5088 | U+F06AF | |
| 5089 | U+F06B0 | |
| 5090 | U+F06B1 | |
| 5091 | U+F06B2 | |
| 5092 | U+F06B3 | |
| 5093 | U+F06B4 | |
| 5094 | U+F06B5 | |
| 5095 | U+F06B6 | |
| 5096 | U+F06B7 | |
| 5097 | U+F06B8 | |
| 5098 | U+F06B9 | |
| 5099 | U+F06BA | |
| 5100 | U+F06BB | |
| 5101 | U+F06BC | |
| 5102 | U+F06BD | |
| 5103 | U+F06BE | |
| 5104 | U+F06BF | |
| 5105 | U+F06C0 | |
| 5106 | U+F06C1 | |
| 5107 | U+F06C2 | |
| 5108 | U+F06C3 | |
| 5109 | U+F06C4 | |
| 5110 | U+F06C5 | |
| 5111 | U+F06C6 | |
| 5112 | U+F06C7 | |
| 5113 | U+F06C8 | |
| 5114 | U+F06C9 | |
| 5115 | U+F06CA | |
| 5116 | U+F06CB | |
| 5117 | U+F06CC | |
| 5118 | U+F06CD | |
| 5119 | U+F06CE | |
| 5120 | U+F06CF | |
| 5121 | U+F06D0 | |
| 5122 | U+F06D1 | |
| 5123 | U+F06D2 | |
| 5124 | U+F06D3 | |
| 5125 | U+F06D4 | |
| 5126 | U+F06D5 | |
| 5127 | U+F06D6 | |
| 5128 | U+F06D7 | |
| 5129 | U+F06D8 | |
| 5130 | U+F06D9 | |
| 5131 | U+F06DA | |
| 5132 | U+F06DB | |
| 5133 | U+F06DC | |
| 5134 | U+F06DD | |
| 5135 | U+F06DE | |
| 5136 | U+F06DF | |
| 5137 | U+F06E0 | |
| 5138 | U+F06E1 | |
| 5139 | U+F06E2 | |
| 5140 | U+F06E3 | |
| 5141 | U+F06E4 | |
| 5142 | U+F06E5 | |
| 5143 | U+F06E6 | |
| 5144 | U+F06E7 | |
| 5145 | U+F06E8 | |
| 5146 | U+F06E9 | |
| 5147 | U+F06EA | |
| 5148 | U+F06EB | |
| 5149 | U+F06EC | |
| 5150 | U+F06ED | |
| 5151 | U+F06EE | |
| 5152 | U+F06EF | |
| 5153 | U+F06F0 | |
| 5154 | U+F06F1 | |
| 5155 | U+F06F2 | |
| 5156 | U+F06F3 | |
| 5157 | U+F06F4 | |
| 5158 | U+F06F5 | |
| 5159 | U+F06F6 | |
| 5160 | U+F06F7 | |
| 5161 | U+F06F8 | |
| 5162 | U+F06F9 | |
| 5163 | U+F06FA | |
| 5164 | U+F06FB | |
| 5165 | U+F06FC | |
| 5166 | U+F06FD | |
| 5167 | U+F06FE | |
| 5168 | U+F06FF | |
| 5169 | U+F0700 | |
| 5170 | U+F0701 | |
| 5171 | U+F0702 | |
| 5172 | U+F0703 | |
| 5173 | U+F0704 | |
| 5174 | U+F0705 | |
| 5175 | U+F0706 | |
| 5176 | U+F0707 | |
| 5177 | U+F0708 | |
| 5178 | U+F0709 | |
| 5179 | U+F070A | |
| 5180 | U+F070B | |
| 5181 | U+F070C | |
| 5182 | U+F070D | |
| 5183 | U+F070E | |
| 5184 | U+F070F | |
| 5185 | U+F0710 | |
| 5186 | U+F0711 | |
| 5187 | U+F0712 | |
| 5188 | U+F0713 | |
| 5189 | U+F0714 | |
| 5190 | U+F0715 | |
| 5191 | U+F0716 | |
| 5192 | U+F0717 | |
| 5193 | U+F0718 | |
| 5194 | U+F0719 | |
| 5195 | U+F071A | |
| 5196 | U+F071B | |
| 5197 | U+F071C | |
| 5198 | U+F071D | |
| 5199 | U+F071E | |
| 5200 | U+F071F | |
| 5201 | U+F0720 | |
| 5202 | U+F0721 | |
| 5203 | U+F0722 | |
| 5204 | U+F0723 | |
| 5205 | U+F0724 | |
| 5206 | U+F0725 | |
| 5207 | U+F0726 | |
| 5208 | U+F0727 | |
| 5209 | U+F0728 | |
| 5210 | U+F0729 | |
| 5211 | U+F072A | |
| 5212 | U+F072B | |
| 5213 | U+F072C | |
| 5214 | U+F072D | |
| 5215 | U+F072E | |
| 5216 | U+F072F | |
| 5217 | U+F0730 | |
| 5218 | U+F0731 | |
| 5219 | U+F0732 | |
| 5220 | U+F0733 | |
| 5221 | U+F0734 | |
| 5222 | U+F0735 | |
| 5223 | U+F0736 | |
| 5224 | U+F0737 | |
| 5225 | U+F0738 | |
| 5226 | U+F0739 | |
| 5227 | U+F073A | |
| 5228 | U+F073B | |
| 5229 | U+F073C | |
| 5230 | U+F073D | |
| 5231 | U+F073E | |
| 5232 | U+F073F | |
| 5233 | U+F0740 | |
| 5234 | U+F0741 | |
| 5235 | U+F0742 | |
| 5236 | U+F0743 | |
| 5237 | U+F0744 | |
| 5238 | U+F0745 | |
| 5239 | U+F0746 | |
| 5240 | U+F0747 | |
| 5241 | U+F0748 | |
| 5242 | U+F0749 | |
| 5243 | U+F074A | |
| 5244 | U+F074B | |
| 5245 | U+F074C | |
| 5246 | U+F074D | |
| 5247 | U+F074E | |
| 5248 | U+F074F | |
| 5249 | U+F0750 | |
| 5250 | U+F0751 | |
| 5251 | U+F0752 | |
| 5252 | U+F0753 | |
| 5253 | U+F0754 | |
| 5254 | U+F0755 | |
| 5255 | U+F0756 | |
| 5256 | U+F0757 | |
| 5257 | U+F0758 | |
| 5258 | U+F0759 | |
| 5259 | U+F075A | |
| 5260 | U+F075B | |
| 5261 | U+F075C | |
| 5262 | U+F075D | |
| 5263 | U+F075E | |
| 5264 | U+F075F | |
| 5265 | U+F0760 | |
| 5266 | U+F0761 | |
| 5267 | U+F0762 | |
| 5268 | U+F0763 | |
| 5269 | U+F0764 | |
| 5270 | U+F0765 | |
| 5271 | U+F0766 | |
| 5272 | U+F0767 | |
| 5273 | U+F0768 | |
| 5274 | U+F0769 | |
| 5275 | U+F076A | |
| 5276 | U+F076B | |
| 5277 | U+F076C | |
| 5278 | U+F076D | |
| 5279 | U+F076E | |
| 5280 | U+F076F | |
| 5281 | U+F0770 | |
| 5282 | U+F0771 | |
| 5283 | U+F0772 | |
| 5284 | U+F0773 | |
| 5285 | U+F0774 | |
| 5286 | U+F0775 | |
| 5287 | U+F0776 | |
| 5288 | U+F0777 | |
| 5289 | U+F0778 | |
| 5290 | U+F0779 | |
| 5291 | U+F077A | |
| 5292 | U+F077B | |
| 5293 | U+F077C | |
| 5294 | U+F077D | |
| 5295 | U+F077E | |
| 5296 | U+F077F | |
| 5297 | U+F0780 | |
| 5298 | U+F0781 | |
| 5299 | U+F0782 | |
| 5300 | U+F0783 | |
| 5301 | U+F0784 | |
| 5302 | U+F0785 | |
| 5303 | U+F0786 | |
| 5304 | U+F0787 | |
| 5305 | U+F0788 | |
| 5306 | U+F0789 | |
| 5307 | U+F078A | |
| 5308 | U+F078B | |
| 5309 | U+F078C | |
| 5310 | U+F078D | |
| 5311 | U+F078E | |
| 5312 | U+F078F | |
| 5313 | U+F0790 | |
| 5314 | U+F0791 | |
| 5315 | U+F0792 | |
| 5316 | U+F0793 | |
| 5317 | U+F0794 | |
| 5318 | U+F0795 | |
| 5319 | U+F0796 | |
| 5320 | U+F0797 | |
| 5321 | U+F0798 | |
| 5322 | U+F0799 | |
| 5323 | U+F079A | |
| 5324 | U+F079B | |
| 5325 | U+F079C | |
| 5326 | U+F079D | |
| 5327 | U+F079E | |
| 5328 | U+F079F | |
| 5329 | U+F07A0 | |
| 5330 | U+F07A1 | |
| 5331 | U+F07A2 | |
| 5332 | U+F07A3 | |
| 5333 | U+F07A4 | |
| 5334 | U+F07A5 | |
| 5335 | U+F07A6 | |
| 5336 | U+F07A7 | |
| 5337 | U+F07A8 | |
| 5338 | U+F07A9 | |
| 5339 | U+F07AA | |
| 5340 | U+F07AB | |
| 5341 | U+F07AC | |
| 5342 | U+F07AD | |
| 5343 | U+F07AE | |
| 5344 | U+F07AF | |
| 5345 | U+F07B0 | |
| 5346 | U+F07B1 | |
| 5347 | U+F07B2 | |
| 5348 | U+F07B3 | |
| 5349 | U+F07B4 | |
| 5350 | U+F07B5 | |
| 5351 | U+F07B6 | |
| 5352 | U+F07B7 | |
| 5353 | U+F07B8 | |
| 5354 | U+F07B9 | |
| 5355 | U+F07BA | |
| 5356 | U+F07BB | |
| 5357 | U+F07BC | |
| 5358 | U+F07BD | |
| 5359 | U+F07BE | |
| 5360 | U+F07BF | |
| 5361 | U+F07C0 | |
| 5362 | U+F07C1 | |
| 5363 | U+F07C2 | |
| 5364 | U+F07C3 | |
| 5365 | U+F07C4 | |
| 5366 | U+F07C5 | |
| 5367 | U+F07C6 | |
| 5368 | U+F07C7 | |
| 5369 | U+F07C8 | |
| 5370 | U+F07C9 | |
| 5371 | U+F07CA | |
| 5372 | U+F07CB | |
| 5373 | U+F07CC | |
| 5374 | U+F07CD | |
| 5375 | U+F07CE | |
| 5376 | U+F07CF | |
| 5377 | U+F07D0 | |
| 5378 | U+F07D1 | |
| 5379 | U+F07D2 | |
| 5380 | U+F07D3 | |
| 5381 | U+F07D4 | |
| 5382 | U+F07D5 | |
| 5383 | U+F07D6 | |
| 5384 | U+F07D7 | |
| 5385 | U+F07D8 | |
| 5386 | U+F07D9 | |
| 5387 | U+F07DA | |
| 5388 | U+F07DB | |
| 5389 | U+F07DC | |
| 5390 | U+F07DD | |
| 5391 | U+F07DE | |
| 5392 | U+F07DF | |
| 5393 | U+F07E0 | |
| 5394 | U+F07E1 | |
| 5395 | U+F07E2 | |
| 5396 | U+F07E3 | |
| 5397 | U+F07E4 | |
| 5398 | U+F07E5 | |
| 5399 | U+F07E6 | |
| 5400 | U+F07E7 | |
| 5401 | U+F07E8 | |
| 5402 | U+F07E9 | |
| 5403 | U+F07EA | |
| 5404 | U+F07EB | |
| 5405 | U+F07EC | |
| 5406 | U+F07ED | |
| 5407 | U+F07EE | |
| 5408 | U+F07EF | |
| 5409 | U+F07F0 | |
| 5410 | U+F07F1 | |
| 5411 | U+F07F2 | |
| 5412 | U+F07F3 | |
| 5413 | U+F07F4 | |
| 5414 | U+F07F5 | |
| 5415 | U+F07F6 | |
| 5416 | U+F07F7 | |
| 5417 | U+F07F8 | |
| 5418 | U+F07F9 | |
| 5419 | U+F07FA | |
| 5420 | U+F07FB | |
| 5421 | U+F07FC | |
| 5422 | U+F07FD | |
| 5423 | U+F07FE | |
| 5424 | U+F07FF | |
| 5425 | U+F0800 | |
| 5426 | U+F0801 | |
| 5427 | U+F0802 | |
| 5428 | U+F0803 | |
| 5429 | U+F0804 | |
| 5430 | U+F0805 | |
| 5431 | U+F0806 | |
| 5432 | U+F0807 | |
| 5433 | U+F0808 | |
| 5434 | U+F0809 | |
| 5435 | U+F080A | |
| 5436 | U+F080B | |
| 5437 | U+F080C | |
| 5438 | U+F080D | |
| 5439 | U+F080E | |
| 5440 | U+F080F | |
| 5441 | U+F0810 | |
| 5442 | U+F0811 | |
| 5443 | U+F0812 | |
| 5444 | U+F0813 | |
| 5445 | U+F0814 | |
| 5446 | U+F0815 | |
| 5447 | U+F0816 | |
| 5448 | U+F0817 | |
| 5449 | U+F0818 | |
| 5450 | U+F0819 | |
| 5451 | U+F081A | |
| 5452 | U+F081B | |
| 5453 | U+F081C | |
| 5454 | U+F081D | |
| 5455 | U+F081E | |
| 5456 | U+F081F | |
| 5457 | U+F0820 | |
| 5458 | U+F0821 | |
| 5459 | U+F0822 | |
| 5460 | U+F0823 | |
| 5461 | U+F0824 | |
| 5462 | U+F0825 | |
| 5463 | U+F0826 | |
| 5464 | U+F0827 | |
| 5465 | U+F0828 | |
| 5466 | U+F0829 | |
| 5467 | U+F082A | |
| 5468 | U+F082B | |
| 5469 | U+F082C | |
| 5470 | U+F082D | |
| 5471 | U+F082E | |
| 5472 | U+F082F | |
| 5473 | U+F0830 | |
| 5474 | U+F0831 | |
| 5475 | U+F0832 | |
| 5476 | U+F0833 | |
| 5477 | U+F0834 | |
| 5478 | U+F0835 | |
| 5479 | U+F0836 | |
| 5480 | U+F0837 | |
| 5481 | U+F0838 | |
| 5482 | U+F0839 | |
| 5483 | U+F083A | |
| 5484 | U+F083B | |
| 5485 | U+F083C | |
| 5486 | U+F083D | |
| 5487 | U+F083E | |
| 5488 | U+F083F | |
| 5489 | U+F0840 | |
| 5490 | U+F0841 | |
| 5491 | U+F0842 | |
| 5492 | U+F0843 | |
| 5493 | U+F0844 | |
| 5494 | U+F0845 | |
| 5495 | U+F0846 | |
| 5496 | U+F0847 | |
| 5497 | U+F0848 | |
| 5498 | U+F0849 | |
| 5499 | U+F084A | |
| 5500 | U+F084B | |
| 5501 | U+F084C | |
| 5502 | U+F084D | |
| 5503 | U+F084E | |
| 5504 | U+F084F | |
| 5505 | U+F0850 | |
| 5506 | U+F0851 | |
| 5507 | U+F0852 | |
| 5508 | U+F0853 | |
| 5509 | U+F0854 | |
| 5510 | U+F0855 | |
| 5511 | U+F0856 | |
| 5512 | U+F0857 | |
| 5513 | U+F0858 | |
| 5514 | U+F0859 | |
| 5515 | U+F085A | |
| 5516 | U+F085B | |
| 5517 | U+F085C | |
| 5518 | U+F085D | |
| 5519 | U+F085E | |
| 5520 | U+F085F | |
| 5521 | U+F0860 | |
| 5522 | U+F0861 | |
| 5523 | U+F0862 | |
| 5524 | U+F0863 | |
| 5525 | U+F0864 | |
| 5526 | U+F0865 | |
| 5527 | U+F0866 | |
| 5528 | U+F0867 | |
| 5529 | U+F0868 | |
| 5530 | U+F0869 | |
| 5531 | U+F086A | |
| 5532 | U+F086B | |
| 5533 | U+F086C | |
| 5534 | U+F086D | |
| 5535 | U+F086E | |
| 5536 | U+F086F | |
| 5537 | U+F0870 | |
| 5538 | U+F0871 | |
| 5539 | U+F0872 | |
| 5540 | U+F0873 | |
| 5541 | U+F0874 | |
| 5542 | U+F0875 | |
| 5543 | U+F0876 | |
| 5544 | U+F0877 | |
| 5545 | U+F0878 | |
| 5546 | U+F0879 | |
| 5547 | U+F087A | |
| 5548 | U+F087B | |
| 5549 | U+F087C | |
| 5550 | U+F087D | |
| 5551 | U+F087E | |
| 5552 | U+F087F | |
| 5553 | U+F0880 | |
| 5554 | U+F0881 | |
| 5555 | U+F0882 | |
| 5556 | U+F0883 | |
| 5557 | U+F0884 | |
| 5558 | U+F0885 | |
| 5559 | U+F0886 | |
| 5560 | U+F0887 | |
| 5561 | U+F0888 | |
| 5562 | U+F0889 | |
| 5563 | U+F088A | |
| 5564 | U+F088B | |
| 5565 | U+F088C | |
| 5566 | U+F088D | |
| 5567 | U+F088E | |
| 5568 | U+F088F | |
| 5569 | U+F0890 | |
| 5570 | U+F0891 | |
| 5571 | U+F0892 | |
| 5572 | U+F0893 | |
| 5573 | U+F0894 | |
| 5574 | U+F0895 | |
| 5575 | U+F0896 | |
| 5576 | U+F0897 | |
| 5577 | U+F0898 | |
| 5578 | U+F0899 | |
| 5579 | U+F089A | |
| 5580 | U+F089B | |
| 5581 | U+F089C | |
| 5582 | U+F089D | |
| 5583 | U+F089E | |
| 5584 | U+F089F | |
| 5585 | U+F08A0 | |
| 5586 | U+F08A1 | |
| 5587 | U+F08A2 | |
| 5588 | U+F08A3 | |
| 5589 | U+F08A4 | |
| 5590 | U+F08A5 | |
| 5591 | U+F08A6 | |
| 5592 | U+F08A7 | |
| 5593 | U+F08A8 | |
| 5594 | U+F08A9 | |
| 5595 | U+F08AA | |
| 5596 | U+F08AB | |
| 5597 | U+F08AC | |
| 5598 | U+F08AD | |
| 5599 | U+F08AE | |
| 5600 | U+F08AF | |
| 5601 | U+F08B0 | |
| 5602 | U+F08B1 | |
| 5603 | U+F08B2 | |
| 5604 | U+F08B3 | |
| 5605 | U+F08B4 | |
| 5606 | U+F08B5 | |
| 5607 | U+F08B6 | |
| 5608 | U+F08B7 | |
| 5609 | U+F08B8 | |
| 5610 | U+F08B9 | |
| 5611 | U+F08BA | |
| 5612 | U+F08BB | |
| 5613 | U+F08BC | |
| 5614 | U+F08BD | |
| 5615 | U+F08BE | |
| 5616 | U+F08BF | |
| 5617 | U+F08C0 | |
| 5618 | U+F08C1 | |
| 5619 | U+F08C2 | |
| 5620 | U+F08C3 | |
| 5621 | U+F08C4 | |
| 5622 | U+F08C5 | |
| 5623 | U+F08C6 | |
| 5624 | U+F08C7 | |
| 5625 | U+F08C8 | |
| 5626 | U+F08C9 | |
| 5627 | U+F08CA | |
| 5628 | U+F08CB | |
| 5629 | U+F08CC | |
| 5630 | U+F08CD | |
| 5631 | U+F08CE | |
| 5632 | U+F08CF | |
| 5633 | U+F08D0 | |
| 5634 | U+F08D1 | |
| 5635 | U+F08D2 | |
| 5636 | U+F08D3 | |
| 5637 | U+F08D4 | |
| 5638 | U+F08D5 | |
| 5639 | U+F08D6 | |
| 5640 | U+F08D7 | |
| 5641 | U+F08D8 | |
| 5642 | U+F08D9 | |
| 5643 | U+F08DA | |
| 5644 | U+F08DB | |
| 5645 | U+F08DC | |
| 5646 | U+F08DD | |
| 5647 | U+F08DE | |
| 5648 | U+F08DF | |
| 5649 | U+F08E0 | |
| 5650 | U+F08E1 | |
| 5651 | U+F08E2 | |
| 5652 | U+F08E3 | |
| 5653 | U+F08E4 | |
| 5654 | U+F08E5 | |
| 5655 | U+F08E6 | |
| 5656 | U+F08E7 | |
| 5657 | U+F08E8 | |
| 5658 | U+F08E9 | |
| 5659 | U+F08EA | |
| 5660 | U+F08EB | |
| 5661 | U+F08EC | |
| 5662 | U+F08ED | |
| 5663 | U+F08EE | |
| 5664 | U+F08EF | |
| 5665 | U+F08F0 | |
| 5666 | U+F08F1 | |
| 5667 | U+F08F2 | |
| 5668 | U+F08F3 | |
| 5669 | U+F08F4 | |
| 5670 | U+F08F5 | |
| 5671 | U+F08F6 | |
| 5672 | U+F08F7 | |
| 5673 | U+F08F8 | |
| 5674 | U+F08F9 | |
| 5675 | U+F08FA | |
| 5676 | U+F08FB | |
| 5677 | U+F08FC | |
| 5678 | U+F08FD | |
| 5679 | U+F08FE | |
| 5680 | U+F08FF | |
| 5681 | U+F0900 | |
| 5682 | U+F0901 | |
| 5683 | U+F0902 | |
| 5684 | U+F0903 | |
| 5685 | U+F0904 | |
| 5686 | U+F0905 | |
| 5687 | U+F0906 | |
| 5688 | U+F0907 | |
| 5689 | U+F0908 | |
| 5690 | U+F0909 | |
| 5691 | U+F090A | |
| 5692 | U+F090B | |
| 5693 | U+F090C | |
| 5694 | U+F090D | |
| 5695 | U+F090E | |
| 5696 | U+F090F | |
| 5697 | U+F0910 | |
| 5698 | U+F0911 | |
| 5699 | U+F0912 | |
| 5700 | U+F0913 | |
| 5701 | U+F0914 | |
| 5702 | U+F0915 | |
| 5703 | U+F0916 | |
| 5704 | U+F0917 | |
| 5705 | U+F0918 | |
| 5706 | U+F0919 | |
| 5707 | U+F091A | |
| 5708 | U+F091B | |
| 5709 | U+F091C | |
| 5710 | U+F091D | |
| 5711 | U+F091E | |
| 5712 | U+F091F | |
| 5713 | U+F0920 | |
| 5714 | U+F0921 | |
| 5715 | U+F0922 | |
| 5716 | U+F0923 | |
| 5717 | U+F0924 | |
| 5718 | U+F0925 | |
| 5719 | U+F0926 | |
| 5720 | U+F0927 | |
| 5721 | U+F0928 | |
| 5722 | U+F0929 | |
| 5723 | U+F092A | |
| 5724 | U+F092B | |
| 5725 | U+F092C | |
| 5726 | U+F092D | |
| 5727 | U+F092E | |
| 5728 | U+F092F | |
| 5729 | U+F0930 | |
| 5730 | U+F0931 | |
| 5731 | U+F0932 | |
| 5732 | U+F0933 | |
| 5733 | U+F0934 | |
| 5734 | U+F0935 | |
| 5735 | U+F0936 | |
| 5736 | U+F0937 | |
| 5737 | U+F0938 | |
| 5738 | U+F0939 | |
| 5739 | U+F093A | |
| 5740 | U+F093B | |
| 5741 | U+F093C | |
| 5742 | U+F093D | |
| 5743 | U+F093E | |
| 5744 | U+F093F | |
| 5745 | U+F0940 | |
| 5746 | U+F0941 | |
| 5747 | U+F0942 | |
| 5748 | U+F0943 | |
| 5749 | U+F0944 | |
| 5750 | U+F0945 | |
| 5751 | U+F0946 | |
| 5752 | U+F0947 | |
| 5753 | U+F0948 | |
| 5754 | U+F0949 | |
| 5755 | U+F094A | |
| 5756 | U+F094B | |
| 5757 | U+F094C | |
| 5758 | U+F094D | |
| 5759 | U+F094E | |
| 5760 | U+F094F | |
| 5761 | U+F0950 | |
| 5762 | U+F0951 | |
| 5763 | U+F0952 | |
| 5764 | U+F0953 | |
| 5765 | U+F0954 | |
| 5766 | U+F0955 | |
| 5767 | U+F0956 | |
| 5768 | U+F0957 | |
| 5769 | U+F0958 | |
| 5770 | U+F0959 | |
| 5771 | U+F095A | |
| 5772 | U+F095B | |
| 5773 | U+F095C | |
| 5774 | U+F095D | |
| 5775 | U+F095E | |
| 5776 | U+F095F | |
| 5777 | U+F0960 | |
| 5778 | U+F0961 | |
| 5779 | U+F0962 | |
| 5780 | U+F0963 | |
| 5781 | U+F0964 | |
| 5782 | U+F0965 | |
| 5783 | U+F0966 | |
| 5784 | U+F0967 | |
| 5785 | U+F0968 | |
| 5786 | U+F0969 | |
| 5787 | U+F096A | |
| 5788 | U+F096B | |
| 5789 | U+F096C | |
| 5790 | U+F096D | |
| 5791 | U+F096E | |
| 5792 | U+F096F | |
| 5793 | U+F0970 | |
| 5794 | U+F0971 | |
| 5795 | U+F0972 | |
| 5796 | U+F0973 | |
| 5797 | U+F0974 | |
| 5798 | U+F0975 | |
| 5799 | U+F0976 | |
| 5800 | U+F0977 | |
| 5801 | U+F0978 | |
| 5802 | U+F0979 | |
| 5803 | U+F097A | |
| 5804 | U+F097B | |
| 5805 | U+F097C | |
| 5806 | U+F097D | |
| 5807 | U+F097E | |
| 5808 | U+F097F | |
| 5809 | U+F0980 | |
| 5810 | U+F0981 | |
| 5811 | U+F0982 | |
| 5812 | U+F0983 | |
| 5813 | U+F0984 | |
| 5814 | U+F0985 | |
| 5815 | U+F0986 | |
| 5816 | U+F0987 | |
| 5817 | U+F0988 | |
| 5818 | U+F0989 | |
| 5819 | U+F098A | |
| 5820 | U+F098B | |
| 5821 | U+F098C | |
| 5822 | U+F098D | |
| 5823 | U+F098E | |
| 5824 | U+F098F | |
| 5825 | U+F0990 | |
| 5826 | U+F0991 | |
| 5827 | U+F0992 | |
| 5828 | U+F0993 | |
| 5829 | U+F0994 | |
| 5830 | U+F0995 | |
| 5831 | U+F0996 | |
| 5832 | U+F0997 | |
| 5833 | U+F0998 | |
| 5834 | U+F0999 | |
| 5835 | U+F099A | |
| 5836 | U+F099B | |
| 5837 | U+F099C | |
| 5838 | U+F099D | |
| 5839 | U+F099E | |
| 5840 | U+F099F | |
| 5841 | U+F09A0 | |
| 5842 | U+F09A1 | |
| 5843 | U+F09A2 | |
| 5844 | U+F09A3 | |
| 5845 | U+F09A4 | |
| 5846 | U+F09A5 | |
| 5847 | U+F09A6 | |
| 5848 | U+F09A7 | |
| 5849 | U+F09A8 | |
| 5850 | U+F09A9 | |
| 5851 | U+F09AA | |
| 5852 | U+F09AB | |
| 5853 | U+F09AC | |
| 5854 | U+F09AD | |
| 5855 | U+F09AE | |
| 5856 | U+F09AF | |
| 5857 | U+F09B0 | |
| 5858 | U+F09B1 | |
| 5859 | U+F09B2 | |
| 5860 | U+F09B3 | |
| 5861 | U+F09B4 | |
| 5862 | U+F09B5 | |
| 5863 | U+F09B6 | |
| 5864 | U+F09B7 | |
| 5865 | U+F09B8 | |
| 5866 | U+F09B9 | |
| 5867 | U+F09BA | |
| 5868 | U+F09BB | |
| 5869 | U+F09BC | |
| 5870 | U+F09BD | |
| 5871 | U+F09BE | |
| 5872 | U+F09BF | |
| 5873 | U+F09C0 | |
| 5874 | U+F09C1 | |
| 5875 | U+F09C2 | |
| 5876 | U+F09C3 | |
| 5877 | U+F09C4 | |
| 5878 | U+F09C5 | |
| 5879 | U+F09C6 | |
| 5880 | U+F09C7 | |
| 5881 | U+F09C8 | |
| 5882 | U+F09C9 | |
| 5883 | U+F09CA | |
| 5884 | U+F09CB | |
| 5885 | U+F09CC | |
| 5886 | U+F09CD | |
| 5887 | U+F09CE | |
| 5888 | U+F09CF | |
| 5889 | U+F09D0 | |
| 5890 | U+F09D1 | |
| 5891 | U+F09D2 | |
| 5892 | U+F09D3 | |
| 5893 | U+F09D4 | |
| 5894 | U+F09D5 | |
| 5895 | U+F09D6 | |
| 5896 | U+F09D7 | |
| 5897 | U+F09D8 | |
| 5898 | U+F09D9 | |
| 5899 | U+F09DA | |
| 5900 | U+F09DB | |
| 5901 | U+F09DC | |
| 5902 | U+F09DD | |
| 5903 | U+F09DE | |
| 5904 | U+F09DF | |
| 5905 | U+F09E0 | |
| 5906 | U+F09E1 | |
| 5907 | U+F09E2 | |
| 5908 | U+F09E3 | |
| 5909 | U+F09E4 | |
| 5910 | U+F09E5 | |
| 5911 | U+F09E6 | |
| 5912 | U+F09E7 | |
| 5913 | U+F09E8 | |
| 5914 | U+F09E9 | |
| 5915 | U+F09EA | |
| 5916 | U+F09EB | |
| 5917 | U+F09EC | |
| 5918 | U+F09ED | |
| 5919 | U+F09EE | |
| 5920 | U+F09EF | |
| 5921 | U+F09F0 | |
| 5922 | U+F09F1 | |
| 5923 | U+F09F2 | |
| 5924 | U+F09F3 | |
| 5925 | U+F09F4 | |
| 5926 | U+F09F5 | |
| 5927 | U+F09F6 | |
| 5928 | U+F09F7 | |
| 5929 | U+F09F8 | |
| 5930 | U+F09F9 | |
| 5931 | U+F09FA | |
| 5932 | U+F09FB | |
| 5933 | U+F09FC | |
| 5934 | U+F09FD | |
| 5935 | U+F09FE | |
| 5936 | U+F09FF | |
| 5937 | U+F0A00 | |
| 5938 | U+F0A01 | |
| 5939 | U+F0A02 | |
| 5940 | U+F0A03 | |
| 5941 | U+F0A04 | |
| 5942 | U+F0A05 | |
| 5943 | U+F0A06 | |
| 5944 | U+F0A07 | |
| 5945 | U+F0A08 | |
| 5946 | U+F0A09 | |
| 5947 | U+F0A0A | |
| 5948 | U+F0A0B | |
| 5949 | U+F0A0C | |
| 5950 | U+F0A0D | |
| 5951 | U+F0A0E | |
| 5952 | U+F0A0F | |
| 5953 | U+F0A10 | |
| 5954 | U+F0A11 | |
| 5955 | U+F0A12 | |
| 5956 | U+F0A13 | |
| 5957 | U+F0A14 | |
| 5958 | U+F0A15 | |
| 5959 | U+F0A16 | |
| 5960 | U+F0A17 | |
| 5961 | U+F0A18 | |
| 5962 | U+F0A19 | |
| 5963 | U+F0A1A | |
| 5964 | U+F0A1B | |
| 5965 | U+F0A1C | |
| 5966 | U+F0A1D | |
| 5967 | U+F0A1E | |
| 5968 | U+F0A1F | |
| 5969 | U+F0A20 | |
| 5970 | U+F0A21 | |
| 5971 | U+F0A22 | |
| 5972 | U+F0A23 | |
| 5973 | U+F0A24 | |
| 5974 | U+F0A25 | |
| 5975 | U+F0A26 | |
| 5976 | U+F0A27 | |
| 5977 | U+F0A28 | |
| 5978 | U+F0A29 | |
| 5979 | U+F0A2A | |
| 5980 | U+F0A2B | |
| 5981 | U+F0A2C | |
| 5982 | U+F0A2D | |
| 5983 | U+F0A2E | |
| 5984 | U+F0A2F | |
| 5985 | U+F0A30 | |
| 5986 | U+F0A31 | |
| 5987 | U+F0A32 | |
| 5988 | U+F0A33 | |
| 5989 | U+F0A34 | |
| 5990 | U+F0A35 | |
| 5991 | U+F0A36 | |
| 5992 | U+F0A37 | |
| 5993 | U+F0A38 | |
| 5994 | U+F0A39 | |
| 5995 | U+F0A3A | |
| 5996 | U+F0A3B | |
| 5997 | U+F0A3C | |
| 5998 | U+F0A3D | |
| 5999 | U+F0A3E | |
| 6000 | U+F0A3F | |
| 6001 | U+F0A40 | |
| 6002 | U+F0A41 | |
| 6003 | U+F0A42 | |
| 6004 | U+F0A43 | |
| 6005 | U+F0A44 | |
| 6006 | U+F0A45 | |
| 6007 | U+F0A46 | |
| 6008 | U+F0A47 | |
| 6009 | U+F0A48 | |
| 6010 | U+F0A49 | |
| 6011 | U+F0A4A | |
| 6012 | U+F0A4B | |
| 6013 | U+F0A4C | |
| 6014 | U+F0A4D | |
| 6015 | U+F0A4E | |
| 6016 | U+F0A4F | |
| 6017 | U+F0A50 | |
| 6018 | U+F0A51 | |
| 6019 | U+F0A52 | |
| 6020 | U+F0A53 | |
| 6021 | U+F0A54 | |
| 6022 | U+F0A55 | |
| 6023 | U+F0A56 | |
| 6024 | U+F0A57 | |
| 6025 | U+F0A58 | |
| 6026 | U+F0A59 | |
| 6027 | U+F0A5A | |
| 6028 | U+F0A5B | |
| 6029 | U+F0A5C | |
| 6030 | U+F0A5D | |
| 6031 | U+F0A5E | |
| 6032 | U+F0A5F | |
| 6033 | U+F0A60 | |
| 6034 | U+F0A61 | |
| 6035 | U+F0A62 | |
| 6036 | U+F0A63 | |
| 6037 | U+F0A64 | |
| 6038 | U+F0A65 | |
| 6039 | U+F0A66 | |
| 6040 | U+F0A67 | |
| 6041 | U+F0A68 | |
| 6042 | U+F0A69 | |
| 6043 | U+F0A6A | |
| 6044 | U+F0A6B | |
| 6045 | U+F0A6C | |
| 6046 | U+F0A6D | |
| 6047 | U+F0A6E | |
| 6048 | U+F0A6F | |
| 6049 | U+F0A70 | |
| 6050 | U+F0A71 | |
| 6051 | U+F0A72 | |
| 6052 | U+F0A73 | |
| 6053 | U+F0A74 | |
| 6054 | U+F0A75 | |
| 6055 | U+F0A76 | |
| 6056 | U+F0A77 | |
| 6057 | U+F0A78 | |
| 6058 | U+F0A79 | |
| 6059 | U+F0A7A | |
| 6060 | U+F0A7B | |
| 6061 | U+F0A7C | |
| 6062 | U+F0A7D | |
| 6063 | U+F0A7E | |
| 6064 | U+F0A7F | |
| 6065 | U+F0A80 | |
| 6066 | U+F0A81 | |
| 6067 | U+F0A82 | |
| 6068 | U+F0A83 | |
| 6069 | U+F0A84 | |
| 6070 | U+F0A85 | |
| 6071 | U+F0A86 | |
| 6072 | U+F0A87 | |
| 6073 | U+F0A88 | |
| 6074 | U+F0A89 | |
| 6075 | U+F0A8A | |
| 6076 | U+F0A8B | |
| 6077 | U+F0A8C | |
| 6078 | U+F0A8D | |
| 6079 | U+F0A8E | |
| 6080 | U+F0A8F | |
| 6081 | U+F0A90 | |
| 6082 | U+F0A91 | |
| 6083 | U+F0A92 | |
| 6084 | U+F0A93 | |
| 6085 | U+F0A94 | |
| 6086 | U+F0A95 | |
| 6087 | U+F0A96 | |
| 6088 | U+F0A97 | |
| 6089 | U+F0A98 | |
| 6090 | U+F0A99 | |
| 6091 | U+F0A9A | |
| 6092 | U+F0A9B | |
| 6093 | U+F0A9C | |
| 6094 | U+F0A9D | |
| 6095 | U+F0A9E | |
| 6096 | U+F0A9F | |
| 6097 | U+F0AA0 | |
| 6098 | U+F0AA1 | |
| 6099 | U+F0AA2 | |
| 6100 | U+F0AA3 | |
| 6101 | U+F0AA4 | |
| 6102 | U+F0AA5 | |
| 6103 | U+F0AA6 | |
| 6104 | U+F0AA7 | |
| 6105 | U+F0AA8 | |
| 6106 | U+F0AA9 | |
| 6107 | U+F0AAA | |
| 6108 | U+F0AAB | |
| 6109 | U+F0AAC | |
| 6110 | U+F0AAD | |
| 6111 | U+F0AAE | |
| 6112 | U+F0AAF | |
| 6113 | U+F0AB0 | |
| 6114 | U+F0AB1 | |
| 6115 | U+F0AB2 | |
| 6116 | U+F0AB3 | |
| 6117 | U+F0AB4 | |
| 6118 | U+F0AB5 | |
| 6119 | U+F0AB6 | |
| 6120 | U+F0AB7 | |
| 6121 | U+F0AB8 | |
| 6122 | U+F0AB9 | |
| 6123 | U+F0ABA | |
| 6124 | U+F0ABB | |
| 6125 | U+F0ABC | |
| 6126 | U+F0ABD | |
| 6127 | U+F0ABE | |
| 6128 | U+F0ABF | |
| 6129 | U+F0AC0 | |
| 6130 | U+F0AC1 | |
| 6131 | U+F0AC2 | |
| 6132 | U+F0AC3 | |
| 6133 | U+F0AC4 | |
| 6134 | U+F0AC5 | |
| 6135 | U+F0AC6 | |
| 6136 | U+F0AC7 | |
| 6137 | U+F0AC8 | |
| 6138 | U+F0AC9 | |
| 6139 | U+F0ACA | |
| 6140 | U+F0ACB | |
| 6141 | U+F0ACC | |
| 6142 | U+F0ACD | |
| 6143 | U+F0ACE | |
| 6144 | U+F0ACF | |
| 6145 | U+F0AD0 | |
| 6146 | U+F0AD1 | |
| 6147 | U+F0AD2 | |
| 6148 | U+F0AD3 | |
| 6149 | U+F0AD4 | |
| 6150 | U+F0AD5 | |
| 6151 | U+F0AD6 | |
| 6152 | U+F0AD7 | |
| 6153 | U+F0AD8 | |
| 6154 | U+F0AD9 | |
| 6155 | U+F0ADA | |
| 6156 | U+F0ADB | |
| 6157 | U+F0ADC | |
| 6158 | U+F0ADD | |
| 6159 | U+F0ADE | |
| 6160 | U+F0ADF | |
| 6161 | U+F0AE0 | |
| 6162 | U+F0AE1 | |
| 6163 | U+F0AE2 | |
| 6164 | U+F0AE3 | |
| 6165 | U+F0AE4 | |
| 6166 | U+F0AE5 | |
| 6167 | U+F0AE6 | |
| 6168 | U+F0AE7 | |
| 6169 | U+F0AE8 | |
| 6170 | U+F0AE9 | |
| 6171 | U+F0AEA | |
| 6172 | U+F0AEB | |
| 6173 | U+F0AEC | |
| 6174 | U+F0AED | |
| 6175 | U+F0AEE | |
| 6176 | U+F0AEF | |
| 6177 | U+F0AF0 | |
| 6178 | U+F0AF1 | |
| 6179 | U+F0AF2 | |
| 6180 | U+F0AF3 | |
| 6181 | U+F0AF4 | |
| 6182 | U+F0AF5 | |
| 6183 | U+F0AF6 | |
| 6184 | U+F0AF7 | |
| 6185 | U+F0AF8 | |
| 6186 | U+F0AF9 | |
| 6187 | U+F0AFA | |
| 6188 | U+F0AFB | |
| 6189 | U+F0AFC | |
| 6190 | U+F0AFD | |
| 6191 | U+F0AFE | |
| 6192 | U+F0AFF | |
| 6193 | U+F0B00 | |
| 6194 | U+F0B01 | |
| 6195 | U+F0B02 | |
| 6196 | U+F0B03 | |
| 6197 | U+F0B04 | |
| 6198 | U+F0B05 | |
| 6199 | U+F0B06 | |
| 6200 | U+F0B07 | |
| 6201 | U+F0B08 | |
| 6202 | U+F0B09 | |
| 6203 | U+F0B0A | |
| 6204 | U+F0B0B | |
| 6205 | U+F0B0C | |
| 6206 | U+F0B0D | |
| 6207 | U+F0B0E | |
| 6208 | U+F0B0F | |
| 6209 | U+F0B10 | |
| 6210 | U+F0B11 | |
| 6211 | U+F0B12 | |
| 6212 | U+F0B13 | |
| 6213 | U+F0B14 | |
| 6214 | U+F0B15 | |
| 6215 | U+F0B16 | |
| 6216 | U+F0B17 | |
| 6217 | U+F0B18 | |
| 6218 | U+F0B19 | |
| 6219 | U+F0B1A | |
| 6220 | U+F0B1B | |
| 6221 | U+F0B1C | |
| 6222 | U+F0B1D | |
| 6223 | U+F0B1E | |
| 6224 | U+F0B1F | |
| 6225 | U+F0B20 | |
| 6226 | U+F0B21 | |
| 6227 | U+F0B22 | |
| 6228 | U+F0B23 | |
| 6229 | U+F0B24 | |
| 6230 | U+F0B25 | |
| 6231 | U+F0B26 | |
| 6232 | U+F0B27 | |
| 6233 | U+F0B28 | |
| 6234 | U+F0B29 | |
| 6235 | U+F0B2A | |
| 6236 | U+F0B2B | |
| 6237 | U+F0B2C | |
| 6238 | U+F0B2D | |
| 6239 | U+F0B2E | |
| 6240 | U+F0B2F | |
| 6241 | U+F0B30 | |
| 6242 | U+F0B31 | |
| 6243 | U+F0B32 | |
| 6244 | U+F0B33 | |
| 6245 | U+F0B34 | |
| 6246 | U+F0B35 | |
| 6247 | U+F0B36 | |
| 6248 | U+F0B37 | |
| 6249 | U+F0B38 | |
| 6250 | U+F0B39 | |
| 6251 | U+F0B3A | |
| 6252 | U+F0B3B | |
| 6253 | U+F0B3C | |
| 6254 | U+F0B3D | |
| 6255 | U+F0B3E | |
| 6256 | U+F0B3F | |
| 6257 | U+F0B40 | |
| 6258 | U+F0B41 | |
| 6259 | U+F0B42 | |
| 6260 | U+F0B43 | |
| 6261 | U+F0B44 | |
| 6262 | U+F0B45 | |
| 6263 | U+F0B46 | |
| 6264 | U+F0B47 | |
| 6265 | U+F0B48 | |
| 6266 | U+F0B49 | |
| 6267 | U+F0B4A | |
| 6268 | U+F0B4B | |
| 6269 | U+F0B4C | |
| 6270 | U+F0B4D | |
| 6271 | U+F0B4E | |
| 6272 | U+F0B4F | |
| 6273 | U+F0B50 | |
| 6274 | U+F0B51 | |
| 6275 | U+F0B52 | |
| 6276 | U+F0B53 | |
| 6277 | U+F0B54 | |
| 6278 | U+F0B55 | |
| 6279 | U+F0B56 | |
| 6280 | U+F0B57 | |
| 6281 | U+F0B58 | |
| 6282 | U+F0B59 | |
| 6283 | U+F0B5A | |
| 6284 | U+F0B5B | |
| 6285 | U+F0B5C | |
| 6286 | U+F0B5D | |
| 6287 | U+F0B5E | |
| 6288 | U+F0B5F | |
| 6289 | U+F0B60 | |
| 6290 | U+F0B61 | |
| 6291 | U+F0B62 | |
| 6292 | U+F0B63 | |
| 6293 | U+F0B64 | |
| 6294 | U+F0B65 | |
| 6295 | U+F0B66 | |
| 6296 | U+F0B67 | |
| 6297 | U+F0B68 | |
| 6298 | U+F0B69 | |
| 6299 | U+F0B6A | |
| 6300 | U+F0B6B | |
| 6301 | U+F0B6C | |
| 6302 | U+F0B6D | |
| 6303 | U+F0B6E | |
| 6304 | U+F0B6F | |
| 6305 | U+F0B70 | |
| 6306 | U+F0B71 | |
| 6307 | U+F0B72 | |
| 6308 | U+F0B73 | |
| 6309 | U+F0B74 | |
| 6310 | U+F0B75 | |
| 6311 | U+F0B76 | |
| 6312 | U+F0B77 | |
| 6313 | U+F0B78 | |
| 6314 | U+F0B79 | |
| 6315 | U+F0B7A | |
| 6316 | U+F0B7B | |
| 6317 | U+F0B7C | |
| 6318 | U+F0B7D | |
| 6319 | U+F0B7E | |
| 6320 | U+F0B7F | |
| 6321 | U+F0B80 | |
| 6322 | U+F0B81 | |
| 6323 | U+F0B82 | |
| 6324 | U+F0B83 | |
| 6325 | U+F0B84 | |
| 6326 | U+F0B85 | |
| 6327 | U+F0B86 | |
| 6328 | U+F0B87 | |
| 6329 | U+F0B88 | |
| 6330 | U+F0B89 | |
| 6331 | U+F0B8A | |
| 6332 | U+F0B8B | |
| 6333 | U+F0B8C | |
| 6334 | U+F0B8D | |
| 6335 | U+F0B8E | |
| 6336 | U+F0B8F | |
| 6337 | U+F0B90 | |
| 6338 | U+F0B91 | |
| 6339 | U+F0B92 | |
| 6340 | U+F0B93 | |
| 6341 | U+F0B94 | |
| 6342 | U+F0B95 | |
| 6343 | U+F0B96 | |
| 6344 | U+F0B97 | |
| 6345 | U+F0B98 | |
| 6346 | U+F0B99 | |
| 6347 | U+F0B9A | |
| 6348 | U+F0B9B | |
| 6349 | U+F0B9C | |
| 6350 | U+F0B9D | |
| 6351 | U+F0B9E | |
| 6352 | U+F0B9F | |
| 6353 | U+F0BA0 | |
| 6354 | U+F0BA1 | |
| 6355 | U+F0BA2 | |
| 6356 | U+F0BA3 | |
| 6357 | U+F0BA4 | |
| 6358 | U+F0BA5 | |
| 6359 | U+F0BA6 | |
| 6360 | U+F0BA7 | |
| 6361 | U+F0BA8 | |
| 6362 | U+F0BA9 | |
| 6363 | U+F0BAA | |
| 6364 | U+F0BAB | |
| 6365 | U+F0BAC | |
| 6366 | U+F0BAD | |
| 6367 | U+F0BAE | |
| 6368 | U+F0BAF | |
| 6369 | U+F0BB0 | |
| 6370 | U+F0BB1 | |
| 6371 | U+F0BB2 | |
| 6372 | U+F0BB3 | |
| 6373 | U+F0BB4 | |
| 6374 | U+F0BB5 | |
| 6375 | U+F0BB6 | |
| 6376 | U+F0BB7 | |
| 6377 | U+F0BB8 | |
| 6378 | U+F0BB9 | |
| 6379 | U+F0BBA | |
| 6380 | U+F0BBB | |
| 6381 | U+F0BBC | |
| 6382 | U+F0BBD | |
| 6383 | U+F0BBE | |
| 6384 | U+F0BBF | |
| 6385 | U+F0BC0 | |
| 6386 | U+F0BC1 | |
| 6387 | U+F0BC2 | |
| 6388 | U+F0BC3 | |
| 6389 | U+F0BC4 | |
| 6390 | U+F0BC5 | |
| 6391 | U+F0BC6 | |
| 6392 | U+F0BC7 | |
| 6393 | U+F0BC8 | |
| 6394 | U+F0BC9 | |
| 6395 | U+F0BCA | |
| 6396 | U+F0BCB | |
| 6397 | U+F0BCC | |
| 6398 | U+F0BCD | |
| 6399 | U+F0BCE | |
| 6400 | U+F0BCF | |
| 6401 | U+F0BD0 | |
| 6402 | U+F0BD1 | |
| 6403 | U+F0BD2 | |
| 6404 | U+F0BD3 | |
| 6405 | U+F0BD4 | |
| 6406 | U+F0BD5 | |
| 6407 | U+F0BD6 | |
| 6408 | U+F0BD7 | |
| 6409 | U+F0BD8 | |
| 6410 | U+F0BD9 | |
| 6411 | U+F0BDA | |
| 6412 | U+F0BDB | |
| 6413 | U+F0BDC | |
| 6414 | U+F0BDD | |
| 6415 | U+F0BDE | |
| 6416 | U+F0BDF | |
| 6417 | U+F0BE0 | |
| 6418 | U+F0BE1 | |
| 6419 | U+F0BE2 | |
| 6420 | U+F0BE3 | |
| 6421 | U+F0BE4 | |
| 6422 | U+F0BE5 | |
| 6423 | U+F0BE6 | |
| 6424 | U+F0BE7 | |
| 6425 | U+F0BE8 | |
| 6426 | U+F0BE9 | |
| 6427 | U+F0BEA | |
| 6428 | U+F0BEB | |
| 6429 | U+F0BEC | |
| 6430 | U+F0BED | |
| 6431 | U+F0BEE | |
| 6432 | U+F0BEF | |
| 6433 | U+F0BF0 | |
| 6434 | U+F0BF1 | |
| 6435 | U+F0BF2 | |
| 6436 | U+F0BF3 | |
| 6437 | U+F0BF4 | |
| 6438 | U+F0BF5 | |
| 6439 | U+F0BF6 | |
| 6440 | U+F0BF7 | |
| 6441 | U+F0BF8 | |
| 6442 | U+F0BF9 | |
| 6443 | U+F0BFA | |
| 6444 | U+F0BFB | |
| 6445 | U+F0BFC | |
| 6446 | U+F0BFD | |
| 6447 | U+F0BFE | |
| 6448 | U+F0BFF | |
| 6449 | U+F0C00 | |
| 6450 | U+F0C01 | |
| 6451 | U+F0C02 | |
| 6452 | U+F0C03 | |
| 6453 | U+F0C04 | |
| 6454 | U+F0C05 | |
| 6455 | U+F0C06 | |
| 6456 | U+F0C07 | |
| 6457 | U+F0C08 | |
| 6458 | U+F0C09 | |
| 6459 | U+F0C0A | |
| 6460 | U+F0C0B | |
| 6461 | U+F0C0C | |
| 6462 | U+F0C0D | |
| 6463 | U+F0C0E | |
| 6464 | U+F0C0F | |
| 6465 | U+F0C10 | |
| 6466 | U+F0C11 | |
| 6467 | U+F0C12 | |
| 6468 | U+F0C13 | |
| 6469 | U+F0C14 | |
| 6470 | U+F0C15 | |
| 6471 | U+F0C16 | |
| 6472 | U+F0C17 | |
| 6473 | U+F0C18 | |
| 6474 | U+F0C19 | |
| 6475 | U+F0C1A | |
| 6476 | U+F0C1B | |
| 6477 | U+F0C1C | |
| 6478 | U+F0C1D | |
| 6479 | U+F0C1E | |
| 6480 | U+F0C1F | |
| 6481 | U+F0C20 | |
| 6482 | U+F0C21 | |
| 6483 | U+F0C22 | |
| 6484 | U+F0C23 | |
| 6485 | U+F0C24 | |
| 6486 | U+F0C25 | |
| 6487 | U+F0C26 | |
| 6488 | U+F0C27 | |
| 6489 | U+F0C28 | |
| 6490 | U+F0C29 | |
| 6491 | U+F0C2A | |
| 6492 | U+F0C2B | |
| 6493 | U+F0C2C | |
| 6494 | U+F0C2D | |
| 6495 | U+F0C2E | |
| 6496 | U+F0C2F | |
| 6497 | U+F0C30 | |
| 6498 | U+F0C31 | |
| 6499 | U+F0C32 | |
| 6500 | U+F0C33 | |
| 6501 | U+F0C34 | |
| 6502 | U+F0C35 | |
| 6503 | U+F0C36 | |
| 6504 | U+F0C37 | |
| 6505 | U+F0C38 | |
| 6506 | U+F0C39 | |
| 6507 | U+F0C3A | |
| 6508 | U+F0C3B | |
| 6509 | U+F0C3C | |
| 6510 | U+F0C3D | |
| 6511 | U+F0C3E | |
| 6512 | U+F0C3F | |
| 6513 | U+F0C40 | |
| 6514 | U+F0C41 | |
| 6515 | U+F0C42 | |
| 6516 | U+F0C43 | |
| 6517 | U+F0C44 | |
| 6518 | U+F0C45 | |
| 6519 | U+F0C46 | |
| 6520 | U+F0C47 | |
| 6521 | U+F0C48 | |
| 6522 | U+F0C49 | |
| 6523 | U+F0C4A | |
| 6524 | U+F0C4B | |
| 6525 | U+F0C4C | |
| 6526 | U+F0C4D | |
| 6527 | U+F0C4E | |
| 6528 | U+F0C4F | |
| 6529 | U+F0C50 | |
| 6530 | U+F0C51 | |
| 6531 | U+F0C52 | |
| 6532 | U+F0C53 | |
| 6533 | U+F0C54 | |
| 6534 | U+F0C55 | |
| 6535 | U+F0C56 | |
| 6536 | U+F0C57 | |
| 6537 | U+F0C58 | |
| 6538 | U+F0C59 | |
| 6539 | U+F0C5A | |
| 6540 | U+F0C5B | |
| 6541 | U+F0C5C | |
| 6542 | U+F0C5D | |
| 6543 | U+F0C5E | |
| 6544 | U+F0C5F | |
| 6545 | U+F0C60 | |
| 6546 | U+F0C61 | |
| 6547 | U+F0C62 | |
| 6548 | U+F0C63 | |
| 6549 | U+F0C64 | |
| 6550 | U+F0C65 | |
| 6551 | U+F0C66 | |
| 6552 | U+F0C67 | |
| 6553 | U+F0C68 | |
| 6554 | U+F0C69 | |
| 6555 | U+F0C6A | |
| 6556 | U+F0C6B | |
| 6557 | U+F0C6C | |
| 6558 | U+F0C6D | |
| 6559 | U+F0C6E | |
| 6560 | U+F0C6F | |
| 6561 | U+F0C70 | |
| 6562 | U+F0C71 | |
| 6563 | U+F0C72 | |
| 6564 | U+F0C73 | |
| 6565 | U+F0C74 | |
| 6566 | U+F0C75 | |
| 6567 | U+F0C76 | |
| 6568 | U+F0C77 | |
| 6569 | U+F0C78 | |
| 6570 | U+F0C79 | |
| 6571 | U+F0C7A | |
| 6572 | U+F0C7B | |
| 6573 | U+F0C7C | |
| 6574 | U+F0C7D | |
| 6575 | U+F0C7E | |
| 6576 | U+F0C7F | |
| 6577 | U+F0C80 | |
| 6578 | U+F0C81 | |
| 6579 | U+F0C82 | |
| 6580 | U+F0C83 | |
| 6581 | U+F0C84 | |
| 6582 | U+F0C85 | |
| 6583 | U+F0C86 | |
| 6584 | U+F0C87 | |
| 6585 | U+F0C88 | |
| 6586 | U+F0C89 | |
| 6587 | U+F0C8A | |
| 6588 | U+F0C8B | |
| 6589 | U+F0C8C | |
| 6590 | U+F0C8D | |
| 6591 | U+F0C8E | |
| 6592 | U+F0C8F | |
| 6593 | U+F0C90 | |
| 6594 | U+F0C91 | |
| 6595 | U+F0C92 | |
| 6596 | U+F0C93 | |
| 6597 | U+F0C94 | |
| 6598 | U+F0C95 | |
| 6599 | U+F0C96 | |
| 6600 | U+F0C97 | |
| 6601 | U+F0C98 | |
| 6602 | U+F0C99 | |
| 6603 | U+F0C9A | |
| 6604 | U+F0C9B | |
| 6605 | U+F0C9C | |
| 6606 | U+F0C9D | |
| 6607 | U+F0C9E | |
| 6608 | U+F0C9F | |
| 6609 | U+F0CA0 | |
| 6610 | U+F0CA1 | |
| 6611 | U+F0CA2 | |
| 6612 | U+F0CA3 | |
| 6613 | U+F0CA4 | |
| 6614 | U+F0CA5 | |
| 6615 | U+F0CA6 | |
| 6616 | U+F0CA7 | |
| 6617 | U+F0CA8 | |
| 6618 | U+F0CA9 | |
| 6619 | U+F0CAA | |
| 6620 | U+F0CAB | |
| 6621 | U+F0CAC | |
| 6622 | U+F0CAD | |
| 6623 | U+F0CAE | |
| 6624 | U+F0CAF | |
| 6625 | U+F0CB0 | |
| 6626 | U+F0CB1 | |
| 6627 | U+F0CB2 | |
| 6628 | U+F0CB3 | |
| 6629 | U+F0CB4 | |
| 6630 | U+F0CB5 | |
| 6631 | U+F0CB6 | |
| 6632 | U+F0CB7 | |
| 6633 | U+F0CB8 | |
| 6634 | U+F0CB9 | |
| 6635 | U+F0CBA | |
| 6636 | U+F0CBB | |
| 6637 | U+F0CBC | |
| 6638 | U+F0CBD | |
| 6639 | U+F0CBE | |
| 6640 | U+F0CBF | |
| 6641 | U+F0CC0 | |
| 6642 | U+F0CC1 | |
| 6643 | U+F0CC2 | |
| 6644 | U+F0CC3 | |
| 6645 | U+F0CC4 | |
| 6646 | U+F0CC5 | |
| 6647 | U+F0CC6 | |
| 6648 | U+F0CC7 | |
| 6649 | U+F0CC8 | |
| 6650 | U+F0CC9 | |
| 6651 | U+F0CCA | |
| 6652 | U+F0CCB | |
| 6653 | U+F0CCC | |
| 6654 | U+F0CCD | |
| 6655 | U+F0CCE | |
| 6656 | U+F0CCF | |
| 6657 | U+F0CD0 | |
| 6658 | U+F0CD1 | |
| 6659 | U+F0CD2 | |
| 6660 | U+F0CD3 | |
| 6661 | U+F0CD4 | |
| 6662 | U+F0CD5 | |
| 6663 | U+F0CD6 | |
| 6664 | U+F0CD7 | |
| 6665 | U+F0CD8 | |
| 6666 | U+F0CD9 | |
| 6667 | U+F0CDA | |
| 6668 | U+F0CDB | |
| 6669 | U+F0CDC | |
| 6670 | U+F0CDD | |
| 6671 | U+F0CDE | |
| 6672 | U+F0CDF | |
| 6673 | U+F0CE0 | |
| 6674 | U+F0CE1 | |
| 6675 | U+F0CE2 | |
| 6676 | U+F0CE3 | |
| 6677 | U+F0CE4 | |
| 6678 | U+F0CE5 | |
| 6679 | U+F0CE6 | |
| 6680 | U+F0CE7 | |
| 6681 | U+F0CE8 | |
| 6682 | U+F0CE9 | |
| 6683 | U+F0CEA | |
| 6684 | U+F0CEB | |
| 6685 | U+F0CEC | |
| 6686 | U+F0CED | |
| 6687 | U+F0CEE | |
| 6688 | U+F0CEF | |
| 6689 | U+F0CF0 | |
| 6690 | U+F0CF1 | |
| 6691 | U+F0CF2 | |
| 6692 | U+F0CF3 | |
| 6693 | U+F0CF4 | |
| 6694 | U+F0CF5 | |
| 6695 | U+F0CF6 | |
| 6696 | U+F0CF7 | |
| 6697 | U+F0CF8 | |
| 6698 | U+F0CF9 | |
| 6699 | U+F0CFA | |
| 6700 | U+F0CFB | |
| 6701 | U+F0CFC | |
| 6702 | U+F0CFD | |
| 6703 | U+F0CFE | |
| 6704 | U+F0CFF | |
| 6705 | U+F0D00 | |
| 6706 | U+F0D01 | |
| 6707 | U+F0D02 | |
| 6708 | U+F0D03 | |
| 6709 | U+F0D04 | |
| 6710 | U+F0D05 | |
| 6711 | U+F0D06 | |
| 6712 | U+F0D07 | |
| 6713 | U+F0D08 | |
| 6714 | U+F0D09 | |
| 6715 | U+F0D0A | |
| 6716 | U+F0D0B | |
| 6717 | U+F0D0C | |
| 6718 | U+F0D0D | |
| 6719 | U+F0D0E | |
| 6720 | U+F0D0F | |
| 6721 | U+F0D10 | |
| 6722 | U+F0D11 | |
| 6723 | U+F0D12 | |
| 6724 | U+F0D13 | |
| 6725 | U+F0D14 | |
| 6726 | U+F0D15 | |
| 6727 | U+F0D16 | |
| 6728 | U+F0D17 | |
| 6729 | U+F0D18 | |
| 6730 | U+F0D19 | |
| 6731 | U+F0D1A | |
| 6732 | U+F0D1B | |
| 6733 | U+F0D1C | |
| 6734 | U+F0D1D | |
| 6735 | U+F0D1E | |
| 6736 | U+F0D1F | |
| 6737 | U+F0D20 | |
| 6738 | U+F0D21 | |
| 6739 | U+F0D22 | |
| 6740 | U+F0D23 | |
| 6741 | U+F0D24 | |
| 6742 | U+F0D25 | |
| 6743 | U+F0D26 | |
| 6744 | U+F0D27 | |
| 6745 | U+F0D28 | |
| 6746 | U+F0D29 | |
| 6747 | U+F0D2A | |
| 6748 | U+F0D2B | |
| 6749 | U+F0D2C | |
| 6750 | U+F0D2D | |
| 6751 | U+F0D2E | |
| 6752 | U+F0D2F | |
| 6753 | U+F0D30 | |
| 6754 | U+F0D31 | |
| 6755 | U+F0D32 | |
| 6756 | U+F0D33 | |
| 6757 | U+F0D34 | |
| 6758 | U+F0D35 | |
| 6759 | U+F0D36 | |
| 6760 | U+F0D37 | |
| 6761 | U+F0D38 | |
| 6762 | U+F0D39 | |
| 6763 | U+F0D3A | |
| 6764 | U+F0D3B | |
| 6765 | U+F0D3C | |
| 6766 | U+F0D3D | |
| 6767 | U+F0D3E | |
| 6768 | U+F0D3F | |
| 6769 | U+F0D40 | |
| 6770 | U+F0D41 | |
| 6771 | U+F0D42 | |
| 6772 | U+F0D43 | |
| 6773 | U+F0D44 | |
| 6774 | U+F0D45 | |
| 6775 | U+F0D46 | |
| 6776 | U+F0D47 | |
| 6777 | U+F0D48 | |
| 6778 | U+F0D49 | |
| 6779 | U+F0D4A | |
| 6780 | U+F0D4B | |
| 6781 | U+F0D4C | |
| 6782 | U+F0D4D | |
| 6783 | U+F0D4E | |
| 6784 | U+F0D4F | |
| 6785 | U+F0D50 | |
| 6786 | U+F0D51 | |
| 6787 | U+F0D52 | |
| 6788 | U+F0D53 | |
| 6789 | U+F0D54 | |
| 6790 | U+F0D55 | |
| 6791 | U+F0D56 | |
| 6792 | U+F0D57 | |
| 6793 | U+F0D58 | |
| 6794 | U+F0D59 | |
| 6795 | U+F0D5A | |
| 6796 | U+F0D5B | |
| 6797 | U+F0D5C | |
| 6798 | U+F0D5D | |
| 6799 | U+F0D5E | |
| 6800 | U+F0D5F | |
| 6801 | U+F0D60 | |
| 6802 | U+F0D61 | |
| 6803 | U+F0D62 | |
| 6804 | U+F0D63 | |
| 6805 | U+F0D64 | |
| 6806 | U+F0D65 | |
| 6807 | U+F0D66 | |
| 6808 | U+F0D67 | |
| 6809 | U+F0D68 | |
| 6810 | U+F0D69 | |
| 6811 | U+F0D6A | |
| 6812 | U+F0D6B | |
| 6813 | U+F0D6C | |
| 6814 | U+F0D6D | |
| 6815 | U+F0D6E | |
| 6816 | U+F0D6F | |
| 6817 | U+F0D70 | |
| 6818 | U+F0D71 | |
| 6819 | U+F0D72 | |
| 6820 | U+F0D73 | |
| 6821 | U+F0D74 | |
| 6822 | U+F0D75 | |
| 6823 | U+F0D76 | |
| 6824 | U+F0D77 | |
| 6825 | U+F0D78 | |
| 6826 | U+F0D79 | |
| 6827 | U+F0D7A | |
| 6828 | U+F0D7B | |
| 6829 | U+F0D7C | |
| 6830 | U+F0D7D | |
| 6831 | U+F0D7E | |
| 6832 | U+F0D7F | |
| 6833 | U+F0D80 | |
| 6834 | U+F0D81 | |
| 6835 | U+F0D82 | |
| 6836 | U+F0D83 | |
| 6837 | U+F0D84 | |
| 6838 | U+F0D85 | |
| 6839 | U+F0D86 | |
| 6840 | U+F0D87 | |
| 6841 | U+F0D88 | |
| 6842 | U+F0D89 | |
| 6843 | U+F0D8A | |
| 6844 | U+F0D8B | |
| 6845 | U+F0D8C | |
| 6846 | U+F0D8D | |
| 6847 | U+F0D8E | |
| 6848 | U+F0D8F | |
| 6849 | U+F0D90 | |
| 6850 | U+F0D91 | |
| 6851 | U+F0D92 | |
| 6852 | U+F0D93 | |
| 6853 | U+F0D94 | |
| 6854 | U+F0D95 | |
| 6855 | U+F0D96 | |
| 6856 | U+F0D97 | |
| 6857 | U+F0D98 | |
| 6858 | U+F0D99 | |
| 6859 | U+F0D9A | |
| 6860 | U+F0D9B | |
| 6861 | U+F0D9C | |
| 6862 | U+F0D9D | |
| 6863 | U+F0D9E | |
| 6864 | U+F0D9F | |
| 6865 | U+F0DA0 | |
| 6866 | U+F0DA1 | |
| 6867 | U+F0DA2 | |
| 6868 | U+F0DA3 | |
| 6869 | U+F0DA4 | |
| 6870 | U+F0DA5 | |
| 6871 | U+F0DA6 | |
| 6872 | U+F0DA7 | |
| 6873 | U+F0DA8 | |
| 6874 | U+F0DA9 | |
| 6875 | U+F0DAA | |
| 6876 | U+F0DAB | |
| 6877 | U+F0DAC | |
| 6878 | U+F0DAD | |
| 6879 | U+F0DAE | |
| 6880 | U+F0DAF | |
| 6881 | U+F0DB0 | |
| 6882 | U+F0DB1 | |
| 6883 | U+F0DB2 | |
| 6884 | U+F0DB3 | |
| 6885 | U+F0DB4 | |
| 6886 | U+F0DB5 | |
| 6887 | U+F0DB6 | |
| 6888 | U+F0DB7 | |
| 6889 | U+F0DB8 | |
| 6890 | U+F0DB9 | |
| 6891 | U+F0DBA | |
| 6892 | U+F0DBB | |
| 6893 | U+F0DBC | |
| 6894 | U+F0DBD | |
| 6895 | U+F0DBE | |
| 6896 | U+F0DBF | |
| 6897 | U+F0DC0 | |
| 6898 | U+F0DC1 | |
| 6899 | U+F0DC2 | |
| 6900 | U+F0DC3 | |
| 6901 | U+F0DC4 | |
| 6902 | U+F0DC5 | |
| 6903 | U+F0DC6 | |
| 6904 | U+F0DC7 | |
| 6905 | U+F0DC8 | |
| 6906 | U+F0DC9 | |
| 6907 | U+F0DCA | |
| 6908 | U+F0DCB | |
| 6909 | U+F0DCC | |
| 6910 | U+F0DCD | |
| 6911 | U+F0DCE | |
| 6912 | U+F0DCF | |
| 6913 | U+F0DD0 | |
| 6914 | U+F0DD1 | |
| 6915 | U+F0DD2 | |
| 6916 | U+F0DD3 | |
| 6917 | U+F0DD4 | |
| 6918 | U+F0DD5 | |
| 6919 | U+F0DD6 | |
| 6920 | U+F0DD7 | |
| 6921 | U+F0DD8 | |
| 6922 | U+F0DD9 | |
| 6923 | U+F0DDA | |
| 6924 | U+F0DDB | |
| 6925 | U+F0DDC | |
| 6926 | U+F0DDD | |
| 6927 | U+F0DDE | |
| 6928 | U+F0DDF | |
| 6929 | U+F0DE0 | |
| 6930 | U+F0DE1 | |
| 6931 | U+F0DE2 | |
| 6932 | U+F0DE3 | |
| 6933 | U+F0DE4 | |
| 6934 | U+F0DE5 | |
| 6935 | U+F0DE6 | |
| 6936 | U+F0DE7 | |
| 6937 | U+F0DE8 | |
| 6938 | U+F0DE9 | |
| 6939 | U+F0DEA | |
| 6940 | U+F0DEB | |
| 6941 | U+F0DEC | |
| 6942 | U+F0DED | |
| 6943 | U+F0DEE | |
| 6944 | U+F0DEF | |
| 6945 | U+F0DF0 | |
| 6946 | U+F0DF1 | |
| 6947 | U+F0DF2 | |
| 6948 | U+F0DF3 | |
| 6949 | U+F0DF4 | |
| 6950 | U+F0DF5 | |
| 6951 | U+F0DF6 | |
| 6952 | U+F0DF7 | |
| 6953 | U+F0DF8 | |
| 6954 | U+F0DF9 | |
| 6955 | U+F0DFA | |
| 6956 | U+F0DFB | |
| 6957 | U+F0DFC | |
| 6958 | U+F0DFD | |
| 6959 | U+F0DFE | |
| 6960 | U+F0DFF | |
| 6961 | U+F0E00 | |
| 6962 | U+F0E01 | |
| 6963 | U+F0E02 | |
| 6964 | U+F0E03 | |
| 6965 | U+F0E04 | |
| 6966 | U+F0E05 | |
| 6967 | U+F0E06 | |
| 6968 | U+F0E07 | |
| 6969 | U+F0E08 | |
| 6970 | U+F0E09 | |
| 6971 | U+F0E0A | |
| 6972 | U+F0E0B | |
| 6973 | U+F0E0C | |
| 6974 | U+F0E0D | |
| 6975 | U+F0E0E | |
| 6976 | U+F0E0F | |
| 6977 | U+F0E10 | |
| 6978 | U+F0E11 | |
| 6979 | U+F0E12 | |
| 6980 | U+F0E13 | |
| 6981 | U+F0E14 | |
| 6982 | U+F0E15 | |
| 6983 | U+F0E16 | |
| 6984 | U+F0E17 | |
| 6985 | U+F0E18 | |
| 6986 | U+F0E19 | |
| 6987 | U+F0E1A | |
| 6988 | U+F0E1B | |
| 6989 | U+F0E1C | |
| 6990 | U+F0E1D | |
| 6991 | U+F0E1E | |
| 6992 | U+F0E1F | |
| 6993 | U+F0E20 | |
| 6994 | U+F0E21 | |
| 6995 | U+F0E22 | |
| 6996 | U+F0E23 | |
| 6997 | U+F0E24 | |
| 6998 | U+F0E25 | |
| 6999 | U+F0E26 | |
| 7000 | U+F0E27 | |
| 7001 | U+F0E28 | |
| 7002 | U+F0E29 | |
| 7003 | U+F0E2A | |
| 7004 | U+F0E2B | |
| 7005 | U+F0E2C | |
| 7006 | U+F0E2D | |
| 7007 | U+F0E2E | |
| 7008 | U+F0E2F | |
| 7009 | U+F0E30 | |
| 7010 | U+F0E31 | |
| 7011 | U+F0E32 | |
| 7012 | U+F0E33 | |
| 7013 | U+F0E34 | |
| 7014 | U+F0E35 | |
| 7015 | U+F0E36 | |
| 7016 | U+F0E37 | |
| 7017 | U+F0E38 | |
| 7018 | U+F0E39 | |
| 7019 | U+F0E3A | |
| 7020 | U+F0E3B | |
| 7021 | U+F0E3C | |
| 7022 | U+F0E3D | |
| 7023 | U+F0E3E | |
| 7024 | U+F0E3F | |
| 7025 | U+F0E40 | |
| 7026 | U+F0E41 | |
| 7027 | U+F0E42 | |
| 7028 | U+F0E43 | |
| 7029 | U+F0E44 | |
| 7030 | U+F0E45 | |
| 7031 | U+F0E46 | |
| 7032 | U+F0E47 | |
| 7033 | U+F0E48 | |
| 7034 | U+F0E49 | |
| 7035 | U+F0E4A | |
| 7036 | U+F0E4B | |
| 7037 | U+F0E4C | |
| 7038 | U+F0E4D | |
| 7039 | U+F0E4E | |
| 7040 | U+F0E4F | |
| 7041 | U+F0E50 | |
| 7042 | U+F0E51 | |
| 7043 | U+F0E52 | |
| 7044 | U+F0E53 | |
| 7045 | U+F0E54 | |
| 7046 | U+F0E55 | |
| 7047 | U+F0E56 | |
| 7048 | U+F0E57 | |
| 7049 | U+F0E58 | |
| 7050 | U+F0E59 | |
| 7051 | U+F0E5A | |
| 7052 | U+F0E5B | |
| 7053 | U+F0E5C | |
| 7054 | U+F0E5D | |
| 7055 | U+F0E5E | |
| 7056 | U+F0E5F | |
| 7057 | U+F0E60 | |
| 7058 | U+F0E61 | |
| 7059 | U+F0E62 | |
| 7060 | U+F0E63 | |
| 7061 | U+F0E64 | |
| 7062 | U+F0E65 | |
| 7063 | U+F0E66 | |
| 7064 | U+F0E67 | |
| 7065 | U+F0E68 | |
| 7066 | U+F0E69 | |
| 7067 | U+F0E6A | |
| 7068 | U+F0E6B | |
| 7069 | U+F0E6C | |
| 7070 | U+F0E6D | |
| 7071 | U+F0E6E | |
| 7072 | U+F0E6F | |
| 7073 | U+F0E70 | |
| 7074 | U+F0E71 | |
| 7075 | U+F0E72 | |
| 7076 | U+F0E73 | |
| 7077 | U+F0E74 | |
| 7078 | U+F0E75 | |
| 7079 | U+F0E76 | |
| 7080 | U+F0E77 | |
| 7081 | U+F0E78 | |
| 7082 | U+F0E79 | |
| 7083 | U+F0E7A | |
| 7084 | U+F0E7B | |
| 7085 | U+F0E7C | |
| 7086 | U+F0E7D | |
| 7087 | U+F0E7E | |
| 7088 | U+F0E7F | |
| 7089 | U+F0E80 | |
| 7090 | U+F0E81 | |
| 7091 | U+F0E82 | |
| 7092 | U+F0E83 | |
| 7093 | U+F0E84 | |
| 7094 | U+F0E85 | |
| 7095 | U+F0E86 | |
| 7096 | U+F0E87 | |
| 7097 | U+F0E88 | |
| 7098 | U+F0E89 | |
| 7099 | U+F0E8A | |
| 7100 | U+F0E8B | |
| 7101 | U+F0E8C | |
| 7102 | U+F0E8D | |
| 7103 | U+F0E8E | |
| 7104 | U+F0E8F | |
| 7105 | U+F0E90 | |
| 7106 | U+F0E91 | |
| 7107 | U+F0E92 | |
| 7108 | U+F0E93 | |
| 7109 | U+F0E94 | |
| 7110 | U+F0E95 | |
| 7111 | U+F0E96 | |
| 7112 | U+F0E97 | |
| 7113 | U+F0E98 | |
| 7114 | U+F0E99 | |
| 7115 | U+F0E9A | |
| 7116 | U+F0E9B | |
| 7117 | U+F0E9C | |
| 7118 | U+F0E9D | |
| 7119 | U+F0E9E | |
| 7120 | U+F0E9F | |
| 7121 | U+F0EA0 | |
| 7122 | U+F0EA1 | |
| 7123 | U+F0EA2 | |
| 7124 | U+F0EA3 | |
| 7125 | U+F0EA4 | |
| 7126 | U+F0EA5 | |
| 7127 | U+F0EA6 | |
| 7128 | U+F0EA7 | |
| 7129 | U+F0EA8 | |
| 7130 | U+F0EA9 | |
| 7131 | U+F0EAA | |
| 7132 | U+F0EAB | |
| 7133 | U+F0EAC | |
| 7134 | U+F0EAD | |
| 7135 | U+F0EAE | |
| 7136 | U+F0EAF | |
| 7137 | U+F0EB0 | |
| 7138 | U+F0EB1 | |
| 7139 | U+F0EB2 | |
| 7140 | U+F0EB3 | |
| 7141 | U+F0EB4 | |
| 7142 | U+F0EB5 | |
| 7143 | U+F0EB6 | |
| 7144 | U+F0EB7 | |
| 7145 | U+F0EB8 | |
| 7146 | U+F0EB9 | |
| 7147 | U+F0EBA | |
| 7148 | U+F0EBB | |
| 7149 | U+F0EBC | |
| 7150 | U+F0EBD | |
| 7151 | U+F0EBE | |
| 7152 | U+F0EBF | |
| 7153 | U+F0EC0 | |
| 7154 | U+F0EC1 | |
| 7155 | U+F0EC2 | |
| 7156 | U+F0EC3 | |
| 7157 | U+F0EC4 | |
| 7158 | U+F0EC5 | |
| 7159 | U+F0EC6 | |
| 7160 | U+F0EC7 | |
| 7161 | U+F0EC8 | |
| 7162 | U+F0EC9 | |
| 7163 | U+F0ECA | |
| 7164 | U+F0ECB | |
| 7165 | U+F0ECC | |
| 7166 | U+F0ECD | |
| 7167 | U+F0ECE | |
| 7168 | U+F0ECF | |
| 7169 | U+F0ED0 | |
| 7170 | U+F0ED1 | |
| 7171 | U+F0ED2 | |
| 7172 | U+F0ED3 | |
| 7173 | U+F0ED4 | |
| 7174 | U+F0ED5 | |
| 7175 | U+F0ED6 | |
| 7176 | U+F0ED7 | |
| 7177 | U+F0ED8 | |
| 7178 | U+F0ED9 | |
| 7179 | U+F0EDA | |
| 7180 | U+F0EDB | |
| 7181 | U+F0EDC | |
| 7182 | U+F0EDD | |
| 7183 | U+F0EDE | |
| 7184 | U+F0EDF | |
| 7185 | U+F0EE0 | |
| 7186 | U+F0EE1 | |
| 7187 | U+F0EE2 | |
| 7188 | U+F0EE3 | |
| 7189 | U+F0EE4 | |
| 7190 | U+F0EE5 | |
| 7191 | U+F0EE6 | |
| 7192 | U+F0EE7 | |
| 7193 | U+F0EE8 | |
| 7194 | U+F0EE9 | |
| 7195 | U+F0EEA | |
| 7196 | U+F0EEB | |
| 7197 | U+F0EEC | |
| 7198 | U+F0EED | |
| 7199 | U+F0EEE | |
| 7200 | U+F0EEF | |
| 7201 | U+F0EF0 | |
| 7202 | U+F0EF1 | |
| 7203 | U+F0EF2 | |
| 7204 | U+F0EF3 | |
| 7205 | U+F0EF4 | |
| 7206 | U+F0EF5 | |
| 7207 | U+F0EF6 | |
| 7208 | U+F0EF7 | |
| 7209 | U+F0EF8 | |
| 7210 | U+F0EF9 | |
| 7211 | U+F0EFA | |
| 7212 | U+F0EFB | |
| 7213 | U+F0EFC | |
| 7214 | U+F0EFD | |
| 7215 | U+F0EFE | |
| 7216 | U+F0EFF | |
| 7217 | U+F0F00 | |
| 7218 | U+F0F01 | |
| 7219 | U+F0F02 | |
| 7220 | U+F0F03 | |
| 7221 | U+F0F04 | |
| 7222 | U+F0F05 | |
| 7223 | U+F0F06 | |
| 7224 | U+F0F07 | |
| 7225 | U+F0F08 | |
| 7226 | U+F0F09 | |
| 7227 | U+F0F0A | |
| 7228 | U+F0F0B | |
| 7229 | U+F0F0C | |
| 7230 | U+F0F0D | |
| 7231 | U+F0F0E | |
| 7232 | U+F0F0F | |
| 7233 | U+F0F10 | |
| 7234 | U+F0F11 | |
| 7235 | U+F0F12 | |
| 7236 | U+F0F13 | |
| 7237 | U+F0F14 | |
| 7238 | U+F0F15 | |
| 7239 | U+F0F16 | |
| 7240 | U+F0F17 | |
| 7241 | U+F0F18 | |
| 7242 | U+F0F19 | |
| 7243 | U+F0F1A | |
| 7244 | U+F0F1B | |
| 7245 | U+F0F1C | |
| 7246 | U+F0F1D | |
| 7247 | U+F0F1E | |
| 7248 | U+F0F1F | |
| 7249 | U+F0F20 | |
| 7250 | U+F0F21 | |
| 7251 | U+F0F22 | |
| 7252 | U+F0F23 | |
| 7253 | U+F0F24 | |
| 7254 | U+F0F25 | |
| 7255 | U+F0F26 | |
| 7256 | U+F0F27 | |
| 7257 | U+F0F28 | |
| 7258 | U+F0F29 | |
| 7259 | U+F0F2A | |
| 7260 | U+F0F2B | |
| 7261 | U+F0F2C | |
| 7262 | U+F0F2D | |
| 7263 | U+F0F2E | |
| 7264 | U+F0F2F | |
| 7265 | U+F0F30 | |
| 7266 | U+F0F31 | |
| 7267 | U+F0F32 | |
| 7268 | U+F0F33 | |
| 7269 | U+F0F34 | |
| 7270 | U+F0F35 | |
| 7271 | U+F0F36 | |
| 7272 | U+F0F37 | |
| 7273 | U+F0F38 | |
| 7274 | U+F0F39 | |
| 7275 | U+F0F3A | |
| 7276 | U+F0F3B | |
| 7277 | U+F0F3C | |
| 7278 | U+F0F3D | |
| 7279 | U+F0F3E | |
| 7280 | U+F0F3F | |
| 7281 | U+F0F40 | |
| 7282 | U+F0F41 | |
| 7283 | U+F0F42 | |
| 7284 | U+F0F43 | |
| 7285 | U+F0F44 | |
| 7286 | U+F0F45 | |
| 7287 | U+F0F46 | |
| 7288 | U+F0F47 | |
| 7289 | U+F0F48 | |
| 7290 | U+F0F49 | |
| 7291 | U+F0F4A | |
| 7292 | U+F0F4B | |
| 7293 | U+F0F4C | |
| 7294 | U+F0F4D | |
| 7295 | U+F0F4E | |
| 7296 | U+F0F4F | |
| 7297 | U+F0F50 | |
| 7298 | U+F0F51 | |
| 7299 | U+F0F52 | |
| 7300 | U+F0F53 | |
| 7301 | U+F0F54 | |
| 7302 | U+F0F55 | |
| 7303 | U+F0F56 | |
| 7304 | U+F0F57 | |
| 7305 | U+F0F58 | |
| 7306 | U+F0F59 | |
| 7307 | U+F0F5A | |
| 7308 | U+F0F5B | |
| 7309 | U+F0F5C | |
| 7310 | U+F0F5D | |
| 7311 | U+F0F5E | |
| 7312 | U+F0F5F | |
| 7313 | U+F0F60 | |
| 7314 | U+F0F61 | |
| 7315 | U+F0F62 | |
| 7316 | U+F0F63 | |
| 7317 | U+F0F64 | |
| 7318 | U+F0F65 | |
| 7319 | U+F0F66 | |
| 7320 | U+F0F67 | |
| 7321 | U+F0F68 | |
| 7322 | U+F0F69 | |
| 7323 | U+F0F6A | |
| 7324 | U+F0F6B | |
| 7325 | U+F0F6C | |
| 7326 | U+F0F6D | |
| 7327 | U+F0F6E | |
| 7328 | U+F0F6F | |
| 7329 | U+F0F70 | |
| 7330 | U+F0F71 | |
| 7331 | U+F0F72 | |
| 7332 | U+F0F73 | |
| 7333 | U+F0F74 | |
| 7334 | U+F0F75 | |
| 7335 | U+F0F76 | |
| 7336 | U+F0F77 | |
| 7337 | U+F0F78 | |
| 7338 | U+F0F79 | |
| 7339 | U+F0F7A | |
| 7340 | U+F0F7B | |
| 7341 | U+F0F7C | |
| 7342 | U+F0F7D | |
| 7343 | U+F0F7E | |
| 7344 | U+F0F7F | |
| 7345 | U+F0F80 | |
| 7346 | U+F0F81 | |
| 7347 | U+F0F82 | |
| 7348 | U+F0F83 | |
| 7349 | U+F0F84 | |
| 7350 | U+F0F85 | |
| 7351 | U+F0F86 | |
| 7352 | U+F0F87 | |
| 7353 | U+F0F88 | |
| 7354 | U+F0F89 | |
| 7355 | U+F0F8A | |
| 7356 | U+F0F8B | |
| 7357 | U+F0F8C | |
| 7358 | U+F0F8D | |
| 7359 | U+F0F8E | |
| 7360 | U+F0F8F | |
| 7361 | U+F0F90 | |
| 7362 | U+F0F91 | |
| 7363 | U+F0F92 | |
| 7364 | U+F0F93 | |
| 7365 | U+F0F94 | |
| 7366 | U+F0F95 | |
| 7367 | U+F0F96 | |
| 7368 | U+F0F97 | |
| 7369 | U+F0F98 | |
| 7370 | U+F0F99 | |
| 7371 | U+F0F9A | |
| 7372 | U+F0F9B | |
| 7373 | U+F0F9C | |
| 7374 | U+F0F9D | |
| 7375 | U+F0F9E | |
| 7376 | U+F0F9F | |
| 7377 | U+F0FA0 | |
| 7378 | U+F0FA1 | |
| 7379 | U+F0FA2 | |
| 7380 | U+F0FA3 | |
| 7381 | U+F0FA4 | |
| 7382 | U+F0FA5 | |
| 7383 | U+F0FA6 | |
| 7384 | U+F0FA7 | |
| 7385 | U+F0FA8 | |
| 7386 | U+F0FA9 | |
| 7387 | U+F0FAA | |
| 7388 | U+F0FAB | |
| 7389 | U+F0FAC | |
| 7390 | U+F0FAD | |
| 7391 | U+F0FAE | |
| 7392 | U+F0FAF | |
| 7393 | U+F0FB0 | |
| 7394 | U+F0FB1 | |
| 7395 | U+F0FB2 | |
| 7396 | U+F0FB3 | |
| 7397 | U+F0FB4 | |
| 7398 | U+F0FB5 | |
| 7399 | U+F0FB6 | |
| 7400 | U+F0FB7 | |
| 7401 | U+F0FB8 | |
| 7402 | U+F0FB9 | |
| 7403 | U+F0FBA | |
| 7404 | U+F0FBB | |
| 7405 | U+F0FBC | |
| 7406 | U+F0FBD | |
| 7407 | U+F0FBE | |
| 7408 | U+F0FBF | |
| 7409 | U+F0FC0 | |
| 7410 | U+F0FC1 | |
| 7411 | U+F0FC2 | |
| 7412 | U+F0FC3 | |
| 7413 | U+F0FC4 | |
| 7414 | U+F0FC5 | |
| 7415 | U+F0FC6 | |
| 7416 | U+F0FC7 | |
| 7417 | U+F0FC8 | |
| 7418 | U+F0FC9 | |
| 7419 | U+F0FCA | |
| 7420 | U+F0FCB | |
| 7421 | U+F0FCC | |
| 7422 | U+F0FCD | |
| 7423 | U+F0FCE | |
| 7424 | U+F0FCF | |
| 7425 | U+F0FD0 | |
| 7426 | U+F0FD1 | |
| 7427 | U+F0FD2 | |
| 7428 | U+F0FD3 | |
| 7429 | U+F0FD4 | |
| 7430 | U+F0FD5 | |
| 7431 | U+F0FD6 | |
| 7432 | U+F0FD7 | |
| 7433 | U+F0FD8 | |
| 7434 | U+F0FD9 | |
| 7435 | U+F0FDA | |
| 7436 | U+F0FDB | |
| 7437 | U+F0FDC | |
| 7438 | U+F0FDD | |
| 7439 | U+F0FDE | |
| 7440 | U+F0FDF | |
| 7441 | U+F0FE0 | |
| 7442 | U+F0FE1 | |
| 7443 | U+F0FE2 | |
| 7444 | U+F0FE3 | |
| 7445 | U+F0FE4 | |
| 7446 | U+F0FE5 | |
| 7447 | U+F0FE6 | |
| 7448 | U+F0FE7 | |
| 7449 | U+F0FE8 | |
| 7450 | U+F0FE9 | |
| 7451 | U+F0FEA | |
| 7452 | U+F0FEB | |
| 7453 | U+F0FEC | |
| 7454 | U+F0FED | |
| 7455 | U+F0FEE | |
| 7456 | U+F0FEF | |
| 7457 | U+F0FF0 | |
| 7458 | U+F0FF1 | |
| 7459 | U+F0FF2 | |
| 7460 | U+F0FF3 | |
| 7461 | U+F0FF4 | |
| 7462 | U+F0FF5 | |
| 7463 | U+F0FF6 | |
| 7464 | U+F0FF7 | |
| 7465 | U+F0FF8 | |
| 7466 | U+F0FF9 | |
| 7467 | U+F0FFA | |
| 7468 | U+F0FFB | |
| 7469 | U+F0FFC | |
| 7470 | U+F0FFD | |
| 7471 | U+F0FFE | |
| 7472 | U+F0FFF | |
| 7473 | U+F1000 | |
| 7474 | U+F1001 | |
| 7475 | U+F1002 | |
| 7476 | U+F1003 | |
| 7477 | U+F1004 | |
| 7478 | U+F1005 | |
| 7479 | U+F1006 | |
| 7480 | U+F1007 | |
| 7481 | U+F1008 | |
| 7482 | U+F1009 | |
| 7483 | U+F100A | |
| 7484 | U+F100B | |
| 7485 | U+F100C | |
| 7486 | U+F100D | |
| 7487 | U+F100E | |
| 7488 | U+F100F | |
| 7489 | U+F1010 | |
| 7490 | U+F1011 | |
| 7491 | U+F1012 | |
| 7492 | U+F1013 | |
| 7493 | U+F1014 | |
| 7494 | U+F1015 | |
| 7495 | U+F1016 | |
| 7496 | U+F1017 | |
| 7497 | U+F1018 | |
| 7498 | U+F1019 | |
| 7499 | U+F101A | |
| 7500 | U+F101B | |
| 7501 | U+F101C | |
| 7502 | U+F101D | |
| 7503 | U+F101E | |
| 7504 | U+F101F | |
| 7505 | U+F1020 | |
| 7506 | U+F1021 | |
| 7507 | U+F1022 | |
| 7508 | U+F1023 | |
| 7509 | U+F1024 | |
| 7510 | U+F1025 | |
| 7511 | U+F1026 | |
| 7512 | U+F1027 | |
| 7513 | U+F1028 | |
| 7514 | U+F1029 | |
| 7515 | U+F102A | |
| 7516 | U+F102B | |
| 7517 | U+F102C | |
| 7518 | U+F102D | |
| 7519 | U+F102E | |
| 7520 | U+F102F | |
| 7521 | U+F1030 | |
| 7522 | U+F1031 | |
| 7523 | U+F1032 | |
| 7524 | U+F1033 | |
| 7525 | U+F1034 | |
| 7526 | U+F1035 | |
| 7527 | U+F1036 | |
| 7528 | U+F1037 | |
| 7529 | U+F1038 | |
| 7530 | U+F1039 | |
| 7531 | U+F103A | |
| 7532 | U+F103B | |
| 7533 | U+F103C | |
| 7534 | U+F103D | |
| 7535 | U+F103E | |
| 7536 | U+F103F | |
| 7537 | U+F1040 | |
| 7538 | U+F1041 | |
| 7539 | U+F1042 | |
| 7540 | U+F1043 | |
| 7541 | U+F1044 | |
| 7542 | U+F1045 | |
| 7543 | U+F1046 | |
| 7544 | U+F1047 | |
| 7545 | U+F1048 | |
| 7546 | U+F1049 | |
| 7547 | U+F104A | |
| 7548 | U+F104B | |
| 7549 | U+F104C | |
| 7550 | U+F104D | |
| 7551 | U+F104E | |
| 7552 | U+F104F | |
| 7553 | U+F1050 | |
| 7554 | U+F1051 | |
| 7555 | U+F1052 | |
| 7556 | U+F1053 | |
| 7557 | U+F1054 | |
| 7558 | U+F1055 | |
| 7559 | U+F1056 | |
| 7560 | U+F1057 | |
| 7561 | U+F1058 | |
| 7562 | U+F1059 | |
| 7563 | U+F105A | |
| 7564 | U+F105B | |
| 7565 | U+F105C | |
| 7566 | U+F105D | |
| 7567 | U+F105E | |
| 7568 | U+F105F | |
| 7569 | U+F1060 | |
| 7570 | U+F1061 | |
| 7571 | U+F1062 | |
| 7572 | U+F1063 | |
| 7573 | U+F1064 | |
| 7574 | U+F1065 | |
| 7575 | U+F1066 | |
| 7576 | U+F1067 | |
| 7577 | U+F1068 | |
| 7578 | U+F1069 | |
| 7579 | U+F106A | |
| 7580 | U+F106B | |
| 7581 | U+F106C | |
| 7582 | U+F106D | |
| 7583 | U+F106E | |
| 7584 | U+F106F | |
| 7585 | U+F1070 | |
| 7586 | U+F1071 | |
| 7587 | U+F1072 | |
| 7588 | U+F1073 | |
| 7589 | U+F1074 | |
| 7590 | U+F1075 | |
| 7591 | U+F1076 | |
| 7592 | U+F1077 | |
| 7593 | U+F1078 | |
| 7594 | U+F1079 | |
| 7595 | U+F107A | |
| 7596 | U+F107B | |
| 7597 | U+F107C | |
| 7598 | U+F107D | |
| 7599 | U+F107E | |
| 7600 | U+F107F | |
| 7601 | U+F1080 | |
| 7602 | U+F1081 | |
| 7603 | U+F1082 | |
| 7604 | U+F1083 | |
| 7605 | U+F1084 | |
| 7606 | U+F1085 | |
| 7607 | U+F1086 | |
| 7608 | U+F1087 | |
| 7609 | U+F1088 | |
| 7610 | U+F1089 | |
| 7611 | U+F108A | |
| 7612 | U+F108B | |
| 7613 | U+F108C | |
| 7614 | U+F108D | |
| 7615 | U+F108E | |
| 7616 | U+F108F | |
| 7617 | U+F1090 | |
| 7618 | U+F1091 | |
| 7619 | U+F1092 | |
| 7620 | U+F1093 | |
| 7621 | U+F1094 | |
| 7622 | U+F1095 | |
| 7623 | U+F1096 | |
| 7624 | U+F1097 | |
| 7625 | U+F1098 | |
| 7626 | U+F1099 | |
| 7627 | U+F109A | |
| 7628 | U+F109B | |
| 7629 | U+F109C | |
| 7630 | U+F109D | |
| 7631 | U+F109E | |
| 7632 | U+F109F | |
| 7633 | U+F10A0 | |
| 7634 | U+F10A1 | |
| 7635 | U+F10A2 | |
| 7636 | U+F10A3 | |
| 7637 | U+F10A4 | |
| 7638 | U+F10A5 | |
| 7639 | U+F10A6 | |
| 7640 | U+F10A7 | |
| 7641 | U+F10A8 | |
| 7642 | U+F10A9 | |
| 7643 | U+F10AA | |
| 7644 | U+F10AB | |
| 7645 | U+F10AC | |
| 7646 | U+F10AD | |
| 7647 | U+F10AE | |
| 7648 | U+F10AF | |
| 7649 | U+F10B0 | |
| 7650 | U+F10B1 | |
| 7651 | U+F10B2 | |
| 7652 | U+F10B3 | |
| 7653 | U+F10B4 | |
| 7654 | U+F10B5 | |
| 7655 | U+F10B6 | |
| 7656 | U+F10B7 | |
| 7657 | U+F10B8 | |
| 7658 | U+F10B9 | |
| 7659 | U+F10BA | |
| 7660 | U+F10BB | |
| 7661 | U+F10BC | |
| 7662 | U+F10BD | |
| 7663 | U+F10BE | |
| 7664 | U+F10BF | |
| 7665 | U+F10C0 | |
| 7666 | U+F10C1 | |
| 7667 | U+F10C2 | |
| 7668 | U+F10C3 | |
| 7669 | U+F10C4 | |
| 7670 | U+F10C5 | |
| 7671 | U+F10C6 | |
| 7672 | U+F10C7 | |
| 7673 | U+F10C8 | |
| 7674 | U+F10C9 | |
| 7675 | U+F10CA | |
| 7676 | U+F10CB | |
| 7677 | U+F10CC | |
| 7678 | U+F10CD | |
| 7679 | U+F10CE | |
| 7680 | U+F10CF | |
| 7681 | U+F10D0 | |
| 7682 | U+F10D1 | |
| 7683 | U+F10D2 | |
| 7684 | U+F10D3 | |
| 7685 | U+F10D4 | |
| 7686 | U+F10D5 | |
| 7687 | U+F10D6 | |
| 7688 | U+F10D7 | |
| 7689 | U+F10D8 | |
| 7690 | U+F10D9 | |
| 7691 | U+F10DA | |
| 7692 | U+F10DB | |
| 7693 | U+F10DC | |
| 7694 | U+F10DD | |
| 7695 | U+F10DE | |
| 7696 | U+F10DF | |
| 7697 | U+F10E0 | |
| 7698 | U+F10E1 | |
| 7699 | U+F10E2 | |
| 7700 | U+F10E3 | |
| 7701 | U+F10E4 | |
| 7702 | U+F10E5 | |
| 7703 | U+F10E6 | |
| 7704 | U+F10E7 | |
| 7705 | U+F10E8 | |
| 7706 | U+F10E9 | |
| 7707 | U+F10EA | |
| 7708 | U+F10EB | |
| 7709 | U+F10EC | |
| 7710 | U+F10ED | |
| 7711 | U+F10EE | |
| 7712 | U+F10EF | |
| 7713 | U+F10F0 | |
| 7714 | U+F10F1 | |
| 7715 | U+F10F2 | |
| 7716 | U+F10F3 | |
| 7717 | U+F10F4 | |
| 7718 | U+F10F5 | |
| 7719 | U+F10F6 | |
| 7720 | U+F10F7 | |
| 7721 | U+F10F8 | |
| 7722 | U+F10F9 | |
| 7723 | U+F10FA | |
| 7724 | U+F10FB | |
| 7725 | U+F10FC | |
| 7726 | U+F10FD | |
| 7727 | U+F10FE | |
| 7728 | U+F10FF | |
| 7729 | U+F1100 | |
| 7730 | U+F1101 | |
| 7731 | U+F1102 | |
| 7732 | U+F1103 | |
| 7733 | U+F1104 | |
| 7734 | U+F1105 | |
| 7735 | U+F1106 | |
| 7736 | U+F1107 | |
| 7737 | U+F1108 | |
| 7738 | U+F1109 | |
| 7739 | U+F110A | |
| 7740 | U+F110B | |
| 7741 | U+F110C | |
| 7742 | U+F110D | |
| 7743 | U+F110E | |
| 7744 | U+F110F | |
| 7745 | U+F1110 | |
| 7746 | U+F1111 | |
| 7747 | U+F1112 | |
| 7748 | U+F1113 | |
| 7749 | U+F1114 | |
| 7750 | U+F1115 | |
| 7751 | U+F1116 | |
| 7752 | U+F1117 | |
| 7753 | U+F1118 | |
| 7754 | U+F1119 | |
| 7755 | U+F111A | |
| 7756 | U+F111B | |
| 7757 | U+F111C | |
| 7758 | U+F111D | |
| 7759 | U+F111E | |
| 7760 | U+F111F | |
| 7761 | U+F1120 | |
| 7762 | U+F1121 | |
| 7763 | U+F1122 | |
| 7764 | U+F1123 | |
| 7765 | U+F1124 | |
| 7766 | U+F1125 | |
| 7767 | U+F1126 | |
| 7768 | U+F1127 | |
| 7769 | U+F1128 | |
| 7770 | U+F1129 | |
| 7771 | U+F112A | |
| 7772 | U+F112B | |
| 7773 | U+F112C | |
| 7774 | U+F112D | |
| 7775 | U+F112E | |
| 7776 | U+F112F | |
| 7777 | U+F1130 | |
| 7778 | U+F1131 | |
| 7779 | U+F1132 | |
| 7780 | U+F1133 | |
| 7781 | U+F1134 | |
| 7782 | U+F1135 | |
| 7783 | U+F1136 | |
| 7784 | U+F1137 | |
| 7785 | U+F1138 | |
| 7786 | U+F1139 | |
| 7787 | U+F113A | |
| 7788 | U+F113B | |
| 7789 | U+F113C | |
| 7790 | U+F113D | |
| 7791 | U+F113E | |
| 7792 | U+F113F | |
| 7793 | U+F1140 | |
| 7794 | U+F1141 | |
| 7795 | U+F1142 | |
| 7796 | U+F1143 | |
| 7797 | U+F1144 | |
| 7798 | U+F1145 | |
| 7799 | U+F1146 | |
| 7800 | U+F1147 | |
| 7801 | U+F1148 | |
| 7802 | U+F1149 | |
| 7803 | U+F114A | |
| 7804 | U+F114B | |
| 7805 | U+F114C | |
| 7806 | U+F114D | |
| 7807 | U+F114E | |
| 7808 | U+F114F | |
| 7809 | U+F1150 | |
| 7810 | U+F1151 | |
| 7811 | U+F1152 | |
| 7812 | U+F1153 | |
| 7813 | U+F1154 | |
| 7814 | U+F1155 | |
| 7815 | U+F1156 | |
| 7816 | U+F1157 | |
| 7817 | U+F1158 | |
| 7818 | U+F1159 | |
| 7819 | U+F115A | |
| 7820 | U+F115B | |
| 7821 | U+F115C | |
| 7822 | U+F115D | |
| 7823 | U+F115E | |
| 7824 | U+F115F | |
| 7825 | U+F1160 | |
| 7826 | U+F1161 | |
| 7827 | U+F1162 | |
| 7828 | U+F1163 | |
| 7829 | U+F1164 | |
| 7830 | U+F1165 | |
| 7831 | U+F1166 | |
| 7832 | U+F1167 | |
| 7833 | U+F1168 | |
| 7834 | U+F1169 | |
| 7835 | U+F116A | |
| 7836 | U+F116B | |
| 7837 | U+F116C | |
| 7838 | U+F116D | |
| 7839 | U+F116E | |
| 7840 | U+F116F | |
| 7841 | U+F1170 | |
| 7842 | U+F1171 | |
| 7843 | U+F1172 | |
| 7844 | U+F1173 | |
| 7845 | U+F1174 | |
| 7846 | U+F1175 | |
| 7847 | U+F1176 | |
| 7848 | U+F1177 | |
| 7849 | U+F1178 | |
| 7850 | U+F1179 | |
| 7851 | U+F117A | |
| 7852 | U+F117B | |
| 7853 | U+F117C | |
| 7854 | U+F117D | |
| 7855 | U+F117E | |
| 7856 | U+F117F | |
| 7857 | U+F1180 | |
| 7858 | U+F1181 | |
| 7859 | U+F1182 | |
| 7860 | U+F1183 | |
| 7861 | U+F1184 | |
| 7862 | U+F1185 | |
| 7863 | U+F1186 | |
| 7864 | U+F1187 | |
| 7865 | U+F1188 | |
| 7866 | U+F1189 | |
| 7867 | U+F118A | |
| 7868 | U+F118B | |
| 7869 | U+F118C | |
| 7870 | U+F118D | |
| 7871 | U+F118E | |
| 7872 | U+F118F | |
| 7873 | U+F1190 | |
| 7874 | U+F1191 | |
| 7875 | U+F1192 | |
| 7876 | U+F1193 | |
| 7877 | U+F1194 | |
| 7878 | U+F1195 | |
| 7879 | U+F1196 | |
| 7880 | U+F1197 | |
| 7881 | U+F1198 | |
| 7882 | U+F1199 | |
| 7883 | U+F119A | |
| 7884 | U+F119B | |
| 7885 | U+F119C | |
| 7886 | U+F119D | |
| 7887 | U+F119E | |
| 7888 | U+F119F | |
| 7889 | U+F11A0 | |
| 7890 | U+F11A1 | |
| 7891 | U+F11A2 | |
| 7892 | U+F11A3 | |
| 7893 | U+F11A4 | |
| 7894 | U+F11A5 | |
| 7895 | U+F11A6 | |
| 7896 | U+F11A7 | |
| 7897 | U+F11A8 | |
| 7898 | U+F11A9 | |
| 7899 | U+F11AA | |
| 7900 | U+F11AB | |
| 7901 | U+F11AC | |
| 7902 | U+F11AD | |
| 7903 | U+F11AE | |
| 7904 | U+F11AF | |
| 7905 | U+F11B0 | |
| 7906 | U+F11B1 | |
| 7907 | U+F11B2 | |
| 7908 | U+F11B3 | |
| 7909 | U+F11B4 | |
| 7910 | U+F11B5 | |
| 7911 | U+F11B6 | |
| 7912 | U+F11B7 | |
| 7913 | U+F11B8 | |
| 7914 | U+F11B9 | |
| 7915 | U+F11BA | |
| 7916 | U+F11BB | |
| 7917 | U+F11BC | |
| 7918 | U+F11BD | |
| 7919 | U+F11BE | |
| 7920 | U+F11BF | |
| 7921 | U+F11C0 | |
| 7922 | U+F11C1 | |
| 7923 | U+F11C2 | |
| 7924 | U+F11C3 | |
| 7925 | U+F11C4 | |
| 7926 | U+F11C5 | |
| 7927 | U+F11C6 | |
| 7928 | U+F11C7 | |
| 7929 | U+F11C8 | |
| 7930 | U+F11C9 | |
| 7931 | U+F11CA | |
| 7932 | U+F11CB | |
| 7933 | U+F11CC | |
| 7934 | U+F11CD | |
| 7935 | U+F11CE | |
| 7936 | U+F11CF | |
| 7937 | U+F11D0 | |
| 7938 | U+F11D1 | |
| 7939 | U+F11D2 | |
| 7940 | U+F11D3 | |
| 7941 | U+F11D4 | |
| 7942 | U+F11D5 | |
| 7943 | U+F11D6 | |
| 7944 | U+F11D7 | |
| 7945 | U+F11D8 | |
| 7946 | U+F11D9 | |
| 7947 | U+F11DA | |
| 7948 | U+F11DB | |
| 7949 | U+F11DC | |
| 7950 | U+F11DD | |
| 7951 | U+F11DE | |
| 7952 | U+F11DF | |
| 7953 | U+F11E0 | |
| 7954 | U+F11E1 | |
| 7955 | U+F11E2 | |
| 7956 | U+F11E3 | |
| 7957 | U+F11E4 | |
| 7958 | U+F11E5 | |
| 7959 | U+F11E6 | |
| 7960 | U+F11E7 | |
| 7961 | U+F11E8 | |
| 7962 | U+F11E9 | |
| 7963 | U+F11EA | |
| 7964 | U+F11EB | |
| 7965 | U+F11EC | |
| 7966 | U+F11ED | |
| 7967 | U+F11EE | |
| 7968 | U+F11EF | |
| 7969 | U+F11F0 | |
| 7970 | U+F11F1 | |
| 7971 | U+F11F2 | |
| 7972 | U+F11F3 | |
| 7973 | U+F11F4 | |
| 7974 | U+F11F5 | |
| 7975 | U+F11F6 | |
| 7976 | U+F11F7 | |
| 7977 | U+F11F8 | |
| 7978 | U+F11F9 | |
| 7979 | U+F11FA | |
| 7980 | U+F11FB | |
| 7981 | U+F11FC | |
| 7982 | U+F11FD | |
| 7983 | U+F11FE | |
| 7984 | U+F11FF | |
| 7985 | U+F1200 | |
| 7986 | U+F1201 | |
| 7987 | U+F1202 | |
| 7988 | U+F1203 | |
| 7989 | U+F1204 | |
| 7990 | U+F1205 | |
| 7991 | U+F1206 | |
| 7992 | U+F1207 | |
| 7993 | U+F1208 | |
| 7994 | U+F1209 | |
| 7995 | U+F120A | |
| 7996 | U+F120B | |
| 7997 | U+F120C | |
| 7998 | U+F120D | |
| 7999 | U+F120E | |
| 8000 | U+F120F | |
| 8001 | U+F1210 | |
| 8002 | U+F1211 | |
| 8003 | U+F1212 | |
| 8004 | U+F1213 | |
| 8005 | U+F1214 | |
| 8006 | U+F1215 | |
| 8007 | U+F1216 | |
| 8008 | U+F1217 | |
| 8009 | U+F1218 | |
| 8010 | U+F1219 | |
| 8011 | U+F121A | |
| 8012 | U+F121B | |
| 8013 | U+F121C | |
| 8014 | U+F121D | |
| 8015 | U+F121E | |
| 8016 | U+F121F | |
| 8017 | U+F1220 | |
| 8018 | U+F1221 | |
| 8019 | U+F1222 | |
| 8020 | U+F1223 | |
| 8021 | U+F1224 | |
| 8022 | U+F1225 | |
| 8023 | U+F1226 | |
| 8024 | U+F1227 | |
| 8025 | U+F1228 | |
| 8026 | U+F1229 | |
| 8027 | U+F122A | |
| 8028 | U+F122B | |
| 8029 | U+F122C | |
| 8030 | U+F122D | |
| 8031 | U+F122E | |
| 8032 | U+F122F | |
| 8033 | U+F1230 | |
| 8034 | U+F1231 | |
| 8035 | U+F1232 | |
| 8036 | U+F1233 | |
| 8037 | U+F1234 | |
| 8038 | U+F1235 | |
| 8039 | U+F1236 | |
| 8040 | U+F1237 | |
| 8041 | U+F1238 | |
| 8042 | U+F1239 | |
| 8043 | U+F123A | |
| 8044 | U+F123B | |
| 8045 | U+F123C | |
| 8046 | U+F123D | |
| 8047 | U+F123E | |
| 8048 | U+F123F | |
| 8049 | U+F1240 | |
| 8050 | U+F1241 | |
| 8051 | U+F1242 | |
| 8052 | U+F1243 | |
| 8053 | U+F1244 | |
| 8054 | U+F1245 | |
| 8055 | U+F1246 | |
| 8056 | U+F1247 | |
| 8057 | U+F1248 | |
| 8058 | U+F1249 | |
| 8059 | U+F124A | |
| 8060 | U+F124B | |
| 8061 | U+F124C | |
| 8062 | U+F124D | |
| 8063 | U+F124E | |
| 8064 | U+F124F | |
| 8065 | U+F1250 | |
| 8066 | U+F1251 | |
| 8067 | U+F1252 | |
| 8068 | U+F1253 | |
| 8069 | U+F1254 | |
| 8070 | U+F1255 | |
| 8071 | U+F1256 | |
| 8072 | U+F1257 | |
| 8073 | U+F1258 | |
| 8074 | U+F1259 | |
| 8075 | U+F125A | |
| 8076 | U+F125B | |
| 8077 | U+F125C | |
| 8078 | U+F125D | |
| 8079 | U+F125E | |
| 8080 | U+F125F | |
| 8081 | U+F1260 | |
| 8082 | U+F1261 | |
| 8083 | U+F1262 | |
| 8084 | U+F1263 | |
| 8085 | U+F1264 | |
| 8086 | U+F1265 | |
| 8087 | U+F1266 | |
| 8088 | U+F1267 | |
| 8089 | U+F1268 | |
| 8090 | U+F1269 | |
| 8091 | U+F126A | |
| 8092 | U+F126B | |
| 8093 | U+F126C | |
| 8094 | U+F126D | |
| 8095 | U+F126E | |
| 8096 | U+F126F | |
| 8097 | U+F1270 | |
| 8098 | U+F1271 | |
| 8099 | U+F1272 | |
| 8100 | U+F1273 | |
| 8101 | U+F1274 | |
| 8102 | U+F1275 | |
| 8103 | U+F1276 | |
| 8104 | U+F1277 | |
| 8105 | U+F1278 | |
| 8106 | U+F1279 | |
| 8107 | U+F127A | |
| 8108 | U+F127B | |
| 8109 | U+F127C | |
| 8110 | U+F127D | |
| 8111 | U+F127E | |
| 8112 | U+F127F | |
| 8113 | U+F1280 | |
| 8114 | U+F1281 | |
| 8115 | U+F1282 | |
| 8116 | U+F1283 | |
| 8117 | U+F1284 | |
| 8118 | U+F1285 | |
| 8119 | U+F1286 | |
| 8120 | U+F1287 | |
| 8121 | U+F1288 | |
| 8122 | U+F1289 | |
| 8123 | U+F128A | |
| 8124 | U+F128B | |
| 8125 | U+F128C | |
| 8126 | U+F128D | |
| 8127 | U+F128E | |
| 8128 | U+F128F | |
| 8129 | U+F1290 | |
| 8130 | U+F1291 | |
| 8131 | U+F1292 | |
| 8132 | U+F1293 | |
| 8133 | U+F1294 | |
| 8134 | U+F1295 | |
| 8135 | U+F1296 | |
| 8136 | U+F1297 | |
| 8137 | U+F1298 | |
| 8138 | U+F1299 | |
| 8139 | U+F129A | |
| 8140 | U+F129B | |
| 8141 | U+F129C | |
| 8142 | U+F129D | |
| 8143 | U+F129E | |
| 8144 | U+F129F | |
| 8145 | U+F12A0 | |
| 8146 | U+F12A1 | |
| 8147 | U+F12A2 | |
| 8148 | U+F12A3 | |
| 8149 | U+F12A4 | |
| 8150 | U+F12A5 | |
| 8151 | U+F12A6 | |
| 8152 | U+F12A7 | |
| 8153 | U+F12A8 | |
| 8154 | U+F12A9 | |
| 8155 | U+F12AA | |
| 8156 | U+F12AB | |
| 8157 | U+F12AC | |
| 8158 | U+F12AD | |
| 8159 | U+F12AE | |
| 8160 | U+F12AF | |
| 8161 | U+F12B0 | |
| 8162 | U+F12B1 | |
| 8163 | U+F12B2 | |
| 8164 | U+F12B3 | |
| 8165 | U+F12B4 | |
| 8166 | U+F12B5 | |
| 8167 | U+F12B6 | |
| 8168 | U+F12B7 | |
| 8169 | U+F12B8 | |
| 8170 | U+F12B9 | |
| 8171 | U+F12BA | |
| 8172 | U+F12BB | |
| 8173 | U+F12BC | |
| 8174 | U+F12BD | |
| 8175 | U+F12BE | |
| 8176 | U+F12BF | |
| 8177 | U+F12C0 | |
| 8178 | U+F12C1 | |
| 8179 | U+F12C2 | |
| 8180 | U+F12C3 | |
| 8181 | U+F12C4 | |
| 8182 | U+F12C5 | |
| 8183 | U+F12C6 | |
| 8184 | U+F12C7 | |
| 8185 | U+F12C8 | |
| 8186 | U+F12C9 | |
| 8187 | U+F12CA | |
| 8188 | U+F12CB | |
| 8189 | U+F12CC | |
| 8190 | U+F12CD | |
| 8191 | U+F12CE | |
| 8192 | U+F12CF | |
| 8193 | U+F12D0 | |
| 8194 | U+F12D1 | |
| 8195 | U+F12D2 | |
| 8196 | U+F12D3 | |
| 8197 | U+F12D4 | |
| 8198 | U+F12D5 | |
| 8199 | U+F12D6 | |
| 8200 | U+F12D7 | |
| 8201 | U+F12D8 | |
| 8202 | U+F12D9 | |
| 8203 | U+F12DA | |
| 8204 | U+F12DB | |
| 8205 | U+F12DC | |
| 8206 | U+F12DD | |
| 8207 | U+F12DE | |
| 8208 | U+F12DF | |
| 8209 | U+F12E0 | |
| 8210 | U+F12E1 | |
| 8211 | U+F12E2 | |
| 8212 | U+F12E3 | |
| 8213 | U+F12E4 | |
| 8214 | U+F12E5 | |
| 8215 | U+F12E6 | |
| 8216 | U+F12E7 | |
| 8217 | U+F12E8 | |
| 8218 | U+F12E9 | |
| 8219 | U+F12EA | |
| 8220 | U+F12EB | |
| 8221 | U+F12EC | |
| 8222 | U+F12ED | |
| 8223 | U+F12EE | |
| 8224 | U+F12EF | |
| 8225 | U+F12F0 | |
| 8226 | U+F12F1 | |
| 8227 | U+F12F2 | |
| 8228 | U+F12F3 | |
| 8229 | U+F12F4 | |
| 8230 | U+F12F5 | |
| 8231 | U+F12F6 | |
| 8232 | U+F12F7 | |
| 8233 | U+F12F8 | |
| 8234 | U+F12F9 | |
| 8235 | U+F12FA | |
| 8236 | U+F12FB | |
| 8237 | U+F12FC | |
| 8238 | U+F12FD | |
| 8239 | U+F12FE | |
| 8240 | U+F12FF | |
| 8241 | U+F1300 | |
| 8242 | U+F1301 | |
| 8243 | U+F1302 | |
| 8244 | U+F1303 | |
| 8245 | U+F1304 | |
| 8246 | U+F1305 | |
| 8247 | U+F1306 | |
| 8248 | U+F1307 | |
| 8249 | U+F1308 | |
| 8250 | U+F1309 | |
| 8251 | U+F130A | |
| 8252 | U+F130B | |
| 8253 | U+F130C | |
| 8254 | U+F130D | |
| 8255 | U+F130E | |
| 8256 | U+F130F | |
| 8257 | U+F1310 | |
| 8258 | U+F1311 | |
| 8259 | U+F1312 | |
| 8260 | U+F1313 | |
| 8261 | U+F1314 | |
| 8262 | U+F1315 | |
| 8263 | U+F1316 | |
| 8264 | U+F1317 | |
| 8265 | U+F1318 | |
| 8266 | U+F1319 | |
| 8267 | U+F131A | |
| 8268 | U+F131B | |
| 8269 | U+F131C | |
| 8270 | U+F131D | |
| 8271 | U+F131E | |
| 8272 | U+F131F | |
| 8273 | U+F1320 | |
| 8274 | U+F1321 | |
| 8275 | U+F1322 | |
| 8276 | U+F1323 | |
| 8277 | U+F1324 | |
| 8278 | U+F1325 | |
| 8279 | U+F1326 | |
| 8280 | U+F1327 | |
| 8281 | U+F1328 | |
| 8282 | U+F1329 | |
| 8283 | U+F132A | |
| 8284 | U+F132B | |
| 8285 | U+F132C | |
| 8286 | U+F132D | |
| 8287 | U+F132E | |
| 8288 | U+F132F | |
| 8289 | U+F1330 | |
| 8290 | U+F1331 | |
| 8291 | U+F1332 | |
| 8292 | U+F1333 | |
| 8293 | U+F1334 | |
| 8294 | U+F1335 | |
| 8295 | U+F1336 | |
| 8296 | U+F1337 | |
| 8297 | U+F1338 | |
| 8298 | U+F1339 | |
| 8299 | U+F133A | |
| 8300 | U+F133B | |
| 8301 | U+F133C | |
| 8302 | U+F133D | |
| 8303 | U+F133E | |
| 8304 | U+F133F | |
| 8305 | U+F1340 | |
| 8306 | U+F1341 | |
| 8307 | U+F1342 | |
| 8308 | U+F1343 | |
| 8309 | U+F1344 | |
| 8310 | U+F1345 | |
| 8311 | U+F1346 | |
| 8312 | U+F1347 | |
| 8313 | U+F1348 | |
| 8314 | U+F1349 | |
| 8315 | U+F134A | |
| 8316 | U+F134B | |
| 8317 | U+F134C | |
| 8318 | U+F134D | |
| 8319 | U+F134E | |
| 8320 | U+F134F | |
| 8321 | U+F1350 | |
| 8322 | U+F1351 | |
| 8323 | U+F1352 | |
| 8324 | U+F1353 | |
| 8325 | U+F1354 | |
| 8326 | U+F1355 | |
| 8327 | U+F1356 | |
| 8328 | U+F1357 | |
| 8329 | U+F1358 | |
| 8330 | U+F1359 | |
| 8331 | U+F135A | |
| 8332 | U+F135B | |
| 8333 | U+F135C | |
| 8334 | U+F135D | |
| 8335 | U+F135E | |
| 8336 | U+F135F | |
| 8337 | U+F1360 | |
| 8338 | U+F1361 | |
| 8339 | U+F1362 | |
| 8340 | U+F1363 | |
| 8341 | U+F1364 | |
| 8342 | U+F1365 | |
| 8343 | U+F1366 | |
| 8344 | U+F1367 | |
| 8345 | U+F1368 | |
| 8346 | U+F1369 | |
| 8347 | U+F136A | |
| 8348 | U+F136B | |
| 8349 | U+F136C | |
| 8350 | U+F136D | |
| 8351 | U+F136E | |
| 8352 | U+F136F | |
| 8353 | U+F1370 | |
| 8354 | U+F1371 | |
| 8355 | U+F1372 | |
| 8356 | U+F1373 | |
| 8357 | U+F1374 | |
| 8358 | U+F1375 | |
| 8359 | U+F1376 | |
| 8360 | U+F1377 | |
| 8361 | U+F1378 | |
| 8362 | U+F1379 | |
| 8363 | U+F137A | |
| 8364 | U+F137B | |
| 8365 | U+F137C | |
| 8366 | U+F137D | |
| 8367 | U+F137E | |
| 8368 | U+F137F | |
| 8369 | U+F1380 | |
| 8370 | U+F1381 | |
| 8371 | U+F1382 | |
| 8372 | U+F1383 | |
| 8373 | U+F1384 | |
| 8374 | U+F1385 | |
| 8375 | U+F1386 | |
| 8376 | U+F1387 | |
| 8377 | U+F1388 | |
| 8378 | U+F1389 | |
| 8379 | U+F138A | |
| 8380 | U+F138B | |
| 8381 | U+F138C | |
| 8382 | U+F138D | |
| 8383 | U+F138E | |
| 8384 | U+F138F | |
| 8385 | U+F1390 | |
| 8386 | U+F1391 | |
| 8387 | U+F1392 | |
| 8388 | U+F1393 | |
| 8389 | U+F1394 | |
| 8390 | U+F1395 | |
| 8391 | U+F1396 | |
| 8392 | U+F1397 | |
| 8393 | U+F1398 | |
| 8394 | U+F1399 | |
| 8395 | U+F139A | |
| 8396 | U+F139B | |
| 8397 | U+F139C | |
| 8398 | U+F139D | |
| 8399 | U+F139E | |
| 8400 | U+F139F | |
| 8401 | U+F13A0 | |
| 8402 | U+F13A1 | |
| 8403 | U+F13A2 | |
| 8404 | U+F13A3 | |
| 8405 | U+F13A4 | |
| 8406 | U+F13A5 | |
| 8407 | U+F13A6 | |
| 8408 | U+F13A7 | |
| 8409 | U+F13A8 | |
| 8410 | U+F13A9 | |
| 8411 | U+F13AA | |
| 8412 | U+F13AB | |
| 8413 | U+F13AC | |
| 8414 | U+F13AD | |
| 8415 | U+F13AE | |
| 8416 | U+F13AF | |
| 8417 | U+F13B0 | |
| 8418 | U+F13B1 | |
| 8419 | U+F13B2 | |
| 8420 | U+F13B3 | |
| 8421 | U+F13B4 | |
| 8422 | U+F13B5 | |
| 8423 | U+F13B6 | |
| 8424 | U+F13B7 | |
| 8425 | U+F13B8 | |
| 8426 | U+F13B9 | |
| 8427 | U+F13BA | |
| 8428 | U+F13BB | |
| 8429 | U+F13BC | |
| 8430 | U+F13BD | |
| 8431 | U+F13BE | |
| 8432 | U+F13BF | |
| 8433 | U+F13C0 | |
| 8434 | U+F13C1 | |
| 8435 | U+F13C2 | |
| 8436 | U+F13C3 | |
| 8437 | U+F13C4 | |
| 8438 | U+F13C5 | |
| 8439 | U+F13C6 | |
| 8440 | U+F13C7 | |
| 8441 | U+F13C8 | |
| 8442 | U+F13C9 | |
| 8443 | U+F13CA | |
| 8444 | U+F13CB | |
| 8445 | U+F13CC | |
| 8446 | U+F13CD | |
| 8447 | U+F13CE | |
| 8448 | U+F13CF | |
| 8449 | U+F13D0 | |
| 8450 | U+F13D1 | |
| 8451 | U+F13D2 | |
| 8452 | U+F13D3 | |
| 8453 | U+F13D4 | |
| 8454 | U+F13D5 | |
| 8455 | U+F13D6 | |
| 8456 | U+F13D7 | |
| 8457 | U+F13D8 | |
| 8458 | U+F13D9 | |
| 8459 | U+F13DA | |
| 8460 | U+F13DB | |
| 8461 | U+F13DC | |
| 8462 | U+F13DD | |
| 8463 | U+F13DE | |
| 8464 | U+F13DF | |
| 8465 | U+F13E0 | |
| 8466 | U+F13E1 | |
| 8467 | U+F13E2 | |
| 8468 | U+F13E3 | |
| 8469 | U+F13E4 | |
| 8470 | U+F13E5 | |
| 8471 | U+F13E6 | |
| 8472 | U+F13E7 | |
| 8473 | U+F13E8 | |
| 8474 | U+F13E9 | |
| 8475 | U+F13EA | |
| 8476 | U+F13EB | |
| 8477 | U+F13EC | |
| 8478 | U+F13ED | |
| 8479 | U+F13EE | |
| 8480 | U+F13EF | |
| 8481 | U+F13F0 | |
| 8482 | U+F13F1 | |
| 8483 | U+F13F2 | |
| 8484 | U+F13F3 | |
| 8485 | U+F13F4 | |
| 8486 | U+F13F5 | |
| 8487 | U+F13F6 | |
| 8488 | U+F13F7 | |
| 8489 | U+F13F8 | |
| 8490 | U+F13F9 | |
| 8491 | U+F13FA | |
| 8492 | U+F13FB | |
| 8493 | U+F13FC | |
| 8494 | U+F13FD | |
| 8495 | U+F13FE | |
| 8496 | U+F13FF | |
| 8497 | U+F1400 | |
| 8498 | U+F1401 | |
| 8499 | U+F1402 | |
| 8500 | U+F1403 | |
| 8501 | U+F1404 | |
| 8502 | U+F1405 | |
| 8503 | U+F1406 | |
| 8504 | U+F1407 | |
| 8505 | U+F1408 | |
| 8506 | U+F1409 | |
| 8507 | U+F140A | |
| 8508 | U+F140B | |
| 8509 | U+F140C | |
| 8510 | U+F140D | |
| 8511 | U+F140E | |
| 8512 | U+F140F | |
| 8513 | U+F1410 | |
| 8514 | U+F1411 | |
| 8515 | U+F1412 | |
| 8516 | U+F1413 | |
| 8517 | U+F1414 | |
| 8518 | U+F1415 | |
| 8519 | U+F1416 | |
| 8520 | U+F1417 | |
| 8521 | U+F1418 | |
| 8522 | U+F1419 | |
| 8523 | U+F141A | |
| 8524 | U+F141B | |
| 8525 | U+F141C | |
| 8526 | U+F141D | |
| 8527 | U+F141E | |
| 8528 | U+F141F | |
| 8529 | U+F1420 | |
| 8530 | U+F1421 | |
| 8531 | U+F1422 | |
| 8532 | U+F1423 | |
| 8533 | U+F1424 | |
| 8534 | U+F1425 | |
| 8535 | U+F1426 | |
| 8536 | U+F1427 | |
| 8537 | U+F1428 | |
| 8538 | U+F1429 | |
| 8539 | U+F142A | |
| 8540 | U+F142B | |
| 8541 | U+F142C | |
| 8542 | U+F142D | |
| 8543 | U+F142E | |
| 8544 | U+F142F | |
| 8545 | U+F1430 | |
| 8546 | U+F1431 | |
| 8547 | U+F1432 | |
| 8548 | U+F1433 | |
| 8549 | U+F1434 | |
| 8550 | U+F1435 | |
| 8551 | U+F1436 | |
| 8552 | U+F1437 | |
| 8553 | U+F1438 | |
| 8554 | U+F1439 | |
| 8555 | U+F143A | |
| 8556 | U+F143B | |
| 8557 | U+F143C | |
| 8558 | U+F143D | |
| 8559 | U+F143E | |
| 8560 | U+F143F | |
| 8561 | U+F1440 | |
| 8562 | U+F1441 | |
| 8563 | U+F1442 | |
| 8564 | U+F1443 | |
| 8565 | U+F1444 | |
| 8566 | U+F1445 | |
| 8567 | U+F1446 | |
| 8568 | U+F1447 | |
| 8569 | U+F1448 | |
| 8570 | U+F1449 | |
| 8571 | U+F144A | |
| 8572 | U+F144B | |
| 8573 | U+F144C | |
| 8574 | U+F144D | |
| 8575 | U+F144E | |
| 8576 | U+F144F | |
| 8577 | U+F1450 | |
| 8578 | U+F1451 | |
| 8579 | U+F1452 | |
| 8580 | U+F1453 | |
| 8581 | U+F1454 | |
| 8582 | U+F1455 | |
| 8583 | U+F1456 | |
| 8584 | U+F1457 | |
| 8585 | U+F1458 | |
| 8586 | U+F1459 | |
| 8587 | U+F145A | |
| 8588 | U+F145B | |
| 8589 | U+F145C | |
| 8590 | U+F145D | |
| 8591 | U+F145E | |
| 8592 | U+F145F | |
| 8593 | U+F1460 | |
| 8594 | U+F1461 | |
| 8595 | U+F1462 | |
| 8596 | U+F1463 | |
| 8597 | U+F1464 | |
| 8598 | U+F1465 | |
| 8599 | U+F1466 | |
| 8600 | U+F1467 | |
| 8601 | U+F1468 | |
| 8602 | U+F1469 | |
| 8603 | U+F146A | |
| 8604 | U+F146B | |
| 8605 | U+F146C | |
| 8606 | U+F146D | |
| 8607 | U+F146E | |
| 8608 | U+F146F | |
| 8609 | U+F1470 | |
| 8610 | U+F1471 | |
| 8611 | U+F1472 | |
| 8612 | U+F1473 | |
| 8613 | U+F1474 | |
| 8614 | U+F1475 | |
| 8615 | U+F1476 | |
| 8616 | U+F1477 | |
| 8617 | U+F1478 | |
| 8618 | U+F1479 | |
| 8619 | U+F147A | |
| 8620 | U+F147B | |
| 8621 | U+F147C | |
| 8622 | U+F147D | |
| 8623 | U+F147E | |
| 8624 | U+F147F | |
| 8625 | U+F1480 | |
| 8626 | U+F1481 | |
| 8627 | U+F1482 | |
| 8628 | U+F1483 | |
| 8629 | U+F1484 | |
| 8630 | U+F1485 | |
| 8631 | U+F1486 | |
| 8632 | U+F1487 | |
| 8633 | U+F1488 | |
| 8634 | U+F1489 | |
| 8635 | U+F148A | |
| 8636 | U+F148B | |
| 8637 | U+F148C | |
| 8638 | U+F148D | |
| 8639 | U+F148E | |
| 8640 | U+F148F | |
| 8641 | U+F1490 | |
| 8642 | U+F1491 | |
| 8643 | U+F1492 | |
| 8644 | U+F1493 | |
| 8645 | U+F1494 | |
| 8646 | U+F1495 | |
| 8647 | U+F1496 | |
| 8648 | U+F1497 | |
| 8649 | U+F1498 | |
| 8650 | U+F1499 | |
| 8651 | U+F149A | |
| 8652 | U+F149B | |
| 8653 | U+F149C | |
| 8654 | U+F149D | |
| 8655 | U+F149E | |
| 8656 | U+F149F | |
| 8657 | U+F14A0 | |
| 8658 | U+F14A1 | |
| 8659 | U+F14A2 | |
| 8660 | U+F14A3 | |
| 8661 | U+F14A4 | |
| 8662 | U+F14A5 | |
| 8663 | U+F14A6 | |
| 8664 | U+F14A7 | |
| 8665 | U+F14A8 | |
| 8666 | U+F14A9 | |
| 8667 | U+F14AA | |
| 8668 | U+F14AB | |
| 8669 | U+F14AC | |
| 8670 | U+F14AD | |
| 8671 | U+F14AE | |
| 8672 | U+F14AF | |
| 8673 | U+F14B0 | |
| 8674 | U+F14B1 | |
| 8675 | U+F14B2 | |
| 8676 | U+F14B3 | |
| 8677 | U+F14B4 | |
| 8678 | U+F14B5 | |
| 8679 | U+F14B6 | |
| 8680 | U+F14B7 | |
| 8681 | U+F14B8 | |
| 8682 | U+F14B9 | |
| 8683 | U+F14BA | |
| 8684 | U+F14BB | |
| 8685 | U+F14BC | |
| 8686 | U+F14BD | |
| 8687 | U+F14BE | |
| 8688 | U+F14BF | |
| 8689 | U+F14C0 | |
| 8690 | U+F14C1 | |
| 8691 | U+F14C2 | |
| 8692 | U+F14C3 | |
| 8693 | U+F14C4 | |
| 8694 | U+F14C5 | |
| 8695 | U+F14C6 | |
| 8696 | U+F14C7 | |
| 8697 | U+F14C8 | |
| 8698 | U+F14C9 | |
| 8699 | U+F14CA | |
| 8700 | U+F14CB | |
| 8701 | U+F14CC | |
| 8702 | U+F14CD | |
| 8703 | U+F14CE | |
| 8704 | U+F14CF | |
| 8705 | U+F14D0 | |
| 8706 | U+F14D1 | |
| 8707 | U+F14D2 | |
| 8708 | U+F14D3 | |
| 8709 | U+F14D4 | |
| 8710 | U+F14D5 | |
| 8711 | U+F14D6 | |
| 8712 | U+F14D7 | |
| 8713 | U+F14D8 | |
| 8714 | U+F14D9 | |
| 8715 | U+F14DA | |
| 8716 | U+F14DB | |
| 8717 | U+F14DC | |
| 8718 | U+F14DD | |
| 8719 | U+F14DE | |
| 8720 | U+F14DF | |
| 8721 | U+F14E0 | |
| 8722 | U+F14E1 | |
| 8723 | U+F14E2 | |
| 8724 | U+F14E3 | |
| 8725 | U+F14E4 | |
| 8726 | U+F14E5 | |
| 8727 | U+F14E6 | |
| 8728 | U+F14E7 | |
| 8729 | U+F14E8 | |
| 8730 | U+F14E9 | |
| 8731 | U+F14EA | |
| 8732 | U+F14EB | |
| 8733 | U+F14EC | |
| 8734 | U+F14ED | |
| 8735 | U+F14EE | |
| 8736 | U+F14EF | |
| 8737 | U+F14F0 | |
| 8738 | U+F14F1 | |
| 8739 | U+F14F2 | |
| 8740 | U+F14F3 | |
| 8741 | U+F14F4 | |
| 8742 | U+F14F5 | |
| 8743 | U+F14F6 | |
| 8744 | U+F14F7 | |
| 8745 | U+F14F8 | |
| 8746 | U+F14F9 | |
| 8747 | U+F14FA | |
| 8748 | U+F14FB | |
| 8749 | U+F14FC | |
| 8750 | U+F14FD | |
| 8751 | U+F14FE | |
| 8752 | U+F14FF | |
| 8753 | U+F1500 | |
| 8754 | U+F1501 | |
| 8755 | U+F1502 | |
| 8756 | U+F1503 | |
| 8757 | U+F1504 | |
| 8758 | U+F1505 | |
| 8759 | U+F1506 | |
| 8760 | U+F1507 | |
| 8761 | U+F1508 | |
| 8762 | U+F1509 | |
| 8763 | U+F150A | |
| 8764 | U+F150B | |
| 8765 | U+F150C | |
| 8766 | U+F150D | |
| 8767 | U+F150E | |
| 8768 | U+F150F | |
| 8769 | U+F1510 | |
| 8770 | U+F1511 | |
| 8771 | U+F1512 | |
| 8772 | U+F1513 | |
| 8773 | U+F1514 | |
| 8774 | U+F1515 | |
| 8775 | U+F1516 | |
| 8776 | U+F1517 | |
| 8777 | U+F1518 | |
| 8778 | U+F1519 | |
| 8779 | U+F151A | |
| 8780 | U+F151B | |
| 8781 | U+F151C | |
| 8782 | U+F151D | |
| 8783 | U+F151E | |
| 8784 | U+F151F | |
| 8785 | U+F1520 | |
| 8786 | U+F1521 | |
| 8787 | U+F1522 | |
| 8788 | U+F1523 | |
| 8789 | U+F1524 | |
| 8790 | U+F1525 | |
| 8791 | U+F1526 | |
| 8792 | U+F1527 | |
| 8793 | U+F1528 | |
| 8794 | U+F1529 | |
| 8795 | U+F152A | |
| 8796 | U+F152B | |
| 8797 | U+F152C | |
| 8798 | U+F152D | |
| 8799 | U+F152E | |
| 8800 | U+F152F | |
| 8801 | U+F1530 | |
| 8802 | U+F1531 | |
| 8803 | U+F1532 | |
| 8804 | U+F1533 | |
| 8805 | U+F1534 | |
| 8806 | U+F1535 | |
| 8807 | U+F1536 | |
| 8808 | U+F1537 | |
| 8809 | U+F1538 | |
| 8810 | U+F1539 | |
| 8811 | U+F153A | |
| 8812 | U+F153B | |
| 8813 | U+F153C | |
| 8814 | U+F153D | |
| 8815 | U+F153E | |
| 8816 | U+F153F | |
| 8817 | U+F1540 | |
| 8818 | U+F1541 | |
| 8819 | U+F1542 | |
| 8820 | U+F1543 | |
| 8821 | U+F1544 | |
| 8822 | U+F1545 | |
| 8823 | U+F1546 | |
| 8824 | U+F1547 | |
| 8825 | U+F1548 | |
| 8826 | U+F1549 | |
| 8827 | U+F154A | |
| 8828 | U+F154B | |
| 8829 | U+F154C | |
| 8830 | U+F154D | |
| 8831 | U+F154E | |
| 8832 | U+F154F | |
| 8833 | U+F1550 | |
| 8834 | U+F1551 | |
| 8835 | U+F1552 | |
| 8836 | U+F1553 | |
| 8837 | U+F1554 | |
| 8838 | U+F1555 | |
| 8839 | U+F1556 | |
| 8840 | U+F1557 | |
| 8841 | U+F1558 | |
| 8842 | U+F1559 | |
| 8843 | U+F155A | |
| 8844 | U+F155B | |
| 8845 | U+F155C | |
| 8846 | U+F155D | |
| 8847 | U+F155E | |
| 8848 | U+F155F | |
| 8849 | U+F1560 | |
| 8850 | U+F1561 | |
| 8851 | U+F1562 | |
| 8852 | U+F1563 | |
| 8853 | U+F1564 | |
| 8854 | U+F1565 | |
| 8855 | U+F1566 | |
| 8856 | U+F1567 | |
| 8857 | U+F1568 | |
| 8858 | U+F1569 | |
| 8859 | U+F156A | |
| 8860 | U+F156B | |
| 8861 | U+F156C | |
| 8862 | U+F156D | |
| 8863 | U+F156E | |
| 8864 | U+F156F | |
| 8865 | U+F1570 | |
| 8866 | U+F1571 | |
| 8867 | U+F1572 | |
| 8868 | U+F1573 | |
| 8869 | U+F1574 | |
| 8870 | U+F1575 | |
| 8871 | U+F1576 | |
| 8872 | U+F1577 | |
| 8873 | U+F1578 | |
| 8874 | U+F1579 | |
| 8875 | U+F157A | |
| 8876 | U+F157B | |
| 8877 | U+F157C | |
| 8878 | U+F157D | |
| 8879 | U+F157E | |
| 8880 | U+F157F | |
| 8881 | U+F1580 | |
| 8882 | U+F1581 | |
| 8883 | U+F1582 | |
| 8884 | U+F1583 | |
| 8885 | U+F1584 | |
| 8886 | U+F1585 | |
| 8887 | U+F1586 | |
| 8888 | U+F1587 | |
| 8889 | U+F1588 | |
| 8890 | U+F1589 | |
| 8891 | U+F158A | |
| 8892 | U+F158B | |
| 8893 | U+F158C | |
| 8894 | U+F158D | |
| 8895 | U+F158E | |
| 8896 | U+F158F | |
| 8897 | U+F1590 | |
| 8898 | U+F1591 | |
| 8899 | U+F1592 | |
| 8900 | U+F1593 | |
| 8901 | U+F1594 | |
| 8902 | U+F1595 | |
| 8903 | U+F1596 | |
| 8904 | U+F1597 | |
| 8905 | U+F1598 | |
| 8906 | U+F1599 | |
| 8907 | U+F159A | |
| 8908 | U+F159B | |
| 8909 | U+F159C | |
| 8910 | U+F159D | |
| 8911 | U+F159E | |
| 8912 | U+F159F | |
| 8913 | U+F15A0 | |
| 8914 | U+F15A1 | |
| 8915 | U+F15A2 | |
| 8916 | U+F15A3 | |
| 8917 | U+F15A4 | |
| 8918 | U+F15A5 | |
| 8919 | U+F15A6 | |
| 8920 | U+F15A7 | |
| 8921 | U+F15A8 | |
| 8922 | U+F15A9 | |
| 8923 | U+F15AA | |
| 8924 | U+F15AB | |
| 8925 | U+F15AC | |
| 8926 | U+F15AD | |
| 8927 | U+F15AE | |
| 8928 | U+F15AF | |
| 8929 | U+F15B0 | |
| 8930 | U+F15B1 | |
| 8931 | U+F15B2 | |
| 8932 | U+F15B3 | |
| 8933 | U+F15B4 | |
| 8934 | U+F15B5 | |
| 8935 | U+F15B6 | |
| 8936 | U+F15B7 | |
| 8937 | U+F15B8 | |
| 8938 | U+F15B9 | |
| 8939 | U+F15BA | |
| 8940 | U+F15BB | |
| 8941 | U+F15BC | |
| 8942 | U+F15BD | |
| 8943 | U+F15BE | |
| 8944 | U+F15BF | |
| 8945 | U+F15C0 | |
| 8946 | U+F15C1 | |
| 8947 | U+F15C2 | |
| 8948 | U+F15C3 | |
| 8949 | U+F15C4 | |
| 8950 | U+F15C5 | |
| 8951 | U+F15C6 | |
| 8952 | U+F15C7 | |
| 8953 | U+F15C8 | |
| 8954 | U+F15C9 | |
| 8955 | U+F15CA | |
| 8956 | U+F15CB | |
| 8957 | U+F15CC | |
| 8958 | U+F15CD | |
| 8959 | U+F15CE | |
| 8960 | U+F15CF | |
| 8961 | U+F15D0 | |
| 8962 | U+F15D1 | |
| 8963 | U+F15D2 | |
| 8964 | U+F15D3 | |
| 8965 | U+F15D4 | |
| 8966 | U+F15D5 | |
| 8967 | U+F15D6 | |
| 8968 | U+F15D7 | |
| 8969 | U+F15D8 | |
| 8970 | U+F15D9 | |
| 8971 | U+F15DA | |
| 8972 | U+F15DB | |
| 8973 | U+F15DC | |
| 8974 | U+F15DD | |
| 8975 | U+F15DE | |
| 8976 | U+F15DF | |
| 8977 | U+F15E0 | |
| 8978 | U+F15E1 | |
| 8979 | U+F15E2 | |
| 8980 | U+F15E3 | |
| 8981 | U+F15E4 | |
| 8982 | U+F15E5 | |
| 8983 | U+F15E6 | |
| 8984 | U+F15E7 | |
| 8985 | U+F15E8 | |
| 8986 | U+F15E9 | |
| 8987 | U+F15EA | |
| 8988 | U+F15EB | |
| 8989 | U+F15EC | |
| 8990 | U+F15ED | |
| 8991 | U+F15EE | |
| 8992 | U+F15EF | |
| 8993 | U+F15F0 | |
| 8994 | U+F15F1 | |
| 8995 | U+F15F2 | |
| 8996 | U+F15F3 | |
| 8997 | U+F15F4 | |
| 8998 | U+F15F5 | |
| 8999 | U+F15F6 | |
| 9000 | U+F15F7 | |
| 9001 | U+F15F8 | |
| 9002 | U+F15F9 | |
| 9003 | U+F15FA | |
| 9004 | U+F15FB | |
| 9005 | U+F15FC | |
| 9006 | U+F15FD | |
| 9007 | U+F15FE | |
| 9008 | U+F15FF | |
| 9009 | U+F1600 | |
| 9010 | U+F1601 | |
| 9011 | U+F1602 | |
| 9012 | U+F1603 | |
| 9013 | U+F1604 | |
| 9014 | U+F1605 | |
| 9015 | U+F1606 | |
| 9016 | U+F1607 | |
| 9017 | U+F1608 | |
| 9018 | U+F1609 | |
| 9019 | U+F160A | |
| 9020 | U+F160B | |
| 9021 | U+F160C | |
| 9022 | U+F160D | |
| 9023 | U+F160E | |
| 9024 | U+F160F | |
| 9025 | U+F1610 | |
| 9026 | U+F1611 | |
| 9027 | U+F1612 | |
| 9028 | U+F1613 | |
| 9029 | U+F1614 | |
| 9030 | U+F1615 | |
| 9031 | U+F1616 | |
| 9032 | U+F1617 | |
| 9033 | U+F1618 | |
| 9034 | U+F1619 | |
| 9035 | U+F161A | |
| 9036 | U+F161B | |
| 9037 | U+F161C | |
| 9038 | U+F161D | |
| 9039 | U+F161E | |
| 9040 | U+F161F | |
| 9041 | U+F1620 | |
| 9042 | U+F1621 | |
| 9043 | U+F1622 | |
| 9044 | U+F1623 | |
| 9045 | U+F1624 | |
| 9046 | U+F1625 | |
| 9047 | U+F1626 | |
| 9048 | U+F1627 | |
| 9049 | U+F1628 | |
| 9050 | U+F1629 | |
| 9051 | U+F162A | |
| 9052 | U+F162B | |
| 9053 | U+F162C | |
| 9054 | U+F162D | |
| 9055 | U+F162E | |
| 9056 | U+F162F | |
| 9057 | U+F1630 | |
| 9058 | U+F1631 | |
| 9059 | U+F1632 | |
| 9060 | U+F1633 | |
| 9061 | U+F1634 | |
| 9062 | U+F1635 | |
| 9063 | U+F1636 | |
| 9064 | U+F1637 | |
| 9065 | U+F1638 | |
| 9066 | U+F1639 | |
| 9067 | U+F163A | |
| 9068 | U+F163B | |
| 9069 | U+F163C | |
| 9070 | U+F163D | |
| 9071 | U+F163E | |
| 9072 | U+F163F | |
| 9073 | U+F1640 | |
| 9074 | U+F1641 | |
| 9075 | U+F1642 | |
| 9076 | U+F1643 | |
| 9077 | U+F1644 | |
| 9078 | U+F1645 | |
| 9079 | U+F1646 | |
| 9080 | U+F1647 | |
| 9081 | U+F1648 | |
| 9082 | U+F1649 | |
| 9083 | U+F164A | |
| 9084 | U+F164B | |
| 9085 | U+F164C | |
| 9086 | U+F164D | |
| 9087 | U+F164E | |
| 9088 | U+F164F | |
| 9089 | U+F1650 | |
| 9090 | U+F1651 | |
| 9091 | U+F1652 | |
| 9092 | U+F1653 | |
| 9093 | U+F1654 | |
| 9094 | U+F1655 | |
| 9095 | U+F1656 | |
| 9096 | U+F1657 | |
| 9097 | U+F1658 | |
| 9098 | U+F1659 | |
| 9099 | U+F165A | |
| 9100 | U+F165B | |
| 9101 | U+F165C | |
| 9102 | U+F165D | |
| 9103 | U+F165E | |
| 9104 | U+F165F | |
| 9105 | U+F1660 | |
| 9106 | U+F1661 | |
| 9107 | U+F1662 | |
| 9108 | U+F1663 | |
| 9109 | U+F1664 | |
| 9110 | U+F1665 | |
| 9111 | U+F1666 | |
| 9112 | U+F1667 | |
| 9113 | U+F1668 | |
| 9114 | U+F1669 | |
| 9115 | U+F166A | |
| 9116 | U+F166B | |
| 9117 | U+F166C | |
| 9118 | U+F166D | |
| 9119 | U+F166E | |
| 9120 | U+F166F | |
| 9121 | U+F1670 | |
| 9122 | U+F1671 | |
| 9123 | U+F1672 | |
| 9124 | U+F1673 | |
| 9125 | U+F1674 | |
| 9126 | U+F1675 | |
| 9127 | U+F1676 | |
| 9128 | U+F1677 | |
| 9129 | U+F1678 | |
| 9130 | U+F1679 | |
| 9131 | U+F167A | |
| 9132 | U+F167B | |
| 9133 | U+F167C | |
| 9134 | U+F167D | |
| 9135 | U+F167E | |
| 9136 | U+F167F | |
| 9137 | U+F1680 | |
| 9138 | U+F1681 | |
| 9139 | U+F1682 | |
| 9140 | U+F1683 | |
| 9141 | U+F1684 | |
| 9142 | U+F1685 | |
| 9143 | U+F1686 | |
| 9144 | U+F1687 | |
| 9145 | U+F1688 | |
| 9146 | U+F1689 | |
| 9147 | U+F168A | |
| 9148 | U+F168B | |
| 9149 | U+F168C | |
| 9150 | U+F168D | |
| 9151 | U+F168E | |
| 9152 | U+F168F | |
| 9153 | U+F1690 | |
| 9154 | U+F1691 | |
| 9155 | U+F1692 | |
| 9156 | U+F1693 | |
| 9157 | U+F1694 | |
| 9158 | U+F1695 | |
| 9159 | U+F1696 | |
| 9160 | U+F1697 | |
| 9161 | U+F1698 | |
| 9162 | U+F1699 | |
| 9163 | U+F169A | |
| 9164 | U+F169B | |
| 9165 | U+F169C | |
| 9166 | U+F169D | |
| 9167 | U+F169E | |
| 9168 | U+F169F | |
| 9169 | U+F16A0 | |
| 9170 | U+F16A1 | |
| 9171 | U+F16A2 | |
| 9172 | U+F16A3 | |
| 9173 | U+F16A4 | |
| 9174 | U+F16A5 | |
| 9175 | U+F16A6 | |
| 9176 | U+F16A7 | |
| 9177 | U+F16A8 | |
| 9178 | U+F16A9 | |
| 9179 | U+F16AA | |
| 9180 | U+F16AB | |
| 9181 | U+F16AC | |
| 9182 | U+F16AD | |
| 9183 | U+F16AE | |
| 9184 | U+F16AF | |
| 9185 | U+F16B0 | |
| 9186 | U+F16B1 | |
| 9187 | U+F16B2 | |
| 9188 | U+F16B3 | |
| 9189 | U+F16B4 | |
| 9190 | U+F16B5 | |
| 9191 | U+F16B6 | |
| 9192 | U+F16B7 | |
| 9193 | U+F16B8 | |
| 9194 | U+F16B9 | |
| 9195 | U+F16BA | |
| 9196 | U+F16BB | |
| 9197 | U+F16BC | |
| 9198 | U+F16BD | |
| 9199 | U+F16BE | |
| 9200 | U+F16BF | |
| 9201 | U+F16C0 | |
| 9202 | U+F16C1 | |
| 9203 | U+F16C2 | |
| 9204 | U+F16C3 | |
| 9205 | U+F16C4 | |
| 9206 | U+F16C5 | |
| 9207 | U+F16C6 | |
| 9208 | U+F16C7 | |
| 9209 | U+F16C8 | |
| 9210 | U+F16C9 | |
| 9211 | U+F16CA | |
| 9212 | U+F16CB | |
| 9213 | U+F16CC | |
| 9214 | U+F16CD | |
| 9215 | U+F16CE | |
| 9216 | U+F16CF | |
| 9217 | U+F16D0 | |
| 9218 | U+F16D1 | |
| 9219 | U+F16D2 | |
| 9220 | U+F16D3 | |
| 9221 | U+F16D4 | |
| 9222 | U+F16D5 | |
| 9223 | U+F16D6 | |
| 9224 | U+F16D7 | |
| 9225 | U+F16D8 | |
| 9226 | U+F16D9 | |
| 9227 | U+F16DA | |
| 9228 | U+F16DB | |
| 9229 | U+F16DC | |
| 9230 | U+F16DD | |
| 9231 | U+F16DE | |
| 9232 | U+F16DF | |
| 9233 | U+F16E0 | |
| 9234 | U+F16E1 | |
| 9235 | U+F16E2 | |
| 9236 | U+F16E3 | |
| 9237 | U+F16E4 | |
| 9238 | U+F16E5 | |
| 9239 | U+F16E6 | |
| 9240 | U+F16E7 | |
| 9241 | U+F16E8 | |
| 9242 | U+F16E9 | |
| 9243 | U+F16EA | |
| 9244 | U+F16EB | |
| 9245 | U+F16EC | |
| 9246 | U+F16ED | |
| 9247 | U+F16EE | |
| 9248 | U+F16EF | |
| 9249 | U+F16F0 | |
| 9250 | U+F16F1 | |
| 9251 | U+F16F2 | |
| 9252 | U+F16F3 | |
| 9253 | U+F16F4 | |
| 9254 | U+F16F5 | |
| 9255 | U+F16F6 | |
| 9256 | U+F16F7 | |
| 9257 | U+F16F8 | |
| 9258 | U+F16F9 | |
| 9259 | U+F16FA | |
| 9260 | U+F16FB | |
| 9261 | U+F16FC | |
| 9262 | U+F16FD | |
| 9263 | U+F16FE | |
| 9264 | U+F16FF | |
| 9265 | U+F1700 | |
| 9266 | U+F1701 | |
| 9267 | U+F1702 | |
| 9268 | U+F1703 | |
| 9269 | U+F1704 | |
| 9270 | U+F1705 | |
| 9271 | U+F1706 | |
| 9272 | U+F1707 | |
| 9273 | U+F1708 | |
| 9274 | U+F1709 | |
| 9275 | U+F170A | |
| 9276 | U+F170B | |
| 9277 | U+F170C | |
| 9278 | U+F170D | |
| 9279 | U+F170E | |
| 9280 | U+F170F | |
| 9281 | U+F1710 | |
| 9282 | U+F1711 | |
| 9283 | U+F1712 | |
| 9284 | U+F1713 | |
| 9285 | U+F1714 | |
| 9286 | U+F1715 | |
| 9287 | U+F1716 | |
| 9288 | U+F1717 | |
| 9289 | U+F1718 | |
| 9290 | U+F1719 | |
| 9291 | U+F171A | |
| 9292 | U+F171B | |
| 9293 | U+F171C | |
| 9294 | U+F171D | |
| 9295 | U+F171E | |
| 9296 | U+F171F | |
| 9297 | U+F1720 | |
| 9298 | U+F1721 | |
| 9299 | U+F1722 | |
| 9300 | U+F1723 | |
| 9301 | U+F1724 | |
| 9302 | U+F1725 | |
| 9303 | U+F1726 | |
| 9304 | U+F1727 | |
| 9305 | U+F1728 | |
| 9306 | U+F1729 | |
| 9307 | U+F172A | |
| 9308 | U+F172B | |
| 9309 | U+F172C | |
| 9310 | U+F172D | |
| 9311 | U+F172E | |
| 9312 | U+F172F | |
| 9313 | U+F1730 | |
| 9314 | U+F1731 | |
| 9315 | U+F1732 | |
| 9316 | U+F1733 | |
| 9317 | U+F1734 | |
| 9318 | U+F1735 | |
| 9319 | U+F1736 | |
| 9320 | U+F1737 | |
| 9321 | U+F1738 | |
| 9322 | U+F1739 | |
| 9323 | U+F173A | |
| 9324 | U+F173B | |
| 9325 | U+F173C | |
| 9326 | U+F173D | |
| 9327 | U+F173E | |
| 9328 | U+F173F | |
| 9329 | U+F1740 | |
| 9330 | U+F1741 | |
| 9331 | U+F1742 | |
| 9332 | U+F1743 | |
| 9333 | U+F1744 | |
| 9334 | U+F1745 | |
| 9335 | U+F1746 | |
| 9336 | U+F1747 | |
| 9337 | U+F1748 | |
| 9338 | U+F1749 | |
| 9339 | U+F174A | |
| 9340 | U+F174B | |
| 9341 | U+F174C | |
| 9342 | U+F174D | |
| 9343 | U+F174E | |
| 9344 | U+F174F | |
| 9345 | U+F1750 | |
| 9346 | U+F1751 | |
| 9347 | U+F1752 | |
| 9348 | U+F1753 | |
| 9349 | U+F1754 | |
| 9350 | U+F1755 | |
| 9351 | U+F1756 | |
| 9352 | U+F1757 | |
| 9353 | U+F1758 | |
| 9354 | U+F1759 | |
| 9355 | U+F175A | |
| 9356 | U+F175B | |
| 9357 | U+F175C | |
| 9358 | U+F175D | |
| 9359 | U+F175E | |
| 9360 | U+F175F | |
| 9361 | U+F1760 | |
| 9362 | U+F1761 | |
| 9363 | U+F1762 | |
| 9364 | U+F1763 | |
| 9365 | U+F1764 | |
| 9366 | U+F1765 | |
| 9367 | U+F1766 | |
| 9368 | U+F1767 | |
| 9369 | U+F1768 | |
| 9370 | U+F1769 | |
| 9371 | U+F176A | |
| 9372 | U+F176B | |
| 9373 | U+F176C | |
| 9374 | U+F176D | |
| 9375 | U+F176E | |
| 9376 | U+F176F | |
| 9377 | U+F1770 | |
| 9378 | U+F1771 | |
| 9379 | U+F1772 | |
| 9380 | U+F1773 | |
| 9381 | U+F1774 | |
| 9382 | U+F1775 | |
| 9383 | U+F1776 | |
| 9384 | U+F1777 | |
| 9385 | U+F1778 | |
| 9386 | U+F1779 | |
| 9387 | U+F177A | |
| 9388 | U+F177B | |
| 9389 | U+F177C | |
| 9390 | U+F177D | |
| 9391 | U+F177E | |
| 9392 | U+F177F | |
| 9393 | U+F1780 | |
| 9394 | U+F1781 | |
| 9395 | U+F1782 | |
| 9396 | U+F1783 | |
| 9397 | U+F1784 | |
| 9398 | U+F1785 | |
| 9399 | U+F1786 | |
| 9400 | U+F1787 | |
| 9401 | U+F1788 | |
| 9402 | U+F1789 | |
| 9403 | U+F178A | |
| 9404 | U+F178B | |
| 9405 | U+F178C | |
| 9406 | U+F178D | |
| 9407 | U+F178E | |
| 9408 | U+F178F | |
| 9409 | U+F1790 | |
| 9410 | U+F1791 | |
| 9411 | U+F1792 | |
| 9412 | U+F1793 | |
| 9413 | U+F1794 | |
| 9414 | U+F1795 | |
| 9415 | U+F1796 | |
| 9416 | U+F1797 | |
| 9417 | U+F1798 | |
| 9418 | U+F1799 | |
| 9419 | U+F179A | |
| 9420 | U+F179B | |
| 9421 | U+F179C | |
| 9422 | U+F179D | |
| 9423 | U+F179E | |
| 9424 | U+F179F | |
| 9425 | U+F17A0 | |
| 9426 | U+F17A1 | |
| 9427 | U+F17A2 | |
| 9428 | U+F17A3 | |
| 9429 | U+F17A4 | |
| 9430 | U+F17A5 | |
| 9431 | U+F17A6 | |
| 9432 | U+F17A7 | |
| 9433 | U+F17A8 | |
| 9434 | U+F17A9 | |
| 9435 | U+F17AA | |
| 9436 | U+F17AB | |
| 9437 | U+F17AC | |
| 9438 | U+F17AD | |
| 9439 | U+F17AE | |
| 9440 | U+F17AF | |
| 9441 | U+F17B0 | |
| 9442 | U+F17B1 | |
| 9443 | U+F17B2 | |
| 9444 | U+F17B3 | |
| 9445 | U+F17B4 | |
| 9446 | U+F17B5 | |
| 9447 | U+F17B6 | |
| 9448 | U+F17B7 | |
| 9449 | U+F17B8 | |
| 9450 | U+F17B9 | |
| 9451 | U+F17BA | |
| 9452 | U+F17BB | |
| 9453 | U+F17BC | |
| 9454 | U+F17BD | |
| 9455 | U+F17BE | |
| 9456 | U+F17BF | |
| 9457 | U+F17C0 | |
| 9458 | U+F17C1 | |
| 9459 | U+F17C2 | |
| 9460 | U+F17C3 | |
| 9461 | U+F17C4 | |
| 9462 | U+F17C5 | |
| 9463 | U+F17C6 | |
| 9464 | U+F17C7 | |
| 9465 | U+F17C8 | |
| 9466 | U+F17C9 | |
| 9467 | U+F17CA | |
| 9468 | U+F17CB | |
| 9469 | U+F17CC | |
| 9470 | U+F17CD | |
| 9471 | U+F17CE | |
| 9472 | U+F17CF | |
| 9473 | U+F17D0 | |
| 9474 | U+F17D1 | |
| 9475 | U+F17D2 | |
| 9476 | U+F17D3 | |
| 9477 | U+F17D4 | |
| 9478 | U+F17D5 | |
| 9479 | U+F17D6 | |
| 9480 | U+F17D7 | |
| 9481 | U+F17D8 | |
| 9482 | U+F17D9 | |
| 9483 | U+F17DA | |
| 9484 | U+F17DB | |
| 9485 | U+F17DC | |
| 9486 | U+F17DD | |
| 9487 | U+F17DE | |
| 9488 | U+F17DF | |
| 9489 | U+F17E0 | |
| 9490 | U+F17E1 | |
| 9491 | U+F17E2 | |
| 9492 | U+F17E3 | |
| 9493 | U+F17E4 | |
| 9494 | U+F17E5 | |
| 9495 | U+F17E6 | |
| 9496 | U+F17E7 | |
| 9497 | U+F17E8 | |
| 9498 | U+F17E9 | |
| 9499 | U+F17EA | |
| 9500 | U+F17EB | |
| 9501 | U+F17EC | |
| 9502 | U+F17ED | |
| 9503 | U+F17EE | |
| 9504 | U+F17EF | |
| 9505 | U+F17F0 | |
| 9506 | U+F17F1 | |
| 9507 | U+F17F2 | |
| 9508 | U+F17F3 | |
| 9509 | U+F17F4 | |
| 9510 | U+F17F5 | |
| 9511 | U+F17F6 | |
| 9512 | U+F17F7 | |
| 9513 | U+F17F8 | |
| 9514 | U+F17F9 | |
| 9515 | U+F17FA | |
| 9516 | U+F17FB | |
| 9517 | U+F17FC | |
| 9518 | U+F17FD | |
| 9519 | U+F17FE | |
| 9520 | U+F17FF | |
| 9521 | U+F1800 | |
| 9522 | U+F1801 | |
| 9523 | U+F1802 | |
| 9524 | U+F1803 | |
| 9525 | U+F1804 | |
| 9526 | U+F1805 | |
| 9527 | U+F1806 | |
| 9528 | U+F1807 | |
| 9529 | U+F1808 | |
| 9530 | U+F1809 | |
| 9531 | U+F180A | |
| 9532 | U+F180B | |
| 9533 | U+F180C | |
| 9534 | U+F180D | |
| 9535 | U+F180E | |
| 9536 | U+F180F | |
| 9537 | U+F1810 | |
| 9538 | U+F1811 | |
| 9539 | U+F1812 | |
| 9540 | U+F1813 | |
| 9541 | U+F1814 | |
| 9542 | U+F1815 | |
| 9543 | U+F1816 | |
| 9544 | U+F1817 | |
| 9545 | U+F1818 | |
| 9546 | U+F1819 | |
| 9547 | U+F181A | |
| 9548 | U+F181B | |
| 9549 | U+F181C | |
| 9550 | U+F181D | |
| 9551 | U+F181E | |
| 9552 | U+F181F | |
| 9553 | U+F1820 | |
| 9554 | U+F1821 | |
| 9555 | U+F1822 | |
| 9556 | U+F1823 | |
| 9557 | U+F1824 | |
| 9558 | U+F1825 | |
| 9559 | U+F1826 | |
| 9560 | U+F1827 | |
| 9561 | U+F1828 | |
| 9562 | U+F1829 | |
| 9563 | U+F182A | |
| 9564 | U+F182B | |
| 9565 | U+F182C | |
| 9566 | U+F182D | |
| 9567 | U+F182E | |
| 9568 | U+F182F | |
| 9569 | U+F1830 | |
| 9570 | U+F1831 | |
| 9571 | U+F1832 | |
| 9572 | U+F1833 | |
| 9573 | U+F1834 | |
| 9574 | U+F1835 | |
| 9575 | U+F1836 | |
| 9576 | U+F1837 | |
| 9577 | U+F1838 | |
| 9578 | U+F1839 | |
| 9579 | U+F183A | |
| 9580 | U+F183B | |
| 9581 | U+F183C | |
| 9582 | U+F183D | |
| 9583 | U+F183E | |
| 9584 | U+F183F | |
| 9585 | U+F1840 | |
| 9586 | U+F1841 | |
| 9587 | U+F1842 | |
| 9588 | U+F1843 | |
| 9589 | U+F1844 | |
| 9590 | U+F1845 | |
| 9591 | U+F1846 | |
| 9592 | U+F1847 | |
| 9593 | U+F1848 | |
| 9594 | U+F1849 | |
| 9595 | U+F184A | |
| 9596 | U+F184B | |
| 9597 | U+F184C | |
| 9598 | U+F184D | |
| 9599 | U+F184E | |
| 9600 | U+F184F | |
| 9601 | U+F1850 | |
| 9602 | U+F1851 | |
| 9603 | U+F1852 | |
| 9604 | U+F1853 | |
| 9605 | U+F1854 | |
| 9606 | U+F1855 | |
| 9607 | U+F1856 | |
| 9608 | U+F1857 | |
| 9609 | U+F1858 | |
| 9610 | U+F1859 | |
| 9611 | U+F185A | |
| 9612 | U+F185B | |
| 9613 | U+F185C | |
| 9614 | U+F185D | |
| 9615 | U+F185E | |
| 9616 | U+F185F | |
| 9617 | U+F1860 | |
| 9618 | U+F1861 | |
| 9619 | U+F1862 | |
| 9620 | U+F1863 | |
| 9621 | U+F1864 | |
| 9622 | U+F1865 | |
| 9623 | U+F1866 | |
| 9624 | U+F1867 | |
| 9625 | U+F1868 | |
| 9626 | U+F1869 | |
| 9627 | U+F186A | |
| 9628 | U+F186B | |
| 9629 | U+F186C | |
| 9630 | U+F186D | |
| 9631 | U+F186E | |
| 9632 | U+F186F | |
| 9633 | U+F1870 | |
| 9634 | U+F1871 | |
| 9635 | U+F1872 | |
| 9636 | U+F1873 | |
| 9637 | U+F1874 | |
| 9638 | U+F1875 | |
| 9639 | U+F1876 | |
| 9640 | U+F1877 | |
| 9641 | U+F1878 | |
| 9642 | U+F1879 | |
| 9643 | U+F187A | |
| 9644 | U+F187B | |
| 9645 | U+F187C | |
| 9646 | U+F187D | |
| 9647 | U+F187E | |
| 9648 | U+F187F | |
| 9649 | U+F1880 | |
| 9650 | U+F1881 | |
| 9651 | U+F1882 | |
| 9652 | U+F1883 | |
| 9653 | U+F1884 | |
| 9654 | U+F1885 | |
| 9655 | U+F1886 | |
| 9656 | U+F1887 | |
| 9657 | U+F1888 | |
| 9658 | U+F1889 | |
| 9659 | U+F188A | |
| 9660 | U+F188B | |
| 9661 | U+F188C | |
| 9662 | U+F188D | |
| 9663 | U+F188E | |
| 9664 | U+F188F | |
| 9665 | U+F1890 | |
| 9666 | U+F1891 | |
| 9667 | U+F1892 | |
| 9668 | U+F1893 | |
| 9669 | U+F1894 | |
| 9670 | U+F1895 | |
| 9671 | U+F1896 | |
| 9672 | U+F1897 | |
| 9673 | U+F1898 | |
| 9674 | U+F1899 | |
| 9675 | U+F189A | |
| 9676 | U+F189B | |
| 9677 | U+F189C | |
| 9678 | U+F189D | |
| 9679 | U+F189E | |
| 9680 | U+F189F | |
| 9681 | U+F18A0 | |
| 9682 | U+F18A1 | |
| 9683 | U+F18A2 | |
| 9684 | U+F18A3 | |
| 9685 | U+F18A4 | |
| 9686 | U+F18A5 | |
| 9687 | U+F18A6 | |
| 9688 | U+F18A7 | |
| 9689 | U+F18A8 | |
| 9690 | U+F18A9 | |
| 9691 | U+F18AA | |
| 9692 | U+F18AB | |
| 9693 | U+F18AC | |
| 9694 | U+F18AD | |
| 9695 | U+F18AE | |
| 9696 | U+F18AF | |
| 9697 | U+F18B0 | |
| 9698 | U+F18B1 | |
| 9699 | U+F18B2 | |
| 9700 | U+F18B3 | |
| 9701 | U+F18B4 | |
| 9702 | U+F18B5 | |
| 9703 | U+F18B6 | |
| 9704 | U+F18B7 | |
| 9705 | U+F18B8 | |
| 9706 | U+F18B9 | |
| 9707 | U+F18BA | |
| 9708 | U+F18BB | |
| 9709 | U+F18BC | |
| 9710 | U+F18BD | |
| 9711 | U+F18BE | |
| 9712 | U+F18BF | |
| 9713 | U+F18C0 | |
| 9714 | U+F18C1 | |
| 9715 | U+F18C2 | |
| 9716 | U+F18C3 | |
| 9717 | U+F18C4 | |
| 9718 | U+F18C5 | |
| 9719 | U+F18C6 | |
| 9720 | U+F18C7 | |
| 9721 | U+F18C8 | |
| 9722 | U+F18C9 | |
| 9723 | U+F18CA | |
| 9724 | U+F18CB | |
| 9725 | U+F18CC | |
| 9726 | U+F18CD | |
| 9727 | U+F18CE | |
| 9728 | U+F18CF | |
| 9729 | U+F18D0 | |
| 9730 | U+F18D1 | |
| 9731 | U+F18D2 | |
| 9732 | U+F18D3 | |
| 9733 | U+F18D4 | |
| 9734 | U+F18D5 | |
| 9735 | U+F18D6 | |
| 9736 | U+F18D7 | |
| 9737 | U+F18D8 | |
| 9738 | U+F18D9 | |
| 9739 | U+F18DA | |
| 9740 | U+F18DB | |
| 9741 | U+F18DC | |
| 9742 | U+F18DD | |
| 9743 | U+F18DE | |
| 9744 | U+F18DF | |
| 9745 | U+F18E0 | |
| 9746 | U+F18E1 | |
| 9747 | U+F18E2 | |
| 9748 | U+F18E3 | |
| 9749 | U+F18E4 | |
| 9750 | U+F18E5 | |
| 9751 | U+F18E6 | |
| 9752 | U+F18E7 | |
| 9753 | U+F18E8 | |
| 9754 | U+F18E9 | |
| 9755 | U+F18EA | |
| 9756 | U+F18EB | |
| 9757 | U+F18EC | |
| 9758 | U+F18ED | |
| 9759 | U+F18EE | |
| 9760 | U+F18EF | |
| 9761 | U+F18F0 | |
| 9762 | U+F18F1 | |
| 9763 | U+F18F2 | |
| 9764 | U+F18F3 | |
| 9765 | U+F18F4 | |
| 9766 | U+F18F5 | |
| 9767 | U+F18F6 | |
| 9768 | U+F18F7 | |
| 9769 | U+F18F8 | |
| 9770 | U+F18F9 | |
| 9771 | U+F18FA | |
| 9772 | U+F18FB | |
| 9773 | U+F18FC | |
| 9774 | U+F18FD | |
| 9775 | U+F18FE | |
| 9776 | U+F18FF | |
| 9777 | U+F1900 | |
| 9778 | U+F1901 | |
| 9779 | U+F1902 | |
| 9780 | U+F1903 | |
| 9781 | U+F1904 | |
| 9782 | U+F1905 | |
| 9783 | U+F1906 | |
| 9784 | U+F1907 | |
| 9785 | U+F1908 | |
| 9786 | U+F1909 | |
| 9787 | U+F190A | |
| 9788 | U+F190B | |
| 9789 | U+F190C | |
| 9790 | U+F190D | |
| 9791 | U+F190E | |
| 9792 | U+F190F | |
| 9793 | U+F1910 | |
| 9794 | U+F1911 | |
| 9795 | U+F1912 | |
| 9796 | U+F1913 | |
| 9797 | U+F1914 | |
| 9798 | U+F1915 | |
| 9799 | U+F1916 | |
| 9800 | U+F1917 | |
| 9801 | U+F1918 | |
| 9802 | U+F1919 | |
| 9803 | U+F191A | |
| 9804 | U+F191B | |
| 9805 | U+F191C | |
| 9806 | U+F191D | |
| 9807 | U+F191E | |
| 9808 | U+F191F | |
| 9809 | U+F1920 | |
| 9810 | U+F1921 | |
| 9811 | U+F1922 | |
| 9812 | U+F1923 | |
| 9813 | U+F1924 | |
| 9814 | U+F1925 | |
| 9815 | U+F1926 | |
| 9816 | U+F1927 | |
| 9817 | U+F1928 | |
| 9818 | U+F1929 | |
| 9819 | U+F192A | |
| 9820 | U+F192B | |
| 9821 | U+F192C | |
| 9822 | U+F192D | |
| 9823 | U+F192E | |
| 9824 | U+F192F | |
| 9825 | U+F1930 | |
| 9826 | U+F1931 | |
| 9827 | U+F1932 | |
| 9828 | U+F1933 | |
| 9829 | U+F1934 | |
| 9830 | U+F1935 | |
| 9831 | U+F1936 | |
| 9832 | U+F1937 | |
| 9833 | U+F1938 | |
| 9834 | U+F1939 | |
| 9835 | U+F193A | |
| 9836 | U+F193B | |
| 9837 | U+F193C | |
| 9838 | U+F193D | |
| 9839 | U+F193E | |
| 9840 | U+F193F | |
| 9841 | U+F1940 | |
| 9842 | U+F1941 | |
| 9843 | U+F1942 | |
| 9844 | U+F1943 | |
| 9845 | U+F1944 | |
| 9846 | U+F1945 | |
| 9847 | U+F1946 | |
| 9848 | U+F1947 | |
| 9849 | U+F1948 | |
| 9850 | U+F1949 | |
| 9851 | U+F194A | |
| 9852 | U+F194B | |
| 9853 | U+F194C | |
| 9854 | U+F194D | |
| 9855 | U+F194E | |
| 9856 | U+F194F | |
| 9857 | U+F1950 | |
| 9858 | U+F1951 | |
| 9859 | U+F1952 | |
| 9860 | U+F1953 | |
| 9861 | U+F1954 | |
| 9862 | U+F1955 | |
| 9863 | U+F1956 | |
| 9864 | U+F1957 | |
| 9865 | U+F1958 | |
| 9866 | U+F1959 | |
| 9867 | U+F195A | |
| 9868 | U+F195B | |
| 9869 | U+F195C | |
| 9870 | U+F195D | |
| 9871 | U+F195E | |
| 9872 | U+F195F | |
| 9873 | U+F1960 | |
| 9874 | U+F1961 | |
| 9875 | U+F1962 | |
| 9876 | U+F1963 | |
| 9877 | U+F1964 | |
| 9878 | U+F1965 | |
| 9879 | U+F1966 | |
| 9880 | U+F1967 | |
| 9881 | U+F1968 | |
| 9882 | U+F1969 | |
| 9883 | U+F196A | |
| 9884 | U+F196B | |
| 9885 | U+F196C | |
| 9886 | U+F196D | |
| 9887 | U+F196E | |
| 9888 | U+F196F | |
| 9889 | U+F1970 | |
| 9890 | U+F1971 | |
| 9891 | U+F1972 | |
| 9892 | U+F1973 | |
| 9893 | U+F1974 | |
| 9894 | U+F1975 | |
| 9895 | U+F1976 | |
| 9896 | U+F1977 | |
| 9897 | U+F1978 | |
| 9898 | U+F1979 | |
| 9899 | U+F197A | |
| 9900 | U+F197B | |
| 9901 | U+F197C | |
| 9902 | U+F197D | |
| 9903 | U+F197E | |
| 9904 | U+F197F | |
| 9905 | U+F1980 | |
| 9906 | U+F1981 | |
| 9907 | U+F1982 | |
| 9908 | U+F1983 | |
| 9909 | U+F1984 | |
| 9910 | U+F1985 | |
| 9911 | U+F1986 | |
| 9912 | U+F1987 | |
| 9913 | U+F1988 | |
| 9914 | U+F1989 | |
| 9915 | U+F198A | |
| 9916 | U+F198B | |
| 9917 | U+F198C | |
| 9918 | U+F198D | |
| 9919 | U+F198E | |
| 9920 | U+F198F | |
| 9921 | U+F1990 | |
| 9922 | U+F1991 | |
| 9923 | U+F1992 | |
| 9924 | U+F1993 | |
| 9925 | U+F1994 | |
| 9926 | U+F1995 | |
| 9927 | U+F1996 | |
| 9928 | U+F1997 | |
| 9929 | U+F1998 | |
| 9930 | U+F1999 | |
| 9931 | U+F199A | |
| 9932 | U+F199B | |
| 9933 | U+F199C | |
| 9934 | U+F199D | |
| 9935 | U+F199E | |
| 9936 | U+F199F | |
| 9937 | U+F19A0 | |
| 9938 | U+F19A1 | |
| 9939 | U+F19A2 | |
| 9940 | U+F19A3 | |
| 9941 | U+F19A4 | |
| 9942 | U+F19A5 | |
| 9943 | U+F19A6 | |
| 9944 | U+F19A7 | |
| 9945 | U+F19A8 | |
| 9946 | U+F19A9 | |
| 9947 | U+F19AA | |
| 9948 | U+F19AB | |
| 9949 | U+F19AC | |
| 9950 | U+F19AD | |
| 9951 | U+F19AE | |
| 9952 | U+F19AF | |
| 9953 | U+F19B0 | |
| 9954 | U+F19B1 | |
| 9955 | U+F19B2 | |
| 9956 | U+F19B3 | |
| 9957 | U+F19B4 | |
| 9958 | U+F19B5 | |
| 9959 | U+F19B6 | |
| 9960 | U+F19B7 | |
| 9961 | U+F19B8 | |
| 9962 | U+F19B9 | |
| 9963 | U+F19BA | |
| 9964 | U+F19BB | |
| 9965 | U+F19BC | |
| 9966 | U+F19BD | |
| 9967 | U+F19BE | |
| 9968 | U+F19BF | |
| 9969 | U+F19C0 | |
| 9970 | U+F19C1 | |
| 9971 | U+F19C2 | |
| 9972 | U+F19C3 | |
| 9973 | U+F19C4 | |
| 9974 | U+F19C5 | |
| 9975 | U+F19C6 | |
| 9976 | U+F19C7 | |
| 9977 | U+F19C8 | |
| 9978 | U+F19C9 | |
| 9979 | U+F19CA | |
| 9980 | U+F19CB | |
| 9981 | U+F19CC | |
| 9982 | U+F19CD | |
| 9983 | U+F19CE | |
| 9984 | U+F19CF | |
| 9985 | U+F19D0 | |
| 9986 | U+F19D1 | |
| 9987 | U+F19D2 | |
| 9988 | U+F19D3 | |
| 9989 | U+F19D4 | |
| 9990 | U+F19D5 | |
| 9991 | U+F19D6 | |
| 9992 | U+F19D7 | |
| 9993 | U+F19D8 | |
| 9994 | U+F19D9 | |
| 9995 | U+F19DA | |
| 9996 | U+F19DB | |
| 9997 | U+F19DC | |
| 9998 | U+F19DD | |
| 9999 | U+F19DE | |
| 10000 | U+F19DF | |
| 10001 | U+F19E0 | |
| 10002 | U+F19E1 | |
| 10003 | U+F19E2 | |
| 10004 | U+F19E3 | |
| 10005 | U+F19E4 | |
| 10006 | U+F19E5 | |
| 10007 | U+F19E6 | |
| 10008 | U+F19E7 | |
| 10009 | U+F19E8 | |
| 10010 | U+F19E9 | |
| 10011 | U+F19EA | |
| 10012 | U+F19EB | |
| 10013 | U+F19EC | |
| 10014 | U+F19ED | |
| 10015 | U+F19EE | |
| 10016 | U+F19EF | |
| 10017 | U+F19F0 | |
| 10018 | U+F19F1 | |
| 10019 | U+F19F2 | |
| 10020 | U+F19F3 | |
| 10021 | U+F19F4 | |
| 10022 | U+F19F5 | |
| 10023 | U+F19F6 | |
| 10024 | U+F19F7 | |
| 10025 | U+F19F8 | |
| 10026 | U+F19F9 | |
| 10027 | U+F19FA | |
| 10028 | U+F19FB | |
| 10029 | U+F19FC | |
| 10030 | U+F19FD | |
| 10031 | U+F19FE | |
| 10032 | U+F19FF | |
| 10033 | U+F1A00 | |
| 10034 | U+F1A01 | |
| 10035 | U+F1A02 | |
| 10036 | U+F1A03 | |
| 10037 | U+F1A04 | |
| 10038 | U+F1A05 | |
| 10039 | U+F1A06 | |
| 10040 | U+F1A07 | |
| 10041 | U+F1A08 | |
| 10042 | U+F1A09 | |
| 10043 | U+F1A0A | |
| 10044 | U+F1A0B | |
| 10045 | U+F1A0C | |
| 10046 | U+F1A0D | |
| 10047 | U+F1A0E | |
| 10048 | U+F1A0F | |
| 10049 | U+F1A10 | |
| 10050 | U+F1A11 | |
| 10051 | U+F1A12 | |
| 10052 | U+F1A13 | |
| 10053 | U+F1A14 | |
| 10054 | U+F1A15 | |
| 10055 | U+F1A16 | |
| 10056 | U+F1A17 | |
| 10057 | U+F1A18 | |
| 10058 | U+F1A19 | |
| 10059 | U+F1A1A | |
| 10060 | U+F1A1B | |
| 10061 | U+F1A1C | |
| 10062 | U+F1A1D | |
| 10063 | U+F1A1E | |
| 10064 | U+F1A1F | |
| 10065 | U+F1A20 | |
| 10066 | U+F1A21 | |
| 10067 | U+F1A22 | |
| 10068 | U+F1A23 | |
| 10069 | U+F1A24 | |
| 10070 | U+F1A25 | |
| 10071 | U+F1A26 | |
| 10072 | U+F1A27 | |
| 10073 | U+F1A28 | |
| 10074 | U+F1A29 | |
| 10075 | U+F1A2A | |
| 10076 | U+F1A2B | |
| 10077 | U+F1A2C | |
| 10078 | U+F1A2D | |
| 10079 | U+F1A2E | |
| 10080 | U+F1A2F | |
| 10081 | U+F1A30 | |
| 10082 | U+F1A31 | |
| 10083 | U+F1A32 | |
| 10084 | U+F1A33 | |
| 10085 | U+F1A34 | |
| 10086 | U+F1A35 | |
| 10087 | U+F1A36 | |
| 10088 | U+F1A37 | |
| 10089 | U+F1A38 | |
| 10090 | U+F1A39 | |
| 10091 | U+F1A3A | |
| 10092 | U+F1A3B | |
| 10093 | U+F1A3C | |
| 10094 | U+F1A3D | |
| 10095 | U+F1A3E | |
| 10096 | U+F1A3F | |
| 10097 | U+F1A40 | |
| 10098 | U+F1A41 | |
| 10099 | U+F1A42 | |
| 10100 | U+F1A43 | |
| 10101 | U+F1A44 | |
| 10102 | U+F1A45 | |
| 10103 | U+F1A46 | |
| 10104 | U+F1A47 | |
| 10105 | U+F1A48 | |
| 10106 | U+F1A49 | |
| 10107 | U+F1A4A | |
| 10108 | U+F1A4B | |
| 10109 | U+F1A4C | |
| 10110 | U+F1A4D | |
| 10111 | U+F1A4E | |
| 10112 | U+F1A4F | |
| 10113 | U+F1A50 | |
| 10114 | U+F1A51 | |
| 10115 | U+F1A52 | |
| 10116 | U+F1A53 | |
| 10117 | U+F1A54 | |
| 10118 | U+F1A55 | |
| 10119 | U+F1A56 | |
| 10120 | U+F1A57 | |
| 10121 | U+F1A58 | |
| 10122 | U+F1A59 | |
| 10123 | U+F1A5A | |
| 10124 | U+F1A5B | |
| 10125 | U+F1A5C | |
| 10126 | U+F1A5D | |
| 10127 | U+F1A5E | |
| 10128 | U+F1A5F | |
| 10129 | U+F1A60 | |
| 10130 | U+F1A61 | |
| 10131 | U+F1A62 | |
| 10132 | U+F1A63 | |
| 10133 | U+F1A64 | |
| 10134 | U+F1A65 | |
| 10135 | U+F1A66 | |
| 10136 | U+F1A67 | |
| 10137 | U+F1A68 | |
| 10138 | U+F1A69 | |
| 10139 | U+F1A6A | |
| 10140 | U+F1A6B | |
| 10141 | U+F1A6C | |
| 10142 | U+F1A6D | |
| 10143 | U+F1A6E | |
| 10144 | U+F1A6F | |
| 10145 | U+F1A70 | |
| 10146 | U+F1A71 | |
| 10147 | U+F1A72 | |
| 10148 | U+F1A73 | |
| 10149 | U+F1A74 | |
| 10150 | U+F1A75 | |
| 10151 | U+F1A76 | |
| 10152 | U+F1A77 | |
| 10153 | U+F1A78 | |
| 10154 | U+F1A79 | |
| 10155 | U+F1A7A | |
| 10156 | U+F1A7B | |
| 10157 | U+F1A7C | |
| 10158 | U+F1A7D | |
| 10159 | U+F1A7E | |
| 10160 | U+F1A7F | |
| 10161 | U+F1A80 | |
| 10162 | U+F1A81 | |
| 10163 | U+F1A82 | |
| 10164 | U+F1A83 | |
| 10165 | U+F1A84 | |
| 10166 | U+F1A85 | |
| 10167 | U+F1A86 | |
| 10168 | U+F1A87 | |
| 10169 | U+F1A88 | |
| 10170 | U+F1A89 | |
| 10171 | U+F1A8A | |
| 10172 | U+F1A8B | |
| 10173 | U+F1A8C | |
| 10174 | U+F1A8D | |
| 10175 | U+F1A8E | |
| 10176 | U+F1A8F | |
| 10177 | U+F1A90 | |
| 10178 | U+F1A91 | |
| 10179 | U+F1A92 | |
| 10180 | U+F1A93 | |
| 10181 | U+F1A94 | |
| 10182 | U+F1A95 | |
| 10183 | U+F1A96 | |
| 10184 | U+F1A97 | |
| 10185 | U+F1A98 | |
| 10186 | U+F1A99 | |
| 10187 | U+F1A9A | |
| 10188 | U+F1A9B | |
| 10189 | U+F1A9C | |
| 10190 | U+F1A9D | |
| 10191 | U+F1A9E | |
| 10192 | U+F1A9F | |
| 10193 | U+F1AA0 | |
| 10194 | U+F1AA1 | |
| 10195 | U+F1AA2 | |
| 10196 | U+F1AA3 | |
| 10197 | U+F1AA4 | |
| 10198 | U+F1AA5 | |
| 10199 | U+F1AA6 | |
| 10200 | U+F1AA7 | |
| 10201 | U+F1AA8 | |
| 10202 | U+F1AA9 | |
| 10203 | U+F1AAA | |
| 10204 | U+F1AAB | |
| 10205 | U+F1AAC | |
| 10206 | U+F1AAD | |
| 10207 | U+F1AAE | |
| 10208 | U+F1AAF | |
| 10209 | U+F1AB0 | |
| 10210 | U+F1AB1 | |
| 10211 | U+F1AB2 | |
| 10212 | U+F1AB3 | |
| 10213 | U+F1AB4 | |
| 10214 | U+F1AB5 | |
| 10215 | U+F1AB6 | |
| 10216 | U+F1AB7 | |
| 10217 | U+F1AB8 | |
| 10218 | U+F1AB9 | |
| 10219 | U+F1ABA | |
| 10220 | U+F1ABB | |
| 10221 | U+F1ABC | |
| 10222 | U+F1ABD | |
| 10223 | U+F1ABE | |
| 10224 | U+F1ABF | |
| 10225 | U+F1AC0 | |
| 10226 | U+F1AC1 | |
| 10227 | U+F1AC2 | |
| 10228 | U+F1AC3 | |
| 10229 | U+F1AC4 | |
| 10230 | U+F1AC5 | |
| 10231 | U+F1AC6 | |
| 10232 | U+F1AC7 | |
| 10233 | U+F1AC8 | |
| 10234 | U+F1AC9 | |
| 10235 | U+F1ACA | |
| 10236 | U+F1ACB | |
| 10237 | U+F1ACC | |
| 10238 | U+F1ACD | |
| 10239 | U+F1ACE | |
| 10240 | U+F1ACF | |
| 10241 | U+F1AD0 | |
| 10242 | U+F1AD1 | |
| 10243 | U+F1AD2 | |
| 10244 | U+F1AD3 | |
| 10245 | U+F1AD4 | |
| 10246 | U+F1AD5 | |
| 10247 | U+F1AD6 | |
| 10248 | U+F1AD7 | |
| 10249 | U+F1AD8 | |
| 10250 | U+F1AD9 | |
| 10251 | U+F1ADA | |
| 10252 | U+F1ADB | |
| 10253 | U+F1ADC | |
| 10254 | U+F1ADD | |
| 10255 | U+F1ADE | |
| 10256 | U+F1ADF | |
| 10257 | U+F1AE0 | |
| 10258 | U+F1AE1 | |
| 10259 | U+F1AE2 | |
| 10260 | U+F1AE3 | |
| 10261 | U+F1AE4 | |
| 10262 | U+F1AE5 | |
| 10263 | U+F1AE6 | |
| 10264 | U+F1AE7 | |
| 10265 | U+F1AE8 | |
| 10266 | U+F1AE9 | |
| 10267 | U+F1AEA | |
| 10268 | U+F1AEB | |
| 10269 | U+F1AEC | |
| 10270 | U+F1AED | |
| 10271 | U+F1AEE | |
| 10272 | U+F1AEF | |
| 10273 | U+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 @@ |
| 1 | Attribution 4.0 International | |
| 2 | ||
| 3 | ======================================================================= | |
| 4 | ||
| 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and | |
| 6 | does not provide legal services or legal advice. Distribution of | |
| 7 | Creative Commons public licenses does not create a lawyer-client or | |
| 8 | other relationship. Creative Commons makes its licenses and related | |
| 9 | information available on an "as-is" basis. Creative Commons gives no | |
| 10 | warranties regarding its licenses, any material licensed under their | |
| 11 | terms and conditions, or any related information. Creative Commons | |
| 12 | disclaims all liability for damages resulting from their use to the | |
| 13 | fullest extent possible. | |
| 14 | ||
| 15 | Using Creative Commons Public Licenses | |
| 16 | ||
| 17 | Creative Commons public licenses provide a standard set of terms and | |
| 18 | conditions that creators and other rights holders may use to share | |
| 19 | original works of authorship and other material subject to copyright | |
| 20 | and certain other rights specified in the public license below. The | |
| 21 | following considerations are for informational purposes only, are not | |
| 22 | exhaustive, 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 | ||
| 57 | Creative Commons Attribution 4.0 International Public License | |
| 58 | ||
| 59 | By exercising the Licensed Rights (defined below), You accept and agree | |
| 60 | to be bound by the terms and conditions of this Creative Commons | |
| 61 | Attribution 4.0 International Public License ("Public License"). To the | |
| 62 | extent this Public License may be interpreted as a contract, You are | |
| 63 | granted the Licensed Rights in consideration of Your acceptance of | |
| 64 | these terms and conditions, and the Licensor grants You such rights in | |
| 65 | consideration of benefits the Licensor receives from making the | |
| 66 | Licensed Material available under these terms and conditions. | |
| 67 | ||
| 68 | ||
| 69 | Section 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 | ||
| 133 | Section 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 | ||
| 210 | Section 3 -- License Conditions. | |
| 211 | ||
| 212 | Your exercise of the Licensed Rights is expressly made subject to the | |
| 213 | following 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 | ||
| 262 | Section 4 -- Sui Generis Database Rights. | |
| 263 | ||
| 264 | Where the Licensed Rights include Sui Generis Database Rights that | |
| 265 | apply 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 | ||
| 279 | For the avoidance of doubt, this Section 4 supplements and does not | |
| 280 | replace Your obligations under this Public License where the Licensed | |
| 281 | Rights include other Copyright and Similar Rights. | |
| 282 | ||
| 283 | ||
| 284 | Section 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 | ||
| 313 | Section 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 | ||
| 342 | Section 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 | ||
| 352 | Section 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 | ||
| 378 | Creative Commons is not a party to its public | |
| 379 | licenses. Notwithstanding, Creative Commons may elect to apply one of | |
| 380 | its public licenses to material it publishes and in those instances | |
| 381 | will be considered the “Licensor.” The text of the Creative Commons | |
| 382 | public licenses is dedicated to the public domain under the CC0 Public | |
| 383 | Domain Dedication. Except for the limited purpose of indicating that | |
| 384 | material is shared under a Creative Commons public license or as | |
| 385 | otherwise permitted by the Creative Commons policies published at | |
| 386 | creativecommons.org/policies, Creative Commons does not authorize the | |
| 387 | use of the trademark "Creative Commons" or any other trademark or logo | |
| 388 | of Creative Commons without its prior written consent including, | |
| 389 | without limitation, in connection with any unauthorized modifications | |
| 390 | to any of its public licenses or any other arrangements, | |
| 391 | understandings, or agreements concerning use of licensed material. For | |
| 392 | the avoidance of doubt, this paragraph does not form part of the | |
| 393 | public licenses. | |
| 394 | ||
| 395 | Creative 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 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2015 konpa | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of | |
| 6 | this software and associated documentation files (the "Software"), to deal in | |
| 7 | the Software without restriction, including without limitation the rights to | |
| 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | |
| 9 | the Software, and to permit persons to whom the Software is furnished to do so, | |
| 10 | subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | |
| 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | |
| 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | |
| 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |
| 20 | CONNECTION 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 @@ |
| 1 | MIT License | |
| 2 | ||
| 3 | Copyright (c) 2017 André Luiz Gava | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. |
packages/terminal/fonts/licenses/Font-Awesome-LICENSE.txt created+165| ... | ... | @@ -0,0 +1,165 @@ |
| 1 | Fonticons, Inc. (https://fontawesome.com) | |
| 2 | ||
| 3 | -------------------------------------------------------------------------------- | |
| 4 | ||
| 5 | Font Awesome Free License | |
| 6 | ||
| 7 | Font Awesome Free is free, open source, and GPL friendly. You can use it for | |
| 8 | commercial projects, open source projects, or really almost whatever you want. | |
| 9 | Full 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 | ||
| 15 | The Font Awesome Free download is licensed under a Creative Commons | |
| 16 | Attribution 4.0 International License and applies to all icons packaged | |
| 17 | as SVG and JS file types. | |
| 18 | ||
| 19 | -------------------------------------------------------------------------------- | |
| 20 | ||
| 21 | # Fonts: SIL OFL 1.1 License | |
| 22 | ||
| 23 | In the Font Awesome Free download, the SIL OFL license applies to all icons | |
| 24 | packaged as web and desktop font files. | |
| 25 | ||
| 26 | Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com) | |
| 27 | with Reserved Font Name: "Font Awesome". | |
| 28 | ||
| 29 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 30 | This license is copied below, and is also available with a FAQ at: | |
| 31 | http://scripts.sil.org/OFL | |
| 32 | ||
| 33 | SIL OPEN FONT LICENSE | |
| 34 | Version 1.1 - 26 February 2007 | |
| 35 | ||
| 36 | PREAMBLE | |
| 37 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 38 | development of collaborative font projects, to support the font creation | |
| 39 | efforts of academic and linguistic communities, and to provide a free and | |
| 40 | open framework in which fonts may be shared and improved in partnership | |
| 41 | with others. | |
| 42 | ||
| 43 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 44 | redistributed freely as long as they are not sold by themselves. The | |
| 45 | fonts, including any derivative works, can be bundled, embedded, | |
| 46 | redistributed and/or sold with any software provided that any reserved | |
| 47 | names are not used by derivative works. The fonts and derivatives, | |
| 48 | however, cannot be released under any other type of license. The | |
| 49 | requirement for fonts to remain under this license does not apply | |
| 50 | to any document created using the fonts or their derivatives. | |
| 51 | ||
| 52 | DEFINITIONS | |
| 53 | "Font Software" refers to the set of files released by the Copyright | |
| 54 | Holder(s) under this license and clearly marked as such. This may | |
| 55 | include source files, build scripts and documentation. | |
| 56 | ||
| 57 | "Reserved Font Name" refers to any names specified as such after the | |
| 58 | copyright statement(s). | |
| 59 | ||
| 60 | "Original Version" refers to the collection of Font Software components as | |
| 61 | distributed by the Copyright Holder(s). | |
| 62 | ||
| 63 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 64 | or substituting — in part or in whole — any of the components of the | |
| 65 | Original Version, by changing formats or by porting the Font Software to a | |
| 66 | new environment. | |
| 67 | ||
| 68 | "Author" refers to any designer, engineer, programmer, technical | |
| 69 | writer or other person who contributed to the Font Software. | |
| 70 | ||
| 71 | PERMISSION & CONDITIONS | |
| 72 | Permission is hereby granted, free of charge, to any person obtaining | |
| 73 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 74 | redistribute, and sell modified and unmodified copies of the Font | |
| 75 | Software, subject to the following conditions: | |
| 76 | ||
| 77 | 1) Neither the Font Software nor any of its individual components, | |
| 78 | in Original or Modified Versions, may be sold by itself. | |
| 79 | ||
| 80 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 81 | redistributed and/or sold with any software, provided that each copy | |
| 82 | contains the above copyright notice and this license. These can be | |
| 83 | included either as stand-alone text files, human-readable headers or | |
| 84 | in the appropriate machine-readable metadata fields within text or | |
| 85 | binary files as long as those fields can be easily viewed by the user. | |
| 86 | ||
| 87 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 88 | Name(s) unless explicit written permission is granted by the corresponding | |
| 89 | Copyright Holder. This restriction only applies to the primary font name as | |
| 90 | presented to the users. | |
| 91 | ||
| 92 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 93 | Software shall not be used to promote, endorse or advertise any | |
| 94 | Modified Version, except to acknowledge the contribution(s) of the | |
| 95 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 96 | permission. | |
| 97 | ||
| 98 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 99 | must be distributed entirely under this license, and must not be | |
| 100 | distributed under any other license. The requirement for fonts to | |
| 101 | remain under this license does not apply to any document created | |
| 102 | using the Font Software. | |
| 103 | ||
| 104 | TERMINATION | |
| 105 | This license becomes null and void if any of the above conditions are | |
| 106 | not met. | |
| 107 | ||
| 108 | DISCLAIMER | |
| 109 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 110 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 111 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 112 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 113 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 114 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 115 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 116 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 117 | OTHER DEALINGS IN THE FONT SOFTWARE. | |
| 118 | ||
| 119 | -------------------------------------------------------------------------------- | |
| 120 | ||
| 121 | # Code: MIT License (https://opensource.org/licenses/MIT) | |
| 122 | ||
| 123 | In the Font Awesome Free download, the MIT license applies to all non-font and | |
| 124 | non-icon files. | |
| 125 | ||
| 126 | Copyright 2022 Fonticons, Inc. | |
| 127 | ||
| 128 | Permission is hereby granted, free of charge, to any person obtaining a copy of | |
| 129 | this software and associated documentation files (the "Software"), to deal in the | |
| 130 | Software without restriction, including without limitation the rights to use, copy, | |
| 131 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, | |
| 132 | and to permit persons to whom the Software is furnished to do so, subject to the | |
| 133 | following conditions: | |
| 134 | ||
| 135 | The above copyright notice and this permission notice shall be included in all | |
| 136 | copies or substantial portions of the Software. | |
| 137 | ||
| 138 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
| 139 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A | |
| 140 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
| 141 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
| 142 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
| 143 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| 144 | ||
| 145 | -------------------------------------------------------------------------------- | |
| 146 | ||
| 147 | # Attribution | |
| 148 | ||
| 149 | Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font | |
| 150 | Awesome Free files already contain embedded comments with sufficient | |
| 151 | attribution, so you shouldn't need to do anything additional when using these | |
| 152 | files normally. | |
| 153 | ||
| 154 | We've kept attribution comments terse, so we ask that you do not actively work | |
| 155 | to remove them from files, especially code. They're a great way for folks to | |
| 156 | learn about Font Awesome. | |
| 157 | ||
| 158 | -------------------------------------------------------------------------------- | |
| 159 | ||
| 160 | # Brand Icons | |
| 161 | ||
| 162 | All brand icons are trademarks of their respective owners. The use of these | |
| 163 | trademarks does not indicate endorsement of the trademark holder by Font | |
| 164 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except | |
| 165 | to 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 @@ |
| 1 | Pictogrammers Free License | |
| 2 | -------------------------- | |
| 3 | ||
| 4 | This icon collection is released as free, open source, and GPL friendly by | |
| 5 | the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it | |
| 6 | for commercial projects, open source projects, or anything really. | |
| 7 | ||
| 8 | # Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) | |
| 9 | Some of the icons are redistributed under the Apache 2.0 license. All other | |
| 10 | icons are either redistributed under their respective licenses or are | |
| 11 | distributed under the Apache 2.0 license. | |
| 12 | ||
| 13 | # Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) | |
| 14 | All web and desktop fonts are distributed under the Apache 2.0 license. Web | |
| 15 | and desktop fonts contain some icons that are redistributed under the Apache | |
| 16 | 2.0 license. All other icons are either redistributed under their respective | |
| 17 | licenses or are distributed under the Apache 2.0 license. | |
| 18 | ||
| 19 | # Code: MIT (https://opensource.org/licenses/MIT) | |
| 20 | The 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 | ||
| 3 | There 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 | ||
| 11 | The MIT License (MIT) | |
| 12 | ||
| 13 | Copyright (c) 2014 Ryan L McIntyre | |
| 14 | ||
| 15 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 16 | of this software and associated documentation files (the "Software"), to deal | |
| 17 | in the Software without restriction, including without limitation the rights | |
| 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 19 | copies of the Software, and to permit persons to whom the Software is | |
| 20 | furnished to do so, subject to the following conditions: | |
| 21 | ||
| 22 | The above copyright notice and this permission notice shall be included in all | |
| 23 | copies or substantial portions of the Software. | |
| 24 | ||
| 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 31 | SOFTWARE. | |
| 32 | ||
| 33 | ## Various Fonts, Patched Fonts, SVGs, Glyph Fonts, and any files in a folder with explicit SIL OFL 1.1 License | |
| 34 | ||
| 35 | Copyright (c) 2014, Ryan L McIntyre (https://ryanlmcintyre.com). | |
| 36 | ||
| 37 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 38 | This license is copied below, and is also available with a FAQ at: | |
| 39 | http://scripts.sil.org/OFL | |
| 40 | ||
| 41 | ----------------------------------------------------------- | |
| 42 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 43 | ----------------------------------------------------------- | |
| 44 | ||
| 45 | PREAMBLE | |
| 46 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 47 | development of collaborative font projects, to support the font creation | |
| 48 | efforts of academic and linguistic communities, and to provide a free and | |
| 49 | open framework in which fonts may be shared and improved in partnership | |
| 50 | with others. | |
| 51 | ||
| 52 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 53 | redistributed freely as long as they are not sold by themselves. The | |
| 54 | fonts, including any derivative works, can be bundled, embedded, | |
| 55 | redistributed and/or sold with any software provided that any reserved | |
| 56 | names are not used by derivative works. The fonts and derivatives, | |
| 57 | however, cannot be released under any other type of license. The | |
| 58 | requirement for fonts to remain under this license does not apply | |
| 59 | to any document created using the fonts or their derivatives. | |
| 60 | ||
| 61 | DEFINITIONS | |
| 62 | "Font Software" refers to the set of files released by the Copyright | |
| 63 | Holder(s) under this license and clearly marked as such. This may | |
| 64 | include source files, build scripts and documentation. | |
| 65 | ||
| 66 | "Reserved Font Name" refers to any names specified as such after the | |
| 67 | copyright statement(s). | |
| 68 | ||
| 69 | "Original Version" refers to the collection of Font Software components as | |
| 70 | distributed by the Copyright Holder(s). | |
| 71 | ||
| 72 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 73 | or substituting -- in part or in whole -- any of the components of the | |
| 74 | Original Version, by changing formats or by porting the Font Software to a | |
| 75 | new environment. | |
| 76 | ||
| 77 | "Author" refers to any designer, engineer, programmer, technical | |
| 78 | writer or other person who contributed to the Font Software. | |
| 79 | ||
| 80 | PERMISSION & CONDITIONS | |
| 81 | Permission is hereby granted, free of charge, to any person obtaining | |
| 82 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 83 | redistribute, and sell modified and unmodified copies of the Font | |
| 84 | Software, subject to the following conditions: | |
| 85 | ||
| 86 | 1) Neither the Font Software nor any of its individual components, | |
| 87 | in Original or Modified Versions, may be sold by itself. | |
| 88 | ||
| 89 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 90 | redistributed and/or sold with any software, provided that each copy | |
| 91 | contains the above copyright notice and this license. These can be | |
| 92 | included either as stand-alone text files, human-readable headers or | |
| 93 | in the appropriate machine-readable metadata fields within text or | |
| 94 | binary files as long as those fields can be easily viewed by the user. | |
| 95 | ||
| 96 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 97 | Name(s) unless explicit written permission is granted by the corresponding | |
| 98 | Copyright Holder. This restriction only applies to the primary font name as | |
| 99 | presented to the users. | |
| 100 | ||
| 101 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 102 | Software shall not be used to promote, endorse or advertise any | |
| 103 | Modified Version, except to acknowledge the contribution(s) of the | |
| 104 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 105 | permission. | |
| 106 | ||
| 107 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 108 | must be distributed entirely under this license, and must not be | |
| 109 | distributed under any other license. The requirement for fonts to | |
| 110 | remain under this license does not apply to any document created | |
| 111 | using the Font Software. | |
| 112 | ||
| 113 | TERMINATION | |
| 114 | This license becomes null and void if any of the above conditions are | |
| 115 | not met. | |
| 116 | ||
| 117 | DISCLAIMER | |
| 118 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 119 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 120 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 121 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 122 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 123 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 124 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 125 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 126 | OTHER DEALINGS IN THE FONT SOFTWARE. |
packages/terminal/fonts/licenses/NerdFonts-Symbols-Only-MIT.txt created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | The MIT License (MIT) | |
| 2 | ||
| 3 | Copyright (c) 2014 Ryan L McIntyre | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. |
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 | |
| 18 | renamed to both (i) retain the name of the Original Version and (ii) add | |
| 19 | additional naming elements to distinguish the Modified Version from the | |
| 20 | Original Version. The name of such Modified Versions must be the name of | |
| 21 | the Original Version, with "derivative X" where X represents the name of | |
| 22 | the new work, appended to that name. | |
| 23 | ||
| 24 | ## Original source works under Nerd Fonts repository | |
| 25 | ||
| 26 | All 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 @@ |
| 1 | MIT License | |
| 2 | ||
| 3 | Copyright (c) 2023 GitHub Inc. | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. |
packages/terminal/fonts/licenses/Pomicons-OFL.txt created+94| ... | ... | @@ -0,0 +1,94 @@ |
| 1 | Copyright (c) 2021, Gabriele Lana gabriele.lana@gmail.com | |
| 2 | with Reserved Font Name Pomicons. | |
| 3 | ||
| 4 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 5 | This license is copied below, and is also available with a FAQ at: | |
| 6 | http://scripts.sil.org/OFL | |
| 7 | ||
| 8 | ||
| 9 | ----------------------------------------------------------- | |
| 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 11 | ----------------------------------------------------------- | |
| 12 | ||
| 13 | PREAMBLE | |
| 14 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 15 | development of collaborative font projects, to support the font creation | |
| 16 | efforts of academic and linguistic communities, and to provide a free and | |
| 17 | open framework in which fonts may be shared and improved in partnership | |
| 18 | with others. | |
| 19 | ||
| 20 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 21 | redistributed freely as long as they are not sold by themselves. The | |
| 22 | fonts, including any derivative works, can be bundled, embedded, | |
| 23 | redistributed and/or sold with any software provided that any reserved | |
| 24 | names are not used by derivative works. The fonts and derivatives, | |
| 25 | however, cannot be released under any other type of license. The | |
| 26 | requirement for fonts to remain under this license does not apply | |
| 27 | to any document created using the fonts or their derivatives. | |
| 28 | ||
| 29 | DEFINITIONS | |
| 30 | "Font Software" refers to the set of files released by the Copyright | |
| 31 | Holder(s) under this license and clearly marked as such. This may | |
| 32 | include source files, build scripts and documentation. | |
| 33 | ||
| 34 | "Reserved Font Name" refers to any names specified as such after the | |
| 35 | copyright statement(s). | |
| 36 | ||
| 37 | "Original Version" refers to the collection of Font Software components as | |
| 38 | distributed by the Copyright Holder(s). | |
| 39 | ||
| 40 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 41 | or substituting -- in part or in whole -- any of the components of the | |
| 42 | Original Version, by changing formats or by porting the Font Software to a | |
| 43 | new environment. | |
| 44 | ||
| 45 | "Author" refers to any designer, engineer, programmer, technical | |
| 46 | writer or other person who contributed to the Font Software. | |
| 47 | ||
| 48 | PERMISSION & CONDITIONS | |
| 49 | Permission is hereby granted, free of charge, to any person obtaining | |
| 50 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 51 | redistribute, and sell modified and unmodified copies of the Font | |
| 52 | Software, subject to the following conditions: | |
| 53 | ||
| 54 | 1) Neither the Font Software nor any of its individual components, | |
| 55 | in Original or Modified Versions, may be sold by itself. | |
| 56 | ||
| 57 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 58 | redistributed and/or sold with any software, provided that each copy | |
| 59 | contains the above copyright notice and this license. These can be | |
| 60 | included either as stand-alone text files, human-readable headers or | |
| 61 | in the appropriate machine-readable metadata fields within text or | |
| 62 | binary files as long as those fields can be easily viewed by the user. | |
| 63 | ||
| 64 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 65 | Name(s) unless explicit written permission is granted by the corresponding | |
| 66 | Copyright Holder. This restriction only applies to the primary font name as | |
| 67 | presented to the users. | |
| 68 | ||
| 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 70 | Software shall not be used to promote, endorse or advertise any | |
| 71 | Modified Version, except to acknowledge the contribution(s) of the | |
| 72 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 73 | permission. | |
| 74 | ||
| 75 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 76 | must be distributed entirely under this license, and must not be | |
| 77 | distributed under any other license. The requirement for fonts to | |
| 78 | remain under this license does not apply to any document created | |
| 79 | using the Font Software. | |
| 80 | ||
| 81 | TERMINATION | |
| 82 | This license becomes null and void if any of the above conditions are | |
| 83 | not met. | |
| 84 | ||
| 85 | DISCLAIMER | |
| 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 94 | OTHER DEALINGS IN THE FONT SOFTWARE. |
packages/terminal/fonts/licenses/Powerline-Extra-MIT.txt created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | MIT License | |
| 2 | ||
| 3 | Copyright (c) 2016 Ryan L McIntyre | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. |
packages/terminal/fonts/licenses/Weather-Icons-OFL.txt created+97| ... | ... | @@ -0,0 +1,97 @@ |
| 1 | Copyright (c) <dates>, <Erik Flowers> (<URL|email>), | |
| 2 | with Reserved Font Name <Reserved Font Name>. | |
| 3 | Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>), | |
| 4 | with Reserved Font Name <additional Reserved Font Name>. | |
| 5 | Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>). | |
| 6 | ||
| 7 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 8 | This license is copied below, and is also available with a FAQ at: | |
| 9 | http://scripts.sil.org/OFL | |
| 10 | ||
| 11 | ||
| 12 | ----------------------------------------------------------- | |
| 13 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 14 | ----------------------------------------------------------- | |
| 15 | ||
| 16 | PREAMBLE | |
| 17 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 18 | development of collaborative font projects, to support the font creation | |
| 19 | efforts of academic and linguistic communities, and to provide a free and | |
| 20 | open framework in which fonts may be shared and improved in partnership | |
| 21 | with others. | |
| 22 | ||
| 23 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 24 | redistributed freely as long as they are not sold by themselves. The | |
| 25 | fonts, including any derivative works, can be bundled, embedded, | |
| 26 | redistributed and/or sold with any software provided that any reserved | |
| 27 | names are not used by derivative works. The fonts and derivatives, | |
| 28 | however, cannot be released under any other type of license. The | |
| 29 | requirement for fonts to remain under this license does not apply | |
| 30 | to any document created using the fonts or their derivatives. | |
| 31 | ||
| 32 | DEFINITIONS | |
| 33 | "Font Software" refers to the set of files released by the Copyright | |
| 34 | Holder(s) under this license and clearly marked as such. This may | |
| 35 | include source files, build scripts and documentation. | |
| 36 | ||
| 37 | "Reserved Font Name" refers to any names specified as such after the | |
| 38 | copyright statement(s). | |
| 39 | ||
| 40 | "Original Version" refers to the collection of Font Software components as | |
| 41 | distributed by the Copyright Holder(s). | |
| 42 | ||
| 43 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 44 | or substituting -- in part or in whole -- any of the components of the | |
| 45 | Original Version, by changing formats or by porting the Font Software to a | |
| 46 | new environment. | |
| 47 | ||
| 48 | "Author" refers to any designer, engineer, programmer, technical | |
| 49 | writer or other person who contributed to the Font Software. | |
| 50 | ||
| 51 | PERMISSION & CONDITIONS | |
| 52 | Permission is hereby granted, free of charge, to any person obtaining | |
| 53 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 54 | redistribute, and sell modified and unmodified copies of the Font | |
| 55 | Software, subject to the following conditions: | |
| 56 | ||
| 57 | 1) Neither the Font Software nor any of its individual components, | |
| 58 | in Original or Modified Versions, may be sold by itself. | |
| 59 | ||
| 60 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 61 | redistributed and/or sold with any software, provided that each copy | |
| 62 | contains the above copyright notice and this license. These can be | |
| 63 | included either as stand-alone text files, human-readable headers or | |
| 64 | in the appropriate machine-readable metadata fields within text or | |
| 65 | binary files as long as those fields can be easily viewed by the user. | |
| 66 | ||
| 67 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 68 | Name(s) unless explicit written permission is granted by the corresponding | |
| 69 | Copyright Holder. This restriction only applies to the primary font name as | |
| 70 | presented to the users. | |
| 71 | ||
| 72 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 73 | Software shall not be used to promote, endorse or advertise any | |
| 74 | Modified Version, except to acknowledge the contribution(s) of the | |
| 75 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 76 | permission. | |
| 77 | ||
| 78 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 79 | must be distributed entirely under this license, and must not be | |
| 80 | distributed under any other license. The requirement for fonts to | |
| 81 | remain under this license does not apply to any document created | |
| 82 | using the Font Software. | |
| 83 | ||
| 84 | TERMINATION | |
| 85 | This license becomes null and void if any of the above conditions are | |
| 86 | not met. | |
| 87 | ||
| 88 | DISCLAIMER | |
| 89 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 90 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 91 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 92 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 93 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 94 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 95 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 96 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 97 | OTHER 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 | ||
| 3 | The ranges are the terminal's own surface: every script and symbol block a cell | |
| 4 | can hold, plus Symbols for Legacy Computing, where the sextants and octants a | |
| 5 | program draws charts with live. What is dropped above them — mathematical | |
| 6 | alphanumerics, musical notation, emoji, and the historic scripts — is most of | |
| 7 | what makes an unsubset face 1.4 MB, and none of it reaches a terminal cell from | |
| 8 | a monospace primary. | |
| 9 | """ | |
| 10 | ||
| 11 | from pathlib import Path | |
| 12 | import sys | |
| 13 | ||
| 14 | from fontTools.ttLib import TTFont | |
| 15 | ||
| 16 | ||
| 17 | RETAINED_RANGES = ( | |
| 18 | (0x0000, 0x2BFF), # Latin through Miscellaneous Symbols and Arrows | |
| 19 | (0x1FB00, 0x1FBFF), # Symbols for Legacy Computing | |
| 20 | ) | |
| 21 | ||
| 22 | ||
| 23 | def 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 | ||
| 42 | if __name__ == "__main__": | |
| 43 | main() |
packages/terminal/fonts/subset-symbols.py created+49| ... | ... | @@ -0,0 +1,49 @@ |
| 1 | from pathlib import Path | |
| 2 | import sys | |
| 3 | ||
| 4 | from fontTools.ttLib import TTFont | |
| 5 | ||
| 6 | ||
| 7 | RETAINED_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 | ||
| 29 | def 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 | ||
| 48 | if __name__ == "__main__": | |
| 49 | main() |
packages/terminal/fonts/symbols-requirements.txt created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | fonttools==4.51.0 | |
| 2 | brotli==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 @@ |
| 1 | const ESC = "\u001b"; | |
| 2 | ||
| 3 | const 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 | ||
| 22 | const UNDERLINES = ["", "solid", "double", "wavy", "dotted", "dashed"] as const; | |
| 23 | ||
| 24 | interface 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 | ||
| 37 | function 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 | ||
| 52 | function byte(value: number): number { | |
| 53 | return Math.max(0, Math.min(255, Number.isFinite(value) ? value : 0)); | |
| 54 | } | |
| 55 | ||
| 56 | function 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 | ||
| 62 | function 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 | ||
| 75 | function setColor(state: AnsiState, background: boolean, color: string): void { | |
| 76 | if (background) state.background = color; | |
| 77 | else state.foreground = color; | |
| 78 | } | |
| 79 | ||
| 80 | function 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 | ||
| 109 | function 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 | ||
| 151 | function 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 | ||
| 179 | function 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 | ||
| 190 | function 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 | */ | |
| 206 | export 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 | ||
| 4 | export 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 | ||
| 18 | export 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 | ||
| 25 | export 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 | ||
| 40 | export const GhosttyCellSemanticContent = { | |
| 41 | GHOSTTY_CELL_SEMANTIC_OUTPUT: 0, | |
| 42 | GHOSTTY_CELL_SEMANTIC_INPUT: 1, | |
| 43 | GHOSTTY_CELL_SEMANTIC_PROMPT: 2, | |
| 44 | } as const; | |
| 45 | ||
| 46 | export 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 | ||
| 53 | export const GhosttyClipboardLocation = { | |
| 54 | GHOSTTY_CLIPBOARD_LOCATION_STANDARD: 0, | |
| 55 | GHOSTTY_CLIPBOARD_LOCATION_SELECTION: 1, | |
| 56 | GHOSTTY_CLIPBOARD_LOCATION_PRIMARY: 2, | |
| 57 | } as const; | |
| 58 | ||
| 59 | export 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 | ||
| 68 | export const GhosttyColorScheme = { | |
| 69 | GHOSTTY_COLOR_SCHEME_LIGHT: 0, | |
| 70 | GHOSTTY_COLOR_SCHEME_DARK: 1, | |
| 71 | } as const; | |
| 72 | ||
| 73 | export const GhosttyFocusEvent = { | |
| 74 | GHOSTTY_FOCUS_GAINED: 0, | |
| 75 | GHOSTTY_FOCUS_LOST: 1, | |
| 76 | } as const; | |
| 77 | ||
| 78 | export const GhosttyFormatterFormat = { | |
| 79 | GHOSTTY_FORMATTER_FORMAT_PLAIN: 0, | |
| 80 | GHOSTTY_FORMATTER_FORMAT_VT: 1, | |
| 81 | GHOSTTY_FORMATTER_FORMAT_HTML: 2, | |
| 82 | } as const; | |
| 83 | ||
| 84 | export 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 | ||
| 263 | export const GhosttyKeyAction = { | |
| 264 | GHOSTTY_KEY_ACTION_RELEASE: 0, | |
| 265 | GHOSTTY_KEY_ACTION_PRESS: 1, | |
| 266 | GHOSTTY_KEY_ACTION_REPEAT: 2, | |
| 267 | } as const; | |
| 268 | ||
| 269 | export 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 | ||
| 280 | export 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 | ||
| 286 | export 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 | ||
| 299 | export 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 | ||
| 315 | export const GhosttyKittyGraphicsPlacementIteratorOption = { | |
| 316 | GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER: 0, | |
| 317 | } as const; | |
| 318 | ||
| 319 | export const GhosttyKittyImageCompression = { | |
| 320 | GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE: 0, | |
| 321 | GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE: 1, | |
| 322 | } as const; | |
| 323 | ||
| 324 | export 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 | ||
| 332 | export 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 | ||
| 339 | export 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 | ||
| 347 | export const GhosttyMouseAction = { | |
| 348 | GHOSTTY_MOUSE_ACTION_PRESS: 0, | |
| 349 | GHOSTTY_MOUSE_ACTION_RELEASE: 1, | |
| 350 | GHOSTTY_MOUSE_ACTION_MOTION: 2, | |
| 351 | } as const; | |
| 352 | ||
| 353 | export 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 | ||
| 368 | export 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 | ||
| 376 | export 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 | ||
| 384 | export 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 | ||
| 392 | export 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 | ||
| 399 | export 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 | ||
| 406 | export const GhosttyOscCommandData = { | |
| 407 | GHOSTTY_OSC_DATA_INVALID: 0, | |
| 408 | GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR: 1, | |
| 409 | } as const; | |
| 410 | ||
| 411 | export 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 | ||
| 437 | export 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 | ||
| 444 | export 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 | ||
| 451 | export 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 | ||
| 472 | export 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 | ||
| 478 | export const GhosttyRenderStateOption = { | |
| 479 | GHOSTTY_RENDER_STATE_OPTION_DIRTY: 0, | |
| 480 | } as const; | |
| 481 | ||
| 482 | export 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 | ||
| 495 | export 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 | ||
| 503 | export const GhosttyRenderStateRowOption = { | |
| 504 | GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY: 0, | |
| 505 | } as const; | |
| 506 | ||
| 507 | export 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 | ||
| 515 | export 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 | ||
| 527 | export 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 | ||
| 533 | export 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 | ||
| 546 | export 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 | ||
| 552 | export 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 | ||
| 559 | export 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 | ||
| 567 | export 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 | ||
| 580 | export 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 | ||
| 588 | export 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 | ||
| 595 | export 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 | ||
| 629 | export 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 | ||
| 638 | export 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 | ||
| 645 | export const GhosttyStyleColorTag = { | |
| 646 | GHOSTTY_STYLE_COLOR_NONE: 0, | |
| 647 | GHOSTTY_STYLE_COLOR_PALETTE: 1, | |
| 648 | GHOSTTY_STYLE_COLOR_RGB: 2, | |
| 649 | } as const; | |
| 650 | ||
| 651 | export 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 | ||
| 658 | export 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 | ||
| 664 | export const GhosttyTerminalCompressionMode = { | |
| 665 | GHOSTTY_TERMINAL_COMPRESSION_MODE_INCREMENTAL: 0, | |
| 666 | GHOSTTY_TERMINAL_COMPRESSION_MODE_FULL: 1, | |
| 667 | } as const; | |
| 668 | ||
| 669 | export 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 | ||
| 675 | export 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 | ||
| 682 | export 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 | ||
| 719 | export 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 | ||
| 749 | export const GhosttyTerminalScreen = { | |
| 750 | GHOSTTY_TERMINAL_SCREEN_PRIMARY: 0, | |
| 751 | GHOSTTY_TERMINAL_SCREEN_ALTERNATE: 1, | |
| 752 | } as const; | |
| 753 | ||
| 754 | export 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 | ||
| 6 | export interface TypeField { | |
| 7 | offset: number; | |
| 8 | size: number; | |
| 9 | type: string; | |
| 10 | } | |
| 11 | ||
| 12 | export interface TypeLayout { | |
| 13 | [struct: string]: { size: number; align: number; fields: Record<string, TypeField> }; | |
| 14 | } | |
| 15 | ||
| 16 | type Fn = (...args: Array<number | bigint>) => number; | |
| 17 | ||
| 18 | export interface WasmTrampoline { | |
| 19 | readonly index: number; | |
| 20 | free(): void; | |
| 21 | } | |
| 22 | ||
| 23 | export interface GhosttyExports { | |
| 24 | memory: WebAssembly.Memory; | |
| 25 | __indirect_function_table: WebAssembly.Table; | |
| 26 | [name: string]: Fn | WebAssembly.Memory | WebAssembly.Table; | |
| 27 | } | |
| 28 | ||
| 29 | const encoder = new TextEncoder(); | |
| 30 | const 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 | */ | |
| 38 | function 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 | ||
| 90 | export 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 | ||
| 8 | import { domCodeToGhosttyKey, domEventMods } from "../keys.ts"; | |
| 9 | import { ShellCommands, type CommandAnchor, type ShellCommand } from "../shell/commands.ts"; | |
| 10 | import { parseShellMark, type ShellMark } from "../shell/marks.ts"; | |
| 11 | import { OscScanner } from "../shell/osc.ts"; | |
| 12 | import { | |
| 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"; | |
| 40 | import { GhosttyModule, type WasmTrampoline } from "./module.ts"; | |
| 41 | ||
| 42 | export 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 | ||
| 57 | export 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 | ||
| 80 | export 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 | ||
| 91 | export 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. */ | |
| 124 | export 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 | ||
| 148 | export 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 | ||
| 154 | export interface RowSnapshot { | |
| 155 | index: number; | |
| 156 | wrap: boolean; | |
| 157 | wrapContinuation: boolean; | |
| 158 | semanticPrompt: "none" | "prompt" | "continuation"; | |
| 159 | hyperlink: boolean; | |
| 160 | cells: CellSnapshot[]; | |
| 161 | } | |
| 162 | ||
| 163 | type RowMetadata = Omit<RowSnapshot, "index" | "cells">; | |
| 164 | ||
| 165 | export 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 | ||
| 175 | export 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. */ | |
| 188 | export interface FrameReader { | |
| 189 | snapshot(options?: { full?: boolean }): FrameSnapshot; | |
| 190 | dispose(): void; | |
| 191 | } | |
| 192 | ||
| 193 | export interface Scrollbar { | |
| 194 | total: number; | |
| 195 | offset: number; | |
| 196 | len: number; | |
| 197 | } | |
| 198 | ||
| 199 | const CURSOR_STYLES = ["bar", "block", "underline", "hollow"] as const; | |
| 200 | const encoder = new TextEncoder(); | |
| 201 | // URI identity must not inherit TextDecoder's default leading-BOM removal or | |
| 202 | // replacement-character collisions for malformed UTF-8. | |
| 203 | const osc8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); | |
| 204 | ||
| 205 | const 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 | ||
| 211 | const 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 | */ | |
| 222 | function 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 | ||
| 243 | const INITIAL_GRAPHEME_CAPACITY = 64; | |
| 244 | const INITIAL_HYPERLINK_URI_CAPACITY = 256; | |
| 245 | const MAX_RETAINED_INPUT_CAPACITY = 65_536; | |
| 246 | const SYNCHRONIZED_OUTPUT_MODE = 2026; | |
| 247 | const UINT16_MAX = 0xffff; | |
| 248 | const UINT32_MAX = 0xffff_ffff; | |
| 249 | const INT32_MIN = -0x8000_0000; | |
| 250 | const INT32_MAX = 0x7fff_ffff; | |
| 251 | const FLOAT32_MAX = 3.4028234663852886e38; | |
| 252 | ||
| 253 | interface 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 | ||
| 261 | function 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 | ||
| 272 | function assertGhosttySuccess(result: number, operation: string): void { | |
| 273 | if (result !== GhosttyResult.GHOSTTY_SUCCESS) throw new Error(`${operation} failed: ${result}`); | |
| 274 | } | |
| 275 | ||
| 276 | function 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 | */ | |
| 288 | class 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 | ||
| 328 | export 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 @@ |
| 1 | export { GhosttyModule } from "./ghostty/module.ts"; | |
| 2 | export { ansiToHtml } from "./ansi.ts"; | |
| 3 | export { mountTerminal, type MountTerminalOptions, type TerminalHandle } from "./mount.ts"; | |
| 4 | export { | |
| 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"; | |
| 16 | export { | |
| 17 | TerminalView, | |
| 18 | type TerminalLinkOptions, | |
| 19 | type TerminalViewOptions, | |
| 20 | } from "./renderer/dom.ts"; | |
| 21 | export { cellsToRuns, cellText, linkCells, rowText, type CellRun, type RowText } from "./runs.ts"; | |
| 22 | export { | |
| 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"; | |
| 34 | export { detectLinks, type DetectedLink, type DetectLinksOptions, type LinkKind } from "./links.ts"; | |
| 35 | export { | |
| 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"; | |
| 50 | export { | |
| 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 | ||
| 5 | import { 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). | |
| 9 | export const MODS_SHIFT = 1 << 0; | |
| 10 | export const MODS_CTRL = 1 << 1; | |
| 11 | export const MODS_ALT = 1 << 2; | |
| 12 | export const MODS_SUPER = 1 << 3; | |
| 13 | export const MODS_CAPS_LOCK = 1 << 4; | |
| 14 | export const MODS_NUM_LOCK = 1 << 5; | |
| 15 | ||
| 16 | const 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. */ | |
| 24 | export 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 | ||
| 46 | export interface ModState { | |
| 47 | ctrlKey: boolean; | |
| 48 | altKey: boolean; | |
| 49 | metaKey: boolean; | |
| 50 | shiftKey: boolean; | |
| 51 | } | |
| 52 | ||
| 53 | export 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 | ||
| 9 | export type LinkKind = "url" | "path" | "osc8"; | |
| 10 | ||
| 11 | export 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 | ||
| 24 | export 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 | ||
| 37 | const URL_RE = /\b(?:https?|file|ftp):\/\/[^\s<>"'`]+/gi; | |
| 38 | ||
| 39 | const 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. */ | |
| 43 | const TRAILING = /[.,:;!?)\]}'"]+$/; | |
| 44 | ||
| 45 | const DEFAULT_LIMIT = 64; | |
| 46 | ||
| 47 | export 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 @@ |
| 1 | import type { GhosttyModule } from "./ghostty/module.ts"; | |
| 2 | import { Terminal, type TerminalEvents, type TerminalOptions } from "./ghostty/terminal.ts"; | |
| 3 | import { TerminalView, type TerminalViewOptions } from "./renderer/dom.ts"; | |
| 4 | ||
| 5 | export 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 | ||
| 16 | export 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 | */ | |
| 31 | export 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. */ | |
| 24 | export type PredictionMode = "off" | "prompt"; | |
| 25 | ||
| 26 | export 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 | ||
| 42 | export interface ResolvedPredictOptions { | |
| 43 | readonly mode: PredictionMode; | |
| 44 | readonly minLatencyMs: number; | |
| 45 | readonly timeoutMs: number; | |
| 46 | readonly underline: boolean; | |
| 47 | readonly maxPredictions: number; | |
| 48 | } | |
| 49 | ||
| 50 | export const DEFAULT_PREDICT_OPTIONS: ResolvedPredictOptions = { | |
| 51 | mode: "off", | |
| 52 | minLatencyMs: 25, | |
| 53 | timeoutMs: 1000, | |
| 54 | underline: true, | |
| 55 | maxPredictions: 128, | |
| 56 | }; | |
| 57 | ||
| 58 | const positive = (value: number | undefined, fallback: number): number => | |
| 59 | value === undefined || !Number.isFinite(value) || value < 0 ? fallback : value; | |
| 60 | ||
| 61 | export 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. */ | |
| 80 | export 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. */ | |
| 96 | export 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. */ | |
| 106 | interface 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. */ | |
| 114 | export 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 | ||
| 120 | const 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 | */ | |
| 128 | const glyph = (text: string): string => (text === "" ? BLANK : text); | |
| 129 | ||
| 130 | /** Smoothing factor from RFC 6298 / mosh: a new sample moves the estimate by an eighth. */ | |
| 131 | const SRTT_ALPHA = 1 / 8; | |
| 132 | const MAX_RTT_SAMPLE_MS = 10_000; | |
| 133 | /** Never call a prediction wrong before the echo has had a plausible chance to arrive. */ | |
| 134 | const MIN_GRACE_MS = 50; | |
| 135 | ||
| 136 | export 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 | ||
| 11 | import 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 | */ | |
| 18 | export 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 @@ |
| 1 | export { | |
| 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"; | |
| 12 | export { 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 | ||
| 4 | import { useEffect, useRef, type CSSProperties, type HTMLAttributes } from "react"; | |
| 5 | ||
| 6 | import { ansiToHtml } from "./ansi.ts"; | |
| 7 | import type { GhosttyModule } from "./ghostty/module.ts"; | |
| 8 | import { mountTerminal, type MountTerminalOptions, type TerminalHandle } from "./mount.ts"; | |
| 9 | ||
| 10 | export 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. */ | |
| 18 | export function AnsiText({ text, ...props }: AnsiTextProps) { | |
| 19 | return <span {...props} dangerouslySetInnerHTML={{ __html: ansiToHtml(text) }} />; | |
| 20 | } | |
| 21 | ||
| 22 | export 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 | ||
| 32 | export 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 | ||
| 6 | import type { | |
| 7 | CellSnapshot, | |
| 8 | FrameReader, | |
| 9 | FrameSnapshot, | |
| 10 | RowSnapshot, | |
| 11 | Scrollbar, | |
| 12 | Terminal, | |
| 13 | } from "../ghostty/terminal.ts"; | |
| 14 | import { detectLinks, type DetectedLink, type DetectLinksOptions } from "../links.ts"; | |
| 15 | import { | |
| 16 | isPredictableText, | |
| 17 | PredictionEngine, | |
| 18 | resolvePredictOptions, | |
| 19 | type PredictionMode, | |
| 20 | type PredictionSite, | |
| 21 | type PredictOptions, | |
| 22 | } from "../predict/engine.ts"; | |
| 23 | import { columnTexts } from "../predict/frame.ts"; | |
| 24 | import { cellsToRuns, cellText, linkCells, rowText } from "../runs.ts"; | |
| 25 | import { stickyCommand, stickyTarget } from "../shell/sticky.ts"; | |
| 26 | ||
| 27 | type GlyphFitMode = "wide" | "symbol" | "powerline"; | |
| 28 | ||
| 29 | interface FittedGlyph { | |
| 30 | element: HTMLElement; | |
| 31 | key: string; | |
| 32 | cache: boolean; | |
| 33 | mode: GlyphFitMode; | |
| 34 | target: number; | |
| 35 | } | |
| 36 | ||
| 37 | interface GlyphFit { | |
| 38 | scaleX: number; | |
| 39 | scaleY: number; | |
| 40 | marginLeft: number; | |
| 41 | } | |
| 42 | ||
| 43 | const GLYPH_FIT_CACHE_LIMIT = 512; | |
| 44 | const GLYPH_FIT_CACHE_TEXT_LIMIT = 64; | |
| 45 | // Matches terminal.css: 10px track + 3px right inset. | |
| 46 | const SCROLLBAR_GUTTER = 13; | |
| 47 | const FONT_LOADING_CLASS = "clo-term-font-loading"; | |
| 48 | const FONT_ERROR_EVENT = "clo-terminal-font-error"; | |
| 49 | const SYNCHRONIZED_OUTPUT_TIMEOUT_MS = 1000; | |
| 50 | const SYMBOL_FONT_FAMILY = "__clo_terminal_symbols_3_4_0"; | |
| 51 | const ROOT_CLASSES = [ | |
| 52 | "clo-term", | |
| 53 | "clo-term-has-scrollbar", | |
| 54 | "clo-term-has-blink", | |
| 55 | FONT_LOADING_CLASS, | |
| 56 | ] as const; | |
| 57 | const 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. */ | |
| 63 | const ANSI_HUES = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]; | |
| 64 | const ANSI_TOKENS = [ | |
| 65 | ...ANSI_HUES.map((hue) => `--clo-term-ansi-${hue}`), | |
| 66 | ...ANSI_HUES.map((hue) => `--clo-term-ansi-bright-${hue}`), | |
| 67 | ]; | |
| 68 | const PRIMARY_FONT_SAMPLE = "W"; | |
| 69 | const SYMBOL_FONT_SAMPLE = "\uE0A3"; | |
| 70 | const 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 | ||
| 81 | function 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 | ||
| 91 | function applyGlyphFit(element: HTMLElement, fit: GlyphFit): void { | |
| 92 | element.style.marginLeft = `${fit.marginLeft}px`; | |
| 93 | element.style.transform = `scale(${fit.scaleX}, ${fit.scaleY})`; | |
| 94 | } | |
| 95 | ||
| 96 | interface FontCompletionEvent extends Event { | |
| 97 | readonly fontfaces: readonly FontFace[]; | |
| 98 | } | |
| 99 | ||
| 100 | interface FontCompletionSource { | |
| 101 | addEventListener(type: "loadingdone", listener: EventListener): void; | |
| 102 | removeEventListener(type: "loadingdone", listener: EventListener): void; | |
| 103 | } | |
| 104 | ||
| 105 | function 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 | ||
| 126 | function 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 | ||
| 136 | function 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 | ||
| 167 | function 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 | ||
| 177 | export 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 | ||
| 188 | export 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 | ||
| 222 | const 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 | */ | |
| 232 | function 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 | ||
| 288 | export 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 | ||
| 6 | import type { CellSnapshot } from "./ghostty/terminal.ts"; | |
| 7 | import type { DetectedLink } from "./links.ts"; | |
| 8 | ||
| 9 | export 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 | ||
| 21 | export interface RowText { | |
| 22 | text: string; | |
| 23 | /** `cellAt[offset]` identifies the canonical cell that owns that UTF-16 code unit. */ | |
| 24 | cellAt: number[]; | |
| 25 | } | |
| 26 | ||
| 27 | function 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. */ | |
| 47 | export 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. */ | |
| 53 | export 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 | */ | |
| 68 | export 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 | ||
| 84 | export 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 | ||
| 11 | import 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 | */ | |
| 17 | export 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 | ||
| 25 | export type CommandState = "prompt" | "editing" | "running" | "finished"; | |
| 26 | ||
| 27 | export 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 | ||
| 44 | export 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 | ||
| 68 | const DEFAULT_LIMIT = 512; | |
| 69 | ||
| 70 | export 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 | ||
| 230 | function 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 @@ |
| 1 | export { OscScanner, type OscSpan } from "./osc.ts"; | |
| 2 | export { parseShellMark, unescapeMarkValue, type MarkSource, type ShellMark } from "./marks.ts"; | |
| 3 | export { stickyCommand, stickyTarget } from "./sticky.ts"; | |
| 4 | export { | |
| 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 | ||
| 13 | export type MarkSource = 133 | 633; | |
| 14 | ||
| 15 | export 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 | */ | |
| 36 | export 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). */ | |
| 65 | export 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). */ | |
| 15 | const MAX_PAYLOAD = 32 * 1024; | |
| 16 | ||
| 17 | const BEL = 0x07; | |
| 18 | const ESC = 0x1b; | |
| 19 | const CAN = 0x18; | |
| 20 | const SUB = 0x1a; | |
| 21 | const BACKSLASH = 0x5c; | |
| 22 | ||
| 23 | export 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. | |
| 35 | const State = { | |
| 36 | Ground: 0, | |
| 37 | Esc: 1, | |
| 38 | Osc: 2, | |
| 39 | OscEsc: 3, | |
| 40 | } as const; | |
| 41 | ||
| 42 | const decoder = new TextDecoder(); | |
| 43 | ||
| 44 | type StateValue = (typeof State)[keyof typeof State]; | |
| 45 | ||
| 46 | export 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 | ||
| 140 | function 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 | ||
| 15 | import 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 | */ | |
| 25 | export 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. */ | |
| 46 | export 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 | ||
| 3 | import { createEffect, onCleanup, splitProps, untrack, type JSX } from "solid-js"; | |
| 4 | ||
| 5 | import { ansiToHtml } from "./ansi.ts"; | |
| 6 | import type { GhosttyModule } from "./ghostty/module.ts"; | |
| 7 | import { mountTerminal, type MountTerminalOptions, type TerminalHandle } from "./mount.ts"; | |
| 8 | ||
| 9 | export 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. */ | |
| 15 | export function AnsiText(props: AnsiTextProps): JSX.Element { | |
| 16 | const [local, attributes] = splitProps(props, ["text"]); | |
| 17 | return <span {...attributes} innerHTML={ansiToHtml(local.text)} />; | |
| 18 | } | |
| 19 | ||
| 20 | export 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. */ | |
| 30 | export 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 | |
| 2 | import * as NodeFS from "node:fs"; | |
| 3 | import * as NodeURL from "node:url"; | |
| 4 | import { afterEach, beforeAll, beforeEach, expect, it, vi } from "vite-plus/test"; | |
| 5 | ||
| 6 | import { GhosttyModule, mountTerminal, type TerminalHandle } from "../src/index.ts"; | |
| 7 | ||
| 8 | let module: GhosttyModule; | |
| 9 | let cellWidth = 7.8; | |
| 10 | let cellHeight = 15.6; | |
| 11 | const handles = new Set<TerminalHandle>(); | |
| 12 | const decoder = new TextDecoder(); | |
| 13 | ||
| 14 | beforeAll(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 | ||
| 27 | beforeEach(() => { | |
| 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 | ||
| 51 | afterEach(() => { | |
| 52 | handles.forEach((handle) => handle.dispose()); | |
| 53 | handles.clear(); | |
| 54 | document.body.replaceChildren(); | |
| 55 | vi.restoreAllMocks(); | |
| 56 | vi.unstubAllGlobals(); | |
| 57 | }); | |
| 58 | ||
| 59 | async 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 | ||
| 77 | it("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 | ||
| 109 | it("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 | ||
| 129 | function 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 | ||
| 150 | it("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 | ||
| 165 | it("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 | ||
| 176 | it("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 | ||
| 190 | it.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 | ||
| 206 | it("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 @@ |
| 1 | import { expect, it, vi } from "vite-plus/test"; | |
| 2 | import { detectLinks } from "../src/links.ts"; | |
| 3 | ||
| 4 | it.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 | ||
| 30 | it("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 | ||
| 41 | it("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 @@ |
| 1 | import * as NodeFS from "node:fs"; | |
| 2 | import * as NodeURL from "node:url"; | |
| 3 | import { afterEach, beforeAll, expect, it } from "vite-plus/test"; | |
| 4 | ||
| 5 | import { GhosttyModule, Terminal } from "../src/index.ts"; | |
| 6 | ||
| 7 | let module: GhosttyModule; | |
| 8 | const terminals = new Set<Terminal>(); | |
| 9 | ||
| 10 | beforeAll(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 | ||
| 18 | afterEach(() => { | |
| 19 | terminals.forEach((terminal) => terminal.free()); | |
| 20 | terminals.clear(); | |
| 21 | }); | |
| 22 | ||
| 23 | function create() { | |
| 24 | const terminal = new Terminal(module, { cols: 20, rows: 4, maxScrollbackBytes: 1024 * 1024 }); | |
| 25 | terminals.add(terminal); | |
| 26 | return terminal; | |
| 27 | } | |
| 28 | ||
| 29 | it("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 | ||
| 40 | it("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 | ||
| 54 | it("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 @@ |
| 1 | declare module "*?url" { | |
| 2 | const url: string; | |
| 3 | export default url; | |
| 4 | } | |
| 5 | ||
| 6 | declare module "*.css"; |
packages/terminal/vendor/COMMIT created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | 4c725242b7dbe8c77c6e227ef1f9540c5ef17921 |
packages/terminal/vendor/GHOSTTY-LICENSE created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | MIT License | |
| 2 | ||
| 3 | Copyright (c) 2024 Mitchell Hashimoto, Ghostty contributors | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. |
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 | ||
| 5 | The 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 | |
| 10 | git clone https://github.com/ghostty-org/ghostty.git # mirror; upstream is migrating forges | |
| 11 | cd ghostty && git checkout $(cat ../COMMIT) | |
| 12 | patch -p1 < <terminal-package>/vendor/patches/wasm-batched-vt.patch | |
| 13 | patch -p1 < <terminal-package>/vendor/patches/wasm-ascii-row.patch | |
| 14 | zig build -Demit-lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall | |
| 15 | cp zig-out/bin/ghostty-vt.wasm <here> | |
| 16 | node <terminal-package>/vendor/verify.mjs <ghostty-source> | |
| 17 | ``` | |
| 18 | ||
| 19 | The local patch keeps Ghostty's existing scalar `print_slice` batching enabled | |
| 20 | for wasm. Upstream's `simd=false` disables native SIMD dependencies, but the | |
| 21 | unpatched stream also falls back to one parser action per byte even though the | |
| 22 | batched path has a scalar decoder. | |
| 23 | ||
| 24 | The ASCII-row patch adds a fail-closed bulk export for narrow printable cells | |
| 25 | with default presentation or an explicit foreground. Unsupported rows return | |
| 26 | `GHOSTTY_NO_VALUE` and continue through the canonical per-cell API. | |
| 27 | ||
| 28 | Zig 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 | ||
| 32 | 1. New commit into `COMMIT`, rebuild wasm, re-vendor `include/`, and replace `SHA256` with the rebuilt wasm's lowercase SHA-256. | |
| 33 | 2. `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. | |
| 34 | 3. `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. | |
| 35 | 4. 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 @@ |
| 1 | d87ad59912c12ed2ca8f33da01137e6ea489a4de5dd181802d0f50d46c75c14d |
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 | |
| 4 | import { readFileSync, readdirSync, writeFileSync } from "node:fs"; | |
| 5 | import { join, dirname } from "node:path"; | |
| 6 | import { fileURLToPath } from "node:url"; | |
| 7 | ||
| 8 | const root = dirname(fileURLToPath(import.meta.url)); | |
| 9 | const headerDir = join(root, "include/ghostty/vt"); | |
| 10 | const headers = readdirSync(headerDir, { recursive: true }) | |
| 11 | .filter((f) => String(f).endsWith(".h")) | |
| 12 | .map((f) => readFileSync(join(headerDir, String(f)), "utf8")); | |
| 13 | ||
| 14 | const enums = new Map(); | |
| 15 | const enumRe = /typedef enum[^{]*\{([\s\S]*?)\}\s*(\w+)\s*;/g; | |
| 16 | for (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 | ||
| 40 | let out = `// GENERATED by vendor/gen-constants.mjs from the vendored libghostty-vt headers\n`; | |
| 41 | out += `// (commit in vendor/COMMIT). Do not edit by hand; re-run the generator on bump.\n\n`; | |
| 42 | for (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 | } | |
| 47 | out = `${out.trimEnd()}\n`; | |
| 48 | const outPath = join(root, "../src/ghostty/constants.ts"); | |
| 49 | if (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 | |
| 12 | extern "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> | |
| 21 | typedef 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 | |
| 57 | typedef void* ghostty_app_t; | |
| 58 | typedef void* ghostty_config_t; | |
| 59 | typedef void* ghostty_surface_t; | |
| 60 | typedef 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. | |
| 65 | typedef enum { | |
| 66 | GHOSTTY_PLATFORM_INVALID, | |
| 67 | GHOSTTY_PLATFORM_MACOS, | |
| 68 | GHOSTTY_PLATFORM_IOS, | |
| 69 | } ghostty_platform_e; | |
| 70 | ||
| 71 | typedef enum { | |
| 72 | GHOSTTY_CLIPBOARD_STANDARD, | |
| 73 | GHOSTTY_CLIPBOARD_SELECTION, | |
| 74 | } ghostty_clipboard_e; | |
| 75 | ||
| 76 | typedef struct { | |
| 77 | const char *mime; | |
| 78 | const char *data; | |
| 79 | } ghostty_clipboard_content_s; | |
| 80 | ||
| 81 | typedef 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 | ||
| 87 | typedef enum { | |
| 88 | GHOSTTY_MOUSE_RELEASE, | |
| 89 | GHOSTTY_MOUSE_PRESS, | |
| 90 | } ghostty_input_mouse_state_e; | |
| 91 | ||
| 92 | typedef 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 | ||
| 107 | typedef 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 | ||
| 117 | typedef 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. | |
| 125 | typedef int ghostty_input_scroll_mods_t; | |
| 126 | ||
| 127 | typedef 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 | ||
| 141 | typedef 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 | ||
| 148 | typedef 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/ | |
| 155 | typedef 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 | ||
| 350 | typedef 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 | ||
| 360 | typedef enum { | |
| 361 | GHOSTTY_TRIGGER_PHYSICAL, | |
| 362 | GHOSTTY_TRIGGER_UNICODE, | |
| 363 | GHOSTTY_TRIGGER_CATCH_ALL, | |
| 364 | } ghostty_input_trigger_tag_e; | |
| 365 | ||
| 366 | typedef union { | |
| 367 | ghostty_input_key_e physical; | |
| 368 | uint32_t unicode; | |
| 369 | // catch_all has no payload | |
| 370 | } ghostty_input_trigger_key_u; | |
| 371 | ||
| 372 | typedef 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 | ||
| 378 | typedef struct { | |
| 379 | const char* action_key; | |
| 380 | const char* action; | |
| 381 | const char* title; | |
| 382 | const char* description; | |
| 383 | } ghostty_command_s; | |
| 384 | ||
| 385 | typedef 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 | ||
| 392 | typedef struct { | |
| 393 | ghostty_build_mode_e build_mode; | |
| 394 | const char* version; | |
| 395 | uintptr_t version_len; | |
| 396 | } ghostty_info_s; | |
| 397 | ||
| 398 | typedef struct { | |
| 399 | const char* message; | |
| 400 | } ghostty_diagnostic_s; | |
| 401 | ||
| 402 | typedef struct { | |
| 403 | const char* ptr; | |
| 404 | uintptr_t len; | |
| 405 | bool sentinel; | |
| 406 | } ghostty_string_s; | |
| 407 | ||
| 408 | typedef 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 | ||
| 417 | typedef enum { | |
| 418 | GHOSTTY_POINT_ACTIVE, | |
| 419 | GHOSTTY_POINT_VIEWPORT, | |
| 420 | GHOSTTY_POINT_SCREEN, | |
| 421 | GHOSTTY_POINT_SURFACE, | |
| 422 | } ghostty_point_tag_e; | |
| 423 | ||
| 424 | typedef enum { | |
| 425 | GHOSTTY_POINT_COORD_EXACT, | |
| 426 | GHOSTTY_POINT_COORD_TOP_LEFT, | |
| 427 | GHOSTTY_POINT_COORD_BOTTOM_RIGHT, | |
| 428 | } ghostty_point_coord_e; | |
| 429 | ||
| 430 | typedef 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 | ||
| 437 | typedef struct { | |
| 438 | ghostty_point_s top_left; | |
| 439 | ghostty_point_s bottom_right; | |
| 440 | bool rectangle; | |
| 441 | } ghostty_selection_s; | |
| 442 | ||
| 443 | typedef struct { | |
| 444 | const char* key; | |
| 445 | const char* value; | |
| 446 | } ghostty_env_var_s; | |
| 447 | ||
| 448 | typedef struct { | |
| 449 | void* nsview; | |
| 450 | } ghostty_platform_macos_s; | |
| 451 | ||
| 452 | typedef struct { | |
| 453 | void* uiview; | |
| 454 | } ghostty_platform_ios_s; | |
| 455 | ||
| 456 | typedef union { | |
| 457 | ghostty_platform_macos_s macos; | |
| 458 | ghostty_platform_ios_s ios; | |
| 459 | } ghostty_platform_u; | |
| 460 | ||
| 461 | typedef 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 | ||
| 467 | typedef 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 | ||
| 482 | typedef 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 | |
| 494 | typedef struct { | |
| 495 | const char* path; | |
| 496 | bool optional; | |
| 497 | } ghostty_config_path_s; | |
| 498 | ||
| 499 | // config.Color | |
| 500 | typedef struct { | |
| 501 | uint8_t r; | |
| 502 | uint8_t g; | |
| 503 | uint8_t b; | |
| 504 | } ghostty_config_color_s; | |
| 505 | ||
| 506 | // config.ColorList | |
| 507 | typedef struct { | |
| 508 | const ghostty_config_color_s* colors; | |
| 509 | size_t len; | |
| 510 | } ghostty_config_color_list_s; | |
| 511 | ||
| 512 | // config.RepeatableCommand | |
| 513 | typedef struct { | |
| 514 | const ghostty_command_s* commands; | |
| 515 | size_t len; | |
| 516 | } ghostty_config_command_list_s; | |
| 517 | ||
| 518 | // config.Palette | |
| 519 | typedef struct { | |
| 520 | ghostty_config_color_s colors[256]; | |
| 521 | } ghostty_config_palette_s; | |
| 522 | ||
| 523 | // config.QuickTerminalSize | |
| 524 | typedef 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 | ||
| 530 | typedef union { | |
| 531 | float percentage; | |
| 532 | uint32_t pixels; | |
| 533 | } ghostty_quick_terminal_size_value_u; | |
| 534 | ||
| 535 | typedef struct { | |
| 536 | ghostty_quick_terminal_size_tag_e tag; | |
| 537 | ghostty_quick_terminal_size_value_u value; | |
| 538 | } ghostty_quick_terminal_size_s; | |
| 539 | ||
| 540 | typedef 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 | |
| 546 | typedef 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 | |
| 555 | typedef enum { | |
| 556 | GHOSTTY_TARGET_APP, | |
| 557 | GHOSTTY_TARGET_SURFACE, | |
| 558 | } ghostty_target_tag_e; | |
| 559 | ||
| 560 | typedef union { | |
| 561 | ghostty_surface_t surface; | |
| 562 | } ghostty_target_u; | |
| 563 | ||
| 564 | typedef struct { | |
| 565 | ghostty_target_tag_e tag; | |
| 566 | ghostty_target_u target; | |
| 567 | } ghostty_target_s; | |
| 568 | ||
| 569 | // apprt.action.SplitDirection | |
| 570 | typedef 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 | |
| 578 | typedef 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 | |
| 588 | typedef enum { | |
| 589 | GHOSTTY_GOTO_WINDOW_PREVIOUS, | |
| 590 | GHOSTTY_GOTO_WINDOW_NEXT, | |
| 591 | } ghostty_action_goto_window_e; | |
| 592 | ||
| 593 | // apprt.action.ResizeSplit.Direction | |
| 594 | typedef 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 | |
| 602 | typedef struct { | |
| 603 | uint16_t amount; | |
| 604 | ghostty_action_resize_split_direction_e direction; | |
| 605 | } ghostty_action_resize_split_s; | |
| 606 | ||
| 607 | // apprt.action.MoveTab | |
| 608 | typedef struct { | |
| 609 | ssize_t amount; | |
| 610 | } ghostty_action_move_tab_s; | |
| 611 | ||
| 612 | // apprt.action.GotoTab | |
| 613 | typedef 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 | |
| 620 | typedef 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 | |
| 628 | typedef 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 | |
| 635 | typedef 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 | |
| 642 | typedef enum { | |
| 643 | GHOSTTY_INSPECTOR_TOGGLE, | |
| 644 | GHOSTTY_INSPECTOR_SHOW, | |
| 645 | GHOSTTY_INSPECTOR_HIDE, | |
| 646 | } ghostty_action_inspector_e; | |
| 647 | ||
| 648 | // apprt.action.QuitTimer | |
| 649 | typedef enum { | |
| 650 | GHOSTTY_QUIT_TIMER_START, | |
| 651 | GHOSTTY_QUIT_TIMER_STOP, | |
| 652 | } ghostty_action_quit_timer_e; | |
| 653 | ||
| 654 | // apprt.action.Readonly | |
| 655 | typedef enum { | |
| 656 | GHOSTTY_READONLY_OFF, | |
| 657 | GHOSTTY_READONLY_ON, | |
| 658 | } ghostty_action_readonly_e; | |
| 659 | ||
| 660 | // apprt.action.DesktopNotification.C | |
| 661 | typedef struct { | |
| 662 | const char* title; | |
| 663 | const char* body; | |
| 664 | } ghostty_action_desktop_notification_s; | |
| 665 | ||
| 666 | // apprt.action.SetTitle.C | |
| 667 | typedef struct { | |
| 668 | const char* title; | |
| 669 | } ghostty_action_set_title_s; | |
| 670 | ||
| 671 | // apprt.action.PromptTitle | |
| 672 | typedef enum { | |
| 673 | GHOSTTY_PROMPT_TITLE_SURFACE, | |
| 674 | GHOSTTY_PROMPT_TITLE_TAB, | |
| 675 | } ghostty_action_prompt_title_e; | |
| 676 | ||
| 677 | // apprt.action.Pwd.C | |
| 678 | typedef struct { | |
| 679 | const char* pwd; | |
| 680 | } ghostty_action_pwd_s; | |
| 681 | ||
| 682 | // terminal.MouseShape | |
| 683 | typedef 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 | |
| 721 | typedef enum { | |
| 722 | GHOSTTY_MOUSE_VISIBLE, | |
| 723 | GHOSTTY_MOUSE_HIDDEN, | |
| 724 | } ghostty_action_mouse_visibility_e; | |
| 725 | ||
| 726 | // apprt.action.MouseOverLink | |
| 727 | typedef struct { | |
| 728 | const char* url; | |
| 729 | size_t len; | |
| 730 | } ghostty_action_mouse_over_link_s; | |
| 731 | ||
| 732 | // apprt.action.SizeLimit | |
| 733 | typedef 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 | |
| 741 | typedef struct { | |
| 742 | uint32_t width; | |
| 743 | uint32_t height; | |
| 744 | } ghostty_action_initial_size_s; | |
| 745 | ||
| 746 | // apprt.action.CellSize | |
| 747 | typedef struct { | |
| 748 | uint32_t width; | |
| 749 | uint32_t height; | |
| 750 | } ghostty_action_cell_size_s; | |
| 751 | ||
| 752 | // renderer.Health | |
| 753 | typedef enum { | |
| 754 | GHOSTTY_RENDERER_HEALTH_HEALTHY, | |
| 755 | GHOSTTY_RENDERER_HEALTH_UNHEALTHY, | |
| 756 | } ghostty_action_renderer_health_e; | |
| 757 | ||
| 758 | // apprt.action.KeySequence | |
| 759 | typedef struct { | |
| 760 | bool active; | |
| 761 | ghostty_input_trigger_s trigger; | |
| 762 | } ghostty_action_key_sequence_s; | |
| 763 | ||
| 764 | // apprt.action.KeyTable.Tag | |
| 765 | typedef 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 | |
| 772 | typedef 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 | |
| 780 | typedef 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 | |
| 786 | typedef 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 | |
| 793 | typedef 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 | |
| 801 | typedef struct { | |
| 802 | ghostty_config_t config; | |
| 803 | } ghostty_action_config_change_s; | |
| 804 | ||
| 805 | // apprt.action.ReloadConfig | |
| 806 | typedef struct { | |
| 807 | bool soft; | |
| 808 | } ghostty_action_reload_config_s; | |
| 809 | ||
| 810 | // apprt.action.OpenUrlKind | |
| 811 | typedef 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 | |
| 818 | typedef 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 | |
| 825 | typedef 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 | |
| 832 | typedef struct { | |
| 833 | uint32_t exit_code; | |
| 834 | uint64_t timetime_ms; | |
| 835 | } ghostty_surface_message_childexited_s; | |
| 836 | ||
| 837 | // terminal.osc.Command.ProgressReport.State | |
| 838 | typedef 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 | |
| 847 | typedef 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 | |
| 855 | typedef 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 | |
| 863 | typedef struct { | |
| 864 | const char* needle; | |
| 865 | } ghostty_action_start_search_s; | |
| 866 | ||
| 867 | // apprt.action.SearchTotal | |
| 868 | typedef struct { | |
| 869 | ssize_t total; | |
| 870 | } ghostty_action_search_total_s; | |
| 871 | ||
| 872 | // apprt.action.SearchSelected | |
| 873 | typedef struct { | |
| 874 | ssize_t selected; | |
| 875 | } ghostty_action_search_selected_s; | |
| 876 | ||
| 877 | // terminal.Scrollbar | |
| 878 | typedef struct { | |
| 879 | uint64_t total; | |
| 880 | uint64_t offset; | |
| 881 | uint64_t len; | |
| 882 | } ghostty_action_scrollbar_s; | |
| 883 | ||
| 884 | // apprt.Action.Key | |
| 885 | typedef 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 | ||
| 954 | typedef 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 | ||
| 995 | typedef struct { | |
| 996 | ghostty_action_tag_e tag; | |
| 997 | ghostty_action_u action; | |
| 998 | } ghostty_action_s; | |
| 999 | ||
| 1000 | typedef void (*ghostty_runtime_wakeup_cb)(void*); | |
| 1001 | typedef bool (*ghostty_runtime_read_clipboard_cb)(void*, | |
| 1002 | ghostty_clipboard_e, | |
| 1003 | void*); | |
| 1004 | typedef void (*ghostty_runtime_confirm_read_clipboard_cb)( | |
| 1005 | void*, | |
| 1006 | const char*, | |
| 1007 | void*, | |
| 1008 | ghostty_clipboard_request_e); | |
| 1009 | typedef void (*ghostty_runtime_write_clipboard_cb)(void*, | |
| 1010 | ghostty_clipboard_e, | |
| 1011 | const ghostty_clipboard_content_s*, | |
| 1012 | size_t, | |
| 1013 | bool); | |
| 1014 | typedef void (*ghostty_runtime_close_surface_cb)(void*, bool); | |
| 1015 | typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t, | |
| 1016 | ghostty_target_s, | |
| 1017 | ghostty_action_s); | |
| 1018 | ||
| 1019 | typedef 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 | |
| 1031 | typedef enum { | |
| 1032 | GHOSTTY_IPC_TARGET_CLASS, | |
| 1033 | GHOSTTY_IPC_TARGET_DETECT, | |
| 1034 | } ghostty_ipc_target_tag_e; | |
| 1035 | ||
| 1036 | typedef union { | |
| 1037 | char *klass; | |
| 1038 | } ghostty_ipc_target_u; | |
| 1039 | ||
| 1040 | typedef 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 | |
| 1046 | typedef struct { | |
| 1047 | // This should be a null terminated list of strings. | |
| 1048 | const char **arguments; | |
| 1049 | } ghostty_ipc_action_new_window_s; | |
| 1050 | ||
| 1051 | typedef union { | |
| 1052 | ghostty_ipc_action_new_window_s new_window; | |
| 1053 | } ghostty_ipc_action_u; | |
| 1054 | ||
| 1055 | // apprt.ipc.Action.Key | |
| 1056 | typedef 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 | ||
| 1064 | GHOSTTY_API int ghostty_init(uintptr_t, char**); | |
| 1065 | GHOSTTY_API void ghostty_cli_try_action(void); | |
| 1066 | GHOSTTY_API ghostty_info_s ghostty_info(void); | |
| 1067 | GHOSTTY_API const char* ghostty_translate(const char*); | |
| 1068 | GHOSTTY_API void ghostty_string_free(ghostty_string_s); | |
| 1069 | ||
| 1070 | GHOSTTY_API ghostty_config_t ghostty_config_new(); | |
| 1071 | GHOSTTY_API void ghostty_config_free(ghostty_config_t); | |
| 1072 | GHOSTTY_API ghostty_config_t ghostty_config_clone(ghostty_config_t); | |
| 1073 | GHOSTTY_API void ghostty_config_load_cli_args(ghostty_config_t); | |
| 1074 | GHOSTTY_API void ghostty_config_load_file(ghostty_config_t, const char*); | |
| 1075 | GHOSTTY_API void ghostty_config_load_default_files(ghostty_config_t); | |
| 1076 | GHOSTTY_API void ghostty_config_load_recursive_files(ghostty_config_t); | |
| 1077 | GHOSTTY_API void ghostty_config_finalize(ghostty_config_t); | |
| 1078 | GHOSTTY_API bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t); | |
| 1079 | GHOSTTY_API ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, | |
| 1080 | const char*, | |
| 1081 | uintptr_t); | |
| 1082 | GHOSTTY_API bool ghostty_config_key_is_binding(ghostty_config_t, ghostty_input_key_s); | |
| 1083 | GHOSTTY_API uint32_t ghostty_config_diagnostics_count(ghostty_config_t); | |
| 1084 | GHOSTTY_API ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t); | |
| 1085 | GHOSTTY_API ghostty_string_s ghostty_config_open_path(void); | |
| 1086 | ||
| 1087 | GHOSTTY_API ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, | |
| 1088 | ghostty_config_t); | |
| 1089 | GHOSTTY_API void ghostty_app_free(ghostty_app_t); | |
| 1090 | GHOSTTY_API void ghostty_app_tick(ghostty_app_t); | |
| 1091 | GHOSTTY_API void* ghostty_app_userdata(ghostty_app_t); | |
| 1092 | GHOSTTY_API void ghostty_app_set_focus(ghostty_app_t, bool); | |
| 1093 | GHOSTTY_API bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); | |
| 1094 | GHOSTTY_API void ghostty_app_keyboard_changed(ghostty_app_t); | |
| 1095 | GHOSTTY_API void ghostty_app_open_config(ghostty_app_t); | |
| 1096 | GHOSTTY_API void ghostty_app_update_config(ghostty_app_t, ghostty_config_t); | |
| 1097 | GHOSTTY_API bool ghostty_app_needs_confirm_quit(ghostty_app_t); | |
| 1098 | GHOSTTY_API bool ghostty_app_has_global_keybinds(ghostty_app_t); | |
| 1099 | GHOSTTY_API void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e); | |
| 1100 | ||
| 1101 | GHOSTTY_API ghostty_surface_config_s ghostty_surface_config_new(); | |
| 1102 | ||
| 1103 | GHOSTTY_API ghostty_surface_t ghostty_surface_new(ghostty_app_t, | |
| 1104 | const ghostty_surface_config_s*); | |
| 1105 | GHOSTTY_API void ghostty_surface_free(ghostty_surface_t); | |
| 1106 | GHOSTTY_API void* ghostty_surface_userdata(ghostty_surface_t); | |
| 1107 | GHOSTTY_API ghostty_app_t ghostty_surface_app(ghostty_surface_t); | |
| 1108 | GHOSTTY_API ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e); | |
| 1109 | GHOSTTY_API void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); | |
| 1110 | GHOSTTY_API bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); | |
| 1111 | GHOSTTY_API bool ghostty_surface_process_exited(ghostty_surface_t); | |
| 1112 | GHOSTTY_API void ghostty_surface_refresh(ghostty_surface_t); | |
| 1113 | GHOSTTY_API void ghostty_surface_draw(ghostty_surface_t); | |
| 1114 | GHOSTTY_API void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); | |
| 1115 | GHOSTTY_API void ghostty_surface_set_focus(ghostty_surface_t, bool); | |
| 1116 | GHOSTTY_API void ghostty_surface_set_occlusion(ghostty_surface_t, bool); | |
| 1117 | GHOSTTY_API void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); | |
| 1118 | GHOSTTY_API ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t); | |
| 1119 | GHOSTTY_API uint64_t ghostty_surface_foreground_pid(ghostty_surface_t); | |
| 1120 | GHOSTTY_API ghostty_string_s ghostty_surface_tty_name(ghostty_surface_t); | |
| 1121 | GHOSTTY_API void ghostty_surface_set_color_scheme(ghostty_surface_t, | |
| 1122 | ghostty_color_scheme_e); | |
| 1123 | GHOSTTY_API ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, | |
| 1124 | ghostty_input_mods_e); | |
| 1125 | GHOSTTY_API bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); | |
| 1126 | GHOSTTY_API bool ghostty_surface_key_is_binding(ghostty_surface_t, | |
| 1127 | ghostty_input_key_s, | |
| 1128 | ghostty_binding_flags_e*); | |
| 1129 | GHOSTTY_API void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); | |
| 1130 | GHOSTTY_API void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t); | |
| 1131 | GHOSTTY_API bool ghostty_surface_mouse_captured(ghostty_surface_t); | |
| 1132 | GHOSTTY_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); | |
| 1136 | GHOSTTY_API void ghostty_surface_mouse_pos(ghostty_surface_t, | |
| 1137 | double, | |
| 1138 | double, | |
| 1139 | ghostty_input_mods_e); | |
| 1140 | GHOSTTY_API void ghostty_surface_mouse_scroll(ghostty_surface_t, | |
| 1141 | double, | |
| 1142 | double, | |
| 1143 | ghostty_input_scroll_mods_t); | |
| 1144 | GHOSTTY_API void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double); | |
| 1145 | GHOSTTY_API void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*); | |
| 1146 | GHOSTTY_API void ghostty_surface_request_close(ghostty_surface_t); | |
| 1147 | GHOSTTY_API void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e); | |
| 1148 | GHOSTTY_API void ghostty_surface_split_focus(ghostty_surface_t, | |
| 1149 | ghostty_action_goto_split_e); | |
| 1150 | GHOSTTY_API void ghostty_surface_split_resize(ghostty_surface_t, | |
| 1151 | ghostty_action_resize_split_direction_e, | |
| 1152 | uint16_t); | |
| 1153 | GHOSTTY_API void ghostty_surface_split_equalize(ghostty_surface_t); | |
| 1154 | GHOSTTY_API bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t); | |
| 1155 | GHOSTTY_API void ghostty_surface_complete_clipboard_request(ghostty_surface_t, | |
| 1156 | const char*, | |
| 1157 | void*, | |
| 1158 | bool); | |
| 1159 | GHOSTTY_API bool ghostty_surface_has_selection(ghostty_surface_t); | |
| 1160 | GHOSTTY_API bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*); | |
| 1161 | GHOSTTY_API bool ghostty_surface_read_text(ghostty_surface_t, | |
| 1162 | ghostty_selection_s, | |
| 1163 | ghostty_text_s*); | |
| 1164 | GHOSTTY_API void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*); | |
| 1165 | ||
| 1166 | #ifdef __APPLE__ | |
| 1167 | GHOSTTY_API void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); | |
| 1168 | GHOSTTY_API void* ghostty_surface_quicklook_font(ghostty_surface_t); | |
| 1169 | GHOSTTY_API bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*); | |
| 1170 | #endif | |
| 1171 | ||
| 1172 | GHOSTTY_API ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); | |
| 1173 | GHOSTTY_API void ghostty_inspector_free(ghostty_surface_t); | |
| 1174 | GHOSTTY_API void ghostty_inspector_set_focus(ghostty_inspector_t, bool); | |
| 1175 | GHOSTTY_API void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double); | |
| 1176 | GHOSTTY_API void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t); | |
| 1177 | GHOSTTY_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); | |
| 1181 | GHOSTTY_API void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double); | |
| 1182 | GHOSTTY_API void ghostty_inspector_mouse_scroll(ghostty_inspector_t, | |
| 1183 | double, | |
| 1184 | double, | |
| 1185 | ghostty_input_scroll_mods_t); | |
| 1186 | GHOSTTY_API void ghostty_inspector_key(ghostty_inspector_t, | |
| 1187 | ghostty_input_action_e, | |
| 1188 | ghostty_input_key_e, | |
| 1189 | ghostty_input_mods_e); | |
| 1190 | GHOSTTY_API void ghostty_inspector_text(ghostty_inspector_t, const char*); | |
| 1191 | ||
| 1192 | #ifdef __APPLE__ | |
| 1193 | GHOSTTY_API bool ghostty_inspector_metal_init(ghostty_inspector_t, void*); | |
| 1194 | GHOSTTY_API void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*); | |
| 1195 | GHOSTTY_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. | |
| 1200 | GHOSTTY_API void ghostty_set_window_background_blur(ghostty_app_t, void*); | |
| 1201 | ||
| 1202 | // Benchmark API, if available. | |
| 1203 | GHOSTTY_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 | |
| 128 | extern "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 | */ | |
| 102 | typedef 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 | */ | |
| 198 | typedef 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 | */ | |
| 226 | GHOSTTY_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 | */ | |
| 251 | GHOSTTY_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 | |
| 32 | extern "C" { | |
| 33 | #endif | |
| 34 | ||
| 35 | /** | |
| 36 | * Build optimization mode. | |
| 37 | */ | |
| 38 | typedef 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 | */ | |
| 51 | typedef 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 | */ | |
| 142 | GHOSTTY_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 | |
| 122 | extern "C" { | |
| 123 | #endif | |
| 124 | ||
| 125 | /** | |
| 126 | * RGB color value. | |
| 127 | * | |
| 128 | * @ingroup color | |
| 129 | */ | |
| 130 | typedef 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 | */ | |
| 141 | typedef 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 | */ | |
| 160 | typedef 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 | */ | |
| 169 | typedef 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 | */ | |
| 275 | GHOSTTY_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 | */ | |
| 295 | GHOSTTY_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 | */ | |
| 317 | GHOSTTY_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 | */ | |
| 339 | GHOSTTY_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 | */ | |
| 356 | GHOSTTY_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 | */ | |
| 386 | GHOSTTY_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 | */ | |
| 405 | GHOSTTY_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 | */ | |
| 420 | GHOSTTY_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 | */ | |
| 434 | GHOSTTY_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 | */ | |
| 459 | GHOSTTY_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 | */ | |
| 471 | GHOSTTY_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 | |
| 35 | extern "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 | */ | |
| 61 | GHOSTTY_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 | |
| 66 | extern "C" { | |
| 67 | #endif | |
| 68 | ||
| 69 | /** | |
| 70 | * Color scheme reported in response to a CSI ? 996 n query. | |
| 71 | * | |
| 72 | * @ingroup terminal | |
| 73 | */ | |
| 74 | typedef 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 | */ | |
| 89 | typedef 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 | */ | |
| 108 | typedef 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 | */ | |
| 127 | typedef 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 | */ | |
| 141 | typedef 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 | |
| 32 | extern "C" { | |
| 33 | #endif | |
| 34 | ||
| 35 | /** | |
| 36 | * Focus event types for focus reporting mode (mode 1004). | |
| 37 | */ | |
| 38 | typedef 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 | */ | |
| 64 | GHOSTTY_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 | |
| 19 | extern "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 | */ | |
| 40 | typedef 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 | */ | |
| 68 | typedef 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 | */ | |
| 99 | typedef 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 | */ | |
| 134 | GHOSTTY_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 | */ | |
| 161 | GHOSTTY_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 | */ | |
| 184 | GHOSTTY_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 | */ | |
| 199 | GHOSTTY_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 | |
| 18 | extern "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 | */ | |
| 107 | typedef 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 | */ | |
| 124 | GHOSTTY_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 | */ | |
| 137 | GHOSTTY_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 | */ | |
| 162 | GHOSTTY_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 | */ | |
| 187 | GHOSTTY_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 | */ | |
| 203 | GHOSTTY_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 | |
| 16 | extern "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 | */ | |
| 37 | GHOSTTY_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 | */ | |
| 50 | GHOSTTY_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 | */ | |
| 79 | GHOSTTY_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 | */ | |
| 105 | GHOSTTY_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 | */ | |
| 131 | GHOSTTY_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 | */ | |
| 25 | typedef 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 | */ | |
| 36 | typedef 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 | */ | |
| 67 | typedef 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 | */ | |
| 87 | typedef 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 | */ | |
| 132 | GHOSTTY_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 | */ | |
| 144 | GHOSTTY_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 | */ | |
| 167 | GHOSTTY_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 | */ | |
| 186 | GHOSTTY_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 | */ | |
| 253 | GHOSTTY_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 | */ | |
| 24 | typedef struct GhosttyKeyEventImpl *GhosttyKeyEvent; | |
| 25 | ||
| 26 | /** | |
| 27 | * Keyboard input event types. | |
| 28 | * | |
| 29 | * @ingroup key | |
| 30 | */ | |
| 31 | typedef 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 | */ | |
| 57 | typedef 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 | */ | |
| 107 | typedef 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 | */ | |
| 315 | GHOSTTY_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 | */ | |
| 327 | GHOSTTY_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 | */ | |
| 337 | GHOSTTY_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 | */ | |
| 347 | GHOSTTY_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 | */ | |
| 357 | GHOSTTY_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 | */ | |
| 367 | GHOSTTY_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 | */ | |
| 377 | GHOSTTY_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 | */ | |
| 387 | GHOSTTY_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 | */ | |
| 397 | GHOSTTY_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 | */ | |
| 407 | GHOSTTY_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 | */ | |
| 417 | GHOSTTY_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 | */ | |
| 427 | GHOSTTY_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 | */ | |
| 447 | GHOSTTY_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 | */ | |
| 460 | GHOSTTY_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 | */ | |
| 470 | GHOSTTY_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 | */ | |
| 480 | GHOSTTY_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 | |
| 19 | extern "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 | */ | |
| 132 | typedef 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 | */ | |
| 175 | typedef 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 | */ | |
| 277 | typedef 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 | */ | |
| 290 | typedef 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 | */ | |
| 311 | typedef 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 | */ | |
| 331 | typedef 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 | */ | |
| 342 | typedef 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 | */ | |
| 450 | typedef 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 | */ | |
| 492 | GHOSTTY_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 | */ | |
| 509 | GHOSTTY_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 | */ | |
| 526 | GHOSTTY_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 | */ | |
| 558 | GHOSTTY_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 | */ | |
| 579 | GHOSTTY_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 | */ | |
| 590 | GHOSTTY_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 | */ | |
| 611 | GHOSTTY_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 | */ | |
| 628 | GHOSTTY_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 | */ | |
| 645 | GHOSTTY_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 | */ | |
| 677 | GHOSTTY_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 | */ | |
| 702 | GHOSTTY_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 | */ | |
| 726 | GHOSTTY_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 | */ | |
| 752 | GHOSTTY_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 | */ | |
| 794 | GHOSTTY_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 | */ | |
| 820 | GHOSTTY_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 | */ | |
| 847 | GHOSTTY_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 | |
| 42 | extern "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 | */ | |
| 105 | typedef 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 | */ | |
| 116 | static 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 | */ | |
| 128 | static 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 | */ | |
| 140 | static 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 | */ | |
| 150 | typedef 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 | */ | |
| 185 | GHOSTTY_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 | */ | |
| 26 | typedef struct GhosttyMouseEncoderImpl *GhosttyMouseEncoder; | |
| 27 | ||
| 28 | /** | |
| 29 | * Mouse tracking mode. | |
| 30 | * | |
| 31 | * @ingroup mouse | |
| 32 | */ | |
| 33 | typedef 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 | */ | |
| 56 | typedef 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 | */ | |
| 73 | typedef 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 | */ | |
| 110 | typedef 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 | */ | |
| 137 | GHOSTTY_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 | */ | |
| 147 | GHOSTTY_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 | */ | |
| 160 | GHOSTTY_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 | */ | |
| 175 | GHOSTTY_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 | */ | |
| 187 | GHOSTTY_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 | */ | |
| 208 | GHOSTTY_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 | */ | |
| 23 | typedef struct GhosttyMouseEventImpl *GhosttyMouseEvent; | |
| 24 | ||
| 25 | /** | |
| 26 | * Mouse event action type. | |
| 27 | * | |
| 28 | * @ingroup mouse | |
| 29 | */ | |
| 30 | typedef 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 | */ | |
| 47 | typedef 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 | */ | |
| 68 | typedef 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 | */ | |
| 82 | GHOSTTY_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 | */ | |
| 92 | GHOSTTY_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 | */ | |
| 102 | GHOSTTY_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 | */ | |
| 113 | GHOSTTY_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 | */ | |
| 127 | GHOSTTY_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 | */ | |
| 139 | GHOSTTY_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 | */ | |
| 150 | GHOSTTY_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 | */ | |
| 161 | GHOSTTY_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 | */ | |
| 172 | GHOSTTY_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 | */ | |
| 182 | GHOSTTY_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 | */ | |
| 193 | GHOSTTY_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 | */ | |
| 42 | typedef 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 | */ | |
| 77 | typedef 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 | */ | |
| 108 | GHOSTTY_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 | */ | |
| 120 | GHOSTTY_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 | */ | |
| 133 | GHOSTTY_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 | */ | |
| 150 | GHOSTTY_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 | */ | |
| 180 | GHOSTTY_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 | */ | |
| 194 | GHOSTTY_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 | */ | |
| 211 | GHOSTTY_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 | |
| 40 | extern "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 | */ | |
| 58 | GHOSTTY_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 | */ | |
| 87 | GHOSTTY_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 | |
| 13 | extern "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 | */ | |
| 30 | typedef 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 | */ | |
| 45 | typedef 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 | */ | |
| 65 | typedef 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 | */ | |
| 78 | typedef 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 | |
| 19 | extern "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 | */ | |
| 101 | typedef 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 | */ | |
| 118 | typedef 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 | */ | |
| 138 | typedef 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 | */ | |
| 210 | typedef 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 | */ | |
| 221 | typedef 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 | */ | |
| 247 | typedef 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 | */ | |
| 265 | typedef 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. */ | |
| 277 | typedef 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 | */ | |
| 297 | typedef 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 | */ | |
| 331 | GHOSTTY_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 | */ | |
| 344 | GHOSTTY_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 | */ | |
| 366 | GHOSTTY_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 | */ | |
| 398 | GHOSTTY_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 | */ | |
| 415 | GHOSTTY_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 | */ | |
| 431 | GHOSTTY_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 | */ | |
| 456 | GHOSTTY_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 | */ | |
| 478 | GHOSTTY_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 | */ | |
| 497 | GHOSTTY_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 | */ | |
| 514 | GHOSTTY_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 | */ | |
| 525 | GHOSTTY_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 | */ | |
| 539 | GHOSTTY_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 | */ | |
| 557 | GHOSTTY_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 | */ | |
| 583 | GHOSTTY_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 | */ | |
| 607 | GHOSTTY_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 | */ | |
| 629 | GHOSTTY_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 | */ | |
| 638 | typedef 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 | */ | |
| 717 | GHOSTTY_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 | */ | |
| 733 | GHOSTTY_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 | */ | |
| 753 | GHOSTTY_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 | */ | |
| 779 | GHOSTTY_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 | */ | |
| 790 | GHOSTTY_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 | */ | |
| 803 | GHOSTTY_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 | |
| 16 | extern "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 | */ | |
| 40 | typedef 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 | */ | |
| 51 | typedef uint64_t GhosttyRow; | |
| 52 | ||
| 53 | /** | |
| 54 | * Cell content tag. | |
| 55 | * | |
| 56 | * Describes what kind of content a cell holds. | |
| 57 | * | |
| 58 | * @ingroup screen | |
| 59 | */ | |
| 60 | typedef 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 | */ | |
| 82 | typedef 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 | */ | |
| 105 | typedef 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 | */ | |
| 125 | typedef 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 | */ | |
| 218 | typedef 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 | */ | |
| 238 | typedef 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 | */ | |
| 316 | GHOSTTY_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 | */ | |
| 341 | GHOSTTY_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 | */ | |
| 363 | GHOSTTY_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 | */ | |
| 388 | GHOSTTY_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 | |
| 19 | extern "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 | */ | |
| 61 | typedef 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 | */ | |
| 71 | typedef 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 | */ | |
| 92 | typedef 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 | */ | |
| 129 | typedef 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 | */ | |
| 153 | typedef 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 | */ | |
| 180 | typedef 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 | */ | |
| 212 | typedef 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 | */ | |
| 246 | typedef 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 | */ | |
| 271 | typedef 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 | */ | |
| 322 | typedef 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 | */ | |
| 343 | typedef 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 | */ | |
| 359 | typedef 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 | */ | |
| 378 | typedef 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 | */ | |
| 397 | typedef 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 | */ | |
| 430 | typedef 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 | */ | |
| 457 | typedef 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 | */ | |
| 527 | GHOSTTY_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 | */ | |
| 541 | GHOSTTY_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 | */ | |
| 559 | GHOSTTY_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 | */ | |
| 609 | GHOSTTY_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 | */ | |
| 629 | GHOSTTY_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 | */ | |
| 653 | GHOSTTY_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 | */ | |
| 669 | GHOSTTY_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 | */ | |
| 691 | GHOSTTY_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 | */ | |
| 720 | GHOSTTY_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 | */ | |
| 744 | GHOSTTY_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 | */ | |
| 778 | GHOSTTY_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 | */ | |
| 799 | GHOSTTY_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 | */ | |
| 818 | GHOSTTY_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 | */ | |
| 837 | GHOSTTY_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 | */ | |
| 870 | GHOSTTY_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 | */ | |
| 903 | GHOSTTY_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 | */ | |
| 935 | GHOSTTY_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 | */ | |
| 959 | GHOSTTY_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 | */ | |
| 990 | GHOSTTY_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 | */ | |
| 1019 | GHOSTTY_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 | */ | |
| 1049 | GHOSTTY_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 | |
| 50 | extern "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 | */ | |
| 61 | typedef 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 | */ | |
| 101 | typedef 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 | */ | |
| 119 | typedef 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 | */ | |
| 135 | typedef 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 | */ | |
| 163 | typedef 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 | */ | |
| 182 | GHOSTTY_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 | */ | |
| 196 | GHOSTTY_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 | */ | |
| 209 | GHOSTTY_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 | */ | |
| 241 | GHOSTTY_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 | */ | |
| 259 | GHOSTTY_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 | */ | |
| 274 | GHOSTTY_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 | */ | |
| 290 | GHOSTTY_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 | */ | |
| 305 | GHOSTTY_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 | */ | |
| 319 | GHOSTTY_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 | */ | |
| 333 | GHOSTTY_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 | */ | |
| 344 | GHOSTTY_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 | |
| 35 | extern "C" { | |
| 36 | #endif | |
| 37 | ||
| 38 | /** | |
| 39 | * Size report style. | |
| 40 | * | |
| 41 | * Determines the output format for the terminal size report. | |
| 42 | */ | |
| 43 | typedef 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 | */ | |
| 58 | typedef 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 | */ | |
| 88 | GHOSTTY_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 | |
| 17 | extern "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 | */ | |
| 39 | typedef 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 | */ | |
| 49 | typedef 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 | */ | |
| 63 | typedef 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 | */ | |
| 77 | typedef 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 | */ | |
| 94 | typedef 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 | */ | |
| 119 | GHOSTTY_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 | */ | |
| 131 | GHOSTTY_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 | |
| 43 | extern "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 | */ | |
| 53 | typedef 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 | */ | |
| 70 | typedef 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 | */ | |
| 98 | typedef 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 | */ | |
| 120 | typedef 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 | */ | |
| 130 | typedef 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 | */ | |
| 184 | GHOSTTY_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 | */ | |
| 197 | GHOSTTY_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 | |
| 26 | extern "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 | */ | |
| 180 | typedef 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 | */ | |
| 200 | typedef 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 | */ | |
| 214 | typedef 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 | */ | |
| 231 | typedef 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 | */ | |
| 263 | typedef 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 | */ | |
| 279 | typedef 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 | */ | |
| 291 | typedef 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 | */ | |
| 305 | typedef 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 | */ | |
| 327 | typedef 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 | */ | |
| 348 | typedef 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 | */ | |
| 359 | typedef 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 | */ | |
| 383 | typedef 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 | */ | |
| 405 | typedef 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 | */ | |
| 427 | typedef 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 | */ | |
| 465 | typedef 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 | */ | |
| 484 | typedef 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 | */ | |
| 505 | typedef 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 | */ | |
| 523 | typedef 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 | */ | |
| 540 | typedef 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 | */ | |
| 556 | typedef 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 | */ | |
| 581 | typedef 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 | */ | |
| 599 | typedef 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 | */ | |
| 618 | typedef 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 | */ | |
| 629 | typedef 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 | */ | |
| 902 | typedef 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 | */ | |
| 1231 | GHOSTTY_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 | */ | |
| 1245 | GHOSTTY_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 | */ | |
| 1258 | GHOSTTY_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 | */ | |
| 1281 | GHOSTTY_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 | */ | |
| 1307 | GHOSTTY_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 | */ | |
| 1332 | GHOSTTY_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 | */ | |
| 1352 | GHOSTTY_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 | */ | |
| 1373 | GHOSTTY_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 | */ | |
| 1404 | GHOSTTY_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 | */ | |
| 1423 | GHOSTTY_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 | */ | |
| 1440 | GHOSTTY_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 | */ | |
| 1460 | GHOSTTY_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 | */ | |
| 1491 | GHOSTTY_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 | */ | |
| 1523 | GHOSTTY_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 | */ | |
| 1554 | GHOSTTY_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 | */ | |
| 1586 | GHOSTTY_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 | */ | |
| 74 | typedef 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 | */ | |
| 95 | typedef 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 | */ | |
| 107 | typedef 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 | */ | |
| 119 | typedef 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 | */ | |
| 130 | typedef const struct GhosttyKittyGraphicsImageImpl* GhosttyKittyGraphicsImage; | |
| 131 | ||
| 132 | /** | |
| 133 | * Opaque handle to a Kitty graphics placement iterator. | |
| 134 | * | |
| 135 | * @ingroup kitty_graphics | |
| 136 | */ | |
| 137 | typedef struct GhosttyKittyGraphicsPlacementIteratorImpl* GhosttyKittyGraphicsPlacementIterator; | |
| 138 | ||
| 139 | /** | |
| 140 | * Opaque handle to a render state instance. | |
| 141 | * | |
| 142 | * @ingroup render | |
| 143 | */ | |
| 144 | typedef struct GhosttyRenderStateImpl* GhosttyRenderState; | |
| 145 | ||
| 146 | /** | |
| 147 | * Opaque handle to a render-state row iterator. | |
| 148 | * | |
| 149 | * @ingroup render | |
| 150 | */ | |
| 151 | typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator; | |
| 152 | ||
| 153 | /** | |
| 154 | * Opaque handle to render-state row cells. | |
| 155 | * | |
| 156 | * @ingroup render | |
| 157 | */ | |
| 158 | typedef 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 | */ | |
| 168 | typedef struct GhosttySgrParserImpl* GhosttySgrParser; | |
| 169 | ||
| 170 | /** | |
| 171 | * Opaque handle to a formatter instance. | |
| 172 | * | |
| 173 | * @ingroup formatter | |
| 174 | */ | |
| 175 | typedef 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 | */ | |
| 185 | typedef 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 | */ | |
| 195 | typedef struct GhosttyOscCommandImpl* GhosttyOscCommand; | |
| 196 | ||
| 197 | /* ---- Common value types ---- */ | |
| 198 | ||
| 199 | /** | |
| 200 | * Terminal content output format. | |
| 201 | * | |
| 202 | * @ingroup formatter | |
| 203 | */ | |
| 204 | typedef 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 | */ | |
| 222 | typedef 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 | */ | |
| 236 | typedef 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 | */ | |
| 254 | typedef 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 | */ | |
| 271 | typedef 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 | */ | |
| 333 | GHOSTTY_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 | |
| 35 | extern "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 | */ | |
| 69 | GHOSTTY_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 | */ | |
| 144 | GHOSTTY_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 | */ | |
| 78 | GHOSTTY_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 | */ | |
| 86 | GHOSTTY_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 | */ | |
| 95 | GHOSTTY_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 | */ | |
| 104 | GHOSTTY_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 | */ | |
| 113 | GHOSTTY_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 | */ | |
| 122 | GHOSTTY_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 | */ | |
| 130 | GHOSTTY_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 | */ | |
| 138 | GHOSTTY_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 | */ | |
| 146 | GHOSTTY_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 | */ | |
| 154 | GHOSTTY_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. | |
| 4 | module 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 @@ |
| 1 | import { execFileSync } from "node:child_process"; | |
| 2 | import { createHash } from "node:crypto"; | |
| 3 | import { readdir, readFile } from "node:fs/promises"; | |
| 4 | import { resolve } from "node:path"; | |
| 5 | import { fileURLToPath } from "node:url"; | |
| 6 | ||
| 7 | const vendorDir = new URL("./", import.meta.url); | |
| 8 | const pinnedCommit = (await readFile(new URL("COMMIT", vendorDir), "utf8")).trim(); | |
| 9 | const wasm = await readFile(new URL("ghostty-vt.wasm", vendorDir)); | |
| 10 | const expectedHash = (await readFile(new URL("SHA256", vendorDir), "utf8")).trim(); | |
| 11 | const actualHash = createHash("sha256").update(wasm).digest("hex"); | |
| 12 | if (actualHash !== expectedHash) { | |
| 13 | throw new Error(`ghostty-vt.wasm SHA-256 mismatch: expected ${expectedHash}, got ${actualHash}`); | |
| 14 | } | |
| 15 | ||
| 16 | const instance = await WebAssembly.instantiate(wasm, { env: { log() {} } }); | |
| 17 | const exports = instance.instance.exports; | |
| 18 | if (!(exports.memory instanceof WebAssembly.Memory)) throw new Error("wasm memory export missing"); | |
| 19 | if (!(exports.__indirect_function_table instanceof WebAssembly.Table)) { | |
| 20 | throw new Error("wasm function table export missing"); | |
| 21 | } | |
| 22 | ||
| 23 | const runtimeSources = await Promise.all( | |
| 24 | ["module.ts", "terminal.ts"].map((file) => | |
| 25 | readFile(new URL(`../src/ghostty/${file}`, vendorDir), "utf8"), | |
| 26 | ), | |
| 27 | ); | |
| 28 | const requiredExports = new Set(["ghostty_type_json"]); | |
| 29 | const requiredLayouts = new Map(); | |
| 30 | const 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 | }; | |
| 38 | for (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 | } | |
| 52 | for (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 | } | |
| 72 | for (const name of requiredExports) { | |
| 73 | if (typeof exports[name] !== "function") throw new Error(`required wasm export missing: ${name}`); | |
| 74 | } | |
| 75 | const jsonPtr = exports.ghostty_type_json(); | |
| 76 | const bytes = new Uint8Array(exports.memory.buffer, jsonPtr); | |
| 77 | const end = bytes.indexOf(0); | |
| 78 | if (end < 0) throw new Error("type metadata is not null-terminated"); | |
| 79 | const layout = JSON.parse(new TextDecoder().decode(bytes.subarray(0, end))); | |
| 80 | for (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 | } | |
| 105 | const ascii = layout.GhosttyRenderStateAsciiCell; | |
| 106 | if ( | |
| 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 | ||
| 119 | try { | |
| 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 | ||
| 132 | const sourceArg = process.argv[2]; | |
| 133 | if (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 | ||
| 199 | console.log(`ghostty vendor verified: commit ${pinnedCommit}, sha256 ${actualHash}`); |
pnpm-lock.yaml+48-11| ... | ... | @@ -760,6 +760,9 @@ importers: |
| 760 | 760 | '@clerk/react': |
| 761 | 761 | specifier: 6.16.1 |
| 762 | 762 | 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 | |
| 763 | 766 | '@daypicker/react': |
| 764 | 767 | specifier: ^10.0.1 |
| 765 | 768 | version: 10.0.1(@types/react@19.3.0)(react@19.2.6) |
| ... | ... | @@ -1399,7 +1402,7 @@ importers: |
| 1399 | 1402 | version: 1.15.13 |
| 1400 | 1403 | '@opencode/client': |
| 1401 | 1404 | 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) | |
| 1403 | 1406 | '@opencode/protocol': |
| 1404 | 1407 | specifier: 2.0.28 |
| 1405 | 1408 | version: 2.0.28 |
| ... | ... | @@ -1832,6 +1835,30 @@ importers: |
| 1832 | 1835 | specifier: 1.0.0 |
| 1833 | 1836 | 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) |
| 1834 | 1837 | |
| 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 | ||
| 1835 | 1862 | scripts: |
| 1836 | 1863 | dependencies: |
| 1837 | 1864 | '@effect/platform-node': |
| ... | ... | @@ -11203,14 +11230,14 @@ packages: |
| 11203 | 11230 | resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} |
| 11204 | 11231 | engines: {node: '>=10'} |
| 11205 | 11232 | |
| 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==} | |
| 11208 | 11235 | engines: {node: '>=10'} |
| 11209 | 11236 | peerDependencies: |
| 11210 | 11237 | seroval: ^1.0 |
| 11211 | 11238 | |
| 11212 | seroval@1.5.4: | |
| 11213 | resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} | |
| 11239 | seroval@1.6.9: | |
| 11240 | resolution: {integrity: sha512-UeEzHwAK+yl0YLbsvROV1t5yMYEwZZ0CfKYxDvUgS2fO9Vxdf8djqVIB+OuVpuawRNPqw68LE1Du/GMsW2ZPqA==} | |
| 11214 | 11241 | engines: {node: '>=10'} |
| 11215 | 11242 | |
| 11216 | 11243 | serve-static@1.16.3: |
| ... | ... | @@ -11333,6 +11360,9 @@ packages: |
| 11333 | 11360 | resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} |
| 11334 | 11361 | engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} |
| 11335 | 11362 | |
| 11363 | solid-js@1.9.17: | |
| 11364 | resolution: {integrity: sha512-4GAmLOWPfNV8H9Jfn2LSFTpNEG6jiOKPiwd5lnLGQtehHaX6l3TOLzJqQ7mqwB/3k1Yy3zyumFVopmc/tm34ow==} | |
| 11365 | ||
| 11336 | 11366 | source-map-js@1.2.1: |
| 11337 | 11367 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} |
| 11338 | 11368 | engines: {node: '>=0.10.0'} |
| ... | ... | @@ -15369,12 +15399,13 @@ snapshots: |
| 15369 | 15399 | dependencies: |
| 15370 | 15400 | cross-spawn: 7.0.6 |
| 15371 | 15401 | |
| 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)': | |
| 15373 | 15403 | dependencies: |
| 15374 | 15404 | '@opencode/protocol': 2.0.28 |
| 15375 | 15405 | '@opencode/schema': 2.0.28 |
| 15376 | 15406 | optionalDependencies: |
| 15377 | 15407 | effect: 4.0.4 |
| 15408 | solid-js: 1.9.17 | |
| 15378 | 15409 | |
| 15379 | 15410 | '@opencode/protocol@2.0.28': |
| 15380 | 15411 | dependencies: |
| ... | ... | @@ -16520,8 +16551,8 @@ snapshots: |
| 16520 | 16551 | dependencies: |
| 16521 | 16552 | '@tanstack/history': 1.162.0 |
| 16522 | 16553 | 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) | |
| 16525 | 16556 | |
| 16526 | 16557 | '@tanstack/router-generator@1.167.12': |
| 16527 | 16558 | dependencies: |
| ... | ... | @@ -22611,11 +22642,11 @@ snapshots: |
| 22611 | 22642 | type-fest: 0.13.1 |
| 22612 | 22643 | optional: true |
| 22613 | 22644 | |
| 22614 | seroval-plugins@1.5.4(seroval@1.5.4): | |
| 22645 | seroval-plugins@1.6.9(seroval@1.6.9): | |
| 22615 | 22646 | dependencies: |
| 22616 | seroval: 1.5.4 | |
| 22647 | seroval: 1.6.9 | |
| 22617 | 22648 | |
| 22618 | seroval@1.5.4: {} | |
| 22649 | seroval@1.6.9: {} | |
| 22619 | 22650 | |
| 22620 | 22651 | serve-static@1.16.3: |
| 22621 | 22652 | dependencies: |
| ... | ... | @@ -22788,6 +22819,12 @@ snapshots: |
| 22788 | 22819 | smart-buffer: 4.2.0 |
| 22789 | 22820 | optional: true |
| 22790 | 22821 | |
| 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 | ||
| 22791 | 22828 | source-map-js@1.2.1: {} |
| 22792 | 22829 | |
| 22793 | 22830 | source-map-support@0.5.21: |
third-party-licenses.config.json+25-3| ... | ... | @@ -101,9 +101,31 @@ |
| 101 | 101 | }, |
| 102 | 102 | { |
| 103 | 103 | "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 | ], | |
| 107 | 129 | "sourceUrl": "https://github.com/ryanoasis/nerd-fonts" |
| 108 | 130 | }, |
| 109 | 131 | { |