i3/sway-like tabbed split container
Description
First of all: Thanks for this awesome project, and congrats to all maintainers & contributors! :heart:
I'm just coming from Sway/i3 and I would like to reproduce a behaviour I am really used to: the tabbed split container layout.
It seems that this is not working out-of-the-box in Hyprland. I read the docs about tabbed windows and I've added bind = $mainMod,w,togglegroup to my config.
However, what this does is just create a new group for the currently focused window. What I would like instead is to have a key binding for having all my current workspace's windows grouped together in the same tab group. And another key binding to switch back from that to a split view where windows are split normally.
What is the right way to achieve that in Hyprland?
Once I have a clear view of how to do that, I can add a section to the documentation (through a PR on the wiki repo) to explain how this can be achieved in Hyprland!
Thanks a lot again for all this work, and for any help you can provide.
// Valentin
make a script that iterates through the windows in a workspace and puts them in the created group
I just made such a script. Note that it was not as trivial to implement as I expected because the moveintogroup dispatcher only accepts a direction (up, down, left, right) as argument.
Anyway, this is it:
#!/bin/bash
# Map this script to a key to toggle "tab mode" (ON and OFF), where all windows
# are placed inside one same (tabbed) group
## Collect some data about the environment #####################################
ACTIVEWINDOW_JSON=$(hyprctl activewindow -j)
CLIENTS_JSON=$( hyprctl clients -j)
ORIGINAL_WORKSPACE=$(echo $ACTIVEWINDOW_JSON | jq ".workspace.id")
ORIGINAL_WINDOW=$( echo $ACTIVEWINDOW_JSON | jq ".address" | sed 's:"::g')
WINDOWS_IN_GROUPS=$(echo $CLIENTS_JSON | \
jq ".[] | select(.workspace.id == $ORIGINAL_WORKSPACE) |
select(.grouped != []) | .address" | \
sed 's:"::g')
WINDOWS_NOT_IN_GROUPS=$(echo $CLIENTS_JSON | \
jq ".[] | select(.workspace.id == $ORIGINAL_WORKSPACE) |
select(.grouped == []) | .address" | \
sed 's:"::g')
## Remove all tab groups #######################################################
COMMAND=""
for w in $WINDOWS_IN_GROUPS; do
COMMAND="$COMMAND dispatch focuswindow address:$w; "
COMMAND="$COMMAND dispatch moveoutofgroup; "
done
! [ -z "$COMMAND" ] && hyprctl --batch "$COMMAND" > /dev/null
## If toggling ON, group all windows under one single group ####################
if ! [ -z "$WINDOWS_NOT_IN_GROUPS" ]; then
# Toggle group mode in the original window
#
COMMAND=" dispatch focuswindow address:$ORIGINAL_WINDOW; "
COMMAND="$COMMAND dispatch togglegroup" > /dev/null
hyprctl --batch $COMMAND > /dev/null
# Keep "merging" all other windows in all directions
#
for combo in u:d d:u l:r r:l; do
direction=$(echo $combo | cut -d: -f1)
opposite=$( echo $combo | cut -d: -f2)
counter=0
while true; do
hyprctl dispatch movefocus $direction > /dev/null
CURRENT_WINDOW=$(hyprctl activewindow -j | \
jq ".address" | sed 's:"::g')
if [ $ORIGINAL_WINDOW == $CURRENT_WINDOW ]; then
break
fi
COMMAND=" dispatch moveintogroup $opposite; "
COMMAND="$COMMAND dispatch changegroupactive 1"
hyprctl --batch $COMMAND > /dev/null
# Security check, in case something goes terribly wrong
#
counter=$(( counter + 1 ))
[ $counter -gt 10 ] && break
done
done
fi
## Move focus back to the original window ######################33##############
hyprctl dispatch focuswindow address:$ORIGINAL_WINDOW > /dev/null
In order to use it, do this:
- Copy the script into your
~/.config/hyprfolder. - Give it execution permissions (
chmod +x ~/.config/hypr/tab_mode.sh) - In
~/.config/hypr/hyprland.con, setno_focus_fallbacktotrue(as the script relies on it in order to know when to "stop" merging windows on each direction), like this:
general {
...
no_focus_fallback = true
}
- Also in
~/.config/hypr/hyprland.conf, create a key binding to toggle "tab mode" on and off, such as this one:
bind = $mainMod, T, exec, ~/.config/hypr/tab_mode.sh
Limitations:
- The original layout when toggling the mode OFF might be different from the original one before toggling the mode ON.
- One you toggle the mode OFF, they will be no groups left, even if there were some before toggling the mode ON.
Despite these limitations, I'm finding the script quite useful so far... :)
I have made a similar script as @greenfoo, though different in certain regards that someone may find more or less useful:
+It does not need any config changes except for the bind+It does not grab windows from other monitors (greenfoo's script did for me)-Unfortunately, with very nested windows or a lot of them, it may not group them all or may crash Hyprland (looking for a workaround or a fix in Hyprland itself)
Point 3 would be easily fixed if hyprctl allowed specifying a window's address for grouping instead of just a direction.
For those interested: https://gist.github.com/Atrate/b08c5b67172abafa5e7286f4a952ca4d
I made another alternative based in @Atrate code, but using Golang and https://github.com/labi-le/hyprland-ipc-client, including some fixes for master layout users: https://github.com/thiagokokada/nix-configs/tree/8e741b6f4100ee3866e3e6fe7832eac6c286898f/home-manager/desktop/wayland/hyprland/hyprtabs
Since it uses Golang and IPC instead of calling hyprctl makes it much faster than the original script. From my benchmarks it decreased from ~80ms to <10ms, so for those that cares about latency like me this is a good improvement.
In case anyone is interested, I took the time to make this as a plugin in C++. The advantage of this is that it's less hacky because it uses the actual C++ API's, meaning I could add the windows to the group by actual window ID instead of having to hack together an algorithm that finds all windows by direction. Also, although I haven't done any benchmarks at all I assume this is faster than using either a script or IPC - it's pretty much instantaneous on my machine at least.
Here's the repo: https://github.com/pianocomposer321/hyprland-monocle
Since there's a plugin available - prolly close?
I couldn't get this plugin to build, and it seems the author is targeting Hyprland 0.39.1 instead of 0.43: https://github.com/pianocomposer321/hyprland-monocle/issues/1#issuecomment-2322950724.
Since there's a plugin available - prolly close?
I'd hope that the functionality is simple enough and wanted enough to warrant it to be included in to Hyprland itself. Maybe someone could make a PR?
Since there's a plugin available - prolly close?
I couldn't get this plugin to build, and it seems the author is targeting Hyprland 0.39.1 instead of 0.43: https://github.com/pianocomposer321/hyprland-monocle/issues/1#issuecomment-2322950724.
Yes this is accurate unfortunately. And on top of that I couldn't get hyprpm working on my machine either, so managing versions that work for the version I'm stuck on and the current version would be a complete pain. Hyprland's plugin ecosystem doesn't seem to be totally mature yet unfortunately.
But the code was easy enough to write, and the plugin is only a couple hundred lines of code iirc. Making it work on the latest version or even making a PR to add it to hyprland natively should be simple enough. I would quite possibly do both of those things myself if I could run the latest hyprland.
I made a simple Python script for non-bash experience ones, support dry_run & notification for easier debug, base on @Atrate bash script Usage:
- change to exec mode:
chmod +x script - add to your hyprland config
bind = $mainMod, T, exec, ~/me/dotfiles/hyprland/toggle-tabs.py --enable-notify true >> ~/.hyprland_script.logs
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess
import json
import argparse
parser = argparse.ArgumentParser()
import traceback
EXCLUDE_TITLE = "Picture-in-Picture"
def shell_exec(cmd, dry_run = False):
if dry_run:
print(cmd)
return
subprocess.call(cmd, shell=True)
if __name__ == "__main__":
parser.add_argument("--enable-notify")
parser.add_argument("--dry-run")
args = vars(parser.parse_args())
for k in list(args.keys()):
if args[k] is None:
args.pop(k)
enable_notify = args.get('enable_notify', 'false').lower() == 'true'
dry_run = args.get('dry_run', 'false').lower() == 'true'
print('Run with mode: ', args)
try:
active_window = json.loads(subprocess.check_output("hyprctl -j activewindow", shell=True))
if len(active_window['grouped']) > 0:
shell_exec("hyprctl dispatch togglegroup", dry_run)
else:
active_window_address = active_window['address']
active_space_id = active_window['workspace']['id']
windows = json.loads(subprocess.check_output(["hyprctl" ,"-j", "clients"]))
window_on_active_space = [w for w in windows if w['workspace']['id'] == active_space_id]
should_group_windows = [w for w in window_on_active_space if EXCLUDE_TITLE not in w['title']]
should_group_windows.sort(key=lambda w: (w['at'][0], w['at'][1]))
if len(should_group_windows) == 1:
shell_exec("hyprctl dispatch togglegroup", dry_run)
else:
first_window = should_group_windows.pop(0)
window_args = f'dispatch focuswindow address:{first_window['address']}; dispatch togglegroup; '
for w in should_group_windows:
window_args += f'dispatch focuswindow address:{w['address']}; '
for d in ['l','r','u','d']:
window_args += f'dispatch moveintogroup {d}; '
batch_args = f'{window_args} dispatch focuswindow address:{active_window_address}'
cmd = f'hyprctl --batch "{batch_args}"'
shell_exec(cmd, dry_run)
except Exception as e:
if enable_notify:
# FIXME: Change to your notification
subprocess.call(
"notify-send -a toggle-tab.py 'something wrong, please check log' ",
shell=True,
)
print(traceback.format_exc())
Hello there!
This issue has been closed, as we are moving from issues to discussions. More about this can be found here and in #9854.
Firstly, please make sure this issue is still relevant in latest Hyprland. If so, we ask you to open a discussion (please read the discussion guidelines first, in the pinned post)
Thank you for your understanding!