diff --git a/core/shared/src/main/scala/terminus/KeyMappings.scala b/core/shared/src/main/scala/terminus/KeyMappings.scala new file mode 100644 index 0000000..232f90b --- /dev/null +++ b/core/shared/src/main/scala/terminus/KeyMappings.scala @@ -0,0 +1,279 @@ +/* + * Copyright 2024 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package terminus + +import terminus.effect.Ascii + +class KeySequence(val root: Key, val sequences: Map[String, Key]): + /** A set of subsequences, used to determine whether a sequence of Chars has + * partially matched a sequence + */ + val subSequences: Set[String] = sequences.keySet.flatMap(s => + if s.length <= 2 then Set.empty + else (2 until s.length).map(s.substring(0, _)).toSet + ) + +object KeyMappings: + lazy val default: Map[Char, Key | KeySequence] = Map( + ' ' -> Key.space, + Ascii.NUL -> Key.controlAt, // Control-At (Also for Ctrl-Space) + Ascii.SOH -> Key.controlA, // Control-A (home) + Ascii.STX -> Key.controlB, // Control-B (emacs cursor left) + Ascii.ETX -> Key.controlC, // Control-C (interrupt) + Ascii.EOT -> Key.controlD, // Control-D (exit) + Ascii.ENQ -> Key.controlE, // Control-E (end) + Ascii.ACK -> Key.controlF, // Control-F (cursor forward) + Ascii.BEL -> Key.controlG, // Control-G + Ascii.BS -> Key.backspace, // Control-H (8) (Identical to '\b') + Ascii.HT -> Key.tab, // Control-I (9) (Identical to '\t') + Ascii.LF -> Key.newLine, // Control-J (10) (Identical to '\n') + Ascii.VT -> Key.controlK, // Control-K (delete until end of line; vertical tab) + Ascii.FF -> Key.controlL, // Control-L (clear; form feed) + Ascii.CR -> Key.enter, + Ascii.SO -> Key.controlN, // Control-N (14) (history forward) + Ascii.SI -> Key.controlO, // Control-O (15) + Ascii.DLE -> Key.controlP, // Control-P (16) (history back) + Ascii.DC1 -> Key.controlQ, // Control-Q + Ascii.DC2 -> Key.controlR, // Control-R (18) (reverse search) + Ascii.DC3 -> Key.controlS, // Control-S (19) (forward search) + Ascii.DC4 -> Key.controlT, // Control-T + Ascii.NAK -> Key.controlU, // Control-U + Ascii.SYN -> Key.controlV, // Control-V + Ascii.ETB -> Key.controlW, // Control-W + Ascii.CAN -> Key.controlX, // Control-X + Ascii.EM -> Key.controlY, // Control-Y (25) + Ascii.SUB -> Key.controlZ, // Control-Z + + '\u009b' -> Key.shiftEscape, + '\u001c' -> Key.controlBackslash, + '\u001d' -> Key.controlSquareClose, + '\u001e' -> Key.controlCircumflex, + '\u001f' -> Key.controlUnderscore, + '\u007f' -> Key.backspace, + Ascii.ESC -> KeySequence(Key.escape, escapeSequences) + ) + + import Ascii.ESC + + // Escape sequences + private lazy val escapeSequences: Map[String, Key] = Map( + // Simple ESC sequences + s"$ESC${Ascii.HT}" -> Key.backTab, + s"${ESC}b" -> Key.controlLeft, + s"${ESC}f" -> Key.controlRight, + s"$ESC§" -> Key('§'), + s"${ESC}1" -> Key('¡'), + s"${ESC}2" -> Key('™'), + s"${ESC}3" -> Key('£'), + s"${ESC}4" -> Key('¢'), + s"${ESC}5" -> Key('∞'), + s"${ESC}6" -> Key('§'), + s"${ESC}7" -> Key('¶'), + s"${ESC}8" -> Key('•'), + s"${ESC}9" -> Key('ª'), + s"${ESC}0" -> Key('º'), + s"$ESC-" -> Key('–'), + s"$ESC=" -> Key('≠'), + + // Normal mode - basic sequences + s"$ESC[a" -> Key.shiftUp, + s"$ESC[b" -> Key.shiftDown, + s"$ESC[c" -> Key.shiftRight, + s"$ESC[d" -> Key.shiftLeft, + s"$ESC[A" -> Key.up, + s"$ESC[B" -> Key.down, + s"$ESC[C" -> Key.right, + s"$ESC[D" -> Key.left, + s"$ESC[F" -> Key.`end`, + s"$ESC[H" -> Key.home, + s"$ESC[Z" -> Key.backTab, + s"$ESC[~" -> Key.backTab, + + // Function keys and control combinations + s"$ESC[11^" -> Key.controlF1, + s"$ESC[11~" -> Key.f1, + s"$ESC[12^" -> Key.controlF2, + s"$ESC[12~" -> Key.f2, + s"$ESC[13^" -> Key.controlF3, + s"$ESC[13~" -> Key.f3, + s"$ESC[14^" -> Key.controlF4, + s"$ESC[14~" -> Key.f4, + s"$ESC[15^" -> Key.controlF5, + s"$ESC[15~" -> Key.f5, + s"$ESC[15;2~" -> Key.f17, + s"$ESC[15;5~" -> Key.controlF5, + s"$ESC[15;6~" -> Key.controlF17, + s"$ESC[17^" -> Key.controlF6, + s"$ESC[17~" -> Key.f6, + s"$ESC[17;2~" -> Key.f18, + s"$ESC[17;5~" -> Key.controlF6, + s"$ESC[17;6~" -> Key.controlF18, + s"$ESC[18^" -> Key.controlF7, + s"$ESC[18~" -> Key.f7, + s"$ESC[18;2~" -> Key.f19, + s"$ESC[18;5~" -> Key.controlF7, + s"$ESC[18;6~" -> Key.controlF19, + s"$ESC[19^" -> Key.controlF8, + s"$ESC[19~" -> Key.f8, + s"$ESC[19;2~" -> Key.f20, + s"$ESC[19;5~" -> Key.controlF8, + s"$ESC[19;6~" -> Key.controlF20, + + // Special key combinations + s"$ESC[1~" -> Key.home, + s"$ESC[1;2A" -> Key.shiftUp, + s"$ESC[1;2B" -> Key.shiftDown, + s"$ESC[1;2C" -> Key.shiftRight, + s"$ESC[1;2D" -> Key.shiftLeft, + s"$ESC[1;2F" -> Key.shiftEnd, + s"$ESC[1;2H" -> Key.shiftHome, + s"$ESC[1;2P" -> Key.f13, + s"$ESC[1;2Q" -> Key.f14, + s"$ESC[1;2R" -> Key.f15, + s"$ESC[1;2S" -> Key.f16, + s"$ESC[1;5A" -> Key.controlUp, + s"$ESC[1;5B" -> Key.controlDown, + s"$ESC[1;5C" -> Key.controlRight, + s"$ESC[1;5D" -> Key.controlLeft, + s"$ESC[1;5F" -> Key.controlEnd, + s"$ESC[1;5H" -> Key.controlHome, + s"$ESC[1;5P" -> Key.controlF1, + s"$ESC[1;5Q" -> Key.controlF2, + s"$ESC[1;5R" -> Key.controlF3, + s"$ESC[1;5S" -> Key.controlF4, + s"$ESC[1;6A" -> Key.controlShiftUp, + s"$ESC[1;6B" -> Key.controlShiftDown, + s"$ESC[1;6C" -> Key.controlShiftRight, + s"$ESC[1;6D" -> Key.controlShiftLeft, + s"$ESC[1;6F" -> Key.controlShiftEnd, + s"$ESC[1;6H" -> Key.controlShiftHome, + s"$ESC[1;6P" -> Key.controlF13, + s"$ESC[1;6Q" -> Key.controlF14, + s"$ESC[1;6R" -> Key.controlF15, + s"$ESC[1;6S" -> Key.controlF16, + + // Function keys 9-24 + s"$ESC[20^" -> Key.controlF9, + s"$ESC[20~" -> Key.f9, + s"$ESC[20;2~" -> Key.f21, + s"$ESC[20;5~" -> Key.controlF9, + s"$ESC[20;6~" -> Key.controlF21, + s"$ESC[21^" -> Key.controlF10, + s"$ESC[21~" -> Key.f10, + s"$ESC[21;2~" -> Key.f22, + s"$ESC[21;5~" -> Key.controlF10, + s"$ESC[21;6~" -> Key.controlF22, + s"$ESC[23@" -> Key.controlF21, + s"$ESC[23^" -> Key.controlF11, + s"$ESC[23$$" -> Key.f23, + s"$ESC[23~" -> Key.f11, + s"$ESC[23;2~" -> Key.f23, + s"$ESC[23;5~" -> Key.controlF11, + s"$ESC[23;6~" -> Key.controlF23, + s"$ESC[24@" -> Key.controlF22, + s"$ESC[24^" -> Key.controlF12, + s"$ESC[24$$" -> Key.f24, + s"$ESC[24~" -> Key.f12, + s"$ESC[24;2~" -> Key.f24, + s"$ESC[24;5~" -> Key.controlF12, + s"$ESC[24;6~" -> Key.controlF24, + s"$ESC[25^" -> Key.controlF13, + s"$ESC[25~" -> Key.f13, + s"$ESC[26^" -> Key.controlF14, + s"$ESC[26~" -> Key.f14, + s"$ESC[28^" -> Key.controlF15, + s"$ESC[28~" -> Key.f15, + s"$ESC[29^" -> Key.controlF16, + s"$ESC[29~" -> Key.f16, + s"$ESC[31^" -> Key.controlF17, + s"$ESC[31~" -> Key.f17, + s"$ESC[32^" -> Key.controlF18, + s"$ESC[32~" -> Key.f18, + s"$ESC[33^" -> Key.controlF19, + s"$ESC[33~" -> Key.f19, + s"$ESC[34^" -> Key.controlF20, + s"$ESC[34~" -> Key.f20, + + // Insert, Delete, Page Up/Down + s"$ESC[2~" -> Key.insert, + s"$ESC[3^" -> Key.controlDelete, + s"$ESC[3$$" -> Key.shiftDelete, + s"$ESC[3~" -> Key.delete, + s"$ESC[3;2~" -> Key.shiftDelete, + s"$ESC[3;5~" -> Key.controlDelete, + s"$ESC[3;6~" -> Key.controlShiftDelete, + s"$ESC[4~" -> Key.`end`, + s"$ESC[5A" -> Key.controlUp, + s"$ESC[5B" -> Key.controlDown, + s"$ESC[5C" -> Key.controlRight, + s"$ESC[5D" -> Key.controlLeft, + s"$ESC[5^" -> Key.controlPageUp, + s"$ESC[5~" -> Key.pageUp, + s"$ESC[5;2~" -> Key.shiftPageUp, + s"$ESC[5;5~" -> Key.controlPageUp, + s"$ESC[5;6~" -> Key.controlShiftPageUp, + s"$ESC[6^" -> Key.controlPageDown, + s"$ESC[6~" -> Key.pageDown, + s"$ESC[6;2~" -> Key.shiftPageDown, + s"$ESC[6;5~" -> Key.controlPageDown, + s"$ESC[6;6~" -> Key.controlShiftPageDown, + s"$ESC[7^" -> Key.controlEnd, + s"$ESC[7$$" -> Key.shiftHome, + s"$ESC[7~" -> Key.home, + s"$ESC[8^" -> Key.controlHome, + s"$ESC[8$$" -> Key.shiftEnd, + s"$ESC[8~" -> Key.`end`, + + // Function key shortcuts + s"$ESC[[A" -> Key.f1, + s"$ESC[[B" -> Key.f2, + s"$ESC[[C" -> Key.f3, + s"$ESC[[D" -> Key.f4, + s"$ESC[[E" -> Key.f5, + + // Application mode + s"${ESC}Oa" -> Key.controlUp, + s"${ESC}Ob" -> Key.controlUp, + s"${ESC}Oc" -> Key.controlRight, + s"${ESC}Od" -> Key.controlLeft, + s"${ESC}Oj" -> Key('*'), + s"${ESC}Ok" -> Key('+'), + s"${ESC}Om" -> Key('-'), + s"${ESC}On" -> Key('.'), + s"${ESC}Oo" -> Key('/'), + s"${ESC}Op" -> Key('0'), + s"${ESC}Oq" -> Key('1'), + s"${ESC}Or" -> Key('2'), + s"${ESC}Os" -> Key('3'), + s"${ESC}Ot" -> Key('4'), + s"${ESC}Ou" -> Key('5'), + s"${ESC}Ov" -> Key('6'), + s"${ESC}Ow" -> Key('7'), + s"${ESC}Ox" -> Key('8'), + s"${ESC}Oy" -> Key('9'), + s"${ESC}OA" -> Key.up, + s"${ESC}OB" -> Key.down, + s"${ESC}OC" -> Key.right, + s"${ESC}OD" -> Key.left, + s"${ESC}OF" -> Key.`end`, + s"${ESC}OH" -> Key.home, + s"${ESC}OM" -> Key.enter, + s"${ESC}OP" -> Key.f1, + s"${ESC}OQ" -> Key.f2, + s"${ESC}OR" -> Key.f3, + s"${ESC}OS" -> Key.f4 + ) diff --git a/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala b/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala index 3a6423a..97d7017 100644 --- a/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala +++ b/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala @@ -16,10 +16,8 @@ package terminus.effect -import terminus.Eof -import terminus.Key -import terminus.Timeout - +import scala.annotation.tailrec +import terminus.{Eof, Key, KeyMappings, KeySequence, Timeout} import scala.concurrent.duration.* /** An implementation of KeyReader that interprets the standard terminal escape @@ -30,688 +28,33 @@ import scala.concurrent.duration.* trait TerminalKeyReader(timeout: Duration = 100.millis) extends KeyReader { self: NonBlockingReader & Reader => - /** Read a character. If it matches char return key otherwise construct an - * unknown key using prefix as the path to reach this point. - */ - private inline def readMatchOrUnknown( - char: Char, - key: Key, - prefix: String - ): Eof | Key = - read() match { - case Eof => Eof - case ch if ch == char => key - case other: Char => Key.unknown(prefix :+ other) - } - // Some references on parsing terminal codes: // https://github.com/crossterm-rs/crossterm/blob/master/src/event/sys/unix/parse.rs // https://github.com/Textualize/textual/blob/main/src/textual/_ansi_sequences.py def readKey(): Eof | Key = - read() match { - case ' ' => Key.space - case Ascii.NUL => Key.controlAt // Control-At (Also for Ctrl-Space) - case Ascii.SOH => Key.controlA // Control-A (home) - case Ascii.STX => Key.controlB // Control-B (emacs cursor left) - case Ascii.ETX => Key.controlC // Control-C (interrupt) - case Ascii.EOT => Key.controlD // Control-D (exit) - case Ascii.ENQ => Key.controlE // Control-E (end) - case Ascii.ACK => Key.controlF // Control-F (cursor forward) - case Ascii.BEL => Key.controlG // Control-G - case Ascii.BS => Key.backspace // Control-H (8) (Identical to '\b') - case Ascii.HT => Key.tab // Control-I (9) (Identical to '\t') - case Ascii.LF => Key.newLine // Control-J (10) (Identical to '\n') - case Ascii.VT => - Key.controlK // Control-K (delete until end of line; vertical tab) - case Ascii.FF => Key.controlL // Control-L (clear; form feed) - case Ascii.CR => Key.enter - case Ascii.SO => Key.controlN // Control-N (14) (history forward) - case Ascii.SI => Key.controlO // Control-O (15) - case Ascii.DLE => Key.controlP // Control-P (16) (history back) - case Ascii.DC1 => Key.controlQ // Control-Q - case Ascii.DC2 => Key.controlR // Control-R (18) (reverse search) - case Ascii.DC3 => Key.controlS // Control-S (19) (forward search) - case Ascii.DC4 => Key.controlT // Control-T - case Ascii.NAK => Key.controlU // Control-U - case Ascii.SYN => Key.controlV // Control-V - case Ascii.ETB => Key.controlW // Control-W - case Ascii.CAN => Key.controlX // Control-X - case Ascii.EM => Key.controlY // Control-Y (25) - case Ascii.SUB => Key.controlZ // Control-Z - - case '\u009b' => Key.shiftEscape - case '\u001c' => Key.controlBackslash - case '\u001d' => Key.controlSquareClose - case '\u001e' => Key.controlCircumflex - case '\u001f' => Key.controlUnderscore - case '\u007f' => Key.backspace - case Ascii.ESC => - read(timeout) match { - case Eof => Key.escape - case Timeout => Key.escape - case Ascii.HT => Key.backTab - case 'b' => Key.controlLeft - case 'f' => Key.controlRight - // Normal mode - case '[' => - read() match { - case Eof => Eof - case 'a' => Key.shiftUp - case 'b' => Key.shiftDown - case 'c' => Key.shiftRight - case 'd' => Key.shiftLeft - case 'A' => Key.up - case 'B' => Key.down - case 'C' => Key.right - case 'D' => Key.left - case 'F' => Key.`end` - case 'H' => Key.home - case 'Z' => Key.backTab - case '1' => - read() match { - case Eof => Eof - case '1' => - read() match { - case Eof => Eof - case '^' => Key.controlF1 - case '~' => Key.f1 - case other => Key.unknown(s"${Ascii.ESC}[11${other}") - } - case '2' => - read() match { - case Eof => Eof - case '^' => Key.controlF2 - case '~' => Key.f2 - case other => Key.unknown(s"${Ascii.ESC}[12${other}") - } - case '3' => - read() match { - case Eof => Eof - case '^' => Key.controlF3 - case '~' => Key.f3 - case other => Key.unknown(s"${Ascii.ESC}[13${other}") - } - case '4' => - read() match { - case Eof => Eof - case '^' => Key.controlF4 - case '~' => Key.f4 - case other => Key.unknown(s"${Ascii.ESC}[14${other}") - } - case '5' => - read() match { - case Eof => Eof - case '^' => Key.controlF5 - case ';' => - read() match { - case Eof => Eof - case '2' => - read() match { - case Eof => Eof - case '~' => Key.f17 - case other => - Key.unknown(s"${Ascii.ESC}[15;2${other}") - } - case '5' => - read() match { - case Eof => Eof - case '~' => Key.controlF5 - case other => - Key.unknown(s"${Ascii.ESC}[15;5${other}") - } - case '6' => - read() match { - case Eof => Eof - case '~' => Key.controlF17 - case other => - Key.unknown(s"${Ascii.ESC}[15;6${other}") - } - case other => Key.unknown(s"${Ascii.ESC}[15;${other}") - } - case '~' => Key.f5 - case other => Key.unknown(s"${Ascii.ESC}[15${other}") - } - case '7' => - read() match { - case Eof => Eof - case '^' => Key.controlF6 - case ';' => - read() match { - case Eof => Eof - case '2' => - read() match { - case Eof => Eof - case '~' => Key.f18 - case other => - Key.unknown(s"${Ascii.ESC}[17;2${other}") - } - case '5' => - read() match { - case Eof => Eof - case '~' => Key.controlF6 - case other => - Key.unknown(s"${Ascii.ESC}[17;5${other}") - } - case '6' => - read() match { - case Eof => Eof - case '~' => Key.controlF18 - case other => - Key.unknown(s"${Ascii.ESC}[17;6${other}") - } - case other => Key.unknown(s"${Ascii.ESC}[17;${other}") - } - case '~' => Key.f6 - case other => Key.unknown(s"${Ascii.ESC}[17${other}") - } - case '8' => - read() match { - case Eof => Eof - case '^' => Key.controlF7 - case ';' => - read() match { - case Eof => Eof - case '2' => - read() match { - case Eof => Eof - case '~' => Key.f19 - case other => - Key.unknown(s"${Ascii.ESC}[18;2${other}") - } - case '5' => - read() match { - case Eof => Eof - case '~' => Key.controlF7 - case other => - Key.unknown(s"${Ascii.ESC}[18;5${other}") - } - case '6' => - read() match { - case Eof => Eof - case '~' => Key.controlF19 - case other => - Key.unknown(s"${Ascii.ESC}[18;6${other}") - } - case other => Key.unknown(s"${Ascii.ESC}[18;${other}") - } - case '~' => Key.f7 - case other => Key.unknown(s"${Ascii.ESC}[18${other}") - } - case '9' => - read() match { - case Eof => Eof - case '^' => Key.controlF8 - case ';' => - read() match { - case Eof => Eof - case '2' => - read() match { - case Eof => Eof - case '~' => Key.f20 - case other => - Key.unknown(s"${Ascii.ESC}[19;2${other}") - } - case '5' => - read() match { - case Eof => Eof - case '~' => Key.controlF8 - case other => - Key.unknown(s"${Ascii.ESC}[19;5${other}") - } - case '6' => - read() match { - case Eof => Eof - case '~' => Key.controlF20 - case other => - Key.unknown(s"${Ascii.ESC}[19;6${other}") - } - case other => Key.unknown(s"${Ascii.ESC}[19;${other}") - } - case '~' => Key.f8 - case other => Key.unknown(s"${Ascii.ESC}[19${other}") - } - case ';' => - read() match { - case Eof => Eof - case '2' => - read() match { - case Eof => Eof - case 'A' => Key.shiftUp - case 'B' => Key.shiftDown - case 'C' => Key.shiftRight - case 'D' => Key.shiftLeft - case 'F' => Key.shiftEnd - case 'H' => Key.shiftHome - case 'P' => Key.f13 - case 'Q' => Key.f14 - case 'R' => Key.f15 - case 'S' => Key.f16 - case other => Key.unknown(s"${Ascii.ESC}[1;2${other}") - } - case '5' => - read() match { - case Eof => Eof - case 'A' => Key.controlUp - case 'B' => Key.controlDown - case 'C' => Key.controlRight - case 'D' => Key.controlLeft - case 'F' => Key.controlEnd - case 'H' => Key.controlHome - case 'P' => Key.controlF1 - case 'Q' => Key.controlF2 - case 'R' => Key.controlF3 - case 'S' => Key.controlF4 - case other => Key.unknown(s"${Ascii.ESC}[1;5${other}") - } - case '6' => - read() match { - case Eof => Eof - case 'A' => Key.controlShiftUp - case 'B' => Key.controlShiftDown - case 'C' => Key.controlShiftRight - case 'D' => Key.controlShiftLeft - case 'F' => Key.controlShiftEnd - case 'H' => Key.controlShiftHome - case 'P' => Key.controlF13 - case 'Q' => Key.controlF14 - case 'R' => Key.controlF15 - case 'S' => Key.controlF16 - case other => Key.unknown(s"${Ascii.ESC}[1;6{other}") - } - case other => Key.unknown(s"${Ascii.ESC}[1;${other}") - } - case '~' => Key.home - case other => Key.unknown(s"${Ascii.ESC}[1${other}") - } - - case '2' => - read() match { - case Eof => Eof - case '0' => - read() match { - case Eof => Eof - case '^' => Key.controlF9 - case ';' => - read() match { - case Eof => Eof - case '2' => - read() match { - case Eof => Eof - case '~' => Key.f21 - case other => - Key.unknown(s"${Ascii.ESC}[20;2${other}") - } - case '5' => - read() match { - case Eof => Eof - case '~' => Key.controlF9 - case other => - Key.unknown(s"${Ascii.ESC}[20;5${other}") - } - case '6' => - read() match { - case Eof => Eof - case '~' => Key.controlF21 - case other => - Key.unknown(s"${Ascii.ESC}[20;6${other}") - } - case other => Key.unknown(s"${Ascii.ESC}[20;${other}") - } - case '~' => Key.f9 - case other => Key.unknown(s"${Ascii.ESC}[20${other}") - } - case '1' => - read() match { - case Eof => Eof - case '^' => Key.controlF10 - case ';' => - read() match { - case Eof => Eof - case '2' => - read() match { - case Eof => Eof - case '~' => Key.f22 - case other => - Key.unknown(s"${Ascii.ESC}[21;2${other}") - } - case '5' => - read() match { - case Eof => Eof - case '~' => Key.controlF10 - case other => - Key.unknown(s"${Ascii.ESC}[21;5${other}") - } - case '6' => - read() match { - case Eof => Eof - case '~' => Key.controlF22 - case other => - Key.unknown(s"${Ascii.ESC}[21;6${other}") - } - case other => Key.unknown(s"${Ascii.ESC}[21;${other}") - } - case '~' => Key.f10 - case other => Key.unknown(s"${Ascii.ESC}[21${other}") - } - case '3' => - read() match { - case Eof => Eof - case '@' => Key.controlF21 - case '^' => Key.controlF11 - case '$' => Key.f23 - case ';' => - read() match { - case Eof => Eof - case '2' => - read() match { - case Eof => Eof - case '~' => Key.f23 - case other => - Key.unknown(s"${Ascii.ESC}[23;2${other}") - } - case '5' => - read() match { - case Eof => Eof - case '~' => Key.controlF11 - case other => - Key.unknown(s"${Ascii.ESC}[23;5${other}") - } - case '6' => - read() match { - case Eof => Eof - case '~' => Key.controlF23 - case other => - Key.unknown(s"${Ascii.ESC}[23;6${other}") - } - case other => Key.unknown(s"${Ascii.ESC}[23;${other}") - } - case '~' => Key.f11 - case other => Key.unknown(s"${Ascii.ESC}[23${other}") - } - case '4' => - read() match { - case Eof => Eof - case '@' => Key.controlF22 - case '^' => Key.controlF12 - case '$' => Key.f24 - case ';' => - read() match { - case Eof => Eof - case '2' => - read() match { - case Eof => Eof - case '~' => Key.f24 - case other => - Key.unknown(s"${Ascii.ESC}[24;2${other}") - } - case '5' => - read() match { - case Eof => Eof - case '~' => Key.controlF12 - case other => - Key.unknown(s"${Ascii.ESC}[24;5${other}") - } - case '6' => - read() match { - case Eof => Eof - case '~' => Key.controlF24 - case other => - Key.unknown(s"${Ascii.ESC}[24;6${other}") - } - case other => Key.unknown(s"${Ascii.ESC}[24;${other}") - } - case '~' => Key.f12 - case other => Key.unknown(s"${Ascii.ESC}[24${other}") - } - case '5' => - read() match { - case Eof => Eof - case '^' => Key.controlF13 - case '~' => Key.f13 - case other => Key.unknown(s"${Ascii.ESC}[25${other}") - } - case '6' => - read() match { - case Eof => Eof - case '^' => Key.controlF14 - case '~' => Key.f14 - case other => Key.unknown(s"${Ascii.ESC}[26${other}") - } - case '8' => - read() match { - case Eof => Eof - case '^' => Key.controlF15 - case '~' => Key.f15 - case other => Key.unknown(s"${Ascii.ESC}[28${other}") - } - case '9' => - read() match { - case Eof => Eof - case '^' => Key.controlF16 - case '~' => Key.f16 - case other => Key.unknown(s"${Ascii.ESC}[29${other}") - } - case '~' => Key.insert - case other => Key.unknown(s"${Ascii.ESC}[2${other}") - } - - case '3' => - read() match { - case Eof => Eof - case '1' => - read() match { - case Eof => Eof - case '^' => Key.controlF17 - case '~' => Key.f17 - case other => Key.unknown(s"${Ascii.ESC}[31${other}") - } - case '2' => - read() match { - case Eof => Eof - case '^' => Key.controlF18 - case '~' => Key.f18 - case other => Key.unknown(s"${Ascii.ESC}[32${other}") - } - case '3' => - read() match { - case Eof => Eof - case '^' => Key.controlF19 - case '~' => Key.f19 - case other => Key.unknown(s"${Ascii.ESC}[33${other}") - } - case '4' => - read() match { - case Eof => Eof - case '^' => Key.controlF20 - case '~' => Key.f20 - case other => Key.unknown(s"${Ascii.ESC}[34${other}") - } - case '^' => Key.controlDelete - case '$' => Key.shiftDelete - case ';' => - read() match { - case Eof => Eof - case '2' => - readMatchOrUnknown( - '~', - Key.shiftDelete, - s"${Ascii.ESC}[3;2" - ) - case '5' => - readMatchOrUnknown( - '~', - Key.controlDelete, - s"${Ascii.ESC}[3;5" - ) - case '6' => - readMatchOrUnknown( - '~', - Key.controlShiftDelete, - s"${Ascii.ESC}[3;6" - ) - case other => Key.unknown(s"${Ascii.ESC}[3;${other}") - } - case '~' => Key.delete - case other => Key.unknown(s"${Ascii.ESC}[3${other}") - } - case '4' => - read() match { - case Eof => Eof - case '~' => Key.`end` - case other => Key.unknown(s"${Ascii.ESC}[4${other}") - } - case '5' => - read() match { - case Eof => Eof - case 'A' => Key.controlUp - case 'B' => Key.controlDown - case 'C' => Key.controlRight - case 'D' => Key.controlLeft - case '^' => Key.controlPageUp - case ';' => - read() match { - case Eof => Eof - case '2' => - readMatchOrUnknown( - '~', - Key.shiftPageUp, - s"${Ascii.ESC}[5;2" - ) - case '5' => - readMatchOrUnknown( - '~', - Key.controlPageUp, - s"${Ascii.ESC}[5;5" - ) - case '6' => - readMatchOrUnknown( - '~', - Key.controlShiftPageUp, - s"${Ascii.ESC}[5;6" - ) - case other => Key.unknown(s"${Ascii.ESC}[5;${other}") - } - case '~' => Key.pageUp - case other => Key.unknown(s"${Ascii.ESC}[5${other}") - } - case '6' => - read() match { - case Eof => Eof - case '^' => Key.controlPageDown - case ';' => - read() match { - case Eof => Eof - case '2' => - readMatchOrUnknown( - '~', - Key.shiftPageDown, - s"${Ascii.ESC}[6;2" - ) - case '5' => - readMatchOrUnknown( - '~', - Key.controlPageDown, - s"${Ascii.ESC}[6;5" - ) - case '6' => - readMatchOrUnknown( - '~', - Key.controlShiftPageDown, - s"${Ascii.ESC}[6;6" - ) - case other => Key.unknown(s"${Ascii.ESC}[6;${other}") - } - case '~' => Key.pageDown - case other => Key.unknown(s"${Ascii.ESC}[6${other}") - } - case '7' => - read() match { - case Eof => Eof - case '^' => Key.controlEnd - case '$' => Key.shiftHome - case '~' => Key.home - case other => Key.unknown(s"${Ascii.ESC}[7${other}") - } - case '8' => - read() match { - case Eof => Eof - case '^' => Key.controlHome - case '$' => Key.shiftEnd - case '~' => Key.`end` - case other => Key.unknown(s"${Ascii.ESC}[8${other}") - } - case '[' => - read() match { - case Eof => Eof - case 'A' => Key.f1 - case 'B' => Key.f2 - case 'C' => Key.f3 - case 'D' => Key.f4 - case 'E' => Key.f5 - case other: Char => Key.unknown(s"${Ascii.ESC}[[${other}") - } - case ';' => - read() match { - case Eof => Eof - case other => Key.unknown(s"${Ascii.ESC}[;{other}") - } - case '~' => Key.backTab - case other: Char => Key.unknown(s"${Ascii.ESC}[${other}") - } - // Application mode - case 'O' => - read() match { - case Eof => Eof - case 'a' => Key.controlUp - case 'b' => Key.controlUp - case 'c' => Key.controlRight - case 'd' => Key.controlLeft - case 'j' => Key('*') - case 'k' => Key('+') - case 'm' => Key('-') - case 'n' => Key('.') - case 'o' => Key('/') - case 'p' => Key('0') - case 'q' => Key('1') - case 'r' => Key('2') - case 's' => Key('3') - case 't' => Key('4') - case 'u' => Key('5') - case 'v' => Key('6') - case 'w' => Key('7') - case 'x' => Key('8') - case 'y' => Key('9') - case 'A' => Key.up - case 'B' => Key.down - case 'C' => Key.right - case 'D' => Key.left - case 'F' => Key.`end` - case 'H' => Key.home - case 'M' => Key.enter - case 'P' => Key.f1 - case 'Q' => Key.f2 - case 'R' => Key.f3 - case 'S' => Key.f4 - case other: Char => Key.unknown(s"${Ascii.ESC}O${other}") + val input = read() + + input match + case Eof => Eof + case c: Char => + KeyMappings.default.get(c) match + case None => Key(c) + case Some(k: Key) => k + case Some(ks: KeySequence) => + read(timeout) match { + case Eof => ks.root + case Timeout => ks.root + case cx => readKeySequence(s"$c$cx", ks) } - case '§' => Key('§') - case '1' => Key('¡') - case '2' => Key('™') - case '3' => Key('£') - case '4' => Key('¢') - case '5' => Key('∞') - case '6' => Key('§') - case '7' => Key('¶') - case '8' => Key('•') - case '9' => Key('ª') - case '0' => Key('º') - case '-' => Key('–') - case '=' => Key('≠') - case other: Char => Key.unknown(s"${Ascii.ESC}${other}") - } - - case Eof => Eof - case other: Char => Key(other) - } + @tailrec + private def readKeySequence(acc: String, sequence: KeySequence): Eof | Key = { + if sequence.sequences.contains(acc) then sequence.sequences(acc) + else if sequence.subSequences.contains(acc) then + read() match { + case Eof => Eof + case c: Char => readKeySequence(acc + c.toString, sequence) + } + else Key.unknown(acc) + } }