Dominic Elm commited on
Commit
e5ed23c
·
unverified ·
1 Parent(s): 4919627

feat: allow to open up to three terminals (#22)

Browse files
packages/bolt/app/components/ui/IconButton.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { memo } from 'react';
2
  import { classNames } from '~/utils/classNames';
3
 
4
- type IconSize = 'sm' | 'md' | 'xl' | 'xxl';
5
 
6
  interface BaseIconButtonProps {
7
  size?: IconSize;
@@ -64,6 +64,8 @@ function getIconSize(size: IconSize) {
64
  return 'text-sm';
65
  } else if (size === 'md') {
66
  return 'text-md';
 
 
67
  } else if (size === 'xl') {
68
  return 'text-xl';
69
  } else {
 
1
  import { memo } from 'react';
2
  import { classNames } from '~/utils/classNames';
3
 
4
+ type IconSize = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
5
 
6
  interface BaseIconButtonProps {
7
  size?: IconSize;
 
64
  return 'text-sm';
65
  } else if (size === 'md') {
66
  return 'text-md';
67
+ } else if (size === 'lg') {
68
+ return 'text-lg';
69
  } else if (size === 'xl') {
70
  return 'text-xl';
71
  } else {
packages/bolt/app/components/workbench/EditorPanel.tsx CHANGED
@@ -9,12 +9,14 @@ import {
9
  type OnSaveCallback as OnEditorSave,
10
  type OnScrollCallback as OnEditorScroll,
11
  } from '~/components/editor/codemirror/CodeMirrorEditor';
 
12
  import { PanelHeader } from '~/components/ui/PanelHeader';
13
  import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
14
  import { shortcutEventEmitter } from '~/lib/hooks';
15
  import type { FileMap } from '~/lib/stores/files';
16
  import { themeStore } from '~/lib/stores/theme';
17
  import { workbenchStore } from '~/lib/stores/workbench';
 
18
  import { renderLogger } from '~/utils/logger';
19
  import { isMobile } from '~/utils/mobile';
20
  import { FileTreePanel } from './FileTreePanel';
@@ -33,6 +35,7 @@ interface EditorPanelProps {
33
  onFileReset?: () => void;
34
  }
35
 
 
36
  const DEFAULT_TERMINAL_SIZE = 25;
37
  const DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE;
38
 
@@ -60,7 +63,8 @@ export const EditorPanel = memo(
60
  const terminalPanelRef = useRef<ImperativePanelHandle>(null);
61
  const terminalToggledByShortcut = useRef(false);
62
 
63
- const [terminalCount] = useState(1);
 
64
 
65
  const activeFile = useMemo(() => {
66
  if (!editorDocument) {
@@ -109,6 +113,13 @@ export const EditorPanel = memo(
109
  terminalToggledByShortcut.current = false;
110
  }, [showTerminal]);
111
 
 
 
 
 
 
 
 
112
  return (
113
  <PanelGroup direction="vertical">
114
  <Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
@@ -180,27 +191,50 @@ export const EditorPanel = memo(
180
  }
181
  }}
182
  >
183
- <div className="border-t h-full">
184
- <PanelHeader>
185
- <span className="i-ph:terminal-window-duotone shrink-0" /> Terminal
186
- </PanelHeader>
187
- <div className="p-3.5">
188
  {Array.from({ length: terminalCount }, (_, index) => {
 
 
189
  return (
190
- <div key={index} className="h-full">
191
- <Terminal
192
- ref={(ref) => {
193
- terminalRefs.current.push(ref);
194
- }}
195
- onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}
196
- onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
197
- className="h-full"
198
- theme={theme}
199
- />
200
- </div>
 
 
 
201
  );
202
  })}
 
 
 
203
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  </div>
205
  </Panel>
206
  </PanelGroup>
 
9
  type OnSaveCallback as OnEditorSave,
10
  type OnScrollCallback as OnEditorScroll,
11
  } from '~/components/editor/codemirror/CodeMirrorEditor';
12
+ import { IconButton } from '~/components/ui/IconButton';
13
  import { PanelHeader } from '~/components/ui/PanelHeader';
14
  import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
15
  import { shortcutEventEmitter } from '~/lib/hooks';
16
  import type { FileMap } from '~/lib/stores/files';
17
  import { themeStore } from '~/lib/stores/theme';
18
  import { workbenchStore } from '~/lib/stores/workbench';
19
+ import { classNames } from '~/utils/classNames';
20
  import { renderLogger } from '~/utils/logger';
21
  import { isMobile } from '~/utils/mobile';
22
  import { FileTreePanel } from './FileTreePanel';
 
35
  onFileReset?: () => void;
36
  }
37
 
38
+ const MAX_TERMINALS = 3;
39
  const DEFAULT_TERMINAL_SIZE = 25;
40
  const DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE;
41
 
 
63
  const terminalPanelRef = useRef<ImperativePanelHandle>(null);
64
  const terminalToggledByShortcut = useRef(false);
65
 
66
+ const [activeTerminal, setActiveTerminal] = useState(0);
67
+ const [terminalCount, setTerminalCount] = useState(1);
68
 
69
  const activeFile = useMemo(() => {
70
  if (!editorDocument) {
 
113
  terminalToggledByShortcut.current = false;
114
  }, [showTerminal]);
115
 
116
+ const addTerminal = () => {
117
+ if (terminalCount < MAX_TERMINALS) {
118
+ setTerminalCount(terminalCount + 1);
119
+ setActiveTerminal(terminalCount);
120
+ }
121
+ };
122
+
123
  return (
124
  <PanelGroup direction="vertical">
125
  <Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
 
191
  }
192
  }}
193
  >
194
+ <div className="border-t h-full flex flex-col">
195
+ <div className="flex items-center bg-gray-50 min-h-[34px]">
 
 
 
196
  {Array.from({ length: terminalCount }, (_, index) => {
197
+ const isActive = activeTerminal === index;
198
+
199
  return (
200
+ <button
201
+ key={index}
202
+ className={classNames(
203
+ 'flex items-center text-sm bg-transparent cursor-pointer gap-1.5 px-3.5 h-full whitespace-nowrap',
204
+ {
205
+ 'bg-white': isActive,
206
+ 'hover:bg-gray-100': !isActive,
207
+ },
208
+ )}
209
+ onClick={() => setActiveTerminal(index)}
210
+ >
211
+ <div className="i-ph:terminal-window-duotone text-md" />
212
+ Terminal {terminalCount > 1 && index + 1}
213
+ </button>
214
  );
215
  })}
216
+ {terminalCount < MAX_TERMINALS && (
217
+ <IconButton className="ml-2" icon="i-ph:plus" size="md" onClick={addTerminal} />
218
+ )}
219
  </div>
220
+ {Array.from({ length: terminalCount }, (_, index) => {
221
+ const isActive = activeTerminal === index;
222
+
223
+ return (
224
+ <Terminal
225
+ key={index}
226
+ className={classNames('h-full overflow-hidden', {
227
+ hidden: !isActive,
228
+ })}
229
+ ref={(ref) => {
230
+ terminalRefs.current.push(ref);
231
+ }}
232
+ onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}
233
+ onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
234
+ theme={theme}
235
+ />
236
+ );
237
+ })}
238
  </div>
239
  </Panel>
240
  </PanelGroup>
packages/bolt/app/components/workbench/terminal/Terminal.tsx CHANGED
@@ -3,9 +3,10 @@ import { WebLinksAddon } from '@xterm/addon-web-links';
3
  import { Terminal as XTerm } from '@xterm/xterm';
4
  import { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react';
5
  import type { Theme } from '~/lib/stores/theme';
 
6
  import { getTerminalTheme } from './theme';
7
 
8
- import '@xterm/xterm/css/xterm.css';
9
 
10
  export interface TerminalRef {
11
  reloadStyles: () => void;
@@ -52,6 +53,8 @@ export const Terminal = memo(
52
 
53
  resizeObserver.observe(element);
54
 
 
 
55
  onTerminalReady?.(terminal);
56
 
57
  return () => {
 
3
  import { Terminal as XTerm } from '@xterm/xterm';
4
  import { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react';
5
  import type { Theme } from '~/lib/stores/theme';
6
+ import { createScopedLogger } from '~/utils/logger';
7
  import { getTerminalTheme } from './theme';
8
 
9
+ const logger = createScopedLogger('Terminal');
10
 
11
  export interface TerminalRef {
12
  reloadStyles: () => void;
 
53
 
54
  resizeObserver.observe(element);
55
 
56
+ logger.info('Attach terminal');
57
+
58
  onTerminalReady?.(terminal);
59
 
60
  return () => {
packages/bolt/app/lib/hooks/useShortcuts.ts CHANGED
@@ -41,7 +41,10 @@ export function useShortcuts(): void {
41
  ) {
42
  shortcutEventEmitter.dispatch(name as keyof Shortcuts);
43
  event.preventDefault();
 
 
44
  shortcut.action();
 
45
  break;
46
  }
47
  }
 
41
  ) {
42
  shortcutEventEmitter.dispatch(name as keyof Shortcuts);
43
  event.preventDefault();
44
+ event.stopPropagation();
45
+
46
  shortcut.action();
47
+
48
  break;
49
  }
50
  }
packages/bolt/app/root.tsx CHANGED
@@ -7,6 +7,7 @@ import { stripIndents } from './utils/stripIndent';
7
 
8
  import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
9
  import globalStyles from './styles/index.scss?url';
 
10
 
11
  import 'virtual:uno.css';
12
 
@@ -19,6 +20,7 @@ export const links: LinksFunction = () => [
19
  { rel: 'stylesheet', href: tailwindReset },
20
  { rel: 'stylesheet', href: globalStyles },
21
  { rel: 'stylesheet', href: reactToastifyStyles },
 
22
  {
23
  rel: 'preconnect',
24
  href: 'https://fonts.googleapis.com',
 
7
 
8
  import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
9
  import globalStyles from './styles/index.scss?url';
10
+ import xtermStyles from '@xterm/xterm/css/xterm.css?url';
11
 
12
  import 'virtual:uno.css';
13
 
 
20
  { rel: 'stylesheet', href: tailwindReset },
21
  { rel: 'stylesheet', href: globalStyles },
22
  { rel: 'stylesheet', href: reactToastifyStyles },
23
+ { rel: 'stylesheet', href: xtermStyles },
24
  {
25
  rel: 'preconnect',
26
  href: 'https://fonts.googleapis.com',
packages/bolt/app/styles/components/terminal.scss CHANGED
@@ -1,3 +1,3 @@
1
  .xterm {
2
- height: 100%;
3
  }
 
1
  .xterm {
2
+ padding: 1rem;
3
  }