Hyprland icon indicating copy to clipboard operation
Hyprland copied to clipboard

i3/sway-like tabbed split container

Open choucavalier opened this issue 2 years ago • 5 comments

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

choucavalier avatar Jul 27 '23 08:07 choucavalier

make a script that iterates through the windows in a workspace and puts them in the created group

MightyPlaza avatar Aug 06 '23 00:08 MightyPlaza

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:

  1. Copy the script into your ~/.config/hypr folder.
  2. Give it execution permissions (chmod +x ~/.config/hypr/tab_mode.sh)
  3. In ~/.config/hypr/hyprland.con, set no_focus_fallback to true (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
}
  1. 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... :)

greenfoo avatar Dec 10 '23 21:12 greenfoo

I have made a similar script as @greenfoo, though different in certain regards that someone may find more or less useful:

  1. + It does not need any config changes except for the bind
  2. + It does not grab windows from other monitors (greenfoo's script did for me)
  3. - 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

Atrate avatar Mar 01 '24 10:03 Atrate

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.

thiagokokada avatar Jul 21 '24 18:07 thiagokokada

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

pianocomposer321 avatar Aug 26 '24 21:08 pianocomposer321

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.

thiagokokada avatar Sep 10 '24 14:09 thiagokokada

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?

Atrate avatar Sep 10 '24 14:09 Atrate

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.

pianocomposer321 avatar Sep 10 '24 15:09 pianocomposer321

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())

duyquang6 avatar Mar 22 '25 09:03 duyquang6

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!

vaxerski avatar Apr 05 '25 19:04 vaxerski