Skip to content

Commit 5419038

Browse files
authored
feat(runtime): option for setting terminal open by default (#246)
1 parent 8fb3322 commit 5419038

File tree

10 files changed

+82
-6
lines changed

10 files changed

+82
-6
lines changed

docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ Configures one or more terminals. TutorialKit provides two types of terminals: r
148148

149149
You can define which terminal panel will be active by default by specifying the `activePanel` value. The value is the given terminal's position in the `panels` array. If you omit the `activePanel` property, the first panel will be the active one.
150150

151+
You can set terminal open by default by specifying the `open` value.
152+
151153
An interactive terminal will disable the output redirect syntax by default. For instance, you cannot create a file `world.txt` with the contents `hello` using the command `echo hello > world.txt`. The reason is that this could disrupt the lesson if a user overwrites certain files. To allow output redirection, you can change the behavior with the `allowRedirects` setting. You can define this setting either per panel or for all panels at once.
152154

153155
Additionally, you may not want users to run arbitrary commands. For example, if you are creating a lesson about `vitest`, you could specify that the only command the user can run is `vitest` by providing a list of `allowCommands`. Any other command executed by the user will be blocked. You can define the `allowCommands` setting either per panel or for all panels at once.
@@ -162,7 +164,8 @@ type Terminal = {
162164
panels: TerminalPanel[],
163165
activePanel?: number,
164166
allowRedirects?: boolean,
165-
allowCommands?: string[]
167+
allowCommands?: string[],
168+
open?: boolean,
166169
}
167170
168171
type TerminalPanel = TerminalType
@@ -177,6 +180,7 @@ Example value:
177180

178181
```yaml
179182
terminal:
183+
open: true
180184
activePanel: 1
181185
panels:
182186
- ['output', 'Dev Server']

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ export function TerminalPanel({ theme, tutorialStore }: TerminalPanelProps) {
7070
},
7171
)}
7272
title={title}
73+
id={`tk-terminal-tab-${index}`}
74+
role="tab"
7375
aria-selected={selected}
76+
aria-controls={`tk-terminal-tapbanel-${index}`}
7477
onClick={() => setTabIndex(index)}
7578
>
7679
<span
@@ -93,6 +96,9 @@ export function TerminalPanel({ theme, tutorialStore }: TerminalPanelProps) {
9396
{terminalConfig.panels.map(({ id, type }, index) => (
9497
<Terminal
9598
key={id}
99+
role="tabpanel"
100+
id={`tk-terminal-tapbanel-${index}`}
101+
aria-labelledby={`tk-terminal-tab-${index}`}
96102
className={tabIndex !== index ? 'hidden h-full' : 'h-full'}
97103
theme={theme}
98104
readonly={type === 'output'}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
9393
setHelpAction('reset');
9494
}
9595

96+
if (tutorialStore.terminalConfig.value?.defaultOpen) {
97+
showTerminal();
98+
}
99+
96100
return () => unsubscribe();
97101
}, [tutorialStore.ref]);
98102

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,23 @@ import { FitAddon } from '@xterm/addon-fit';
22
import { WebLinksAddon } from '@xterm/addon-web-links';
33
import { Terminal as XTerm } from '@xterm/xterm';
44
import '@xterm/xterm/css/xterm.css';
5-
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
5+
import { forwardRef, useEffect, useImperativeHandle, useRef, type ComponentProps } from 'react';
66
import '../../styles/terminal.css';
77
import { getTerminalTheme } from './theme.js';
88

99
export interface TerminalRef {
1010
reloadStyles: () => void;
1111
}
1212

13-
export interface TerminalProps {
13+
export interface TerminalProps extends ComponentProps<'div'> {
1414
theme: 'dark' | 'light';
15-
className?: string;
1615
readonly?: boolean;
1716
onTerminalReady?: (terminal: XTerm) => void;
1817
onTerminalResize?: (cols: number, rows: number) => void;
1918
}
2019

2120
export const Terminal = forwardRef<TerminalRef, TerminalProps>(
22-
({ theme, className = '', readonly = true, onTerminalReady, onTerminalResize }, ref) => {
21+
({ theme, readonly = true, onTerminalReady, onTerminalResize, ...props }, ref) => {
2322
const divRef = useRef<HTMLDivElement>(null);
2423
const terminalRef = useRef<XTerm>();
2524

@@ -79,7 +78,7 @@ export const Terminal = forwardRef<TerminalRef, TerminalProps>(
7978
};
8079
}, []);
8180

82-
return <div className={className} ref={divRef} />;
81+
return <div {...props} ref={divRef} />;
8382
},
8483
);
8584

packages/runtime/src/webcontainer/terminal-config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ITerminal } from '../utils/terminal.js';
55
interface NormalizedTerminalConfig {
66
panels: TerminalPanel[];
77
activePanel: number;
8+
defaultOpen: boolean;
89
}
910

1011
interface TerminalPanelOptions {
@@ -30,6 +31,10 @@ export class TerminalConfig {
3031
get activePanel() {
3132
return this._config.activePanel;
3233
}
34+
35+
get defaultOpen() {
36+
return this._config.defaultOpen;
37+
}
3338
}
3439

3540
const TERMINAL_PANEL_TITLES: Record<TerminalPanelType, string> = {
@@ -192,6 +197,7 @@ function normalizeTerminalConfig(config?: TerminalSchema): NormalizedTerminalCon
192197
return {
193198
panels: [],
194199
activePanel,
200+
defaultOpen: false,
195201
};
196202
}
197203

@@ -203,6 +209,7 @@ function normalizeTerminalConfig(config?: TerminalSchema): NormalizedTerminalCon
203209
return {
204210
panels: [new TerminalPanel('output')],
205211
activePanel,
212+
defaultOpen: false,
206213
};
207214
}
208215

@@ -254,5 +261,6 @@ function normalizeTerminalConfig(config?: TerminalSchema): NormalizedTerminalCon
254261
return {
255262
activePanel,
256263
panels,
264+
defaultOpen: config.open || false,
257265
};
258266
}

packages/types/src/schemas/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export const terminalSchema = z.union([
7474
z.boolean(),
7575

7676
z.strictObject({
77+
open: z.boolean().optional().describe('Defines if terminal should be open by default'),
78+
7779
panels: z.union([
7880
// either literally just `output`
7981
z.literal('output'),
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
type: lesson
3+
title: Default
4+
terminal:
5+
panels: terminal
6+
---
7+
8+
# Terminal test - Default
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
type: chapter
3+
title: Terminal
4+
---
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
type: lesson
3+
title: Open by default
4+
terminal:
5+
open: true
6+
panels: "terminal"
7+
---
8+
9+
# Terminal test - Open by default

test/ui/test/terminal.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const BASE_URL = '/tests/terminal';
4+
5+
test('user can open terminal', async ({ page }) => {
6+
await page.goto(`${BASE_URL}/default`);
7+
8+
await expect(page.getByRole('heading', { level: 1, name: 'Terminal test - Default' })).toBeVisible();
9+
10+
const tab = page.getByRole('tab', { name: 'Terminal' });
11+
const panel = page.getByRole('tabpanel', { name: 'Terminal' });
12+
13+
/* eslint-disable multiline-comment-style */
14+
// TODO: Requires #245
15+
// await expect(tab).not.toBeVisible();
16+
// await expect(panel).not.toBeVisible();
17+
18+
await page.getByRole('button', { name: 'Toggle Terminal' }).click();
19+
20+
await expect(tab).toBeVisible();
21+
await expect(panel).toBeVisible();
22+
await expect(panel).toContainText('~/tutorial', { useInnerText: true });
23+
});
24+
25+
test('user can see terminal open by default', async ({ page }) => {
26+
await page.goto(`${BASE_URL}/open-by-default`);
27+
28+
await expect(page.getByRole('heading', { level: 1, name: 'Terminal test - Open by default' })).toBeVisible();
29+
30+
await expect(page.getByRole('tab', { name: 'Terminal', selected: true })).toBeVisible();
31+
await expect(page.getByRole('tabpanel', { name: 'Terminal' })).toContainText('~/tutorial', { useInnerText: true });
32+
});

0 commit comments

Comments
 (0)