Question: How to avoid killing an i3 scratchpad window
Answer
It’s kinda complicated. i3 exposes a scratchpad_state on every node, but it
is not relevant for the node you initially check. The window node of the
application you are targetting. Today we are scratching an itch.
When I saw this question posted on Reddit I first thought: let’s look at the data. My Workspace on Demand daemon has an IPC event debugger. So I fired it up and went looking at the data, my debugger told me:
Processing window with payload close
The event looks like this, I removed the non-interesting bits:
$VAR1 = {
'container' => {
'window_properties' => {
'machine' => 'sputnik-odin',
'title' => '~',
'class' => 'kitty',
'instance' => 'kitty',
'transient_for' => undef
},
'floating' => 'auto_off',
'window' => 209715214,
'name' => '~',
'scratchpad_state' => 'none',
'sticky' => $VAR1->{'container'}{'urgent'},
},
'change' => 'close'
};
You can see the scratchpad_state… But this is an event that has happened,
so I don’t think you can block it.
Than someone else posted an answer on how to kill windows of a particular type:
bindsym $mod+Shift+x [floating] kill
This lead me into, oh, you can do two kill options:
bindsym $mod+k [tiling] kill
bindsym $mod+Shift+k [floating] kill
bindsym $mod+Shift+Ctrl+k kill
But that lead me to think: You can ask i3 anything and it will answer the question. The concept:
- get the current active window
- if the window has property scratchpad other then
nonethen, don’t kill else i3-msg kill
To get the focussed window:
i3-msg -t get_tree | jq '.. | objects | select(.focused == true)'
Is it a scratchpad?
i3-msg -t get_tree | jq -r '.. | objects | select(.focused == true) | .scratchpad_state' | grep -qw 'none'
Now you can script it:
# script it
if [ $(i3-msg -t get_tree | jq -r '.. | objects | select(.focused == true) | .scratchpad_state') = "none" ]
then
i3-msg kill
fi
# or...
i3-msg -t get_tree | jq -r '.. | objects | select(.focused == true) | .scratchpad_state' | grep -qw none && i3-msg kill
OP tested the solution and said: computer said no. Hmmmm. Ok, so windows in the
scratchpad always have none as a state. This is breaking expectations. This
must be a bug!
So, when you send a window to the scratchpad (i3-msg move scratchpad) you can
look up the scratchpad state:
i3-msg -t get_tree | jq '.. | objects | select(.name == "__i3_scratch")'
When you ask i3 to show the scratchpad it sends it to your workspace as a floating window:
i3-msg -t get_tree | jq '[.. | objects | select(.floating == "user_on")]'
Great! You can than compare the “window” property against the window property of what you try to kill:
i3-msg -t get_tree | jq '
(.. | objects | select(.focused == true) | .window) as $focused |
[.. | objects | select(.floating == "user_on") | .window] |
any(. == $focused)
'
There is a catch. I didn’t notice it at first either:
I don’t do select(.name == "__i3_scratch") anywhere. But.. if you do.. it
still doesn’t do what you expect it to do:
i3-msg -t get_tree | jq '
[.. | objects | select(.name == "__i3_scratch") | .. | objects | select(.floating == "user_on") | .window]
'
This shows nothing useful, nodes: [] and floating_nodes: []. Empty.
I went looking in the i3 source code, because I thought get_tree was broken,
because I’ve tested i3-msg scratchpad show and it consistently sets the focus
to the scratchpad. So it knows, it just isn’t telling us. From
src/scratchpad.c:
/* If this was 'scratchpad show' without criteria, we check if there is a
* unfocused scratchpad on the current workspace and focus it */
Con *walk_con;
Con *focused_ws = con_get_workspace(focused);
TAILQ_FOREACH (walk_con, &(focused_ws->floating_head), floating_windows) {
if (!con && (floating = con_inside_floating(walk_con)) &&
floating->scratchpad_state != SCRATCHPAD_NONE &&
floating != con_inside_floating(focused)) {
DLOG("Found an unfocused scratchpad window on this workspace\n");
DLOG("Focusing it: %p\n", walk_con);
/* use con_descend_tiling_focused to get the last focused
* window inside this scratch container in order to
* keep the focus the same within this container */
con_activate(con_descend_tiling_focused(walk_con));
return true;
}
}
/* If this was 'scratchpad show' without criteria, we check if there is a
* visible scratchpad window on another workspace. In this case we move it
* to the current workspace. */
focused_ws = con_get_workspace(focused); TAILQ_FOREACH (walk_con, &all_cons, all_cons) {
Con *walk_ws = con_get_workspace(walk_con);
if (!con && walk_ws &&
!con_is_internal(walk_ws) && focused_ws != walk_ws &&
(floating = con_inside_floating(walk_con)) &&
floating->scratchpad_state != SCRATCHPAD_NONE) {
DLOG("Found a visible scratchpad window on another workspace,\n");
DLOG("moving it to this workspace: con = %p\n", walk_con);
con_move_to_workspace(floating, focused_ws, true, false, false);
con_activate(con_descend_focused(walk_con));
return true;
}
}
This is a bug for sure. We see floating->scratchpad_state != SCRATCHPAD_NONE
so the serialization is wrong. Now let’s look at src/ipc.c and we see in
dump_node this code:
ystr("scratchpad_state");
switch (con->scratchpad_state) {
case SCRATCHPAD_NONE:
ystr("none");
break;
case SCRATCHPAD_FRESH:
ystr("fresh");
break;
case SCRATCHPAD_CHANGED:
ystr("changed");
break;
}
This looks to be good. But why isn’t it showing it on the window node itself?
That is the tricky part of it. It is part of the node above it, its parent!
The code in src/scratchpad.c loops over the nodes and checks for
floating_con nodes and descends into the node of your application and then
activates that node. So let’s see how that plays out when we ask i3 this
information:
i3-msg -t get_tree | jq '[.. | objects | select(.type == "floating_con" and .scratchpad_state != "none")]'
This works, it shows the window in the nodes section. Thus making your
query:
i3-msg -t get_tree | jq '
(.. | objects | select(.focused == true) | .window) as $focused |
[.. | objects | select(.type == "floating_con" and .scratchpad_state != "none") | .nodes[].window] |
any(. == $focused)
'
Now this either says true or false. Now you can script something that is
able to ignore killing scratchpadded windows or not.
maybe=$(i3-msg -t get_tree | jq '
(.. | objects | select(.focused == true) | .window) as $focused |
[.. | objects | select(.type == "floating_con" and .scratchpad_state != "none") | .nodes[].window] |
any(. == $focused)
')
[ $maybe = 'true' ] && exit 0
i3-msg kill
# or use case
case $maybe in
'true') i3-msg scratchpad show;;
'false') i3-msg kill;;
esac
And your i3 config:
bindsym $mod+k exec $HOME/bin/dont-kill-scratchpads
bindsym $mod+Shift+k kill
You can now safely hit the kill switch on your scratchpads and not kill them and have a escape hatch that kills everything, including scratchpads.