docs: update README, AGENTS.md, Makefile, and architecture docs
CI / check (push) Has been cancelled
CI / check (push) Has been cancelled
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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)}%`;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user