Download src/modules/explorer/lib/useFileTree.ts from lekmikdok/termix: direct link, hf CLI and curl.
- Browser
- Download file 6.67 kB
-
https://huggingface.co/spaces/lekmikdok/termix/resolve/main/src/modules/explorer/lib/useFileTree.ts
- Command line
-
hf download hf://spaces/lekmikdok/termix/src/modules/explorer/lib/useFileTree.ts
-
curl -L -o useFileTree.ts https://huggingface.co/spaces/lekmikdok/termix/resolve/main/src/modules/explorer/lib/useFileTree.ts
6.67 kB
| import { invoke } from "@tauri-apps/api/core"; | |
| import { useCallback, useEffect, useRef, useState } from "react"; | |
| import { usePreferencesStore } from "@/modules/settings/preferences"; | |
| export type DirEntry = { | |
| name: string; | |
| kind: "file" | "dir" | "symlink"; | |
| size: number; | |
| mtime: number; | |
| }; | |
| type ChildrenState = | |
| | { status: "idle" } | |
| | { status: "loading" } | |
| | { status: "loaded"; entries: DirEntry[] } | |
| | { status: "error"; message: string }; | |
| type TreeState = Record<string, ChildrenState>; | |
| export type PendingCreate = { | |
| parentPath: string; | |
| kind: "file" | "dir"; | |
| }; | |
| export function joinPath(parent: string, name: string): string { | |
| if (parent.endsWith("/")) return `${parent}${name}`; | |
| return `${parent}/${name}`; | |
| } | |
| export function dirname(path: string): string { | |
| const i = path.lastIndexOf("/"); | |
| if (i <= 0) return "/"; | |
| return path.slice(0, i); | |
| } | |
| type Options = { | |
| onPathRenamed?: (from: string, to: string) => void; | |
| onPathDeleted?: (path: string) => void; | |
| }; | |
| export function useFileTree(rootPath: string | null, options?: Options) { | |
| const showHidden = usePreferencesStore((s) => s.showHidden); | |
| const showHiddenRef = useRef(showHidden); | |
| const [nodes, setNodes] = useState<TreeState>({}); | |
| const [expanded, setExpanded] = useState<Set<string>>(new Set()); | |
| const [pendingCreate, setPendingCreate] = useState<PendingCreate | null>( | |
| null, | |
| ); | |
| const [renaming, setRenaming] = useState<string | null>(null); | |
| useEffect(() => { | |
| showHiddenRef.current = showHidden; | |
| }, [showHidden]); | |
| const fetchChildren = useCallback(async (path: string) => { | |
| setNodes((s) => ({ ...s, [path]: { status: "loading" } })); | |
| try { | |
| const entries = await invoke<DirEntry[]>("fs_read_dir", { | |
| path, | |
| showHidden: showHiddenRef.current, | |
| }); | |
| setNodes((s) => ({ ...s, [path]: { status: "loaded", entries } })); | |
| } catch (e) { | |
| setNodes((s) => ({ | |
| ...s, | |
| [path]: { status: "error", message: String(e) }, | |
| })); | |
| } | |
| }, []); | |
| // Root change → reset state. | |
| useEffect(() => { | |
| if (!rootPath) { | |
| setNodes({}); | |
| setExpanded(new Set()); | |
| setPendingCreate(null); | |
| setRenaming(null); | |
| return; | |
| } | |
| setPendingCreate(null); | |
| setRenaming(null); | |
| setExpanded(new Set()); | |
| setNodes({}); | |
| void fetchChildren(rootPath); | |
| }, [rootPath, fetchChildren]); | |
| useEffect(() => { | |
| if (!rootPath) return; | |
| const loadedPaths = Object.entries(nodes) | |
| .filter(([, state]) => state.status === "loaded") | |
| .map(([path]) => path); | |
| for (const path of loadedPaths) void fetchChildren(path); | |
| // Re-list loaded directories when the visibility preference changes. | |
| // `nodes` is intentionally omitted so ordinary tree edits don't refetch | |
| // every expanded directory. | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [showHidden, rootPath, fetchChildren]); | |
| const toggle = useCallback( | |
| (path: string) => { | |
| setExpanded((curr) => { | |
| const next = new Set(curr); | |
| if (next.has(path)) next.delete(path); | |
| else next.add(path); | |
| return next; | |
| }); | |
| setNodes((curr) => { | |
| if (!curr[path] || curr[path].status === "error") { | |
| void fetchChildren(path); | |
| } | |
| return curr; | |
| }); | |
| }, | |
| [fetchChildren], | |
| ); | |
| const expand = useCallback( | |
| (path: string) => { | |
| setExpanded((curr) => { | |
| if (curr.has(path)) return curr; | |
| const next = new Set(curr); | |
| next.add(path); | |
| return next; | |
| }); | |
| setNodes((curr) => { | |
| if (!curr[path]) void fetchChildren(path); | |
| return curr; | |
| }); | |
| }, | |
| [fetchChildren], | |
| ); | |
| const refresh = useCallback( | |
| (path: string) => { | |
| void fetchChildren(path); | |
| }, | |
| [fetchChildren], | |
| ); | |
| // --- mutations --- | |
| const beginCreate = useCallback( | |
| (parentPath: string, kind: "file" | "dir") => { | |
| setRenaming(null); | |
| setPendingCreate({ parentPath, kind }); | |
| // Ensure the parent is expanded so the input row is visible. | |
| if (rootPath && parentPath !== rootPath) { | |
| setExpanded((curr) => { | |
| if (curr.has(parentPath)) return curr; | |
| const next = new Set(curr); | |
| next.add(parentPath); | |
| return next; | |
| }); | |
| } | |
| setNodes((curr) => { | |
| if (!curr[parentPath]) void fetchChildren(parentPath); | |
| return curr; | |
| }); | |
| }, | |
| [rootPath, fetchChildren], | |
| ); | |
| const cancelCreate = useCallback(() => setPendingCreate(null), []); | |
| const commitCreate = useCallback( | |
| async (name: string) => { | |
| if (!pendingCreate) return; | |
| const trimmed = name.trim(); | |
| if (!trimmed) { | |
| setPendingCreate(null); | |
| return; | |
| } | |
| const path = joinPath(pendingCreate.parentPath, trimmed); | |
| const cmd = | |
| pendingCreate.kind === "dir" ? "fs_create_dir" : "fs_create_file"; | |
| try { | |
| await invoke(cmd, { path }); | |
| await fetchChildren(pendingCreate.parentPath); | |
| } catch (e) { | |
| console.error(`${cmd} failed:`, e); | |
| } finally { | |
| setPendingCreate(null); | |
| } | |
| }, | |
| [pendingCreate, fetchChildren], | |
| ); | |
| const beginRename = useCallback((path: string) => { | |
| setPendingCreate(null); | |
| setRenaming(path); | |
| }, []); | |
| const cancelRename = useCallback(() => setRenaming(null), []); | |
| const commitRename = useCallback( | |
| async (newName: string) => { | |
| if (!renaming) return; | |
| const trimmed = newName.trim(); | |
| const parent = dirname(renaming); | |
| const oldName = renaming.slice(parent === "/" ? 1 : parent.length + 1); | |
| if (!trimmed || trimmed === oldName) { | |
| setRenaming(null); | |
| return; | |
| } | |
| const to = joinPath(parent, trimmed); | |
| try { | |
| await invoke("fs_rename", { from: renaming, to }); | |
| options?.onPathRenamed?.(renaming, to); | |
| await fetchChildren(parent); | |
| } catch (e) { | |
| console.error("fs_rename failed:", e); | |
| } finally { | |
| setRenaming(null); | |
| } | |
| }, | |
| [renaming, fetchChildren, options], | |
| ); | |
| const deletePath = useCallback( | |
| async (path: string) => { | |
| try { | |
| await invoke("fs_delete", { path }); | |
| options?.onPathDeleted?.(path); | |
| await fetchChildren(dirname(path)); | |
| } catch (e) { | |
| console.error("fs_delete failed:", e); | |
| } | |
| }, | |
| [fetchChildren, options], | |
| ); | |
| return { | |
| nodes, | |
| expanded, | |
| pendingCreate, | |
| renaming, | |
| toggle, | |
| expand, | |
| refresh, | |
| beginCreate, | |
| cancelCreate, | |
| commitCreate, | |
| beginRename, | |
| cancelRename, | |
| commitRename, | |
| deletePath, | |
| joinPath, | |
| }; | |
| } | |