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

Experimental Sixel and embedded graphics support. #1940

Closed
wants to merge 10 commits into from
115 changes: 84 additions & 31 deletions src/Buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import { IMarker } from 'xterm';
import { BufferLine } from './BufferLine';
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
import { CircularList, IDeleteEvent, IInsertEvent } from './common/CircularList';
import { CircularList, IMoveEvent } from './common/CircularList';
import { EventEmitter } from './common/EventEmitter';
import { DEFAULT_COLOR } from './renderer/atlas/Types';
import { BufferIndex, CharData, IBuffer, IBufferLine, IBufferStringIterator, IBufferStringIteratorResult, ITerminal } from './Types';
import { BufferIndex, CharData, IBuffer, IBufferLine, IBufferStringIterator, IBufferStringIteratorResult, ITerminal, IElementInset } from './Types';

export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0;
Expand Down Expand Up @@ -385,7 +385,7 @@ export class Buffer implements IBuffer {
if (toInsert.length > 0) {
// Record buffer insert events and then play them back backwards so that the indexes are
// correct
const insertEvents: IInsertEvent[] = [];
const insertEvents: IMoveEvent[] = [];

// Record original lines so they don't get overridden when we rearrange the list
const originalLines: BufferLine[] = [];
Expand All @@ -409,9 +409,9 @@ export class Buffer implements IBuffer {

// Create insert events for later
insertEvents.push({
index: originalLineIndex + 1,
amount: nextToInsert.newLines.length
} as IInsertEvent);
start: originalLineIndex + 1,
amount: - nextToInsert.newLines.length
} as IMoveEvent);

countInsertedSoFar += nextToInsert.newLines.length;
nextToInsert = toInsert[++nextToInsertIndex];
Expand All @@ -423,13 +423,14 @@ export class Buffer implements IBuffer {
// Update markers
let insertCountEmitted = 0;
for (let i = insertEvents.length - 1; i >= 0; i--) {
insertEvents[i].index += insertCountEmitted;
this.lines.emit('insert', insertEvents[i]);
insertEvents[i].start += insertCountEmitted;
this.lines.emit('move', insertEvents[i]);
insertCountEmitted += insertEvents[i].amount;
}
const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength);
if (amountToTrim > 0) {
this.lines.emitMayRemoveListeners('trim', amountToTrim);
this.lines.emitMayRemoveListeners('trim', amountToTrim); // deprecated
this.lines.emitMayRemoveListeners('move', { start: 0, amount: amountToTrim});
}
}
}
Expand Down Expand Up @@ -549,29 +550,14 @@ export class Buffer implements IBuffer {
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
marker.register(this.lines.addDisposableListener('trim', amount => {
marker.line -= amount;
// The marker should be disposed when the line is trimmed from the buffer
if (marker.line < 0) {
marker.dispose();
}
}));
marker.register(this.lines.addDisposableListener('insert', (event: IInsertEvent) => {
if (marker.line >= event.index) {
marker.line += event.amount;
}
}));
marker.register(this.lines.addDisposableListener('delete', (event: IDeleteEvent) => {
// Delete the marker if it's within the range
if (marker.line >= event.index && marker.line < event.index + event.amount) {
marker.dispose();
}

// Shift the marker if it's after the deleted range
if (marker.line > event.index) {
marker.register(this.lines.addDisposableListener('move', (event: IMoveEvent) => {
if (marker.line >= event.start && (! event.end || marker.line < event.end)) {
marker.line -= event.amount;
}
}));
if (marker.line < event.start ||
(event.end && marker.line >= event.end)) {
marker.dispose();
}
}}));
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
return marker;
}
Expand Down Expand Up @@ -666,3 +652,70 @@ export class BufferStringIterator implements IBufferStringIterator {
return {range: range, content: result};
}
}

export class ElementInset extends EventEmitter implements IElementInset {
element: HTMLElement;
wrapper: HTMLElement;
owner: IBufferLine;
marker: Marker;

constructor(private _terminal: any, element: HTMLElement) {
super();
const wrapper = document.createElement('div');
wrapper.classList.add('inset-wrapper');
wrapper.setAttribute('style', 'position: absolute; z-index: 1');
if (element !== null) {
this.element = element;
wrapper.appendChild(element);
}
this.wrapper = wrapper;
}
put(line: number, column: number,
owner: IBufferLine = this._terminal.buffer.lines.get(line)) {
const cheight = this._terminal.renderer.dimensions.actualCellHeight;
const cwidth = this._terminal.renderer.dimensions.actualCellWidth;
this.wrapper.style.left = (column * cwidth) + 'px';
this.wrapper.style.top = (line * cheight) + 'px';
this._terminal._viewportScrollArea.appendChild(this.wrapper);
this.register(this._terminal.buffer.lines.addDisposableListener('move',
(event: IMoveEvent) => {
if (line >= event.start && (event.end === undefined || line < event.end)) {
line -= event.amount;
if (line < event.start ||
(event.end !== undefined && line >= event.end)) {
this.dispose();
} else {
this.wrapper.style.top = (line * cheight) + 'px';
}
}}));
this.owner = owner;
if (owner.insetElements === null) {
owner.insetElements = [this];
} else {
owner.insetElements.push(this);
}
this.marker = this._terminal.addMarker(line);
}
public updatePosition(height: number, width: number = -1): void {
const terminal = this._terminal;
const cheight = terminal.renderer.dimensions.actualCellHeight;
let ydelta = Math.ceil(height / cheight);
if (true || width <= 0) {
terminal.buffer.x = 0;
} else {
terminal.buffer.x += Math.ceil(width / terminal.charMeasure.width);
ydelta--;
}
while (--ydelta >= 0) {
terminal._inputHandler.lineFeed();
}
}
public dispose(): void {
super.dispose();
const wrapper = this.wrapper;
if (wrapper && wrapper.parentNode) {
wrapper.parentNode.removeChild(wrapper);
}
this.wrapper = null;
}
}
3 changes: 2 additions & 1 deletion src/BufferLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @license MIT
*/
import { CharData, IBufferLine } from './Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, WHITESPACE_CELL_CHAR } from './Buffer';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, WHITESPACE_CELL_CHAR, ElementInset } from './Buffer';


/** typed array slots taken by one cell */
Expand All @@ -26,6 +26,7 @@ export class BufferLine implements IBufferLine {
protected _data: Uint32Array | null = null;
protected _combined: {[index: number]: string} = {};
public length: number;
insetElements: ElementInset[] | null = null;

constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) {
if (!fillCharData) {
Expand Down
8 changes: 4 additions & 4 deletions src/BufferReflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { FILL_CHAR_DATA } from './Buffer';
import { BufferLine } from './BufferLine';
import { CircularList, IDeleteEvent } from './common/CircularList';
import { CircularList, IMoveEvent } from './common/CircularList';
import { IBufferLine } from './Types';

export interface INewLayoutResult {
Expand Down Expand Up @@ -119,10 +119,10 @@ export function reflowLargerCreateNewLayout(lines: CircularList<IBufferLine>, to
const countToRemove = toRemove[++nextToRemoveIndex];

// Tell markers that there was a deletion
lines.emit('delete', {
index: i - countRemovedSoFar,
lines.emit('move', {
start: i - countRemovedSoFar,
amount: countToRemove
} as IDeleteEvent);
} as IMoveEvent);

i += countToRemove - 1;
countRemovedSoFar += countToRemove;
Expand Down
2 changes: 2 additions & 0 deletions src/BufferSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
this._alt.clear();
this._activeBuffer = this._normal;
this.emit('activate', {
alt: false,
activeBuffer: this._normal,
inactiveBuffer: this._alt
});
Expand All @@ -88,6 +89,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
this._alt.y = this._normal.y;
this._activeBuffer = this._alt;
this.emit('activate', {
alt: true,
activeBuffer: this._alt,
inactiveBuffer: this._normal
});
Expand Down
56 changes: 53 additions & 3 deletions src/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
* @license MIT
*/

import { SixelImage } from './sixel-image';
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
import { C0, C1 } from './common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, ElementInset } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
Expand Down Expand Up @@ -39,7 +40,7 @@ class DECRQSS implements IDcsHandler {
constructor(private _terminal: any) { }

hook(collect: string, params: number[], flag: number): void {
this._data = new Uint32Array(0);
this._data = new Uint32Array(0); // REDUNDANT?
}

put(data: Uint32Array, start: number, end: number): void {
Expand Down Expand Up @@ -75,6 +76,40 @@ class DECRQSS implements IDcsHandler {
}
}

class SIXEL implements IDcsHandler {
private _data: Uint32Array = new Uint32Array(0);
constructor(private _terminal: any) { }
hook(collect: string, params: number[], flag: number): void {
this._data = new Uint32Array(0);
}
put(data: Uint32Array, start: number, end: number): void {
this._data = concat(this._data, data.subarray(start, end));
}
unhook(): void {
const buffer = this._terminal.buffer;
const six = new SixelImage();
six.write(this._data);

const w = six.width;
const h = six.height;
this._terminal._inputHandler.eraseInDisplay([0]);
const canvas = document.createElement('canvas');
canvas.setAttribute('width', String(w));
canvas.setAttribute('height', String(h));
const ctx = canvas.getContext('2d');
const idata = ctx.createImageData(w, h);
six.toImageData(idata.data, w, h);
ctx.putImageData(idata, 0, 0);

const image = new Image(w, h);
image.src = canvas.toDataURL();
const inset = new ElementInset(this._terminal, image);
const celly = buffer.y + buffer.ybase;
inset.put(celly, this._terminal.buffer.x);
inset.updatePosition(h, w);
}
}

/**
* DCS Ps; Ps| Pt ST
* DECUDK (https://vt100.net/docs/vt510-rm/DECUDK.html)
Expand Down Expand Up @@ -201,7 +236,6 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.setOscHandler(0, (data) => this.setTitle(data));
// 1 - icon name
// 2 - title
this._parser.setOscHandler(2, (data) => this.setTitle(data));
// 3 - set property X in the form "prop=value"
// 4 - Change Color Number
// 5 - Change Special Color Number
Expand All @@ -221,6 +255,18 @@ export class InputHandler extends Disposable implements IInputHandler {
// 50 - Set Font to Pt.
// 51 - reserved for Emacs shell.
// 52 - Manipulate Selection Data.
this._parser.setOscHandler(72, (data) => {
function scrub(htmlText: string) {
return htmlText; // FIXME
}
let terminal: any = this._terminal; // FIXME
const buffer = this._terminal.buffer;
const inset = new ElementInset(this._terminal, null);
inset.wrapper.insertAdjacentHTML("beforeend", scrub(data));
const celly = buffer.y + buffer.ybase;
inset.put(celly, terminal.buffer.x);
inset.updatePosition(inset.wrapper.clientHeight);
});
// 104 ; c - Reset Color Number c.
// 105 ; c - Reset Special Color Number c.
// 106 ; c; f - Enable/disable Special Color Number c.
Expand Down Expand Up @@ -276,6 +322,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* DCS handler
*/
this._parser.setDcsHandler('$q', new DECRQSS(this._terminal));
this._parser.setDcsHandler('q', new SIXEL(this._terminal));
}

public dispose(): void {
Expand Down Expand Up @@ -742,6 +789,7 @@ export class InputHandler extends Disposable implements IInputHandler {
case 0:
j = this._terminal.buffer.y;
this._terminal.updateRange(j);
this._terminal.buffer.lines.emitMayRemoveListeners('move', { start: j + this._terminal.buffer.ybase, amount: this._terminal.rows - j });
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols, this._terminal.buffer.x === 0);
for (; j < this._terminal.rows; j++) {
this._resetBufferLine(j);
Expand All @@ -764,6 +812,7 @@ export class InputHandler extends Disposable implements IInputHandler {
break;
case 2:
j = this._terminal.rows;
this._terminal.buffer.lines.emitMayRemoveListeners('move', { start: this._terminal.buffer.ybase, amount: j });
this._terminal.updateRange(j - 1);
while (j--) {
this._resetBufferLine(j);
Expand All @@ -779,6 +828,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
// Force a scroll event to refresh viewport
this._terminal.emit('scroll', 0);
this._terminal.buffer.lines.emit('move', { start: 0, end: scrollBackSize, amount: scrollBackSize });
}
break;
}
Expand Down
17 changes: 9 additions & 8 deletions src/SelectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*/

import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types';
import { XtermListener } from './common/Types';
import { XtermListener } from './common/Types'; // deprecated
import { IMoveEvent } from './common/CircularList';
import { MouseHelper } from './ui/MouseHelper';
import * as Browser from './core/Platform';
import { CharMeasure } from './ui/CharMeasure';
Expand Down Expand Up @@ -102,7 +103,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager

private _mouseMoveListener: EventListener;
private _mouseUpListener: EventListener;
private _trimListener: XtermListener;
private _moveListener: XtermListener;

private _mouseDownTimeStamp: number;

Expand Down Expand Up @@ -133,13 +134,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _initListeners(): void {
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
this._trimListener = (amount: number) => this._onTrim(amount);
this._moveListener = (event: IMoveEvent) => this._onMove(event);

this.initBuffersListeners();
}

public initBuffersListeners(): void {
this._terminal.buffer.lines.on('trim', this._trimListener);
this._terminal.buffer.lines.on('move', this._moveListener);
this._terminal.buffers.on('activate', e => this._onBufferActivate(e));
}

Expand Down Expand Up @@ -335,8 +336,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Handle the buffer being trimmed, adjust the selection position.
* @param amount The amount the buffer is being trimmed.
*/
private _onTrim(amount: number): void {
const needsRefresh = this._model.onTrim(amount);
private _onMove(event: IMoveEvent): void {
const needsRefresh = this._model.onMove(event);
if (needsRefresh) {
this.refresh();
}
Expand Down Expand Up @@ -658,8 +659,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
e.inactiveBuffer.lines.off('trim', this._trimListener);
e.activeBuffer.lines.on('trim', this._trimListener);
e.inactiveBuffer.lines.off('move', this._moveListener);
e.activeBuffer.lines.on('move', this._moveListener);
}

/**
Expand Down
Loading