Skip to content

Commit cb1de9a

Browse files
chore: rename terminal to output and refactor terminal component (stackblitz#22)
1 parent b2b2bfb commit cb1de9a

File tree

3 files changed

+39
-25
lines changed

3 files changed

+39
-25
lines changed

packages/components/react/src/Panels/TerminalPanel.tsx

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { lazy, Suspense, useEffect, useState } from 'react';
2-
import type { Props } from '../Terminal';
2+
import type { TutorialRunner } from '@tutorialkit/runtime';
33

44
const Terminal = lazy(() => import('../Terminal/index.js'));
55

6-
export function TerminalPanel(props: Props) {
6+
interface TerminalPanelProps {
7+
theme: 'dark' | 'light';
8+
tutorialRunner: TutorialRunner
9+
}
10+
11+
export function TerminalPanel({ theme, tutorialRunner }: TerminalPanelProps) {
712
const [domLoaded, setDomLoaded] = useState(false);
813

914
useEffect(() => {
@@ -14,14 +19,18 @@ export function TerminalPanel(props: Props) {
1419
<div className="panel-container bg-tk-elements-app-backgroundColor">
1520
<div className="panel-header border-y border-tk-elements-app-borderColor">
1621
<div className="panel-title">
17-
<div className="panel-icon i-ph-terminal-window-duotone"></div>
18-
<span className="text-sm">Terminal</span>
22+
<div className="panel-icon i-ph-newspaper-duotone"></div>
23+
<span className="text-sm">Output</span>
1924
</div>
2025
</div>
2126
<div className="h-full overflow-hidden">
2227
{domLoaded && (
2328
<Suspense>
24-
<Terminal {...props} />
29+
<Terminal
30+
theme={theme}
31+
readonly={true}
32+
onTerminalReady={(terminal) => tutorialRunner.hookOutputPanel(terminal)}
33+
onTerminalResize={() => tutorialRunner.onOutputResize()}/>
2534
</Suspense>
2635
)}
2736
</div>

packages/components/react/src/Terminal/index.tsx

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { TutorialRunner } from '@tutorialkit/runtime';
21
import { FitAddon } from '@xterm/addon-fit';
32
import { WebLinksAddon } from '@xterm/addon-web-links';
43
import { Terminal as XTerm } from '@xterm/xterm';
@@ -7,12 +6,13 @@ import { useEffect, useRef } from 'react';
76
import { darkTheme, lightTheme } from './theme.js';
87

98
export interface Props {
10-
readonly?: boolean;
11-
tutorialRunner: TutorialRunner;
129
theme: 'dark' | 'light';
10+
readonly?: boolean;
11+
onTerminalReady?: (terminal: XTerm) => void;
12+
onTerminalResize?: () => void;
1313
}
1414

15-
export function Terminal({ tutorialRunner, theme, readonly = true }: Props) {
15+
export function Terminal({ theme, readonly = true, onTerminalReady, onTerminalResize }: Props) {
1616
const divRef = useRef<HTMLDivElement>(null);
1717
const terminalRef = useRef<XTerm>();
1818

@@ -30,12 +30,17 @@ export function Terminal({ tutorialRunner, theme, readonly = true }: Props) {
3030
const terminal = new XTerm({
3131
cursorBlink: true,
3232
convertEol: true,
33-
disableStdin: false,
33+
disableStdin: readonly,
3434
theme: theme === 'dark' ? darkTheme : lightTheme,
3535
fontSize: 13,
3636
fontFamily: 'Menlo, courier-new, courier, monospace',
3737
});
3838

39+
if (readonly) {
40+
// write DECTCEM to the terminal to hide the cursor if we are in readonly mode
41+
terminal.write('\x1b[?25l');
42+
}
43+
3944
terminalRef.current = terminal;
4045

4146
terminal.loadAddon(fitAddon);
@@ -44,15 +49,15 @@ export function Terminal({ tutorialRunner, theme, readonly = true }: Props) {
4449

4550
fitAddon.fit();
4651

47-
tutorialRunner.hookTerminal(terminal);
48-
4952
const resizeObserver = new ResizeObserver(() => {
5053
fitAddon.fit();
51-
tutorialRunner.onTerminalResize();
54+
onTerminalResize?.();
5255
});
5356

5457
resizeObserver.observe(element);
5558

59+
onTerminalReady?.(terminal);
60+
5661
return () => {
5762
resizeObserver.disconnect();
5863
terminal.dispose();

packages/runtime/src/tutorial-runner.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export class TutorialRunner {
6666
private _currentTemplate: Files | undefined = undefined;
6767
private _currentFiles: Files | undefined = undefined;
6868
private _currentRunCommands: Commands | undefined = undefined;
69-
private _terminal: ITerminal | undefined = undefined;
69+
private _output: ITerminal | undefined = undefined;
7070
private _packageJsonDirty = false;
7171

7272
// this strongly assumes that there's a single package json which might not be true
@@ -287,8 +287,8 @@ export class TutorialRunner {
287287
*
288288
* @param terminal Terminal to hook up to WebContainer.
289289
*/
290-
hookTerminal(terminal: ITerminal) {
291-
this._terminal = terminal;
290+
hookOutputPanel(terminal: ITerminal) {
291+
this._output = terminal;
292292

293293
if (!isWebContainerSupported()) {
294294
terminal.write(
@@ -348,8 +348,8 @@ export class TutorialRunner {
348348
}
349349
}
350350

351-
onTerminalResize() {
352-
const { cols, rows } = this._terminal ?? {};
351+
onOutputResize() {
352+
const { cols, rows } = this._output ?? {};
353353

354354
if (cols && rows) {
355355
this._currentCommandProcess?.resize({ cols, rows });
@@ -464,7 +464,7 @@ export class TutorialRunner {
464464
}
465465

466466
private async _runCommands(webcontainer: WebContainer, commands: Commands, signal: AbortSignal) {
467-
clearTerminal(this._terminal);
467+
clearTerminal(this._output);
468468

469469
const abortListener = () => this._currentCommandProcess?.kill();
470470
signal.addEventListener('abort', abortListener, { once: true });
@@ -495,7 +495,7 @@ export class TutorialRunner {
495495

496496
// print newlines between commands to visually separate them from one another
497497
if (index > 0) {
498-
this._terminal?.write('\n');
498+
this._output?.write('\n');
499499
}
500500

501501
this._currentCommandProcess = await this._newProcess(webcontainer, command.shellCommand);
@@ -547,18 +547,18 @@ export class TutorialRunner {
547547
private async _newProcess(webcontainer: WebContainer, shellCommand: string) {
548548
const [command, ...args] = shellCommand.split(' ');
549549

550-
this._terminal?.write(`${escapeCodes.magenta('❯')} ${escapeCodes.green(command)} ${args.join(' ')}\n`);
550+
this._output?.write(`${escapeCodes.magenta('❯')} ${escapeCodes.green(command)} ${args.join(' ')}\n`);
551551

552552
const process = await webcontainer.spawn(command, args, {
553-
terminal: this._terminal
553+
terminal: this._output
554554
? {
555-
cols: this._terminal.cols,
556-
rows: this._terminal.rows,
555+
cols: this._output.cols,
556+
rows: this._output.rows,
557557
}
558558
: undefined,
559559
});
560560

561-
process.output.pipeTo(new WritableStream({ write: (data) => this._terminal?.write(data) }));
561+
process.output.pipeTo(new WritableStream({ write: (data) => this._output?.write(data) }));
562562

563563
return process;
564564
}

0 commit comments

Comments
 (0)