Skip to content

feat(runtime): option for setting terminal open by default #246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Comment on lines +13 to +16
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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 });
});