Skip to content

Commit

Permalink
feat(runtime): option for setting terminal open by default (#246)
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio authored Aug 19, 2024
1 parent 8fb3322 commit 5419038
Show file tree
Hide file tree
Showing 10 changed files with 82 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ Configures one or more terminals. TutorialKit provides two types of terminals: r

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.

You can set terminal open by default by specifying the `open` value.

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.

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.
Expand All @@ -162,7 +164,8 @@ type Terminal = {
panels: TerminalPanel[],
activePanel?: number,
allowRedirects?: boolean,
allowCommands?: string[]
allowCommands?: string[],
open?: boolean,
}
type TerminalPanel = TerminalType
Expand All @@ -177,6 +180,7 @@ Example value:

```yaml
terminal:
open: true
activePanel: 1
panels:
- ['output', 'Dev Server']
Expand Down
6 changes: 6 additions & 0 deletions packages/components/react/src/Panels/TerminalPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ export function TerminalPanel({ theme, tutorialStore }: TerminalPanelProps) {
},
)}
title={title}
id={`tk-terminal-tab-${index}`}
role="tab"
aria-selected={selected}
aria-controls={`tk-terminal-tapbanel-${index}`}
onClick={() => setTabIndex(index)}
>
<span
Expand All @@ -93,6 +96,9 @@ export function TerminalPanel({ theme, tutorialStore }: TerminalPanelProps) {
{terminalConfig.panels.map(({ id, type }, index) => (
<Terminal
key={id}
role="tabpanel"
id={`tk-terminal-tapbanel-${index}`}
aria-labelledby={`tk-terminal-tab-${index}`}
className={tabIndex !== index ? 'hidden h-full' : 'h-full'}
theme={theme}
readonly={type === 'output'}
Expand Down
4 changes: 4 additions & 0 deletions packages/components/react/src/Panels/WorkspacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
setHelpAction('reset');
}

if (tutorialStore.terminalConfig.value?.defaultOpen) {
showTerminal();
}

return () => unsubscribe();
}, [tutorialStore.ref]);

Expand Down
9 changes: 4 additions & 5 deletions packages/components/react/src/core/Terminal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@ import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { Terminal as XTerm } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import { forwardRef, useEffect, useImperativeHandle, useRef, type ComponentProps } from 'react';
import '../../styles/terminal.css';
import { getTerminalTheme } from './theme.js';

export interface TerminalRef {
reloadStyles: () => void;
}

export interface TerminalProps {
export interface TerminalProps extends ComponentProps<'div'> {
theme: 'dark' | 'light';
className?: string;
readonly?: boolean;
onTerminalReady?: (terminal: XTerm) => void;
onTerminalResize?: (cols: number, rows: number) => void;
}

export const Terminal = forwardRef<TerminalRef, TerminalProps>(
({ theme, className = '', readonly = true, onTerminalReady, onTerminalResize }, ref) => {
({ theme, readonly = true, onTerminalReady, onTerminalResize, ...props }, ref) => {
const divRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<XTerm>();

Expand Down Expand Up @@ -79,7 +78,7 @@ export const Terminal = forwardRef<TerminalRef, TerminalProps>(
};
}, []);

return <div className={className} ref={divRef} />;
return <div {...props} ref={divRef} />;
},
);

Expand Down
8 changes: 8 additions & 0 deletions packages/runtime/src/webcontainer/terminal-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ITerminal } from '../utils/terminal.js';
interface NormalizedTerminalConfig {
panels: TerminalPanel[];
activePanel: number;
defaultOpen: boolean;
}

interface TerminalPanelOptions {
Expand All @@ -30,6 +31,10 @@ export class TerminalConfig {
get activePanel() {
return this._config.activePanel;
}

get defaultOpen() {
return this._config.defaultOpen;
}
}

const TERMINAL_PANEL_TITLES: Record<TerminalPanelType, string> = {
Expand Down Expand Up @@ -192,6 +197,7 @@ function normalizeTerminalConfig(config?: TerminalSchema): NormalizedTerminalCon
return {
panels: [],
activePanel,
defaultOpen: false,
};
}

Expand All @@ -203,6 +209,7 @@ function normalizeTerminalConfig(config?: TerminalSchema): NormalizedTerminalCon
return {
panels: [new TerminalPanel('output')],
activePanel,
defaultOpen: false,
};
}

Expand Down Expand Up @@ -254,5 +261,6 @@ function normalizeTerminalConfig(config?: TerminalSchema): NormalizedTerminalCon
return {
activePanel,
panels,
defaultOpen: config.open || false,
};
}
2 changes: 2 additions & 0 deletions packages/types/src/schemas/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export const terminalSchema = z.union([
z.boolean(),

z.strictObject({
open: z.boolean().optional().describe('Defines if terminal should be open by default'),

panels: z.union([
// either literally just `output`
z.literal('output'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: lesson
title: Default
terminal:
panels: terminal
---

# Terminal test - Default
4 changes: 4 additions & 0 deletions test/ui/src/content/tutorial/tests/terminal/meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
type: chapter
title: Terminal
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
type: lesson
title: Open by default
terminal:
open: true
panels: "terminal"
---

# Terminal test - Open by default
32 changes: 32 additions & 0 deletions test/ui/test/terminal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test';

const BASE_URL = '/tests/terminal';

test('user can open terminal', async ({ page }) => {
await page.goto(`${BASE_URL}/default`);

await expect(page.getByRole('heading', { level: 1, name: 'Terminal test - Default' })).toBeVisible();

const tab = page.getByRole('tab', { name: 'Terminal' });
const panel = page.getByRole('tabpanel', { name: 'Terminal' });

/* eslint-disable multiline-comment-style */
// TODO: Requires #245
// await expect(tab).not.toBeVisible();
// await expect(panel).not.toBeVisible();

await page.getByRole('button', { name: 'Toggle Terminal' }).click();

await expect(tab).toBeVisible();
await expect(panel).toBeVisible();
await expect(panel).toContainText('~/tutorial', { useInnerText: true });
});

test('user can see terminal open by default', async ({ page }) => {
await page.goto(`${BASE_URL}/open-by-default`);

await expect(page.getByRole('heading', { level: 1, name: 'Terminal test - Open by default' })).toBeVisible();

await expect(page.getByRole('tab', { name: 'Terminal', selected: true })).toBeVisible();
await expect(page.getByRole('tabpanel', { name: 'Terminal' })).toContainText('~/tutorial', { useInnerText: true });
});

0 comments on commit 5419038

Please sign in to comment.