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

No mouse support on Windows #2676

Open
mplattner opened this issue Jan 11, 2025 · 8 comments
Open

No mouse support on Windows #2676

mplattner opened this issue Jan 11, 2025 · 8 comments
Labels

Comments

@mplattner
Copy link

mplattner commented Jan 11, 2025

I run visidata v3.1.1 on Windows 10 with Windows Terminal 1.21.3231.0. (Python 3.8.5 and Python 3.13.1)
Mouse clicks are not registered in the application. They are on Linux. It also works when I use ssh to connect to a Linux server that runs visidata v3.1.1 - so it shouldn't be the terminal's fault (I think).

Is there a way to fix this?

@mplattner mplattner added the bug label Jan 11, 2025
@midichef
Copy link
Contributor

midichef commented Jan 11, 2025

I do not know if this can be fixed. The lack of mouse clicks was reported a few months ago by @daviewales. It was reported when windows-curses released version 2.4.* (which fixed bugs making some keyboard keys unusable). So it may be a problem in windows-curses. However, I have not seen any reports on the windows-curses repo mentioning mouse clicks being a problem.

The dev team does not use Windows so the recommendation for running Windows is to use visidata inside WSL. Fixes to support Windows without WSL are welcome. They're just difficult to test and maintain.

If I had a Windows machine right now, I'd check whether handleMouse() is even being called when a click happens:

if keystroke == 'KEY_MOUSE':
try:
keystroke = vd.handleMouse(sheet) # if it was handled, don't handle again as a regular keystroke

And if no mouse event is being generated, maybe try looking at initCurses:

def initCurses(vd):

@mplattner
Copy link
Author

Thanks @midichef. I did some testing and found a fix:

Adding curses.def_prog_mode() after the call to mousemask in mouse.py:17

curses.mousemask(curses.MOUSE_ALL if vd.options.mouse_interval else 0)
enables mouse support on Windows 10 with Windows Terminal.

Result:

    curses.mousemask(curses.MOUSE_ALL if vd.options.mouse_interval else 0)
    curses.def_prog_mode()
    curses.mouseinterval(vd.options.mouse_interval)

I found it by trial-and-error and honestly I have no idea what curses.def_prog_mode() is doing ;-)
However, if it doesn't cause any side-effects on Linux, then we could add this line to mouse.py. Can anyone test this?

@daviewales
Copy link
Contributor

Some links about def_prog_mode:

This seems like a strange fix to me as well. Is it possible that there is a bug in windows-curses that this works around somehow?

@midichef
Copy link
Contributor

midichef commented Jan 14, 2025

Wow, great investigation @mplattner!

Some details of what happens in Visidata. def_prog_mode saves curses settings when initCurses() is first called. Those settings are restored by reset_prog_mode() in getkeystroke():

curses.reset_prog_mode() #1347

That restoration happens several times per second because getkeystroke() is called often in mainloop.py.

So maybe the sequence of events in Windows is:
Visidata calls the initCurses() that is defined in mainloop.py.
Then def_prog_mode() is called. Perhaps it saves the default mousemask of 0, allowing no mouse events. initCurses finishes.
Then Visidata calls the initCurses() defined in mouse.py. It sets mousemask to 0xffffffff.
Eventually the main loop runs getkeystroke(). In it, reset_prog_mode() is called, which restores the mousemask to 0.

And perhaps the Python implementations of curses in Mac and Linux have different behavior, where they do not save/restore the mousemask in calls to def_prog_mode()/reset_prog_mode().

I'm not sure how to check this guess. I can't find a detailed implementation of def_prog_mode() in a search of the windows-curses repo.

You could try checking whether the mousemask is being changed in the first call to getkeystroke():

            #add these lines
            availmask, oldmask = curses.mousemask(5)
            curses.mousemask(oldmask)
            self.status(f'mousemask before reset_prog_mode(): {oldmask}')

            curses.reset_prog_mode()  #1347

            #and add these lines
            availmask, oldmask = curses.mousemask(5)
            curses.mousemask(oldmask)
            self.status(f'mousemask after  reset_prog_mode(): {oldmask}')

On my system, Ubuntu 24.04, the mousemask is not changed by reset_prog_mode() here.

@daviewales
Copy link
Contributor

I think you're on the money @midichef.

Without @mplattner's def_prog_mode() fix, I get the following status:

 mousemask before reset_prog_mode(): 4294950383 
 [10x] mousemask after  reset_prog_mode(): 0    
 Listen.                                        
 [9x] mousemask before reset_prog_mode(): 0     

With def_prog_mode() added in mouse.py, I get:

[10x] mousemask before reset_prog_mode(): 4294950383
[10x] mousemask after  reset_prog_mode(): 4294950383

@midichef
Copy link
Contributor

Great. Looks like we've got a fix! I don't think adding def_prog_mode would cause problems on Linux/Mac. But it may be a good idea to move it to the very end of initCurses(), in case the Windows version saves mouseinterval or mouseEvents. Otherwise those would be reset in getkeystroke() too.

@mplattner Do you want to put together a PR?

It's probably a good idea to add a comment after the line like #2676 to highlight it as a trouble spot.

@midichef
Copy link
Contributor

midichef commented Jan 22, 2025

I found more implementation details. They confirm the guesses above about how the mouse was being disabled. windows-curses uses a fork of PDcurses. The PDcurses documentation says their reset_prog_mode() changes the mouse behavior on Windows only:

The non-portable functionality of reset_prog_mode() is handled here -- [...] In current ports: In OS/2, this sets the keyboard to binary mode; in Windows console, it enables or disables the mouse pointer to match the saved mode; in others it does nothing.

I'll leave some links here, in case we ever need to investigate this further in the future:
reset_prog_mode() calls PDC_mouse_set() in wincon/pdcscrn.c, and PDC_mouse_set() changes the mouse mode using SetConsoleMode(). I don't understand the particular flags that are set, but Windows documents the flags for SetConsoleMode().

So now we know precisely why @mplattner's fix works.

@mplattner
Copy link
Author

Thank you so much @midichef and @daviewales for your inputs! 👏
I'll try to put together a PR when I find time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants