├── 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 | image/svg+xml 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 | [![Github Actions Status](https://github.com/jupyterlite/terminal/workflows/Build/badge.svg)](https://github.com/jupyterlite/terminal/actions/workflows/build.yml) 4 | [![lite-badge](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](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 | ![a screenshot showing a terminal running in JupyterLite](https://raw.githubusercontent.com/jupyterlite/terminal/main/screenshot.png) 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 | --------------------------------------------------------------------------------