diff --git a/app/components/workbench/EditorPanel.tsx b/app/components/workbench/EditorPanel.tsx index 0a18658..f68e030 100644 --- a/app/components/workbench/EditorPanel.tsx +++ b/app/components/workbench/EditorPanel.tsx @@ -1,6 +1,6 @@ import { useStore } from '@nanostores/react'; -import { memo, useEffect, useMemo, useRef, useState } from 'react'; -import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels'; +import { memo, useMemo } from 'react'; +import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { CodeMirrorEditor, type EditorDocument, @@ -9,21 +9,17 @@ import { type OnSaveCallback as OnEditorSave, type OnScrollCallback as OnEditorScroll, } from '~/components/editor/codemirror/CodeMirrorEditor'; -import { IconButton } from '~/components/ui/IconButton'; import { PanelHeader } from '~/components/ui/PanelHeader'; import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton'; -import { shortcutEventEmitter } from '~/lib/hooks'; import type { FileMap } from '~/lib/stores/files'; import { themeStore } from '~/lib/stores/theme'; -import { workbenchStore } from '~/lib/stores/workbench'; -import { classNames } from '~/utils/classNames'; import { WORK_DIR } from '~/utils/constants'; -import { logger, renderLogger } from '~/utils/logger'; +import { renderLogger } from '~/utils/logger'; import { isMobile } from '~/utils/mobile'; import { FileBreadcrumb } from './FileBreadcrumb'; import { FileTree } from './FileTree'; -import { Terminal, type TerminalRef } from './terminal/Terminal'; -import React from 'react'; +import { DEFAULT_TERMINAL_SIZE, TerminalTabs } from './terminal/TerminalTabs'; +import { workbenchStore } from '~/lib/stores/workbench'; interface EditorPanelProps { files?: FileMap; @@ -38,8 +34,6 @@ interface EditorPanelProps { onFileReset?: () => void; } -const MAX_TERMINALS = 3; -const DEFAULT_TERMINAL_SIZE = 25; const DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE; const editorSettings: EditorSettings = { tabSize: 2 }; @@ -62,13 +56,6 @@ export const EditorPanel = memo( const theme = useStore(themeStore); const showTerminal = useStore(workbenchStore.showTerminal); - const terminalRefs = useRef>([]); - const terminalPanelRef = useRef(null); - const terminalToggledByShortcut = useRef(false); - - const [activeTerminal, setActiveTerminal] = useState(0); - const [terminalCount, setTerminalCount] = useState(1); - const activeFileSegments = useMemo(() => { if (!editorDocument) { return undefined; @@ -81,48 +68,6 @@ export const EditorPanel = memo( return editorDocument !== undefined && unsavedFiles?.has(editorDocument.filePath); }, [editorDocument, unsavedFiles]); - useEffect(() => { - const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => { - terminalToggledByShortcut.current = true; - }); - - const unsubscribeFromThemeStore = themeStore.subscribe(() => { - for (const ref of Object.values(terminalRefs.current)) { - ref?.reloadStyles(); - } - }); - - return () => { - unsubscribeFromEventEmitter(); - unsubscribeFromThemeStore(); - }; - }, []); - - useEffect(() => { - const { current: terminal } = terminalPanelRef; - - if (!terminal) { - return; - } - - const isCollapsed = terminal.isCollapsed(); - - if (!showTerminal && !isCollapsed) { - terminal.collapse(); - } else if (showTerminal && isCollapsed) { - terminal.resize(DEFAULT_TERMINAL_SIZE); - } - - terminalToggledByShortcut.current = false; - }, [showTerminal]); - - const addTerminal = () => { - if (terminalCount < MAX_TERMINALS) { - setTerminalCount(terminalCount + 1); - setActiveTerminal(terminalCount); - } - }; - return ( @@ -181,118 +126,7 @@ export const EditorPanel = memo( - { - if (!terminalToggledByShortcut.current) { - workbenchStore.toggleTerminal(true); - } - }} - onCollapse={() => { - if (!terminalToggledByShortcut.current) { - workbenchStore.toggleTerminal(false); - } - }} - > -
-
-
- {Array.from({ length: terminalCount + 1 }, (_, index) => { - const isActive = activeTerminal === index; - - return ( - - {index == 0 ? ( - - ) : ( - - - - )} - - ); - })} - {terminalCount < MAX_TERMINALS && } - workbenchStore.toggleTerminal(false)} - /> -
- {Array.from({ length: terminalCount + 1 }, (_, index) => { - const isActive = activeTerminal === index; - - if (index == 0) { - logger.info('Starting bolt terminal'); - - return ( - { - terminalRefs.current.push(ref); - }} - onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)} - onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)} - theme={theme} - /> - ); - } - - return ( - { - terminalRefs.current.push(ref); - }} - onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)} - onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)} - theme={theme} - /> - ); - })} -
-
-
+ ); }, diff --git a/app/components/workbench/terminal/Terminal.tsx b/app/components/workbench/terminal/Terminal.tsx index b9b84dc..337a72a 100644 --- a/app/components/workbench/terminal/Terminal.tsx +++ b/app/components/workbench/terminal/Terminal.tsx @@ -16,71 +16,74 @@ export interface TerminalProps { className?: string; theme: Theme; readonly?: boolean; + id: string; onTerminalReady?: (terminal: XTerm) => void; onTerminalResize?: (cols: number, rows: number) => void; } export const Terminal = memo( - forwardRef(({ className, theme, readonly, onTerminalReady, onTerminalResize }, ref) => { - const terminalElementRef = useRef(null); - const terminalRef = useRef(); + forwardRef( + ({ className, theme, readonly, id, onTerminalReady, onTerminalResize }, ref) => { + const terminalElementRef = useRef(null); + const terminalRef = useRef(); - useEffect(() => { - const element = terminalElementRef.current!; + useEffect(() => { + const element = terminalElementRef.current!; - const fitAddon = new FitAddon(); - const webLinksAddon = new WebLinksAddon(); + const fitAddon = new FitAddon(); + const webLinksAddon = new WebLinksAddon(); - const terminal = new XTerm({ - cursorBlink: true, - convertEol: true, - disableStdin: readonly, - theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}), - fontSize: 12, - fontFamily: 'Menlo, courier-new, courier, monospace', - }); + const terminal = new XTerm({ + cursorBlink: true, + convertEol: true, + disableStdin: readonly, + theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}), + fontSize: 12, + fontFamily: 'Menlo, courier-new, courier, monospace', + }); - terminalRef.current = terminal; + terminalRef.current = terminal; - terminal.loadAddon(fitAddon); - terminal.loadAddon(webLinksAddon); - terminal.open(element); + terminal.loadAddon(fitAddon); + terminal.loadAddon(webLinksAddon); + terminal.open(element); - const resizeObserver = new ResizeObserver(() => { - fitAddon.fit(); - onTerminalResize?.(terminal.cols, terminal.rows); - }); + const resizeObserver = new ResizeObserver(() => { + fitAddon.fit(); + onTerminalResize?.(terminal.cols, terminal.rows); + }); - resizeObserver.observe(element); + resizeObserver.observe(element); - logger.info('Attach terminal'); + logger.debug(`Attach [${id}]`); - onTerminalReady?.(terminal); + onTerminalReady?.(terminal); - return () => { - resizeObserver.disconnect(); - terminal.dispose(); - }; - }, []); + return () => { + resizeObserver.disconnect(); + terminal.dispose(); + }; + }, []); - useEffect(() => { - const terminal = terminalRef.current!; + useEffect(() => { + const terminal = terminalRef.current!; - // we render a transparent cursor in case the terminal is readonly - terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {}); + // we render a transparent cursor in case the terminal is readonly + terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {}); - terminal.options.disableStdin = readonly; - }, [theme, readonly]); + terminal.options.disableStdin = readonly; + }, [theme, readonly]); - useImperativeHandle(ref, () => { - return { - reloadStyles: () => { - const terminal = terminalRef.current!; - terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {}); - }, - }; - }, []); + useImperativeHandle(ref, () => { + return { + reloadStyles: () => { + const terminal = terminalRef.current!; + terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {}); + }, + }; + }, []); - return
; - }), + return
; + }, + ), ); diff --git a/app/components/workbench/terminal/TerminalTabs.tsx b/app/components/workbench/terminal/TerminalTabs.tsx new file mode 100644 index 0000000..cecc5dc --- /dev/null +++ b/app/components/workbench/terminal/TerminalTabs.tsx @@ -0,0 +1,186 @@ +import { useStore } from '@nanostores/react'; +import React, { memo, useEffect, useRef, useState } from 'react'; +import { Panel, type ImperativePanelHandle } from 'react-resizable-panels'; +import { IconButton } from '~/components/ui/IconButton'; +import { shortcutEventEmitter } from '~/lib/hooks'; +import { themeStore } from '~/lib/stores/theme'; +import { workbenchStore } from '~/lib/stores/workbench'; +import { classNames } from '~/utils/classNames'; +import { Terminal, type TerminalRef } from './Terminal'; +import { createScopedLogger } from '~/utils/logger'; + +const logger = createScopedLogger('Terminal'); + +const MAX_TERMINALS = 3; +export const DEFAULT_TERMINAL_SIZE = 25; + +export const TerminalTabs = memo(() => { + const showTerminal = useStore(workbenchStore.showTerminal); + const theme = useStore(themeStore); + + const terminalRefs = useRef>([]); + const terminalPanelRef = useRef(null); + const terminalToggledByShortcut = useRef(false); + + const [activeTerminal, setActiveTerminal] = useState(0); + const [terminalCount, setTerminalCount] = useState(1); + + const addTerminal = () => { + if (terminalCount < MAX_TERMINALS) { + setTerminalCount(terminalCount + 1); + setActiveTerminal(terminalCount); + } + }; + + useEffect(() => { + const { current: terminal } = terminalPanelRef; + + if (!terminal) { + return; + } + + const isCollapsed = terminal.isCollapsed(); + + if (!showTerminal && !isCollapsed) { + terminal.collapse(); + } else if (showTerminal && isCollapsed) { + terminal.resize(DEFAULT_TERMINAL_SIZE); + } + + terminalToggledByShortcut.current = false; + }, [showTerminal]); + + useEffect(() => { + const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => { + terminalToggledByShortcut.current = true; + }); + + const unsubscribeFromThemeStore = themeStore.subscribe(() => { + for (const ref of Object.values(terminalRefs.current)) { + ref?.reloadStyles(); + } + }); + + return () => { + unsubscribeFromEventEmitter(); + unsubscribeFromThemeStore(); + }; + }, []); + + return ( + { + if (!terminalToggledByShortcut.current) { + workbenchStore.toggleTerminal(true); + } + }} + onCollapse={() => { + if (!terminalToggledByShortcut.current) { + workbenchStore.toggleTerminal(false); + } + }} + > +
+
+
+ {Array.from({ length: terminalCount + 1 }, (_, index) => { + const isActive = activeTerminal === index; + + return ( + + {index == 0 ? ( + + ) : ( + + + + )} + + ); + })} + {terminalCount < MAX_TERMINALS && } + workbenchStore.toggleTerminal(false)} + /> +
+ {Array.from({ length: terminalCount + 1 }, (_, index) => { + const isActive = activeTerminal === index; + + logger.debug(`Starting bolt terminal [${index}]`); + + if (index == 0) { + return ( + { + terminalRefs.current.push(ref); + }} + onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)} + onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)} + theme={theme} + /> + ); + } else { + return ( + { + terminalRefs.current.push(ref); + }} + onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)} + onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)} + theme={theme} + /> + ); + } + })} +
+
+
+ ); +});