mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 06:44:32 +00:00
[Electron] Terminal commands (#1531)
* Add live terminal output * Fix scrolling * Refactor loading * Fallback to polling if endpoint fails * Comment * Move clientId to executionStore Refactor types * Remove polling * wip terminal command input * Refactor to use node-pty * Hide tabs if not electron * Lint fix * ts fix * Refactor tab components
This commit is contained in:
@@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full bg-black">
|
||||
<p v-if="errorMessage" class="p-4 text-center">{{ errorMessage }}</p>
|
||||
<ProgressSpinner
|
||||
v-else-if="loading"
|
||||
class="absolute inset-0 flex justify-center items-center h-full z-10"
|
||||
/>
|
||||
<div v-show="!loading" class="p-terminal rounded-none h-full w-full p-2">
|
||||
<div class="h-full" ref="terminalEl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { api } from '@/scripts/api'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { debounce } from 'lodash'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { until } from '@vueuse/core'
|
||||
import { LogEntry, LogsWsMessage, TerminalSize } from '@/types/apiTypes'
|
||||
|
||||
const errorMessage = ref('')
|
||||
const loading = ref(true)
|
||||
const terminalEl = ref<HTMLDivElement>()
|
||||
const fitAddon = new FitAddon()
|
||||
const terminal = new Terminal({
|
||||
convertEol: true
|
||||
})
|
||||
terminal.loadAddon(fitAddon)
|
||||
|
||||
const resizeTerminal = () =>
|
||||
terminal.resize(terminal.cols, fitAddon.proposeDimensions().rows)
|
||||
|
||||
const resizeObserver = new ResizeObserver(debounce(resizeTerminal, 50))
|
||||
|
||||
const update = (entries: Array<LogEntry>, size?: TerminalSize) => {
|
||||
if (size) {
|
||||
terminal.resize(size.cols, fitAddon.proposeDimensions().rows)
|
||||
}
|
||||
terminal.write(entries.map((e) => e.m).join(''))
|
||||
}
|
||||
|
||||
const logReceived = (e: CustomEvent<LogsWsMessage>) => {
|
||||
update(e.detail.entries, e.detail.size)
|
||||
}
|
||||
|
||||
const loadLogEntries = async () => {
|
||||
const logs = await api.getRawLogs()
|
||||
update(logs.entries, logs.size)
|
||||
}
|
||||
|
||||
const watchLogs = async () => {
|
||||
const { clientId } = storeToRefs(useExecutionStore())
|
||||
if (!clientId.value) {
|
||||
await until(clientId).not.toBeNull()
|
||||
}
|
||||
api.subscribeLogs(true)
|
||||
api.addEventListener('logs', logReceived)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
terminal.open(terminalEl.value)
|
||||
|
||||
try {
|
||||
await loadLogEntries()
|
||||
} catch (err) {
|
||||
console.error('Error loading logs', err)
|
||||
// On older backends the endpoints wont exist
|
||||
errorMessage.value =
|
||||
'Unable to load logs, please ensure you have updated your ComfyUI backend.'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
resizeObserver.observe(terminalEl.value)
|
||||
|
||||
await watchLogs()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (api.clientId) {
|
||||
api.subscribeLogs(false)
|
||||
}
|
||||
api.removeEventListener('logs', logReceived)
|
||||
|
||||
resizeObserver.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-terminal) .xterm {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
30
src/components/bottomPanel/tabs/terminal/BaseTerminal.vue
Normal file
30
src/components/bottomPanel/tabs/terminal/BaseTerminal.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full bg-black" ref="rootEl">
|
||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
||||
<div class="h-full terminal-host" ref="terminalEl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineEmits, Ref } from 'vue'
|
||||
import { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
|
||||
|
||||
const emit = defineEmits<{
|
||||
created: [ReturnType<typeof useTerminal>, Ref<HTMLElement>]
|
||||
}>()
|
||||
const terminalEl = ref<HTMLElement>()
|
||||
const rootEl = ref<HTMLElement>()
|
||||
emit('created', useTerminal(terminalEl), rootEl)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-terminal) .xterm {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
73
src/components/bottomPanel/tabs/terminal/CommandTerminal.vue
Normal file
73
src/components/bottomPanel/tabs/terminal/CommandTerminal.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, Ref } from 'vue'
|
||||
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { IDisposable } from '@xterm/xterm'
|
||||
import BaseTerminal from './BaseTerminal.vue'
|
||||
|
||||
const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement>
|
||||
) => {
|
||||
// TODO: use types from electron package
|
||||
const terminalApi = electronAPI()['Terminal'] as {
|
||||
onOutput(cb: (message: string) => void): () => void
|
||||
resize(cols: number, rows: number): void
|
||||
restore(): Promise<{
|
||||
buffer: string[]
|
||||
pos: { x: number; y: number }
|
||||
size: { cols: number; rows: number }
|
||||
}>
|
||||
storePos(x: number, y: number): void
|
||||
write(data: string): void
|
||||
}
|
||||
|
||||
let offData: IDisposable
|
||||
let offOutput: () => void
|
||||
|
||||
useAutoSize(root, true, true, () => {
|
||||
// If we aren't visible, don't resize
|
||||
if (!terminal.element?.offsetParent) return
|
||||
|
||||
terminalApi.resize(terminal.cols, terminal.rows)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
offData = terminal.onData(async (message: string) => {
|
||||
terminalApi.write(message)
|
||||
})
|
||||
|
||||
offOutput = terminalApi.onOutput((message) => {
|
||||
terminal.write(message)
|
||||
})
|
||||
|
||||
const restore = await terminalApi.restore()
|
||||
setTimeout(() => {
|
||||
if (restore.buffer.length) {
|
||||
terminal.resize(restore.size.cols, restore.size.rows)
|
||||
terminal.write(restore.buffer.join(''))
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
offData?.dispose()
|
||||
offOutput?.()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-terminal) .xterm {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
90
src/components/bottomPanel/tabs/terminal/LogsTerminal.vue
Normal file
90
src/components/bottomPanel/tabs/terminal/LogsTerminal.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="bg-black h-full w-full">
|
||||
<p v-if="errorMessage" class="p-4 text-center">{{ errorMessage }}</p>
|
||||
<ProgressSpinner
|
||||
v-else-if="loading"
|
||||
class="relative inset-0 flex justify-center items-center h-full z-10"
|
||||
/>
|
||||
<BaseTerminal v-show="!loading" @created="terminalCreated" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, Ref, ref } from 'vue'
|
||||
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
|
||||
import { LogEntry, LogsWsMessage, TerminalSize } from '@/types/apiTypes'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { until } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import BaseTerminal from './BaseTerminal.vue'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
|
||||
const errorMessage = ref('')
|
||||
const loading = ref(true)
|
||||
|
||||
const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement>
|
||||
) => {
|
||||
useAutoSize(root, true, false)
|
||||
|
||||
const update = (entries: Array<LogEntry>, size?: TerminalSize) => {
|
||||
if (size) {
|
||||
terminal.resize(size.cols, terminal.rows)
|
||||
}
|
||||
terminal.write(entries.map((e) => e.m).join(''))
|
||||
}
|
||||
|
||||
const logReceived = (e: CustomEvent<LogsWsMessage>) => {
|
||||
update(e.detail.entries, e.detail.size)
|
||||
}
|
||||
|
||||
const loadLogEntries = async () => {
|
||||
const logs = await api.getRawLogs()
|
||||
update(logs.entries, logs.size)
|
||||
}
|
||||
|
||||
const watchLogs = async () => {
|
||||
const { clientId } = storeToRefs(useExecutionStore())
|
||||
if (!clientId.value) {
|
||||
await until(clientId).not.toBeNull()
|
||||
}
|
||||
api.subscribeLogs(true)
|
||||
api.addEventListener('logs', logReceived)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await loadLogEntries()
|
||||
} catch (err) {
|
||||
console.error('Error loading logs', err)
|
||||
// On older backends the endpoints wont exist
|
||||
errorMessage.value =
|
||||
'Unable to load logs, please ensure you have updated your ComfyUI backend.'
|
||||
return
|
||||
}
|
||||
|
||||
await watchLogs()
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (api.clientId) {
|
||||
api.subscribeLogs(false)
|
||||
}
|
||||
api.removeEventListener('logs', logReceived)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-terminal) .xterm {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user