Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create a node.js target for xterm.js #3212

Merged
merged 39 commits into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
157055c
Original files
joyceerhl Jan 10, 2021
5e6e65c
Get dupe of src/browser/public/Terminal compiling
joyceerhl Jan 11, 2021
2f88498
Add index.js used to test xterm-core
joyceerhl Jan 11, 2021
c015379
Merge branch 'master' into nodetarget
Tyriar Jan 11, 2021
238f6ab
Merge branch 'master' into nodetarget
Tyriar Jul 22, 2021
91c7032
Use core terminal in common, remove duplicate classes
Tyriar Jul 22, 2021
625e17e
Fix some errors, move TerminalCore to common/
Tyriar Jul 22, 2021
df17288
Create headless project, fix compile
Tyriar Jul 22, 2021
61ede1c
Update instructions to run
Tyriar Jul 22, 2021
7117951
Output headless to out/headless
Tyriar Jul 22, 2021
9b2e511
Fix webpack headless
Tyriar Jul 22, 2021
2191d1a
Output commonjs for headless
Tyriar Jul 22, 2021
01babd1
Do a pass of headless/Terminal members
Tyriar Jul 22, 2021
40992a7
Update node test
Tyriar Jul 22, 2021
a6e6c07
Add first headless test
Tyriar Jul 22, 2021
f43fcaf
Move addon manager to common and use in xterm-core
Tyriar Aug 10, 2021
468f419
Merge branch 'master' into nodetarget
Tyriar Aug 10, 2021
f769d03
xterm-core -> xterm-headless
Tyriar Aug 10, 2021
4a200e5
Create headless package script
Tyriar Aug 10, 2021
15bd827
Improve headless packaging
Tyriar Aug 10, 2021
6aa0782
Publish dry run in package_headless
Tyriar Aug 10, 2021
83ddf0f
Force publish of headless even in PR
Tyriar Aug 11, 2021
f685d8e
Force publish
Tyriar Aug 11, 2021
97fa4a5
Package headless in release step
Tyriar Aug 11, 2021
b6e5430
Revert "Force publish"
Tyriar Aug 11, 2021
5affa70
Undo headless force publish changes
Tyriar Aug 11, 2021
3e9b1ba
Remove force publish changes
Tyriar Aug 11, 2021
370d0c5
Copy logo-full.png
Tyriar Aug 11, 2021
be16429
Remove unused compile script
Tyriar Aug 11, 2021
454c272
Add npmignore
Tyriar Aug 11, 2021
dda4904
Fix xterm-headless lib file
Tyriar Aug 11, 2021
6ac7e18
Merge branch 'master' into nodetarget
Tyriar Aug 12, 2021
7e7dcd9
Move node-test into headless folder
Tyriar Aug 12, 2021
ec80fc9
Remove alpha from package.json version
Tyriar Aug 12, 2021
e96b472
Start on headless unit tests
Tyriar Aug 12, 2021
df48e9f
Headless event tests
Tyriar Aug 12, 2021
f8066d6
Full tests for xterm-headless
Tyriar Aug 12, 2021
f45f5b4
Remove unneeded manual headless test
Tyriar Aug 12, 2021
d694feb
Polish readme
Tyriar Aug 12, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"project": [
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"src/headless/tsconfig.json",
"test/api/tsconfig.json",
"test/benchmark/tsconfig.json",
"addons/xterm-addon-attach/src/tsconfig.json",
Expand Down
2 changes: 2 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,5 +164,7 @@ jobs:
displayName: Cache node modules
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- script: node ./bin/package_headless.js
displayName: 'Package xterm-headless'
- script: NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
displayName: 'Package and publish to npm'
57 changes: 57 additions & 0 deletions bin/package_headless.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
* @license MIT
*/

const { exec } = require('child_process');
const fs = require('fs');
const { join } = require('path');

const repoRoot = join(__dirname, '..');
const headlessRoot = join(repoRoot, 'headless');

console.log('> headless/package.json');
const xtermPackageJson = require('../package.json');
const xtermHeadlessPackageJson = {
...xtermPackageJson,
name: 'xterm-headless',
description: 'A headless terminal component that runs in Node.js',
main: 'lib-headless/xterm-headless.js',
types: 'typings/xterm-headless.d.ts',
};
delete xtermHeadlessPackageJson['scripts'];
delete xtermHeadlessPackageJson['devDependencies'];
delete xtermHeadlessPackageJson['style'];
fs.writeFileSync(join(headlessRoot, 'package.json'), JSON.stringify(xtermHeadlessPackageJson, null, 1));
console.log(fs.readFileSync(join(headlessRoot, 'package.json')).toString());

console.log('> headless/typings/');
mkdirF(join(headlessRoot, 'typings'));
fs.copyFileSync(
join(repoRoot, 'typings/xterm-headless.d.ts'),
join(headlessRoot, 'typings/xterm-headless.d.ts')
);

console.log('> headless/logo-full.png');
fs.copyFileSync(
join(repoRoot, 'logo-full.png'),
join(headlessRoot, 'logo-full.png')
);

function mkdirF(p) {
if (!fs.existsSync(p)) {
fs.mkdirSync(p);
}
}

console.log('> Publish dry run');
exec('npm publish --dry-run', { cwd: headlessRoot }, (error, stdout, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.error(`stderr:\n${stderr}`);
}
console.log(`stdout:\n${stdout}`);
});
1 change: 1 addition & 0 deletions bin/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const changedFiles = getChangedFilesInCommit('HEAD');
let isStableRelease = false;
if (changedFiles.some(e => e.search(/^addons\//) === -1)) {
isStableRelease = checkAndPublishPackage(path.resolve(__dirname, '..'));
checkAndPublishPackage(path.resolve(__dirname, '../headless'));
}

// Publish addons if any files were changed inside of the addon
Expand Down
4 changes: 4 additions & 0 deletions headless/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
lib-headless/
typings/
logo-full.png
./package.json
5 changes: 5 additions & 0 deletions headless/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Include
!typings/*.d.ts

# Exclude
test/
31 changes: 31 additions & 0 deletions headless/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org)

⚠ This package is experimental

`xterm-headless` is a headless terminal that can be run in node.js. This is useful in combination with the frontend [`xterm`](https://www.npmjs.com/package/xterm) for example to keep track of a terminal's state on a remote server where the process is hosted.

## Getting Started

First, you need to install the module, we ship exclusively through npm, so you need that installed and then add xterm.js as a dependency by running:

```sh
npm install xterm-headless
```

Then import as you would a regular node package. The recommended way to load `xterm-headless` is with TypeScript and the ES6 module syntax:

```javascript
import { Terminal } from 'xterm-headless';
```

## API

The full API for `xterm-headless` is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm-headless.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.

Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.

### Addons

Addons in `xterm-headless` work the [same as in `xterm`](https://github.com/xtermjs/xterm.js/blob/master/README.md#addons) with the one caveat being that the addon needs to be packaged for node.js and not use any DOM APIs.

Currently no official addons are packaged on npm.
9 changes: 9 additions & 0 deletions headless/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "xterm-headless",
"description": "A headless terminal component that runs in Node.js",
"version": "4.13.0-alpha3",
"main": "lib-headless/xterm-headless.js",
"types": "typings/xterm-headless.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT"
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"scripts": {
"prepackage": "npm run build",
"package": "webpack",
"package-headless": "webpack --config ./webpack.config.headless.js",
"postpackage-headless": "node ./bin/package_headless.js",
"start": "node demo/start",
"start-debug": "node --inspect-brk demo/start",
"lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/",
Expand Down
4 changes: 2 additions & 2 deletions src/browser/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onBell = new EventEmitter<void>();
public get onBell (): IEvent<void> { return this._onBell.event; }
private _onBell = new EventEmitter<void>();
public get onBell(): IEvent<void> { return this._onBell.event; }

private _onFocus = new EventEmitter<void>();
public get onFocus(): IEvent<void> { return this._onFocus.event; }
Expand Down
18 changes: 9 additions & 9 deletions src/browser/public/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight } from 'xterm';
import { ITerminal } from 'browser/Types';
import { Terminal as TerminalCore } from '../Terminal';
import * as Strings from '../LocalizableStrings';
import { Terminal as TerminalCore } from 'browser/Terminal';
import * as Strings from 'browser/LocalizableStrings';
import { IEvent } from 'common/EventEmitter';
import { ParserApi } from 'common/public/ParserApi';
import { UnicodeApi } from 'common/public/UnicodeApi';
import { AddonManager } from './AddonManager';
import { AddonManager } from 'common/public/AddonManager';
import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';

export class Terminal implements ITerminalApi {
Expand All @@ -30,17 +30,17 @@ export class Terminal implements ITerminalApi {
}
}

public get onBell(): IEvent<void> { return this._core.onBell; }
public get onBinary(): IEvent<string> { return this._core.onBinary; }
public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
public get onData(): IEvent<string> { return this._core.onData; }
public get onBinary(): IEvent<string> { return this._core.onBinary; }
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
public get onBell(): IEvent<void> { return this._core.onBell; }
public get onScroll(): IEvent<number> { return this._core.onScroll; }
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
public get onScroll(): IEvent<number> { return this._core.onScroll; }
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }

public get element(): HTMLElement | undefined { return this._core.element; }
public get parser(): IParser {
Expand Down
File renamed without changes.
170 changes: 170 additions & 0 deletions src/headless/Terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*
* Terminal Emulation References:
* http://vt100.net/
* http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt
* http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
* http://invisible-island.net/vttest/
* http://www.inwap.com/pdp10/ansicode.txt
* http://linux.die.net/man/4/console_codes
* http://linux.die.net/man/7/urxvt
*/

import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBuffer } from 'common/buffer/Types';
import { CoreTerminal } from 'common/CoreTerminal';
import { EventEmitter, forwardEvent, IEvent } from 'common/EventEmitter';
import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services';
import { IMarker, ITerminalOptions, ScrollSource } from 'common/Types';

export class Terminal extends CoreTerminal {
// TODO: We should remove options once components adopt optionsService
public get options(): IInitializedTerminalOptions { return this.optionsService.options; }

private _onBell = new EventEmitter<void>();
public get onBell(): IEvent<void> { return this._onBell.event; }
private _onCursorMove = new EventEmitter<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }

private _onA11yCharEmitter = new EventEmitter<string>();
public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; }
private _onA11yTabEmitter = new EventEmitter<number>();
public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; }

/**
* Creates a new `Terminal` object.
*
* @param options An object containing a set of options, the available options are:
* - `cursorBlink` (boolean): Whether the terminal cursor blinks
* - `cols` (number): The number of columns of the terminal (horizontal size)
* - `rows` (number): The number of rows of the terminal (vertical size)
*
* @public
* @class Xterm Xterm
* @alias module:xterm/src/xterm
*/
constructor(
options: ITerminalOptions = {}
) {
super(options);

this._setup();

// Setup InputHandler listeners
this.register(this._inputHandler.onRequestBell(() => this.bell()));
this.register(this._inputHandler.onRequestReset(() => this.reset()));
this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove));
this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange));
this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter));
this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter));
}

public dispose(): void {
if (this._isDisposed) {
return;
}
super.dispose();
this.write = () => { };
}

/**
* Convenience property to active buffer.
*/
public get buffer(): IBuffer {
return this.buffers.active;
}

protected _updateOptions(key: string): void {
super._updateOptions(key);

// TODO: These listeners should be owned by individual components
switch (key) {
case 'tabStopWidth': this.buffers.setupTabStops(); break;
}
}

// TODO: Support paste here?

public get markers(): IMarker[] {
return this.buffer.markers;
}

public addMarker(cursorYOffset: number): IMarker | undefined {
// Disallow markers on the alt buffer
if (this.buffer !== this.buffers.normal) {
return;
}

return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);
}

public bell(): void {
this._onBell.fire();
}

/**
* Resizes the terminal.
*
* @param x The number of columns to resize to.
* @param y The number of rows to resize to.
*/
public resize(x: number, y: number): void {
if (x === this.cols && y === this.rows) {
return;
}

super.resize(x, y);
}

/**
* Clear the entire buffer, making the prompt line the new first line.
*/
public clear(): void {
if (this.buffer.ybase === 0 && this.buffer.y === 0) {
// Don't clear if it's already clear
return;
}
this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);
this.buffer.lines.length = 1;
this.buffer.ydisp = 0;
this.buffer.ybase = 0;
this.buffer.y = 0;
for (let i = 1; i < this.rows; i++) {
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL });
}

/**
* Reset terminal.
* Note: Calling this directly from JS is synchronous but does not clear
* input buffers and does not reset the parser, thus the terminal will
* continue to apply pending input data.
* If you need in band reset (synchronous with input data) consider
* using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c).
*/
public reset(): void {
/**
* Since _setup handles a full terminal creation, we have to carry forward
* a few things that should not reset.
*/
this.options.rows = this.rows;
this.options.cols = this.cols;

this._setup();
super.reset();
}
}
31 changes: 31 additions & 0 deletions src/headless/Types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IEvent } from 'common/EventEmitter';
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { ICoreTerminal, IDisposable, IMarker, ITerminalOptions } from 'common/Types';

export interface ITerminal extends ICoreTerminal {
rows: number;
cols: number;
buffer: IBuffer;
buffers: IBufferSet;
markers: IMarker[];
// TODO: We should remove options once components adopt optionsService
options: ITerminalOptions;

onCursorMove: IEvent<void>;
onData: IEvent<string>;
onBinary: IEvent<string>;
onLineFeed: IEvent<void>;
onResize: IEvent<{ cols: number, rows: number }>;
onTitleChange: IEvent<string>;
resize(columns: number, rows: number): void;
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
addMarker(cursorYOffset: number): IMarker | undefined;
dispose(): void;
clear(): void;
write(data: string | Uint8Array, callback?: () => void): void;
reset(): void;
}
Loading