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

Fix Control+Space not sent to program running in terminal #16298

Merged
merged 1 commit into from
Nov 27, 2023

Conversation

lonnywong
Copy link
Contributor

@lonnywong lonnywong commented Nov 12, 2023

Converts null byte to specific input event, so that it's properly delivered to the program running in the terminal.

Closes #15939

@microsoft-github-policy-service microsoft-github-policy-service bot added Issue-Bug It either shouldn't be doing this or needs an investigation. Area-Input Related to input processing (key presses, mouse, etc.) Priority-2 A description (P2) Product-Terminal The new Windows Terminal. labels Nov 12, 2023
@lonnywong lonnywong changed the title Fix Control+Space not sent to program running in terminal (#15939) Fix Control+Space not sent to program running in terminal Nov 13, 2023
@lonnywong
Copy link
Contributor Author

This is a test program written in go:

package main

import (
	"fmt"
	"os"
	"syscall"

	"golang.org/x/sys/windows"
	"golang.org/x/term"
)

func main() {
	inHandle, err := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
	if err != nil {
		fmt.Printf("GetStdHandle: %v\r\n", err)
		return
	}
	var inMode uint32
	if err := windows.GetConsoleMode(windows.Handle(inHandle), &inMode); err != nil {
		fmt.Printf("GetConsoleMode: %v\r\n", err)
		return
	}
	defer windows.SetConsoleMode(windows.Handle(inHandle), inMode)
	if err := windows.SetConsoleMode(windows.Handle(inHandle), inMode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil {
		fmt.Printf("SetConsoleMode: %v\r\n", err)
		return
	}

	state, err := term.MakeRaw(int(os.Stdin.Fd()))
	if err != nil {
		fmt.Printf("MakeRaw: %v\r\n", err)
		return
	}
	defer term.Restore(int(os.Stdin.Fd()), state)

	buf := make([]byte, 100)
	for {
		n, err := os.Stdin.Read(buf)
		if err != nil {
			fmt.Printf("os.Stdin.Read(buf): %v\r\n", err)
			return
		}
		if n > 0 {
			fmt.Printf("%v\r\n", buf[:n])
		} else {
			fmt.Println("nil")
		}
		if n == 1 && buf[0] == '\x03' {
			return
		}
	}
}

src/host/inputBuffer.cpp Outdated Show resolved Hide resolved
Copy link
Contributor

@tusharsnx tusharsnx left a comment

Choose a reason for hiding this comment

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

Just a tiny nit.

I think we can pull this if block out of the loop if we assume NUL always comes as a single wchar_t:

        // If the text is just a null character, synthesize a key event that
        // exactly matches the null key event.
        if (const auto wch = til::at(text, 0); wch == UNICODE_NULL)
        {
            const auto zeroKey = OneCoreSafeVkKeyScanW(0);
            const auto vkey = LOWORD(zeroKey); 

            uint32_t modifiers = 0;
            if (WI_IsFlagSet(zeroKey, 0x100))
            {
                modifiers |= SHIFT_PRESSED;
            }
            if (WI_IsFlagSet(zeroKey, 0x200))
            {
                modifiers |= CTRL_PRESSED;
            }
            if (WI_IsFlagSet(zeroKey, 0x400))
            {
                modifiers |= ALT_PRESSED;
            }

            _storage.push_back(SynthesizeKeyEvent(true, 1, vkey, 0, wch, modifiers));
            return;
        }

        for (const auto& wch : text)
        {
            _storage.push_back(SynthesizeKeyEvent(true, 1, 0, 0, wch, 0));
        }

Haven't tested the code so take it with a grain of salt 😅

@lonnywong
Copy link
Contributor Author

lonnywong commented Nov 18, 2023

I think we can pull this if block out of the loop if we assume NUL always comes as a single wchar_t:

@tusharsnx Do we need to do it before the for loop? Is it possible that it is an input simulated by another program?

src/host/inputBuffer.cpp Outdated Show resolved Hide resolved
@tusharsnx
Copy link
Contributor

tusharsnx commented Nov 18, 2023

@lonnywong

Do we need to do it before the for loop? Is it possible that it is an input simulated by another program?

My thought process is that we process one inEvent, get the set of characters that is generated, and with that call _HandleTerminalInputCallback().

Since a null inEvent generates a single \0 (using _makeCharOutput(0)), we can assume it'll always be one character sent to the _HandleTerminalInputCallback(). That being said, it's worth some testing before making the change 🙂

@lonnywong
Copy link
Contributor Author

@tusharsnx Is it the same if we do it in the for loop? There are two more instructions ( test and jmp ) in each loop. But the performance impact should be negligible. Doing in the for loop also brings benefits: If some places are modified in the future, it will be compatible here.

@j4james
Copy link
Collaborator

j4james commented Nov 18, 2023

Since a null inEvent generates a single \0 (using _makeCharOutput(0)), we can assume it'll always be one character sent to the _HandleTerminalInputCallback().

I'm fairly sure you could also get a string like \x1B\x00 from a Ctrl+Alt+Space keypress. I don't think you can rely on the \0 always being the first character.

@lonnywong
Copy link
Contributor Author

Since a null inEvent generates a single \0 (using _makeCharOutput(0)), we can assume it'll always be one character sent to the _HandleTerminalInputCallback().

I'm fairly sure you could also get a string like \x1B\x00 from a Ctrl+Alt+Space keypress. I don't think you can rely on the \0 always being the first character.

I get \x1b\x00 from ctrl + alt + shift + 2 on my computer too.

@lhecker
Copy link
Member

lhecker commented Nov 18, 2023

FWIW I personally don't think this is the right approach, as it just works around the if condition in stream.cpp:

const auto zeroKey = OneCoreSafeVkKeyScanW(0);
if (LOBYTE(zeroKey) == Event.Event.KeyEvent.wVirtualKeyCode &&
WI_IsAnyFlagSet(Event.Event.KeyEvent.dwControlKeyState, ALT_PRESSED) == WI_IsFlagSet(zeroKey, 0x400) &&
WI_IsAnyFlagSet(Event.Event.KeyEvent.dwControlKeyState, CTRL_PRESSED) == WI_IsFlagSet(zeroKey, 0x200) &&
WI_IsAnyFlagSet(Event.Event.KeyEvent.dwControlKeyState, SHIFT_PRESSED) == WI_IsFlagSet(zeroKey, 0x100))
{
// This really is the character 0x0000
*pwchOut = Event.Event.KeyEvent.uChar.UnicodeChar;
return STATUS_SUCCESS;
}

Additionally, it introduces a disparity between how \0 is presented to a console application vs. any other character. Because now only \0 gets a vkey while all others don't.

Unfortunately, I can't really criticize this PR for it, because the framework for "my way" doesn't exist yet. Right now, InputBuffer is basically a wrapper around a

std::deque<INPUT_RECORD>

This is fairly limiting for us, because it requires us to translate raw strings into INPUT_RECORDs just to turn them back into strings if a function like GetChar in stream.cpp is called. Instead, I'd prefer if we had a

struct StringRecord {
    std::wstring str;
    size_t consumed = 0;
};

so that we can have a

std::deque<std::variant<INPUT_RECORD, StringRecord>>

That way we can store inputs like the above as their raw strings and extract them them as raw strings without any uncertainty over whether any given INPUT_RECORD "truly stores a null character" or not.

I'm marking this for our upcoming (internal) discussion, because I'm curious what others think about this.

@lhecker lhecker added the Needs-Discussion Something that requires a team discussion before we can proceed label Nov 18, 2023
@lonnywong
Copy link
Contributor Author

@lhecker Consider it a temporary solution until StringRecord is released?

@lhecker
Copy link
Member

lhecker commented Nov 18, 2023

Oh yeah, please don't get me wrong: I do think it's acceptable in the meantime. The only thing I find somewhat concerning is the vkey thing I mentioned. But I'm not entirely sure how important that is.

@lonnywong
Copy link
Contributor Author

Oh yeah, please don't get me wrong: I do think it's acceptable in the meantime. The only thing I find somewhat concerning is the vkey thing I mentioned. But I'm not entirely sure how important that is.

@lhecker You may want to check

// Pressing the control key causes all bits but the 5 least
// significant ones to be zeroed out (when using ASCII).
// This results in Ctrl+Space and Ctrl+@ being equal to a null byte.
// Normally the C0 control code set only defines Ctrl+@,
// but Ctrl+Space is also widely accepted by most terminals.
// -> Send a "null input sequence" in that case.
// We don't need to handle other kinds of Ctrl combinations,
// as we rely on the caller to pretranslate those to characters for us.
if (!WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED))
{
const auto ch = keyEvent.uChar.UnicodeChar;
const auto vkey = keyEvent.wVirtualKeyCode;
// Currently, when we're called with Ctrl+@, ch will be 0, since Ctrl+@ equals a null byte.
// VkKeyScanW(0) in turn returns the vkey for the null character (ASCII @).
// -> Use the vkey to alternatively determine if Ctrl+@ is being pressed.
if (ch == UNICODE_SPACE || (ch == UNICODE_NULL && vkey == LOBYTE(OneCoreSafeVkKeyScanW(0))))
{
return _makeCharOutput(0);
}
// Not all keyboard layouts contain mappings for Ctrl-key combinations.
// For instance the US one contains a mapping of Ctrl+\ to ^\,
// but the UK extended layout doesn't, in which case ch is null.
if (ch == UNICODE_NULL)
{
// -> Try to infer the character from the vkey.
auto mappedChar = LOWORD(OneCoreSafeMapVirtualKeyW(keyEvent.wVirtualKeyCode, MAPVK_VK_TO_CHAR));
if (mappedChar)
{
// Pressing the control key causes all bits but the 5 least
// significant ones to be zeroed out (when using ASCII).
mappedChar &= 0b11111;
return _makeCharOutput(mappedChar);
}
}
}

Copy link
Contributor

@tusharsnx tusharsnx left a comment

Choose a reason for hiding this comment

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

LGTM, and wait for the discussion to be over, I think it'll be helpful in guiding any additional changes we might require.

@lonnywong Thanks for the PR! This not only fixes the mentioned issue but also highlights a much needed change in the inputBuffer to disambiguate a VT-translated input vs an actual KeyEvent. 😃

Copy link
Collaborator

@j4james j4james left a comment

Choose a reason for hiding this comment

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

I'm in agreement with lhecker that it would be better if we could get rid of these translations altogether, but if that's not likely to happen soon, this PR seems like a reasonable short term fix.

@lonnywong
Copy link
Contributor Author

@tusharsnx @j4james Thanks for your help. Is it okay to be merged?

@j4james
Copy link
Collaborator

j4james commented Nov 19, 2023

@lonnywong You'll need approvals from two of the core team members to get the PR merged. Approval from outside contributors doesn't usually count. And I think lhecker was saying that they were going to discuss this issue internally before making a decision.

Copy link
Member

@DHowett DHowett left a comment

Choose a reason for hiding this comment

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

@lhecker brought this up with the team today, and while it does introduce another special case translation I think I'm okay with it. We should try to solve the problem all-up at some future point...

Thanks for all the discussion @j4james and the review notes @tusharsnx 😄

@DHowett
Copy link
Member

DHowett commented Nov 21, 2023

@lonnywong thanks for your contribution as well, of course!

@zadjii-msft zadjii-msft merged commit 8747a39 into microsoft:main Nov 27, 2023
12 of 14 checks passed
@zadjii-msft zadjii-msft added zInbox-Bug Ignore me! and removed Needs-Discussion Something that requires a team discussion before we can proceed labels Nov 27, 2023
DHowett pushed a commit that referenced this pull request Dec 4, 2023
Converts null byte to specific input event, so that it's properly
delivered to the program running in the terminal.

Closes #15939

(cherry picked from commit 8747a39)
Service-Card-Id: 91201444
Service-Version: 1.19
@GitMurf
Copy link

GitMurf commented Jan 20, 2024

I am really bad at following PRs to their release ;) is this something that has landed? if not, any ideas when it will? I would like to be able to confirm the fix solved the problem as I have this problem using neovim in windows terminal. Thanks!

@GitMurf
Copy link

GitMurf commented Jan 20, 2024

I found the canary branch and I installed that and the control-space issue does not seem to be solved for my case using neovim at least. Should this fix be expected to be landed in the canary branch by now? Thanks!

@lonnywong
Copy link
Contributor Author

I found the canary branch and I installed that and the control-space issue does not seem to be solved for my case using neovim at least. Should this fix be expected to be landed in the canary branch by now? Thanks!

@GitMurf Which version of ssh are you using? You can try #2865 (comment).
Or, you can try https://github.com/trzsz/trzsz-ssh ( an openssh client alternative ), it works for me.

@lonnywong
Copy link
Contributor Author

@GitMurf Are you using nvim locally, without ssh?
Is ctrl + space a shortcut key for switching input methods?

@GitMurf
Copy link

GitMurf commented Jan 21, 2024

@GitMurf Are you using nvim locally, without ssh?

Is ctrl + space a shortcut key for switching input methods?

@lonnywong i am not using ssh. Just straight nvim.exe locally. For example I currently am using the nightly build using the zip source from here: https://github.com/neovim/neovim/releases/tag/nightly

What do you mean about switching input methods?

@escritorio-gustavo
Copy link

I am really bad at following PRs to their release ;) is this something that has landed? if not, any ideas when it will? I would like to be able to confirm the fix solved the problem as I have this problem using neovim in windows terminal. Thanks!

Seems like this is the release that includes the fix https://github.com/microsoft/terminal/releases/tag/v1.20.10293.0

@BPplays
Copy link

BPplays commented Aug 13, 2024

@GitMurf ctrl + space is seems to also not be getting through to neovim for me too, did you manage to fix it. my wt version is 1.21.1772.0

What do you mean about switching input methods?

i think what they were talking about was that ctrl + space is an option for switching input method,
for example i have english and japanese as input methods that i can switch between with alt + shift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Input Related to input processing (key presses, mouse, etc.) Issue-Bug It either shouldn't be doing this or needs an investigation. Priority-2 A description (P2) Product-Terminal The new Windows Terminal. zInbox-Bug Ignore me!
Projects
Development

Successfully merging this pull request may close these issues.

Control+Space not sent to program running in terminal
9 participants