The installation may currently fail. We recommend copying the code below and creating the extension manually in Eidos.
By: Mayne
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>
)
}