export type EntryType = "file" | "dir" | "symlink"; export interface FileEntry { name: string; path: string; // absolute path on its filesystem type: EntryType; size: number; // bytes mtime: number; // epoch milliseconds mode: number; // permission bits only (masked & 0o7777) owner?: string; // resolved name when the provider knows it group?: string; } /** Optional file-list columns (Name is always shown). */ export type ColumnKey = "size" | "mtime" | "kind" | "rights" | "owner" | "group"; /** Every RPC response uses this envelope; errors never cross the bridge as exceptions. */ export type Result = ({ ok: true } & T) | { ok: false; error: string }; export type AuthConfig = | { kind: "password"; password: string } | { kind: "key"; privateKeyPath: string; passphrase?: string } | { kind: "agent" }; export interface ConnectionConfig { host: string; port: number; username: string; auth: AuthConfig; initialPath?: string | null; } export interface SavedConnection extends ConnectionConfig { id: string; stackId: string | null; name: string; /** Other-panel local dir applied when that panel is local and untouched. */ localDir?: string | null; } /** A named group of connections that can be opened together as tabs. */ export interface Stack { id: string; name: string; /** Collapsed in the connection-manager list (UI state, persisted). */ collapsed?: boolean; } /** A concrete host alias resolved from ~/.ssh/config (read-only). */ export interface SshConfigHost { name: string; // the Host alias as written hostname: string; // resolved HostName (defaults to alias) port: number; username: string; identityFile?: string; // first existing IdentityFile, absolute path unsupported?: "proxy"; // ProxyJump/ProxyCommand present — direct connect can't work } export interface ConnectionsFile { version: 2; stacks: Stack[]; connections: SavedConnection[]; } /** UI languages the app ships translations for. */ export type SupportedLocale = "en" | "es" | "eu" | "fr" | "de" | "zh" | "ja"; export type AccentTone = | "graphite" | "blue" | "indigo" | "purple" | "rose" | "orange" | "green" | "teal"; export interface AppSettings { /** UI language; "system" resolves to the OS locale (fallback English). */ locale: "system" | SupportedLocale; /** * Visual skin. "modern" is the shadcn-style look; "win7" swaps in 7.css * (Windows 7 Aero) and switches the window to custom frameless chrome. * Each skin is a whole component tree under ui/src/app/skins/ — "modern" is * shadlike/, "win7" is 7like/ — selected at bootstrap in ui/src/main.ts. * The value is NOT named after its folder on purpose: it is persisted in * user settings, and "win7" is also the literal class 7.scoped.css is * precompiled to require, so neither can be renamed freely. * Changing this needs a window reopen to take effect on the frame itself — * the CSS half applies live. */ skin: "modern" | "win7"; theme: "system" | "light" | "dark"; /** Dark appearance: grey ("dark") or near-black AMOLED ("darker"). */ darkTone: "dark" | "darker"; /** Accent tone for chrome and selection tint; graphite = neutral zinc. */ accent: AccentTone; /** File-list icons: plain neutral glyphs, or per-category glyphs and colors. */ iconColors: "plain" | "multi"; /** Visible optional file-list columns (both panels; Name always shows). */ columns: ColumnKey[]; speedUnit: "MBps" | "Mbps"; /** * Interface scale factor, 0.5–4 in 0.1 steps (shown as 50%–400%). Works by * setting the root font-size — every size in the UI is rem, so one lever * scales BOTH skins whole. Replaced the 3-tier `textSize` * (compact|default|large); SettingsStore.load migrates old files. */ uiScale: number; /** Run transfer jobs one at a time, or several in parallel. */ transferMode: "single" | "multi"; /** How many jobs run at once when transferMode is "multi". */ transferConcurrency: number; /** Remove completed transfers from the list automatically. */ autoClearCompleted: boolean; /** Edit files with the built-in editor or an external application. */ editor: "builtin" | "custom"; /** Full path to the external editor .app; empty = OS default application. */ editorApp: string; // Auto-refresh the active tab's panels (local watched, remote polled). Always // on for now; uncomment (+ the settings.service and tab-bar breadcrumbs, and // gate the poll/watch in panels.service) to expose a toggle later. // autoRefresh: boolean; } export const DEFAULT_SETTINGS: AppSettings = { locale: "system", skin: "modern", theme: "system", darkTone: "dark", accent: "blue", iconColors: "multi", columns: ["size", "mtime"], speedUnit: "MBps", uiScale: 1, transferMode: "single", transferConcurrency: 3, autoClearCompleted: true, editor: "builtin", editorApp: "", // autoRefresh: true, }; export const UI_SCALE_MIN = 0.5; export const UI_SCALE_MAX = 4; export const UI_SCALE_STEP = 0.1; /** * Clamp + snap a scale factor to the legal range and step grid. Shared by the * store (a hand-edited settings.json must not be able to render the UI * unusable), the settings service and the stepper — one definition of "valid", * same as effectiveConcurrency below. */ export function clampUiScale(value: number): number { if (!Number.isFinite(value)) return 1; const clamped = Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, value)); // snap to the step grid; toFixed strips float noise (0.7000000000000001 -> 0.7) return Number((Math.round(clamped / UI_SCALE_STEP) * UI_SCALE_STEP).toFixed(2)); } export function effectiveConcurrency(settings: AppSettings): number { if (settings.transferMode === "single") return 1; return Math.max(1, Math.min(8, Math.round(settings.transferConcurrency) || 1)); } export type TransferState = | "queued" | "scanning" | "running" | "completed" | "failed" | "cancelled"; export interface TransferSnapshot { id: string; name: string; // display name (basename of source) srcSessionId: string; srcPath: string; dstSessionId: string; dstDir: string; state: TransferState; totalFiles: number; totalBytes: number; doneFiles: number; doneBytes: number; currentFile: string; currentFileBytes: number; currentFileTotal: number; error?: string; }