/** * useVfsStore — Virtual File System state for Raspberry Pi 3B boards. * * Each Pi board gets its own VFS tree rooted at "/". * Files can be created, renamed, deleted, and edited. * The VFS can be uploaded to the running Pi via RaspberryPi3Bridge. */ import { create } from 'zustand'; import { nanoid } from 'nanoid'; export interface VfsNode { id: string; name: string; type: 'file' | 'directory'; content?: string; // undefined for directories children?: string[]; // child node IDs, undefined for files parentId: string | null; } type VfsTree = Record; // nodeId → VfsNode const DEFAULT_PY_CONTENT = `#!/usr/bin/env python3 # Raspberry Pi script — runs on the emulated Pi import time import sys print("Hello from Raspberry Pi!", flush=True) while True: print("Running...", flush=True) time.sleep(1) `; const DEFAULT_SH_CONTENT = `#!/bin/bash echo "Hello from Pi!" `; function makeDefaultTree(): { tree: VfsTree; rootId: string } { const rootId = nanoid(8); const homeId = nanoid(8); const piId = nanoid(8); const scriptId = nanoid(8); const shellId = nanoid(8); const tree: VfsTree = { [rootId]: { id: rootId, name: '/', type: 'directory', children: [homeId], parentId: null }, [homeId]: { id: homeId, name: 'home', type: 'directory', children: [piId], parentId: rootId }, [piId]: { id: piId, name: 'pi', type: 'directory', children: [scriptId, shellId], parentId: homeId }, [scriptId]: { id: scriptId, name: 'script.py', type: 'file', content: DEFAULT_PY_CONTENT, parentId: piId }, [shellId]: { id: shellId, name: 'hello.sh', type: 'file', content: DEFAULT_SH_CONTENT, parentId: piId }, }; return { tree, rootId }; } interface VfsState { // Per-board: boardId → { tree, rootId } boards: Record; // Per-board: boardId → selected nodeId (for editor focus) selectedNodeId: Record; initBoardVfs: (boardId: string) => void; getTree: (boardId: string) => VfsTree; getRootId: (boardId: string) => string | null; getNode: (boardId: string, nodeId: string) => VfsNode | null; setSelectedNode: (boardId: string, nodeId: string | null) => void; getSelectedNode: (boardId: string) => VfsNode | null; createNode: (boardId: string, parentId: string, name: string, type: 'file' | 'directory') => string | null; deleteNode: (boardId: string, nodeId: string) => void; renameNode: (boardId: string, nodeId: string, newName: string) => void; setContent: (boardId: string, nodeId: string, content: string) => void; /** Serialize the VFS for a board as a flat list of { path, content } for upload */ serializeForUpload: (boardId: string) => Array<{ path: string; content: string }>; } function buildPath(tree: VfsTree, nodeId: string): string { const parts: string[] = []; let current: VfsNode | undefined = tree[nodeId]; while (current && current.parentId !== null) { parts.unshift(current.name); current = tree[current.parentId]; } return '/' + parts.join('/'); } function serializeTree(tree: VfsTree, rootId: string): Array<{ path: string; content: string }> { const result: Array<{ path: string; content: string }> = []; const visit = (nodeId: string) => { const node = tree[nodeId]; if (!node) return; if (node.type === 'file' && node.parentId !== null) { result.push({ path: buildPath(tree, nodeId), content: node.content ?? '' }); } if (node.type === 'directory') { for (const childId of node.children ?? []) { visit(childId); } } }; visit(rootId); return result; } export const useVfsStore = create((set, get) => ({ boards: {}, selectedNodeId: {}, initBoardVfs: (boardId) => { if (get().boards[boardId]) return; // already initialized const { tree, rootId } = makeDefaultTree(); set((s) => ({ boards: { ...s.boards, [boardId]: { tree, rootId } }, selectedNodeId: { ...s.selectedNodeId, [boardId]: null }, })); }, getTree: (boardId) => get().boards[boardId]?.tree ?? {}, getRootId: (boardId) => get().boards[boardId]?.rootId ?? null, getNode: (boardId, nodeId) => get().boards[boardId]?.tree[nodeId] ?? null, setSelectedNode: (boardId, nodeId) => set((s) => ({ selectedNodeId: { ...s.selectedNodeId, [boardId]: nodeId } })), getSelectedNode: (boardId) => { const nodeId = get().selectedNodeId[boardId]; if (!nodeId) return null; return get().boards[boardId]?.tree[nodeId] ?? null; }, createNode: (boardId, parentId, name, type) => { const board = get().boards[boardId]; if (!board) return null; const parent = board.tree[parentId]; if (!parent || parent.type !== 'directory') return null; const newId = nanoid(8); const newNode: VfsNode = { id: newId, name, type, parentId, ...(type === 'file' ? { content: '' } : { children: [] }), }; const updatedParent = { ...parent, children: [...(parent.children ?? []), newId] }; const newTree = { ...board.tree, [newId]: newNode, [parentId]: updatedParent }; set((s) => ({ boards: { ...s.boards, [boardId]: { ...board, tree: newTree } }, })); return newId; }, deleteNode: (boardId, nodeId) => { const board = get().boards[boardId]; if (!board) return; const node = board.tree[nodeId]; if (!node || node.parentId === null) return; // can't delete root // Collect all node IDs to remove (node + all descendants) const toRemove = new Set(); const collect = (id: string) => { toRemove.add(id); const n = board.tree[id]; if (n?.type === 'directory') { for (const childId of n.children ?? []) collect(childId); } }; collect(nodeId); const parent = board.tree[node.parentId]; const updatedParent = parent ? { ...parent, children: (parent.children ?? []).filter((c) => c !== nodeId) } : parent; const newTree = Object.fromEntries( Object.entries(board.tree) .filter(([id]) => !toRemove.has(id)) .map(([id, n]) => id === node.parentId ? [id, updatedParent] : [id, n]) ) as VfsTree; set((s) => ({ boards: { ...s.boards, [boardId]: { ...board, tree: newTree } }, selectedNodeId: toRemove.has(s.selectedNodeId[boardId] ?? '') ? { ...s.selectedNodeId, [boardId]: null } : s.selectedNodeId, })); }, renameNode: (boardId, nodeId, newName) => { const board = get().boards[boardId]; if (!board || !board.tree[nodeId]) return; const newTree = { ...board.tree, [nodeId]: { ...board.tree[nodeId], name: newName } }; set((s) => ({ boards: { ...s.boards, [boardId]: { ...board, tree: newTree } } })); }, setContent: (boardId, nodeId, content) => { const board = get().boards[boardId]; if (!board || board.tree[nodeId]?.type !== 'file') return; const newTree = { ...board.tree, [nodeId]: { ...board.tree[nodeId], content } }; set((s) => ({ boards: { ...s.boards, [boardId]: { ...board, tree: newTree } } })); }, serializeForUpload: (boardId) => { const board = get().boards[boardId]; if (!board) return []; return serializeTree(board.tree, board.rootId); }, }));