Skip to main content

A flexible custom layout for the qtile window manager that supports arbitrarily nestable tabs and splits.

Project description

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.


https://github.com/aravinda0/qtile-bonsai/assets/960763/0e77b61e-1830-4972-9098-516d111b942b


Getting Started

Installation

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

pip install 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.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 more bindings
    # to directly open 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("A-d", lazy.layout.prev_tab()),
    EzKey("A-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_tab_prev()),
    EzKey("A-S-f", lazy.layout.swap_tab_next()),

    # 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)),
            
            EzKey("r", lazy.layout.rename_tab()),
            
            EzKey("o", lazy.layout.pull_out()),
            EzKey("u", lazy.layout.pull_out_to_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
    # ...
]

Visual Guide

Click on the image to open a full-size web view.

Visual Guide

Reference

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 #6d531f Color of the border around windows
window.active.border_color #d79921 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 #d79921 Color of border around tab bars
tab_bar.bg_color #282828 Background color of tab bars, beind their tabs
tab_bar.tab.min_width 50 Minimum width of a tab on a tab bar
tab_bar.tab.margin 0 Size of the margin space around individual tabs
tab_bar.tab.padding 20 Size of the padding space inside individual tabs
tab_bar.tab.bg_color #3c3836 Background color of individual tabs
tab_bar.tab.fg_color #ebdbb2 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 #7c6f64 Background color of active tabs
tab_bar.tab.active.fg_color #ebdbb2 Foreground text color of the active tab
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.

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 top/bottom windows.
        A y split will end up with two left/right 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 y-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.

Args:
    wrap:
        If True, will wrap around the edge and select windows 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.
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_mo="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 SplitContainer, 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.

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.

Roadmap

  • Basic mouse support (click on tab to focus)
  • Grow/shrink style resizing
  • Some nicer ricing possibilities for subtabs

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qtile_bonsai-0.1.0.tar.gz (75.4 kB view details)

Uploaded Source

Built Distribution

qtile_bonsai-0.1.0-py3-none-any.whl (49.0 kB view details)

Uploaded Python 3

File details

Details for the file qtile_bonsai-0.1.0.tar.gz.

File metadata

  • Download URL: qtile_bonsai-0.1.0.tar.gz
  • Upload date:
  • Size: 75.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: pdm/2.15.1 CPython/3.11.8 Linux/6.8.8-2-MANJARO

File hashes

Hashes for qtile_bonsai-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d9883ca8d18a20512650dd35da01e43b5074665d7cc1512ecb4ac7eb2ae54dc8
MD5 4f2f61f5b933a3b2e7a6a6f5783623b9
BLAKE2b-256 be99f4eb1dda8e49fd1fe7ce923d2aaf9c6ec97e9ce1ce23ff7145cb4f594722

See more details on using hashes here.

File details

Details for the file qtile_bonsai-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: qtile_bonsai-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 49.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: pdm/2.15.1 CPython/3.11.8 Linux/6.8.8-2-MANJARO

File hashes

Hashes for qtile_bonsai-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6623a7db2e365abe1530f47d73e51f10e50996e18c9c15664451268dc8c08a60
MD5 aee79b0614b8430a25e966250672fdbb
BLAKE2b-256 b0b6251a3f6b0ca730cc46f51edfb892a5baf71b26dc3af59bc0d12d90749af7

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page