From 6272fa313eae9eed1d43fe4e7197edf338b1f8c1 Mon Sep 17 00:00:00 2001 From: Frantisek Bohacek Date: Sat, 23 Sep 2023 23:05:29 +0200 Subject: [PATCH] feat: add qtile xmonad layout --- modules/desktop/qtile/config/config.py | 15 +- modules/desktop/qtile/config/xmonad.py | 695 +++++++++++++++++++++++++ modules/desktop/qtile/home.nix | 1 + 3 files changed, 706 insertions(+), 5 deletions(-) create mode 100644 modules/desktop/qtile/config/xmonad.py diff --git a/modules/desktop/qtile/config/config.py b/modules/desktop/qtile/config/config.py index 7bb78a1..4956b85 100644 --- a/modules/desktop/qtile/config/config.py +++ b/modules/desktop/qtile/config/config.py @@ -16,6 +16,7 @@ from qtile_extras.widget.decorations import BorderDecoration, PowerLineDecoratio from tasklist import TaskList from mpris2widget import Mpris2 from bluetooth import Bluetooth +import xmonad from nixenvironment import setupLocation, configLocation colors = { @@ -109,9 +110,9 @@ layout_theme = { } layouts = [ - layout.MonadTall(**layout_theme), + xmonad.MonadTall(**layout_theme), layout.Max(**layout_theme), - layout.MonadWide(**layout_theme), + xmonad.MonadWide(**layout_theme), ] widget_defaults = dict( @@ -332,8 +333,8 @@ def init_screen(top_bar, wallpaper): return Screen(top=top_bar, bottom=create_bottom_bar(), wallpaper=wallpaper, width=1920, height=1080) screens = [ - init_screen(create_top_bar(systray = True), f'{setupLocation}/wall.png'), init_screen(create_top_bar(), f'{setupLocation}/wall.png'), + init_screen(create_top_bar(systray = True), f'{setupLocation}/wall.png'), init_screen(create_top_bar(), f'{setupLocation}/wall.png'), ] @@ -359,6 +360,8 @@ keys.extend([ EzKey('M-', lazy.layout.swap_main()), EzKey('M-', lazy.next_layout()), EzKey('M-S-', lazy.to_layout_index(0), desc = 'Default layout'), + EzKey('M-', lazy.layout.increase_nmaster()), + EzKey('M-', lazy.layout.decrease_nmaster()), ]) # Spwning programs @@ -426,12 +429,14 @@ keys.extend([ EzKey('M-S-c', lazy.window.kill()), EzKey('M-C-r', lazy.reload_config()), EzKey('M-C-q', lazy.shutdown()), + + #EzKey(f'M-r', lazy.to_screen(0), desc = f'Move focus to screen {i}'), ]) # Monitor navigation monitor_navigation_keys = ['w', 'e', 'r'] -monitor_index_map = [ 1, 2, 0 ] for i, key in enumerate(monitor_navigation_keys): + monitor_index_map = [ 2, 1, 0 ] keys.extend([ EzKey(f'M-{key}', lazy.to_screen(monitor_index_map[i]), desc = f'Move focus to screen {i}'), EzKey(f'M-S-{key}', lazy.window.toscreen(monitor_index_map[i]), desc = f'Move focus to screen {i}'), @@ -576,7 +581,7 @@ def startup_applications(client: Window): if client.match(Match(wm_class = 'firefox')) and firefoxInstance <= 1: client.togroup(groups[firefoxInstance].name) firefoxInstance += 1 - elif client.match(Match(wm_class = 'discord')) or client.match(Match(wm_class = 'cinny')): + elif client.match(Match(wm_class = 'discord')) or client.match(Match(wm_class = 'telegram-desktop')) or client.match(Match(wm_class = 'cinny')): client.togroup(groups[8].name) # Turn off fullscreen on unfocus diff --git a/modules/desktop/qtile/config/xmonad.py b/modules/desktop/qtile/config/xmonad.py new file mode 100644 index 0000000..41e3227 --- /dev/null +++ b/modules/desktop/qtile/config/xmonad.py @@ -0,0 +1,695 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2011-2012 Dustin Lacewell +# Copyright (c) 2011 Mounier Florian +# Copyright (c) 2012 Craig Barnes +# Copyright (c) 2012 Maximilian Köhl +# Copyright (c) 2012, 2014-2015 Tycho Andersen +# Copyright (c) 2013 jpic +# Copyright (c) 2013 babadoo +# Copyright (c) 2013 Jure Ham +# Copyright (c) 2013 Tao Sauvage +# Copyright (c) 2014 ramnes +# Copyright (c) 2014 Sean Vig +# Copyright (c) 2014 dmpayton +# Copyright (c) 2014 dequis +# Copyright (c) 2014 Florian Scherf +# Copyright (c) 2017 Dirk Hartmann +# Copyright (c) 2021 Jakob Helmecke +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from libqtile.layout.base import _SimpleLayoutBase + + +class MonadTall(_SimpleLayoutBase): + """Emulate the behavior of XMonad's default tiling scheme. + + Master-Pane: + + A master pane that contains a set number of windows takes up a vertical portion of + the screen_rect based on the ratio setting. This ratio can be adjusted with + the ``cmd_grow_master`` and ``cmd_shrink_master``. + + :: + + --------------------- + | | | + | | | + | | | + | | | + | | | + | | | + --------------------- + + Using the ``cmd_flip`` method will switch which horizontal side the master + pane will occupy. The master pane is considered the "top" of the stack. + + :: + + --------------------- + | | | + | | | + | | | + | | | + | | | + | | | + --------------------- + + Slave-panes: + + Occupying the rest of the screen_rect are one or more slave panes. The + slave panes will share the vertical space of the screen_rect. + + :: + + --------------------- + | | | + | |______| + | | | + | |______| + | | | + | | | + --------------------- + + Panes can be moved with the ``cmd_shuffle_up`` and ``cmd_shuffle_down`` + methods. As mentioned the master pane is considered the top of the stack; + moving up is counter-clockwise and moving down is clockwise. + + The opposite is true if the layout is "flipped". + + :: + + --------------------- --------------------- + | | 2 | | 2 | | + | |______| |_______| | + | | 3 | | 3 | | + | 1 |______| |_______| 1 | + | | 4 | | 4 | | + | | | | | | + --------------------- --------------------- + + + Normalizing/Resetting: + + To restore master slave ratio use the ``cmd_normalize`` method. + + To reset the layout to its default state, including the master + windows, master slave ratio and flip, use the ``cmd_reset`` method. + + Maximizing: + + To toggle a maximized layout, showing a single window, simply use the + ``cmd_maximize`` on a focused client. + + Suggested Bindings:: + # these mirror the xmonad default config + + Key([modkey], "j", lazy.layout.down()), + Key([modkey], "k", lazy.layout.up()), + Key([modkey, "shift"], "j", lazy.layout.shuffle_down()), + Key([modkey, "shift"], "k", lazy.layout.shuffle_up()), + Key([modkey], "h", lazy.layout.shink_master()), + Key([modkey], "l", lazy.layout.grow_master()), + Key([modkey], "n", lazy.layout.normalize()), + Key([modkey], "m", lazy.layout.master()), + Key([modkey], "comma", lazy.layout.decrease_nmaster()), + Key([modkey], "period", lazy.layout.increase_nmaster()), + Key([modkey], "Return", lazy.layout.swap_master()) + + # keybindings not in xmonad default config + + Key([modkey, "shift"], "n", lazy.layout.reset()), + Key([modkey], "space", lazy.layout.flip()), + Key([modkey, "shift"], "space", lazy.layout.flip_master()), + Key([modkey, "shift"], "m", lazy.layout.maximize()), + """ + + _left = 0 + _right = 1 + _vert = 0 + _hori = 1 + _med_ratio = 0.5 + + defaults = [ + ("border_focus", "#ff0000", "Border colour(s) for the focused window."), + ("border_normal", "#000000", "Border colour(s) for un-focused windows."), + ("border_width", 2, "Border width."), + ("single_border_width", None, "Border width for single window"), + ("single_margin", None, "Margin size for single window"), + ("margin", 0, "Margin of the layout"), + ( + "ratio", + 0.5, + "The percent of the screen-space the master pane should occupy by default.", + ), + ( + "min_ratio", + 0.25, + "The percent of the screen-space the master pane should occupy at minimum.", + ), + ( + "max_ratio", + 0.75, + "The percent of the screen-space the master pane should occupy at maximum.", + ), + ( + "align", + _left, + "Which side master plane will be placed (one of ``MonadTall._left`` or " + "``MonadTall._right``)", + ), + ("change_ratio", 0.05, "Resize ratio"), + ( + "new_client_position", + "before_current", + "Place new windows: " + " after_current - after the active window." + " before_current - before the active window," + " top - at the top of the stack," + " bottom - at the bottom of the stack,", + ), + ( + "master_length", + 1, + "Amount of windows displayed in the master stack. Surplus windows will be moved to " + "the slave stack.", + ), + ( + "orientation", + _vert, + "Orientation in which master windows will be " + "placed (one of ``MonadTall._vert`` or ``MonadTall._hori``)", + ), + ("maximized", False, "Start maximized"), + ] + + def __init__(self, **config): + _SimpleLayoutBase.__init__(self, **config) + self.add_defaults(MonadTall.defaults) + if self.single_border_width is None: + self.single_border_width = self.border_width + if self.single_margin is None: + self.single_margin = self.margin + self.screen_rect = None + + @property + def focused(self): + return self.clients.current_index + + @property + def master_windows(self): + return self.clients[: self.master_length] + + @property + def slave_windows(self): + return self.clients[self.master_length :] + + def clone(self, group): + "Clone layout for other groups" + c = _SimpleLayoutBase.clone(self, group) + c.screen_rect = group.screen.get_rect() if group.screen else None + c.ratio = self.ratio + c.align = self.align + c.orientation = self.orientation + return c + + def add(self, client): + "Add client to layout" + self.clients.add(client, client_position=self.new_client_position) + self.do_normalize = True + + def remove(self, client): + "Remove client from layout" + self.do_normalize = True + return self.clients.remove(client) + + def cmd_normalize(self, redraw=True): + "Evenly distribute screen-space between master and slave pane" + if redraw: + self.ratio = self._med_ratio + self.group.layout_all() + self.do_normalize = False + + def cmd_reset(self, redraw=True): + "Reset Layout." + self.ratio = self._med_ratio + if self.align == self._right: + self.align = self._left + if self.orientation == self._hori: + self.orientation = self._vert + self.master_length = 1 + self.cmd_normalize(redraw) + + def cmd_maximize(self): + "Grow the currently focused client to the max size" + if self.maximized: + self.maximized = False + else: + self.maximized = True + self.group.layout_all() + + def configure(self, client, screen_rect): + "Position client based on order and sizes" + self.screen_rect = screen_rect + + if self.do_normalize: + self.cmd_normalize(False) + + # if client not in this layout + if not self.clients or client not in self.clients: + client.hide() + return + + # determine focus border-color + if client.has_focus: + px = self.border_focus + else: + px = self.border_normal + + # single client - fullscreen + if len(self.clients) == 1 or self.maximized: + if self.clients and client is self.clients.current_client: + client.place( + self.screen_rect.x, + self.screen_rect.y, + self.screen_rect.width - 2 * self.single_border_width, + self.screen_rect.height - 2 * self.single_border_width, + self.single_border_width, + px, + margin=self.single_margin, + ) + client.unhide() + else: + client.hide() + return + + cidx = self.clients.index(client) + self._configure_specific(client, screen_rect, px, cidx) + client.unhide() + + def _configure_specific(self, client, screen_rect, px, cidx): + """Specific configuration for xmonad tall.""" + self.screen_rect = screen_rect + + # calculate master/slave pane size + width_master = int(self.screen_rect.width * self.ratio) + width_slave = self.screen_rect.width - width_master + + if len(self.master_windows) == 0: + width_master = 0 + width_slave = self.screen_rect.width + if len(self.slave_windows) == 0: + width_master = self.screen_rect.width + width_slave = 0 + + # calculate client's x offset + if self.align == self._left: # left or up orientation + if client in self.master_windows: + # master client + xpos = self.screen_rect.x + else: + # slave client + xpos = self.screen_rect.x + width_master + else: # right or down orientation + if client in self.master_windows: + # master client + xpos = self.screen_rect.x + width_slave - self.margin + else: + # slave client + xpos = self.screen_rect.x + + # calculate client height and place + if client in self.slave_windows: + pos = self.clients.index(client) + # slave client + width = width_slave - 2 * self.border_width + # ypos is the sum of all clients above it + height = self.screen_rect.height // len(self.slave_windows) + ypos = self.screen_rect.y + self.clients[self.master_length :].index(client) * height + # fix double margin + if cidx > 1: + ypos -= self.margin + height += self.margin + # place client based on calculated dimensions + client.place( + xpos, + ypos, + width, + height - 2 * self.border_width, + self.border_width, + px, + margin=self.margin, + ) + else: + pos = self.clients.index(client) + if self.orientation == self._vert: + height = self.screen_rect.height // self.master_length + width = width_master + ypos = self.screen_rect.y + pos * height + else: + height = self.screen_rect.height + width = width_master // self.master_length + overflow = width_master % self.master_length + ypos = self.screen_rect.y + if self.align == self._left: + xpos = self.screen_rect.x + pos * width + overflow + else: + xpos = self.screen_rect.x + width_slave + pos * width + + # master client + client.place( + xpos, + ypos, + width, + height, + self.border_width, + px, + margin=[ + self.margin, + 2 * self.border_width, + self.margin + 2 * self.border_width, + self.margin, + ], + ) + + def info(self): + d = _SimpleLayoutBase.info(self) + d.update( + dict( + master=[c.name for c in self.master_windows], + slave=[c.name for c in self.slave_windows], + ) + ) + return d + + def _grow_master(self, amt): + """Will grow the client that is currently in the master pane""" + self.ratio += amt + self.ratio = min(self.max_ratio, self.ratio) + + def cmd_grow_master(self): + """Grow master pane + + Will grow the master pane, reducing the size of clients in the slave + pane. + """ + self._grow_master(self.change_ratio) + self.group.layout_all() + + def cmd_shrink_master(self): + """Shrink master pane + + Will shrink the master pane, increasing the size of clients in the + slave pane. + """ + self._shrink_master(self.change_ratio) + self.group.layout_all() + + def _shrink_master(self, amt): + """Will shrink the client that currently in the master pane""" + self.ratio -= amt + self.ratio = max(self.min_ratio, self.ratio) + + cmd_next = _SimpleLayoutBase.next + cmd_previous = _SimpleLayoutBase.previous + + cmd_up = cmd_previous + cmd_down = cmd_next + + def cmd_shuffle_up(self): + """Shuffle the client up the stack""" + self.clients.shuffle_up() + self.group.layout_all() + self.group.focus(self.clients.current_client) + + def cmd_shuffle_down(self): + """Shuffle the client down the stack""" + self.clients.shuffle_down() + self.group.layout_all() + self.group.focus(self.clients[self.focused]) + + def cmd_flip(self): + """Flip the layout horizontally""" + self.align = self._left if self.align == self._right else self._right + self.group.layout_all() + + def cmd_swap(self, window1, window2): + """Swap two windows""" + self.clients.swap(window1, window2, 1) + self.group.layout_all() + self.group.focus(window1) + + def cmd_swap_master(self): + """Swap current window to master pane""" + win = self.clients.current_client + cidx = self.clients.index(win) + + if cidx < self.master_length - 1: + target = self.clients[cidx + 1] + else: + target = self.clients[0] + + self.cmd_swap(win, target) + + def cmd_decrease_nmaster(self): + """Decrease number of windows in master pane""" + self.master_length -= 1 + if self.master_length <= 0: + self.master_length = 0 + self.group.layout_all() + + def cmd_increase_nmaster(self): + """Increase number of windows in master pane""" + self.master_length += 1 + if self.master_length >= len(self.clients): + self.master_length = len(self.clients) + self.group.layout_all() + + def cmd_master(self): + """Focus windows in master pane""" + win = self.clients.current_client + cidx = self.clients.index(win) + if cidx < self.master_length - 1: + self.group.focus(self.clients[cidx + 1]) + else: + self.group.focus(self.clients[0]) + + def cmd_flip_master(self): + """Flip the layout horizontally""" + self.orientation = self._vert if self.orientation == self._hori else self._hori + self.group.layout_all() + + +class MonadWide(MonadTall): + """Emulate the behavior of XMonad's horizontal tiling scheme. + + This layout attempts to emulate the behavior of XMonad wide + tiling scheme. + + Master-Pane: + + A master pane that contains a single window takes up a horizontal + portion of the screen_rect based on the ratio setting. This ratio can be + adjusted with the ``cmd_grow_master`` and ``cmd_shrink_master`` or. + + :: + + --------------------- + | | + | | + | | + |___________________| + | | + | | + --------------------- + + Using the ``cmd_flip`` method will switch which vertical side the + master pane will occupy. The master pane is considered the "top" of + the stack. + + :: + + --------------------- + | | + |___________________| + | | + | | + | | + | | + --------------------- + + Slave-panes: + + Occupying the rest of the screen_rect are one or more slave panes. + The slave panes will share the horizontal space of the screen_rect. + + :: + + --------------------- + | | + | | + | | + |___________________| + | | | | + | | | | + --------------------- + + Panes can be moved with the ``cmd_shuffle_up`` and ``cmd_shuffle_down`` + methods. As mentioned the master pane is considered the top of the + stack; moving up is counter-clockwise and moving down is clockwise. + + The opposite is true if the layout is "flipped". + + :: + + --------------------- --------------------- + | | | 2 | 3 | 4 | + | 1 | |_____|_______|_____| + | | | | + |___________________| | | + | | | | | 1 | + | 2 | 3 | 4 | | | + --------------------- --------------------- + + Normalizing/Resetting: + + To restore master slave ratio use the ``cmd_normalize`` method. + + To reset the layout to its default state, including the master + windows, master slave ratio and flip, use the ``cmd_reset`` method. + + Maximizing: + + To toggle a maximized layout, showing a single window, simply use the + ``cmd_maximize`` on a focused client. + + Suggested Bindings:: + # these mirror the xmonad default config + + Key([modkey], "j", lazy.layout.down()), + Key([modkey], "k", lazy.layout.up()), + Key([modkey, "shift"], "j", lazy.layout.shuffle_down()), + Key([modkey, "shift"], "k", lazy.layout.shuffle_up()), + Key([modkey], "h", lazy.layout.shink_master()), + Key([modkey], "l", lazy.layout.grow_master()), + Key([modkey], "n", lazy.layout.normalize()), + Key([modkey], "m", lazy.layout.master()), + Key([modkey], "comma", lazy.layout.decrease_nmaster()), + Key([modkey], "period", lazy.layout.increase_nmaster()), + Key([modkey], "Return", lazy.layout.swap_master()) + + # keybindings not in xmonad default config + + Key([modkey, "shift"], "n", lazy.layout.reset()), + Key([modkey], "space", lazy.layout.flip()), + Key([modkey, "shift"], "space", lazy.layout.flip_master()), + Key([modkey, "shift"], "m", lazy.layout.maximize()), + """ + + _up = 0 + _down = 1 + _hori = 0 + _vert = 1 + + def _configure_specific(self, client, screen_rect, px, cidx): + """Specific configuration for xmonad wide.""" + self.screen_rect = screen_rect + + # calculate master/slave column widths + height_master = int(self.screen_rect.height * self.ratio) + height_slave = self.screen_rect.height - height_master + + if len(self.master_windows) == 0: + height_master = 0 + height_slave = self.screen_rect.height + if len(self.slave_windows) == 0: + height_master = self.screen_rect.height + height_slave = 0 + + # calculate client's x offset + if self.align == self._up: # up orientation + if client in self.master_windows: + # master client + ypos = self.screen_rect.y + else: + # slave client + ypos = self.screen_rect.y + height_master + else: # right or down orientation + if client in self.master_windows: + # master client + ypos = self.screen_rect.y + height_slave - self.margin + else: + # slave client + ypos = self.screen_rect.y + + # calculate client height and place + if client in self.slave_windows: + # slave client + height = height_slave - 2 * self.border_width + # xpos is the sum of all clients left of it + width = self.screen_rect.width // len(self.slave_windows) + xpos = self.screen_rect.x + self.clients[self.master_length :].index(client) * width + # get width from precalculated width list + width = self.screen_rect.width // len(self.slave_windows) + + # fix double margin + if cidx > 1: + xpos -= self.margin + width += self.margin + # place client based on calculated dimensions + client.place( + xpos, + ypos, + width - 2 * self.border_width, + height, + self.border_width, + px, + margin=self.margin, + ) + else: + pos = self.clients.index(client) + if self.orientation == self._hori: + width = self.screen_rect.width // self.master_length + height = height_master + xpos = self.screen_rect.x + pos * width + else: + width = self.screen_rect.width + height = height_master // self.master_length + overflow = height_master % self.master_length + xpos = self.screen_rect.x + if self.align == self._up: + ypos = self.screen_rect.y + pos * height + overflow + else: + ypos = self.screen_rect.y + height_slave + pos * height + + # master client + client.place( + xpos, + ypos, + width, + height, + self.border_width, + px, + margin=[ + self.margin, + self.margin + 2 * self.border_width, + 2 * self.border_width, + self.margin, + ], + ) diff --git a/modules/desktop/qtile/home.nix b/modules/desktop/qtile/home.nix index 62d5a50..48cb1b8 100644 --- a/modules/desktop/qtile/home.nix +++ b/modules/desktop/qtile/home.nix @@ -74,6 +74,7 @@ xdg.configFile."qtile/bluetooth.py".source = ./config/bluetooth.py; xdg.configFile."qtile/mpris2widget.py".source = ./config/mpris2widget.py; xdg.configFile."qtile/tasklist.py".source = ./config/tasklist.py; + xdg.configFile."qtile/xmonad.py".source = ./config/xmonad.py; xdg.configFile."qtile/nixenvironment.py".text = '' from string import Template -- 2.48.1