feat: initial ui redesign from demo

This commit is contained in:
LukeGus
2026-05-13 01:29:43 -05:00
parent eaa758effe
commit 33dcde0827
349 changed files with 23984 additions and 31634 deletions
+218
View File
@@ -0,0 +1,218 @@
import React, {
createContext,
useContext,
useState,
useEffect,
useCallback,
useRef,
useMemo,
} from "react";
import { getAllServerStatuses, getSSHHosts } from "@/main-axios";
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
type StatusValue = "online" | "offline" | "degraded";
interface ServerStatusEntry {
status: StatusValue;
lastChecked: string;
}
interface ServerStatusContextType {
statuses: Map<number, ServerStatusEntry>;
isLoading: boolean;
refreshStatuses: () => Promise<void>;
getStatus: (hostId: number) => StatusValue;
}
const ServerStatusContext = createContext<ServerStatusContextType | null>(null);
const POLL_INTERVAL = 30000;
export function ServerStatusProvider({
children,
isAuthenticated = false,
}: {
children: React.ReactNode;
isAuthenticated?: boolean;
}) {
const [statuses, setStatuses] = useState<Map<number, ServerStatusEntry>>(
new Map(),
);
const [isLoading, setIsLoading] = useState(false);
const [enabledHostIds, setEnabledHostIds] = useState<Set<number>>(new Set());
const mountedRef = useRef(true);
const enabledHostIdsRef = useRef(enabledHostIds);
useEffect(() => {
enabledHostIdsRef.current = enabledHostIds;
}, [enabledHostIds]);
const fetchEnabledHosts = useCallback(async () => {
if (!isAuthenticated) {
return new Set<number>();
}
try {
const hosts = await getSSHHosts();
const enabled = new Set<number>();
hosts.forEach((host) => {
const statsConfig = (() => {
try {
return host.statsConfig
? JSON.parse(host.statsConfig)
: DEFAULT_STATS_CONFIG;
} catch {
return DEFAULT_STATS_CONFIG;
}
})();
if (statsConfig.statusCheckEnabled !== false) {
enabled.add(host.id);
}
});
setEnabledHostIds((prev) => {
if (prev.size !== enabled.size) return enabled;
for (const id of enabled) {
if (!prev.has(id)) return enabled;
}
return prev;
});
return enabled;
} catch {
return new Set<number>();
}
}, [isAuthenticated]);
const refreshStatuses = useCallback(async () => {
if (!mountedRef.current || !isAuthenticated) return;
setIsLoading(true);
try {
const data = await getAllServerStatuses();
if (!mountedRef.current) return;
const newStatuses = new Map<number, ServerStatusEntry>();
const now = new Date().toISOString();
if (data && typeof data === "object") {
Object.entries(data).forEach(([idStr, statusData]) => {
const id = parseInt(idStr, 10);
if (!isNaN(id)) {
const status =
statusData?.status === "online" ? "online" : "offline";
newStatuses.set(id, {
status,
lastChecked: statusData?.lastChecked || now,
});
}
});
}
setStatuses(newStatuses);
} catch {
if (mountedRef.current) {
setStatuses((prev) => {
const updated = new Map(prev);
enabledHostIdsRef.current.forEach((id) => {
const existing = updated.get(id);
updated.set(id, {
status: "degraded",
lastChecked: existing?.lastChecked || new Date().toISOString(),
});
});
return updated;
});
}
} finally {
if (mountedRef.current) {
setIsLoading(false);
}
}
}, [isAuthenticated]);
const stableEnabledHostIds = useMemo(
() => enabledHostIds,
[[...enabledHostIds].sort().join(",")],
);
const getStatus = useCallback(
(hostId: number): StatusValue => {
if (!stableEnabledHostIds.has(hostId)) {
return "offline";
}
return statuses.get(hostId)?.status || "degraded";
},
[statuses, stableEnabledHostIds],
);
useEffect(() => {
mountedRef.current = true;
const init = async () => {
await fetchEnabledHosts();
await refreshStatuses();
};
init();
const intervalId = setInterval(refreshStatuses, POLL_INTERVAL);
return () => {
mountedRef.current = false;
clearInterval(intervalId);
};
}, [fetchEnabledHosts, refreshStatuses]);
useEffect(() => {
const handleHostsChanged = async () => {
await fetchEnabledHosts();
await refreshStatuses();
};
window.addEventListener("ssh-hosts:changed", handleHostsChanged);
window.addEventListener("hosts:refresh", handleHostsChanged);
return () => {
window.removeEventListener("ssh-hosts:changed", handleHostsChanged);
window.removeEventListener("hosts:refresh", handleHostsChanged);
};
}, [fetchEnabledHosts, refreshStatuses]);
return (
<ServerStatusContext.Provider
value={{
statuses,
isLoading,
refreshStatuses,
getStatus,
}}
>
{children}
</ServerStatusContext.Provider>
);
}
export function useServerStatus() {
const context = useContext(ServerStatusContext);
if (!context) {
throw new Error(
"useServerStatus must be used within a ServerStatusProvider",
);
}
return context;
}
export function useHostStatus(
hostId: number,
statusCheckEnabled: boolean = true,
) {
const { getStatus } = useServerStatus();
if (!statusCheckEnabled) {
return "offline" as StatusValue;
}
return getStatus(hostId);
}
+63
View File
@@ -0,0 +1,63 @@
import React from "react";
import { cn } from "@/lib/utils.ts";
interface SimpleLoaderProps {
visible: boolean;
message?: string;
className?: string;
backgroundColor?: string;
}
export function SimpleLoader({
visible,
message,
className,
backgroundColor,
}: SimpleLoaderProps) {
if (!visible) {
return null;
}
return (
<>
<style>
{`
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.simple-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border-base);
border-top-color: var(--foreground);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
`}
</style>
<div
className={cn(
"absolute inset-0 flex items-center justify-center z-[100]",
className,
)}
style={{ backgroundColor: backgroundColor || "var(--bg-base)" }}
>
<div className="flex flex-col items-center gap-4">
<div className="simple-spinner"></div>
{message && (
<p className="text-sm text-foreground-secondary font-medium">
{message}
</p>
)}
</div>
</div>
</>
);
}
+5
View File
@@ -0,0 +1,5 @@
export function getBasePath(): string {
const base = import.meta.env.BASE_URL || "/";
if (base === "./" || base === "/") return "";
return base.endsWith("/") ? base.slice(0, -1) : base;
}
+45
View File
@@ -0,0 +1,45 @@
const CLIENT_CACHE_VERSION_KEY = "termix_client_cache_version";
const CURRENT_CLIENT_VERSION = import.meta.env.VITE_APP_VERSION || "0.0.0";
async function clearCacheStorage(): Promise<void> {
if (!("caches" in window)) return;
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map((name) => caches.delete(name)));
}
async function clearServiceWorkers(): Promise<void> {
if (!("serviceWorker" in navigator)) return;
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(
registrations.map((registration) => registration.unregister()),
);
}
function storeCurrentVersion(): void {
try {
localStorage.setItem(CLIENT_CACHE_VERSION_KEY, CURRENT_CLIENT_VERSION);
} catch {
// expected - storage can be unavailable in restricted contexts
}
}
export async function prepareClientCacheVersion(): Promise<void> {
if (typeof window === "undefined") return;
let storedVersion: string | null = null;
try {
storedVersion = localStorage.getItem(CLIENT_CACHE_VERSION_KEY);
} catch {
storedVersion = null;
}
if (storedVersion === CURRENT_CLIENT_VERSION) {
return;
}
await Promise.allSettled([clearCacheStorage(), clearServiceWorkers()]);
storeCurrentVersion();
}
+55
View File
@@ -0,0 +1,55 @@
import type {
IClipboardProvider,
ClipboardSelectionType,
} from "@xterm/addon-clipboard";
export class RobustClipboardProvider implements IClipboardProvider {
private pendingWrite: string | null = null;
private readonly focusHandler: () => void;
constructor() {
this.focusHandler = () => {
if (this.pendingWrite !== null) {
const text = this.pendingWrite;
this.pendingWrite = null;
if (window.electronClipboard) {
window.electronClipboard.writeText(text).catch(() => {
this.pendingWrite = text;
});
return;
}
navigator.clipboard.writeText(text).catch(() => {
this.pendingWrite = text;
});
}
};
window.addEventListener("focus", this.focusHandler);
}
dispose(): void {
window.removeEventListener("focus", this.focusHandler);
this.pendingWrite = null;
}
readText(_selection: ClipboardSelectionType): string | Promise<string> {
if (window.electronClipboard) {
return window.electronClipboard.readText();
}
return navigator.clipboard?.readText?.() ?? "";
}
async writeText(
_selection: ClipboardSelectionType,
text: string,
): Promise<void> {
try {
if (window.electronClipboard) {
await window.electronClipboard.writeText(text);
return;
}
await navigator.clipboard.writeText(text);
} catch {
this.pendingWrite = text;
}
}
}
+130
View File
@@ -0,0 +1,130 @@
/**
* DatabaseHealthMonitor
*
* Non-blocking health tracker for backend/database connectivity. The
* monitor no longer gates the whole UI: there is no full-screen overlay.
* When a transient failure is observed we emit a "degraded" event so the
* UI can surface a persistent but non-intrusive toast. A success from any
* API request clears the state. Session-expired events are also relayed
* to the UI.
*
* The previous "database-connection-lost" / "database-connection-restored"
* events have been retired along with the overlay. Listeners should use
* "database-connection-degraded" / "database-connection-degraded-cleared"
* to reflect the current UX contract: users can keep working regardless
* of backend hiccups and are simply informed via a toast.
*/
type EventListener = (...args: unknown[]) => void;
interface HttpLikeError {
message?: string;
code?: string;
response?: {
data?: {
error?: string;
code?: string;
};
};
}
class DatabaseHealthMonitor {
private static instance: DatabaseHealthMonitor;
private listeners: Map<string, EventListener[]> = new Map();
private degradedActive: boolean = false;
private constructor() {}
static getInstance(): DatabaseHealthMonitor {
if (!DatabaseHealthMonitor.instance) {
DatabaseHealthMonitor.instance = new DatabaseHealthMonitor();
}
return DatabaseHealthMonitor.instance;
}
on(event: string, listener: EventListener): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(listener);
}
off(event: string, listener: EventListener): void {
const eventListeners = this.listeners.get(event);
if (eventListeners) {
const index = eventListeners.indexOf(listener);
if (index !== -1) {
eventListeners.splice(index, 1);
}
}
}
private emit(event: string, ...args: unknown[]): void {
const eventListeners = this.listeners.get(event);
if (eventListeners) {
eventListeners.forEach((listener) => listener(...args));
}
}
reportSessionExpired() {
this.emit("session-expired", { timestamp: Date.now() });
}
reportDatabaseError(error: unknown) {
const errorLike = error as HttpLikeError;
const errorMessage =
errorLike.response?.data?.error || errorLike.message || "";
const errorCode = errorLike.response?.data?.code || errorLike.code;
const lowerMessage = errorMessage.toLowerCase();
const isDatabaseError =
lowerMessage.includes("database") ||
lowerMessage.includes("sqlite") ||
lowerMessage.includes("drizzle") ||
errorCode === "DATABASE_ERROR" ||
errorCode === "DB_CONNECTION_FAILED";
const isBackendUnreachable =
errorCode === "ERR_NETWORK" ||
errorCode === "ECONNREFUSED" ||
errorCode === "ECONNABORTED" ||
errorCode === "ECONNRESET" ||
errorCode === "ETIMEDOUT" ||
errorCode === "ERR_CANCELED" ||
(lowerMessage.includes("network error") &&
errorLike.response === undefined) ||
lowerMessage.includes("request aborted") ||
lowerMessage.includes("timeout");
if (!(isDatabaseError || isBackendUnreachable)) {
return;
}
if (!this.degradedActive) {
this.degradedActive = true;
this.emit("database-connection-degraded", {
error: errorMessage || "Background request failed",
code: errorCode,
timestamp: Date.now(),
});
}
}
reportDatabaseSuccess() {
if (this.degradedActive) {
this.degradedActive = false;
this.emit("database-connection-degraded-cleared", {
timestamp: Date.now(),
});
}
}
isDegraded(): boolean {
return this.degradedActive;
}
reset() {
this.degradedActive = false;
}
}
export const dbHealthMonitor = DatabaseHealthMonitor.getInstance();
+18
View File
@@ -0,0 +1,18 @@
type ElectronWindow = Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: {
isElectron?: boolean;
};
};
export function isElectron(): boolean {
if (typeof window === "undefined") return false;
const win = window as ElectronWindow;
const hasISElectron = win.IS_ELECTRON === true;
const hasElectronAPI = !!win.electronAPI;
const isElectronProp = win.electronAPI?.isElectron === true;
return hasISElectron || hasElectronAPI || isElectronProp;
}
+384
View File
@@ -0,0 +1,384 @@
export type LogLevel = "debug" | "info" | "warn" | "error" | "success";
export interface LogContext {
operation?: string;
userId?: string;
hostId?: number;
tunnelName?: string;
sessionId?: string;
requestId?: string;
duration?: number;
method?: string;
url?: string;
status?: number;
statusText?: string;
responseTime?: number;
retryCount?: number;
errorCode?: string;
errorMessage?: string;
[key: string]: unknown;
}
class FrontendLogger {
private serviceName: string;
private serviceIcon: string;
private serviceColor: string;
private isDevelopment: boolean;
constructor(serviceName: string, serviceIcon: string, serviceColor: string) {
this.serviceName = serviceName;
this.serviceIcon = serviceIcon;
this.serviceColor = serviceColor;
this.isDevelopment = process.env.NODE_ENV === "development";
}
private getTimeStamp(): string {
const now = new Date();
return `[${now.toLocaleTimeString()}.${now.getMilliseconds().toString().padStart(3, "0")}]`;
}
private formatMessage(
level: LogLevel,
message: string,
context?: LogContext,
): string {
const timestamp = this.getTimeStamp();
const levelTag = this.getLevelTag(level);
const serviceTag = this.getServiceTag();
let contextStr = "";
if (context && this.isDevelopment) {
const contextParts = [];
if (context.operation) contextParts.push(context.operation);
if (context.userId) contextParts.push(`user:${context.userId}`);
if (context.hostId) contextParts.push(`host:${context.hostId}`);
if (context.tunnelName) contextParts.push(`tunnel:${context.tunnelName}`);
if (context.sessionId) contextParts.push(`session:${context.sessionId}`);
if (context.responseTime) contextParts.push(`${context.responseTime}ms`);
if (context.status) contextParts.push(`status:${context.status}`);
if (context.errorCode) contextParts.push(`code:${context.errorCode}`);
if (contextParts.length > 0) {
contextStr = ` (${contextParts.join(", ")})`;
}
}
return `${timestamp} ${levelTag} ${serviceTag} ${message}${contextStr}`;
}
private getLevelTag(level: LogLevel): string {
const symbols = {
debug: "🔍",
info: "️",
warn: "⚠️",
error: "❌",
success: "✅",
};
return `${symbols[level]} [${level.toUpperCase()}]`;
}
private getServiceTag(): string {
return `${this.serviceIcon} [${this.serviceName}]`;
}
private shouldLog(level: LogLevel): boolean {
if (level === "debug" && !this.isDevelopment) {
return false;
}
return true;
}
private log(
level: LogLevel,
message: string,
context?: LogContext,
error?: unknown,
): void {
if (!this.shouldLog(level)) return;
const formattedMessage = this.formatMessage(level, message, context);
switch (level) {
case "debug":
console.debug(formattedMessage);
break;
case "info":
console.log(formattedMessage);
break;
case "warn":
console.warn(formattedMessage);
break;
case "error":
console.error(formattedMessage);
if (error) {
console.error("Error details:", error);
}
break;
case "success":
console.log(formattedMessage);
break;
}
}
debug(message: string, context?: LogContext): void {
this.log("debug", message, context);
}
info(message: string, context?: LogContext): void {
this.log("info", message, context);
}
warn(message: string, context?: LogContext): void {
this.log("warn", message, context);
}
error(message: string, error?: unknown, context?: LogContext): void {
this.log("error", message, context, error);
}
success(message: string, context?: LogContext): void {
this.log("success", message, context);
}
api(message: string, context?: LogContext): void {
this.info(`API: ${message}`, { ...context, operation: "api" });
}
request(message: string, context?: LogContext): void {
this.info(`REQUEST: ${message}`, { ...context, operation: "request" });
}
response(message: string, context?: LogContext): void {
this.info(`RESPONSE: ${message}`, { ...context, operation: "response" });
}
auth(message: string, context?: LogContext): void {
this.info(`AUTH: ${message}`, { ...context, operation: "auth" });
}
ssh(message: string, context?: LogContext): void {
this.info(`SSH: ${message}`, { ...context, operation: "ssh" });
}
tunnel(message: string, context?: LogContext): void {
this.info(`TUNNEL: ${message}`, { ...context, operation: "tunnel" });
}
file(message: string, context?: LogContext): void {
this.info(`FILE: ${message}`, { ...context, operation: "file" });
}
connection(message: string, context?: LogContext): void {
this.info(`CONNECTION: ${message}`, {
...context,
operation: "connection",
});
}
disconnect(message: string, context?: LogContext): void {
this.info(`DISCONNECT: ${message}`, {
...context,
operation: "disconnect",
});
}
retry(message: string, context?: LogContext): void {
this.warn(`RETRY: ${message}`, { ...context, operation: "retry" });
}
performance(message: string, context?: LogContext): void {
this.info(`PERFORMANCE: ${message}`, {
...context,
operation: "performance",
});
}
security(message: string, context?: LogContext): void {
this.warn(`SECURITY: ${message}`, { ...context, operation: "security" });
}
requestStart(method: string, url: string, context?: LogContext): void {
const cleanUrl = this.sanitizeUrl(url);
const shortUrl = this.getShortUrl(cleanUrl);
console.group(`🚀 ${method.toUpperCase()} ${shortUrl}`);
this.request(`→ Starting request to ${cleanUrl}`, {
...context,
method: method.toUpperCase(),
url: cleanUrl,
});
}
requestSuccess(
method: string,
url: string,
status: number,
responseTime: number,
context?: LogContext,
): void {
const cleanUrl = this.sanitizeUrl(url);
const statusIcon = this.getStatusIcon(status);
const performanceIcon = this.getPerformanceIcon(responseTime);
this.response(
`${statusIcon} ${status} ${performanceIcon} ${responseTime}ms`,
{
...context,
method: method.toUpperCase(),
url: cleanUrl,
status,
responseTime,
},
);
console.groupEnd();
}
requestError(
method: string,
url: string,
status: number,
errorMessage: string,
responseTime?: number,
context?: LogContext,
): void {
const cleanUrl = this.sanitizeUrl(url);
const statusIcon = this.getStatusIcon(status);
this.error(`${statusIcon} ${status} ${errorMessage}`, undefined, {
...context,
method: method.toUpperCase(),
url: cleanUrl,
status,
errorMessage,
responseTime,
});
console.groupEnd();
}
networkError(
method: string,
url: string,
errorMessage: string,
context?: LogContext,
): void {
const cleanUrl = this.sanitizeUrl(url);
this.error(`🌐 Network Error: ${errorMessage}`, undefined, {
...context,
method: method.toUpperCase(),
url: cleanUrl,
errorMessage,
errorCode: "NETWORK_ERROR",
});
console.groupEnd();
}
authError(method: string, url: string, context?: LogContext): void {
const cleanUrl = this.sanitizeUrl(url);
this.security(`🔐 Authentication Required`, {
...context,
method: method.toUpperCase(),
url: cleanUrl,
errorCode: "AUTH_REQUIRED",
});
console.groupEnd();
}
retryAttempt(
method: string,
url: string,
attempt: number,
maxAttempts: number,
context?: LogContext,
): void {
const cleanUrl = this.sanitizeUrl(url);
this.retry(`🔄 Retry ${attempt}/${maxAttempts}`, {
...context,
method: method.toUpperCase(),
url: cleanUrl,
retryCount: attempt,
});
}
apiOperation(operation: string, details: string, context?: LogContext): void {
this.info(`🔧 ${operation}: ${details}`, {
...context,
operation: "api_operation",
});
}
requestSummary(
method: string,
url: string,
status: number,
responseTime: number,
context?: LogContext,
): void {
const cleanUrl = this.sanitizeUrl(url);
const shortUrl = this.getShortUrl(cleanUrl);
const statusIcon = this.getStatusIcon(status);
const performanceIcon = this.getPerformanceIcon(responseTime);
console.log(
`%c📊 ${method} ${shortUrl} ${statusIcon} ${status} ${performanceIcon} ${responseTime}ms`,
"color: #666; font-style: italic; font-size: 0.9em;",
context,
);
}
private getShortUrl(url: string): string {
try {
const urlObj = new URL(url);
const path = urlObj.pathname;
const query = urlObj.search;
return `${urlObj.hostname}${path}${query}`;
} catch {
return url.length > 50 ? url.substring(0, 47) + "..." : url;
}
}
private getStatusIcon(status: number): string {
if (status >= 200 && status < 300) return "✅";
if (status >= 300 && status < 400) return "↩️";
if (status >= 400 && status < 500) return "⚠️";
if (status >= 500) return "❌";
return "❓";
}
private getPerformanceIcon(responseTime: number): string {
if (responseTime < 100) return "⚡";
if (responseTime < 500) return "🚀";
if (responseTime < 1000) return "🏃";
if (responseTime < 3000) return "🚶";
return "🐌";
}
private sanitizeUrl(url: string): string {
try {
const urlObj = new URL(url);
if (
urlObj.searchParams.has("password") ||
urlObj.searchParams.has("token")
) {
urlObj.search = "";
}
return urlObj.toString();
} catch {
return url;
}
}
}
export const apiLogger = new FrontendLogger("API", "🌐", "#3b82f6");
export const authLogger = new FrontendLogger("AUTH", "🔐", "#dc2626");
export const sshLogger = new FrontendLogger("SSH", "🖥️", "#1e3a8a");
export const tunnelLogger = new FrontendLogger("TUNNEL", "📡", "#1e3a8a");
export const fileLogger = new FrontendLogger("FILE", "📁", "#1e3a8a");
export const statsLogger = new FrontendLogger("STATS", "📊", "#22c55e");
export const systemLogger = new FrontendLogger("SYSTEM", "🚀", "#1e3a8a");
export const dashboardLogger = new FrontendLogger("DASHBOARD", "📊", "#ec4899");
export const logger = systemLogger;
+15
View File
@@ -0,0 +1,15 @@
// Module-level flag: true while a split pane divider is being dragged.
// TerminalTab reads this to suppress fit() calls during drag.
export const splitDragState = { active: false };
// Callbacks registered by terminal instances to trigger a fit after drag ends.
const fitCallbacks = new Set<() => void>();
export function registerFitCallback(fn: () => void) {
fitCallbacks.add(fn);
return () => fitCallbacks.delete(fn);
}
export function notifyDragEnd() {
for (const fn of fitCallbacks) fn();
}
+263
View File
@@ -0,0 +1,263 @@
const ANSI_CODES = {
reset: "\x1b[0m",
colors: {
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
brightBlack: "\x1b[90m",
brightRed: "\x1b[91m",
brightGreen: "\x1b[92m",
brightYellow: "\x1b[93m",
brightBlue: "\x1b[94m",
brightMagenta: "\x1b[95m",
brightCyan: "\x1b[96m",
brightWhite: "\x1b[97m",
},
styles: {
bold: "\x1b[1m",
dim: "\x1b[2m",
italic: "\x1b[3m",
underline: "\x1b[4m",
},
} as const;
interface HighlightPattern {
name: string;
regex: RegExp;
ansiCode: string;
priority: number;
quickCheck?: string;
}
interface MatchResult {
start: number;
end: number;
ansiCode: string;
priority: number;
}
const MAX_LINE_LENGTH = 5000;
const MAX_ANSI_CODES = 10;
const PATTERNS: HighlightPattern[] = [
{
name: "ipv4",
regex:
/(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?::\d{1,5})?/g,
ansiCode: ANSI_CODES.colors.magenta,
priority: 10,
},
{
name: "log-error",
regex:
/\b(ERROR|FATAL|CRITICAL|FAIL(?:ED)?|denied|invalid|DENIED)\b|\[ERROR\]/gi,
ansiCode: ANSI_CODES.colors.brightRed,
priority: 9,
},
{
name: "log-warn",
regex: /\b(WARN(?:ING)?|ALERT)\b|\[WARN(?:ING)?\]/gi,
ansiCode: ANSI_CODES.colors.yellow,
priority: 9,
},
{
name: "log-success",
regex:
/\b(SUCCESS|OK|PASS(?:ED)?|COMPLETE(?:D)?|connected|active|up|Up|UP|FULL)\b/gi,
ansiCode: ANSI_CODES.colors.brightGreen,
priority: 8,
},
{
name: "url",
regex: /https?:\/\/[^\s\])}]+/g,
ansiCode: `${ANSI_CODES.colors.blue}${ANSI_CODES.styles.underline}`,
priority: 8,
},
{
name: "path-absolute",
regex: /\/[a-zA-Z][a-zA-Z0-9_\-@.]*(?:\/[a-zA-Z0-9_\-@.]+)+/g,
ansiCode: ANSI_CODES.colors.cyan,
priority: 7,
},
{
name: "path-home",
regex: /~\/[a-zA-Z0-9_\-@./]+/g,
ansiCode: ANSI_CODES.colors.cyan,
priority: 7,
},
{
name: "log-info",
regex: /\bINFO\b|\[INFO\]/gi,
ansiCode: ANSI_CODES.colors.blue,
priority: 6,
},
{
name: "log-debug",
regex: /\b(?:DEBUG|TRACE)\b|\[(?:DEBUG|TRACE)\]/gi,
ansiCode: ANSI_CODES.colors.brightBlack,
priority: 6,
},
];
function hasExistingAnsiCodes(text: string): boolean {
const ansiCount = (
text.match(
/\x1b[[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nq-uy=><~]/g,
) || []
).length;
return ansiCount > MAX_ANSI_CODES;
}
function hasIncompleteAnsiSequence(text: string): boolean {
return /\x1b(?:\[(?:[0-9;?>=!]*)?)?$/.test(text);
}
interface TextSegment {
isAnsi: boolean;
content: string;
}
function parseAnsiSegments(text: string): TextSegment[] {
const segments: TextSegment[] = [];
const ansiRegex = /\x1b(?:[@-Z\\-_]|\[[0-9;?>=!]*[@-~])/g;
let lastIndex = 0;
let match;
while ((match = ansiRegex.exec(text)) !== null) {
if (match.index > lastIndex) {
segments.push({
isAnsi: false,
content: text.slice(lastIndex, match.index),
});
}
segments.push({
isAnsi: true,
content: match[0],
});
lastIndex = ansiRegex.lastIndex;
}
if (lastIndex < text.length) {
segments.push({
isAnsi: false,
content: text.slice(lastIndex),
});
}
return segments;
}
function highlightPlainText(text: string): string {
if (text.length > MAX_LINE_LENGTH) {
return text;
}
if (!text.trim()) {
return text;
}
const matches: MatchResult[] = [];
for (const pattern of PATTERNS) {
pattern.regex.lastIndex = 0;
let match;
while ((match = pattern.regex.exec(text)) !== null) {
matches.push({
start: match.index,
end: match.index + match[0].length,
ansiCode: pattern.ansiCode,
priority: pattern.priority,
});
}
}
if (matches.length === 0) {
return text;
}
matches.sort((a, b) => {
if (a.priority !== b.priority) {
return b.priority - a.priority;
}
return a.start - b.start;
});
const appliedRanges: Array<{ start: number; end: number }> = [];
const finalMatches = matches.filter((match) => {
const overlaps = appliedRanges.some(
(range) =>
(match.start >= range.start && match.start < range.end) ||
(match.end > range.start && match.end <= range.end) ||
(match.start <= range.start && match.end >= range.end),
);
if (!overlaps) {
appliedRanges.push({ start: match.start, end: match.end });
return true;
}
return false;
});
let result = text;
finalMatches.reverse().forEach((match) => {
const before = result.slice(0, match.start);
const matched = result.slice(match.start, match.end);
const after = result.slice(match.end);
result = before + match.ansiCode + matched + ANSI_CODES.reset + after;
});
return result;
}
export function highlightTerminalOutput(text: string): string {
if (!text || !text.trim()) {
return text;
}
if (hasIncompleteAnsiSequence(text)) {
return text;
}
if (hasExistingAnsiCodes(text)) {
return text;
}
const segments = parseAnsiSegments(text);
if (segments.length === 0) {
return highlightPlainText(text);
}
const highlightedSegments = segments.map((segment) => {
if (segment.isAnsi) {
return segment.content;
} else {
return highlightPlainText(segment.content);
}
});
return highlightedSegments.join("");
}
export function isSyntaxHighlightingEnabled(): boolean {
try {
return localStorage.getItem("terminalSyntaxHighlighting") === "true";
} catch {
return false;
}
}
+826
View File
@@ -0,0 +1,826 @@
export interface TerminalTheme {
name: string;
category: "dark" | "light" | "colorful";
colors: {
background: string;
foreground: string;
cursor?: string;
cursorAccent?: string;
selectionBackground?: string;
selectionForeground?: string;
black: string;
red: string;
green: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
brightBlack: string;
brightRed: string;
brightGreen: string;
brightYellow: string;
brightBlue: string;
brightMagenta: string;
brightCyan: string;
brightWhite: string;
};
}
export const TERMINAL_THEMES: Record<string, TerminalTheme> = {
termix: {
name: "Termix Default",
category: "dark",
colors: {
background: "#0c0d0b",
foreground: "#f7f7f7",
cursor: "#fb923c",
cursorAccent: "#0c0d0b",
selectionBackground: "#3a3a3d",
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",
},
},
termixDark: {
name: "Termix Dark",
category: "dark",
colors: {
background: "#0c0d0b",
foreground: "#f7f7f7",
cursor: "#fb923c",
cursorAccent: "#0c0d0b",
selectionBackground: "#3a3a3d",
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",
},
},
termixLight: {
name: "Termix Light",
category: "light",
colors: {
background: "#ffffff",
foreground: "#18181b",
cursor: "#18181b",
cursorAccent: "#ffffff",
selectionBackground: "#d1d5db",
black: "#18181b",
red: "#dc2626",
green: "#16a34a",
yellow: "#ca8a04",
blue: "#2563eb",
magenta: "#9333ea",
cyan: "#0891b2",
white: "#f4f4f5",
brightBlack: "#71717a",
brightRed: "#ef4444",
brightGreen: "#22c55e",
brightYellow: "#eab308",
brightBlue: "#3b82f6",
brightMagenta: "#a855f7",
brightCyan: "#06b6d4",
brightWhite: "#ffffff",
},
},
dracula: {
name: "Dracula",
category: "dark",
colors: {
background: "#282a36",
foreground: "#f8f8f2",
cursor: "#f8f8f2",
cursorAccent: "#282a36",
selectionBackground: "#44475a",
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",
},
},
monokai: {
name: "Monokai",
category: "dark",
colors: {
background: "#272822",
foreground: "#f8f8f2",
cursor: "#f8f8f0",
cursorAccent: "#272822",
selectionBackground: "#49483e",
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",
},
},
nord: {
name: "Nord",
category: "dark",
colors: {
background: "#2e3440",
foreground: "#d8dee9",
cursor: "#d8dee9",
cursorAccent: "#2e3440",
selectionBackground: "#434c5e",
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",
},
},
gruvboxDark: {
name: "Gruvbox Dark",
category: "dark",
colors: {
background: "#282828",
foreground: "#ebdbb2",
cursor: "#ebdbb2",
cursorAccent: "#282828",
selectionBackground: "#504945",
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",
},
},
gruvboxLight: {
name: "Gruvbox Light",
category: "light",
colors: {
background: "#fbf1c7",
foreground: "#3c3836",
cursor: "#3c3836",
cursorAccent: "#fbf1c7",
selectionBackground: "#d5c4a1",
black: "#fbf1c7",
red: "#cc241d",
green: "#98971a",
yellow: "#d79921",
blue: "#458588",
magenta: "#b16286",
cyan: "#689d6a",
white: "#7c6f64",
brightBlack: "#928374",
brightRed: "#9d0006",
brightGreen: "#79740e",
brightYellow: "#b57614",
brightBlue: "#076678",
brightMagenta: "#8f3f71",
brightCyan: "#427b58",
brightWhite: "#3c3836",
},
},
solarizedDark: {
name: "Solarized Dark",
category: "dark",
colors: {
background: "#002b36",
foreground: "#839496",
cursor: "#839496",
cursorAccent: "#002b36",
selectionBackground: "#073642",
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",
},
},
solarizedLight: {
name: "Solarized Light",
category: "light",
colors: {
background: "#fdf6e3",
foreground: "#657b83",
cursor: "#657b83",
cursorAccent: "#fdf6e3",
selectionBackground: "#eee8d5",
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",
},
},
oneDark: {
name: "One Dark",
category: "dark",
colors: {
background: "#282c34",
foreground: "#abb2bf",
cursor: "#528bff",
cursorAccent: "#282c34",
selectionBackground: "#3e4451",
black: "#282c34",
red: "#e06c75",
green: "#98c379",
yellow: "#e5c07b",
blue: "#61afef",
magenta: "#c678dd",
cyan: "#56b6c2",
white: "#abb2bf",
brightBlack: "#5c6370",
brightRed: "#e06c75",
brightGreen: "#98c379",
brightYellow: "#e5c07b",
brightBlue: "#61afef",
brightMagenta: "#c678dd",
brightCyan: "#56b6c2",
brightWhite: "#ffffff",
},
},
tokyoNight: {
name: "Tokyo Night",
category: "dark",
colors: {
background: "#1a1b26",
foreground: "#a9b1d6",
cursor: "#a9b1d6",
cursorAccent: "#1a1b26",
selectionBackground: "#283457",
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",
},
},
ayuDark: {
name: "Ayu Dark",
category: "dark",
colors: {
background: "#0a0e14",
foreground: "#b3b1ad",
cursor: "#e6b450",
cursorAccent: "#0a0e14",
selectionBackground: "#253340",
black: "#01060e",
red: "#ea6c73",
green: "#91b362",
yellow: "#f9af4f",
blue: "#53bdfa",
magenta: "#fae994",
cyan: "#90e1c6",
white: "#c7c7c7",
brightBlack: "#686868",
brightRed: "#f07178",
brightGreen: "#c2d94c",
brightYellow: "#ffb454",
brightBlue: "#59c2ff",
brightMagenta: "#ffee99",
brightCyan: "#95e6cb",
brightWhite: "#ffffff",
},
},
ayuLight: {
name: "Ayu Light",
category: "light",
colors: {
background: "#fafafa",
foreground: "#5c6166",
cursor: "#ff9940",
cursorAccent: "#fafafa",
selectionBackground: "#d1e4f4",
black: "#000000",
red: "#f51818",
green: "#86b300",
yellow: "#f2ae49",
blue: "#399ee6",
magenta: "#a37acc",
cyan: "#4cbf99",
white: "#c7c7c7",
brightBlack: "#686868",
brightRed: "#ff3333",
brightGreen: "#b8e532",
brightYellow: "#ffc849",
brightBlue: "#59c2ff",
brightMagenta: "#bf7ce0",
brightCyan: "#5cf7a0",
brightWhite: "#ffffff",
},
},
materialTheme: {
name: "Material Theme",
category: "dark",
colors: {
background: "#263238",
foreground: "#eeffff",
cursor: "#ffcc00",
cursorAccent: "#263238",
selectionBackground: "#546e7a",
black: "#000000",
red: "#e53935",
green: "#91b859",
yellow: "#ffb62c",
blue: "#6182b8",
magenta: "#7c4dff",
cyan: "#39adb5",
white: "#ffffff",
brightBlack: "#546e7a",
brightRed: "#ff5370",
brightGreen: "#c3e88d",
brightYellow: "#ffcb6b",
brightBlue: "#82aaff",
brightMagenta: "#c792ea",
brightCyan: "#89ddff",
brightWhite: "#ffffff",
},
},
palenight: {
name: "Palenight",
category: "dark",
colors: {
background: "#292d3e",
foreground: "#a6accd",
cursor: "#ffcc00",
cursorAccent: "#292d3e",
selectionBackground: "#676e95",
black: "#292d3e",
red: "#f07178",
green: "#c3e88d",
yellow: "#ffcb6b",
blue: "#82aaff",
magenta: "#c792ea",
cyan: "#89ddff",
white: "#d0d0d0",
brightBlack: "#434758",
brightRed: "#ff8b92",
brightGreen: "#ddffa7",
brightYellow: "#ffe585",
brightBlue: "#9cc4ff",
brightMagenta: "#e1acff",
brightCyan: "#a3f7ff",
brightWhite: "#ffffff",
},
},
oceanicNext: {
name: "Oceanic Next",
category: "dark",
colors: {
background: "#1b2b34",
foreground: "#cdd3de",
cursor: "#c0c5ce",
cursorAccent: "#1b2b34",
selectionBackground: "#343d46",
black: "#343d46",
red: "#ec5f67",
green: "#99c794",
yellow: "#fac863",
blue: "#6699cc",
magenta: "#c594c5",
cyan: "#5fb3b3",
white: "#cdd3de",
brightBlack: "#65737e",
brightRed: "#ec5f67",
brightGreen: "#99c794",
brightYellow: "#fac863",
brightBlue: "#6699cc",
brightMagenta: "#c594c5",
brightCyan: "#5fb3b3",
brightWhite: "#d8dee9",
},
},
nightOwl: {
name: "Night Owl",
category: "dark",
colors: {
background: "#011627",
foreground: "#d6deeb",
cursor: "#80a4c2",
cursorAccent: "#011627",
selectionBackground: "#1d3b53",
black: "#011627",
red: "#ef5350",
green: "#22da6e",
yellow: "#c5e478",
blue: "#82aaff",
magenta: "#c792ea",
cyan: "#21c7a8",
white: "#ffffff",
brightBlack: "#575656",
brightRed: "#ef5350",
brightGreen: "#22da6e",
brightYellow: "#ffeb95",
brightBlue: "#82aaff",
brightMagenta: "#c792ea",
brightCyan: "#7fdbca",
brightWhite: "#ffffff",
},
},
synthwave84: {
name: "Synthwave '84",
category: "colorful",
colors: {
background: "#241b2f",
foreground: "#f92aad",
cursor: "#f92aad",
cursorAccent: "#241b2f",
selectionBackground: "#495495",
black: "#000000",
red: "#f6188f",
green: "#1eff8e",
yellow: "#ffe261",
blue: "#03edf9",
magenta: "#f10596",
cyan: "#03edf9",
white: "#ffffff",
brightBlack: "#5a5a5a",
brightRed: "#ff1a8e",
brightGreen: "#1eff8e",
brightYellow: "#ffff00",
brightBlue: "#00d8ff",
brightMagenta: "#ff00d4",
brightCyan: "#00ffff",
brightWhite: "#ffffff",
},
},
cobalt2: {
name: "Cobalt2",
category: "dark",
colors: {
background: "#193549",
foreground: "#ffffff",
cursor: "#f0cc09",
cursorAccent: "#193549",
selectionBackground: "#0050a4",
black: "#000000",
red: "#ff0000",
green: "#38de21",
yellow: "#ffe50a",
blue: "#1460d2",
magenta: "#ff005d",
cyan: "#00bbbb",
white: "#bbbbbb",
brightBlack: "#555555",
brightRed: "#f40e17",
brightGreen: "#3bd01d",
brightYellow: "#edc809",
brightBlue: "#5555ff",
brightMagenta: "#ff55ff",
brightCyan: "#6ae3fa",
brightWhite: "#ffffff",
},
},
snazzy: {
name: "Snazzy",
category: "dark",
colors: {
background: "#282a36",
foreground: "#eff0eb",
cursor: "#97979b",
cursorAccent: "#282a36",
selectionBackground: "#97979b",
black: "#282a36",
red: "#ff5c57",
green: "#5af78e",
yellow: "#f3f99d",
blue: "#57c7ff",
magenta: "#ff6ac1",
cyan: "#9aedfe",
white: "#f1f1f0",
brightBlack: "#686868",
brightRed: "#ff5c57",
brightGreen: "#5af78e",
brightYellow: "#f3f99d",
brightBlue: "#57c7ff",
brightMagenta: "#ff6ac1",
brightCyan: "#9aedfe",
brightWhite: "#eff0eb",
},
},
atomOneDark: {
name: "Atom One Dark",
category: "dark",
colors: {
background: "#1e2127",
foreground: "#abb2bf",
cursor: "#528bff",
cursorAccent: "#1e2127",
selectionBackground: "#3e4451",
black: "#000000",
red: "#e06c75",
green: "#98c379",
yellow: "#d19a66",
blue: "#61afef",
magenta: "#c678dd",
cyan: "#56b6c2",
white: "#abb2bf",
brightBlack: "#5c6370",
brightRed: "#e06c75",
brightGreen: "#98c379",
brightYellow: "#d19a66",
brightBlue: "#61afef",
brightMagenta: "#c678dd",
brightCyan: "#56b6c2",
brightWhite: "#ffffff",
},
},
catppuccinMocha: {
name: "Catppuccin Mocha",
category: "dark",
colors: {
background: "#1e1e2e",
foreground: "#cdd6f4",
cursor: "#f5e0dc",
cursorAccent: "#1e1e2e",
selectionBackground: "#585b70",
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",
},
},
gentlemansChoice: {
name: "Gentleman's Choice",
category: "dark",
colors: {
background: "#1a1c1a",
foreground: "#d1c7a3",
cursor: "#d1c7a3",
cursorAccent: "#1a1c1a",
selectionBackground: "#3e4437",
black: "#1a1c1a",
red: "#9d3a3a",
green: "#5a7a3a",
yellow: "#b39a3a",
blue: "#3a5a7a",
magenta: "#7a3a5a",
cyan: "#3a7a7a",
white: "#d1c7a3",
brightBlack: "#3e4437",
brightRed: "#bf4a4a",
brightGreen: "#7a9a4a",
brightYellow: "#d1b34a",
brightBlue: "#4a7abf",
brightMagenta: "#9a4abf",
brightCyan: "#4abfbf",
brightWhite: "#e3dbc3",
},
},
midnightEspresso: {
name: "Midnight Espresso",
category: "dark",
colors: {
background: "#120f0d",
foreground: "#ceb195",
cursor: "#ceb195",
cursorAccent: "#120f0d",
selectionBackground: "#3d2b1f",
black: "#120f0d",
red: "#a05a4a",
green: "#7a8a5a",
yellow: "#b08a4a",
blue: "#5a7a9a",
magenta: "#8a5a7a",
cyan: "#5a8a8a",
white: "#ceb195",
brightBlack: "#3d2b1f",
brightRed: "#c07a6a",
brightGreen: "#9aaa7a",
brightYellow: "#d0aa6a",
brightBlue: "#7a9aba",
brightMagenta: "#aa7aba",
brightCyan: "#7ababa",
brightWhite: "#e0cbb5",
},
},
};
export const TERMINAL_FONTS = [
{
value: "Caskaydia Cove Nerd Font Mono",
label: "Caskaydia Cove Nerd Font Mono",
fallback:
'"Caskaydia Cove Nerd Font Mono", "SF Mono", Consolas, "Liberation Mono", monospace',
},
{
value: "JetBrains Mono",
label: "JetBrains Mono",
fallback:
'"JetBrains Mono", "SF Mono", Consolas, "Liberation Mono", monospace',
},
{
value: "Fira Code",
label: "Fira Code",
fallback: '"Fira Code", "SF Mono", Consolas, "Liberation Mono", monospace',
},
{
value: "Cascadia Code",
label: "Cascadia Code",
fallback:
'"Cascadia Code", "SF Mono", Consolas, "Liberation Mono", monospace',
},
{
value: "Source Code Pro",
label: "Source Code Pro",
fallback:
'"Source Code Pro", "SF Mono", Consolas, "Liberation Mono", monospace',
},
{
value: "SF Mono",
label: "SF Mono",
fallback: '"SF Mono", Consolas, "Liberation Mono", monospace',
},
{
value: "Consolas",
label: "Consolas",
fallback: 'Consolas, "Liberation Mono", monospace',
},
{
value: "Monaco",
label: "Monaco",
fallback: 'Monaco, "Liberation Mono", monospace',
},
];
export const CURSOR_STYLES = [
{ value: "block", label: "Block" },
{ value: "underline", label: "Underline" },
{ value: "bar", label: "Bar" },
] as const;
export const BELL_STYLES = [
{ value: "none", label: "None" },
{ value: "sound", label: "Sound" },
{ value: "visual", label: "Visual" },
{ value: "both", label: "Both" },
] as const;
export const FAST_SCROLL_MODIFIERS = [
{ value: "alt", label: "Alt" },
{ value: "ctrl", label: "Ctrl" },
{ value: "shift", label: "Shift" },
] as const;
export const DEFAULT_TERMINAL_CONFIG = {
cursorBlink: true,
cursorStyle: "bar" as const,
fontSize: 14,
fontFamily: "Caskaydia Cove Nerd Font Mono",
letterSpacing: 0,
lineHeight: 1.0,
theme: "termix",
scrollback: 10000,
bellStyle: "none" as const,
rightClickSelectsWord: false,
fastScrollModifier: "alt" as const,
fastScrollSensitivity: 5,
minimumContrastRatio: 1,
backspaceMode: "normal" as const,
agentForwarding: false,
environmentVariables: [] as Array<{ key: string; value: string }>,
startupSnippetId: null as number | null,
autoMosh: false,
moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8",
sudoPasswordAutoFill: false,
keepaliveInterval: undefined as number | undefined,
keepaliveCountMax: undefined as number | undefined,
autoTmux: false,
};
export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG;
+123
View File
@@ -0,0 +1,123 @@
import type {
AccentColorId,
DashboardCardConfig,
FontSizeId,
SplitMode,
} from "./types";
export const DASHBOARD_CARDS: DashboardCardConfig[] = [
{
id: "stats_bar",
label: "Status Bar",
description: "Version, uptime, database health, hosts online",
defaultEnabled: true,
},
{
id: "counters_bar",
label: "Counters Bar",
description: "Total hosts, credentials, and tunnels count",
defaultEnabled: true,
},
{
id: "quick_actions",
label: "Quick Actions",
description: "Shortcuts to add hosts, credentials, settings",
defaultEnabled: true,
},
{
id: "host_status",
label: "Host Status",
description: "Live status list with CPU/RAM per host",
defaultEnabled: true,
},
{
id: "recent_activity",
label: "Recent Activity",
description: "Feed of recent connection events",
defaultEnabled: true,
},
{
id: "network_graph",
label: "Network Graph",
description: "Visual map of host network topology",
defaultEnabled: false,
},
];
export const ACCENT_PRESET_COLORS = [
{ label: "Orange", value: "#f59145" },
{ label: "Blue", value: "#3b82f6" },
{ label: "Green", value: "#22c55e" },
{ label: "Purple", value: "#a855f7" },
{ label: "Pink", value: "#ec4899" },
{ label: "Cyan", value: "#06b6d4" },
{ label: "Red", value: "#ef4444" },
{ label: "Yellow", value: "#eab308" },
{ label: "Teal", value: "#14b8a6" },
{ label: "Indigo", value: "#6366f1" },
{ label: "Rose", value: "#f43f5e" },
{ label: "Lime", value: "#84cc16" },
];
export const ACCENT_COLORS = ACCENT_PRESET_COLORS.map((c) => ({
id: c.label.toLowerCase() as AccentColorId,
label: c.label,
value: c.value,
}));
export function applyAccentColor(colorValue: string) {
document.documentElement.style.setProperty("--accent-brand", colorValue);
}
export const FONT_SIZES: { id: FontSizeId; label: string }[] = [
{ id: "xs", label: "XS" },
{ id: "sm", label: "Small" },
{ id: "md", label: "Normal" },
{ id: "lg", label: "Large" },
{ id: "xl", label: "XL" },
];
export function applyFontSize(id: FontSizeId) {
const root = document.documentElement;
root.classList.remove("fs-xs", "fs-sm", "fs-md", "fs-lg", "fs-xl");
root.classList.add(`fs-${id}`);
localStorage.setItem("termix-font-size", id);
}
export const FOLDER_COLORS = [
"#ef4444",
"#f97316",
"#eab308",
"#22c55e",
"#3b82f6",
"#a855f7",
"#ec4899",
"#6b7280",
];
export const SPLIT_MODES: { id: SplitMode; label: string }[] = [
{ id: "none", label: "None" },
{ id: "2-way", label: "2-Way" },
{ id: "3-way", label: "3-Way" },
{ id: "4-way", label: "4-Way" },
{ id: "5-way", label: "5-Way" },
{ id: "6-way", label: "6-Way" },
];
export const PANE_COUNTS: Record<SplitMode, number> = {
none: 0,
"2-way": 2,
"3-way": 3,
"4-way": 4,
"5-way": 5,
"6-way": 6,
};
export const PANE_LAYOUTS: Record<SplitMode, string> = {
none: "",
"2-way": "grid-cols-2 grid-rows-1",
"3-way": "grid-cols-2 grid-rows-2",
"4-way": "grid-cols-2 grid-rows-2",
"5-way": "grid-cols-3 grid-rows-2",
"6-way": "grid-cols-3 grid-rows-2",
};
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}