Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e60d791d5 | ||
|
|
b3503f4916 | ||
|
|
0dffbedf76 | ||
|
|
c4b707e9ca | ||
|
|
9d35d59fb3 | ||
|
|
877b37d3cc | ||
|
|
f14f55c951 | ||
|
|
8a16a551e0 | ||
|
|
2a5239578a | ||
|
|
d28eb696ec | ||
|
|
4c0c2559c3 | ||
|
|
ee0a3deb6a | ||
|
|
076f051bb5 | ||
|
|
658c57546c | ||
|
|
223a650e3e | ||
|
|
4b6316e510 | ||
|
|
41546230dc | ||
|
|
8413c0f94b | ||
|
|
8828b47cea | ||
|
|
b0f300e61b |
@@ -0,0 +1,10 @@
|
|||||||
|
This is a TypeScript-based repository with a Ruby client for certain API endpoints. It includes several sub projects called addons which are built separately. It contains a demo application which showcases the functionality of the project.
|
||||||
|
|
||||||
|
Please follow these guidelines when contributing:
|
||||||
|
|
||||||
|
## Development Flow
|
||||||
|
|
||||||
|
- Install dependencies: `npm install && npm run setup`
|
||||||
|
- Build and bundle demo: `npm run build && npm run esbuild`
|
||||||
|
- Run unit tests: `npm run test-unit`
|
||||||
|
- Run playwright-based integration tests: `npm run test-integration`
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
name: "Copilot Setup Steps"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
copilot-setup-steps:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js 20.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Setup and run tsc
|
||||||
|
run: yarn setup
|
||||||
|
|
||||||
|
- name: Esbuild
|
||||||
|
run: yarn esbuild
|
||||||
@@ -14,6 +14,10 @@ declare module '@xterm/addon-web-links' {
|
|||||||
/**
|
/**
|
||||||
* Creates a new web links addon.
|
* Creates a new web links addon.
|
||||||
* @param handler The callback when the link is called.
|
* @param handler The callback when the link is called.
|
||||||
|
*
|
||||||
|
* Note that this may not work when the terminal is hosted inside an iframe,
|
||||||
|
* in that case provide a custom handler in that case being mindful of
|
||||||
|
* possible security issues like reverse tabnapping.
|
||||||
* @param options Options for the link provider.
|
* @param options Options for the link provider.
|
||||||
*/
|
*/
|
||||||
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkProviderOptions);
|
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkProviderOptions);
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
|||||||
import { NULL_COLOR } from 'common/Color';
|
import { NULL_COLOR } from 'common/Color';
|
||||||
|
|
||||||
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number): ICharAtlasConfig {
|
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number): ICharAtlasConfig {
|
||||||
|
// Convert lineHeight to a numeric multiplier for the char atlas
|
||||||
|
// If it's a px value, calculate the equivalent multiplier
|
||||||
|
let lineHeightMultiplier: number = typeof options.lineHeight === 'number' ? options.lineHeight : 1;
|
||||||
|
if (typeof options.lineHeight === 'string' && options.lineHeight.endsWith('px')) {
|
||||||
|
const pxValue = parseFloat(options.lineHeight.slice(0, -2));
|
||||||
|
// Calculate the multiplier based on font size
|
||||||
|
lineHeightMultiplier = pxValue / options.fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
// null out some fields that don't matter
|
// null out some fields that don't matter
|
||||||
const clonedColors: IColorSet = {
|
const clonedColors: IColorSet = {
|
||||||
foreground: colors.foreground,
|
foreground: colors.foreground,
|
||||||
@@ -36,7 +45,7 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number
|
|||||||
devicePixelRatio,
|
devicePixelRatio,
|
||||||
deviceMaxTextureSize,
|
deviceMaxTextureSize,
|
||||||
letterSpacing: options.letterSpacing,
|
letterSpacing: options.letterSpacing,
|
||||||
lineHeight: options.lineHeight,
|
lineHeight: lineHeightMultiplier,
|
||||||
deviceCellWidth: deviceCellWidth,
|
deviceCellWidth: deviceCellWidth,
|
||||||
deviceCellHeight: deviceCellHeight,
|
deviceCellHeight: deviceCellHeight,
|
||||||
deviceCharWidth: deviceCharWidth,
|
deviceCharWidth: deviceCharWidth,
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ import { addDisposableListener } from 'vs/base/browser/dom';
|
|||||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the line height in pixels based on the lineHeight option and character height.
|
||||||
|
*/
|
||||||
|
function calculateLineHeightInPixels(lineHeight: number | string, charHeight: number): number {
|
||||||
|
if (typeof lineHeight === 'string' && lineHeight.endsWith('px')) {
|
||||||
|
const pxValue = parseFloat(lineHeight.slice(0, -2));
|
||||||
|
// Ensure the pixel value is at least as large as the character height
|
||||||
|
return Math.max(pxValue, charHeight);
|
||||||
|
}
|
||||||
|
// Numeric lineHeight acts as a multiplier
|
||||||
|
return Math.floor(charHeight * (lineHeight as number));
|
||||||
|
}
|
||||||
|
|
||||||
export class WebglRenderer extends Disposable implements IRenderer {
|
export class WebglRenderer extends Disposable implements IRenderer {
|
||||||
private _renderLayers: IRenderLayer[];
|
private _renderLayers: IRenderLayer[];
|
||||||
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
|
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
|
||||||
@@ -570,14 +583,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
// cell.
|
// cell.
|
||||||
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio);
|
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio);
|
||||||
|
|
||||||
// Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be
|
// Calculate the device cell height, supporting both numeric multipliers and 'px' values
|
||||||
// floored since lineHeight can never be lower then 1, this guarentees the device cell height
|
this.dimensions.device.cell.height = calculateLineHeightInPixels(this._optionsService.rawOptions.lineHeight, this.dimensions.device.char.height);
|
||||||
// will always be larger than device char height.
|
|
||||||
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
|
|
||||||
|
|
||||||
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
|
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
|
||||||
// correctly within the cell.
|
// correctly within the cell.
|
||||||
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
|
const isDefaultLineHeight = (typeof this._optionsService.rawOptions.lineHeight === 'number' && this._optionsService.rawOptions.lineHeight === 1) ||
|
||||||
|
(typeof this._optionsService.rawOptions.lineHeight === 'string' && this.dimensions.device.cell.height === this.dimensions.device.char.height);
|
||||||
|
this.dimensions.device.char.top = isDefaultLineHeight ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
|
||||||
|
|
||||||
// Calculate the device cell width, taking the letterSpacing into account.
|
// Calculate the device cell width, taking the letterSpacing into account.
|
||||||
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@
|
|||||||
"jsdom": "^18.0.1",
|
"jsdom": "^18.0.1",
|
||||||
"mocha": "^10.1.0",
|
"mocha": "^10.1.0",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"node-pty": "^1.1.0-beta31",
|
"node-pty": "1.1.0-beta19",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"source-map-loader": "^3.0.0",
|
"source-map-loader": "^3.0.0",
|
||||||
"source-map-support": "^0.5.20",
|
"source-map-support": "^0.5.20",
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the line height in pixels based on the lineHeight option and character height.
|
||||||
|
* This is the same function used in both DomRenderer and WebglRenderer.
|
||||||
|
*/
|
||||||
|
function calculateLineHeightInPixels(lineHeight: number | string, charHeight: number): number {
|
||||||
|
if (typeof lineHeight === 'string' && lineHeight.endsWith('px')) {
|
||||||
|
const pxValue = parseFloat(lineHeight.slice(0, -2));
|
||||||
|
// Ensure the pixel value is at least as large as the character height
|
||||||
|
return Math.max(pxValue, charHeight);
|
||||||
|
}
|
||||||
|
// Numeric lineHeight acts as a multiplier
|
||||||
|
return Math.floor(charHeight * (lineHeight as number));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('calculateLineHeightInPixels', () => {
|
||||||
|
describe('numeric lineHeight', () => {
|
||||||
|
it('should calculate correctly with multiplier 1', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels(1, 20), 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with multiplier 1.2', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels(1.2, 20), 24);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with multiplier 1.5', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels(1.5, 20), 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should floor the result', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels(1.3, 20), 26); // Math.floor(26)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('string lineHeight with px format', () => {
|
||||||
|
it('should use px value directly when larger than char height', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels('25px', 20), 25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use char height when px value is smaller', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels('15px', 20), 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle decimal px values', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels('23.5px', 20), 23.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge case where px equals char height', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels('20px', 20), 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle very large px values', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels('100px', 20), 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle zero char height with numeric multiplier', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels(1.5, 0), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle zero char height with px value', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels('25px', 0), 25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 1px with various char heights', () => {
|
||||||
|
assert.equal(calculateLineHeightInPixels('1px', 5), 5);
|
||||||
|
assert.equal(calculateLineHeightInPixels('1px', 20), 20);
|
||||||
|
assert.equal(calculateLineHeightInPixels('1px', 0), 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,6 +24,19 @@ const BG_CLASS_PREFIX = 'xterm-bg-';
|
|||||||
const FOCUS_CLASS = 'xterm-focus';
|
const FOCUS_CLASS = 'xterm-focus';
|
||||||
const SELECTION_CLASS = 'xterm-selection';
|
const SELECTION_CLASS = 'xterm-selection';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the line height in pixels based on the lineHeight option and character height.
|
||||||
|
*/
|
||||||
|
function calculateLineHeightInPixels(lineHeight: number | string, charHeight: number): number {
|
||||||
|
if (typeof lineHeight === 'string' && lineHeight.endsWith('px')) {
|
||||||
|
const pxValue = parseFloat(lineHeight.slice(0, -2));
|
||||||
|
// Ensure the pixel value is at least as large as the character height
|
||||||
|
return Math.max(pxValue, charHeight);
|
||||||
|
}
|
||||||
|
// Numeric lineHeight acts as a multiplier
|
||||||
|
return Math.floor(charHeight * (lineHeight as number));
|
||||||
|
}
|
||||||
|
|
||||||
let nextTerminalId = 1;
|
let nextTerminalId = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,7 +129,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
|||||||
this.dimensions.device.char.width = this._charSizeService.width * dpr;
|
this.dimensions.device.char.width = this._charSizeService.width * dpr;
|
||||||
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
|
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
|
||||||
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||||
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
|
this.dimensions.device.cell.height = calculateLineHeightInPixels(this._optionsService.rawOptions.lineHeight, this.dimensions.device.char.height);
|
||||||
this.dimensions.device.char.left = 0;
|
this.dimensions.device.char.left = 0;
|
||||||
this.dimensions.device.char.top = 0;
|
this.dimensions.device.char.top = 0;
|
||||||
this.dimensions.device.canvas.width = this.dimensions.device.cell.width * this._bufferService.cols;
|
this.dimensions.device.canvas.width = this.dimensions.device.cell.width * this._bufferService.cols;
|
||||||
|
|||||||
@@ -320,37 +320,7 @@ export class Buffer implements IBuffer {
|
|||||||
if (toRemove.length > 0) {
|
if (toRemove.length > 0) {
|
||||||
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
|
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
|
||||||
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
|
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
|
||||||
|
|
||||||
// For conpty, it has its own copy of the buffer _without scrollback_ internally. Its behavior
|
|
||||||
// when reflowing larger is to insert empty lines at the bottom of the buffer as when lines
|
|
||||||
// unwrap conpty's view cannot pull scrollback down, so it adds empty lines at the end.
|
|
||||||
let removedInViewport = 0;
|
|
||||||
const isWindowsMode = this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined;
|
|
||||||
if (isWindowsMode) {
|
|
||||||
for (let i = (toRemove.length / 2) - 1; i >= 0; i--) {
|
|
||||||
if (toRemove[i * 2 + 0] > this.ybase + removedInViewport) {
|
|
||||||
removedInViewport += toRemove[i * 2 + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);
|
this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);
|
||||||
|
|
||||||
// Apply empty lines for any removed in viewport for conpty.
|
|
||||||
if (isWindowsMode) {
|
|
||||||
if (removedInViewport > 0) {
|
|
||||||
for (let i = 0; i < removedInViewport; i++) {
|
|
||||||
// Just add the new missing rows on Windows as conpty reprints the screen with it's
|
|
||||||
// view of the world. Once a line enters scrollback for conpty it remains there
|
|
||||||
this.lines.push(new BufferLine(newCols, this.getNullCell(DEFAULT_ATTR_DATA)));
|
|
||||||
}
|
|
||||||
if (this.ybase === this.ydisp) {
|
|
||||||
this.ydisp += removedInViewport;
|
|
||||||
}
|
|
||||||
this.ybase += removedInViewport;
|
|
||||||
this.y -= removedInViewport;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +352,7 @@ export class Buffer implements IBuffer {
|
|||||||
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
|
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
|
||||||
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
|
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
|
||||||
// batched up and only committed once
|
// batched up and only committed once
|
||||||
const toInsert: { start: number, newLines: IBufferLine[] }[] = [];
|
const toInsert = [];
|
||||||
let countToInsert = 0;
|
let countToInsert = 0;
|
||||||
// Go backwards as many lines may be trimmed and this will avoid considering them
|
// Go backwards as many lines may be trimmed and this will avoid considering them
|
||||||
for (let y = this.lines.length - 1; y >= 0; y--) {
|
for (let y = this.lines.length - 1; y >= 0; y--) {
|
||||||
@@ -497,20 +467,6 @@ export class Buffer implements IBuffer {
|
|||||||
this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1);
|
this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For conpty, it has its own copy of the buffer _without scrollback_ internally. Its behavior
|
|
||||||
// when reflowing smaller is to reflow all lines inside the viewport, and removing empty or
|
|
||||||
// whitespace only lines from the bottom, until non-whitespace is hit in order to prevent
|
|
||||||
// content from being pushed into the scrollback.
|
|
||||||
let addedInViewport = 0;
|
|
||||||
const isWindowsMode = this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined;
|
|
||||||
if (isWindowsMode) {
|
|
||||||
for (let i = toInsert.length - 1; i >= 0; i--) {
|
|
||||||
if (toInsert[i].start > this.ybase + addedInViewport) {
|
|
||||||
addedInViewport += toInsert[i].newLines.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rearrange lines in the buffer if there are any insertions, this is done at the end rather
|
// Rearrange lines in the buffer if there are any insertions, this is done at the end rather
|
||||||
// than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
|
// than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
|
||||||
// costly calls to CircularList.splice.
|
// costly calls to CircularList.splice.
|
||||||
@@ -564,35 +520,6 @@ export class Buffer implements IBuffer {
|
|||||||
this.lines.onTrimEmitter.fire(amountToTrim);
|
this.lines.onTrimEmitter.fire(amountToTrim);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply empty lines to remove calculated earlier for conpty.
|
|
||||||
if (isWindowsMode) {
|
|
||||||
if (addedInViewport > 0) {
|
|
||||||
let emptyLinesAtBottom = 0;
|
|
||||||
for (let i = this.lines.length - 1; i >= this.ybase + this.y; i--) {
|
|
||||||
const line = this.lines.get(i) as BufferLine;
|
|
||||||
if (line.isWrapped || line.getTrimmedLength() > 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
emptyLinesAtBottom++;
|
|
||||||
}
|
|
||||||
const emptyLinesToRemove = Math.min(addedInViewport, emptyLinesAtBottom);
|
|
||||||
if (emptyLinesToRemove > 0) {
|
|
||||||
for (let i = 0; i < emptyLinesToRemove; i++) {
|
|
||||||
this.lines.pop();
|
|
||||||
}
|
|
||||||
if (this.ybase === this.ydisp) {
|
|
||||||
this.ydisp -= emptyLinesToRemove;
|
|
||||||
}
|
|
||||||
this.ybase -= emptyLinesToRemove;
|
|
||||||
this.y += emptyLinesToRemove;
|
|
||||||
this.lines.onDeleteEmitter.fire({
|
|
||||||
index: this.lines.length - emptyLinesToRemove,
|
|
||||||
amount: emptyLinesToRemove
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -125,17 +125,17 @@ describe('Keyboard', () => {
|
|||||||
describe('On non-macOS platforms', () => {
|
describe('On non-macOS platforms', () => {
|
||||||
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
|
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
|
||||||
// http://unix.stackexchange.com/a/108106
|
// http://unix.stackexchange.com/a/108106
|
||||||
it('should return \\x1b[5D for alt+left', () => {
|
it('should return \\x1b[1;3D for alt+left', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: false }).key, '\x1b[1;5D'); // CSI 5 D
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: false }).key, '\x1b[1;3D'); // CSI 1;3 D
|
||||||
});
|
});
|
||||||
it('should return \\x1b[5C for alt+right', () => {
|
it('should return \\x1b[1;3C for alt+right', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;3C'); // CSI 1;3 C
|
||||||
});
|
});
|
||||||
it('should return \\x1b[5D for alt+up', () => {
|
it('should return \\x1b[1;3A for alt+up', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: false }).key, '\x1b[1;5A'); // CSI 5 D
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: false }).key, '\x1b[1;3A'); // CSI 1;3 A
|
||||||
});
|
});
|
||||||
it('should return \\x1b[5C for alt+down', () => {
|
it('should return \\x1b[1;3B for alt+down', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: false }).key, '\x1b[1;5B'); // CSI 5 C
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: false }).key, '\x1b[1;3B'); // CSI 1;3 B
|
||||||
});
|
});
|
||||||
it('should return \\x1ba for alt+a', () => {
|
it('should return \\x1ba for alt+a', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
|
||||||
@@ -149,17 +149,17 @@ describe('Keyboard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('On macOS platforms', () => {
|
describe('On macOS platforms', () => {
|
||||||
it('should return \\x1bb for alt+left', () => {
|
it('should return \\x1b[1;3D for alt+left', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: true }).key, '\x1bb'); // CSI 5 D
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: true }).key, '\x1b[1;3D'); // CSI 1;3 D
|
||||||
});
|
});
|
||||||
it('should return \\x1bf for alt+right', () => {
|
it('should return \\x1b[1;3C for alt+right', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1b[1;3C'); // CSI 1;3 C
|
||||||
});
|
});
|
||||||
it('should return \\x1bb for alt+up', () => {
|
it('should return \\x1b[1;3A for alt+up', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: true }).key, '\x1b[1;3A'); // CSI 5 D
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: true }).key, '\x1b[1;3A'); // CSI 1;3 A
|
||||||
});
|
});
|
||||||
it('should return \\x1bf for alt+down', () => {
|
it('should return \\x1b[1;3B for alt+down', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: true }).key, '\x1b[1;3B'); // CSI 5 C
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: true }).key, '\x1b[1;3B'); // CSI 1;3 B
|
||||||
});
|
});
|
||||||
it('should return undefined for alt+a', () => {
|
it('should return undefined for alt+a', () => {
|
||||||
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined);
|
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined);
|
||||||
@@ -176,11 +176,11 @@ describe('Keyboard', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return \\x1b[5A for alt+up', () => {
|
it('should return \\x1b[1;3A for alt+up', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }).key, '\x1b[1;3A'); // CSI 1;3 A
|
||||||
});
|
});
|
||||||
it('should return \\x1b[5B for alt+down', () => {
|
it('should return \\x1b[1;3B for alt+down', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }).key, '\x1b[1;3B'); // CSI 1;3 B
|
||||||
});
|
});
|
||||||
it('should return the correct escape sequence for modified F1-F12 keys', () => {
|
it('should return the correct escape sequence for modified F1-F12 keys', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
|
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
|
||||||
@@ -336,6 +336,19 @@ describe('Keyboard', () => {
|
|||||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!');
|
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Characters using alt+shift sequences (letters)
|
||||||
|
it('should return proper sequences for alt+shift+letter combinations', () => {
|
||||||
|
// Test alt+shift combinations produce uppercase letters
|
||||||
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 65 }).key, '\x1bA'); // alt+shift+a
|
||||||
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 72 }).key, '\x1bH'); // alt+shift+h
|
||||||
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 90 }).key, '\x1bZ'); // alt+shift+z
|
||||||
|
|
||||||
|
// Test alt without shift produces lowercase letters
|
||||||
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 65 }).key, '\x1ba'); // alt+a
|
||||||
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 72 }).key, '\x1bh'); // alt+h
|
||||||
|
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 90 }).key, '\x1bz'); // alt+z
|
||||||
|
});
|
||||||
|
|
||||||
it('should return proper sequence for ctrl+@', () => {
|
it('should return proper sequence for ctrl+@', () => {
|
||||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, shiftKey: true, keyCode: 50, key: '@' }).key, '\x00');
|
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, shiftKey: true, keyCode: 50, key: '@' }).key, '\x00');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,12 +117,6 @@ export function evaluateKeyboardEvent(
|
|||||||
}
|
}
|
||||||
if (modifiers) {
|
if (modifiers) {
|
||||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
|
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
|
||||||
// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
|
|
||||||
// http://unix.stackexchange.com/a/108106
|
|
||||||
// macOS uses different escape sequences than linux
|
|
||||||
if (result.key === C0.ESC + '[1;3D') {
|
|
||||||
result.key = C0.ESC + (isMac ? 'b' : '[1;5D');
|
|
||||||
}
|
|
||||||
} else if (applicationCursorMode) {
|
} else if (applicationCursorMode) {
|
||||||
result.key = C0.ESC + 'OD';
|
result.key = C0.ESC + 'OD';
|
||||||
} else {
|
} else {
|
||||||
@@ -136,12 +130,6 @@ export function evaluateKeyboardEvent(
|
|||||||
}
|
}
|
||||||
if (modifiers) {
|
if (modifiers) {
|
||||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
|
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
|
||||||
// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
|
|
||||||
// http://unix.stackexchange.com/a/108106
|
|
||||||
// macOS uses different escape sequences than linux
|
|
||||||
if (result.key === C0.ESC + '[1;3C') {
|
|
||||||
result.key = C0.ESC + (isMac ? 'f' : '[1;5C');
|
|
||||||
}
|
|
||||||
} else if (applicationCursorMode) {
|
} else if (applicationCursorMode) {
|
||||||
result.key = C0.ESC + 'OC';
|
result.key = C0.ESC + 'OC';
|
||||||
} else {
|
} else {
|
||||||
@@ -155,12 +143,6 @@ export function evaluateKeyboardEvent(
|
|||||||
}
|
}
|
||||||
if (modifiers) {
|
if (modifiers) {
|
||||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
|
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
|
||||||
// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
|
|
||||||
// http://unix.stackexchange.com/a/108106
|
|
||||||
// macOS uses different escape sequences than linux
|
|
||||||
if (!isMac && result.key === C0.ESC + '[1;3A') {
|
|
||||||
result.key = C0.ESC + '[1;5A';
|
|
||||||
}
|
|
||||||
} else if (applicationCursorMode) {
|
} else if (applicationCursorMode) {
|
||||||
result.key = C0.ESC + 'OA';
|
result.key = C0.ESC + 'OA';
|
||||||
} else {
|
} else {
|
||||||
@@ -174,12 +156,6 @@ export function evaluateKeyboardEvent(
|
|||||||
}
|
}
|
||||||
if (modifiers) {
|
if (modifiers) {
|
||||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
|
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
|
||||||
// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
|
|
||||||
// http://unix.stackexchange.com/a/108106
|
|
||||||
// macOS uses different escape sequences than linux
|
|
||||||
if (!isMac && result.key === C0.ESC + '[1;3B') {
|
|
||||||
result.key = C0.ESC + '[1;5B';
|
|
||||||
}
|
|
||||||
} else if (applicationCursorMode) {
|
} else if (applicationCursorMode) {
|
||||||
result.key = C0.ESC + 'OB';
|
result.key = C0.ESC + 'OB';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { OptionsService } from 'common/services/OptionsService';
|
||||||
|
|
||||||
|
describe('OptionsService lineHeight', () => {
|
||||||
|
let optionsService: OptionsService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
optionsService = new OptionsService({});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('numeric lineHeight', () => {
|
||||||
|
it('should accept numeric values', () => {
|
||||||
|
optionsService.options.lineHeight = 1.5;
|
||||||
|
assert.equal(optionsService.rawOptions.lineHeight, 1.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject values less than 1', () => {
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept 1 as minimum value', () => {
|
||||||
|
optionsService.options.lineHeight = 1;
|
||||||
|
assert.equal(optionsService.rawOptions.lineHeight, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('string lineHeight with px format', () => {
|
||||||
|
it('should accept px values', () => {
|
||||||
|
optionsService.options.lineHeight = '24px';
|
||||||
|
assert.equal(optionsService.rawOptions.lineHeight, '24px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept decimal px values', () => {
|
||||||
|
optionsService.options.lineHeight = '23.5px';
|
||||||
|
assert.equal(optionsService.rawOptions.lineHeight, '23.5px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject px values less than 1', () => {
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = '0.5px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept 1px as minimum value', () => {
|
||||||
|
optionsService.options.lineHeight = '1px';
|
||||||
|
assert.equal(optionsService.rawOptions.lineHeight, '1px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject non-px string values', () => {
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = '24' as any);
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = '24em' as any);
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = 'normal' as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid px values', () => {
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = 'invalidpx' as any);
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = 'px' as any);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('invalid lineHeight types', () => {
|
||||||
|
it('should reject null', () => {
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = null as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject undefined when explicitly set', () => {
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject boolean values', () => {
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = true as any);
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = false as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject arrays', () => {
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = [] as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject objects', () => {
|
||||||
|
assert.throws(() => optionsService.options.lineHeight = {} as any);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -172,12 +172,31 @@ export class OptionsService extends Disposable implements IOptionsService {
|
|||||||
case 'cursorWidth':
|
case 'cursorWidth':
|
||||||
value = Math.floor(value);
|
value = Math.floor(value);
|
||||||
// Fall through for bounds check
|
// Fall through for bounds check
|
||||||
case 'lineHeight':
|
|
||||||
case 'tabStopWidth':
|
case 'tabStopWidth':
|
||||||
if (value < 1) {
|
if (value < 1) {
|
||||||
throw new Error(`${key} cannot be less than 1, value: ${value}`);
|
throw new Error(`${key} cannot be less than 1, value: ${value}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'lineHeight':
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// Handle 'px' format
|
||||||
|
if (value.endsWith('px')) {
|
||||||
|
const pxValue = parseFloat(value.slice(0, -2));
|
||||||
|
if (isNaN(pxValue) || pxValue < 1) {
|
||||||
|
throw new Error(`${key} with 'px' format must be a valid number >= 1, value: ${value}`);
|
||||||
|
}
|
||||||
|
// Keep the string value as-is for downstream processing
|
||||||
|
} else {
|
||||||
|
throw new Error(`${key} string value must end with 'px', value: ${value}`);
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
if (value < 1) {
|
||||||
|
throw new Error(`${key} cannot be less than 1, value: ${value}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`${key} must be a number or string with 'px' suffix, value: ${value}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'minimumContrastRatio':
|
case 'minimumContrastRatio':
|
||||||
value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));
|
value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ export interface ITerminalOptions {
|
|||||||
fontWeightBold?: FontWeight;
|
fontWeightBold?: FontWeight;
|
||||||
ignoreBracketedPasteMode?: boolean;
|
ignoreBracketedPasteMode?: boolean;
|
||||||
letterSpacing?: number;
|
letterSpacing?: number;
|
||||||
lineHeight?: number;
|
lineHeight?: number | string;
|
||||||
linkHandler?: ILinkHandler | null;
|
linkHandler?: ILinkHandler | null;
|
||||||
logLevel?: LogLevel;
|
logLevel?: LogLevel;
|
||||||
logger?: ILogger | null;
|
logger?: ILogger | null;
|
||||||
|
|||||||
Vendored
+4
-2
@@ -94,9 +94,11 @@ declare module '@xterm/headless' {
|
|||||||
letterSpacing?: number;
|
letterSpacing?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The line height used to render text.
|
* The line height used to render text. This can be specified as:
|
||||||
|
* - A number (e.g., 1.2) which acts as a multiplier of the font size
|
||||||
|
* - A string with 'px' suffix (e.g., '24px') which specifies an absolute pixel height
|
||||||
*/
|
*/
|
||||||
lineHeight?: number;
|
lineHeight?: number | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What log level to use, this will log for all levels below and including
|
* What log level to use, this will log for all levels below and including
|
||||||
|
|||||||
Vendored
+4
-2
@@ -152,9 +152,11 @@ declare module '@xterm/xterm' {
|
|||||||
letterSpacing?: number;
|
letterSpacing?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The line height used to render text.
|
* The line height used to render text. This can be specified as:
|
||||||
|
* - A number (e.g., 1.2) which acts as a multiplier of the font size
|
||||||
|
* - A string with 'px' suffix (e.g., '24px') which specifies an absolute pixel height
|
||||||
*/
|
*/
|
||||||
lineHeight?: number;
|
lineHeight?: number | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The handler for OSC 8 hyperlinks. Links will use the `confirm` browser
|
* The handler for OSC 8 hyperlinks. Links will use the `confirm` browser
|
||||||
|
|||||||
@@ -3302,10 +3302,10 @@ node-preload@^0.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
process-on-spawn "^1.0.0"
|
process-on-spawn "^1.0.0"
|
||||||
|
|
||||||
node-pty@^1.1.0-beta31:
|
node-pty@1.1.0-beta19:
|
||||||
version "1.1.0-beta9"
|
version "1.1.0-beta19"
|
||||||
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta9.tgz#ed643cb3b398d031b4e31c216e8f3b0042435f1d"
|
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta19.tgz#a74dc04429903c5ac49ee81a15a24590da67d4f3"
|
||||||
integrity sha512-/Ue38pvXJdgRZ3+me1FgfglLd301GhJN0NStiotdt61tm43N5htUyR/IXOUzOKuNaFmCwIhy6nwb77Ky41LMbw==
|
integrity sha512-/p4Zu56EYDdXjjaLWzrIlFyrBnND11LQGP0/L6GEVGURfCNkAlHc3Twg/2I4NPxghimHXgvDlwp7Z2GtvDIh8A==
|
||||||
dependencies:
|
dependencies:
|
||||||
node-addon-api "^7.1.0"
|
node-addon-api "^7.1.0"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user