Set up basic ascii parsing and display
This commit is contained in:
parent
4854796168
commit
7dcd42da13
12
deno.jsonc
12
deno.jsonc
@ -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
|
||||
}
|
||||
|
16
justfile
16
justfile
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
60
src/common/strings.ts
Normal 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;
|
||||
}
|
8
src/common/strings_test.ts
Normal file
8
src/common/strings_test.ts
Normal 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('😺😸😹'), ['😺', '😸', '😹']);
|
||||
});
|
@ -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;
|
||||
};
|
||||
|
@ -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
7
src/deno/test_base.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const test = Deno.test;
|
||||
export {
|
||||
assert,
|
||||
assertEquals,
|
||||
assertNotEquals,
|
||||
assertStrictEquals,
|
||||
} from 'https://deno.land/std/assert/mod.ts';
|
Loading…
Reference in New Issue
Block a user