Code/text files get a VS Code action in the preview header; a backend open_in_vscode command resolves the depot file to its local working copy and launches `code <path>` via the shell. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
6.4 KiB
TypeScript
163 lines
6.4 KiB
TypeScript
// 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;
|
||
[k: string]: unknown;
|
||
}
|
||
|
||
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<Client[]>("p4_clients", { port, user }),
|
||
login: (port: string, user: string, password: string, client: string) =>
|
||
invoke<P4Info>("p4_login", { port, user, password, client }),
|
||
restore: (port: string, user: string, client: string) =>
|
||
invoke<P4Info>("p4_restore", { port, user, client }),
|
||
info: () => invoke<P4Info>("p4_info"),
|
||
opened: (scope = "") => invoke<OpenedFile[]>("p4_opened", { scope }),
|
||
dirs: (path = "") => invoke<Dir[]>("p4_dirs", { path }),
|
||
changes: () => invoke<Change[]>("p4_changes"),
|
||
history: (path: string) => invoke<Change[]>("p4_history", { path }),
|
||
disconnect: () => invoke<void>("p4_disconnect"),
|
||
sync: (scope = "") => invoke<string>("p4_sync", { scope }),
|
||
diff: (file: string) => invoke<string>("p4_diff", { file }),
|
||
revert: (files: string[]) => invoke<string>("p4_revert", { files }),
|
||
submit: (description: string, files: string[]) =>
|
||
invoke<string>("p4_submit", { description, files }),
|
||
commit: (description: string, files: string[]) =>
|
||
invoke<string>("p4_commit", { description, files }),
|
||
submitChange: (change: string) => invoke<string>("p4_submit_change", { change }),
|
||
add: (files: string[]) => invoke<OpenedFile[]>("p4_add", { files }),
|
||
edit: (files: string[]) => invoke<OpenedFile[]>("p4_edit", { files }),
|
||
del: (files: string[]) => invoke<OpenedFile[]>("p4_delete", { files }),
|
||
reconcile: (path: string) => invoke<OpenedFile[]>("p4_reconcile", { path }),
|
||
scan: (scope = "") => invoke<OpenedFile[]>("p4_scan", { scope }),
|
||
describe: (change: string) => invoke<Describe>("p4_describe", { change }),
|
||
undoChange: (change: string) => invoke<string>("p4_undo_change", { change }),
|
||
openInExplorer: (target: string) => invoke<void>("open_in_explorer", { target }),
|
||
findUproject: (scope: string) => invoke<string>("find_uproject", { scope }),
|
||
launchFile: (path: string) => invoke<void>("launch_file", { path }),
|
||
openInVscode: (depot: string) => invoke<void>("open_in_vscode", { depot }),
|
||
readDepot: async (depot: string): Promise<ArrayBuffer> => {
|
||
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;
|
||
},
|
||
switchClient: (client: string) => invoke<P4Info>("p4_switch_client", { client }),
|
||
};
|
||
|
||
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);
|
||
}
|
||
|
||
// 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";
|
||
}
|