Decode raw terminal bytes into key events.
A state machine rather than a lookup, because escape sequences arrive
in whatever chunks the terminal and the kernel decide on: ESC [ A
may come as one read or three. Anything that ends mid-sequence is
kept in pending and completed by the next call.
A lone ESC – the user pressing Escape, as opposed to the start of
a sequence – is indistinguishable from a truncated sequence until
more bytes arrive or do not. It is held in pending, and reported as
KEY_ESC on the next call if nothing followed it. That is one read’s
worth of latency on the Escape key and no ambiguity, which is the
trade every terminal program makes.
Multi-byte UTF-8 is passed through as individual KEY_CHAR bytes; a
caller that wants code points reassembles them.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(pending_t), | intent(inout) | :: | pending |
Carried partial sequence; pass the same one every call. |
||
| character(len=*), | intent(in) | :: | bytes |
Bytes just read from the terminal. May be empty, which is how a
caller flushes a held |
||
| type(key_event_t), | intent(out) | :: | events(:) |
Decoded events, filled from 1. |
||
| integer(kind=default_int), | intent(out) | :: | n_events |
How many entries of |
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| logical, | private | :: | complete | ||||
| integer(kind=default_int), | private | :: | consumed | ||||
| type(key_event_t), | private | :: | event | ||||
| integer(kind=default_int), | private | :: | i | ||||
| integer(kind=default_int), | private | :: | n | ||||
| integer(kind=default_int), | private | :: | used | ||||
| character(len=PENDING_CAPACITY), | private | :: | work |
pure subroutine decode_keys(pending, bytes, events, n_events) !! Decode raw terminal bytes into key events. !! !! A state machine rather than a lookup, because escape sequences arrive !! in whatever chunks the terminal and the kernel decide on: `ESC [ A` !! may come as one read or three. Anything that ends mid-sequence is !! kept in `pending` and completed by the next call. !! !! A lone `ESC` -- the user pressing Escape, as opposed to the start of !! a sequence -- is indistinguishable from a truncated sequence until !! more bytes arrive or do not. It is held in `pending`, and reported as !! `KEY_ESC` on the next call if nothing followed it. That is one read's !! worth of latency on the Escape key and no ambiguity, which is the !! trade every terminal program makes. !! !! Multi-byte UTF-8 is passed through as individual `KEY_CHAR` bytes; a !! caller that wants code points reassembles them. type(pending_t), intent(inout) :: pending !! Carried partial sequence; pass the same one every call. character(len=*), intent(in) :: bytes !! Bytes just read from the terminal. May be empty, which is how a !! caller flushes a held `ESC`. type(key_event_t), intent(out) :: events(:) !! Decoded events, filled from 1. integer(default_int), intent(out) :: n_events !! How many entries of `events` were filled. character(len=PENDING_CAPACITY + 1) :: work integer(default_int) :: used, i, n, consumed type(key_event_t) :: event logical :: complete n_events = 0_default_int ! An empty read with a held ESC means the user really did press Escape: ! nothing followed it, so it cannot be the start of a sequence. if (len(bytes) == 0) then if (pending%length > 0_default_int) then if (pending%buffer(1:1) == ESC .and. pending%length == 1_default_int) then call emit(events, n_events, KEY_ESC, 0_int32) end if call pending%clear() end if return end if used = pending%length work = "" if (used > 0_default_int) work(1:used) = pending%buffer(1:used) n = len(bytes, kind=default_int) i = 1_default_int do while (i <= n) if (used == 0_default_int .and. bytes(i:i) /= ESC) then ! the common case: an ordinary byte, no sequence in progress call decode_single(iachar(bytes(i:i), default_int), event) call emit(events, n_events, event%code, event%char_code) i = i + 1_default_int cycle end if ! inside a sequence: accumulate and test for completeness if (used >= PENDING_CAPACITY) then ! Not a sequence we know, and too long to be one. Drop it rather ! than grow without bound, and resynchronise on the next byte. used = 0_default_int cycle end if used = used + 1_default_int work(used:used) = bytes(i:i) i = i + 1_default_int call match_sequence(work(1:used), complete, event, consumed) if (complete) then if (event%code /= KEY_NONE) call emit(events, n_events, event%code, event%char_code) used = used - consumed ! A leftover can only be the byte appended a moment ago, since ! `match_sequence` consumes everything else it decides on. Put it ! back in the input rather than leaving it in the buffer, so the ! next turn classifies it from scratch -- it may be an ESC opening ! a new sequence. if (used > 0_default_int) then i = i - used used = 0_default_int end if end if end do call pending%clear() if (used > 0_default_int) then pending%buffer(1:used) = work(1:used) pending%length = used end if end subroutine decode_keys