Eidos

Installation Notice

The installation may currently fail. We recommend copying the code below and creating the extension manually in Eidos.

Project File Tree

By: Mayne

Install Latest (v0.0.1)

No description provided.

"use sidebar"
import React, { useState, useEffect } from "react"
import { ChevronRight, ChevronDown, File } from "lucide-react"
import { ScrollArea } from "@/components/ui/scroll-area"

const ROOT_DIR = "~/"

const FileTree = () => {
  const [treeData, setTreeData] = useState([])
  const [expandedNodes, setExpandedNodes] = useState(new Set())
  const [loadingNodes, setLoadingNodes] = useState(new Set())

  const loadRootDirectory = async () => {
    try {
      const entries = await eidos.currentSpace.fs.readdir(ROOT_DIR, {
        withFileTypes: true
      })

      const sortedEntries = entries.sort((a, b) => {
        if (a.kind === 'directory' && b.kind !== 'directory') return -1
        if (a.kind !== 'directory' && b.kind === 'directory') return 1
        return a.name.localeCompare(b.name)
      })

      setTreeData(sortedEntries)
    } catch (error) {
      console.error("Failed to load root directory:", error)
    }
  }

  const loadSubDirectory = async (path) => {
    if (loadingNodes.has(path)) return

    setLoadingNodes(prev => new Set(prev).add(path))

    try {
      const entries = await eidos.currentSpace.fs.readdir(path, {
        withFileTypes: true
      })

      const sortedEntries = entries.sort((a, b) => {
        if (a.kind === 'directory' && b.kind !== 'directory') return -1
        if (a.kind !== 'directory' && b.kind === 'directory') return 1
        return a.name.localeCompare(b.name)
      })

      const updateTreeData = (nodes, targetPath, newChildren) => {
        return nodes.map(node => {
          if (node.path === targetPath) {
            return { ...node, children: newChildren }
          }
          if (node.children) {
            return { ...node, children: updateTreeData(node.children, targetPath, newChildren) }
          }
          return node
        })
      }

      setTreeData(prev => updateTreeData(prev, path, sortedEntries))
    } catch (error) {
      console.error(`Failed to load directory ${path}:`, error)
    } finally {
      setLoadingNodes(prev => {
        const newSet = new Set(prev)
        newSet.delete(path)
        return newSet
      })
    }
  }

  const toggleNode = async (node) => {
    const newExpanded = new Set(expandedNodes)

    if (expandedNodes.has(node.path)) {
      newExpanded.delete(node.path)
    } else {
      newExpanded.add(node.path)
      if (node.kind === 'directory' && !node.children) {
        await loadSubDirectory(node.path)
      }
    }

    setExpandedNodes(newExpanded)
  }

  const handleFileLick = (path: string) => {
    eidos.currentSpace.navigate(`/file-handler/#${path}`)
  }

  const renderTreeNode = (node, level = 0) => {
    const isExpanded = expandedNodes.has(node.path);
    const isLoading = loadingNodes.has(node.path);
    const hasChildren = node.kind === "directory";
    return (
      <div key={node.path} className="min-w-0">
        <div
          className="flex items-center hover:bg-accent rounded transition-colors cursor-pointer select-none"
          onClick={() => hasChildren && toggleNode(node)}
        >
          <div style={{ width: level * 18 }} className="flex-shrink-0" />
          <div className="w-4 flex-shrink-0 flex items-center justify-center">
            {hasChildren ? (
              <button
                onClick={(e) => {
                  e.stopPropagation();
                  toggleNode(node);
                }}
                className="p-0 hover:bg-accent rounded transition-colors"
                disabled={isLoading}
              >
                {isLoading ? (
                  <div className="w-4 h-4 animate-spin rounded-full border-2 border-border border-t-primary" />
                ) : isExpanded ? (
                  <ChevronDown className="w-4 h-4 text-muted-foreground" />
                ) : (
                  <ChevronRight className="w-4 h-4 text-muted-foreground" />
                )}
              </button>
            ) : (
              <File className="w-4 h-4 text-muted-foreground" />
            )}
          </div>
          <div className="flex items-center gap-1 px-2 py-1 min-w-0" onClick={() => {
            !hasChildren && handleFileLick(node.path)
          }}>
            <span className="truncate text-foreground">{node.name}</span>
          </div>
        </div>
        {hasChildren && isExpanded && node.children && (
          <div className="ml-0">
            {node.children.map((child) => renderTreeNode(child, level + 1))}
          </div>
        )}
      </div>
    );
  }

  useEffect(() => {
    loadRootDirectory()
  }, [])

  return (
    <ScrollArea className="h-full">
      <div className="space-y-1 px-4 bg-sidebar">
        {treeData.map((node, index) =>
          renderTreeNode(
            node,
            0,
          )
        )}
      </div>
    </ScrollArea>
  )
}

export default function () {
  return <FileTree />
}

Information

Author
Mayne
Type
block
Latest Version
0.0.1
Last Updated
11/05/2025
Published
11/05/2025

Version History

  • v0.0.1 11/05/2025