hyprland-community/pyprland

[FEAT] Multiple windows support for scratchpads

Closed this issue · 57 comments

Some applications have a launch or login window and then it will invocate a new runing window and destroy itself. This will cause pyprland to miss the new window. So can we add a existed window to a scratchpad? The config can be

[scratchpads.tmp]
command = ""
size = "70% 60%"

Use empty command to mark the scratchpad will take any exited windows.

For the first time, the pyprland toggle tmp will add the avtivate window to scratchpad tmp, and hide it.
Then the behavior will be as same as normal command util the window are killed.

Hm.... do you have a concrete example ?
Maybe showing the output of hyprctl -j clients at the different stages ?

I recently added a feature to add any window to an existing scratchpad, but there are no options at the moment to have an "empty" scratchpad and it's hard for me to imagine how it would work...

I see something like:

  • open scratchpad with no command (still recorded but does nothing)
  • "attach" the new window => detects that the scratchpad was empty & make this window the main app of the scratchpad

but I'm not sure it matches what you have in mind...

For example, we use a chat app called QQ.
There will be a login interface:
240424_15h53m25s_screenshot
After I login, it will destory and a new window will be created:
240424_15h54m07s_screenshot

I see something like:

  • open scratchpad with no command (still recorded but does nothing)
  • "attach" the new window => detects that the scratchpad was empty & make this window the main app of the scratchpad

but I'm not sure it matches what you have in mind...

Yes, it is.

can you send either the full information or filter on the concerned client ?
class & title were not what I was looking for... I understood there are two different windows, I would like to check other properties, including pid.

Thank you!
I can try something like this, tell me if you feel that it would work (You may need to test an experimental branch of pyprland to provide feedback if/when I implement it):

New option like "multi_window=true" would change the behavior as follows:

  • When a scratchpad window has been closed and the scratchpad is requested to be shown
    • check if there is another window with the same PID, waiting for it ~1s
    • if found, use this as the scratchpad window
    • else proceed as of today: start the program again

Which leads me to one question: isn't pyprland trying to start "QQ" multiple times and fail at it with the current implementation ? (once the login window has been dismissed)

New option like "multi_window=true" would change the behavior as follows:

  • When a scratchpad window has been closed and the scratchpad is requested to be shown

    • check if there is another window with the same PID, waiting for it ~1s
    • if found, use this as the scratchpad window
    • else proceed as of today: start the program again

It looks good to me.

Which leads me to one question: isn't pyprland trying to start "QQ" multiple times and fail at it with the current implementation ? (once the login window has been dismissed)

No, it will be silent. But I remeber in the previous version of pyprland it would raise error that cannot find the target window.

May I also ask for the pypr's logs (launch pypr with --debug /tmp/pypr.log) when you call the scratchpad again after the login window has been closed ? (or include everything and I'll try to figure it out)

It seems that the dispatch is now functioning properly, but QQ is still not being displayed.
At the lower version (I upgraded both pyprland and QQ, so I don't know what's the reason), pyprland will directly give a error message about cannot find the window.

              scratchpads - run_toggle('qq',) // command.py:189
              scratchpads - visibility_check: ('1', 'eDP-1') == ('1', 'eDP-1') // __init__.py:423
              scratchpads - qq is visible = True (but True) // __init__.py:443
              scratchpads - Hiding qq // __init__.py:683
              scratchpads - dispatch movewindowpixel -326 0,address:0x55efce183cb0 // ipc.py:131
              scratchpads - dispatch movetoworkspacesilent special:scratch_qq,address:0x55efce183cb0 // ipc.py:131
              scratchpads - run_toggle('qq',) // command.py:189
              scratchpads - visibility_check: ('1', 'eDP-1') == ('1', 'eDP-1') // __init__.py:423
              scratchpads - qq is visible = False (but False) // __init__.py:443
              scratchpads - Showing qq // __init__.py:533
              scratchpads - clients // ipc.py:89
              scratchpads - monitors // ipc.py:89
              scratchpads - dispatch ['moveworkspacetomonitor special:scratch_qq eDP-1', 'movetoworkspacesilent 1,address:0x55efce183cb0', 'alterzorder top,address:0x55efce183cb0'] // ipc.py:131
              scratchpads - dispatch resizewindowpixel exact 326 464,address:0x55efce183cb0 // ipc.py:131
              scratchpads - clients // ipc.py:89
              scratchpads - dispatch movewindowpixel exact 806 531,address:0x55efce183cb0 // ipc.py:131
              scratchpads - dispatch focuswindow address:0x55efce183cb0 // ipc.py:131
              scratchpads - run_toggle('qq',) // command.py:189
              scratchpads - visibility_check: ('1', 'eDP-1') == ('1', 'eDP-1') // __init__.py:423
              scratchpads - qq is visible = True (but True) // __init__.py:443
              scratchpads - Hiding qq // __init__.py:683
              scratchpads - dispatch movewindowpixel 0 0,address:0x55efce183cb0 // ipc.py:131
              scratchpads - dispatch movetoworkspacesilent special:scratch_qq,address:0x55efce183cb0 // ipc.py:131
              scratchpads - run_toggle('qq',) // command.py:189
              scratchpads - visibility_check: ('1', 'eDP-1') == ('1', 'eDP-1') // __init__.py:423
              scratchpads - qq is visible = False (but False) // __init__.py:443
              scratchpads - Showing qq // __init__.py:533
              scratchpads - clients // ipc.py:89
              scratchpads - monitors // ipc.py:89
              scratchpads - dispatch ['moveworkspacetomonitor special:scratch_qq eDP-1', 'movetoworkspacesilent 1,address:0x55efce183cb0', 'alterzorder top,address:0x55efce183cb0'] // ipc.py:131
              scratchpads - dispatch resizewindowpixel exact 326 464,address:0x55efce183cb0 // ipc.py:131
              scratchpads - clients // ipc.py:89
              scratchpads - dispatch movewindowpixel exact 806 531,address:0x55efce183cb0 // ipc.py:131
              scratchpads - dispatch focuswindow address:0x55efce183cb0 // ipc.py:131
              scratchpads - run_toggle('qq',) // command.py:189
              scratchpads - visibility_check: ('1', 'eDP-1') == ('1', 'eDP-1') // __init__.py:423
              scratchpads - qq is visible = True (but True) // __init__.py:443
              scratchpads - Hiding qq // __init__.py:683
              scratchpads - dispatch movewindowpixel 0 0,address:0x55efce183cb0 // ipc.py:131
              scratchpads - dispatch movetoworkspacesilent special:scratch_qq,address:0x55efce183cb0 // ipc.py:131
                 pyprland - event_activewindowv2('55efce169a90',) // command.py:189
                 pyprland - active_window = 0x55efce169a90 // pyprland.py:44
              scratchpads - event_activewindowv2('55efce169a90',) // command.py:189
            layout_center - event_activewindowv2('55efce169a90',) // command.py:189
                 pyprland - event_activewindowv2('55efce169a90',) // command.py:189
                 pyprland - active_window = 0x55efce169a90 // pyprland.py:44
              scratchpads - event_activewindowv2('55efce169a90',) // command.py:189
            layout_center - event_activewindowv2('55efce169a90',) // command.py:189

Ok, I guess this is because the process is still running, so pyprland isn't restarting it.
There is at the moment no additional check regarding the client window, it's assumed to not have changed...

If I understand it well, in your scenario there is always at least one window we can "link", do you confirm ?

I'll try to implement something, would be great if you can test when I push it on git...

If I understand it well, in your scenario there is always at least one window we can "link", do you confirm ?

yes!

I'll try to implement something, would be great if you can test when I push it on git...

You can push code to a test branch, and I will test it once I have time!

Ok, I believe we didn't get something in your scenario, after a code review + writing a test app to simulate your case, it should raise an error + show a notification when you try to show again.

It means the original window is still available... so I'll ask you a more detailed info, please provide pypr's log for the full session (after a fresh start of the window manager + pypr) + the result of hyprctl -j clients | jq '.[] | select(.class=="QQ")' and execute the following:

  • toggle QQ scratchpad => should show
  • login => should disappear
  • toggle QQ scratchpad => should "break"
  • repeat last toggle operation, just in case we capture something interesting...

One more thing, having a binding such as:
bind = $mainMod, S, togglefloating,

when you "show" the client, but it's not visible, can you try triggering this to see if it appears in the current workspace as a tiled window ?

I believe it can be a problem with the coordinates... but I need a full picture of a single scenario to better understand what's going on :)

Are you using preserve_aspect ? Can you provide the config for the QQ scratchpad ? :)

One more thing, having a binding such as: bind = $mainMod, S, togglefloating,

when you "show" the client, but it's not visible, can you try triggering this to see if it appears in the current workspace as a tiled window ?

The login window cannot be set to tiled

At the lower version (I upgraded both pyprland and QQ, so I don't know what's the reason), pyprland will directly give a error message about cannot find the window.

I reproduce this ! This will happen when I enable multi monitors.
240425_11h33m24s_screenshot
THe traceback is:

ERROR:scratch:client_info of 5588bc402d30 must be a dict: None
This could be a bug in Pyprland, if you think so, report on https://github.com/fdev31/pyprland/issues
Not a dict: None
Traceback (most recent call last):
  File "/usr/lib/python3.11/site-packages/pyprland/command.py", line 195, in _run_plugin_handler
    await getattr(plugin, full_name)(*params)
  File "/usr/lib/python3.11/site-packages/pyprland/plugins/scratchpads/__init__.py", line 450, in run_toggle
    await asyncio.gather(*(asyncio.create_task(t()) for t in tasks))
  File "/usr/lib/python3.11/site-packages/pyprland/plugins/scratchpads/__init__.py", line 563, in run_show
    await self._show_transition(scratch, monitor, was_alive)
  File "/usr/lib/python3.11/site-packages/pyprland/plugins/scratchpads/__init__.py", line 606, in _show_transition
    await scratch.updateClientInfo()  # update position, size & workspace information (workspace properties have been created)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/pyprland/plugins/scratchpads/objects.py", line 118, in updateClientInfo
    raise AssertionError(f"Not a dict: {client_info}")
AssertionError: Not a dict: None

Are you using preserve_aspect ? Can you provide the config for the QQ scratchpad ? :)

[scratchpads.qq]
animation = "fromLeft"
command = "linuxqq"
position = "42% 30%"
class = "QQ"
size = "17% 43%"
margin = 0
lazy = true

Ok, I believe we didn't get something in your scenario, after a code review + writing a test app to simulate your case, it should raise an error + show a notification when you try to show again.

It means the original window is still available... so I'll ask you a more detailed info, please provide pypr's log for the full session (after a fresh start of the window manager + pypr) + the result of hyprctl -j clients | jq '.[] | select(.class=="QQ")' and execute the following:

  • toggle QQ scratchpad => should show
  • login => should disappear
  • toggle QQ scratchpad => should "break"
  • repeat last toggle operation, just in case we capture something interesting...

error.log
normal.log
the result of hyprctl -j clients | jq '.[] | select(.class=="QQ"):
Before login:
240425_11h57m34s_screenshot
after login:
240425_11h57m49s_screenshot
Then it will not change when I pypr toggle qq.
And I also reproduce the error in single monitor. I think this because there is a little gap between the login window destory and new window create. So if I toggle pypr during the period, It will raise error.

What I'm very surprised with, is that if there was no window with the "correct" address, it should ALSO throw this error... there is current absolutely no code (that I know!) which handles that kind of unexpected case, and I tested it yesterday on an app.

It makes me doubt this will be detected on your system... I need to know if the client address of the first window is still available later (which is what would make sense when you don't get an error).
To achieve this, just note the address of the login / first client window:

image

Then, later on, type something like:

hyprctl -j clients | jq '.[] | select(.address=="0xXXXX")'

Replacing "0xXXX...*" with the address of the initial client window....

@fdev31, Hi, I don't know is it still avilable, It will quit when I stop the whole process of QQ.

240426_09h23m31s_screenshot

Ok, I think it starts to make sense, QQ main window stays, but get a pid = -1 and no title or class anymore... hence when the scratchpad thinks the window is still fine.
Do you confirm this understanding ?

It almost sounds like a bug in QQ.

I can try to find a solution for that too but it looks very "qq" specific...

Ok, I think it starts to make sense, QQ main window stays, but get a pid = -1 and no title or class anymore... hence when the scratchpad thinks the window is still fine. Do you confirm this understanding ?

Yes

I can try to find a solution for that too but it looks very "qq" specific...

So for more general, can we implement a method to catch an existing window? So that I just need to login, then add it to scratch pad.

Yes, I could automatically attach new windows to the same scratch, the new windows seemed to preserve the pid.
The only challenge I see here is detecting the "dead" client window, having a trigger for it which isn't using additional API calls to not slow down anything.

having a pid==-1 seems to be a decent test, but this may be detected with a delay... I'll do some experiments over the weekend when I have time.

Oh, I mean just manully toggle pypr to add a window to scratchpad like the title of this issue. Automatically detecting the dead window seems not cost-effective for its narrow scenario.

Oh so you can find your way with the current attach command, if that works "okay" it's good :)
I may still try to push some code with tricks if they are not impacting the overall complexity too much... in short if it fits well in the code I'll make experiments that you can test to see if it improves the user experience...
I'll update this ticket accordingly to let you know if I abandon the idea or if I come with something in the future...

Thank you for your efforts!

I pushed some experimental option which sounds generic and may address your scenario, try setting multi = true on this scratchpad using the latest git version, hoping it helps!

Hi, It almost works, but it seems there should be a full address:

address = client["address"][2:]

After I change this, the QQ window can be dispatched between workspace:
240427_16h22m55s_screenshot

But the position of window are not handled correctly.

But it works well for my typora, which will pop a menu and not handle properly in the pervious version:

240427_16h25m55s_screenshot

I don't know why the position isn't changed in case of QQ... I guess this is because the original process & window are still here so every other are only "attached", but they still should slide in some way...

Thank you for reporting the address issue, I just pushed the fix.

We can try to improve it even further but it may lead to too much specific code & complexity... are you satisfied with the current version or do you think there is still something that can be done to try to improve the QQ scenarios?

We can try starting again generating full logs & dumps with the current version on QQ and try to figure what is still wrong with it. The position should be at least a bit changed when you toggle it.

We can try to improve it even further but it may lead to too much specific code & complexity... are you satisfied with the current version or do you think there is still something that can be done to try to improve the QQ scenarios?

I think simple is better. So I just write a script to do this:

addr=$(hyprctl -j clients | jq '.[] | select(.class=="'$1'") | .["address"]' | sed 's/"//g')
hyprctl dispatch focuswindow address:"$addr"
hyprctl dispatch centerwindow

Once I move it into the workspace, pypr will work well with QQ.
But it seems there's no animation of multi window.
And I think multi should be disabled by default for its search costs

You might be interested in https://github.com/hyprland-community/pyprland/wiki/fetch_client_menu - I think it can also fit the requirement... if not, I'm happy to improve it by supporting the "centerwindow" call for instance... (I could use this if I detect a floating window).

About the search cost, I managed to not do additional IPC call for it if I recall, so it should not be noticeable...
My main worry is that maybe for some scenarios this is not a good thing to have, but I imagine it's more useful than it's annoying...

You got the Typora app working out of the box, which is good, not everybody will dig the advanced options to fix such app...

You might be interested in https://github.com/hyprland-community/pyprland/wiki/fetch_client_menu - I think it can also fit the requirement... if not, I'm happy to improve it by supporting the "centerwindow" call for instance... (I could use this if I detect a floating window).

It seems fetch_client_menu do not works well with scrtachpads. The chosen client will disappear from the menu, and no window will show. But I am busy with my ungraduate design and paper. So I will dig this functionality after them done.

About the search cost, I managed to not do additional IPC call for it if I recall, so it should not be noticeable... My main worry is that maybe for some scenarios this is not a good thing to have, but I imagine it's more useful than it's annoying...

You got the Typora app working out of the box, which is good, not everybody will dig the advanced options to fix such app...

I agree with you. It's a great exprience to use out of the box.

Hi, I just update hyprland to 0.39.0. allow_special_worspace works as expect. And need to set smart_focus to False, I think this should be written in wiki.

And the animation of special workspace is abnormal. It looks like the special workspace are closed and reopened. This maybe a bug of Hyprland

What do you experience wrong with smart focus when you enable special workspaces?

Just cannot keep the special workspace open. I don't know what will smart_focus do.

EDIT: I also experienced the flickering, I don't think it's in pyprland, I guess this is because this feature is a bit fragile since scratchpads are heavily relying on special workspaces and from experience they are a bit less stable than standard workspaces.

Yes, the focuswindow still does not work well with sepcial workspace.

Just cannot keep the special workspace open. I don't know what will smart_focus do.

You mean, when you hide a scratchpad when a special workspace was open, then it hides the special workspace at the same time ?

If so, this is something I can try to improve, for instance disabling smart focus in that case...

You mean, when you hide a scratchpad when a special workspace was open, then it hides the special workspace at the same time ?

yes. And I want to know what is the behavior of smart focus. There'is no document about this.

So, this is because it's a bit complicated and subject to changes/refinements... the explanation is rather long to fit in the wiki under this option, and I consider this a topic to be worked on, not optimal at the moment with some corner cases not well handled as you noticed.

First, it's useful only for multi-monitor scenarios, so I could change the defaults according to that.

The problem with monitors is the following:

  • given 2 screens, positioned one on top of another
  • When you invoke a scratchpad which is "fromTop" or "fromBottom" it will slide over the other screen when hidding (depending from where you started it)
  • there is a chance (seems not deterministic) to get the other screen/workspace focused after the hide command, which is super annoying

So the scratchpads plugin is constantly keeping track of the window which was focused before the scratchpad was first displayed or refocused.
When you hide the scratchpad, after a tiny delay, it will force the focus back on this tracked client window.

There are few cases where this is auto-disabled, but it's quite heuristic.

I wanted to experiment with different solutions, but in my tests using the focused monitor or workspace (instead of focused client) didn't work well while it sounded optimal.

Hope that clarifies!

I'm really open for suggestions & experiments here, I was even considering opening for multiple heuristics (say changing smart_focus from a boolean to a string with the name of the "smart" heuristic ;P)

I don't see the issue with special workspaces, would you mind sharing the relevant configuration items + a video of the problem so I better understand how it's produced?

Please try the last push, keeping the smart focus on :)

EDIT: it wasn't good enough, I reverted, we need to focus specific client else the "recovered" focus is random :(

I tested and if there is a focused client window in the special workspace it works as expected here, not for you ?

I don't see the issue with special workspaces, would you mind sharing the relevant configuration items + a video of the problem so I better understand how it's produced?

Sorry, I have no proper app to record my screen due to the dependencies of hyprland-git. xdg-desktop-portal-hyprland-git cannot work.

In fact, I use single monitor at most time. When I enable smart_focus, then I show scratchpads in a special workspace, nothing abnormal. But when hiding the scratchpads, it will "close" the special workspace, and lost the focus.

I believe this is a bug of Hyprland. If you are using verison of 0.39.0, you may reproduce it by the following procedure:

  1. Open a special workspace and open a app, get the address of it.
  2. Open a terminal in this sepcial workspace, use hyprctl dispatch focuswindow address:.......

Then you will see the special workspace "close", and lost your focus. But in fact it just hide, if you toggle this speical workspace again, it will truly close.

It may be fixed by this commit. But I cannot verify it util the new version publish.

Then you will see the special workspace "close", and lost your focus. But in fact it just hide, if you toggle this speical workspace again, it will truly close.

Looks like an issue with your Hyprland version, I can't reproduce this problem... using v0.39.1

Ok, I worked around some bug and hit the same behavior as you over special workspaces and can now explain:

.1 Hyprland sends an invalid address when showing a scratchpad, which was masking the other bug
.2 Hyprland blinks when we show the scratchpad for no obvious reason, as a side effect, it will send an event telling that some window BELOW the special workspace is focused, which is messing up the focus tracking

As a workaround, once a scratchpad is displayed over a special workspace, focus at least once/shortly another window.

The latest git version may improve things a bit.

The latest git version may improve things a bit.

I use the latest commit, but it can not detect pypr daemon.
240501_17h48m32s_screenshot
240501_17h49m41s_screenshot

And it's running? If yes try killing and restarting

Did you figure your problem? Check #98, it might be of interest for you.

Now smart focus can work well with scratchpads in speical workspace!

Is there remaining issues or can I close this ticket?

There is still another problem. I update Hyprland to 0.4.0.

I believe this is a bug of Hyprland. If you are using verison of 0.39.0, you may reproduce it by the following procedure:

  1. Open a special workspace and open a app, get the address of it.
  2. Open a terminal in this sepcial workspace, use hyprctl dispatch focuswindow address:.......

Then you will see the special workspace "close", and lost your focus. But in fact it just hide, if you toggle this speical workspace again, it will truly close.

This bug is fixed, but toggling scratchpads in s special workspace still reproduce this.

Reproduces this ?
The only issue I see now is the blinking of the special workspace (which I don't think I can fix, looks like in Hyprland):

[DEBUG] scratchpads :: run_toggle('term',) :: command.py:188
[DEBUG] scratchpads :: visibility_check: ('1', 'DP-1') == ('1', 'DP-1') :: __init__.py:448
[DEBUG] scratchpads :: term visibility: False and False :: __init__.py:475
[INFO] scratchpads :: Showing term :: __init__.py:566
[DEBUG] scratchpads :: monitors :: ipc.py:84
[DEBUG] scratchpads :: dispatch resizewindowpixel exact 1920 864,address:0x63d7a25c9f00 :: ipc.py:126
[DEBUG] scratchpads :: clients :: ipc.py:84
[DEBUG] scratchpads :: dispatch ['moveworkspacetomonitor special:scratch_term DP-1', 'movetoworkspacesilent special:stash,address:0x63d7a25c9f00', 'alterzorder top,address:0x63d7a25c9f00'] :: ipc.py:126
[DEBUG] pyprland :: event_activewindowv2('63d7a2930080',) :: command.py:188
[DEBUG] pyprland :: active_window = 0x63d7a2930080 :: pyprland.py:60
[DEBUG] layout_center :: event_activewindowv2('63d7a2930080',) :: command.py:188
[DEBUG] scratchpads :: dispatch movewindowpixel exact 1520 60,address:0x63d7a25c9f00 :: ipc.py:126
[DEBUG] scratchpads :: dispatch focuswindow address:0x63d7a25c9f00 :: ipc.py:126
[DEBUG] pyprland :: event_activewindowv2('63d7a267c110',) :: command.py:188
[DEBUG] pyprland :: active_window = 0x63d7a267c110 :: pyprland.py:60
[DEBUG] layout_center :: event_activewindowv2('63d7a267c110',) :: command.py:188
[DEBUG] pyprland :: event_activewindowv2('63d7a25c9f00',) :: command.py:188
[DEBUG] pyprland :: active_window = 0x63d7a25c9f00 :: pyprland.py:60
[DEBUG] layout_center :: event_activewindowv2('63d7a25c9f00',) :: command.py:188

In this sequence, the only "ipc command" that I can imagine could trigger an animation is the "moveworkspacetomonitor" and even removing it I get the unexpected animation.
Is it the problem you are referring to?

EDIT: link added: hyprwm/Hyprland#5430

At first I think the blinking is due to focusing a window in the same special workspace. But latest Hyprland fixs this, the blinking still remainds.
And I found this

Ok, but then you confirm there is no remaining issue to fix in Pyprland, just wait for a fix in Hyprland isn't it? I would really like to close this long ticket ^^

Ok, but then you confirm there is no remaining issue to fix in Pyprland, just wait for a fix in Hyprland isn't it? I would really like to close this long ticket ^^

yes, thanks for your helps!

Thank you for the report & testing.
If there are some sub-optimal things you are welcome to open a new ticket, but until then It looks like those deffects we see are related to Hyprland instance.