Last active
March 22, 2025 13:01
-
-
Save Atrate/b08c5b67172abafa5e7286f4a952ca4d to your computer and use it in GitHub Desktop.
i3-like tabs for Hyprland. Usage: save the script as `~/.config/hypr/hyprtabs.sh` or somewhere else and add the following to your `hyprland.conf`, changing the keybind or path as you see fit: `bind = $mainMod SHIFT, w, exec, ~/.config/hypr/hyprtabs.sh`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash --posix | |
# ------------------------------------------------------------------------------ | |
# Copyright (C) 2024 Atrate | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU Affero General Public License as | |
# published by the Free Software Foundation, either version 3 of the | |
# License, or (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU Affero General Public License for more details. | |
# | |
# You should have received a copy of the GNU Affero General Public License | |
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |
# ------------------------------------------------------------------------------ | |
# ------------------------------------------------------------------------------ | |
# Script to simulate the way i3 creates tabs, but in Hyprland! | |
# --- | |
# Version: 0.5.0 | |
# --- | |
# Known issues: | |
# - Hyprland crashes when using this script with too many windows. Sounds like | |
# a Hyprland issue, but if that gets too annoying for me I'll find some | |
# workaround for this script. | |
# - Grouping does not work if the layout is too "deep". | |
# ------------------------------------------------------------------------------ | |
# Set POSIX-compliant mode for security and unset possible overrides | |
# NOTE: This does not mean that we are restricted to POSIX-only constructs | |
# ------------------------------------------------------------------------ | |
POSIXLY_CORRECT=1 | |
set -o posix | |
readonly POSIXLY_CORRECT | |
export POSIXLY_CORRECT | |
# Set IFS explicitly. POSIX does not enforce whether IFS should be inherited | |
# from the environment, so it's safer to set it expliticly | |
# -------------------------------------------------------------------------- | |
IFS=$' \t\n' | |
export IFS | |
# Set up fd 3 for discarding output, necessary for set -r | |
# ------------------------------------------------------- | |
exec 3>/dev/null | |
# ------------------------------------------------------------------------------ | |
# Options description: | |
# -o pipefail: exit on error in any part of pipeline | |
# -eE: exit on any error, go through error handler | |
# -u: exit on accessing uninitialized variable | |
# -r: set bash restricted mode for security | |
# The restricted mode option necessitates the usage of tee | |
# instead of simple output redirection when writing to files | |
# ------------------------------------------------------------------------------ | |
set -o pipefail -eEur | |
# Speed up script by not using unicode | |
# ------------------------------------ | |
LC_ALL=C | |
LANG=C | |
# Check whether to group or ungroup windows | |
# ----------------------------------------- | |
if hyprctl -j activewindow | jq -cr '.grouped' | grep -vFq '[' | |
then | |
# -------------------------------------------------------------------------- | |
# Ungroup current window group | |
# -------------------------------------------------------------------------- | |
hyprctl dispatch togglegroup | |
else | |
# -------------------------------------------------------------------------- | |
# Group all windows on focused workspace | |
# -------------------------------------------------------------------------- | |
# Save original window's address | |
# ------------------------------ | |
ORIGWINDOW="$(hyprctl -j activewindow | jq -cr '.address')" | |
# Get current workspace's windows' addresses | |
# ------------------------------------------ | |
WINDOWS="$(hyprctl -j clients | jq -cr ".[] | select(.workspace.id == $(hyprctl activeworkspace -j | jq -cr '.id')) | .address")" | |
# If there's just one window, just group it normally for better UX | |
# ---------------------------------------------------------------- | |
if [ "$(echo "$WINDOWS" | wc -l)" -eq 1 ] | |
then | |
hyprctl dispatch togglegroup | |
else | |
# Move to each window and try to group it every which way | |
# ------------------------------------------------------- | |
window_args="" | |
for window in $WINDOWS | |
do | |
window_args="$window_args dispatch focuswindow address:$window; dispatch moveintogroup l; dispatch moveintogroup r; dispatch moveintogroup u; dispatch moveintogroup d;" | |
done | |
# Group the first window | |
# ---------------------- | |
batch_args="dispatch togglegroup;" | |
# Group all other windows twice (once isn't enough in case of very | |
# "deep" layouts". This ugly workaround could be fixed if hyprland | |
# allowed moving into groups based on addresses and not positions | |
# ---------------------------------------------------------------- | |
batch_args="$batch_args $window_args $window_args" | |
# Also focus the original window at the very end | |
# ---------------------------------------------- | |
batch_args="$batch_args dispatch focuswindow address:$ORIGWINDOW" | |
# Execute the grouping using hyprctl --batch for performance | |
# ---------------------------------------------------------- | |
hyprctl --batch "$batch_args" | |
fi | |
fi | |
I use Alt+Tab and Alt+Shift+Tab with those bindings @0x00Jeff
bind = $mainMod, Tab, changegroupactive, f
bind = $mainMod SHIFT, Tab, changegroupactive, b
Hi, nice script, I re-write it to Python script for non bash shell experience ones, also tweak a little bit and support dry-run for easy debug
#!/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())
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
how to cycle between tabs with the keyboard after executing this?