From e20a3e51a0bf66a2c2a4ca2075faf9fd255319f8 Mon Sep 17 00:00:00 2001 From: cerberus Date: Tue, 24 Feb 2026 11:51:37 +0100 Subject: [PATCH] initial commit --- .gitignore | 176 ++++++++++++ config.py | 48 ++++ modules/groups.py | 84 ++++++ modules/hooks.py | 25 ++ modules/keys.py | 196 +++++++++++++ modules/layouts.py | 78 +++++ modules/screens.py | 191 +++++++++++++ plugins/notifications.py | 455 ++++++++++++++++++++++++++++++ popups/brightness_notification.py | 41 +++ popups/calendar.py | 135 +++++++++ popups/mpris2_layout.py | 125 ++++++++ popups/network.py | 48 ++++ popups/powermenu.py | 70 +++++ popups/powermenu_sub.py | 90 ++++++ popups/settings.py | 59 ++++ popups/start_menu.py | 172 +++++++++++ popups/volume_notification.py | 40 +++ res/images/no_cover.svg | 22 ++ res/images/normal.png | Bin 0 -> 3895 bytes res/images/standby_rotated.png | Bin 0 -> 5174 bytes res/scripts/autostart.sh | 45 +++ res/scripts/keybinds.py | 59 ++++ res/themes/colors.py | 25 ++ 23 files changed, 2184 insertions(+) create mode 100644 .gitignore create mode 100644 config.py create mode 100644 modules/groups.py create mode 100644 modules/hooks.py create mode 100644 modules/keys.py create mode 100644 modules/layouts.py create mode 100644 modules/screens.py create mode 100644 plugins/notifications.py create mode 100644 popups/brightness_notification.py create mode 100644 popups/calendar.py create mode 100644 popups/mpris2_layout.py create mode 100644 popups/network.py create mode 100644 popups/powermenu.py create mode 100644 popups/powermenu_sub.py create mode 100644 popups/settings.py create mode 100644 popups/start_menu.py create mode 100644 popups/volume_notification.py create mode 100644 res/images/no_cover.svg create mode 100644 res/images/normal.png create mode 100644 res/images/standby_rotated.png create mode 100755 res/scripts/autostart.sh create mode 100755 res/scripts/keybinds.py create mode 100644 res/themes/colors.py 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 0000000000000000000000000000000000000000..505e12c93392262f693add945b7fdf9e671f7dd7 GIT binary patch literal 3895 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 literal 0 HcmV?d00001 diff --git a/res/images/standby_rotated.png b/res/images/standby_rotated.png new file mode 100644 index 0000000000000000000000000000000000000000..556c87f20efa65fcdd61a20c6d9bdd02bc0d3fb1 GIT binary patch literal 5174 zcmYjV4?NTP|Nm^W&7W=l|C@iamBdxZzx>@;{-m^|J8|xolvR>{NmA`kUo+*;%GI5d zK1P?iQ@T^6oS{EU#V4fv=|Z}QN>L%T-`lb6=kb{FdHs35{=Z(Eh!q-0)Ff*{5JU_P zVupeDgW$ze$ARCkkt>1VO(QXAa|#3*>(0GUEjAOr5TxH4%w&Y87st%X<-W~9sAI>D z;Rpl*I_RJJak*TsO|yaMTpkYv#N~3bk+@YRQ_l&)*y1i*z+n_0UyXq1ST-A4%*V4b zQA{9V#njf8N~M5*PT;th^x!gyMToiA(bywEn2FF4VZ278lCB5ncVTrBHzq83MI}@b z4lU+l{VyzH1D*34I_Dw#)o?hRoY8Nd*>8=(G=qP#GyocYla|2P!NFmHIdzV?0?Hlc zGme)9V_`kWU|I`zcGSegWP!t3OsYJ`(6#VYFZL&n>l#cWY7XhI4g-A)4C6t<09V_@ zF?^?WHAZ0n-c%tP1FfE+vjHQ^kz|LK=j#x1#YDEHs^F0*K} z!wu<5Aho9`|Ba&}N|Md*K$L3P&P~DSFd*{M)YNq2j-6T<3bAFPs3FvMV9-U#pAPv)2G?kQ^23GiXQ1!MH(e*=ET;a zDy;SdUCEA9KCZQPBM&&^;el6Lpz;R8FN+klCb2!|KRT{ehFPA9S_1NC4glo>YOgYt zRL^rXm!1Gxv5yq(EV(O_$r$3jDm%;5tt_^c`9iB_!*l-FrU0bw+DY>8^$PoqiKitj zIk5AC{P05JsY{wL0hohQ$BG}c7umPV<#F?&q@K6NM>nKxQNP;h>u2oPkA^?qWleO1 zAesZfpl6g|V@2GkF6}D_ybN=LMwpwK)QEdcELZv&M9qk6Q1 zz?CK`F}cb!?)M=E2)P{^Gb-=53;PKJYzFPjGPyF^YrMPR9}?b?Jt zA6=+=x_J274E~!t8&gd6Xf&GbdxnNp;V47=)=@^as(xX|v-Mr6s2Qrsh#=ltt-|CP zZ*iP2_YM8*G+HenAb@pKknF{&x3YbI-qmZ3HD^1U&7nmUDQm6CYy{a*gBO(h^lDh- zFmc-OuNotN*Xs0~K&|$&-AKp^)2Y5<_Z*lAujQF38#%_B;%jo%sQ$&q^JYWSl*!iV8CGPH`$Z(%$##bO<(Cw z(@!0Er&C*xj${2_4BJhqFpJ(^Iy4L1O69+ z?qK1vBWE#e7u$BmHH+y!FQ8K~EB)F=FyecA{NnDg*-x161h7IHggoAW`{_!o%S}cK zuA*LD#hjm-;nsghV)z^%blS|)kFpCY*7RiQqLdBI5A>+!ocn|p>B~M$0k5@@?n8Ab zj-#)*wTo{fbcatcb(G%wwT`&|I^~SED(xt9a2PU6U_yvr;ri5S?Qd_=sHYXl^~+1m zc>GePdnm@lcr+2({oqqacO*+AFFRFf7`dYOtA5=R9xC#Nc5jS@7W}D{-~W^ExR0kI zF6cmyR9Rk0^F`Uj;F7GUT^3f{(7Vga%YLeoWg*J!@n8H0W&b^59B@pH-qzQeSKXBl z+HPTa@>hGbHtr{Lg+Ulcl7*I>9e>N(P`8X}JV(v%rd9s(+2 zVLQh7U6*BbLHi%Qs;V>o-&!d0Qm8hx^FEIY0Y@?ajs6%(%0bf6Nh{h7ju4e$lPSxy zP{v8JFH~swTJW_+%L6rY>2X}F!Tf+ER}3C>KIIR2r+6$XF*Q+}e=3Q4_f=oyUDRRC zEg67BAl#${!o7#jSlr_4fqh(ps{_T&)6pSloxAXMqqc!f6r-%Sjcry!(Cl-#p25aE!*31$~;Ko z%FC=Cuigh$Ht$F}MB@0Bm59X;)8H>bI-q;!p<^V^S(KKwZ4j#Xz}52*ebcR&;oxv4 zDn2TYqfUuli_5ZFV5AOKdGADvcsZ$@<>x-|=y1RQSugM9Z->j3!y9-S2jBkXMJ$x@ z;rB|gN4D#gCDOCDC^rfSS}XT&zMz#`VtMVFTMy#MKdI-U6V5z~2Wctbn2i{_-z!r$9HeC6At zsFBP^Vu&uvwCA}Jyp4jI^t(W>|2VG%^oVzgr)@iSxG&l?h|Pqz;B!)0??UXM3qI@* z(?n2=3+sB0^D+B(|D)+dYG;@}@|Uwe-rXz>SsEFcX9X8`-BMOWjNiYAE`h1*i+p~T zheLnN4tj082u$C z^Q`0`%sg^qH(^k9F2*$31mUImyZ`c9)(FMx-o_=mj=^s*r#(NN*8Ir!Jfhp4puBrf9u2{ z0s8fy2Df6S$Z0UGi>|SCMbDo}eo*0c2Hw=#c%&+2ukNUxGM|2{u;jXLkCh1V{N!j< z>BQah#JLj^|GrIf?sNoX>lY0|e%R#KMb&TekA&(qlK-yyWzElrSmk_j+>?a!=+oo8 z{r8kZ!7qQ3S9|FN#d&a~N5GDa{r;|0suQr@^D>0&uf^4#JmNokh-IkWbTc$;pkeTc zV&3`Xr?PguhvQes8emCy?mr(TDC8{EH5EF<4e)utd&4l4Ml>CW zx}aPJO=+iI@lmxQlh{ek;6GR7E}oFESycSq6S$eL;jBx10kcN1K9}14OfFwJAFQVB zKQ}}B%{w8n2{#-60&Yy)kzgG?J1h5{pKVQsI@&n9TOFS5rC+fwV>pj?#;xmG559a> zF{o=`hNu5P<)pKk6JVNU_=BZL=Oq=uzSWO~sJc7Y&Z?up8~K`yD6edCTB~!f{T-=v z!H62QqLV9qr=W@zbp@cS_d{c@V7gDaLLaFZM0)?5qDq=Fy-nZsX%N^?zgl_VPWwW) zX4yNQJVI{fD*>qn5N2~tb7C9BoTFjGo*q`wY#^LdP4l>EhI1k^>m5N(%LxC_E0-_q z0_yDv%%eSlcYcJMuS_2M&z~;`jlv&=J@`?jqHDqo7kpDLY_)l~Z8AY8_emvn-y{(u zm$%J3L%lU|2Df33LB0gB9u$7p{6=*y-fLa_Om90aq$ulG5nnki%L(k(E`SrTOeXhuxMGad%DUwHS*=xScak$t$Q)KBJ?ujHU zx}+jX%9B>^`Z;j6WlML}XXU50>P^>ofQLnllHp*h%c|$L?WH9Rp7&{%agLf6mX@4P zynwOnACo2(P?CUh0om=7FsWP+c^8#F>r?&ze3!Vli@oF=lRz(yNhC)23?I^bQB*NC zqeN|4m5lh5Ed?#Vd*{z?>L||0Wjv`tF!Qm~=U9O`>oz!5{aG}{LYDLl@Ib)uhAA1% zlYDQaBm-e+M%1vsfc|v_OzPT;ph|Z;MuWWtMzvsGiNpx@a~{}6pKJ2Uh(9qxNH$p0vy(XD)EQPZHj4$V4Z>*kMKQRsRp@`_*I-v3fnSL0Yv=z=x zk%m0w{*!cfR{?=GkRe;LT9n{CF|ou5ySuk`V}`%JA1H%T~J}f0RE%S$V*IGjrgmoS?!|fjuT%Z-|8waD1EDPD0N%YiHnTpTMWD8WZ%f`XUsf7Eljn=ToXWkQc z5s~{Uc_<}H=0OGXV{2@z8gZlG-+>((^i3My@M7BDDQzbsLBhG5p8=!x29!IgkG#oh zgv=l366m@lkDoB#2J($IACIaLn5BUmXbt>fclf@gXo{7Vu$~;F*c{XyTNZgk?bCKf zh|f8k!S5XpO{>&}R>!oxi;hZqX{A~rjFj<~&LkU6XjzZire7$Khn~Ubi(8eEA*YJ- z2=EDf6(-HSfYkGl?)k&fBBupm9FfLHWk%qZ*pHPfb zT`YP4y+9AT`q#F_@M1oidkdwuEB(U}jkz}v4OF{%_Lh6*|1MQWuv9W)E5zL`&^sM;x=vcs@9%#n- z6+wOvL|`H#Vu-dCS6!@)f%{OJw&SSG!fNDh>K=FSHOmqW<1hR*wk$WjRmqIAhPF)_ z&(^gmbDt*mF+f!(;mK0eLpV(9U@_V`5t|diY7N+v((i>avR8etl{?8m!+1!LhSNWA z!mz>}@8T5P3xn0VP;P*ZE2ul)zm 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