-rw-r--r-- config.py
1 """ 2 Qtile main config file 3 ====================== 4 """ 5 6 from __future__ import annotations 7 8 import importlib 9 import os 10 import re 11 import subprocess 12 import sys 13 from typing import TYPE_CHECKING 14 15 from libqtile import bar, hook, layout, qtile, utils, widget 16 from libqtile.backend import base 17 from libqtile.config import Click, Drag, Key, Match, Screen 18 from libqtile.lazy import lazy 19 from libqtile.widget.backlight import ChangeDirection 20 from libqtile.widget.battery import Battery, BatteryState 21 22 import qtile_extras.widget as widget_extras 23 24 assert qtile is not None 25 26 if TYPE_CHECKING: 27 from typing import Any 28 29 from libqtile.core.manager import Qtile 30 31 32 # Config imports 33 def reload(module): 34 if module in sys.modules: 35 importlib.reload(sys.modules[module]) 36 37 38 import traverse 39 from toggle_debug import toggle_debug 40 41 use_tags = False 42 43 if use_tags: 44 reload("tags") 45 from tags import groups, keys_group 46 else: 47 reload("groups") 48 from groups import groups, keys_group 49 50 reload("scratchpad") 51 from scratchpad import keys_scratchpad, scratchpad 52 53 reload("power_menu") 54 from power_menu import keys_power_menu 55 56 IS_WAYLAND: bool = qtile.core.name == "wayland" 57 IS_XEPHYR: bool = int(os.environ.get("QTILE_XEPHYR", 0)) > 0 58 59 reload(qtile.core.name) 60 if IS_WAYLAND: 61 from wayland import keys_backend, term, wl_input_rules, wl_shadows # noqa: F401 62 else: 63 from x11 import keys_backend, term, wmname # noqa: F401 64 65 # Colours 66 theme = dict( 67 foreground="#CFCCD6", 68 background="#0C1F20", 69 color0="#030405", 70 color8="#1f1c32", 71 color1="#8742a5", 72 color9="#9a5eb3", 73 color2="#406794", 74 color10="#5fd75f", 75 color3="#653c21", 76 color11="#6e9fcd", 77 color4="#8f4ff0", 78 color12="#BBC2E2", 79 color5="#5d479d", 80 color13="#998dd1", 81 color6="#3e3e73", 82 color14="#9a9dcc", 83 color7="#495068", 84 color15="#e1e1e4", 85 ) 86 87 colours = [theme[f"color{n}"] for n in range(16)] 88 background = theme["background"] 89 foreground = theme["foreground"] 90 colour_focussed = "#6fa3e0" 91 colour_unfocussed = "#0e101c" 92 inner_gaps = 8 93 outer_gaps = 0 94 95 # https://coolors.co/1b2021-cfccd6-bbc2e2-b7b5e4-847979 96 97 98 # Keys 99 100 if IS_XEPHYR: 101 mod = "mod1" 102 alt = "control" 103 else: 104 mod = "mod4" 105 alt = "mod1" 106 107 # My keybindings - they are converted at the bottom of this file 108 my_keys: list[tuple[list[str], str, Any, str]] = [] 109 my_keys.extend(keys_backend) 110 my_keys.extend(keys_group) 111 my_keys.extend(keys_scratchpad) 112 my_keys.extend(keys_power_menu) 113 114 115 def float_to_front(qtile: Qtile) -> None: 116 """Bring all floating windows of the group to front""" 117 for window in qtile.current_group.windows: 118 if window.floating: 119 window.bring_to_front() 120 121 122 my_keys.extend( 123 [ 124 # Window management 125 ([mod, "control"], "q", lazy.window.kill(), "Close window"), 126 ([mod], "f", lazy.window.toggle_fullscreen(), "Toggle fullscreen"), 127 ([mod, "shift"], "space", lazy.window.toggle_floating(), "Toggle floating"), 128 ([mod], "s", lazy.spawn("ddg_rofi"), "Search web via rofi"), 129 ([mod, "shift"], "s", lazy.window.static(), "Make window static"), 130 ( 131 [mod], 132 "space", 133 lazy.function(float_to_front), 134 "Move floating windows to the front", 135 ), 136 ([mod], "j", lazy.function(traverse.down), "Traverse down"), 137 ([mod], "k", lazy.function(traverse.up), "Traverse up"), 138 ([mod], "h", lazy.function(traverse.left), "Traverse left"), 139 ([mod], "l", lazy.function(traverse.right), "Traverse right"), 140 ([mod, "shift"], "j", lazy.layout.shuffle_down(), "Shuffle down"), 141 ([mod, "shift"], "k", lazy.layout.shuffle_up(), "Shuffle up"), 142 ([mod, "shift"], "h", lazy.layout.shuffle_left(), "Shuffle left"), 143 ([mod, "shift"], "l", lazy.layout.shuffle_right(), "Shuffle right"), 144 ([mod, alt], "j", lazy.layout.grow_down(), "Grow down"), 145 ([mod, alt], "k", lazy.layout.grow_up(), "Grow up"), 146 ([mod, alt], "h", lazy.layout.grow_left(), "Grow left"), 147 ([mod, alt], "l", lazy.layout.grow_right(), "Grow right"), 148 ([mod, "control"], "r", lazy.reload_config(), "Reload the config"), 149 ([mod, "control"], "Escape", lazy.shutdown(), "Shutdown Qtile"), 150 ([mod], "Tab", lazy.next_layout(), "Next layout"), 151 ([mod], "b", lazy.hide_show_bar("bottom"), "Hide bar"), 152 ([mod, "control"], "d", lazy.function(toggle_debug), "Toggle debug logging"), 153 # Volume control - MyVolume widget is defined further down 154 ([], "XF86AudioMute", lazy.widget["myvolume"].mute(), "Mute audio"), 155 ([mod], "F10", lazy.widget["myvolume"].mute(), "Mute audio"), 156 ( 157 [], 158 "XF86AudioRaiseVolume", 159 lazy.widget["myvolume"].increase_vol(), 160 "Increase volume", 161 ), 162 ([mod], "F12", lazy.widget["myvolume"].increase_vol(), "Increase volume"), 163 ( 164 [], 165 "XF86AudioLowerVolume", 166 lazy.widget["myvolume"].decrease_vol(), 167 "Decrease volume", 168 ), 169 ([mod], "F11", lazy.widget["myvolume"].decrease_vol(), "Decrease volume"), 170 # Backlight control 171 ( 172 [], 173 "XF86MonBrightnessUp", 174 lazy.widget["backlight"].change_backlight(ChangeDirection.UP, 3), 175 "Increase backlight", 176 ), 177 ( 178 [], 179 "XF86MonBrightnessDown", 180 lazy.widget["backlight"].change_backlight(ChangeDirection.DOWN, 3), 181 "Decrease backlight", 182 ), 183 ( 184 [mod], 185 "F6", 186 lazy.widget["backlight"].change_backlight(ChangeDirection.UP, 3), 187 "Increase backlight", 188 ), 189 ( 190 [mod], 191 "F5", 192 lazy.widget["backlight"].change_backlight(ChangeDirection.DOWN, 3), 193 "Decrease backlight", 194 ), 195 # Music control 196 ( 197 [mod], 198 "bracketright", 199 lazy.spawn("playerctl next"), 200 "Next song", 201 ), 202 ([], "XF86AudioNext", lazy.spawn("playerctl next"), "Next song"), 203 ( 204 [mod], 205 "bracketleft", 206 lazy.spawn("playerctl previous"), 207 "Previous song", 208 ), 209 ([], "XF86AudioPrev", lazy.spawn("playerctl previous"), "Previous song"), 210 ([], "Pause", lazy.spawn("playerctl play-pause"), "Play/unpause music"), 211 ([], "XF86AudioPlay", lazy.spawn("playerctl play-pause"), "Play/unpause music"), 212 # Launchers 213 ( 214 [mod], 215 "d", 216 lazy.spawn("rofi -show run -theme ~/.config/rofi/common.rasi"), 217 "Spawn with rofi", 218 ), 219 ([mod], "Return", lazy.spawn(term), "Spawn terminal"), 220 ([mod, "shift"], "f", lazy.spawn("firefox"), "Spawn Firefox"), 221 ( 222 [mod, "control"], 223 "f", 224 lazy.spawn("cleanfox"), 225 "Spawn Firefox in clean profile", 226 ), 227 ([], "Print", lazy.spawn("screenshot copy"), "Screenshot to clipboard"), 228 (["shift"], "Print", lazy.spawn("screenshot"), "Screenshot to file"), 229 ([mod], "p", lazy.spawn("get_password_rofi"), "Keepass passwords"), 230 ([mod], "i", lazy.spawn("systemctl suspend -i"), "Suspend system"), 231 ( 232 [mod, "shift"], 233 "i", 234 lazy.spawn("gtklock & systemctl suspend -i", shell=True), 235 "Suspend and lock", 236 ), 237 ( 238 ["control"], 239 "space", 240 lazy.spawn("swaync-client -t -sw"), 241 "Toggle notification panel", 242 ), 243 ( 244 [mod, "control"], 245 "y", 246 lazy.spawn("yt-first"), 247 "yt-first script", 248 ), 249 ] 250 ) 251 252 253 # Mouse control 254 mouse = [ 255 Click([mod], "Button2", lazy.window.kill()), 256 257 # Move with drag or swipe 258 Drag( 259 [mod], 260 "Button1", 261 lazy.window.set_position_floating(), 262 start=lazy.window.get_position(), 263 ), 264 #Swipe( 265 # [mod], 266 # 3, 267 # lazy.window.set_position_floating(), 268 # start=lazy.window.get_position(), 269 #), 270 271 # Resize with drag or swipe 272 Drag( 273 [mod, alt], 274 "Button1", 275 lazy.window.set_size_floating(), 276 start=lazy.window.get_size(), 277 ), 278 #Swipe( 279 # [mod, alt], 280 # 3, 281 # lazy.window.set_size_floating(), 282 # start=lazy.window.get_size(), 283 #), 284 285 # Slide between groups 286 Drag( 287 [mod, "shift"], 288 "Button1", 289 lazy.screen.group_slide(), 290 start=lazy.screen.start_group_slide(scale=2.5), 291 ), 292 ] 293 294 if IS_WAYLAND: 295 try: 296 from libqtile.config import Swipe 297 except ImportError: 298 pass 299 else: 300 301 def _slide(qtile): 302 groups = qtile.groups 303 if len(qtile.screens) > 1: 304 if qtile.current_screen.index == 0: 305 groups = groups[:4] 306 else: 307 groups = groups[4:] 308 return qtile.current_screen.start_group_slide( 309 groups=groups, scale=1.6, inertia_threshold=15, 310 ) 311 312 mouse.append( 313 Swipe( 314 [], 315 3 if IS_XEPHYR else 4, 316 lazy.screen.group_slide(), 317 start=lazy.function(_slide), 318 ), 319 ) 320 321 322 ## Firefox 3-finger vertical swipe to zoom 323 #def swipe_zoom(qtile, _dx: int, dy: int) -> None: 324 # if dy % 3: 325 # # Only send key every 10 steps 326 # return 327 # if dy < 0: 328 # qtile.spawn("wlrctl keyboard type +", shell=True) 329 # else: 330 # qtile.spawn("wlrctl keyboard type -", shell=True) 331 # 332 # 333 #mouse.append( 334 # Swipe( 335 # [], 336 # 3, 337 # lazy.function(swipe_zoom).when(focused=Match(wm_class="firefox")), 338 # ) 339 #) 340 341 # Layouts 342 border_focus = [colours[5], "323974"] 343 border_normal = "001122" 344 345 346 class MyColumns(layout.Columns): 347 """ 348 I only override this method so I can make 'foot' have wide margins at each side if 349 it's the only window open. 350 """ 351 352 def configure(self, client, screen_rect): 353 pos = 0 354 for col in self.columns: 355 if client in col: 356 break 357 pos += col.width 358 else: 359 client.hide() 360 return 361 if client.has_focus: 362 color = self.border_focus if col.split else self.border_focus_stack 363 else: 364 color = self.border_normal if col.split else self.border_normal_stack 365 border = self.border_width 366 margin_size = self.margin 367 if len(self.columns) == 1 and (len(col) == 1 or not col.split): 368 if not self.border_on_single: 369 border = 0 370 if "foot" in (client.get_wm_class() or []): 371 margin_size = [0, 200, 0, 200] 372 width = int(0.5 + col.width * screen_rect.width * 0.01 / len(self.columns)) 373 x = screen_rect.x + int( 374 0.5 + pos * screen_rect.width * 0.01 / len(self.columns) 375 ) 376 if col.split: 377 pos = 0 378 for c in col: 379 if client == c: 380 break 381 pos += col.heights[c] 382 height = int( 383 0.5 + col.heights[client] * screen_rect.height * 0.01 / len(col) 384 ) 385 y = screen_rect.y + int(0.5 + pos * screen_rect.height * 0.01 / len(col)) 386 client.place( 387 x, 388 y, 389 width - 2 * border, 390 height - 2 * border, 391 border, 392 color, 393 margin=margin_size, 394 ) 395 client.unhide() 396 elif client == col.cw: 397 client.place( 398 x, 399 screen_rect.y, 400 width - 2 * border, 401 screen_rect.height - 2 * border, 402 border, 403 color, 404 margin=margin_size, 405 ) 406 client.unhide() 407 else: 408 client.hide() 409 410 411 # Weird custom behaviour here but isn't that what qtile is for? 412 # I want a Columns layout but also sometimes i want 'foot' windows to be thinner and 413 # centred in a 'central' column, so I define a whole new MyColumns layout that does that 414 # and I just jump between the two when I want that. 415 col_opts = dict( 416 insert_position=1, 417 border_width=5, 418 border_focus=border_focus, 419 border_normal="00000000", 420 border_on_single=False, 421 wrap_focus_columns=False, 422 wrap_focus_rows=False, 423 margin=0, 424 # margin_on_single=30, # 4 425 ) 426 427 layouts = [ 428 MyColumns(**col_opts), 429 layout.Columns(**col_opts), 430 ] 431 432 floating_layout = layout.Floating( 433 border_width=3, 434 border_focus=border_focus, 435 border_normal="00000000", 436 fullscreen_border_width=0, 437 float_rules=[ 438 Match(func=base.Window.has_fixed_size), 439 Match(func=base.Window.has_fixed_ratio), 440 Match(func=lambda c: bool(c.is_transient_for())), 441 Match(role="gimp-file-export"), 442 Match(title="Bluetooth Devices"), 443 Match(title="File Operation Progress", wm_class=re.compile("[Tt]hunar")), 444 Match(title="Firefox — Sharing Indicator"), 445 Match(title="KDE Connect Daemon"), 446 Match(title="Open File"), 447 Match(title="Unlock Database - KeePassXC"), 448 Match(title="KeePassXC - Access Request"), 449 Match(title=re.compile("Presenting: .*"), wm_class="libreoffice-impress"), 450 Match(wm_class="Arandr"), 451 Match(wm_class="Dragon"), 452 Match(wm_class="Dragon-drag-and-drop"), 453 Match(wm_class="Pinentry-gtk-2"), 454 Match(wm_class="Xephyr"), 455 Match(wm_class="confirm"), 456 Match(wm_class="dialog"), 457 Match(wm_class="download"), 458 Match(wm_class="eog"), 459 Match(wm_class="error"), 460 Match(wm_class="file_progress"), 461 Match(wm_class="imv"), 462 Match(wm_class="io.github.celluloid_player.Celluloid"), 463 Match(wm_class="lxappearance"), 464 Match(wm_class="matplotlib"), 465 #Match(wm_class="mpv"), 466 Match(wm_class="nm-connection-editor"), 467 Match(wm_class="notification"), 468 Match(wm_class="org.gnome.clocks"), 469 Match(wm_class="org.kde.ark"), 470 Match(wm_class="pavucontrol"), 471 Match(wm_class="qt5ct"), 472 Match(wm_class="ssh-askpass"), 473 Match(wm_class="thunar"), 474 Match(wm_class="toolbar"), 475 Match(wm_class="tridactyl"), 476 Match(wm_class="wdisplays"), 477 Match(wm_class="wlroots"), 478 Match(wm_class="zoom"), 479 Match(wm_type="dialog"), 480 ], 481 ) 482 483 484 # Screens and Bars 485 widget_defaults = { 486 "padding": 10, 487 "foreground": colours[6], 488 "font": "Font Awesome 5 Free", 489 "fontsize": 16, 490 } 491 492 icon_font_size = 22 493 494 groupbox_config = { 495 "active": foreground, 496 "highlight_method": "block", 497 "this_current_screen_border": colour_focussed, 498 "other_current_screen_border": colours[5], 499 "highlight_color": [background, colours[5]], 500 "disable_drag": True, 501 "padding": 4, 502 "font": "TamzenForPowerline Bold", 503 "fontsize": 12, 504 } 505 506 # mpd2 = widget.Mpd2( 507 # no_connection="", 508 # status_format="{artist} - {title}", 509 # status_format_stopped="", 510 # foreground=colours[12], 511 # idle_format="", 512 # font="TamzenForPowerline Bold", 513 # update_interval=10, 514 # scroll=True, 515 # ) 516 517 mpd2 = widget.Mpris2( 518 name="mpris", 519 width=1000, 520 objname=None, 521 format="{xesam:title} - {xesam:artist}", 522 font="TamzenForPowerline Bold", 523 ) 524 525 526 class MyVolume(widget.Volume): 527 def _configure(self, qtile, bar): 528 widget.Volume._configure(self, qtile, bar) 529 self.volume = self.get_volume() 530 if self.volume <= 0: 531 self.text = "" 532 elif self.volume <= 15: 533 self.text = "" 534 elif self.volume < 50: 535 self.text = "" 536 else: 537 self.text = "" 538 539 def _update_drawer(self): 540 if self.volume <= 0: 541 self.text = "" 542 elif self.volume <= 15: 543 self.text = "" 544 elif self.volume < 50: 545 self.text = "" 546 else: 547 self.text = "" 548 self.draw() 549 550 def increase_vol(self): 551 subprocess.run("amixer -c PCH set PCM 3%+".split(), capture_output=True) 552 self.volume = self.get_volume() 553 554 def decrease_vol(self): 555 subprocess.run("amixer -c PCH set PCM 3%-".split(), capture_output=True) 556 self.volume = self.get_volume() 557 558 def mute(self): 559 subprocess.run("amixer -c PCH set PCM toggle".split(), capture_output=True) 560 self.volume = self.get_volume() 561 562 563 bklight = widget.Backlight( 564 backlight_name=os.listdir("/sys/class/backlight")[-1], 565 step=1, 566 update_interval=None, 567 format="", 568 fontsize=icon_font_size, 569 change_command=None, 570 ) 571 572 volume = MyVolume( 573 fontsize=icon_font_size, 574 channel="PCM", 575 font="Font Awesome 5 Free", 576 update_interval=60, 577 cardid="PCH", 578 device=None, 579 ) 580 581 if IS_WAYLAND: 582 systray = widget_extras.StatusNotifier(padding=20) 583 else: 584 systray = widget.Systray(padding=20, icon_size=24) 585 586 587 class MyBattery(Battery): 588 """ 589 This is basically the Battery widget except it uses some icons, and if you click it 590 it will show the percentage numerically for 1 second. 591 """ 592 593 def build_string(self, status): 594 if self.layout is not None: 595 self.layout.colour = self.foreground 596 if ( 597 status.state == BatteryState.DISCHARGING 598 and status.percent < self.low_percentage 599 ): 600 self.background = self.low_background 601 else: 602 self.background = self.normal_background 603 if status.state == BatteryState.DISCHARGING: 604 if status.percent > 0.75: 605 char = "" 606 elif status.percent > 0.45: 607 char = "" 608 elif status.percent > 0.25: 609 char = "" 610 else: 611 char = "" 612 elif status.percent >= 1 or status.state == BatteryState.FULL: 613 char = "" 614 elif status.state == BatteryState.EMPTY or ( 615 status.state == BatteryState.UNKNOWN and status.percent == 0 616 ): 617 char = "" 618 else: 619 char = "" 620 return self.format.format(char=char, percent=status.percent) 621 622 def restore(self): 623 self.format = "{char}" 624 self.font = "Font Awesome 5 Free" 625 self.timer_setup() 626 627 def button_press(self, x, y, button): 628 self.format = "{percent:2.0%}" 629 self.font = "TamzenForPowerline Bold" 630 self.timer_setup() 631 self.timeout_add(1, self.restore) 632 633 634 battery = MyBattery( 635 format="{char}", 636 low_background=colours[1], 637 show_short_text=False, 638 low_percentage=0.12, 639 notify_below=12, 640 fontsize=icon_font_size + 10, 641 ) 642 643 date = widget.Clock( 644 format="%e/%m/%g", 645 fontsize=16, 646 font="TamzenForPowerline Bold", 647 update_interval=60, 648 name="date", 649 ) 650 651 time = widget.Clock( 652 fontsize=20, 653 font="TamzenForPowerline Medium", 654 update_interval=60, 655 name="time", 656 ) 657 658 groupboxes = [ 659 widget.GroupBox(**groupbox_config), 660 widget.GroupBox(**groupbox_config, visible_groups=["q", "w", "e", "r"]), 661 ] 662 663 664 @hook.subscribe.startup 665 def _(): 666 # Set up initial GroupBox visible groups 667 if len(qtile.screens) > 1: 668 groupboxes[0].visible_groups = ["1", "2", "3", "4"] 669 else: 670 groupboxes[0].visible_groups = None 671 672 673 @hook.subscribe.screens_reconfigured 674 def _(): 675 # Reconfigure GroupBox visible groups 676 if len(qtile.screens) > 1: 677 groupboxes[0].visible_groups = ["1", "2", "3", "4"] 678 else: 679 groupboxes[0].visible_groups = None 680 if hasattr(groupboxes[0], "bar"): 681 groupboxes[0].bar.draw() 682 683 684 #bar_border_width = [0, 3, 0, 3] 685 #bar_border_color = ["000000", colours[5], "000000", colours[5]] 686 687 screens = [ 688 Screen( 689 top=bar.Bar( 690 [ 691 groupboxes[0], 692 widget.Spacer(name="s1"), 693 mpd2, 694 widget.Spacer(name="s2"), 695 systray, 696 volume, 697 bklight, 698 battery, 699 date, 700 time, 701 ], 702 28, 703 background=background, 704 ), 705 bottom=bar.Gap(outer_gaps), 706 left=bar.Gap(outer_gaps), 707 right=bar.Gap(outer_gaps), 708 #wallpaper="~/pictures/Wallpapers/fractal.bmp", 709 #wallpaper_mode="fill", 710 ), 711 Screen( 712 top=bar.Bar( 713 [ 714 groupboxes[1], 715 widget.Spacer(name="s3"), 716 mpd2, 717 widget.Spacer(name="s4"), 718 volume, 719 bklight, 720 battery, 721 date, 722 time, 723 ], 724 28, 725 background=background, 726 ), 727 #wallpaper="~/pictures/Wallpapers/fractal.bmp", 728 #wallpaper_mode="fill", 729 bottom=bar.Gap(outer_gaps), 730 left=bar.Gap(outer_gaps), 731 right=bar.Gap(outer_gaps), 732 ), 733 ] 734 735 736 @hook.subscribe.client_focus 737 def _(_): 738 # Keep Static windows on top 739 for window in qtile.windows_map.values(): 740 if isinstance(window, base.Static): 741 window.bring_to_front() 742 743 744 reconfigure_screens = True 745 follow_mouse_focus = True 746 bring_front_click = True 747 cursor_warp = False 748 auto_fullscreen = True 749 focus_on_window_activation = "focus" 750 751 keys = [Key(mods, key, cmd, desc=desc) for mods, key, cmd, desc in my_keys] 752 groups.append(scratchpad) 753