// Typed wrappers around the Rust p4 commands. import { invoke } from "@tauri-apps/api/core"; export interface P4Info { serverAddress?: string; serverVersion?: string; userName?: string; clientName?: string; clientRoot?: string; serverUptime?: string; serverDate?: string; [k: string]: unknown; } export interface OpenedFile { depotFile?: string; clientFile?: string; rev?: string; haveRev?: string; action?: string; // edit | add | delete | branch | integrate | move/add | move/delete type?: string; // text | binary | ... change?: string; // "default" or a number user?: string; client?: string; _spec?: string; // when set, read this exact rev via `p4 print` (submitted/historical file) [k: string]: unknown; } // choose the byte source: an exact submitted revision (`p4 print`) when `_spec` // is set, otherwise the working copy (local file via `p4 where`). export function fileBytes(f: OpenedFile): Promise { return f._spec ? p4.printDepot(f._spec) : p4.readDepot(f.depotFile || ""); } export interface Client { client?: string; Root?: string; Host?: string; Stream?: string; [k: string]: unknown; } export interface Change { change?: string; time?: string; user?: string; client?: string; status?: string; desc?: string; [k: string]: unknown; } export const p4 = { clients: (port: string, user: string) => invoke("p4_clients", { port, user }), login: (port: string, user: string, password: string, client: string) => invoke("p4_login", { port, user, password, client }), restore: (port: string, user: string, client: string) => invoke("p4_restore", { port, user, client }), info: () => invoke("p4_info"), opened: (scope = "") => invoke("p4_opened", { scope }), dirs: (path = "") => invoke("p4_dirs", { path }), changes: () => invoke("p4_changes"), history: (path: string) => invoke("p4_history", { path }), disconnect: () => invoke("p4_disconnect"), sync: (scope = "") => invoke("p4_sync", { scope }), syncTo: (scope: string, rev: string) => invoke("p4_sync_to", { scope, rev }), labels: () => invoke<{ label?: string; Update?: string; Owner?: string; Description?: string; [k: string]: unknown }[]>("p4_labels"), labelTag: (label: string, scope: string, rev = "") => invoke("p4_label_tag", { label, scope, rev }), branchOp: (op: "merge" | "copy" | "integrate", source: string, target: string) => invoke("p4_branch_op", { op, source, target }), streams: () => invoke<{ Stream?: string; Name?: string; Type?: string; Parent?: string; [k: string]: unknown }[]>("p4_streams"), switchStream: (stream: string) => invoke("p4_switch_stream", { stream }), streamInteg: (stream: string, direction: "down" | "up") => invoke("p4_stream_integ", { stream, direction }), filelog: (depot: string) => invoke>("p4_filelog", { depot }), jobs: () => invoke<{ Job?: string; Status?: string; Description?: string; User?: string; [k: string]: unknown }[]>("p4_jobs"), fix: (job: string, change: string) => invoke("p4_fix", { job, change }), jobSpec: (job: string) => invoke("p4_job_spec", { job }), jobSave: (spec: string) => invoke("p4_job_save", { spec }), diff: (file: string) => invoke("p4_diff", { file }), revert: (files: string[]) => invoke("p4_revert", { files }), submit: (description: string, files: string[]) => invoke("p4_submit", { description, files }), commit: (description: string, files: string[]) => invoke("p4_commit", { description, files }), submitChange: (change: string) => invoke("p4_submit_change", { change }), reopenDefault: (files: string[]) => invoke("p4_reopen_default", { files }), setDesc: (change: string, description: string) => invoke("p4_set_desc", { change, description }), add: (files: string[]) => invoke("p4_add", { files }), edit: (files: string[]) => invoke("p4_edit", { files }), del: (files: string[]) => invoke("p4_delete", { files }), reconcile: (path: string) => invoke("p4_reconcile", { path }), scan: (scope = "") => invoke("p4_scan", { scope }), describe: (change: string) => invoke("p4_describe", { change }), undoChange: (change: string) => invoke("p4_undo_change", { change }), openInExplorer: (target: string) => invoke("open_in_explorer", { target }), findUproject: (scope: string) => invoke("find_uproject", { scope }), findSln: (scope: string) => invoke("find_sln", { scope }), buildSln: (path: string, config: string) => invoke("build_sln", { path, config }), launchFile: (path: string) => invoke("launch_file", { path }), listEditors: () => invoke("list_editors"), openInEditor: (depot: string, editor: string) => invoke("open_in_editor", { depot, editor }), readDepot: async (depot: string): Promise => { const r: unknown = await invoke("read_depot", { depot }); if (r instanceof ArrayBuffer) return r; if (ArrayBuffer.isView(r)) return (r as ArrayBufferView).buffer as ArrayBuffer; if (Array.isArray(r)) return new Uint8Array(r as number[]).buffer; return r as ArrayBuffer; }, // read raw bytes at an exact revision (submitted/historical), e.g. "//depot/f.cpp#7" printDepot: async (spec: string): Promise => { const r: unknown = await invoke("print_depot", { spec }); if (r instanceof ArrayBuffer) return r; if (ArrayBuffer.isView(r)) return (r as ArrayBufferView).buffer as ArrayBuffer; if (Array.isArray(r)) return new Uint8Array(r as number[]).buffer; return r as ArrayBuffer; }, // built-in editor: write text back to a depot file's working copy writeDepot: (depot: string, content: string) => invoke("write_depot", { depot, content }), switchClient: (client: string) => invoke("p4_switch_client", { client }), clientSpec: (client: string) => invoke("p4_client_spec", { client }), clientSave: (spec: string) => invoke("p4_client_save", { spec }), users: () => invoke("p4_users"), groups: (user = "") => invoke<{ group?: string }[]>("p4_groups", { user }), groupMember: (group: string, user: string, add: boolean) => invoke("p4_group_member", { group, user, add }), readAiKey: () => invoke("read_ai_key"), saveAiKey: (key: string) => invoke("save_ai_key", { key }), // run an arbitrary p4 command in the built-in terminal (combined stdout+stderr) console: (args: string[]) => invoke("p4_console", { args }), // tail the newest Unreal log for a project (.uproject path → Saved/Logs/*.log) ueLog: (uproject: string, tailBytes = 200_000) => invoke("ue_log", { uproject, tailBytes }), // --- collaboration / advanced Perforce --- openedAll: (scope = "") => invoke("p4_opened_all", { scope }), lock: (files: string[]) => invoke("p4_lock", { files }), unlock: (files: string[]) => invoke("p4_unlock", { files }), typemapGet: () => invoke("p4_typemap_get"), typemapSet: (entries: string[]) => invoke("p4_typemap_set", { entries }), reopenType: (files: string[], filetype: string) => invoke("p4_reopen_type", { files, filetype }), shelve: (change: string) => invoke("p4_shelve", { change }), unshelve: (change: string) => invoke("p4_unshelve", { change }), deleteShelf: (change: string) => invoke("p4_delete_shelf", { change }), shelvedFiles: (change: string) => invoke("p4_shelved_files", { change }), resolveList: () => invoke("p4_resolve_list"), resolveFile: (file: string, mode: "am" | "ay" | "at") => invoke("p4_resolve_file", { file, mode }), diff2: (spec1: string, spec2: string) => invoke("p4_diff2", { spec1, spec2 }), annotate: (spec: string) => invoke("p4_annotate", { spec }), searchFiles: (query: string, scope = "") => invoke<{ depotFile?: string; rev?: string }[]>("p4_search_files", { query, scope }), reopenTo: (change: string, files: string[]) => invoke("p4_reopen_to", { change, files }), latestChange: (scope = "") => invoke("p4_latest_change", { scope }), // embedded Unreal .uasset thumbnail (PNG bytes), for working copy or history uassetThumb: async (spec: string, historical: boolean): Promise => { const r: unknown = await invoke("uasset_thumbnail", { spec, historical }); if (r instanceof ArrayBuffer) return r; if (ArrayBuffer.isView(r)) return (r as ArrayBufferView).buffer as ArrayBuffer; if (Array.isArray(r)) return new Uint8Array(r as number[]).buffer; return r as ArrayBuffer; }, // export a working-copy .uasset mesh to FBX via headless Unreal → real 3D exportUasset3d: async (depot: string, uproject: string): Promise => { const r: unknown = await invoke("uasset_export_3d", { depot, uproject }); if (r instanceof ArrayBuffer) return r; if (ArrayBuffer.isView(r)) return (r as ArrayBufferView).buffer as ArrayBuffer; if (Array.isArray(r)) return new Uint8Array(r as number[]).buffer; return r as ArrayBuffer; }, }; export interface User { User?: string; Email?: string; FullName?: string; Access?: string; Update?: string; [k: string]: unknown } // a detected code editor / IDE export interface Editor { id: string; name: string; exe: string; args: string[] } // which editor to open code files in (persisted; "" → auto-pick) export function getEditor(): string { try { return localStorage.getItem("exd-editor") || ""; } catch { return ""; } } export function setEditorPref(id: string) { try { localStorage.setItem("exd-editor", id); } catch {} } export interface Dir { dir?: string; [k: string]: unknown } // depot path -> short leaf name (last segment) export function leaf(path?: string): string { if (!path) return ""; const p = path.replace(/\/+$/, ""); return p.slice(p.lastIndexOf("/") + 1); } // per-file revision history parsed from p4 filelog's indexed fields export interface FileRev { rev: string; change: string; action: string; user: string; time: string; desc: string; type: string } export function filelogRevs(v: Record): FileRev[] { const out: FileRev[] = []; for (let i = 0; ; i++) { const rev = v[`rev${i}`] as string | undefined; if (rev == null) break; out.push({ rev, change: (v[`change${i}`] as string) || "", action: (v[`action${i}`] as string) || "", user: (v[`user${i}`] as string) || "", time: (v[`time${i}`] as string) || "", desc: (v[`desc${i}`] as string) || "", type: (v[`type${i}`] as string) || "", }); } return out; } // a submitted changelist with indexed file fields (depotFile0, action0, ...) export interface Describe extends Change { [k: string]: unknown } export function describeFiles(d: Describe): { depotFile: string; action: string; rev: string }[] { const out: { depotFile: string; action: string; rev: string }[] = []; for (let i = 0; ; i++) { const dp = d[`depotFile${i}`] as string | undefined; if (!dp) break; out.push({ depotFile: dp, action: (d[`action${i}`] as string) || "edit", rev: (d[`rev${i}`] as string) || "" }); } return out; } // --- session persistence (server/user/workspace — никогда не пароль) --- export interface Session { server: string; user: string; client: string } const SESSION_KEY = "exd-session"; export function saveSession(s: Session) { try { localStorage.setItem(SESSION_KEY, JSON.stringify(s)); } catch {} } export function loadSession(): Session | null { try { const r = localStorage.getItem(SESSION_KEY); return r ? JSON.parse(r) : null; } catch { return null; } } export function clearSession() { try { localStorage.removeItem(SESSION_KEY); } catch {} } // --- working-folder scope persistence (per workspace) --- export function saveScope(client: string, path: string) { try { localStorage.setItem("exd-scope-" + client, path); } catch {} } export function loadScope(client: string): string { try { return client ? (localStorage.getItem("exd-scope-" + client) || "") : ""; } catch { return ""; } } // format a unix-epoch string as a short local datetime export function fmtTime(t?: string): string { if (!t) return ""; const n = Number(t); if (!n) return ""; const d = new Date(n * 1000); return d.toLocaleString(undefined, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" }); } // action -> single-char status + css class export function statusOf(action?: string): { ch: string; cls: string } { const a = (action || "").toLowerCase(); if (a.includes("add")) return { ch: "+", cls: "st-a" }; if (a.includes("delete")) return { ch: "−", cls: "st-d" }; return { ch: "±", cls: "st-e" }; // edit / integrate / branch / move } // depot or client path -> { name, dir } export function splitPath(p?: string): { name: string; dir: string } { if (!p) return { name: "(unknown)", dir: "" }; const norm = p.replace(/\\/g, "/"); const i = norm.lastIndexOf("/"); return { name: norm.slice(i + 1), dir: norm.slice(0, i + 1) }; } // classify by extension for the preview panel export type Kind = "model" | "uasset" | "image" | "code"; export function kindOf(name: string): Kind { const n = name.toLowerCase(); if (/\.(fbx|obj|gltf|glb|stl|ply|3mf|dae)$/.test(n)) return "model"; if (/\.(uasset|umap)$/.test(n)) return "uasset"; if (/\.(png|jpe?g|webp|gif|bmp|svg)$/.test(n)) return "image"; return "code"; } // true only for real text/source files — gates the "Summary" and "Open in IDE" // buttons so they never appear on binaries (.pak/.dll/.exe/.bin/etc.) that // happen to fall through kindOf() as "code". const CODE_EXT = /\.(c|cc|cpp|cxx|h|hpp|hh|inl|cs|ts|tsx|js|jsx|mjs|cjs|py|rs|go|java|kt|kts|swift|m|mm|rb|php|lua|sh|bash|ps1|bat|cmd|sql|json|jsonc|yaml|yml|toml|ini|cfg|conf|xml|html|htm|css|scss|sass|less|md|markdown|txt|log|csv|gitignore|editorconfig|uproject|uplugin|build|target|usf|ush|hlsl|glsl|shader|cginc|props|targets|sln|vcxproj|csproj|gradle|cmake|make|mk)$/i; export function isCodeFile(name: string): boolean { const n = name.toLowerCase(); if (/\.(uasset|umap|pak|exe|dll|so|dylib|bin|obj|lib|a|pdb|zip|7z|rar|gz|tar|png|jpe?g|gif|bmp|webp|svg|fbx|obj|gltf|glb|stl|ply|dae|wav|mp3|ogg|mp4|mov|ttf|otf)$/i.test(n)) return false; return CODE_EXT.test(n); }