100% plant-based blog.

Better PiP (picture-in-picture) in i3

Much like this guy, I, too, wanted a quick way to move a video into a small PiP-style window - as often you just want to listen and kind of see what's going on, but not have the video occupy valuable screen real estate. Not to mention that the larger the video is, the more distracting it is.

The Bad Way

I initially used his method of adding this binding:

bindsym $mod+c exec "i3-msg 'floating toggle; sticky toggle; resize shrink
width 10000px; resize grow width 400px; resize shrink height 10000px; resize
grow height 250px;move position 10px 10px;'"

However, it turned out to be a very inconvenient solution - by resizing the window also when toggling floating state back to tiling, it messes up your container sizes. Furthermore, if you have multiple videos in a playlist, each new video is started in normal size, thus requiring you to double activate your PiP binding to get the size right - messing up your container sizes everytime.

Another consideration is that to use the binding, you have to have the mpv (or whatever video player you are using) window in focus - an unnecessary step when you'd rarely if ever want to target another window.

The Better Way

So instead, we can use i3-msg -t get_tree to get the entire container tree from i3, and use jq (community/jq) with a hardcoded selector to find our window - and its window ID. Ok, so what we have so far is:

# The window we want to retrieve from the tree (here, any where class is mpv).
selector='.. | select(.window_properties.class == "mpv")?'

# Get the entire window tree from i3.
tree=$(i3-msg -t get_tree)

# Get the window ID of what is hopefully our window.
win_id=$(jq -rc "${selector} | .window" <<< "${tree}" | tail -n1)

Of course, it's possible that we have multiple mpv windows open - here we just use tail -n1 to limit the results to one and not break if that is indeed the case. This could of course be improved, but depending on your typical use case it may be a "good enough" solution.

We also need the current floating state of the window - this is important as we do not want to resize the window except when its floating.

# Get the floating state.
float=$(jq -rc "${selector} | .floating" <<< "${tree}" | tail -n1)

Finally, armed with the information we need, we can move our mpv window to/from our PiP container:

# There seems to be a bug in i3 where doing multiple commands after specifying
# a target window may cause i3 to not apply all the commands on the right
# window but instead reselect a window somewhere along the way (perhaps due to
# mouse movement), so we select separately for each command.
# ${float} could be 'user_off' or 'auto_off', but we want it either way.
if [[ ${float:(-4)} == '_off' ]]; then
    i3-msg "[id=${win_id}] floating enable"
    i3-msg "[id=${win_id}] sticky enable"
    # Only resize when going _to_ a floating state, else messes up tile sizes.
    i3-msg "[id=${win_id}] resize set 432 288"
    i3-msg "[id=${win_id}] move absolute position 2103 785"
elif [[ ${float:(-3)} == '_on' ]]; then
    i3-msg "[id=${win_id}] floating disable"
    i3-msg "[id=${win_id}] sticky disable"
fi

And voilà! The 432x288 PiP window at 2103 785 works well for me on a 1440p monitor, but change it according to your needs. Also, don't name your script pip if you plan on using python.

Transparency

Mentioned in the post referenced at the top is also how to set transparency for your PiP instance using picom (community/picom, previously compton):

opacity-rule = [
    "100:_NET_WM_STATE@:a !*?= '_NET_WM_STATE_STICKY' && class_g='mpv'",
    "70:_NET_WM_STATE@:a *?= '_NET_WM_STATE_STICKY' && class_g='mpv'"
];

This is also bad. For one, using && in your picom config breaks your default transparency (and apparently shadows?), but even if this gets fixed, you're still stuck with your mpv being transparent any time you float it, when this is probably not what you want in basically every other case.

Instead we can just use transset-df (aur/transset-df), and add the following line to our if-clause:

transset-df -i "${win_id}" 0.7

And at the end of our else-clause:

transset-df -i "${win_id}" 1