~sircmpwn/hare-vt

5aa0e8def2bac93ac17090d8e9c2257e8c574e00 — Drew DeVault 1 year, 1 month ago 60fe6ee
Merge input handling into driver
4 files changed, 349 insertions(+), 362 deletions(-)

M example.ha
A vt/event.ha
D vt/input.ha
M vt/xterm.ha
M example.ha => example.ha +1 -2
@@ 30,9 30,8 @@ fn run() (void | vt::error) = {
	const p2 = vt::newpen(color::YELLOW, color::RED);
	vt::print(term, "oh! ", tag(p1, "hello, ", tag(p2, "vt", 100, "!")), "\r\n")?;

	let in = vt::stdinreader()?;
	for (true) {
		const ev = match (vt::getevent(&in)?) {
		const ev = match (vt::getevent(term)?) {
		case io::EOF =>
			break;
		case let ev: vt::event =>

A vt/event.ha => vt/event.ha +56 -0
@@ 0,0 1,56 @@
use ascii;
use encoding::utf8;
use fmt;
use io;
use os;
use unix::tty;
use unix::poll;

export type reader = struct {
	buf: [os::BUFSIZ]u8,
	end: size,
};

// A terminal event.
export type event = struct {
	value: key,
	mods: modflag,
};

// A key press event value.
export type key = (rune | specialkey | functionkey);

// A function key, e.g. F1.
export type functionkey = u8;

// A special key, e.g. UP.
export type specialkey = enum {
	UP,
	DOWN,
	RIGHT,
	LEFT,
	PGUP,
	PGDN,
	HOME,
	END,
	INSERT,
	DELETE,
};

// Key modifier flags.
export type modflag = enum u8 {
	SHIFT = 1 << 0,
	ALT   = 1 << 1,
	CTRL  = 1 << 2,
	META  = 1 << 3,
};

// Returns the next event, or void if none are pending.
export fn pollevent(term: *term) (void | event | io::EOF | error) = {
	return term.driver.pollev(term, false);
};

// Blocks until the next event is available from a [[reader]].
export fn getevent(term: *term) (event | io::EOF | error) = {
	return term.driver.pollev(term, true) as (event | io::EOF | error);
};

D vt/input.ha => vt/input.ha +0 -359
@@ 1,359 0,0 @@
use ascii;
use encoding::utf8;
use fmt;
use io;
use os;
use unix::tty;
use unix::poll;

export type reader = struct {
	buf: [os::BUFSIZ]u8,
	end: size,
};

// A terminal event.
export type event = struct {
	value: key,
	mods: modflag,
};

// A key press event value.
export type key = (rune | specialkey | functionkey);

// A function key, e.g. F1.
export type functionkey = u8;

// A special key, e.g. UP.
export type specialkey = enum {
	UP,
	DOWN,
	RIGHT,
	LEFT,
	PGUP,
	PGDN,
	HOME,
	END,
	INSERT,
	DELETE,
};

// Key modifier flags.
export type modflag = enum u8 {
	SHIFT = 1 << 0,
	ALT   = 1 << 1,
	CTRL  = 1 << 2,
	META  = 1 << 3,
};

// Returns a new [[reader]] which reads from the standard input.
export fn stdinreader() (reader | error) = {
	if (tty::isatty(os::stdin_file)) {
		let termios = tty::termios_query(os::stdin_file)?;
		tty::makeraw(&termios)?;
		tty::noecho(&termios)?;
	};
	return reader {...};
};

type more = void;
def ESC: rune = '\x1b';

// Returns the next event, or void if none are pending.
export fn pollevent(rd: *reader) (void | event | io::EOF | error) = {
	return _getevent(rd, false);
};

// Blocks until the next event is available from a [[reader]].
export fn getevent(rd: *reader) (event | io::EOF | error) = {
	return _getevent(rd, true) as (event | io::EOF | error);
};

// TODO: Move remainder of file to xterm driver

// Gets the next event from a [[reader]].
export fn _getevent(rd: *reader, block: bool) (void | event | io::EOF | error) = {
	for (let first = true; first || block; first = false) {
		// opportunistic nonblocking poll on first loop
		let eof = pollbytes(rd, !first)? is io::EOF;
		let d = utf8::decode(rd.buf[..rd.end]);
		let res = event { value = '?', ... };

		let r = match (nextrune(rd, &d, eof)?) {
		case void =>
			if (eof) {
				return io::EOF;
			} else {
				continue;
			};
		case utf8::more =>
			continue;
		case let r: rune =>
			yield r;
		};

		match (decode_event(rd, &d, eof, &res, r)) {
		case more =>
			continue;
		case void =>
			yield;
		};

		if (!(res.value is rune)) {
			// TODO: Accessing utf8::decoder implementation detail
			trimbuf(rd, d.offs);
			return res;
		};

		r = res.value as rune;
		switch (r) {
		case '\0' =>
			res.value = ' ';
			res.mods |= modflag::CTRL;
		case '\b' =>
			res.value = '\x7f';
			res.mods |= modflag::CTRL;
		case '\x1c','\x1d','\x1e','\x1f' =>
			res.value = ('4': u32 + r: u32 - 0x1c): rune;
			res.mods |= modflag::CTRL;
		case => if (ascii::iscntrl(r)) switch (r) {
			case '\t', '\r', '\x1b', '\x7f' => void;
			case =>
				res.value = ('a': u32 + r: u32 - 1): rune;
				res.mods |= modflag::CTRL;
			};
		};
		trimbuf(rd, d.offs);
		return res;
	};
};

// TODO: Merge dec and eof into [[reader]]
fn decode_event(
	rd: *reader,
	dec: *utf8::decoder,
	eof: bool,
	res: *event,
	r: rune,
) (void | more | invalid) = {
	if (r != ESC) {
		// not an escape sequence
		res.value = r;
		return;
	};

	r = match (nextrune(rd, dec, eof)?) {
	case void =>
		yield ESC;
	case let r: rune =>
		yield r;
	case utf8::more =>
		return more;
	};

	switch (r) {
	case '[', 'O' =>
		yield; // SS3 sequence
	case ESC =>
		res.value = ESC;
		return;
	case =>
		res.mods = modflag::ALT;
		res.value = r;
		return;
	};

	let ss3 = r == 'O';
	match (nextrune(rd, dec, eof)?) {
	case void =>
		res.mods = modflag::ALT;
		res.value = r;
	case let r: rune =>
		let n: [3]u32 = [0...];
		for (let i = 0z; i < len(n); i += 1) {
			for (ascii::isdigit(r)) {
				n[i] *= 10;
				n[i] += r: u32 - '0': u32;

				r = match (nextrune(rd, dec, eof)?) {
				case void =>
					if (eof) {
						trimbuf(rd, dec.offs);
						return invalid;
					} else {
						return more;
					};
				case let r: rune =>
					yield r;
				case utf8::more =>
					return more;
				};
			};

			if (n[i] == 0) {
				n[i] = 1;
			};

			switch (r) {
			case '~' =>
				match (decode_key_tilde(n, i)) {
				case let key: key =>
					res.value = key;
				case invalid =>
					trimbuf(rd, dec.offs);
					return invalid;
				};

				if (0 < i) {
					if (16 < n[1]) {
						trimbuf(rd, dec.offs);
						return invalid;
					};
					res.mods = (n[1]: u8 - 1): modflag;
				};
				break;
			case 'A','B','C','D','F','H','K','M','P','Q','R','S','Z' =>
				switch (r) {
				case 'A' =>
					res.value = specialkey::UP;
				case 'B' =>
					res.value = specialkey::DOWN;
				case 'C' =>
					res.value = specialkey::RIGHT;
				case 'D' =>
					res.value = specialkey::LEFT;
				case 'F' =>
					res.value = specialkey::END;
				case 'H' =>
					res.value = specialkey::HOME;
				case 'K' =>
					res.value = specialkey::DELETE;
				case 'M' =>
					n[i] = 5;
					res.value = specialkey::DELETE;
				case 'P' =>
					if (ss3) {
						res.value = 1: functionkey;
					} else {
						res.value = specialkey::DELETE;
					};
				case 'Q' =>
					res.value = 2: functionkey;
				case 'R' =>
					res.value = 3: functionkey;
				case 'S' =>
					res.value = 4: functionkey;
				case 'Z' =>
					res.value = '\t';
					res.mods = modflag::SHIFT;
					break;
				case =>
					abort();
				};
				if (16 < n[i]) {
					trimbuf(rd, dec.offs);
					return invalid;
				};
				res.mods = (n[i]: u8 - 1): modflag;
				break;
			case ';' =>
				r = match (nextrune(rd, dec, eof)?) {
				case let r: rune =>
					yield r;
				case void =>
					if (eof) {
						trimbuf(rd, dec.offs);
						return invalid;
					} else {
						return more;
					};
				case utf8::more =>
					return more;
				};
			case =>
				trimbuf(rd, dec.offs);
				return invalid;
			};
		};
	case utf8::more =>
		return more;
	};
};

const tilde_map_spkey: [_]specialkey = [
	specialkey::HOME,
	specialkey::INSERT,
	specialkey::DELETE,
	specialkey::END,
	specialkey::PGUP,
	specialkey::PGDN,
	specialkey::HOME,
	specialkey::END,
];

fn decode_key_tilde(n: [3]u32, i: size) (key | invalid) = {
	if (n[0] == 27) {
		if (i < 2) {
			return '\x1b';
		};
		return n[2]: rune;
	};
	if (n[0] <= 8) {
		return tilde_map_spkey[n[0] - 1];
	};
	if (n[0] <= 34) {
		return (n[0] - 10): functionkey;
	};
	return invalid;
};

// helper function to turn utf8::more into invalid, and trim the buffer
fn nextrune(
	rd: *reader,
	d: *utf8::decoder,
	eof: bool,
) (void | rune | utf8::more | invalid) = {
	match (utf8::next(d)) {
	case let r: rune =>
		return r;
	case void =>
		return void;
	case utf8::more =>
		if (eof) {
			trimbuf(rd, d.offs);
			return invalid;
		} else {
			return utf8::more;
		};
	case utf8::invalid =>
		trimbuf(rd, d.offs);
		return invalid;
	};
};

fn trimbuf(rd: *reader, pos: size) void = {
	static delete(rd.buf[..][..pos]);
	rd.end -= pos;
};

fn pollbytes(rd: *reader, block: bool) (size | io::EOF | error) = {
	const polls = [
		poll::pollfd {
			fd = os::stdin_file,
			events = poll::event::POLLIN | poll::event::POLLHUP,
			...
		},
	];

	const timeout = if (block) poll::INDEF else poll::NONBLOCK;
	if (poll::poll(polls, timeout)? == 0) {
		return 0z;
	};

	match (io::read(os::stdin_file, rd.buf[rd.end..])?) {
	case io::EOF =>
		return io::EOF;
	case let n: size =>
		rd.end += n;
		return n;
	};
};

M vt/xterm.ha => vt/xterm.ha +292 -1
@@ 1,8 1,15 @@
use ascii;
use encoding::utf8;
use fmt;
use io;
use os;
use unix::poll;
use unix::tty;

type more = void;

def ESC: rune = '\x1b';

type xterm = struct {
	term,
	in: io::file,


@@ 61,7 68,291 @@ fn xterm_write(term: *io::stream, buf: const []u8) (size | io::error) = {

fn xterm_pollev(term: *term, block: bool) (void | event | io::EOF | error) = {
	let term = term: *xterm;
	abort(); // TODO

	for (let first = true; first || block; first = false) {
		// opportunistic nonblocking poll on first loop
		let eof = xterm_poll(term, !first)? is io::EOF;
		let d = utf8::decode(term.buf[..term.end]);
		let res = event { value = '?', ... };

		let r = match (xterm_nextrune(term, &d, eof)?) {
		case void =>
			if (eof) {
				return io::EOF;
			} else {
				continue;
			};
		case utf8::more =>
			continue;
		case let r: rune =>
			yield r;
		};

		match (xterm_decode_event(term, &d, eof, &res, r)) {
		case more =>
			continue;
		case void =>
			yield;
		};

		if (!(res.value is rune)) {
			// TODO: Accessing utf8::decoder implementation detail
			xterm_trimbuf(term, d.offs);
			return res;
		};

		r = res.value as rune;
		switch (r) {
		case '\0' =>
			res.value = ' ';
			res.mods |= modflag::CTRL;
		case '\b' =>
			res.value = '\x7f';
			res.mods |= modflag::CTRL;
		case '\x1c','\x1d','\x1e','\x1f' =>
			res.value = ('4': u32 + r: u32 - 0x1c): rune;
			res.mods |= modflag::CTRL;
		case => if (ascii::iscntrl(r)) switch (r) {
			case '\t', '\r', '\x1b', '\x7f' => void;
			case =>
				res.value = ('a': u32 + r: u32 - 1): rune;
				res.mods |= modflag::CTRL;
			};
		};
		xterm_trimbuf(term, d.offs);
		return res;
	};
};

// TODO: Merge dec and eof into [[term]]
fn xterm_decode_event(
	term: *xterm,
	dec: *utf8::decoder,
	eof: bool,
	res: *event,
	r: rune,
) (void | more | invalid) = {
	if (r != ESC) {
		// not an escape sequence
		res.value = r;
		return;
	};

	r = match (xterm_nextrune(term, dec, eof)?) {
	case void =>
		yield ESC;
	case let r: rune =>
		yield r;
	case utf8::more =>
		return more;
	};

	switch (r) {
	case '[', 'O' =>
		yield; // SS3 sequence
	case ESC =>
		res.value = ESC;
		return;
	case =>
		res.mods = modflag::ALT;
		res.value = r;
		return;
	};

	let ss3 = r == 'O';
	match (xterm_nextrune(term, dec, eof)?) {
	case void =>
		res.mods = modflag::ALT;
		res.value = r;
	case let r: rune =>
		let n: [3]u32 = [0...];
		for (let i = 0z; i < len(n); i += 1) {
			for (ascii::isdigit(r)) {
				n[i] *= 10;
				n[i] += r: u32 - '0': u32;

				r = match (xterm_nextrune(term, dec, eof)?) {
				case void =>
					if (eof) {
						xterm_trimbuf(term, dec.offs);
						return invalid;
					} else {
						return more;
					};
				case let r: rune =>
					yield r;
				case utf8::more =>
					return more;
				};
			};

			if (n[i] == 0) {
				n[i] = 1;
			};

			switch (r) {
			case '~' =>
				match (xterm_decode_key_tilde(n, i)) {
				case let key: key =>
					res.value = key;
				case invalid =>
					xterm_trimbuf(term, dec.offs);
					return invalid;
				};

				if (0 < i) {
					if (16 < n[1]) {
						xterm_trimbuf(term, dec.offs);
						return invalid;
					};
					res.mods = (n[1]: u8 - 1): modflag;
				};
				break;
			case 'A','B','C','D','F','H','K','M','P','Q','R','S','Z' =>
				switch (r) {
				case 'A' =>
					res.value = specialkey::UP;
				case 'B' =>
					res.value = specialkey::DOWN;
				case 'C' =>
					res.value = specialkey::RIGHT;
				case 'D' =>
					res.value = specialkey::LEFT;
				case 'F' =>
					res.value = specialkey::END;
				case 'H' =>
					res.value = specialkey::HOME;
				case 'K' =>
					res.value = specialkey::DELETE;
				case 'M' =>
					n[i] = 5;
					res.value = specialkey::DELETE;
				case 'P' =>
					if (ss3) {
						res.value = 1: functionkey;
					} else {
						res.value = specialkey::DELETE;
					};
				case 'Q' =>
					res.value = 2: functionkey;
				case 'R' =>
					res.value = 3: functionkey;
				case 'S' =>
					res.value = 4: functionkey;
				case 'Z' =>
					res.value = '\t';
					res.mods = modflag::SHIFT;
					break;
				case =>
					abort();
				};
				if (16 < n[i]) {
					xterm_trimbuf(term, dec.offs);
					return invalid;
				};
				res.mods = (n[i]: u8 - 1): modflag;
				break;
			case ';' =>
				r = match (xterm_nextrune(term, dec, eof)?) {
				case let r: rune =>
					yield r;
				case void =>
					if (eof) {
						xterm_trimbuf(term, dec.offs);
						return invalid;
					} else {
						return more;
					};
				case utf8::more =>
					return more;
				};
			case =>
				xterm_trimbuf(term, dec.offs);
				return invalid;
			};
		};
	case utf8::more =>
		return more;
	};
};

const xterm_tilde_map_spkey: [_]specialkey = [
	specialkey::HOME,
	specialkey::INSERT,
	specialkey::DELETE,
	specialkey::END,
	specialkey::PGUP,
	specialkey::PGDN,
	specialkey::HOME,
	specialkey::END,
];

fn xterm_decode_key_tilde(n: [3]u32, i: size) (key | invalid) = {
	if (n[0] == 27) {
		if (i < 2) {
			return '\x1b';
		};
		return n[2]: rune;
	};
	if (n[0] <= 8) {
		return xterm_tilde_map_spkey[n[0] - 1];
	};
	if (n[0] <= 34) {
		return (n[0] - 10): functionkey;
	};
	return invalid;
};

// helper function to turn utf8::more into invalid, and trim the buffer
fn xterm_nextrune(
	term: *xterm,
	d: *utf8::decoder,
	eof: bool,
) (void | rune | utf8::more | invalid) = {
	match (utf8::next(d)) {
	case let r: rune =>
		return r;
	case void =>
		return void;
	case utf8::more =>
		if (eof) {
			xterm_trimbuf(term, d.offs);
			return invalid;
		} else {
			return utf8::more;
		};
	case utf8::invalid =>
		xterm_trimbuf(term, d.offs);
		return invalid;
	};
};

fn xterm_trimbuf(term: *xterm, pos: size) void = {
	static delete(term.buf[..][..pos]);
	term.end -= pos;
};

fn xterm_poll(term: *xterm, block: bool) (size | io::EOF | error) = {
	const polls = [
		poll::pollfd {
			fd = os::stdin_file,
			events = poll::event::POLLIN | poll::event::POLLHUP,
			...
		},
	];

	const timeout = if (block) poll::INDEF else poll::NONBLOCK;
	if (poll::poll(polls, timeout)? == 0) {
		return 0z;
	};

	match (io::read(os::stdin_file, term.buf[term.end..])?) {
	case io::EOF =>
		return io::EOF;
	case let n: size =>
		term.end += n;
		return n;
	};
};

fn xterm_getpen(term: *term) pen = {