runtime: Console.ReadKey returns incomplete information for some key sequences on Linux

AB#1115513 Using PowerShell 6.0.0-alpha.10 on Ubuntu 14.04, I run

PS> [Console]::ReadKey()

and type Ctrl-@. I get back

KeyChar Key Modifiers
------- --- ---------
        0   0

I see somewhat similar results with other key presses like Ctrl-! or Ctrl-%. In some cases, KeyChar has the correct key, but Modifiers is still 0, resulting in incorrectly assuming Ctrl was not down and e.g. inserting a ! instead of doing whatever action Ctrl+! was bound to.

This issue affects some of the key bindings in PSReadline.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 5
  • Comments: 33 (26 by maintainers)

Commits related to this issue

Most upvoted comments

Below is a summary of the existing shortcomings and how they relate to how things work in the Unix world.

Please note:

  • I haven’t looked at the source code, so some of this is speculative. Do tell me where I’m wrong.

Context:

The fundamental restrictions regarding <kbd>Control</kbd>-based chords and <kbd>Alt</kbd>-based chords - which come from most Unix-like OSs running xterm-emulation terminal programs are:

  • <kbd>Control</kbd>-based chords are in effect limited to the 33 well-defined chords of caret notation - plus a few effective (but presumable accidental) aliases (see bottom); the ^ symbol is known as the caret and represents the <kbd>Control</kbd> key; for instance, ^A represents the chord <kbd>Control+A</kbd> (strictly speaking, it is the uppercase A, but at least for letters it doesn’t matter whether <kbd>Shift</kbd> is held down or not):

  • There are no <kbd>Alt</kbd>-based chords at all, but most modern terminal programs translate <kbd>Alt+{char}</kbd>-based chords (with some terminals on an opt-in basis, as on macOS) into <kbd>Esc</kbd>, <kbd>{char}</kbd> sequences, and programs running inside that terminal see only the resulting sequence.
    That’s why, in the bash world, for instance, readline definitions are expressed in terms of the latter.

  • (Additionally, a given terminal program itself may have key bindings for <kbd>Control</kbd>-based and/or <kbd>Alt</kbd>-based chords, which may preempt any attempts to use these chords by programs running inside these terminals.)

Shortcomings of the existing [Console]::ReadKey() implementation on Unix-like platforms:

Note: I’m using lowercase letters in the chord representations below to indicate chords not involving the <kbd>Shift</kbd> key.

On Unix-like platforms, instead of letting programs see the translation of key chords the same way as described above - which may be a single ASCII control character or an <kbd>Esc</kbd>, <kbd>{char}</kbd> sequence - CoreFX selectively retranslates that into a chord, as you would see it on Windows_:

<kbd>Control</kbd>-based chords

  • Sensibly, <kbd>Control</kbd>-based chords that translate into control characters that have dedicated keys are reflected as such in the .Key property:

    • <kbd>Control+i</kbd> a.k.a ^I, which is control char. HT, corresponds to the <kbd>Tab</kbd> key

    • <kbd>Control+h</kbd> a.k.a ^H, which is control char. BS, corresponds to the <kbd>Backspace</kbd> key

    • <kbd>Control+[</kbd> a.k.a ^[, which is control char. ESC, corresponds to the <kbd>Escape</kbd> key.

    • Unfortunately, <kbd>Control+j</kbd> a.k.a ^J, which is control char. LF (0xA) is mistakenly conflated with <kbd>Control+m</kbd> a.k.a ^M, which is control char. CR (0xD): both result in the .KeyChar property containing `n (i.e., LF), and .Key containing Enter, which is incorrect:

      • Only ^M corresponds to key <kbd>Enter</kbd>, and the .KeyChar property should contain `r (i.e., CR), not `n.
      • ^J has the correct .KeyChar value (`n), but its .Key property should be J, and its .Modifier property should be Control.
    * Somewhat ironically, this distinction is correctly observed on _Windows_.
    
  • Most letter-based <kbd>Control</kbd>-based chords (e.g., <kbd>Control+a</kbd>) are passed through the same way they are on Windows (rather than being translated into ASCII control characters).

    • However, letter-based chords with uppercase letters, i.e. with <kbd>Shift</kbd> held down (e.g., <kbd>Control+Shift+a</kbd>) are currently quietly ignored.

    • Additionally, curiously, <kbd>Control+z</kbd>, even in its “shift-less” (lowercase) variant is quietly ignored as well; note that <kbd>Control+z</kbd> has special meaning in traditional shells (not the terminal emulators themselves): in cmd.exe, it signals EOF during interactive input (copy con ...); in bash, it suspends the currently executing program and sends it to the background.

    • For <kbd>Control+c</kbd> to be recognized as a keypress (as opposed to being treated as the signal to terminate a command), [console]::TreatControlCAsInput = $true must be executed first; without that, the terminating effect is curiously delayed: nothing appears to happen at first, but the very next next keypress then terminates the call, without returning anything (only the character typed is printed to the terminal).

  • Another quietly ignored chord is <kbd>Control+\ </kbd>

  • The remaining punctuation-based chords (other than <kbd>Control+[</kbd> a.k.a. <kbd>Esc</kbd>) pass the control character correctly in the .KeyChar property, but fail to populate the .Key and .Modifier properties:

    • <kbd>Control+]</kbd>
    • <kbd>Control+^</kbd>
    • <kbd>Control+_</kbd>

<kbd>Alt</kbd>-based chords

CoreFX tries to translate <kbd>Esc</kbd>, <kbd>{char}</kbd> sequences , as reported by the terminal emulator, back into <kbd>Alt+{char}</kbd>-based chords, but not consistently:

  • Letter-based <kbd>Alt</kbd>-based chords, including uppercase variants, cords are correctly translated, and behave as on Windows.

  • By contrast, symbol-/punctuation-based <kbd>Alt</kbd> chords (e.g., <kbd>Alt+'</kbd>) do appear to arrive as their raw <kbd>Esc</kbd>, <kbd>{char}</kbd> sequences, which [Console]::ReadKey() and therefore PSReadLine cannot handle: [Console]::ReadKey() only reports the <kbd>Esc</kbd> keypress


To predict if a given chord will currently work on Unix-like platforms, run the following from PowerShell:

# Press one key chord at a time to see its interpretation as a [ConsoleKeyInfo] instance,
# followed by the hex. representation of the resulting character (of the `.KeyChar` property).
# To exit, press Ctrl+c, then Enter.
while ($true) { $ck = [console]::readkey(); $ck | out-string; 'KeyChar code point: {0} (0x{0:x})' -f [int] [char] $ck.KeyChar }

To see what key chords [Console]::ReadKey() and therefore PSReadline should be able to handle, as @lzybkr has stated, run showkey -a in a terminal on a Linux platform (you may have to install the kbd package first).

  • Column 1 is the resulting character(s).

    • For most <kbd>Control</kbd>- and <kbd>Alt</kbd>-based chords the character is represented in caret notation; and, as stated, since <kbd>Alt+{char}</kbd>-based chords are translated into <kbd>Esc</kbd>, then <kbd>{char}</kbd> sequences, you’ll see the two resulting characters, the first of which is ^[, i.e., ESC (27 / 0x1b) in caret notation.
  • Columns 2-4 are the resulting character’s code point in decimal, octal, and hexadecimal notation


The full list of caret-notation (<kbd>Control</kbd>-based) key chords and their - inconsistently supported - aliases:

CaretNotation TargetControlChar ShiftAlias DeFactoAliases Comment                             
------------- ----------------- ---------- -------------- -------                             
^@            0x0 (NUL)         ^`         {^<space>, ^2}                                     
^A            0x1 (SOH)         ^a                                                            
^B            0x2 (STX)         ^b                                                            
^C            0x3 (ETX)         ^c                                                            
^D            0x4 (EOT)         ^d                                                            
^E            0x5 (ENQ)         ^e                                                            
^F            0x6 (ACK)         ^f                                                            
^G            0x7 (BEL)         ^g                                                            
^H            0x8 (BS)          ^h                        NEEDED FOR NORMAL TERMINAL OPERATION
^I            0x9 (HT)          ^i                        NEEDED FOR NORMAL TERMINAL OPERATION
^J            0xa (LF)          ^j                                                            
^K            0xb (VT)          ^k                                                            
^L            0xc (FF)          ^l                                                            
^M            0xd (CR)          ^m                        NEEDED FOR NORMAL TERMINAL OPERATION
^N            0xe (SO)          ^n                                                            
^O            0xf (SI)          ^o                                                            
^P            0x10 (DLE)        ^p                                                            
^Q            0x11 (DC1)        ^q                                                            
^R            0x12 (DC2)        ^r                                                            
^S            0x13 (DC3)        ^s                                                            
^T            0x14 (DC4)        ^t                                                            
^U            0x15 (NAK)        ^u                                                            
^V            0x16 (SYN)        ^v                                                            
^W            0x17 (ETB)        ^w                                                            
^X            0x18 (CAN)        ^x                                                            
^Y            0x19 (EM)         ^y                                                            
^Z            0x1a (SUB)        ^z                                                            
^[            0x1b (ESC)        ^{                        NEEDED FOR NORMAL TERMINAL OPERATION
^\            0x1c (FS)         ^|         ^5                                                 
^]            0x1d (GS)         ^}                                                            
^^            0x1e (RS)         ^~                                                            
^_            0x1f (US)                    {^7, ^/}                                           
^?            0x7f (DEL)                   ^8                                                 

Caveats re aliases:

  • Alias chords ultimately map to the same, single control character, so they are effectively indistinguishable from the canonical caret-notation chord.

    • This fact needs to be documented, so that users understand why different chords can trigger the same PSReadLine key binding.
  • To avoid confusion - and given that the aliases function inconsistently across terminal emulators - PSReadLine keybindings should only be defined in terms of the caret-notation chords.


  • Column ShiftAliases refers to, loosely speaking, aliases that are the “+ Shift” / uppercase equivalents, calculated by adding 0x20 to a caret-notation character’s code point.

    • With letter-based chords that means that whether you hold down <kbd>Shift</kbd> or not doesn’t matter - this should apply to all Latin-alphabet-based keyboard layouts.

    • With punctuation-based chords, whether you hold down <kbd>Shift</kbd> or not also doesn’t matter with an US-English keyboard layout, except for _ and ? (the “shifted” ^_ and ^? actually produce each other, i.e. produce a different control char.)

      • On macOS, curiously, ^` for ^@ works neither in the default terminal (Terminal.app) nor in popular replacement iTerm.app (the former ignores ^` , the latter treats it as plain `); if you run an X11 xterm emulator, however, it does work.
  • Column DeFactoAliases lists additional aliases:

  • It is unclear to me why these aliases work and whether they do so accidentally. Their functioning may depend on the language-specific keyboard layout.

  • ^space is a de-facto alias for ^@ that works in all terminals I’ve looked at.

  • By contrast, all the other ones - discovered experimentally with an US-English keyboard layout - seem to ONLY work in Linux xterm-256color terminals (and also in an X11 xterm emulator on macOS).

We are going to move this out to .NET 7 with the intention of grouping this with other Console-related efforts as being discussed in #52374.

I don’t see this specifically mentioned, but <kbd>shift</kbd>+<kbd>enter</kbd> is also incomplete on Linux (I’m testing on WSL2 Ubuntu). Using the following program returns different results on Windows vs Linux:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Press a key:");
        var key = Console.ReadKey();
        Console.WriteLine("Key: " + key.Key);
        Console.WriteLine("KeyChar: " + key.KeyChar);
        Console.WriteLine("Modifiers: " + key.Modifiers);
    }
}

On Windows, when we press <kbd>shift</kbd>+<kbd>enter</kbd> we get:

Key: Enter
KeyChar:
Modifiers: Shift

On Linux, we get:

Key: Enter
KeyChar:
Modifiers: 0

On Linux the System.Console.ReadKey is sending Shift+PageDown on a Shift+CursorDown and Shift+PageUp on a Shift+CursorUp. Also it doesn’t respond on pressing Shift+End, Shift+Home, Shift+PageDown and Shift+PageUp.

The ConsoleKeyInfo modifiers in Linux (NET.Core) does not caught without decode the Ansi Escape Sequence. Only Windows OS caught the modifiers keys. Unfortunately, System.Console don’t uses the Console Virtual Terminal Sequences on Windows, but only on Linux. Also, some keys aren’t caught by Windows or Linux because these keys are used by the OS.