Integrated terminal (#1295)

* Add terminal tab

* Add basic terminal

* Style terminal

* Add keybinding

* Auto scroll:

* Mock for jest test
This commit is contained in:
Chenlei Hu
2024-10-24 23:31:00 +02:00
committed by GitHub
parent d142893244
commit 14a6687cc9
8 changed files with 138 additions and 31 deletions

View File

@@ -1,34 +1,42 @@
<template> <template>
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId"> <div class="flex flex-col h-full">
<TabList pt:tabList="border-none"> <Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
<div class="w-full flex justify-between"> <TabList pt:tabList="border-none">
<div class="tabs-container"> <div class="w-full flex justify-between">
<Tab <div class="tabs-container">
v-for="tab in bottomPanelStore.bottomPanelTabs" <Tab
:key="tab.id" v-for="tab in bottomPanelStore.bottomPanelTabs"
:value="tab.id" :key="tab.id"
class="p-3 border-none" :value="tab.id"
> class="p-3 border-none"
<span class="font-bold"> >
{{ tab.title.toUpperCase() }} <span class="font-bold">
</span> {{ tab.title.toUpperCase() }}
</Tab> </span>
</Tab>
</div>
<Button
class="justify-self-end"
icon="pi pi-times"
severity="secondary"
size="small"
text
@click="bottomPanelStore.bottomPanelVisible = false"
/>
</div> </div>
<Button </TabList>
class="justify-self-end" </Tabs>
icon="pi pi-times" <!-- h-0 to force the div to flex-grow -->
severity="secondary" <div class="flex-grow h-0">
size="small" <ExtensionSlot
text v-if="
@click="bottomPanelStore.bottomPanelVisible = false" bottomPanelStore.bottomPanelVisible &&
/> bottomPanelStore.activeBottomPanelTab
</div> "
</TabList> :extension="bottomPanelStore.activeBottomPanelTab"
</Tabs> />
<ExtensionSlot </div>
v-if="bottomPanelStore.activeBottomPanelTab" </div>
:extension="bottomPanelStore.activeBottomPanelTab"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -0,0 +1,61 @@
<template>
<div class="p-terminal rounded-none h-full w-full">
<ScrollPanel class="h-full w-full" ref="scrollPanelRef">
<pre class="px-4 whitespace-pre-wrap">{{ log }}</pre>
</ScrollPanel>
</div>
</template>
<script setup lang="ts">
import ScrollPanel from 'primevue/scrollpanel'
import { api } from '@/scripts/api'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const log = ref<string>('')
const scrollPanelRef = ref<InstanceType<typeof ScrollPanel> | null>(null)
/**
* Whether the user has scrolled to the bottom of the terminal.
* This is used to prevent the terminal from scrolling to the bottom
* when new logs are fetched.
*/
const scrolledToBottom = ref(false)
let intervalId: number = 0
onMounted(async () => {
const element = scrollPanelRef.value?.$el
const scrollContainer = element?.querySelector('.p-scrollpanel-content')
if (scrollContainer) {
scrollContainer.addEventListener('scroll', () => {
scrolledToBottom.value =
scrollContainer.scrollTop + scrollContainer.clientHeight ===
scrollContainer.scrollHeight
})
}
const scrollToBottom = () => {
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight
}
}
watch(log, () => {
if (scrolledToBottom.value) {
scrollToBottom()
}
})
const fetchLogs = async () => {
log.value = await api.getLogs()
}
await fetchLogs()
scrollToBottom()
intervalId = window.setInterval(fetchLogs, 500)
})
onBeforeUnmount(() => {
window.clearInterval(intervalId)
})
</script>

View File

@@ -0,0 +1,14 @@
import { useI18n } from 'vue-i18n'
import { markRaw } from 'vue'
import IntegratedTerminal from '@/components/bottomPanel/tabs/IntegratedTerminal.vue'
import { BottomPanelExtension } from '@/types/extensionTypes'
export const useIntegratedTerminalTab = (): BottomPanelExtension => {
const { t } = useI18n()
return {
id: 'integrated-terminal',
title: t('terminal'),
component: markRaw(IntegratedTerminal),
type: 'vue'
}
}

View File

@@ -2,6 +2,7 @@ import { createI18n } from 'vue-i18n'
const messages = { const messages = {
en: { en: {
terminal: 'Terminal',
videoFailedToLoad: 'Video failed to load', videoFailedToLoad: 'Video failed to load',
extensionName: 'Extension Name', extensionName: 'Extension Name',
reloadToApplyChanges: 'Reload to apply changes', reloadToApplyChanges: 'Reload to apply changes',
@@ -113,6 +114,7 @@ const messages = {
} }
}, },
zh: { zh: {
terminal: '终端',
videoFailedToLoad: '视频加载失败', videoFailedToLoad: '视频加载失败',
extensionName: '扩展名称', extensionName: '扩展名称',
reloadToApplyChanges: '重新加载以应用更改', reloadToApplyChanges: '重新加载以应用更改',
@@ -223,6 +225,7 @@ const messages = {
} }
}, },
ru: { ru: {
terminal: 'Терминал',
videoFailedToLoad: 'Видео не удалось загрузить', videoFailedToLoad: 'Видео не удалось загрузить',
extensionName: 'Название расширения', extensionName: 'Название расширения',
reloadToApplyChanges: 'Перезагрузите, чтобы применить изменения', reloadToApplyChanges: 'Перезагрузите, чтобы применить изменения',

View File

@@ -160,5 +160,12 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
}, },
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute', commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
targetSelector: '#graph-canvas' targetSelector: '#graph-canvas'
},
{
combo: {
key: '`',
ctrl: true
},
commandId: 'Workspace.ToggleBottomPanelTab.integrated-terminal'
} }
] ]

View File

@@ -2,6 +2,7 @@ import type { BottomPanelExtension } from '@/types/extensionTypes'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { useIntegratedTerminalTab } from '@/hooks/bottomPanelTabs/integratedTerminalTab'
export const useBottomPanelStore = defineStore('bottomPanel', () => { export const useBottomPanelStore = defineStore('bottomPanel', () => {
const bottomPanelVisible = ref(false) const bottomPanelVisible = ref(false)
@@ -26,7 +27,7 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
activeBottomPanelTabId.value = tabId activeBottomPanelTabId.value = tabId
} }
const toggleBottomPanelTab = (tabId: string) => { const toggleBottomPanelTab = (tabId: string) => {
if (activeBottomPanelTabId.value === tabId) { if (activeBottomPanelTabId.value === tabId && bottomPanelVisible.value) {
bottomPanelVisible.value = false bottomPanelVisible.value = false
} else { } else {
activeBottomPanelTabId.value = tabId activeBottomPanelTabId.value = tabId
@@ -46,6 +47,10 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
}) })
} }
const registerCoreBottomPanelTabs = () => {
registerBottomPanelTab(useIntegratedTerminalTab())
}
return { return {
bottomPanelVisible, bottomPanelVisible,
toggleBottomPanel, toggleBottomPanel,
@@ -54,6 +59,7 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
activeBottomPanelTabId, activeBottomPanelTabId,
setActiveTab, setActiveTab,
toggleBottomPanelTab, toggleBottomPanelTab,
registerBottomPanelTab registerBottomPanelTab,
registerCoreBottomPanelTabs
} }
}) })

View File

@@ -36,6 +36,7 @@ import { useKeybindingStore } from '@/stores/keybindingStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore' import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore' import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
setupAutoQueueHandler() setupAutoQueueHandler()
@@ -97,6 +98,7 @@ const init = () => {
settingStore.addSettings(app.ui.settings) settingStore.addSettings(app.ui.settings)
useKeybindingStore().loadCoreKeybindings() useKeybindingStore().loadCoreKeybindings()
useSidebarTabStore().registerCoreSidebarTabs() useSidebarTabStore().registerCoreSidebarTabs()
useBottomPanelStore().registerCoreBottomPanelTabs()
app.extensionManager = useWorkspaceStore() app.extensionManager = useWorkspaceStore()
} }

View File

@@ -54,6 +54,12 @@ module.exports = async function () {
} }
}) })
jest.mock('@/stores/workspace/bottomPanelStore', () => {
return {
toggleBottomPanel: jest.fn()
}
})
jest.mock('vue-i18n', () => { jest.mock('vue-i18n', () => {
return { return {
useI18n: jest.fn() useI18n: jest.fn()