Kalmat / PyWinCtl

Cross-Platform module to get info on and control windows on screen
Other
179 stars 19 forks source link

Program blocks completely when import pywinctl #73

Closed SamuMazzi closed 11 months ago

SamuMazzi commented 1 year ago

When I import pywinctl, even if I don't use it, the program blocks and I need to press CTRL+C in order to make it works Even with a simple script like this:

from pywinctl import getActiveWindow

print('Hello world')

I've tested it with Python 3.8 and 3.10 with Ubuntu 22.04

Kalmat commented 1 year ago

Hi! Thank you for your feedback!

Are you by chance using Wayland? If so, unfortunately PyWInCtl will not work, since it heavily relies on X11 (thru Xlbi) and EWMH protocol. Besides, there is no way in Wayland to retrieve the active window or the list of open windows.

On the other hand, it shouldn't freeze. I will look into it anyway although the module, as I said, will not work.

SamuMazzi commented 1 year ago

Oh yes I use Wayland... But actually, once I press CTRL+C the script keeps running acting perfectly (altough I didn't use other functions but "getActiveWindow") So if I could resolve the freezing everything would be perfect for me

Kalmat commented 1 year ago

OK, I will look into it but, again, getActiveWindow() will NOT work in Wayland (neither getAllWindows() nor getAllTitles() nor getAllWindowsWithTitle()...)! Wayland doesn't have any way to implement it, because of allegedly "security reasons".

SamuMazzi commented 1 year ago

Okay, got it! But do the "listeners" in the watchdog still work? Even the "isActive" one? Anyway pls let me know if you find the solution to this bug, because I really like this package, and I'd like to use it more, thanks :)

Kalmat commented 1 year ago

Sure!!!

Furthermore, I think I found the "guilty". Apart from some weird issues related with paths (which do not happen on GNOME/X11, I will have to check more carefully), this part of ewmhlib package hangs FOREVER because of a fake display named ":1" (default display uses to be ":0", which is also present. But I don't know why Wayland creates two displays, one of them seems to be "not normal"):

def _getDisplaysCount() -> int:
    count = 0
    files: List[str] = os.listdir("/tmp/.X11-unix")
    for d in files:
        if d.startswith("X"):
            name: str = ":" + d[1:]
            try:
                display: Xlib.display.Display = Xlib.display.Display(name)  # This part is the one that freezes until Ctl-C is pressed!!!
                display.close()
                count += 1
            except:
                pass
    return count

displaysCount = _getDisplaysCount()

I will think of the better solution for this and will upload a new version fixing it. Apart from this, I don't think that the part of the Watchdog about monitoring if the window is active will work on Wayland (I will try to also find a workaround, but with very low expectations, to be honest).

Question 1: Do you already have the XID of the window you want to monitor? Question 2: Do you have to stick to Wayland, or you can enter into GNOME/X11? (This would solve ALL problems!)

SamuMazzi commented 12 months ago

Here I am, sorry for the delay! Anyway it's great that you found the bug! Question 1: No, I only want to monitor one window created by my python script with a defined Title Question 2: The point is that I chose this library because it's cross-platform and my goal is to create a program which can run in any environment, especially Linux, so yes, I'd like to work also on Wayland

But actually, just for you to know, I tested the watchdog and it works in my system.. At this point I don't know why

Thank you as always anyway!

Kalmat commented 12 months ago

Wow! That's really great!! So happy it works in your case... Anyway, if you find any issue afterwards, just let me know!

Just one last thing. If the window you want to monitor is yours, you can access all PyWinCtl features just instantiating the pywinctl.Window() class using the X-Window id (XID). For instance, you can get the XID from PyQt's self.winId() or Tkinter's root.frame(), and so on.

SamuMazzi commented 11 months ago

Hi! I'm sure you have a lot of work to do, but when are you planning to release this patch? I'd need it... Thank you as always btw! :)

Kalmat commented 11 months ago

Hi! I was fixing some other issues raised by other users (for PyWinCtl and for PyMonCtl too), and also waiting to have the opportunity to borrow an actual mac to test on it. If you need it, I will do my best to release a fixed version. Do you need it to be in PyPi or you can use a custom wheel in the meantime? If you can use a wheel by the moment, it should be easier and faster to me to prepare a fixed patch.

SamuMazzi commented 11 months ago

If you're planning on releasing it on PyPi sooner or later, then a custom wheel will be enough so far, thank you! Also, if this can help I have a Mac and I can do some tests for you if it's feasible

Kalmat commented 11 months ago

For sure I'm planning to release a new version as soon as possible! I am having some troubles on Manjaro by the moment, and also to test in actual macOS systems... In the meantime, use this wheel I am attaching and, please, let me know if everything is OK on your side (note it requires PyMonCtl-0.6).

Regarding testing on macOS... Thank you so much! If you can also test this wheel on your system, by running test_pywinctl.py and test_MacOSWindow.py, it would be really nice! Also, if you have access to two monitors or more, it would also be very helpful to test PyMonCtl-0.6 (used by PyWinCtl).

PyWinCtl-0.2-py3-none-any.whl.zip

SamuMazzi commented 11 months ago

Okay, I'll test it on Monday if it's not a problem (sorry but I'm not at home this week), also with another monitor

I also tested the release but now it tells me that with wayland I cannot get the active window... but it was working before! I'm pretty sure that I had PyMonCtl-0.5 and with that, apart from the usual problem, it was working! The only thing is that I cannot install that version anymore (it's not even listed in PyPi).. can this help somehow?

Kalmat commented 11 months ago

Hi! Do not worry until monday.

Regarding getActiveWindow(), I was obtaining None in all cases when testing on Wayland, so weird that it was working in your case! I can prepare another wheel rolling back to previous function... but still it is really weird! I will think of it until monday! Regarding the versions, I just uploaded new PyMonCtl-0.6, perhaps it was not propagated when you tried to install it. It is available now. You only have to uninstall everything (python3 -m pip uninstall pywinctl pymonctl pywinbox), then install wheel (python3 -m pip install PyWinCtl-0.2-py3-none-any.whl) and everything will update to proper version.

SamuMazzi commented 11 months ago

Yes yes, I installed the 0.6 version, but I'm telling you that it was working with the version 0.5 (that I cannot find anymore)

Kalmat commented 11 months ago

No, sorry, I had to delete it because I messed up with ewmhlib versions!

Apart from this, PyMonCtl has nothing to do with getActiveWindow(). In the case of PyWInCtl it is only used in getAllScreens(), getScreenSize(), getWorkArea() and getDisplay(). In short, I don't think PyMonCtl is the cause of the error.

When you have time, of course, can you run this on Wayland? I'ts a simple test of new / old code in getActiveWindow(), just to try to understand what might be happening in your case.

import pywinctl as pwc
from ewmhlib import defaultEwmhRoot
print(pwc.getActiveWindow())                 # New version
print(defaultEwmhRoot.getActiveWindow())    # Old version

In my case, it throws:

None
0

Note that "0" is not a valid window identifier.

SamuMazzi commented 11 months ago

Okay so, in the second case it gives me just a regular ID (different from 0) and in the first case it throws this error:

  File "/home/samuele/Documenti/GitHub/pygui/venv/lib/python3.10/site-packages/pywinctl/_pywinctl_linux.py", line 77, in getActiveWindow
    _, activeWindow = _WgetAllWindows()
  File "/home/samuele/Documenti/GitHub/pygui/venv/lib/python3.10/site-packages/pywinctl/_pywinctl_linux.py", line 299, in _WgetAllWindows
    raise NotImplementedError("PyWinCtl won't likely work in Wayland, since it doesn't allow retrieving active window nor list or open windows.\n"
NotImplementedError: PyWinCtl won't likely work in Wayland, since it doesn't allow retrieving active window nor list or open windows.
Possible workarounds:
    1. enable global.context.unsafe_mode (although this will only work with some apps/windows, not all) 2. Instantiate Window() class with a valid xid, obtained somewhere else (perhaps your own application window?)
Kalmat commented 11 months ago

Thank you!!! So the "old" code (based on XOrg/X11) is returning a valid ID in Wayland?!?!?!? Ok then, I will use both, so it will work in your case (and maybe some others). Let me prepare a new version to upload.

Just curious: which application / window is the one you are targeting?

Thank you again!

SamuMazzi commented 11 months ago

It seems so! :) Thank you!!

Anyway I'm targeting my own GUI built with DearPyGUI

Kalmat commented 11 months ago

So you don't need to retrieve the active window, but just to instantiate pywinctl.Window() class passing your own window Xid.

Do you know if this is possible in DearPyGui? In PyQt the command to retrieve your Xid is self.winId(), or in Tkinter it is root.frame().

This will assure full compatibility in other distributions, platforms and future versions.

Kalmat commented 11 months ago

Whenever you are back and have time, try the new v0.3 in which I introduced the changes we have been discussing. It should work in your case now. It is already uploaded to PyPi, so just install it with pip.

SamuMazzi commented 11 months ago

Sure, I'm already trying and it works (I tried getActiveWindow and it worked but getWindowsWithTitle gave me an empty list)

Also, just for you to know, defaultEwmhRoot.getActiveWindow() sometimes gives me 0. To understand better: I have this main loop which runs for every frame and every time I'm checking which is the active window.. well sometimes it gives me 0 when I switch windows (but it's probably temporary)

Kalmat commented 11 months ago

Thank you so much for your quick reply and your feedback! It really helps me a lot.

It is totally normal that, if you switch windows, the getActiveWindow() method returns '0'... In fact, it is the expected behavior in Wayland. What it is totally unexpected is that you are able to get it working with your window! Give a try to the alternative I was suggesting before (instantiate pywinctl.Window() class with your own Xid). This way, your window will be permanently targeted, it will be much faster and will have less impact in your main process (not continuously querying getActiveWindow()), thus using the watchdog in a separate thread to be notified only when changes happen.

Whenever you have the time, it would be very very helpful if you could test pywinctl and pymonctl on an actual macOS with 2 or more monitors plugged. No hurries, of course!

Thank you again!

SamuMazzi commented 11 months ago

Yes yes, I know about that, but just for you to know, in my case I need to check at every frame which is the active window so the only solution that I came up with was with defaultEwmhRoot.getActiveWindow()

Anyway sure, on Monday I'll test everything I can :)

SamuMazzi commented 11 months ago

Hello! So I made all the tests, the ones from PyWinCtl with both 1 monitor and 2 monitors, and they gave the same output, the one from PyMonCtl only with 2 monitors. So here the results:

{'system_name': 'Built-in Retina Display', 'id': 69734272, 'is_primary': True, 'position': Point(x=0, y=0), 'size': Size(width=1440, height=900), 'workarea': Rect(left=0, top=74, right=1440, bottom=801), 'scale': (200.0, 200.0), 'dpi': (144, 144), 'orientation': 0, 'frequency': 0.0, 'colordepth': 32} {'system_name': 'PHL 275E2F', 'id': 1127275602, 'is_primary': False, 'position': Point(x=1440, y=-540), 'size': Size(width=2560, height=1440), 'workarea': Rect(left=1440, top=-540, right=2560, bottom=1415), 'scale': (100.0, 100.0), 'dpi': (72, 72), 'orientation': 0, 'frequency': 60.0, 'colordepth': 32}

INITIAL POSITIONS: {'Built-in Retina Display_69734272': {'relativePos': Point(x=0, y=0)}, 'PHL 275E2F_1127275602': {'relativePos': Point(x=1440, y=-540)}}

NAME Built-in Retina Display_69734272 HANDLE/ID: 69734272 IS PRIMARY: True SIZE: Size(width=1440, height=900) POSITION: Point(x=0, y=0) FREQUENCY: 0.0 ORIENTATION: 0 SCALE (200.0, 200.0) DPI: (144, 144) COLOR DEPTH: 32 BRIGHTNESS: 0.8701171875 CONTRAST: 100 CURRENT MODE: DisplayMode(width=1440, height=900, frequency=0.0) DEFAULT MODE: DisplayMode(width=1440, height=900, frequency=0.0)

CHANGE MODE DisplayMode(width=800, height=600, frequency=0.0) MODE CHANGED?: DisplayMode(width=1440, height=900, frequency=0.0) SET DEFAULT MODE DisplayMode(width=1440, height=900, frequency=0.0) DEFAULT MODE SET?: DisplayMode(width=1440, height=900, frequency=0.0) RESTORE MODE DisplayMode(width=1440, height=900, frequency=0.0) MODE RESTORED?: DisplayMode(width=1440, height=900, frequency=0.0)

CHANGE BRIGHTNESS RESTORE BRIGHTNESS

CHANGE CONTRAST RESTORE CONTRAST

CHANGE ORIENTATION RESTORE ORIENTATION

CHANGE SCALE. CURRENT: (200.0, 200.0) SCALE CHANGED?: (200.0, 200.0) RESTORE SCALE SCALE RESTORED?: (200.0, 200.0) IS ON?: True TURN OFF IS ON?: True TURN ON IS ON?: True IS SUSPENDED?: False STANDBY IS ON?: False IS SUSPENDED?: True WAKEUP IS ON?: False IS SUSPENDED?: True

IS ATTACHED?: True DETACH IS ATTACHED?: True ATTACH IS ATTACHED?: True

NAME PHL 275E2F_1127275602 HANDLE/ID: 1127275602 IS PRIMARY: False SIZE: Size(width=2560, height=1440) POSITION: Point(x=1440, y=-540) FREQUENCY: 60.0 ORIENTATION: 0 SCALE (100.0, 100.0) DPI: (72, 72) COLOR DEPTH: 32 BRIGHTNESS: None CONTRAST: 100 CURRENT MODE: DisplayMode(width=2560, height=1440, frequency=60.0) DEFAULT MODE: DisplayMode(width=2560, height=1440, frequency=60.0)

CHANGE MODE DisplayMode(width=1344, height=1008, frequency=75.0) MODE CHANGED?: DisplayMode(width=2560, height=1440, frequency=60.0) SET DEFAULT MODE DisplayMode(width=2560, height=1440, frequency=60.0) DEFAULT MODE SET?: DisplayMode(width=2560, height=1440, frequency=60.0) RESTORE MODE DisplayMode(width=2560, height=1440, frequency=60.0) MODE RESTORED?: DisplayMode(width=2560, height=1440, frequency=60.0)

CHANGE BRIGHTNESS RESTORE BRIGHTNESS

CHANGE CONTRAST RESTORE CONTRAST

CHANGE ORIENTATION RESTORE ORIENTATION

CHANGE SCALE. CURRENT: (100.0, 100.0) SCALE CHANGED?: (100.0, 100.0) RESTORE SCALE SCALE RESTORED?: (100.0, 100.0) IS ON?: True TURN OFF IS ON?: True TURN ON IS ON?: True IS SUSPENDED?: False STANDBY IS ON?: False IS SUSPENDED?: True WAKEUP IS ON?: False IS SUSPENDED?: True

IS ATTACHED?: True DETACH IS ATTACHED?: True ATTACH IS ATTACHED?: True

MANAGING MONITORS MONITOR 1: True Point(x=0, y=0) Size(width=1440, height=900) MONITOR 2: False Point(x=1440, y=-540) Size(width=2560, height=1440)

MONITOR 2 AS PRIMARY MONITOR 1: True Point(x=0, y=0) Size(width=1440, height=900) MONITOR 2: False Point(x=1440, y=-540) Size(width=2560, height=1440) MONITOR 1: True Point(x=0, y=0) Size(width=1440, height=900) MONITOR 2: False Point(x=1440, y=-540) Size(width=2560, height=1440) MONITOR 1 AS PRIMARY MONITOR 1: True Point(x=0, y=0) Size(width=1440, height=900) MONITOR 2: False Point(x=1440, y=-540) Size(width=2560, height=1440)

POSITION MONITOR 2 AT FREE BELOW POSITION (200, -900) MONITOR 2 POSITIONED? Point(x=1440, y=-540)

CHANGE ARRANGEMENT: MONITOR 2 AS PRIMARY, REST OF MONITORS AT LEFT_BOTTOM {'PHL 275E2F_1127275602': {'relativePos': <Position.PRIMARY: 0>, 'relativeTo': ''}, 'Built-in Retina Display_69734272': {'relativePos': <Position.LEFT_BOTTOM: 11>, 'relativeTo': 'PHL 275E2F_1127275602'}} MONITOR Built-in Retina Display_69734272 IS PRIMARY False POSITION Point(x=0, y=0) SIZE Size(width=1440, height=900) MONITOR PHL 275E2F_1127275602 IS PRIMARY True POSITION Point(x=1440, y=-540) SIZE Size(width=2560, height=1440)

CHANGE ARRANGEMENT: MONITOR 1 AS PRIMARY, REST OF MONITORS AT RIGHT_TOP {'Built-in Retina Display_69734272': {'relativePos': <Position.PRIMARY: 0>, 'relativeTo': ''}, 'PHL 275E2F_1127275602': {'relativePos': <Position.RIGHT_TOP: 30>, 'relativeTo': 'Built-in Retina Display_69734272'}} MONITOR Built-in Retina Display_69734272 IS PRIMARY True POSITION Point(x=0, y=0) SIZE Size(width=1440, height=900) MONITOR PHL 275E2F_1127275602 IS PRIMARY False POSITION Point(x=1440, y=-540) SIZE Size(width=2560, height=1440)

RESTORE INITIAL MONITOR CONFIG {'Built-in Retina Display_69734272': {'relativePos': Point(x=0, y=0)}, 'PHL 275E2F_1127275602': {'relativePos': Point(x=1440, y=-540)}}



Tell me if you need something else :)
Kalmat commented 11 months ago

Hi! Sorry for my late reply. I've been out these days, with no access to my "stuff". Thank you SO MUCH for your help! This really helps me a lot!!!

Regarding the PyWinCtl crashes, I am totally lost, to be honest. I will have to look into it very carefully... Not sure if the size (810, 610) is not valid for a TextEdit window in macOS or if it means the window going out the screen (also maybe not allowed by macOS when resizing a window, for instance, beyond the dock). If I run it in my macOS VM, with a 1920x1080 resolution, it works fine...

Regarding PyMonCtl, everything seems to work OK! Wonderful! I didn't find a way to wakeup the monitors after suspending them in macOS... not even moving the mouse using events. I'm still looking for a solution.

Keep pushing!

SamuMazzi commented 11 months ago

Hello! No problem for the late answer. Actually I tried to find a way to wake up the monitor but it seems impossible, instead I found out why the test gives that error. The problem is that my Mac (13-inch) has a too small screen, so when you move the window and then you try to set the size with a size that would exceed the size of the monitor, the script enlarge the window as much as it can until it "touches" the border which results in a smaller size than expected (I hope I explained myself well enough) I'll upload a pull request with the fix and a mini refactor

Thank you as always for your work :)

Kalmat commented 11 months ago

Yes! You totally explained yourself, and it is very good news there is nothing wrong in the module!!!

Thanks A LOT for your help!!!