VT/xterm mouse reporting not working at all outside of WSL #20491

Closed
opened 2026-01-31 07:15:19 +00:00 by claunia · 12 comments
Owner

Originally created by @jhmaster2000 on GitHub (Sep 17, 2023).

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 https://github.com/microsoft/terminal/issues/545#issuecomment-509623410, #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:

jhmaster@Ubuntu:~$ echo -e '\E[?1006h'

jhmaster@Ubuntu:~$ echo -e '\E[?1003h'

jhmaster@Ubuntu:~$ 35;66;17m35;65;17m35;64;17m35;64;16m35;63;16m35;62;16m35;61;16m...

from moving the mouse around after the two inputs. I expected the same feedback outside of WSL.

Actual Behavior

Outside of WSL:

PS C:\...> Write-Output "$([char]27)[?1006h"

PS C:\...> Write-Output "$([char]27)[?1003h"

PS C:\...>

No matter what is done with the mouse, move, drag, click, scroll, etc no feedback is given by the terminal.

Originally created by @jhmaster2000 on GitHub (Sep 17, 2023). ### 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 https://github.com/microsoft/terminal/issues/545#issuecomment-509623410, #14958, [this article](https://www.zdnet.com/article/microsofts-windows-terminal-now-has-mouse-support-for-windows-subsystem-for-linux/) 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: ``` jhmaster@Ubuntu:~$ echo -e '\E[?1006h' jhmaster@Ubuntu:~$ echo -e '\E[?1003h' jhmaster@Ubuntu:~$ 35;66;17m35;65;17m35;64;17m35;64;16m35;63;16m35;62;16m35;61;16m... ``` from moving the mouse around after the two inputs. I expected the same feedback outside of WSL. ### Actual Behavior Outside of WSL: ``` PS C:\...> Write-Output "$([char]27)[?1006h" PS C:\...> Write-Output "$([char]27)[?1003h" PS C:\...> ``` No matter what is done with the mouse, move, drag, click, scroll, etc no feedback is given by the terminal.
Author
Owner

@lhecker commented on GitHub (Sep 18, 2023):

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 (\e[35;66;17m, etc.) to the shell / terminal application which then reads them via stdin. Whether the shell then writes this input to stdout is up to the shell. When you say "WSL" for instance, what you actually see is bash writing the stdin input to stdout. Other shells under WSL like zsh or fish don't do that. PowerShell under Windows doesn't do it either.

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 commented on GitHub (Sep 18, 2023): 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 (`\e[35;66;17m`, etc.) to the shell / terminal application which then reads them via `stdin`. Whether the shell then writes this input to `stdout` is up to the shell. When you say "WSL" for instance, what you actually see is `bash` writing the `stdin` input to `stdout`. Other shells under WSL like zsh or fish don't do that. PowerShell under Windows doesn't do it either. 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.
Author
Owner

@jhmaster2000 commented on GitHub (Sep 18, 2023):

@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: node vt.mjs (please note the .mjs extension is important if trying to repro) on both sides (inside WSL and out)

And here's an example of expected output from the script:

PS C:\...> wsl node ./js/vt.mjs
Listening to mouse events:
Released none        at (  1,  14) [MOTION] [SHIFT] [META] [CTRL]
Released none        at (  2,  14) [MOTION] [SHIFT] [META] [CTRL]
Released none        at (  3,  14) [MOTION] [SHIFT] [META] [CTRL]
Released none        at (  4,  14) [MOTION] [SHIFT] [META] [CTRL]
Released none        at (  5,  14) [MOTION] [SHIFT] [META] [CTRL]
Released none        at (  6,  14) [MOTION] [SHIFT] [META] [CTRL]
Released none        at (  7,  14) [MOTION] [SHIFT] [META] [CTRL]
Pressed  left        at (  7,  14) [MOTION] [SHIFT] [META] [CTRL]
Released left        at (  7,  14) [MOTION] [SHIFT] [META] [CTRL]
Pressed  right       at (  7,  14) [MOTION] [SHIFT] [META] [CTRL]
Released right       at (  7,  14) [MOTION] [SHIFT] [META] [CTRL]
Pressed  left        at (  7,  14) [MOTION] [SHIFT] [META] [CTRL]
Released left        at (  7,  13) [MOTION] [SHIFT] [META] [CTRL]
Pressed  scroll_down at (  7,  12) [MOTION] [SHIFT] [META] [CTRL]
...

whereas without wsl only the first line Listening to mouse events: gets printed and no further feedback.

Edit:
Additionally in the JS source I also tried adding an unconditional console.log('seq:', seq); before the if (!seq.startsWith('\x1B[<')) return; // not a mouse event check, incase it was some encoding issue that caused that check to fail or something of the sort, but negative, truly nothing is being sent from mouse actions. (But from that new log I do can see keyboard presses being received, so its also not Node.js on Windows that is failing to receive TTY events or something like that either)

@jhmaster2000 commented on GitHub (Sep 18, 2023): @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: ```js // @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: `node vt.mjs` (please note the .mjs extension is important if trying to repro) on both sides (inside WSL and out) And here's an example of expected output from the script: ``` PS C:\...> wsl node ./js/vt.mjs Listening to mouse events: Released none at ( 1, 14) [MOTION] [SHIFT] [META] [CTRL] Released none at ( 2, 14) [MOTION] [SHIFT] [META] [CTRL] Released none at ( 3, 14) [MOTION] [SHIFT] [META] [CTRL] Released none at ( 4, 14) [MOTION] [SHIFT] [META] [CTRL] Released none at ( 5, 14) [MOTION] [SHIFT] [META] [CTRL] Released none at ( 6, 14) [MOTION] [SHIFT] [META] [CTRL] Released none at ( 7, 14) [MOTION] [SHIFT] [META] [CTRL] Pressed left at ( 7, 14) [MOTION] [SHIFT] [META] [CTRL] Released left at ( 7, 14) [MOTION] [SHIFT] [META] [CTRL] Pressed right at ( 7, 14) [MOTION] [SHIFT] [META] [CTRL] Released right at ( 7, 14) [MOTION] [SHIFT] [META] [CTRL] Pressed left at ( 7, 14) [MOTION] [SHIFT] [META] [CTRL] Released left at ( 7, 13) [MOTION] [SHIFT] [META] [CTRL] Pressed scroll_down at ( 7, 12) [MOTION] [SHIFT] [META] [CTRL] ... ``` whereas without `wsl` only the first line `Listening to mouse events:` gets printed and no further feedback. **Edit:** Additionally in the JS source I also tried adding an unconditional `console.log('seq:', seq);` before the `if (!seq.startsWith('\x1B[<')) return; // not a mouse event` check, incase it was some encoding issue that caused that check to fail or something of the sort, but negative, truly nothing is being sent from mouse actions. (But from that new log I do can see keyboard presses being received, so its also not Node.js on Windows that is failing to receive TTY events or something like that either)
Author
Owner

@lhecker commented on GitHub (Sep 18, 2023):

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

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 stdin.

Additionally in the JS source I also tried adding an unconditional console.log('seq:', seq); before the if (!seq.startsWith('\x1B[<')) return; // not a mouse event check, incase it was some encoding issue that caused that check to fail or something of the sort, but negative, truly nothing is being sent from mouse actions.

If you do it like that you won't see any output since seq will contain an escape sequence, which aren't visible text (the terminal parses them away). Try this instead:

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). 😥

@lhecker commented on GitHub (Sep 18, 2023): > 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 FYI The shell doesn't really sit in the middle between the terminal and the application it spawns. It's not like this: ```mermaid flowchart TD Terminal --> Shell --> Application ``` but rather like this: ```mermaid 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 `stdin`. > Additionally in the JS source I also tried adding an unconditional `console.log('seq:', seq);` before the `if (!seq.startsWith('\x1B[<')) return; // not a mouse event` check, incase it was some encoding issue that caused that check to fail or something of the sort, but negative, truly nothing is being sent from mouse actions. If you do it like that you won't see any output since `seq` will contain an escape sequence, which aren't visible text (the terminal parses them away). Try this instead: ```js 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). 😥
Author
Owner

@lhecker commented on GitHub (Sep 18, 2023):

Oh right, I should've asked: Do you have node installed inside WSL? Because if you don't then wsl node will call the Windows node.exe through a compatibility shim (ConPTY) through WSL which goes through the shim again and back into Windows Terminal. If you do that, and if the above JSON.stringify didn't help resolve the issue, try installing node inside WSL. If that finally resolves the issue it's probably a bug in system ConPTY (which we maintain).

@lhecker commented on GitHub (Sep 18, 2023): Oh right, I should've asked: Do you have `node` installed inside WSL? Because if you don't then `wsl node` will call the Windows `node.exe` through a compatibility shim (ConPTY) through WSL which goes through the shim again and back into Windows Terminal. If you do that, and if the above `JSON.stringify` didn't help resolve the issue, try installing `node` inside WSL. If _that_ finally resolves the issue it's probably a bug in system ConPTY (which we maintain).
Author
Owner

@jhmaster2000 commented on GitHub (Sep 18, 2023):

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

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 stdin.

Additionally in the JS source I also tried adding an unconditional console.log('seq:', seq); before the if (!seq.startsWith('\x1B[<')) return; // not a mouse event check, incase it was some encoding issue that caused that check to fail or something of the sort, but negative, truly nothing is being sent from mouse actions.

If you do it like that you won't see any output since seq will contain an escape sequence, which aren't visible text (the terminal parses them away). Try this instead:

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). 😥

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 seq value itself not showing with that, I actually originally did it with console.dir(seq) which showed the string's inspected value properly, that was just a bit of a poor recreation, my bad. But even with that poor recreation you can still tell whether something is being sent at all or not because the 'seq:' string before the actual value would print :P (which I also tested it indeed doesn't)

Oh right, I should've asked: Do you have node installed inside WSL? Because if you don't then wsl node will call the Windows node.exe through a compatibility shim (ConPTY) through WSL which goes through the shim again and back into Windows Terminal. If you do that, and if the above JSON.stringify didn't help resolve the issue, try installing node inside WSL. If that finally resolves the issue it's probably a bug in system ConPTY (which we maintain).

Well oops again, I thought wsl <cmd> would just be a shortcut for wsl and then entering the command inside, which is just what I wanted to signal was happening in that snippet, I do have node installed inside WSL and get the same working-as-intended output from running node vt.mjs inside WSL manually instead of wsl <cmd>.

@jhmaster2000 commented on GitHub (Sep 18, 2023): > > 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 > > FYI The shell doesn't really sit in the middle between the terminal and the application it spawns. It's not like this: > > ```mermaid > flowchart TD > Terminal --> Shell --> Application > ``` > but rather like this: > > ```mermaid > 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 `stdin`. > > > Additionally in the JS source I also tried adding an unconditional `console.log('seq:', seq);` before the `if (!seq.startsWith('\x1B[<')) return; // not a mouse event` check, incase it was some encoding issue that caused that check to fail or something of the sort, but negative, truly nothing is being sent from mouse actions. > > If you do it like that you won't see any output since `seq` will contain an escape sequence, which aren't visible text (the terminal parses them away). Try this instead: > > ```js > 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). 😥 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 `seq` value itself not showing with that, I actually originally did it with `console.dir(seq)` which showed the string's inspected value properly, that was just a bit of a poor recreation, my bad. But even with that poor recreation you can still tell whether something is being sent at all or not because the `'seq:'` string before the actual value would print :P (which I also tested it indeed doesn't) > Oh right, I should've asked: Do you have `node` installed inside WSL? Because if you don't then `wsl node` will call the Windows `node.exe` through a compatibility shim (ConPTY) through WSL which goes through the shim again and back into Windows Terminal. If you do that, and if the above `JSON.stringify` didn't help resolve the issue, try installing `node` inside WSL. If _that_ finally resolves the issue it's probably a bug in system ConPTY (which we maintain). Well oops again, I thought `wsl <cmd>` would just be a shortcut for `wsl` and then entering the command inside, which is just what I wanted to signal was happening in that snippet, I do have `node` installed inside WSL and get the same working-as-intended output from running `node vt.mjs` inside WSL manually instead of `wsl <cmd>`.
Author
Owner

@bcdev-com commented on GitHub (Oct 2, 2023):

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 process.stdin.setRawMode() should fix things. WSL and/or bash are likely doing this for you, but PowerShell does not.

HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
SetConsoleMode(h, ENABLE_EXTENDED_FLAGS | ENABLE_MOUSE_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT);

Error handling and restoring the mode before you exit omitted for clarity.

@bcdev-com commented on GitHub (Oct 2, 2023): 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 `process.stdin.setRawMode()` should fix things. WSL and/or bash are likely doing this for you, but PowerShell does not. ``` HANDLE h = GetStdHandle(STD_INPUT_HANDLE); SetConsoleMode(h, ENABLE_EXTENDED_FLAGS | ENABLE_MOUSE_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT); ``` Error handling and restoring the mode before you exit omitted for clarity.
Author
Owner

@jhmaster2000 commented on GitHub (Oct 2, 2023):

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.

@jhmaster2000 commented on GitHub (Oct 2, 2023): That's what these lines are doing already: ```js 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.
Author
Owner

@bcdev-com commented on GitHub (Oct 3, 2023):

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.)

@bcdev-com commented on GitHub (Oct 3, 2023): 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.)
Author
Owner

@jhmaster2000 commented on GitHub (Oct 3, 2023):

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...

@jhmaster2000 commented on GitHub (Oct 3, 2023): 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...
Author
Owner

@bcdev-com commented on GitHub (Oct 3, 2023):

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:

Exceptions for using Windows Console APIs
A limited subset of Windows Console APIs is still necessary to establish the initial environment. The Windows platform still differs from others in process, signal, device, and encoding handling:

The standard handles to a process will still be controlled with GetStdHandle and SetStdHandle.

Configuration of the console modes on a handle to opt in to Virtual Terminal Sequence support will be handled with GetConsoleMode and SetConsoleMode.

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#.

@bcdev-com commented on GitHub (Oct 3, 2023): 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](https://learn.microsoft.com/en-us/windows/console/classic-vs-vt) In particular, towards the end there you'll see this section: > **Exceptions for using Windows Console APIs** > A limited subset of Windows Console APIs is still necessary to establish the initial environment. The Windows platform still differs from others in process, signal, device, and encoding handling: > > The standard handles to a process will still be controlled with [GetStdHandle](https://learn.microsoft.com/en-us/windows/console/getstdhandle) and [SetStdHandle](https://learn.microsoft.com/en-us/windows/console/setstdhandle). > > Configuration of the console modes on a handle to opt in to Virtual Terminal Sequence support will be handled with [GetConsoleMode](https://learn.microsoft.com/en-us/windows/console/getconsolemode) and [SetConsoleMode](https://learn.microsoft.com/en-us/windows/console/setconsolemode). 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 ](https://github.com/nodejs/node/blob/main/deps/uv/src/win/tty.c) 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#.
Author
Owner

@bcdev-com commented on GitHub (Oct 3, 2023):

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.
Here is SetConsoleInputMode.c:

#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:

        ENABLE_PROCESSED_INPUT = 0x0001
        ENABLE_LINE_INPUT = 0x0002
        ENABLE_ECHO_INPUT = 0x0004
        ENABLE_WINDOW_INPUT = 0x0008
        ENABLE_MOUSE_INPUT = 0x0010
        ENABLE_INSERT_MODE = 0x0020
        ENABLE_QUICK_EDIT_MODE = 0x0040
        ENABLE_EXTENDED_FLAGS = 0x0080
        ENABLE_AUTO_POSITION = 0x0100
        ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
@bcdev-com commented on GitHub (Oct 3, 2023): 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](https://github.com/nodejs/node/blob/main/deps/uv/src/win/tty.c#L626) 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. Here is SetConsoleInputMode.c: ```c #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: ```js 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: ``` ENABLE_PROCESSED_INPUT = 0x0001 ENABLE_LINE_INPUT = 0x0002 ENABLE_ECHO_INPUT = 0x0004 ENABLE_WINDOW_INPUT = 0x0008 ENABLE_MOUSE_INPUT = 0x0010 ENABLE_INSERT_MODE = 0x0020 ENABLE_QUICK_EDIT_MODE = 0x0040 ENABLE_EXTENDED_FLAGS = 0x0080 ENABLE_AUTO_POSITION = 0x0100 ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 ```
Author
Owner

@jhmaster2000 commented on GitHub (Nov 8, 2023):

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 👍

@jhmaster2000 commented on GitHub (Nov 8, 2023): 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 👍
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#20491