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