From 2dc1938a5915f0cdd9bbdb0c3171f8aaf99cc33e Mon Sep 17 00:00:00 2001 From: Paul J Thordarson Date: Wed, 16 Apr 2025 11:55:47 -0400 Subject: [PATCH 1/6] Create initial pure key structure Create an inital structure for key code interpretation. --- .../src/main/scala/terminus/KeyMappings.scala | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 core/shared/src/main/scala/terminus/KeyMappings.scala 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..8bbb194 --- /dev/null +++ b/core/shared/src/main/scala/terminus/KeyMappings.scala @@ -0,0 +1,241 @@ +package terminus + +import terminus.effect.Ascii + +object KeyMappings: + lazy val default: Map[Char, Key | Map[String, Key]] = 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 -> Map( + // Simple ESC sequences + s"${Ascii.ESC}${Ascii.HT}" -> Key.backTab, + s"${Ascii.ESC}b" -> Key.controlLeft, + s"${Ascii.ESC}f" -> Key.controlRight, + s"${Ascii.ESC}§" -> Key('§'), + s"${Ascii.ESC}1" -> Key('¡'), + s"${Ascii.ESC}2" -> Key('™'), + s"${Ascii.ESC}3" -> Key('£'), + s"${Ascii.ESC}4" -> Key('¢'), + s"${Ascii.ESC}5" -> Key('∞'), + s"${Ascii.ESC}6" -> Key('§'), + s"${Ascii.ESC}7" -> Key('¶'), + s"${Ascii.ESC}8" -> Key('•'), + s"${Ascii.ESC}9" -> Key('ª'), + s"${Ascii.ESC}0" -> Key('º'), + s"${Ascii.ESC}-" -> Key('–'), + s"${Ascii.ESC}=" -> Key('≠'), + + // Normal mode - basic sequences + s"${Ascii.ESC}[a" -> Key.shiftUp, + s"${Ascii.ESC}[b" -> Key.shiftDown, + s"${Ascii.ESC}[c" -> Key.shiftRight, + s"${Ascii.ESC}[d" -> Key.shiftLeft, + s"${Ascii.ESC}[A" -> Key.up, + s"${Ascii.ESC}[B" -> Key.down, + s"${Ascii.ESC}[C" -> Key.right, + s"${Ascii.ESC}[D" -> Key.left, + s"${Ascii.ESC}[F" -> Key.`end`, + s"${Ascii.ESC}[H" -> Key.home, + s"${Ascii.ESC}[Z" -> Key.backTab, + s"${Ascii.ESC}[~" -> Key.backTab, + + // Function keys and control combinations + s"${Ascii.ESC}[11^" -> Key.controlF1, + s"${Ascii.ESC}[11~" -> Key.f1, + s"${Ascii.ESC}[12^" -> Key.controlF2, + s"${Ascii.ESC}[12~" -> Key.f2, + s"${Ascii.ESC}[13^" -> Key.controlF3, + s"${Ascii.ESC}[13~" -> Key.f3, + s"${Ascii.ESC}[14^" -> Key.controlF4, + s"${Ascii.ESC}[14~" -> Key.f4, + s"${Ascii.ESC}[15^" -> Key.controlF5, + s"${Ascii.ESC}[15~" -> Key.f5, + s"${Ascii.ESC}[15;2~" -> Key.f17, + s"${Ascii.ESC}[15;5~" -> Key.controlF5, + s"${Ascii.ESC}[15;6~" -> Key.controlF17, + s"${Ascii.ESC}[17^" -> Key.controlF6, + s"${Ascii.ESC}[17~" -> Key.f6, + s"${Ascii.ESC}[17;2~" -> Key.f18, + s"${Ascii.ESC}[17;5~" -> Key.controlF6, + s"${Ascii.ESC}[17;6~" -> Key.controlF18, + s"${Ascii.ESC}[18^" -> Key.controlF7, + s"${Ascii.ESC}[18~" -> Key.f7, + s"${Ascii.ESC}[18;2~" -> Key.f19, + s"${Ascii.ESC}[18;5~" -> Key.controlF7, + s"${Ascii.ESC}[18;6~" -> Key.controlF19, + s"${Ascii.ESC}[19^" -> Key.controlF8, + s"${Ascii.ESC}[19~" -> Key.f8, + s"${Ascii.ESC}[19;2~" -> Key.f20, + s"${Ascii.ESC}[19;5~" -> Key.controlF8, + s"${Ascii.ESC}[19;6~" -> Key.controlF20, + + // Special key combinations + s"${Ascii.ESC}[1~" -> Key.home, + s"${Ascii.ESC}[1;2A" -> Key.shiftUp, + s"${Ascii.ESC}[1;2B" -> Key.shiftDown, + s"${Ascii.ESC}[1;2C" -> Key.shiftRight, + s"${Ascii.ESC}[1;2D" -> Key.shiftLeft, + s"${Ascii.ESC}[1;2F" -> Key.shiftEnd, + s"${Ascii.ESC}[1;2H" -> Key.shiftHome, + s"${Ascii.ESC}[1;2P" -> Key.f13, + s"${Ascii.ESC}[1;2Q" -> Key.f14, + s"${Ascii.ESC}[1;2R" -> Key.f15, + s"${Ascii.ESC}[1;2S" -> Key.f16, + s"${Ascii.ESC}[1;5A" -> Key.controlUp, + s"${Ascii.ESC}[1;5B" -> Key.controlDown, + s"${Ascii.ESC}[1;5C" -> Key.controlRight, + s"${Ascii.ESC}[1;5D" -> Key.controlLeft, + s"${Ascii.ESC}[1;5F" -> Key.controlEnd, + s"${Ascii.ESC}[1;5H" -> Key.controlHome, + s"${Ascii.ESC}[1;5P" -> Key.controlF1, + s"${Ascii.ESC}[1;5Q" -> Key.controlF2, + s"${Ascii.ESC}[1;5R" -> Key.controlF3, + s"${Ascii.ESC}[1;5S" -> Key.controlF4, + s"${Ascii.ESC}[1;6A" -> Key.controlShiftUp, + s"${Ascii.ESC}[1;6B" -> Key.controlShiftDown, + s"${Ascii.ESC}[1;6C" -> Key.controlShiftRight, + s"${Ascii.ESC}[1;6D" -> Key.controlShiftLeft, + s"${Ascii.ESC}[1;6F" -> Key.controlShiftEnd, + s"${Ascii.ESC}[1;6H" -> Key.controlShiftHome, + s"${Ascii.ESC}[1;6P" -> Key.controlF13, + s"${Ascii.ESC}[1;6Q" -> Key.controlF14, + s"${Ascii.ESC}[1;6R" -> Key.controlF15, + s"${Ascii.ESC}[1;6S" -> Key.controlF16, + + // Function keys 9-24 + s"${Ascii.ESC}[20^" -> Key.controlF9, + s"${Ascii.ESC}[20~" -> Key.f9, + s"${Ascii.ESC}[20;2~" -> Key.f21, + s"${Ascii.ESC}[20;5~" -> Key.controlF9, + s"${Ascii.ESC}[20;6~" -> Key.controlF21, + s"${Ascii.ESC}[21^" -> Key.controlF10, + s"${Ascii.ESC}[21~" -> Key.f10, + s"${Ascii.ESC}[21;2~" -> Key.f22, + s"${Ascii.ESC}[21;5~" -> Key.controlF10, + s"${Ascii.ESC}[21;6~" -> Key.controlF22, + s"${Ascii.ESC}[23@" -> Key.controlF21, + s"${Ascii.ESC}[23^" -> Key.controlF11, + s"${Ascii.ESC}[23$$" -> Key.f23, + s"${Ascii.ESC}[23~" -> Key.f11, + s"${Ascii.ESC}[23;2~" -> Key.f23, + s"${Ascii.ESC}[23;5~" -> Key.controlF11, + s"${Ascii.ESC}[23;6~" -> Key.controlF23, + s"${Ascii.ESC}[24@" -> Key.controlF22, + s"${Ascii.ESC}[24^" -> Key.controlF12, + s"${Ascii.ESC}[24$$" -> Key.f24, + s"${Ascii.ESC}[24~" -> Key.f12, + s"${Ascii.ESC}[24;2~" -> Key.f24, + s"${Ascii.ESC}[24;5~" -> Key.controlF12, + s"${Ascii.ESC}[24;6~" -> Key.controlF24, + s"${Ascii.ESC}[25^" -> Key.controlF13, + s"${Ascii.ESC}[25~" -> Key.f13, + s"${Ascii.ESC}[26^" -> Key.controlF14, + s"${Ascii.ESC}[26~" -> Key.f14, + s"${Ascii.ESC}[28^" -> Key.controlF15, + s"${Ascii.ESC}[28~" -> Key.f15, + s"${Ascii.ESC}[29^" -> Key.controlF16, + s"${Ascii.ESC}[29~" -> Key.f16, + + // Insert, Delete, Page Up/Down + s"${Ascii.ESC}[2~" -> Key.insert, + s"${Ascii.ESC}[3^" -> Key.controlDelete, + s"${Ascii.ESC}[3$$" -> Key.shiftDelete, + s"${Ascii.ESC}[3~" -> Key.delete, + s"${Ascii.ESC}[3;2~" -> Key.shiftDelete, + s"${Ascii.ESC}[3;5~" -> Key.controlDelete, + s"${Ascii.ESC}[3;6~" -> Key.controlShiftDelete, + s"${Ascii.ESC}[4~" -> Key.`end`, + s"${Ascii.ESC}[5A" -> Key.controlUp, + s"${Ascii.ESC}[5B" -> Key.controlDown, + s"${Ascii.ESC}[5C" -> Key.controlRight, + s"${Ascii.ESC}[5D" -> Key.controlLeft, + s"${Ascii.ESC}[5^" -> Key.controlPageUp, + s"${Ascii.ESC}[5~" -> Key.pageUp, + s"${Ascii.ESC}[5;2~" -> Key.shiftPageUp, + s"${Ascii.ESC}[5;5~" -> Key.controlPageUp, + s"${Ascii.ESC}[5;6~" -> Key.controlShiftPageUp, + s"${Ascii.ESC}[6^" -> Key.controlPageDown, + s"${Ascii.ESC}[6~" -> Key.pageDown, + s"${Ascii.ESC}[6;2~" -> Key.shiftPageDown, + s"${Ascii.ESC}[6;5~" -> Key.controlPageDown, + s"${Ascii.ESC}[6;6~" -> Key.controlShiftPageDown, + s"${Ascii.ESC}[7^" -> Key.controlEnd, + s"${Ascii.ESC}[7$$" -> Key.shiftHome, + s"${Ascii.ESC}[7~" -> Key.home, + s"${Ascii.ESC}[8^" -> Key.controlHome, + s"${Ascii.ESC}[8$$" -> Key.shiftEnd, + s"${Ascii.ESC}[8~" -> Key.`end`, + + // Function key shortcuts + s"${Ascii.ESC}[[A" -> Key.f1, + s"${Ascii.ESC}[[B" -> Key.f2, + s"${Ascii.ESC}[[C" -> Key.f3, + s"${Ascii.ESC}[[D" -> Key.f4, + s"${Ascii.ESC}[[E" -> Key.f5, + + // Application mode + s"${Ascii.ESC}Oa" -> Key.controlUp, + s"${Ascii.ESC}Ob" -> Key.controlUp, + s"${Ascii.ESC}Oc" -> Key.controlRight, + s"${Ascii.ESC}Od" -> Key.controlLeft, + s"${Ascii.ESC}Oj" -> Key('*'), + s"${Ascii.ESC}Ok" -> Key('+'), + s"${Ascii.ESC}Om" -> Key('-'), + s"${Ascii.ESC}On" -> Key('.'), + s"${Ascii.ESC}Oo" -> Key('/'), + s"${Ascii.ESC}Op" -> Key('0'), + s"${Ascii.ESC}Oq" -> Key('1'), + s"${Ascii.ESC}Or" -> Key('2'), + s"${Ascii.ESC}Os" -> Key('3'), + s"${Ascii.ESC}Ot" -> Key('4'), + s"${Ascii.ESC}Ou" -> Key('5'), + s"${Ascii.ESC}Ov" -> Key('6'), + s"${Ascii.ESC}Ow" -> Key('7'), + s"${Ascii.ESC}Ox" -> Key('8'), + s"${Ascii.ESC}Oy" -> Key('9'), + s"${Ascii.ESC}OA" -> Key.up, + s"${Ascii.ESC}OB" -> Key.down, + s"${Ascii.ESC}OC" -> Key.right, + s"${Ascii.ESC}OD" -> Key.left, + s"${Ascii.ESC}OF" -> Key.`end`, + s"${Ascii.ESC}OH" -> Key.home, + s"${Ascii.ESC}OM" -> Key.enter, + s"${Ascii.ESC}OP" -> Key.f1, + s"${Ascii.ESC}OQ" -> Key.f2, + s"${Ascii.ESC}OR" -> Key.f3, + s"${Ascii.ESC}OS" -> Key.f4 + ) + ) \ No newline at end of file From 0da9ee4f0675b75857c3aa9752124b63898d473f Mon Sep 17 00:00:00 2001 From: Paul J Thordarson Date: Wed, 16 Apr 2025 12:08:19 -0400 Subject: [PATCH 2/6] Refine keycode structure Split keycode structure into base keys and escapeSequences. Create escapeSubsequnces to support key parsing. --- .../src/main/scala/terminus/KeyMappings.scala | 415 +++++++++--------- 1 file changed, 214 insertions(+), 201 deletions(-) diff --git a/core/shared/src/main/scala/terminus/KeyMappings.scala b/core/shared/src/main/scala/terminus/KeyMappings.scala index 8bbb194..203380e 100644 --- a/core/shared/src/main/scala/terminus/KeyMappings.scala +++ b/core/shared/src/main/scala/terminus/KeyMappings.scala @@ -3,6 +3,211 @@ package terminus import terminus.effect.Ascii object KeyMappings: + lazy val escapeSequences: Map[String, Key] = Map( + // Simple ESC sequences + s"${Ascii.ESC}${Ascii.HT}" -> Key.backTab, + s"${Ascii.ESC}b" -> Key.controlLeft, + s"${Ascii.ESC}f" -> Key.controlRight, + s"${Ascii.ESC}§" -> Key('§'), + s"${Ascii.ESC}1" -> Key('¡'), + s"${Ascii.ESC}2" -> Key('™'), + s"${Ascii.ESC}3" -> Key('£'), + s"${Ascii.ESC}4" -> Key('¢'), + s"${Ascii.ESC}5" -> Key('∞'), + s"${Ascii.ESC}6" -> Key('§'), + s"${Ascii.ESC}7" -> Key('¶'), + s"${Ascii.ESC}8" -> Key('•'), + s"${Ascii.ESC}9" -> Key('ª'), + s"${Ascii.ESC}0" -> Key('º'), + s"${Ascii.ESC}-" -> Key('–'), + s"${Ascii.ESC}=" -> Key('≠'), + + // Normal mode - basic sequences + s"${Ascii.ESC}[a" -> Key.shiftUp, + s"${Ascii.ESC}[b" -> Key.shiftDown, + s"${Ascii.ESC}[c" -> Key.shiftRight, + s"${Ascii.ESC}[d" -> Key.shiftLeft, + s"${Ascii.ESC}[A" -> Key.up, + s"${Ascii.ESC}[B" -> Key.down, + s"${Ascii.ESC}[C" -> Key.right, + s"${Ascii.ESC}[D" -> Key.left, + s"${Ascii.ESC}[F" -> Key.`end`, + s"${Ascii.ESC}[H" -> Key.home, + s"${Ascii.ESC}[Z" -> Key.backTab, + s"${Ascii.ESC}[~" -> Key.backTab, + + // Function keys and control combinations + s"${Ascii.ESC}[11^" -> Key.controlF1, + s"${Ascii.ESC}[11~" -> Key.f1, + s"${Ascii.ESC}[12^" -> Key.controlF2, + s"${Ascii.ESC}[12~" -> Key.f2, + s"${Ascii.ESC}[13^" -> Key.controlF3, + s"${Ascii.ESC}[13~" -> Key.f3, + s"${Ascii.ESC}[14^" -> Key.controlF4, + s"${Ascii.ESC}[14~" -> Key.f4, + s"${Ascii.ESC}[15^" -> Key.controlF5, + s"${Ascii.ESC}[15~" -> Key.f5, + s"${Ascii.ESC}[15;2~" -> Key.f17, + s"${Ascii.ESC}[15;5~" -> Key.controlF5, + s"${Ascii.ESC}[15;6~" -> Key.controlF17, + s"${Ascii.ESC}[17^" -> Key.controlF6, + s"${Ascii.ESC}[17~" -> Key.f6, + s"${Ascii.ESC}[17;2~" -> Key.f18, + s"${Ascii.ESC}[17;5~" -> Key.controlF6, + s"${Ascii.ESC}[17;6~" -> Key.controlF18, + s"${Ascii.ESC}[18^" -> Key.controlF7, + s"${Ascii.ESC}[18~" -> Key.f7, + s"${Ascii.ESC}[18;2~" -> Key.f19, + s"${Ascii.ESC}[18;5~" -> Key.controlF7, + s"${Ascii.ESC}[18;6~" -> Key.controlF19, + s"${Ascii.ESC}[19^" -> Key.controlF8, + s"${Ascii.ESC}[19~" -> Key.f8, + s"${Ascii.ESC}[19;2~" -> Key.f20, + s"${Ascii.ESC}[19;5~" -> Key.controlF8, + s"${Ascii.ESC}[19;6~" -> Key.controlF20, + + // Special key combinations + s"${Ascii.ESC}[1~" -> Key.home, + s"${Ascii.ESC}[1;2A" -> Key.shiftUp, + s"${Ascii.ESC}[1;2B" -> Key.shiftDown, + s"${Ascii.ESC}[1;2C" -> Key.shiftRight, + s"${Ascii.ESC}[1;2D" -> Key.shiftLeft, + s"${Ascii.ESC}[1;2F" -> Key.shiftEnd, + s"${Ascii.ESC}[1;2H" -> Key.shiftHome, + s"${Ascii.ESC}[1;2P" -> Key.f13, + s"${Ascii.ESC}[1;2Q" -> Key.f14, + s"${Ascii.ESC}[1;2R" -> Key.f15, + s"${Ascii.ESC}[1;2S" -> Key.f16, + s"${Ascii.ESC}[1;5A" -> Key.controlUp, + s"${Ascii.ESC}[1;5B" -> Key.controlDown, + s"${Ascii.ESC}[1;5C" -> Key.controlRight, + s"${Ascii.ESC}[1;5D" -> Key.controlLeft, + s"${Ascii.ESC}[1;5F" -> Key.controlEnd, + s"${Ascii.ESC}[1;5H" -> Key.controlHome, + s"${Ascii.ESC}[1;5P" -> Key.controlF1, + s"${Ascii.ESC}[1;5Q" -> Key.controlF2, + s"${Ascii.ESC}[1;5R" -> Key.controlF3, + s"${Ascii.ESC}[1;5S" -> Key.controlF4, + s"${Ascii.ESC}[1;6A" -> Key.controlShiftUp, + s"${Ascii.ESC}[1;6B" -> Key.controlShiftDown, + s"${Ascii.ESC}[1;6C" -> Key.controlShiftRight, + s"${Ascii.ESC}[1;6D" -> Key.controlShiftLeft, + s"${Ascii.ESC}[1;6F" -> Key.controlShiftEnd, + s"${Ascii.ESC}[1;6H" -> Key.controlShiftHome, + s"${Ascii.ESC}[1;6P" -> Key.controlF13, + s"${Ascii.ESC}[1;6Q" -> Key.controlF14, + s"${Ascii.ESC}[1;6R" -> Key.controlF15, + s"${Ascii.ESC}[1;6S" -> Key.controlF16, + + // Function keys 9-24 + s"${Ascii.ESC}[20^" -> Key.controlF9, + s"${Ascii.ESC}[20~" -> Key.f9, + s"${Ascii.ESC}[20;2~" -> Key.f21, + s"${Ascii.ESC}[20;5~" -> Key.controlF9, + s"${Ascii.ESC}[20;6~" -> Key.controlF21, + s"${Ascii.ESC}[21^" -> Key.controlF10, + s"${Ascii.ESC}[21~" -> Key.f10, + s"${Ascii.ESC}[21;2~" -> Key.f22, + s"${Ascii.ESC}[21;5~" -> Key.controlF10, + s"${Ascii.ESC}[21;6~" -> Key.controlF22, + s"${Ascii.ESC}[23@" -> Key.controlF21, + s"${Ascii.ESC}[23^" -> Key.controlF11, + s"${Ascii.ESC}[23$$" -> Key.f23, + s"${Ascii.ESC}[23~" -> Key.f11, + s"${Ascii.ESC}[23;2~" -> Key.f23, + s"${Ascii.ESC}[23;5~" -> Key.controlF11, + s"${Ascii.ESC}[23;6~" -> Key.controlF23, + s"${Ascii.ESC}[24@" -> Key.controlF22, + s"${Ascii.ESC}[24^" -> Key.controlF12, + s"${Ascii.ESC}[24$$" -> Key.f24, + s"${Ascii.ESC}[24~" -> Key.f12, + s"${Ascii.ESC}[24;2~" -> Key.f24, + s"${Ascii.ESC}[24;5~" -> Key.controlF12, + s"${Ascii.ESC}[24;6~" -> Key.controlF24, + s"${Ascii.ESC}[25^" -> Key.controlF13, + s"${Ascii.ESC}[25~" -> Key.f13, + s"${Ascii.ESC}[26^" -> Key.controlF14, + s"${Ascii.ESC}[26~" -> Key.f14, + s"${Ascii.ESC}[28^" -> Key.controlF15, + s"${Ascii.ESC}[28~" -> Key.f15, + s"${Ascii.ESC}[29^" -> Key.controlF16, + s"${Ascii.ESC}[29~" -> Key.f16, + + // Insert, Delete, Page Up/Down + s"${Ascii.ESC}[2~" -> Key.insert, + s"${Ascii.ESC}[3^" -> Key.controlDelete, + s"${Ascii.ESC}[3$$" -> Key.shiftDelete, + s"${Ascii.ESC}[3~" -> Key.delete, + s"${Ascii.ESC}[3;2~" -> Key.shiftDelete, + s"${Ascii.ESC}[3;5~" -> Key.controlDelete, + s"${Ascii.ESC}[3;6~" -> Key.controlShiftDelete, + s"${Ascii.ESC}[4~" -> Key.`end`, + s"${Ascii.ESC}[5A" -> Key.controlUp, + s"${Ascii.ESC}[5B" -> Key.controlDown, + s"${Ascii.ESC}[5C" -> Key.controlRight, + s"${Ascii.ESC}[5D" -> Key.controlLeft, + s"${Ascii.ESC}[5^" -> Key.controlPageUp, + s"${Ascii.ESC}[5~" -> Key.pageUp, + s"${Ascii.ESC}[5;2~" -> Key.shiftPageUp, + s"${Ascii.ESC}[5;5~" -> Key.controlPageUp, + s"${Ascii.ESC}[5;6~" -> Key.controlShiftPageUp, + s"${Ascii.ESC}[6^" -> Key.controlPageDown, + s"${Ascii.ESC}[6~" -> Key.pageDown, + s"${Ascii.ESC}[6;2~" -> Key.shiftPageDown, + s"${Ascii.ESC}[6;5~" -> Key.controlPageDown, + s"${Ascii.ESC}[6;6~" -> Key.controlShiftPageDown, + s"${Ascii.ESC}[7^" -> Key.controlEnd, + s"${Ascii.ESC}[7$$" -> Key.shiftHome, + s"${Ascii.ESC}[7~" -> Key.home, + s"${Ascii.ESC}[8^" -> Key.controlHome, + s"${Ascii.ESC}[8$$" -> Key.shiftEnd, + s"${Ascii.ESC}[8~" -> Key.`end`, + + // Function key shortcuts + s"${Ascii.ESC}[[A" -> Key.f1, + s"${Ascii.ESC}[[B" -> Key.f2, + s"${Ascii.ESC}[[C" -> Key.f3, + s"${Ascii.ESC}[[D" -> Key.f4, + s"${Ascii.ESC}[[E" -> Key.f5, + + // Application mode + s"${Ascii.ESC}Oa" -> Key.controlUp, + s"${Ascii.ESC}Ob" -> Key.controlUp, + s"${Ascii.ESC}Oc" -> Key.controlRight, + s"${Ascii.ESC}Od" -> Key.controlLeft, + s"${Ascii.ESC}Oj" -> Key('*'), + s"${Ascii.ESC}Ok" -> Key('+'), + s"${Ascii.ESC}Om" -> Key('-'), + s"${Ascii.ESC}On" -> Key('.'), + s"${Ascii.ESC}Oo" -> Key('/'), + s"${Ascii.ESC}Op" -> Key('0'), + s"${Ascii.ESC}Oq" -> Key('1'), + s"${Ascii.ESC}Or" -> Key('2'), + s"${Ascii.ESC}Os" -> Key('3'), + s"${Ascii.ESC}Ot" -> Key('4'), + s"${Ascii.ESC}Ou" -> Key('5'), + s"${Ascii.ESC}Ov" -> Key('6'), + s"${Ascii.ESC}Ow" -> Key('7'), + s"${Ascii.ESC}Ox" -> Key('8'), + s"${Ascii.ESC}Oy" -> Key('9'), + s"${Ascii.ESC}OA" -> Key.up, + s"${Ascii.ESC}OB" -> Key.down, + s"${Ascii.ESC}OC" -> Key.right, + s"${Ascii.ESC}OD" -> Key.left, + s"${Ascii.ESC}OF" -> Key.`end`, + s"${Ascii.ESC}OH" -> Key.home, + s"${Ascii.ESC}OM" -> Key.enter, + s"${Ascii.ESC}OP" -> Key.f1, + s"${Ascii.ESC}OQ" -> Key.f2, + s"${Ascii.ESC}OR" -> Key.f3, + s"${Ascii.ESC}OS" -> Key.f4 + ) + + lazy val escapeSubsequences: Set[String] = + escapeSequences.keySet.flatMap(s => + if s.length <= 2 then Set.empty + else (2 until s.length).map(s.substring(0, _)).toSet) + lazy val default: Map[Char, Key | Map[String, Key]] = Map( ' ' -> Key.space, Ascii.NUL -> Key.controlAt, // Control-At (Also for Ctrl-Space) @@ -32,210 +237,18 @@ object KeyMappings: 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 -> Map( - // Simple ESC sequences - s"${Ascii.ESC}${Ascii.HT}" -> Key.backTab, - s"${Ascii.ESC}b" -> Key.controlLeft, - s"${Ascii.ESC}f" -> Key.controlRight, - s"${Ascii.ESC}§" -> Key('§'), - s"${Ascii.ESC}1" -> Key('¡'), - s"${Ascii.ESC}2" -> Key('™'), - s"${Ascii.ESC}3" -> Key('£'), - s"${Ascii.ESC}4" -> Key('¢'), - s"${Ascii.ESC}5" -> Key('∞'), - s"${Ascii.ESC}6" -> Key('§'), - s"${Ascii.ESC}7" -> Key('¶'), - s"${Ascii.ESC}8" -> Key('•'), - s"${Ascii.ESC}9" -> Key('ª'), - s"${Ascii.ESC}0" -> Key('º'), - s"${Ascii.ESC}-" -> Key('–'), - s"${Ascii.ESC}=" -> Key('≠'), - - // Normal mode - basic sequences - s"${Ascii.ESC}[a" -> Key.shiftUp, - s"${Ascii.ESC}[b" -> Key.shiftDown, - s"${Ascii.ESC}[c" -> Key.shiftRight, - s"${Ascii.ESC}[d" -> Key.shiftLeft, - s"${Ascii.ESC}[A" -> Key.up, - s"${Ascii.ESC}[B" -> Key.down, - s"${Ascii.ESC}[C" -> Key.right, - s"${Ascii.ESC}[D" -> Key.left, - s"${Ascii.ESC}[F" -> Key.`end`, - s"${Ascii.ESC}[H" -> Key.home, - s"${Ascii.ESC}[Z" -> Key.backTab, - s"${Ascii.ESC}[~" -> Key.backTab, - - // Function keys and control combinations - s"${Ascii.ESC}[11^" -> Key.controlF1, - s"${Ascii.ESC}[11~" -> Key.f1, - s"${Ascii.ESC}[12^" -> Key.controlF2, - s"${Ascii.ESC}[12~" -> Key.f2, - s"${Ascii.ESC}[13^" -> Key.controlF3, - s"${Ascii.ESC}[13~" -> Key.f3, - s"${Ascii.ESC}[14^" -> Key.controlF4, - s"${Ascii.ESC}[14~" -> Key.f4, - s"${Ascii.ESC}[15^" -> Key.controlF5, - s"${Ascii.ESC}[15~" -> Key.f5, - s"${Ascii.ESC}[15;2~" -> Key.f17, - s"${Ascii.ESC}[15;5~" -> Key.controlF5, - s"${Ascii.ESC}[15;6~" -> Key.controlF17, - s"${Ascii.ESC}[17^" -> Key.controlF6, - s"${Ascii.ESC}[17~" -> Key.f6, - s"${Ascii.ESC}[17;2~" -> Key.f18, - s"${Ascii.ESC}[17;5~" -> Key.controlF6, - s"${Ascii.ESC}[17;6~" -> Key.controlF18, - s"${Ascii.ESC}[18^" -> Key.controlF7, - s"${Ascii.ESC}[18~" -> Key.f7, - s"${Ascii.ESC}[18;2~" -> Key.f19, - s"${Ascii.ESC}[18;5~" -> Key.controlF7, - s"${Ascii.ESC}[18;6~" -> Key.controlF19, - s"${Ascii.ESC}[19^" -> Key.controlF8, - s"${Ascii.ESC}[19~" -> Key.f8, - s"${Ascii.ESC}[19;2~" -> Key.f20, - s"${Ascii.ESC}[19;5~" -> Key.controlF8, - s"${Ascii.ESC}[19;6~" -> Key.controlF20, - - // Special key combinations - s"${Ascii.ESC}[1~" -> Key.home, - s"${Ascii.ESC}[1;2A" -> Key.shiftUp, - s"${Ascii.ESC}[1;2B" -> Key.shiftDown, - s"${Ascii.ESC}[1;2C" -> Key.shiftRight, - s"${Ascii.ESC}[1;2D" -> Key.shiftLeft, - s"${Ascii.ESC}[1;2F" -> Key.shiftEnd, - s"${Ascii.ESC}[1;2H" -> Key.shiftHome, - s"${Ascii.ESC}[1;2P" -> Key.f13, - s"${Ascii.ESC}[1;2Q" -> Key.f14, - s"${Ascii.ESC}[1;2R" -> Key.f15, - s"${Ascii.ESC}[1;2S" -> Key.f16, - s"${Ascii.ESC}[1;5A" -> Key.controlUp, - s"${Ascii.ESC}[1;5B" -> Key.controlDown, - s"${Ascii.ESC}[1;5C" -> Key.controlRight, - s"${Ascii.ESC}[1;5D" -> Key.controlLeft, - s"${Ascii.ESC}[1;5F" -> Key.controlEnd, - s"${Ascii.ESC}[1;5H" -> Key.controlHome, - s"${Ascii.ESC}[1;5P" -> Key.controlF1, - s"${Ascii.ESC}[1;5Q" -> Key.controlF2, - s"${Ascii.ESC}[1;5R" -> Key.controlF3, - s"${Ascii.ESC}[1;5S" -> Key.controlF4, - s"${Ascii.ESC}[1;6A" -> Key.controlShiftUp, - s"${Ascii.ESC}[1;6B" -> Key.controlShiftDown, - s"${Ascii.ESC}[1;6C" -> Key.controlShiftRight, - s"${Ascii.ESC}[1;6D" -> Key.controlShiftLeft, - s"${Ascii.ESC}[1;6F" -> Key.controlShiftEnd, - s"${Ascii.ESC}[1;6H" -> Key.controlShiftHome, - s"${Ascii.ESC}[1;6P" -> Key.controlF13, - s"${Ascii.ESC}[1;6Q" -> Key.controlF14, - s"${Ascii.ESC}[1;6R" -> Key.controlF15, - s"${Ascii.ESC}[1;6S" -> Key.controlF16, - - // Function keys 9-24 - s"${Ascii.ESC}[20^" -> Key.controlF9, - s"${Ascii.ESC}[20~" -> Key.f9, - s"${Ascii.ESC}[20;2~" -> Key.f21, - s"${Ascii.ESC}[20;5~" -> Key.controlF9, - s"${Ascii.ESC}[20;6~" -> Key.controlF21, - s"${Ascii.ESC}[21^" -> Key.controlF10, - s"${Ascii.ESC}[21~" -> Key.f10, - s"${Ascii.ESC}[21;2~" -> Key.f22, - s"${Ascii.ESC}[21;5~" -> Key.controlF10, - s"${Ascii.ESC}[21;6~" -> Key.controlF22, - s"${Ascii.ESC}[23@" -> Key.controlF21, - s"${Ascii.ESC}[23^" -> Key.controlF11, - s"${Ascii.ESC}[23$$" -> Key.f23, - s"${Ascii.ESC}[23~" -> Key.f11, - s"${Ascii.ESC}[23;2~" -> Key.f23, - s"${Ascii.ESC}[23;5~" -> Key.controlF11, - s"${Ascii.ESC}[23;6~" -> Key.controlF23, - s"${Ascii.ESC}[24@" -> Key.controlF22, - s"${Ascii.ESC}[24^" -> Key.controlF12, - s"${Ascii.ESC}[24$$" -> Key.f24, - s"${Ascii.ESC}[24~" -> Key.f12, - s"${Ascii.ESC}[24;2~" -> Key.f24, - s"${Ascii.ESC}[24;5~" -> Key.controlF12, - s"${Ascii.ESC}[24;6~" -> Key.controlF24, - s"${Ascii.ESC}[25^" -> Key.controlF13, - s"${Ascii.ESC}[25~" -> Key.f13, - s"${Ascii.ESC}[26^" -> Key.controlF14, - s"${Ascii.ESC}[26~" -> Key.f14, - s"${Ascii.ESC}[28^" -> Key.controlF15, - s"${Ascii.ESC}[28~" -> Key.f15, - s"${Ascii.ESC}[29^" -> Key.controlF16, - s"${Ascii.ESC}[29~" -> Key.f16, - - // Insert, Delete, Page Up/Down - s"${Ascii.ESC}[2~" -> Key.insert, - s"${Ascii.ESC}[3^" -> Key.controlDelete, - s"${Ascii.ESC}[3$$" -> Key.shiftDelete, - s"${Ascii.ESC}[3~" -> Key.delete, - s"${Ascii.ESC}[3;2~" -> Key.shiftDelete, - s"${Ascii.ESC}[3;5~" -> Key.controlDelete, - s"${Ascii.ESC}[3;6~" -> Key.controlShiftDelete, - s"${Ascii.ESC}[4~" -> Key.`end`, - s"${Ascii.ESC}[5A" -> Key.controlUp, - s"${Ascii.ESC}[5B" -> Key.controlDown, - s"${Ascii.ESC}[5C" -> Key.controlRight, - s"${Ascii.ESC}[5D" -> Key.controlLeft, - s"${Ascii.ESC}[5^" -> Key.controlPageUp, - s"${Ascii.ESC}[5~" -> Key.pageUp, - s"${Ascii.ESC}[5;2~" -> Key.shiftPageUp, - s"${Ascii.ESC}[5;5~" -> Key.controlPageUp, - s"${Ascii.ESC}[5;6~" -> Key.controlShiftPageUp, - s"${Ascii.ESC}[6^" -> Key.controlPageDown, - s"${Ascii.ESC}[6~" -> Key.pageDown, - s"${Ascii.ESC}[6;2~" -> Key.shiftPageDown, - s"${Ascii.ESC}[6;5~" -> Key.controlPageDown, - s"${Ascii.ESC}[6;6~" -> Key.controlShiftPageDown, - s"${Ascii.ESC}[7^" -> Key.controlEnd, - s"${Ascii.ESC}[7$$" -> Key.shiftHome, - s"${Ascii.ESC}[7~" -> Key.home, - s"${Ascii.ESC}[8^" -> Key.controlHome, - s"${Ascii.ESC}[8$$" -> Key.shiftEnd, - s"${Ascii.ESC}[8~" -> Key.`end`, - - // Function key shortcuts - s"${Ascii.ESC}[[A" -> Key.f1, - s"${Ascii.ESC}[[B" -> Key.f2, - s"${Ascii.ESC}[[C" -> Key.f3, - s"${Ascii.ESC}[[D" -> Key.f4, - s"${Ascii.ESC}[[E" -> Key.f5, - - // Application mode - s"${Ascii.ESC}Oa" -> Key.controlUp, - s"${Ascii.ESC}Ob" -> Key.controlUp, - s"${Ascii.ESC}Oc" -> Key.controlRight, - s"${Ascii.ESC}Od" -> Key.controlLeft, - s"${Ascii.ESC}Oj" -> Key('*'), - s"${Ascii.ESC}Ok" -> Key('+'), - s"${Ascii.ESC}Om" -> Key('-'), - s"${Ascii.ESC}On" -> Key('.'), - s"${Ascii.ESC}Oo" -> Key('/'), - s"${Ascii.ESC}Op" -> Key('0'), - s"${Ascii.ESC}Oq" -> Key('1'), - s"${Ascii.ESC}Or" -> Key('2'), - s"${Ascii.ESC}Os" -> Key('3'), - s"${Ascii.ESC}Ot" -> Key('4'), - s"${Ascii.ESC}Ou" -> Key('5'), - s"${Ascii.ESC}Ov" -> Key('6'), - s"${Ascii.ESC}Ow" -> Key('7'), - s"${Ascii.ESC}Ox" -> Key('8'), - s"${Ascii.ESC}Oy" -> Key('9'), - s"${Ascii.ESC}OA" -> Key.up, - s"${Ascii.ESC}OB" -> Key.down, - s"${Ascii.ESC}OC" -> Key.right, - s"${Ascii.ESC}OD" -> Key.left, - s"${Ascii.ESC}OF" -> Key.`end`, - s"${Ascii.ESC}OH" -> Key.home, - s"${Ascii.ESC}OM" -> Key.enter, - s"${Ascii.ESC}OP" -> Key.f1, - s"${Ascii.ESC}OQ" -> Key.f2, - s"${Ascii.ESC}OR" -> Key.f3, - s"${Ascii.ESC}OS" -> Key.f4 - ) - ) \ No newline at end of file + Ascii.ESC -> escapeSequences + ) + + private def prefixSubstrings(s: String): Set[String] = + if s.length <= 2 then Set.empty + else (2 until s.length).map(s.substring(0, _)).toSet + + From 618089d8c4a40ce68cd03a1f9f4296d884e2bbb4 Mon Sep 17 00:00:00 2001 From: Paul J Thordarson Date: Wed, 16 Apr 2025 17:18:50 -0400 Subject: [PATCH 3/6] Replace key reader implementation --- .../src/main/scala/terminus/KeyMappings.scala | 20 +- .../terminus/effect/TerminalKeyReader.scala | 706 +----------------- 2 files changed, 32 insertions(+), 694 deletions(-) diff --git a/core/shared/src/main/scala/terminus/KeyMappings.scala b/core/shared/src/main/scala/terminus/KeyMappings.scala index 203380e..d1cc4e1 100644 --- a/core/shared/src/main/scala/terminus/KeyMappings.scala +++ b/core/shared/src/main/scala/terminus/KeyMappings.scala @@ -2,6 +2,11 @@ package terminus import terminus.effect.Ascii +class KeySequence(val root: Key, val sequences: Map[String, Key]): + 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 escapeSequences: Map[String, Key] = Map( // Simple ESC sequences @@ -203,12 +208,7 @@ object KeyMappings: s"${Ascii.ESC}OS" -> Key.f4 ) - lazy val escapeSubsequences: Set[String] = - escapeSequences.keySet.flatMap(s => - if s.length <= 2 then Set.empty - else (2 until s.length).map(s.substring(0, _)).toSet) - - lazy val default: Map[Char, Key | Map[String, Key]] = Map( + 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) @@ -244,11 +244,5 @@ object KeyMappings: '\u001e' -> Key.controlCircumflex, '\u001f' -> Key.controlUnderscore, '\u007f' -> Key.backspace, - Ascii.ESC -> escapeSequences + Ascii.ESC -> KeySequence(Key.escape, escapeSequences) ) - - private def prefixSubstrings(s: String): Set[String] = - if s.length <= 2 then Set.empty - else (2 until s.length).map(s.substring(0, _)).toSet - - diff --git a/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala b/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala index 3a6423a..6f508cd 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,34 @@ 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}") + @tailrec + private def readKeySequence(acc: String, sequence: KeySequence): Eof | Key = { + (sequence.sequences.get(acc), sequence.subSequences.contains(acc)) match + case (Some(key), _) => key + case (None, false) => Key.unknown(acc) + case (None, true) => + read() match { + case Eof => Eof + case c: Char => readKeySequence(acc :+ c, sequence) } - - case Eof => Eof - case other: Char => Key(other) - } + } } From ed89bfc5b87c040f2f111e13b235ad8dc9cb96c9 Mon Sep 17 00:00:00 2001 From: Paul J Thordarson Date: Thu, 17 Apr 2025 09:31:07 -0400 Subject: [PATCH 4/6] Fixed key mappings --- .../src/main/scala/terminus/KeyMappings.scala | 449 ++++++++++-------- .../terminus/effect/TerminalKeyReader.scala | 4 +- 2 files changed, 240 insertions(+), 213 deletions(-) diff --git a/core/shared/src/main/scala/terminus/KeyMappings.scala b/core/shared/src/main/scala/terminus/KeyMappings.scala index d1cc4e1..916d839 100644 --- a/core/shared/src/main/scala/terminus/KeyMappings.scala +++ b/core/shared/src/main/scala/terminus/KeyMappings.scala @@ -1,3 +1,19 @@ +/* + * 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 @@ -5,211 +21,12 @@ import terminus.effect.Ascii class KeySequence(val root: Key, val sequences: Map[String, Key]): 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 escapeSequences: Map[String, Key] = Map( - // Simple ESC sequences - s"${Ascii.ESC}${Ascii.HT}" -> Key.backTab, - s"${Ascii.ESC}b" -> Key.controlLeft, - s"${Ascii.ESC}f" -> Key.controlRight, - s"${Ascii.ESC}§" -> Key('§'), - s"${Ascii.ESC}1" -> Key('¡'), - s"${Ascii.ESC}2" -> Key('™'), - s"${Ascii.ESC}3" -> Key('£'), - s"${Ascii.ESC}4" -> Key('¢'), - s"${Ascii.ESC}5" -> Key('∞'), - s"${Ascii.ESC}6" -> Key('§'), - s"${Ascii.ESC}7" -> Key('¶'), - s"${Ascii.ESC}8" -> Key('•'), - s"${Ascii.ESC}9" -> Key('ª'), - s"${Ascii.ESC}0" -> Key('º'), - s"${Ascii.ESC}-" -> Key('–'), - s"${Ascii.ESC}=" -> Key('≠'), - - // Normal mode - basic sequences - s"${Ascii.ESC}[a" -> Key.shiftUp, - s"${Ascii.ESC}[b" -> Key.shiftDown, - s"${Ascii.ESC}[c" -> Key.shiftRight, - s"${Ascii.ESC}[d" -> Key.shiftLeft, - s"${Ascii.ESC}[A" -> Key.up, - s"${Ascii.ESC}[B" -> Key.down, - s"${Ascii.ESC}[C" -> Key.right, - s"${Ascii.ESC}[D" -> Key.left, - s"${Ascii.ESC}[F" -> Key.`end`, - s"${Ascii.ESC}[H" -> Key.home, - s"${Ascii.ESC}[Z" -> Key.backTab, - s"${Ascii.ESC}[~" -> Key.backTab, - - // Function keys and control combinations - s"${Ascii.ESC}[11^" -> Key.controlF1, - s"${Ascii.ESC}[11~" -> Key.f1, - s"${Ascii.ESC}[12^" -> Key.controlF2, - s"${Ascii.ESC}[12~" -> Key.f2, - s"${Ascii.ESC}[13^" -> Key.controlF3, - s"${Ascii.ESC}[13~" -> Key.f3, - s"${Ascii.ESC}[14^" -> Key.controlF4, - s"${Ascii.ESC}[14~" -> Key.f4, - s"${Ascii.ESC}[15^" -> Key.controlF5, - s"${Ascii.ESC}[15~" -> Key.f5, - s"${Ascii.ESC}[15;2~" -> Key.f17, - s"${Ascii.ESC}[15;5~" -> Key.controlF5, - s"${Ascii.ESC}[15;6~" -> Key.controlF17, - s"${Ascii.ESC}[17^" -> Key.controlF6, - s"${Ascii.ESC}[17~" -> Key.f6, - s"${Ascii.ESC}[17;2~" -> Key.f18, - s"${Ascii.ESC}[17;5~" -> Key.controlF6, - s"${Ascii.ESC}[17;6~" -> Key.controlF18, - s"${Ascii.ESC}[18^" -> Key.controlF7, - s"${Ascii.ESC}[18~" -> Key.f7, - s"${Ascii.ESC}[18;2~" -> Key.f19, - s"${Ascii.ESC}[18;5~" -> Key.controlF7, - s"${Ascii.ESC}[18;6~" -> Key.controlF19, - s"${Ascii.ESC}[19^" -> Key.controlF8, - s"${Ascii.ESC}[19~" -> Key.f8, - s"${Ascii.ESC}[19;2~" -> Key.f20, - s"${Ascii.ESC}[19;5~" -> Key.controlF8, - s"${Ascii.ESC}[19;6~" -> Key.controlF20, - - // Special key combinations - s"${Ascii.ESC}[1~" -> Key.home, - s"${Ascii.ESC}[1;2A" -> Key.shiftUp, - s"${Ascii.ESC}[1;2B" -> Key.shiftDown, - s"${Ascii.ESC}[1;2C" -> Key.shiftRight, - s"${Ascii.ESC}[1;2D" -> Key.shiftLeft, - s"${Ascii.ESC}[1;2F" -> Key.shiftEnd, - s"${Ascii.ESC}[1;2H" -> Key.shiftHome, - s"${Ascii.ESC}[1;2P" -> Key.f13, - s"${Ascii.ESC}[1;2Q" -> Key.f14, - s"${Ascii.ESC}[1;2R" -> Key.f15, - s"${Ascii.ESC}[1;2S" -> Key.f16, - s"${Ascii.ESC}[1;5A" -> Key.controlUp, - s"${Ascii.ESC}[1;5B" -> Key.controlDown, - s"${Ascii.ESC}[1;5C" -> Key.controlRight, - s"${Ascii.ESC}[1;5D" -> Key.controlLeft, - s"${Ascii.ESC}[1;5F" -> Key.controlEnd, - s"${Ascii.ESC}[1;5H" -> Key.controlHome, - s"${Ascii.ESC}[1;5P" -> Key.controlF1, - s"${Ascii.ESC}[1;5Q" -> Key.controlF2, - s"${Ascii.ESC}[1;5R" -> Key.controlF3, - s"${Ascii.ESC}[1;5S" -> Key.controlF4, - s"${Ascii.ESC}[1;6A" -> Key.controlShiftUp, - s"${Ascii.ESC}[1;6B" -> Key.controlShiftDown, - s"${Ascii.ESC}[1;6C" -> Key.controlShiftRight, - s"${Ascii.ESC}[1;6D" -> Key.controlShiftLeft, - s"${Ascii.ESC}[1;6F" -> Key.controlShiftEnd, - s"${Ascii.ESC}[1;6H" -> Key.controlShiftHome, - s"${Ascii.ESC}[1;6P" -> Key.controlF13, - s"${Ascii.ESC}[1;6Q" -> Key.controlF14, - s"${Ascii.ESC}[1;6R" -> Key.controlF15, - s"${Ascii.ESC}[1;6S" -> Key.controlF16, - - // Function keys 9-24 - s"${Ascii.ESC}[20^" -> Key.controlF9, - s"${Ascii.ESC}[20~" -> Key.f9, - s"${Ascii.ESC}[20;2~" -> Key.f21, - s"${Ascii.ESC}[20;5~" -> Key.controlF9, - s"${Ascii.ESC}[20;6~" -> Key.controlF21, - s"${Ascii.ESC}[21^" -> Key.controlF10, - s"${Ascii.ESC}[21~" -> Key.f10, - s"${Ascii.ESC}[21;2~" -> Key.f22, - s"${Ascii.ESC}[21;5~" -> Key.controlF10, - s"${Ascii.ESC}[21;6~" -> Key.controlF22, - s"${Ascii.ESC}[23@" -> Key.controlF21, - s"${Ascii.ESC}[23^" -> Key.controlF11, - s"${Ascii.ESC}[23$$" -> Key.f23, - s"${Ascii.ESC}[23~" -> Key.f11, - s"${Ascii.ESC}[23;2~" -> Key.f23, - s"${Ascii.ESC}[23;5~" -> Key.controlF11, - s"${Ascii.ESC}[23;6~" -> Key.controlF23, - s"${Ascii.ESC}[24@" -> Key.controlF22, - s"${Ascii.ESC}[24^" -> Key.controlF12, - s"${Ascii.ESC}[24$$" -> Key.f24, - s"${Ascii.ESC}[24~" -> Key.f12, - s"${Ascii.ESC}[24;2~" -> Key.f24, - s"${Ascii.ESC}[24;5~" -> Key.controlF12, - s"${Ascii.ESC}[24;6~" -> Key.controlF24, - s"${Ascii.ESC}[25^" -> Key.controlF13, - s"${Ascii.ESC}[25~" -> Key.f13, - s"${Ascii.ESC}[26^" -> Key.controlF14, - s"${Ascii.ESC}[26~" -> Key.f14, - s"${Ascii.ESC}[28^" -> Key.controlF15, - s"${Ascii.ESC}[28~" -> Key.f15, - s"${Ascii.ESC}[29^" -> Key.controlF16, - s"${Ascii.ESC}[29~" -> Key.f16, - - // Insert, Delete, Page Up/Down - s"${Ascii.ESC}[2~" -> Key.insert, - s"${Ascii.ESC}[3^" -> Key.controlDelete, - s"${Ascii.ESC}[3$$" -> Key.shiftDelete, - s"${Ascii.ESC}[3~" -> Key.delete, - s"${Ascii.ESC}[3;2~" -> Key.shiftDelete, - s"${Ascii.ESC}[3;5~" -> Key.controlDelete, - s"${Ascii.ESC}[3;6~" -> Key.controlShiftDelete, - s"${Ascii.ESC}[4~" -> Key.`end`, - s"${Ascii.ESC}[5A" -> Key.controlUp, - s"${Ascii.ESC}[5B" -> Key.controlDown, - s"${Ascii.ESC}[5C" -> Key.controlRight, - s"${Ascii.ESC}[5D" -> Key.controlLeft, - s"${Ascii.ESC}[5^" -> Key.controlPageUp, - s"${Ascii.ESC}[5~" -> Key.pageUp, - s"${Ascii.ESC}[5;2~" -> Key.shiftPageUp, - s"${Ascii.ESC}[5;5~" -> Key.controlPageUp, - s"${Ascii.ESC}[5;6~" -> Key.controlShiftPageUp, - s"${Ascii.ESC}[6^" -> Key.controlPageDown, - s"${Ascii.ESC}[6~" -> Key.pageDown, - s"${Ascii.ESC}[6;2~" -> Key.shiftPageDown, - s"${Ascii.ESC}[6;5~" -> Key.controlPageDown, - s"${Ascii.ESC}[6;6~" -> Key.controlShiftPageDown, - s"${Ascii.ESC}[7^" -> Key.controlEnd, - s"${Ascii.ESC}[7$$" -> Key.shiftHome, - s"${Ascii.ESC}[7~" -> Key.home, - s"${Ascii.ESC}[8^" -> Key.controlHome, - s"${Ascii.ESC}[8$$" -> Key.shiftEnd, - s"${Ascii.ESC}[8~" -> Key.`end`, - - // Function key shortcuts - s"${Ascii.ESC}[[A" -> Key.f1, - s"${Ascii.ESC}[[B" -> Key.f2, - s"${Ascii.ESC}[[C" -> Key.f3, - s"${Ascii.ESC}[[D" -> Key.f4, - s"${Ascii.ESC}[[E" -> Key.f5, - - // Application mode - s"${Ascii.ESC}Oa" -> Key.controlUp, - s"${Ascii.ESC}Ob" -> Key.controlUp, - s"${Ascii.ESC}Oc" -> Key.controlRight, - s"${Ascii.ESC}Od" -> Key.controlLeft, - s"${Ascii.ESC}Oj" -> Key('*'), - s"${Ascii.ESC}Ok" -> Key('+'), - s"${Ascii.ESC}Om" -> Key('-'), - s"${Ascii.ESC}On" -> Key('.'), - s"${Ascii.ESC}Oo" -> Key('/'), - s"${Ascii.ESC}Op" -> Key('0'), - s"${Ascii.ESC}Oq" -> Key('1'), - s"${Ascii.ESC}Or" -> Key('2'), - s"${Ascii.ESC}Os" -> Key('3'), - s"${Ascii.ESC}Ot" -> Key('4'), - s"${Ascii.ESC}Ou" -> Key('5'), - s"${Ascii.ESC}Ov" -> Key('6'), - s"${Ascii.ESC}Ow" -> Key('7'), - s"${Ascii.ESC}Ox" -> Key('8'), - s"${Ascii.ESC}Oy" -> Key('9'), - s"${Ascii.ESC}OA" -> Key.up, - s"${Ascii.ESC}OB" -> Key.down, - s"${Ascii.ESC}OC" -> Key.right, - s"${Ascii.ESC}OD" -> Key.left, - s"${Ascii.ESC}OF" -> Key.`end`, - s"${Ascii.ESC}OH" -> Key.home, - s"${Ascii.ESC}OM" -> Key.enter, - s"${Ascii.ESC}OP" -> Key.f1, - s"${Ascii.ESC}OQ" -> Key.f2, - s"${Ascii.ESC}OR" -> Key.f3, - s"${Ascii.ESC}OS" -> Key.f4 + else (2 until s.length).map(s.substring(0, _)).toSet ) +object KeyMappings: lazy val default: Map[Char, Key | KeySequence] = Map( - ' ' -> Key.space, + ' ' -> 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) @@ -218,14 +35,14 @@ object KeyMappings: 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.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) @@ -235,7 +52,7 @@ object KeyMappings: 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.EM -> Key.controlY, // Control-Y (25) Ascii.SUB -> Key.controlZ, // Control-Z '\u009b' -> Key.shiftEscape, @@ -246,3 +63,213 @@ object KeyMappings: '\u007f' -> Key.backspace, Ascii.ESC -> KeySequence(Key.escape, escapeSequences) ) + + import Ascii.ESC + + 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 6f508cd..33efab1 100644 --- a/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala +++ b/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala @@ -38,7 +38,7 @@ trait TerminalKeyReader(timeout: Duration = 100.millis) extends KeyReader { case Eof => Eof case c: Char => KeyMappings.default.get(c) match - case None => Key(c) + case None => Key(c) case Some(k: Key) => k case Some(ks: KeySequence) => read(timeout) match { @@ -51,7 +51,7 @@ trait TerminalKeyReader(timeout: Duration = 100.millis) extends KeyReader { private def readKeySequence(acc: String, sequence: KeySequence): Eof | Key = { (sequence.sequences.get(acc), sequence.subSequences.contains(acc)) match case (Some(key), _) => key - case (None, false) => Key.unknown(acc) + case (None, false) => Key.unknown(acc) case (None, true) => read() match { case Eof => Eof From 8c123b921fef3d9902c77b6c82607f0b108817ff Mon Sep 17 00:00:00 2001 From: Paul J Thordarson Date: Fri, 18 Apr 2025 08:56:14 -0400 Subject: [PATCH 5/6] Additional documentation --- core/shared/src/main/scala/terminus/KeyMappings.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/terminus/KeyMappings.scala b/core/shared/src/main/scala/terminus/KeyMappings.scala index 916d839..232f90b 100644 --- a/core/shared/src/main/scala/terminus/KeyMappings.scala +++ b/core/shared/src/main/scala/terminus/KeyMappings.scala @@ -19,6 +19,9 @@ 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 @@ -63,9 +66,10 @@ object KeyMappings: '\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, From fe4d797a6343e6f02518ef54c6ccfd2adf659282 Mon Sep 17 00:00:00 2001 From: Paul J Thordarson Date: Sun, 4 May 2025 17:58:43 -0400 Subject: [PATCH 6/6] Refactor readKeySequence --- .../scala/terminus/effect/TerminalKeyReader.scala | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala b/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala index 33efab1..97d7017 100644 --- a/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala +++ b/core/shared/src/main/scala/terminus/effect/TerminalKeyReader.scala @@ -49,13 +49,12 @@ trait TerminalKeyReader(timeout: Duration = 100.millis) extends KeyReader { @tailrec private def readKeySequence(acc: String, sequence: KeySequence): Eof | Key = { - (sequence.sequences.get(acc), sequence.subSequences.contains(acc)) match - case (Some(key), _) => key - case (None, false) => Key.unknown(acc) - case (None, true) => - read() match { - case Eof => Eof - case c: Char => readKeySequence(acc :+ c, sequence) - } + 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) } }