diff --git a/app/src-tauri/src/bridge.rs b/app/src-tauri/src/bridge.rs index 02d6de1..8fb9010 100644 --- a/app/src-tauri/src/bridge.rs +++ b/app/src-tauri/src/bridge.rs @@ -255,3 +255,8 @@ pub async fn set_group(state: BridgeState<'_>, group_id: String, name: Option, group_id: String) -> Result { data_of(state.request(Cmd::DeleteGroup { group_id: GroupId(group_id) }).await.map_err(|e| e.to_string())?) } + +#[tauri::command] +pub async fn focus(state: BridgeState<'_>, surface_id: String) -> Result { + data_of(state.request(Cmd::Focus { surface_id: SurfaceId(surface_id) }).await.map_err(|e| e.to_string())?) +} diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index d7ff402..606a8a2 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -35,6 +35,7 @@ pub fn run() { bridge::create_group, bridge::set_group, bridge::delete_group, + bridge::focus, ]) .run(tauri::generate_context!()) .expect("error while running spacesh"); diff --git a/app/src/LayoutEngine.tsx b/app/src/LayoutEngine.tsx index 2ca517e..af8c6b5 100644 --- a/app/src/LayoutEngine.tsx +++ b/app/src/LayoutEngine.tsx @@ -1,6 +1,7 @@ import { useRef } from "react"; import { TerminalView } from "./TerminalView"; -import type { LayoutNode } from "./layoutTypes"; +import { StatusRing } from "./StatusRing"; +import type { LayoutNode, SurfaceState } from "./layoutTypes"; import { setRatios, restartSurface } from "./socketBridge"; interface Props { @@ -8,16 +9,17 @@ interface Props { layout: LayoutNode | null; /** surface_id -> running flag, from the latest status/events. */ running: Record; + states: Record; } -export function LayoutEngine({ workspaceId, layout, running }: Props) { +export function LayoutEngine({ workspaceId, layout, running, states }: Props) { if (!layout) { return
Empty workspace — apply a preset to add panels.
; } - return ; + return ; } -function Node({ workspaceId, node, path, running }: { workspaceId: string; node: LayoutNode; path: number[]; running: Record }) { +function Node({ workspaceId, node, path, running, states }: { workspaceId: string; node: LayoutNode; path: number[]; running: Record; states: Record }) { if ("leaf" in node) { const id = node.leaf.surface_id; if (running[id] === false) { @@ -28,7 +30,17 @@ function Node({ workspaceId, node, path, running }: { workspaceId: string; node: ); } - return ; + return ( +
+
+ + {id} +
+
+ +
+
+ ); } const { orient, ratios, children } = node.split; @@ -43,7 +55,7 @@ function Node({ workspaceId, node, path, running }: { workspaceId: string; node: next[i + 1] = Math.max(0.05, (next[i + 1] ?? 1) - deltaFrac); void setRatios(workspaceId, path, next); }}> - + ))} diff --git a/app/src/Sidebar.tsx b/app/src/Sidebar.tsx index d85d8fa..6f8794a 100644 --- a/app/src/Sidebar.tsx +++ b/app/src/Sidebar.tsx @@ -1,4 +1,18 @@ -import type { Group, WorkspaceView } from "./layoutTypes"; +import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes"; + +const RING: Record = { + error: "#F4544E", wait: "#F2B84B", work: "#4C8DFF", done: "#3FB950", idle: "#5A6573", stopped: "#5A6573", +}; + +function aggregate(w: WorkspaceView): SurfaceState | "stopped" { + const order: SurfaceState[] = ["error", "wait", "work", "done", "idle"]; + const running = Object.values(w.surfaces).filter((s) => s.running); + if (running.length === 0) return "stopped"; + for (const st of order) { + if (running.some((s) => s.state === st)) return st; + } + return "idle"; +} export function Sidebar({ groups, workspaces, activeId, onSelect, onNew, @@ -19,7 +33,7 @@ export function Sidebar({ background: w.id === activeId ? "#1A2029" : "transparent", fontFamily: "Inter", fontSize: 13, color: w.id === activeId ? "#E6EDF3" : "#8B97A6", }}> - + {w.name} {w.unread && } {Object.keys(w.surfaces).length} diff --git a/app/src/StatusRing.tsx b/app/src/StatusRing.tsx new file mode 100644 index 0000000..2c9570c --- /dev/null +++ b/app/src/StatusRing.tsx @@ -0,0 +1,27 @@ +import type { SurfaceState } from "./layoutTypes"; + +const COLOR: Record = { + work: "#4C8DFF", + wait: "#F2B84B", + done: "#3FB950", + error: "#F4544E", + idle: "#5A6573", +}; + +export function StatusRing({ state, running }: { state: SurfaceState; running: boolean }) { + const color = running ? COLOR[state] : "#5A6573"; + return ( + + ); +} diff --git a/app/src/layoutTypes.ts b/app/src/layoutTypes.ts index e8ca7c0..f725993 100644 --- a/app/src/layoutTypes.ts +++ b/app/src/layoutTypes.ts @@ -1,5 +1,7 @@ export type Orient = "h" | "v"; +export type SurfaceState = "work" | "wait" | "done" | "error" | "idle"; + export type LayoutNode = | { leaf: { surface_id: string } } | { split: { orient: Orient; ratios: number[]; children: LayoutNode[] } }; @@ -15,6 +17,7 @@ export interface SurfaceView { autostart: boolean; }; running: boolean; + state: SurfaceState; } export interface Group { diff --git a/app/src/socketBridge.ts b/app/src/socketBridge.ts index 0abc342..8dd29bd 100644 --- a/app/src/socketBridge.ts +++ b/app/src/socketBridge.ts @@ -65,12 +65,20 @@ export async function getStatus(): Promise { 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: "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 }; export function onDaemonEvent(handler: (evt: DaemonEvt) => void): Promise<() => void> { return listen("spacesh:evt", (e) => handler(e.payload)); } +export async function focusSurface(surfaceId: string): Promise { + await invoke("focus", { surfaceId }); +} + export function onDaemonRawEvent(name: string, handler: () => void): Promise<() => void> { return listen(name, () => handler()); }