Files
spaceshell/app/src/socketBridge.ts
T
2026-06-15 16:07:32 +07:00

254 lines
8.4 KiB
TypeScript

import { invoke, Channel } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { Group, WorkspaceView } from "./layoutTypes";
export interface WorkspaceStatus {
workspace_id: string;
path: string;
surfaces: string[];
}
export async function openWorkspace(path: string): Promise<string> {
const data = await invoke<{ workspace_id: string }>("open", { path });
return data.workspace_id;
}
export async function newSurface(
workspaceId: string,
cols: number,
rows: number,
command?: string,
args: string[] = []
): Promise<string> {
const data = await invoke<{ surface_id: string }>("new_surface", {
workspaceId,
command: command ?? null,
args,
cols,
rows,
});
return data.surface_id;
}
export async function sendInput(surfaceId: string, data: Uint8Array): Promise<void> {
await invoke("input", { surfaceId, data: Array.from(data) });
}
export async function resizeSurface(surfaceId: string, cols: number, rows: number): Promise<void> {
await invoke("resize", { surfaceId, cols, rows });
}
export interface AttachResult {
snapshot: string;
cols: number;
rows: number;
cursor_row?: number;
cursor_col?: number;
stopped?: boolean;
}
export async function attachSurface(
surfaceId: string,
onOutput: (bytes: Uint8Array) => void
): Promise<AttachResult> {
const channel = new Channel<number[]>();
channel.onmessage = (msg) => onOutput(new Uint8Array(msg));
return await invoke<AttachResult>("attach", { surfaceId, onOutput: channel });
}
export async function detachSurface(surfaceId: string): Promise<void> {
await invoke("detach", { surfaceId });
}
export async function getStatus(): Promise<WorkspaceStatus[]> {
const data = await invoke<{ workspaces: WorkspaceStatus[] }>("status");
return data.workspaces;
}
export interface EventRecord {
id: number;
surface_id: string;
workspace_id: string;
workspace_name: string;
agent_label: string | null;
kind: "done" | "wait" | "error" | "exit";
ts: number;
read: boolean;
}
export type MarkReadTarget =
| { target: "all" }
| { target: "ids"; value: number[] }
| { target: "surface"; value: string };
export async function getEventLog(limit?: number): Promise<{ events: EventRecord[]; unread: number }> {
return await invoke<{ events: EventRecord[]; unread: number }>("event_log", { limit: limit ?? null });
}
export async function markEventsRead(target: MarkReadTarget): Promise<void> {
await invoke("mark_read", { target });
}
export async function clearEvents(): Promise<void> {
await invoke("clear_events");
}
export type DaemonEvt =
| { evt: "exit"; data: { surface_id: string; code: number } }
| { evt: "surface_created"; data: { surface_id: string; workspace_id: string } }
| { evt: "surface_closed"; data: { surface_id: string } }
| { evt: "state"; data: { surface_id: string; state: import("./layoutTypes").SurfaceState } }
| { evt: "layout_changed"; data: { workspace_id: string } }
| { evt: "workspace_changed"; data: unknown }
| { evt: "groups_changed"; data: unknown }
| { evt: "config_changed"; data: { config: ConfigView } }
| { evt: "event"; data: { record: EventRecord } }
| { evt: "events_read"; data: { ids: number[] } }
| { evt: "events_cleared"; data: unknown };
export function onDaemonEvent(handler: (evt: DaemonEvt) => void): Promise<() => void> {
return listen<DaemonEvt>("spacesh:evt", (e) => handler(e.payload));
}
export async function focusSurface(surfaceId: string): Promise<void> {
await invoke("focus", { surfaceId });
}
export function onDaemonRawEvent(name: string, handler: () => void): Promise<() => void> {
return listen(name, () => handler());
}
// ---- M2 additions ----
export interface StatusResult {
groups: Group[];
workspaces: WorkspaceView[];
}
export async function getStatusFull(): Promise<StatusResult> {
return await invoke<StatusResult>("status");
}
export async function splitSurface(surfaceId: string, dir: "right" | "down", command?: string, args: string[] = []): Promise<string> {
const data = await invoke<{ surface_id: string }>("split_surface", { surfaceId, dir, command: command ?? null, args });
return data.surface_id;
}
export async function setRatios(workspaceId: string, nodePath: number[], ratios: number[]): Promise<void> {
await invoke("set_ratios", { workspaceId, nodePath, ratios });
}
export async function moveSurface(surfaceId: string, targetSurfaceId: string, edge: "left" | "right" | "top" | "bottom"): Promise<void> {
await invoke("move_surface", { surfaceId, targetSurfaceId, edge });
}
export async function applyPreset(workspaceId: string, presetId: string, slots: { command?: string; args?: string[] }[]): Promise<string[]> {
const data = await invoke<{ surface_ids: string[] }>("apply_preset", {
workspaceId, presetId,
slots: slots.map((s) => ({ command: s.command ?? null, args: s.args ?? [] })),
});
return data.surface_ids;
}
export async function restartSurface(surfaceId: string, resume = false): Promise<void> {
await invoke("restart_surface", { surfaceId, resume });
}
export async function closeWorkspaceCmd(workspaceId: string): Promise<void> {
await invoke("close_workspace", { workspaceId });
}
export async function setWorkspaceMeta(workspaceId: string, meta: { name?: string; groupId?: string | null; unread?: boolean; order?: number; pinned?: boolean }): Promise<void> {
await invoke("set_workspace_meta", {
workspaceId,
name: meta.name ?? null,
groupId: meta.groupId === undefined ? null : meta.groupId,
unread: meta.unread ?? null,
order: meta.order ?? null,
pinned: meta.pinned ?? null,
});
}
export async function createGroup(name: string, color: string): Promise<string> {
const data = await invoke<{ group_id: string }>("create_group", { name, color });
return data.group_id;
}
export async function setGroup(groupId: string, meta: { name?: string; color?: string; order?: number }): Promise<void> {
await invoke("set_group", { groupId, name: meta.name ?? null, color: meta.color ?? null, order: meta.order ?? null });
}
export async function deleteGroup(groupId: string): Promise<void> {
await invoke("delete_group", { groupId });
}
export async function closeSurfaceCmd(surfaceId: string): Promise<void> {
await invoke("close_surface", { surfaceId });
}
export interface DaemonHealth { version: string; build?: string; pid: number; started_at_ms: number }
export async function getHealth(): Promise<DaemonHealth> {
return await invoke<DaemonHealth>("health");
}
export interface UpdateInfo { current: string; latest: string; has_update: boolean; url: string }
export async function checkUpdate(): Promise<UpdateInfo> {
return await invoke<UpdateInfo>("check_update");
}
export async function openExternal(url: string): Promise<void> {
await invoke("open_external", { url });
}
export async function listFonts(): Promise<string[]> {
return await invoke<string[]>("list_fonts");
}
/** Which of the given CLI candidates are actually installed on the daemon's spawn PATH. */
export async function whichAgents(candidates: string[]): Promise<string[]> {
const data = await invoke<{ available: string[] }>("which_agents", { candidates });
return data.available;
}
export async function setZoom(workspaceId: string, surfaceId: string | null): Promise<void> {
await invoke("set_zoom", { workspaceId, surfaceId });
}
// ---- Settings ----
export interface ConfigView {
default_shell: string;
font_family: string;
font_size: number;
theme: "dark" | "light";
accent: string;
}
export async function getConfig(): Promise<ConfigView> {
return await invoke<ConfigView>("get_config");
}
export async function setConfig(patch: Partial<Pick<ConfigView, "default_shell" | "font_family" | "font_size" | "theme" | "accent">>): Promise<void> {
await invoke("set_config", {
defaultShell: patch.default_shell ?? null,
fontFamily: patch.font_family ?? null,
fontSize: patch.font_size ?? null,
theme: patch.theme ?? null,
accent: patch.accent ?? null,
});
}
export async function shutdownDaemon(): Promise<void> {
try { await invoke("shutdown_daemon"); } catch { /* connection drops as the daemon exits — expected */ }
}
export async function restartDaemon(): Promise<void> {
await shutdownDaemon();
// Let the old process exit; the next request triggers the bridge's
// ensure_daemon respawn (or launchd KeepAlive) and reconnects.
await new Promise((r) => setTimeout(r, 600));
try { await getHealth(); } catch { /* reconnect loop will retry */ }
}