First output of welcome message

This commit is contained in:
Timothy Warren 2023-11-09 12:05:30 -05:00
parent 7eb07520ae
commit abee0a80bf
8 changed files with 116 additions and 20 deletions

View File

@ -14,8 +14,8 @@ function getSizeFromTput(): ITerminalSize {
); );
return { return {
rows: (rows > 0) ? rows : 25, rows: (rows > 0) ? rows + 1 : 25,
cols: (cols > 0) ? cols : 80, cols: (cols > 0) ? cols + 1 : 80,
}; };
} }

View File

@ -3,8 +3,11 @@ function esc(pieces: TemplateStringsArray): string {
} }
export const Ansi = { export const Ansi = {
ClearLine: esc`K`,
ClearScreen: esc`2J`, ClearScreen: esc`2J`,
ResetCursor: esc`H`, ResetCursor: esc`H`,
HideCursor: esc`?25l`,
ShowCursor: esc`?25h`,
moveCursor: function moveCursor(row: number, col: number): string { moveCursor: function moveCursor(row: number, col: number): string {
return `\x1b${row};${col}H`; return `\x1b${row};${col}H`;
}, },

View File

@ -1,3 +1,5 @@
import { strlen } from '../strings.ts';
class Buffer { class Buffer {
#b = ''; #b = '';
@ -19,6 +21,10 @@ class Buffer {
getBuffer(): string { getBuffer(): string {
return this.#b; return this.#b;
} }
strlen(): number {
return strlen(this.#b);
}
} }
export default Buffer; export default Buffer;

View File

@ -1,8 +1,13 @@
import Ansi from './ansi.ts'; import Ansi from './ansi.ts';
import Buffer from './buffer.ts'; import Buffer from './buffer.ts';
import { importDefaultForRuntime } from '../runtime.ts'; import {
import { ctrl_key } from '../strings.ts'; ctrl_key,
import { ITerminalSize } from '../types.ts'; importDefaultForRuntime,
ITerminalSize,
strlen,
truncate,
VERSION,
} from '../mod.ts';
export class Editor { export class Editor {
#buffer: Buffer; #buffer: Buffer;
@ -29,27 +34,46 @@ export class Editor {
} }
} }
// -------------------------------------------------------------------------------------------------------------------
// Terminal Output / Drawing
// -------------------------------------------------------------------------------------------------------------------
/** /**
* Clear the screen and write out the buffer * Clear the screen and write out the buffer
*/ */
public async refreshScreen(): Promise<void> { public async refreshScreen(): Promise<void> {
const { write } = await importDefaultForRuntime('terminal_io'); const { write } = await importDefaultForRuntime('terminal_io');
this.clearScreen(); this.#buffer.append(Ansi.HideCursor);
this.#buffer.append(Ansi.ResetCursor);
this.drawRows(); this.drawRows();
this.#buffer.append(Ansi.ShowCursor);
await write(this.#buffer.getBuffer()); await write(this.#buffer.getBuffer());
this.#buffer.clear(); this.#buffer.clear();
} }
private drawRows(): void { private clearScreen(): void {
for (let y = 0; y <= this.#screenRows; y++) { importDefaultForRuntime('terminal_io').then(({ write }) => {
this.#buffer.appendLine('~'); this.#buffer.append(Ansi.ClearScreen);
} this.#buffer.append(Ansi.ResetCursor);
write(this.#buffer.getBuffer()).then(() => {});
});
} }
private clearScreen(): void { private drawRows(): void {
this.#buffer.append(Ansi.ClearScreen); for (let y = 0; y < this.#screenRows; y++) {
this.#buffer.append(Ansi.ResetCursor); 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');
}
}
} }
} }

View File

@ -3,3 +3,5 @@ export * from './runtime.ts';
export * from './strings.ts'; export * from './strings.ts';
export * from './termios.ts'; export * from './termios.ts';
export type * from './types.ts'; export type * from './types.ts';
export const VERSION = '0.0.1';

View File

@ -6,19 +6,23 @@ export function chars(s: string): string[] {
return s.split(/(?:)/u); 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? * Is the character part of ascii?
* *
* @param char - a one character string to check * @param char - a one character string to check
*/ */
export function is_ascii(char: string): boolean { export function is_ascii(char: string): boolean {
if (typeof char !== 'string') {
return false;
}
return chars(char).every((char) => { return chars(char).every((char) => {
const point = char.codePointAt(0); const point = char.codePointAt(0);
if (typeof point === 'undefined') { if (point === undefined) {
return false; return false;
} }
@ -58,3 +62,17 @@ export function ctrl_key(char: string): string {
// If it's not ascii, just return the input key code // If it's not ascii, just return the input key code
return char; 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('');
}

View File

@ -1,5 +1,12 @@
import { importDefaultForRuntime, ITestBase } from './mod.ts'; 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'); 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.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.test('is_ascii properly discerns ascii chars', () => {
t.assertTrue(is_ascii('asjyverkjhsdf1928374')); t.assertTrue(is_ascii('asjyverkjhsdf1928374'));
t.assertFalse(is_ascii('😺acalskjsdf')); 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), '👨‍👩‍👧');
});

View File

@ -20,12 +20,13 @@ export async function main() {
const t = await getTermios(); const t = await getTermios();
t.enableRawMode(); t.enableRawMode();
onExit(() => { onExit(() => {
console.info('Exit handler called, disabling raw mode'); console.log('Exit handler called, disabling raw mode\r\n');
t.disableRawMode(); t.disableRawMode();
}); });
// Create the editor itself // Create the editor itself
const editor = new Editor(getSize()); const editor = new Editor(getSize());
await editor.refreshScreen();
// The main event loop // The main event loop
for await (const chunk of inputLoop()) { for await (const chunk of inputLoop()) {