Eidos

Installation Notice

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

Text Editor

By: Mayne

Install Latest (v0.0.1)

A Monaco-based text editor

import React, { useState, useEffect } from "react"
import Editor from "@monaco-editor/react"

export const meta = {
  type: "fileHandler",
  componentName: "FileHandler",
  fileHandler: {
    title: "Text Editor",
    description: "A Monaco-based text editor",
    extensions: [".md", ".txt", ".js", ".jsx", ".ts", ".tsx", ".json", ".html", ".css", ".scss", ".sass", ".less", ".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", ".sh", ".bash", ".zsh", ".fish", ".ps1", ".sql", ".tex", ".r", ".m", ".pl", ".lua", ".vim", ".dockerfile", ".makefile", ".cmake", ".py", ".java", ".cpp", ".c", ".cs", ".php", ".rb", ".go", ".rs", ".swift", ".kt", ".scala"],
    icon: "📄",
  },
}

export default function FileHandler() {
  const [filePath, setFilePath] = useState("")
  const [content, setContent] = useState("")
  const [originalContent, setOriginalContent] = useState("")
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState("")

  useEffect(() => {
    const updateFilePath = () => {
      const hash = window.location.hash
      const path = hash.startsWith("#") ? hash.substring(1) : hash
      setFilePath(path)
    }

    updateFilePath()
    window.addEventListener("hashchange", updateFilePath)
    return () => window.removeEventListener("hashchange", updateFilePath)
  }, [])

  useEffect(() => {
    if (!filePath) return

    const loadFile = async () => {
      try {
        setIsLoading(true)
        const text = await eidos.currentSpace.fs.readFile(filePath, "utf8")
        setContent(text)
        setOriginalContent(text)
        setError("")
      } catch (err) {
        setError(err.message || "Failed to read file")
      } finally {
        setIsLoading(false)
      }
    }

    loadFile()
  }, [filePath])

  const handleSave = async () => {
    if (!filePath) return

    try {
      await eidos.currentSpace.fs.writeFile(filePath, content, "utf8")
      setOriginalContent(content)
      eidos.currentSpace.notify({ title: "Success", description: "File saved" })
    } catch (err) {
      setError(err.message || "Failed to save file")
    }
  }

  const getEditorTheme = () => {
    const html = document.documentElement
    return html.classList.contains("dark") ? "vs-dark" : "light"
  }

  const [editorTheme, setEditorTheme] = useState(getEditorTheme())

  useEffect(() => {
    const observer = new MutationObserver(() => {
      setEditorTheme(getEditorTheme())
    })
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    })
    return () => observer.disconnect()
  }, [])

  const getLanguage = (filePath) => {
    if (!filePath) return "plaintext"
    const ext = filePath.split('.').pop()?.toLowerCase()
    const langMap = {
      'js': 'javascript',
      'jsx': 'javascript',
      'ts': 'typescript',
      'tsx': 'typescript',
      'py': 'python',
      'java': 'java',
      'cpp': 'cpp',
      'c': 'c',
      'cs': 'csharp',
      'php': 'php',
      'rb': 'ruby',
      'go': 'go',
      'rs': 'rust',
      'swift': 'swift',
      'kt': 'kotlin',
      'scala': 'scala',
      'html': 'html',
      'css': 'css',
      'scss': 'scss',
      'sass': 'sass',
      'less': 'less',
      'xml': 'xml',
      'json': 'json',
      'yaml': 'yaml',
      'yml': 'yaml',
      'toml': 'toml',
      'ini': 'ini',
      'cfg': 'ini',
      'conf': 'ini',
      'sh': 'shell',
      'bash': 'shell',
      'zsh': 'shell',
      'fish': 'shell',
      'ps1': 'powershell',
      'sql': 'sql',
      'md': 'markdown',
      'markdown': 'markdown',
      'tex': 'latex',
      'r': 'r',
      'm': 'matlab',
      'pl': 'perl',
      'lua': 'lua',
      'vim': 'vim',
      'dockerfile': 'dockerfile',
      'makefile': 'makefile',
      'cmake': 'cmake'
    }
    return langMap[ext] || 'plaintext'
  }

  useEffect(() => {
    const handleKeyDown = (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === "s") {
        e.preventDefault()
        if (content !== originalContent) {
          handleSave()
        }
      }
    }
    window.addEventListener("keydown", handleKeyDown)
    return () => window.removeEventListener("keydown", handleKeyDown)
  }, [content, originalContent])

  if (isLoading) {
    return (
      <div className="flex h-screen items-center justify-center">
        <div className="text-muted-foreground">Loading file...</div>
      </div>
    )
  }

  if (error) {
    return (
      <div className="flex h-screen items-center justify-center">
        <div className="text-center">
          <div className="text-destructive mb-2">Error</div>
          <div className="text-sm text-muted-foreground">{error}</div>
        </div>
      </div>
    )
  }

  return (
    <div className="flex h-screen flex-col">
      <div className="border-b bg-background px-4 py-2 flex items-center justify-between">
        <div className="text-sm font-medium truncate flex items-center gap-2">
          <span>{filePath || "Untitled"}</span>
          {content !== originalContent && (
            <span className="text-orange-500 text-xs font-normal">●</span>
          )}
        </div>
        <div className="flex items-center gap-2">
          {content !== originalContent && (
            <span className="text-xs text-orange-500 font-medium">Unsaved changes</span>
          )}
        </div>
      </div>

      <div className="flex-1 overflow-hidden">
        <style>
          {`.monaco-editor .monaco-editor-background,
            .monaco-editor .margin {
              background-color: var(--custom-editor-background)!important;
            }`}
        </style>
        <Editor
          height="100%"
          language={getLanguage(filePath)}
          value={content}
          onChange={(value) => setContent(value || "")}
          theme={editorTheme}
          options={{
            minimap: { enabled: false },
            scrollBeyondLastLine: false,
            fontSize: 14,
            lineNumbers: "on",
            wordWrap: "on",
            automaticLayout: true,
          }}
        />
      </div>
    </div>
  )
}

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