Skip to content

Commit a6e45c7

Browse files
committed
wip: terminals
1 parent 7731a46 commit a6e45c7

File tree

12 files changed

+175
-22
lines changed

12 files changed

+175
-22
lines changed

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
"@vitejs/devtools": "workspace:*",
7474
"@vitejs/devtools-vite": "workspace:*",
7575
"@vitejs/plugin-vue": "catalog:build",
76+
"@xterm/addon-fit": "catalog:frontend",
77+
"@xterm/xterm": "catalog:frontend",
7678
"tsdown": "catalog:build",
7779
"typescript": "catalog:devtools",
7880
"unplugin-vue": "catalog:build",

packages/core/playground/vite.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ export default defineConfig({
102102
launcher: {
103103
title: 'Launcher My Cool App',
104104
onLaunch: async () => {
105+
await ctx.terminals.startChildProcess({
106+
command: 'vite',
107+
args: ['dev'],
108+
cwd: process.cwd(),
109+
}, {
110+
id: 'vite-run',
111+
title: 'Vite Run',
112+
})
105113
await new Promise(resolve => setTimeout(resolve, 1000))
106114

107115
ctx.docks.update({

packages/core/src/client/webcomponents/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<!-- eslint-disable vue/no-mutating-props -->
2+
<script setup lang="ts">
3+
import type { DocksContext } from '@vitejs/devtools-kit/client'
4+
import type { TerminalState } from '../state/terminals'
5+
import { useEventListener } from '@vueuse/core'
6+
import { FitAddon } from '@xterm/addon-fit'
7+
import { Terminal } from '@xterm/xterm'
8+
import { markRaw, onMounted, ref } from 'vue'
9+
10+
const props = defineProps<{
11+
context: DocksContext
12+
terminal: TerminalState
13+
}>()
14+
15+
const container = ref<HTMLElement>()
16+
let term: Terminal
17+
18+
onMounted(async () => {
19+
term = markRaw(new Terminal({
20+
convertEol: true,
21+
cols: 80,
22+
screenReaderMode: true,
23+
}))
24+
const fitAddon = new FitAddon()
25+
term.loadAddon(fitAddon)
26+
term.open(container.value!)
27+
fitAddon.fit()
28+
29+
useEventListener(window, 'resize', () => {
30+
fitAddon.fit()
31+
})
32+
33+
if (props.terminal.buffer == null) {
34+
const { buffer } = await props.context.rpc.$call('vite:internal:terminals:read', props.terminal.info.id)
35+
props.terminal.buffer = markRaw(buffer)
36+
for (const chunk of buffer)
37+
term.write(chunk)
38+
}
39+
40+
props.terminal.terminal = term
41+
})
42+
43+
// async function clear() {
44+
// rpc.runTerminalAction(await ensureDevAuthToken(), props.id, 'clear')
45+
// term?.clear()
46+
// }
47+
48+
// async function restart() {
49+
// rpc.runTerminalAction(await ensureDevAuthToken(), props.id, 'restart')
50+
// }
51+
52+
// async function terminate() {
53+
// rpc.runTerminalAction(await ensureDevAuthToken(), props.id, 'terminate')
54+
// }
55+
</script>
56+
57+
<template>
58+
<div ref="container" class="h-full w-full of-auto bg-black" />
59+
<!-- <div border="t base" flex="~ gap-2" items-center p2>
60+
<NButton title="Clear" icon="i-carbon-clean" :border="false" @click="clear()" />
61+
<NButton v-if="info?.restartable" title="Restart" icon="carbon-renew" :border="false" @click="restart()" />
62+
<NButton v-if="info?.terminatable" title="Terminate" icon="carbon-delete" :border="false" @click="terminate()" />
63+
<span text-sm op50>{{ info?.description }}</span>
64+
</div> -->
65+
</template>
Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
11
<script setup lang="ts">
22
import type { DocksContext } from '@vitejs/devtools-kit/client'
3+
import type { TerminalState } from '../state/terminals'
4+
import { shallowRef } from 'vue'
35
import { useTerminals } from '../state/terminals'
6+
import ViewBuiltinTerminalPanel from './ViewBuiltinTerminalPanel.vue'
47
58
const props = defineProps<{
69
context: DocksContext
710
}>()
811
912
const terminals = useTerminals(props.context)
13+
const selectedTerminal = shallowRef<TerminalState | null>(null)
1014
</script>
1115

1216
<template>
13-
<div>
14-
{{ terminals }}
17+
<div class="w-full h-full grid-cols-[max-content_1fr]">
18+
<div class="border-base border-b">
19+
<button
20+
v-for="terminal of terminals.values()"
21+
:key="terminal.info.id"
22+
class="px3 py2 border-r border-base hover:bg-active"
23+
@click="selectedTerminal = terminal"
24+
>
25+
{{ terminal.info.title }}
26+
</button>
27+
</div>
28+
<div class="h-full flex relative">
29+
<ViewBuiltinTerminalPanel
30+
v-if="selectedTerminal"
31+
:context
32+
:terminal="selectedTerminal"
33+
/>
34+
<div v-else class="flex items-center justify-center h-full text-center">
35+
Select a terminal tab to start
36+
</div>
37+
</div>
1538
</div>
1639
</template>

packages/core/src/client/webcomponents/scripts/build-css.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const MINIFY = true
1919

2020
export async function buildCSS() {
2121
const reset = await fs.readFile(resolveModulePath('@unocss/reset/tailwind.css'), 'utf-8')
22+
const xtermCss = await fs.readFile(resolveModulePath('@xterm/xterm/css/xterm.css'), 'utf-8')
2223
const files = await glob(GLOBS, {
2324
cwd: SRC_DIR,
2425
absolute: true,
@@ -45,6 +46,7 @@ export async function buildCSS() {
4546
const unoResult = await generater.generate(tokens)
4647
const input = [
4748
reset,
49+
xtermCss,
4850
userStyle.toString(),
4951
unoResult.css,
5052
].join('\n')
Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,60 @@
1-
import type { DevToolsRpcClientFunctions } from '@vitejs/devtools-kit'
1+
import type { DevToolsRpcClientFunctions, DevToolsTerminalSessionStreamChunkEvent } from '@vitejs/devtools-kit'
22
import type { DocksContext } from '@vitejs/devtools-kit/client'
3-
import type { Ref, ShallowRef } from 'vue'
3+
import type { Terminal } from '@xterm/xterm'
4+
import type { Reactive } from 'vue'
45
import type { DevToolsTerminalSessionBase } from '../../../../../kit/src'
5-
import { shallowRef } from 'vue'
6+
import { reactive } from 'vue'
67

78
export interface TerminalState {
89
info: DevToolsTerminalSessionBase
9-
buffer?: string[] | null
10+
buffer: string[] | null
11+
terminal: Terminal | null
1012
}
1113

12-
let _terminalsRef: ShallowRef<TerminalState[]> | undefined
13-
export function useTerminals(context: DocksContext): Ref<TerminalState[]> {
14-
if (_terminalsRef) {
15-
return _terminalsRef
14+
let _terminalsMap: Reactive<Map<string, TerminalState>> | undefined
15+
export function useTerminals(context: DocksContext): Reactive<Map<string, TerminalState>> {
16+
if (_terminalsMap) {
17+
return _terminalsMap
1618
}
17-
const terminals = _terminalsRef = shallowRef<TerminalState[]>([])
19+
const map = _terminalsMap = reactive(new Map())
1820
async function udpateTerminals() {
19-
terminals.value = (await context.rpc.$call('vite:internal:terminals:list'))
20-
.map((info) => {
21-
return {
22-
info: Object.freeze(info),
23-
buffer: null,
24-
}
21+
const terminals = await context.rpc.$call('vite:internal:terminals:list')
22+
23+
for (const terminal of terminals) {
24+
if (map.has(terminal.id)) {
25+
map.get(terminal.id)!.info = Object.freeze(terminal)
26+
continue
27+
}
28+
map.set(terminal.id, {
29+
info: Object.freeze(terminal),
30+
buffer: null,
31+
terminal: null,
2532
})
33+
}
34+
2635
// eslint-disable-next-line no-console
27-
console.log('[VITE DEVTOOLS] Terminals Updated', [...terminals.value])
36+
console.log('[VITE DEVTOOLS] Terminals Updated', [...map.values()])
2837
}
2938
context.clientRpc.register({
3039
name: 'vite:internal:terminals:updated' satisfies keyof DevToolsRpcClientFunctions,
3140
type: 'action',
3241
handler: () => udpateTerminals(),
3342
})
43+
context.clientRpc.register({
44+
name: 'vite:internal:terminals:stream-chunk' satisfies keyof DevToolsRpcClientFunctions,
45+
type: 'action',
46+
handler: (data: DevToolsTerminalSessionStreamChunkEvent) => {
47+
const terminal = map.get(data.id)
48+
if (!terminal) {
49+
console.warn(`[VITE DEVTOOLS] Terminal with id "${data.id}" not found`)
50+
return
51+
}
52+
terminal.buffer?.push(...data.chunks)
53+
for (const chunk of data.chunks)
54+
terminal.terminal?.write(chunk)
55+
},
56+
})
3457
udpateTerminals()
35-
return terminals
58+
59+
return map
3660
}

packages/core/src/client/webcomponents/uno.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default defineConfig({
1010
{
1111
'color-base': 'color-neutral-800 dark:color-neutral-200',
1212
'bg-base': 'bg-white dark:bg-#111',
13+
'bg-active': 'bg-#8881',
1314
'bg-secondary': 'bg-#eee dark:bg-#222',
1415
'border-base': 'border-#8882',
1516
'ring-base': 'ring-#8882',

packages/core/src/node/host-terminals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType {
8080

8181
async startChildProcess(
8282
executeOptions: DevToolsChildProcessExecuteOptions,
83-
terminal: DevToolsTerminalSessionBase,
83+
terminal: Omit<DevToolsTerminalSessionBase, 'status'>,
8484
): Promise<DevToolsChildProcessTerminalSession> {
8585
if (this.sessions.has(terminal.id)) {
8686
throw new Error(`Terminal session with id "${terminal.id}" already registered`)

packages/kit/src/types/terminals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface DevToolsTerminalHost {
1919

2020
startChildProcess: (
2121
executeOptions: DevToolsChildProcessExecuteOptions,
22-
terminal: DevToolsTerminalSessionBase,
22+
terminal: Omit<DevToolsTerminalSessionBase, 'status'>,
2323
) => Promise<DevToolsChildProcessTerminalSession>
2424
}
2525

0 commit comments

Comments
 (0)