From 03bd1beecaca1791ec12f96481816170f2f36149 Mon Sep 17 00:00:00 2001 From: Compositr <43405050+Compositr@users.noreply.github.com> Date: Thu, 25 May 2023 21:45:03 +1000 Subject: [PATCH 01/14] Handle tabs and deletes Make it so "special" keys such as home don't break the prompt (ignores) --- src/components/widgets/InputPopup.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index dddd209..6334fc1 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -194,7 +194,7 @@ export class InputPopup extends EventEmitter { return } // Continue only if the result is 0 const v = this.value - if (v.toString().length < 20) { + if (v.toString().length < 20 && key.sequence.length === 1) { let tmp = v.toString() tmp += key.sequence this.value = tmp @@ -230,6 +230,18 @@ export class InputPopup extends EventEmitter { //delete this } break + case "delete": + { + // no-op for now + } + break; + case "tab": + { + // Add two spaces + this.value = v.toString() + " " + } + break; + default: break } From b3a534b061d58e7dcedbb694607130f6ae2af884 Mon Sep 17 00:00:00 2001 From: Compositr <43405050+Compositr@users.noreply.github.com> Date: Thu, 25 May 2023 22:05:08 +1000 Subject: [PATCH 02/14] lint: make eslint happy --- src/components/widgets/InputPopup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index 6334fc1..b5050d8 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -234,13 +234,13 @@ export class InputPopup extends EventEmitter { { // no-op for now } - break; + break case "tab": { // Add two spaces this.value = v.toString() + " " } - break; + break default: break From 44ca5bc088385fae98ada0cdbac059d0b8cb932f Mon Sep 17 00:00:00 2001 From: Compositr <43405050+Compositr@users.noreply.github.com> Date: Thu, 25 May 2023 22:06:17 +1000 Subject: [PATCH 03/14] Force CRLF on settings.json to match eslint --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fa03d4e..7041835 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, - "eslint.validate": ["javascript"] + "eslint.validate": ["javascript"], + "files.eol": "\r\n" } \ No newline at end of file From 5f3d457bdee41e013d287a7e1076209aa13fee5a Mon Sep 17 00:00:00 2001 From: Compositr Date: Fri, 26 May 2023 14:45:26 +1000 Subject: [PATCH 04/14] Prioritise switch/cases over input Signed-off-by: Compositr --- src/components/widgets/InputPopup.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index b5050d8..53e7b93 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -194,11 +194,6 @@ export class InputPopup extends EventEmitter { return } // Continue only if the result is 0 const v = this.value - if (v.toString().length < 20 && key.sequence.length === 1) { - let tmp = v.toString() - tmp += key.sequence - this.value = tmp - } switch (key.name) { case "backspace": // If backspace is pressed I remove the last character from the typed value @@ -243,6 +238,11 @@ export class InputPopup extends EventEmitter { break default: + if (v.toString().length < 20 && key.sequence.length === 1) { + let tmp = v.toString() + tmp += key.sequence + this.value = tmp + } break } this.CM.refresh() From 74703f802f7c9136e0ed7334b138cddda04ea6cb Mon Sep 17 00:00:00 2001 From: Compositr Date: Fri, 26 May 2023 14:49:39 +1000 Subject: [PATCH 05/14] Add cursor position (no implementation) Signed-off-by: Compositr --- src/components/widgets/InputPopup.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index 53e7b93..f17587a 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -52,6 +52,8 @@ export class InputPopup extends EventEmitter { readonly id: string title: string value: string | number + // Position of the cursor. 0-indexed (0 = before all the text) + cursorPos: number private numeric: boolean private visible: boolean private marginTop: number @@ -77,6 +79,7 @@ export class InputPopup extends EventEmitter { this.id = id this.title = title this.value = value + this.cursorPos = 0 this.numeric = numeric || false this.visible = visible this.marginTop = 4 From 7f0348260e8e6e2ebd05f98ce2f3af7b14c4aedc Mon Sep 17 00:00:00 2001 From: Compositr Date: Fri, 26 May 2023 14:59:31 +1000 Subject: [PATCH 06/14] Fix NaN appearing when backspacing a negative Signed-off-by: Compositr --- src/components/widgets/InputPopup.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index f17587a..cc6b8e7 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -144,7 +144,10 @@ export class InputPopup extends EventEmitter { this.value = v.toString() } else if (this.value.toString().indexOf(".") === this.value.toString().length - 2) { this.value = this.value.toString().slice(0, this.value.toString().length - 1) - } else { + } else if (this.value.toString().indexOf("-") === 0 && this.value.toString().length === 2) { + this.value = 0 + } + else { this.value = Number(v.toString().slice(0, v.toString().length - 1)) } } From aac4364c84af3b0d4019215bc37ccb8440768425 Mon Sep 17 00:00:00 2001 From: Compositr Date: Fri, 26 May 2023 15:13:12 +1000 Subject: [PATCH 07/14] (not working) placeholder code Signed-off-by: Compositr --- examples/tcp_simulator.mjs | 1 + src/components/widgets/InputPopup.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/tcp_simulator.mjs b/examples/tcp_simulator.mjs index d5cbcfe..203048e 100644 --- a/examples/tcp_simulator.mjs +++ b/examples/tcp_simulator.mjs @@ -268,6 +268,7 @@ GUI.on("keypressed", (key) => { id: "popupTypeMax", title: "Type max value", value: max, + placeholder: "100", numeric: true }).show().on("confirm", (_max) => { max = _max diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index cc6b8e7..c4896ad 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -2,6 +2,7 @@ import { EventEmitter } from "events" import { ConsoleManager, KeyListenerArgs, EOL } from "../../ConsoleGui.js" import { MouseEvent } from "../MouseManager.js" import { boxChars, PhisicalValues } from "../Utils.js" +import chalk from "chalk" /** * @description The configuration for the InputPopup class. @@ -12,6 +13,7 @@ import { boxChars, PhisicalValues } from "../Utils.js" * @prop {string | number} value - The value of the popup. * @prop {boolean} numeric - If the input is numeric. * @prop {boolean} [visible] - If the popup is visible. + * @prop {string} [placeholder] - Optional placeholder to show if empty * * @export * @interface InputPopupConfig @@ -23,6 +25,7 @@ export interface InputPopupConfig { value: string | number, numeric?: boolean, visible?: boolean, + placeholder?: string } /** @@ -66,6 +69,7 @@ export class InputPopup extends EventEmitter { private dragging = false private dragStart: { x: number, y: number } = { x: 0, y: 0 } private focused = false + private placeholder?: string public constructor(config: InputPopupConfig) { if (!config) throw new Error("InputPopup config is required") @@ -91,6 +95,7 @@ export class InputPopup extends EventEmitter { width: 0, height: 0, } + this.placeholder = config.placeholder if (this.CM.popupCollection[this.id]) { this.CM.unregisterPopup(this) const message = `InputPopup ${this.id} already exists.` @@ -438,7 +443,14 @@ export class InputPopup extends EventEmitter { let content = "" // Draw an input field - content += `${boxChars["normal"].vertical}${"> "}${this.value}█${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical}${EOL}` + if(this.value.toString().length === 0 && this.placeholder?.length) + content += `${boxChars["normal"].vertical}${"> "}█${chalk.dim.gray( + `${this.placeholder}` + )}${" ".repeat(windowWidth - this.value.toString().length - 3)}${ + boxChars["normal"].vertical + }${EOL}` + else + content += `${boxChars["normal"].vertical}${"> "}${this.value}█${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical}${EOL}` const windowDesign = `${header}${content}${footer}` const windowDesignLines = windowDesign.split(EOL) From 3d90157e01f5a4bd594e34570cd63645d172c3ce Mon Sep 17 00:00:00 2001 From: Compositr <43405050+Compositr@users.noreply.github.com> Date: Fri, 26 May 2023 16:41:16 +1000 Subject: [PATCH 08/14] lint --- src/components/widgets/InputPopup.ts | 218 ++++++++++++++++----------- 1 file changed, 130 insertions(+), 88 deletions(-) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index c4896ad..b90fc43 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -7,7 +7,7 @@ import chalk from "chalk" /** * @description The configuration for the InputPopup class. * @typedef {Object} InputPopupConfig - * + * * @prop {string} id - The id of the popup. * @prop {string} title - The title of the popup. * @prop {string | number} value - The value of the popup. @@ -31,21 +31,21 @@ export interface InputPopupConfig { /** * @class InputPopup * @extends EventEmitter - * @description This class is used to create a popup with a text or numeric input. - * + * @description This class is used to create a popup with a text or numeric input. + * * ![InputPopup](https://user-images.githubusercontent.com/14907987/165752281-e836b862-a54a-48d5-b4e7-954374d6509f.gif) - * - * Emits the following events: + * + * Emits the following events: * - "confirm" when the user confirm the input * - "cancel" when the user cancel the input * - "exit" when the user exit the input * @param {InputPopupConfig} config - The config of the popup. - * - * @example ```ts + * + * @example ```ts * const popup = new InputPopup({ - * id: "popup1", - * title: "Choose the number", - * value: selectedNumber, + * id: "popup1", + * title: "Choose the number", + * value: selectedNumber, * numeric: true * }).show().on("confirm", (value) => { console.log(value) }) // show the popup and wait for the user to confirm * ``` @@ -67,7 +67,7 @@ export class InputPopup extends EventEmitter { private offsetY: number private absoluteValues: PhisicalValues private dragging = false - private dragStart: { x: number, y: number } = { x: 0, y: 0 } + private dragStart: { x: number; y: number } = { x: 0, y: 0 } private focused = false private placeholder?: string @@ -106,12 +106,12 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to make the ConsoleManager handle the key events when the input is numeric and it is showed. - * Inside this function are defined all the keys that can be pressed and the actions to do when they are pressed. - * @param {string} _str - The string of the input. - * @param {Object} key - The key object. - * @memberof InputPopup - */ + * @description This function is used to make the ConsoleManager handle the key events when the input is numeric and it is showed. + * Inside this function are defined all the keys that can be pressed and the actions to do when they are pressed. + * @param {string} _str - The string of the input. + * @param {Object} key - The key object. + * @memberof InputPopup + */ public keyListenerNumeric(_str: string, key: KeyListenerArgs): void { const checkResult = this.CM.mouse.isMouseFrame(key, this.parsingMouseFrame) if (checkResult === 1) { @@ -145,7 +145,10 @@ export class InputPopup extends EventEmitter { case "backspace": // If backspace is pressed I remove the last character from the typed value if (this.value.toString().length > 0) { - if (this.value.toString().indexOf(".") === this.value.toString().length - 1) { + if ( + this.value.toString().indexOf(".") === + this.value.toString().length - 1 + ) { this.value = v.toString() } else if (this.value.toString().indexOf(".") === this.value.toString().length - 2) { this.value = this.value.toString().slice(0, this.value.toString().length - 1) @@ -189,12 +192,12 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to make the ConsoleManager handle the key events when the input is text and it is showed. - * Inside this function are defined all the keys that can be pressed and the actions to do when they are pressed. - * @param {string} _str - The string of the input. - * @param {Object} key - The key object. - * @memberof InputPopup - */ + * @description This function is used to make the ConsoleManager handle the key events when the input is text and it is showed. + * Inside this function are defined all the keys that can be pressed and the actions to do when they are pressed. + * @param {string} _str - The string of the input. + * @param {Object} key - The key object. + * @memberof InputPopup + */ public keyListenerText(_str: string, key: KeyListenerArgs): void { const checkResult = this.CM.mouse.isMouseFrame(key, this.parsingMouseFrame) if (checkResult === 1) { @@ -207,7 +210,7 @@ export class InputPopup extends EventEmitter { const v = this.value switch (key.name) { case "backspace": - // If backspace is pressed I remove the last character from the typed value + // If backspace is pressed I remove the last character from the typed value if (v.toString().length > 0) { this.value = v.toString().slice(0, v.toString().length - 1) } @@ -247,7 +250,7 @@ export class InputPopup extends EventEmitter { this.value = v.toString() + " " } break - + default: if (v.toString().length < 20 && key.sequence.length === 1) { let tmp = v.toString() @@ -260,20 +263,20 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to get the value of the input. - * @returns {string | number} The value of the input. - * @memberof InputPopup - */ + * @description This function is used to get the value of the input. + * @returns {string | number} The value of the input. + * @memberof InputPopup + */ public getValue(): string | number { return this.value } /** - * @description This function is used to change the value of the input. It also refresh the ConsoleManager. - * @param {string | number} newValue - The new value of the input. - * @memberof InputPopup - * @returns {InputPopup} The instance of the InputPopup. - */ + * @description This function is used to change the value of the input. It also refresh the ConsoleManager. + * @param {string | number} newValue - The new value of the input. + * @memberof InputPopup + * @returns {InputPopup} The instance of the InputPopup. + */ public setValue(newValue: string | number): this { this.value = newValue this.CM.refresh() @@ -281,10 +284,10 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to show the popup. It also register the key events and refresh the ConsoleManager. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to show the popup. It also register the key events and refresh the ConsoleManager. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ public show(): InputPopup { if (!this.visible) { this.manageInput() @@ -296,10 +299,10 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to hide the popup. It also unregister the key events and refresh the ConsoleManager. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to hide the popup. It also unregister the key events and refresh the ConsoleManager. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ public hide(): InputPopup { if (this.visible) { this.unManageInput() @@ -311,69 +314,79 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to get the visibility of the popup. - * @returns {boolean} The visibility of the popup. - * @memberof InputPopup - */ + * @description This function is used to get the visibility of the popup. + * @returns {boolean} The visibility of the popup. + * @memberof InputPopup + */ public isVisible(): boolean { return this.visible } - /** - * @description This function is used to return the PhisicalValues of the popup (x, y, width, height). - * @memberof InputPopup - * @private - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to return the PhisicalValues of the popup (x, y, width, height). + * @memberof InputPopup + * @private + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ public getPosition(): PhisicalValues { return this.absoluteValues } /** - * @description This function is used to add the InputPopup key listener callback to te ConsoleManager. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to add the InputPopup key listener callback to te ConsoleManager. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ private manageInput(): InputPopup { - // Add a command input listener to change mode + // Add a command input listener to change mode if (this.numeric) { this.CM.setKeyListener(this.id, this.keyListenerNumeric.bind(this)) } else { this.CM.setKeyListener(this.id, this.keyListenerText.bind(this)) } - if (this.CM.mouse) this.CM.setMouseListener(`${this.id}_mouse`, this.mouseListener.bind(this)) + if (this.CM.mouse) + this.CM.setMouseListener( + `${this.id}_mouse`, + this.mouseListener.bind(this) + ) return this } /** - * @description This function is used to remove the InputPopup key listener callback to te ConsoleManager. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to remove the InputPopup key listener callback to te ConsoleManager. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ private unManageInput(): InputPopup { - // Add a command input listener to change mode + // Add a command input listener to change mode if (this.numeric) { - this.CM.removeKeyListener(this.id/*, this.keyListenerNumeric.bind(this)*/) + this.CM.removeKeyListener( + this.id /*, this.keyListenerNumeric.bind(this)*/ + ) } else { - this.CM.removeKeyListener(this.id/*, this.keyListenerText.bind(this)*/) + this.CM.removeKeyListener(this.id /*, this.keyListenerText.bind(this)*/) } if (this.CM.mouse) this.CM.removeMouseListener(`${this.id}_mouse`) return this } /** - * @description This function is used to manage the mouse events on the OptionPopup. - * @param {MouseEvent} event - The string of the input. - * @memberof OptionPopup - */ + * @description This function is used to manage the mouse events on the OptionPopup. + * @param {MouseEvent} event - The string of the input. + * @memberof OptionPopup + */ private mouseListener = (event: MouseEvent) => { const x = event.data.x const y = event.data.y //this.CM.log(event.name) - if (x > this.absoluteValues.x && x < this.absoluteValues.x + this.absoluteValues.width && y > this.absoluteValues.y && y < this.absoluteValues.y + this.absoluteValues.height) { + if ( + x > this.absoluteValues.x && + x < this.absoluteValues.x + this.absoluteValues.width && + y > this.absoluteValues.y && + y < this.absoluteValues.y + this.absoluteValues.height + ) { // The mouse is inside the popup //this.CM.log("Mouse inside popup") if (event.name === "MOUSE_WHEEL_DOWN") { @@ -395,45 +408,71 @@ export class InputPopup extends EventEmitter { } else { this.focused = false } - if (event.name === "MOUSE_DRAG" && event.data.left === true && this.dragging === false && this.focused) { + if ( + event.name === "MOUSE_DRAG" && + event.data.left === true && + this.dragging === false && + this.focused + ) { // check if the mouse is on the header of the popup (first three lines) - if (x > this.absoluteValues.x && x < this.absoluteValues.x + this.absoluteValues.width && y > this.absoluteValues.y && y < this.absoluteValues.y + 3/* 3 = header height */) { + if ( + x > this.absoluteValues.x && + x < this.absoluteValues.x + this.absoluteValues.width && + y > this.absoluteValues.y && + y < this.absoluteValues.y + 3 /* 3 = header height */ + ) { this.dragging = true this.dragStart = { x: x, y: y } } - } else if (event.name === "MOUSE_DRAG" && event.data.left === true && this.dragging === true) { - if ((y - this.dragStart.y) + this.absoluteValues.y < 0) { + } else if ( + event.name === "MOUSE_DRAG" && + event.data.left === true && + this.dragging === true + ) { + if (y - this.dragStart.y + this.absoluteValues.y < 0) { return // prevent the popup to go out of the top of the screen } - if ((x - this.dragStart.x) + this.absoluteValues.x < 0) { + if (x - this.dragStart.x + this.absoluteValues.x < 0) { return // prevent the popup to go out of the left of the screen } this.offsetX += x - this.dragStart.x this.offsetY += y - this.dragStart.y this.dragStart = { x: x, y: y } this.CM.refresh() - } else if (event.name === "MOUSE_LEFT_BUTTON_RELEASED" && this.dragging === true) { + } else if ( + event.name === "MOUSE_LEFT_BUTTON_RELEASED" && + this.dragging === true + ) { this.dragging = false this.CM.refresh() } } /** - * @description This function is used to draw the InputPopup to the screen in the middle. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to draw the InputPopup to the screen in the middle. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ public draw(): InputPopup { const offset = 2 - const windowWidth = this.title.length > this.value.toString().length ? this.title.length + (2 * offset) : this.value.toString().length + (2 * offset) + 1 + const windowWidth = + this.title.length > this.value.toString().length + ? this.title.length + 2 * offset + : this.value.toString().length + 2 * offset + 1 const halfWidth = Math.round((windowWidth - this.title.length) / 2) let header = boxChars["normal"].topLeft for (let i = 0; i < windowWidth; i++) { header += boxChars["normal"].horizontal } header += `${boxChars["normal"].topRight}${EOL}` - header += `${boxChars["normal"].vertical}${" ".repeat(halfWidth)}${this.title}${" ".repeat(windowWidth - halfWidth - this.title.length)}${boxChars["normal"].vertical}${EOL}` - header += `${boxChars["normal"].left}${boxChars["normal"].horizontal.repeat(windowWidth)}${boxChars["normal"].right}${EOL}` + header += `${boxChars["normal"].vertical}${" ".repeat(halfWidth)}${ + this.title + }${" ".repeat(windowWidth - halfWidth - this.title.length)}${ + boxChars["normal"].vertical + }${EOL}` + header += `${boxChars["normal"].left}${boxChars["normal"].horizontal.repeat( + windowWidth + )}${boxChars["normal"].right}${EOL}` let footer = boxChars["normal"].bottomLeft for (let i = 0; i < windowWidth; i++) { @@ -454,9 +493,12 @@ export class InputPopup extends EventEmitter { const windowDesign = `${header}${content}${footer}` const windowDesignLines = windowDesign.split(EOL) - const centerScreen = Math.round((this.CM.Screen.width / 2) - (windowWidth / 2)) + const centerScreen = Math.round(this.CM.Screen.width / 2 - windowWidth / 2) windowDesign.split(EOL).forEach((line, index) => { - this.CM.Screen.cursorTo(centerScreen + this.offsetX, this.marginTop + index + this.offsetY) + this.CM.Screen.cursorTo( + centerScreen + this.offsetX, + this.marginTop + index + this.offsetY + ) this.CM.Screen.write({ text: line, style: { color: "white" } }) }) this.absoluteValues = { @@ -469,4 +511,4 @@ export class InputPopup extends EventEmitter { } } -export default InputPopup \ No newline at end of file +export default InputPopup From b3b1662fb6898ed7628ebf1c27fa3e0d99d62d11 Mon Sep 17 00:00:00 2001 From: Compositr <43405050+Compositr@users.noreply.github.com> Date: Fri, 26 May 2023 16:53:43 +1000 Subject: [PATCH 09/14] Fix incorrect spacing causing box to break --- src/components/widgets/InputPopup.ts | 45 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index b90fc43..1f96b38 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -20,12 +20,12 @@ import chalk from "chalk" */ // @type definition export interface InputPopupConfig { - id: string, - title: string, - value: string | number, - numeric?: boolean, - visible?: boolean, - placeholder?: string + id: string; + title: string; + value: string | number; + numeric?: boolean; + visible?: boolean; + placeholder?: string; } /** @@ -150,13 +150,22 @@ export class InputPopup extends EventEmitter { this.value.toString().length - 1 ) { this.value = v.toString() - } else if (this.value.toString().indexOf(".") === this.value.toString().length - 2) { - this.value = this.value.toString().slice(0, this.value.toString().length - 1) - } else if (this.value.toString().indexOf("-") === 0 && this.value.toString().length === 2) { + } else if ( + this.value.toString().indexOf(".") === + this.value.toString().length - 2 + ) { + this.value = this.value + .toString() + .slice(0, this.value.toString().length - 1) + } else if ( + this.value.toString().indexOf("-") === 0 && + this.value.toString().length === 2 + ) { this.value = 0 - } - else { - this.value = Number(v.toString().slice(0, v.toString().length - 1)) + } else { + this.value = Number( + v.toString().slice(0, v.toString().length - 1) + ) } } break @@ -482,14 +491,18 @@ export class InputPopup extends EventEmitter { let content = "" // Draw an input field - if(this.value.toString().length === 0 && this.placeholder?.length) - content += `${boxChars["normal"].vertical}${"> "}█${chalk.dim.gray( + if (this.value.toString().length === 0 && this.placeholder?.length) + content += `${boxChars["normal"].vertical}${"> "}${chalk.dim.gray( `${this.placeholder}` - )}${" ".repeat(windowWidth - this.value.toString().length - 3)}${ + )}${" ".repeat(windowWidth - this.placeholder.length - 2)}${ boxChars["normal"].vertical }${EOL}` else - content += `${boxChars["normal"].vertical}${"> "}${this.value}█${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical}${EOL}` + content += `${boxChars["normal"].vertical}${"> "}${ + this.value + }█${" ".repeat(windowWidth - this.value.toString().length - 3)}${ + boxChars["normal"].vertical + }${EOL}` const windowDesign = `${header}${content}${footer}` const windowDesignLines = windowDesign.split(EOL) From 8a9641c3a4f2246cc1280cc2c0c46858433d5b7b Mon Sep 17 00:00:00 2001 From: Elia Lazzari Date: Mon, 29 May 2023 11:46:41 +0200 Subject: [PATCH 10/14] Just changed some syntax styles --- src/components/widgets/InputPopup.ts | 81 ++++++++++++---------------- 1 file changed, 35 insertions(+), 46 deletions(-) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index 1f96b38..099c483 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -20,12 +20,12 @@ import chalk from "chalk" */ // @type definition export interface InputPopupConfig { - id: string; - title: string; - value: string | number; - numeric?: boolean; - visible?: boolean; - placeholder?: string; + id: string; + title: string; + value: string | number; + numeric?: boolean; + visible?: boolean; + placeholder?: string; } /** @@ -147,19 +147,19 @@ export class InputPopup extends EventEmitter { if (this.value.toString().length > 0) { if ( this.value.toString().indexOf(".") === - this.value.toString().length - 1 + this.value.toString().length - 1 ) { this.value = v.toString() } else if ( this.value.toString().indexOf(".") === - this.value.toString().length - 2 + this.value.toString().length - 2 ) { this.value = this.value .toString() .slice(0, this.value.toString().length - 1) } else if ( this.value.toString().indexOf("-") === 0 && - this.value.toString().length === 2 + this.value.toString().length === 2 ) { this.value = 0 } else { @@ -219,7 +219,7 @@ export class InputPopup extends EventEmitter { const v = this.value switch (key.name) { case "backspace": - // If backspace is pressed I remove the last character from the typed value + // If backspace is pressed I remove the last character from the typed value if (v.toString().length > 0) { this.value = v.toString().slice(0, v.toString().length - 1) } @@ -348,7 +348,7 @@ export class InputPopup extends EventEmitter { * @memberof InputPopup */ private manageInput(): InputPopup { - // Add a command input listener to change mode + // Add a command input listener to change mode if (this.numeric) { this.CM.setKeyListener(this.id, this.keyListenerNumeric.bind(this)) } else { @@ -368,7 +368,7 @@ export class InputPopup extends EventEmitter { * @memberof InputPopup */ private unManageInput(): InputPopup { - // Add a command input listener to change mode + // Add a command input listener to change mode if (this.numeric) { this.CM.removeKeyListener( this.id /*, this.keyListenerNumeric.bind(this)*/ @@ -390,12 +390,10 @@ export class InputPopup extends EventEmitter { const y = event.data.y //this.CM.log(event.name) - if ( - x > this.absoluteValues.x && - x < this.absoluteValues.x + this.absoluteValues.width && - y > this.absoluteValues.y && - y < this.absoluteValues.y + this.absoluteValues.height - ) { + if (x > this.absoluteValues.x && + x < this.absoluteValues.x + this.absoluteValues.width && + y > this.absoluteValues.y && + y < this.absoluteValues.y + this.absoluteValues.height) { // The mouse is inside the popup //this.CM.log("Mouse inside popup") if (event.name === "MOUSE_WHEEL_DOWN") { @@ -419,25 +417,21 @@ export class InputPopup extends EventEmitter { } if ( event.name === "MOUSE_DRAG" && - event.data.left === true && - this.dragging === false && - this.focused + event.data.left === true && + this.dragging === false && + this.focused ) { // check if the mouse is on the header of the popup (first three lines) - if ( - x > this.absoluteValues.x && - x < this.absoluteValues.x + this.absoluteValues.width && - y > this.absoluteValues.y && - y < this.absoluteValues.y + 3 /* 3 = header height */ - ) { + if (x > this.absoluteValues.x && + x < this.absoluteValues.x + this.absoluteValues.width && + y > this.absoluteValues.y && + y < this.absoluteValues.y + 3 /* 3 = header height */) { this.dragging = true this.dragStart = { x: x, y: y } } - } else if ( - event.name === "MOUSE_DRAG" && - event.data.left === true && - this.dragging === true - ) { + } else if (event.name === "MOUSE_DRAG" && + event.data.left === true && + this.dragging === true) { if (y - this.dragStart.y + this.absoluteValues.y < 0) { return // prevent the popup to go out of the top of the screen } @@ -450,7 +444,7 @@ export class InputPopup extends EventEmitter { this.CM.refresh() } else if ( event.name === "MOUSE_LEFT_BUTTON_RELEASED" && - this.dragging === true + this.dragging === true ) { this.dragging = false this.CM.refresh() @@ -465,19 +459,17 @@ export class InputPopup extends EventEmitter { public draw(): InputPopup { const offset = 2 const windowWidth = - this.title.length > this.value.toString().length - ? this.title.length + 2 * offset - : this.value.toString().length + 2 * offset + 1 + this.title.length > this.value.toString().length + ? this.title.length + 2 * offset + : this.value.toString().length + 2 * offset + 1 const halfWidth = Math.round((windowWidth - this.title.length) / 2) let header = boxChars["normal"].topLeft for (let i = 0; i < windowWidth; i++) { header += boxChars["normal"].horizontal } header += `${boxChars["normal"].topRight}${EOL}` - header += `${boxChars["normal"].vertical}${" ".repeat(halfWidth)}${ - this.title - }${" ".repeat(windowWidth - halfWidth - this.title.length)}${ - boxChars["normal"].vertical + header += `${boxChars["normal"].vertical}${" ".repeat(halfWidth)}${this.title + }${" ".repeat(windowWidth - halfWidth - this.title.length)}${boxChars["normal"].vertical }${EOL}` header += `${boxChars["normal"].left}${boxChars["normal"].horizontal.repeat( windowWidth @@ -494,14 +486,11 @@ export class InputPopup extends EventEmitter { if (this.value.toString().length === 0 && this.placeholder?.length) content += `${boxChars["normal"].vertical}${"> "}${chalk.dim.gray( `${this.placeholder}` - )}${" ".repeat(windowWidth - this.placeholder.length - 2)}${ - boxChars["normal"].vertical + )}${" ".repeat(windowWidth - this.placeholder.length - 2)}${boxChars["normal"].vertical }${EOL}` else - content += `${boxChars["normal"].vertical}${"> "}${ - this.value - }█${" ".repeat(windowWidth - this.value.toString().length - 3)}${ - boxChars["normal"].vertical + content += `${boxChars["normal"].vertical}${"> "}${this.value + }█${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical }${EOL}` const windowDesign = `${header}${content}${footer}` From 7ee5a8f41f38c1c22ffe076126372e8e5c37f144 Mon Sep 17 00:00:00 2001 From: Compositr <43405050+Compositr@users.noreply.github.com> Date: Sat, 27 May 2023 18:15:09 +1000 Subject: [PATCH 11/14] Properly count invisible/visible chars --- src/components/Utils.ts | 130 +++--- src/components/layout/DoubleLayout.ts | 543 +++++++++++++++++++------- 2 files changed, 483 insertions(+), 190 deletions(-) diff --git a/src/components/Utils.ts b/src/components/Utils.ts index 25b94c8..0d1187a 100644 --- a/src/components/Utils.ts +++ b/src/components/Utils.ts @@ -3,16 +3,18 @@ import { BackgroundColorName, ForegroundColorName } from "chalk" /** * @typedef {string} HEX - The type of the HEX color. * @example const hexColor = "#FF0000" - * + * * @typedef {string} RGB - The type of the RGB color. * @example const rgbColor = "rgb(255, 0, 0)" */ export type HEX = `#${string}`; -export type RGB = `rgb(${number}, ${number}, ${number})` | `rgb(${number},${number},${number})`; +export type RGB = + | `rgb(${number}, ${number}, ${number})` + | `rgb(${number},${number},${number})`; /** * @description The type containing all the possible styles for the text. - * + * * @typedef {Object} StyleObject * @prop {chalk.ForegroundColorName | HEX | RGB | ""} [color] - The color of the text taken from the chalk library. * @prop {chalk.BackgroundColorName | HEX | RGB | ""} [backgroundColor] - The background color of the text taken from the chalk library. @@ -24,7 +26,7 @@ export type RGB = `rgb(${number}, ${number}, ${number})` | `rgb(${number},${numb * @prop {boolean} [hidden] - If the text is hidden. * @prop {boolean} [strikethrough] - If the text is strikethrough. * @prop {boolean} [overline] - If the text is overlined. - * + * * @example const textStyle = { color: "red", backgroundColor: "blue", bold: true, italic: true } * * @export @@ -32,25 +34,25 @@ export type RGB = `rgb(${number}, ${number}, ${number})` | `rgb(${number},${numb */ // @type definition export interface StyleObject { - color?: ForegroundColorName | HEX | RGB | ""; - bg?: BackgroundColorName | HEX | RGB | ""; - italic?: boolean; - bold?: boolean; - dim?: boolean; - underline?: boolean; - inverse?: boolean; - hidden?: boolean; - strikethrough?: boolean; - overline?: boolean; + color?: ForegroundColorName | HEX | RGB | ""; + bg?: BackgroundColorName | HEX | RGB | ""; + italic?: boolean; + bold?: boolean; + dim?: boolean; + underline?: boolean; + inverse?: boolean; + hidden?: boolean; + strikethrough?: boolean; + overline?: boolean; } /** * @description The type of the single styled text, stored in a line of the PageBuilder. - * + * * @typedef {Object} StyledElement * @prop {string} text - The text of the styled text. * @prop {StyleObject} style - The style of the styled text. - * + * * @example const styledText = { text: "Hello", style: { color: "red", backgroundColor: "blue", bold: true, italic: true } } * * @export @@ -58,13 +60,13 @@ export interface StyleObject { */ // @type definition export interface StyledElement { - text: string; - style: StyleObject; + text: string; + style: StyleObject; } /** * @description The type containing all the possible styles for the text and the text on the same level. It's used on the higher level. - * + * * @typedef {Object} SimplifiedStyledElement * @prop {string} text - The text of the styled text. * @prop {chalk.ForegroundColorName | HEX | RGB | ""} [color] - The color of the text taken from the chalk library. @@ -77,7 +79,7 @@ export interface StyledElement { * @prop {boolean} [hidden] - If the text is hidden. * @prop {boolean} [strikethrough] - If the text is strikethrough. * @prop {boolean} [overline] - If the text is overlined. - * + * * @example const textStyle = { color: "red", backgroundColor: "blue", bold: true, italic: true } * * @export @@ -85,17 +87,17 @@ export interface StyledElement { */ // @type definition export interface SimplifiedStyledElement { - text: string; - color?: ForegroundColorName | HEX | RGB | ""; - bg?: BackgroundColorName | HEX | RGB | "" | ""; - italic?: boolean; - bold?: boolean; - dim?: boolean; - underline?: boolean; - inverse?: boolean; - hidden?: boolean; - strikethrough?: boolean; - overline?: boolean; + text: string; + color?: ForegroundColorName | HEX | RGB | ""; + bg?: BackgroundColorName | HEX | RGB | "" | ""; + italic?: boolean; + bold?: boolean; + dim?: boolean; + underline?: boolean; + inverse?: boolean; + hidden?: boolean; + strikethrough?: boolean; + overline?: boolean; } /** @@ -106,11 +108,11 @@ export interface SimplifiedStyledElement { */ // @type definition export interface PhisicalValues { - x: number - y: number - width: number - height: number - id?: number + x: number; + y: number; + width: number; + height: number; + id?: number; } /** @const {Object} boxChars - The characters used to draw the box. */ @@ -162,7 +164,7 @@ export const boxChars = { start: "", end: "", color: "" as ForegroundColorName | HEX | RGB | "", - } + }, } /** @@ -172,12 +174,20 @@ export const boxChars = { * @param {boolean} useWordBoundary - If true, the truncation will be done at the end of the word. * @example CM.truncate("Hello world", 5, true) // "Hello..." */ -export function truncate(str: string, n: number, useWordBoundary: boolean): string { - if (str.length <= n) { return str } +export function truncate( + str: string, + n: number, + useWordBoundary: boolean +): string { + if (str.length <= n) { + return str + } const subString = str.substring(0, n - 1) // the original check - return (useWordBoundary ? - subString.substring(0, subString.lastIndexOf(" ")) : - subString) + "…" + return ( + (useWordBoundary + ? subString.substring(0, subString.lastIndexOf(" ")) + : subString) + "…" + ) } /** @@ -186,11 +196,13 @@ export function truncate(str: string, n: number, useWordBoundary: boolean): stri * @export * @param {StyledElement} styled * @return {*} {SimplifiedStyledElement} - * + * * @example const simplifiedStyledElement = styledToSimplifiedStyled({ text: "Hello world", style: { color: "red", backgroundColor: "blue", bold: true, italic: true } }) * // returns { text: "Hello world", color: "red", backgroundColor: "blue", bold: true, italic: true } */ -export function styledToSimplifiedStyled(styled: StyledElement): SimplifiedStyledElement { +export function styledToSimplifiedStyled( + styled: StyledElement +): SimplifiedStyledElement { return { text: styled.text, color: styled.style?.color, @@ -212,11 +224,13 @@ export function styledToSimplifiedStyled(styled: StyledElement): SimplifiedStyle * @export * @param {SimplifiedStyledElement} simplifiedStyled * @return {*} {StyledElement} - * + * * @example const styledElement = simplifiedStyledToStyled({ text: "Hello world", color: "red", bold: true }) * // returns { text: "Hello world", style: { color: "red", bold: true } } */ -export function simplifiedStyledToStyled(simplifiedStyled: SimplifiedStyledElement): StyledElement { +export function simplifiedStyledToStyled( + simplifiedStyled: SimplifiedStyledElement +): StyledElement { return { text: simplifiedStyled.text, style: { @@ -230,6 +244,26 @@ export function simplifiedStyledToStyled(simplifiedStyled: SimplifiedStyledEleme hidden: simplifiedStyled?.hidden, strikethrough: simplifiedStyled?.strikethrough, overline: simplifiedStyled?.overline, - } + }, } -} \ No newline at end of file +} + +/** + * @description Count true visible length of a string + * + * @export + * @param {string} input + * @return {number} + * + * @author Vitalik Gordon (xpl) + */ +export function visibleLength(input: string): number { + // eslint-disable-next-line no-control-regex + const regex = new RegExp( + /* eslint-disable-next-line no-control-regex */ + "\u0000-\u0008\u000B-\u0019\u001b\u009b\u00ad\u200b\u2028\u2029\ufeff\ufe00-\ufe0f", + "g" + ) + // Array.from is used to correctly count emojis + return Array.from(input.replace(regex, "")).length +} diff --git a/src/components/layout/DoubleLayout.ts b/src/components/layout/DoubleLayout.ts index 990c514..b295607 100644 --- a/src/components/layout/DoubleLayout.ts +++ b/src/components/layout/DoubleLayout.ts @@ -1,6 +1,13 @@ import { ForegroundColorName } from "chalk" import { ConsoleManager, PageBuilder } from "../../ConsoleGui.js" -import { boxChars, HEX, RGB, StyledElement, truncate } from "../Utils.js" +import { + boxChars, + visibleLength, + HEX, + RGB, + StyledElement, + truncate, +} from "../Utils.js" /** * @description The type containing all the possible options for the DoubleLayout. @@ -20,23 +27,23 @@ import { boxChars, HEX, RGB, StyledElement, truncate } from "../Utils.js" */ // @type definition export interface DoubleLayoutOptions { - showTitle?: boolean; - boxed?: boolean; - boxColor?: ForegroundColorName | HEX | RGB | ""; // add color list from chalk - boxStyle?: "bold"; - changeFocusKey: string; - direction?: "horizontal" | "vertical"; - page1Title?: string; - page2Title?: string; - pageRatio?: [number, number]; + showTitle?: boolean; + boxed?: boolean; + boxColor?: ForegroundColorName | HEX | RGB | ""; // add color list from chalk + boxStyle?: "bold"; + changeFocusKey: string; + direction?: "horizontal" | "vertical"; + page1Title?: string; + page2Title?: string; + pageRatio?: [number, number]; } /** * @class DoubleLayout * @description This class is a layout that has two pages. - * + * * ![double layout](https://user-images.githubusercontent.com/14907987/170996957-cb28414b-7be2-4aa0-938b-f6d1724cfa4c.png) - * + * * @param {PageBuilder} page1 The first page. * @param {PageBuilder} page2 The second page. * @param {boolean} options Layout options. @@ -56,8 +63,13 @@ export class DoubleLayout { realWidth: number | [number, number] = 0 isOdd: boolean | undefined - public constructor(page1 : PageBuilder, page2: PageBuilder, options: DoubleLayoutOptions, selected: 0 | 1 = 0) { - /** @const {ConsoleManager} CM the instance of ConsoleManager (singleton) */ + public constructor( + page1: PageBuilder, + page2: PageBuilder, + options: DoubleLayoutOptions, + selected: 0 | 1 = 0 + ) { + /** @const {ConsoleManager} CM the instance of ConsoleManager (singleton) */ this.CM = new ConsoleManager() this.options = options @@ -76,10 +88,10 @@ export class DoubleLayout { } /** - * @description This function is used to overwrite the page content. - * @param {PageBuilder} page the page to be added - * @memberof DoubleLayout - */ + * @description This function is used to overwrite the page content. + * @param {PageBuilder} page the page to be added + * @memberof DoubleLayout + */ public setPage(page: PageBuilder, index: number): void { if (index == 0) { this.page1 = page @@ -89,37 +101,41 @@ export class DoubleLayout { } /** - * @description This function is used to overwrite the page content. - * @param {PageBuilder} page the page to be added - * @memberof DoubleLayout - */ - public setPage1(page: PageBuilder): void { this.page1 = page } + * @description This function is used to overwrite the page content. + * @param {PageBuilder} page the page to be added + * @memberof DoubleLayout + */ + public setPage1(page: PageBuilder): void { + this.page1 = page + } /** - * @description This function is used to overwrite the page content. - * @param {PageBuilder} page the page to be added - * @memberof DoubleLayout - */ - public setPage2(page: PageBuilder): void { this.page2 = page } + * @description This function is used to overwrite the page content. + * @param {PageBuilder} page the page to be added + * @memberof DoubleLayout + */ + public setPage2(page: PageBuilder): void { + this.page2 = page + } /** - * @description This function is used to set the page titles. - * @param {string[]} titles the titles of the pages - * @memberof DoubleLayout - * @example layout.setTitles(["Page 1", "Page 2"]) - */ + * @description This function is used to set the page titles. + * @param {string[]} titles the titles of the pages + * @memberof DoubleLayout + * @example layout.setTitles(["Page 1", "Page 2"]) + */ public setTitles(titles: string[]) { this.page1Title = titles[0] this.page2Title = titles[1] } /** - * @description This function is used to set the page title at the given index. - * @param {string} title the title of the page - * @param {number} index the index of the page - * @memberof DoubleLayout - * @example layout.setTitle("Page 1", 0) - */ + * @description This function is used to set the page title at the given index. + * @param {string} title the title of the page + * @param {number} index the index of the page + * @memberof DoubleLayout + * @example layout.setTitle("Page 1", 0) + */ public setTitle(title: string, index: number): void { if (index == 0) { this.page1Title = title @@ -129,33 +145,37 @@ export class DoubleLayout { } /** - * @description This function is used to enable or disable the layout border. - * @param {boolean} border enable or disable the border - * @memberof DoubleLayout - */ - public setBorder(border: boolean): void { this.options.boxed = border } + * @description This function is used to enable or disable the layout border. + * @param {boolean} border enable or disable the border + * @memberof DoubleLayout + */ + public setBorder(border: boolean): void { + this.options.boxed = border + } /** - * @description This function is used to choose the page to be highlighted. - * @param {number} selected 0 for page1, 1 for page2 - * @memberof DoubleLayout - */ - public setSelected(selected: 0 | 1): void { this.selected = selected } + * @description This function is used to choose the page to be highlighted. + * @param {number} selected 0 for page1, 1 for page2 + * @memberof DoubleLayout + */ + public setSelected(selected: 0 | 1): void { + this.selected = selected + } /** - * @description This function is used to get the selected page. - * @returns {number} 0 for page1, 1 for page2 - * @memberof DoubleLayout - */ + * @description This function is used to get the selected page. + * @returns {number} 0 for page1, 1 for page2 + * @memberof DoubleLayout + */ public getSelected(): number { return this.selected } /** - * @description This function is used to get switch the selected page. - * @returns {void} - * @memberof DoubleLayout - */ + * @description This function is used to get switch the selected page. + * @returns {void} + * @memberof DoubleLayout + */ public changeLayout(): void { if (this.selected == 0) { this.selected = 1 @@ -165,93 +185,123 @@ export class DoubleLayout { } /** - * @description This function is used to change the page ratio. - * @param {Array} ratio the ratio of pages - * @memberof QuadLayout - * @example layout.setRatio([0.4, 0.6]) - */ + * @description This function is used to change the page ratio. + * @param {Array} ratio the ratio of pages + * @memberof QuadLayout + * @example layout.setRatio([0.4, 0.6]) + */ public setRatio(ratio: [number, number]): void { this.proportions = ratio } /** - * @description This function is used to increase the page ratio by the given ratio to add. (Only works if the direction is horizontal) - * @param {number} quantity the ratio to add - * @memberof QuadLayout - * @example layout.increaseRatio(0.01) - */ + * @description This function is used to increase the page ratio by the given ratio to add. (Only works if the direction is horizontal) + * @param {number} quantity the ratio to add + * @memberof QuadLayout + * @example layout.increaseRatio(0.01) + */ public increaseRatio(quantity: number): void { if (this.options.direction == "horizontal") { if (this.proportions[0] < 0.9) { - this.proportions[0] = Number((this.proportions[0] + quantity).toFixed(2)) - this.proportions[1] = Number((this.proportions[1] - quantity).toFixed(2)) + this.proportions[0] = Number( + (this.proportions[0] + quantity).toFixed(2) + ) + this.proportions[1] = Number( + (this.proportions[1] - quantity).toFixed(2) + ) } } } /** - * @description This function is used to decrease the page ratio by the given ratio to subtract. (Only works if the direction is horizontal). - * @param {number} quantity the ratio to subtract - * @memberof QuadLayout - * @example layout.decreaseRatio(0.01) - */ + * @description This function is used to decrease the page ratio by the given ratio to subtract. (Only works if the direction is horizontal). + * @param {number} quantity the ratio to subtract + * @memberof QuadLayout + * @example layout.decreaseRatio(0.01) + */ public decreaseRatio(quantity: number): void { if (this.options.direction == "horizontal") { if (this.proportions[0] > 0.1) { - this.proportions[0] = Number((this.proportions[0] - quantity).toFixed(2)) - this.proportions[1] = Number((this.proportions[1] + quantity).toFixed(2)) + this.proportions[0] = Number( + (this.proportions[0] - quantity).toFixed(2) + ) + this.proportions[1] = Number( + (this.proportions[1] + quantity).toFixed(2) + ) } } } /** - * @description This function is used to draw a single line of the layout to the screen. It also trim the line if it is too long. - * @param {Array} line the line to be drawn - * @param {number} lineIndex the index of the selected line - * @memberof DoubleLayout - * @returns {void} - */ - private drawLine(line : Array, secondLine? : Array, index = 0): void { - const dir = !this.options.direction || this.options.direction === "vertical" ? "vertical" : "horizontal" - const bsize = this.options.boxed ? dir === "vertical" ? 2 : 3 : 0 + * @description This function is used to draw a single line of the layout to the screen. It also trim the line if it is too long. + * @param {Array} line the line to be drawn + * @param {number} lineIndex the index of the selected line + * @memberof DoubleLayout + * @returns {void} + */ + private drawLine( + line: Array, + secondLine?: Array, + index = 0 + ): void { + const dir = + !this.options.direction || this.options.direction === "vertical" + ? "vertical" + : "horizontal" + const bsize = this.options.boxed ? (dir === "vertical" ? 2 : 3) : 0 let unformattedLine = [""] - let newLine = [ - [...line] - ] + let newLine = [[...line]] if (dir === "vertical") { - line.forEach(element => { + line.forEach((element) => { unformattedLine[0] += element.text }) } else { - newLine = [ - [...line], - [...secondLine? secondLine : line] - ] + newLine = [[...line], [...(secondLine ? secondLine : line)]] unformattedLine.push("") - line.forEach((element : StyledElement) => { + line.forEach((element: StyledElement) => { unformattedLine[0] += element.text }) - secondLine?.forEach((element : StyledElement) => { + secondLine?.forEach((element: StyledElement) => { unformattedLine[1] += element.text }) } - - if (unformattedLine.filter((e, i) => e.length > (typeof this.realWidth === "number" ? this.realWidth : this.realWidth[i]) - bsize).length > 0) { + + if ( + unformattedLine.filter( + (e, i) => + e.length > + (typeof this.realWidth === "number" + ? this.realWidth + : this.realWidth[i]) - + bsize + ).length > 0 + ) { unformattedLine = unformattedLine.map((e, i) => { - const width = typeof this.realWidth === "number" ? this.realWidth : this.realWidth[i] - if (e.length > width - bsize) { // Need to truncate + const width = + typeof this.realWidth === "number" + ? this.realWidth + : this.realWidth[i] + if (e.length > width - bsize) { + // Need to truncate const offset = 2 if (dir === "vertical") { newLine[i] = [...JSON.parse(JSON.stringify(line))] // Shallow copy because I just want to modify the values but not the original } else { - newLine[i] = i === 0 ? JSON.parse(JSON.stringify(line)) : JSON.parse(JSON.stringify(secondLine)) + newLine[i] = + i === 0 + ? JSON.parse(JSON.stringify(line)) + : JSON.parse(JSON.stringify(secondLine)) } let diff = e.length - width + 1 // remove truncated text for (let j = newLine[i].length - 1; j >= 0; j--) { if (newLine[i][j].text.length > diff + offset) { - newLine[i][j].text = truncate(newLine[i][j].text, (newLine[i][j].text.length - diff) - offset, false) + newLine[i][j].text = truncate( + newLine[i][j].text, + newLine[i][j].text.length - diff - offset, + false + ) break } else { diff -= newLine[i][j].text.length @@ -259,106 +309,315 @@ export class DoubleLayout { } } // Update unformatted line - return newLine[i].map(element => element.text).join("") + return newLine[i].map((element) => element.text).join("") } return e }) } if (dir === "vertical") { - if (this.options.boxed) newLine[0].unshift({ text: boxChars["normal"].vertical, style: { color: this.selected === index ? this.options.boxColor : "white", bold: this.boxBold } }) - if (unformattedLine[0].length <= this.CM.Screen.width - bsize) { - newLine[0].push({ text: `${" ".repeat((this.CM.Screen.width - unformattedLine[0].length) - bsize)}`, style: { color: "" } }) + if (this.options.boxed) + newLine[0].unshift({ + text: boxChars["normal"].vertical, + style: { + color: this.selected === index ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) + if (visibleLength(unformattedLine[0]) <= this.CM.Screen.width - bsize) { + newLine[0].push({ + text: `${" ".repeat( + this.CM.Screen.width - + visibleLength(unformattedLine[0]) - + bsize + )}`, + style: { color: "" }, + }) } - if (this.options.boxed) newLine[0].push({ text: boxChars["normal"].vertical, style: { color: this.selected === index ? this.options.boxColor : "white", bold: this.boxBold } }) + if (this.options.boxed) + newLine[0].push({ + text: boxChars["normal"].vertical, + style: { + color: this.selected === index ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) this.CM.Screen.write(...newLine[0]) } else { - const width = typeof this.realWidth === "number" ? [this.realWidth, 0] : [this.realWidth[0], this.realWidth[1]] + const width = + typeof this.realWidth === "number" + ? [this.realWidth, 0] + : [this.realWidth[0], this.realWidth[1]] const ret: StyledElement[] = [] - if (this.options.boxed) ret.push({ text: boxChars["normal"].vertical, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + if (this.options.boxed) + ret.push({ + text: boxChars["normal"].vertical, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) ret.push(...newLine[0]) - if (unformattedLine[0].length <= width[0] - bsize) { - ret.push({ text: `${" ".repeat((width[0] - unformattedLine[0].length) - (bsize > 0 ? 2 : 0))}`, style: { color: "" } }) + if (visibleLength(unformattedLine[0]) <= width[0] - bsize) { + ret.push({ + text: `${" ".repeat( + width[0] - visibleLength(unformattedLine[0]) - (bsize > 0 ? 2 : 0) + )}`, + style: { color: "" }, + }) } - if (this.options.boxed) ret.push({ text: boxChars["normal"].vertical, style: { color: this.options.boxColor, bold: this.boxBold } }) + if (this.options.boxed) + ret.push({ + text: boxChars["normal"].vertical, + style: { color: this.options.boxColor, bold: this.boxBold }, + }) ret.push(...newLine[1]) if (unformattedLine[1].length <= width[1] - bsize) { - ret.push({ text: `${" ".repeat((width[1] - unformattedLine[1].length) - (bsize > 0 ? 1 : 0))}`, style: { color: "" } }) + ret.push({ + text: `${" ".repeat( + width[1] - unformattedLine[1].length - (bsize > 0 ? 1 : 0) + )}`, + style: { color: "" }, + }) } - if (this.options.boxed) ret.push({ text: boxChars["normal"].vertical, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) + if (this.options.boxed) + ret.push({ + text: boxChars["normal"].vertical, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) this.CM.Screen.write(...ret) } } /** - * @description This function is used to draw the layout to the screen. - * @memberof DoubleLayout - * @returns {void} - * @example layout.draw() - */ + * @description This function is used to draw the layout to the screen. + * @memberof DoubleLayout + * @returns {void} + * @example layout.draw() + */ public draw(): void { this.isOdd = this.CM.Screen.width % 2 === 1 if (!this.options.direction || this.options.direction === "vertical") { - this.realWidth = [Math.round(this.CM.Screen.width * 1), Math.round(this.CM.Screen.width * 1)] - const trimmedTitle = [truncate(this.page1Title, this.realWidth[0] - 4, false), truncate(this.page2Title, this.realWidth[1] - 4, false)] - if (this.options.boxed) { // Draw pages with borders + this.realWidth = [ + Math.round(this.CM.Screen.width * 1), + Math.round(this.CM.Screen.width * 1), + ] + const trimmedTitle = [ + truncate(this.page1Title, this.realWidth[0] - 4, false), + truncate(this.page2Title, this.realWidth[1] - 4, false), + ] + if (this.options.boxed) { + // Draw pages with borders if (this.options.showTitle) { - this.CM.Screen.write({ text: `${boxChars["normal"].topLeft}${boxChars["normal"].horizontal}${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - trimmedTitle[0].length - 3)}${boxChars["normal"].topRight}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${boxChars["normal"].topLeft}${ + boxChars["normal"].horizontal + }${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat( + this.CM.Screen.width - trimmedTitle[0].length - 3 + )}${boxChars["normal"].topRight}`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } else { - this.CM.Screen.write({ text: `${boxChars["normal"].topLeft}${boxChars["normal"].horizontal}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - 3)}${boxChars["normal"].topRight}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${boxChars["normal"].topLeft}${ + boxChars["normal"].horizontal + }${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - 3)}${ + boxChars["normal"].topRight + }`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } this.page1.getContent().forEach((line: StyledElement[]) => { this.drawLine(line, undefined, 0) }) if (this.options.showTitle) { - this.CM.Screen.write({ text: `${boxChars["normal"].left}${boxChars["normal"].horizontal}${trimmedTitle[1]}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - trimmedTitle[1].length - 3)}${boxChars["normal"].right}`, style: { color: this.options.boxColor, bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${boxChars["normal"].left}${boxChars["normal"].horizontal}${ + trimmedTitle[1] + }${boxChars["normal"].horizontal.repeat( + this.CM.Screen.width - trimmedTitle[1].length - 3 + )}${boxChars["normal"].right}`, + style: { color: this.options.boxColor, bold: this.boxBold }, + }) } else { - this.CM.Screen.write({ text: `${boxChars["normal"].left}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - 2)}${boxChars["normal"].right}`, style: { color: this.options.boxColor, bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${boxChars["normal"].left}${boxChars[ + "normal" + ].horizontal.repeat(this.CM.Screen.width - 2)}${ + boxChars["normal"].right + }`, + style: { color: this.options.boxColor, bold: this.boxBold }, + }) } this.page2.getContent().forEach((line: StyledElement[]) => { this.drawLine(line, undefined, 1) }) - this.CM.Screen.write({ text: `${boxChars["normal"].bottomLeft}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - 2)}${boxChars["normal"].bottomRight}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) - } else { // Draw pages without borders + this.CM.Screen.write({ + text: `${boxChars["normal"].bottomLeft}${boxChars[ + "normal" + ].horizontal.repeat(this.CM.Screen.width - 2)}${ + boxChars["normal"].bottomRight + }`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) + } else { + // Draw pages without borders if (this.options.showTitle) { - this.CM.Screen.write({ text: `${trimmedTitle[0]}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${trimmedTitle[0]}`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } this.page1.getContent().forEach((line: StyledElement[]) => { this.drawLine(line, undefined, 0) }) if (this.options.showTitle) { - this.CM.Screen.write({ text: `${trimmedTitle[1]}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${trimmedTitle[1]}`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } this.page2.getContent().forEach((line: StyledElement[]) => { this.drawLine(line, undefined, 1) }) } - } else { // Draw horizontally - this.realWidth = [Math.round(this.CM.Screen.width * this.proportions[0]), Math.round(this.CM.Screen.width * this.proportions[1])] - const trimmedTitle = [truncate(this.page1Title, this.realWidth[0] - 4, false), truncate(this.page2Title, this.realWidth[1] - 3, false)] - const maxPageHeight = Math.max(this.page1.getViewedPageHeight(), this.page2.getViewedPageHeight()) + } else { + // Draw horizontally + this.realWidth = [ + Math.round(this.CM.Screen.width * this.proportions[0]), + Math.round(this.CM.Screen.width * this.proportions[1]), + ] + const trimmedTitle = [ + truncate(this.page1Title, this.realWidth[0] - 4, false), + truncate(this.page2Title, this.realWidth[1] - 3, false), + ] + const maxPageHeight = Math.max( + this.page1.getViewedPageHeight(), + this.page2.getViewedPageHeight() + ) const p1 = this.page1.getContent() const p2 = this.page2.getContent() - if (this.options.boxed) { // Draw pages with borders + if (this.options.boxed) { + // Draw pages with borders if (this.options.showTitle) { - this.CM.Screen.write({ text: `${boxChars["normal"].topLeft}${boxChars["normal"].horizontal}${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat(this.realWidth[0] - trimmedTitle[0].length - 3)}${boxChars["normal"].top}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }, { text: `${boxChars["normal"].horizontal}${trimmedTitle[1]}${boxChars["normal"].horizontal.repeat(this.realWidth[1] - trimmedTitle[1].length - 2)}${boxChars["normal"].topRight}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write( + { + text: `${boxChars["normal"].topLeft}${ + boxChars["normal"].horizontal + }${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat( + this.realWidth[0] - trimmedTitle[0].length - 3 + )}${boxChars["normal"].top}`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }, + { + text: `${boxChars["normal"].horizontal}${ + trimmedTitle[1] + }${boxChars["normal"].horizontal.repeat( + this.realWidth[1] - trimmedTitle[1].length - 2 + )}${boxChars["normal"].topRight}`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + } + ) } else { - this.CM.Screen.write({ text: `${boxChars["normal"].topLeft}${boxChars["normal"].horizontal}${boxChars["normal"].horizontal.repeat(this.realWidth[0] - 3)}${boxChars["normal"].top}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }, { text: `${boxChars["normal"].horizontal}${boxChars["normal"].horizontal.repeat(this.realWidth[1] - 2)}${boxChars["normal"].topRight}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write( + { + text: `${boxChars["normal"].topLeft}${ + boxChars["normal"].horizontal + }${boxChars["normal"].horizontal.repeat(this.realWidth[0] - 3)}${ + boxChars["normal"].top + }`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }, + { + text: `${boxChars["normal"].horizontal}${boxChars[ + "normal" + ].horizontal.repeat(this.realWidth[1] - 2)}${ + boxChars["normal"].topRight + }`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + } + ) } for (let i = 0; i < maxPageHeight; i++) { - this.drawLine(p1[i] || [{ text: "", style: { color: "" } }], p2[i] || [{ text: "", style: { color: "" } }]) + this.drawLine( + p1[i] || [{ text: "", style: { color: "" } }], + p2[i] || [{ text: "", style: { color: "" } }] + ) } // Draw the bottom border - this.CM.Screen.write({ text: `${boxChars["normal"].bottomLeft}${boxChars["normal"].horizontal.repeat(this.realWidth[0] - 2)}${boxChars["normal"].bottom}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }, { text: `${boxChars["normal"].horizontal.repeat(this.realWidth[1] - 1)}${boxChars["normal"].bottomRight}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) - } else { // Draw pages without borders + this.CM.Screen.write( + { + text: `${boxChars["normal"].bottomLeft}${boxChars[ + "normal" + ].horizontal.repeat(this.realWidth[0] - 2)}${ + boxChars["normal"].bottom + }`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }, + { + text: `${boxChars["normal"].horizontal.repeat( + this.realWidth[1] - 1 + )}${boxChars["normal"].bottomRight}`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + } + ) + } else { + // Draw pages without borders if (this.options.showTitle) { - this.CM.Screen.write({ text: `${trimmedTitle[0]}${" ".repeat(this.realWidth[0] - trimmedTitle[0].length)}${trimmedTitle[1]}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${trimmedTitle[0]}${" ".repeat( + this.realWidth[0] - trimmedTitle[0].length + )}${trimmedTitle[1]}`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } for (let i = 0; i < maxPageHeight; i++) { - this.drawLine(p1[i] || [{ text: "", style: { color: "" } }], p2[i] || [{ text: "", style: { color: "" } }]) + this.drawLine( + p1[i] || [{ text: "", style: { color: "" } }], + p2[i] || [{ text: "", style: { color: "" } }] + ) } } } } } -export default DoubleLayout \ No newline at end of file +export default DoubleLayout From 92d0877f8e9602ee4e89076c6854d5ddbcc3a957 Mon Sep 17 00:00:00 2001 From: Compositr <43405050+Compositr@users.noreply.github.com> Date: Sun, 4 Jun 2023 14:14:33 +1000 Subject: [PATCH 12/14] feat: use visibleLength measurements everywhere --- src/components/Screen.ts | 6 +++--- src/components/Utils.ts | 4 ++-- src/components/layout/DoubleLayout.ts | 28 +++++++++++++-------------- src/components/widgets/Box.ts | 22 ++++++++++----------- src/components/widgets/Control.ts | 16 +++++++-------- src/components/widgets/CustomPopup.ts | 16 +++++++-------- src/components/widgets/InputPopup.ts | 4 ++-- 7 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/components/Screen.ts b/src/components/Screen.ts index c1fc1f8..c1cfd56 100644 --- a/src/components/Screen.ts +++ b/src/components/Screen.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "events" import chalk, { BackgroundColorName, ForegroundColorName } from "chalk" -import { StyledElement, StyleObject } from "./Utils.js" +import { StyledElement, StyleObject, visibleLength } from "./Utils.js" chalk.level = 3 /** @@ -81,7 +81,7 @@ export class Screen extends EventEmitter { const arg = args[i] if (arg.text !== undefined) { const txt = arg.text.toString() - const style: StyleIndexObject = { ...arg.style, index: [row.length, row.length + txt.length] } + const style: StyleIndexObject = { ...arg.style, index: [visibleLength(row), visibleLength(row) + visibleLength(txt)] } newStyleIndex.push(style) row += txt } @@ -90,7 +90,7 @@ export class Screen extends EventEmitter { // Now recalculate the styleIndex for the current row mixing the old one with the new one // Create a new styleIndex merging the old one with the new one - const mergedStyleIndex = this.mergeStyles(newStyleIndex, currentStyleIndex, this.cursor.x, row.length) + const mergedStyleIndex = this.mergeStyles(newStyleIndex, currentStyleIndex, this.cursor.x, visibleLength(row)) this.buffer[this.cursor.y].styleIndex = mergedStyleIndex this.buffer[this.cursor.y].text = this.replaceAt(this.buffer[this.cursor.y].text, this.cursor.x, row) diff --git a/src/components/Utils.ts b/src/components/Utils.ts index 0d1187a..d7fb232 100644 --- a/src/components/Utils.ts +++ b/src/components/Utils.ts @@ -179,7 +179,7 @@ export function truncate( n: number, useWordBoundary: boolean ): string { - if (str.length <= n) { + if (visibleLength(str) <= n) { return str } const subString = str.substring(0, n - 1) // the original check @@ -261,7 +261,7 @@ export function visibleLength(input: string): number { // eslint-disable-next-line no-control-regex const regex = new RegExp( /* eslint-disable-next-line no-control-regex */ - "\u0000-\u0008\u000B-\u0019\u001b\u009b\u00ad\u200b\u2028\u2029\ufeff\ufe00-\ufe0f", + "[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]", "g" ) // Array.from is used to correctly count emojis diff --git a/src/components/layout/DoubleLayout.ts b/src/components/layout/DoubleLayout.ts index b295607..477e194 100644 --- a/src/components/layout/DoubleLayout.ts +++ b/src/components/layout/DoubleLayout.ts @@ -269,7 +269,7 @@ export class DoubleLayout { if ( unformattedLine.filter( (e, i) => - e.length > + visibleLength(e) > (typeof this.realWidth === "number" ? this.realWidth : this.realWidth[i]) - @@ -281,7 +281,7 @@ export class DoubleLayout { typeof this.realWidth === "number" ? this.realWidth : this.realWidth[i] - if (e.length > width - bsize) { + if (visibleLength(e) > width - bsize) { // Need to truncate const offset = 2 if (dir === "vertical") { @@ -292,19 +292,19 @@ export class DoubleLayout { ? JSON.parse(JSON.stringify(line)) : JSON.parse(JSON.stringify(secondLine)) } - let diff = e.length - width + 1 + let diff = visibleLength(e) - width + 1 // remove truncated text for (let j = newLine[i].length - 1; j >= 0; j--) { - if (newLine[i][j].text.length > diff + offset) { + if (visibleLength(newLine[i][j].text) > diff + offset) { newLine[i][j].text = truncate( newLine[i][j].text, - newLine[i][j].text.length - diff - offset, + visibleLength(newLine[i][j].text) - diff - offset, false ) break } else { - diff -= newLine[i][j].text.length + diff -= visibleLength(newLine[i][j].text) newLine[i].splice(j, 1) } } @@ -330,7 +330,7 @@ export class DoubleLayout { visibleLength(unformattedLine[0]) - bsize )}`, - style: { color: "" }, + style: { }, }) } if (this.options.boxed) @@ -371,10 +371,10 @@ export class DoubleLayout { style: { color: this.options.boxColor, bold: this.boxBold }, }) ret.push(...newLine[1]) - if (unformattedLine[1].length <= width[1] - bsize) { + if (visibleLength(unformattedLine[1]) <= width[1] - bsize) { ret.push({ text: `${" ".repeat( - width[1] - unformattedLine[1].length - (bsize > 0 ? 1 : 0) + width[1] - visibleLength(unformattedLine[1]) - (bsize > 0 ? 1 : 0) )}`, style: { color: "" }, }) @@ -415,7 +415,7 @@ export class DoubleLayout { text: `${boxChars["normal"].topLeft}${ boxChars["normal"].horizontal }${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat( - this.CM.Screen.width - trimmedTitle[0].length - 3 + this.CM.Screen.width - visibleLength(trimmedTitle[0]) - 3 )}${boxChars["normal"].topRight}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", @@ -443,7 +443,7 @@ export class DoubleLayout { text: `${boxChars["normal"].left}${boxChars["normal"].horizontal}${ trimmedTitle[1] }${boxChars["normal"].horizontal.repeat( - this.CM.Screen.width - trimmedTitle[1].length - 3 + this.CM.Screen.width - visibleLength(trimmedTitle[1]) - 3 )}${boxChars["normal"].right}`, style: { color: this.options.boxColor, bold: this.boxBold }, }) @@ -522,7 +522,7 @@ export class DoubleLayout { text: `${boxChars["normal"].topLeft}${ boxChars["normal"].horizontal }${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat( - this.realWidth[0] - trimmedTitle[0].length - 3 + this.realWidth[0] - visibleLength(trimmedTitle[0]) - 3 )}${boxChars["normal"].top}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", @@ -533,7 +533,7 @@ export class DoubleLayout { text: `${boxChars["normal"].horizontal}${ trimmedTitle[1] }${boxChars["normal"].horizontal.repeat( - this.realWidth[1] - trimmedTitle[1].length - 2 + this.realWidth[1] - visibleLength(trimmedTitle[1]) - 2 )}${boxChars["normal"].topRight}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", @@ -601,7 +601,7 @@ export class DoubleLayout { if (this.options.showTitle) { this.CM.Screen.write({ text: `${trimmedTitle[0]}${" ".repeat( - this.realWidth[0] - trimmedTitle[0].length + this.realWidth[0] - visibleLength(trimmedTitle[0]) )}${trimmedTitle[1]}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", diff --git a/src/components/widgets/Box.ts b/src/components/widgets/Box.ts index 8ff6edd..0a35f5f 100644 --- a/src/components/widgets/Box.ts +++ b/src/components/widgets/Box.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { ForegroundColorName } from "chalk/source/vendor/ansi-styles/index.js" import InPageWidgetBuilder from "../InPageWidgetBuilder.js" -import { boxChars, HEX, PhisicalValues, RGB, StyledElement, styledToSimplifiedStyled, truncate } from "../Utils.js" +import { boxChars, HEX, PhisicalValues, RGB, StyledElement, styledToSimplifiedStyled, truncate, visibleLength } from "../Utils.js" import Control from "./Control.js" import { KeyListenerArgs } from "../../ConsoleGui.js" import { RelativeMouseEvent } from "../MouseManager.js" @@ -137,19 +137,19 @@ export class Box extends Control { unformattedLine += element.text }) - if (unformattedLine.length > this.absoluteValues.width) { + if (visibleLength(unformattedLine) > this.absoluteValues.width) { const offset = 2 newLine = [...JSON.parse(JSON.stringify(line))] // Shallow copy because I just want to modify the values but not the original - let diff = unformattedLine.length - this.absoluteValues.width + 1 + let diff = visibleLength(unformattedLine) - this.absoluteValues.width + 1 // remove truncated text for (let j = newLine.length - 1; j >= 0; j--) { - if (newLine[j].text.length > diff + offset) { - newLine[j].text = truncate(newLine[j].text, (newLine[j].text.length - diff) - offset, false) + if (visibleLength(newLine[j].text) > diff + offset) { + newLine[j].text = truncate(newLine[j].text, (visibleLength(newLine[j].text) - diff) - offset, false) break } else { - diff -= newLine[j].text.length + diff -= visibleLength(newLine[j].text) newLine.splice(j, 1) } } @@ -157,11 +157,11 @@ export class Box extends Control { unformattedLine = newLine.map((element: { text: string; }) => element.text).join("") if (this.style.boxed) { - newLine.push({ text: `${" ".repeat(this.absoluteValues.width - unformattedLine.length - 1)}${boxChars["normal"].vertical}`, style: { color: this.style.color } }) + newLine.push({ text: `${" ".repeat(this.absoluteValues.width - visibleLength(unformattedLine) - 1)}${boxChars["normal"].vertical}`, style: { color: this.style.color } }) } } - if (unformattedLine.length <= this.absoluteValues.width) { - newLine.push({ text: `${" ".repeat(this.absoluteValues.width - unformattedLine.length)}`, style: { color: "" } }) + if (visibleLength(unformattedLine) <= this.absoluteValues.width) { + newLine.push({ text: `${" ".repeat(this.absoluteValues.width - visibleLength(unformattedLine))}`, style: { color: "" } }) } this.getContent().addRow(...newLine.map((element: StyledElement) => styledToSimplifiedStyled(element))) } @@ -178,10 +178,10 @@ export class Box extends Control { const truncatedText = this.style.label ? truncate(this.style.label, absVal.width - 2, false) : "" this.getContent().clear() - this.getContent().addRow({ text: `${boxChars["normal"].topLeft}${truncatedText}${boxChars["normal"].horizontal.repeat(absVal.width - (2 + truncatedText.length))}${boxChars["normal"].topRight}`, color: this.style.color }) + this.getContent().addRow({ text: `${boxChars["normal"].topLeft}${truncatedText}${boxChars["normal"].horizontal.repeat(absVal.width - (2 + visibleLength(truncatedText)))}${boxChars["normal"].topRight}`, color: this.style.color }) for (let i = 0; i < absVal.height - 2; i++) { if (this.content.getViewedPageHeight() > i) { - const rowlength = this.content.getContent()[i].reduce((acc, curr) => acc + curr.text.length, 0) + const rowlength = this.content.getContent()[i].reduce((acc, curr) => acc + visibleLength(curr.text), 0) const spaces = absVal.width - (rowlength + 2) const styledArr = [{ text: `${boxChars["normal"].vertical}`, style: { color: this.style.color }}, ...this.content.getContent()[i], { text: `${" ".repeat(spaces > 0 ? spaces : 0)}${boxChars["normal"].vertical}`, style: { color: this.style.color } }] as StyledElement[] diff --git a/src/components/widgets/Control.ts b/src/components/widgets/Control.ts index cc238e1..01bf96f 100644 --- a/src/components/widgets/Control.ts +++ b/src/components/widgets/Control.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events" import { ConsoleManager, KeyListenerArgs, InPageWidgetBuilder } from "../../ConsoleGui.js" import { MouseEvent, RelativeMouseEvent } from "../MouseManager.js" -import { PhisicalValues, StyledElement, truncate } from "../Utils.js" +import { PhisicalValues, StyledElement, truncate, visibleLength } from "../Utils.js" /** * @typedef {Object} ControlConfig @@ -355,27 +355,27 @@ export class Control extends EventEmitter { unformattedLine += element.text }) - if (unformattedLine.length > this.absoluteValues.width) { + if (visibleLength(unformattedLine) > this.absoluteValues.width) { const offset = 2 newLine = [...JSON.parse(JSON.stringify(line))] // Shallow copy because I just want to modify the values but not the original - let diff = unformattedLine.length - this.CM.Screen.width + 1 + let diff = visibleLength(unformattedLine) - this.CM.Screen.width + 1 // remove truncated text for (let j = newLine.length - 1; j >= 0; j--) { - if (newLine[j].text.length > diff + offset) { - newLine[j].text = truncate(newLine[j].text, (newLine[j].text.length - diff) - offset, true) + if (visibleLength(newLine[j].text) > diff + offset) { + newLine[j].text = truncate(newLine[j].text, (visibleLength(newLine[j].text) - diff) - offset, true) break } else { - diff -= newLine[j].text.length + diff -= visibleLength(newLine[j].text) newLine.splice(j, 1) } } // Update unformatted line unformattedLine = newLine.map((element: { text: string; }) => element.text).join("") } - if (unformattedLine.length <= this.absoluteValues.width) { - newLine.push({ text: `${" ".repeat(this.absoluteValues.width - unformattedLine.length)}`, style: { color: "" } }) + if (visibleLength(unformattedLine) <= this.absoluteValues.width) { + newLine.push({ text: `${" ".repeat(this.absoluteValues.width - visibleLength(unformattedLine))}`, style: { color: "" } }) } this.CM.Screen.write(...newLine) } diff --git a/src/components/widgets/CustomPopup.ts b/src/components/widgets/CustomPopup.ts index a6ccb62..d3d3474 100644 --- a/src/components/widgets/CustomPopup.ts +++ b/src/components/widgets/CustomPopup.ts @@ -2,7 +2,7 @@ import { EventEmitter } from "events" import { ConsoleManager, KeyListenerArgs, EOL } from "../../ConsoleGui.js" import { MouseEvent } from "../MouseManager.js" import PageBuilder from "../PageBuilder.js" -import { boxChars, PhisicalValues, StyledElement, truncate } from "../Utils.js" +import { boxChars, PhisicalValues, StyledElement, truncate, visibleLength } from "../Utils.js" /** * @description The configuration for the CustomPopup class. @@ -270,17 +270,17 @@ export class CustomPopup extends EventEmitter { line.forEach((element: { text: string }) => { unformattedLine += element.text }) - if (unformattedLine.length > width - 2) { // Need to truncate + if (visibleLength(unformattedLine) > width - 2) { // Need to truncate const offset = 2 newLine = JSON.parse(JSON.stringify(line)) // Shallow copy because I don't want to modify the values but not the original - let diff = unformattedLine.length - width + let diff = visibleLength(unformattedLine) - width // remove truncated text for (let i = newLine.length - 1; i >= 0; i--) { - if (newLine[i].text.length > diff + offset) { - newLine[i].text = truncate(newLine[i].text, (newLine[i].text.length - diff) - offset, true) + if (visibleLength(newLine[i].text) > diff + offset) { + newLine[i].text = truncate(newLine[i].text, (visibleLength(newLine[i].text) - diff) - offset, true) break } else { - diff -= newLine[i].text.length + diff -= visibleLength(newLine[i].text) newLine.splice(i, 1) } } @@ -291,8 +291,8 @@ export class CustomPopup extends EventEmitter { }) } newLine.unshift({ text: boxChars["normal"].vertical, style: { color: "white" } }) - if (unformattedLine.length <= width) { - newLine.push({ text: `${" ".repeat((width - unformattedLine.length))}`, style: { color: "" } }) + if (visibleLength(unformattedLine) <= width) { + newLine.push({ text: `${" ".repeat((width - visibleLength(unformattedLine)))}`, style: { color: "" } }) } newLine.push({ text: boxChars["normal"].vertical, style: { color: "white" } }) this.CM.Screen.write(...newLine) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index 099c483..a9603a2 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events" import { ConsoleManager, KeyListenerArgs, EOL } from "../../ConsoleGui.js" import { MouseEvent } from "../MouseManager.js" -import { boxChars, PhisicalValues } from "../Utils.js" +import { boxChars, PhisicalValues, visibleLength } from "../Utils.js" import chalk from "chalk" /** @@ -261,7 +261,7 @@ export class InputPopup extends EventEmitter { break default: - if (v.toString().length < 20 && key.sequence.length === 1) { + if (visibleLength(v.toString()) < 20 && key.sequence.length === 1) { let tmp = v.toString() tmp += key.sequence this.value = tmp From ad6ff139f348b50eb288538eb83eecefa8d08471 Mon Sep 17 00:00:00 2001 From: Compositr <43405050+Compositr@users.noreply.github.com> Date: Tue, 27 Jun 2023 18:30:00 +1000 Subject: [PATCH 13/14] feat: flashing cursor * consolidate delete and confirm functions --- src/components/widgets/InputPopup.ts | 70 +++++++++++++++------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index a9603a2..1e6743a 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -57,6 +57,9 @@ export class InputPopup extends EventEmitter { value: string | number // Position of the cursor. 0-indexed (0 = before all the text) cursorPos: number + flashLoop = setInterval(() => { + this.draw(); this.CM.refresh() + }, 500) private numeric: boolean private visible: boolean private marginTop: number @@ -171,26 +174,17 @@ export class InputPopup extends EventEmitter { break case "return": { - this.emit("confirm", Number(this.value)) - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.confirmDel() } break case "escape": { - this.emit("cancel") - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.delete() } break case "q": { - this.CM.emit("exit") - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.delete() } break default: @@ -226,26 +220,17 @@ export class InputPopup extends EventEmitter { break case "return": { - this.emit("confirm", this.value) - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.confirmDel() } break case "escape": { - this.emit("cancel") - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.delete() } break case "q": { - this.CM.emit("exit") - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.delete() } break case "delete": @@ -483,15 +468,15 @@ export class InputPopup extends EventEmitter { let content = "" // Draw an input field - if (this.value.toString().length === 0 && this.placeholder?.length) - content += `${boxChars["normal"].vertical}${"> "}${chalk.dim.gray( - `${this.placeholder}` - )}${" ".repeat(windowWidth - this.placeholder.length - 2)}${boxChars["normal"].vertical - }${EOL}` - else - content += `${boxChars["normal"].vertical}${"> "}${this.value - }█${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical - }${EOL}` + // if (this.value.toString().length === 0 && this.placeholder?.length) + // content += `${boxChars["normal"].vertical}${"> "}${chalk.gray( + // this.placeholder + // )}${" ".repeat(windowWidth - this.placeholder.length - 2)}${boxChars["normal"].vertical + // }${EOL}` + // else + content += `${boxChars["normal"].vertical}${"> "}${this.value + }█${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical + }${EOL}` const windowDesign = `${header}${content}${footer}` const windowDesignLines = windowDesign.split(EOL) @@ -501,6 +486,14 @@ export class InputPopup extends EventEmitter { centerScreen + this.offsetX, this.marginTop + index + this.offsetY ) + + if(index === 3 && this.placeholder?.length && this.value.toString().length === 0) { + const isOddSecond = Math.round(Date.now() / 100) % 2 + return this.CM.Screen.write({ text: `${boxChars["normal"].vertical}${"> "}${isOddSecond ? "█" : " "}${chalk.gray( + this.placeholder + )}${" ".repeat(windowWidth - this.placeholder.length - 3)}${boxChars["normal"].vertical + }${EOL}`, style: { color: "white" } }) + } this.CM.Screen.write({ text: line, style: { color: "white" } }) }) this.absoluteValues = { @@ -511,6 +504,17 @@ export class InputPopup extends EventEmitter { } return this } + + confirmDel() { + this.emit("confirm", Number(this.value)) + this.delete() + } + + delete() { + this.CM.unregisterPopup(this) + this.hide() + clearInterval(this.flashLoop) + } } export default InputPopup From 20768908f2f3a949bb389f99c93dfaf6a03f4f02 Mon Sep 17 00:00:00 2001 From: Elia Lazzari Date: Tue, 27 Jun 2023 11:11:30 +0200 Subject: [PATCH 14/14] Add cursor blink also if the input is filled I've added this to blink the cursor also when there's a value. For the rest, it's a really nice work, thank you so much =) --- src/components/widgets/InputPopup.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index 1e6743a..255a59c 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -487,12 +487,22 @@ export class InputPopup extends EventEmitter { this.marginTop + index + this.offsetY ) - if(index === 3 && this.placeholder?.length && this.value.toString().length === 0) { + if (index === 3 && this.placeholder?.length && this.value.toString().length === 0) { const isOddSecond = Math.round(Date.now() / 100) % 2 - return this.CM.Screen.write({ text: `${boxChars["normal"].vertical}${"> "}${isOddSecond ? "█" : " "}${chalk.gray( - this.placeholder - )}${" ".repeat(windowWidth - this.placeholder.length - 3)}${boxChars["normal"].vertical - }${EOL}`, style: { color: "white" } }) + return this.CM.Screen.write({ + text: `${boxChars["normal"].vertical}${"> "}${isOddSecond ? "█" : " "}${chalk.gray( + this.placeholder + )}${" ".repeat(windowWidth - this.placeholder.length - 3)}${boxChars["normal"].vertical + }${EOL}`, style: { color: "white" } + }) + } else if (index === 3) { + const isOddSecond = Math.round(Date.now() / 100) % 2 + // write value and then the cursor (█) + return this.CM.Screen.write({ + text: `${boxChars["normal"].vertical}${"> "}${this.value + }${isOddSecond ? "█" : " "}${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical + }${EOL}`, style: { color: "white" } + }) } this.CM.Screen.write({ text: line, style: { color: "white" } }) })