12420
Comment:
|
40289
Explicit REPLY argument to read is not required. Fix various other things that drew my attention.
|
Deletions are marked like this. | Additions are marked like this. |
Line 7: | Line 7: |
This is sample code to allow more intuitive usage of Function keys in bash. It abstracts the escape sequence messiness for these keys and as such allows for a more portable/intuitive solutions. |
''This page may not have been peer-reviewed for correctness, usefulness, portability or style. Use at your own risk.'' Ok so mostly the initial response to this is "why bother?", the answer is because it is more robust than simply using read. The main problem with directly calling read is that the escape sequences use digits and letter so if you use something like the following it can lead to unexpected consequences/behavior. |
Line 12: | Line 11: |
#!/bin/bash #set -o errexit #set -o errtrace set -o nounset |
while read -sN1 ; do echo "${REPLY}"; case "${REPLY}" in A) echo 'either "A" or up arrow ';; B) echo 'either "B" or down arrow ';; 1) echo 'either "1" or any function key ';; esac done }}} It's even worse than that because an escape sequence can look something like this. {{{ $'\e[1;12A' }}} So you can just not use the contentious keys "0123456789ABCDEPQRSZ;?+", but no guarantee that that's a full list. A better solution is to use something like this where we delineate each key with a Enter/Return key press. {{{ while read ; do echo "${REPLY}"; case "${REPLY}" in A) echo 'Is A';; 1) echo 'Is 1';; esac done }}} However that makes for a somewhat stilted interface. Now we could also do something like this {{{ ReadKey() { # Wait for first char if read -sN1 _REPLY; then # Read rest of chars while read -sN1 -t 0.001 ; do _REPLY+="${REPLY}" done fi } while ReadKey ; do echo "${_REPLY}"; case "${_REPLY}" in A) echo 'Is A';; 1) echo 'Is 1';; $'\e[A') echo 'Up Arrow' $'\e[1;2A') echo 'Shift + Up Arrow' $'\e[4~' | $'\e[4z') echo 'F4' $'\e[4;2~' | $'\e[4;2z') echo 'Shift + F4' esac done }}} Now the above example is ok if you don't want to require the user to press the enter key and you don't want to use function keys however using function keys in the above manner is a mess there can be something like 4 different escape sequences for a key depending on the emulation mode. Furthermore it is difficult to ignore modifier keys. Technically an escape sequence for an up arrow ignoring the modifier keys requires a regex like this {{{ # Requires extglob case "${_REPLY}" in $'\e['?(+([0-9);+([0-9))A) echo 'Up Arrow Ignoring Modifiers' esac }}} Though mostly this should do {{{ # no extglob case "${_REPLY}" in $'\e[A' | $'\e[1;'[2-9]A) echo 'Up Arrow Ignoring Modifiers' esac }}} The following example builds on the last example and allows a more intuitive usage of Function keys in bash. It abstracts the escape sequence messiness for these keys and as such allows for a more portable/intuitive solutions. As it understands about escape sequences it also protects the caller from the problems with the first example above. Furthermore as no sub shells are required it is quicker than calling an external program or function to read the key. {{{ #!/bin/bash #set -o errexit #set -o errtrace set -o nounset |
Line 27: | Line 96: |
function vt100_DECRST { | vt100_DECRST() { |
Line 30: | Line 99: |
function vt100_DECSET { | vt100_DECSET() { |
Line 43: | Line 112: |
function ord { | ord() { |
Line 46: | Line 115: |
function ord_eascii { | ord_eascii() { |
Line 49: | Line 118: |
function AdjustMousePos { | AdjustMousePos() { |
Line 79: | Line 148: |
# A B C D E F H [0x41]="up" [0x42]="down" [0x43]="right" [0x44]="left" [0x45]="keypad-five" [0x46]="end" [0x48]="home" |
# A B C D E F H [0x41]="up" [0x42]="down" [0x43]="right" [0x44]="left" [0x45]="keypad-five" [0x46]="end" [0x48]="home" |
Line 82: | Line 151: |
[0x49]="InFocus" [0x4f]="OutOfFocus" # P Q R S Z [0x50]="f1" [0x51]="f2" [0x52]="f3" [0x53]="f4" [0x5a]="S-HT" |
[0x49]="InFocus" [0x4f]="OutOfFocus" # P Q R S Z [0x50]="f1" [0x51]="f2" [0x52]="f3" [0x53]="f4" [0x5a]="S-HT" |
Line 87: | Line 156: |
[0x00]="Null" [0x01]="SOH" [0x02]="STX" [0x03]="ETX" [0x04]="EOT" [0x05]="ENQ" [0x06]="ACK" [0x07]="BEL" [0x08]="BS" [0x09]="HT" [0x0A]="LF" [0x0B]="VT" [0x0C]="FF" [0x0D]="CR" [0x0E]="SO" [0x0F]="SI" [0x10]="DLE" [0x11]="DC1" [0x12]="DC2" [0x13]="DC3" [0x14]="DC4" [0x15]="NAK" [0x16]="SYN" [0x17]="ETB" [0x18]="CAN" [0x19]="EM" [0x1A]="SUB" [0x1B]="ESC" [0x1C]="FS" [0x1D]="GS" [0x1E]="RS" [0x1F]="US" [0x20]="SP" [0x7F]="DEL" |
[0x00]="Null" [0x01]="SOH" [0x02]="STX" [0x03]="ETX" [0x04]="EOT" [0x05]="ENQ" [0x06]="ACK" [0x07]="BEL" [0x08]="BS" [0x09]="HT" [0x0A]="LF" [0x0B]="VT" [0x0C]="FF" [0x0D]="CR" [0x0E]="SO" [0x0F]="SI" [0x10]="DLE" [0x11]="DC1" [0x12]="DC2" [0x13]="DC3" [0x14]="DC4" [0x15]="NAK" [0x16]="SYN" [0x17]="ETB" [0x18]="CAN" [0x19]="EM" [0x1A]="SUB" [0x1B]="ESC" [0x1C]="FS" [0x1D]="GS" [0x1E]="RS" [0x1F]="US" [0x20]="SP" [0x7F]="DEL" |
Line 94: | Line 163: |
[0x01]="C-A" [0x02]="C-B" [0x03]="C-C" [0x04]="C-D" [0x05]="C-E" [0x06]="C-F" [0x07]="C-G" [0x08]="C-H" [0x09]="C-I" [0x0a]="C-J" [0x0b]="C-K" [0x0c]="C-L" [0x0d]="C-M" [0x0e]="C-N" [0x0f]="C-O" [0x10]="C-P" [0x11]="C-Q" [0x12]="C-R" [0x13]="C-S" [0x14]="C-T" [0x15]="C-U" [0x16]="C-V" [0x17]="C-W" [0x18]="C-X" [0x19]="C-Y" [0x1a]="C-Z" [0x1b]="C-[" [0x1c]="C-]" [0x1d]="C-}" [0x1e]="C-^" [0x1f]="C-_" [0x20]="C-SP" [0x7F]="DEL" |
[0x01]="C-A" [0x02]="C-B" [0x03]="C-C" [0x04]="C-D" [0x05]="C-E" [0x06]="C-F" [0x07]="C-G" [0x08]="C-H" [0x09]="C-I" [0x0a]="C-J" [0x0b]="C-K" [0x0c]="C-L" [0x0d]="C-M" [0x0e]="C-N" [0x0f]="C-O" [0x10]="C-P" [0x11]="C-Q" [0x12]="C-R" [0x13]="C-S" [0x14]="C-T" [0x15]="C-U" [0x16]="C-V" [0x17]="C-W" [0x18]="C-X" [0x19]="C-Y" [0x1a]="C-Z" [0x1b]="C-[" [0x1c]="C-]" [0x1d]="C-}" [0x1e]="C-^" [0x1f]="C-_" [0x20]="C-SP" [0x7F]="DEL" |
Line 101: | Line 170: |
[0x40]="PAD" [0x41]="HOP" [0x42]="BPH" [0x43]="NBH" [0x44]="IND" [0x45]="NEL" [0x46]="SSA" [0x47]="ESA" [0x48]="HTS" [0x49]="HTJ" [0x4A]="VTS" [0x4B]="PLD" [0x4C]="PLU" [0x4D]="RI" [0x4E]="SS2" [0x4F]="SS3" [0x50]="DCS" [0x51]="PU1" [0x52]="PU2" [0x53]="STS" [0x54]="CCH" [0x55]="MW" [0x56]="SPA" [0x57]="EPA" [0x58]="SOS" [0x59]="SGCI" [0x5A]="SCI" [0x5B]="CSI" [0x5C]="ST" [0x5D]="OSC" [0x5E]="PM" [0x5F]="APC" |
[0x40]="PAD" [0x41]="HOP" [0x42]="BPH" [0x43]="NBH" [0x44]="IND" [0x45]="NEL" [0x46]="SSA" [0x47]="ESA" [0x48]="HTS" [0x49]="HTJ" [0x4A]="VTS" [0x4B]="PLD" [0x4C]="PLU" [0x4D]="RI" [0x4E]="SS2" [0x4F]="SS3" [0x50]="DCS" [0x51]="PU1" [0x52]="PU2" [0x53]="STS" [0x54]="CCH" [0x55]="MW" [0x56]="SPA" [0x57]="EPA" [0x58]="SOS" [0x59]="SGCI" [0x5A]="SCI" [0x5B]="CSI" [0x5C]="ST" [0x5D]="OSC" [0x5E]="PM" [0x5F]="APC" |
Line 111: | Line 180: |
[0x80]="PAD" [0x81]="HOP" [0x82]="BPH" [0x83]="NBH" [0x84]="IND" [0x85]="NEL" [0x86]="SSA" [0x87]="ESA" [0x88]="HTS" [0x89]="HTJ" [0x8A]="VTS" [0x8B]="PLD" [0x8C]="PLU" [0x8D]="RI" [0x8E]="SS2" [0x8F]="SS3" [0x90]="DCS" [0x91]="PU1" [0x92]="PU2" [0x93]="STS" [0x94]="CCH" [0x95]="MW" [0x96]="SPA" [0x97]="EPA" [0x98]="SOS" [0x99]="SGCI" [0x9A]="SCI" [0x9B]="CSI" [0x9C]="ST" [0x9D]="OSC" [0x9E]="PM" [0x9F]="APC" ) |
[0x80]="PAD" [0x81]="HOP" [0x82]="BPH" [0x83]="NBH" [0x84]="IND" [0x85]="NEL" [0x86]="SSA" [0x87]="ESA" [0x88]="HTS" [0x89]="HTJ" [0x8A]="VTS" [0x8B]="PLD" [0x8C]="PLU" [0x8D]="RI" [0x8E]="SS2" [0x8F]="SS3" [0x90]="DCS" [0x91]="PU1" [0x92]="PU2" [0x93]="STS" [0x94]="CCH" [0x95]="MW" [0x96]="SPA" [0x97]="EPA" [0x98]="SOS" [0x99]="SGCI" [0x9A]="SCI" [0x9B]="CSI" [0x9C]="ST" [0x9D]="OSC" [0x9E]="PM" [0x9F]="APC" ) |
Line 121: | Line 190: |
[0x01]="CA-A" [0x02]="CA-B" [0x03]="CA-C" [0x04]="CA-D" [0x05]="CA-E" [0x06]="CA-F" [0x07]="CA-G" [0x08]="CA-H" [0x09]="CA-I" [0x0a]="CA-J" [0x0b]="CA-K" [0x0c]="CA-L" [0x0d]="CA-M" [0x0e]="CA-N" [0x0f]="CA-O" [0x10]="CA-P" [0x11]="CA-Q" [0x12]="CA-R" [0x13]="CA-S" [0x14]="CA-T" [0x15]="CA-U" [0x16]="CA-V" [0x17]="CA-W" [0x18]="CA-X" [0x19]="CA-Y" [0x1a]="CA-Z" [0x1b]="CA-[" [0x1c]="CA-]" [0x1d]="CA-}" [0x1e]="CA-^" [0x1f]="CA-_" [0x20]="CA-SP" [0x7F]="A-DEL" |
[0x01]="CA-A" [0x02]="CA-B" [0x03]="CA-C" [0x04]="CA-D" [0x05]="CA-E" [0x06]="CA-F" [0x07]="CA-G" [0x08]="CA-H" [0x09]="CA-I" [0x0a]="CA-J" [0x0b]="CA-K" [0x0c]="CA-L" [0x0d]="CA-M" [0x0e]="CA-N" [0x0f]="CA-O" [0x10]="CA-P" [0x11]="CA-Q" [0x12]="CA-R" [0x13]="CA-S" [0x14]="CA-T" [0x15]="CA-U" [0x16]="CA-V" [0x17]="CA-W" [0x18]="CA-X" [0x19]="CA-Y" [0x1a]="CA-Z" [0x1b]="CA-[" [0x1c]="CA-]" [0x1d]="CA-}" [0x1e]="CA-^" [0x1f]="CA-_" [0x20]="CA-SP" [0x7F]="A-DEL" |
Line 130: | Line 199: |
[0x40]="MB4-P" [0x41]="MB5-P" | [0x40]="MB4-P" [0x41]="MB5-P" |
Line 133: | Line 202: |
[0x04]="S-" [0x08]="A-" [0x0c]="AS-" | [0x04]="S-" [0x08]="A-" [0x0c]="AS-" |
Line 136: | Line 205: |
function GetMouseButton { | GetMouseButton() { |
Line 146: | Line 215: |
function ReadKey { | ReadKey() { |
Line 148: | Line 217: |
local escapeSequence local REPLY |
local escapeSequence local REPLY |
Line 154: | Line 223: |
[^[:cntrl:]]) UInput[0]="${escapeSequence}" |
[^[:cntrl:]]) UInput[0]="${escapeSequence}" |
Line 163: | Line 232: |
${CSI}t) | ${CSI}t) |
Line 168: | Line 237: |
${CSI}T) | ${CSI}T) |
Line 177: | Line 246: |
${CSI}M*) | ${CSI}M*) |
Line 179: | Line 248: |
if [ -n "${UInput[0]}" ]; then | if [ -n "${UInput[0]}" ]; then |
Line 258: | Line 327: |
}}} | }}} |
Line 260: | Line 329: |
{{{ function HandleKey { |
{{{ HandleKey() { |
Line 266: | Line 335: |
CR|NULL|LF|q) | CR|NULL|LF|q) |
Line 270: | Line 339: |
*up) echo "Up Arrow ignoring modifier keys" ;; down) echo "Down Arrow No modifier keys" ;; S-down) echo "Shift + Down Arrow" ;; A-down) echo "Alt + Down Arrow" ;; AS-down) echo "Alt + Shift + Down Arrow" ;; C-down) echo "Control + Down Arrow" ;; CS-down) echo "Control + Shift + Down Arrow" ;; CA-down) echo "Control + Alt + Down Arrow" ;; CAS-down) echo "Control + Alt + Shift + Down Arrow";; MCAS-down) echo "Meta + Control + Alt + Shift + Down Arrow";; |
|
Line 280: | Line 359: |
The main problem with the previous example is that like most such bash implementations it requires a break between function keys. Now in a user interface this is not a problem because there is always a minimal delay between key presses. However on slower systems it could happen that 2 key preses get evaluated together, the only downside is that in those rare cases the keys get ignored. But this also means that it can't be used with buffered function key sequences. === vt100 Parser Example === A more complex solution using a full vt100 parser. This following solution fixes the problems of the previous example as it doesn't blindly gobble up the entire buffer but instead intelligently reads the key sequence. The downside is that it is quite a bit more complex, and takes about 0.005s per function key instead of 0.001 sec. {{{ declare -gri VTPARSE_ACTION_CLEAR="1" declare -gri VTPARSE_ACTION_COLLECT="2" declare -gri VTPARSE_ACTION_CSI_DISPATCH="3" declare -gri VTPARSE_ACTION_ESC_DISPATCH="4" declare -gri VTPARSE_ACTION_ESC_EXECUTE="5" declare -gri VTPARSE_ACTION_EXECUTE="6" declare -gri VTPARSE_ACTION_HOOK="7" declare -gri VTPARSE_ACTION_IGNORE="8" declare -gri VTPARSE_ACTION_OSC_END="9" declare -gri VTPARSE_ACTION_OSC_PUT="10" declare -gri VTPARSE_ACTION_OSC_START="11" declare -gri VTPARSE_ACTION_PARAM="12" declare -gri VTPARSE_ACTION_PRINT="13" declare -gri VTPARSE_ACTION_PUT="14" declare -gri VTPARSE_ACTION_SS2_DISPATCH="15" declare -gri VTPARSE_ACTION_SS3_DISPATCH="16" declare -gri VTPARSE_ACTION_UNHOOK="17" declare -gra VTPARSE_ACTION_NAMES=( [0 ]="<no action>" [${VTPARSE_ACTION_CLEAR}]="CLEAR" [${VTPARSE_ACTION_COLLECT}]="COLLECT" [${VTPARSE_ACTION_CSI_DISPATCH}]="CSI_DISPATCH" [${VTPARSE_ACTION_ESC_DISPATCH}]="ESC_DISPATCH" [${VTPARSE_ACTION_ESC_EXECUTE}]="ESC_EXECUTE" [${VTPARSE_ACTION_EXECUTE}]="EXECUTE" [${VTPARSE_ACTION_HOOK}]="HOOK" [${VTPARSE_ACTION_IGNORE}]="IGNORE" [${VTPARSE_ACTION_OSC_END}]="OSC_END" [${VTPARSE_ACTION_OSC_PUT}]="OSC_PUT" [${VTPARSE_ACTION_OSC_START}]="OSC_START" [${VTPARSE_ACTION_PARAM}]="PARAM" [${VTPARSE_ACTION_PRINT}]="PRINT" [${VTPARSE_ACTION_PUT}]="PUT" [${VTPARSE_ACTION_SS2_DISPATCH}]="SS2_DISPATCH" [${VTPARSE_ACTION_SS3_DISPATCH}]="SS3_DISPATCH" [${VTPARSE_ACTION_UNHOOK}]="UNHOOK" ) declare -gri VTPARSE_STATE_ANYWHERE="0" declare -gri VTPARSE_STATE_CSI_ENTRY="1" declare -gri VTPARSE_STATE_CSI_IGNORE="2" declare -gri VTPARSE_STATE_CSI_INTERMEDIATE="3" declare -gri VTPARSE_STATE_CSI_PARAM="4" declare -gri VTPARSE_STATE_DCS_ENTRY="5" declare -gri VTPARSE_STATE_DCS_IGNORE="6" declare -gri VTPARSE_STATE_DCS_INTERMEDIATE="7" declare -gri VTPARSE_STATE_DCS_PARAM="8" declare -gri VTPARSE_STATE_DCS_PASSTHROUGH="9" declare -gri VTPARSE_STATE_ESCAPE="10" declare -gri VTPARSE_STATE_ESCAPE_INTERMEDIATE="11" declare -gri VTPARSE_STATE_GROUND="12" declare -gri VTPARSE_STATE_OSC_STRING="13" declare -gri VTPARSE_STATE_SOS_PM_APC_STRING="14" declare -gri VTPARSE_STATE_SS2_ENTRY="15" declare -gri VTPARSE_STATE_SS3_ENTRY="16" declare -gri VTPARSE_STATE_SS3_PARAM="17" declare -gra VTPARSE_STATE_NAMES=( [${VTPARSE_STATE_ANYWHERE}]="ANYWHERE" [${VTPARSE_STATE_CSI_ENTRY}]="CSI_ENTRY" [${VTPARSE_STATE_CSI_IGNORE}]="CSI_IGNORE" [${VTPARSE_STATE_CSI_INTERMEDIATE}]="CSI_INTERMEDIATE" [${VTPARSE_STATE_CSI_PARAM}]="CSI_PARAM" [${VTPARSE_STATE_DCS_ENTRY}]="DCS_ENTRY" [${VTPARSE_STATE_DCS_IGNORE}]="DCS_IGNORE" [${VTPARSE_STATE_DCS_INTERMEDIATE}]="DCS_INTERMEDIATE" [${VTPARSE_STATE_DCS_PARAM}]="DCS_PARAM" [${VTPARSE_STATE_DCS_PASSTHROUGH}]="DCS_PASSTHROUGH" [${VTPARSE_STATE_ESCAPE}]="ESCAPE" [${VTPARSE_STATE_ESCAPE_INTERMEDIATE}]="ESCAPE_INTERMEDIATE" [${VTPARSE_STATE_GROUND}]="GROUND" [${VTPARSE_STATE_OSC_STRING}]="OSC_STRING" [${VTPARSE_STATE_SOS_PM_APC_STRING}]="SOS_PM_APC_STRING" [${VTPARSE_STATE_SS2_ENTRY}]="SS2_ENTRY" [${VTPARSE_STATE_SS3_ENTRY}]="SS3_ENTRY" [${VTPARSE_STATE_SS3_PARAM}]="SS3_PARAM" ) declare -gra VTPARSE_STATE_TABLE=( [${VTPARSE_STATE_ANYWHERE}]="0000000000000000000000000000000000000000000000006c006c0a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006c6c6c6c6c6c6c6c6c6c6c6c6c6c0f6c056c6c6c6c6c6c6c0e6c6c016c0d0e0e" [${VTPARSE_STATE_CSI_ENTRY}]="606060606060606060606060606060606060606060606060006000006060606023232323232323232323232323232323c4c4c4c4c4c4c4c4c4c402c4242424243c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c80" [${VTPARSE_STATE_CSI_IGNORE}]="606060606060606060606060606060606060606060606060006000006060606080808080808080808080808080808080808080808080808080808080808080800c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c80" [${VTPARSE_STATE_CSI_INTERMEDIATE}]="606060606060606060606060606060606060606060606060006000006060606020202020202020202020202020202020020202020202020202020202020202023c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c80" [${VTPARSE_STATE_CSI_PARAM}]="606060606060606060606060606060606060606060606060006000006060606023232323232323232323232323232323c0c0c0c0c0c0c0c0c0c002c0020202023c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c80" [${VTPARSE_STATE_DCS_ENTRY}]="808080808080808080808080808080808080808080808080008000008080808027272727272727272727272727272727c8c8c8c8c8c8c8c8c8c806c82828282809090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090980" [${VTPARSE_STATE_DCS_IGNORE}]="8080808080808080808080808080808080808080808080800080000080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080000000000000000000000000000000000000000000000000000000000c" [${VTPARSE_STATE_DCS_INTERMEDIATE}]="8080808080808080808080808080808080808080808080800080000080808080202020202020202020202020202020200606060606060606060606060606060609090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090980" [${VTPARSE_STATE_DCS_PARAM}]="808080808080808080808080808080808080808080808080008000008080808027272727272727272727272727272727c0c0c0c0c0c0c0c0c0c006c00606060609090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090980" [${VTPARSE_STATE_DCS_PASSTHROUGH}]="e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e000e00000e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e080000000000000000000000000000000000000000000000000000000000c" [${VTPARSE_STATE_ESCAPE}]="50505050505050505050505050505050505050505050505000500000505050502b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c0f0g054c4c4c4c4c4c4c0e4c4c014c0d0e0e4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c5c" [${VTPARSE_STATE_ESCAPE_INTERMEDIATE}]="6060606060606060606060606060606060606060606060600060000060606060202020202020202020202020202020204c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c80" [${VTPARSE_STATE_GROUND}]="6060606060606060606060606060606060606060606060600060000060606060d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d06060606060606060606060606060606000606060606060606060600060" [${VTPARSE_STATE_OSC_STRING}]="8080808080808080808080808080808080808080808080800080000080808080a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0000000000000000000000000000000000000000000000000000000000c" [${VTPARSE_STATE_SOS_PM_APC_STRING}]="8080808080808080808080808080808080808080808080800080000080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080000000000000000000000000000000000000000000000000000000000c" [${VTPARSE_STATE_SS2_ENTRY}]="60606060606060606060606060606060606060606060606000600000606060600000000000000000000000000000000000000000000000000000000000000000fcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfc80" [${VTPARSE_STATE_SS3_ENTRY}]="606060606060606060606060606060606060606060606060006000006060606000000000000000000000000000000000chchchchchchchchchch000000000000gcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgc80" [${VTPARSE_STATE_SS3_PARAM}]="606060606060606060606060606060606060606060606060006000006060606000000000000000000000000000000000c0c0c0c0c0c0c0c0c0c0000000000000gcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgc80" ) declare -gr VTPARSE_BASE="0123456789abcdefghijklmnopqrstuvwxyz" declare -gr VTPARSE_ENTRY_ACTIONS="0100010007100b0110" declare -gr VTPARSE_EXIT_ACTIONS="000000000h00090000" ord() { printf -v "${1?Missing Dest Variable}" "${3:-%d}" "'${2?Missing Char}" } ord_eascii() { LC_CTYPE=C ord "${@}" } substr() { local idx typeset -gi _INDEX case "${1}" in *${2}*) idx="${1%"${2}"*}"; _INDEX=${#idx} ;; *) _INDEX=0 ;; esac } declare -gr MAX_INTERMEDIATE_CHARS=2 declare -gi vtparser_state declare -g vtparser_intermediate_chars declare -ga vtparser_params declare -gi vtparser_ignore_flagged declare -g vtparser_cb declare -g vtparser_read vtparse_init() { vtparser_state="${VTPARSE_STATE_GROUND}" vtparser_intermediate_chars="" vtparser_params=( ) vtparser_ignore_flagged=0 vtparser_cb=${1?Missing Callback Function} vtparser_read=${2?Missing Read Function} } nop() { echo -n "" } do_action() { # Some actions we handle internally (like parsing parameters), others # we hand to our client for processing case "${1}" in ${VTPARSE_ACTION_ESC_EXECUTE}|${VTPARSE_ACTION_PRINT}|${VTPARSE_ACTION_EXECUTE}|${VTPARSE_ACTION_HOOK}|${VTPARSE_ACTION_PUT}|${VTPARSE_ACTION_OSC_START}|${VTPARSE_ACTION_OSC_PUT}|${VTPARSE_ACTION_OSC_END}|${VTPARSE_ACTION_UNHOOK}|${VTPARSE_ACTION_CSI_DISPATCH}|${VTPARSE_ACTION_ESC_DISPATCH}|${VTPARSE_STATE_SS2_ENTRY}|${VTPARSE_STATE_SS3_ENTRY}) ${vtparser_cb} "${@}" || return $? ;; ${VTPARSE_ACTION_IGNORE}) nop ;; ${VTPARSE_ACTION_COLLECT}) if [ ${#vtparser_intermediate_chars} -ge ${MAX_INTERMEDIATE_CHARS} ]; then vtparser_ignore_flagged=1 else vtparser_intermediate_chars+="${2}"; fi ;; ${VTPARSE_ACTION_PARAM}) # process the param character if [ "${2}" = ";" ]; then vtparser_params[${#vtparser_params[@]}]=""; else # the character is a digit if [ ${#vtparser_params[@]} -eq 0 ]; then vtparser_params[0]="" fi vtparser_params[${#vtparser_params[@]}-1]+="${2}" fi ;; ${VTPARSE_ACTION_CLEAR}) vtparser_intermediate_chars="" vtparser_params=( ) vtparser_ignore_flagged=0 ;; *) echo "Unknown action ${1}" ;; esac return 0 } vtparse_conv_base() { local _INDEX substr "${VTPARSE_BASE}" "${2:0:1}" eval ${1}'="${_INDEX}"' } do_state_change() { # A state change is an action and/or a new state to transition to. local -i action local -i new_state local -i RValue=0 vtparse_conv_base action "${1:0:1}" vtparse_conv_base new_state "${1:1:1}" #printf "action=%s new_state=%s vtparser_state=%s change=%s\n" "${action}" "${new_state}" "${vtparser_state}" "${1}" if [ "${new_state:=0}" != "0" ]; then # Perform up to three actions: # 1. the exit action of the old state # 2. the action associated with the transition # 3. the entry actionk of the new action local -i exit_action local -i entry_action vtparse_conv_base exit_action "${VTPARSE_EXIT_ACTIONS:${vtparser_state}:1}" vtparse_conv_base entry_action "${VTPARSE_ENTRY_ACTIONS:${new_state}:1}" [ "${exit_action}" = "0" ] || do_action "${exit_action}" $'\x00' 0 [ "${action}" = "0" ] || do_action "${action}" "${2}" "${3}" || RValue=$? [ "${entry_action}" = "0" ] || do_action "${entry_action}" $'\x00' 0 vtparser_state=${new_state} return ${RValue} else [ "${action}" = "0" ] || do_action "${action}" "${2}" "${3}" || return $? fi } vtparse() { local -i _INDEX local REPLY while ${vtparser_read} 1 ; do ord_eascii _INDEX "${REPLY:0:1}" # If a transition is defined from the "anywhere" state, always # use that. Otherwise use the transition from the current state. _INDEX=${_INDEX}*2 # 2 characters per entry local change="${VTPARSE_STATE_TABLE[${VTPARSE_STATE_ANYWHERE}]:${_INDEX}:2}" [ "${change:-00}" != "00" ] || change="${VTPARSE_STATE_TABLE[${vtparser_state}]:${_INDEX}:2}" _INDEX=${_INDEX}/2 do_state_change "${change}" "${REPLY}" "${_INDEX}" || break done } vtparse_read_stdin() { read -srN${1:-1} -d '' "${@:2}" } declare -g vtparse_read_buffer vtparse_read_buffer() { REPLY="${vtparse_read_buffer:0:${1:-1}}" vtparse_read_buffer="${vtparse_read_buffer:${1}}" [ ${#REPLY} -eq ${1} ] || return $? } vtparser_callback_debug() { local CArg printf "Received action %s, char=0x%02x char=%q\n" "${VTPARSE_ACTION_NAMES[${1}]}" "'${2}" "${2}" printf "Intermediate chars: '%s'\n" "${vtparser_intermediate_chars}" printf "%d Parameters:\n" ${#vtparser_params[@]} for CArg in "${vtparser_params[@]-}"; do printf "\t%d\n" "${CArg}" done printf "\n" } ############################### ## ## READ KEY CRAP ## ## ############################### KeyModifiers=( [1]="" [2]="S-" [3]="A-" [4]="AS-" [5]="C-" [6]="CS-" [7]="CA-" [8]="CAS-" [9]="M-" [10]="MS-" [11]="MA-" [12]="MAS-" [13]="MC-" [14]="MCS-" [15]="MCA-" [16]="MCAS-" ) KeybFntKeys=( [1]="home" [2]="insert" [3]="delete" [4]="end" [5]="pageUp" [6]="pageDown" [11]="f1" [12]="f2" [13]="f3" [14]="f4" [15]="f5" [17]="f6" [18]="f7" [19]="f8" [20]="f9" [21]="f10" [23]="f11" [24]="f12" [25]="f13" [26]="f14" [28]="f15" [29]="f16" [31]="f17" [32]="f18" [33]="f19" [34]="f20" ) SunKeybFntKeys=( [214]="home" [2]="insert" [3]="delete" [4]="end" [216]="pageUp" [222]="pageDown" [224]="f1" [225]="f2" [226]="f3" [227]="f4" [228]="f5" [229]="f6" [230]="f7" [231]="f8" [232]="f9" [233]="f10" [192]="f11" [193]="f12" [218]="keypad-five" [220]="keypad-delete" ) KeybFntKeysAlt=( # A B C D E F H [0x41]="up" [0x42]="down" [0x43]="right" [0x44]="left" [0x45]="keypad-five" [0x46]="end" [0x48]="home" # I O [0x49]="InFocus" [0x4f]="OutOfFocus" # P Q R S Z [0x50]="f1" [0x51]="f2" [0x52]="f3" [0x53]="f4" [0x5a]="S-HT" ) C0CtrlChars=( [0x00]="Null" [0x01]="SOH" [0x02]="STX" [0x03]="ETX" [0x04]="EOT" [0x05]="ENQ" [0x06]="ACK" [0x07]="BEL" [0x08]="BS" [0x09]="HT" [0x0A]="LF" [0x0B]="VT" [0x0C]="FF" [0x0D]="CR" [0x0E]="SO" [0x0F]="SI" [0x10]="DLE" [0x11]="DC1" [0x12]="DC2" [0x13]="DC3" [0x14]="DC4" [0x15]="NAK" [0x16]="SYN" [0x17]="ETB" [0x18]="CAN" [0x19]="EM" [0x1A]="SUB" [0x1B]="ESC" [0x1C]="FS" [0x1D]="GS" [0x1E]="RS" [0x1F]="US" [0x20]="SP" [0x7F]="DEL" ) C0CtrlCharsAlt=( [0x01]="C-A" [0x02]="C-B" [0x03]="C-C" [0x04]="C-D" [0x05]="C-E" [0x06]="C-F" [0x07]="C-G" [0x08]="C-H" [0x09]="C-I" [0x0a]="C-J" [0x0b]="C-K" [0x0c]="C-L" [0x0d]="C-M" [0x0e]="C-N" [0x0f]="C-O" [0x10]="C-P" [0x11]="C-Q" [0x12]="C-R" [0x13]="C-S" [0x14]="C-T" [0x15]="C-U" [0x16]="C-V" [0x17]="C-W" [0x18]="C-X" [0x19]="C-Y" [0x1a]="C-Z" [0x1b]="C-[" [0x1c]="C-]" [0x1d]="C-}" [0x1e]="C-^" [0x1f]="C-_" [0x20]="C-SP" [0x7F]="DEL" ) C1CtrlCharsEsc=( [0x40]="PAD" [0x41]="HOP" [0x42]="BPH" [0x43]="NBH" [0x44]="IND" [0x45]="NEL" [0x46]="SSA" [0x47]="ESA" [0x48]="HTS" [0x49]="HTJ" [0x4A]="VTS" [0x4B]="PLD" [0x4C]="PLU" [0x4D]="RI" [0x4E]="SS2" [0x4F]="SS3" [0x50]="DCS" [0x51]="PU1" [0x52]="PU2" [0x53]="STS" [0x54]="CCH" [0x55]="MW" [0x56]="SPA" [0x57]="EPA" [0x58]="SOS" [0x59]="SGCI" [0x5A]="SCI" [0x5B]="CSI" [0x5C]="ST" [0x5D]="OSC" [0x5E]="PM" [0x5F]="APC" ) C1CtrlChars=( [0x80]="PAD" [0x81]="HOP" [0x82]="BPH" [0x83]="NBH" [0x84]="IND" [0x85]="NEL" [0x86]="SSA" [0x87]="ESA" [0x88]="HTS" [0x89]="HTJ" [0x8A]="VTS" [0x8B]="PLD" [0x8C]="PLU" [0x8D]="RI" [0x8E]="SS2" [0x8F]="SS3" [0x90]="DCS" [0x91]="PU1" [0x92]="PU2" [0x93]="STS" [0x94]="CCH" [0x95]="MW" [0x96]="SPA" [0x97]="EPA" [0x98]="SOS" [0x99]="SGCI" [0x9A]="SCI" [0x9B]="CSI" [0x9C]="ST" [0x9D]="OSC" [0x9E]="PM" [0x9F]="APC" ) C1CtrlCharsAlt=( [0x01]="CA-A" [0x02]="CA-B" [0x03]="CA-C" [0x04]="CA-D" [0x05]="CA-E" [0x06]="CA-F" [0x07]="CA-G" [0x08]="CA-H" [0x09]="CA-I" [0x0a]="CA-J" [0x0b]="CA-K" [0x0c]="CA-L" [0x0d]="CA-M" [0x0e]="CA-N" [0x0f]="CA-O" [0x10]="CA-P" [0x11]="CA-Q" [0x12]="CA-R" [0x13]="CA-S" [0x14]="CA-T" [0x15]="CA-U" [0x16]="CA-V" [0x17]="CA-W" [0x18]="CA-X" [0x19]="CA-Y" [0x1a]="CA-Z" [0x1b]="CA-[" [0x1c]="CA-]" [0x1d]="CA-}" [0x1e]="CA-^" [0x1f]="CA-_" [0x20]="CA-SP" [0x7F]="A-DEL" ) MouseButtons=( [0x00]="MB1-P" [0x01]="MB2-P" [0x02]="MB3-P" [0x03]="MB-R" [0x20]="MB1-M" [0x21]="MB2-M" [0x22]="MB3-M" [0x23]="MB-M" [0x40]="MB4-P" [0x41]="MB5-P" ) MouseMetaButtons=( [0x04]="S-" [0x08]="A-" [0x0c]="AS-" [0x10]="C-" [0x14]="CS-" [0x1c]="CAS-" ) AdjustMousePos() { local -i _INDEX ord_eascii _INDEX "${2}" eval ${1}'=$(( ${_INDEX}-32))' } GetMouseButton() { local MouseBtn AdjustMousePos MouseBtn "${2}" MouseBtn="${MouseMetaButtons[$(( ${MouseBtn} & 0x1C))]-}${MouseButtons[$(( ${MouseBtn} & 0xe3))]}" eval ${1}='"${MouseBtn}"' } vtparser_callback_readkey() { unset UInput[@] case "${1}" in ${VTPARSE_ACTION_ESC_EXECUTE}) case "${2}" in [[:cntrl:]]) UInput[0]="${C1CtrlCharsAlt[${3}]:-$(printf "%q" $'\e'"${2}")}" ;; *) UInput[0]="${C1CtrlChars[${3}]:-$(printf "%q" $'\e'"${2}")}" ;; esac ;; ${VTPARSE_ACTION_EXECUTE}) UInput[0]="${C0CtrlChars[${3}]:-$(printf "%q" "${2}")}" ;; ${VTPARSE_ACTION_CSI_DISPATCH}) case "${2}" in z) # Sun Function Keys UInput[0]="${SunKeybFntKeys[${vtparser_params[0]}]:-}" if [ -n "${UInput[0]}" ]; then UInput[0]="${KeyModifiers[${vtparser_params[1]:-1}]}${UInput[0]}" UInput[1]="1" # Repeat Count else UInput[0]="CSI ${vtparser_params[*]} ${2}" fi ;; '~') # Function Keys UInput[0]="${KeybFntKeys[${vtparser_params[0]}]}" if [ -n "${UInput[0]}" ]; then UInput[0]="${KeyModifiers[${vtparser_params[1]:-1}]}${UInput[0]}" UInput[1]="1" # Repeat Count else UInput[0]="CSI ${vtparser_params[*]} ${2}" fi ;; A|B|C|D|E|F|H|I|O|Z|P|Q|R|S) UInput[0]="${KeybFntKeysAlt[${3}]:-}" if [ -n "${UInput[0]}" ]; then UInput[0]="${KeyModifiers[${vtparser_params[1]:-1}]}${UInput[0]}" UInput[1]="${vtparser_params[0]:-1}" # Repeat Count else UInput[0]="CSI ${vtparser_params[*]} ${2}" fi ;; t) ${vtparser_read} 2 UInput[0]="MouseTrack" AdjustMousePos UInput[1] "${REPLY:0:1}" AdjustMousePos UInput[2] "${REPLY:1:1}" ;; T) ${vtparser_read} 6 UInput[0]="MouseTrack" AdjustMousePos UInput[1] "${REPLY:0:1}" AdjustMousePos UInput[2] "${REPLY:1:1}" AdjustMousePos UInput[3] "${REPLY:2:1}" AdjustMousePos UInput[4] "${REPLY:3:1}" AdjustMousePos UInput[5] "${REPLY:4:1}" AdjustMousePos UInput[6] "${REPLY:5:1}" ;; M) # Mouse ${vtparser_read} 3 GetMouseButton UInput[0] "${REPLY:0:1}" if [ -n "${UInput[0]}" ]; then AdjustMousePos UInput[1] "${REPLY:1:1}" AdjustMousePos UInput[2] "${REPLY:2:1}" else UInput[0]=$(printf 'Mouse-\\x%02x %q' "'${escapeSequence:0:1}" "${escapeSequence:1}") fi ;; *) UInput[0]="CSI ${vtparser_params[*]} ${2}" ;; esac ;; ${VTPARSE_ACTION_SS3_DISPATCH}) case "${2}" in A|B|C|D|E|F|H|P|Q|R|S|~) UInput[0]+="${KeybFntKeysAlt[${3}]}" if [ -n "${UInput[0]}" ]; then UInput[0]="${KeyModifiers[${vtparser_params[1]:-1}]}${UInput[0]}" UInput[1]="${vtparser_params[0]:-1}" # Repeat Count else UInput[0]="SS3 ${vtparser_params[*]} ${2}" fi ;; *) UInput[0]="SS3 ${vtparser_params[*]} ${2}" ;; esac ;; ${VTPARSE_ACTION_ESC_DISPATCH}) case "${2}" in [][}^_A-Z]) UInput[0]="${C1CtrlChars[${3}]}" ;; [^[:cntrl:]]) UInput[0]="CA-${2}" ;; *) UInput[0]="${C1CtrlChars[${3}]:-$(printf "%q" $'\e'"${2}")}" ;; esac ;; ${VTPARSE_ACTION_PRINT}) case "${2}" in [^[:cntrl:]]) UInput[0]="${2}" ;; *) UInput[0]="${C0CtrlChars[${3}]:-$(printf "%q" $'\e'"${2}")}" ;; esac ;; *) vtparser_callback_debug "${@}" || return $? ;; esac return 1 } HandleKey() { vtparse_init vtparser_callback_readkey vtparse_read_stdin while true; do vtparse case "${UInput[0]}" in LF) echo "${UInput[@]}" break ;; *up) echo "Up Arrow ignoring modifier keys" ;; down) echo "Down Arrow No modifier keys" ;; S-down) echo "Shift + Down Arrow" ;; A-down) echo "Alt + Down Arrow" ;; AS-down) echo "Alt + Shift + Down Arrow" ;; C-down) echo "Control + Down Arrow" ;; CS-down) echo "Control + Shift + Down Arrow" ;; CA-down) echo "Control + Alt + Down Arrow" ;; CAS-down) echo "Control + Alt + Shift + Down Arrow";; MCAS-down) echo "Meta + Control + Alt + Shift + Down Arrow";; *) echo "${UInput[@]}" ;; esac done } HandleKey }}} |
Reading Function Keys in bash
This page may not have been peer-reviewed for correctness, usefulness, portability or style. Use at your own risk.
Ok so mostly the initial response to this is "why bother?", the answer is because it is more robust than simply using read. The main problem with directly calling read is that the escape sequences use digits and letter so if you use something like the following it can lead to unexpected consequences/behavior.
while read -sN1 ; do echo "${REPLY}"; case "${REPLY}" in A) echo 'either "A" or up arrow ';; B) echo 'either "B" or down arrow ';; 1) echo 'either "1" or any function key ';; esac done
It's even worse than that because an escape sequence can look something like this.
$'\e[1;12A'
So you can just not use the contentious keys "0123456789ABCDEPQRSZ;?+", but no guarantee that that's a full list.
A better solution is to use something like this where we delineate each key with a Enter/Return key press.
while read ; do echo "${REPLY}"; case "${REPLY}" in A) echo 'Is A';; 1) echo 'Is 1';; esac done
However that makes for a somewhat stilted interface.
Now we could also do something like this
ReadKey() { # Wait for first char if read -sN1 _REPLY; then # Read rest of chars while read -sN1 -t 0.001 ; do _REPLY+="${REPLY}" done fi } while ReadKey ; do echo "${_REPLY}"; case "${_REPLY}" in A) echo 'Is A';; 1) echo 'Is 1';; $'\e[A') echo 'Up Arrow' $'\e[1;2A') echo 'Shift + Up Arrow' $'\e[4~' | $'\e[4z') echo 'F4' $'\e[4;2~' | $'\e[4;2z') echo 'Shift + F4' esac done
Now the above example is ok if you don't want to require the user to press the enter key and you don't want to use function keys however using function keys in the above manner is a mess there can be something like 4 different escape sequences for a key depending on the emulation mode. Furthermore it is difficult to ignore modifier keys. Technically an escape sequence for an up arrow ignoring the modifier keys requires a regex like this
# Requires extglob case "${_REPLY}" in $'\e['?(+([0-9);+([0-9))A) echo 'Up Arrow Ignoring Modifiers' esac
Though mostly this should do
# no extglob case "${_REPLY}" in $'\e[A' | $'\e[1;'[2-9]A) echo 'Up Arrow Ignoring Modifiers' esac
The following example builds on the last example and allows a more intuitive usage of Function keys in bash. It abstracts the escape sequence messiness for these keys and as such allows for a more portable/intuitive solutions. As it understands about escape sequences it also protects the caller from the problems with the first example above. Furthermore as no sub shells are required it is quicker than calling an external program or function to read the key.
#set -o errexit #set -o errtrace set -o nounset if [ "${S8C1T:-0}" != "1" ] ; then declare -gr SS3=$'\eO' # Single Shift Select of G3 Character Set ( SS3 is 0x8f): affects next character only declare -gr CSI=$'\e[' # Control Sequence Introducer ( CSI is 0x9b) else declare -gr SS3=$'\x8f' # Single Shift Select of G3 Character Set ( SS3 is 0x8f): affects next character only declare -gr CSI=$'\x9b' # Control Sequence Introducer ( CSI is 0x9b) fi vt100_DECRST() { IFS=';' eval 'echo -n "${CSI}?${*?Missing Pm}l"' } vt100_DECSET() { IFS=';' eval 'echo -n "${CSI}?${*?Missing Pm}h"' } mouse_type=( [0]=9 ## X10 mouse reporting, for compatibility with X10's xterm, reports on button press. [1]=1000 ## X11 mouse reporting, reports on button press and release. [2]=1001 ## highlight reporting, useful for reporting mouse highlights [3]=1002 ## button movement reporting, reports movement when a button is presse [4]=1003 ## all movement reporting, reports all movements. [5]=1004 ## FocusIn/FocusOut can be combined with any of the mouse events since it uses a different protocol. When set, it causes xterm to send CSI I when the terminal gains focus, and CSI O when it loses focus. [6]=1005 ## Extended mouse mode enables UTF-8 encoding for C x and C y under all tracking modes, expanding the maximum encodable position from 223 to 2015. For positions less than 95, the resulting output is identical under both modes. Under extended mouse mode, positions greater than 95 generate "extra" bytes which will confuse applications which do not treat their input as a UTF-8 stream. Likewise, C b will be UTF-8 encoded, to reduce confusion with wheel mouse events. ) ord() { printf -v "${1?Missing Dest Variable}" "${3:-%d}" "'${2?Missing Char}" } ord_eascii() { LC_CTYPE=C ord "${@}" } AdjustMousePos() { local -i _INDEX ord_eascii _INDEX "${2}" eval ${1}'=$(( ${_INDEX}-32))' } ############################### ## ## READ KEY CRAP ## ## ############################### KeyModifiers=( [2]="S" [3]="A" [4]="AS" [5]="C" [6]="CS" [7]="CA" [8]="CAS" [9]="M" [10]="MS" [11]="MA" [12]="MAS" [13]="MC" [14]="MCS" [15]="MCA" [16]="MCAS" ) KeybFntKeys=( [1]="home" [2]="insert" [3]="delete" [4]="end" [5]="pageUp" [6]="pageDown" [11]="f1" [12]="f2" [13]="f3" [14]="f4" [15]="f5" [17]="f6" [18]="f7" [19]="f8" [20]="f9" [21]="f10" [23]="f11" [24]="f12" [25]="f13" [26]="f14" [28]="f15" [29]="f16" [31]="f17" [32]="f18" [33]="f19" [34]="f20" ) SunKeybFntKeys=( [214]="home" [2]="insert" [3]="delete" [4]="end" [216]="pageUp" [222]="pageDown" [224]="f1" [225]="f2" [226]="f3" [227]="f4" [228]="f5" [229]="f6" [230]="f7" [231]="f8" [232]="f9" [233]="f10" [192]="f11" [193]="f12" [218]="keypad-five" [220]="keypad-delete" ) KeybFntKeysAlt=( # A B C D E F H [0x41]="up" [0x42]="down" [0x43]="right" [0x44]="left" [0x45]="keypad-five" [0x46]="end" [0x48]="home" # I O [0x49]="InFocus" [0x4f]="OutOfFocus" # P Q R S Z [0x50]="f1" [0x51]="f2" [0x52]="f3" [0x53]="f4" [0x5a]="S-HT" ) C0CtrlChars=( [0x00]="Null" [0x01]="SOH" [0x02]="STX" [0x03]="ETX" [0x04]="EOT" [0x05]="ENQ" [0x06]="ACK" [0x07]="BEL" [0x08]="BS" [0x09]="HT" [0x0A]="LF" [0x0B]="VT" [0x0C]="FF" [0x0D]="CR" [0x0E]="SO" [0x0F]="SI" [0x10]="DLE" [0x11]="DC1" [0x12]="DC2" [0x13]="DC3" [0x14]="DC4" [0x15]="NAK" [0x16]="SYN" [0x17]="ETB" [0x18]="CAN" [0x19]="EM" [0x1A]="SUB" [0x1B]="ESC" [0x1C]="FS" [0x1D]="GS" [0x1E]="RS" [0x1F]="US" [0x20]="SP" [0x7F]="DEL" ) C0CtrlCharsAlt=( [0x01]="C-A" [0x02]="C-B" [0x03]="C-C" [0x04]="C-D" [0x05]="C-E" [0x06]="C-F" [0x07]="C-G" [0x08]="C-H" [0x09]="C-I" [0x0a]="C-J" [0x0b]="C-K" [0x0c]="C-L" [0x0d]="C-M" [0x0e]="C-N" [0x0f]="C-O" [0x10]="C-P" [0x11]="C-Q" [0x12]="C-R" [0x13]="C-S" [0x14]="C-T" [0x15]="C-U" [0x16]="C-V" [0x17]="C-W" [0x18]="C-X" [0x19]="C-Y" [0x1a]="C-Z" [0x1b]="C-[" [0x1c]="C-]" [0x1d]="C-}" [0x1e]="C-^" [0x1f]="C-_" [0x20]="C-SP" [0x7F]="DEL" ) C1CtrlCharsEsc=( [0x40]="PAD" [0x41]="HOP" [0x42]="BPH" [0x43]="NBH" [0x44]="IND" [0x45]="NEL" [0x46]="SSA" [0x47]="ESA" [0x48]="HTS" [0x49]="HTJ" [0x4A]="VTS" [0x4B]="PLD" [0x4C]="PLU" [0x4D]="RI" [0x4E]="SS2" [0x4F]="SS3" [0x50]="DCS" [0x51]="PU1" [0x52]="PU2" [0x53]="STS" [0x54]="CCH" [0x55]="MW" [0x56]="SPA" [0x57]="EPA" [0x58]="SOS" [0x59]="SGCI" [0x5A]="SCI" [0x5B]="CSI" [0x5C]="ST" [0x5D]="OSC" [0x5E]="PM" [0x5F]="APC" ) C1CtrlChars=( [0x80]="PAD" [0x81]="HOP" [0x82]="BPH" [0x83]="NBH" [0x84]="IND" [0x85]="NEL" [0x86]="SSA" [0x87]="ESA" [0x88]="HTS" [0x89]="HTJ" [0x8A]="VTS" [0x8B]="PLD" [0x8C]="PLU" [0x8D]="RI" [0x8E]="SS2" [0x8F]="SS3" [0x90]="DCS" [0x91]="PU1" [0x92]="PU2" [0x93]="STS" [0x94]="CCH" [0x95]="MW" [0x96]="SPA" [0x97]="EPA" [0x98]="SOS" [0x99]="SGCI" [0x9A]="SCI" [0x9B]="CSI" [0x9C]="ST" [0x9D]="OSC" [0x9E]="PM" [0x9F]="APC" ) C1CtrlCharsAlt=( [0x01]="CA-A" [0x02]="CA-B" [0x03]="CA-C" [0x04]="CA-D" [0x05]="CA-E" [0x06]="CA-F" [0x07]="CA-G" [0x08]="CA-H" [0x09]="CA-I" [0x0a]="CA-J" [0x0b]="CA-K" [0x0c]="CA-L" [0x0d]="CA-M" [0x0e]="CA-N" [0x0f]="CA-O" [0x10]="CA-P" [0x11]="CA-Q" [0x12]="CA-R" [0x13]="CA-S" [0x14]="CA-T" [0x15]="CA-U" [0x16]="CA-V" [0x17]="CA-W" [0x18]="CA-X" [0x19]="CA-Y" [0x1a]="CA-Z" [0x1b]="CA-[" [0x1c]="CA-]" [0x1d]="CA-}" [0x1e]="CA-^" [0x1f]="CA-_" [0x20]="CA-SP" [0x7F]="A-DEL" ) MouseButtons=( [0x00]="MB1-P" [0x01]="MB2-P" [0x02]="MB3-P" [0x03]="MB-R" [0x20]="MB1-M" [0x21]="MB2-M" [0x22]="MB3-M" [0x23]="MB-M" [0x40]="MB4-P" [0x41]="MB5-P" ) MouseMetaButtons=( [0x04]="S-" [0x08]="A-" [0x0c]="AS-" [0x10]="C-" [0x14]="CS-" [0x1c]="CAS-" ) GetMouseButton() { local MouseBtn AdjustMousePos MouseBtn "${2}" MouseBtn="${MouseMetaButtons[$(( ${MouseBtn} & 0x1C))]-}${MouseButtons[$(( ${MouseBtn} & 0xe3))]}" eval ${1}='"${MouseBtn}"' } mouse_on="$(vt100_DECSET ${mouse_type[1]})" mouse_off="$(vt100_DECRST "${mouse_type[1]}" )" ReadKey() { unset UInput[@] local escapeSequence local REPLY echo -n "${mouse_on}" if IFS='' read -srN1 ${1:-} escapeSequence; then case "${escapeSequence}" in [^[:cntrl:]]) UInput[0]="${escapeSequence}" ;; $'\e') while IFS='' read -srN1 -t0.0001 ; do escapeSequence+="${REPLY}" done case "${escapeSequence}" in $'\e'[^[:cntrl:]]) echo -n "A-${escapeSequence:1}" ;; ${CSI}t) UInput[0]="MouseTrack" AdjustMousePos UInput[1] "${escapeSequence:3:1}" AdjustMousePos UInput[2] "${escapeSequence:4:1}" ;; ${CSI}T) UInput[0]="MouseTrack" AdjustMousePos UInput[1] "${escapeSequence:3:1}" AdjustMousePos UInput[2] "${escapeSequence:4:1}" AdjustMousePos UInput[3] "${escapeSequence:5:1}" AdjustMousePos UInput[4] "${escapeSequence:6:1}" AdjustMousePos UInput[5] "${escapeSequence:7:1}" AdjustMousePos UInput[6] "${escapeSequence:8:1}" ;; ${CSI}M*) GetMouseButton UInput[0] "${escapeSequence:3:1}" if [ -n "${UInput[0]}" ]; then AdjustMousePos UInput[1] "${escapeSequence:4:1}" AdjustMousePos UInput[2] "${escapeSequence:5:1}" else UInput[0]=$(printf 'Mouse-\\x%02x %q' "'${escapeSequence:3:1}" "${escapeSequence:4}") fi ;; ${CSI}[0-9]*[ABCDEFHIOZPQRSz~]) local CSI_Params=( ${escapeSequence//[!0-9]/ } ) local CSI_Func="${escapeSequence:${#escapeSequence}-1}" case "${CSI_Func}" in z) # Sun Function Keys UInput[0]="${SunKeybFntKeys[${CSI_Params[0]}]-}" if [ -n "${UInput[0]}" ]; then [ ${#CSI_Params[@]} -le 1 ] || UInput[0]="${KeyModifiers[${CSI_Params[1]}]}-${UInput[0]}" else UInput[0]="CSI ${CSI_Params[*]} ${CSI_Func}" fi ;; '~') # Function Keys UInput[0]="${KeybFntKeys[${CSI_Params[0]}]-}" if [ -n "${UInput[0]}" ]; then [ ${#CSI_Params[@]} -le 1 ] || UInput[0]="${KeyModifiers[${CSI_Params[1]}]}-${UInput[0]}" else UInput[0]="CSI ${CSI_Params[*]} ${CSI_Func}" fi ;; A|B|C|D|E|F|H|I|O|Z|P|Q|R|S) ord_eascii CSI_Func "${CSI_Func}" UInput[0]="${KeybFntKeysAlt[${CSI_Func}]}" if [ -n "${UInput[0]}" ]; then [ ${#CSI_Params[@]} -le 1 ] || UInput[0]="${KeyModifiers[${CSI_Params[1]}]}-${UInput[0]}" else UInput[0]="CSI ${CSI_Params[*]} ${CSI_Func}" fi ;; *) UInput[0]="CSI ${CSI_Params[*]} ${CSI_Func}" ;; esac ;; ${SS3}*[ABCDEFHPQRSIO~]) local SS3_Params=( ${escapeSequence//[!0-9]/ } ) local SS3_Func="${escapeSequence:${#escapeSequence}-1}" case "${SS3_Func}" in A|B|C|D|E|F|H|P|Q|R|S|~) ord_eascii SS3_Func "${SS3_Func}" UInput[0]="${KeybFntKeysAlt[${SS3_Func}]-}" if [ -n "${UInput[0]}" ]; then [ ${#SS3_Params[@]} -lt 1 ] || UInput[0]="${KeyModifiers[${SS3_Params[0]}]}-${UInput[0]}" else UInput[0]="SS3 ${SS3_Params[*]-} ${SS3_Func}" fi ;; *) UInput[0]="SS3 ${SS3_Params[*]-} ${SS3_Func}" ;; esac ;; $'\e'[[:cntrl:]]) ord_eascii UInput[0] "${escapeSequence:1:1}" UInput[0]="${C1CtrlCharsAlt[${UInput[0]}]:-}" [ -n "${UInput[0]:-}" ] || UInput[0]="$(printf "%q" "${escapeSequence}")" ;; $'\e') UInput[0]="ESC" ;; *) UInput[0]="$(printf "%q" "${escapeSequence}")" ;; esac ;; *) ord_eascii UInput[0] "${escapeSequence}" UInput[0]="${C0CtrlChars[${UInput[0]}]:-}" [ -n "${UInput[0]:-}" ] || UInput[0]="$(printf '%q' "'${escapeSequence}")" ;; esac fi echo -n "${mouse_off}" }
The ReadKey function can then be called in this manor.
HandleKey() { local -a UInput while true; do if ReadKey ; then case "${UInput[0]:-}" in CR|NULL|LF|q) echo "\"${UInput[*]-}\"" break ;; *up) echo "Up Arrow ignoring modifier keys" ;; down) echo "Down Arrow No modifier keys" ;; S-down) echo "Shift + Down Arrow" ;; A-down) echo "Alt + Down Arrow" ;; AS-down) echo "Alt + Shift + Down Arrow" ;; C-down) echo "Control + Down Arrow" ;; CS-down) echo "Control + Shift + Down Arrow" ;; CA-down) echo "Control + Alt + Down Arrow" ;; CAS-down) echo "Control + Alt + Shift + Down Arrow";; MCAS-down) echo "Meta + Control + Alt + Shift + Down Arrow";; *) echo "\"${UInput[*]-}\"" ;; esac fi done } HandleKey
The main problem with the previous example is that like most such bash implementations it requires a break between function keys. Now in a user interface this is not a problem because there is always a minimal delay between key presses. However on slower systems it could happen that 2 key preses get evaluated together, the only downside is that in those rare cases the keys get ignored. But this also means that it can't be used with buffered function key sequences.
vt100 Parser Example
A more complex solution using a full vt100 parser. This following solution fixes the problems of the previous example as it doesn't blindly gobble up the entire buffer but instead intelligently reads the key sequence. The downside is that it is quite a bit more complex, and takes about 0.005s per function key instead of 0.001 sec.
declare -gri VTPARSE_ACTION_CLEAR="1" declare -gri VTPARSE_ACTION_COLLECT="2" declare -gri VTPARSE_ACTION_CSI_DISPATCH="3" declare -gri VTPARSE_ACTION_ESC_DISPATCH="4" declare -gri VTPARSE_ACTION_ESC_EXECUTE="5" declare -gri VTPARSE_ACTION_EXECUTE="6" declare -gri VTPARSE_ACTION_HOOK="7" declare -gri VTPARSE_ACTION_IGNORE="8" declare -gri VTPARSE_ACTION_OSC_END="9" declare -gri VTPARSE_ACTION_OSC_PUT="10" declare -gri VTPARSE_ACTION_OSC_START="11" declare -gri VTPARSE_ACTION_PARAM="12" declare -gri VTPARSE_ACTION_PRINT="13" declare -gri VTPARSE_ACTION_PUT="14" declare -gri VTPARSE_ACTION_SS2_DISPATCH="15" declare -gri VTPARSE_ACTION_SS3_DISPATCH="16" declare -gri VTPARSE_ACTION_UNHOOK="17" declare -gra VTPARSE_ACTION_NAMES=( [0 ]="<no action>" [${VTPARSE_ACTION_CLEAR}]="CLEAR" [${VTPARSE_ACTION_COLLECT}]="COLLECT" [${VTPARSE_ACTION_CSI_DISPATCH}]="CSI_DISPATCH" [${VTPARSE_ACTION_ESC_DISPATCH}]="ESC_DISPATCH" [${VTPARSE_ACTION_ESC_EXECUTE}]="ESC_EXECUTE" [${VTPARSE_ACTION_EXECUTE}]="EXECUTE" [${VTPARSE_ACTION_HOOK}]="HOOK" [${VTPARSE_ACTION_IGNORE}]="IGNORE" [${VTPARSE_ACTION_OSC_END}]="OSC_END" [${VTPARSE_ACTION_OSC_PUT}]="OSC_PUT" [${VTPARSE_ACTION_OSC_START}]="OSC_START" [${VTPARSE_ACTION_PARAM}]="PARAM" [${VTPARSE_ACTION_PRINT}]="PRINT" [${VTPARSE_ACTION_PUT}]="PUT" [${VTPARSE_ACTION_SS2_DISPATCH}]="SS2_DISPATCH" [${VTPARSE_ACTION_SS3_DISPATCH}]="SS3_DISPATCH" [${VTPARSE_ACTION_UNHOOK}]="UNHOOK" ) declare -gri VTPARSE_STATE_ANYWHERE="0" declare -gri VTPARSE_STATE_CSI_ENTRY="1" declare -gri VTPARSE_STATE_CSI_IGNORE="2" declare -gri VTPARSE_STATE_CSI_INTERMEDIATE="3" declare -gri VTPARSE_STATE_CSI_PARAM="4" declare -gri VTPARSE_STATE_DCS_ENTRY="5" declare -gri VTPARSE_STATE_DCS_IGNORE="6" declare -gri VTPARSE_STATE_DCS_INTERMEDIATE="7" declare -gri VTPARSE_STATE_DCS_PARAM="8" declare -gri VTPARSE_STATE_DCS_PASSTHROUGH="9" declare -gri VTPARSE_STATE_ESCAPE="10" declare -gri VTPARSE_STATE_ESCAPE_INTERMEDIATE="11" declare -gri VTPARSE_STATE_GROUND="12" declare -gri VTPARSE_STATE_OSC_STRING="13" declare -gri VTPARSE_STATE_SOS_PM_APC_STRING="14" declare -gri VTPARSE_STATE_SS2_ENTRY="15" declare -gri VTPARSE_STATE_SS3_ENTRY="16" declare -gri VTPARSE_STATE_SS3_PARAM="17" declare -gra VTPARSE_STATE_NAMES=( [${VTPARSE_STATE_ANYWHERE}]="ANYWHERE" [${VTPARSE_STATE_CSI_ENTRY}]="CSI_ENTRY" [${VTPARSE_STATE_CSI_IGNORE}]="CSI_IGNORE" [${VTPARSE_STATE_CSI_INTERMEDIATE}]="CSI_INTERMEDIATE" [${VTPARSE_STATE_CSI_PARAM}]="CSI_PARAM" [${VTPARSE_STATE_DCS_ENTRY}]="DCS_ENTRY" [${VTPARSE_STATE_DCS_IGNORE}]="DCS_IGNORE" [${VTPARSE_STATE_DCS_INTERMEDIATE}]="DCS_INTERMEDIATE" [${VTPARSE_STATE_DCS_PARAM}]="DCS_PARAM" [${VTPARSE_STATE_DCS_PASSTHROUGH}]="DCS_PASSTHROUGH" [${VTPARSE_STATE_ESCAPE}]="ESCAPE" [${VTPARSE_STATE_ESCAPE_INTERMEDIATE}]="ESCAPE_INTERMEDIATE" [${VTPARSE_STATE_GROUND}]="GROUND" [${VTPARSE_STATE_OSC_STRING}]="OSC_STRING" [${VTPARSE_STATE_SOS_PM_APC_STRING}]="SOS_PM_APC_STRING" [${VTPARSE_STATE_SS2_ENTRY}]="SS2_ENTRY" [${VTPARSE_STATE_SS3_ENTRY}]="SS3_ENTRY" [${VTPARSE_STATE_SS3_PARAM}]="SS3_PARAM" ) declare -gra VTPARSE_STATE_TABLE=( [${VTPARSE_STATE_ANYWHERE}]="0000000000000000000000000000000000000000000000006c006c0a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006c6c6c6c6c6c6c6c6c6c6c6c6c6c0f6c056c6c6c6c6c6c6c0e6c6c016c0d0e0e" [${VTPARSE_STATE_CSI_ENTRY}]="606060606060606060606060606060606060606060606060006000006060606023232323232323232323232323232323c4c4c4c4c4c4c4c4c4c402c4242424243c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c80" [${VTPARSE_STATE_CSI_IGNORE}]="606060606060606060606060606060606060606060606060006000006060606080808080808080808080808080808080808080808080808080808080808080800c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c80" [${VTPARSE_STATE_CSI_INTERMEDIATE}]="606060606060606060606060606060606060606060606060006000006060606020202020202020202020202020202020020202020202020202020202020202023c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c80" [${VTPARSE_STATE_CSI_PARAM}]="606060606060606060606060606060606060606060606060006000006060606023232323232323232323232323232323c0c0c0c0c0c0c0c0c0c002c0020202023c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c80" [${VTPARSE_STATE_DCS_ENTRY}]="808080808080808080808080808080808080808080808080008000008080808027272727272727272727272727272727c8c8c8c8c8c8c8c8c8c806c82828282809090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090980" [${VTPARSE_STATE_DCS_IGNORE}]="8080808080808080808080808080808080808080808080800080000080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080000000000000000000000000000000000000000000000000000000000c" [${VTPARSE_STATE_DCS_INTERMEDIATE}]="8080808080808080808080808080808080808080808080800080000080808080202020202020202020202020202020200606060606060606060606060606060609090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090980" [${VTPARSE_STATE_DCS_PARAM}]="808080808080808080808080808080808080808080808080008000008080808027272727272727272727272727272727c0c0c0c0c0c0c0c0c0c006c00606060609090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090980" [${VTPARSE_STATE_DCS_PASSTHROUGH}]="e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e000e00000e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e080000000000000000000000000000000000000000000000000000000000c" [${VTPARSE_STATE_ESCAPE}]="50505050505050505050505050505050505050505050505000500000505050502b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c0f0g054c4c4c4c4c4c4c0e4c4c014c0d0e0e4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c5c" [${VTPARSE_STATE_ESCAPE_INTERMEDIATE}]="6060606060606060606060606060606060606060606060600060000060606060202020202020202020202020202020204c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c80" [${VTPARSE_STATE_GROUND}]="6060606060606060606060606060606060606060606060600060000060606060d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d06060606060606060606060606060606000606060606060606060600060" [${VTPARSE_STATE_OSC_STRING}]="8080808080808080808080808080808080808080808080800080000080808080a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0000000000000000000000000000000000000000000000000000000000c" [${VTPARSE_STATE_SOS_PM_APC_STRING}]="8080808080808080808080808080808080808080808080800080000080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080000000000000000000000000000000000000000000000000000000000c" [${VTPARSE_STATE_SS2_ENTRY}]="60606060606060606060606060606060606060606060606000600000606060600000000000000000000000000000000000000000000000000000000000000000fcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfc80" [${VTPARSE_STATE_SS3_ENTRY}]="606060606060606060606060606060606060606060606060006000006060606000000000000000000000000000000000chchchchchchchchchch000000000000gcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgc80" [${VTPARSE_STATE_SS3_PARAM}]="606060606060606060606060606060606060606060606060006000006060606000000000000000000000000000000000c0c0c0c0c0c0c0c0c0c0000000000000gcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgcgc80" ) declare -gr VTPARSE_BASE="0123456789abcdefghijklmnopqrstuvwxyz" declare -gr VTPARSE_ENTRY_ACTIONS="0100010007100b0110" declare -gr VTPARSE_EXIT_ACTIONS="000000000h00090000" ord() { printf -v "${1?Missing Dest Variable}" "${3:-%d}" "'${2?Missing Char}" } ord_eascii() { LC_CTYPE=C ord "${@}" } substr() { local idx typeset -gi _INDEX case "${1}" in *${2}*) idx="${1%"${2}"*}"; _INDEX=${#idx} ;; *) _INDEX=0 ;; esac } declare -gr MAX_INTERMEDIATE_CHARS=2 declare -gi vtparser_state declare -g vtparser_intermediate_chars declare -ga vtparser_params declare -gi vtparser_ignore_flagged declare -g vtparser_cb declare -g vtparser_read vtparse_init() { vtparser_state="${VTPARSE_STATE_GROUND}" vtparser_intermediate_chars="" vtparser_params=( ) vtparser_ignore_flagged=0 vtparser_cb=${1?Missing Callback Function} vtparser_read=${2?Missing Read Function} } nop() { echo -n "" } do_action() { # Some actions we handle internally (like parsing parameters), others # we hand to our client for processing case "${1}" in ${VTPARSE_ACTION_ESC_EXECUTE}|${VTPARSE_ACTION_PRINT}|${VTPARSE_ACTION_EXECUTE}|${VTPARSE_ACTION_HOOK}|${VTPARSE_ACTION_PUT}|${VTPARSE_ACTION_OSC_START}|${VTPARSE_ACTION_OSC_PUT}|${VTPARSE_ACTION_OSC_END}|${VTPARSE_ACTION_UNHOOK}|${VTPARSE_ACTION_CSI_DISPATCH}|${VTPARSE_ACTION_ESC_DISPATCH}|${VTPARSE_STATE_SS2_ENTRY}|${VTPARSE_STATE_SS3_ENTRY}) ${vtparser_cb} "${@}" || return $? ;; ${VTPARSE_ACTION_IGNORE}) nop ;; ${VTPARSE_ACTION_COLLECT}) if [ ${#vtparser_intermediate_chars} -ge ${MAX_INTERMEDIATE_CHARS} ]; then vtparser_ignore_flagged=1 else vtparser_intermediate_chars+="${2}"; fi ;; ${VTPARSE_ACTION_PARAM}) # process the param character if [ "${2}" = ";" ]; then vtparser_params[${#vtparser_params[@]}]=""; else # the character is a digit if [ ${#vtparser_params[@]} -eq 0 ]; then vtparser_params[0]="" fi vtparser_params[${#vtparser_params[@]}-1]+="${2}" fi ;; ${VTPARSE_ACTION_CLEAR}) vtparser_intermediate_chars="" vtparser_params=( ) vtparser_ignore_flagged=0 ;; *) echo "Unknown action ${1}" ;; esac return 0 } vtparse_conv_base() { local _INDEX substr "${VTPARSE_BASE}" "${2:0:1}" eval ${1}'="${_INDEX}"' } do_state_change() { # A state change is an action and/or a new state to transition to. local -i action local -i new_state local -i RValue=0 vtparse_conv_base action "${1:0:1}" vtparse_conv_base new_state "${1:1:1}" #printf "action=%s new_state=%s vtparser_state=%s change=%s\n" "${action}" "${new_state}" "${vtparser_state}" "${1}" if [ "${new_state:=0}" != "0" ]; then # Perform up to three actions: # 1. the exit action of the old state # 2. the action associated with the transition # 3. the entry actionk of the new action local -i exit_action local -i entry_action vtparse_conv_base exit_action "${VTPARSE_EXIT_ACTIONS:${vtparser_state}:1}" vtparse_conv_base entry_action "${VTPARSE_ENTRY_ACTIONS:${new_state}:1}" [ "${exit_action}" = "0" ] || do_action "${exit_action}" $'\x00' 0 [ "${action}" = "0" ] || do_action "${action}" "${2}" "${3}" || RValue=$? [ "${entry_action}" = "0" ] || do_action "${entry_action}" $'\x00' 0 vtparser_state=${new_state} return ${RValue} else [ "${action}" = "0" ] || do_action "${action}" "${2}" "${3}" || return $? fi } vtparse() { local -i _INDEX local REPLY while ${vtparser_read} 1 ; do ord_eascii _INDEX "${REPLY:0:1}" # If a transition is defined from the "anywhere" state, always # use that. Otherwise use the transition from the current state. _INDEX=${_INDEX}*2 # 2 characters per entry local change="${VTPARSE_STATE_TABLE[${VTPARSE_STATE_ANYWHERE}]:${_INDEX}:2}" [ "${change:-00}" != "00" ] || change="${VTPARSE_STATE_TABLE[${vtparser_state}]:${_INDEX}:2}" _INDEX=${_INDEX}/2 do_state_change "${change}" "${REPLY}" "${_INDEX}" || break done } vtparse_read_stdin() { read -srN${1:-1} -d '' "${@:2}" } declare -g vtparse_read_buffer vtparse_read_buffer() { REPLY="${vtparse_read_buffer:0:${1:-1}}" vtparse_read_buffer="${vtparse_read_buffer:${1}}" [ ${#REPLY} -eq ${1} ] || return $? } vtparser_callback_debug() { local CArg printf "Received action %s, char=0x%02x char=%q\n" "${VTPARSE_ACTION_NAMES[${1}]}" "'${2}" "${2}" printf "Intermediate chars: '%s'\n" "${vtparser_intermediate_chars}" printf "%d Parameters:\n" ${#vtparser_params[@]} for CArg in "${vtparser_params[@]-}"; do printf "\t%d\n" "${CArg}" done printf "\n" } ############################### ## ## READ KEY CRAP ## ## ############################### KeyModifiers=( [1]="" [2]="S-" [3]="A-" [4]="AS-" [5]="C-" [6]="CS-" [7]="CA-" [8]="CAS-" [9]="M-" [10]="MS-" [11]="MA-" [12]="MAS-" [13]="MC-" [14]="MCS-" [15]="MCA-" [16]="MCAS-" ) KeybFntKeys=( [1]="home" [2]="insert" [3]="delete" [4]="end" [5]="pageUp" [6]="pageDown" [11]="f1" [12]="f2" [13]="f3" [14]="f4" [15]="f5" [17]="f6" [18]="f7" [19]="f8" [20]="f9" [21]="f10" [23]="f11" [24]="f12" [25]="f13" [26]="f14" [28]="f15" [29]="f16" [31]="f17" [32]="f18" [33]="f19" [34]="f20" ) SunKeybFntKeys=( [214]="home" [2]="insert" [3]="delete" [4]="end" [216]="pageUp" [222]="pageDown" [224]="f1" [225]="f2" [226]="f3" [227]="f4" [228]="f5" [229]="f6" [230]="f7" [231]="f8" [232]="f9" [233]="f10" [192]="f11" [193]="f12" [218]="keypad-five" [220]="keypad-delete" ) KeybFntKeysAlt=( # A B C D E F H [0x41]="up" [0x42]="down" [0x43]="right" [0x44]="left" [0x45]="keypad-five" [0x46]="end" [0x48]="home" # I O [0x49]="InFocus" [0x4f]="OutOfFocus" # P Q R S Z [0x50]="f1" [0x51]="f2" [0x52]="f3" [0x53]="f4" [0x5a]="S-HT" ) C0CtrlChars=( [0x00]="Null" [0x01]="SOH" [0x02]="STX" [0x03]="ETX" [0x04]="EOT" [0x05]="ENQ" [0x06]="ACK" [0x07]="BEL" [0x08]="BS" [0x09]="HT" [0x0A]="LF" [0x0B]="VT" [0x0C]="FF" [0x0D]="CR" [0x0E]="SO" [0x0F]="SI" [0x10]="DLE" [0x11]="DC1" [0x12]="DC2" [0x13]="DC3" [0x14]="DC4" [0x15]="NAK" [0x16]="SYN" [0x17]="ETB" [0x18]="CAN" [0x19]="EM" [0x1A]="SUB" [0x1B]="ESC" [0x1C]="FS" [0x1D]="GS" [0x1E]="RS" [0x1F]="US" [0x20]="SP" [0x7F]="DEL" ) C0CtrlCharsAlt=( [0x01]="C-A" [0x02]="C-B" [0x03]="C-C" [0x04]="C-D" [0x05]="C-E" [0x06]="C-F" [0x07]="C-G" [0x08]="C-H" [0x09]="C-I" [0x0a]="C-J" [0x0b]="C-K" [0x0c]="C-L" [0x0d]="C-M" [0x0e]="C-N" [0x0f]="C-O" [0x10]="C-P" [0x11]="C-Q" [0x12]="C-R" [0x13]="C-S" [0x14]="C-T" [0x15]="C-U" [0x16]="C-V" [0x17]="C-W" [0x18]="C-X" [0x19]="C-Y" [0x1a]="C-Z" [0x1b]="C-[" [0x1c]="C-]" [0x1d]="C-}" [0x1e]="C-^" [0x1f]="C-_" [0x20]="C-SP" [0x7F]="DEL" ) C1CtrlCharsEsc=( [0x40]="PAD" [0x41]="HOP" [0x42]="BPH" [0x43]="NBH" [0x44]="IND" [0x45]="NEL" [0x46]="SSA" [0x47]="ESA" [0x48]="HTS" [0x49]="HTJ" [0x4A]="VTS" [0x4B]="PLD" [0x4C]="PLU" [0x4D]="RI" [0x4E]="SS2" [0x4F]="SS3" [0x50]="DCS" [0x51]="PU1" [0x52]="PU2" [0x53]="STS" [0x54]="CCH" [0x55]="MW" [0x56]="SPA" [0x57]="EPA" [0x58]="SOS" [0x59]="SGCI" [0x5A]="SCI" [0x5B]="CSI" [0x5C]="ST" [0x5D]="OSC" [0x5E]="PM" [0x5F]="APC" ) C1CtrlChars=( [0x80]="PAD" [0x81]="HOP" [0x82]="BPH" [0x83]="NBH" [0x84]="IND" [0x85]="NEL" [0x86]="SSA" [0x87]="ESA" [0x88]="HTS" [0x89]="HTJ" [0x8A]="VTS" [0x8B]="PLD" [0x8C]="PLU" [0x8D]="RI" [0x8E]="SS2" [0x8F]="SS3" [0x90]="DCS" [0x91]="PU1" [0x92]="PU2" [0x93]="STS" [0x94]="CCH" [0x95]="MW" [0x96]="SPA" [0x97]="EPA" [0x98]="SOS" [0x99]="SGCI" [0x9A]="SCI" [0x9B]="CSI" [0x9C]="ST" [0x9D]="OSC" [0x9E]="PM" [0x9F]="APC" ) C1CtrlCharsAlt=( [0x01]="CA-A" [0x02]="CA-B" [0x03]="CA-C" [0x04]="CA-D" [0x05]="CA-E" [0x06]="CA-F" [0x07]="CA-G" [0x08]="CA-H" [0x09]="CA-I" [0x0a]="CA-J" [0x0b]="CA-K" [0x0c]="CA-L" [0x0d]="CA-M" [0x0e]="CA-N" [0x0f]="CA-O" [0x10]="CA-P" [0x11]="CA-Q" [0x12]="CA-R" [0x13]="CA-S" [0x14]="CA-T" [0x15]="CA-U" [0x16]="CA-V" [0x17]="CA-W" [0x18]="CA-X" [0x19]="CA-Y" [0x1a]="CA-Z" [0x1b]="CA-[" [0x1c]="CA-]" [0x1d]="CA-}" [0x1e]="CA-^" [0x1f]="CA-_" [0x20]="CA-SP" [0x7F]="A-DEL" ) MouseButtons=( [0x00]="MB1-P" [0x01]="MB2-P" [0x02]="MB3-P" [0x03]="MB-R" [0x20]="MB1-M" [0x21]="MB2-M" [0x22]="MB3-M" [0x23]="MB-M" [0x40]="MB4-P" [0x41]="MB5-P" ) MouseMetaButtons=( [0x04]="S-" [0x08]="A-" [0x0c]="AS-" [0x10]="C-" [0x14]="CS-" [0x1c]="CAS-" ) AdjustMousePos() { local -i _INDEX ord_eascii _INDEX "${2}" eval ${1}'=$(( ${_INDEX}-32))' } GetMouseButton() { local MouseBtn AdjustMousePos MouseBtn "${2}" MouseBtn="${MouseMetaButtons[$(( ${MouseBtn} & 0x1C))]-}${MouseButtons[$(( ${MouseBtn} & 0xe3))]}" eval ${1}='"${MouseBtn}"' } vtparser_callback_readkey() { unset UInput[@] case "${1}" in ${VTPARSE_ACTION_ESC_EXECUTE}) case "${2}" in [[:cntrl:]]) UInput[0]="${C1CtrlCharsAlt[${3}]:-$(printf "%q" $'\e'"${2}")}" ;; *) UInput[0]="${C1CtrlChars[${3}]:-$(printf "%q" $'\e'"${2}")}" ;; esac ;; ${VTPARSE_ACTION_EXECUTE}) UInput[0]="${C0CtrlChars[${3}]:-$(printf "%q" "${2}")}" ;; ${VTPARSE_ACTION_CSI_DISPATCH}) case "${2}" in z) # Sun Function Keys UInput[0]="${SunKeybFntKeys[${vtparser_params[0]}]:-}" if [ -n "${UInput[0]}" ]; then UInput[0]="${KeyModifiers[${vtparser_params[1]:-1}]}${UInput[0]}" UInput[1]="1" # Repeat Count else UInput[0]="CSI ${vtparser_params[*]} ${2}" fi ;; '~') # Function Keys UInput[0]="${KeybFntKeys[${vtparser_params[0]}]}" if [ -n "${UInput[0]}" ]; then UInput[0]="${KeyModifiers[${vtparser_params[1]:-1}]}${UInput[0]}" UInput[1]="1" # Repeat Count else UInput[0]="CSI ${vtparser_params[*]} ${2}" fi ;; A|B|C|D|E|F|H|I|O|Z|P|Q|R|S) UInput[0]="${KeybFntKeysAlt[${3}]:-}" if [ -n "${UInput[0]}" ]; then UInput[0]="${KeyModifiers[${vtparser_params[1]:-1}]}${UInput[0]}" UInput[1]="${vtparser_params[0]:-1}" # Repeat Count else UInput[0]="CSI ${vtparser_params[*]} ${2}" fi ;; t) ${vtparser_read} 2 UInput[0]="MouseTrack" AdjustMousePos UInput[1] "${REPLY:0:1}" AdjustMousePos UInput[2] "${REPLY:1:1}" ;; T) ${vtparser_read} 6 UInput[0]="MouseTrack" AdjustMousePos UInput[1] "${REPLY:0:1}" AdjustMousePos UInput[2] "${REPLY:1:1}" AdjustMousePos UInput[3] "${REPLY:2:1}" AdjustMousePos UInput[4] "${REPLY:3:1}" AdjustMousePos UInput[5] "${REPLY:4:1}" AdjustMousePos UInput[6] "${REPLY:5:1}" ;; M) # Mouse ${vtparser_read} 3 GetMouseButton UInput[0] "${REPLY:0:1}" if [ -n "${UInput[0]}" ]; then AdjustMousePos UInput[1] "${REPLY:1:1}" AdjustMousePos UInput[2] "${REPLY:2:1}" else UInput[0]=$(printf 'Mouse-\\x%02x %q' "'${escapeSequence:0:1}" "${escapeSequence:1}") fi ;; *) UInput[0]="CSI ${vtparser_params[*]} ${2}" ;; esac ;; ${VTPARSE_ACTION_SS3_DISPATCH}) case "${2}" in A|B|C|D|E|F|H|P|Q|R|S|~) UInput[0]+="${KeybFntKeysAlt[${3}]}" if [ -n "${UInput[0]}" ]; then UInput[0]="${KeyModifiers[${vtparser_params[1]:-1}]}${UInput[0]}" UInput[1]="${vtparser_params[0]:-1}" # Repeat Count else UInput[0]="SS3 ${vtparser_params[*]} ${2}" fi ;; *) UInput[0]="SS3 ${vtparser_params[*]} ${2}" ;; esac ;; ${VTPARSE_ACTION_ESC_DISPATCH}) case "${2}" in [][}^_A-Z]) UInput[0]="${C1CtrlChars[${3}]}" ;; [^[:cntrl:]]) UInput[0]="CA-${2}" ;; *) UInput[0]="${C1CtrlChars[${3}]:-$(printf "%q" $'\e'"${2}")}" ;; esac ;; ${VTPARSE_ACTION_PRINT}) case "${2}" in [^[:cntrl:]]) UInput[0]="${2}" ;; *) UInput[0]="${C0CtrlChars[${3}]:-$(printf "%q" $'\e'"${2}")}" ;; esac ;; *) vtparser_callback_debug "${@}" || return $? ;; esac return 1 } HandleKey() { vtparse_init vtparser_callback_readkey vtparse_read_stdin while true; do vtparse case "${UInput[0]}" in LF) echo "${UInput[@]}" break ;; *up) echo "Up Arrow ignoring modifier keys" ;; down) echo "Down Arrow No modifier keys" ;; S-down) echo "Shift + Down Arrow" ;; A-down) echo "Alt + Down Arrow" ;; AS-down) echo "Alt + Shift + Down Arrow" ;; C-down) echo "Control + Down Arrow" ;; CS-down) echo "Control + Shift + Down Arrow" ;; CA-down) echo "Control + Alt + Down Arrow" ;; CAS-down) echo "Control + Alt + Shift + Down Arrow";; MCAS-down) echo "Meta + Control + Alt + Shift + Down Arrow";; *) echo "${UInput[@]}" ;; esac done } HandleKey