You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
269 lines
11 KiB
JavaScript
269 lines
11 KiB
JavaScript
import { useSignal, signal, useComputed, batch, effect } from '@preact/signals'
|
|
|
|
import { TreeView, EXAMPLE_TREE } from '@/client/components/TreeView.jsx'
|
|
import { Editor } from '@/client/components/Editor.jsx'
|
|
import clsx from 'clsx'
|
|
import { useEffect, useState } from 'preact/hooks'
|
|
import { fetchJson } from '../utils.js'
|
|
import { Terminal } from './Terminal.jsx'
|
|
|
|
function ensurePrefix(s, prefix) {
|
|
return s.slice(0, prefix.length) === prefix ? s : prefix + s
|
|
}
|
|
|
|
function stripPrefix(s, prefix) {
|
|
return s.slice(0, prefix.length) === prefix ? s.slice(prefix.length) : s
|
|
}
|
|
|
|
function clamp(min, value, max) {
|
|
return Math.max(min, Math.min(value, max))
|
|
}
|
|
|
|
export const Ide = ({}) => {
|
|
const ws = useSignal(null)
|
|
const uuid = useSignal(null)
|
|
|
|
useEffect(() => {
|
|
ws.value = new WebSocket(`ws://${location.host}/api/docker/node:current-alpine/pty`)
|
|
ws.value.addEventListener('message', e => {
|
|
const event = JSON.parse(e.data)
|
|
if (event.type === 'created') {
|
|
uuid.value = event.id
|
|
history.pushState(event.id, '', '/@container/' + event.id)
|
|
}
|
|
})
|
|
}, [])
|
|
|
|
if (!uuid.value) return <>Creating container...</>
|
|
|
|
const activeTab = useSignal(null)
|
|
|
|
const tabs = useSignal([
|
|
// {
|
|
// id: '/project/main.c',
|
|
// content: signal('...main.c...'),
|
|
// },
|
|
// {
|
|
// id: '/project/data.csv',
|
|
// content: signal('...data.csv...'),
|
|
// },
|
|
])
|
|
|
|
const activePath = useComputed(() => {
|
|
if (activeTab.value === null) return null
|
|
|
|
return tabs.value[activeTab.value].id
|
|
})
|
|
|
|
const activeContent = useComputed(() => {
|
|
if (activeTab.value === null) return null
|
|
|
|
return tabs.value[activeTab.value].content
|
|
})
|
|
|
|
effect(() => {
|
|
const path = activePath.value
|
|
const contentSig = activeContent.value
|
|
|
|
if (contentSig === null || path === null) return
|
|
|
|
console.log(path, contentSig.value)
|
|
})
|
|
|
|
return (
|
|
<div class="ide">
|
|
<div class="sidebar">
|
|
<div class="logo">PHC / Run</div>
|
|
|
|
<div class="flex-row">
|
|
<button class="icon">
|
|
<div class="material-symbols-outlined">home</div>
|
|
</button>
|
|
<div class="project-details flex-grow">
|
|
<div class="title">{uuid.value}</div>
|
|
<div class="open">
|
|
<div class="material-symbols-outlined">unfold_more</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="spoiler">
|
|
<div class="material-symbols-outlined">account_tree</div>
|
|
<div class="title">Files</div>
|
|
<div class="material-symbols-outlined">keyboard_arrow_down</div>
|
|
</div>
|
|
<div class="content">
|
|
<TreeView
|
|
listDir={async path => {
|
|
const entries = await fetchJson(
|
|
`/api/container/${uuid.value}/exec/ls/${stripPrefix(path, '/')}`
|
|
)
|
|
|
|
return entries
|
|
}}
|
|
actionOpenFile={async path => {
|
|
const content = await fetchJson(
|
|
`/api/container/${uuid.value}/fs/${stripPrefix(path, '/')}`
|
|
)
|
|
|
|
console.log('content', content)
|
|
|
|
batch(() => {
|
|
tabs.value = [
|
|
...tabs.value,
|
|
{
|
|
id: path,
|
|
content: signal(content),
|
|
},
|
|
]
|
|
|
|
activeTab.value = tabs.value.length - 1
|
|
})
|
|
}}
|
|
rootPath={'/'}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="section">
|
|
<div class="spoiler">
|
|
<div class="material-symbols-outlined">chat</div>
|
|
<div class="title">Conversation</div>
|
|
<div class="material-symbols-outlined">keyboard_arrow_down</div>
|
|
</div>
|
|
<div class="content">...</div>
|
|
</div>
|
|
<div class="section">
|
|
<div class="spoiler">
|
|
<div class="material-symbols-outlined">settings</div>
|
|
<div class="title">Settings</div>
|
|
<div class="material-symbols-outlined">keyboard_arrow_down</div>
|
|
</div>
|
|
</div>
|
|
<div class="spacer"></div>
|
|
<div class="user-details">
|
|
<div class="profile">
|
|
<img
|
|
src={`https://api.dicebear.com/9.x/identicon/svg?seed=${uuid.value}`}
|
|
alt="profile picture"
|
|
/>
|
|
</div>
|
|
<div class="username">{'aziis98'}</div>
|
|
<div class="email">{'aziis98'}@exmaple.com</div>
|
|
</div>
|
|
</div>
|
|
<div class="header">
|
|
<div class="search">
|
|
<div class="material-symbols-outlined">search</div>
|
|
<input type="text" placeholder="Search a snippet..." />
|
|
</div>
|
|
<div class="actions">
|
|
<button class="icon">
|
|
<div class="material-symbols-outlined">terminal</div>
|
|
</button>
|
|
<button
|
|
class="run"
|
|
onClick={() => {
|
|
console.log('Running file:', activePath.value)
|
|
if (!activePath.value) return
|
|
|
|
const fullPath = '/project' + activePath.value
|
|
|
|
if (fullPath.endsWith('.sh')) {
|
|
ws.value.send(`sh ${fullPath}\n`)
|
|
return
|
|
}
|
|
if (fullPath.endsWith('.js')) {
|
|
ws.value.send(`node ${fullPath}\n`)
|
|
return
|
|
}
|
|
}}
|
|
>
|
|
<div class="material-symbols-outlined">play_arrow</div>
|
|
Run File
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="tabbed-editor">
|
|
<div class="tabs">
|
|
{tabs.value.map((tab, index) => (
|
|
<div
|
|
class={clsx('tab', activeTab.value === index && 'active')}
|
|
onClick={() => (activeTab.value = index)}
|
|
>
|
|
<div class="title">{tab.id.split('/').at(-1)}</div>
|
|
<div class="buttons">
|
|
<button
|
|
class="flat icon"
|
|
onClick={e => {
|
|
e.stopPropagation()
|
|
|
|
batch(() => {
|
|
const newValues = [...tabs.value]
|
|
newValues.splice(index, 1)
|
|
|
|
console.log(
|
|
newValues.length,
|
|
activeTab.value,
|
|
clamp(0, activeTab.value, newValues.length - 1)
|
|
)
|
|
|
|
tabs.value = newValues
|
|
activeTab.value =
|
|
newValues.length === 0
|
|
? null
|
|
: clamp(
|
|
0,
|
|
activeTab.value > index
|
|
? activeTab.value - 1
|
|
: activeTab.value,
|
|
newValues.length - 1
|
|
)
|
|
})
|
|
}}
|
|
>
|
|
<div class="material-symbols-outlined">close</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div class="editor">
|
|
{activeContent.value === null ? (
|
|
<>No file opened</>
|
|
) : (
|
|
<Editor
|
|
code={activeContent.value}
|
|
setCode={async source => {
|
|
activeContent.value.value = source
|
|
|
|
const path = activePath.value
|
|
|
|
await fetchJson(`/api/container/${uuid.value}/fs/${stripPrefix(path, '/')}`, {
|
|
method: 'PUT',
|
|
body: source,
|
|
})
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div class="terminal">
|
|
<div class="title">Terminal</div>
|
|
<div class="actions">
|
|
<button class="flat icon">
|
|
<div class="material-symbols-outlined">delete</div>
|
|
</button>
|
|
<button class="flat icon">
|
|
<div class="material-symbols-outlined">close</div>
|
|
</button>
|
|
</div>
|
|
<div class="content" xterm-dimensions>
|
|
<Terminal ws={ws.value} />
|
|
</div>
|
|
</div>
|
|
<div class="status">Websocket connection active</div>
|
|
</div>
|
|
)
|
|
}
|