Skip to content

A flexible layout for the qtile tiling window manager that allows arbitrarily nestable tabs and splits

License

Notifications You must be signed in to change notification settings

aravinda0/qtile-bonsai

Repository files navigation

Qtile Bonsai


Introduction

Qtile Bonsai provides a flexible layout for the qtile tiling window manager that allows you to arrange windows as tabs, splits and even subtabs inside splits.

For a quick feeler, take a look at the demo video below, or the visual guide further below.


qtile_bonsai_demo.mp4

Getting Started

Installation

Assuming you already have qtile up and running, you can just install qtile-bonsai from PyPI.

pip install qtile-bonsai

Note

If you've installed qtile using your distro's package manager, you'll have to run:

pip install qtile-bonsai --break-system-packages.

That will allow the system-installed qtile and qtile-bonsai to 'see' each other.

The alternative would be to have installed qtile via pipx and then 'inject' qtile-bonsai into the same virtualenv where qtile resides:

pipx inject qtile qtile-bonsai

Configuration

1. Make Bonsai available as a layout in your qtile config

from qtile_bonsai import Bonsai


layouts = [
    Bonsai(**{
      # Specify any desired options here. These examples are defaults.
      "window.border_size": 1,
      "tab_bar.height": 20,
      
      # You can specify subtab level specific options if desired by prefixing
      # the option key with the appropriate level, eg. L1, L2, L3 etc.
      # For example, the following options affect only 2nd level subtabs and
      # their windows. eg:
      # "L2.window.border_color": "#ff0000",
      # "L2.window.margin": 5,
    }),
]

2. Add your personal keybindings to your qtile config

from libqtile.config import EzKey, KeyChord
from libqtile.lazy import lazy
from libqtile.utils import guess_terminal


terminal = guess_terminal()
rofi_run_cmd = "rofi -show drun -m -1"

keys = [
    # Open your terminal emulator quickly. See further below for how to
    # directly open other apps as splits/tabs using something like rofi.
    EzKey("M-v", lazy.layout.spawn_split(terminal, "x")),
    EzKey("M-x", lazy.layout.spawn_split(terminal, "y")),
    EzKey("M-t", lazy.layout.spawn_tab(terminal)),
    EzKey("M-S-t", lazy.layout.spawn_tab(terminal, new_level=True)),

    # Motions to move focus. The names are compatible with built-in layouts.
    EzKey("M-h", lazy.layout.left()),
    EzKey("M-l", lazy.layout.right()),
    EzKey("M-k", lazy.layout.up()),
    EzKey("M-j", lazy.layout.down()),
    EzKey("M-d", lazy.layout.prev_tab()),
    EzKey("M-f", lazy.layout.next_tab()),

    # Resize operations
    EzKey("M-C-h", lazy.layout.resize("left", 100)),
    EzKey("M-C-l", lazy.layout.resize("right", 100)),
    EzKey("M-C-k", lazy.layout.resize("up", 100)),
    EzKey("M-C-j", lazy.layout.resize("down", 100)),

    # Swap windows/tabs with neighbors
    EzKey("M-S-h", lazy.layout.swap("left")),
    EzKey("M-S-l", lazy.layout.swap("right")),
    EzKey("M-S-k", lazy.layout.swap("up")),
    EzKey("M-S-j", lazy.layout.swap("down")),
    EzKey("A-S-d", lazy.layout.swap_tabs("previous")),
    EzKey("A-S-f", lazy.layout.swap_tabs("next")),
    
    # Manipulate selections after entering branch-select mode
    EzKey("M-o", lazy.layout.select_branch_out()),
    EzKey("M-i", lazy.layout.select_branch_in()),

    # It's kinda nice to have more advanced window management commands under a
    # qtile key chord.
    KeyChord(
        ["mod4"],
        "w",
        [
            # Use something like rofi to pick GUI apps to open as splits/tabs.
            EzKey("v", lazy.layout.spawn_split(rofi_run_cmd, "x")),
            EzKey("x", lazy.layout.spawn_split(rofi_run_cmd, "y")),
            EzKey("t", lazy.layout.spawn_tab(rofi_run_cmd)),
            EzKey("S-t", lazy.layout.spawn_tab(rofi_run_cmd, new_level=True)),
            
            # Toggle branch-selection mode to split/tab over containers of
            # multiple windows. Manipulate using select_branch_out()/select_branch_in()
            EzKey("C-v", lazy.layout.toggle_branch_select_mode()),
            
            EzKey("o", lazy.layout.pull_out()),
            EzKey("u", lazy.layout.pull_out_to_tab()),
            
            EzKey("r", lazy.layout.rename_tab()),
            
            # Directional commands to merge windows with their neighbor into subtabs.
            KeyChord(
                [],
                "m",
                [
                    EzKey("h", lazy.layout.merge_to_subtab("left")),
                    EzKey("l", lazy.layout.merge_to_subtab("right")),
                    EzKey("j", lazy.layout.merge_to_subtab("down")),
                    EzKey("k", lazy.layout.merge_to_subtab("up")),

                    # Merge entire tabs with each other as splits
                    EzKey("S-h", lazy.layout.merge_tabs("previous")),
                    EzKey("S-l", lazy.layout.merge_tabs("next")),
                ],
            ),
            
            # Directional commands for push_in() to move window inside neighbor space.
            KeyChord(
                [],
                "i",
                [
                    EzKey("j", lazy.layout.push_in("down")),
                    EzKey("k", lazy.layout.push_in("up")),
                    EzKey("h", lazy.layout.push_in("left")),
                    EzKey("l", lazy.layout.push_in("right")),
                    
                    # It's nice to be able to push directly into the deepest
                    # neighbor node when desired. The default bindings above
                    # will have us push into the largest neighbor container.
                    EzKey(
                        "S-j",
                        lazy.layout.push_in("down", dest_selection="mru_deepest"),
                    ),
                    EzKey(
                        "S-k",
                        lazy.layout.push_in("up", dest_selection="mru_deepest"),
                    ),
                    EzKey(
                        "S-h",
                        lazy.layout.push_in("left", dest_selection="mru_deepest"),
                    ),
                    EzKey(
                        "S-l",
                        lazy.layout.push_in("right", dest_selection="mru_deepest"),
                    ),
                ],
            ),
        ]
    ),
    
    # Your other bindings
    # ...
]

(Optional) 3. Add the BonsaiBar widget to your qtile bar

qtile-bonsai comes with an optional BonsaiBar widget that can let you hide the top-level tab bar and display it as a widget on the qtile screen bar instead.

from libqtile import bar
from libqtile.config import Screen

from qtile_bonsai import Bonsai, BonsaiBar


# Hide away only the top level of the default tab bar that is available on the
# layout itself. Subtab bars will remain visible.
layouts = [
    Bonsai(**{
      "L1.tab_bar.hide_when": "always",
    }),
]

screens = [
    Screen(top=bar.Bar([
        BonsaiBar(**{
            # "length": 500,
            # "tab.width": 50,
            # ...
        }),
        # ... your other widgets ...
    ])),
]

Visual Guide

Click on the image to open a web view with the full guide.

Visual Guide

Reference

Layout Configuration

Tip

Most options have subtab-level support! ie. you can have one setting for top level windows (eg. "window.margin" = 10) and another setting for windows under 2nd level subtabs (eg. "L2.window.margin" = 5). Just prefix the option with L<subtab-level>.


Option Name Default Value Description
window.margin 0 Size of the margin space around windows.
Can be an int or a list of ints in [top, right, bottom,
left] ordering.
window.border_size 1 Width of the border around windows. Must be a single integer
value since that's what qtile allows for window borders.
window.border_color Gruvbox.dull_yellow Color of the border around windows
window.active.border_color Gruvbox.vivid_yellow Color of the border around an active window
window.normalize_on_remove True Whether or not to normalize the remaining windows after a
window is removed.
If True, the remaining sibling windows will all become of
equal size.
If False, the next (right/down) window will take up the
free space.
window.default_add_mode tab (Experimental)

Determines how windows get added if they are not explicitly
spawned as a split or a tab.
Can be one of "tab" or "match_previous".
If "match_previous", then then new window will get added in
the same way the previous window was. eg. if the previous
window was added as a y-split, so will the new window.

NOTE:
Setting this to "tab" may seem convenient, since externally
spawned GUI apps get added as background tabs instead of
messing up the current split layout.
But due to how the window creation flow happens, when many
splits are requested in quick succession, this may cause
some windows requested as a split to open up as a tab
instead.
tab_bar.height 20 Height of tab bars
tab_bar.hide_when single_tab When to hide the tab bar. Allowed values are 'never',
'always', 'single_tab'.

When 'single_tab' is configured, the bar is not shown
whenever there is a lone tab remaining, but shows up again
when another tab is added.

For nested tab levels, configuring 'always' or 'single_tab'
actually means that when only a single tab remains, its
contents get 'merged' upwards, eliminating the sub-tab
level.
tab_bar.margin 0 Size of the margin space around tab bars.

Can be an int or a list of ints in [top, right, bottom,
left] ordering.
tab_bar.border_size 0 Size of the border around tab bars
tab_bar.border_color Gruvbox.dark_yellow Color of border around tab bars
tab_bar.bg_color Gruvbox.bg0 Background color of tab bars, beind their tabs
tab_bar.tab.width 50 Width of a tab on a tab bar.

Can be an int or auto. If auto, the tabs take up as much
of the available screen space as possible.

Note that this width follows the 'margin box'/'principal
box' model, so it includes any configured margin amount.
tab_bar.tab.margin 0 Size of the space on either outer side of individual tabs.
tab_bar.tab.padding 0 Size of the space on either inner side of individual tabs.
tab_bar.tab.bg_color Gruvbox.dull_yellow Background color of individual tabs
tab_bar.tab.fg_color Gruvbox.fg1 Foreground text color of individual tabs
tab_bar.tab.font_family Mono Font family to use for tab titles
tab_bar.tab.font_size 15 Font size to use for tab titles
tab_bar.tab.active.bg_color Gruvbox.vivid_yellow Background color of active tabs
tab_bar.tab.active.fg_color Gruvbox.bg0_hard Foreground text color of the active tab
tab_bar.tab.title_provider None A callback that generates the title for a tab. The callback
accepts 3 parameters and returns the final title string. The
params are:
1. index:
    The index of the current tab in the list of tabs.
2. active_pane:
    The active Pane instance under this tab. A Pane is
    just a container for a window and can be accessed via
    pane.window.
3. tab:
    The current Tab instance.

For example, here's a callback that returns the active
window's title:
def my_title_provider(index, active_pane, tab):
    return active_pane.window.name
branch_select_mode.border_size 3 Size of the border around the active selection when
branch_select_mode is active.
branch_select_mode.border_color Gruvbox.dark_purple Color of the border around the active selection when
branch_select_mode is active.
auto_cwd_for_terminals True (Experimental)

If True, when spawning new windows by specifying a
program that happens to be a well-known terminal emulator,
will try to open the new terminal window in same working
directory as the last focused window.
restore.threshold_seconds 4 You likely don't need to tweak this.
Controls the time within which a persisted state file is
considered to be from a recent qtile config-reload/restart
event. If the persisted file is this many seconds old, we
restore our window tree from it.

Layout Commands

Command Name Description
spawn_split Launch the provided program into a new window that splits the currently
focused window along the specified axis.

Args:
    program:
        The program to launch.
    axis:
        The axis along which to split the currently focused window. Can be 'x'
        or 'y'.
        An x split will end up with two left/right windows.
        A y split will end up with two top/bottom windows.
    ratio:
        The ratio of sizes by which to split the current window.
        If a window has a width of 100, then splitting on the x-axis with a
        ratio = 0.3 will result in a left window of width 30 and a right window
        of width 70.
        Defaults to 0.5.
    normalize:
        If True, overrides ratio and leads to the new window and all sibling
        windows becoming of equal size along the corresponding split axis.
        Defaults to True.
    position:
        Whether the new split content appears after or before the currently
        focused window.
        Can be "next" or "previous". Defaults to "next".

Examples:
    - layout.spawn_split(my_terminal, "x")
    - layout.spawn_split(my_terminal, "y", ratio=0.2, normalize=False)
    - layout.spawn_split(my_terminal, "x", position="previous")
spawn_tab Launch the provided program into a new window as a new tab.

Args:
    program:
        The program to launch.
    new_level:
        If True, create a new sub-tab level with 2 tabs. The first sub-tab
        being the currently focused window, the second sub-tab being the newly
        launched program.
    level:
        If provided, launch the new window as a tab at the provided level of
        tabs in the currently focused window's tab hierarchy.
        Level 1 is the topmost level.

Examples:
    - layout.spawn_tab(my_terminal)
    - layout.spawn_tab(my_terminal, new_level=True)
    - layout.spawn_tab("qutebrowser", level=1)
move_focus Move focus to the window in the specified direction relative to the currently
focused window. If there are multiple candidates, the most recently focused of
them will be chosen.
When branch_select_mode is active, will similarly pick neighboring nodes,
which may consist of multiple windows under it.

Args:
    direction:
        The direction in which a neighbor is found to move focus to. Can be
        "up"/"down"/"left"/"right".
    wrap:
        If True, will wrap around the edge and select items from the other end
        of the screen. Defaults to True.
left Same as move_focus("left"). For compatibility with API of other built-in
layouts.
right Same as move_focus("right"). For compatibility with API of other built-in
layouts.
up Same as move_focus("up"). For compatibility with API of other built-in
layouts.
down Same as move_focus("down"). For compatibility with API of other built-in
layouts.
next_tab Switch focus to the next tab. The window that was previously active there will
be focused.

Args:
    wrap:
        If True, will cycle back to the fist tab if invoked on the last tab.
        Defaults to True.
prev_tab Same as next_tab() but switches focus to the previous tab.
focus_tab Switches focus to the tab at the position specified by index. When subtabs are
present, the nearest TabContainer is used as the context, unless level is
specified.

Args:
    index:
        The index of the tab that should be focused.
    level:
        When there are subtab levels at play, specifies which TabContainer's
        tabs are being considered for focus.
        level = 1 indicates top level tabs.
        level = None (default) indicates the 'nearest' tabs.

Examples:
    - layout.focus_tab(0, level=1) # Always pick from topmost tabs
    - layout.focus_tab(3)
resize Resizes by moving an appropriate border leftwards. Usually this is the
right/bottom border, but for the 'last' node under a SplitContainer, it will be
the left/top border.

Basically the way tmux does resizing.

If there are multiple nested windows under the area being resized, those windows
are resized proportionally.

Args:
    amount:
        The amount by which to resize.

Examples:
    - layout.resize("left", 100)
    - layout.resize("right", 100)
swap Swaps the currently focused window with the nearest window in the specified
direction. If there are multiple candidates to pick from, then the most recently
focused one is chosen.

Args:
    wrap:
        If True, will wrap around the edge and select windows from the other
        end of the screen to swap.
        Defaults to False.
swap_tabs Swaps the currently active tab with the previous tab.

Args:
    wrap:
        If True, will wrap around the edge of the tab bar and swap with the
        last tab.
        Defaults to True.
rename_tab Rename the currently active tab.

Args:
    widget:
        The qtile widget that should be used for obtaining user input for the
        renaming. The 'prompt' widget is used by default.
merge_tabs Merge the currently active tab with another tab, such that both tabs' contents
now appear in 2 splits.

Args:
    direction:
        Which neighbor tab to merge with. Can be either "next" or "previous".
    axis:
        The axis along which the merged content should appear as splits.

Examples:
    - layout.merge_tabs("previous")
    - layout.merge_tabs("next", "y")
merge_to_subtab Merge the currently focused window (or an ancestor node) with a neighboring node
in the specified direction, so that they both come under a (possibly new)
subtab.

Args:
    direction:
        The direction in which to find a neighbor to merge with.
    src_selection:
        Determines how the source window/node should be resolved. ie. do we pick
        just the current window, or all windows under an appropriate ancestor
        container.
        Valid values are defined in NodeHierarchySelectionMode. See below.
    dest_selection:
        Determines how the neighboring node should be resolved, similar to how
        src_selection is resolved.
        Valid values are defined in NodeHierarchySelectionMode. See below.
    normalize:
        If True, any removals during the merge process will ensure all sibling
        nodes are resized to be of equal dimensions.

Valid values for NodeHierarchySelectionMode are:
    "mru_deepest":
        Pick a single innermost window. If there are multiple such neighboring
        windows, pick the most recently used (MRU) one.
    "mru_subtab_else_deepest" (default):
        If the target is under a subtab, pick the subtab. If there is no subtab
        in play, behaves like mru_deepest.
    "mru_largest"
        Given a window, pick the largest ancestor node that the window's border
        is a fragment of. This resolves to a SplitContainer or a TabContainer.
    "mru_subtab_else_largest"
        If the target is under a subtab, pick the subtab. If there is no subtab
        in play, behaves like mru_largest.

Examples:
    - layout.merge_to_subtab( "right", dest_selection="mru_subtab_else_deepest",
    )
    - layout.merge_to_subtab( "up", src_selection="mru_deepest",
    dest_selection="mru_deepest", )
push_in Move the currently focused window (or a related node in its hierarchy) into a
neighboring window's container.

Args:
    direction:
        The direction in which to find a neighbor whose container we push into.
    src_selection:
        (See docs in merge_to_subtab())
    dest_selection:
        (See docs in merge_to_subtab())
    normalize:
        If True, any removals during the process will ensure all sibling nodes
        are resized to be of equal dimensions.
    wrap:
        If True, will wrap around the edge of the screen and push into the
        container on the other end.

Examples:
    - layout.push_in("right", dest_selection="mru_deepest")
    - layout.push_in("down", dest_selection="mru_largest", wrap=False)
pull_out Move the currently focused window out from its SplitContainer into an ancestor
SplitContainer at a higher level. It effectively moves a window 'outwards'.

Args:
    position:
        Whether the pulled out node appears before or after its original
        container node.
        Can be "next" or "previous". Defaults to "previous".
    src_selection:
        Can either be "mru_deepest" (default) or "mru_subtab_else_deepest".
        (See docs in merge_to_subtab())
    normalize:
        If True, all sibling nodes involved in the rearrangement are resized
        to be of equal dimensions.

Examples:
    - layout.pull_out()
    - layout.pull_out(src_selection="mru_subtab_else_deepest")
    - layout.pull_out(position="next")
pull_out_to_tab Extract the currently focused window into a new tab at the nearest TabContainer.

Args:
    normalize:
        If True, any removals during the process will ensure all sibling nodes
        are resized to be of equal dimensions.
normalize Starting from the focused window's container, make all windows in the container
of equal size.

Args:
    recurse:
        If True, then nested nodes are also normalized similarly.
normalize_tab Starting from the focused window's tab, make all windows in the tab of equal
size under their respective containers.

Args:
    recurse:
        If True, then nested nodes are also normalized similarly.
        Defaults to True.
normalize_all Make all windows under all tabs be of equal size under their respective
containers.
toggle_branch_select_mode Enable branch-select mode where we can select not just a window, but even their
container nodes.

This will activate a special border around the active selection. You can move
its focus around using the same bindings as for switching window focus. You can
also select upper/parent or lower/child nodes with the select_branch_out() and
select_branch_in() commands.

Handy for cases where you want to split over a collection of windows or make a
new subtab level over a collection of windows.

Aside from focus-switching motions, the only operations supported are
spawn_split() and spawn_tab(). Triggering other commands will simply exit
branch-select mode.
select_branch_in When in branch-select mode, it will narrow the active selection by selecting the
first descendent node.
select_branch_out When in branch-select mode, it will expand the active selection by selecting the
next ancestor node.
tree_repr Returns a YAML-like text representation of the internal tree hierarchy.

BonsaiBar Widget

Option Name Default Value Description
length 500 The standard length property of qtile widgets.
bg_color None Background color of the bar.
If None, the qtile-bar's' background color is used.
font_family Mono Font family to use for tab titles
font_size 15 Size of the font to use for tab titles
tab.width 50 Width of a tab on a tab bar.
tab.margin 0 Size of the space on either outer side of individual tabs.
tab.padding 0 Size of the space on either inner side of individual tabs.
tab.bg_color Gruvbox.dull_yellow Background color of the inactive tabs
tab.fg_color Gruvbox.fg1 Foreground color of the inactive tabs
tab.active.bg_color Gruvbox.vivid_yellow Background color of active tab
tab.active.fg_color Gruvbox.bg0_hard Foreground color of active tab
branch_select_mode.indicator.bg_color Gruvbox.bg0_hard Background color of active tab when in branch_select_mode.
branch_select_mode.indicator.fg_color Gruvbox.bg0_hard Foreground color of active tab when in branch_select_mode.

Support

For any bug reports, please file an issue. For questions/discussions, use the GitHub Discussions section, or you can ask on the qtile subreddit.

About

A flexible layout for the qtile tiling window manager that allows arbitrarily nestable tabs and splits

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Languages