Finally get the terminal size via ansi codes for bun
This commit is contained in:
parent
1723219452
commit
61d222a9af
3
justfile
3
justfile
@ -5,6 +5,9 @@ default:
|
||||
# Typescript checking
|
||||
check: deno-check bun-check
|
||||
|
||||
docs:
|
||||
deno doc --html --unstable-ffi --private --name="Scroll" ./src/common/mod.ts
|
||||
|
||||
# Reformat the code
|
||||
fmt:
|
||||
deno fmt
|
||||
|
@ -2,8 +2,26 @@
|
||||
* The main entrypoint when using Bun as the runtime
|
||||
*/
|
||||
|
||||
export * from './terminal_io.ts';
|
||||
import { getTermios, IRuntime, RunTimeType } from '../common/mod.ts';
|
||||
import BunFFI from './ffi.ts';
|
||||
import BunTerminalIO from './terminal_io.ts';
|
||||
|
||||
export const onExit = (cb: () => void): void => {
|
||||
process.on('beforeExit', cb);
|
||||
process.on('error', async (e) => {
|
||||
(await getTermios()).disableRawMode();
|
||||
console.error(e);
|
||||
process.exit();
|
||||
});
|
||||
|
||||
const BunRuntime: IRuntime = {
|
||||
name: RunTimeType.Bun,
|
||||
ffi: BunFFI,
|
||||
io: BunTerminalIO,
|
||||
onExit: (cb: () => void): void => {
|
||||
process.on('beforeExit', cb);
|
||||
process.on('exit', cb);
|
||||
process.on('SIGINT', cb);
|
||||
},
|
||||
exit: (code?: number) => process.exit(code),
|
||||
};
|
||||
|
||||
export default BunRuntime;
|
||||
|
@ -2,22 +2,7 @@
|
||||
* Wrap the runtime-specific hook into stdin
|
||||
*/
|
||||
import { ITerminalIO, ITerminalSize } from '../common/mod.ts';
|
||||
|
||||
function getSizeFromTput(): ITerminalSize {
|
||||
const rows = parseInt(
|
||||
Bun.spawnSync(['tput', 'lines']).stdout.toString().trim(),
|
||||
10,
|
||||
);
|
||||
const cols = parseInt(
|
||||
Bun.spawnSync(['tput', 'cols']).stdout.toString().trim(),
|
||||
10,
|
||||
);
|
||||
|
||||
return {
|
||||
rows: (rows > 0) ? rows + 1 : 25,
|
||||
cols: (cols > 0) ? cols + 1 : 80,
|
||||
};
|
||||
}
|
||||
import Ansi from '../common/editor/ansi.ts';
|
||||
|
||||
const BunTerminalIO: ITerminalIO = {
|
||||
inputLoop: async function* inputLoop() {
|
||||
@ -25,8 +10,40 @@ const BunTerminalIO: ITerminalIO = {
|
||||
yield chunk;
|
||||
}
|
||||
},
|
||||
getSize: function getSize(): ITerminalSize {
|
||||
return getSizeFromTput();
|
||||
getTerminalSize: async function getTerminalSize(): Promise<ITerminalSize> {
|
||||
const encoder = new TextEncoder();
|
||||
const write = (s: string) => Bun.write(Bun.stdout, encoder.encode(s));
|
||||
|
||||
// Tell the cursor to move to Row 999 and Column 999
|
||||
// Since this command specifically doesn't go off the screen
|
||||
// When we ask where the cursor is, we should get the size of the screen
|
||||
await write(Ansi.moveCursorForward(999) + Ansi.moveCursorDown(999));
|
||||
|
||||
// Ask where the cursor is
|
||||
await write(Ansi.GetCursorLocation);
|
||||
|
||||
// Get the first chunk from stdin
|
||||
// The response is \x1b[(rows);(cols)R..
|
||||
for await (const chunk of Bun.stdin.stream()) {
|
||||
const rawCode = (new TextDecoder()).decode(chunk);
|
||||
const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1');
|
||||
const [srows, scols] = res.split(';');
|
||||
const rows = parseInt(srows, 10);
|
||||
const cols = parseInt(scols, 10);
|
||||
|
||||
// Clear the screen
|
||||
await write(Ansi.ClearScreen + Ansi.ResetCursor);
|
||||
|
||||
return {
|
||||
rows,
|
||||
cols,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
};
|
||||
},
|
||||
write: async function write(s: string): Promise<void> {
|
||||
const buffer = new TextEncoder().encode(s);
|
||||
|
@ -1,5 +1,17 @@
|
||||
export const ANSI_PREFIX = '\x1b[';
|
||||
|
||||
function esc(pieces: TemplateStringsArray): string {
|
||||
return '\x1b[' + pieces[0];
|
||||
return ANSI_PREFIX + pieces[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* ANSI escapes for various inputs
|
||||
*/
|
||||
export enum KeyCommand {
|
||||
ArrowUp = `${ANSI_PREFIX}A`,
|
||||
ArrowDown = `${ANSI_PREFIX}B`,
|
||||
ArrowRight = `${ANSI_PREFIX}C`,
|
||||
ArrowLeft = `${ANSI_PREFIX}D`,
|
||||
}
|
||||
|
||||
export const Ansi = {
|
||||
@ -8,9 +20,16 @@ export const Ansi = {
|
||||
ResetCursor: esc`H`,
|
||||
HideCursor: esc`?25l`,
|
||||
ShowCursor: esc`?25h`,
|
||||
GetCursorLocation: esc`6n`,
|
||||
moveCursor: function moveCursor(row: number, col: number): string {
|
||||
// Convert to 1-based counting
|
||||
row++;
|
||||
col++;
|
||||
|
||||
return `\x1b[${row};${col}H`;
|
||||
},
|
||||
moveCursorForward: (col: number): string => `${ANSI_PREFIX}${col}C`,
|
||||
moveCursorDown: (row: number): string => `${ANSI_PREFIX}${row}B`,
|
||||
};
|
||||
|
||||
export default Ansi;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { strlen } from '../utils.ts';
|
||||
import { getRuntime } from '../runtime.ts';
|
||||
|
||||
class Buffer {
|
||||
#b = '';
|
||||
@ -22,6 +23,12 @@ class Buffer {
|
||||
return this.#b;
|
||||
}
|
||||
|
||||
async flush() {
|
||||
const { io } = await getRuntime();
|
||||
await io.write(this.#b);
|
||||
this.clear();
|
||||
}
|
||||
|
||||
strlen(): number {
|
||||
return strlen(this.#b);
|
||||
}
|
||||
|
@ -1,13 +1,6 @@
|
||||
import Ansi from './ansi.ts';
|
||||
import Ansi, { KeyCommand } from './ansi.ts';
|
||||
import Buffer from './buffer.ts';
|
||||
import {
|
||||
ctrl_key,
|
||||
importDefaultForRuntime,
|
||||
IPoint,
|
||||
ITerminalSize,
|
||||
truncate,
|
||||
VERSION,
|
||||
} from '../mod.ts';
|
||||
import { ctrl_key, IPoint, ITerminalSize, truncate, VERSION } from '../mod.ts';
|
||||
|
||||
export class Editor {
|
||||
#buffer: Buffer;
|
||||
@ -36,12 +29,12 @@ export class Editor {
|
||||
this.clearScreen().then(() => {});
|
||||
return false;
|
||||
|
||||
case 'w':
|
||||
case 's':
|
||||
case 'a':
|
||||
case 'd':
|
||||
this.moveCursor(input);
|
||||
break;
|
||||
case KeyCommand.ArrowUp:
|
||||
case KeyCommand.ArrowDown:
|
||||
case KeyCommand.ArrowRight:
|
||||
case KeyCommand.ArrowLeft:
|
||||
this.moveCursor(input);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -49,16 +42,16 @@ export class Editor {
|
||||
|
||||
private moveCursor(char: string): void {
|
||||
switch (char) {
|
||||
case 'a':
|
||||
case KeyCommand.ArrowLeft:
|
||||
this.#cursor.x--;
|
||||
break;
|
||||
case 'd':
|
||||
case KeyCommand.ArrowRight:
|
||||
this.#cursor.x++;
|
||||
break;
|
||||
case 'w':
|
||||
case KeyCommand.ArrowUp:
|
||||
this.#cursor.y--;
|
||||
break;
|
||||
case 's':
|
||||
case KeyCommand.ArrowDown:
|
||||
this.#cursor.y++;
|
||||
break;
|
||||
}
|
||||
@ -76,18 +69,18 @@ export class Editor {
|
||||
this.#buffer.append(Ansi.ResetCursor);
|
||||
this.drawRows();
|
||||
this.#buffer.append(
|
||||
Ansi.moveCursor(this.#cursor.y + 1, this.#cursor.x + 1),
|
||||
Ansi.moveCursor(this.#cursor.y, this.#cursor.x),
|
||||
);
|
||||
this.#buffer.append(Ansi.ShowCursor);
|
||||
|
||||
await this.writeToScreen();
|
||||
await this.#buffer.flush();
|
||||
}
|
||||
|
||||
private async clearScreen(): Promise<void> {
|
||||
this.#buffer.append(Ansi.ClearScreen);
|
||||
this.#buffer.append(Ansi.ResetCursor);
|
||||
|
||||
await this.writeToScreen();
|
||||
await this.#buffer.flush();
|
||||
}
|
||||
|
||||
private drawRows(): void {
|
||||
@ -120,11 +113,4 @@ export class Editor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async writeToScreen(): Promise<void> {
|
||||
const io = await importDefaultForRuntime('terminal_io');
|
||||
|
||||
await io.write(this.#buffer.getBuffer());
|
||||
this.#buffer.clear();
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ export * from './editor/mod.ts';
|
||||
export * from './runtime.ts';
|
||||
export * from './termios.ts';
|
||||
export * from './utils.ts';
|
||||
|
||||
export * from './types.ts';
|
||||
export type * from './types.ts';
|
||||
|
||||
export const VERSION = '0.0.1';
|
||||
|
@ -1,31 +1,47 @@
|
||||
import { getTermios } from './termios.ts';
|
||||
import { IRuntime, RunTimeType } from './types.ts';
|
||||
|
||||
export enum RunTime {
|
||||
Bun = 'bun',
|
||||
Deno = 'deno',
|
||||
Unknown = 'common',
|
||||
}
|
||||
let scrollRuntime: IRuntime | null = null;
|
||||
|
||||
export function die(s: string): void {
|
||||
getTermios().then((t) => t.disableRawMode());
|
||||
/**
|
||||
* Kill program, displaying an error message
|
||||
* @param s
|
||||
*/
|
||||
export async function die(s: string | Error): Promise<void> {
|
||||
(await getTermios()).disableRawMode();
|
||||
console.error(s);
|
||||
(await getRuntime()).exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which Typescript runtime we are operating under
|
||||
*/
|
||||
export const getRuntime = (): RunTime => {
|
||||
let runtime = RunTime.Unknown;
|
||||
export function getRuntimeType(): RunTimeType {
|
||||
let runtime = RunTimeType.Unknown;
|
||||
|
||||
if ('Deno' in globalThis) {
|
||||
runtime = RunTime.Deno;
|
||||
runtime = RunTimeType.Deno;
|
||||
}
|
||||
if ('Bun' in globalThis) {
|
||||
runtime = RunTime.Bun;
|
||||
runtime = RunTimeType.Bun;
|
||||
}
|
||||
|
||||
return runtime;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getRuntime(): Promise<IRuntime> {
|
||||
if (scrollRuntime === null) {
|
||||
const runtime = getRuntimeType();
|
||||
const path = `../${runtime}/mod.ts`;
|
||||
|
||||
const pkg = await import(path);
|
||||
if ('default' in pkg) {
|
||||
scrollRuntime = pkg.default;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(scrollRuntime!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a runtime-specific module
|
||||
@ -36,7 +52,7 @@ export const getRuntime = (): RunTime => {
|
||||
* @param path - the path within the runtime module
|
||||
*/
|
||||
export const importForRuntime = async (path: string) => {
|
||||
const runtime = getRuntime();
|
||||
const runtime = getRuntimeType();
|
||||
const suffix = '.ts';
|
||||
const base = `../${runtime}/`;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { die, IFFI, importDefaultForRuntime } from './mod.ts';
|
||||
import { die, getRuntime, IFFI } from './mod.ts';
|
||||
|
||||
export const STDIN_FILENO = 0;
|
||||
export const TCSANOW = 0;
|
||||
@ -63,7 +63,7 @@ class Termios {
|
||||
// Get the current termios settings
|
||||
let res = this.#ffi.tcgetattr(STDIN_FILENO, this.#ptr);
|
||||
if (res === -1) {
|
||||
die('Failed to get terminal settings');
|
||||
die('Failed to get terminal settings').then(() => {});
|
||||
}
|
||||
|
||||
// The #ptr property is pointing to the #termios TypedArray. As the pointer
|
||||
@ -80,7 +80,9 @@ class Termios {
|
||||
// Actually set the new termios settings
|
||||
res = this.#ffi.tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr);
|
||||
if (res === -1) {
|
||||
die('Failed to update terminal settings. Can\'t enter raw mode');
|
||||
die('Failed to update terminal settings. Can\'t enter raw mode').then(
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
||||
this.#inRawMode = true;
|
||||
@ -96,7 +98,7 @@ class Termios {
|
||||
const oldTermiosPtr = this.#ffi.getPointer(this.#cookedTermios);
|
||||
const res = this.#ffi.tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr);
|
||||
if (res === -1) {
|
||||
die('Failed to restore canonical mode.');
|
||||
die('Failed to restore canonical mode.').then(() => {});
|
||||
}
|
||||
|
||||
this.#inRawMode = false;
|
||||
@ -111,7 +113,7 @@ export const getTermios = async () => {
|
||||
}
|
||||
|
||||
// Get the runtime-specific ffi wrappers
|
||||
const ffi: IFFI = await importDefaultForRuntime('ffi');
|
||||
const { ffi } = await getRuntime();
|
||||
termiosSingleton = new Termios(ffi);
|
||||
|
||||
return termiosSingleton;
|
||||
|
@ -10,6 +10,12 @@ export interface IPoint {
|
||||
// ----------------------------------------------------------------------------
|
||||
// Runtime adapter interfaces
|
||||
// ----------------------------------------------------------------------------
|
||||
export enum RunTimeType {
|
||||
Bun = 'bun',
|
||||
Deno = 'deno',
|
||||
Unknown = 'common',
|
||||
}
|
||||
|
||||
/**
|
||||
* The native functions for terminal settings
|
||||
*/
|
||||
@ -52,7 +58,7 @@ export interface ITerminalIO {
|
||||
/**
|
||||
* Get the size of the terminal
|
||||
*/
|
||||
getSize(): ITerminalSize;
|
||||
getTerminalSize(): Promise<ITerminalSize>;
|
||||
|
||||
/**
|
||||
* Pipe a string to stdout
|
||||
@ -61,11 +67,31 @@ export interface ITerminalIO {
|
||||
}
|
||||
|
||||
export interface IRuntime {
|
||||
/**
|
||||
* The name of the runtime
|
||||
*/
|
||||
name: RunTimeType;
|
||||
/**
|
||||
* Runtime-specific FFI
|
||||
*/
|
||||
ffi: IFFI;
|
||||
/**
|
||||
* Runtime-specific terminal functionality
|
||||
*/
|
||||
io: ITerminalIO;
|
||||
|
||||
/**
|
||||
* Set a beforeExit/beforeUnload event handler for the runtime
|
||||
* @param cb - The event handler
|
||||
*/
|
||||
onExit(cb: () => void): void;
|
||||
|
||||
/**
|
||||
* Stop execution
|
||||
*
|
||||
* @param code
|
||||
*/
|
||||
exit(code?: number): void;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -55,11 +55,7 @@ export function is_control(char: string): boolean {
|
||||
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;
|
||||
}
|
||||
|
||||
const point = char.codePointAt(0)!;
|
||||
return String.fromCodePoint(point & 0x1f);
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,19 @@
|
||||
/**
|
||||
* The main entrypoint when using Deno as the runtime
|
||||
*/
|
||||
import { IRuntime } from '../common/types.ts';
|
||||
|
||||
export * from './terminal_io.ts';
|
||||
|
||||
export const onExit = (cb: () => void): void => {
|
||||
globalThis.addEventListener('onbeforeunload', cb);
|
||||
};
|
||||
import { IRuntime, RunTimeType } from '../common/mod.ts';
|
||||
import DenoFFI from './ffi.ts';
|
||||
import DenoTerminalIO from './terminal_io.ts';
|
||||
|
||||
const DenoRuntime: IRuntime = {
|
||||
onExit,
|
||||
name: RunTimeType.Deno,
|
||||
ffi: DenoFFI,
|
||||
io: DenoTerminalIO,
|
||||
onExit: (cb: () => void): void => {
|
||||
globalThis.addEventListener('onbeforeunload', cb);
|
||||
globalThis.onbeforeunload = cb;
|
||||
},
|
||||
exit: (code?: number) => Deno.exit(code),
|
||||
};
|
||||
|
||||
export default DenoRuntime;
|
||||
|
@ -4,29 +4,27 @@ const DenoTerminalIO: ITerminalIO = {
|
||||
/**
|
||||
* Wrap the runtime-specific hook into stdin
|
||||
*/
|
||||
inputLoop: async function* inputLoop(): AsyncGenerator<
|
||||
Uint8Array,
|
||||
void,
|
||||
unknown
|
||||
> {
|
||||
inputLoop: async function* inputLoop() {
|
||||
for await (const chunk of Deno.stdin.readable) {
|
||||
yield chunk;
|
||||
}
|
||||
},
|
||||
getSize: function getSize(): ITerminalSize {
|
||||
getTerminalSize: function getSize(): Promise<ITerminalSize> {
|
||||
const size: { rows: number; columns: number } = Deno.consoleSize();
|
||||
|
||||
return {
|
||||
return Promise.resolve({
|
||||
rows: size.rows,
|
||||
cols: size.columns,
|
||||
};
|
||||
});
|
||||
},
|
||||
write: async function write(s: string): Promise<void> {
|
||||
const buffer = new TextEncoder().encode(s);
|
||||
const buffer: Uint8Array = new TextEncoder().encode(s);
|
||||
const stdout: WritableStream<Uint8Array> = Deno.stdout.writable;
|
||||
|
||||
const stdout = Deno.stdout.writable.getWriter();
|
||||
await stdout.write(buffer);
|
||||
stdout.releaseLock();
|
||||
const writer: WritableStreamDefaultWriter<Uint8Array> = stdout.getWriter();
|
||||
await writer.ready;
|
||||
await writer.write(buffer);
|
||||
writer.releaseLock();
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,47 +1,38 @@
|
||||
/**
|
||||
* The starting point for running scroll
|
||||
*/
|
||||
import {
|
||||
Editor,
|
||||
getTermios,
|
||||
importDefaultForRuntime,
|
||||
importForRuntime,
|
||||
} from './common/mod.ts';
|
||||
import { Editor, getRuntime, getTermios } from './common/mod.ts';
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export async function main() {
|
||||
const { inputLoop, getSize } = await importDefaultForRuntime(
|
||||
'terminal_io.ts',
|
||||
);
|
||||
const { onExit } = await importForRuntime('mod.ts');
|
||||
const runTime = await getRuntime();
|
||||
const { io, onExit } = runTime;
|
||||
|
||||
// Setup raw mode, and tear down on error or normal exit
|
||||
const t = await getTermios();
|
||||
t.enableRawMode();
|
||||
onExit(() => {
|
||||
console.log('Exit handler called, disabling raw mode\r\n');
|
||||
t.disableRawMode();
|
||||
});
|
||||
|
||||
const terminalSize = await io.getTerminalSize();
|
||||
|
||||
// Create the editor itself
|
||||
const terminalSize = getSize();
|
||||
const editor = new Editor(terminalSize);
|
||||
await editor.refreshScreen();
|
||||
|
||||
// The main event loop
|
||||
for await (const chunk of inputLoop()) {
|
||||
const char = String(decoder.decode(chunk));
|
||||
|
||||
// Clear the screen for output
|
||||
await editor.refreshScreen();
|
||||
|
||||
for await (const chunk of io.inputLoop()) {
|
||||
// Process input
|
||||
const char = String(decoder.decode(chunk));
|
||||
const shouldLoop = editor.processKeyPress(char);
|
||||
|
||||
if (!shouldLoop) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Render output
|
||||
await editor.refreshScreen();
|
||||
}
|
||||
|
||||
return -1;
|
||||
|
Loading…
Reference in New Issue
Block a user