import Ansi, { KeyCommand } from './ansi.ts'; import Buffer from './buffer.ts'; import Document from './document.ts'; import { ctrl_key, IPoint, ITerminalSize, truncate, VERSION } from './mod.ts'; export class Editor { #buffer: Buffer; #screen: ITerminalSize; #cursor: IPoint; #document: Document; constructor(terminalSize: ITerminalSize, args: string[]) { this.#buffer = new Buffer(); this.#screen = terminalSize; this.#cursor = { x: 0, y: 0, }; this.#document = (args.length >= 2) ? Document.open(args[1]) : Document.empty(); } // -------------------------------------------------------------------------- // Command/input mapping // -------------------------------------------------------------------------- /** * Determine what to do based on input * @param input - the decoded chunk of stdin */ public processKeyPress(input: string): boolean { switch (input) { case ctrl_key('q'): this.clearScreen().then(() => {}); return false; case KeyCommand.Home: this.#cursor.x = 0; break; case KeyCommand.End: this.#cursor.x = this.#screen.cols - 1; break; case KeyCommand.PageUp: case KeyCommand.PageDown: { let times = this.#screen.rows; while (times--) { this.moveCursor( input === KeyCommand.PageUp ? KeyCommand.ArrowUp : KeyCommand.ArrowDown, ); } } break; case KeyCommand.ArrowUp: case KeyCommand.ArrowDown: case KeyCommand.ArrowRight: case KeyCommand.ArrowLeft: this.moveCursor(input); break; } return true; } private moveCursor(char: string): void { switch (char) { case KeyCommand.ArrowLeft: if (this.#cursor.x > 0) { this.#cursor.x--; } break; case KeyCommand.ArrowRight: if (this.#cursor.x < this.#screen.cols) { this.#cursor.x++; } break; case KeyCommand.ArrowUp: if (this.#cursor.y > 0) { this.#cursor.y--; } break; case KeyCommand.ArrowDown: if (this.#cursor.y < this.#screen.rows) { this.#cursor.y++; } break; } } // -------------------------------------------------------------------------- // Terminal Output / Drawing // -------------------------------------------------------------------------- /** * Clear the screen and write out the buffer */ public async refreshScreen(): Promise { this.#buffer.append(Ansi.HideCursor); this.#buffer.append(Ansi.ResetCursor); this.drawRows(); this.#buffer.append( Ansi.moveCursor(this.#cursor.y, this.#cursor.x), ); this.#buffer.append(Ansi.ShowCursor); await this.#buffer.flush(); } private async clearScreen(): Promise { this.#buffer.append(Ansi.ClearScreen); this.#buffer.append(Ansi.ResetCursor); await this.#buffer.flush(); } private drawRows(): void { for (let y = 0; y < this.#screen.rows; y++) { if (y >= this.#document.numrows) { this.drawPlaceholderRow(y); } else { this.drawFileRow(y); } } } private drawFileRow(_y: number): void { const row = this.#document.getRow(0); let len = row?.chars.length ?? 0; if (len > this.#screen.cols) { len = this.#screen.cols; } this.#buffer.appendLine(truncate(row!.toString(), len)); } private drawPlaceholderRow(y: number): void { if (y === Math.trunc(this.#screen.rows / 2)) { const message = `Kilo editor -- version ${VERSION}`; const messageLen = (message.length > this.#screen.cols) ? this.#screen.cols : message.length; let padding = Math.trunc((this.#screen.cols - messageLen) / 2); if (padding > 0) { this.#buffer.append('~'); padding -= 1; this.#buffer.append(' '.repeat(padding)); } this.#buffer.append(truncate(message, messageLen)); } else { this.#buffer.append('~'); } this.#buffer.append(Ansi.ClearLine); if (y < this.#screen.rows - 1) { this.#buffer.appendLine(''); } } }