~yerinalexey/csq

b85274f956aa58b8bcfc57295b90406dbcdb14d2 — Alexey Yerin a month ago f856ff1
Use madeline instead of a custom line editor
9 files changed, 206 insertions(+), 665 deletions(-)

M README.md
M eval.ha
M main.ha
D nui.ha
M parse.ha
D ui.ha
D vt/cmd/vttest/main.ha
D vt/parser.ha
D vt/types.ha
M README.md => README.md +4 -1
@@ 39,7 39,9 @@ $ csq
```

# Installation
At build time, a recent [Hare] toolchain is required.
Dependencies:
* A recent [Hare] toolchain
* [madeline]

```shell-session
$ make


@@ 55,5 57,6 @@ $ git config format.subjectPrefix "PATCH csq"
```

[Hare]: https://harelang.org/documentation/install/
[madeline]: https://sr.ht/~ecs/madeline
[archives]: https://lists.sr.ht/~yerinalexey/public-inbox
[git send-email]: https://git-send-email.io

M eval.ha => eval.ha +51 -19
@@ 12,30 12,62 @@ let factorials: [21]i64 = [0...];
	};
};

fn eval(u: *ui, e: *expr) (rational::number | noresult | invalid) = {
type state = struct {
	options: options,
	results: [](rational::number | invalid),
};

fn init() state = {
	return state {
		options = default_options,
		results = [],
	};
};

fn eval_line(st: *state, line: str) (rational::number | noresult | invalid) = {
	let expr = match (parse_expr(line)) {
	case let e: *expr =>
		yield e;
	case invalid =>
		append(st.results, invalid);
		return invalid;
	};
	defer expr_free(expr);

	match (eval(st, expr)) {
	case let result: (rational::number | invalid) =>
		append(st.results, result);
		return result;
	case noresult =>
		append(st.results, invalid);
		return noresult;
	};
};

fn eval(st: *state, e: *expr) (rational::number | noresult | invalid) = {
	match (*e) {
	case let i: rational::number =>
		return i;
	case let r: reference =>
		if (r == -1) {
			if (len(u.results) == 0) {
			if (len(st.results) == 0) {
				return invalid;
			};
			return u.results[len(u.results) - 1];
			return st.results[len(st.results) - 1];
		};
		const r = (r - 1): size;
		if (r >= len(u.results)) {
		if (r >= len(st.results)) {
			return invalid;
		};
		return u.results[r];
		return st.results[r];
	case let s: sqrt_expr =>
		const s = eval(u, s)?;
		const s = eval(st, s)?;
		if (rational::sign(s) < 0) {
			return invalid;
		};
		return rational::sqrt(s);
	case let f: factorial_expr =>
		const f = eval(u, f)?;
		const f = eval(st, f)?;
		if (rational::sign(f) < 0 || f.denominator != 1) {
			return invalid;
		};


@@ 44,38 76,38 @@ fn eval(u: *ui, e: *expr) (rational::number | noresult | invalid) = {
		};
		return rational::num(factorials[f.numerator], 1);
	case let s: negate_expr =>
		const s = eval(u, s)?;
		const s = eval(st, s)?;
		return rational::neg(s);
	case let o: option_expr =>
		const (name, value) = o;
		match (value) {
		case let value: *expr =>
			set_option(&u.options, name, eval(u, value)?)?;
			set_option(&st.options, name, eval(st, value)?)?;
			return noresult;
		case =>
			return get_option(&u.options, name);
			return get_option(&st.options, name);
		};
	case let o: op_expr =>
		switch (o.op) {
		case operation::ADD =>
			return rational::add(eval(u, o.lhs)?, eval(u, o.rhs)?);
			return rational::add(eval(st, o.lhs)?, eval(st, o.rhs)?);
		case operation::SUBTRACT =>
			return rational::sub(eval(u, o.lhs)?, eval(u, o.rhs)?);
			return rational::sub(eval(st, o.lhs)?, eval(st, o.rhs)?);
		case operation::MULTIPLY =>
			return rational::mul(eval(u, o.lhs)?, eval(u, o.rhs)?);
			return rational::mul(eval(st, o.lhs)?, eval(st, o.rhs)?);
		case operation::DIVIDE =>
			const rhs = eval(u, o.rhs)?;
			const rhs = eval(st, o.rhs)?;
			if (rhs.numerator == 0) {
				return invalid;
			};
			return rational::div(eval(u, o.lhs)?, rhs);
			return rational::div(eval(st, o.lhs)?, rhs);
		case operation::EXPONENT =>
			const lhs = eval(u, o.lhs)?;
			const rhs = eval(u, o.rhs)?;
			const lhs = eval(st, o.lhs)?;
			const rhs = eval(st, o.rhs)?;
			return exponent(lhs, rhs)?;
		case operation::MODULO =>
			const lhs = eval(u, o.lhs)?;
			const rhs = eval(u, o.rhs)?;
			const lhs = eval(st, o.lhs)?;
			const rhs = eval(st, o.rhs)?;
			if (rational::sign(rhs) == 0) {
				return invalid;
			};

M main.ha => main.ha +148 -21
@@ 1,37 1,164 @@
use errors;
use bufio;
use memio;
use encoding::utf8;
use fmt;
use io;
use made;
use os;
use rational;
use strings;
use unix::tty;
use vt;

export fn main() void = {
	if (!tty::isatty(os::stdin_file)) {
		// Non-interactive mode
		const u = nui_init();
		defer nui_finish(&u);
	let state = init();

		match (nui_run(&u)) {
		case => void;
	if (tty::isatty(os::stdin_file) && tty::isatty(os::stdout_file)) {
		match (run_interactive(&state)) {
		case void => void;
		case let err: made::error =>
			fmt::fatal("Error:", made::strerror(err));
		};
	} else {
		match (run_non_interactive(&state)) {
		case void => void;
		case let err: io::error =>
			fmt::fatal("Error:", io::strerror(err));
		case let err: utf8::invalid =>
			fmt::fatal("Error:", utf8::strerror(err));
		};
	};
};

type context = struct {
	mctx: made::context,
	state: *state,
};

fn hint_preview(ctx: *made::context, line: []u8) str = {
	let ctx = ctx: *context;
	if (!ctx.state.options.preview) {
		return "";
	};

	let line = strings::fromutf8(line)!;

	let expr = match (parse_expr(line)) {
	case let e: *expr =>
		yield e;
	case invalid =>
		return "";
	};
	defer expr_free(expr);

	if (!expr_pure(expr)) {
		return "";
	};

	match (eval(ctx.state, expr)) {
	case let result: rational::number =>
		let buf = memio::dynamic();
		memio::concat(&buf, " = ")!;
		print_rational(&buf, ctx.state, result)!;
		return memio::string(&buf)!;
	case invalid =>
		return " = invalid";
	case noresult =>
		return "";
	};
};

fn run_interactive(st: *state) (void | made::error) = {
	let ctx = context {
		mctx = made::context {
			complete = &made::complete_none,
			hint = &hint_preview,
			hist = &made::histmem(),
			prompt = &made::prompt_string("> "),
			split = made::split_default,
			cfg = made::config_default("csq")?,
			...
		},
		state = st,
	};
	defer made::ctx_finish(&ctx)!;

	for (true) {
		match (made::line(&ctx)?) {
		case let line: str =>
			defer free(line);

			// Since madeline outputs a newline character after
			// pressing enter, it's necessary to clear that line so
			// expression number, the expression and its result can
			// be written on one line.
			//
			// ESC[F  moves to the beginning of the previous line
			// ESC[2K clears that line
			//
			// TODO: make this work for lines that exceed terminal
			// width
			fmt::print("\x1b[F" "\x1b[2K")?;

			fmt::printf("({}) {}", len(st.results) + 1, line)?;
			let empty = (len(strings::trim(line)) == 0);
			if (!empty) {
				match (eval_line(st, line)) {
				case let n: rational::number =>
					fmt::print(" = ")?;
					print_rational(os::stdout, st, n)?;
				case invalid =>
					fmt::print(" = invalid")?;
				case noresult => void;
				};
			};
			fmt::println()?;
		case io::EOF =>
			break;
		case void =>
			// Ctrl-C
			// See above for explaination of the sequence
			fmt::print("\x1b[F" "\x1b[2K")?;
			bufio::flush(os::stdout)?;
		};
		return;
	};
};

	const u = match (ui_init()) {
	case let u: ui =>
		yield u;
	case let err: io::error =>
		fmt::fatal("I/O error:", io::strerror(err));
	case let err: errors::error =>
		fmt::fatal("Error:", errors::strerror(err));
fn run_non_interactive(st: *state) (void | io::error | utf8::invalid) = {
	for (let line => bufio::read_line(os::stdin)?) {
		let line = strings::fromutf8(line)?;
		match (eval_line(st, line)) {
		case let n: rational::number =>
			print_rational(os::stdout, st, n)?;
			fmt::println()?;
		case invalid =>
			fmt::println("invalid")?;
		case noresult => void;
		};
	};
	defer ui_finish(&u);
};

	match (ui_run(&u)) {
	case => void;
	case let err: vt::error =>
		fmt::fatal("Terminal error:", vt::strerror(err));
fn print_rational(
	out: io::handle,
	st: *state,
	n: rational::number,
) (size | io::error) = {
	if (!st.options.mixed) {
		return rational::fmt(out, n);
	};

	const mixed = n.numerator / n.denominator: i64;
	n.numerator %= n.denominator: i64;
	if (mixed == 0) {
		return rational::fmt(out, n);
	};
	if (n.numerator == 0) {
		return fmt::fprint(out, mixed);
	};
	if (rational::sign(n) > 0) {
		return fmt::fprintf(out, "{} + {}/{}",
			mixed, n.numerator, n.denominator);
	} else {
		return fmt::fprintf(out, "-({} + {}/{})",
			-mixed, -n.numerator, n.denominator);
	};
};

D nui.ha => nui.ha +0 -42
@@ 1,42 0,0 @@
// nui = Non-interactive UI
// Why did I name it that? No idea
use bufio;
use fmt;
use io;
use os;
use rational;
use strings;

fn nui_init() ui = ui {
	term = os::stdout_file,
	options = default_options,
	...
};

fn nui_finish(u: *ui) void = {
	free(u.results);
};

fn nui_run(u: *ui) (void | io::error) = {
	for (let line => bufio::read_line(os::stdin)?) {
		let line = strings::fromutf8(line)!;

		match (parse_expr(line)) {
		case let e: *expr =>
			match (eval(u, e)) {
			case let i: rational::number =>
				append(u.results, i);
				print_rational(u, i)?;
				fmt::fprintln(u.term)?;
			case noresult =>
				append(u.results, invalid);
			case invalid =>
				append(u.results, invalid);
				fmt::fprintln(u.term, "invalid")?;
			};
		case =>
			append(u.results, invalid);
			fmt::fprintln(u.term, "invalid")?;
		};
	};
};

M parse.ha => parse.ha +3 -3
@@ 35,12 35,12 @@ fn expr_free(e: *expr) void = {
	free(e);
};

fn expr_void(e: *expr) bool = {
fn expr_pure(e: *expr) bool = {
	match (*e) {
	case let opt: option_expr =>
		return opt.1 is *expr;
		return !(opt.1 is *expr);
	case =>
		return false;
		return true;
	};
};


D ui.ha => ui.ha +0 -302
@@ 1,302 0,0 @@
use ascii;
use errors;
use fmt;
use io;
use math;
use rational;
use strings;
use unix::tty;
use vt;

type ui = struct {
	term: io::file,
	termios: tty::termios,
	running: bool,

	options: options,

	results: [](rational::number | invalid),
	entry: []rune,
	entry_pos: size,
};

fn ui_init() (ui | io::error | errors::error) = {
	const term = match (tty::open()) {
	case let f: io::file =>
		yield f;
	case let err: tty::error =>
		return err: errors::error;
	};
	const termios = tty::termios_query(term)?;
	tty::makeraw(&termios)?;
	fmt::fprint(term, "> ")?;

	return ui {
		term = term,
		termios = termios,
		options = default_options,
		...
	};
};

fn ui_finish(u: *ui) void = {
	fmt::fprint(u.term, "\x1b[2K\r")!;
	tty::termios_restore(&u.termios);
	io::close(u.term)!;
	free(u.entry);
	free(u.results);
};

fn ui_run(u: *ui) (void | vt::error) = {
	u.running = true;
	for (u.running) {
		ui_tick(u)?;
	};
};

fn ui_tick(u: *ui) (void | vt::error) = {
	const (key, modifiers) = match (vt::nextkey(u.term)) {
	case let k: vt::key =>
		yield k;
	case let err: vt::error =>
		match (err) {
		case vt::invalid =>
			return;
		case =>
			return err;
		};
	};
	match (key) {
	case let kk: vt::keycode =>
		switch (kk) {
		case vt::keycode::DELETE =>
			ui_entry_delete(u);
		case vt::keycode::BACKSPACE =>
			ui_entry_backspace(u);
		case vt::keycode::HOME =>
			ui_entry_home(u);
		case vt::keycode::END =>
			ui_entry_end(u);
		case vt::keycode::RIGHT =>
			if (modifiers & vt::modifiers::CONTROL != 0) {
				ui_entry_right_wordwise(u);
			} else {
				ui_entry_right(u);
			};
		case vt::keycode::LEFT =>
			if (modifiers & vt::modifiers::CONTROL != 0) {
				ui_entry_left_wordwise(u);
			} else {
				ui_entry_left(u);
			};
		case vt::keycode::ENTER =>
			append_expr(u)?;
		case => void;
		};
	case let r: rune =>
		if (modifiers & vt::modifiers::CONTROL != 0) {
			switch (r) {
			case 'd' =>
				u.running = false;
			case 'c' =>
				ui_entry_clear(u);
			case 'w' =>
				ui_entry_kill_word(u);
			case =>
				// TODO: ctrl+r(?)
				ui_entry_insert(u, 'C');
				ui_entry_insert(u, '-');
				ui_entry_insert(u, r);
			};
		} else if (ascii::isprint(r)) {
			ui_entry_insert(u, r);
		};
	};

	ui_entry_draw(u)?;
};

fn append_expr(u: *ui) (void | io::error) = {
	const s = strings::fromrunes(u.entry);
	defer free(s);

	ui_entry_clear(u);

	if (len(strings::trim(s)) == 0) {
		fmt::fprintln(u.term)?;
		return;
	};

	fmt::fprintf(u.term, "\r\x1b[2K({}) {}", len(u.results) + 1, s)?;
	match (parse_expr(s)) {
	case let e: *expr =>
		match (eval(u, e)) {
		case let i: rational::number =>
			append(u.results, i);
			fmt::fprint(u.term, " = ")?;
			print_rational(u, i)?;
			fmt::fprintln(u.term)?;
		case noresult =>
			append(u.results, invalid);
			fmt::fprintln(u.term)?;
		case invalid =>
			append(u.results, invalid);
			fmt::fprintfln(u.term, " = invalid")?;
		};
	case =>
		append(u.results, invalid);
		fmt::fprintfln(u.term, " = invalid")?;
	};
};

fn ui_entry_draw(u: *ui) (void | io::error) = {
	fmt::fprint(u.term, "\r\x1b[2K> ")?;
	for (let i = 0z; i < u.entry_pos; i += 1) {
		fmt::fprint(u.term, u.entry[i])?;
	};
	fmt::fprint(u.term, "\x1b7")?; // Save cursor
	for (let i = u.entry_pos; i < len(u.entry); i += 1) {
		fmt::fprint(u.term, u.entry[i])?;
	};

	if (u.options.preview) {
		const s = strings::fromrunes(u.entry);
		defer free(s);
		match (parse_expr(s)) {
		case let e: *expr =>
			defer expr_free(e);
			if (expr_void(e)) yield;

			fmt::fprint(u.term, " \x1b[33m = ")?;
			match (eval(u, e)) {
			case let i: rational::number =>
				print_rational(u, i)?;
			case noresult => void;
			case invalid =>
				fmt::fprintf(u.term, "invalid")?;
			};
			fmt::fprint(u.term, "\x1b[0m")?;
		case invalid => void;
		};
	};

	// Restore cursor
	fmt::fprint(u.term, "\x1b8")?;
};

fn ui_entry_insert(u: *ui, r: rune) void = {
	insert(u.entry[u.entry_pos], r);
	u.entry_pos += 1;
};

fn ui_entry_backspace(u: *ui) void = {
	if (len(u.entry) > 0 && u.entry_pos > 0) {
		delete(u.entry[u.entry_pos - 1]);
		u.entry_pos -= 1;
	};
};

fn ui_entry_delete(u: *ui) void = {
	if (len(u.entry) > 0 && u.entry_pos < len(u.entry)) {
		delete(u.entry[u.entry_pos]);
	};
};

fn ui_entry_right(u: *ui) void = {
	if (u.entry_pos + 1 <= len(u.entry)) {
		u.entry_pos += 1;
	};
};

fn ui_entry_right_wordwise(u: *ui) void = {
	for (u.entry_pos < len(u.entry) && u.entry[u.entry_pos] == ' ') {
		u.entry_pos += 1;
	};
	for (let i = u.entry_pos; i < len(u.entry); i += 1) {
		if (u.entry[i] == ' ') {
			u.entry_pos = i;
			return;
		};
	};
	u.entry_pos = len(u.entry);
};

fn ui_entry_left(u: *ui) void = {
	if (u.entry_pos > 0) {
		u.entry_pos -= 1;
	};
};

fn ui_entry_left_wordwise(u: *ui) void = {
	if (u.entry_pos == len(u.entry)) {
		u.entry_pos -= 1;
	};
	if (u.entry[u.entry_pos] != ' ' && u.entry[u.entry_pos - 1] == ' ') {
		u.entry_pos -= 1;
	};
	for (u.entry_pos > 0 && u.entry[u.entry_pos] == ' ') {
		u.entry_pos -= 1;
	};
	for (let i = u.entry_pos; i > 0; i -= 1) {
		if (u.entry[i] != ' ' && u.entry[i - 1] == ' ') {
			u.entry_pos = i;
			return;
		};
	};
	u.entry_pos = 0;
};

fn ui_entry_home(u: *ui) void = {
	u.entry_pos = 0;
};

fn ui_entry_end(u: *ui) void = {
	u.entry_pos = len(u.entry);
};

fn ui_entry_clear(u: *ui) void = {
	delete(u.entry[..]);
	u.entry_pos = 0;
};

fn ui_entry_kill_word(u: *ui) void = {
	if (u.entry_pos == 0) {
		return;
	};
	let seen_space = false;
	for (let i = u.entry_pos - 1; i > 0; i -= 1) {
		if (u.entry[i] == ' ') {
			seen_space = true;
		} else {
			if (seen_space) {
				delete(u.entry[i + 1..]);
				u.entry_pos = i + 1;
				return;
			};
		};
	};
	delete(u.entry[..]);
	u.entry_pos = 0;
};

fn print_rational(u: *ui, n: rational::number) (size | io::error) = {
	if (!u.options.mixed) {
		return rational::fmt(u.term, n);
	};

	const mixed = n.numerator / n.denominator: i64;
	n.numerator %= n.denominator: i64;
	if (mixed == 0) {
		return rational::fmt(u.term, n);
	};
	if (n.numerator == 0) {
		return fmt::fprint(u.term, mixed);
	};
	if (rational::sign(n) > 0) {
		return fmt::fprintf(u.term, "{} + {}/{}",
			mixed, n.numerator, n.denominator);
	} else {
		return fmt::fprintf(u.term, "-({} + {}/{})",
			-mixed, -n.numerator, n.denominator);
	};
};

D vt/cmd/vttest/main.ha => vt/cmd/vttest/main.ha +0 -64
@@ 1,64 0,0 @@
use fmt;
use unix::tty;
use vt;
use io;

export fn main() void = {
	const term = tty::open()!;
	defer io::close(term)!;

	const termios = tty::termios_query(term)!;
	defer tty::termios_restore(&termios);

	tty::makeraw(&termios)!;

	let exit = false;
	for (!exit) {
		const (key, modifiers) = match (vt::nextkey(term)) {
		case let k: vt::key =>
			yield k;
		case let err: vt::error =>
			continue;
		case io::EOF =>
			break;
		};

		match (key) {
		case let r: rune =>
			fmt::fprintf(term, "Key: {:x}, modifiers: [", r: u32)!;
		case let kk: vt::keycode =>
			const s = switch (kk) {
			case vt::keycode::ESC =>
				exit = true;
				yield "ESC";
			case vt::keycode::DOWN      => yield "DOWN";
			case vt::keycode::UP        => yield "UP";
			case vt::keycode::LEFT      => yield "LEFT";
			case vt::keycode::RIGHT     => yield "RIGHT";
			case vt::keycode::HOME      => yield "HOME";
			case vt::keycode::END       => yield "END";
			case vt::keycode::PAGE_DOWN => yield "PAGE_DOWN";
			case vt::keycode::PAGE_UP   => yield "PAGE_UP";
			case vt::keycode::INSERT    => yield "INSERT";
			case vt::keycode::DELETE    => yield "DELETE";
			case vt::keycode::FUNCTION  => yield "FUNCTION";
			case => abort();
			};
			fmt::fprintf(term, "Key: {}, modifiers: [", s)!;
		};

		if (modifiers & vt::modifiers::SHIFT != 0) {
			fmt::fprint(term, " SHIFT")!;
		};
		if (modifiers & vt::modifiers::ALT != 0) {
			fmt::fprint(term, " ALT")!;
		};
		if (modifiers & vt::modifiers::CONTROL != 0) {
			fmt::fprint(term, " CONTROL")!;
		};
		if (modifiers & vt::modifiers::META != 0) {
			fmt::fprint(term, " META")!;
		};
		fmt::fprint(term, " ]\n\r")!;
	};
};

D vt/parser.ha => vt/parser.ha +0 -193
@@ 1,193 0,0 @@
use ascii;
use bufio;
use io;

// Special keys.
export type keycode = enum {
	ESC,

	DOWN,
	UP,
	LEFT,
	RIGHT,
	HOME,
	END,
	PAGE_DOWN,
	PAGE_UP,

	BACKSPACE,
	DELETE,
	ENTER,
	INSERT,

	FUNCTION, // F1 - F20, all TODO
};

// Modifiers that are applied to a key press.
export type modifiers = enum uint {
	NONE    = 0,
	SHIFT   = (1 << 0),
	ALT     = (1 << 1),
	CONTROL = (1 << 2),
	META    = (1 << 3),
};

// A key press event.
export type key = ((rune | keycode), modifiers);

def ESC: rune = '\u001b';

// Parses an input sequence from a TTY.
export fn nextkey(in: io::handle) (key | io::EOF | error) = {
	const r = match (bufio::read_rune(in)?) {
	case let r: rune =>
		yield r;
	case io::EOF =>
		return io::EOF;
	};

	if (r == ESC) {
		return parse_escape(in);
	} else if (ascii::iscntrl(r)) {
		return control_lookup(r);
	} else {
		return (r, modifiers::NONE);
	};
};

fn parse_escape(in: io::handle) (key | error) = {
	const r = wantrune(in)?;
	switch (r) {
	case '[' =>
		// <esc> '[' -> keycode sequence
		return parse_sequence(in);
	case ESC =>
		// <esc> <esc> -> esc
		return (keycode::ESC, modifiers::NONE);
	case =>
		// <esc> <char> -> Alt-keypress
		return (r, modifiers::ALT);
	};
};

fn parse_sequence(in: io::handle) (key | error) = {
	let r = wantrune(in)?;

	if (!ascii::isdigit(r) && !ascii::isupper(r)) {
		return invalid;
	};

	if (ascii::isupper(r)) {
		// <esc> '[' <char>, i.e. xterm sequence without modifiers
		return (xterm_lookup(r)?, modifiers::NONE);
	};

	// Either xterm with modifiers or vt
	let n = 0;
	for (ascii::isdigit(r); r = wantrune(in)?) {
		n *= 10;
		n += todigit(r);
	};
	switch (r) {
	case ';' =>
		// <esc> '[' <keycode> ';' <modifier> '~', i.e. vt with modifiers
		// or
		// <esc> '[' <> ';' <modifier> <char>, i.e. xterm with modifiers
		let mod = 0;
		let r = wantrune(in)?;
		for (ascii::isdigit(r); r = wantrune(in)?) {
			mod *= 10;
			mod += todigit(r);
		};
		if (ascii::isupper(r)) {
			return (xterm_lookup(r)?, modifier_lookup(mod)?);
		} else if (r == '~') {
			return (vt_lookup(n)?, modifier_lookup(mod)?);
		} else {
			return invalid;
		};
	case '~' =>
		// <esc> '[' <keycode> '~', i.e. vt without modifiers
		return (vt_lookup(n)?, modifiers::NONE);
	case => void;
	};

	// The only remaining variant is xterm with modifiers
	if (!ascii::isupper(r)) {
		return invalid;
	};
	return (xterm_lookup(r)?, modifier_lookup(n)?);
};

fn todigit(r: rune) int = (r: u32 - '0': u32): int;

fn wantrune(in: io::handle) (rune | error) = {
	match (bufio::read_rune(in)?) {
	case let r: rune =>
		return r;
	case io::EOF =>
		return invalid;
	};
};

fn xterm_lookup(r: rune) (keycode | error) = {
	switch (r) {
	case 'A' => return keycode::UP;
	case 'B' => return keycode::DOWN;
	case 'C' => return keycode::RIGHT;
	case 'D' => return keycode::LEFT;
	case 'F' => return keycode::END;
	case 'G' => abort(); // I have some questions
	case 'H' => return keycode::HOME;
	case 'P', 'Q', 'R', 'S' => return keycode::FUNCTION;
	case => return invalid;
	};
};

fn vt_lookup(n: int) (keycode | error) = {
	switch (n) {
	case 1, 7 =>
		return keycode::HOME;
	case 2 =>
		return keycode::INSERT;
	case 3 =>
		return keycode::DELETE;
	case 4, 8 =>
		return keycode::END;
	case 5 =>
		return keycode::PAGE_UP;
	case 6 =>
		return keycode::PAGE_DOWN;
	case 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28,
	29, 31, 32, 33, 34, 35 =>
	     return keycode::FUNCTION;
	case => return invalid;
	};
};

fn control_lookup(r: rune) (key | error) = {
	const r = r: u32;
	switch (r) {
	case 0x08 =>
		return (keycode::DELETE, modifiers::NONE);
	case 0x7f =>
		return (keycode::BACKSPACE, modifiers::NONE);
	case '\r' =>
		return (keycode::ENTER, modifiers::NONE);
	case => void;
	};
	if (r >= 0x01 && r <= 0x1a) {
		return ((r - 1 + 'a': u32): rune, modifiers::CONTROL);
	};
	return invalid;
};

fn modifier_lookup(n: int) (modifiers | error) = {
	if (n == 0) {
		// Should we default to 1 instead?
		return invalid;
	};
	n -= 1;
	// TODO: check if other flags are set
	return n: modifiers;
};

D vt/types.ha => vt/types.ha +0 -20
@@ 1,20 0,0 @@
use encoding::utf8;
use io;

// Invalid input sequence.
export type invalid = !void;

// Any error that may occur.
export type error = !(io::error | utf8::invalid | invalid);

// Returns a string representation of an [[error]].
export fn strerror(err: error) const str = {
	match (err) {
	case let err: io::error =>
		return io::strerror(err);
	case utf8::invalid =>
		return "Invalid UTF-8 input";
	case invalid =>
		return "Invalid input sequence";
	};
};