Set up basic ascii parsing and display
This commit is contained in:
parent
4854796168
commit
7dcd42da13
10
deno.jsonc
10
deno.jsonc
@ -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
|
||||||
}
|
}
|
14
justfile
14
justfile
@ -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:
|
||||||
@ -16,3 +21,6 @@ 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
|
@ -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) => {
|
||||||
|
@ -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
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 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;
|
||||||
};
|
};
|
||||||
|
@ -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
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