diff --git a/AGENTS.md b/AGENTS.md index 860d0ef..8535dea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## 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 @@ -18,7 +18,7 @@ ## 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 diff --git a/Makefile b/Makefile index 821bf83..2839a2a 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,7 @@ bundle-watch: node_modules ## Watch mode for frontend development @test -f $(GHOSTTY_WASM) || bun run copy-wasm 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 clean: ## Remove coverage artifacts diff --git a/README.md b/README.md index db74895..10713e3 100644 --- a/README.md +++ b/README.md @@ -35,23 +35,24 @@ go install github.com/rcarmo/webterm/cmd/webterm@latest ```bash git clone https://github.com/rcarmo/webterm.git cd webterm -mkdir -p bin -go build -o ./bin/webterm ./cmd/webterm +make build-go ``` -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 Run a default shell session: ```bash +make build-fast go run ./cmd/webterm ``` Run a specific command: ```bash +make build-fast go run ./cmd/webterm -- htop ``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2a0dd47..ceed0a8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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. ``` -Browser (terminal.js + ghostty-vt.wasm) +Browser (generated terminal.js + ghostty-vt.wasm) │ │ WS / HTTP / SSE ▼ @@ -42,7 +42,7 @@ webterm/server.go (LocalServer) Assets live in `webterm/static`: - `js/terminal.ts` source -- `js/terminal.js` bundled client +- `js/terminal.js` generated bundled client - `js/ghostty-vt.wasm` - `monospace.css`, icons, `manifest.json` diff --git a/webterm/static/js/frontend-log.ts b/webterm/static/js/frontend-log.ts new file mode 100644 index 0000000..c26613a --- /dev/null +++ b/webterm/static/js/frontend-log.ts @@ -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) => void; + info: (text: string, extra?: Record) => void; + warn: (text: string, extra?: Record) => void; + error: (text: string, extra?: Record) => void; +} + +declare global { + interface Window { + webtermClientLog?: (text: string, extra?: Record) => Promise; + 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).map(([key, item]) => [key, normalizeFrontendLogValue(item)]) + ); + } + return value; +} + +function serializeFrontendLogContext(extra: Record = {}): 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 = {} +): Promise { + 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 = {}): void { + void postFrontendLog(level, text, extra); +} + +function logFrontendDebug(text: string, extra: Record = {}): void { + logFrontend("debug", text, extra); +} + +function logFrontendInfo(text: string, extra: Record = {}): void { + logFrontend("info", text, extra); +} + +function logFrontendWarn(text: string, extra: Record = {}): void { + logFrontend("warn", text, extra); +} + +function logFrontendError(text: string, extra: Record = {}): void { + logFrontend("error", text, extra); +} + +export function createScopedFrontendLogger(scope: string): WebtermScopedLogger { + const withScope = (extra: Record = {}): Record => ({ + 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 = {}) => + postFrontendLog("info", text, extra); +window.webtermClientLogSessionID = frontendLogSessionID; +window.webtermClientBuildVersion = FRONTEND_BUILD_VERSION; +window.webtermLogs = { + bug: bugLog, + ui: uiLog, + voice: voiceLog, + ws: wsLog, +}; diff --git a/webterm/static/js/terminal-assets.ts b/webterm/static/js/terminal-assets.ts new file mode 100644 index 0000000..d866913 --- /dev/null +++ b/webterm/static/js/terminal-assets.ts @@ -0,0 +1,65 @@ +const sharedScriptLoads = new Map>(); + +/** 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 { + const existing = sharedScriptLoads.get(src); + if (existing) { + return existing; + } + + const promise = new Promise((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)}%`; +} diff --git a/webterm/static/js/terminal-config.ts b/webterm/static/js/terminal-config.ts new file mode 100644 index 0000000..d3beb4c --- /dev/null +++ b/webterm/static/js/terminal-config.ts @@ -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; +} diff --git a/webterm/static/js/terminal-input.ts b/webterm/static/js/terminal-input.ts new file mode 100644 index 0000000..3aed1f4 --- /dev/null +++ b/webterm/static/js/terminal-input.ts @@ -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 = { + "`": "~", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + "[": "{", + "]": "}", + "\\": "|", + ";": ":", + "'": "\"", + ",": "<", + ".": ">", + "/": "?", +}; + +const CTRL_KEY_MAP: Record = { + "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; +} diff --git a/webterm/static/js/terminal-themes.ts b/webterm/static/js/terminal-themes.ts new file mode 100644 index 0000000..31eea78 --- /dev/null +++ b/webterm/static/js/terminal-themes.ts @@ -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 = { + // 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", + }, +};