Terminal
use "time"
use "signals"
use @ioctl[I32](fx: I32, cmd: ULong, ...) if posix
struct _TermSize
var row: U16 = 0
var col: U16 = 0
var xpixel: U16 = 0
var ypixel: U16 = 0
primitive _EscapeNone
primitive _EscapeStart
primitive _EscapeSS3
primitive _EscapeCSI
primitive _EscapeMod
primitive _EscapeMouseStart
primitive _EscapeMouseX
primitive _EscapeMouseY
type _EscapeState is
( _EscapeNone
| _EscapeStart
| _EscapeSS3
| _EscapeCSI
| _EscapeMod
| _EscapeMouseStart
| _EscapeMouseX
| _EscapeMouseY
)
class _TermResizeNotify is SignalNotify
let _term: Terminal tag
new create(term: Terminal tag) =>
_term = term
fun apply(times: U32): Bool =>
_term.size()
true
class _TermSigKeyNotify is SignalNotify
let _term: Terminal tag
let _input: U8 val
new create(term: Terminal tag, input: U8 val) =>
_term = term
_input = input
fun apply(times: U32): Bool =>
_term.input(_input)
true
primitive _TIOCGWINSZ
fun apply(): ULong =>
ifdef linux then
21523
elseif osx or bsd then
1074295912
else
0
end
actor Terminal
"""
Handles terminal escape codes from stdin.
"""
let _options: TermOptions val
let _timers: Timers
var _timer: (Timer tag | None) = None
let _notify: TerminalNotify
let _source: DisposableActor
var _escape: _EscapeState = _EscapeNone
var _esc_num: U8 = 0
var _esc_mod: U8 = 0
var _esc_mouse_x: U32 = 0
var _esc_mouse_y: U32 = 0
embed _esc_buf: Array[U8] = Array[U8]
var _closed: Bool = false
new create(
notify: TerminalNotify iso,
source: DisposableActor,
timers: Timers = Timers,
options: TermOptions val = TermOptions) =>
"""
Create a new ANSI term.
"""
_timers = timers
_notify = consume notify
_source = source
_options = options
ifdef not windows then
SignalHandler(recover _TermResizeNotify(this) end, Sig.winch())
end
// Catch and send Ctrl-C (3) and Ctrl-Z (26) as inputs
if _options.catch_ctrl_C then
SignalHandler(recover _TermSigKeyNotify(this, Key.ctrl_C()) end, Sig.int())
end
if _options.catch_ctrl_Z then
SignalHandler(recover _TermSigKeyNotify(this, Key.ctrl_Z()) end, Sig.tstp())
end
_size()
be apply(data: Array[U8] iso) =>
"""
Receives input from stdin.
"""
if _closed then
return
end
for c in (consume data).values() do
match _escape
| _EscapeNone =>
if c == 0x1B then
_escape = _EscapeStart
_esc_buf.push(0x1B)
else
_notify(this, c)
end
| _EscapeStart => _in_escape_start(c)
| _EscapeSS3 => _in_escape_SS3(c)
| _EscapeCSI => _in_escape_CSI(c)
| _EscapeMod => _in_escape_modifier(c)
| _EscapeMouseStart => _in_escape_mouse_start(c)
| _EscapeMouseX => _in_escape_mouse_X(c)
| _EscapeMouseY => _in_escape_mouse_Y(c)
end
end
// If we are in the middle of an escape sequence, set a timer for 25 ms.
// If it fires, we send the escape sequence as if it was normal data.
if _escape isnt _EscapeNone then
if _timer isnt None then
try _timers.cancel(_timer as Timer tag) end
end
let t = recover
object is TimerNotify
let term: Terminal = this
fun ref apply(timer: Timer, count: U64): Bool =>
term._timeout()
false
end
end
let timer = Timer(consume t, 25000000)
_timer = timer
_timers(consume timer)
end
fun ref _in_escape_start(c: U8) =>
match c
| 'b' => // alt-left
_esc_mod = 3
_left()
| 'f' => // alt-right
_esc_mod = 3
_right()
| 'O' =>
_escape = _EscapeSS3
_esc_buf.push(c)
| '[' =>
_escape = _EscapeCSI
_esc_buf.push(c)
else
_esc_flush()
end
fun ref _in_escape_SS3(c: U8) =>
match c
| 'A' => _up()
| 'B' => _down()
| 'C' => _right()
| 'D' => _left()
| 'H' => _home()
| 'F' => _end()
| 'P' => _fn_key(1)
| 'Q' => _fn_key(2)
| 'R' => _fn_key(3)
| 'S' => _fn_key(4)
else
_esc_flush()
end
fun ref _in_escape_CSI(c: U8) =>
match c
| 'A' => _up()
| 'B' => _down()
| 'C' => _right()
| 'D' => _left()
| 'H' => _home()
| 'F' => _end()
| '~' => _keypad()
| '<' =>
_escape = _EscapeMouseStart
| ';' =>
_escape = _EscapeMod
| if (c >= '0') and (c <= '9') =>
// Escape number.
_esc_num = (_esc_num * 10) + (c - '0')
_esc_buf.push(c)
else
_esc_flush()
end
fun ref _in_escape_modifier(c: U8) =>
match c
| 'A' => _up()
| 'B' => _down()
| 'C' => _right()
| 'D' => _left()
| 'H' => _home()
| 'F' => _end()
| '~' => _keypad()
| if (c >= '0') and (c <= '9') =>
// Escape modifier.
_esc_mod = (_esc_mod * 10) + (c - '0')
_esc_buf.push(c)
else
_esc_flush()
end
fun ref _in_escape_mouse_start(c: U8) =>
match c
| ';' =>
_escape = _EscapeMouseX
| if (c >= '0') and (c <= '9') =>
// Escape number.
_esc_num = (_esc_num * 10) + (c - '0')
_esc_buf.push(c)
else
_esc_flush()
end
fun ref _in_escape_mouse_X(c: U8) =>
match c
| ';' =>
_escape = _EscapeMouseY
| if (c >= '0') and (c <= '9') =>
// mouse x coordinate
_esc_mouse_x = (_esc_mouse_x * 10) + (c - '0').u32()
_esc_buf.push(c)
else
_esc_flush()
end
fun ref _in_escape_mouse_Y(c: U8) =>
match c
| 'M' =>
match (_esc_num and 0b11100000)
| 0 => _mouse_press()
| 32 => if (_esc_num and 0b11) == 0b11 then _mouse_move() else _mouse_drag() end
| 64 => _mouse_wheel(where dir=(_esc_num and 1))
else
_esc_flush()
end
| 'm' =>
if (_esc_num and 0b11100000) == 0 then
_mouse_release()
else
_esc_flush()
end
| if (c >= '0') and (c <= '9') =>
// mouse y coordinate
_esc_mouse_y = (_esc_mouse_y * 10) + (c - '0').u32()
_esc_buf.push(c)
else
_esc_flush()
end
be prompt(value: String) =>
"""
Pass a prompt along to the notifier.
"""
_notify.prompt(this, value)
be size() =>
_size()
be input(input': U8 val) =>
"""
Pass the provided input to the notifier.
"""
_notify(this, input')
fun ref _size() =>
"""
Pass the window size to the notifier.
"""
let ws: _TermSize = _TermSize
ifdef posix then
@ioctl(0, _TIOCGWINSZ(), ws) // do error handling
_notify.size(ws.row, ws.col)
end
be dispose() =>
"""
Stop accepting input, inform the notifier we have closed, and dispose of
our source.
"""
if not _closed then
_esc_clear()
_notify.closed()
_source.dispose()
_closed = true
end
be _timeout() =>
"""
Our timer since receiving an ESC has expired. Send the buffered data as if
it was not an escape sequence.
"""
_timer = None
_esc_flush()
fun ref _mouse_button() : MouseButton =>
match (_esc_num and 0b00000011)
| 0b00 => LeftMouseButton
| 0b01 => MiddleMouseButton
| 0b10 => RightMouseButton
else
UnknownMouseButton
end
fun ref _mouse_wheel(dir: U8) =>
(let ctrl, let alt, let shift) = _mouse_kbd_mod()
_notify.mouse_wheel(
if dir == 0 then ScrollDown else ScrollUp end,
ctrl, alt, shift, _esc_mouse_x, _esc_mouse_y)
_esc_clear()
fun ref _mouse_drag() =>
(let ctrl, let alt, let shift) = _mouse_kbd_mod()
_notify.mouse_drag(_mouse_button(), ctrl, alt, shift, _esc_mouse_x, _esc_mouse_y)
_esc_clear()
fun ref _mouse_move() =>
(let ctrl, let alt, let shift) = _mouse_kbd_mod()
_notify.mouse_move(ctrl, alt, shift, _esc_mouse_x, _esc_mouse_y)
_esc_clear()
fun ref _mouse_release() =>
(let ctrl, let alt, let shift) = _mouse_kbd_mod()
_notify.mouse_release(_mouse_button(), ctrl, alt, shift, _esc_mouse_x, _esc_mouse_y)
_esc_clear()
fun ref _mouse_press() =>
(let ctrl, let alt, let shift) = _mouse_kbd_mod()
_notify.mouse_press(_mouse_button(), ctrl, alt, shift, _esc_mouse_x, _esc_mouse_y)
_esc_clear()
fun ref _mouse_kbd_mod(): (Bool, Bool, Bool) =>
/*
* Map the modifier bits in the mouse input code (_esc_num) to
* a tuple of modifier booleans
*/
match (_esc_num and 0b00011100)
// ctrl alt shift
| 0b00000100 => (false, false, true)
| 0b00001000 => (false, true, false)
| 0b00010000 => (true, false, false)
| 0b00001100 => (false, true, true)
| 0b00010100 => (true, false, true)
| 0b00011000 => (true, true, false)
| 0b00011100 => (true, true, true)
else (false, false, false)
end
fun ref _mod(): (Bool, Bool, Bool) =>
"""
Set the modifier bools.
"""
let r = match _esc_mod
| 2 => (false, false, true)
| 3 => (false, true, false)
| 4 => (false, true, true)
| 5 => (true, false, false)
| 6 => (true, false, true)
| 7 => (true, true, false)
| 8 => (true, true, true)
else (false, false, false)
end
_esc_mod = 0
r
fun ref _keypad() =>
"""
An extended key.
"""
match _esc_num
| 1 => _home()
| 2 => _insert()
| 3 => _delete()
| 4 => _end()
| 5 => _page_up()
| 6 => _page_down()
| 11 => _fn_key(1)
| 12 => _fn_key(2)
| 13 => _fn_key(3)
| 14 => _fn_key(4)
| 15 => _fn_key(5)
| 17 => _fn_key(6)
| 18 => _fn_key(7)
| 19 => _fn_key(8)
| 20 => _fn_key(9)
| 21 => _fn_key(10)
| 23 => _fn_key(11)
| 24 => _fn_key(12)
| 25 => _fn_key(13)
| 26 => _fn_key(14)
| 28 => _fn_key(15)
| 29 => _fn_key(16)
| 31 => _fn_key(17)
| 32 => _fn_key(18)
| 33 => _fn_key(19)
| 34 => _fn_key(20)
end
fun ref _up() =>
"""
Up arrow.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.up(ctrl, alt, shift)
_esc_clear()
fun ref _down() =>
"""
Down arrow.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.down(ctrl, alt, shift)
_esc_clear()
fun ref _left() =>
"""
Left arrow.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.left(ctrl, alt, shift)
_esc_clear()
fun ref _right() =>
"""
Right arrow.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.right(ctrl, alt, shift)
_esc_clear()
fun ref _delete() =>
"""
Delete key.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.delete(ctrl, alt, shift)
_esc_clear()
fun ref _insert() =>
"""
Insert key.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.insert(ctrl, alt, shift)
_esc_clear()
fun ref _home() =>
"""
Home key.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.home(ctrl, alt, shift)
_esc_clear()
fun ref _end() =>
"""
End key.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.end_key(ctrl, alt, shift)
_esc_clear()
fun ref _page_up() =>
"""
Page up key.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.page_up(ctrl, alt, shift)
_esc_clear()
fun ref _page_down() =>
"""
Page down key.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.page_down(ctrl, alt, shift)
_esc_clear()
fun ref _fn_key(i: U8) =>
"""
Function key.
"""
(let ctrl, let alt, let shift) = _mod()
_notify.fn_key(i, ctrl, alt, shift)
_esc_clear()
fun ref _esc_flush() =>
"""
Pass a partial or unrecognised escape sequence to the notifier.
"""
for c in _esc_buf.values() do
_notify(this, c)
end
_esc_clear()
fun ref _esc_clear() =>
"""
Clear the escape state.
"""
if _timer isnt None then
try _timers.cancel(_timer as Timer tag) end
_timer = None
end
_escape = _EscapeNone
_esc_buf.clear()
_esc_num = 0
_esc_mod = 0
_esc_mouse_x = 0
_esc_mouse_y = 0