Skip to content

Commit 90e8e08

Browse files
authored
enable terminal ui (#1918)
1 parent 29108cd commit 90e8e08

File tree

5 files changed

+183
-28
lines changed

5 files changed

+183
-28
lines changed

apps/web/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@types/webfontloader": "^1.6.38",
5555
"@uiw/codemirror-extensions-basic-setup": "^4.23.10",
5656
"@uiw/react-codemirror": "^4.23.10",
57+
"@xterm/xterm": "^5.5.0",
5758
"ai": "^4.3.10",
5859
"class-variance-authority": "^0.7.1",
5960
"clsx": "^2.1.1",

apps/web/client/src/app/project/[id]/_components/bottom-bar/index.tsx

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@onlook/ui/tooltip';
1010
import { observer } from 'mobx-react-lite';
1111
import { AnimatePresence, motion } from 'motion/react';
1212
import { useTranslations } from 'next-intl';
13+
import { TerminalArea } from './terminal-area';
1314

1415
const TOOLBAR_ITEMS = ({ t }: { t: (key: string) => string }) => [
1516
{
@@ -71,33 +72,35 @@ export const BottomBar = observer(() => {
7172
damping: 25,
7273
}}
7374
>
74-
<ToggleGroup
75-
type="single"
76-
value={editorEngine.state.editorMode}
77-
onValueChange={(value) => {
78-
if (value) {
79-
editorEngine.state.editorMode = value as EditorMode;
80-
}
81-
}}
82-
>
83-
{toolbarItems.map((item) => (
84-
<Tooltip key={item.mode}>
85-
<TooltipTrigger >
86-
<ToggleGroupItem
87-
value={item.mode}
88-
aria-label={item.hotkey.description}
89-
disabled={item.disabled}
90-
className="hover:text-foreground-hover text-foreground-tertiary"
91-
>
92-
<item.icon />
93-
</ToggleGroupItem>
94-
</TooltipTrigger>
95-
<TooltipContent>
96-
<HotkeyLabel hotkey={item.hotkey} />
97-
</TooltipContent>
98-
</Tooltip>
99-
))}
100-
</ToggleGroup>
75+
<TerminalArea>
76+
<ToggleGroup
77+
type="single"
78+
value={editorEngine.state.editorMode}
79+
onValueChange={(value) => {
80+
if (value) {
81+
editorEngine.state.editorMode = value as EditorMode;
82+
}
83+
}}
84+
>
85+
{toolbarItems.map((item) => (
86+
<Tooltip key={item.mode}>
87+
<TooltipTrigger >
88+
<ToggleGroupItem
89+
value={item.mode}
90+
aria-label={item.hotkey.description}
91+
disabled={item.disabled}
92+
className="hover:text-foreground-hover text-foreground-tertiary"
93+
>
94+
<item.icon />
95+
</ToggleGroupItem>
96+
</TooltipTrigger>
97+
<TooltipContent>
98+
<HotkeyLabel hotkey={item.hotkey} />
99+
</TooltipContent>
100+
</Tooltip>
101+
))}
102+
</ToggleGroup>
103+
</TerminalArea>
101104
</motion.div>
102105
)}
103106
</AnimatePresence>

apps/web/client/src/app/project/[id]/_components/bottom-bar/terminal-area.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Icons } from '@onlook/ui/icons';
22
import { Tooltip, TooltipContent, TooltipTrigger } from '@onlook/ui/tooltip';
33
import { motion } from 'motion/react';
44
import { useState } from 'react';
5+
import { Terminal } from './terminal';
56

67
export const TerminalArea = ({ children }: { children: React.ReactNode }) => {
78
const [terminalHidden, setTerminalHidden] = useState(true);
@@ -53,7 +54,7 @@ export const TerminalArea = ({ children }: { children: React.ReactNode }) => {
5354
</div>
5455
</motion.div>
5556
)}
56-
{/* <Terminal hidden={terminalHidden} /> */}
57+
<Terminal hidden={terminalHidden} />
5758
</>
5859
);
5960
};
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
'use client';
2+
3+
import '@xterm/xterm/css/xterm.css';
4+
5+
import { useEditorEngine } from '@/components/store/editor';
6+
import { useProjectsManager } from '@/components/store/projects';
7+
import type { Terminal as CsbTerminal, WebSocketSession } from '@codesandbox/sdk';
8+
import { cn } from '@onlook/ui/utils';
9+
import { Terminal as XTerm, type ITheme } from '@xterm/xterm';
10+
import { observer } from 'mobx-react-lite';
11+
import { useTheme } from 'next-themes';
12+
import { useEffect, useRef, useState } from 'react';
13+
14+
interface TerminalProps {
15+
hidden?: boolean;
16+
}
17+
18+
const TERMINAL_THEME: Record<'LIGHT' | 'DARK', ITheme> = {
19+
LIGHT: {
20+
background: '#ffffff',
21+
foreground: '#2d2d2d',
22+
cursor: '#333333',
23+
cursorAccent: '#ffffff',
24+
black: '#2d2d2d',
25+
red: '#d64646',
26+
green: '#4e9a06',
27+
yellow: '#c4a000',
28+
blue: '#3465a4',
29+
magenta: '#75507b',
30+
cyan: '#06989a',
31+
white: '#d3d7cf',
32+
brightBlack: '#555753',
33+
brightRed: '#ef2929',
34+
brightGreen: '#8ae234',
35+
brightYellow: '#fce94f',
36+
brightBlue: '#729fcf',
37+
brightMagenta: '#ad7fa8',
38+
brightCyan: '#34e2e2',
39+
brightWhite: '#eeeeec',
40+
selectionBackground: '#bfbfbf',
41+
},
42+
DARK: {}, // Use default dark theme
43+
};
44+
45+
export const Terminal = observer(({ hidden = false }: TerminalProps) => {
46+
const editorEngine = useEditorEngine();
47+
const sandboxSession = editorEngine.sandbox.session.session;
48+
const terminalRef = useRef<HTMLDivElement>(null);
49+
const [xterm, setXterm] = useState<XTerm | null>(null);
50+
const [terminal, setTerminal] = useState<CsbTerminal | null>(null);
51+
const projectsManager = useProjectsManager();
52+
53+
const { theme } = useTheme();
54+
55+
useEffect(() => {
56+
if (xterm) {
57+
xterm.options.theme = theme === 'light' ? TERMINAL_THEME.LIGHT : TERMINAL_THEME.DARK;
58+
}
59+
}, [theme]);
60+
61+
62+
63+
useEffect(() => {
64+
if (!sandboxSession) {
65+
console.error('sandboxSession is null');
66+
return;
67+
}
68+
69+
let terminalOutputListener: { dispose: () => void } | undefined;
70+
let xtermDataListener: { dispose: () => void } | undefined;
71+
72+
(async () => {
73+
const { terminalOutputListener: outputListener, xtermDataListener: dataListener } = await initTerminal(
74+
sandboxSession,
75+
);
76+
terminalOutputListener = outputListener;
77+
xtermDataListener = dataListener;
78+
})();
79+
80+
return () => {
81+
xterm?.dispose();
82+
terminal?.kill();
83+
setTerminal(null);
84+
setXterm(null);
85+
terminalOutputListener?.dispose();
86+
xtermDataListener?.dispose();
87+
};
88+
}, [sandboxSession]);
89+
90+
async function initTerminal(session: WebSocketSession): Promise<{ terminalOutputListener: { dispose: () => void }, xtermDataListener: { dispose: () => void } }> {
91+
if (!terminalRef.current) {
92+
return {
93+
terminalOutputListener: { dispose: () => { } },
94+
xtermDataListener: { dispose: () => { } },
95+
};
96+
}
97+
const terminal = await session.terminals.create()
98+
99+
const xterm = new XTerm({
100+
cursorBlink: true,
101+
fontSize: 12,
102+
fontFamily: 'monospace',
103+
theme: theme === 'light' ? TERMINAL_THEME.LIGHT : TERMINAL_THEME.DARK,
104+
convertEol: true,
105+
allowTransparency: true,
106+
disableStdin: false,
107+
allowProposedApi: true,
108+
macOptionIsMeta: true,
109+
});
110+
111+
xterm.open(terminalRef.current);
112+
await terminal.open();
113+
114+
const terminalOutputListener = terminal.onOutput((output: string) => {
115+
xterm.write(output)
116+
});
117+
118+
const xtermDataListener = xterm.onData((data: string) => {
119+
terminal.write(data)
120+
})
121+
122+
setXterm(xterm);
123+
setTerminal(terminal);
124+
125+
return {
126+
terminalOutputListener,
127+
xtermDataListener,
128+
};
129+
}
130+
131+
return (
132+
<div
133+
className={cn(
134+
'bg-background rounded-lg overflow-auto transition-all duration-300',
135+
hidden ? 'h-0 w-0 invisible' : 'h-[22rem] w-[37rem]',
136+
)}
137+
>
138+
<div
139+
ref={terminalRef}
140+
className={cn(
141+
'h-full w-full p-2 transition-opacity duration-200',
142+
hidden ? 'opacity-0' : 'opacity-100 delay-300',
143+
)}
144+
/>
145+
</div>
146+
);
147+
});

bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@types/webfontloader": "^1.6.38",
5959
"@uiw/codemirror-extensions-basic-setup": "^4.23.10",
6060
"@uiw/react-codemirror": "^4.23.10",
61+
"@xterm/xterm": "^5.5.0",
6162
"ai": "^4.3.10",
6263
"class-variance-authority": "^0.7.1",
6364
"clsx": "^2.1.1",
@@ -1568,6 +1569,8 @@
15681569

15691570
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA=="],
15701571

1572+
"@xterm/xterm": ["@xterm/[email protected]", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="],
1573+
15711574
"abort-controller": ["[email protected]", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
15721575

15731576
"abstract-logging": ["[email protected]", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],

0 commit comments

Comments
 (0)