Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-128388: pyrepl on Windows: add meta and ctrl+arrow keybindings #128389

Merged
merged 10 commits into from
Jan 10, 2025
42 changes: 26 additions & 16 deletions Lib/_pyrepl/windows_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ def __init__(self, err: int | None, descr: str | None = None) -> None:
MOVE_DOWN = "\x1b[{}B"
CLEAR = "\x1b[H\x1b[J"

# State of control keys: https://learn.microsoft.com/en-us/windows/console/key-event-record-str
ALT_ACTIVE = 0x01 | 0x02
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about including the right ALT in ALT_ACTIVE. On Linux, ALT+Backspace removes a whole word, whereas AltGr+Backspace removes a single character.

Copy link
Contributor Author

@paulie4 paulie4 Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lib/_pyrepl/keymap.py doesn't distinguish between right and left (it just has C- and M-), so I'm not sure that was a conscious decision that was made for the Linux code. Maybe that's actually a bug in the _pyrepl/unix_* code?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lib/_pyrepl/keymap.py doesn't distinguish between right and left

Python 3.14 on Linux behaves differently for left Alt and right AltGr:

  • Left Alt + backspace: remove a word
  • Right AltGr + backspace: remove a single character

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my Linux, the English keyboard layout has Right Alt which generally works like Left Alt (in a terminal).
The French or Czech layout has AltGr instead, which is apparently not Meta enough, so AltGr+Backspace works like Backspace (in both pyrepl and bash).

I tried on some Windows consoles:

LAlt+Backspace RAlt+Backspace AltGr+Backspace
cmd Delete single character Nothing Nothing
powershell Nothing Nothing Nothing

Doesn't look useful, so it's probably best to have Linux-like behaviour here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you are saying. Are you saying because the existing functionality isn't useful, we should continue to make the right Alt key not be useful in _pyrepl?

Either way, if this is about AltGr, I think we have to really understand how it works in Windows... I don't have an AltGr key, so I can't test it, but https://en.wikipedia.org/wiki/AltGr_key says, "Windows interprets Ctrl+Alt as AltGr," and since the _KEY_EVENT_RECORD doc doesn't show a dwControlKeyState value for AltGr, doesn't that mean that RIGHT_ALT_PRESSED is not the same thing as AltGr and that the Lib/_pyrepl/windows_console.py code would actually have to look for a combination of Ctrl (left/right) and Alt (left/right) (or maybe just lefts?) being pressed to know if AltGr is being pressed, or am I misunderstanding how the _KEY_EVENT_RECORD works?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know that other keyboards have a right ALT key different than AltGr. In this case, I'm fine with ALT_ACTIVE = 0x01 | 0x02.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you are saying.

Sorry for being unclear!
I meant that the behaviour of Windows terminals I tried (cmd & powershell) is not useful, so Python should not emulate that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have an AltGr key, so I can't test it

It's a feature of the keyboard layout, not the physical keyboard. To test it you can add, for example, French "AZERTY" in system settings.

CTRL_ACTIVE = 0x04 | 0x08


class _error(Exception):
pass
Expand Down Expand Up @@ -407,31 +411,37 @@ def get_event(self, block: bool = True) -> Event | None:
continue
return None

key = rec.Event.KeyEvent.uChar.UnicodeChar
key_event = rec.Event.KeyEvent
raw_key = key = key_event.uChar.UnicodeChar
eendebakpt marked this conversation as resolved.
Show resolved Hide resolved

if rec.Event.KeyEvent.uChar.UnicodeChar == "\r":
# Make enter make unix-like
if key == "\r":
# Make enter unix-like
return Event(evt="key", data="\n", raw=b"\n")
elif rec.Event.KeyEvent.wVirtualKeyCode == 8:
elif key_event.wVirtualKeyCode == 8:
# Turn backspace directly into the command
return Event(
evt="key",
data="backspace",
raw=rec.Event.KeyEvent.uChar.UnicodeChar,
)
elif rec.Event.KeyEvent.uChar.UnicodeChar == "\x00":
key = "backspace"
elif key == "\x00":
# Handle special keys like arrow keys and translate them into the appropriate command
code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode)
if code:
return Event(
evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar
)
key = VK_MAP.get(key_event.wVirtualKeyCode)
if key:
if key_event.dwControlKeyState & CTRL_ACTIVE:
key = f"ctrl {key}"
elif key_event.dwControlKeyState & ALT_ACTIVE:
# queue the key, return the meta command
self.event_queue.insert(0, Event(evt="key", data=key, raw=key))
return Event(evt="key", data="\033") # keymap.py uses this for meta
Comment on lines +431 to +432
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the answer. But why pass the ctrl modifier as a key f'ctrl {key}`, and the alt modifier using a special event?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it took me a while to figure out what was going on in Lib\_pyrepl\keymap.py, cause the code is not very readable, but that's what it expects. If we wanted Ctrl and Alt to be treated in a similar way, we'd probably have to make some major edits to Lib\_pyrepl\keymap.py and Lib\_pyrepl\unix_eventqueue.py.

return Event(evt="key", data=key, raw=key)
if block:
continue

return None

return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar)
if key_event.dwControlKeyState & ALT_ACTIVE:
# queue the key, return the meta command
self.event_queue.insert(0, Event(evt="key", data=key, raw=raw_key))
return Event(evt="key", data="\033") # keymap.py uses this for meta
paulie4 marked this conversation as resolved.
Show resolved Hide resolved

return Event(evt="key", data=key, raw=raw_key)

def push_char(self, char: int | bytes) -> None:
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix ``PyREPL`` on Windows to support more keybindings, like the :kbd:`Control-` and :kbd:`Control-` word-skipping keybindings and those with meta (i.e. :kbd:`Alt`), e.g. :kbd:`Alt-d` to ``kill-word`` or :kbd:`Alt-Backspace` ``backward-kill-word``.
Loading