From abee0a80bf583bdc8a18f8286232103969050ddc Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 9 Nov 2023 12:05:30 -0500 Subject: [PATCH] First output of welcome message --- src/bun/terminal_io.ts | 4 ++-- src/common/editor/ansi.ts | 3 +++ src/common/editor/buffer.ts | 6 +++++ src/common/editor/editor.ts | 46 ++++++++++++++++++++++++++++--------- src/common/mod.ts | 2 ++ src/common/strings.ts | 28 ++++++++++++++++++---- src/common/strings_test.ts | 44 ++++++++++++++++++++++++++++++++++- src/scroll.ts | 3 ++- 8 files changed, 116 insertions(+), 20 deletions(-) diff --git a/src/bun/terminal_io.ts b/src/bun/terminal_io.ts index 7d28184..7da04ba 100644 --- a/src/bun/terminal_io.ts +++ b/src/bun/terminal_io.ts @@ -14,8 +14,8 @@ function getSizeFromTput(): ITerminalSize { ); return { - rows: (rows > 0) ? rows : 25, - cols: (cols > 0) ? cols : 80, + rows: (rows > 0) ? rows + 1 : 25, + cols: (cols > 0) ? cols + 1 : 80, }; } diff --git a/src/common/editor/ansi.ts b/src/common/editor/ansi.ts index 1da01af..4b90daa 100644 --- a/src/common/editor/ansi.ts +++ b/src/common/editor/ansi.ts @@ -3,8 +3,11 @@ function esc(pieces: TemplateStringsArray): string { } export const Ansi = { + ClearLine: esc`K`, ClearScreen: esc`2J`, ResetCursor: esc`H`, + HideCursor: esc`?25l`, + ShowCursor: esc`?25h`, moveCursor: function moveCursor(row: number, col: number): string { return `\x1b${row};${col}H`; }, diff --git a/src/common/editor/buffer.ts b/src/common/editor/buffer.ts index 32e0b3f..132a009 100644 --- a/src/common/editor/buffer.ts +++ b/src/common/editor/buffer.ts @@ -1,3 +1,5 @@ +import { strlen } from '../strings.ts'; + class Buffer { #b = ''; @@ -19,6 +21,10 @@ class Buffer { getBuffer(): string { return this.#b; } + + strlen(): number { + return strlen(this.#b); + } } export default Buffer; diff --git a/src/common/editor/editor.ts b/src/common/editor/editor.ts index c97402e..cb9c143 100644 --- a/src/common/editor/editor.ts +++ b/src/common/editor/editor.ts @@ -1,8 +1,13 @@ import Ansi from './ansi.ts'; import Buffer from './buffer.ts'; -import { importDefaultForRuntime } from '../runtime.ts'; -import { ctrl_key } from '../strings.ts'; -import { ITerminalSize } from '../types.ts'; +import { + ctrl_key, + importDefaultForRuntime, + ITerminalSize, + strlen, + truncate, + VERSION, +} from '../mod.ts'; export class Editor { #buffer: Buffer; @@ -29,27 +34,46 @@ export class Editor { } } + // ------------------------------------------------------------------------------------------------------------------- + // Terminal Output / Drawing + // ------------------------------------------------------------------------------------------------------------------- + /** * Clear the screen and write out the buffer */ public async refreshScreen(): Promise { const { write } = await importDefaultForRuntime('terminal_io'); - this.clearScreen(); + this.#buffer.append(Ansi.HideCursor); + this.#buffer.append(Ansi.ResetCursor); this.drawRows(); + this.#buffer.append(Ansi.ShowCursor); await write(this.#buffer.getBuffer()); this.#buffer.clear(); } - private drawRows(): void { - for (let y = 0; y <= this.#screenRows; y++) { - this.#buffer.appendLine('~'); - } + private clearScreen(): void { + importDefaultForRuntime('terminal_io').then(({ write }) => { + this.#buffer.append(Ansi.ClearScreen); + this.#buffer.append(Ansi.ResetCursor); + write(this.#buffer.getBuffer()).then(() => {}); + }); } - private clearScreen(): void { - this.#buffer.append(Ansi.ClearScreen); - this.#buffer.append(Ansi.ResetCursor); + private drawRows(): void { + for (let y = 0; y < this.#screenRows; y++) { + if (y === this.#screenRows / 3) { + const message = `Kilo editor -- version ${VERSION}`; + this.#buffer.append(truncate(message, this.#screenCols)); + } else { + this.#buffer.append('~'); + } + + this.#buffer.append(Ansi.ClearLine); + if (y < this.#screenRows - 1) { + this.#buffer.append('\r\n'); + } + } } } diff --git a/src/common/mod.ts b/src/common/mod.ts index 995788f..ca261b4 100644 --- a/src/common/mod.ts +++ b/src/common/mod.ts @@ -3,3 +3,5 @@ export * from './runtime.ts'; export * from './strings.ts'; export * from './termios.ts'; export type * from './types.ts'; + +export const VERSION = '0.0.1'; diff --git a/src/common/strings.ts b/src/common/strings.ts index 3bbc44f..e320b8c 100644 --- a/src/common/strings.ts +++ b/src/common/strings.ts @@ -6,19 +6,23 @@ export function chars(s: string): string[] { return s.split(/(?:)/u); } +/** + * Get the 'character length' of a string, not its UTF16 byte count + * @param s - the string to check + */ +export function strlen(s: string): number { + return chars(s).length; +} + /** * Is the character part of ascii? * * @param char - a one character string to check */ export function is_ascii(char: string): boolean { - if (typeof char !== 'string') { - return false; - } - return chars(char).every((char) => { const point = char.codePointAt(0); - if (typeof point === 'undefined') { + if (point === undefined) { return false; } @@ -58,3 +62,17 @@ export function ctrl_key(char: string): string { // If it's not ascii, just return the input key code return char; } + +/** + * Trim a string to a max number of characters + * @param s + * @param maxLen + */ +export function truncate(s: string, maxLen: number): string { + const chin = chars(s); + if (maxLen >= chin.length) { + return s; + } + + return chin.slice(0, maxLen).join(''); +} diff --git a/src/common/strings_test.ts b/src/common/strings_test.ts index 8483ad0..51ed4a1 100644 --- a/src/common/strings_test.ts +++ b/src/common/strings_test.ts @@ -1,5 +1,12 @@ import { importDefaultForRuntime, ITestBase } from './mod.ts'; -import { chars, is_ascii } from './strings.ts'; +import { + chars, + ctrl_key, + is_ascii, + is_control, + strlen, + truncate, +} from './strings.ts'; const t: ITestBase = await importDefaultForRuntime('test_base'); @@ -7,7 +14,42 @@ t.test('chars fn properly splits strings into unicode characters', () => { t.assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']); }); +t.test('ctrl_key fn returns expected values', () => { + const ctrl_a = ctrl_key('a'); + t.assertTrue(is_control(ctrl_a)); + t.assertEquals(ctrl_a, String.fromCodePoint(0x01)); + + const invalid = ctrl_key('😺'); + t.assertFalse(is_control(invalid)); + t.assertEquals(invalid, '😺'); +}); + t.test('is_ascii properly discerns ascii chars', () => { t.assertTrue(is_ascii('asjyverkjhsdf1928374')); t.assertFalse(is_ascii('😺acalskjsdf')); }); + +t.test('is_control fn works as expected', () => { + t.assertFalse(is_control('abc')); + t.assertTrue(is_control(String.fromCodePoint(0x01))); + t.assertFalse(is_control('😺')); +}); + +t.test('strlen fn returns expected length for multibyte characters', () => { + t.assertEquals(strlen('😺😸😹'), 3); + t.assertNotEquals('😺😸😹'.length, strlen('😺😸😹')); + + // Skin tone modifier + base character + t.assertEquals(strlen('🀰🏼'), 2); + t.assertNotEquals('🀰🏼'.length, strlen('🀰🏼')); + + // This has 4 sub-characters, and 3 zero-width-joiners + t.assertEquals(strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'), 7); + t.assertNotEquals('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'.length, strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦')); +}); + +t.test('truncate shortens strings', () => { + t.assertEquals(truncate('😺😸😹', 1), '😺'); + t.assertEquals(truncate('😺😸😹', 5), '😺😸😹'); + t.assertEquals(truncate('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦', 5), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); +}); diff --git a/src/scroll.ts b/src/scroll.ts index f5c74f5..c7e379f 100644 --- a/src/scroll.ts +++ b/src/scroll.ts @@ -20,12 +20,13 @@ export async function main() { const t = await getTermios(); t.enableRawMode(); onExit(() => { - console.info('Exit handler called, disabling raw mode'); + console.log('Exit handler called, disabling raw mode\r\n'); t.disableRawMode(); }); // Create the editor itself const editor = new Editor(getSize()); + await editor.refreshScreen(); // The main event loop for await (const chunk of inputLoop()) {