decode_keys Subroutine

public 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.

Arguments

Type IntentOptional 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 ESC.

type(key_event_t), intent(out) :: events(:)

Decoded events, filled from 1.

integer(kind=default_int), intent(out) :: n_events

How many entries of events were filled.


Calls

proc~~decode_keys~~CallsGraph proc~decode_keys decode_keys proc~decode_single decode_single proc~decode_keys->proc~decode_single proc~emit emit proc~decode_keys->proc~emit proc~match_sequence match_sequence proc~decode_keys->proc~match_sequence proc~pending_clear pending_t%pending_clear proc~decode_keys->proc~pending_clear

Variables

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

Source Code

   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