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

View File

@ -2,12 +2,17 @@
default:
@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
# Code linting with bun
bun-lint:
# Reformat the code
fmt:
deno fmt
# Run with bun
bun-run:
@ -15,4 +20,7 @@ bun-run:
# Run with deno
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 {ctrl_key, is_control} from "../common/strings";
export async function main(): Promise<number> {
const t = await getTermios();
@ -10,12 +11,19 @@ export async function main(): Promise<number> {
const decoder = new TextDecoder();
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();
return 0;
}
if (is_control(char)) {
console.log(char.codePointAt(0) + '\r');
} else {
console.log(`${char} ('${char.codePointAt(0)}')\r`);
}
}
process.on('exit', (code) => {
@ -24,4 +32,4 @@ export async function main(): Promise<number> {
});
return -1;
}
}

View File

@ -1,9 +1,16 @@
import { getTermios } from './termios.ts';
export enum RunTime {
Bun = 'bun',
Deno = 'deno',
Unknown = 'common',
}
export function die(s: string): void {
getTermios().then((t) => t.disableRawMode());
console.error(s);
}
/**
* 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 STOUT_FILENO = 1;
@ -27,7 +27,13 @@ export interface ITermios {
disableRawMode(): void;
}
let termiosSingleton: ITermios | null = null;
export const getTermios = async () => {
if (termiosSingleton !== null) {
return termiosSingleton;
}
// Get the runtime-specific ffi wrappers
const { tcgetattr, tcsetattr, cfmakeraw, getPointer } =
await importForRuntime('ffi');
@ -81,18 +87,30 @@ export const getTermios = async () => {
}
// 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
// is manipulated, the TypedArray is as well. We will use this to save
// the original canonical/cooked terminal settings for disabling raw mode later.
this.#cookedTermios = new Uint8Array(this.#termios, 0, 60);
// Update termios struct with raw settings
cfmakeraw(this.#ptr);
// Update termios struct with (most of the) raw settings
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
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;
}
@ -104,11 +122,16 @@ export const getTermios = async () => {
}
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;
}
}
return new Termios();
termiosSingleton = new Termios();
return termiosSingleton;
};

View File

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