@teaui/core
Version:
A high-level terminal UI library for Node
1,109 lines • 37.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Input = void 0;
const sys_1 = require("../sys");
const events_1 = require("../events");
const View_1 = require("../View");
const Style_1 = require("../Style");
const geometry_1 = require("../geometry");
const fonts_1 = require("./fonts");
const NL_SIGIL = '⤦';
/**
* Text input. Supports selection, word movement via alt+←→, single and multiline
* input, and wrapped lines.
*/
class Input extends View_1.View {
/**
* Array of graphemes, with pre-calculated length
*/
#placeholder = [];
#printableLines = [];
/**
* Cached after assignment - this is converted to #chars and #lines
*/
#value = '';
/**
* For easy edit operations. Gets converted to #lines for printing.
*/
#chars = [];
#wrappedLines = [];
// formatting options
#wrap = false;
#multiline = false;
#font = 'default';
#onChange;
#onSubmit;
// Printable width
#maxLineWidth = 0;
#cursor = { start: 0, end: 0 };
#visibleWidth = 0;
constructor(props = {}) {
super(props);
this.#update(props);
this.#cursor = { start: this.#chars.length, end: this.#chars.length };
}
update(props) {
this.#update(props);
super.update(props);
}
#update({ value, wrap, multiline, font, placeholder, onChange, onSubmit, }) {
this.#onChange = onChange;
this.#onSubmit = onSubmit;
this.#wrap = wrap ?? false;
this.#multiline = multiline ?? false;
this.#updatePlaceholderLines(placeholder ?? '');
this.#updateLines(sys_1.unicode.printableChars(value ?? ''), font ?? 'default');
}
#updatePlaceholderLines(placeholder) {
const placeholderLines = placeholder === ''
? []
: placeholder.split('\n').map(line => sys_1.unicode.printableChars(line));
this.#placeholder = placeholderLines.map(line => [
line,
line.reduce((w, c) => w + sys_1.unicode.charWidth(c), 0),
]);
}
#updateLines(_chars, font) {
let chars = _chars ?? this.#chars;
if (font === undefined) {
font = this.#font;
}
else {
this.#font = font;
}
const startIsAtEnd = this.#cursor.start === this.#chars.length;
const endIsAtEnd = this.#cursor.end === this.#chars.length;
if (chars.length > 0) {
if (!this.#multiline) {
chars = chars.map(char => (char === '\n' ? ' ' : char));
}
this.#value = chars.filter(char => !isAccentChar(char)).join('');
this.#chars = chars;
const [charLines] = this.#chars.reduce(([lines, line], char, index) => {
if (char === '\n') {
lines.push(line);
if (index === this.#chars.length - 1) {
lines.push([]);
}
return [lines, []];
}
line.push(char);
if (index === this.#chars.length - 1) {
lines.push(line);
return [lines, []];
}
return [lines, line];
}, [[], []]);
this.#printableLines = charLines.map((printableLine, index, all) => {
// every line needs a ' ' or NL_SIGIL at the end, for the EOL cursor
return [
printableLine.concat(index === all.length - 1 ? ' ' : NL_SIGIL),
printableLine.reduce((width, char) => width + sys_1.unicode.charWidth(char), 0) + 1,
];
});
}
else {
this.#value = '';
this.#printableLines = this.#placeholder.map(([line, width]) => {
return [line.concat(' '), width];
});
}
this.#visibleWidth = 0;
if (endIsAtEnd) {
this.#cursor.end = this.#chars.length;
}
else {
this.#cursor.end = Math.min(this.#cursor.end, this.#chars.length);
}
if (startIsAtEnd) {
this.#cursor.start = this.#chars.length;
}
else {
this.#cursor.start = Math.min(this.#cursor.start, this.#chars.length);
}
this.#maxLineWidth = this.#printableLines.reduce((maxWidth, [, width]) => {
// the _printable_ width, not the number of characters
return Math.max(maxWidth, width);
}, 0);
this.invalidateSize();
}
get value() {
return this.#value;
}
set value(value) {
if (value !== this.#value) {
this.#updateLines(sys_1.unicode.printableChars(value), undefined);
}
}
get placeholder() {
return this.#placeholder.map(([chars]) => chars.join('')).join('\n');
}
set placeholder(placeholder) {
this.#updatePlaceholderLines(placeholder ?? '');
}
get font() {
return this.#font;
}
set font(font) {
if (font !== this.#font) {
this.#updateLines(undefined, font);
}
}
get wrap() {
return this.#wrap;
}
set wrap(wrap) {
if (wrap !== this.#wrap) {
this.#wrap = wrap;
this.#updateLines(undefined, undefined);
}
}
get multiline() {
return this.#multiline;
}
set multiline(multiline) {
if (multiline !== this.#multiline) {
this.#multiline = multiline;
this.#updateLines(undefined, undefined);
}
}
naturalSize(available) {
let lines = this.#printableLines;
if (!lines.length || !available.width) {
return geometry_1.Size.one;
}
let height = 0;
if (this.#wrap) {
for (const [, width] of lines) {
// width + 1 because there should always be room for the cursor to be _after_
// the last character.
height += Math.ceil((width + 1) / available.width);
}
}
else {
height = lines.length;
}
return new geometry_1.Size(this.#maxLineWidth, height);
}
minSelected() {
return Math.min(this.#cursor.start, this.#cursor.end);
}
maxSelected() {
return isEmptySelection(this.#cursor)
? this.#cursor.start + 1
: Math.max(this.#cursor.start, this.#cursor.end);
}
receiveKey(event) {
const prevChars = this.#chars;
const prevText = this.#value;
let removeAccent = true;
if (event.name === 'enter' || event.name === 'return') {
if (this.#multiline) {
this.#receiveChar('\n', true);
}
else {
this.#onSubmit?.(this.#value);
return;
}
}
else if (event.full === 'C-a') {
this.#receiveGotoStart();
}
else if (event.full === 'C-e') {
this.#receiveGotoEnd();
}
else if (event.name === 'up') {
this.#receiveKeyUpArrow(event);
}
else if (event.name === 'down') {
this.#receiveKeyDownArrow(event);
}
else if (event.name === 'home') {
this.#receiveHome(event);
}
else if (event.name === 'end') {
this.#receiveEnd(event);
}
else if (event.name === 'left') {
this.#receiveKeyLeftArrow(event);
}
else if (event.name === 'right') {
this.#receiveKeyRightArrow(event);
}
else if (event.full === 'backspace') {
this.#receiveKeyBackspace();
}
else if (event.name === 'delete') {
this.#receiveKeyDelete();
}
else if (event.full === 'M-backspace' || event.full === 'C-w') {
this.#receiveKeyDeleteWord();
}
else if (isKeyAccent(event)) {
this.#receiveKeyAccent(event);
removeAccent = false;
}
else if ((0, events_1.isKeyPrintable)(event)) {
this.#receiveKeyPrintable(event);
}
if (removeAccent) {
this.#chars = this.#chars.filter(char => !isAccentChar(char));
}
if (prevChars !== this.#chars) {
this.#updateLines(this.#chars, undefined);
}
if (prevText !== this.#value) {
this.#onChange?.(this.#value);
}
}
receiveMouse(event, system) {
if (event.name === 'mouse.button.down') {
system.requestFocus();
}
}
render(viewport) {
const hasFocus = viewport.registerFocus();
if (viewport.isEmpty) {
return;
}
const visibleSize = viewport.contentSize;
if (hasFocus) {
viewport.registerTick();
}
viewport.registerMouse('mouse.button.left');
// cursorEnd: the location of the cursor relative to the text
// (ie if the text had been drawn at 0,0, cursorEnd is the screen location of
// the cursor)
// cursorPosition: the location of the cursor relative to the viewport
const [cursorEnd, cursorPosition] = this.#cursorPosition(visibleSize);
const cursorMin = this.#toPosition(this.minSelected(), visibleSize.width);
const cursorMax = this.#toPosition(this.maxSelected(), visibleSize.width);
// cursorVisible: the text location of the first line & char to draw
const cursorVisible = new geometry_1.Point(cursorEnd.x - cursorPosition.x, cursorEnd.y - cursorPosition.y);
let lines = this.#printableLines;
if (visibleSize.width !== this.#visibleWidth ||
this.#wrappedLines.length === 0) {
if (this.#wrap) {
lines = lines.flatMap(line => {
const wrappedLines = [];
let currentLine = [];
let currentWidth = 0;
for (const char of line[0]) {
const charWidth = sys_1.unicode.charWidth(char);
currentLine.push(char);
currentWidth += charWidth;
if (currentWidth >= visibleSize.width) {
wrappedLines.push([currentLine, currentWidth]);
currentLine = [];
currentWidth = 0;
}
}
if (currentLine.length) {
wrappedLines.push([currentLine, currentWidth]);
}
return wrappedLines;
});
}
this.#wrappedLines = lines;
this.#visibleWidth = visibleSize.width;
}
else {
lines = this.#wrappedLines;
}
let isPlaceholder = !Boolean(this.#chars.length);
let currentStyle = Style_1.Style.NONE;
const plainStyle = this.theme.text({
isPlaceholder,
hasFocus,
});
const selectedStyle = this.theme.text({
isSelected: true,
hasFocus,
});
const cursorStyle = plainStyle.merge({ underline: true });
const nlStyle = this.theme.text({ isPlaceholder: true });
const fontMap = this.#font && fonts_1.FONTS[this.#font];
viewport.usingPen(pen => {
let style = plainStyle;
const visibleLines = lines.slice(cursorVisible.y);
if (visibleLines.length === 0) {
visibleLines.push([[' '], 0]);
}
// is the viewport tall/wide enough to show ellipses …
const isTallEnough = viewport.contentSize.height > 4;
const isWideEnough = viewport.contentSize.width > 9;
// do we need to show vertical ellipses
const isTooTall = visibleLines.length > visibleSize.height;
// firstPoint is top-left corner of the viewport
const firstPoint = new geometry_1.Point(0, cursorVisible.y);
// lastPoint is bottom-right corner of the viewport
const lastPoint = new geometry_1.Point(visibleSize.width + cursorVisible.x - 1, cursorVisible.y + visibleSize.height - 1);
let scanTextPosition = firstPoint.mutableCopy();
for (const [line, width] of visibleLines) {
// used to determine whether to draw a final …
const isTooWide = this.#wrap
? false
: width - cursorVisible.x > viewport.contentSize.width;
// set to true if any character is skipped
let drawInitialEllipses = false;
scanTextPosition.x = 0;
for (let char of line) {
char = fontMap?.get(char) ?? char;
const charWidth = sys_1.unicode.charWidth(char);
if (scanTextPosition.x >= cursorVisible.x) {
const inSelection = isInSelection(cursorMin, cursorMax, scanTextPosition);
const inCursor = scanTextPosition.x === cursorEnd.x &&
scanTextPosition.y === cursorEnd.y;
const inNewline = char === NL_SIGIL && scanTextPosition.x + charWidth === width;
if (isEmptySelection(this.#cursor)) {
if (isAccentChar(char)) {
style = plainStyle.merge({ underline: true, inverse: true });
}
else if (hasFocus && inCursor) {
style = inNewline
? nlStyle.merge({ underline: true })
: cursorStyle;
}
else if (inNewline) {
style = nlStyle;
}
else {
style = plainStyle;
}
}
else {
if (inSelection) {
style = inNewline
? nlStyle.merge({ background: selectedStyle.foreground })
: selectedStyle.merge({ underline: inCursor });
}
else if (inNewline) {
style = nlStyle;
}
else {
style = plainStyle;
}
}
if (!currentStyle.isEqual(style)) {
pen.replacePen(style);
currentStyle = style;
}
let drawEllipses = false;
if (cursorVisible.y > 0 && scanTextPosition.isEqual(firstPoint)) {
drawEllipses = isTallEnough;
}
else if (isTooTall && scanTextPosition.isEqual(lastPoint)) {
drawEllipses = isTallEnough;
}
else if (isWideEnough) {
if (drawInitialEllipses) {
drawEllipses = true;
}
else if (isTooWide &&
scanTextPosition.x - cursorVisible.x + charWidth >=
viewport.contentSize.width) {
drawEllipses = true;
}
}
viewport.write(drawEllipses ? '…' : char, scanTextPosition.offset(-cursorVisible.x, -cursorVisible.y));
drawInitialEllipses = false;
}
else {
drawInitialEllipses = true;
}
scanTextPosition.x += charWidth;
if (scanTextPosition.x - cursorVisible.x >=
viewport.contentSize.width) {
break;
}
}
scanTextPosition.y += 1;
if (scanTextPosition.y - cursorVisible.y >=
viewport.contentSize.height) {
break;
}
}
});
}
/**
* The position of the character that is at the desired cursor offset, taking
* character widths into account, relative to the text (as if the text were drawn
* at 0,0), and 'wrap' setting.
*/
#toPosition(offset, visibleWidth) {
if (this.#wrap) {
let y = 0, index = 0;
let x = 0;
// immediately after a line wrap, we don't want to also increase y by 1
let isFirst = true;
for (const [chars] of this.#printableLines) {
if (!isFirst) {
y += 1;
}
isFirst = false;
x = 0;
for (const char of chars) {
if (index === offset) {
if (x === visibleWidth) {
x = 0;
y += 1;
}
return new geometry_1.Point(x, y);
}
const charWidth = sys_1.unicode.charWidth(char);
if (x + charWidth > visibleWidth) {
x = charWidth;
y += 1;
}
else {
x += charWidth;
}
index += 1;
}
}
return new geometry_1.Point(x, y);
}
let y = 0, index = 0;
for (const [chars] of this.#printableLines) {
if (index + chars.length > offset) {
let x = 0;
for (const char of chars.slice(0, offset - index)) {
x += sys_1.unicode.charWidth(char);
}
return new geometry_1.Point({ x, y });
}
index += chars.length;
y += 1;
}
return new geometry_1.Point(0, y);
}
/**
* Returns the cursor offset that points to the character at the desired screen
* position, taking into account character widths.
*/
#toOffset(position, visibleWidth) {
if (this.#wrap) {
let y = 0, index = 0;
let x = 0;
for (const [chars] of this.#printableLines) {
if (y) {
y += 1;
}
x = 0;
for (const char of chars) {
if (position.isEqual(x, y)) {
return index;
}
const charWidth = sys_1.unicode.charWidth(char);
if (x + charWidth >= visibleWidth) {
x = 0;
y += 1;
index += 1;
}
else {
x += charWidth;
index += 1;
}
}
}
return index;
}
else {
if (position.y >= this.#printableLines.length) {
return this.#chars.length;
}
let y = 0, index = 0;
for (const [chars, width] of this.#printableLines) {
if (y === position.y) {
let x = 0;
for (const char of chars) {
x += sys_1.unicode.charWidth(char);
if (x > position.x) {
return index;
}
index += 1;
}
return index;
}
y += 1;
index += chars.length + 1;
}
return this.#chars.length;
}
}
/**
* Determine the position of the cursor, relative to the viewport, based on the
* text and viewport sizes.
*
* The cursor is placed so that it will appear at the start or end of the viewport
* when it is near the start or end of the line, otherwise it tries to be centered.
*/
#cursorPosition(visibleSize) {
const halfWidth = Math.floor(visibleSize.width / 2);
const halfHeight = Math.floor(visibleSize.height / 2);
// the cursor, relative to the start of text (as if all text was visible),
// ie in the "coordinate system" of the text.
let cursorEnd = this.#toPosition(this.#cursor.end, visibleSize.width);
let currentLineWidth, totalHeight;
if (!this.#printableLines.length) {
return [cursorEnd, new geometry_1.Point(0, 0)];
}
if (this.#wrap) {
// run through the lines until we get to our desired cursorEnd.y
// but also add all the heights to calculate currentHeight
let h = 0;
currentLineWidth = -1;
totalHeight = 0;
for (const [, width] of this.#printableLines) {
const dh = Math.ceil(width / visibleSize.width);
totalHeight += dh;
if (currentLineWidth === -1 && dh >= cursorEnd.y) {
if (cursorEnd.y - h === dh) {
// the cursor is on the last wrapped line, use modulo divide to calculate the
// last line width, add 1 for the EOL cursor
currentLineWidth = (visibleSize.width % width) + 1;
}
else {
currentLineWidth = visibleSize.width;
}
break;
}
}
currentLineWidth = Math.max(0, currentLineWidth);
}
else {
currentLineWidth = this.#printableLines[cursorEnd.y]?.[1] ?? 0;
totalHeight = this.#printableLines.length;
}
// Calculate the viewport location where the cursor will be drawn
// x location:
let cursorX;
if (currentLineWidth <= visibleSize.width) {
// If the viewport can accommodate the entire line
// draw the cursor at its natural location.
cursorX = cursorEnd.x;
}
else if (cursorEnd.x < halfWidth) {
// If the cursor is at the start of the line
// place the cursor at the start of the viewport
cursorX = cursorEnd.x;
}
else if (cursorEnd.x > currentLineWidth - halfWidth) {
// or if the cursor is at the end of the line
// draw it at the end of the viewport
cursorX = visibleSize.width - currentLineWidth + cursorEnd.x;
}
else {
// otherwise place it in the middle.
cursorX = halfWidth;
}
// y location:
let cursorY;
if (totalHeight <= visibleSize.height) {
// If the viewport can accommodate the entire height
// draw the cursor at its natural location.
cursorY = cursorEnd.y;
}
else if (cursorEnd.y < halfHeight) {
// If the cursor is at the start of the text
// place the cursor at the start of the viewport
cursorY = cursorEnd.y;
}
else if (cursorEnd.y >= totalHeight - halfHeight) {
// or if the cursor is at the end of the text
// draw it at the end of the viewport
cursorY = visibleSize.height - totalHeight + cursorEnd.y;
}
else {
// otherwise place it in the middle.
cursorY = halfHeight;
}
// The viewport location where the cursor will be drawn
return [cursorEnd, new geometry_1.Point(cursorX, cursorY)];
}
#receiveKeyAccent(event) {
this.#chars = this.#chars.filter(char => !isAccentChar(char));
let char = ACCENT_KEYS[event.full];
if (!char) {
return;
}
this.#receiveChar(char, false);
}
#receiveKeyPrintable({ char }) {
if (this.#cursor.start === this.#cursor.end &&
isAccentChar(this.#chars[this.#cursor.start])) {
// if character under cursor is an accent, replace it.
const accented = accentChar(this.#chars[this.#cursor.start], char);
this.#receiveChar(accented, true);
return;
}
this.#receiveChar(char, true);
}
#receiveChar(char, advance) {
if (isEmptySelection(this.#cursor)) {
this.#chars = this.#chars
.slice(0, this.#cursor.start)
.concat(char, this.#chars.slice(this.#cursor.start));
this.#cursor.start = this.#cursor.end =
this.#cursor.start + (advance ? 1 : 0);
}
else {
this.#chars = this.#chars
.slice(0, this.minSelected())
.concat(char, this.#chars.slice(this.maxSelected()));
this.#cursor.start = this.#cursor.end =
this.minSelected() + (advance ? 1 : 0);
}
}
#receiveGotoStart() {
this.#cursor = { start: 0, end: 0 };
}
#receiveGotoEnd() {
this.#cursor = { start: this.#chars.length, end: this.#chars.length };
}
#receiveHome({ shift }) {
let dest = 0;
// move the cursor to the previous line, moving the cursor until it is at the
// same X position.
let cursorPosition = this.#toPosition(this.#cursor.end, this.#visibleWidth).mutableCopy();
if (cursorPosition.y === 0) {
dest = 0;
}
else {
const [targetChars, targetWidth] = this.#wrappedLines[cursorPosition.y];
dest = this.#wrappedLines
.slice(0, cursorPosition.y)
.reduce((dest, [, width]) => {
return dest + width;
}, 0);
}
if (shift) {
this.#cursor.end = dest;
}
else {
this.#cursor = { start: dest, end: dest };
}
}
#receiveEnd({ shift }) {
let dest = 0;
// move the cursor to the next line, moving the cursor until it is at the
// same X position.
let cursorPosition = this.#toPosition(this.#cursor.end, this.#visibleWidth).mutableCopy();
if (cursorPosition.y === this.#wrappedLines.length - 1) {
dest = this.#chars.length;
}
else {
const [targetChars, targetWidth] = this.#wrappedLines[cursorPosition.y + 1];
dest =
this.#wrappedLines
.slice(0, cursorPosition.y + 1)
.reduce((dest, [, width]) => {
return dest + width;
}, 0) - 1;
}
if (shift) {
this.#cursor.end = dest;
}
else {
this.#cursor = { start: dest, end: dest };
}
}
#receiveKeyUpArrow({ shift }) {
let dest = 0;
// move the cursor to the previous line, moving the cursor until it is at the
// same X position.
let cursorPosition = this.#toPosition(this.#cursor.end, this.#visibleWidth).mutableCopy();
if (cursorPosition.y === 0) {
dest = 0;
}
else if (cursorPosition.y <= this.#wrappedLines.length) {
const [targetChars, targetWidth] = this.#wrappedLines[cursorPosition.y - 1];
dest = this.#wrappedLines
.slice(0, cursorPosition.y - 1)
.reduce((dest, [, width]) => {
return dest + width;
}, 0);
if (targetWidth <= cursorPosition.x) {
dest += targetWidth - 1;
}
else {
let destOffset = 0;
for (const char of targetChars) {
const charWidth = sys_1.unicode.charWidth(char);
if (destOffset + charWidth > cursorPosition.x) {
break;
}
destOffset += 1;
}
dest += destOffset;
}
}
if (shift) {
this.#cursor.end = dest;
}
else {
this.#cursor = { start: dest, end: dest };
}
}
#receiveKeyDownArrow({ shift }) {
let dest = 0;
// move the cursor to the next line, moving the cursor until it is at the
// same X position.
let cursorPosition = this.#toPosition(this.#cursor.end, this.#visibleWidth).mutableCopy();
if (cursorPosition.y === this.#wrappedLines.length - 1 ||
this.#wrappedLines.length === 0) {
dest = this.#chars.length;
}
else {
const [targetChars, targetWidth] = this.#wrappedLines[cursorPosition.y + 1];
dest = this.#wrappedLines
.slice(0, cursorPosition.y + 1)
.reduce((dest, [, width]) => {
return dest + width;
}, 0);
if (targetWidth <= cursorPosition.x) {
dest += targetWidth - 1;
}
else {
let destOffset = 0;
for (const char of targetChars) {
const charWidth = sys_1.unicode.charWidth(char);
if (destOffset + charWidth > cursorPosition.x) {
break;
}
destOffset += 1;
}
dest += destOffset;
}
}
if (shift) {
this.#cursor.end = dest;
}
else {
this.#cursor = { start: dest, end: dest };
}
}
#prevWordOffset(shift) {
let cursor;
if (shift) {
cursor = this.#cursor.end;
}
else if (isEmptySelection(this.#cursor)) {
cursor = this.#cursor.start;
}
else {
cursor = this.minSelected();
}
let prevWordOffset = 0;
for (const [chars, offset] of sys_1.unicode.words(this.#chars)) {
prevWordOffset = offset;
if (cursor <= offset + chars.length) {
break;
}
}
return prevWordOffset;
}
#nextWordOffset(shift) {
let cursor;
if (shift) {
cursor = this.#cursor.end;
}
else if (isEmptySelection(this.#cursor)) {
cursor = this.#cursor.start;
}
else {
cursor = this.maxSelected();
}
let nextWordOffset = 0;
for (const [chars, offset] of sys_1.unicode.words(this.#chars)) {
nextWordOffset = offset + chars.length;
if (cursor < offset + chars.length) {
break;
}
}
return nextWordOffset;
}
#receiveKeyLeftArrow({ shift, meta }) {
if (meta) {
const prevWordOffset = this.#prevWordOffset(shift);
if (shift) {
this.#cursor.end = prevWordOffset;
}
else {
this.#cursor.start = this.#cursor.end = prevWordOffset;
}
}
else if (shift) {
this.#cursor.end = Math.max(0, this.#cursor.end - 1);
}
else if (isEmptySelection(this.#cursor)) {
this.#cursor.start = this.#cursor.end = Math.max(0, this.#cursor.start - 1);
}
else {
this.#cursor.start = this.#cursor.end = this.minSelected();
}
}
#receiveKeyRightArrow({ shift, meta }) {
if (meta) {
const nextWordOffset = this.#nextWordOffset(shift);
if (shift) {
this.#cursor.end = nextWordOffset;
}
else {
this.#cursor.start = this.#cursor.end = nextWordOffset;
}
}
else if (shift) {
this.#cursor.end = Math.min(this.#chars.length, this.#cursor.end + 1);
}
else if (isEmptySelection(this.#cursor)) {
this.#cursor.start = this.#cursor.end = Math.min(this.#chars.length, this.#cursor.start + 1);
}
else {
this.#cursor.start = this.#cursor.end = this.maxSelected();
}
}
#updateWidth() {
this.#maxLineWidth = this.#chars
.map(sys_1.unicode.charWidth)
.reduce((a, b) => a + b, 0);
}
#deleteSelection() {
this.#chars = this.#chars
.slice(0, this.minSelected())
.concat(this.#chars.slice(this.maxSelected()));
this.#cursor.start = this.#cursor.end = this.minSelected();
this.#updateWidth();
}
#receiveKeyBackspace() {
if (isEmptySelection(this.#cursor)) {
if (this.#cursor.start === 0) {
return;
}
this.#chars = this.#chars
.slice(0, this.#cursor.start - 1)
.concat(this.#chars.slice(this.#cursor.start));
this.#cursor.start = this.#cursor.end = this.#cursor.start - 1;
}
else {
this.#deleteSelection();
}
}
#receiveKeyDelete() {
if (isEmptySelection(this.#cursor)) {
if (this.#cursor.start > this.#chars.length - 1) {
return;
}
this.#maxLineWidth -= sys_1.unicode.charWidth(this.#chars[this.#cursor.start]);
this.#chars = this.#chars
.slice(0, this.#cursor.start)
.concat(this.#chars.slice(this.#cursor.start + 1));
}
else {
this.#deleteSelection();
}
}
#receiveKeyDeleteWord() {
if (!isEmptySelection(this.#cursor)) {
return this.#deleteSelection();
}
if (this.#cursor.start === 0) {
return;
}
const offset = this.#prevWordOffset(false);
this.#chars = this.#chars
.slice(0, offset)
.concat(this.#chars.slice(this.#cursor.start));
this.#cursor.start = this.#cursor.end = offset;
this.#updateWidth();
}
}
exports.Input = Input;
function isEmptySelection(cursor) {
return cursor.start === cursor.end;
}
function isInSelection(cursorMin, cursorMax, scanTextPosition) {
if (scanTextPosition.y < cursorMin.y || scanTextPosition.y > cursorMax.y) {
return false;
}
if (scanTextPosition.y === cursorMin.y) {
if (scanTextPosition.x < cursorMin.x) {
return false;
}
}
if (scanTextPosition.y === cursorMax.y) {
if (scanTextPosition.x >= cursorMax.x) {
return false;
}
}
return true;
}
function isAccentChar(char) {
return ACCENTS[char] !== undefined;
}
const ACCENTS = {
'‵': {
A: 'À',
E: 'È',
I: 'Ì',
O: 'Ò',
U: 'Ù',
N: 'Ǹ',
a: 'à',
e: 'è',
i: 'ì',
o: 'ò',
u: 'ù',
n: 'ǹ',
},
'¸': {
C: 'Ç',
D: 'Ḑ',
E: 'Ȩ',
G: 'Ģ',
H: 'Ḩ',
K: 'Ķ',
L: 'Ļ',
N: 'Ņ',
R: 'Ŗ',
S: 'Ş',
T: 'Ţ',
c: 'ç',
d: 'ḑ',
e: 'ȩ',
g: 'ģ',
h: 'ḩ',
k: 'ķ',
l: 'ļ',
n: 'ņ',
r: 'ŗ',
s: 'ş',
t: 'ţ',
},
'´': {
A: 'Á',
C: 'Ć',
E: 'É',
G: 'Ǵ',
I: 'Í',
K: 'Ḱ',
L: 'Ĺ',
M: 'Ḿ',
N: 'Ń',
O: 'Ó',
P: 'Ṕ',
R: 'Ŕ',
S: 'Ś',
U: 'Ú',
W: 'Ẃ',
Y: 'Ý',
a: 'á',
c: 'ć',
e: 'é',
g: 'ǵ',
i: 'í',
k: 'ḱ',
l: 'ĺ',
m: 'ḿ',
n: 'ń',
o: 'ó',
p: 'ṕ',
r: 'ŕ',
s: 'ś',
u: 'ú',
w: 'ẃ',
y: 'ý',
},
ˆ: {
A: 'Â',
C: 'Ĉ',
E: 'Ê',
G: 'Ĝ',
H: 'Ĥ',
I: 'Î',
J: 'Ĵ',
O: 'Ô',
S: 'Ŝ',
U: 'Û',
W: 'Ŵ',
Y: 'Ŷ',
a: 'â',
c: 'ĉ',
e: 'ê',
g: 'ĝ',
h: 'ĥ',
i: 'î',
j: 'ĵ',
o: 'ô',
s: 'ŝ',
u: 'û',
w: 'ŵ',
y: 'ŷ',
},
'˜': {
A: 'Ã',
I: 'Ĩ',
N: 'Ñ',
O: 'Õ',
U: 'Ũ',
Y: 'Ỹ',
a: 'ã',
i: 'ĩ',
n: 'ñ',
o: 'õ',
u: 'ũ',
y: 'ỹ',
},
'¯': {
A: 'Ā',
E: 'Ē',
I: 'Ī',
O: 'Ō',
U: 'Ū',
Y: 'Ȳ',
a: 'ā',
e: 'ē',
i: 'ī',
o: 'ō',
u: 'ū',
y: 'ȳ',
},
'¨': {
A: 'Ä',
E: 'Ë',
I: 'Ï',
O: 'Ö',
U: 'Ü',
W: 'Ẅ',
X: 'Ẍ',
Y: 'Ÿ',
a: 'ä',
e: 'ë',
i: 'ï',
o: 'ö',
u: 'ü',
w: 'ẅ',
x: 'ẍ',
y: 'ÿ',
},
};
const ACCENT_KEYS = {
'M-a': '‵',
'M-c': '¸',
'M-e': '´',
'M-i': 'ˆ',
'M-n': '˜',
'M-o': '¯',
'M-s': '¸',
'M-u': '¨',
};
function accentChar(accent, char) {
return ACCENTS[accent]?.[char] ?? char;
}
function isKeyAccent(event) {
if (!event.meta || event.ctrl) {
return false;
}
return ACCENT_KEYS[event.full] !== undefined;
}
//# sourceMappingURL=Input.js.map