mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-09-13 18:58:52 +00:00
feat: initial ui redesign from demo
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user