├── docs
├── changelog.md
├── index.md
├── environment-docs.yml
├── Makefile
├── make.bat
├── conf.py
└── _static
│ └── terminal_logo.svg
├── .yarnrc.yml
├── style
├── index.js
├── index.css
└── base.css
├── setup.py
├── ui-tests
├── .yarnrc.yml
├── cockle-config-in.json
├── jupyter_lite_config.json
├── contents
│ ├── months.txt
│ └── fact.lua
├── tests
│ ├── jupyterlite_terminal.spec.ts-snapshots
│ │ ├── initial-linux.png
│ │ ├── stdin-sab-linux.png
│ │ ├── stdin-sw-linux.png
│ │ ├── set-sw-stdin-linux.png
│ │ ├── both-sab-and-sw-linux.png
│ │ └── various-commands-linux.png
│ ├── extension.spec.ts
│ ├── utils
│ │ ├── misc.ts
│ │ └── contents.ts
│ ├── lifetime.spec.ts
│ ├── fs.spec.ts
│ ├── command.spec.ts
│ └── jupyterlite_terminal.spec.ts
├── jupyter-lite.json
├── build.py
├── playwright.config.js
├── package.json
└── README.md
├── babel.config.js
├── screenshot.png
├── deploy
├── requirements-deploy.txt
├── jupyter-lite.json
├── contents
│ ├── months.txt
│ └── fact.lua
└── cockle-config-in.json
├── tsconfig.test.json
├── .prettierignore
├── install.json
├── src
├── __tests__
│ └── jupyterlite_terminal.spec.ts
├── shell.ts
├── worker.ts
├── tokens.ts
├── index.ts
└── client.ts
├── .github
├── dependabot.yml
└── workflows
│ ├── enforce-label.yml
│ ├── rtd-preview.yml
│ ├── check-release.yml
│ ├── deploy.yml
│ ├── prep-release.yml
│ ├── update-integration-tests.yml
│ ├── publish-release.yml
│ └── build.yml
├── .readthedocs.yaml
├── .copier-answers.yml
├── tsconfig.json
├── jupyterlite_terminal
├── __init__.py
└── add_on.py
├── jest.config.js
├── LICENSE
├── webpack.worker.config.js
├── .gitignore
├── RELEASE.md
├── pyproject.toml
├── README.md
├── package.json
└── CHANGELOG.md
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | ../CHANGELOG.md
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/style/index.js:
--------------------------------------------------------------------------------
1 | import './base.css';
2 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | __import__("setuptools").setup()
2 |
--------------------------------------------------------------------------------
/style/index.css:
--------------------------------------------------------------------------------
1 | @import url('base.css');
2 |
--------------------------------------------------------------------------------
/ui-tests/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | enableImmutableInstalls: false
2 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require('@jupyterlab/testutils/lib/babel.config');
2 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlite/terminal/HEAD/screenshot.png
--------------------------------------------------------------------------------
/deploy/requirements-deploy.txt:
--------------------------------------------------------------------------------
1 | jupyterlab
2 | jupyterlite-core
3 | jupyterlite-pyodide-kernel
4 | ..
5 |
--------------------------------------------------------------------------------
/ui-tests/cockle-config-in.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": {
3 | "nano": {},
4 | "vim": {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ui-tests/jupyter_lite_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "LiteBuildConfig": {
3 | "output_dir": "dist"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "types": ["jest"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | **/node_modules
3 | **/lib
4 | **/package.json
5 | !/package.json
6 | jupyterlite_terminal
7 |
--------------------------------------------------------------------------------
/deploy/jupyter-lite.json:
--------------------------------------------------------------------------------
1 | {
2 | "jupyter-lite-schema-version": 0,
3 | "jupyter-config-data": {
4 | "terminalsAvailable": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/deploy/contents/months.txt:
--------------------------------------------------------------------------------
1 | January
2 | February
3 | March
4 | April
5 | May
6 | June
7 | July
8 | August
9 | September
10 | October
11 | November
12 | December
13 |
--------------------------------------------------------------------------------
/style/base.css:
--------------------------------------------------------------------------------
1 | /*
2 | See the JupyterLab Developer Guide for useful CSS Patterns:
3 |
4 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html
5 | */
6 |
--------------------------------------------------------------------------------
/ui-tests/contents/months.txt:
--------------------------------------------------------------------------------
1 | January
2 | February
3 | March
4 | April
5 | May
6 | June
7 | July
8 | August
9 | September
10 | October
11 | November
12 | December
13 |
--------------------------------------------------------------------------------
/deploy/contents/fact.lua:
--------------------------------------------------------------------------------
1 | function fact(n, acc)
2 | acc = acc or 1
3 | if n == 0 then
4 | return acc
5 | end
6 | return fact(n-1, n*acc)
7 | end
8 | print(fact(tonumber(arg[1])))
9 |
--------------------------------------------------------------------------------
/ui-tests/contents/fact.lua:
--------------------------------------------------------------------------------
1 | function fact(n, acc)
2 | acc = acc or 1
3 | if n == 0 then
4 | return acc
5 | end
6 | return fact(n-1, n*acc)
7 | end
8 | print(fact(tonumber(arg[1])))
9 |
--------------------------------------------------------------------------------
/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/initial-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlite/terminal/HEAD/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/initial-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/stdin-sab-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlite/terminal/HEAD/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/stdin-sab-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/stdin-sw-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlite/terminal/HEAD/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/stdin-sw-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/set-sw-stdin-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlite/terminal/HEAD/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/set-sw-stdin-linux.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # JupyterLite Terminal
2 |
3 | Blah blah blah.
4 |
5 | ## Contents
6 |
7 | ```{toctree}
8 | :maxdepth: 1
9 |
10 | changelog
11 | ```
12 |
13 | Link to deployment.
14 |
--------------------------------------------------------------------------------
/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/both-sab-and-sw-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlite/terminal/HEAD/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/both-sab-and-sw-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/various-commands-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlite/terminal/HEAD/ui-tests/tests/jupyterlite_terminal.spec.ts-snapshots/various-commands-linux.png
--------------------------------------------------------------------------------
/install.json:
--------------------------------------------------------------------------------
1 | {
2 | "packageManager": "python",
3 | "packageName": "jupyterlite_terminal",
4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlite_terminal"
5 | }
6 |
--------------------------------------------------------------------------------
/ui-tests/jupyter-lite.json:
--------------------------------------------------------------------------------
1 | {
2 | "jupyter-lite-schema-version": 0,
3 | "jupyter-config-data": {
4 | "appName": "JupyterLite terminal UI Tests",
5 | "exposeAppInBrowser": true,
6 | "terminalsAvailable": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/__tests__/jupyterlite_terminal.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
3 | */
4 |
5 | describe('jupyterlite-terminal', () => {
6 | it('should be tested', () => {
7 | expect(1 + 1).toEqual(2);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule:
6 | interval: weekly
7 | labels:
8 | - 'maintenance'
9 | groups:
10 | actions:
11 | patterns:
12 | - '*'
13 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: ubuntu-24.04
5 | tools:
6 | python: mambaforge-23.11
7 | jobs:
8 | pre_build:
9 | - pip install -v .
10 |
11 | conda:
12 | environment: docs/environment-docs.yml
13 |
14 | sphinx:
15 | builder: html
16 | configuration: docs/conf.py
17 | fail_on_warning: true
18 |
--------------------------------------------------------------------------------
/docs/environment-docs.yml:
--------------------------------------------------------------------------------
1 | name: terminal-docs
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | # General build dependencies
6 | - micromamba
7 | - nodejs =20
8 | - python =3.13
9 |
10 | # Jupyter dependencies
11 | - jupyterlite-core >=0.7,<0.8.0
12 |
13 | # Docs dependencies
14 | - jupyterlite-sphinx
15 | - myst-parser
16 | - pydata-sphinx-theme
17 | - sphinx
18 |
--------------------------------------------------------------------------------
/.github/workflows/enforce-label.yml:
--------------------------------------------------------------------------------
1 | name: Enforce PR label
2 |
3 | on:
4 | pull_request:
5 | types: [labeled, unlabeled, opened, edited, synchronize]
6 | jobs:
7 | enforce-label:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | pull-requests: write
11 | steps:
12 | - name: enforce-triage-label
13 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1
14 |
--------------------------------------------------------------------------------
/ui-tests/build.py:
--------------------------------------------------------------------------------
1 | # Build jupyterlite deployment containing terminal extension for playwright tests.
2 |
3 | from pathlib import Path
4 | from subprocess import run
5 |
6 | import jupyterlab
7 |
8 | extra_labextensions_path = str(Path(jupyterlab.__file__).parent / "galata")
9 | cmd = [
10 | "jupyter",
11 | "lite",
12 | "build",
13 | "--contents",
14 | "contents",
15 | f"--FederatedExtensionAddon.extra_labextensions_path={extra_labextensions_path}",
16 | ]
17 | run(cmd, check=True)
18 |
--------------------------------------------------------------------------------
/.copier-answers.yml:
--------------------------------------------------------------------------------
1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2 | _commit: v4.3.0
3 | _src_path: https://github.com/jupyterlab/extension-template
4 | author_email:
5 | author_name: JupyterLite Contributors
6 | has_binder: false
7 | has_settings: false
8 | kind: frontend
9 | labextension_name: "@jupyterlite/terminal"
10 | project_short_description: A terminal for JupyterLite
11 | python_name: jupyterlite_terminal
12 | repository: https://github.com/jupyterlite/terminal
13 | test: true
14 |
15 |
--------------------------------------------------------------------------------
/deploy/cockle-config-in.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": {
3 | "git2cpp": {},
4 | "lua": {},
5 | "nano": {},
6 | "tree": {},
7 | "vim": {}
8 | },
9 | "aliases": {
10 | "git": "git2cpp",
11 | "vi": "vim"
12 | },
13 | "environment": {
14 | "GIT_CORS_PROXY": "https://corsproxy.io/?url=",
15 | "GIT_AUTHOR_NAME": "Jane Doe",
16 | "GIT_AUTHOR_EMAIL": "jane.doe@blabla.com",
17 | "GIT_COMMITTER_NAME": "Jane Doe",
18 | "GIT_COMMITTER_EMAIL": "jane.doe@blabla.com"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ui-tests/playwright.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configuration for Playwright using default from @jupyterlab/galata
3 | */
4 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config');
5 |
6 | module.exports = {
7 | ...baseConfig,
8 | use: {
9 | acceptDownloads: true,
10 | autoGoto: false,
11 | baseURL: 'http://localhost:8000'
12 | },
13 | retries: process.env.CI ? 2 : 0,
14 | workers: 1,
15 | webServer: {
16 | command: 'jlpm start',
17 | port: 8000,
18 | timeout: 120 * 1000,
19 | reuseExistingServer: true
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "composite": true,
6 | "declaration": true,
7 | "esModuleInterop": true,
8 | "incremental": true,
9 | "jsx": "react",
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "noEmitOnError": true,
13 | "noImplicitAny": true,
14 | "noUnusedLocals": true,
15 | "preserveWatchOutput": true,
16 | "resolveJsonModule": true,
17 | "outDir": "lib",
18 | "rootDir": "src",
19 | "strict": true,
20 | "strictNullChecks": true,
21 | "target": "ES2018"
22 | },
23 | "include": ["src/*"]
24 | }
25 |
--------------------------------------------------------------------------------
/jupyterlite_terminal/__init__.py:
--------------------------------------------------------------------------------
1 | try:
2 | from ._version import __version__
3 | except ImportError:
4 | # Fallback when using the package in dev mode without installing
5 | # in editable mode with pip. It is highly recommended to install
6 | # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs
7 | import warnings
8 | warnings.warn("Importing 'jupyterlite_terminal' outside a proper installation.")
9 | __version__ = "dev"
10 |
11 |
12 | def _jupyter_labextension_paths():
13 | return [{
14 | "src": "labextension",
15 | "dest": "jupyterlite-terminal"
16 | }]
17 |
--------------------------------------------------------------------------------
/ui-tests/tests/extension.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jupyterlab/galata';
2 |
3 | import { LONG_WAIT_MS } from './utils/misc';
4 |
5 | test.describe('Terminal extension', () => {
6 | test('should emit activation console messages', async ({ page }) => {
7 | const logs: string[] = [];
8 | page.on('console', message => {
9 | logs.push(message.text());
10 | });
11 |
12 | await page.goto();
13 | await page.waitForTimeout(LONG_WAIT_MS);
14 |
15 | expect(
16 | logs.filter(s =>
17 | s.match(
18 | /^JupyterLite extension @jupyterlite\/terminal:manager activated/
19 | )
20 | )
21 | ).toHaveLength(1);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config');
2 |
3 | const esModules = [
4 | '@codemirror',
5 | '@jupyter/ydoc',
6 | '@jupyterlab/',
7 | 'lib0',
8 | 'nanoid',
9 | 'vscode-ws-jsonrpc',
10 | 'y-protocols',
11 | 'y-websocket',
12 | 'yjs'
13 | ].join('|');
14 |
15 | const baseConfig = jestJupyterLab(__dirname);
16 |
17 | module.exports = {
18 | ...baseConfig,
19 | automock: false,
20 | collectCoverageFrom: [
21 | 'src/**/*.{ts,tsx}',
22 | '!src/**/*.d.ts',
23 | '!src/**/.ipynb_checkpoints/*'
24 | ],
25 | coverageReporters: ['lcov', 'text'],
26 | testRegex: 'src/.*/.*.spec.ts[x]?$',
27 | transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`]
28 | };
29 |
--------------------------------------------------------------------------------
/ui-tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jupyterlite-terminal-ui-tests",
3 | "version": "1.0.0",
4 | "description": "JupyterLab jupyterlite-terminal Integration Tests",
5 | "private": true,
6 | "scripts": {
7 | "build": "jlpm clean && python build.py",
8 | "clean": "rimraf cockle_wasm_env dist .cockle_temp .jupyterlite.doit.db",
9 | "start": "npx static-handler -p 8000 --cors --coop --coep --corp ./dist",
10 | "test": "jlpm playwright test",
11 | "test:ui": "jlpm playwright test --ui",
12 | "test:update": "jlpm playwright test --update-snapshots"
13 | },
14 | "devDependencies": {
15 | "@jupyterlab/galata": "^5.4.3",
16 | "@playwright/test": "^1.51.0",
17 | "rimraf": "^6.0.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/rtd-preview.yml:
--------------------------------------------------------------------------------
1 | name: Read the Docs preview
2 | on:
3 | pull_request_target:
4 | types: [opened]
5 |
6 | permissions:
7 | pull-requests: write
8 |
9 | jobs:
10 | binder:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Comment on the PR with the RTD preview
14 | uses: actions/github-script@v8
15 | with:
16 | github-token: ${{secrets.GITHUB_TOKEN}}
17 | script: |
18 | var PR_NUMBER = context.issue.number
19 | github.rest.issues.createComment({
20 | issue_number: context.issue.number,
21 | owner: context.repo.owner,
22 | repo: context.repo.repo,
23 | body: `[View PR preview of docs and deployment on Read the Docs](https://jupyterlite-terminal--${PR_NUMBER}.org.readthedocs.build/en/${PR_NUMBER})`
24 | })
25 |
--------------------------------------------------------------------------------
/ui-tests/tests/utils/misc.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from 'node:buffer';
2 | import type { Page } from '@playwright/test';
3 |
4 | export const WAIT_MS = 100;
5 |
6 | // Long wait such as for starting/stopping a complex WebAssembly command.
7 | export const LONG_WAIT_MS = 300;
8 |
9 | export const TERMINAL_SELECTOR = '.jp-Terminal';
10 |
11 | export function decode64(encoded: string): string {
12 | return Buffer.from(encoded, 'base64').toString('binary');
13 | }
14 |
15 | export async function inputLine(
16 | page: Page,
17 | text: string,
18 | enter: boolean = true
19 | ) {
20 | const ms = 20;
21 | await page.waitForTimeout(ms);
22 | for (const char of text) {
23 | await page.keyboard.type(char);
24 | await page.waitForTimeout(ms);
25 | }
26 | if (enter) {
27 | await page.keyboard.press('Enter');
28 | await page.waitForTimeout(ms);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/shell.ts:
--------------------------------------------------------------------------------
1 | import type { IShell } from '@jupyterlite/cockle';
2 | import { BaseShell } from '@jupyterlite/cockle';
3 |
4 | import type { Client as WebSocketClient } from 'mock-socket';
5 |
6 | /**
7 | * Shell class that uses web worker that plugs into a DriveFS via the service worker.
8 | */
9 | export class Shell extends BaseShell {
10 | /**
11 | * Instantiate a new Shell
12 | *
13 | * @param options The instantiation options for a new shell
14 | */
15 | constructor(options: IShell.IOptions) {
16 | super(options);
17 | }
18 |
19 | /**
20 | * Load the web worker.
21 | */
22 | protected override initWorker(options: IShell.IOptions): Worker {
23 | console.log('Terminal create webworker');
24 | return new Worker(new URL('./worker.js', import.meta.url), {
25 | type: 'module'
26 | });
27 | }
28 |
29 | socket?: WebSocketClient;
30 | }
31 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/.github/workflows/check-release.yml:
--------------------------------------------------------------------------------
1 | name: Check Release
2 | on:
3 | push:
4 | branches: ["main"]
5 | pull_request:
6 | branches: ["*"]
7 |
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | check_release:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v6
18 | - name: Base Setup
19 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
20 | - name: Check Release
21 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2
22 | with:
23 |
24 | token: ${{ secrets.GITHUB_TOKEN }}
25 |
26 | - name: Upload Distributions
27 | uses: actions/upload-artifact@v6
28 | with:
29 | name: jupyterlite_terminal-releaser-dist-${{ github.run_number }}
30 | path: .jupyter_releaser_checkout/dist
31 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 | import json
3 | from pathlib import Path
4 |
5 | PACKAGE_JSON_FILENAME = Path(__file__).parent.parent / "package.json"
6 | PACKAGE_JSON = json.loads(PACKAGE_JSON_FILENAME.read_text(encoding="utf-8"))
7 |
8 | project = 'JupyterLite Terminal'
9 | author = PACKAGE_JSON["author"]["name"]
10 | copyright = f"2024-{date.today().year}, {author}"
11 | release = PACKAGE_JSON["version"]
12 |
13 | extensions = [
14 | "jupyterlite_sphinx",
15 | "myst_parser"
16 | ]
17 |
18 | templates_path = ['_templates']
19 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
20 |
21 | html_favicon = "_static/terminal_logo.svg"
22 | html_logo = "_static/terminal_logo.svg"
23 | html_static_path = ['_static']
24 | html_theme = "pydata_sphinx_theme"
25 | html_theme_options = {
26 | "github_url": PACKAGE_JSON["homepage"],
27 | "logo": {
28 | "alt_text": "JupyterLite Terminal - Home",
29 | "text": "JupyterLite Terminal",
30 | }
31 | }
32 | html_title = "JupyterLite Terminal"
33 |
34 | # jupyterlite_sphonx settings
35 | jupyterlite_contents = "../deploy/contents/"
36 | jupyterlite_dir = "../deploy/"
37 | jupyterlite_silence = False
38 |
--------------------------------------------------------------------------------
/src/worker.ts:
--------------------------------------------------------------------------------
1 | import { expose } from 'comlink';
2 |
3 | import type { IDriveFSOptions } from '@jupyterlite/cockle';
4 | import { BaseShellWorker } from '@jupyterlite/cockle';
5 | import { DriveFS } from '@jupyterlite/services';
6 |
7 | /**
8 | * Shell web worker that uses DriveFS via service worker.
9 | * Note that this is not exported as it is accessed from Shell via the filename.
10 | */
11 | class ShellWorker extends BaseShellWorker {
12 | /**
13 | * Initialize the DriveFS to mount an external file system, if available.
14 | */
15 | protected override initDriveFS(options: IDriveFSOptions): void {
16 | const { baseUrl, browsingContextId, fileSystem, mountpoint } = options;
17 | console.log('Terminal initDriveFS', baseUrl, mountpoint, browsingContextId);
18 | if (
19 | mountpoint !== '' &&
20 | baseUrl !== undefined &&
21 | browsingContextId !== undefined
22 | ) {
23 | const { FS, ERRNO_CODES, PATH } = fileSystem;
24 | const driveFS = new DriveFS({
25 | FS,
26 | PATH,
27 | ERRNO_CODES,
28 | baseUrl,
29 | driveName: '',
30 | mountpoint,
31 | browsingContextId
32 | });
33 | FS.mount(driveFS, {}, mountpoint);
34 | console.log('Terminal connected to shared drive');
35 | } else {
36 | console.warn('Terminal not connected to shared drive');
37 | }
38 | }
39 | }
40 |
41 | const worker = new ShellWorker();
42 | expose(worker);
43 |
--------------------------------------------------------------------------------
/ui-tests/tests/utils/contents.ts:
--------------------------------------------------------------------------------
1 | import type { Contents } from '@jupyterlab/services';
2 | import type { Page } from '@playwright/test';
3 |
4 | /**
5 | * Helper class to interact with JupyterLite contents manager.
6 | *
7 | * A subset of the functionality of galata's implementation
8 | */
9 | export class ContentsHelper {
10 | constructor(readonly page: Page) {}
11 |
12 | async directoryExists(dirPath: string): Promise {
13 | const content = await this._get(dirPath, false, 'directory');
14 | return content?.type === 'directory';
15 | }
16 |
17 | async fileExists(filePath: string): Promise {
18 | const content = await this._get(filePath, false);
19 | return content?.type === 'notebook' || content?.type === 'file';
20 | }
21 |
22 | async getContentMetadata(
23 | path: string,
24 | type: 'file' | 'directory' = 'file'
25 | ): Promise {
26 | return await this._get(path, true, type);
27 | }
28 |
29 | private async _get(
30 | path: string,
31 | wantContents: boolean,
32 | type: 'file' | 'directory' = 'file'
33 | ): Promise {
34 | const model = await this.page.evaluate(
35 | async ({ path, wantContents, type }) => {
36 | const contents = window.galata.app.serviceManager.contents;
37 | const options = { type, content: wantContents };
38 | try {
39 | return await contents.get(path, options);
40 | } catch (error) {
41 | return null;
42 | }
43 | },
44 | { path, wantContents, type }
45 | );
46 | return model;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c), Ian Thomas
4 | Copyright (c), JupyterLite Contributors
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions are met:
9 |
10 | 1. Redistributions of source code must retain the above copyright notice, this
11 | list of conditions and the following disclaimer.
12 |
13 | 2. Redistributions in binary form must reproduce the above copyright notice,
14 | this list of conditions and the following disclaimer in the documentation
15 | and/or other materials provided with the distribution.
16 |
17 | 3. Neither the name of the copyright holder nor the names of its
18 | contributors may be used to endorse or promote products derived from
19 | this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - '*'
10 |
11 | jobs:
12 | build:
13 | name: Build for deployment
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v6
18 |
19 | - name: Setup Python
20 | uses: actions/setup-python@v6
21 | with:
22 | python-version: '3.14'
23 |
24 | - name: Install the dependencies
25 | run: |
26 | python -m pip install jupyterlite-pyodide-kernel
27 |
28 | # install a dev version of the terminal extension
29 | python -m pip install .
30 |
31 | - name: List installed packages
32 | run: |
33 | python -m pip list
34 |
35 | - name: Micromamba needed for cockle_wasm_env
36 | uses: mamba-org/setup-micromamba@main
37 |
38 | - name: Build the JupyterLite site
39 | working-directory: deploy
40 | run: |
41 | jupyter lite build --output-dir dist
42 |
43 | - name: Upload artifact
44 | uses: actions/upload-pages-artifact@v4
45 | with:
46 | path: ./deploy/dist
47 |
48 | deploy:
49 | name: Deploy to github pages
50 | needs: build
51 | if: github.ref == 'refs/heads/main'
52 | permissions:
53 | pages: write
54 | id-token: write
55 |
56 | environment:
57 | name: github-pages
58 | url: ${{ steps.deployment.outputs.page_url }}
59 |
60 | runs-on: ubuntu-latest
61 | steps:
62 | - name: Deploy to GitHub Pages
63 | id: deployment
64 | uses: actions/deploy-pages@v4
65 |
--------------------------------------------------------------------------------
/.github/workflows/prep-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 1: Prep Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version_spec:
6 | description: "New Version Specifier"
7 | default: "next"
8 | required: false
9 | branch:
10 | description: "The branch to target"
11 | required: false
12 | post_version_spec:
13 | description: "Post Version Specifier"
14 | required: false
15 | # silent:
16 | # description: "Set a placeholder in the changelog and don't publish the release."
17 | # required: false
18 | # type: boolean
19 | since:
20 | description: "Use PRs with activity since this date or git reference"
21 | required: false
22 | since_last_stable:
23 | description: "Use PRs with activity since the last stable git tag"
24 | required: false
25 | type: boolean
26 | jobs:
27 | prep_release:
28 | runs-on: ubuntu-latest
29 | permissions:
30 | contents: write
31 | steps:
32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
33 |
34 | - name: Prep Release
35 | id: prep-release
36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
37 | with:
38 | token: ${{ secrets.GITHUB_TOKEN }}
39 | version_spec: ${{ github.event.inputs.version_spec }}
40 | # silent: ${{ github.event.inputs.silent }}
41 | post_version_spec: ${{ github.event.inputs.post_version_spec }}
42 | branch: ${{ github.event.inputs.branch }}
43 | since: ${{ github.event.inputs.since }}
44 | since_last_stable: ${{ github.event.inputs.since_last_stable }}
45 |
46 | - name: "** Next Step **"
47 | run: |
48 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}"
49 |
--------------------------------------------------------------------------------
/src/tokens.ts:
--------------------------------------------------------------------------------
1 | import type { Terminal } from '@jupyterlab/services';
2 | import type {
3 | IExternalCommand,
4 | IStdinReply,
5 | IStdinRequest
6 | } from '@jupyterlite/cockle';
7 | import { Token } from '@lumino/coreutils';
8 | import type { ISignal } from '@lumino/signaling';
9 |
10 | export const ILiteTerminalAPIClient = new Token(
11 | '@jupyterlite/terminal:client'
12 | );
13 |
14 | export interface ILiteTerminalAPIClient extends Terminal.ITerminalAPIClient {
15 | /**
16 | * Identifier for communicating with service worker.
17 | */
18 | browsingContextId: string;
19 |
20 | /**
21 | * Function that handles stdin requests received from service worker.
22 | */
23 | handleStdin(request: IStdinRequest): Promise;
24 |
25 | /**
26 | * Register an alias that will be available in all terminals.
27 | * If the key has already been registered, it will be overwritten.
28 | */
29 | registerAlias(key: string, value: string): void;
30 |
31 | /**
32 | * Register an environment variable that will be available in all terminals.
33 | * If the key has already been registered, it will be overwritten.
34 | * A key with an undefined value will be deleted if already registered.
35 | */
36 | registerEnvironmentVariable(key: string, value: string | undefined): void;
37 |
38 | /**
39 | * Register an external command that will be available in all terminals.
40 | */
41 | registerExternalCommand(options: IExternalCommand.IOptions): void;
42 |
43 | /**
44 | * Signal emitted when a terminal is disposed.
45 | * The string argument is the terminal `name` which is the same as the Shell's `shellId`.
46 | */
47 | terminalDisposed: ISignal;
48 |
49 | /**
50 | * Inform all terminals that the theme has changed so that they can react to it if they wish.
51 | */
52 | themeChange(isDarkMode?: boolean): void;
53 | }
54 |
--------------------------------------------------------------------------------
/.github/workflows/update-integration-tests.yml:
--------------------------------------------------------------------------------
1 | name: Update Playwright Snapshots
2 |
3 | on:
4 | issue_comment:
5 | types: [created, edited]
6 |
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 |
11 | jobs:
12 | update-snapshots:
13 | if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots') }}
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: React to the triggering comment
18 | run: |
19 | gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions --raw-field 'content=+1'
20 | env:
21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 |
23 | - name: Checkout
24 | uses: actions/checkout@v6
25 | with:
26 | token: ${{ secrets.GITHUB_TOKEN }}
27 |
28 | - name: Setup Python
29 | uses: actions/setup-python@v6
30 | with:
31 | python-version: '3.14'
32 |
33 | - name: Checkout the branch from the PR that triggered the job
34 | run: gh pr checkout ${{ github.event.issue.number }}
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 |
38 | - name: Base Setup
39 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
40 |
41 | - name: Install dependencies
42 | run: python -m pip install -U "jupyterlab>=4,<5"
43 |
44 | - name: Install extension
45 | run: |
46 | set -eux
47 | jlpm
48 | python -m pip install .
49 |
50 | - name: Micromamba needed for cockle_wasm_env
51 | uses: mamba-org/setup-micromamba@main
52 |
53 | - name: Install dependencies
54 | working-directory: ui-tests
55 | run: |
56 | jlpm install
57 | jlpm build
58 |
59 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1
60 | with:
61 | github_token: ${{ secrets.GITHUB_TOKEN }}
62 | # Playwright knows how to start JupyterLab server
63 | start_server_script: 'null'
64 | test_folder: ui-tests
65 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 2: Publish Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | branch:
6 | description: "The target branch"
7 | required: false
8 | release_url:
9 | description: "The URL of the draft GitHub release"
10 | required: false
11 | steps_to_skip:
12 | description: "Comma separated list of steps to skip"
13 | required: false
14 |
15 | jobs:
16 | publish_release:
17 | runs-on: ubuntu-latest
18 | environment: release
19 | permissions:
20 | id-token: write
21 | steps:
22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
23 |
24 | - uses: actions/create-github-app-token@v2
25 | id: app-token
26 | with:
27 | app-id: ${{ vars.APP_ID }}
28 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
29 |
30 | - name: Populate Release
31 | id: populate-release
32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
33 | with:
34 | token: ${{ steps.app-token.outputs.token }}
35 | branch: ${{ github.event.inputs.branch }}
36 | release_url: ${{ github.event.inputs.release_url }}
37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }}
38 |
39 | - name: Finalize Release
40 | id: finalize-release
41 | env:
42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
44 | with:
45 | token: ${{ steps.app-token.outputs.token }}
46 | release_url: ${{ steps.populate-release.outputs.release_url }}
47 |
48 | - name: "** Next Step **"
49 | if: ${{ success() }}
50 | run: |
51 | echo "Verify the final release"
52 | echo ${{ steps.finalize-release.outputs.release_url }}
53 |
54 | - name: "** Failure Message **"
55 | if: ${{ failure() }}
56 | run: |
57 | echo "Failed to Publish the Draft Release Url:"
58 | echo ${{ steps.populate-release.outputs.release_url }}
59 |
--------------------------------------------------------------------------------
/webpack.worker.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build and bundle the shell web worker and overwrite the file generated by the default
3 | * labextension build process that does not build it as a web worker.
4 | */
5 | const fs = require('fs');
6 | const path = require('path');
7 |
8 | const rules = [
9 | {
10 | test: /\.js$/,
11 | exclude: /node_modules/,
12 | loader: 'source-map-loader'
13 | },
14 | {
15 | test: /\.ts$/,
16 | use: 'ts-loader',
17 | exclude: /node_modules/
18 | }
19 | ];
20 |
21 | const resolve = {
22 | fallback: {
23 | fs: false,
24 | child_process: false,
25 | crypto: false
26 | },
27 | extensions: ['.js', '.ts']
28 | };
29 |
30 | const labextensionStaticDir = path.resolve(
31 | __dirname,
32 | 'jupyterlite_terminal',
33 | 'labextension',
34 | 'static'
35 | );
36 |
37 | /**
38 | * Return the filename that the default labextension build process uses for the web worker.
39 | */
40 | function getOutputFilename(devMode) {
41 | const lsProd = fs
42 | .readdirSync(labextensionStaticDir)
43 | .filter(f => f.endsWith('js'));
44 | const regex = /extends (\w+)\.BaseShellWorker/;
45 |
46 | let outputFilename = '';
47 | for (file of lsProd) {
48 | const content = fs.readFileSync(path.join(labextensionStaticDir, file));
49 | if (regex.test(content)) {
50 | outputFilename = file;
51 | break;
52 | }
53 | }
54 |
55 | if (outputFilename == '') {
56 | console.error(
57 | 'ERROR: Failed to find JavaScript web worker file to replace'
58 | );
59 | process.exit(1);
60 | }
61 |
62 | console.log('JavaScript web worker file to replace:' + outputFilename);
63 | return outputFilename;
64 | }
65 |
66 | module.exports = env => {
67 | const devMode = env.dev ?? false;
68 |
69 | const config = {
70 | entry: './src/worker.ts',
71 | output: {
72 | filename: getOutputFilename(devMode),
73 | path: labextensionStaticDir
74 | },
75 | module: {
76 | rules
77 | },
78 | mode: devMode ? 'development' : 'production',
79 | target: 'webworker',
80 | resolve
81 | };
82 |
83 | if (devMode) {
84 | config.devtool = 'source-map';
85 | }
86 |
87 | return [config];
88 | };
89 |
--------------------------------------------------------------------------------
/jupyterlite_terminal/add_on.py:
--------------------------------------------------------------------------------
1 | """JupyterLite addon to manage obtaining Wasm packages when building deployment"""
2 | import os
3 | from pathlib import Path
4 | import subprocess
5 |
6 | from jupyterlite_core.addons.federated_extensions import FederatedExtensionAddon
7 | from jupyterlite_core.constants import (
8 | FEDERATED_EXTENSIONS,
9 | JUPYTERLITE_JSON,
10 | LAB_EXTENSIONS,
11 | SHARE_LABEXTENSIONS,
12 | UTF8,
13 | )
14 |
15 |
16 | class TerminalAddon(FederatedExtensionAddon):
17 | __all__ = ["post_build"]
18 |
19 | def __init__(self, *args, **kwargs):
20 | super().__init__(*args, **kwargs)
21 |
22 | def post_build(self, manager):
23 | lite_dir = manager.lite_dir
24 | output_dir = manager.output_dir
25 |
26 | cockleTool = Path("node_modules", "@jupyterlite", "cockle", "lib", "tools", "prepare_wasm.js")
27 | if not cockleTool.is_file():
28 | cockleTool = ".cockle_temp" / cockleTool
29 | cmd = ["npm", "install", "--no-save", "--prefix", cockleTool.parts[0], "@jupyterlite/cockle"]
30 | print("TerminalAddon:", " ".join(cmd))
31 | subprocess.run(cmd, check=True, cwd=lite_dir)
32 |
33 | assetDir = output_dir / "extensions" / "@jupyterlite" / "terminal" / "static" / "wasm"
34 |
35 | # Although cockle's prepare_wasm is perfectly capable of copying the wasm and associated
36 | # files to the asset directory, here we just get the list of required files and let the
37 | # add-on do the copying for consistency with other extensions.
38 | tempFilename = lite_dir / 'cockle-files.txt'
39 | cmd = ["node", str(cockleTool), "--list", str(tempFilename)]
40 | print("TerminalAddon:", " ".join(cmd))
41 | subprocess.run(cmd, check=True, cwd=lite_dir)
42 |
43 | with open(tempFilename, 'r') as f:
44 | for source in f:
45 | source = Path(source.strip())
46 | basename = source.name
47 | packageName = next(f).strip()
48 | yield dict(
49 | name=f"copy:{basename}",
50 | actions=[(self.copy_one, [lite_dir / source, assetDir / packageName / basename])],
51 | )
52 |
53 | os.remove(tempFilename)
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.bundle.*
2 | lib/
3 | node_modules/
4 | *.log
5 | .eslintcache
6 | .stylelintcache
7 | *.egg-info/
8 | .ipynb_checkpoints
9 | *.tsbuildinfo
10 | jupyterlite_terminal/labextension
11 | # Version file is handled by hatchling
12 | jupyterlite_terminal/_version.py
13 |
14 | # Integration tests
15 | ui-tests/test-results/
16 | ui-tests/playwright-report/
17 |
18 | # Created by https://www.gitignore.io/api/python
19 | # Edit at https://www.gitignore.io/?templates=python
20 |
21 | ### Python ###
22 | # Byte-compiled / optimized / DLL files
23 | __pycache__/
24 | *.py[cod]
25 | *$py.class
26 |
27 | # C extensions
28 | *.so
29 |
30 | # Distribution / packaging
31 | .Python
32 | build/
33 | develop-eggs/
34 | dist/
35 | downloads/
36 | eggs/
37 | .eggs/
38 | lib/
39 | lib64/
40 | parts/
41 | sdist/
42 | var/
43 | wheels/
44 | pip-wheel-metadata/
45 | share/python-wheels/
46 | .installed.cfg
47 | *.egg
48 | MANIFEST
49 |
50 | # PyInstaller
51 | # Usually these files are written by a python script from a template
52 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
53 | *.manifest
54 | *.spec
55 |
56 | # Installer logs
57 | pip-log.txt
58 | pip-delete-this-directory.txt
59 |
60 | # Unit test / coverage reports
61 | htmlcov/
62 | .tox/
63 | .nox/
64 | .coverage
65 | .coverage.*
66 | .cache
67 | nosetests.xml
68 | coverage/
69 | coverage.xml
70 | *.cover
71 | .hypothesis/
72 | .pytest_cache/
73 |
74 | # Translations
75 | *.mo
76 | *.pot
77 |
78 | # Scrapy stuff:
79 | .scrapy
80 |
81 | # Sphinx documentation
82 | docs/_build/
83 |
84 | # PyBuilder
85 | target/
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # celery beat schedule file
91 | celerybeat-schedule
92 |
93 | # SageMath parsed files
94 | *.sage.py
95 |
96 | # Spyder project settings
97 | .spyderproject
98 | .spyproject
99 |
100 | # Rope project settings
101 | .ropeproject
102 |
103 | # Mr Developer
104 | .mr.developer.cfg
105 | .project
106 | .pydevproject
107 |
108 | # mkdocs documentation
109 | /site
110 |
111 | # mypy
112 | .mypy_cache/
113 | .dmypy.json
114 | dmypy.json
115 |
116 | # Pyre type checker
117 | .pyre/
118 |
119 | # End of https://www.gitignore.io/api/python
120 |
121 | # OSX files
122 | .DS_Store
123 |
124 | # Yarn cache
125 | .yarn/
126 |
127 | .jupyterlite.doit.db
128 | _output/
129 | cockle_wasm_env/
130 | cockle-config.json
131 | .cockle_temp/
132 |
--------------------------------------------------------------------------------
/ui-tests/tests/lifetime.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jupyterlab/galata';
2 |
3 | import { TERMINAL_SELECTOR, inputLine } from './utils/misc';
4 |
5 | const OPEN_TERMINAL_1 =
6 | 'span.jp-RunningSessions-itemLabel:has-text("Terminal 1")';
7 | const TERMINALS_1 = 'text=terminals/1';
8 |
9 | test.describe('New', () => {
10 | test('should open via File menu', async ({ page }) => {
11 | await page.goto();
12 | await page.menu.clickMenuItem('File>New>Terminal');
13 | await page.locator(TERMINAL_SELECTOR).waitFor();
14 | });
15 |
16 | test('should appear in sidebar', async ({ page }) => {
17 | await page.goto();
18 | await page.menu.clickMenuItem('File>New>Terminal');
19 | await page.locator(TERMINAL_SELECTOR).waitFor();
20 | await page.sidebar.openTab('jp-running-sessions');
21 |
22 | await expect(page.locator(OPEN_TERMINAL_1)).toBeVisible();
23 | await expect(page.locator(TERMINALS_1)).toBeVisible();
24 | });
25 |
26 | test('should open via launcher', async ({ page }) => {
27 | await page.goto();
28 | await page
29 | .locator('.jp-LauncherCard-label >> p:has-text("Terminal")')
30 | .click();
31 | await page.locator(TERMINAL_SELECTOR).waitFor();
32 | });
33 | });
34 |
35 | test.describe('Shutdown', () => {
36 | test('should close via menu', async ({ page }) => {
37 | await page.goto();
38 | await page.menu.clickMenuItem('File>New>Terminal');
39 | await page.locator(TERMINAL_SELECTOR).waitFor();
40 | await page.sidebar.openTab('jp-running-sessions');
41 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
42 | await page.menu.clickMenuItem('File>Shutdown Terminal');
43 | await page.waitForTimeout(100);
44 |
45 | await expect(page.locator(OPEN_TERMINAL_1)).toHaveCount(0);
46 | await expect(page.locator(TERMINALS_1)).toHaveCount(0);
47 | });
48 |
49 | test('should close via exit command in terminal', async ({ page }) => {
50 | await page.goto();
51 | await page.menu.clickMenuItem('File>New>Terminal');
52 | await page.locator(TERMINAL_SELECTOR).waitFor();
53 | await page.sidebar.openTab('jp-running-sessions');
54 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
55 | await inputLine(page, 'exit');
56 | await page.waitForTimeout(100);
57 |
58 | await expect(page.locator(OPEN_TERMINAL_1)).toHaveCount(0);
59 | await expect(page.locator(TERMINALS_1)).toHaveCount(0);
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Making a new release of jupyterlite_terminal
2 |
3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser).
4 |
5 | ## Manual release
6 |
7 | ### Python package
8 |
9 | This extension can be distributed as Python packages. All of the Python
10 | packaging instructions are in the `pyproject.toml` file to wrap your extension in a
11 | Python package. Before generating a package, you first need to install some tools:
12 |
13 | ```bash
14 | pip install build twine hatch
15 | ```
16 |
17 | Bump the version using `hatch`. By default this will create a tag.
18 | See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details.
19 |
20 | ```bash
21 | hatch version
22 | ```
23 |
24 | Make sure to clean up all the development files before building the package:
25 |
26 | ```bash
27 | jlpm clean:all
28 | ```
29 |
30 | You could also clean up the local git repository:
31 |
32 | ```bash
33 | git clean -dfX
34 | ```
35 |
36 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do:
37 |
38 | ```bash
39 | python -m build
40 | ```
41 |
42 | > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package.
43 |
44 | Then to upload the package to PyPI, do:
45 |
46 | ```bash
47 | twine upload dist/*
48 | ```
49 |
50 | ### NPM package
51 |
52 | To publish the frontend part of the extension as a NPM package, do:
53 |
54 | ```bash
55 | npm login
56 | npm publish --access public
57 | ```
58 |
59 | ## Automated releases with the Jupyter Releaser
60 |
61 | The extension repository should already be compatible with the Jupyter Releaser. But
62 | the GitHub repository and the package managers need to be properly set up. Please
63 | follow the instructions of the Jupyter Releaser [checklist](https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_repo.html).
64 |
65 | Here is a summary of the steps to cut a new release:
66 |
67 | - Go to the Actions panel
68 | - Run the "Step 1: Prep Release" workflow
69 | - Check the draft changelog
70 | - Run the "Step 2: Publish Release" workflow
71 |
72 | > [!NOTE]
73 | > Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html)
74 | > for more information.
75 |
76 | ## Publishing to `conda-forge`
77 |
78 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html
79 |
80 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically.
81 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "jupyterlite_terminal"
7 | readme = "README.md"
8 | license = { file = "LICENSE" }
9 | requires-python = ">=3.10"
10 | classifiers = [
11 | "Framework :: Jupyter",
12 | "Framework :: Jupyter :: JupyterLab",
13 | "Framework :: Jupyter :: JupyterLab :: 4",
14 | "Framework :: Jupyter :: JupyterLab :: Extensions",
15 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",
16 | "License :: OSI Approved :: BSD License",
17 | "Programming Language :: Python",
18 | "Programming Language :: Python :: 3",
19 | "Programming Language :: Python :: 3.10",
20 | "Programming Language :: Python :: 3.11",
21 | "Programming Language :: Python :: 3.12",
22 | "Programming Language :: Python :: 3.13",
23 | "Programming Language :: Python :: 3.14",
24 | ]
25 | dependencies = [
26 | # Changes here should be synchronised with .github/workflows/build.yml
27 | "jupyterlite-core>=0.7.0,<0.8.0"
28 | ]
29 | dynamic = ["version", "description", "authors", "urls", "keywords"]
30 |
31 | [project.entry-points."jupyterlite.addon.v0"]
32 | jupyterlite-terminal = "jupyterlite_terminal.add_on:TerminalAddon"
33 |
34 | [tool.hatch.version]
35 | source = "nodejs"
36 |
37 | [tool.hatch.metadata.hooks.nodejs]
38 | fields = ["description", "authors", "urls"]
39 |
40 | [tool.hatch.build.targets.sdist]
41 | artifacts = ["jupyterlite_terminal/labextension"]
42 | exclude = [".github", "binder"]
43 |
44 | [tool.hatch.build.targets.wheel.shared-data]
45 | "jupyterlite_terminal/labextension" = "share/jupyter/labextensions/jupyterlite-terminal"
46 | "install.json" = "share/jupyter/labextensions/jupyterlite-terminal/install.json"
47 |
48 | [tool.hatch.build.hooks.version]
49 | path = "jupyterlite_terminal/_version.py"
50 |
51 | [tool.hatch.build.hooks.jupyter-builder]
52 | dependencies = ["hatch-jupyter-builder>=0.5"]
53 | build-function = "hatch_jupyter_builder.npm_builder"
54 | ensured-targets = [
55 | "jupyterlite_terminal/labextension/static/style.js",
56 | "jupyterlite_terminal/labextension/package.json",
57 | ]
58 | skip-if-exists = ["jupyterlite_terminal/labextension/static/style.js"]
59 |
60 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs]
61 | build_cmd = "build:prod"
62 | npm = ["jlpm"]
63 |
64 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs]
65 | build_cmd = "install:extension"
66 | npm = ["jlpm"]
67 | source_dir = "src"
68 | build_dir = "jupyterlite_terminal/labextension"
69 |
70 | [tool.jupyter-releaser.options]
71 | version_cmd = "hatch version"
72 |
73 | [tool.jupyter-releaser.hooks]
74 | before-build-npm = [
75 | "python -m pip install 'jupyterlab>=4.0.0,<5'",
76 | "jlpm",
77 | "jlpm build:prod"
78 | ]
79 | before-build-python = ["jlpm clean:all"]
80 |
81 | [tool.check-wheel-contents]
82 | ignore = ["W002"]
83 |
--------------------------------------------------------------------------------
/docs/_static/terminal_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
64 |
--------------------------------------------------------------------------------
/ui-tests/tests/fs.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jupyterlab/galata';
2 |
3 | import { ContentsHelper } from './utils/contents';
4 | import {
5 | LONG_WAIT_MS,
6 | TERMINAL_SELECTOR,
7 | WAIT_MS,
8 | decode64,
9 | inputLine
10 | } from './utils/misc';
11 |
12 | const MONTHS_TXT =
13 | 'January\nFebruary\nMarch\nApril\nMay\nJune\nJuly\nAugust\nSeptember\nOctober\nNovember\nDecember\n';
14 | const FACT_LUA =
15 | 'function fact(n, acc)\n' +
16 | ' acc = acc or 1\n' +
17 | ' if n == 0 then\n' +
18 | ' return acc\n' +
19 | ' end\n' +
20 | ' return fact(n-1, n*acc)\n' +
21 | 'end\n' +
22 | 'print(fact(tonumber(arg[1])))\n';
23 |
24 | test.describe('Filesystem', () => {
25 | test.beforeEach(async ({ page }) => {
26 | await page.goto();
27 | await page.waitForTimeout(LONG_WAIT_MS);
28 |
29 | // Overwrite the (read-only) page.contents with our own ContentsHelper.
30 | // @ts-ignore
31 | page.contents = new ContentsHelper(page);
32 |
33 | await page.menu.clickMenuItem('File>New>Terminal');
34 | await page.locator(TERMINAL_SELECTOR).waitFor();
35 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
36 | await page.waitForTimeout(WAIT_MS);
37 | });
38 |
39 | test('should have initial files', async ({ page }) => {
40 | // Directory contents.
41 | const content = await page.contents.getContentMetadata('', 'directory');
42 | expect(content).not.toBeNull();
43 | const filenames = content?.content.map(item => item.name);
44 | expect(filenames).toEqual(
45 | expect.arrayContaining(['fact.lua', 'months.txt'])
46 | );
47 |
48 | // File contents.
49 | const months = await page.contents.getContentMetadata('months.txt');
50 | expect(months?.content).toEqual(MONTHS_TXT);
51 |
52 | // Note fact.lua contents are returned base64 encoded.
53 | const fact = await page.contents.getContentMetadata('fact.lua');
54 | expect(decode64(fact?.content)).toEqual(FACT_LUA);
55 | });
56 |
57 | test('should create a new file', async ({ page }) => {
58 | await page.goto();
59 | await page.waitForTimeout(LONG_WAIT_MS);
60 | await page.menu.clickMenuItem('File>New>Terminal');
61 | await page.locator(TERMINAL_SELECTOR).waitFor();
62 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
63 | await page.waitForTimeout(LONG_WAIT_MS);
64 |
65 | await inputLine(page, 'echo Hello > out.txt');
66 | await page.getByTitle('Name: out.txt').waitFor();
67 | });
68 |
69 | test('should support cp', async ({ page }) => {
70 | await inputLine(page, 'cp months.txt other.txt');
71 | await page.waitForTimeout(WAIT_MS);
72 | await page.filebrowser.refresh();
73 |
74 | expect(await page.contents.fileExists('months.txt')).toBeTruthy();
75 | expect(await page.contents.fileExists('other.txt')).toBeTruthy();
76 |
77 | const other = await page.contents.getContentMetadata('other.txt');
78 | expect(other?.content).toEqual(MONTHS_TXT);
79 | });
80 |
81 | test('should support touch', async ({ page }) => {
82 | await inputLine(page, 'touch touched.txt');
83 | await page.waitForTimeout(WAIT_MS);
84 | await page.filebrowser.refresh();
85 |
86 | expect(await page.contents.fileExists('touched.txt')).toBeTruthy();
87 |
88 | const other = await page.contents.getContentMetadata('touched.txt');
89 | expect(other?.content).toEqual('');
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/ui-tests/README.md:
--------------------------------------------------------------------------------
1 | # Integration Testing
2 |
3 | This folder contains the integration tests of the extension.
4 |
5 | They are defined using [Playwright](https://playwright.dev/docs/intro) test runner
6 | and [Galata](https://github.com/jupyterlab/jupyterlab/tree/main/galata) helper.
7 |
8 | The Playwright configuration is defined in [playwright.config.js](./playwright.config.js).
9 |
10 | The default configuration will produce video for failing tests and an HTML report.
11 |
12 | > There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0).
13 |
14 | ## Run the tests
15 |
16 | > All commands are assumed to be executed from the root directory
17 |
18 | To run the tests, you need to:
19 |
20 | 1. Compile the extension:
21 |
22 | ```sh
23 | jlpm install
24 | jlpm build:prod
25 | ```
26 |
27 | > Check the extension is installed in JupyterLab.
28 |
29 | 2. Install test dependencies (needed only once):
30 |
31 | ```sh
32 | cd ./ui-tests
33 | jlpm install
34 | jlpm build
35 | jlpm playwright install
36 | cd ..
37 | ```
38 |
39 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) tests:
40 |
41 | ```sh
42 | cd ./ui-tests
43 | jlpm test
44 | ```
45 |
46 | Test results will be shown in the terminal. In case of any test failures, the test report
47 | will be opened in your browser at the end of the tests execution; see
48 | [Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter)
49 | for configuring that behavior.
50 |
51 | ## Update the tests snapshots
52 |
53 | > All commands are assumed to be executed from the root directory
54 |
55 | If you are comparing snapshots to validate your tests, you may need to update
56 | the reference snapshots stored in the repository. To do that, you need to:
57 |
58 | 1. Compile the extension:
59 |
60 | ```sh
61 | jlpm install
62 | jlpm build:prod
63 | ```
64 |
65 | > Check the extension is installed in JupyterLab.
66 |
67 | 2. Install test dependencies (needed only once):
68 |
69 | ```sh
70 | cd ./ui-tests
71 | jlpm install
72 | jlpm build
73 | jlpm playwright install
74 | cd ..
75 | ```
76 |
77 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) command:
78 |
79 | ```sh
80 | cd ./ui-tests
81 | jlpm test -u
82 | ```
83 |
84 | > Some discrepancy may occurs between the snapshots generated on your computer and
85 | > the one generated on the CI. To ease updating the snapshots on a PR, you can
86 | > type `please update playwright snapshots` to trigger the update by a bot on the CI.
87 | > Once the bot has computed new snapshots, it will commit them to the PR branch.
88 |
89 | ## Create tests
90 |
91 | > All commands are assumed to be executed from the root directory
92 |
93 | To create tests, the easiest way is to use the code generator tool of playwright:
94 |
95 | 1. Compile the extension:
96 |
97 | ```sh
98 | jlpm install
99 | jlpm build:prod
100 | ```
101 |
102 | > Check the extension is installed in JupyterLab.
103 |
104 | 2. Install test dependencies (needed only once):
105 |
106 | ```sh
107 | cd ./ui-tests
108 | jlpm install
109 | jlpm build
110 | jlpm playwright install
111 | cd ..
112 | ```
113 |
114 | 3. Start the server:
115 |
116 | ```sh
117 | cd ./ui-tests
118 | jlpm start
119 | ```
120 |
121 | 4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**:
122 |
123 | ```sh
124 | cd ./ui-tests
125 | jlpm playwright codegen localhost:8888
126 | ```
127 |
128 | ## Debug tests
129 |
130 | > All commands are assumed to be executed from the root directory
131 |
132 | To debug tests, a good way is to use the inspector tool of playwright:
133 |
134 | 1. Compile the extension:
135 |
136 | ```sh
137 | jlpm install
138 | jlpm build:prod
139 | ```
140 |
141 | > Check the extension is installed in JupyterLab.
142 |
143 | 2. Install test dependencies (needed only once):
144 |
145 | ```sh
146 | cd ./ui-tests
147 | jlpm install
148 | jlpm build
149 | jlpm playwright install
150 | cd ..
151 | ```
152 |
153 | 3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug):
154 |
155 | ```sh
156 | cd ./ui-tests
157 | jlpm test --debug
158 | ```
159 |
160 | ## Upgrade Playwright and the browsers
161 |
162 | To update the web browser versions, you must update the package `@playwright/test`:
163 |
164 | ```sh
165 | cd ./ui-tests
166 | jlpm up "@playwright/test"
167 | jlpm playwright install
168 | ```
169 |
--------------------------------------------------------------------------------
/ui-tests/tests/command.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jupyterlab/galata';
2 |
3 | import { ContentsHelper } from './utils/contents';
4 | import {
5 | LONG_WAIT_MS,
6 | TERMINAL_SELECTOR,
7 | WAIT_MS,
8 | inputLine
9 | } from './utils/misc';
10 |
11 | test.describe('individual command', () => {
12 | test.beforeEach(async ({ page }) => {
13 | await page.goto();
14 | await page.waitForTimeout(LONG_WAIT_MS);
15 |
16 | // Overwrite the (read-only) page.contents with our own ContentsHelper.
17 | // @ts-ignore
18 | page.contents = new ContentsHelper(page);
19 |
20 | await page.menu.clickMenuItem('File>New>Terminal');
21 | await page.locator(TERMINAL_SELECTOR).waitFor();
22 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
23 | await page.waitForTimeout(LONG_WAIT_MS);
24 | });
25 |
26 | test.describe('nano', () => {
27 | const stdinOptions = ['sab', 'sw'];
28 | stdinOptions.forEach(stdinOption => {
29 | test(`should create new file using ${stdinOption} for stdin`, async ({
30 | page
31 | }) => {
32 | await inputLine(page, `cockle-config stdin ${stdinOption}`);
33 | await page.waitForTimeout(LONG_WAIT_MS);
34 |
35 | await inputLine(page, 'nano a.txt');
36 | await page.waitForTimeout(LONG_WAIT_MS);
37 |
38 | // Insert new characters.
39 | await inputLine(page, 'mnopqrst', false);
40 |
41 | // Save and quit.
42 | await page.keyboard.press('Control+x');
43 | await inputLine(page, 'y');
44 | await page.waitForTimeout(LONG_WAIT_MS);
45 |
46 | const outputFile = await page.contents.getContentMetadata('a.txt');
47 | expect(outputFile?.content).toEqual('mnopqrst\n');
48 | });
49 |
50 | test(`should delete data from file using ${stdinOption} for stdin`, async ({
51 | page
52 | }) => {
53 | await inputLine(page, `cockle-config stdin ${stdinOption}`);
54 | await page.waitForTimeout(LONG_WAIT_MS);
55 |
56 | // Prepare file to delete from.
57 | await inputLine(page, 'echo mnopqrst > b.txt');
58 | await page.waitForTimeout(LONG_WAIT_MS);
59 |
60 | await inputLine(page, 'nano b.txt');
61 | await page.waitForTimeout(LONG_WAIT_MS);
62 |
63 | // Delete first 4 characters.
64 | for (let i = 0; i < 4; i++) {
65 | await page.keyboard.press('Delete');
66 | }
67 |
68 | // Save and quit.
69 | await page.keyboard.press('Control+x');
70 | await inputLine(page, 'y');
71 | await page.waitForTimeout(LONG_WAIT_MS);
72 |
73 | const outputFile = await page.contents.getContentMetadata('b.txt');
74 | expect(outputFile?.content).toEqual('qrst\n');
75 | });
76 | });
77 | });
78 |
79 | test.describe('vim', () => {
80 | const stdinOptions = ['sab', 'sw'];
81 | stdinOptions.forEach(stdinOption => {
82 | test(`should create new file using ${stdinOption} for stdin`, async ({
83 | page
84 | }) => {
85 | await inputLine(page, `cockle-config stdin ${stdinOption}`);
86 | await page.waitForTimeout(LONG_WAIT_MS);
87 |
88 | await inputLine(page, 'vim c.txt');
89 | await page.waitForTimeout(LONG_WAIT_MS);
90 |
91 | // Insert new characters.
92 | await inputLine(page, 'iabcdefgh', false);
93 |
94 | // Save and quit.
95 | await page.keyboard.press('Escape');
96 | await inputLine(page, ':wq');
97 | await page.waitForTimeout(LONG_WAIT_MS);
98 |
99 | const outputFile = await page.contents.getContentMetadata('c.txt');
100 | expect(outputFile?.content).toEqual('abcdefgh\n');
101 | });
102 |
103 | test(`should delete data from file using ${stdinOption} for stdin`, async ({
104 | page
105 | }) => {
106 | await inputLine(page, `cockle-config stdin ${stdinOption}`);
107 | await page.waitForTimeout(LONG_WAIT_MS);
108 |
109 | // Prepare file to delete from.
110 | await inputLine(page, 'echo abcdefgh > d.txt');
111 | await page.waitForTimeout(LONG_WAIT_MS);
112 |
113 | await inputLine(page, 'vim d.txt');
114 | await page.waitForTimeout(LONG_WAIT_MS);
115 |
116 | // Delete first 4 characters.
117 | await inputLine(page, 'd4l', false);
118 |
119 | // Save and quit.
120 | await page.keyboard.press('Escape');
121 | await inputLine(page, ':wq');
122 | await page.waitForTimeout(LONG_WAIT_MS);
123 |
124 | const outputFile = await page.contents.getContentMetadata('d.txt');
125 | expect(outputFile?.content).toEqual('efgh\n');
126 | });
127 | });
128 | });
129 | });
130 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JupyterLite Terminal
2 |
3 | [](https://github.com/jupyterlite/terminal/actions/workflows/build.yml)
4 | [](https://jupyterlite.github.io/terminal/)
5 |
6 | A terminal for JupyterLite.
7 |
8 | ⚠️ This extension is still in development and not yet ready for general use. ⚠️
9 |
10 | 
11 |
12 | ## Requirements
13 |
14 | - JupyterLite >= 0.7.0, < 0.8.0
15 |
16 | ## Install
17 |
18 | To install the extension, execute:
19 |
20 | ```bash
21 | pip install jupyterlite-terminal
22 | ```
23 |
24 | You will also need to install the JupyterLite CLI:
25 |
26 | ```bash
27 | python -m pip install jupyterlite-core
28 | ```
29 |
30 | ## Usage
31 |
32 | After installing `jupyterlite-core` and `jupyterlite-terminal`, create a `jupyter-lite.json` file with the following content to activate the terminal extension:
33 |
34 | ```json
35 | {
36 | "jupyter-lite-schema-version": 0,
37 | "jupyter-config-data": {
38 | "terminalsAvailable": true
39 | }
40 | }
41 | ```
42 |
43 | Then build a new JupyterLite site:
44 |
45 | ```bash
46 | jupyter lite build
47 | ```
48 |
49 | ## Version compatibility
50 |
51 | Each `jupyterlite-terminal` release is built against a specific version of `cockle`. If you need to
52 | include imports from both `jupyterlite-terminal` and `cockle`, such as if you are implementing
53 | `cockle` external commands, you should ensure that you are using the correct version combination.
54 |
55 | | `jupyterlite-terminal` | `cockle` | `jupyterlite-core` | Release date |
56 | | ---------------------- | -------- | ------------------ | ------------ |
57 | | 1.2.0 | 1.3.0 | >= 0.7, < 0.8 | 2025-12-03 |
58 | | 1.1.0 | 1.2.0 | >= 0.6, < 0.8 | 2025-10-27 |
59 | | 1.0.1 | 1.0.0 | >= 0.6, < 0.8 | 2025-09-03 |
60 | | 1.0.0 | 1.0.0 | >= 0.6, < 0.7 | 2025-08-11 |
61 | | 0.2.2 | 0.1.3 | >= 0.6, < 0.7 | 2025-06-27 |
62 |
63 | ## Contributing
64 |
65 | ### Development install
66 |
67 | Note: You will need NodeJS to build the extension package.
68 |
69 | The `jlpm` command is JupyterLab's pinned version of
70 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
71 | `yarn` or `npm` in lieu of `jlpm` below.
72 |
73 | ```bash
74 | # Clone the repo to your local environment
75 | # Change directory to the jupyterlite_terminal directory
76 | # Install package in development mode
77 | pip install -e "."
78 | # Link your development version of the extension with JupyterLab
79 | jupyter labextension develop . --overwrite
80 | # Rebuild extension Typescript source after making changes
81 | jlpm build
82 | ```
83 |
84 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
85 |
86 | ```bash
87 | # Watch the source directory in one terminal, automatically rebuilding when needed
88 | jlpm watch
89 | # Run JupyterLab in another terminal
90 | jupyter lab
91 | ```
92 |
93 | Then build a JupyterLite distribution with the extension installed:
94 |
95 | ```bash
96 | cd deploy
97 | jupyter lite build --contents contents
98 | ```
99 |
100 | And serve it either using:
101 |
102 | ```bash
103 | npx static-handler _output/
104 | ```
105 |
106 | or:
107 |
108 | ```bash
109 | jupyter lite serve
110 | ```
111 |
112 | To enable use of SharedArrayBuffer rather than ServiceWorker for `stdin` you will have to configure your server to add the `Cross-Origin-Embedder-Policy` and `Cross-Origin-Opener-Policy` headers. Do this using either:
113 |
114 | ```bash
115 | npx static-handler --cors --coop --coep --corp _output/
116 | ```
117 |
118 | or:
119 |
120 | ```bash
121 | jupyter lite serve --LiteBuildConfig.extra_http_headers=Cross-Origin-Embedder-Policy=require-corp --LiteBuildConfig.extra_http_headers=Cross-Origin-Opener-Policy=same-origin
122 | ```
123 |
124 | ### Building the documentation
125 |
126 | The project documentation includes a demo deployment, and is built on every PR so that the changes can be checked manually before merging. To build the documentation and demo locally use:
127 |
128 | ```bash
129 | micromamba create -f docs/environment-docs.yml
130 | micromamba activate terminal-docs
131 | pip install -v .
132 | cd docs
133 | make html
134 | ```
135 |
136 | To serve this locally use:
137 |
138 | ```bash
139 | cd _build/html
140 | python -m http.server
141 | ```
142 |
143 | ### Packaging the extension
144 |
145 | See [RELEASE](RELEASE.md)
146 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import type {
5 | JupyterFrontEnd,
6 | JupyterFrontEndPlugin
7 | } from '@jupyterlab/application';
8 | import { IThemeManager } from '@jupyterlab/apputils';
9 | import type { ServiceManagerPlugin, Terminal } from '@jupyterlab/services';
10 | import {
11 | IServerSettings,
12 | ITerminalManager,
13 | ServerConnection,
14 | TerminalManager
15 | } from '@jupyterlab/services';
16 | import { IServiceWorkerManager } from '@jupyterlite/apputils';
17 | import { ISettingRegistry } from '@jupyterlab/settingregistry';
18 |
19 | import { WebSocket } from 'mock-socket';
20 |
21 | import { LiteTerminalAPIClient } from './client';
22 | import { ILiteTerminalAPIClient } from './tokens';
23 |
24 | /**
25 | * Plugin containing client for in-browser terminals.
26 | */
27 | const terminalClientPlugin: ServiceManagerPlugin =
28 | {
29 | id: '@jupyterlite/terminal:client',
30 | description: 'The client for Lite terminals',
31 | autoStart: true,
32 | provides: ILiteTerminalAPIClient,
33 | optional: [IServerSettings],
34 | activate: (
35 | _: null,
36 | serverSettings?: ServerConnection.ISettings
37 | ): ILiteTerminalAPIClient => {
38 | return new LiteTerminalAPIClient({
39 | serverSettings: {
40 | ...ServerConnection.makeSettings(),
41 | ...serverSettings,
42 | WebSocket
43 | }
44 | });
45 | }
46 | };
47 |
48 | /**
49 | * Plugin containing manager for in-browser terminals.
50 | */
51 | const terminalManagerPlugin: ServiceManagerPlugin = {
52 | id: '@jupyterlite/terminal:manager',
53 | description: 'A JupyterLite extension providing a custom terminal manager',
54 | autoStart: true,
55 | provides: ITerminalManager,
56 | requires: [ILiteTerminalAPIClient],
57 | activate: (
58 | _: null,
59 | terminalAPIClient: Terminal.ITerminalAPIClient
60 | ): Terminal.IManager => {
61 | console.log(
62 | 'JupyterLite extension @jupyterlite/terminal:manager activated'
63 | );
64 | return new TerminalManager({
65 | terminalAPIClient,
66 | serverSettings: terminalAPIClient.serverSettings
67 | });
68 | }
69 | };
70 |
71 | /**
72 | * Plugin that connects in-browser terminals and service worker.
73 | */
74 | const terminalServiceWorkerPlugin: JupyterFrontEndPlugin = {
75 | id: '@jupyterlite/terminal:service-worker',
76 | autoStart: true,
77 | requires: [ILiteTerminalAPIClient],
78 | optional: [IServiceWorkerManager],
79 | activate: (
80 | _: JupyterFrontEnd,
81 | liteTerminalAPIClient: ILiteTerminalAPIClient,
82 | serviceWorkerManager?: IServiceWorkerManager
83 | ): void => {
84 | if (serviceWorkerManager !== undefined) {
85 | liteTerminalAPIClient.browsingContextId =
86 | serviceWorkerManager.browsingContextId;
87 |
88 | serviceWorkerManager.registerStdinHandler(
89 | 'terminal',
90 | liteTerminalAPIClient.handleStdin.bind(liteTerminalAPIClient)
91 | );
92 | } else {
93 | console.warn('Service worker is not available for terminals');
94 | }
95 | }
96 | };
97 |
98 | const terminalThemeChangePlugin: JupyterFrontEndPlugin = {
99 | id: '@jupyterlite/terminal:theme-change',
100 | autoStart: true,
101 | requires: [ILiteTerminalAPIClient, ISettingRegistry],
102 | optional: [IThemeManager],
103 | activate: (
104 | _: JupyterFrontEnd,
105 | liteTerminalAPIClient: ILiteTerminalAPIClient,
106 | settingRegistry: ISettingRegistry,
107 | themeManager?: IThemeManager
108 | ): void => {
109 | // Cache latest terminal theme so can identify if it has changed.
110 | let terminalTheme: string | undefined;
111 |
112 | themeManager?.themeChanged.connect(async (_, changedArgs) => {
113 | // An overall Lab theme change only affects terminals if the terminaTheme is 'inherit'.
114 | if (terminalTheme === 'inherit') {
115 | const isDarkMode = !themeManager.isLight(changedArgs.newValue);
116 | liteTerminalAPIClient.themeChange(isDarkMode);
117 | }
118 | });
119 |
120 | // There is no signal for a terminal theme change, so use settings change.
121 | settingRegistry
122 | .load('@jupyterlab/terminal-extension:plugin')
123 | .then(setting => {
124 | terminalTheme = setting.composite.theme as string;
125 |
126 | setting.changed.connect(() => {
127 | // This signal is fired for any change to the terminal settings, not just the theme.
128 | // Hence compare with the cached terminalTheme to identify if it has changed.
129 | const newTerminalTheme = setting.composite.theme as string;
130 | if (newTerminalTheme !== terminalTheme) {
131 | liteTerminalAPIClient.themeChange();
132 | terminalTheme = newTerminalTheme;
133 | }
134 | });
135 | });
136 | }
137 | };
138 |
139 | export default [
140 | terminalClientPlugin,
141 | terminalManagerPlugin,
142 | terminalServiceWorkerPlugin,
143 | terminalThemeChangePlugin
144 | ];
145 |
146 | // Export ILiteTerminalAPIClient so that other extensions can register external commands.
147 | export { ILiteTerminalAPIClient };
148 |
--------------------------------------------------------------------------------
/ui-tests/tests/jupyterlite_terminal.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jupyterlab/galata';
2 |
3 | import {
4 | LONG_WAIT_MS,
5 | TERMINAL_SELECTOR,
6 | WAIT_MS,
7 | inputLine
8 | } from './utils/misc';
9 |
10 | test.describe('Terminal', () => {
11 | test('should emit service worker console message', async ({ page }) => {
12 | const logs: string[] = [];
13 | page.on('console', message => {
14 | logs.push(message.text());
15 | });
16 |
17 | await page.goto();
18 | await page.waitForTimeout(LONG_WAIT_MS);
19 | await page.menu.clickMenuItem('File>New>Terminal');
20 | await page.locator(TERMINAL_SELECTOR).waitFor();
21 | await page.waitForTimeout(LONG_WAIT_MS);
22 |
23 | expect(
24 | logs.filter(s => s.match(/^Service worker supports terminal stdin/))
25 | ).toHaveLength(1);
26 | });
27 |
28 | test('should show initial prompt', async ({ page }) => {
29 | await page.goto();
30 | await page.waitForTimeout(LONG_WAIT_MS);
31 | await page.menu.clickMenuItem('File>New>Terminal');
32 | await page.locator(TERMINAL_SELECTOR).waitFor();
33 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
34 | await page.waitForTimeout(LONG_WAIT_MS);
35 |
36 | // Hide modification times.
37 | const modified = page.locator('span.jp-DirListing-itemModified');
38 | await modified.evaluateAll(els => els.map(el => (el.innerHTML = '')));
39 |
40 | const imageName = 'initial.png';
41 | expect(await page.screenshot()).toMatchSnapshot('initial.png');
42 | });
43 |
44 | test('should run various commands', async ({ page }) => {
45 | await page.goto();
46 | await page.waitForTimeout(LONG_WAIT_MS);
47 | await page.menu.clickMenuItem('File>New>Terminal');
48 | await page.locator(TERMINAL_SELECTOR).waitFor();
49 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
50 | await page.waitForTimeout(LONG_WAIT_MS);
51 |
52 | await inputLine(page, 'ls'); // avoid timestamps
53 | await page.waitForTimeout(WAIT_MS);
54 |
55 | await inputLine(page, 'cp months.txt other.txt');
56 | await page.waitForTimeout(WAIT_MS);
57 |
58 | await inputLine(page, 'ls'); // avoid timestamps
59 | await page.waitForTimeout(WAIT_MS);
60 |
61 | await inputLine(page, 'una\t'); // tab complete command name
62 | await page.waitForTimeout(WAIT_MS);
63 |
64 | await inputLine(page, 'grep ember mon\t'); // tab complete filename
65 | await page.waitForTimeout(WAIT_MS);
66 |
67 | await page.keyboard.press('Tab'); // list all commands
68 | await page.waitForTimeout(WAIT_MS);
69 |
70 | await inputLine(page, 'abc'); // no such command
71 | await page.waitForTimeout(WAIT_MS);
72 |
73 | // Hide modification times.
74 | const modified = page.locator('span.jp-DirListing-itemModified');
75 | await modified.evaluateAll(els => els.map(el => (el.innerHTML = '')));
76 |
77 | const imageName = 'various-commands.png';
78 | expect(await page.screenshot()).toMatchSnapshot('various-commands.png');
79 | });
80 |
81 | test('should support both SharedArrayBuffer and ServiceWorker for stdin', async ({
82 | page
83 | }) => {
84 | await page.goto();
85 | await page.waitForTimeout(LONG_WAIT_MS);
86 | await page.menu.clickMenuItem('File>New>Terminal');
87 | await page.locator(TERMINAL_SELECTOR).waitFor();
88 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
89 | await page.waitForTimeout(LONG_WAIT_MS);
90 |
91 | await inputLine(page, 'cockle-config stdin');
92 | await page.waitForTimeout(WAIT_MS);
93 |
94 | const term = page.locator('div.xterm-viewport');
95 | expect(await term.screenshot()).toMatchSnapshot('both-sab-and-sw.png');
96 | });
97 |
98 | test('should support setting ServiceWorker for stdin', async ({ page }) => {
99 | await page.goto();
100 | await page.waitForTimeout(LONG_WAIT_MS);
101 | await page.menu.clickMenuItem('File>New>Terminal');
102 | await page.locator(TERMINAL_SELECTOR).waitFor();
103 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
104 | await page.waitForTimeout(LONG_WAIT_MS);
105 |
106 | await inputLine(page, 'cockle-config stdin sw');
107 | await page.waitForTimeout(WAIT_MS);
108 |
109 | const term = page.locator('div.xterm-viewport');
110 | expect(await term.screenshot()).toMatchSnapshot('set-sw-stdin.png');
111 | });
112 |
113 | const stdinOptions = ['sab', 'sw'];
114 | stdinOptions.forEach(stdinOption => {
115 | test(`should support using ${stdinOption} for stdin`, async ({ page }) => {
116 | await page.goto();
117 | await page.waitForTimeout(LONG_WAIT_MS);
118 | await page.menu.clickMenuItem('File>New>Terminal');
119 | await page.locator(TERMINAL_SELECTOR).waitFor();
120 | await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
121 | await page.waitForTimeout(LONG_WAIT_MS);
122 |
123 | await inputLine(page, `cockle-config stdin ${stdinOption}`);
124 | await page.waitForTimeout(WAIT_MS);
125 |
126 | // Start interactive grep command.
127 | await inputLine(page, 'grep o');
128 | await page.waitForTimeout(WAIT_MS);
129 |
130 | await inputLine(page, 'abcod');
131 | await page.waitForTimeout(WAIT_MS);
132 | await inputLine(page, 'def');
133 | await page.waitForTimeout(WAIT_MS);
134 | await inputLine(page, 'oogoo');
135 | await page.waitForTimeout(WAIT_MS);
136 |
137 | // Finish interactive grep command.
138 | await page.keyboard.press('Control+d');
139 | await page.waitForTimeout(WAIT_MS);
140 |
141 | const term = page.locator('div.xterm-viewport');
142 | expect(await term.screenshot()).toMatchSnapshot(
143 | `stdin-${stdinOption}.png`
144 | );
145 | });
146 | });
147 | });
148 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | push:
5 | branches: main
6 | pull_request:
7 | branches: '*'
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11 | cancel-in-progress: true
12 |
13 | env:
14 | # Min and max limits of jupyterlite-core versions that this should work on.
15 | # Changes here should be synchronised with pyproject.toml dependencies section.
16 | MIN_LITE_VERSION: jupyterlite-core==0.7.0
17 | MAX_LITE_VERSION: --pre jupyterlite-core<0.8.0
18 |
19 | LAB_VERSION: jupyterlab>=4.0.0,<5
20 |
21 | jobs:
22 | build:
23 | name: Build and upload wheel
24 | runs-on: ubuntu-latest
25 |
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v6
29 |
30 | - name: Setup Python
31 | uses: actions/setup-python@v6
32 | with:
33 | python-version: '3.14'
34 |
35 | - name: Base Setup
36 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
37 |
38 | - name: Install dependencies
39 | run: python -m pip install -U ${LAB_VERSION}
40 |
41 | - name: Lint the extension
42 | run: |
43 | set -eux
44 | jlpm
45 | jlpm run lint:check
46 |
47 | - name: Test the extension
48 | run: |
49 | set -eux
50 | jlpm run test
51 |
52 | - name: Build the extension
53 | run: |
54 | set -eux
55 | python -m pip install .[test]
56 |
57 | jupyter labextension list
58 | jupyter labextension list 2>&1 | grep -ie "@jupyterlite/terminal.*OK"
59 |
60 | - name: List installed packages
61 | run: |
62 | python -m pip list
63 |
64 | - name: Package the extension
65 | run: |
66 | set -eux
67 |
68 | pip install build
69 | python -m build
70 | pip uninstall -y "jupyterlite_terminal" jupyterlab
71 |
72 | - name: Upload extension packages
73 | uses: actions/upload-artifact@v6
74 | with:
75 | name: extension-artifacts
76 | path: dist/jupyterlite_terminal*
77 | if-no-files-found: error
78 |
79 | test_isolated:
80 | name: Test isolated
81 | needs: build
82 | runs-on: ubuntu-latest
83 |
84 | steps:
85 | - name: Setup Python
86 | uses: actions/setup-python@v6
87 | with:
88 | python-version: '3.14'
89 |
90 | - uses: actions/download-artifact@v7
91 | with:
92 | name: extension-artifacts
93 |
94 | - name: Install and Test
95 | run: |
96 | set -eux
97 | # Remove NodeJS, twice to take care of system and locally installed node versions.
98 | sudo rm -rf $(which node)
99 | sudo rm -rf $(which node)
100 |
101 | pip install ${LAB_VERSION} jupyterlite_terminal*.whl
102 |
103 | jupyter labextension list
104 | jupyter labextension list 2>&1 | grep -ie "@jupyterlite/terminal.*OK"
105 |
106 | integration-tests:
107 | name: Integration tests ${{ matrix.python-version}} ${{ matrix.lite-version}}
108 | needs: build
109 | runs-on: ubuntu-latest
110 |
111 | strategy:
112 | fail-fast: false
113 | matrix:
114 | lite-version: ['min', 'normal', 'max']
115 | python-version: ['3.10', '3.14']
116 |
117 | env:
118 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers
119 |
120 | steps:
121 | - name: Checkout
122 | uses: actions/checkout@v6
123 |
124 | - name: Setup Python ${{ matrix.python-version }}
125 | uses: actions/setup-python@v6
126 | with:
127 | python-version: ${{ matrix.python-version }}
128 |
129 | - name: Base Setup
130 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
131 |
132 | - name: Download extension package
133 | uses: actions/download-artifact@v7
134 | with:
135 | name: extension-artifacts
136 |
137 | - name: Pre-install minimum jupyterlite-core
138 | if: ${{ matrix.lite-version == 'min' }}
139 | run: |
140 | python -m pip install ${MIN_LITE_VERSION}
141 |
142 | - name: Pre-install maximum jupyterlite-core
143 | if: ${{ matrix.lite-version == 'max' }}
144 | run: |
145 | python -m pip install ${MAX_LITE_VERSION}
146 |
147 | - name: Install the extension
148 | run: |
149 | set -eux
150 | python -m pip install jupyterlite_terminal*.whl ${LAB_VERSION}
151 |
152 | - name: List installed packages
153 | run: |
154 | python -m pip list
155 |
156 | - name: Micromamba needed for cockle_wasm_env
157 | uses: mamba-org/setup-micromamba@main
158 |
159 | - name: Install dependencies
160 | working-directory: ui-tests
161 | env:
162 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0
163 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
164 | run: |
165 | jlpm install
166 | jlpm build
167 |
168 | - name: Set up browser cache
169 | uses: actions/cache@v5
170 | with:
171 | path: |
172 | ${{ github.workspace }}/pw-browsers
173 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }}
174 |
175 | - name: Install browser
176 | run: jlpm playwright install chromium
177 | working-directory: ui-tests
178 |
179 | - name: Execute integration tests
180 | working-directory: ui-tests
181 | run: |
182 | jlpm test
183 |
184 | - name: Upload Playwright Test report
185 | if: always()
186 | uses: actions/upload-artifact@v6
187 | with:
188 | name: jupyterlite_terminal-playwright-tests
189 | path: |
190 | ui-tests/test-results_${{ matrix.lite-version}}
191 | ui-tests/playwright-report_${{ matrix.lite-version}}
192 |
193 | check_links:
194 | name: Check Links
195 | runs-on: ubuntu-latest
196 | timeout-minutes: 15
197 | steps:
198 | - uses: actions/checkout@v6
199 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
200 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1
201 | with:
202 | ignore_glob: docs/*.md
203 |
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
1 | import { PageConfig, URLExt } from '@jupyterlab/coreutils';
2 | import type { Terminal } from '@jupyterlab/services';
3 | import { ServerConnection } from '@jupyterlab/services';
4 | import type {
5 | IExternalCommand,
6 | IShellManager,
7 | IStdinReply,
8 | IStdinRequest
9 | } from '@jupyterlite/cockle';
10 | import { ShellManager } from '@jupyterlite/cockle';
11 | import type { JSONPrimitive } from '@lumino/coreutils';
12 | import type { ISignal } from '@lumino/signaling';
13 | import { Signal } from '@lumino/signaling';
14 |
15 | import type { Client as WebSocketClient } from 'mock-socket';
16 | import { Server as WebSocketServer } from 'mock-socket';
17 |
18 | import { Shell } from './shell';
19 | import type { ILiteTerminalAPIClient } from './tokens';
20 |
21 | export class LiteTerminalAPIClient implements ILiteTerminalAPIClient {
22 | constructor(options: { serverSettings?: ServerConnection.ISettings } = {}) {
23 | this.serverSettings =
24 | options.serverSettings ?? ServerConnection.makeSettings();
25 | this._shellManager = new ShellManager();
26 | }
27 |
28 | /**
29 | * Set identifier for communicating with service worker.
30 | */
31 | set browsingContextId(browsingContextId: string) {
32 | console.log('LiteTerminalAPIClient browsingContextId', browsingContextId);
33 | this._browsingContextId = browsingContextId;
34 | }
35 |
36 | /**
37 | * Function that handles stdin requests received from service worker.
38 | */
39 | async handleStdin(request: IStdinRequest): Promise {
40 | return await this._shellManager.handleStdin(request);
41 | }
42 |
43 | get isAvailable(): boolean {
44 | const available = String(PageConfig.getOption('terminalsAvailable'));
45 | return available.toLowerCase() === 'true';
46 | }
47 |
48 | readonly serverSettings: ServerConnection.ISettings;
49 |
50 | async startNew(
51 | options?: Terminal.ITerminal.IOptions
52 | ): Promise {
53 | // Create shell.
54 | const name = options?.name ?? this._nextAvailableName();
55 | const { baseUrl, wsUrl } = this.serverSettings;
56 | const shell = new Shell({
57 | mountpoint: '/drive',
58 | baseUrl,
59 | wasmBaseUrl: URLExt.join(
60 | baseUrl,
61 | 'extensions/@jupyterlite/terminal/static/wasm/'
62 | ),
63 | browsingContextId: this._browsingContextId,
64 | aliases: this._aliases,
65 | environment: this._environment,
66 | externalCommands: this._externalCommands,
67 | shellId: name,
68 | shellManager: this._shellManager,
69 | outputCallback: text => {
70 | const msg = JSON.stringify(['stdout', text]);
71 | shell.socket?.send(msg);
72 | }
73 | });
74 | this._shells.set(name, shell);
75 |
76 | // Hook to connect socket to shell.
77 | const hook = async (
78 | shell: Shell,
79 | socket: WebSocketClient
80 | ): Promise => {
81 | shell.socket = socket;
82 |
83 | socket.on('message', async (message: any) => {
84 | // Message from xtermjs to pass to shell.
85 | const data = JSON.parse(message) as JSONPrimitive[];
86 | const message_type = data[0];
87 | const content = data.slice(1);
88 | await shell.ready;
89 | if (message_type === 'stdin') {
90 | await shell.input(content[0] as string);
91 | } else if (message_type === 'set_size') {
92 | const rows = content[0] as number;
93 | const columns = content[1] as number;
94 | await shell.setSize(rows, columns);
95 | }
96 | });
97 |
98 | // Return handshake.
99 | const res = JSON.stringify(['setup']);
100 | console.log('Terminal returning handshake via socket');
101 | socket.send(res);
102 |
103 | shell.start();
104 | };
105 |
106 | const url = URLExt.join(wsUrl, 'terminals', 'websocket', name);
107 | const wsServer = new WebSocketServer(url);
108 | wsServer.on('connection', (socket: WebSocketClient): void => {
109 | hook(shell, socket);
110 | });
111 |
112 | shell.disposed.connect(() => {
113 | this.shutdown(name);
114 | wsServer.close();
115 | this._terminalDisposed.emit(shell.shellId);
116 | });
117 |
118 | return { name };
119 | }
120 |
121 | async listRunning(): Promise {
122 | return this._models;
123 | }
124 |
125 | registerAlias(key: string, value: string): void {
126 | if (this._aliases === undefined) {
127 | this._aliases = {};
128 | }
129 | this._aliases[key] = value;
130 | }
131 |
132 | registerEnvironmentVariable(key: string, value: string | undefined): void {
133 | if (this._environment === undefined) {
134 | this._environment = {};
135 | }
136 | this._environment[key] = value;
137 | }
138 |
139 | registerExternalCommand(options: IExternalCommand.IOptions): void {
140 | this._externalCommands.push(options);
141 | }
142 |
143 | async shutdown(name: string): Promise {
144 | const shell = this._shells.get(name);
145 | if (shell !== undefined) {
146 | shell.socket?.send(JSON.stringify(['disconnect']));
147 | shell.socket?.close();
148 | this._shells.delete(name);
149 | shell.dispose();
150 | }
151 | }
152 |
153 | get terminalDisposed(): ISignal {
154 | return this._terminalDisposed;
155 | }
156 |
157 | themeChange(isDarkMode?: boolean): void {
158 | for (const shell of this._shells.values()) {
159 | shell.themeChange(isDarkMode);
160 | }
161 | }
162 |
163 | private get _models(): Terminal.IModel[] {
164 | return Array.from(this._shells.keys(), name => {
165 | return { name };
166 | });
167 | }
168 |
169 | private _nextAvailableName(): string {
170 | for (let i = 1; ; ++i) {
171 | const name = `${i}`;
172 | if (!this._shells.has(name)) {
173 | return name;
174 | }
175 | }
176 | }
177 |
178 | private _aliases?: { [key: string]: string };
179 | private _environment?: { [key: string]: string | undefined };
180 | private _browsingContextId?: string;
181 | private _externalCommands: IExternalCommand.IOptions[] = [];
182 | private _shellManager: IShellManager;
183 | private _shells = new Map();
184 | private _terminalDisposed = new Signal(this);
185 | }
186 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jupyterlite/terminal",
3 | "version": "1.2.0",
4 | "description": "A terminal for JupyterLite",
5 | "keywords": [
6 | "jupyter",
7 | "jupyterlab",
8 | "jupyterlite",
9 | "jupyterlite-extension"
10 | ],
11 | "homepage": "https://github.com/jupyterlite/terminal",
12 | "bugs": {
13 | "url": "https://github.com/jupyterlite/terminal/issues"
14 | },
15 | "license": "BSD-3-Clause",
16 | "author": {
17 | "name": "JupyterLite Contributors",
18 | "email": ""
19 | },
20 | "files": [
21 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
22 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
23 | "src/**/*.{ts,tsx}"
24 | ],
25 | "main": "lib/index.js",
26 | "types": "lib/index.d.ts",
27 | "style": "style/index.css",
28 | "repository": {
29 | "type": "git",
30 | "url": "https://github.com/jupyterlite/terminal.git"
31 | },
32 | "scripts": {
33 | "build": "jlpm build:lib && jlpm build:labextension:dev && jlpm build:webworker",
34 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension && jlpm build:webworker:prod",
35 | "build:labextension": "jupyter labextension build .",
36 | "build:labextension:dev": "jupyter labextension build --development True .",
37 | "build:lib": "tsc --sourceMap",
38 | "build:lib:prod": "tsc",
39 | "build:webworker": "webpack -c webpack.worker.config.js --env=dev",
40 | "build:webworker:prod": "webpack -c webpack.worker.config.js",
41 | "clean": "jlpm clean:lib",
42 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
43 | "clean:lintcache": "rimraf .eslintcache .stylelintcache",
44 | "clean:labextension": "rimraf jupyterlite_terminal/labextension jupyterlite_terminal/_version.py",
45 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
46 | "eslint": "jlpm eslint:check --fix",
47 | "eslint:check": "eslint . --cache --ext .ts,.tsx",
48 | "install:extension": "jlpm build",
49 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
50 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
51 | "prettier": "jlpm prettier:base --write --list-different",
52 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
53 | "prettier:check": "jlpm prettier:base --check",
54 | "stylelint": "jlpm stylelint:check --fix",
55 | "stylelint:check": "stylelint --cache \"style/**/*.css\"",
56 | "test": "jest --coverage",
57 | "watch": "run-p watch:src watch:labextension",
58 | "watch:src": "tsc -w --sourceMap",
59 | "watch:labextension": "jupyter labextension watch ."
60 | },
61 | "dependencies": {
62 | "@jupyterlab/apputils": "^4.6.0",
63 | "@jupyterlab/coreutils": "^6.5.0",
64 | "@jupyterlab/pluginmanager": "^4.5.0",
65 | "@jupyterlab/services": "^7.5.0",
66 | "@jupyterlab/settingregistry": "^4.5.0",
67 | "@jupyterlite/apputils": "^0.7.0",
68 | "@jupyterlite/cockle": "^1.3.0",
69 | "@jupyterlite/services": "^0.7.0",
70 | "@lumino/coreutils": "^2.2.1",
71 | "@lumino/signaling": "^2.1.4",
72 | "mock-socket": "^9.3.1"
73 | },
74 | "devDependencies": {
75 | "@jupyterlab/builder": "^4.5.0",
76 | "@jupyterlab/testutils": "^4.5.0",
77 | "@types/jest": "^29.2.0",
78 | "@types/json-schema": "^7.0.11",
79 | "@types/react": "^18.0.26",
80 | "@types/react-addons-linked-state-mixin": "^0.14.22",
81 | "@typescript-eslint/eslint-plugin": "^7.16.1",
82 | "@typescript-eslint/parser": "^7.16.1",
83 | "css-loader": "^6.7.1",
84 | "eslint": "^8.36.0",
85 | "eslint-config-prettier": "^8.8.0",
86 | "eslint-plugin-prettier": "^5.0.0",
87 | "jest": "^29.2.0",
88 | "npm-run-all": "^4.1.5",
89 | "prettier": "^3.0.0",
90 | "rimraf": "^5.0.1",
91 | "source-map-loader": "^1.0.2",
92 | "style-loader": "^3.3.1",
93 | "stylelint": "^15.10.1",
94 | "stylelint-config-recommended": "^13.0.0",
95 | "stylelint-config-standard": "^34.0.0",
96 | "stylelint-csstree-validator": "^3.0.0",
97 | "stylelint-prettier": "^4.0.0",
98 | "ts-loader": "^9.5.2",
99 | "typescript": "~5.7.0",
100 | "webpack": "^5.87.0",
101 | "webpack-cli": "^5.1.4",
102 | "yjs": "^13.5.0"
103 | },
104 | "resolutions": {
105 | "parse5": "7.2.1"
106 | },
107 | "sideEffects": [
108 | "style/*.css",
109 | "style/index.js"
110 | ],
111 | "styleModule": "style/index.js",
112 | "publishConfig": {
113 | "access": "public"
114 | },
115 | "jupyterlab": {
116 | "extension": true,
117 | "outputDir": "jupyterlite_terminal/labextension",
118 | "sharedPackages": {
119 | "@jupyterlab/apputils": {
120 | "bundled": false,
121 | "singleton": true
122 | },
123 | "@jupyterlab/settingregistry": {
124 | "bundled": false,
125 | "singleton": true
126 | },
127 | "@jupyterlite/apputils": {
128 | "bundled": false,
129 | "singleton": true
130 | },
131 | "@jupyterlite/services": {
132 | "bundled": false,
133 | "singleton": true
134 | }
135 | }
136 | },
137 | "eslintIgnore": [
138 | "node_modules",
139 | "dist",
140 | "coverage",
141 | "**/*.d.ts",
142 | "tests",
143 | "**/__tests__",
144 | "ui-tests"
145 | ],
146 | "eslintConfig": {
147 | "extends": [
148 | "eslint:recommended",
149 | "plugin:@typescript-eslint/eslint-recommended",
150 | "plugin:@typescript-eslint/recommended",
151 | "plugin:prettier/recommended"
152 | ],
153 | "parser": "@typescript-eslint/parser",
154 | "parserOptions": {
155 | "project": "tsconfig.json",
156 | "sourceType": "module"
157 | },
158 | "plugins": [
159 | "@typescript-eslint"
160 | ],
161 | "rules": {
162 | "@typescript-eslint/consistent-type-imports": [
163 | "error",
164 | {
165 | "prefer": "type-imports",
166 | "fixStyle": "separate-type-imports"
167 | }
168 | ],
169 | "@typescript-eslint/naming-convention": [
170 | "error",
171 | {
172 | "selector": "interface",
173 | "format": [
174 | "PascalCase"
175 | ],
176 | "custom": {
177 | "regex": "^I[A-Z]",
178 | "match": true
179 | }
180 | }
181 | ],
182 | "@typescript-eslint/no-unused-vars": [
183 | "warn",
184 | {
185 | "args": "none"
186 | }
187 | ],
188 | "@typescript-eslint/no-explicit-any": "off",
189 | "@typescript-eslint/no-namespace": "off",
190 | "@typescript-eslint/no-use-before-define": "off",
191 | "@typescript-eslint/quotes": [
192 | "error",
193 | "single",
194 | {
195 | "avoidEscape": true,
196 | "allowTemplateLiterals": false
197 | }
198 | ],
199 | "curly": [
200 | "error",
201 | "all"
202 | ],
203 | "eqeqeq": "error",
204 | "prefer-arrow-callback": "error"
205 | }
206 | },
207 | "prettier": {
208 | "singleQuote": true,
209 | "trailingComma": "none",
210 | "arrowParens": "avoid",
211 | "endOfLine": "auto",
212 | "overrides": [
213 | {
214 | "files": "package.json",
215 | "options": {
216 | "tabWidth": 4
217 | }
218 | }
219 | ]
220 | },
221 | "stylelint": {
222 | "extends": [
223 | "stylelint-config-recommended",
224 | "stylelint-config-standard",
225 | "stylelint-prettier/recommended"
226 | ],
227 | "plugins": [
228 | "stylelint-csstree-validator"
229 | ],
230 | "rules": {
231 | "csstree/validator": true,
232 | "property-no-vendor-prefix": null,
233 | "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
234 | "selector-no-vendor-prefix": null,
235 | "value-no-vendor-prefix": null
236 | }
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 |
5 | ## 1.2.0
6 |
7 | This release updates to `jupyterlite 0.7.0` and `cockle 1.3.0`. The latter includes a fix for a significant bug when using the service worker for `stdin` when running interactive commands such as `vim`. For full details see the [Cockle changelog](https://github.com/jupyterlite/cockle/blob/main/CHANGELOG.md#130).
8 |
9 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v1.1.0...a9285fb3d615280f3d62502ff86e18a7ed874ec8))
10 |
11 | ### Enhancements made
12 |
13 | - Update cockle from 1.2.0 to 1.3.0 [#85](https://github.com/jupyterlite/terminal/pull/85) ([@ianthomas23](https://github.com/ianthomas23))
14 | - Add git2cpp to deployment [#76](https://github.com/jupyterlite/terminal/pull/76) ([@ianthomas23](https://github.com/ianthomas23))
15 |
16 | ### Maintenance and upkeep improvements
17 |
18 | - Update to jupyterlite 0.7.0 [#87](https://github.com/jupyterlite/terminal/pull/87) ([@ianthomas23](https://github.com/ianthomas23))
19 | - Make ui-tests more robust [#86](https://github.com/jupyterlite/terminal/pull/86) ([@ianthomas23](https://github.com/ianthomas23))
20 | - Add python 3.14, remove 3.9 [#84](https://github.com/jupyterlite/terminal/pull/84) ([@ianthomas23](https://github.com/ianthomas23))
21 | - Enforce type imports [#82](https://github.com/jupyterlite/terminal/pull/82) ([@ianthomas23](https://github.com/ianthomas23))
22 | - Update to jupyterlite 0.7.0rc0 and jupyterlab 4.5.0 [#81](https://github.com/jupyterlite/terminal/pull/81) ([@ianthomas23](https://github.com/ianthomas23))
23 |
24 | ### Documentation improvements
25 |
26 | - Update README for 1.1.0 release [#80](https://github.com/jupyterlite/terminal/pull/80) ([@ianthomas23](https://github.com/ianthomas23))
27 |
28 | ### Contributors to this release
29 |
30 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2025-10-27&to=2025-12-03&type=c))
31 |
32 | [@github-actions](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Agithub-actions+updated%3A2025-10-27..2025-12-03&type=Issues) | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2025-10-27..2025-12-03&type=Issues)
33 |
34 |
35 |
36 | ## 1.1.0
37 |
38 | This updates from `cockle` 1.0.0 to 1.2.0 bringing the following enhancements:
39 |
40 | - Support `termios` settings in `JavaScript` and `External` commands.
41 | - New environment variable `COCKLE_DARK_MODE` to indicate if terminal is currently dark or light mode.
42 | - Include `shellId` in all run and tab completion contexts.
43 | - Various improvements to tab completion.
44 |
45 | For full details see the [Cockle changelog](https://github.com/jupyterlite/cockle/blob/main/CHANGELOG.md).
46 |
47 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v1.0.1...dee237ce758e2bbfba208a6a9cffa55b4d95c61a))
48 |
49 | ### Enhancements made
50 |
51 | - Update to cockle 1.2.0 [#77](https://github.com/jupyterlite/terminal/pull/77) ([@ianthomas23](https://github.com/ianthomas23))
52 | - Pass on themeChanged boolean to cockle [#75](https://github.com/jupyterlite/terminal/pull/75) ([@ianthomas23](https://github.com/ianthomas23))
53 | - Add terminalDisposed Signal to ILiteTerminalAPIClient [#74](https://github.com/jupyterlite/terminal/pull/74) ([@ianthomas23](https://github.com/ianthomas23))
54 | - Support use of `lite_dir` when deploying [#70](https://github.com/jupyterlite/terminal/pull/70) ([@ianthomas23](https://github.com/ianthomas23))
55 |
56 | ### Maintenance and upkeep improvements
57 |
58 | - Update to cockle 1.1.0 [#72](https://github.com/jupyterlite/terminal/pull/72) ([@ianthomas23](https://github.com/ianthomas23))
59 | - Add github action containing link to Read the Docs PR preview [#71](https://github.com/jupyterlite/terminal/pull/71) ([@ianthomas23](https://github.com/ianthomas23))
60 | - Remove use of vercel for demo deployment [#69](https://github.com/jupyterlite/terminal/pull/69) ([@ianthomas23](https://github.com/ianthomas23))
61 |
62 | ### Documentation improvements
63 |
64 | - Add log and favicon to docs [#73](https://github.com/jupyterlite/terminal/pull/73) ([@ianthomas23](https://github.com/ianthomas23))
65 | - Add infrastructure for project docs [#68](https://github.com/jupyterlite/terminal/pull/68) ([@ianthomas23](https://github.com/ianthomas23))
66 | - Update docs for 1.0.1 release [#66](https://github.com/jupyterlite/terminal/pull/66) ([@ianthomas23](https://github.com/ianthomas23))
67 |
68 | ### Contributors to this release
69 |
70 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2025-09-03&to=2025-10-27&type=c))
71 |
72 | [@github-actions](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Agithub-actions+updated%3A2025-09-03..2025-10-27&type=Issues) | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2025-09-03..2025-10-27&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Ajtpio+updated%3A2025-09-03..2025-10-27&type=Issues) | [@vercel](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Avercel+updated%3A2025-09-03..2025-10-27&type=Issues)
73 |
74 | ## 1.0.1
75 |
76 | This is a maintenance release to support JupyterLite 0.7 as well as 0.6.
77 |
78 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v1.0.0...84fedfaec1965a7d1ebac77b69c0543a63199495))
79 |
80 | ### Maintenance and upkeep improvements
81 |
82 | - Run CI on earliest and latest supported jupyterlite-core [#65](https://github.com/jupyterlite/terminal/pull/65) ([@ianthomas23](https://github.com/ianthomas23))
83 | - Allow for JupyterLite 0.7 pre-releases [#64](https://github.com/jupyterlite/terminal/pull/64) ([@jtpio](https://github.com/jtpio))
84 |
85 | ### Contributors to this release
86 |
87 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2025-08-11&to=2025-09-03&type=c))
88 |
89 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2025-08-11..2025-09-03&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Ajtpio+updated%3A2025-08-11..2025-09-03&type=Issues) | [@vercel](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Avercel+updated%3A2025-08-11..2025-09-03&type=Issues)
90 |
91 | ## 1.0.0
92 |
93 | This is a major release introducing support for tab completion in built-in, external and javascript commands via `CommandArguments` classes. There are also new built-in commands `false`, `true`, `help` and `which`, and support for handling theme changes.
94 |
95 | The changes in external commands, command contexts and command argument classes are backwards incompatible, hence the major version bump.
96 |
97 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.2.2...07f77bd4f3c69d409cc668381cd613a87c5542e5))
98 |
99 | ### Enhancements made
100 |
101 | - Update to cockle 1.0.0 [#63](https://github.com/jupyterlite/terminal/pull/63) ([@ianthomas23](https://github.com/ianthomas23))
102 | - Pass on theme change to cockle [#62](https://github.com/jupyterlite/terminal/pull/62) ([@ianthomas23](https://github.com/ianthomas23))
103 |
104 | ### Contributors to this release
105 |
106 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2025-06-27&to=2025-08-11&type=c))
107 |
108 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2025-06-27..2025-08-11&type=Issues) | [@vercel](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Avercel+updated%3A2025-06-27..2025-08-11&type=Issues)
109 |
110 | ## 0.2.2
111 |
112 | This release adds support for the `less` command (with limitations), various enhancements to external commands (TypeScript commands that run in the main UI thread), and initial support to determine the terminal background color to identify dark mode.
113 |
114 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.2.1...5c8447f9b3c8a147b829d8e83fa8145f628cc9f0))
115 |
116 | ### Enhancements made
117 |
118 | - Update to cockle 0.1.2 [#61](https://github.com/jupyterlite/terminal/pull/61) ([@ianthomas23](https://github.com/ianthomas23))
119 |
120 | ### Bugs fixed
121 |
122 | - Revert PR 57 [#58](https://github.com/jupyterlite/terminal/pull/58) ([@ianthomas23](https://github.com/ianthomas23))
123 | - Disable jupyterlab's terminal-manager extension [#57](https://github.com/jupyterlite/terminal/pull/57) ([@ianthomas23](https://github.com/ianthomas23))
124 |
125 | ### Maintenance and upkeep improvements
126 |
127 | - Add UI tests for `nano` and `vim` commands [#60](https://github.com/jupyterlite/terminal/pull/60) ([@ianthomas23](https://github.com/ianthomas23))
128 |
129 | ### Contributors to this release
130 |
131 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2025-06-09&to=2025-06-27&type=c))
132 |
133 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2025-06-09..2025-06-27&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Ajtpio+updated%3A2025-06-09..2025-06-27&type=Issues) | [@vercel](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Avercel+updated%3A2025-06-09..2025-06-27&type=Issues)
134 |
135 | ## 0.2.1
136 |
137 | This is a bug fix release to fix bugs in URLs and the use of ServiceWorker for `stdin` from `cockle 0.1.1`.
138 |
139 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.2.0...4a27983d45168a80eff58c4be27b606db6874088))
140 |
141 | ### Maintenance and upkeep improvements
142 |
143 | - Bump cockle to 0.1.1 [#56](https://github.com/jupyterlite/terminal/pull/56) ([@ianthomas23](https://github.com/ianthomas23))
144 |
145 | ### Contributors to this release
146 |
147 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2025-06-04&to=2025-06-09&type=c))
148 |
149 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2025-06-04..2025-06-09&type=Issues)
150 |
151 | ## 0.2.0
152 |
153 | This release is a significant rewrite to work with JupyterLite 0.6.0 and to add support for using the JupyterLite ServiceWorker to provide `stdin` whilst commands are running, as an alternative to the existing SharedArrayBuffer implementation. Use of a ServiceWorker means it is no longer necessary to serve the terminal extension using cross-origin headers.
154 |
155 | If served with cross-origin headers both the SharedArrayBuffer and ServiceWorker stdin implementations will be available, with SharedArrayBuffer used by default. The user can switch between them at runtime using the shell command `cockle-config -s`.
156 |
157 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/312424ac...9b840f74385fda59b84fe68086a11bfb51e08a3c))
158 |
159 | ### Enhancements made
160 |
161 | - Update to cockle 0.1.0 [#55](https://github.com/jupyterlite/terminal/pull/55) ([@ianthomas23](https://github.com/ianthomas23))
162 | - Add experimental support for registering external commands [#54](https://github.com/jupyterlite/terminal/pull/54) ([@ianthomas23](https://github.com/ianthomas23))
163 | - Implement extension using `ITerminalAPIClient` [#53](https://github.com/jupyterlite/terminal/pull/53) ([@ianthomas23](https://github.com/ianthomas23))
164 | - Support use of service worker to handle stdin [#51](https://github.com/jupyterlite/terminal/pull/51) ([@ianthomas23](https://github.com/ianthomas23))
165 | - Rewrite as JupyterLab frontend plugin [#49](https://github.com/jupyterlite/terminal/pull/49) ([@ianthomas23](https://github.com/ianthomas23))
166 | - Update to cockle 0.0.18 to support nano and sed commands [#48](https://github.com/jupyterlite/terminal/pull/48) ([@ianthomas23](https://github.com/ianthomas23))
167 |
168 | ### Maintenance and upkeep improvements
169 |
170 | - Update to jupyterlite 0.6.0 [#52](https://github.com/jupyterlite/terminal/pull/52) ([@ianthomas23](https://github.com/ianthomas23))
171 | - Remove micromamba pin in CI [#50](https://github.com/jupyterlite/terminal/pull/50) ([@ianthomas23](https://github.com/ianthomas23))
172 |
173 | ### Contributors to this release
174 |
175 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2025-02-26&to=2025-06-04&type=c))
176 |
177 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2025-02-26..2025-06-04&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Ajtpio+updated%3A2025-02-26..2025-06-04&type=Issues) | [@vercel](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Avercel+updated%3A2025-02-26..2025-06-04&type=Issues)
178 |
179 | ## 0.2.0a0
180 |
181 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.1.6...24a17cd549c024b9f7325c11012c92c70ba6d038))
182 |
183 | ### Enhancements made
184 |
185 | - Rewrite as JupyterLab frontend plugin [#49](https://github.com/jupyterlite/terminal/pull/49) ([@ianthomas23](https://github.com/ianthomas23))
186 | - Update to cockle 0.0.18 to support nano and sed commands [#48](https://github.com/jupyterlite/terminal/pull/48) ([@ianthomas23](https://github.com/ianthomas23))
187 |
188 | ### Maintenance and upkeep improvements
189 |
190 | - Remove micromamba pin in CI [#50](https://github.com/jupyterlite/terminal/pull/50) ([@ianthomas23](https://github.com/ianthomas23))
191 |
192 | ### Contributors to this release
193 |
194 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2025-02-26&to=2025-05-19&type=c))
195 |
196 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2025-02-26..2025-05-19&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Ajtpio+updated%3A2025-02-26..2025-05-19&type=Issues) | [@vercel](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Avercel+updated%3A2025-02-26..2025-05-19&type=Issues)
197 |
198 | ## 0.1.6
199 |
200 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.1.5...a3ffc6b6c3c9dfdd3d4920ae7f76435f3d0bc9f3))
201 |
202 | ### Enhancements made
203 |
204 | - Build and use own shell web worker using DriveFS [#47](https://github.com/jupyterlite/terminal/pull/47) ([@ianthomas23](https://github.com/ianthomas23))
205 |
206 | ### Contributors to this release
207 |
208 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2025-02-05&to=2025-02-26&type=c))
209 |
210 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2025-02-05..2025-02-26&type=Issues)
211 |
212 | ## 0.1.5
213 |
214 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.1.4...02b85a5ca55ecdbe855deffafd4e3188f9f7395b))
215 |
216 | ### Enhancements made
217 |
218 | - Implement terminal shutdown [#41](https://github.com/jupyterlite/terminal/pull/41) ([@ianthomas23](https://github.com/ianthomas23))
219 | - Rename Terminals to TerminalManager [#40](https://github.com/jupyterlite/terminal/pull/40) ([@ianthomas23](https://github.com/ianthomas23))
220 | - Update to jupyterlite 0.5.0 and jupyterlab 4.3.4 [#39](https://github.com/jupyterlite/terminal/pull/39) ([@ianthomas23](https://github.com/ianthomas23))
221 |
222 | ### Maintenance and upkeep improvements
223 |
224 | - Update to cockle 0.0.15 [#45](https://github.com/jupyterlite/terminal/pull/45) ([@ianthomas23](https://github.com/ianthomas23))
225 | - Update to cockle 0.0.13 [#38](https://github.com/jupyterlite/terminal/pull/38) ([@ianthomas23](https://github.com/ianthomas23))
226 |
227 | ### Contributors to this release
228 |
229 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2024-12-13&to=2025-02-05&type=c))
230 |
231 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2024-12-13..2025-02-05&type=Issues) | [@vercel](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Avercel+updated%3A2024-12-13..2025-02-05&type=Issues)
232 |
233 | ## 0.1.4
234 |
235 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.1.3...b314c09e9c24ef9c1ea881022724edfe27a66bf4))
236 |
237 | ### Enhancements made
238 |
239 | - Update to cockle 0.0.12, adding tree and vim commands [#37](https://github.com/jupyterlite/terminal/pull/37) ([@ianthomas23](https://github.com/ianthomas23))
240 | - Add some file system tests [#34](https://github.com/jupyterlite/terminal/pull/34) ([@ianthomas23](https://github.com/ianthomas23))
241 | - Add some playwright ui tests [#33](https://github.com/jupyterlite/terminal/pull/33) ([@ianthomas23](https://github.com/ianthomas23))
242 |
243 | ### Contributors to this release
244 |
245 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2024-10-29&to=2024-12-13&type=c))
246 |
247 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2024-10-29..2024-12-13&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Ajtpio+updated%3A2024-10-29..2024-12-13&type=Issues) | [@vercel](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Avercel+updated%3A2024-10-29..2024-12-13&type=Issues)
248 |
249 | ## 0.1.3
250 |
251 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.1.2...8f8a93f74b9dfd29775badf53e9dbd67406e2213))
252 |
253 | ### Enhancements made
254 |
255 | - Support use of em-forge wasm files in standalone JupyterLite deployment [#31](https://github.com/jupyterlite/terminal/pull/31) ([@ianthomas23](https://github.com/ianthomas23))
256 |
257 | ### Contributors to this release
258 |
259 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2024-10-23&to=2024-10-29&type=c))
260 |
261 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2024-10-23..2024-10-29&type=Issues)
262 |
263 | ## 0.1.2
264 |
265 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.1.1...03a24763b3e14e04fe09373b8ed0f2ee040b729f))
266 |
267 | ### Enhancements made
268 |
269 | - Update to cockle 0.0.10 [#30](https://github.com/jupyterlite/terminal/pull/30) ([@ianthomas23](https://github.com/ianthomas23))
270 | - Obtain wasm packages from emscripten-forge when building deployment [#27](https://github.com/jupyterlite/terminal/pull/27) ([@ianthomas23](https://github.com/ianthomas23))
271 |
272 | ### Maintenance and upkeep improvements
273 |
274 | - Add config files to deploy to Vercel with the COOP / COEP headers [#28](https://github.com/jupyterlite/terminal/pull/28) ([@jtpio](https://github.com/jtpio))
275 |
276 | ### Documentation improvements
277 |
278 | - Update link to the demo, add demo files [#29](https://github.com/jupyterlite/terminal/pull/29) ([@jtpio](https://github.com/jtpio))
279 | - Add use of static-handler to README [#26](https://github.com/jupyterlite/terminal/pull/26) ([@ianthomas23](https://github.com/ianthomas23))
280 | - Update readme with screenshot and extra http headers [#25](https://github.com/jupyterlite/terminal/pull/25) ([@ianthomas23](https://github.com/ianthomas23))
281 | - Add better screenshot [#24](https://github.com/jupyterlite/terminal/pull/24) ([@ianthomas23](https://github.com/ianthomas23))
282 |
283 | ### Contributors to this release
284 |
285 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2024-09-16&to=2024-10-23&type=c))
286 |
287 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2024-09-16..2024-10-23&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Ajtpio+updated%3A2024-09-16..2024-10-23&type=Issues) | [@vercel](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Avercel+updated%3A2024-09-16..2024-10-23&type=Issues)
288 |
289 | ## 0.1.1
290 |
291 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/v0.1.0...f730b658ac11ee7299f697bb81781e2746c83655))
292 |
293 | ### Enhancements made
294 |
295 | - Remove WebWorker code that is now in cockle [#21](https://github.com/jupyterlite/terminal/pull/21) ([@ianthomas23](https://github.com/ianthomas23))
296 | - Replace postMessage from webworker with comlink callback [#16](https://github.com/jupyterlite/terminal/pull/16) ([@ianthomas23](https://github.com/ianthomas23))
297 | - Use WASM commands running in webworker [#15](https://github.com/jupyterlite/terminal/pull/15) ([@ianthomas23](https://github.com/ianthomas23))
298 |
299 | ### Bugs fixed
300 |
301 | - Fix listing of terminals [#11](https://github.com/jupyterlite/terminal/pull/11) ([@jtpio](https://github.com/jtpio))
302 |
303 | ### Maintenance and upkeep improvements
304 |
305 | - Update jupyterlite to 0.4.0 and cockle to 0.0.5 [#18](https://github.com/jupyterlite/terminal/pull/18) ([@ianthomas23](https://github.com/ianthomas23))
306 | - Support JupyterLite 0.4.0 packages [#14](https://github.com/jupyterlite/terminal/pull/14) ([@jtpio](https://github.com/jtpio))
307 |
308 | ### Documentation improvements
309 |
310 | - Add JupyterLite badge to the README [#10](https://github.com/jupyterlite/terminal/pull/10) ([@jtpio](https://github.com/jtpio))
311 | - Add workflow for deploying a demo to GitHub Pages [#9](https://github.com/jupyterlite/terminal/pull/9) ([@jtpio](https://github.com/jtpio))
312 |
313 | ### Contributors to this release
314 |
315 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2024-05-29&to=2024-09-16&type=c))
316 |
317 | [@ianthomas23](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Aianthomas23+updated%3A2024-05-29..2024-09-16&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Ajtpio+updated%3A2024-05-29..2024-09-16&type=Issues)
318 |
319 | ## 0.1.0
320 |
321 | ([Full Changelog](https://github.com/jupyterlite/terminal/compare/1076c3fb09302a306d7084f72d2fb58ead8adc84...b3ae8d8409eaa8d883ad52eb829016951001790b))
322 |
323 | ### Enhancements made
324 |
325 | - Add missing dependencies and handling of terminal clients [#2](https://github.com/jupyterlite/terminal/pull/2) ([@jtpio](https://github.com/jtpio))
326 | - Skip the browser check for now [#1](https://github.com/jupyterlite/terminal/pull/1) ([@jtpio](https://github.com/jtpio))
327 |
328 | ### Maintenance and upkeep improvements
329 |
330 | - Reset version for initial release [#8](https://github.com/jupyterlite/terminal/pull/8) ([@jtpio](https://github.com/jtpio))
331 | - Rename package to `@jupyterlite/terminal` [#7](https://github.com/jupyterlite/terminal/pull/7) ([@jtpio](https://github.com/jtpio))
332 |
333 | ### Contributors to this release
334 |
335 | ([GitHub contributors page for this release](https://github.com/jupyterlite/terminal/graphs/contributors?from=2024-05-16&to=2024-05-29&type=c))
336 |
337 | [@jtpio](https://github.com/search?q=repo%3Ajupyterlite%2Fterminal+involves%3Ajtpio+updated%3A2024-05-16..2024-05-29&type=Issues)
338 |
--------------------------------------------------------------------------------