text editor inspired vim and yi
import ViE.State
import ViE.Types

namespace ViE.Key

def flushEscSequence (seq : String) : List Key :=
  seq.toList.map (fun ch => if ch == '\x1b' then Key.esc else Key.char ch)

def isAsciiInRange (ch : Char) (lo hi : Nat) : Bool :=
  let n := ch.toNat
  lo <= n && n <= hi

def isCsiParamChar (ch : Char) : Bool :=
  ch.isDigit || ch == ';' || ch == '?' || ch == ':' || ch == '<' || ch == '=' || ch == '>'

def isCsiFinalChar (ch : Char) : Bool :=
  isAsciiInRange ch 0x40 0x7e

def decodeCsiFinal (ch : Char) : Key :=
  match ch with
  | 'A' => Key.up
  | 'B' => Key.down
  | 'C' => Key.right
  | 'D' => Key.left
  | 'H' => Key.unknown 'H'
  | 'F' => Key.unknown 'F'
  | _ => Key.unknown ch

def withPending (state : EditorState) (pending : String) (currentTime : Nat) : EditorState :=
  { state with inputState := { state.inputState with pendingKeys := pending, lastInputTime := currentTime } }

def clearPending (state : EditorState) : EditorState :=
  { state with inputState := { state.inputState with pendingKeys := "" } }

/-- Parse a character into a list of keys, handling escape sequences statefully. -/
def parseKey (state : EditorState) (c : Char) (currentTime : Nat) : (EditorState × List Key) :=
  if state.inputState.pendingKeys.length > 0 then
    let pending := state.inputState.pendingKeys.push c
    match pending with
    | "\x1b" =>
      (withPending state pending currentTime, [])
    | "\x1b[" =>
      (withPending state pending currentTime, [])
    | _ =>
      match pending.toList with
      | ['\x1b', ch] =>
        if ch == '[' then
          (withPending state pending currentTime, [])
        else
          (clearPending state, [Key.alt ch])
      | _ =>
        if pending.startsWith "\x1b[" then
          let chars := pending.toList
          let last := chars.getLastD '\x00'
          if isCsiFinalChar last then
            (clearPending state, [decodeCsiFinal last])
          else if isCsiParamChar last && pending.length <= 12 then
            (withPending state pending currentTime, [])
          else if pending.length > 12 then
            (clearPending state, flushEscSequence pending)
          else
            (clearPending state, flushEscSequence pending)
        else
          (clearPending state, flushEscSequence pending)
  else
    if c == '\x1b' then
       (withPending state "\x1b" currentTime, [])
    else
       let k := match c with
        | '\x01' => Key.ctrl 'a'
        | '\x02' => Key.ctrl 'b'
        | '\x03' => Key.ctrl 'c'
        | '\x04' => Key.ctrl 'd'
        | '\x05' => Key.ctrl 'e'
        | '\x06' => Key.ctrl 'f'
        | '\x07' => Key.ctrl 'g'
        | '\x08' => Key.ctrl 'h'
        | '\x09' => Key.char '\t'
        | '\x0b' => Key.ctrl 'k'
        | '\x0c' => Key.ctrl 'l'
        | '\x0e' => Key.ctrl 'n'
        | '\x0f' => Key.ctrl 'o'
        | '\x10' => Key.ctrl 'p'
        | '\x11' => Key.ctrl 'q'
        | '\x12' => Key.ctrl 'r'
        | '\x13' => Key.ctrl 's'
        | '\x14' => Key.ctrl 't'
        | '\x15' => Key.ctrl 'u'
        | '\x16' => Key.ctrl 'v'
        | '\x17' => Key.ctrl 'w'
        | '\x18' => Key.ctrl 'x'
        | '\x19' => Key.ctrl 'y'
        | '\x1a' => Key.ctrl 'z'
        | '\x7f' => Key.backspace
        | '\x0a' | '\x0d' => Key.enter
        | _ => Key.char c
       (state, [k])

/-- Check for escape sequence timeout. -/
def checkTimeout (state : EditorState) (currentTime : Nat) : (EditorState × List Key) :=
  if state.inputState.pendingKeys.length > 0 then
    if currentTime - state.inputState.lastInputTime > 50 then
       let keys := flushEscSequence state.inputState.pendingKeys
       (clearPending state, keys)
    else
       (state, [])
  else
    (state, [])

end ViE.Key