Wesley Schwengle IncludeIf: A digital space written in pencil Articles Series Answerrit /dev/null About AnswerritQuestion: 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 none then, 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.

You might also like