skx / kpie

Simple devilspie-like program for window manipulation, with Lua.
GNU General Public License v2.0
80 stars 13 forks source link

Logic based on window count #26

Open alexwhitman opened 5 years ago

alexwhitman commented 5 years ago

Is there any way to perform logic based on window count? I currently use kpie to position and size Firefox which works fine, except it also positions and sizes any pop-up windows. What I would like is to only position and size the first "main" window and ignore anything else. The window types and classes are the same for both the main and pop-up windows so I can't use that to distinguish which window is which.

skx commented 5 years ago

No. Windows are processed as they appear, with no relationship information determined.

That said I suspect you can work around this for firefox. IF you run this when you have several firefox windows open, and a popup:

  kpie --single ./samples/dump.lua

You'll probably see what I see, which is a main-window:

    ( window_class() == "Firefox" ) and
    ( window_type() == "WINDOW_NORMAL" ) and 
    ( window_role() == "browser" ) and 
    ( window_application() == "Firefox" ) ) then
             xy(0,26 )
             size(1920,1030 )
             workspace(1)
             maximize()
      end

vs the window you get from Help | About:

 if ( ( window_title() == "About Mozilla Firefox" ) and 
      ( window_class() == "Firefox" ) and
      ( window_type() == "WINDOW_NORMAL" ) and 
      ( window_role() == "About" ) and 
      ( window_application() == "Firefox" ) ) then
      xy(579,352 )
      size(771,383 )
      workspace(1)
 end

Notice that window_role() is Firefox, or not? I suspect you can use that to handle "browser" vs. "popup".

skx commented 5 years ago

Test to see if window_role() is browser is of course what I meant :)

alexwhitman commented 5 years ago

In this case the window_role() is browser in both cases. An example of what is happening can be seen by testing with https://codepen.io/anon/pen/KJYqQV

skx commented 5 years ago

Ahh you do mean a literal popup. Maybe that should have been obvious, but it wasn't.

Sadly I think you're out of luck here. We literally get called when a new window appears, and we get given that window - and nothing else. I can imagine iterating anew each time, to look to see if it were related to the same process (PID) as an existing idea, but even then it wouldn't necessarily help unless the ordering was stable, and I suspect it wouldn't be.

If you know the popup is created at a "small" size then you could catch it that way, but that would be fragile and annoying too :/

alexwhitman commented 5 years ago

Could anything be done by checking for a parent window? https://stackoverflow.com/questions/25951656/get-parent-window-of-newly-created-window

This would of course be a new feature for kpie and I don't know enough about X to know if every window would have the "root" window as a parent.

alexwhitman commented 5 years ago

Although thinking about it, in Firefox's case the pop-up window might not be a child of the window that opened it.

skx commented 5 years ago

Yeah I've been experimenting with this and there seems to be little bearing between the window ID and the parent IDs I'm finding.

Here's the popup:

  if ( ( window_title() == "Google - Mozilla Firefox" ) and 
       ( window_class() == "Firefox" ) and
       ( window_type() == "WINDOW_NORMAL" ) and 
       ( window_role() == "browser" ) and 
       ( window_application() == "Firefox" ) ) then
       xy(754,330 )
       id(46153216)
       size(412,421 )
       parent(8527323)
       workspace(1)
     end

Here's the parent:

    if ( ( window_title() == "An Anonymous Pen on CodePen - Mozilla Firefox" ) and 
          ( window_class() == "Firefox" ) and
         ( window_type() == "WINDOW_NORMAL" ) and 
         ( window_role() == "browser" ) and 
         ( window_application() == "Firefox" ) ) then
         xy(0,26 )
         id(46137361)
         size(1920,1030 )
         parent(8388965)
         workspace(1)
        maximize()
  end

The IDs don't match .. Though they are stable between runs; I wonder if I'm missing something?

I added an id() function and a parent() function to investigage:

    int lua_id(lua_State * L)
    {
        debug("id");
        Window w = wnck_window_get_xid(g_window);
        lua_pushinteger(L, w & INT_MAX );
        return 1;
    }

then

    int lua_parent(lua_State * L)
    {
        debug("parent");
        Window root, parent, *children = NULL;
        unsigned int num_children;

        Display *d = gdk_x11_get_default_xdisplay();
        Window   w = wnck_window_get_xid(g_window);

        if(!XQueryTree(d, w,
                       &root,
                       &parent,
                       &children,
                       &num_children)) {
            printf("Failed to query tree\n");
            return 0;
        }

        if (children)
            XFree((char *)children);

        lua_pushinteger(L, parent & INT_MAX);
        return 1;
    }

}

I'll sleep on things, but I'm not horribly optimistic ..

alexwhitman commented 5 years ago

Running xwininfo -tree -root shows that Firefox has a lot of windows but there doesn't appear to be any parent/child relationship between the windows I'm interested in.

skx commented 5 years ago

I think the conclusion here was that there wasn't a simple/obvious way of giving you what you want.

I did think of keeping state - since the way the program works is that we get a function invoked every time a window is created. I could record :

  windows[application][class] += 1

Then allow that to be tested, but it doesn't really help you because we still can't tell what kind of window it is. Maybe it's a popup-dialog, maybe it's a new window. Regardless the problem is that there's no notification of when a window is closed, so we'd end up with bogus counts anyway. (I guess I could retest each known window .. but that gets horrible quickly.)

I think the best I can do is close this, or tag it "can't fix".

Sorry!

alexwhitman commented 5 years ago

For my case recording the window count would work as I tend to only want to resize the first window but I don't know how useful the feature might be to anyone else to warrant adding it.

I don't know if it's possible, but can the concept of global variables/state be made available in lua scripts? That way I could add a window counter in lua without the need to have it added to kpie directly.

skx commented 5 years ago

I don't know if it's possible, but can the concept of global variables/state be made available in lua scripts?

That's an interesting question! Although the script is run again each time a window is created you can preserve state, as I just tested:

   --
   -- See if we have a global state hash present
   --
   function exists(var)
     for k, _ in pairs(_G) do
       if k == var then
         return true
       end
     end
   end

   -- keep track of windows here...
   if not exists("global_state") then
      global_state = {}
   end

   -- get the current class
   c = window_class()
   cur = global_state[c] or 0
   -- increase and store
   cur = cur + 1
   global_state[c] = cur

   -- Show output.
   print( "Count of windows with class " .. c .. " is " .. global_state[c] .. "\n" )

The problem here is that the state will increment each time you open a new window, but never decrease.

Again the problem is that the callback is invoked when a window is created, but nothing happens for windows being closed. Not sure how to handle that without reworking the API singificantly.

alexwhitman commented 5 years ago

The global state is a good starting point.

Would the API require significant rework, or does it just need an extra signal to be connected?

g_signal_connect(screen, "window-closed", G_CALLBACK(on_window_closed), NULL);

In theory the callback could also invoke_lua() as the window-closed signal also provides a WnckWindow window parameter.

I can't see a wnck_window_ function which would check the window status (open or closed) so it might just be an extra var to assign within the callbacks g_window_status = "open"; with a lua binding around it to access that status.

I'm speculating a lot, just going by the existing code so the above might not be sensible or workable.

skx commented 5 years ago

Right now the script is free-form, and executed from the top every time, when a window is created.

So I was kinda thinking if it we invoked a lua-callback on two different events (window open and window close) it would be best if it were changed:

function window_opened()
    -- All code that used to live in the script moved here.
end

function window_closed()
    -- extra code added here, if the user needs.
end

In short all users would need to add the function window_opened() / end around their code. That would make it 100% explicit when things were run, and would ensure that the window_title(), etc, functions were not called by the user when a window were closed/destroyed. (Not sure when the window-closed event would fire, just before/just after the window destruction. But if after then any calls that try to interrogate the window would fail.)

Of course I could just parse the code and invoke window_closed() if it exists when a window is closed. But then you see how things don't match.

Does that clarify my thinking?

alexwhitman commented 5 years ago

Makes sense, I was thinking more along the lines of

if (window_closed) then
    -- do something
end

but didn't take into account the backwards incompatibilities.

skx commented 5 years ago

Let me update the code to monitor window close-events. If I can make that work, I could add a window_closed() method to allow control-flow.

If that all works out I suspect I'll break compatibility in the future - but I think if you could get the window-closed to decrement the count in the global state that'll allow you to differentiate between "first" and "subsequent" windows.

Will test this evening anyway.

skx commented 5 years ago

This whole thing is horrid! I've added logic to trigger a callback on window-close, but when it is fired the window passed to it is null.

i.e. I can call lua when the window close event happens, but all the calls fail. For example:

 if window_closed() then
     print("This is a window close event" )
     print("Class " .. window_class())
     print("App   " .. window_application())
     return
  end

The output is:

    This is a window close event
    (kpie:6063): Wnck-CRITICAL **: wnck_class_group_get_res_class: assertion 'class_group != NULL' failed
    ERROR: ./tmp.lua:22: attempt to concatenate a nil value

:(

Diff attached of code:

diff.txt

Sample program that demonstrates how this fails:

tmp.lua.txt

skx commented 5 years ago

The obvious solution is to have a cache:

Then:

That would allow a destroyed window, which has an ID, to return values. The downside is that it would be a huge change, and it would lead to stale data - for the cases of browsers the title changes on tab-change, etc, not to mention X,Y coords change for other windows.

I think we're back to "can't fix" and another hour or two of our lives wasted. Computers are hard!

alexwhitman commented 5 years ago

Bizarre that the window-closed callback has a window parameter if it's going to be null. Oh well. Thanks for looking.

dawsers commented 5 years ago

What I did to solve a problem similar to this one is add to functions to the bindings.{h,c} and then write a script like this:

-- Change geometry and position only for the first window of
-- Firefox
if (window_class() == "firefox") then
    local num = window_class_list_length()
    if (num == 1) then
        xy(1315, 29)
        size(1245, 1435)
    end
end

My changes, which if you want I can do a pull request for:

diff --git a/bindings.h b/bindings.h
index a60d355..7ae327b 100644
--- a/bindings.h
+++ b/bindings.h
@@ -97,6 +97,8 @@ int lua_unpin(lua_State * L);
 int lua_unshade(lua_State * L);
 int lua_window_application(lua_State * L);
 int lua_window_class(lua_State * L);
+int lua_window_class_list_length(lua_State * L);
+int lua_window_class_window_index(lua_State * L);
 int lua_window_decoration(lua_State * L);
 int lua_window_id(lua_State * L);
 int lua_window_xid(lua_State * L);
diff --git a/bindings.c b/bindings.c
index 327c956..90aee1b 100644
--- a/bindings.c
+++ b/bindings.c
@@ -167,6 +167,8 @@ void init_lua(int _debug, const char *config_file)
     lua_register(g_L, "unshade", lua_unshade);
     lua_register(g_L, "window_application", lua_window_application);
     lua_register(g_L, "window_class", lua_window_class);
+    lua_register(g_L, "window_class_list_length", lua_window_class_list_length);
+    lua_register(g_L, "window_class_window_index", lua_window_class_window_index);
     lua_register(g_L, "window_id", lua_window_id);
     lua_register(g_L, "window_xid", lua_window_xid);
     lua_register(g_L, "window_pid", lua_window_pid);
@@ -759,6 +761,39 @@ int lua_window_class(lua_State * L)
 }

+/**
+ * Return the number of windows in the class of this window
+ */
+int lua_window_class_list_length(lua_State * L)
+{
+    WnckClassGroup *x = wnck_window_get_class_group(g_window);
+    GList *list = wnck_class_group_get_windows(x);
+    lua_pushinteger(L, g_list_length(list));
+    return 1;
+}
+
+
+/**
+ * Return the index of this window within its class
+ */
+int lua_window_class_window_index(lua_State * L)
+{
+    WnckClassGroup *x = wnck_window_get_class_group(g_window);
+    GList *list = wnck_class_group_get_windows(x);
+    int idx = 0;
+    for (GList *l = list; l != NULL; l = l->next, ++idx) {
+        if (l->data == g_window) {
+            lua_pushinteger(L, idx);
+            return 1;
+        }
+    }
+    /* Should never be here. A window should belong to its own group */
+ * Return the index of this window within its class
+ */
+int lua_window_class_window_index(lua_State * L)
+{
+    WnckClassGroup *x = wnck_window_get_class_group(g_window);
+    GList *list = wnck_class_group_get_windows(x);
+    int idx = 0;
+    for (GList *l = list; l != NULL; l = l->next, ++idx) {
+        if (l->data == g_window) {
+            lua_pushinteger(L, idx);
+            return 1;
+        }
+    }
+    /* Should never be here. A window should belong to its own group */
+    lua_pushinteger(L, -1);
+    return 1;
+}
+
+
+
 /**
  * Set the decorations for the current window.
  */

This is just an idea. I am sure skx can do a much better job with these functions. I also use Gnome so it adds a dependency that in the optimal case shouldn't be needed.