docs: update README, AGENTS.md, Makefile, and architecture docs
CI / check (push) Has been cancelled

This commit is contained in:
2026-06-04 22:10:50 -04:00
parent a7b5c13d4b
commit ac18f65094
9 changed files with 951 additions and 8 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
## Project Structure & Module Organization ## Project Structure & Module Organization
`cmd/webterm/` contains the CLI entrypoint. Core server, session, Docker, replay, screenshot, and static-serving code lives in `webterm/`. Shared internal helpers live in `internal/`. Frontend terminal code is in `webterm/static/js/terminal.ts`, with the bundled output committed as `webterm/static/js/terminal.js`. Static assets such as fonts, icons, and WASM files live under `webterm/static/`. Documentation and reference media live in `docs/`. `cmd/webterm/` contains the CLI entrypoint. Core server, session, Docker, replay, screenshot, and static-serving code lives in `webterm/`. Shared internal helpers live in `internal/`. Frontend terminal code is in `webterm/static/js/terminal.ts`, which bundles to generated output at `webterm/static/js/terminal.js`. Static assets such as fonts, icons, and WASM files live under `webterm/static/`. Documentation and reference media live in `docs/`.
## Build, Test, and Development Commands ## Build, Test, and Development Commands
@@ -18,7 +18,7 @@
## Coding Style & Naming Conventions ## Coding Style & Naming Conventions
Use `gofmt` for Go formatting; run `make format` before submitting Go-heavy changes. Follow existing Go naming: exported identifiers use `CamelCase`, internal helpers use `camelCase`, and tests live beside source files. TypeScript in `webterm/static/js/` uses strict mode, 2-space indentation, and direct DOM-oriented code rather than framework abstractions. Keep generated bundles in sync with source changes. Use `gofmt` for Go formatting; run `make format` before submitting Go-heavy changes. Follow existing Go naming: exported identifiers use `CamelCase`, internal helpers use `camelCase`, and tests live beside source files. TypeScript in `webterm/static/js/` uses strict mode, 2-space indentation, and direct DOM-oriented code rather than framework abstractions. Rebuild generated bundles when frontend source changes.
## Testing Guidelines ## Testing Guidelines
+1 -1
View File
@@ -57,7 +57,7 @@ bundle-watch: node_modules ## Watch mode for frontend development
@test -f $(GHOSTTY_WASM) || bun run copy-wasm @test -f $(GHOSTTY_WASM) || bun run copy-wasm
bun run watch bun run watch
build-go: ## Build Go CLI binary build-go: build-fast ## Build Go CLI binary
cd $(GO_DIR) && mkdir -p bin && go build -ldflags "$(GO_VERSION_LDFLAGS)" -o ./bin/webterm ./cmd/webterm cd $(GO_DIR) && mkdir -p bin && go build -ldflags "$(GO_VERSION_LDFLAGS)" -o ./bin/webterm ./cmd/webterm
clean: ## Remove coverage artifacts clean: ## Remove coverage artifacts
+4 -3
View File
@@ -35,23 +35,24 @@ go install github.com/rcarmo/webterm/cmd/webterm@latest
```bash ```bash
git clone https://github.com/rcarmo/webterm.git git clone https://github.com/rcarmo/webterm.git
cd webterm cd webterm
mkdir -p bin make build-go
go build -o ./bin/webterm ./cmd/webterm
``` ```
The command above produces `bin/webterm`; you can also build it from repo root with `make build-go`. That produces `bin/webterm` and rebuilds generated frontend assets first.
## Quick start ## Quick start
Run a default shell session: Run a default shell session:
```bash ```bash
make build-fast
go run ./cmd/webterm go run ./cmd/webterm
``` ```
Run a specific command: Run a specific command:
```bash ```bash
make build-fast
go run ./cmd/webterm -- htop go run ./cmd/webterm -- htop
``` ```
+2 -2
View File
@@ -5,7 +5,7 @@
`webterm` is a Go HTTP/WebSocket server that hosts one or more terminal sessions and renders screenshot/telemetry surfaces for a dashboard UI. `webterm` is a Go HTTP/WebSocket server that hosts one or more terminal sessions and renders screenshot/telemetry surfaces for a dashboard UI.
``` ```
Browser (terminal.js + ghostty-vt.wasm) Browser (generated terminal.js + ghostty-vt.wasm)
│ WS / HTTP / SSE │ WS / HTTP / SSE
@@ -42,7 +42,7 @@ webterm/server.go (LocalServer)
Assets live in `webterm/static`: Assets live in `webterm/static`:
- `js/terminal.ts` source - `js/terminal.ts` source
- `js/terminal.js` bundled client - `js/terminal.js` generated bundled client
- `js/ghostty-vt.wasm` - `js/ghostty-vt.wasm`
- `monospace.css`, icons, `manifest.json` - `monospace.css`, icons, `manifest.json`
+146
View File
@@ -0,0 +1,146 @@
declare const __WEBTERM_BUILD_VERSION__: string;
export type FrontendLogLevel = "debug" | "info" | "warn" | "error";
export interface WebtermScopedLogger {
debug: (text: string, extra?: Record<string, unknown>) => void;
info: (text: string, extra?: Record<string, unknown>) => void;
warn: (text: string, extra?: Record<string, unknown>) => void;
error: (text: string, extra?: Record<string, unknown>) => void;
}
declare global {
interface Window {
webtermClientLog?: (text: string, extra?: Record<string, unknown>) => Promise<void>;
webtermClientLogSessionID?: string;
webtermClientBuildVersion?: string;
webtermLogs?: {
bug: WebtermScopedLogger;
ui: WebtermScopedLogger;
voice: WebtermScopedLogger;
ws: WebtermScopedLogger;
};
}
}
export const FRONTEND_BUILD_VERSION = __WEBTERM_BUILD_VERSION__;
function createFrontendLogSessionID(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let out = "";
for (let i = 0; i < 24; i += 1) {
out += chars[Math.floor(Math.random() * chars.length)];
}
return out;
}
export const frontendLogSessionID = createFrontendLogSessionID();
function normalizeFrontendLogValue(value: unknown): unknown {
if (value instanceof Error) {
return {
name: value.name,
message: value.message,
stack: value.stack,
};
}
if (Array.isArray(value)) {
return value.map((item) => normalizeFrontendLogValue(item));
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([key, item]) => [key, normalizeFrontendLogValue(item)])
);
}
return value;
}
function serializeFrontendLogContext(extra: Record<string, unknown> = {}): string {
const entries = Object.entries(extra);
if (entries.length === 0) {
return "";
}
try {
return JSON.stringify(Object.fromEntries(entries.map(([key, value]) => [key, normalizeFrontendLogValue(value)])));
} catch {
return String(extra);
}
}
export async function postFrontendLog(
level: FrontendLogLevel,
text: string,
extra: Record<string, unknown> = {}
): Promise<void> {
const message = typeof text === "string" ? text.trim() : "";
if (!message) return;
try {
await fetch("/api/client-log", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({
session_id: frontendLogSessionID,
text: message,
frontend_version: FRONTEND_BUILD_VERSION,
level,
context: serializeFrontendLogContext(extra),
}),
keepalive: true,
});
} catch (error) {
console.warn("[webterm] Failed to post client log:", error);
}
}
function logFrontend(level: FrontendLogLevel, text: string, extra: Record<string, unknown> = {}): void {
void postFrontendLog(level, text, extra);
}
function logFrontendDebug(text: string, extra: Record<string, unknown> = {}): void {
logFrontend("debug", text, extra);
}
function logFrontendInfo(text: string, extra: Record<string, unknown> = {}): void {
logFrontend("info", text, extra);
}
function logFrontendWarn(text: string, extra: Record<string, unknown> = {}): void {
logFrontend("warn", text, extra);
}
function logFrontendError(text: string, extra: Record<string, unknown> = {}): void {
logFrontend("error", text, extra);
}
export function createScopedFrontendLogger(scope: string): WebtermScopedLogger {
const withScope = (extra: Record<string, unknown> = {}): Record<string, unknown> => ({
scope,
...extra,
});
return {
debug: (text, extra = {}) => logFrontendDebug(text, withScope(extra)),
info: (text, extra = {}) => logFrontendInfo(text, withScope(extra)),
warn: (text, extra = {}) => logFrontendWarn(text, withScope(extra)),
error: (text, extra = {}) => logFrontendError(text, withScope(extra)),
};
}
export const bugLog = createScopedFrontendLogger("bug");
export const uiLog = createScopedFrontendLogger("ui");
export const voiceLog = createScopedFrontendLogger("voice");
export const wsLog = createScopedFrontendLogger("ws");
window.webtermClientLog = (text: string, extra: Record<string, unknown> = {}) =>
postFrontendLog("info", text, extra);
window.webtermClientLogSessionID = frontendLogSessionID;
window.webtermClientBuildVersion = FRONTEND_BUILD_VERSION;
window.webtermLogs = {
bug: bugLog,
ui: uiLog,
voice: voiceLog,
ws: wsLog,
};
+65
View File
@@ -0,0 +1,65 @@
const sharedScriptLoads = new Map<string, Promise<void>>();
/** Shared TextDecoder (stateless for UTF-8, safe to share) */
export const sharedTextDecoder = new TextDecoder();
/** Get WASM path based on script location */
export function getWasmPath(): string {
const scripts = document.querySelectorAll('script[src*="terminal.js"]');
if (scripts.length > 0) {
const scriptSrc = (scripts[0] as HTMLScriptElement).src;
const basePath = scriptSrc.substring(0, scriptSrc.lastIndexOf("/") + 1);
return basePath + "ghostty-vt.wasm";
}
return "/static/js/ghostty-vt.wasm";
}
export function getStaticJSBasePath(): string {
const scripts = document.querySelectorAll('script[src*="terminal.js"]');
if (scripts.length > 0) {
const scriptSrc = (scripts[0] as HTMLScriptElement).src;
return scriptSrc.substring(0, scriptSrc.lastIndexOf("/") + 1);
}
return "/static/js/";
}
export function loadScriptOnce(src: string): Promise<void> {
const existing = sharedScriptLoads.get(src);
if (existing) {
return existing;
}
const promise = new Promise<void>((resolve, reject) => {
const script = document.createElement("script");
script.src = src;
script.async = true;
script.onload = () => resolve();
script.onerror = () => {
sharedScriptLoads.delete(src);
reject(new Error(`Failed to load script: ${src}`));
};
document.head.appendChild(script);
});
sharedScriptLoads.set(src, promise);
return promise;
}
export function formatSherpaStatus(status: string): string {
if (!status) {
return "";
}
if (status === "Running...") {
return "Model ready";
}
const downloadMatch = status.match(/Downloading data... \((\d+)\/(\d+)\)/);
if (!downloadMatch) {
return status;
}
const downloaded = BigInt(downloadMatch[1]);
const total = BigInt(downloadMatch[2]);
const percent =
total === 0n ? 0 : Number((downloaded * 10_000n) / total) / 100;
return `Downloading ${percent.toFixed(2)}%`;
}
+55
View File
@@ -0,0 +1,55 @@
import type { ITheme } from "ghostty-web";
import { uiLog } from "./frontend-log";
import { DEFAULT_FONT_FAMILY, THEMES } from "./terminal-themes";
export interface TerminalConfig {
fontFamily?: string;
fontSize?: number;
scrollback?: number;
theme?: ITheme;
}
/** Parse configuration from element data attributes */
export function parseConfig(element: HTMLElement): TerminalConfig {
const config: TerminalConfig = {};
if (element.dataset.fontFamily) {
let fontFamily = element.dataset.fontFamily;
// Resolve CSS variables - Canvas 2D context doesn't understand var(--name) syntax
if (fontFamily.startsWith("var(")) {
const varMatch = fontFamily.match(/var\(([^)]+)\)/);
if (varMatch) {
const varName = varMatch[1].trim();
const resolved = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
if (resolved) {
fontFamily = resolved;
} else {
uiLog.warn("CSS variable not found; using default font", { varName });
fontFamily = DEFAULT_FONT_FAMILY;
}
}
}
config.fontFamily = fontFamily;
}
if (element.dataset.fontSize) {
config.fontSize = parseInt(element.dataset.fontSize, 10);
}
if (element.dataset.scrollback) {
config.scrollback = parseInt(element.dataset.scrollback, 10);
}
if (element.dataset.theme) {
const themeName = element.dataset.theme.toLowerCase();
if (themeName in THEMES) {
config.theme = THEMES[themeName];
} else {
try {
config.theme = JSON.parse(element.dataset.theme) as ITheme;
} catch (error) {
uiLog.warn("Unknown theme configuration", { theme: element.dataset.theme, error });
}
}
}
return config;
}
+278
View File
@@ -0,0 +1,278 @@
export function isMobileDevice(): boolean {
const touchPoints = navigator.maxTouchPoints || 0;
const coarsePointer =
window.matchMedia?.("(pointer: coarse)").matches ||
window.matchMedia?.("(any-pointer: coarse)").matches ||
false;
const isiPadDesktopMode =
/Macintosh/i.test(navigator.userAgent) && touchPoints > 1;
const hasTouchEvents = "ontouchstart" in window;
return (
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
) ||
isiPadDesktopMode ||
touchPoints > 0 ||
hasTouchEvents ||
coarsePointer
);
}
const SHIFT_KEY_MAP: Record<string, string> = {
"`": "~",
"1": "!",
"2": "@",
"3": "#",
"4": "$",
"5": "%",
"6": "^",
"7": "&",
"8": "*",
"9": "(",
"0": ")",
"-": "_",
"=": "+",
"[": "{",
"]": "}",
"\\": "|",
";": ":",
"'": "\"",
",": "<",
".": ">",
"/": "?",
};
const CTRL_KEY_MAP: Record<string, string> = {
"2": "@",
"3": "[",
"4": "\\",
"5": "]",
"6": "^",
"7": "_",
"8": "?",
};
export type VirtualKeyboardActionId =
| "mode-alpha"
| "mode-symbol"
| "toggle-voice";
export type VirtualKeyboardKey = {
kind: "char" | "modifier" | "action" | "arrow" | "space";
label: string;
modifier?: "ctrl" | "alt" | "shift" | "fn";
shiftLabel?: string;
shiftValue?: string;
value?: string;
seq?: string;
width?: number;
actionId?: VirtualKeyboardActionId;
};
export const VIRTUAL_KEYBOARD_ALPHA_LAYOUT: VirtualKeyboardKey[][] = [
[
{ kind: "char", label: "q", shiftLabel: "Q" },
{ kind: "char", label: "w", shiftLabel: "W" },
{ kind: "char", label: "e", shiftLabel: "E" },
{ kind: "char", label: "r", shiftLabel: "R" },
{ kind: "char", label: "t", shiftLabel: "T" },
{ kind: "char", label: "y", shiftLabel: "Y" },
{ kind: "char", label: "u", shiftLabel: "U" },
{ kind: "char", label: "i", shiftLabel: "I" },
{ kind: "char", label: "o", shiftLabel: "O" },
{ kind: "char", label: "p", shiftLabel: "P" },
],
[
{ kind: "char", label: "a", shiftLabel: "A" },
{ kind: "char", label: "s", shiftLabel: "S" },
{ kind: "char", label: "d", shiftLabel: "D" },
{ kind: "char", label: "f", shiftLabel: "F" },
{ kind: "char", label: "g", shiftLabel: "G" },
{ kind: "char", label: "h", shiftLabel: "H" },
{ kind: "char", label: "j", shiftLabel: "J" },
{ kind: "char", label: "k", shiftLabel: "K" },
{ kind: "char", label: "l", shiftLabel: "L" },
],
[
{ kind: "modifier", label: "⇧", modifier: "shift" },
{ kind: "char", label: "z", shiftLabel: "Z" },
{ kind: "char", label: "x", shiftLabel: "X" },
{ kind: "char", label: "c", shiftLabel: "C" },
{ kind: "char", label: "v", shiftLabel: "V" },
{ kind: "char", label: "b", shiftLabel: "B" },
{ kind: "char", label: "n", shiftLabel: "N" },
{ kind: "char", label: "m", shiftLabel: "M" },
{ kind: "action", label: "⌫", value: "Backspace", seq: "\x7f" },
],
[
{ kind: "action", label: "123", actionId: "mode-symbol", width: 1.05 },
{ kind: "modifier", label: "Ctrl", modifier: "ctrl", width: 1.05 },
{ kind: "modifier", label: "Alt", modifier: "alt", width: 1.05 },
{ kind: "space", label: "␣", value: " ", width: 3.1 },
{ kind: "action", label: "Mic", actionId: "toggle-voice", width: 1.15 },
{ kind: "action", label: "⏎", seq: "\r", width: 1.15 },
],
];
export const VIRTUAL_KEYBOARD_SYMBOL_LAYOUT: VirtualKeyboardKey[][] = [
[
{ kind: "char", label: "1", value: "1" },
{ kind: "char", label: "2", value: "2" },
{ kind: "char", label: "3", value: "3" },
{ kind: "char", label: "4", value: "4" },
{ kind: "char", label: "5", value: "5" },
{ kind: "char", label: "6", value: "6" },
{ kind: "char", label: "7", value: "7" },
{ kind: "char", label: "8", value: "8" },
{ kind: "char", label: "9", value: "9" },
{ kind: "char", label: "0", value: "0" },
],
[
{ kind: "char", label: "-", value: "-" },
{ kind: "char", label: "/", value: "/" },
{ kind: "char", label: ":", value: ":" },
{ kind: "char", label: ";", value: ";" },
{ kind: "char", label: "(", value: "(" },
{ kind: "char", label: ")", value: ")" },
{ kind: "char", label: "$", value: "$" },
{ kind: "char", label: "&", value: "&" },
{ kind: "char", label: "@", value: "@" },
{ kind: "char", label: "\"", value: "\"" },
],
[
{ kind: "action", label: "Esc", seq: "\x1b", width: 1.35 },
{ kind: "char", label: ".", value: "." },
{ kind: "char", label: ",", value: "," },
{ kind: "char", label: "?", value: "?" },
{ kind: "char", label: "!", value: "!" },
{ kind: "char", label: "'", value: "'" },
{ kind: "char", label: "[", value: "[" },
{ kind: "char", label: "]", value: "]" },
{ kind: "action", label: "⌫", value: "Backspace", seq: "\x7f", width: 1.35 },
],
[
{ kind: "action", label: "ABC", actionId: "mode-alpha", width: 1.1 },
{ kind: "modifier", label: "Ctrl", modifier: "ctrl", width: 1.1 },
{ kind: "modifier", label: "Alt", modifier: "alt", width: 1.1 },
{ kind: "space", label: "␣", value: " ", width: 4 },
{ kind: "action", label: "⏎", seq: "\r", width: 1.35 },
],
];
export type VirtualKeyboardKeyBounds = {
x: number;
y: number;
w: number;
h: number;
rowIndex: number;
colIndex: number;
key: VirtualKeyboardKey;
label: string;
value: string;
};
export type ActiveVirtualKeyboardPress = {
pointerId: number;
keyIndex: number;
key: VirtualKeyboardKeyBounds | null;
repeatTimeout?: number;
};
const FN_NORMAL_KEYS = [
"\x1bOP",
"\x1bOQ",
"\x1bOR",
"\x1bOS",
"\x1b[15~",
"\x1b[17~",
"\x1b[18~",
"\x1b[19~",
"\x1b[20~",
"\x1b[21~",
];
const FN_SHIFT_KEYS = [
"\x1b[23~",
"\x1b[24~",
"\x1b[25~",
"\x1b[26~",
"\x1b[28~",
"\x1b[29~",
"\x1b[31~",
"\x1b[32~",
"\x1b[33~",
"\x1b[34~",
];
function applyShiftModifier(key: string): string {
if (key.length !== 1) {
return key;
}
if (key >= "a" && key <= "z") {
return key.toUpperCase();
}
return SHIFT_KEY_MAP[key] ?? key;
}
export function applyCtrlModifier(key: string): string {
if (key.length !== 1) {
return key;
}
const mapped = CTRL_KEY_MAP[key] ?? key;
if (mapped === "?") {
return "\x7f";
}
const code = mapped.toUpperCase().charCodeAt(0);
if (code >= 64 && code <= 95) {
return String.fromCharCode(code - 64);
}
return key;
}
export function applyFnModifier(key: string, useShift: boolean): string | null {
if (key.length !== 1) {
return null;
}
const index = "1234567890".indexOf(key);
if (index < 0) {
return null;
}
return useShift ? FN_SHIFT_KEYS[index] : FN_NORMAL_KEYS[index];
}
export function applyAltModifier(text: string): string {
if (!text || text.startsWith("\x1b")) {
return text;
}
return `\x1b${text}`;
}
export function applyModifiers(
text: string,
useShift: boolean,
useCtrl: boolean,
useAlt: boolean,
useFn: boolean
): string {
if (text.length !== 1) {
return text;
}
if (useFn) {
const fnApplied = applyFnModifier(text, useShift);
if (fnApplied) {
return useAlt ? applyAltModifier(fnApplied) : fnApplied;
}
}
if (useCtrl) {
const ctrlApplied = applyCtrlModifier(text);
if (ctrlApplied !== text) {
return useAlt ? applyAltModifier(ctrlApplied) : ctrlApplied;
}
}
if (useShift) {
const shifted = applyShiftModifier(text);
return useAlt ? applyAltModifier(shifted) : shifted;
}
return useAlt ? applyAltModifier(text) : text;
}
+398
View File
@@ -0,0 +1,398 @@
import type { ITheme } from "ghostty-web";
/** Default font stack - prefers system monospace, falls back through programming fonts */
export const DEFAULT_FONT_FAMILY =
'ui-monospace, "SFMono-Regular", "FiraCode Nerd Font", "FiraMono Nerd Font", ' +
'"Fira Code", "Roboto Mono", Menlo, Monaco, Consolas, "Liberation Mono", ' +
'"DejaVu Sans Mono", "Courier New", monospace';
/** Predefined terminal themes */
export const THEMES: Record<string, ITheme> = {
// Tango - default theme (GNOME/xterm.js colors)
tango: {
background: "#000000",
foreground: "#d3d7cf",
cursor: "#d3d7cf",
cursorAccent: "#000000",
selectionBackground: "#d3d7cf",
selectionForeground: "#000000",
black: "#2e3436",
red: "#cc0000",
green: "#4e9a06",
yellow: "#c4a000",
blue: "#3465a4",
magenta: "#75507b",
cyan: "#06989a",
white: "#d3d7cf",
brightBlack: "#555753",
brightRed: "#ef2929",
brightGreen: "#8ae234",
brightYellow: "#fce94f",
brightBlue: "#729fcf",
brightMagenta: "#ad7fa8",
brightCyan: "#34e2e2",
brightWhite: "#eeeeec",
},
// Classic xterm (VGA colors, pure black background)
xterm: {
background: "#000000",
foreground: "#e5e5e5",
cursor: "#e5e5e5",
cursorAccent: "#000000",
selectionBackground: "#e5e5e5",
selectionForeground: "#000000",
black: "#000000",
red: "#cd0000",
green: "#00cd00",
yellow: "#cdcd00",
blue: "#0000cd",
magenta: "#cd00cd",
cyan: "#00cdcd",
white: "#e5e5e5",
brightBlack: "#4d4d4d",
brightRed: "#ff0000",
brightGreen: "#00ff00",
brightYellow: "#ffff00",
brightBlue: "#0000ff",
brightMagenta: "#ff00ff",
brightCyan: "#00ffff",
brightWhite: "#ffffff",
},
// Monokai Classic
monokai: {
background: "#272822",
foreground: "#f8f8f2",
cursor: "#f8f8f2",
cursorAccent: "#272822",
selectionBackground: "#75715e",
selectionForeground: "#f8f8f2",
black: "#272822",
red: "#f92672",
green: "#a6e22e",
yellow: "#f4bf75",
blue: "#66d9ef",
magenta: "#ae81ff",
cyan: "#a1efe4",
white: "#f8f8f2",
brightBlack: "#75715e",
brightRed: "#f92672",
brightGreen: "#a6e22e",
brightYellow: "#f4bf75",
brightBlue: "#66d9ef",
brightMagenta: "#ae81ff",
brightCyan: "#a1efe4",
brightWhite: "#f9f8f5",
},
"monokai-pro": {
background: "#2d2a2e",
foreground: "#fcfcfa",
cursor: "#fcfcfa",
cursorAccent: "#2d2a2e",
selectionBackground: "#5b595c",
selectionForeground: "#fcfcfa",
black: "#403e41",
red: "#ff6188",
green: "#a9dc76",
yellow: "#ffd866",
blue: "#78dce8",
magenta: "#ab9df2",
cyan: "#78dce8",
white: "#fcfcfa",
brightBlack: "#727072",
brightRed: "#ff6188",
brightGreen: "#a9dc76",
brightYellow: "#ffd866",
brightBlue: "#78dce8",
brightMagenta: "#ab9df2",
brightCyan: "#78dce8",
brightWhite: "#ffffff",
},
ristretto: {
background: "#2c2c2c",
foreground: "#eeeeee",
cursor: "#eeeeee",
cursorAccent: "#2c2c2c",
selectionBackground: "#7f7f7f",
selectionForeground: "#eeeeee",
black: "#2c2c2c",
red: "#cdaf95",
green: "#a8ff60",
yellow: "#bfbb1f",
blue: "#75a5b0",
magenta: "#ff73fd",
cyan: "#5ffdff",
white: "#b9b9b9",
brightBlack: "#545454",
brightRed: "#fcb08f",
brightGreen: "#a8ff60",
brightYellow: "#fffeb7",
brightBlue: "#a5c7ff",
brightMagenta: "#ff9cfe",
brightCyan: "#d5ffff",
brightWhite: "#ffffff",
},
dark: {
background: "#1e1e1e",
foreground: "#d4d4d4",
cursor: "#d4d4d4",
cursorAccent: "#1e1e1e",
selectionBackground: "#264f78",
selectionForeground: "#d4d4d4",
black: "#000000",
red: "#cd3131",
green: "#0dbc79",
yellow: "#e5e510",
blue: "#2472c8",
magenta: "#bc3fbc",
cyan: "#11a8cd",
white: "#e5e5e5",
brightBlack: "#666666",
brightRed: "#f14c4c",
brightGreen: "#23d18b",
brightYellow: "#f5f543",
brightBlue: "#3b8eea",
brightMagenta: "#d670d6",
brightCyan: "#29b8db",
brightWhite: "#e5e5e5",
},
light: {
background: "#ffffff",
foreground: "#000000",
cursor: "#000000",
cursorAccent: "#ffffff",
selectionBackground: "#add6ff",
selectionForeground: "#000000",
black: "#000000",
red: "#cd3131",
green: "#00bc00",
yellow: "#949800",
blue: "#0451a5",
magenta: "#bc05bc",
cyan: "#0598bc",
white: "#555555",
brightBlack: "#666666",
brightRed: "#cd3131",
brightGreen: "#14ce14",
brightYellow: "#b5ba00",
brightBlue: "#0451a5",
brightMagenta: "#bc05bc",
brightCyan: "#0598bc",
brightWhite: "#a5a5a5",
},
dracula: {
background: "#282a36",
foreground: "#f8f8f2",
cursor: "#f8f8f2",
cursorAccent: "#282a36",
selectionBackground: "#44475a",
selectionForeground: "#f8f8f2",
black: "#21222c",
red: "#ff5555",
green: "#50fa7b",
yellow: "#f1fa8c",
blue: "#bd93f9",
magenta: "#ff79c6",
cyan: "#8be9fd",
white: "#f8f8f2",
brightBlack: "#6272a4",
brightRed: "#ff6e6e",
brightGreen: "#69ff94",
brightYellow: "#ffffa5",
brightBlue: "#d6acff",
brightMagenta: "#ff92df",
brightCyan: "#a4ffff",
brightWhite: "#ffffff",
},
catppuccin: {
background: "#1e1e2e",
foreground: "#cdd6f4",
cursor: "#f5e0dc",
cursorAccent: "#1e1e2e",
selectionBackground: "#585b70",
selectionForeground: "#cdd6f4",
black: "#45475a",
red: "#f38ba8",
green: "#a6e3a1",
yellow: "#f9e2af",
blue: "#89b4fa",
magenta: "#f5c2e7",
cyan: "#94e2d5",
white: "#bac2de",
brightBlack: "#585b70",
brightRed: "#f38ba8",
brightGreen: "#a6e3a1",
brightYellow: "#f9e2af",
brightBlue: "#89b4fa",
brightMagenta: "#f5c2e7",
brightCyan: "#94e2d5",
brightWhite: "#a6adc8",
},
nord: {
background: "#2e3440",
foreground: "#d8dee9",
cursor: "#d8dee9",
cursorAccent: "#2e3440",
selectionBackground: "#4c566a",
selectionForeground: "#eceff4",
black: "#3b4252",
red: "#bf616a",
green: "#a3be8c",
yellow: "#ebcb8b",
blue: "#81a1c1",
magenta: "#b48ead",
cyan: "#88c0d0",
white: "#e5e9f0",
brightBlack: "#4c566a",
brightRed: "#bf616a",
brightGreen: "#a3be8c",
brightYellow: "#ebcb8b",
brightBlue: "#81a1c1",
brightMagenta: "#b48ead",
brightCyan: "#8fbcbb",
brightWhite: "#eceff4",
},
gruvbox: {
background: "#282828",
foreground: "#ebdbb2",
cursor: "#ebdbb2",
cursorAccent: "#282828",
selectionBackground: "#504945",
selectionForeground: "#fbf1c7",
black: "#282828",
red: "#cc241d",
green: "#98971a",
yellow: "#d79921",
blue: "#458588",
magenta: "#b16286",
cyan: "#689d6a",
white: "#a89984",
brightBlack: "#928374",
brightRed: "#fb4934",
brightGreen: "#b8bb26",
brightYellow: "#fabd2f",
brightBlue: "#83a598",
brightMagenta: "#d3869b",
brightCyan: "#8ec07c",
brightWhite: "#ebdbb2",
},
solarized: {
background: "#002b36",
foreground: "#839496",
cursor: "#93a1a1",
cursorAccent: "#002b36",
selectionBackground: "#073642",
selectionForeground: "#93a1a1",
black: "#073642",
red: "#dc322f",
green: "#859900",
yellow: "#b58900",
blue: "#268bd2",
magenta: "#d33682",
cyan: "#2aa198",
white: "#eee8d5",
brightBlack: "#002b36",
brightRed: "#cb4b16",
brightGreen: "#586e75",
brightYellow: "#657b83",
brightBlue: "#839496",
brightMagenta: "#6c71c4",
brightCyan: "#93a1a1",
brightWhite: "#fdf6e3",
},
tokyo: {
background: "#1a1b26",
foreground: "#c0caf5",
cursor: "#c0caf5",
cursorAccent: "#1a1b26",
selectionBackground: "#33467c",
selectionForeground: "#c0caf5",
black: "#15161e",
red: "#f7768e",
green: "#9ece6a",
yellow: "#e0af68",
blue: "#7aa2f7",
magenta: "#bb9af7",
cyan: "#7dcfff",
white: "#a9b1d6",
brightBlack: "#414868",
brightRed: "#f7768e",
brightGreen: "#9ece6a",
brightYellow: "#e0af68",
brightBlue: "#7aa2f7",
brightMagenta: "#bb9af7",
brightCyan: "#7dcfff",
brightWhite: "#c0caf5",
},
miasma: {
background: "#222222",
foreground: "#c2c2b0",
cursor: "#c2c2b0",
cursorAccent: "#222222",
selectionBackground: "#5f5f5f",
selectionForeground: "#ffffff",
black: "#222222",
red: "#685742",
green: "#5f875f",
yellow: "#b36d43",
blue: "#78824b",
magenta: "#bb7744",
cyan: "#c9a554",
white: "#c2c2b0",
brightBlack: "#666666",
brightRed: "#685742",
brightGreen: "#5f875f",
brightYellow: "#b36d43",
brightBlue: "#78824b",
brightMagenta: "#bb7744",
brightCyan: "#c9a554",
brightWhite: "#d7d7c7",
},
github: {
background: "#0d1117",
foreground: "#c9d1d9",
cursor: "#c9d1d9",
cursorAccent: "#0d1117",
selectionBackground: "#264f78",
selectionForeground: "#c9d1d9",
black: "#484f58",
red: "#ff7b72",
green: "#7ee787",
yellow: "#d29922",
blue: "#58a6ff",
magenta: "#bc8cff",
cyan: "#39c5cf",
white: "#b1bac4",
brightBlack: "#6e7681",
brightRed: "#ffa198",
brightGreen: "#56d364",
brightYellow: "#e3b341",
brightBlue: "#79c0ff",
brightMagenta: "#d2a8ff",
brightCyan: "#56d4dd",
brightWhite: "#f0f6fc",
},
gotham: {
background: "#0a0f14",
foreground: "#98d1ce",
cursor: "#98d1ce",
cursorAccent: "#0a0f14",
selectionBackground: "#1f2233",
selectionForeground: "#98d1ce",
black: "#0a0f14",
red: "#c33027",
green: "#26a98b",
yellow: "#edb54b",
blue: "#195465",
magenta: "#4e5165",
cyan: "#33859e",
white: "#98d1ce",
brightBlack: "#10151b",
brightRed: "#d26939",
brightGreen: "#081f2d",
brightYellow: "#245361",
brightBlue: "#093748",
brightMagenta: "#888ba5",
brightCyan: "#599caa",
brightWhite: "#d3ebe9",
},
};