diff options
Diffstat (limited to 'v2')
-rw-r--r-- | v2/elements.ts | 76 | ||||
-rw-r--r-- | v2/screen.ts | 97 | ||||
-rw-r--r-- | v2/screen/login.ts | 33 | ||||
-rw-r--r-- | v2/screenbuilder.ts | 30 |
4 files changed, 236 insertions, 0 deletions
diff --git a/v2/elements.ts b/v2/elements.ts new file mode 100644 index 0000000..d712d65 --- /dev/null +++ b/v2/elements.ts @@ -0,0 +1,76 @@ +import chalk from "chalk"; +import { type Key } from 'node:readline'; +import { type Screen } from "./screen.ts"; + +export abstract class Element { + override focusable: boolean = false; + focused: boolean = false; + // screen property is asigned by the addElement function of Scren + //@ts-ignore + screen: Screen; + abstract render(): void; + onkeypres(key: Key): void {}; +} + +export class Text extends Element { + text: string; + constructor(text: string) { + super(); + this.text = text; + } + render() { + process.stdout.write(this.text) + } +} + +export class Input extends Element { + focusable: boolean = true; + value: string = ""; + + isPassword: boolean = false; + + render(): void { + let text = this.value + if (this.isPassword) text = text.replace(/[^]/g, '*'); + if (this.focused) text += "_" + console.log(text) + } + + onkeypres(key: Key): void { + //@ts-ignore + if (key.meta || key.code || ["return", "backspace"].includes(key.name)) { + switch (key.name) { + case "return": + this.focused = false; + const focusableIDs = Object.keys(this.screen.getFocusable()); + const focusedIndex = focusableIDs.indexOf(this.screen.focusedElementId); + this.screen.focus(focusableIDs[(focusedIndex - 1) % focusableIDs.length]); + break; + + case "backspace": + const prevValue = '' + this.value + // logs.push(`doing backspace : before ${prevValue}, after ${prevValue.substring(0, prevValue.length - 1)} : 0-${prevValue.length - 1}`) + this.value = prevValue.substring(0, prevValue.length - 1) + break; + } + return; + } + if (!key.sequence || key.sequence.length > 1 || key.name != key.sequence?.toLowerCase()) return; + this.value += key.sequence; + } + + constructor(isPassword: boolean, ) { + super() + this.isPassword = isPassword + } +} + +export class Button extends Text { + focusable: boolean = true; + constructor (text: string) { + super(text) + } + render(): void { + console.log(`(${(this.focused ? chalk.bgWhite : (a:string)=>a)(this.text)})`) + } +} diff --git a/v2/screen.ts b/v2/screen.ts new file mode 100644 index 0000000..0ff6f98 --- /dev/null +++ b/v2/screen.ts @@ -0,0 +1,97 @@ +import type { Element } from "./elements.ts" +import readline from 'node:readline'; + +readline.emitKeypressEvents(process.stdin); + +const logs: string[] = []; + +function onexit() { + console.clear() + console.log("\nQuitting meower CL") + for (const log of logs) { + console.log(log) + } +} + +export class Screen { + elements: Map<string, Element> = new Map<string, Element>(); + name: string; + focusedElementId: string = ''; + + constructor(name: string) { + this.name = name; + } + + handleKeypress(chunk: any, key: any, screen: Screen) { + const focusableIDs = Object.keys(screen.getFocusable()); + const focusedIndex = focusableIDs.indexOf(screen.focusedElementId); + if (key && key.name == 'escape') { + onexit(); + process.exit(); + } + + if (['up', 'left'].includes(key.name)) { + // logs.push(`Got up key, moving focus upward ${focusedIndex} ${(focusedIndex - 1) % focusableIDs.length}`) + screen.focus(focusableIDs[(focusedIndex - 1) % focusableIDs.length]); + return screen.render() + } + if (['down', 'right'].includes(key.name)) { + // logs.push(`Got down key, moving focus downward ${focusedIndex} ${(focusedIndex + 1) % focusableIDs.length}`) + screen.focus(focusableIDs[(focusedIndex + 1) % focusableIDs.length]); + return screen.render() + } + + // logs.push("pressed key, data: " + JSON.stringify(key)) + if (!screen.focusedElementId) return; + const focusedElement = screen.getFocusedElement(); + focusedElement?.onkeypres(key) + this.render() + } + + getKeypressHandler(screen: Screen) { + return (chunk: any, key: any) => this.handleKeypress(chunk,key, screen); + } + + ready() { + process.stdin.on('keypress', this.getKeypressHandler(this)); + this.render() + } + + off() { + process.stdin.off('keypress', this.getKeypressHandler(this)) + } + + addElement(name: string, element: Element) { + if(this.elements.has(name)) throw new Error(); + element.screen = this; + this.elements.set(name, element); + } + + render() { + console.clear() + this.elements.forEach(element => { + element.render() + }); + } + + getFocusable() { + return Object.fromEntries([...this.elements.entries()].filter(([k, v]) => v.focusable)) + } + + getElements() { + return Object.fromEntries([...this.elements.entries()]) + } + + focus(id: string) { + this.elements.forEach(e => e.focused = false); + const focusElem = this.elements.get(id) as Element + focusElem.focused = true; + this.focusedElementId = id + } + + getFocusedElement(): Element|undefined { + return this.focusedElementId ? this.elements.get(this.focusedElementId) as Element : undefined + } +} + +if (process.stdin.isTTY) process.stdin.setRawMode(true); // makes the terminal send stdin without the user pressing enter diff --git a/v2/screen/login.ts b/v2/screen/login.ts new file mode 100644 index 0000000..c9e4fec --- /dev/null +++ b/v2/screen/login.ts @@ -0,0 +1,33 @@ +import { ElemType } from "../screenbuilder.ts"; + +export default { + elements: [ + { + type: ElemType.TextElem, + id: 'username-label', + data: ["Username: \n"] + }, + { + type: ElemType.InputElem, + id: 'username-input', + data: [false] + }, + { + type: ElemType.TextElem, + id: 'password-label', + data: ["Password: \n"] + }, + { + type: ElemType.InputElem, + id: 'password-input', + data: [true] + }, + { + type: ElemType.ButtonElem, + id: 'done-btn', + data: ["Done"] + } + ], + focus: "username-input", + name: 'login' +} \ No newline at end of file diff --git a/v2/screenbuilder.ts b/v2/screenbuilder.ts new file mode 100644 index 0000000..dcf1af7 --- /dev/null +++ b/v2/screenbuilder.ts @@ -0,0 +1,30 @@ +import { Screen } from "./screen.ts"; +import * as elements from "./elements.ts"; + +export enum ElemType { + TextElem, + InputElem, + ButtonElem, +} + +const types = { + 0: elements.Text, + 1: elements.Input, + 2: elements.Button +} + +type BuilderElem = { + type: ElemType, + id: string, + data: any[] +} + +export function build(data: {elements: BuilderElem[],focus?:string,name:string}) { + const screen = new Screen(data.name); + for (const element of data.elements) { + //@ts-ignore + screen.addElement(element.id, new types[element.type](...element.data)) + } + if (data.focus) screen.focus(data.focus); + screen.ready() +} \ No newline at end of file |