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
type _EscapeState is
( _EscapeNone
| _EscapeStart
| _EscapeSS3
| _EscapeCSI
| _EscapeMod
)
class _TermResizeNotify is SignalNotify
let _term: ANSITerm tag
new create(term: ANSITerm tag) =>
_term = term
fun apply(times: U32): Bool =>
_term.size()
true
primitive _TIOCGWINSZ
fun apply(): ULong =>
ifdef linux then
21523
elseif osx or bsd then
1074295912
else
0
end
actor ANSITerm
"""
Handles ANSI escape codes from stdin.
"""
let _timers: Timers
var _timer: (Timer tag | None) = None
let _notify: ANSINotify
let _source: DisposableActor
var _escape: _EscapeState = _EscapeNone
var _esc_num: U8 = 0
var _esc_mod: U8 = 0
embed _esc_buf: Array[U8] = Array[U8]
var _closed: Bool = false
new create(
notify: ANSINotify iso,
source: DisposableActor,
timers: Timers = Timers)
=>
"""
Create a new ANSI term.
"""
_timers = timers
_notify = consume notify
_source = source
ifdef not windows then
SignalHandler(recover _TermResizeNotify(this) end, Sig.winch())
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 =>
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
| _EscapeSS3 =>
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
| _EscapeCSI =>
match c
| 'A' => _up()
| 'B' => _down()
| 'C' => _right()
| 'D' => _left()
| 'H' => _home()
| 'F' => _end()
| '~' => _keypad()
| ';' =>
_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
| _EscapeMod =>
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
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: ANSITerm = 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
be prompt(value: String) =>
"""
Pass a prompt along to the notifier.
"""
_notify.prompt(this, value)
be size() =>
_size()
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 _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