Files
exbyte-depot-viewer-perforce/src/p4.ts
Bonchellon 72b163f592 Add "Edit in VS Code" button to the code preview
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>
2026-07-07 15:51:06 +03:00

163 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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";
}