Last updated:
0 purchases
qtilebonsai 0.4.0
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.
It also provides an API with window-management operations that allow for quick
access and rearrangements of tabs/windows.
For a quick feeler, look at the demo below, or the visual guide
further below.
https://github.com/aravinda0/qtile-bonsai/assets/960763/b74e559a-30c0-4de7-86f4-ecea8e68125c
Getting Started
Installation
Assuming you already have
qtile up and running, you
have the following options for installation.
PyPI
pip install qtile-bonsai
[!NOTE]
If you have qtile installed globally (eg. via your distro's package manager),
you likely have to do:
pip install qtile-bonsai --break-system-packages
Nowadays pip tries to play it safe and not potentially modify some
dependency that your system's Python-software may depend on. For qtile-bonsai,
using this flag should be safe.
An alternative approach would be to have installed qtile via pipx
and then 'inject' qtile-bonsai into the same virtualenv where qtile resides:
pipx install qtile
pipx inject qtile qtile-bonsai
AUR
For arch-based distros, you can install it from the AUR either manually or with
your favorite AUR-helper. For example:
yay -S qtile-bonsai
Configuration
1. Make Bonsai available as a layout in your qtile config
from qtile_bonsai import Bonsai
layouts = [
Bonsai(**{
# Specify your 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:
# "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)),
# Sometimes it's handy to have a split open in the 'previous' position
EzKey("M-S-v", lazy.layout.spawn_split(terminal, "x", position="previous")),
EzKey("M-S-x", lazy.layout.spawn_split(terminal, "y", position="previous")),
# 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()),
# Precise motions to move directly to specific tabs at the nearest tab level
EzKey("M-1", lazy.layout.focus_nth_tab(1, level=-1)),
EzKey("M-2", lazy.layout.focus_nth_tab(2, level=-1)),
EzKey("M-3", lazy.layout.focus_nth_tab(3, level=-1)),
EzKey("M-4", lazy.layout.focus_nth_tab(4, level=-1)),
EzKey("M-5", lazy.layout.focus_nth_tab(5, level=-1)),
# Precise motions to move to specific windows. The options provided here let
# us pick the nth window counting only from under currently active [sub]tabs
EzKey("C-1", lazy.layout.focus_nth_window(1, ignore_inactive_tabs_at_levels=[1,2])),
EzKey("C-2", lazy.layout.focus_nth_window(2, ignore_inactive_tabs_at_levels=[1,2])),
EzKey("C-3", lazy.layout.focus_nth_window(3, ignore_inactive_tabs_at_levels=[1,2])),
EzKey("C-4", lazy.layout.focus_nth_window(4, ignore_inactive_tabs_at_levels=[1,2])),
EzKey("C-5", lazy.layout.focus_nth_window(5, ignore_inactive_tabs_at_levels=[1,2])),
# 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 container-select mode
EzKey("M-o", lazy.layout.select_container_outer()),
EzKey("M-i", lazy.layout.select_container_inner()),
# 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 container-selection mode to split/tab over containers of
# multiple windows. Manipulate using select_container_outer()/select_container_inner()
EzKey("C-v", lazy.layout.toggle_container_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
# ...
]
3. [Optional] Add the BonsaiBar widget to your qtile bar
qtile-bonsai comes with an optional BonsaiBar widget that lets you view all
your top-level tabs on the qtile-bar.
The default behavior is to automatically hide the top-level/outermost tab-bar if
there is a BonsaiBar widget on the relevant screen. If there isn't, the tab
bar is shown as usual.
from libqtile import bar
from libqtile.config import Screen
from qtile_bonsai import BonsaiBar
screens = [
Screen(top=bar.Bar([
BonsaiBar(**{
# "length": 500,
# "sync_with": "bonsai_on_same_screen",
# "tab.width": 50,
# ...
}),
# ... your other widgets ...
])),
]
Visual Guide
Click on the image to open a web view with the full guide.
Reference
Layout Configuration
[!TIP]
Most options have subtab-level support! ie. you can have one setting for top
level windows and another setting for windows under 2nd level subtabs. eg:
Bonsai({
"window.margin": 10,
"L2.window.margin": 5,
})
The format is L<subtab-level>.<option-name> = <value>
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.single.margin
(unset)
Size of the margin space around a windowwhen it is the single window remainingunder a top-level tab.Can be an int or a list of ints in [top,right, bottom, left] ordering.If not specified, will fall back toreading from window.margin.
window.border_size
1
Width of the border around windows. Mustbe a single integer value since that'swhat qtile allows for window borders.
window.single.border_size
(unset)
Size of the border around a window whenit is the single window remaining undera top-level tab.Must be a single integer value sincethat's what qtile allows for windowborders.If not specified, will fall back toreading from window.border_size.
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 activewindow
window.normalize_on_remove
True
Whether or not to normalize theremaining windows after a window isremoved.If True, the remaining sibling windowswill all become of equal size.If False, the next (right/down) windowwill take up the free space.
window.default_add_mode
tab
(Experimental)Determines how windows get added if theyare not explicitly spawned as a split ora tab.Can be one of "tab" or "match_previous".If "match_previous", then then newwindow will get added in the same waythe previous window was. eg. if theprevious window was added as a y-split,so will the new window.NOTE:Setting this to "tab" may seemconvenient, since externally spawned GUIapps get added as background tabsinstead of messing up the current splitlayout.But due to how the window creation flowhappens, when many splits are requestedin quick succession, this may cause somewindows requested as a split to open upas a tab instead.
tab_bar.height
20
Height of tab bars
tab_bar.hide_when
single_tab
When to hide the tab bar. Allowed valuesare 'never', 'always', 'single_tab'.When 'single_tab' is configured, the baris not shown whenever there is a lonetab remaining, but shows up again whenanother tab is added.For nested tab levels, configuring'always' or 'single_tab' actually meansthat when only a single tab remains, itscontents get 'merged' upwards,eliminating the sub-tab level.
tab_bar.hide_L1_when_bonsai_bar_on_screen
True
For L1 (top level) tab bars only. IfTrue, the L1 tab bar is hidden away ifthere is a BonsaiBar widget on thescreen this layout's group is on.Otherwise the the L1 tab bar is shown(depending on tab_bar.hide_when).This is dynamic and essentially makes itso the L1 tab bar shows up 'whenrequired'.Handy in multi-screen setups if somescreens aren't configured to have aqtile-bar, but the main screen does andhas a BonsaiBar widget as well.Note that this takes precedence overtab_bar.hide_when for L1 bars.
tab_bar.margin
0
Size of the margin space around tabbars.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.Must be a single integer value sincethat's what qtile allows for windowborders.
tab_bar.border_color
Gruvbox.dark_yellow
Color of border around tab bars
tab_bar.bg_color
Gruvbox.bg0
Background color of tab bars, beindtheir tabs
tab_bar.tab.width
50
Width of a tab on a tab bar.Can be an int or auto. If auto, thetabs take up as much of the availablescreen space as possible.Note that this width follows the 'marginbox'/'principal box' model, so itincludes any configured margin amount.
tab_bar.tab.margin
0
Size of the space on either outer sideof individual tabs.Can be an int or a list of ints in [top,right, bottom, left] ordering.
tab_bar.tab.padding
0
Size of the space on either inner sideof individual tabs.Can be an int or a list of ints in [top,right, bottom, left] ordering.
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
13
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 fora tab. The callback accepts 3 parametersand returns the final title string. Theparams 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 thatreturns the active window's title:def my_title_provider(index,active_pane, tab): return active_pane.window.name
container_select_mode.border_size
3
Size of the border around the activeselection when container_select_modeis active.
container_select_mode.border_color
Gruvbox.dark_purple
Color of the border around the activeselection when container_select_modeis active.
auto_cwd_for_terminals
True
(Experimental)If True, when spawning new windows byspecifying a program that happens tobe a well-known terminal emulator, willtry to open the new terminal window insame working directory as the lastfocused window.
restore.threshold_seconds
4
You likely don't need to tweak this.Controls the time within which apersisted state file is considered to befrom a recent qtile config-reload/restart event. If the persistedfile is this many seconds old, werestore our window tree from it.
Layout Commands
Command Name
Description
spawn_split
Launch the provided program into a new window that splits thecurrently 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 thecurrently focused window. If there are multiple candidates, the mostrecently focused of them will be chosen.When container_select_mode is active, will similarly pick neighboringnodes, 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 otherbuilt-in layouts.
right
Same as move_focus("right"). For compatibility with API of otherbuilt-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 otherbuilt-in layouts.
next_tab
Switch focus to the next tab. The window that was previously activethere 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_nth_tab
Switches focus to the nth tab at the specified tab level.Args: n: The 1-based index of the tab that should be focused. level: When there are subtab levels at play, specifies which TabContainer's tabs among the hierarchy of active TabContainers is being acted upon. Tab levels are 1-based. level=1 indicates outermost/top-level tabs. level=-1 (default) indicates the innermost/nearest tabs.Examples: - layout.focus_nth_tab(2) # 2nd top-level tab - layout.focus_nth_tab(3, level=-1) # 3rd from nearest tabs
focus_nth_window
Switches focus to the nth window.Counting is always done based on the geospatial position of windows -ie.starting from the leftmost+innermost window (ie. we traverse leaves ofthe tree, left to right).Args: n: The 1-based index of the window in the list of all candidate windows. ignore_inactive_tabs_at_levels: For the specified list of tab levels, only consider windows under the active tab at that level, ignoring windows under inactive/background tabs. eg. [1] means we should start counting n from the first window in the currently active level 1 (top-level) tab, ignoring windows under inactive tabs. But if there are any subtabs under this active tabs, we DO consider the inactive windows under background/inactive subtabs. eg. [1,2] means we start counting n from the first window of the active top-level tab, and if there are any level 2 subtabs under the active tab, we pick windows only from the active level 2 tab as well, ignoring inactive subtabs. eg. [] or None (default) means consider every single window - even if it's inactive under a background tab. eg. [2] means we start counting from the very first window at the top level, even if it is inactive under a background tab. But whenever there are level 2 subtabs to consider, we only count its windows that are under the active level 2 subtab. Examples: - layout.focus_nth_window(1) - layout.focus_nth_window(3, ignore_inactive_tabs_at_levels=[1]) - layout.focus_nth_window(2, ignore_inactive_tabs_at_levels=[1, 2])
resize
Resizes by moving an appropriate border leftwards. Usually this is theright/bottom border, but for the 'last' node under a SplitContainer, itwill 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 thespecified 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 aneighboring node in the specified direction, so that they both comeunder 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 anancestor SplitContainer at a higher level. It effectively moves awindow '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 nearestTabContainer.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 thecontainer 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 ofequal 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 respectivecontainers.
toggle_container_select_mode
Enable container-select mode where we can select not just a window, buteven their container nodes.This will activate a special border around the active selection. Youcan move its focus around using the same bindings as for switchingwindow focus. You can also select upper/parent or lower/child nodeswith the select_container_outer() and select_container_inner()commands.Handy for cases where you want to split over a collection of windows ormake a new subtab level over a collection of windows.Aside from focus-switching motions, the only operations supported arespawn_split() and spawn_tab(). Triggering other commands willsimply exit container-select mode.
select_container_inner
When in container-select mode, it will narrow the active selection byselecting the first descendent node.
select_container_outer
When in container-select mode, it will expand the active selection byselecting 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 qtilewidgets.As usual, it can be a fixed integer, orone of the 'special' bar constants:bar.CALCULATED or bar.STRETCH.
sync_with
bonsai_on_same_screen
The Bonsai layout whose state should berendered on this widget.Can be one of the following: - bonsai_with_focus: The Bonsai layout of the window that is currently focused. This is relevant in a multi-screen setup - the widget will keep updating based on which screen's Bonsai layout has focus. - bonsai_on_same_screen: The widget will stick to displaying the state of the Bonsai layout that is on the same screen as the widget's bar.
bg_color
None
Background color of the bar.If None, the qtile-bar's' backgroundcolor 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 the bar.Can be an int or auto. If auto, thetabs take up as much of the availablespace on the bar as possible.Note that if the length option is setto bar.CALCULATED, then you cannotprovide auto here, as we would needfixed tab width values to perform thebar.CALCULATED computation.Note that this width follows the 'marginbox'/'principal box' model, so itincludes any configured margin amount.
tab.margin
0
Size of the space on either outer sideof individual tabs.Can be an int or a list of ints in [top,right, bottom, left] ordering.
tab.padding
0
Size of the space on either inner sideof individual tabs.Can be an int or a list of ints in [top,right, bottom, left] ordering.
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
container_select_mode.indicator.bg_color
Gruvbox.bg0_hard
Background color of active tab when incontainer_select_mode.
container_select_mode.indicator.fg_color
Gruvbox.bg0_hard
Foreground color of active tab when incontainer_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.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.