diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..36b13f1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,176 @@
+# ---> Python
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..e7d3a0e
--- /dev/null
+++ b/config.py
@@ -0,0 +1,48 @@
+# ___ _____ ___ _ _____ ____ __ _
+# / _ \_ _|_ _| | | ____| / ___|___ _ __ / _(_) __ _
+# | | | || | | || | | _| | | / _ \| '_ \| |_| |/ _` |
+# | |_| || | | || |___| |___ | |__| (_) | | | | _| | (_| |
+# \__\_\|_| |___|_____|_____| \____\___/|_| |_|_| |_|\__, |
+# |___/
+# by cerberus
+# --------------------------------------------------------------------
+
+# --------------------------------------------------------------------
+# Imports
+# --------------------------------------------------------------------
+from modules.screens import screens, widget_defaults
+from modules.keys import keys, mouse
+from modules.layouts import layouts, floating_layout
+from modules.groups import groups
+from modules.hooks import *
+
+# --------------------------------------------------------
+# General Setup
+# --------------------------------------------------------
+
+dgroups_key_binder = None
+dgroups_app_rules = [] # type: list
+follow_mouse_focus = True
+bring_front_click = True
+floats_kept_above = True
+cursor_warp = True
+auto_fullscreen = True
+focus_on_window_activation = "smart"
+reconfigure_screens = True
+auto_minimize = True
+
+# xcursor theme (string or None) and size (integer) for Wayland backend
+wl_xcursor_theme = None
+wl_xcursor_size = 18
+
+wmname = "Qtile"
+
+# XXX: Gasp! We're lying here. In fact, nobody really uses or cares about this
+# string besides java UI toolkits; you can see several discussions on the
+# mailing lists, GitHub issues, and other WM documentation that suggest setting
+# this string if your java app doesn't work correctly. We may as well just lie
+# and say that we're a working one by default.
+#
+# We choose LG3D to maximize irony: it is a 3D non-reparenting WM written in
+# java that happens to be on java's whitelist.
+# wmname = "LG3D"
\ No newline at end of file
diff --git a/modules/groups.py b/modules/groups.py
new file mode 100644
index 0000000..0ca8a0f
--- /dev/null
+++ b/modules/groups.py
@@ -0,0 +1,84 @@
+import re
+from libqtile.config import Group, Key, Match, DropDown, ScratchPad
+from libqtile.lazy import lazy
+from modules.keys import keys, mod
+
+
+groups = [
+ Group(name="1", label=""),
+ Group(name="2", label=""),
+ Group(name="3", label=""),
+ Group(name="4", label=""),
+ Group(name="5", label=""),
+ Group(name="6", label=""),
+ Group(name="7", label=""),
+ Group(name="8", label="", matches=[Match(wm_class=re.compile(r"^(firefox)$"))]),
+ Group(name="9", label="", matches=[Match(wm_class="AFFiNE")]),
+]
+
+for i in groups:
+ keys.extend(
+ [
+ Key(
+ [mod],
+ i.name,
+ lazy.group[i.name].toscreen(toggle=True),
+ desc=f"Switch to group {i.name}",
+ ),
+ Key(
+ [mod, "shift"],
+ i.name,
+ lazy.window.togroup(i.name, switch_group=True),
+ desc=f"Switch to & move focused window to group {i.name}",
+ ),
+ ]
+ )
+
+groups.append(
+ ScratchPad(
+ "scratchpad",
+ [
+ DropDown(
+ "term",
+ "alacritty",
+ width=0.4,
+ height=0.5,
+ x=0.3,
+ y=0.25,
+ opacity=1,
+ on_focus_lost_hide=True,
+ ),
+ DropDown(
+ "discord",
+ "discord",
+ match=Match(title=re.compile(r".*Discord$")),
+ opacity=1,
+ x=0.1,
+ y=0.05,
+ width=0.8,
+ height=0.9,
+ on_focus_lost_hide=True,
+ ),
+ DropDown(
+ "files",
+ "nemo",
+ width=0.6,
+ height=0.6,
+ x=0.2,
+ y=0.2,
+ opacity=1,
+ on_focus_lost_hide=False,
+ ),
+ DropDown(
+ "calc",
+ "qalculate-gtk",
+ width=0.3,
+ height=0.6,
+ x=0.35,
+ y=0.2,
+ opacity=1,
+ on_focus_lost_hide=False,
+ ),
+ ],
+ )
+)
diff --git a/modules/hooks.py b/modules/hooks.py
new file mode 100644
index 0000000..8bb2474
--- /dev/null
+++ b/modules/hooks.py
@@ -0,0 +1,25 @@
+# _ _ _ _ _
+# __ _| |_(_) | ___ | |__ ___ ___ | | _____
+# / _` | __| | |/ _ \ | '_ \ / _ \ / _ \| |/ / __|
+# | (_| | |_| | | __/ | | | | (_) | (_) | <\__ \
+# \__, |\__|_|_|\___| |_| |_|\___/ \___/|_|\_\___/
+# |_|
+# by cerberus
+# --------------------------------------------------------------------
+
+# --------------------------------------------------------------------
+# Imports
+# --------------------------------------------------------------------
+from libqtile import hook, qtile
+import subprocess
+import os.path
+
+# --------------------------------------------------------------------
+# HOOK startup
+# --------------------------------------------------------------------
+
+@hook.subscribe.startup_once
+def autostart():
+ autostartscript = "~/.config/qtile/res/scripts/autostart.sh"
+ home = os.path.expanduser(autostartscript)
+ subprocess.Popen([home])
diff --git a/modules/keys.py b/modules/keys.py
new file mode 100644
index 0000000..a469bf9
--- /dev/null
+++ b/modules/keys.py
@@ -0,0 +1,196 @@
+# _ _ _ _
+# __ _| |_(_) | ___ | | _____ _ _ ___
+# / _` | __| | |/ _ \ | |/ / _ \ | | / __|
+# | (_| | |_| | | __/ | < __/ |_| \__ \
+# \__, |\__|_|_|\___| |_|\_\___|\__, |___/
+# |_| |___/
+# by cerberus
+# --------------------------------------------------------------------
+
+# --------------------------------------------------------------------
+# Imports
+# --------------------------------------------------------------------
+
+from libqtile import qtile
+from libqtile.config import Key, Drag, Click
+from libqtile.lazy import lazy
+from modules.screens import notifier
+
+# --------------------------------------------------------------------
+# Set default apps
+# --------------------------------------------------------------------
+
+terminal = "kitty"
+browser = "firefox"
+filemanager = "nemo"
+
+# --------------------------------------------------------------------
+# Keybindings
+# --------------------------------------------------------------------
+
+mod = "mod4" # SUPER KEY
+alt = "mod1"
+# KeybindStart
+keys = [
+ # KB_GROUP-Focus Window
+ Key([mod], "h", lazy.layout.left(), desc="Move focus to left"),
+ Key([mod], "l", lazy.layout.right(), desc="Move focus to right"),
+ Key([mod], "j", lazy.layout.down(), desc="Move focus down"),
+ Key([mod], "k", lazy.layout.up(), desc="Move focus up"),
+ # KB_GROUP-Move Window
+ # Key([mod, "shift"], "h", lazy.layout.shuffle_left(), desc="Move window to the left"),
+ # Key([mod, "shift"], "l", lazy.layout.shuffle_right(), desc="Move window to the right"),
+ Key(
+ [mod, "shift"],
+ "h",
+ lazy.layout.swap_left(),
+ desc="Move window to the left - Monad",
+ ),
+ Key(
+ [mod, "shift"],
+ "l",
+ lazy.layout.swap_right(),
+ desc="Move window to the left - Monad",
+ ),
+ Key([mod, "shift"], "j", lazy.layout.shuffle_down(), desc="Move window down"),
+ Key([mod, "shift"], "k", lazy.layout.shuffle_up(), desc="Move window up"),
+ # KB_GROUP-Resize Window
+ # Key([mod, "control"], "h", lazy.layout.grow_left(), desc="Grow window to the left"),
+ # Key([mod, "control"], "l", lazy.layout.grow_right(), desc="Grow window to the right"),
+ Key([mod], "m", lazy.layout.shrink(), desc="Grow window to the top"),
+ Key([mod], "i", lazy.layout.grow(), desc="Grow window to the bottom"),
+ # KB_GROUP-Window Controls
+ Key([mod], "n", lazy.layout.normalize(), desc="Normalize all window sizes"),
+ Key([mod, "shift"], "n", lazy.layout.reset(), desc="Reset all window sizes"),
+ Key([mod], "t", lazy.window.toggle_floating(), desc="Toggle floating"),
+ Key([mod], "o", lazy.layout.maximize(), desc="Maximize window"),
+ Key(
+ [mod, "shift"],
+ "s",
+ lazy.layout.toggle_auto_maximize(),
+ desc="Toggle auto maximize",
+ ),
+ Key([mod], "f", lazy.window.toggle_fullscreen(), desc="Toggle full screen"),
+ Key([mod, "shift"], "space", lazy.layout.flip(), desc="Flip windows"),
+ Key(
+ [mod, "shift"],
+ "Return",
+ lazy.layout.toggle_split(),
+ desc="Toggle between split and unsplit sides of stack",
+ ),
+ # KB_GROUP-System Controls
+ Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"),
+ Key([mod], "c", lazy.window.kill(), desc="Kill focused window"),
+ Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"),
+ Key(
+ [mod, "mod1"],
+ "l",
+ lazy.spawn("betterlockscreen --lock blur"),
+ desc="Lock Computer",
+ ),
+ # KB_GROUP-Audio and Media Control
+ Key([], "XF86AudioMute", lazy.spawn("pamixer -t"), desc="Mute Audio"),
+ Key([], "XF86AudioLowerVolume", lazy.spawn("pamixer -d 2"), desc="Lower Volume"),
+ Key([], "XF86AudioRaiseVolume", lazy.spawn("pamixer -i 2"), desc="Next Song"),
+ Key([], "XF86AudioPrev", lazy.spawn("playerctl previous"), desc="Previous Media"),
+ Key(
+ [], "XF86AudioPlay", lazy.spawn("playerctl play-pause"), desc="Play Pause Media"
+ ),
+ Key([], "XF86AudioNext", lazy.spawn("playerctl next"), desc="Next Media"),
+ # XF86MonBrightnessUp
+ Key(
+ [],
+ "XF86MonBrightnessUp",
+ lazy.widget["brightnesscontrol"].brightness_up(),
+ desc="Increase brightness",
+ ),
+ Key(
+ [],
+ "XF86MonBrightnessDown",
+ lazy.widget["brightnesscontrol"].brightness_down(),
+ desc="Decrease brightness",
+ ),
+ # Key([], "XF86MonBrightnessUp", lazy.spawn("brightness_up")),
+ # Key([], "XF86MonBrightnessDown", lazy.spawncmd(brightness_down)),
+ # KB_GROUP-Rofi Menus
+ Key([mod], "r", lazy.spawn("rofi -show drun -show-icons"), desc="Spawn Rofi D-Run"),
+ Key(
+ [alt], "Tab", lazy.spawn("rofi -show window -show-icons"), desc="Spawn Windows"
+ ),
+ Key([mod], "p", lazy.spawn("rofi -show ssh"), desc="Spawn Rofi SSH-Connections"),
+ # KB_GROUP-ScratchPad
+ Key(
+ ["control"],
+ "1",
+ lazy.group["scratchpad"].dropdown_toggle("term"),
+ desc="PopUp Terminal",
+ ),
+ Key(
+ ["control"],
+ "2",
+ lazy.group["scratchpad"].dropdown_toggle("discord"),
+ desc="Discord",
+ ),
+ Key(
+ ["control"],
+ "3",
+ lazy.group["scratchpad"].dropdown_toggle("files"),
+ desc="PopUp Filemanager",
+ ),
+ Key(
+ ["control"],
+ "4",
+ lazy.group["scratchpad"].dropdown_toggle("calc"),
+ desc="Calculator",
+ ),
+ # Key([], "XF86Calculator", lazy.group['scratchpad'].dropdown_toggle('calc'), desc="Open Calculator in scratchpad"),
+ # KB_GROUP-Programs
+ Key([mod], "Return", lazy.spawn(terminal), desc="Launch terminal"),
+ Key([mod], "v", lazy.spawn("copyq toggle"), desc="Shows Clipboard"),
+ Key([mod, "shift"], "q", lazy.spawn("flameshot gui"), desc="Screenshot selection"),
+ Key(
+ [mod, "shift"], "b", lazy.spawn(browser), desc="Opens Browser on current screen"
+ ),
+ Key(
+ [mod, "shift"],
+ "p",
+ lazy.spawn("firefox --private-window"),
+ desc="Opens Private Browser on current screen",
+ ),
+ Key([mod, "shift"], "e", lazy.spawn(filemanager), desc="Opens File Manager"),
+ Key([mod], "b", lazy.spawn("bitwarden-desktop"), desc="Opens Bitwarden"),
+ Key(
+ [mod, "shift"],
+ "i",
+ lazy.spawn(
+ "alacritty --config-file=/home/cerberus/.config/alacritty/cheat-sheet.toml -T FloatWindow -e /home/cerberus/.config/qtile/res/scripts/keybinds.py"
+ ),
+ desc="Opens Cheat-Sheet",
+ ),
+]
+
+# --------------------------------------------------------------------
+# Drag floating layouts
+# --------------------------------------------------------------------
+
+mouse = [
+ Drag(
+ [mod],
+ "Button1",
+ lazy.window.set_position_floating(),
+ start=lazy.window.get_position(),
+ ),
+ Drag(
+ [mod], "Button3", lazy.window.set_size_floating(), start=lazy.window.get_size()
+ ),
+ Click([mod], "Button2", lazy.window.bring_to_front()),
+]
+
+# KeybindEnd
+keys.extend(
+ [
+ Key([mod], "grave", lazy.function(notifier.prev)),
+ Key([mod, "shift"], "grave", lazy.function(notifier.next)),
+ Key(["control"], "space", lazy.function(notifier.close)),
+ ]
+)
diff --git a/modules/layouts.py b/modules/layouts.py
new file mode 100644
index 0000000..a0bcbe1
--- /dev/null
+++ b/modules/layouts.py
@@ -0,0 +1,78 @@
+# _ _ _ _ _
+# __ _| |_(_) | ___ | | __ _ _ _ ___ _ _| |_ ___
+# / _` | __| | |/ _ \ | |/ _` | | | |/ _ \| | | | __/ __|
+# | (_| | |_| | | __/ | | (_| | |_| | (_) | |_| | |_\__ \
+# \__, |\__|_|_|\___| |_|\__,_|\__, |\___/ \__,_|\__|___/
+# |_| |___/
+# --------------------------------------------------------------------
+
+# --------------------------------------------------------------------
+# Imports
+# --------------------------------------------------------------------
+from libqtile import layout
+from libqtile.config import Match
+
+layout_defaults = dict(
+ margin = 3,
+ border_width = 0,
+ # border_focus=gruvbox_dark["red"],
+ # border_normal=gruvbox_dark["fg1"],
+ grow_amount = 2,
+ )
+
+floating_layout_defaults = layout_defaults.copy()
+
+layouts = [
+ # layout.Max(**layout_defaults),
+ # layout.Plasma(),
+ # layout.Stack(num_stacks=2),
+ # layout.Columns(),
+ # layout.Matrix(),
+ layout.MonadTall(name="Monad",
+ auto_maximize=True,
+ change_ratio=0.05,
+ change_size=20,
+ ratio=0.55,
+ min_ratio=0.30,
+ max_ratio=0.75,
+ single_border_width=0,
+ **layout_defaults
+ ),
+ layout.Bsp(name="bsp",
+ ratio = 0.5,
+ # border_on_single= True,
+ lower_right = True,
+ **layout_defaults,
+ ),
+ layout.Spiral(**layout_defaults),
+ # layout.MonadWide(),
+ # layout.RatioTile(),
+ # layout.Tile(name="Tile", **layout_defaults),
+ # layout.TreeTab(),
+ # layout.VerticalTile(),
+ # layout.Zoomy(**layout_defaults),
+]
+
+floating_layout = layout.Floating(
+ float_rules=[
+ # Run the utility of `xprop` to see the wm class and name of an X client.
+ *layout.Floating.default_float_rules,
+ Match(wm_class="confirmreset"), # gitk
+ Match(wm_class="makebranch"), # gitk
+ Match(wm_class="maketag"), # gitk
+ Match(wm_class="ssh-askpass"), # ssh-askpass
+ Match(wm_class="nm-connection-editor"), # networkmanager
+ # Match(wm_class="bitwarden"), # bitwarden
+ Match(title="branchdialog"), # gitk
+ Match(title="pinentry"), # GPG key password entry
+ Match(title="FloatWindow"),
+ Match(wm_class="qalculate-qt"),
+ Match(wm_class="copyq"),
+ Match(wm_class="nitrogen"),
+ Match(wm_class="nemo-preview-start"),
+ Match(wm_class="wireguird"),
+ Match(wm_class="blueman-manager"),
+
+ ],
+ **floating_layout_defaults
+)
\ No newline at end of file
diff --git a/modules/screens.py b/modules/screens.py
new file mode 100644
index 0000000..4fccfe3
--- /dev/null
+++ b/modules/screens.py
@@ -0,0 +1,191 @@
+# _ _ _
+# __ _| |_(_) | ___ ___ ___ _ __ ___ ___ _ __ ___
+# / _` | __| | |/ _ \ / __|/ __| '__/ _ \/ _ \ '_ \/ __|
+# | (_| | |_| | | __/ \__ \ (__| | | __/ __/ | | \__ \
+# \__, |\__|_|_|\___| |___/\___|_| \___|\___|_| |_|___/
+# |_|
+# --------------------------------------------------------------------
+# Imports
+# --------------------------------------------------------------------
+from libqtile.config import Screen
+from libqtile import bar
+from libqtile.lazy import lazy
+
+from qtile_extras import widget
+from qtile_extras.widget.groupbox2 import GroupBoxRule
+
+from plugins.notifications import Notifier
+
+from popups.powermenu import power_menu
+from popups.start_menu import start_menu
+from popups.calendar import calendar
+from popups.mpris2_layout import MPRIS2_LAYOUT
+from popups.volume_notification import VOL_POPUP
+from popups.brightness_notification import BRIGHTNESS_NOTIFICATION
+
+from res.themes.colors import gruvbox_dark
+
+# --------------------------------------------------------
+# GroupBox2 rules
+# --------------------------------------------------------
+# def set_app_group_color(rule, box):
+# if box.has.win.name("Jellyfin Media Player"):
+# rule.foreground = gruvbox_dark
+# # elif box.occupied:
+# # rule.text = "◎"
+# # else:
+# # rule.text = "○"
+
+# return True
+
+
+def get_groupbox_rules(monitor_specific=True):
+ # Base rules applied to all GroupBoxes
+ rules = [
+ GroupBoxRule(text_colour=gruvbox_dark["bg3"]).when(
+ focused=False, occupied=True
+ ),
+ # GroupBoxRule(text_colour=gruvbox_dark["aqua"]).when(focused=False, occupied=False),
+ GroupBoxRule(text_colour=gruvbox_dark["fg3"]).when(focused=True),
+ GroupBoxRule(text_colour=gruvbox_dark["red"]).when(
+ focused=False, occupied=True, urgent=True
+ ),
+ GroupBoxRule(visible=False).when(focused=False, occupied=False),
+ # GroupBoxRule().when(func=set_app_group_color)
+ ]
+
+ # Add extra rule for a specific monitor (e.g., show "X" as label)
+ if monitor_specific:
+ rules.append(GroupBoxRule(text=""))
+ return rules
+
+
+# --------------------------------------------------------
+# Widget Defaults
+# --------------------------------------------------------
+widget_defaults = dict(font="Open Sans", fontsize=18, foreground=gruvbox_dark["fg1"])
+extension_defaults = widget_defaults.copy()
+
+# --------------------------------------------------------
+# Screens
+# --------------------------------------------------------
+bar.Bar
+screens = [
+ Screen(
+ top=bar.Bar(
+ [
+ widget.TextBox(
+ text="",
+ fontsize=24, # PopUp-Toolkit?
+ foreground=gruvbox_dark["blue"],
+ mouse_callbacks={"Button1": lazy.function(start_menu)},
+ ),
+ widget.GroupBox2(
+ padding=5,
+ fontsize=22,
+ font="Open Sans",
+ center_aligned=True,
+ visible_groups=["1", "2", "3", "4", "5", "6", "7"], # , '8', '9'
+ hide_unused=True,
+ rules=get_groupbox_rules(monitor_specific=False),
+ ),
+ widget.Spacer(length=20),
+ widget.Mpris2(
+ name="mpris2",
+ width=350,
+ scroll=True,
+ scroll_clear=True,
+ foreground=gruvbox_dark["fg1"],
+ format="{xesam:title} - {xesam:artist}",
+ paused_text="{track} ",
+ popup_layout=MPRIS2_LAYOUT,
+ poll_interval=15,
+ popup_show_args={"relative_to": 2, "relative_to_bar": True, "y": 3},
+ mouse_callbacks={"Button1": lazy.widget["mpris2"].toggle_player()},
+ ),
+ widget.Spacer(),
+ widget.Battery(
+ format="{char} {percent:2.0%}",
+ low_percentage=0.25,
+ low_foreground=gruvbox_dark["red"],
+ charging_foreground=gruvbox_dark["green"],
+ charge_char="",
+ discharge_char="",
+ empty_char="!",
+ full_char="",
+ not_charging_char="",
+ unknown_char="?",
+ update_interval=1,
+ ),
+ widget.WidgetBox(
+ fontsize=22,
+ text_closed="",
+ text_open=" ",
+ widgets=[
+ widget.Memory(format=" {MemPercent}%"),
+ widget.CPU(format=" {load_percent}%"),
+ ],
+ ),
+ widget.Spacer(length=2),
+ widget.Systray(
+ icon_size=21,
+ ),
+ widget.Spacer(length=6),
+ widget.Clock(mouse_callbacks={"Button1": lazy.function(calendar)}),
+ widget.Spacer(length=2),
+ widget.TextBox(
+ fontsize=20,
+ text=" ",
+ mouse_callbacks={"Button1": lazy.function(power_menu)},
+ ),
+ # Invisible widgets for popup notifications at value change
+ widget.BrightnessControl(
+ mode="popup",
+ popup_layout=BRIGHTNESS_NOTIFICATION,
+ device="/sys/class/backlight/intel_backlight",
+ brightness_path="brightness",
+ max_brightness_path="max_brightness",
+ popup_show_args={"relative_to": 8, "y": -70},
+ ),
+ widget.PulseVolumeExtra(
+ mode="popup",
+ fmt="",
+ popup_layout=VOL_POPUP,
+ popup_hide_timeout=3,
+ popup_show_args={"relative_to": 8, "y": -70},
+ ),
+ ],
+ background=gruvbox_dark["bg0_hard"],
+ opacity=0.75,
+ size=32,
+ margin=[3, 3, 0, 3],
+ ),
+ ),
+]
+
+notifier = Notifier(
+ x=835,
+ y=38,
+ width=250,
+ height=96,
+ format="{summary}\n{app_name}\n{body}",
+ # file_name='/home/cerberus/.config/qtile/normal.png', # Not working
+ foreground=gruvbox_dark["fg1"],
+ background=(
+ gruvbox_dark["bg0_hard"],
+ gruvbox_dark["bg0_hard"],
+ gruvbox_dark["orange"],
+ ),
+ horizontal_padding=8,
+ vertical_padding=8,
+ opacity=0.65,
+ border_width=0,
+ font="Open Sans",
+ font_size=16,
+ # overflow='more_width',
+ fullscreen="queue",
+ screen="focus",
+ actions=True,
+ # wrap=True
+)
+
diff --git a/plugins/notifications.py b/plugins/notifications.py
new file mode 100644
index 0000000..77c941e
--- /dev/null
+++ b/plugins/notifications.py
@@ -0,0 +1,455 @@
+"""
+Qtile plugin that acts as a notification server and draws notification windows.
+
+Clicking on a notification will trigger the default action, e.g. telling Firefox to open
+the tab that sent the notification. If you want access to a notification's non-default
+actions then you need to disable the "actions" capability of the `Notifier` by passing
+`actions=False`.
+
+Usage:
+
+ from graphical_notifications import Notifier
+
+ notifier = Notifier()
+
+ keys.extend([
+ Key([mod], 'grave', lazy.function(notifier.prev)),
+ Key([mod, 'shift'], 'grave', lazy.function(notifier.next)),
+ Key(['control'], 'space', lazy.function(notifier.close)),
+ ])
+
+Qtile versions known to work: 0.17 - 0.18
+"""
+
+from __future__ import annotations
+
+import asyncio
+from typing import TYPE_CHECKING
+
+from libqtile import configurable, hook, images, pangocffi, qtile
+from libqtile.lazy import lazy
+from libqtile.log_utils import logger
+from libqtile.notify import notifier, ClosedReason
+from libqtile.popup import Popup
+
+if TYPE_CHECKING:
+ from typing import Any, Callable, Dict, List, Optional, Tuple
+
+ from cairocffi import ImageSurface
+
+ from libqtile.core.manager import Qtile
+ try:
+ from libqtile.notify import Notification
+ except ImportError: # no dbus_next
+ Notification = Any # type: ignore
+
+
+class Notifier(configurable.Configurable):
+ """
+ This class provides a full graphical notification manager for the
+ org.freedesktop.Notifications service implemented in libqtile.notify.
+
+ The format option determines what text is shown on the popup windows, and supports
+ markup and new line characters e.g. '{summary}\n{body}'. Available
+ placeholders are summary, body and app_name.
+
+ Foreground and background colours can be specified either as tuples/lists of 3
+ strings, corresponding to low, normal and critical urgencies, or just a single
+ string which will then be used for all urgencies. The timeout and border options can
+ be set in the same way.
+
+ The max_windows option limits how many popup windows can be drawn at a time. When
+ more notifications are recieved while the maximum number are already drawn,
+ notifications are queued and displayed when existing notifications are closed.
+
+ TODO:
+ - text overflow
+ - select screen / follow mouse/keyboard focus
+ - critical notifications to replace any visible non-critical notifs immediately?
+ - hints: image-path, desktop-entry (for icon)
+ - hints: Notifier parameters set for single notification?
+ - hints: progress value e.g. int:value:42 with drawing
+
+ """
+ defaults = [
+ ('x', 32, 'x position on screen to start drawing notifications.'),
+ ('y', 64, 'y position on screen to start drawing notifications.'),
+ ('width', 192, 'Width of notifications.'),
+ ('height', 64, 'Height of notifications.'),
+ ('format', '{summary}\n{body}', 'Text format.'),
+ (
+ 'foreground',
+ ('#ffffff', '#ffffff', '#ffffff'),
+ 'Foreground colour of notifications, in ascending order of urgency.',
+ ),
+ (
+ 'background',
+ ('#111111', '#111111', '#111111'),
+ 'Background colour of notifications, in ascending order of urgency.',
+ ),
+ (
+ 'border',
+ ('#111111', '#111111', '#111111'),
+ 'Border colours in ascending order of urgency. Or None for none.',
+ ),
+ (
+ 'timeout',
+ (5000, 5000, 0),
+ 'Millisecond timeout duration, in ascending order of urgency.',
+ ),
+ ('opacity', 1.0, 'Opacity of notifications.'),
+ ('border_width', 4, 'Line width of drawn borders.'),
+ ('corner_radius', None, 'Corner radius for round corners, or None.'),
+ ('font', 'sans', 'Font used in notifications.'),
+ ('fontsize', 14, 'Size of font.'),
+ ('fontshadow', None, 'Color for text shadows, or None for no shadows.'),
+ ('text_alignment', 'left', 'Text alignment: left, center or right.'),
+ ('horizontal_padding', None, 'Padding at sides of text.'),
+ ('vertical_padding', None, 'Padding at top and bottom of text.'),
+ ('line_spacing', 4, 'Space between lines.'),
+ (
+ 'overflow',
+ 'truncate',
+ 'How to deal with too much text: more_width, more_height, or truncate.',
+ ),
+ ('max_windows', 2, 'Maximum number of windows to show at once.'),
+ ('gap', 12, 'Vertical gap between popup windows.'),
+ ('sticky_history', True, 'Disable timeout when browsing history.'),
+ ('icon_size', 36, 'Pixel size of any icons.'),
+ ('fullscreen', 'show', 'What to do when in fullscreen: show, hide, or queue.'),
+ ('screen', 'focus', 'How to select a screen: focus, mouse, or an int.'),
+ ('actions', True, 'Whether to enable the actions capability.'),
+ ]
+ capabilities = {'body', 'body-markup', 'actions'}
+ # specification: https://developer.gnome.org/notification-spec/
+
+ def __init__(self, **config) -> None:
+ configurable.Configurable.__init__(self, **config)
+ self.add_defaults(Notifier.defaults)
+ self._hidden: List[Popup] = []
+ self._shown: List[Popup] = []
+ self._queue: List[Notification] = []
+ self._positions: List[Tuple[int, int]] = []
+ self._scroll_popup: Optional[Popup] = None
+ self._current_id: int = 0
+ self._notif_id: Optional[int] = None
+ self._paused: bool = False
+ self._icons: Dict[str, Tuple[ImageSurface, int]] = {}
+
+ self._make_attr_list('foreground')
+ self._make_attr_list('background')
+ self._make_attr_list('timeout')
+ self._make_attr_list('border')
+
+ hook.subscribe.startup(lambda: asyncio.create_task(self._configure()))
+
+ if self.actions is False:
+ Notifier.capabilities.remove("actions")
+
+ def _make_attr_list(self, attr: str) -> None:
+ """
+ Turns '#000000' into ('#000000', '#000000', '#000000')
+ """
+ value = getattr(self, attr)
+ if not isinstance(value, (tuple, list)):
+ setattr(self, attr, (value,) * 3)
+
+ async def _configure(self) -> None:
+ """
+ This method needs to be called to set up the Notifier with the Qtile manager and
+ create the required popup windows.
+ """
+ if self.horizontal_padding is None:
+ self.horizontal_padding = self.fontsize // 2
+ if self.vertical_padding is None:
+ self.vertical_padding = self.fontsize // 2
+
+ popup_config = {
+ "x": self.x,
+ "y": self.y,
+ "width": self.width,
+ "height": self.height,
+ }
+
+ for opt in Popup.defaults:
+ key = opt[0]
+ if hasattr(self, key):
+ value = getattr(self, key)
+ if isinstance(value, (tuple, list)):
+ popup_config[key] = value[1]
+ else:
+ popup_config[key] = value
+
+ for win in range(self.max_windows):
+ popup = Popup(qtile, **popup_config)
+ popup.win.process_button_click = self._process_button_click(popup)
+ popup.notif = None
+ self._hidden.append(popup)
+ self._positions.append(
+ (
+ self.x,
+ self.y + win * (self.height + 2 * self.border_width + self.gap)
+ )
+ )
+
+ # Clear defunct callbacks left when reloading the config
+ notifier.callbacks.clear()
+ notifier.close_callbacks.clear()
+
+ await notifier.register(
+ self._notify, Notifier.capabilities, on_close=self._on_close
+ )
+ logger.info("Notification server started up successfully")
+
+ def _process_button_click(self, popup: Popup) -> Callable:
+ def _(x: int, y: int, button: int) -> None:
+ if button == 1:
+ self._act(popup)
+ self._close(popup, reason=ClosedReason.dismissed)
+ if button == 3:
+ self._close(popup, reason=ClosedReason.dismissed)
+ return _
+
+ def _notify(self, notif: Notification) -> None:
+ """
+ This method is registered with the NotificationManager to handle notifications
+ received via dbus. They will either be drawn now or queued to be drawn soon.
+ """
+ if self._paused:
+ self._queue.append(notif)
+ return
+
+ if qtile.current_window and qtile.current_window.fullscreen:
+ if self.fullscreen != 'show':
+ if self.fullscreen == 'queue':
+ if self._unfullscreen not in hook.subscriptions:
+ hook.subscribe.float_change(self._unfullscreen)
+ self._queue.append(notif)
+ return
+
+ if notif.replaces_id:
+ for popup in self._shown:
+ if notif.replaces_id == popup.notif.replaces_id:
+ self._shown.remove(popup)
+ self._send(notif, popup)
+ self._reposition()
+ return
+
+ if self._hidden:
+ self._send(notif, self._hidden.pop())
+ else:
+ self._queue.append(notif)
+
+ def _on_close(self, nid: int) -> None:
+ for popup in self._shown:
+ self._close(popup, nid=nid, reason=ClosedReason.method)
+
+ def _unfullscreen(self) -> None:
+ """
+ Begin displaying of queue notifications after leaving fullscreen.
+ """
+ if not qtile.current_window.fullscreen:
+ hook.unsubscribe.float_change(self._unfullscreen)
+ self._renotify()
+
+ def _renotify(self) -> None:
+ """
+ If we hold off temporarily on sending notifications and accumulate a queue, we
+ should use this to send the queue through self._notify again.
+ """
+ queue = self._queue.copy()
+ self._queue.clear()
+ while queue:
+ self._notify(queue.pop(0))
+
+ def _send(
+ self, notif: Notification, popup: Popup, timeout: Optional[int] = None
+ ) -> None:
+ """
+ Draw the desired notification using the specified Popup instance.
+ """
+ text = self._get_text(notif)
+
+ if "urgency" in notif.hints:
+ urgency = notif.hints["urgency"].value
+ else:
+ urgency = 1
+
+ self._current_id += 1
+ popup.id = self._current_id # Used for closing the popup
+ popup.notif = notif # Used for finding the visible popup's notif for actions
+ if popup not in self._shown:
+ self._shown.append(popup)
+ popup.x, popup.y = self._get_coordinates()
+ popup.place()
+ icon = self._load_icon(notif)
+ popup.unhide()
+
+ popup.background = self.background[urgency]
+ popup.layout.colour = self.foreground[urgency]
+ popup.clear()
+
+ if icon:
+ popup.draw_image(
+ icon[0],
+ self.horizontal_padding,
+ 1 + (self.height - icon[1]) / 2,
+ )
+ popup.horizontal_padding += self.icon_size + self.horizontal_padding / 2
+
+ for num, line in enumerate(text.split('\n')):
+ popup.layout.text = line
+ y = self.vertical_padding + num * (popup.layout.height + self.line_spacing)
+ popup.draw_text(y=y)
+ if self.border_width:
+ popup.set_border(self.border[urgency])
+ popup.draw()
+ if icon:
+ popup.horizontal_padding = self.horizontal_padding
+
+ if timeout is None:
+ if notif.timeout is None or notif.timeout < 0:
+ timeout = self.timeout[urgency]
+ else:
+ timeout = notif.timeout
+ elif timeout < 0:
+ timeout = self.timeout[urgency]
+ if timeout > 0:
+ qtile.call_later(timeout / 1000, self._close, popup, self._current_id)
+
+ def _get_text(self, notif: Notification) -> str:
+ summary = ''
+ body = ''
+ app_name = ''
+ if notif.summary:
+ summary = pangocffi.markup_escape_text(notif.summary)
+ if notif.body:
+ body = pangocffi.markup_escape_text(notif.body)
+ if notif.app_name:
+ app_name = pangocffi.markup_escape_text(notif.app_name)
+ return self.format.format(summary=summary, body=body, app_name=app_name)
+
+ def _get_coordinates(self) -> Tuple[int, int]:
+ x, y = self._positions[len(self._shown) - 1]
+ if isinstance(self.screen, int):
+ screen = qtile.screens[self.screen]
+ elif self.screen == 'focus':
+ screen = qtile.current_screen
+ elif self.screen == 'mouse':
+ screen = qtile.find_screen(*qtile.mouse_position)
+ return x + screen.x, y + screen.y
+
+ def _close(self, popup: Popup, nid: Optional[int] = None, reason=1) -> None:
+ """
+ Close the specified Popup instance.
+ """
+ if popup in self._shown:
+ if nid is not None and popup.id != nid:
+ return
+ self._shown.remove(popup)
+ if self._scroll_popup is popup:
+ self._scroll_popup = None
+ self._notif_id = None
+ popup.hide()
+ if self._queue and not self._paused:
+ self._send(self._queue.pop(0), popup)
+ else:
+ self._hidden.append(popup)
+ notifier._service.NotificationClosed(popup.notif.id, reason)
+ self._reposition()
+
+ def _act(self, popup: Popup) -> None:
+ """
+ Execute the actions specified by the notification visible on a clicked popup.
+ """
+ # Currently this always invokes default action
+ #actions = {i: l for i, l in zip(notif.actions[:-1:2], notif.actions[1::2])}
+ if popup.notif.actions:
+ notifier._service.ActionInvoked(popup.notif.id, popup.notif.actions[0])
+
+ def _reposition(self) -> None:
+ for index, shown in enumerate(self._shown):
+ shown.x, shown.y = self._positions[index]
+ shown.place()
+
+ def _load_icon(self, notif: Notification) -> Optional[Tuple[ImageSurface, int]]:
+ if not notif.app_icon:
+ return None
+ if notif.app_icon in self._icons:
+ return self._icons.get(notif.app_icon)
+ try:
+ img = images.Img.from_path(notif.app_icon)
+ if img.width > img.height:
+ img.resize(width=self.icon_size)
+ else:
+ img.resize(height=self.icon_size)
+ surface, _ = images._decode_to_image_surface(
+ img.bytes_img, img.width, img.height
+ )
+ self._icons[notif.app_icon] = surface, surface.get_height()
+ except (FileNotFoundError, images.LoadingError, IsADirectoryError) as e:
+ logger.exception(e)
+ self._icons[notif.app_icon] = None
+ return self._icons[notif.app_icon]
+
+ def close(self, _qtile=None) -> None:
+ """
+ Close the oldest of all visible popup windows.
+ """
+ if self._shown:
+ self._close(self._shown[0])
+
+ def close_all(self, _qtile=None) -> None:
+ """
+ Close all popup windows.
+ """
+ self._queue.clear()
+ while self._shown:
+ self._close(self._shown[0])
+
+ def prev(self, _qtile=None) -> None:
+ """
+ Display the previous notification in the history.
+ """
+ if notifier.notifications:
+ if self._scroll_popup is None:
+ if self._hidden:
+ self._scroll_popup = self._hidden.pop(0)
+ else:
+ self._scroll_popup = self._shown[0]
+ self._notif_id = len(notifier.notifications)
+ if self._notif_id > 0:
+ self._notif_id -= 1
+ self._send(
+ notifier.notifications[self._notif_id],
+ self._scroll_popup,
+ 0 if self.sticky_history else None,
+ )
+
+ def next(self, _qtile=None) -> None:
+ """
+ Display the next notification in the history.
+ """
+ if self._scroll_popup:
+ if self._notif_id < len(notifier.notifications) - 1:
+ self._notif_id += 1
+ if self._scroll_popup in self._shown:
+ self._shown.remove(self._scroll_popup)
+ self._send(
+ notifier.notifications[self._notif_id],
+ self._scroll_popup,
+ 0 if self.sticky_history else None,
+ )
+
+ def pause(self, _qtile=None) -> None:
+ """
+ Pause display of notifications on screen. Notifications will be queued and
+ presented as usual when this is called again.
+ """
+ if self._paused:
+ self._paused = False
+ self._renotify()
+ else:
+ self._paused = True
+ while self._shown:
+ self._close(self._shown[0])
diff --git a/popups/brightness_notification.py b/popups/brightness_notification.py
new file mode 100644
index 0000000..2ced74f
--- /dev/null
+++ b/popups/brightness_notification.py
@@ -0,0 +1,41 @@
+from res.themes.colors import gruvbox_dark
+from qtile_extras.popup.toolkit import (
+ PopupRelativeLayout,
+ PopupText,
+ PopupSlider
+)
+
+
+BRIGHTNESS_NOTIFICATION = PopupRelativeLayout(
+ width=150,
+ height=150,
+ opacity=0.7,
+ background=gruvbox_dark["bg0_soft"],
+ controls=[
+ PopupText(
+ text="",
+ fontsize=70,
+ foreground=gruvbox_dark["fg1"],
+ # name="text",
+ pos_x=0,
+ pos_y=0,
+ height=0.8,
+ width=0.8,
+ v_align="middle",
+ h_align="center",
+ ),
+ PopupSlider(
+ name="brightness",
+ pos_x=0.1,
+ pos_y=0.7,
+ width=0.8,
+ height=0.2,
+ colour_below=gruvbox_dark["blue"],
+ # bar_border_size=2,
+ bar_border_margin=1,
+ bar_size=8,
+ marker_size=0,
+ end_margin=0,
+ ),
+ ],
+)
\ No newline at end of file
diff --git a/popups/calendar.py b/popups/calendar.py
new file mode 100644
index 0000000..b5eac0d
--- /dev/null
+++ b/popups/calendar.py
@@ -0,0 +1,135 @@
+import subprocess
+
+from libqtile import qtile
+
+from qtile_extras import widget
+from qtile_extras.popup.toolkit import (PopupRelativeLayout,
+ PopupWidget,
+ )
+
+from res.themes.colors import gruvbox_dark
+
+# https://discord.com/channels/955163559086665728/1166312212223250482/1322614846155657370
+# check for this PR to change back the code with the message contents to the prev code of the widget:
+# PopupWidget(
+# pos_x=0.051,
+# pos_y=0.415,
+# height=0.6,
+# width=0.9,
+# widget=widget.GenPollCommand(
+# cmd="cal",
+# shell=True,
+# font='mono',
+# fontsize=20,
+# markup=False,
+# # background=gruvbox_dark["blue"],
+# )
+
+
+def parse_cal():
+ process = subprocess.run(
+ "cal",
+ capture_output=True,
+ text=True,
+ )
+ body = process.stdout.strip()
+ lines = body.splitlines()
+ maxlen = max(len(l) for l in lines)
+ output = []
+ for line in body.splitlines():
+ if len(line) < maxlen:
+ line += " " * (maxlen - len(line))
+ output.append(line)
+ return "\n".join(output).strip("\n")
+
+
+def calendar(qtile):
+ layout = PopupRelativeLayout(
+ qtile,
+ rows=7,
+ cols=9,
+ width=300,
+ height=310,
+ opacity=0.8,
+ hide_on_mouse_leave=True,
+ close_on_click=False,
+ border_width=0,
+ background=gruvbox_dark["bg0_soft"],
+ controls=[
+ PopupWidget(
+ pos_x=0,
+ pos_y=0,
+ height=0.2,
+ width=0.9,
+ v_align="middle",
+ h_align="center",
+ widget=widget.Wttr(
+ fontsize=40,
+ format='%c'
+ )
+ ),
+ PopupWidget(
+ pos_x=0.3,
+ pos_y=0.05,
+ height=0.05,
+ width=0.9,
+ v_align="middle",
+ h_align="center",
+ widget=widget.Wttr(
+ font='Open Sans Bold',
+ fontsize=18,
+ format='Actual: %t'
+ )
+ ),
+ PopupWidget(
+ pos_x=0.3,
+ pos_y=0.12,
+ height=0.05,
+ width=0.9,
+ widget=widget.Wttr(
+ font='Open Sans',
+ fontsize=14,
+ format='Feels: %f'
+ )
+ ),
+ PopupWidget(
+ pos_x=0.05,
+ pos_y=0.2,
+ height=0.05,
+ width=0.9,
+ widget=widget.Wttr(
+ font='Open Sans',
+ fontsize=14,
+ format='Wind: %w Prec: %p'
+ )
+ ),
+ PopupWidget(
+ pos_x=0.05,
+ pos_y=0.25,
+ height=0.11,
+ width=0.9,
+ widget=widget.Wttr(
+ font='Open Sans Bold',
+ fontsize=14,
+ format='City: %l', # \nFeel;%f Wind: %w'
+ )
+ ),
+ PopupWidget(
+ pos_x=0.051,
+ pos_y=0.38,
+ height=0.6,
+ width=0.9,
+ widget=widget.GenPollText(
+ func=parse_cal,
+ font='mono',
+ fontsize=20,
+ markup=False,
+ )
+ ),
+ ]
+ )
+ layout.show(relative_to=3,
+ relative_to_bar=True,
+ y=3,
+ x=-3,
+ )
\ No newline at end of file
diff --git a/popups/mpris2_layout.py b/popups/mpris2_layout.py
new file mode 100644
index 0000000..34998bc
--- /dev/null
+++ b/popups/mpris2_layout.py
@@ -0,0 +1,125 @@
+from res.themes.colors import gruvbox_dark
+from qtile_extras.popup.toolkit import (
+ PopupRelativeLayout,
+ PopupImage,
+ PopupText,
+ PopupSlider
+)
+image='/home/cerberus/.config/qtile/res/images/no_cover.svg'
+
+MPRIS2_LAYOUT = PopupRelativeLayout(
+ None,
+ width=400,
+ height=200,
+ opacity=0.7,
+ background=gruvbox_dark["bg0_soft"],
+ hide_on_mouse_leave=True,
+ controls=[
+ PopupText(
+ "",
+ name="title",
+ font='Open Sans Bold',
+ fontsize=18,
+ pos_x=0.35,
+ pos_y=0.1,
+ width=0.55,
+ height=0.14,
+ h_align="left",
+ v_align="top",
+ ),
+ PopupText(
+ "",
+ name="artist",
+ font='Open Sans Medium',
+ fontsize=14,
+ pos_x=0.35,
+ pos_y=0.24,
+ width=0.55,
+ height=0.14,
+ h_align="left",
+ v_align="middle",
+ ),
+ PopupText(
+ "",
+ name="album",
+ font='Open Sans',
+ fontsize=14,
+ pos_x=0.35,
+ pos_y=0.38,
+ width=0.55,
+ height=0.14,
+ h_align="left",
+ v_align="bottom",
+ ),
+ PopupImage(
+ name="artwork",
+ filename=image,
+ pos_x=0.1,
+ pos_y=0.1,
+ width=0.21,
+ height=0.42,
+ ),
+ PopupSlider(name="progress", pos_x=0.1, pos_y=0.6, width=0.8, height=0.1, marker_size=0),
+ PopupText(
+ name="previous",
+ text='',
+ fontsize=30,
+ mask=True,
+ pos_x=0.125,
+ pos_y=0.8,
+ width=0.15,
+ height=0.1,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ ),
+ PopupText(
+ name="play_pause",
+ text='',
+ fontsize=30,
+ mask=True,
+ pos_x=0.325,
+ pos_y=0.8,
+ width=0.15,
+ height=0.1,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ ),
+ PopupText(
+ name="stop",
+ text='',
+ fontsize=30,
+ mask=True,
+ pos_x=0.525,
+ pos_y=0.8,
+ width=0.15,
+ height=0.1,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ ),
+ PopupText(
+ name="next",
+ text='',
+ fontsize=30,
+ mask=True,
+ pos_x=0.725,
+ pos_y=0.8,
+ width=0.15,
+ height=0.1,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ ),
+ ],
+ close_on_click=False,
+)
\ No newline at end of file
diff --git a/popups/network.py b/popups/network.py
new file mode 100644
index 0000000..e0d46f4
--- /dev/null
+++ b/popups/network.py
@@ -0,0 +1,48 @@
+from libqtile import qtile
+from libqtile.lazy import lazy
+
+from res.themes.colors import gruvbox_dark
+
+from qtile_extras.popup.menu import (
+ PopupMenu,
+ PopupMenuItem,
+ PopupMenuSeparator,
+ )
+
+items=[
+ PopupMenuItem(
+ show_icon=False,
+ text=' Network Manager',
+ font='Open Sans',
+ fontsize=16,
+ can_focus=True,
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ mouse_callbacks={"Button1": lazy.spawn("nm-connection-editor")},
+ ),
+ PopupMenuSeparator(),
+ PopupMenuItem(
+ show_icon=False,
+ text=' Wireguard',
+ font='Open Sans',
+ fontsize=16,
+ highlight_method='text',
+ can_focus=True,
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ mouse_callbacks={"Button1": lazy.spawn("wireguird")},
+ )
+]
+
+def network_menu(qtile):
+ layout = PopupMenu.generate(
+ qtile,
+ pos_x=100,
+ pos_y=100,
+ width=225,
+ opacity=0.7,
+ menuitems=items,
+ background=gruvbox_dark["bg0_soft"]
+ )
+ layout.show(relative_to=1, relative_to_bar=True, y=136, x=220)
\ No newline at end of file
diff --git a/popups/powermenu.py b/popups/powermenu.py
new file mode 100644
index 0000000..4912e8c
--- /dev/null
+++ b/popups/powermenu.py
@@ -0,0 +1,70 @@
+from libqtile.lazy import lazy
+from res.themes.colors import gruvbox_dark
+from qtile_extras.popup.toolkit import (
+ PopupRelativeLayout,
+ PopupText,
+)
+# qtile/resources/themes/colors.py
+def power_menu(qtile):
+ layout = PopupRelativeLayout(
+ qtile,
+ width=800,
+ height=250,
+ opacity=0.7,
+ # border=gruvbox_dark["red"],
+ # border_width=3,
+ background=gruvbox_dark["bg0_soft"],
+ initial_focus=None,
+ controls=[
+
+ PopupText(
+ # Lock betterlockscreen --lock blur
+ text="",
+ fontsize=80,
+ pos_y=0,
+ pos_x=0.1,
+ width=0.2,
+ height=1,
+ mouse_callbacks={"Button1": lazy.spawn("betterlockscreen --lock blur")},
+ highlight_method='text',
+ highlight=gruvbox_dark["green"],
+ ),
+ PopupText(
+ # Hybrid Sleep systemctl hybrid-sleep
+ text="",
+ fontsize=80,
+ pos_y=0,
+ pos_x=0.32,
+ width=0.2,
+ height=1,
+ mouse_callbacks={"Button1": lazy.spawn("systemctl hybrid-sleep")},
+ highlight_method='text',
+ highlight=gruvbox_dark["yellow"],
+ ),
+ PopupText(
+ # Hibernate systemctl hibernate
+ text="",
+ fontsize=80,
+ pos_y=0,
+ pos_x=0.55,
+ width=0.2,
+ height=1,
+ mouse_callbacks={"Button1": lazy.spawn("systemctl hibernate")},
+ highlight_method='text',
+ highlight=gruvbox_dark["orange"],
+ ),
+ PopupText(
+ # Power off systemctl poweroff
+ text="",
+ fontsize=80,
+ pos_y=0,
+ pos_x=0.8,
+ width=0.2,
+ height=1,
+ mouse_callbacks={"Button1": lazy.spawn("systemctl poweroff")},
+ highlight_method='text',
+ highlight=gruvbox_dark["red"],
+ ),
+ ],
+ )
+ layout.show(relative_to=5, relative_to_bar=True, hide_on_timeout=5)
diff --git a/popups/powermenu_sub.py b/popups/powermenu_sub.py
new file mode 100644
index 0000000..00efd79
--- /dev/null
+++ b/popups/powermenu_sub.py
@@ -0,0 +1,90 @@
+from pydoc import importfile
+from libqtile import qtile
+from libqtile.lazy import lazy
+
+from res.themes.colors import gruvbox_dark
+
+from qtile_extras.popup.toolkit import (
+ PopupRelativeLayout,
+ PopupImage,
+ PopupText,
+)
+
+def powermenu_2(qtile):
+ layout = PopupRelativeLayout(
+ qtile,
+ width=170,
+ height=50,
+ opacity=0.7,
+ hide_on_mouse_leave=True,
+ close_on_click=False,
+ border_width=0,
+ background=gruvbox_dark["bg0_soft"],
+ controls=[
+ PopupText(
+ # Lock
+ text='',
+ fontsize=22,
+ pos_x=0.07,
+ pos_y=0.05,
+ height=0.8,
+ width=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ mouse_callbacks={"Button1": lazy.spawn("betterlockscreen --lock blur")},
+ ),
+ PopupText(
+ # Reboot
+ text='',
+ fontsize=22,
+ pos_x=0.3,
+ pos_y=0.05,
+ height=0.8,
+ width=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ mouse_callbacks={"Button1": lazy.spawn("systemctl reboot")},
+ ),
+ PopupText(
+ # Suspend
+ text='',
+ fontsize=22,
+ pos_x=0.54,
+ pos_y=0.05,
+ height=0.8,
+ width=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ mouse_callbacks={"Button1": lazy.spawn("systemctl suspend")},
+ ),
+ PopupText(
+ # Shutdown
+ text='',
+ fontsize=22,
+ pos_x=0.78,
+ pos_y=0.05,
+ height=0.8,
+ width=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ mouse_callbacks={"Button1": lazy.spawn("systemctl poweroff")},
+ ),
+ ]
+ )
+ layout.show(relative_to=1, relative_to_bar=True, y=136, x=30)
\ No newline at end of file
diff --git a/popups/settings.py b/popups/settings.py
new file mode 100644
index 0000000..7da8ec9
--- /dev/null
+++ b/popups/settings.py
@@ -0,0 +1,59 @@
+from libqtile import qtile
+from libqtile.lazy import lazy
+
+from res.themes.colors import gruvbox_dark
+
+from qtile_extras.popup.menu import (
+ PopupMenu,
+ PopupMenuItem,
+ PopupMenuSeparator,
+ )
+
+items = [
+ PopupMenuItem(# Wallpaper setting
+ show_icon=False,
+ text=' Nitrogen Wallpaper',
+ font='Open Sans',
+ fontsize=16,
+ can_focus=True,
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ mouse_callbacks={"Button1": lazy.spawn("nitrogen")},
+ ),
+ PopupMenuSeparator(),
+ PopupMenuItem(# Arandr
+ show_icon=False,
+ text=' Arandr Display',
+ font='Open Sans',
+ fontsize=16,
+ can_focus=True,
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ mouse_callbacks={"Button1": lazy.spawn("arandr")},),
+ PopupMenuSeparator(),
+ PopupMenuItem(# VS-Code qtile config
+ show_icon=False,
+ text=' Qtile Config',
+ font='Open Sans',
+ fontsize=16,
+ can_focus=True,
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ mouse_callbacks={"Button1": lazy.spawn("code /home/cerberus/.config/")},),
+]
+
+def settings(qtile):
+ layout = PopupMenu.generate(
+ qtile,
+ pos_x=100,
+ pos_y=100,
+ width=225,
+ opacity=0.7,
+ menuitems=items,
+ background=gruvbox_dark["bg0_soft"]
+ )
+ layout.show(relative_to=1, relative_to_bar=True, y=75, x=325)
+
diff --git a/popups/start_menu.py b/popups/start_menu.py
new file mode 100644
index 0000000..092dccf
--- /dev/null
+++ b/popups/start_menu.py
@@ -0,0 +1,172 @@
+from libqtile import qtile
+from libqtile.lazy import lazy
+
+from qtile_extras import widget
+from qtile_extras.popup.toolkit import (
+ PopupRelativeLayout,
+ PopupImage,
+ PopupText,
+ PopupWidget,
+)
+
+from res.themes.colors import gruvbox_dark
+from popups.settings import settings
+from popups.network import network_menu
+from popups.powermenu_sub import powermenu_2
+
+
+def start_menu(qtile):
+ layout = PopupRelativeLayout(
+ qtile,
+ width=350,
+ height=150,
+ opacity=0.7,
+ hide_on_mouse_leave=True,
+ close_on_click=False,
+ border_width=0,
+ background=gruvbox_dark["bg0_soft"],
+ controls=[
+# Row 1
+
+ PopupImage(
+ # Qtile logo
+ pos_x=-0.09,
+ pos_y=0.117,
+ height=0.45,
+ width=0.45,
+ mask=True,
+ colour=gruvbox_dark["blue"],
+ filename='/home/cerberus/.config/qtile/res/images/standby_rotated.png',
+ ),
+ PopupWidget(
+ # Welcome banner, fetching user name from $USER
+ pos_x=0.241,
+ pos_y=0.12,
+ height=0.15,
+ width=0.8,
+ widget=widget.GenPollCommand(
+ foreground=gruvbox_dark["orange"],
+ cmd="echo Welcome $USER",
+ shell=True,
+ font='Open Sans Bold',
+ fontsize=20,
+ width=250,
+ scroll=True,
+ ),
+ ),
+ PopupText(
+ # Steam Gamemode (Controller)
+ text='',
+ fontsize=34,
+ pos_x=0.271,
+ pos_y=0.4,
+ width=0.34,
+ height=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ background=gruvbox_dark["bg2"],
+ mouse_callbacks={"Button1": lazy.spawn("steam -gamepadui")},
+ ),
+ PopupText(
+ # Settings
+ text='',
+ fontsize=22,
+ pos_x=0.631,
+ pos_y=0.4,
+ width=0.34,
+ height=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ background=gruvbox_dark["bg2"],
+ mouse_callbacks={"Button1": lazy.function(settings)},
+ ),
+ PopupText(
+ # Power-Menu Popup
+ text='',
+ fontsize=24,
+ pos_x=0.035,
+ pos_y=0.7,
+ width=0.22,
+ height=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ background=gruvbox_dark["bg2"],
+ mouse_callbacks={"Button1": lazy.function(powermenu_2)},
+ ),
+ PopupText(
+ # Bluetooth
+ text='',
+ fontsize=24,
+ pos_x=0.271,
+ pos_y=0.7,
+ width=0.22,
+ height=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ background=gruvbox_dark["bg2"],
+ mouse_callbacks={"Button1": lazy.spawn("blueman-manager")},
+ ),
+ PopupText(
+ # Network Popup
+ text='',
+ fontsize=24,
+ pos_x=0.511,
+ pos_y=0.7,
+ width=0.22,
+ height=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ background=gruvbox_dark["bg2"],
+ mouse_callbacks={"Button1": lazy.function(network_menu)},
+ ),
+ PopupText(
+ # Audiocontrol
+ text='',
+ fontsize=24,
+ pos_x=0.75,
+ pos_y=0.7,
+ width=0.22,
+ height=0.2,
+ can_focus=True,
+ v_align="middle",
+ h_align="center",
+ highlight_method='text',
+ foreground=gruvbox_dark["fg0"],
+ highlight=gruvbox_dark["green"],
+ background=gruvbox_dark["bg2"],
+ mouse_callbacks={"Button1": lazy.spawn("pavucontrol-qt")},
+ ),
+ PopupText(
+ # "extras"
+ text='extras',
+ font='Open Sans Bold',
+ foreground=gruvbox_dark["fg2"],
+ fontsize=10,
+ pos_x=0.055,
+ pos_y=0.57,
+ height=0.05,
+ width=0.15,
+ )
+ ]
+ )
+ layout.show(relative_to=1, relative_to_bar=True, y=3, x=3)
\ No newline at end of file
diff --git a/popups/volume_notification.py b/popups/volume_notification.py
new file mode 100644
index 0000000..26bbccb
--- /dev/null
+++ b/popups/volume_notification.py
@@ -0,0 +1,40 @@
+from res.themes.colors import gruvbox_dark
+
+from qtile_extras.popup.toolkit import (
+ PopupRelativeLayout,
+ PopupText,
+ PopupSlider
+)
+
+
+VOL_POPUP = PopupRelativeLayout(
+ width=150,
+ height=150,
+ opacity=0.7,
+ background=gruvbox_dark["bg0_soft"],
+ controls=[
+ PopupText(
+ text="",
+ fontsize=60,
+ foreground=gruvbox_dark["fg1"],
+ pos_x=0,
+ pos_y=0,
+ height=0.8,
+ width=0.8,
+ v_align="middle",
+ h_align="center",
+ ),
+ PopupSlider(
+ name="volume",
+ pos_x=0.1,
+ pos_y=0.7,
+ width=0.8,
+ height=0.2,
+ colour_below=gruvbox_dark["blue"],
+ bar_border_margin=1,
+ bar_size=8,
+ marker_size=0,
+ end_margin=0,
+ ),
+ ],
+)
\ No newline at end of file
diff --git a/res/images/no_cover.svg b/res/images/no_cover.svg
new file mode 100644
index 0000000..f2bc738
--- /dev/null
+++ b/res/images/no_cover.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/images/normal.png b/res/images/normal.png
new file mode 100644
index 0000000..505e12c
Binary files /dev/null and b/res/images/normal.png differ
diff --git a/res/images/standby_rotated.png b/res/images/standby_rotated.png
new file mode 100644
index 0000000..556c87f
Binary files /dev/null and b/res/images/standby_rotated.png differ
diff --git a/res/scripts/autostart.sh b/res/scripts/autostart.sh
new file mode 100755
index 0000000..373cb83
--- /dev/null
+++ b/res/scripts/autostart.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+# ___ _____ ___ _ _____ ____ _ _
+# / _ \_ _|_ _| | | ____| / ___|| |_ __ _ _ __| |_
+# | | | || | | || | | _| \___ \| __/ _` | '__| __|
+# | |_| || | | || |___| |___ ___) | || (_| | | | |_
+# \__\_\|_| |___|_____|_____| |____/ \__\__,_|_| \__|
+#
+# by cerberus
+# -----------------------------------------------------
+
+# -----------------------------------------------------
+# Autostart Services
+# -----------------------------------------------------
+
+# Load compositor
+picom &
+
+# Load polkit agent
+gnome-keyring-daemon --start --components=pkcs11,secrets,ssh &
+/usr/lib/mate-polkit/polkit-mate-authentication-agent-1 &
+
+# Set Wallpaper after restart
+nitrogen --restore &
+
+qtile cmd-obj -o cmd -f restart
+
+# Load Clipboardmanager
+copyq &
+
+# Load power manager
+xfce4-power-manager &
+
+# -----------------------------------------------------
+# Autostart Apps
+# -----------------------------------------------------
+# Configure Screen Layout
+$HOME/.screenlayout/screen1.sh
+# Start flameshot
+flameshot &
+
+# Load discord
+firefox &
+nm-applet &
+sleep 10
+affine &
diff --git a/res/scripts/keybinds.py b/res/scripts/keybinds.py
new file mode 100755
index 0000000..a642925
--- /dev/null
+++ b/res/scripts/keybinds.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+import re
+from pathlib import Path
+
+# Define the file path using Path and expand user directory
+file_path = Path("~/dotfiles/.config/qtile/modules/keys.py").expanduser()
+
+# Initialize a flag for header printing
+header_printed = False
+
+def capitalize_first_letter(s):
+ """Capitalize the first letter of each key."""
+ return s.capitalize() if s else ""
+
+def replace_keys(key):
+ """Replace Mod and Control with Super and Ctrl."""
+ if key == "mod":
+ return "Super"
+ if key == "xf86calculator":
+ return "Calculator"
+ elif key == "control":
+ return "Ctrl"
+ return key
+
+with open(file_path, 'r') as file:
+ for line in file:
+ # Skip lines that contain "# Key("
+ if '# Key(' in line:
+ continue
+
+ # Check for KB_GROUP headers
+ if '# KB_GROUP-' in line:
+ if header_printed:
+ print("") # Add a blank line before the next header
+ # Print the header in bold yellow, removing "KB_GROUP-"
+ print(f"\n\033[1;33m{line.strip().replace('KB_GROUP-', '').strip()}\033[0m\n")
+ header_printed = True
+
+ # Check for Key bindings
+ match = re.search(r'Key\(\[(.*?)\], "(.*?)", lazy\..*, desc="(.*)"\)', line)
+ if match:
+ # Get the modifier keys
+ keys = match.group(1).replace("'", "").replace('"', '').split(', ')
+ # Get the main key
+ key = match.group(2)
+ # Get the description
+ description = match.group(3)
+
+ # Prepare the key strings for each key position
+ mod1 = capitalize_first_letter(replace_keys(keys[0])) if len(keys) > 0 else ""
+ mod2 = capitalize_first_letter(replace_keys(keys[1])) if len(keys) > 1 else ""
+ key_str = capitalize_first_letter(key) # The main key
+
+ # Print the keys in their respective columns
+ print(f" {mod1:<6} {mod2:<8} {key_str:<30}{description}")
+
+# Wait for user input before exiting
+input("Press [Enter] to exit...")
+
diff --git a/res/themes/colors.py b/res/themes/colors.py
new file mode 100644
index 0000000..31b1a77
--- /dev/null
+++ b/res/themes/colors.py
@@ -0,0 +1,25 @@
+# --------------------------------------------------------
+# Gruvbox Dark Theme Colors
+# --------------------------------------------------------
+gruvbox_dark = {
+ "bg0_hard": "#1d2021", # Background, hard
+ "bg0_soft": "#32302f", # Background, soft
+ "bg0_normal": "#282828", # Background, normal
+ "bg1": "#3c3836", # Secondary background
+ "bg2": "#504945", # Background, darker
+ "bg3": "#665c54", # Background, lighter
+ "bg4": "#7c6f64", # Background, lightest
+
+ "fg0": "#fbf1c7", # Foreground, light
+ "fg1": "#ebdbb2", # Foreground, normal
+ "fg2": "#d5c4a1", # Foreground, slightly dark
+ "fg3": "#bdae93", # Foreground, dark
+
+ "red": "#cc241d", # Red
+ "orange": "#d65d0e", # Orange
+ "yellow": "#d79921", # Yellow
+ "green": "#98971a", # Green
+ "aqua": "#689d6a", # Aqua
+ "blue": "#458588", # Blue
+ "purple": "#b16286" # Purple
+}
\ No newline at end of file