diff --git a/package-lock.json b/package-lock.json
index eafefad..f61a671 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,12 +13,15 @@
"@fontsource-variable/fira-code": "^5.1.0",
"@fontsource-variable/inter": "^5.1.0",
"@fontsource-variable/material-symbols-outlined": "^5.1.0",
+ "@fontsource/jetbrains-mono": "^5.1.0",
+ "@preact/signals": "^1.3.0",
"@uiw/codemirror-theme-github": "^4.23.2",
"@uiw/react-codemirror": "^4.23.2",
"@uiw/react-monacoeditor": "^3.6.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"astro": "^4.15.6",
+ "clsx": "^2.1.1",
"codemirror": "^5.65.17",
"express": "^4.21.0",
"node-pty": "^1.0.0",
@@ -603,6 +606,12 @@
"integrity": "sha512-t5lXmVtDZDha0iYp9//cwaftAjHrcLmWeWi6NILWsV/SvpQ8vnRQbVxMK7VIxHJy3czm4diHYA7I8w6MmqbLfQ==",
"license": "Apache-2.0"
},
+ "node_modules/@fontsource/jetbrains-mono": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.1.0.tgz",
+ "integrity": "sha512-YzWpGYY3PoqvmvwW2Hb4NwkN5C7LO0FmZugEGcFe6cNNE/cMgH9JhW5LI6hjG+1+nwgSHyJtKmblHNrUqlyhWg==",
+ "license": "OFL-1.1"
+ },
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"cpu": [
@@ -815,6 +824,8 @@
},
"node_modules/@preact/signals": {
"version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.3.0.tgz",
+ "integrity": "sha512-EOMeg42SlLS72dhoq6Vjq08havnLseWmPQ8A0YsgIAqMgWgx7V1a39+Pxo6i7SY5NwJtH4849JogFq3M67AzWg==",
"license": "MIT",
"dependencies": {
"@preact/signals-core": "^1.7.0"
@@ -1868,6 +1879,8 @@
},
"node_modules/clsx": {
"version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
diff --git a/package.json b/package.json
index addb029..15443e7 100644
--- a/package.json
+++ b/package.json
@@ -14,12 +14,15 @@
"@fontsource-variable/fira-code": "^5.1.0",
"@fontsource-variable/inter": "^5.1.0",
"@fontsource-variable/material-symbols-outlined": "^5.1.0",
+ "@fontsource/jetbrains-mono": "^5.1.0",
+ "@preact/signals": "^1.3.0",
"@uiw/codemirror-theme-github": "^4.23.2",
"@uiw/react-codemirror": "^4.23.2",
"@uiw/react-monacoeditor": "^3.6.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"astro": "^4.15.6",
+ "clsx": "^2.1.1",
"codemirror": "^5.65.17",
"express": "^4.21.0",
"node-pty": "^1.0.0",
diff --git a/server/docker.js b/server/docker.js
index c370ffe..28b7673 100644
--- a/server/docker.js
+++ b/server/docker.js
@@ -31,7 +31,7 @@ async function cleanContainers() {
}
async function createContainer(tag) {
- const id = randomUUID()
+ const id = randomUUID().split('-').at(-1)
await fs.mkdir(getContainerPath('phc', id), { recursive: true })
@@ -50,6 +50,21 @@ async function createContainer(tag) {
containerIds.add(id)
await storeContainers()
+ await runCommandContainer(id, `echo "echo hello world" > example.sh; chmod +x example.sh`)
+
+ // Example file tree
+
+ await runCommandContainer(id, `mkdir -p /project/src /project/bin`)
+
+ await runCommandContainer(id, `echo 'console.log("Hello from index.js")' > /project/src/index.js`)
+ await runCommandContainer(id, `echo 'console.log("Hello from app.js")' > /project/src/app.js`)
+
+ await runCommandContainer(id, `echo 'echo "Hello from script.sh"' > /project/bin/script.sh`)
+ await runCommandContainer(id, `chmod +x /project/bin/script.sh`)
+
+ await runCommandContainer(id, `echo 'This is the README file.' > /project/README.md`)
+ await runCommandContainer(id, `echo '{"name": "project", "version": "1.0.0"}' > /project/package.json`)
+
return id
}
@@ -87,3 +102,7 @@ async function destroyContainer(id) {
containerIds.delete(id)
await storeContainers()
}
+
+async function runCommandContainer(uuid, command) {
+ return await runCommand('docker', ['exec', '-w', '/project', uuid, '/bin/sh', '-c', command])
+}
diff --git a/server/server.js b/server/server.js
index fe69be6..8d8e290 100644
--- a/server/server.js
+++ b/server/server.js
@@ -8,18 +8,59 @@ import fs from 'fs/promises'
const app = express()
-app.use(express.json())
+app.use(express.json({ strict: false }))
-app.get('/api/status', c => c.json(42))
+// TODO: Convert to spawn + websocket
+app.get('/api/container/:uuid([a-zA-Z0-9-]+)/exec/shell', async (req, res) => {
+ const uuid = req.params.uuid
+
+ /** @type {string} */
+ const command = JSON.parse(req.query.command)
+
+ try {
+ const output = await runCommand('docker', ['exec', uuid, '/bin/sh', '-c', command])
+ res.json(output)
+ } catch (e) {
+ res.json({ error: e.toString() })
+ }
+})
+
+// app.get('/api/container/:uuid([a-zA-Z0-9-]+)/exec/ls/*', async (req, res) => {
+// const uuid = req.params.uuid
+// const p = path.normalize(req.params[0])
+
+// try {
+// const files = await runCommand('/bin/ls', [path.join(getContainerPath('phc', uuid), p)])
+
+// res.json(files.trim().split('\n'))
+// } catch (e) {
+// res.json({ error: e.toString() })
+// }
+// })
app.get('/api/container/:uuid([a-zA-Z0-9-]+)/exec/ls/*', async (req, res) => {
const uuid = req.params.uuid
const p = path.normalize(req.params[0])
try {
- const files = await runCommand('/bin/ls', [path.join(getContainerPath('phc', uuid), p)])
+ const dirPath = path.join(getContainerPath('phc', uuid), p)
+ const fileList = await fs.readdir(dirPath)
+
+ const result = await Promise.all(
+ fileList.map(async name => {
+ const fullPath = path.join(dirPath, name)
+ const stats = await fs.lstat(fullPath)
+
+ return {
+ type: stats.isDirectory() ? 'folder' : 'file',
+ name,
+ }
+ })
+ )
- res.json(files.trim().split('\n'))
+ result.sort((e1, e2) => e2.type.localeCompare(e1.type))
+
+ res.json(result)
} catch (e) {
res.json({ error: e.toString() })
}
@@ -40,13 +81,18 @@ app.get('/api/container/:uuid([a-zA-Z0-9-]+)/fs/*', async (req, res) => {
})
app.put('/api/container/:uuid([a-zA-Z0-9-]+)/fs/*', async (req, res) => {
+ const uuid = req.params.uuid
const p = path.normalize(req.params[0])
const content = req.body
console.log(content)
try {
- await fs.writeFile(path.join(getContainerPath('phc', uuid), p), content)
+ // await fs.writeFile(path.join(getContainerPath('phc', uuid), p), content)
+
+ await runCommand('docker', ['exec', '-i', uuid, '/usr/bin/tee', `/project/${p}`], {
+ stdin: content,
+ })
res.json('ok')
} catch (e) {
diff --git a/server/utils.js b/server/utils.js
index 5247e4d..0b435aa 100644
--- a/server/utils.js
+++ b/server/utils.js
@@ -4,7 +4,10 @@ export const CONTAINERS_ROOT = '/tmp/phc-run/containers'
export const getContainerPath = (ns, uuid) => `${CONTAINERS_ROOT}/${ns}/${uuid}`
-export function runCommand(command, args, options = { stdin: null, capture: ['stdout', 'stderr'] }) {
+export function runCommand(command, args, options = {}) {
+ options.stdin ??= null
+ options.capture ??= ['stdout', 'stderr']
+
console.log(`Running: ${command} ${JSON.stringify(args)}`)
return new Promise((resolve, reject) => {
@@ -14,18 +17,24 @@ export function runCommand(command, args, options = { stdin: null, capture: ['st
if (options.capture.includes('stdout')) {
child.stdout.on('data', data => {
+ console.log('[command]', '[stdout]', JSON.stringify(data.toString()))
output += data.toString()
})
}
if (options.capture.includes('stderr')) {
child.stderr.on('data', data => {
+ console.log('[command]', '[stderr]', JSON.stringify(data.toString()))
output += data.toString()
})
}
if (options.stdin) {
+ console.log('[command]', '[stdin]', options.stdin)
+
+ child.stdin.setDefaultEncoding('utf8')
child.stdin.write(options.stdin)
+ child.stdin.end()
}
child.on('close', code => {
diff --git a/src/client/components/Editor.jsx b/src/client/components/Editor.jsx
index 082a9a0..70c7632 100644
--- a/src/client/components/Editor.jsx
+++ b/src/client/components/Editor.jsx
@@ -1,3 +1,18 @@
-export const Editor = ({}) => {
- return
+import { useEffect } from 'preact/hooks'
+
+export const Editor = ({ code, setCode }) => {
+ return (
+
+ )
}
diff --git a/src/client/components/Ide.jsx b/src/client/components/Ide.jsx
new file mode 100644
index 0000000..8aa8b89
--- /dev/null
+++ b/src/client/components/Ide.jsx
@@ -0,0 +1,268 @@
+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 (
+
+
+
+
+
+ {tabs.value.map((tab, index) => (
+
(activeTab.value = index)}
+ >
+
{tab.id.split('/').at(-1)}
+
+
+ ))}
+
+
+ {activeContent.value === null ? (
+ <>No file opened>
+ ) : (
+ {
+ activeContent.value.value = source
+
+ const path = activePath.value
+
+ await fetchJson(`/api/container/${uuid.value}/fs/${stripPrefix(path, '/')}`, {
+ method: 'PUT',
+ body: source,
+ })
+ }}
+ />
+ )}
+
+
+
+
Terminal
+
+
+
+
+
+
+
+
+
Websocket connection active
+
+ )
+}
diff --git a/src/client/components/Terminal.jsx b/src/client/components/Terminal.jsx
new file mode 100644
index 0000000..daaea56
--- /dev/null
+++ b/src/client/components/Terminal.jsx
@@ -0,0 +1,53 @@
+import { useMemo } from 'preact/hooks'
+import { memo } from 'preact/compat'
+
+import { Terminal as XTerm } from '@xterm/xterm'
+
+import { Piatto_Light as Theme } from 'xterm-theme'
+import { useEventListener } from '../utils.js'
+import { FitAddon } from '../customFit'
+
+export const Terminal = memo(({ ws }) => {
+ const [fitAddon, term] = useMemo(() => {
+ const term = new XTerm({
+ theme: Theme,
+ })
+
+ const fitAddon = new FitAddon()
+ term.loadAddon(fitAddon)
+
+ term.onData(data => {
+ ws.send(data)
+ })
+
+ return [fitAddon, term]
+ }, [])
+
+ useEventListener(ws, 'message', e => {
+ const event = JSON.parse(e.data)
+
+ if (event.type === 'pty') {
+ term.write(event.data)
+ }
+ })
+
+ useEventListener(ws, 'close', () => {
+ console.log('Disconnected from backend container')
+ term.write('\r\nDisconnected from backend container')
+ })
+
+ return (
+ {
+ console.log('Starting terminal...')
+
+ term.open(el)
+
+ setTimeout(() => {
+ fitAddon.fit()
+ }, 500)
+ }}
+ >
+ )
+})
diff --git a/src/client/components/TreeView.jsx b/src/client/components/TreeView.jsx
index 43a86aa..a3062c7 100644
--- a/src/client/components/TreeView.jsx
+++ b/src/client/components/TreeView.jsx
@@ -1,3 +1,7 @@
+import { useSignal } from '@preact/signals'
+import clsx from 'clsx'
+import { useEffect, useState } from 'preact/hooks'
+
export const EXAMPLE_TREE = {
type: 'root',
children: [
@@ -22,48 +26,122 @@ export const EXAMPLE_TREE = {
],
}
-const flattenTree = (node, depth = 0) => {
- if (node.type === 'root') return node.children.flatMap(entry => flattenTree(entry, depth))
-
+const flattenTree = (node, depth = 0, path = []) => {
+ if (node.type === 'root') {
+ return node.children.flatMap(entry => flattenTree(entry, depth, []))
+ }
if (node.type === 'folder')
return [
{
depth,
type: 'folder',
name: node.name,
+ path: path.join('/') + '/',
},
- ...node.children.flatMap(entry => flattenTree(entry, depth + 1)),
+ ...node.children.flatMap(entry => flattenTree(entry, depth + 1, [...path, node.name])),
]
-
- if (node.type === 'file')
+ if (node.type === 'file') {
return [
{
depth,
type: 'file',
name: node.name,
+ path: path.join('/'),
},
]
+ }
throw new Error(`invalid node type "${node?.type ?? ''}"`)
}
+const TreeViewNode = ({ listDir, actionOpenFile, node, depth, path }) => {
+ if (node.type === 'root') {
+ const [children, setChildren] = useState([])
+
+ useEffect(async () => {
+ setChildren(await listDir(path))
+ }, [])
+
+ return children.flatMap(entry => (
+
+ ))
+ }
+
+ if (node.type === 'folder') {
+ const [collapsed, setCollapsed] = useState(true)
+
+ const [children, setChildren] = useState([])
+
+ useEffect(async () => {
+ setChildren(collapsed ? [] : await listDir(path))
+ }, [collapsed])
+
+ return (
+ <>
+ setCollapsed(c => !c)}
+ >
+
folder
+
{node.name}/
+
+
+ {!collapsed &&
+ children.flatMap(entry => (
+
+ ))}
+ >
+ )
+ }
+
+ if (node.type === 'file') {
+ return (
+ {
+ console.log('open', path)
+ actionOpenFile?.(path)
+ }}
+ >
+
description
+
{node.name}
+
+
+ )
+ }
+}
+
const ICONS = {
['folder']: 'folder',
['file']: 'description',
}
-export const TreeView = ({ value }) => {
- const flatNodes = flattenTree(value)
-
+export const TreeView = ({ listDir, actionOpenFile, rootPath }) => {
return (
- {flatNodes.map(node => (
-
-
{ICONS[node.type]}
-
{node.name}
-
-
- ))}
+
)
}
diff --git a/src/client/customFit.ts b/src/client/customFit.ts
new file mode 100644
index 0000000..a71e38a
--- /dev/null
+++ b/src/client/customFit.ts
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { Terminal, type ITerminalAddon } from '@xterm/xterm'
+
+interface ITerminalDimensions {
+ /**
+ * The number of rows in the terminal.
+ */
+ rows: number
+
+ /**
+ * The number of columns in the terminal.
+ */
+ cols: number
+}
+
+export class FitAddon implements ITerminalAddon {
+ private _terminal: Terminal | undefined
+
+ constructor() {}
+
+ public activate(terminal: Terminal): void {
+ this._terminal = terminal
+ try {
+ this.fit()
+ } catch (error) {
+ console.log(error)
+ }
+ }
+
+ public dispose(): void {}
+
+ public fit(): void {
+ const dims = this.proposeDimensions()
+ if (!dims || !this._terminal) {
+ return
+ }
+
+ // TODO: Remove reliance on private API
+ // const core: any = (this._terminal)._core
+
+ // Force a full render
+ if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {
+ // core._renderCoordinator.clear()
+ this._terminal.resize(dims.cols, dims.rows)
+ }
+ }
+
+ public proposeDimensions(): ITerminalDimensions | undefined {
+ if (!this._terminal) {
+ return undefined
+ }
+
+ if (!this._terminal.element.parentElement) {
+ return undefined
+ }
+
+ const dimEl = this._terminal.element.closest('[xterm-dimensions]') ?? this._terminal.element.parentElement
+
+ // TODO: Remove reliance on private API
+ const core = (this._terminal)._core
+
+ const parentElementStyle = window.getComputedStyle(dimEl)
+ const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'))
+ const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')))
+
+ console.log(parentElementWidth, parentElementHeight)
+
+ const elementStyle = window.getComputedStyle(this._terminal.element)
+ const elementPadding = {
+ top: parseInt(elementStyle.getPropertyValue('padding-top')),
+ bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
+ right: parseInt(elementStyle.getPropertyValue('padding-right')),
+ left: parseInt(elementStyle.getPropertyValue('padding-left')),
+ }
+ const elementPaddingVer = elementPadding.top + elementPadding.bottom
+ const elementPaddingHor = elementPadding.right + elementPadding.left
+ const availableHeight = parentElementHeight - elementPaddingVer
+ const availableWidth = parentElementWidth - elementPaddingHor
+ const geometry = {
+ cols: Math.floor(availableWidth / core._renderService.dimensions.device.cell.width),
+ rows: Math.floor(availableHeight / core._renderService.dimensions.device.cell.height),
+ }
+ return geometry
+ }
+}
diff --git a/src/client/style.css b/src/client/style.css
index 172ea23..8b9b9da 100644
--- a/src/client/style.css
+++ b/src/client/style.css
@@ -58,16 +58,22 @@ body {
}
.xterm {
- margin: 2rem;
+ /* margin: 2rem;
padding: 0.5rem;
border-radius: 0.5rem;
overflow: clip;
- box-shadow: 0 0 2rem 0 #0003;
+ box-shadow: 0 0 2rem 0 #0003; */
&.xterm-dom-renderer-owner-1 .xterm-rows {
+ font-size: 14px;
font-family: 'JetBrains Mono', monospace;
}
+
+ * {
+ font-variant-ligatures: none;
+ font-variant-ligatures: no-common-ligatures;
+ }
}
textarea.code-editor-prototype {
@@ -80,9 +86,10 @@ textarea.code-editor-prototype {
width: 100%;
height: 100%;
- font-family: 'Fira Code Variable', monospace;
- font-weight: 400;
+ font-family: 'JetBrains Mono', monospace;
font-size: 15px;
+ font-weight: 300;
+ line-height: 1.35;
}
input[type='text'] {
@@ -281,7 +288,7 @@ input[type='submit'],
height: 100%;
grid-template-columns: 20rem 1fr;
- grid-template-rows: 3rem 1fr 33vh auto;
+ grid-template-rows: 3.25rem 1fr 33vh auto;
grid-template-areas:
'sidebar header'
@@ -362,7 +369,7 @@ input[type='submit'],
gap: 1rem;
- padding: 0 0.5rem;
+ padding: 0 0.75rem;
border-bottom: 1px solid #d9d9d9;
@@ -433,7 +440,10 @@ input[type='submit'],
padding: 0.35rem;
- font-size: 15px;
+ min-width: 150px;
+ margin-left: -1px;
+
+ font-size: 13px;
font-weight: 450;
z-index: 1;
@@ -447,6 +457,8 @@ input[type='submit'],
padding: 0 0 0 0.15rem;
}
+ border-bottom: 1px solid #fff;
+
&:not(.active) {
border-bottom: 1px solid #d9d9d9;
@@ -454,10 +466,6 @@ input[type='submit'],
background: #f8f8f8;
}
}
-
- &:not(:first-child) {
- border-left: none;
- }
}
}
@@ -505,6 +513,20 @@ input[type='submit'],
> .content {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
+
+ display: grid;
+ overflow: hidden;
+
+ width: 100%;
+ height: 100%;
+
+ > .terminal {
+ display: grid;
+ overflow: hidden;
+
+ width: 100%;
+ height: 100%;
+ }
}
}
diff --git a/src/client/term.js b/src/client/term.js
index e444487..a4c47cf 100644
--- a/src/client/term.js
+++ b/src/client/term.js
@@ -43,9 +43,9 @@ ws.addEventListener('close', () => {
term.write('\r\nDisconnected from backend container')
})
-setInterval(async () => {
- const req = await fetch(`/api/container/${id}/exec/ls/`)
- const files = await req.json()
+// setInterval(async () => {
+// const req = await fetch(`/api/container/${id}/exec/ls/`)
+// const files = await req.json()
- console.log(files)
-}, 1000)
+// console.log(files)
+// }, 1000)
diff --git a/src/client/utils.js b/src/client/utils.js
new file mode 100644
index 0000000..6c1b955
--- /dev/null
+++ b/src/client/utils.js
@@ -0,0 +1,21 @@
+import { useEffect } from 'preact/hooks'
+
+export async function fetchJson(url, options = {}) {
+ if (options.hasOwnProperty('method')) {
+ options.headers = { 'Content-Type': 'application/json' }
+ options.body = JSON.stringify(options.body)
+ }
+
+ const res = await fetch(url, options)
+ return await res.json()
+}
+
+export function useEventListener(target, event, handler, deps = []) {
+ useEffect(() => {
+ target.addEventListener(event, handler)
+
+ return () => {
+ target.removeEventListener(event, handler)
+ }
+ }, deps)
+}
diff --git a/src/components/Ide.astro b/src/components/Ide.astro
index 377b776..3fbff45 100644
--- a/src/components/Ide.astro
+++ b/src/components/Ide.astro
@@ -1,6 +1,5 @@
---
-import { TreeView, EXAMPLE_TREE } from '@/client/components/TreeView.jsx'
-import { Editor } from '@/client/components/Editor.jsx'
+import { Ide as IdePreact } from '@/client/components/Ide.jsx'
const { project } = Astro.props
@@ -8,109 +7,4 @@ const userName = project.split('/').at(0).slice(1)
const projectName = project.split('/').at(-1)
---
-
-
-
-
-
-
Terminal
-
-
-
-
-
-
-
Websocket connection active
-
+
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
index 3d3e8e5..9da26ca 100644
--- a/src/layouts/Base.astro
+++ b/src/layouts/Base.astro
@@ -1,7 +1,9 @@
---
import '@fontsource-variable/material-symbols-outlined/full.css'
import '@fontsource-variable/inter/index.css'
-import '@fontsource-variable/fira-code/index.css'
+
+import '@fontsource/jetbrains-mono/latin.css'
+import '@fontsource/jetbrains-mono/latin-ext.css'
import '@/client/style.css'
diff --git a/src/pages/@container/new.astro b/src/pages/@container/new.astro
new file mode 100644
index 0000000..f52717f
--- /dev/null
+++ b/src/pages/@container/new.astro
@@ -0,0 +1,10 @@
+---
+import Base from '@/layouts/Base.astro'
+import { Ide } from '@/client/components/Ide.jsx'
+
+import '@xterm/xterm/css/xterm.css'
+---
+
+
+
+