-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
VT/xterm mouse reporting not working at all outside of WSL #15977
Comments
You cannot use the existence of visible output in the shell as an indication whether mouse mode is enabled or not. When mouse mode is enabled, the terminal writes these sequences ( Do you have an application that runs outside of WSL where mouse mode doesn't work? You could try writing your own test application in C or C# for instance. |
@lhecker Ah I didn't know that, thanks for the info. My intent there was to try to do a reproduction as close to the shells themselves as possible to cut out any possible middleman like a runtime, but I suppose I shot myself in the foot instead, oops. In that case I did originally run into this with a small test Node.js script: // @file vt.mjs
const
PRESSED_LMB = 0b00_0_000_00, // 0 [MB1]
PRESSED_MMB = 0b00_0_000_01, // 1 [MB2]
PRESSED_RMB = 0b00_0_000_10, // 2 [MB3]
PRESSED_NONE = 0b00_0_000_11, // 3 (1+2)
MODIF_SHIFT = 0b00_0_001_00, // 4
MODIF_META = 0b00_0_010_00, // 8
MODIF_CTRL = 0b00_0_100_00, // 16
MOTION_EVENT = 0b00_1_000_00, // 32
FLAG_MB4_7 = 0b01_0_000_00, // 64
SCROLL_UP = 0b01_0_000_00, // 64 [MB4]
SCROLL_DOWN = 0b01_0_000_01, // 65 (64+1) [MB5]
USED_MB6 = 0b01_0_000_10, // 66 (64+2) [MB6]
USED_MB7 = 0b01_0_000_11, // 67 (64+3) [MB7]
FLAG_MB8_11 = 0b10_0_000_00, // 128
USED_MB8 = 0b10_0_000_00, // 128 [MB8]
USED_MB9 = 0b10_0_000_01, // 129 (128+1) [MB9]
USED_MB10 = 0b10_0_000_10, // 130 (128+2) [MB10]
USED_MB11 = 0b10_0_000_11, // 131 (128+3) [MB11]
__$stub__ = null;
const BUTTON_STATE_PRESSED = 'M';
const BUTTON_STATE_RELEASED = 'm';
process.stdin.resume();
process.stdin.setRawMode(true);
//process.stdout.write('\x1B[?2004h'); // Enable bracketed paste mode
process.stdout.write('\x1B[?1006h'); // Enable SGR mouse mode
process.stdout.write('\x1B[?1003h'); // Enable any event mouse mode
// 1000 -> only listen to button press and release
// 1002 -> listen to button press and release + mouse motion only while pressing button
// 1003 -> listen to button press and release + mouse motion at all times
process.on('exit', () => {
// disable all the modes
//process.stdout.write('\x1B[?2004l');
process.stdout.write('\x1B[?1006l');
process.stdout.write('\x1B[?1003l');
});
process.stdin.on('data', (buf) => {
const seq = buf.toString('utf8');
if (seq === '\u0003') {
console.error('Ctrl+C');
return process.stdin.pause();
}
if (!seq.startsWith('\x1B[<')) return; // not a mouse event
const [btn, x, y] = seq.slice(3, -1).split(';').map(Number);
const event = {};
if (btn & FLAG_MB8_11) {
if ((btn & USED_MB11) === USED_MB11) event.button = 'MB11';
else if ((btn & USED_MB10) === USED_MB10) event.button = 'MB10';
else if ((btn & USED_MB9) === USED_MB9) event.button = 'MB9';
else event.button = 'MB8';
}
else if (btn & FLAG_MB4_7) {
if ((btn & USED_MB7) === USED_MB7) event.button = 'MB7';
else if ((btn & USED_MB6) === USED_MB6) event.button = 'MB6';
else if ((btn & SCROLL_DOWN) === SCROLL_DOWN) event.button = 'scroll_down';
else event.button = 'scroll_up';
}
else {
if ((btn & PRESSED_NONE) === PRESSED_NONE) event.button = null;
else if (btn & PRESSED_RMB) event.button = 'right';
else if (btn & PRESSED_MMB) event.button = 'middle';
else event.button = 'left';
}
event.state = seq.at(-1) === BUTTON_STATE_PRESSED ? 'pressed' : 'released';
event.x = x;
event.y = y;
event.motion = !!(btn & MOTION_EVENT);
event.shift = !!(btn & MODIF_SHIFT);
event.meta = !!(btn & MODIF_META);
event.ctrl = !!(btn & MODIF_CTRL);
logMouseEvent(event);
});
const $ = {
bold: '\x1B[1m',
dim: '\x1B[2m',
underline: '\x1B[4m',
blink: '\x1B[5m',
invert: '\x1B[7m',
invisible: '\x1B[8m',
reset: '\x1B[0m',
//noBold: '\x1B[21m', (broken)
noDim: '\x1B[22m',
noUnderline: '\x1B[24m',
noBlink: '\x1B[25m',
noInvert: '\x1B[27m',
visible: '\x1B[28m',
black: '\x1B[30m',
red: '\x1B[31m',
green: '\x1B[32m',
yellow: '\x1B[33m',
blue: '\x1B[34m',
purple: '\x1B[35m',
cyan: '\x1B[36m',
white: '\x1B[37m',
gray: '\x1B[90m',
redBright: '\x1B[91m',
greenBright: '\x1B[92m',
yellowBright: '\x1B[93m',
blueBright: '\x1B[94m',
purpleBright: '\x1B[95m',
cyanBright: '\x1B[96m',
whiteBright: '\x1B[97m',
};
console.log($.gray + 'Listening to mouse events:' + $.reset);
function logMouseEvent(event) {
const { button, state, x, y, motion, shift, meta, ctrl } = event;
console.log(
`${(state[0] === 'r' ? $.green : $.red) + upperFirst(state).padEnd(8) + $.reset} ` +
`${(button ? '' : $.dim) + (button ?? 'none').padEnd(11) + $.reset} at ${$.gray}(` +
`${$.reset + $.yellow}${x.toString().padStart(3)}${$.reset + $.gray}, ` +
`${$.reset + $.yellow}${y.toString().padStart(3)}${$.reset + $.gray})${$.reset} ` +
`${motion ? $.purple : $.dim}[MOTION]${$.reset} ${shift ? $.blue : $.dim}[SHIFT]${$.reset} ` +
`${meta ? $.cyan : $.dim}[META]${$.reset} ${ctrl ? $.yellow : $.dim}[CTRL]${$.reset}`
);
}
function upperFirst(str) { return str[0].toUpperCase() + str.slice(1); } Ran with Node.js v20.2.0 as: And here's an example of expected output from the script:
whereas without Edit: |
FYI The shell doesn't really sit in the middle between the terminal and the application it spawns. It's not like this: flowchart TD
Terminal --> Shell --> Application
but rather like this: flowchart TD
Terminal --> Shell
Terminal --> Application
The "Shell" doesn't accidentally snatch away input from your "Application" simply because the shell waits for the application to exit before it continues reading from
If you do it like that you won't see any output since console.log('seq: ', JSON.stringify(seq)); I currently don't have nodejs installed and haven't used it in a while. So if the above doesn't help you resolve the issue and if no one else comes along in the meantime, I might only come around to debug this in a little bit (after finishing up 1.19). 😥 |
Oh right, I should've asked: Do you have |
Holy bonkers, quick offtopic here but I didn't know GitHub had support for fancy flowcharts like that, that's amazing. Thanks for showing me those (and the info itself of course) Anyway, you're right about the
Well oops again, I thought |
Isn't this just the console input mode not being set properly? I wouldn't know how to do this with Node, but I believe adding the moral equivalent of this after the
Error handling and restoring the mode before you exit omitted for clarity. |
That's what these lines are doing already: process.stdout.write('\x1B[?1006h'); // Enable SGR mouse mode
process.stdout.write('\x1B[?1003h'); // Enable any event mouse mode It's PowerShell that isn't listening to them. Neither WSL/bash have mouse mode enabled by default so it couldn't just be on prior. |
No. Those are acting at a very different level of abstraction. You still need the those 1006 & 1003 DECSET sequences to ask Terminal to start reporting mouse events, but first you need to tell the ConPTY through which you are talking to Terminal that you want VT style input rather than INPUT_RECORDs via ReadConsoleInput. Also note that the lack of other flags there are as important as the ones I turned on. Leaving out ENABLE_LINE_INPUT, ENABLE_ECHO_INPUT and ENABLE_QUICK_EDIT_MODE are important too for most this sort of scenario under Windows. (And for historical reasons you need that ENABLE_EXTENDED_FLAGS to make the lack of ENABLE_QUICK_EDIT_MODE mean anything.) |
Ah well in this case I suppose it's still a lacking feature and parity issue with other shells that powershell is the one standing out that doesn't do this for you, ideally you'd want that code to work hassle-free on any shell/term. What exactly is SetConsoleMode under the hood though? Is it another but different kind of terminal sequence or is it a syscall? If its the latter thats pretty unusable for simple scripts too to have to FFI with native program linked with the windows console apis just for expected behavior from powershell... |
Again, no. This is absolutely nothing to do with PowerShell. (And if it did, this wouldn't be the right repo.) You can see this for yourself if you open a "Command Prompt", aka cmd.exe tab, in Terminal instead and run your tests there. Zero PowerShell involved, same results. Or similarly if you run your script directly, with Win+R or from a shortcut icon, where there's no cmd.exe involved either. It is a Windows Console subsystem thing, not a shell thing. SetConsoleMode is effectively a syscall, not just more terminal sequences. (Whether it's technically a syscall in the sense of there being a transition to kernel mode, I have no idea, but it is an API that changes things completely out of band from the stdio streams.) I understand your frustration - the docs on this aren't super clear yet and I had to go through the same discovery cycle myself not long ago. This is the important, official, primer explaining how things work currently and are expected to work in the future: Classic Console APIs versus Virtual Terminal Sequences In particular, towards the end there you'll see this section:
It is that last paragraph that's catching you. If you don't tell the system you want VT input, you aren't going to get it. You can see this independent of the mouse support. If you don't turn on ENABLE_VIRTUAL_TERMINAL_INPUT you aren't, for example, going to get "\e[D" for a left arrow either. A little searching though the nodejs source suggests it is getting in the middle and trying to help you, but incompletely. In here you'll see it's using both SetConsoleMode and ReadConsoleInput to simulate some VT codes itself, but doesn't ask for or pass through mouse input at all. I don't think it will get in the way if you enable it yourself though. Probably easier than FFI, you could use a tiny stub in some compiled language that you execute at the start of your script to set the proper modes and again at the end to restore them. If that would be helpful, I can provide that code in C or C#. |
I got to thinking about the hackery in nodejs's tty.c and the implications of it. If you look at the function get_vt100_fn_key you'll see that nodejs reading the input on Windows via ReadConsoleInput and then generating it's own VT sequences for a set of inputs it knows about, which notably does not include mouse inputs. As @lhecker point out up thread, the shell you are using isn't between you and the terminal when your code is running. The runtime you are using, nodejs in this case, certainly is. Hopefully in the future it will learn to either generate mouse input or enable VT input when you put it in raw mode, but right now it does neither. You can work around it with the approach I mentioned above, and I've gone ahead and made sample code for that. #define UNICODE
#include<windows.h>
#include<stdio.h>
#define MAX_MESSAGE_SIZE 1024
wchar_t *LastErrorMessage() {
static wchar_t message[MAX_MESSAGE_SIZE];
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, GetLastError(), 0, message, MAX_MESSAGE_SIZE, NULL);
return message;
}
void fail(wchar_t *message, ...) {
va_list args;
va_start(args, message);
vfwprintf_s(stderr, message, args);
va_end(args);
exit(1);
}
int wmain(int argc, wchar_t *argv[]) {
wchar_t *endOfArg, *errorMessage;
DWORD oldMode, newMode;
HANDLE h;
if (argc < 2)
fail(L"usage: SetConsoleInputMode <hex-input-mode>\nThe current mode will be output as hex for restoring later.");
else {
newMode = wcstoul(argv[1], &endOfArg, 16);
if (endOfArg != argv[1] + wcslen(argv[1]))
fail(L"New input mode '%ls' did not parse as hex.\n", argv[1]);
h = GetStdHandle(STD_INPUT_HANDLE);
if (h == INVALID_HANDLE_VALUE)
fail(L"Unexpected error getting standard input: %ls\n", LastErrorMessage());
if (!GetConsoleMode(h, &oldMode))
fail(L"Unexpected error getting current console input mode: %ls\n", LastErrorMessage());
fwprintf(stdout, L"%08lx", oldMode);
if (!SetConsoleMode(h, newMode)) {
errorMessage = LastErrorMessage();
SetConsoleMode(h, oldMode); /* SetConsoleMode failures can leave some modes set so try and restore */
fail(L"Unable to set console input mode: %ls\n", errorMessage);
}
}
return 0;
} And here is test.mjs that uses the above and displays mouse input: import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
const setConsoleInputModeBinary = join(process.cwd(), 'SetConsoleInputMode.exe');
const setConsoleInputMode = (mode) =>
execFileSync(setConsoleInputModeBinary, [mode], { stdio: ['inherit', 'pipe', 'pipe'] });
process.stdin.on('data', (buf) => {
const seq = buf.toString('utf8');
if (seq == 'q') {
process.stdout.write('\x1B[?1006l');
process.stdout.write('\x1B[?1003l');
setConsoleInputMode('0x01e7');
process.exit(0);
} else {
console.log(JSON.stringify(seq));
}
});
process.stdin.setRawMode(true);
setConsoleInputMode('0x0290');
process.stdout.write('\x1B[?1006h');
process.stdout.write('\x1B[?1003h'); The magic numbers, 290 and 1e7 are composed from:
|
Hey sorry for the late response, this was for a bit of a random coding side-quest which I ended up forgetting about for a while...! I was definitely not expecting such in-depth response! Really appreciate all of the info and example code (which works perfectly!). I understand the semantics going on here all clearly now (the concrete code example really helped tons), and I can see it really has nothing to do with Windows Terminal or PowerShell, but rather Node getting in the way with the wrong console mode, so I'll close this here and maybe take up a new issue on Node's repo assuming there isn't already one for this. Thanks for all the detailed info and assistance 👍 |
Windows Terminal version
1.17.11461.0
Windows build number
Microsoft Windows [Version 10.0.19045.3448]
Other Software
PowerShell 7.4.0-preview.5
WSL version: 1.2.5.0
Steps to reproduce
To reproduce the expected behavior inside WSL:
echo -e '\E[?1006h'
+echo -e '\E[?1003h'
To reproduce the faulty behavior outside WSL:
Write-Output "$([char]27)[?1006h"
+Write-Output "$([char]27)[?1003h"
Extra info
According to #545 (comment), #14958, this article and lots of other sources everything seems to indicate this is supposed to be supported, even outside of WSL according to the last article.
I've already found references of some comments saying "QuickEdit" mode might interfere with VT mouse capture, but I already turned that off and it still doesn't work. I've also already tried the command:
Set-ItemProperty HKCU:\Console VirtualTerminalLevel -Type DWORD 1
but I'm pretty sure that was already set anyway.I can also reproduce this outside of WT with pwsh.exe directly, and have looked through the PowerShell repository issues for this too, but found conflicting cases of people being redirected back and forth between that repository and this one for similar issues to mine (because apparently conhost issues are also tracked here?), so I'm not really sure which one this would fit better, feel free to move the issue to PowerShell if I guessed wrong.
Expected Behavior
Inside WSL, I obtain the following:
from moving the mouse around after the two inputs. I expected the same feedback outside of WSL.
Actual Behavior
Outside of WSL:
No matter what is done with the mouse, move, drag, click, scroll, etc no feedback is given by the terminal.
The text was updated successfully, but these errors were encountered: