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

Can xterm export more bufferline / chardata classes? #2105

Closed
vincentwoo opened this issue May 21, 2019 · 18 comments
Closed

Can xterm export more bufferline / chardata classes? #2105

vincentwoo opened this issue May 21, 2019 · 18 comments

Comments

@vincentwoo
Copy link
Contributor

vincentwoo commented May 21, 2019

If we want to modify the terminal contents from the outside, we do need to reach into the terminal guts on occasion. It would be nice to be able to easily instantiate CellData, for instance. Could we export more of these at the top level xterm module?

@Tyriar
Copy link
Member

Tyriar commented May 21, 2019

If you want to modify the terminal contents the best way to do that is to use write. Coming in 3.13 is the ability to read from the buffer in a easy way but this method is readonly, for example:

term.buffer.getLine(y).getCell(x).char

This API could change, mainly depending on how #2005 pans out.

xterm.js/typings/xterm.d.ts

Lines 930 to 1012 in 739723f

interface IBuffer {
/**
* The y position of the cursor. This ranges between `0` (when the
* cursor is at baseY) and `Terminal.rows - 1` (when the cursor is on the
* last row).
*/
readonly cursorY: number;
/**
* The x position of the cursor. This ranges between `0` (left side) and
* `Terminal.cols - 1` (right side).
*/
readonly cursorX: number;
/**
* The line within the buffer where the top of the viewport is.
*/
readonly viewportY: number;
/**
* The line within the buffer where the top of the bottom page is (when
* fully scrolled down);
*/
readonly baseY: number;
/**
* The amount of lines in the buffer.
*/
readonly length: number;
/**
* Gets a line from the buffer, or undefined if the line index does not exist.
*
* Note that the result of this function should be used immediately after calling as when the
* terminal updates it could lead to unexpected behavior.
*
* @param y The line index to get.
*/
getLine(y: number): IBufferLine | undefined;
}
interface IBufferLine {
/**
* Whether the line is wrapped from the previous line.
*/
readonly isWrapped: boolean;
/**
* Gets a cell from the line, or undefined if the line index does not exist.
*
* Note that the result of this function should be used immediately after calling as when the
* terminal updates it could lead to unexpected behavior.
*
* @param x The character index to get.
*/
getCell(x: number): IBufferCell;
/**
* Gets the line as a string. Note that this is gets only the string for the line, not taking
* isWrapped into account.
*
* @param trimRight Whether to trim any whitespace at the right of the line.
* @param startColumn The column to start from (inclusive).
* @param endColumn The column to end at (exclusive).
*/
translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string;
}
interface IBufferCell {
/**
* The character within the cell.
*/
readonly char: string;
/**
* The width of the character. Some examples:
*
* - This is `1` for most cells.
* - This is `2` for wide character like CJK glyphs.
* - This is `0` for cells immediately following cells with a width of `2`.
*/
readonly width: number;
}

@jerch
Copy link
Member

jerch commented May 21, 2019

I second @Tyriar's answer - direct buffer manipulation should not be encouraged by any API, its way to easy to break things with such an approach (thats the main reason why CellData only has reading access). Instead consider using escape sequences to reposition the cursor and to write your stuff as needed.

@Tyriar
Copy link
Member

Tyriar commented May 21, 2019

I'll close this off as it's possible via write and the upcoming API, I definitely still want us to support #595 which is probably related

@Tyriar Tyriar closed this as completed May 21, 2019
@vincentwoo
Copy link
Contributor Author

Basically, my approach currently is to have the server emit "placeholder" invisible characters that the client fills in later by modifying its own buffer.

I'm not very familiar with the VT100 escape sequences, so any guidance you can provide is very much appreciated. What would be the best way to make the cursor jump to an arbitrary point in the past, write out some new characters, and then return to where it was previously?

@Tyriar
Copy link
Member

Tyriar commented May 24, 2019

@vincentwoo grab the cursor position from term.buffer.cursorX/Y (also baseY if you need it) then use something like absolute cursor CUP (see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html), for example:

const row = 2;
const col = 3;
term.write(`\x1b[${row};${col}Hfoo`);

@vincentwoo
Copy link
Contributor Author

Just to clarify:

  1. Is there an easy way to get the coordinates of a placeholder emitted by the stream previously? The client doesn't know exactly when it'll need to do a replacement necessarily.

  2. After running a CUP, how do you return the cursor to its previous state? Do you pull the current cursor position and then restore to that?

@Tyriar
Copy link
Member

Tyriar commented May 24, 2019

@vincentwoo

  1. If you're writing the placeholder you should remember where you put it (rows y1-y2?)
  2. Run another CUP with the term.buffer.cursorY/X that you recorded earlier

@vincentwoo
Copy link
Contributor Author

I've taken the time to develop this technique, and at first I thought absolute CUP was working for our needs. Unfortunately, what I've found is that you cannot overwrite data that is scrolled outside of the current viewport with CUP. This was what was sort of nice about reaching into the buffer in our previous approach - xterm would correctly render what we wanted when scrolling.

Do you have any other suggestions for how to achieve this effect?

@Tyriar
Copy link
Member

Tyriar commented Nov 9, 2019

The plan is to expose the actual CellData just under a readonly interface in the API so that addons can achieve the same perf as internally when reading cells. #2075 (comment)

@vincentwoo
Copy link
Contributor Author

Hm. That may help for reads, but I assume that also means there's no plan to allow editing the buffer from outside?

@jerch
Copy link
Member

jerch commented Nov 9, 2019

Hm. That may help for reads, but I assume that also means there's no plan to allow editing the buffer from outside?

Thats true. Altering buffer content directly has to trigger several post actions, otherwise things can go wrong in very weird ways. Scrollback is seen as static in all emulators, none allows to alter content here. Our input handling with render triggers is bound to cursor actions and cursor adressible area (the lower rows in the terminal buffer), changing that would clearly break with the de-facto terminal standard. Thus we cannot do that.

For your problem at hand I see two possible hacky solutions:

  • temporarily resize the terminal to include more rows, this would expand the cursor range into scrollback area, thus any cursor commands + write will work as expected (but note that resizing twice between render cycles is not tested, you might have to temp. disable screen updates as well)
  • work with private addon shims: Ofc this involves much more maintenance work on your side, still addons can reference private stuff of xterm.js, if they are carefully crafted. Best way to achieve this:
    • fork repo
    • create custom addon within the fork with private access (watch out for not pulling half of xterm into the addon output, see other addons and their tsconfig how to avoid that)
    • do your private hooks (lit. all is allowed now), for your problem at hand:
      • look at BufferLine.ts how to insert/alter data for specific cells
      • follow flow control path of InputHandler.print to catch all needed post actions, put those on your shim as well (print manages the wrapped character input to the buffer, if you need more advanced stuff like colored input you might have to alter the attributes, see InputHandler.charAttributes and CellData.ts / AttributeData.ts)
      • watch out to not touch/alter terminal wide properties (or reset them to prev value before returning)
    • update fork from upstream to keep in line - thx to TS you can easily spot internal changes and adjust your shims. This will be the tedious part of that solution, as internal stuff is not guaranteed to be stable by any means. Still the buffer stuff is quite mature and unlikely to see big overhauls anytime soon.

In general I dont recommend the second way above, do this only if you have enough resources to put into the maintenance overhead.

@vincentwoo
Copy link
Contributor Author

vincentwoo commented Nov 12, 2019

I ended up just writing the internal packed array directly. This is probably not super sustainable, but it is somewhat straightforward to express once you understand the new buffer format. If this is of interest to any other anonymous readers out there:

// writing msg to point at row, col in the buffer
const lineData = term.buffer.getLine(row)._line._data

for (let i = 0; i < msg.length; i++) {
  lineData[(col + i) * /* CELL_SIZE */ 3 + /* Cell.CONTENT */ 0] =
    msg.charCodeAt(i) | (1 << /* Content.WIDTH_SHIFT */ 22)
}
term.refresh(row, row)

@jerch
Copy link
Member

jerch commented Nov 12, 2019

@vincentwoo I'd say you've found the most unreliable way to do it skipping really everything that those methods of bufferline try to encapsulate. Dont do this at home kids! 🤣

Seriously, this is broken in many ways, at least you should set the correct wcwidth and do proper UTF16 --> UTF32 conversion. Also attribs should be set.

@vincentwoo
Copy link
Contributor Author

Ya, I just happen to know the source text in this case is safe, all single-width chars. Definitely not a scalaeble solution :)

@jerch
Copy link
Member

jerch commented Nov 12, 2019

@vincentwoo Sorry I cannot let this go uncommented as it will fail at to many conditions and should not be used by others if they dont understand these limitations. The "official" way to set buffer content from a string would look more like this (untested):

import { StringToUtf32 } from 'common/input/TextDecoder';
import { wcwidth } from 'common/CharWidth';

const someString = '...';
const buffer = new Uint32Array(someString.length);
const stringDecoder = new StringToUtf32();
const length = stringDecoder.decode(someString, buffer);

// some start row
let bufferRow = term.buffer.getLine(...)._line;
// some start col offset
let pos = ...;
let lastPos = pos;

// some attrib fg / bg attrs (see common/buffer/Constants.ts for memory layout)
const fg = ...;
const bg = ...;

// walk the data
for (let i = 0; i < length; ++i) {
  const code = buffer[i];
  const chWidth = wcwidth(code);
  if (!chWidth) {
    // we have a combining / diacritical mark, should add to last active cell
    bufferRow.addCodepointToCell(lastPos, code);
    continue;
  }
  if (pos + chWidth >= bufferRow.length) {
    // re-position if we write beyond last cell in line
    bufferRow = ...;
    pos = ...;
  }
  lastPos = pos;
  bufferRow.setCellFromCodePoint(pos++, code, chWidth, fg, bg);
  if (chWidth > 0) {
    while (--cellWidth) {
      // wide char found, thus we have to add empty cells behind
      bufferRow.setCellFromCodePoint(pos++, 0, 0, fg, bg);
    }
  }
}

// since we didnt track the current scroll offset simply trigger
// a whole refresh when done
term._core?._dirtyRowService?.markDirty(0, term.rows);

Please dont try to circumvent basic guards (as you do above with writing directly to the memory), its as bad as putting some random data to a void* in C (minus the segfault risk). We have internal APIs for good reasons 😸.

@jerch
Copy link
Member

jerch commented Nov 12, 2019

Since handling attribs above is still a nightmare here a way to make it easier (untested):

import { Params } from 'common/parser/Params';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';

// store current terminal attrs
const savedAttr = term._core.curAttrData;

// start with default attrs
term._core.curAttrData = DEFAULT_ATTR_DATA.clone();

// now you can use Inputhandler.charAttributes to do the nasty attr construction for you
// it takes SGR params e.g. CSI 1;31 m for bold red fg --> [1, 31]
term._core._inputHandler.charAttributes(Params.fromArray([1, 31]));
const fg = term._core.curAttrData.fg;
const bg = term._core.curAttrData.bg;

// do the stuff above
...

// never forget to restore old attrs when done
term._core.curAttrData = savedAttr;

@Tyriar
Copy link
Member

Tyriar commented Nov 13, 2019

Did you consider serializing with the (wip) serialize addon, amending the buffer and writing that after calling Terminal.reset? Using any private api will eventually break and block you from updating until you fix it or come up with another solution. While doing a reset may not be ideal in other ways, it should always work.

@vincentwoo
Copy link
Contributor Author

ah, love 2 learn how to do things by posting the wrong way to do things :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants