More tests
This commit is contained in:
parent
301196352a
commit
8b5fb17603
9
coverage.sh
Executable file
9
coverage.sh
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
rm -fr /cov_profile/
|
||||||
|
deno test --allow-all --coverage=cov_profile
|
||||||
|
deno coverage cov_profile --lcov > cov_profile/cov_profile.lcov
|
||||||
|
genhtml -o cov_profile cov_profile/cov_profile.lcov
|
||||||
|
rm cov_profile/*.json
|
||||||
|
open cov_profile/index.html
|
||||||
|
|
||||||
|
|
2
justfile
2
justfile
@ -48,7 +48,7 @@ deno-check:
|
|||||||
|
|
||||||
# Test with deno
|
# Test with deno
|
||||||
deno-test:
|
deno-test:
|
||||||
deno test --allow-all
|
deno test --allow-all --unstable
|
||||||
|
|
||||||
# Create test coverage report with deno
|
# Create test coverage report with deno
|
||||||
deno-coverage:
|
deno-coverage:
|
||||||
|
@ -42,6 +42,7 @@ const BunFFI: IFFI = {
|
|||||||
tcsetattr,
|
tcsetattr,
|
||||||
cfmakeraw,
|
cfmakeraw,
|
||||||
getPointer: ptr,
|
getPointer: ptr,
|
||||||
|
close: cStdLib.close,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BunFFI;
|
export default BunFFI;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { IFIO } from '../common/runtime.ts';
|
import { IFileIO } from '../common/runtime.ts';
|
||||||
|
|
||||||
import { appendFileSync, readFileSync } from 'node:fs';
|
import { appendFileSync, readFileSync } from 'node:fs';
|
||||||
import { appendFile } from 'node:fs/promises';
|
import { appendFile } from 'node:fs/promises';
|
||||||
|
|
||||||
const BunFileIO: IFIO = {
|
const BunFileIO: IFileIO = {
|
||||||
openFile: async (path: string): Promise<string> => {
|
openFile: async (path: string): Promise<string> => {
|
||||||
const file = await Bun.file(path);
|
const file = await Bun.file(path);
|
||||||
return await file.text();
|
return await file.text();
|
||||||
|
@ -4,6 +4,11 @@
|
|||||||
import { ITerminal, ITerminalSize } from '../common/mod.ts';
|
import { ITerminal, ITerminalSize } from '../common/mod.ts';
|
||||||
import Ansi from '../common/ansi.ts';
|
import Ansi from '../common/ansi.ts';
|
||||||
|
|
||||||
|
const defaultTerminalSize: ITerminalSize = {
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
};
|
||||||
|
|
||||||
const BunTerminalIO: ITerminal = {
|
const BunTerminalIO: ITerminal = {
|
||||||
// Deno only returns arguments passed to the script, so
|
// Deno only returns arguments passed to the script, so
|
||||||
// remove the bun runtime executable, and entry script arguments
|
// remove the bun runtime executable, and entry script arguments
|
||||||
@ -32,8 +37,8 @@ const BunTerminalIO: ITerminal = {
|
|||||||
const rawCode = (new TextDecoder()).decode(chunk);
|
const rawCode = (new TextDecoder()).decode(chunk);
|
||||||
const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1');
|
const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1');
|
||||||
const [srows, scols] = res.split(';');
|
const [srows, scols] = res.split(';');
|
||||||
const rows = parseInt(srows, 10);
|
const rows = parseInt(srows, 10) ?? 24;
|
||||||
const cols = parseInt(scols, 10);
|
const cols = parseInt(scols, 10) ?? 80;
|
||||||
|
|
||||||
// Clear the screen
|
// Clear the screen
|
||||||
await write(Ansi.ClearScreen + Ansi.ResetCursor);
|
await write(Ansi.ClearScreen + Ansi.ResetCursor);
|
||||||
@ -44,10 +49,7 @@ const BunTerminalIO: ITerminal = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return defaultTerminalSize;
|
||||||
rows: 24,
|
|
||||||
cols: 80,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
writeStdout: async function write(s: string): Promise<void> {
|
writeStdout: async function write(s: string): Promise<void> {
|
||||||
const buffer = new TextEncoder().encode(s);
|
const buffer = new TextEncoder().encode(s);
|
||||||
|
@ -43,11 +43,13 @@ export const Ansi = {
|
|||||||
moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`,
|
moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
/**
|
||||||
|
* Convert input from ANSI escape sequences into a form
|
||||||
export function readKey(raw: Uint8Array): string {
|
* that can be more easily mapped to editor commands
|
||||||
const parsed = decoder.decode(raw);
|
*
|
||||||
|
* @param parsed - the decoded chunk of input
|
||||||
|
*/
|
||||||
|
export function readKey(parsed: string): string {
|
||||||
// Return the input if it's unambiguous
|
// Return the input if it's unambiguous
|
||||||
if (parsed in KeyCommand) {
|
if (parsed in KeyCommand) {
|
||||||
return parsed;
|
return parsed;
|
||||||
|
33
src/common/ansi_test.ts
Normal file
33
src/common/ansi_test.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { getTestRunner } from './mod.ts';
|
||||||
|
import Ansi, { KeyCommand, readKey } from './ansi.ts';
|
||||||
|
|
||||||
|
getTestRunner().then((t) => {
|
||||||
|
t.test('Ansi.moveCursor', () => {
|
||||||
|
t.assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test('Ansi.moveCursorForward', () => {
|
||||||
|
t.assertEquals(Ansi.moveCursorForward(2), '\x1b[2C');
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test('Ansi.moveCursorDown', () => {
|
||||||
|
t.assertEquals(Ansi.moveCursorDown(7), '\x1b[7B');
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test('readKey', () => {
|
||||||
|
// Ignore unhandled escape sequences
|
||||||
|
t.assertEquals(readKey('\x1b[]'), '\x1b[]');
|
||||||
|
|
||||||
|
// Pass explicitly mapped values right through
|
||||||
|
t.assertEquals(readKey(KeyCommand.ArrowUp), KeyCommand.ArrowUp);
|
||||||
|
t.assertEquals(readKey(KeyCommand.Home), KeyCommand.Home);
|
||||||
|
|
||||||
|
['\x1bOH', '\x1b[7~', '\x1b[1~', '\x1b[H'].forEach((code) => {
|
||||||
|
t.assertEquals(readKey(code), KeyCommand.Home);
|
||||||
|
});
|
||||||
|
|
||||||
|
['\x1bOF', '\x1b[8~', '\x1b[4~', '\x1b[F'].forEach((code) => {
|
||||||
|
t.assertEquals(readKey(code), KeyCommand.End);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,5 @@
|
|||||||
import { strlen, truncate } from './utils.ts';
|
import { strlen, truncate } from './utils.ts';
|
||||||
import { getRuntime } from './runtime.ts';
|
import { getRuntime, importForRuntime } from './runtime.ts';
|
||||||
|
|
||||||
class Buffer {
|
class Buffer {
|
||||||
#b = '';
|
#b = '';
|
||||||
@ -12,7 +12,7 @@ class Buffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public appendLine(s = ''): void {
|
public appendLine(s = ''): void {
|
||||||
this.#b += (s ?? '') + '\r\n';
|
this.#b += s + '\r\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
@ -23,7 +23,7 @@ class Buffer {
|
|||||||
* Output the contents of the buffer into stdout
|
* Output the contents of the buffer into stdout
|
||||||
*/
|
*/
|
||||||
public async flush() {
|
public async flush() {
|
||||||
const { term } = await getRuntime();
|
const term = await importForRuntime('terminal_io');
|
||||||
await term.writeStdout(this.#b);
|
await term.writeStdout(this.#b);
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
45
src/common/buffer_test.ts
Normal file
45
src/common/buffer_test.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { getTestRunner } from './runtime.ts';
|
||||||
|
import Buffer from './buffer.ts';
|
||||||
|
|
||||||
|
getTestRunner().then((t) => {
|
||||||
|
t.test('Buffer exists', () => {
|
||||||
|
const b = new Buffer();
|
||||||
|
t.assertInstanceOf(b, Buffer);
|
||||||
|
t.assertEquals(b.strlen(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test('Buffer.appendLine', () => {
|
||||||
|
const b = new Buffer();
|
||||||
|
|
||||||
|
// Carriage return and line feed
|
||||||
|
b.appendLine();
|
||||||
|
t.assertEquals(b.strlen(), 2);
|
||||||
|
|
||||||
|
b.clear();
|
||||||
|
t.assertEquals(b.strlen(), 0);
|
||||||
|
|
||||||
|
b.appendLine('foo');
|
||||||
|
t.assertEquals(b.strlen(), 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test('Buffer.append', () => {
|
||||||
|
const b = new Buffer();
|
||||||
|
|
||||||
|
b.append('foobar');
|
||||||
|
t.assertEquals(b.strlen(), 6);
|
||||||
|
b.clear();
|
||||||
|
|
||||||
|
b.append('foobar', 3);
|
||||||
|
t.assertEquals(b.strlen(), 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test('Buffer.flush', async () => {
|
||||||
|
const b = new Buffer();
|
||||||
|
b.append('foobarbaz');
|
||||||
|
t.assertEquals(b.strlen(), 9);
|
||||||
|
|
||||||
|
await b.flush();
|
||||||
|
|
||||||
|
t.assertEquals(b.strlen(), 0);
|
||||||
|
});
|
||||||
|
});
|
@ -1,7 +1,7 @@
|
|||||||
import Ansi, { KeyCommand } from './ansi.ts';
|
import Ansi, { KeyCommand } from './ansi.ts';
|
||||||
import Buffer from './buffer.ts';
|
import Buffer from './buffer.ts';
|
||||||
import Document from './document.ts';
|
import Document from './document.ts';
|
||||||
import { IPoint, ITerminalSize, VERSION } from './mod.ts';
|
import { IPoint, ITerminalSize, logToFile, VERSION } from './mod.ts';
|
||||||
import { ctrl_key } from './utils.ts';
|
import { ctrl_key } from './utils.ts';
|
||||||
|
|
||||||
export class Editor {
|
export class Editor {
|
||||||
@ -152,12 +152,17 @@ export class Editor {
|
|||||||
|
|
||||||
private drawFileRow(y: number): void {
|
private drawFileRow(y: number): void {
|
||||||
const row = this.#document.row(y);
|
const row = this.#document.row(y);
|
||||||
let len = row?.chars.length ?? 0;
|
if (row === null) {
|
||||||
|
logToFile(`Warning: trying to draw non-existent row '${y}'`);
|
||||||
|
return this.drawPlaceholderRow(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = row.chars.length ?? 0;
|
||||||
if (len > this.#screen.cols) {
|
if (len > this.#screen.cols) {
|
||||||
len = this.#screen.cols;
|
len = this.#screen.cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#buffer.append(row!.toString(), len);
|
this.#buffer.append(row.toString(), len);
|
||||||
this.#buffer.appendLine(Ansi.ClearLine);
|
this.#buffer.appendLine(Ansi.ClearLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { getTermios } from './termios.ts';
|
|||||||
import { Editor } from './editor.ts';
|
import { Editor } from './editor.ts';
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
const runTime = await getRuntime();
|
const runTime = await getRuntime();
|
||||||
const { term, file, onExit, onEvent } = runTime;
|
const { term, file, onExit, onEvent } = runTime;
|
||||||
|
|
||||||
@ -12,14 +13,13 @@ export async function main() {
|
|||||||
t.enableRawMode();
|
t.enableRawMode();
|
||||||
onExit(() => {
|
onExit(() => {
|
||||||
t.disableRawMode();
|
t.disableRawMode();
|
||||||
|
t.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup error handler to log to file
|
// Setup error handler to log to file
|
||||||
onEvent('error', (error) => {
|
onEvent('error', (error) => {
|
||||||
t.disableRawMode();
|
t.disableRawMode();
|
||||||
if (error instanceof ErrorEvent) {
|
file.appendFileSync('./scroll.err', JSON.stringify(error, null, 2));
|
||||||
file.appendFileSync('./scroll.err', JSON.stringify(error, null, 2));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const terminalSize = await term.getTerminalSize();
|
const terminalSize = await term.getTerminalSize();
|
||||||
@ -39,7 +39,7 @@ export async function main() {
|
|||||||
// The main event loop
|
// The main event loop
|
||||||
for await (const chunk of term.inputLoop()) {
|
for await (const chunk of term.inputLoop()) {
|
||||||
// Process input
|
// Process input
|
||||||
const char = readKey(chunk);
|
const char = readKey(decoder.decode(chunk));
|
||||||
const shouldLoop = editor.processKeyPress(char);
|
const shouldLoop = editor.processKeyPress(char);
|
||||||
if (!shouldLoop) {
|
if (!shouldLoop) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -1,14 +1,25 @@
|
|||||||
import { getTermios } from './termios.ts';
|
import { getTermios } from './termios.ts';
|
||||||
|
import { noop } from './utils.ts';
|
||||||
|
import { ITestBase } from './types.ts';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Runtime adapter interfaces
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
export enum RunTimeType {
|
export enum RunTimeType {
|
||||||
Bun = 'bun',
|
Bun = 'bun',
|
||||||
Deno = 'deno',
|
Deno = 'deno',
|
||||||
Unknown = 'common',
|
Unknown = 'common',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Runtime adapter interfaces
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of terminal in rows and columns
|
||||||
|
*/
|
||||||
|
export interface ITerminalSize {
|
||||||
|
rows: number;
|
||||||
|
cols: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The native functions for getting/setting terminal settings
|
* The native functions for getting/setting terminal settings
|
||||||
*/
|
*/
|
||||||
@ -33,14 +44,11 @@ export interface IFFI {
|
|||||||
*/
|
*/
|
||||||
// deno-lint-ignore no-explicit-any
|
// deno-lint-ignore no-explicit-any
|
||||||
getPointer(ta: any): unknown;
|
getPointer(ta: any): unknown;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The size of terminal in rows and columns
|
* Closes the FFI handle
|
||||||
*/
|
*/
|
||||||
export interface ITerminalSize {
|
close(): void;
|
||||||
rows: number;
|
|
||||||
cols: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,10 +59,11 @@ export interface ITerminal {
|
|||||||
* The arguments passed to the program on launch
|
* The arguments passed to the program on launch
|
||||||
*/
|
*/
|
||||||
argv: string[];
|
argv: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The generator function returning chunks of input from the stdin stream
|
* The generator function returning chunks of input from the stdin stream
|
||||||
*/
|
*/
|
||||||
inputLoop(): AsyncGenerator<Uint8Array, void, unknown>;
|
inputLoop(): AsyncGenerator<Uint8Array, void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the size of the terminal
|
* Get the size of the terminal
|
||||||
@ -70,7 +79,7 @@ export interface ITerminal {
|
|||||||
/**
|
/**
|
||||||
* Runtime-specific file handling
|
* Runtime-specific file handling
|
||||||
*/
|
*/
|
||||||
export interface IFIO {
|
export interface IFileIO {
|
||||||
openFile(path: string): Promise<string>;
|
openFile(path: string): Promise<string>;
|
||||||
openFileSync(path: string): string;
|
openFileSync(path: string): string;
|
||||||
appendFile(path: string, contents: string): Promise<void>;
|
appendFile(path: string, contents: string): Promise<void>;
|
||||||
@ -99,7 +108,7 @@ export interface IRuntime {
|
|||||||
/**
|
/**
|
||||||
* Runtime-specific file system io
|
* Runtime-specific file system io
|
||||||
*/
|
*/
|
||||||
file: IFIO;
|
file: IFileIO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up an event handler
|
* Set up an event handler
|
||||||
@ -132,6 +141,15 @@ export interface IRuntime {
|
|||||||
|
|
||||||
let scrollRuntime: IRuntime | null = null;
|
let scrollRuntime: IRuntime | null = null;
|
||||||
|
|
||||||
|
export function logToFile(s: unknown) {
|
||||||
|
importForRuntime('file_io').then((f) => {
|
||||||
|
const raw = (typeof s === 'string') ? s : JSON.stringify(s, null, 2);
|
||||||
|
const output = raw + '\n';
|
||||||
|
|
||||||
|
f.appendFile('./scroll.log', output).then(noop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kill program, displaying an error message
|
* Kill program, displaying an error message
|
||||||
* @param s
|
* @param s
|
||||||
@ -148,7 +166,7 @@ export function die(s: string | Error): void {
|
|||||||
/**
|
/**
|
||||||
* Determine which Typescript runtime we are operating under
|
* Determine which Typescript runtime we are operating under
|
||||||
*/
|
*/
|
||||||
export function getRuntimeType(): RunTimeType {
|
export function runtimeType(): RunTimeType {
|
||||||
let runtime = RunTimeType.Unknown;
|
let runtime = RunTimeType.Unknown;
|
||||||
|
|
||||||
if ('Deno' in globalThis) {
|
if ('Deno' in globalThis) {
|
||||||
@ -166,7 +184,7 @@ export function getRuntimeType(): RunTimeType {
|
|||||||
*/
|
*/
|
||||||
export async function getRuntime(): Promise<IRuntime> {
|
export async function getRuntime(): Promise<IRuntime> {
|
||||||
if (scrollRuntime === null) {
|
if (scrollRuntime === null) {
|
||||||
const runtime = getRuntimeType();
|
const runtime = runtimeType();
|
||||||
const path = `../${runtime}/mod.ts`;
|
const path = `../${runtime}/mod.ts`;
|
||||||
|
|
||||||
const pkg = await import(path);
|
const pkg = await import(path);
|
||||||
@ -178,16 +196,30 @@ export async function getRuntime(): Promise<IRuntime> {
|
|||||||
return Promise.resolve(scrollRuntime!);
|
return Promise.resolve(scrollRuntime!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the common test interface object
|
||||||
|
*/
|
||||||
|
export async function getTestRunner(): Promise<ITestBase> {
|
||||||
|
const runtime = runtimeType();
|
||||||
|
const path = `../${runtime}/test_base.ts`;
|
||||||
|
const pkg = await import(path);
|
||||||
|
if ('default' in pkg) {
|
||||||
|
return pkg.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import a runtime-specific module
|
* Import a runtime-specific module
|
||||||
*
|
*
|
||||||
* eg. to load "src/bun/mod.ts", if the runtime is bun,
|
* e.g. to load "src/bun/mod.ts", if the runtime is bun,
|
||||||
* you can use like so `await importForRuntime('index')`;
|
* you can use like so `await importForRuntime('index')`;
|
||||||
*
|
*
|
||||||
* @param path - the path within the runtime module
|
* @param path - the path within the runtime module
|
||||||
*/
|
*/
|
||||||
export const importForRuntime = async (path: string) => {
|
export const importForRuntime = async (path: string) => {
|
||||||
const runtime = getRuntimeType();
|
const runtime = runtimeType();
|
||||||
const suffix = '.ts';
|
const suffix = '.ts';
|
||||||
const base = `../${runtime}/`;
|
const base = `../${runtime}/`;
|
||||||
|
|
||||||
@ -198,20 +230,10 @@ export const importForRuntime = async (path: string) => {
|
|||||||
const cleanedPath = pathParts.join('/');
|
const cleanedPath = pathParts.join('/');
|
||||||
const importPath = base + cleanedPath + suffix;
|
const importPath = base + cleanedPath + suffix;
|
||||||
|
|
||||||
return await import(importPath);
|
const pkg = await import(importPath);
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import the default export for a runtime-specific module
|
|
||||||
* (this is just a simple wrapper of `importForRuntime`)
|
|
||||||
*
|
|
||||||
* @param path - the path within the runtime module
|
|
||||||
*/
|
|
||||||
export const importDefaultForRuntime = async (path: string) => {
|
|
||||||
const pkg = await importForRuntime(path);
|
|
||||||
if ('default' in pkg) {
|
if ('default' in pkg) {
|
||||||
return pkg.default;
|
return pkg.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return pkg;
|
||||||
};
|
};
|
||||||
|
@ -31,13 +31,13 @@ class Termios {
|
|||||||
* The data for the termios struct we are manipulating
|
* The data for the termios struct we are manipulating
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
readonly #termios: Uint8Array;
|
#termios: Uint8Array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The pointer to the termios struct
|
* The pointer to the termios struct
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
readonly #ptr: any;
|
#ptr;
|
||||||
|
|
||||||
constructor(ffi: IFFI) {
|
constructor(ffi: IFFI) {
|
||||||
this.#ffi = ffi;
|
this.#ffi = ffi;
|
||||||
@ -51,6 +51,13 @@ class Termios {
|
|||||||
this.#ptr = ffi.getPointer(this.#termios);
|
this.#ptr = ffi.getPointer(this.#termios);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.#ptr = null;
|
||||||
|
this.#cookedTermios = new Uint8Array(0);
|
||||||
|
this.#termios = new Uint8Array(0);
|
||||||
|
this.#ffi.close();
|
||||||
|
}
|
||||||
|
|
||||||
enableRawMode() {
|
enableRawMode() {
|
||||||
if (this.#inRawMode) {
|
if (this.#inRawMode) {
|
||||||
throw new Error('Can not enable raw mode when in raw mode');
|
throw new Error('Can not enable raw mode when in raw mode');
|
||||||
|
@ -1,11 +1,30 @@
|
|||||||
export function noop() {}
|
// ----------------------------------------------------------------------------
|
||||||
|
// Misc
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const noop = () => {};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Strings
|
// Strings
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the codepoint of the first byte of a string. If the string
|
||||||
|
* is empty, this will return 256
|
||||||
|
*
|
||||||
|
* @param s - the string
|
||||||
|
*/
|
||||||
|
export function ord(s: string): number {
|
||||||
|
if (s.length > 0) {
|
||||||
|
return s.codePointAt(0)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 256;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a string by graphemes, not just bytes
|
* Split a string by graphemes, not just bytes
|
||||||
|
*
|
||||||
* @param s - the string to split into 'characters'
|
* @param s - the string to split into 'characters'
|
||||||
*/
|
*/
|
||||||
export function chars(s: string): string[] {
|
export function chars(s: string): string[] {
|
||||||
@ -14,6 +33,7 @@ export function chars(s: string): string[] {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the 'character length' of a string, not its UTF16 byte count
|
* Get the 'character length' of a string, not its UTF16 byte count
|
||||||
|
*
|
||||||
* @param s - the string to check
|
* @param s - the string to check
|
||||||
*/
|
*/
|
||||||
export function strlen(s: string): number {
|
export function strlen(s: string): number {
|
||||||
@ -21,19 +41,12 @@ export function strlen(s: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the character part of ascii?
|
* Are all the characters in the string in ASCII range?
|
||||||
*
|
*
|
||||||
* @param char - a one character string to check
|
* @param char - string to check
|
||||||
*/
|
*/
|
||||||
export function is_ascii(char: string): boolean {
|
export function is_ascii(char: string): boolean {
|
||||||
return chars(char).every((char) => {
|
return chars(char).every((char) => ord(char) < 0x80);
|
||||||
const point = char.codePointAt(0);
|
|
||||||
if (point === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return point < 0x80;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,22 +55,19 @@ export function is_ascii(char: string): boolean {
|
|||||||
* @param char - a one character string to check
|
* @param char - a one character string to check
|
||||||
*/
|
*/
|
||||||
export function is_control(char: string): boolean {
|
export function is_control(char: string): boolean {
|
||||||
const code = char.codePointAt(0);
|
const code = ord(char);
|
||||||
if (code === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return is_ascii(char) && (code === 0x7f || code < 0x20);
|
return is_ascii(char) && (code === 0x7f || code < 0x20);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the key code for a ctrl chord
|
* Get the key code for a ctrl chord
|
||||||
|
*
|
||||||
* @param char - a one character string
|
* @param char - a one character string
|
||||||
*/
|
*/
|
||||||
export function ctrl_key(char: string): string {
|
export function ctrl_key(char: string): string {
|
||||||
// This is the normal use case, of course
|
// This is the normal use case, of course
|
||||||
if (is_ascii(char)) {
|
if (is_ascii(char)) {
|
||||||
const point = char.codePointAt(0)!;
|
const point = ord(char);
|
||||||
return String.fromCodePoint(point & 0x1f);
|
return String.fromCodePoint(point & 0x1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,59 +1,74 @@
|
|||||||
import { importDefaultForRuntime, ITestBase } from './mod.ts';
|
import { getTestRunner } from './mod.ts';
|
||||||
import {
|
import {
|
||||||
chars,
|
chars,
|
||||||
ctrl_key,
|
ctrl_key,
|
||||||
is_ascii,
|
is_ascii,
|
||||||
is_control,
|
is_control,
|
||||||
|
noop,
|
||||||
|
ord,
|
||||||
strlen,
|
strlen,
|
||||||
truncate,
|
truncate,
|
||||||
} from './utils.ts';
|
} from './utils.ts';
|
||||||
|
|
||||||
const t: ITestBase = await importDefaultForRuntime('test_base');
|
getTestRunner().then((t) => {
|
||||||
|
t.test('noop fn', () => {
|
||||||
|
t.assertExists(noop);
|
||||||
|
t.assertEquals(noop(), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Strings
|
// Strings
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
t.test('ord fn returns 256 on invalid string', () => {
|
||||||
|
t.assertEquals(ord(''), 256);
|
||||||
|
});
|
||||||
|
|
||||||
t.test('chars fn properly splits strings into unicode characters', () => {
|
t.test('chars fn properly splits strings into unicode characters', () => {
|
||||||
t.assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']);
|
t.assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']);
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('ctrl_key fn returns expected values', () => {
|
t.test('ctrl_key fn returns expected values', () => {
|
||||||
const ctrl_a = ctrl_key('a');
|
const ctrl_a = ctrl_key('a');
|
||||||
t.assertTrue(is_control(ctrl_a));
|
t.assertTrue(is_control(ctrl_a));
|
||||||
t.assertEquals(ctrl_a, String.fromCodePoint(0x01));
|
t.assertEquals(ctrl_a, String.fromCodePoint(0x01));
|
||||||
|
|
||||||
const invalid = ctrl_key('😺');
|
const invalid = ctrl_key('😺');
|
||||||
t.assertFalse(is_control(invalid));
|
t.assertFalse(is_control(invalid));
|
||||||
t.assertEquals(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.assertFalse(is_ascii('ab😺ac'));
|
||||||
|
});
|
||||||
t.test('is_control fn works as expected', () => {
|
|
||||||
t.assertFalse(is_control('abc'));
|
t.test('is_control fn works as expected', () => {
|
||||||
t.assertTrue(is_control(String.fromCodePoint(0x01)));
|
t.assertFalse(is_control('abc'));
|
||||||
t.assertFalse(is_control('😺'));
|
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.test('strlen fn returns expected length for ascii strings', () => {
|
||||||
t.assertNotEquals('😺😸😹'.length, strlen('😺😸😹'));
|
t.assertEquals(strlen('abc'), 'abc'.length);
|
||||||
|
});
|
||||||
// Skin tone modifier + base character
|
|
||||||
t.assertEquals(strlen('🤰🏼'), 2);
|
t.test('strlen fn returns expected length for multibyte characters', () => {
|
||||||
t.assertNotEquals('🤰🏼'.length, strlen('🤰🏼'));
|
t.assertEquals(strlen('😺😸😹'), 3);
|
||||||
|
t.assertNotEquals('😺😸😹'.length, strlen('😺😸😹'));
|
||||||
// This has 4 sub-characters, and 3 zero-width-joiners
|
|
||||||
t.assertEquals(strlen('👨👩👧👦'), 7);
|
// Skin tone modifier + base character
|
||||||
t.assertNotEquals('👨👩👧👦'.length, strlen('👨👩👧👦'));
|
t.assertEquals(strlen('🤰🏼'), 2);
|
||||||
});
|
t.assertNotEquals('🤰🏼'.length, strlen('🤰🏼'));
|
||||||
|
|
||||||
t.test('truncate shortens strings', () => {
|
// This has 4 sub-characters, and 3 zero-width-joiners
|
||||||
t.assertEquals(truncate('😺😸😹', 1), '😺');
|
t.assertEquals(strlen('👨👩👧👦'), 7);
|
||||||
t.assertEquals(truncate('😺😸😹', 5), '😺😸😹');
|
t.assertNotEquals('👨👩👧👦'.length, strlen('👨👩👧👦'));
|
||||||
t.assertEquals(truncate('👨👩👧👦', 5), '👨👩👧');
|
});
|
||||||
|
|
||||||
|
t.test('truncate shortens strings', () => {
|
||||||
|
t.assertEquals(truncate('😺😸😹', 1), '😺');
|
||||||
|
t.assertEquals(truncate('😺😸😹', 5), '😺😸😹');
|
||||||
|
t.assertEquals(truncate('👨👩👧👦', 5), '👨👩👧');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
1
src/deno/deps.ts
Normal file
1
src/deno/deps.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * as stdAssert from 'https://deno.land/std@0.205.0/assert/mod.ts';
|
@ -39,6 +39,7 @@ const DenoFFI: IFFI = {
|
|||||||
tcsetattr,
|
tcsetattr,
|
||||||
cfmakeraw,
|
cfmakeraw,
|
||||||
getPointer: Deno.UnsafePointer.of,
|
getPointer: Deno.UnsafePointer.of,
|
||||||
|
close: cStdLib.close,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DenoFFI;
|
export default DenoFFI;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { IFIO } from '../common/runtime.ts';
|
import { IFileIO } from '../common/runtime.ts';
|
||||||
|
|
||||||
const DenoFileIO: IFIO = {
|
const DenoFileIO: IFileIO = {
|
||||||
openFile: async function (path: string): Promise<string> {
|
openFile: async function (path: string): Promise<string> {
|
||||||
const decoder = new TextDecoder('utf-8');
|
const decoder = new TextDecoder('utf-8');
|
||||||
const data = await Deno.readFile(path);
|
const data = await Deno.readFile(path);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { ITestBase } from '../common/mod.ts';
|
import { ITestBase } from '../common/mod.ts';
|
||||||
|
import { stdAssert } from './deps.ts';
|
||||||
import {
|
const {
|
||||||
assertEquals,
|
assertEquals,
|
||||||
assertExists,
|
assertExists,
|
||||||
assertInstanceOf,
|
assertInstanceOf,
|
||||||
AssertionError,
|
AssertionError,
|
||||||
assertNotEquals,
|
assertNotEquals,
|
||||||
assertStrictEquals,
|
assertStrictEquals,
|
||||||
} from 'https://deno.land/std/assert/mod.ts';
|
} = stdAssert;
|
||||||
|
|
||||||
class TestBase implements ITestBase {
|
class TestBase implements ITestBase {
|
||||||
test(name: string, fn: () => void): void {
|
test(name: string, fn: () => void): void {
|
||||||
|
Loading…
Reference in New Issue
Block a user