Set up basic ascii parsing and display

This commit is contained in:
Timothy Warren 2023-11-02 13:06:48 -04:00
parent 4854796168
commit 7dcd42da13
9 changed files with 149 additions and 23 deletions

View File

@ -1,11 +1,9 @@
{ {
"imports": { "exclude": ["src/bun/**/*.ts"],
"std": "https://deno.land/std@0.204.0/",
},
"lint": { "lint": {
"include": ["src/"], "include": ["src/"],
"rules": { "rules": {
"tags": ["recommended"], "tags": ["recommended"]
} }
}, },
"fmt": { "fmt": {
@ -13,7 +11,7 @@
"lineWidth": 80, "lineWidth": 80,
"indentWidth": 2, "indentWidth": 2,
"semiColons": true, "semiColons": true,
"singleQuote": true, "singleQuote": true
}, },
"nodeModulesDir": true, "nodeModulesDir": true
} }

View File

@ -2,12 +2,17 @@
default: default:
@just --list @just --list
# Code linting with deno # Typescript checking
check:
deno check --unstable --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts
# Code linting
deno-lint: deno-lint:
deno lint deno lint
# Code linting with bun # Reformat the code
bun-lint: fmt:
deno fmt
# Run with bun # Run with bun
bun-run: bun-run:
@ -15,4 +20,7 @@ bun-run:
# Run with deno # Run with deno
deno-run: deno-run:
deno run --allow-all --allow-ffi --deny-net --deny-hrtime --unstable ./src/scroll.ts deno run --allow-all --allow-ffi --deny-net --deny-hrtime --unstable ./src/scroll.ts
deno-test:
deno test --allow-all

View File

@ -3,6 +3,7 @@
*/ */
import { getTermios } from '../common/termios'; import { getTermios } from '../common/termios';
import {ctrl_key, is_control} from "../common/strings";
export async function main(): Promise<number> { export async function main(): Promise<number> {
const t = await getTermios(); const t = await getTermios();
@ -10,12 +11,19 @@ export async function main(): Promise<number> {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
for await (const chunk of Bun.stdin.stream()) { for await (const chunk of Bun.stdin.stream()) {
const char = String(decoder.decode(chunk)).trim(); const char = String(decoder.decode(chunk));
if (char === 'q') { if (char === ctrl_key('q')) {
t.disableRawMode(); t.disableRawMode();
return 0; return 0;
} }
if (is_control(char)) {
console.log(char.codePointAt(0) + '\r');
} else {
console.log(`${char} ('${char.codePointAt(0)}')\r`);
}
} }
process.on('exit', (code) => { process.on('exit', (code) => {
@ -24,4 +32,4 @@ export async function main(): Promise<number> {
}); });
return -1; return -1;
} }

View File

@ -1,9 +1,16 @@
import { getTermios } from './termios.ts';
export enum RunTime { export enum RunTime {
Bun = 'bun', Bun = 'bun',
Deno = 'deno', Deno = 'deno',
Unknown = 'common', Unknown = 'common',
} }
export function die(s: string): void {
getTermios().then((t) => t.disableRawMode());
console.error(s);
}
/** /**
* Determine which Typescript runtime we are operating under * Determine which Typescript runtime we are operating under
*/ */

60
src/common/strings.ts Normal file
View File

@ -0,0 +1,60 @@
/**
* Split a string by graphemes, not just bytes
* @param s - the string to split into 'characters'
*/
export function chars(s: string): string[] {
return s.split(/(?:)/u);
}
/**
* 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') {
return false;
}
return point < 0x80;
});
}
/**
* Is the one char in the string an ascii control character?
*
* @param char - a one character string to check
*/
export function is_control(char: string): boolean {
const code = char.codePointAt(0);
if (code === undefined) {
return false;
}
return is_ascii(char) && (code === 0x7f || code < 0x20);
}
/**
* Get the key code for a ctrl chord
* @param char - a one character string
*/
export function ctrl_key(char: string): string {
// This is the normal use case, of course
if (is_ascii(char)) {
const point = char.codePointAt(0);
if (point === undefined) {
return char;
}
return String.fromCodePoint(point & 0x1f);
}
// If it's not ascii, just return the input key code
return char;
}

View File

@ -0,0 +1,8 @@
import { importForRuntime } from './index.ts';
import { chars } from './strings.ts';
const { test, assertEquals } = await importForRuntime('test_base');
test('chars fn properly splits strings into unicode characters', () => {
assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']);
});

View File

@ -1,4 +1,4 @@
import { importForRuntime } from './index.ts'; import { die, importForRuntime } from './index.ts';
export const STDIN_FILENO = 0; export const STDIN_FILENO = 0;
export const STOUT_FILENO = 1; export const STOUT_FILENO = 1;
@ -27,7 +27,13 @@ export interface ITermios {
disableRawMode(): void; disableRawMode(): void;
} }
let termiosSingleton: ITermios | null = null;
export const getTermios = async () => { export const getTermios = async () => {
if (termiosSingleton !== null) {
return termiosSingleton;
}
// Get the runtime-specific ffi wrappers // Get the runtime-specific ffi wrappers
const { tcgetattr, tcsetattr, cfmakeraw, getPointer } = const { tcgetattr, tcsetattr, cfmakeraw, getPointer } =
await importForRuntime('ffi'); await importForRuntime('ffi');
@ -81,18 +87,30 @@ export const getTermios = async () => {
} }
// Get the current termios settings // Get the current termios settings
tcgetattr(STDIN_FILENO, this.#ptr); let res = tcgetattr(STDIN_FILENO, this.#ptr);
if (res === -1) {
die('Failed to get terminal settings');
}
// The #ptr property is pointing to the #termios TypedArray. As the pointer // The #ptr property is pointing to the #termios TypedArray. As the pointer
// is manipulated, the TypedArray is as well. We will use this to save // is manipulated, the TypedArray is as well. We will use this to save
// the original canonical/cooked terminal settings for disabling raw mode later. // the original canonical/cooked terminal settings for disabling raw mode later.
this.#cookedTermios = new Uint8Array(this.#termios, 0, 60); this.#cookedTermios = new Uint8Array(this.#termios, 0, 60);
// Update termios struct with raw settings // Update termios struct with (most of the) raw settings
cfmakeraw(this.#ptr); res = cfmakeraw(this.#ptr);
if (res === -1) {
die('Failed to call cfmakeraw');
}
// @TODO: Tweak a few more terminal settings
// Actually set the new termios settings // Actually set the new termios settings
tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr); res = tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr);
if (res === -1) {
die('Failed to update terminal settings. Can\'t enter raw mode');
}
this.#inRawMode = true; this.#inRawMode = true;
} }
@ -104,11 +122,16 @@ export const getTermios = async () => {
} }
const oldTermiosPtr = getPointer(this.#cookedTermios); const oldTermiosPtr = getPointer(this.#cookedTermios);
tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr); let res = tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr);
if (res === -1) {
die('Failed to restore canonical mode.');
}
this.#inRawMode = false; this.#inRawMode = false;
} }
} }
return new Termios(); termiosSingleton = new Termios();
return termiosSingleton;
}; };

View File

@ -2,6 +2,7 @@
* The main entrypoint when using Deno as the runtime * The main entrypoint when using Deno as the runtime
*/ */
import { getTermios } from '../common/termios.ts'; import { getTermios } from '../common/termios.ts';
import { ctrl_key, is_control } from '../common/strings.ts';
export async function main(): Promise<number> { export async function main(): Promise<number> {
const t = await getTermios(); const t = await getTermios();
@ -9,12 +10,18 @@ export async function main(): Promise<number> {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
for await (const chunk of Deno.stdin.readable) { for await (const chunk of Deno.stdin.readable) {
const char = String(decoder.decode(chunk)).trim(); const char = String(decoder.decode(chunk));
if (char === 'q') { if (char === ctrl_key('q')) {
t.disableRawMode(); t.disableRawMode();
return 0; return 0;
} }
if (is_control(char)) {
console.log(char.codePointAt(0) + '\r');
} else {
console.log(`${char} ('${char.codePointAt(0)}')\r`);
}
} }
return -1; return -1;

7
src/deno/test_base.ts Normal file
View File

@ -0,0 +1,7 @@
export const test = Deno.test;
export {
assert,
assertEquals,
assertNotEquals,
assertStrictEquals,
} from 'https://deno.land/std/assert/mod.ts';