commit 9c29be09bf21d9760d617965dfaba95e6e770620 Author: cerberus Date: Mon Feb 23 17:56:46 2026 +0100 first commit diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..40a608f Binary files /dev/null and b/__pycache__/config.cpython-313.pyc differ diff --git a/__pycache__/config.cpython-314.pyc b/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000..5ead622 Binary files /dev/null and b/__pycache__/config.cpython-314.pyc differ diff --git a/config.py b/config.py new file mode 100644 index 0000000..92077a2 --- /dev/null +++ b/config.py @@ -0,0 +1,50 @@ +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 * + +from libqtile import qtile +from libqtile.backend.wayland.inputs import InputConfig + +# fixing the import warning +widget_defaults = widget_defaults +screens = screens +keys = keys +mouse = mouse +layouts = layouts +floating_layout = floating_layout +groups = groups + +# 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 + + +wmname = "Qtile" + +# Wayland configuration +if qtile.core.name == "X11": + term = "urvx" +elif qtile.core.name == "wayland": + term = "foot" + +wl_input_rules = { + "type:keyboard": InputConfig( + kb_repeat_delay=200, + kb_repeat_rate=60, + kb_layout="de", + kb_options="nodeadkeys", + ), +} +# xcursor theme (string or None) and size (integer) for Wayland backend +wl_xcursor_theme = None +wl_xcursor_size = 18 diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/__pycache__/__init__.cpython-313.pyc b/modules/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..6b00aa5 Binary files /dev/null and b/modules/__pycache__/__init__.cpython-313.pyc differ diff --git a/modules/__pycache__/__init__.cpython-314.pyc b/modules/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..3ab35a7 Binary files /dev/null and b/modules/__pycache__/__init__.cpython-314.pyc differ diff --git a/modules/__pycache__/groups.cpython-313.pyc b/modules/__pycache__/groups.cpython-313.pyc new file mode 100644 index 0000000..8737784 Binary files /dev/null and b/modules/__pycache__/groups.cpython-313.pyc differ diff --git a/modules/__pycache__/groups.cpython-314.pyc b/modules/__pycache__/groups.cpython-314.pyc new file mode 100644 index 0000000..6c5a6b1 Binary files /dev/null and b/modules/__pycache__/groups.cpython-314.pyc differ diff --git a/modules/__pycache__/hooks.cpython-313.pyc b/modules/__pycache__/hooks.cpython-313.pyc new file mode 100644 index 0000000..f477448 Binary files /dev/null and b/modules/__pycache__/hooks.cpython-313.pyc differ diff --git a/modules/__pycache__/hooks.cpython-314.pyc b/modules/__pycache__/hooks.cpython-314.pyc new file mode 100644 index 0000000..cb68d00 Binary files /dev/null and b/modules/__pycache__/hooks.cpython-314.pyc differ diff --git a/modules/__pycache__/keys.cpython-313.pyc b/modules/__pycache__/keys.cpython-313.pyc new file mode 100644 index 0000000..be44367 Binary files /dev/null and b/modules/__pycache__/keys.cpython-313.pyc differ diff --git a/modules/__pycache__/keys.cpython-314.pyc b/modules/__pycache__/keys.cpython-314.pyc new file mode 100644 index 0000000..98ad8c8 Binary files /dev/null and b/modules/__pycache__/keys.cpython-314.pyc differ diff --git a/modules/__pycache__/layouts.cpython-313.pyc b/modules/__pycache__/layouts.cpython-313.pyc new file mode 100644 index 0000000..59c1f83 Binary files /dev/null and b/modules/__pycache__/layouts.cpython-313.pyc differ diff --git a/modules/__pycache__/layouts.cpython-314.pyc b/modules/__pycache__/layouts.cpython-314.pyc new file mode 100644 index 0000000..2427448 Binary files /dev/null and b/modules/__pycache__/layouts.cpython-314.pyc differ diff --git a/modules/__pycache__/screens.cpython-313.pyc b/modules/__pycache__/screens.cpython-313.pyc new file mode 100644 index 0000000..381e285 Binary files /dev/null and b/modules/__pycache__/screens.cpython-313.pyc differ diff --git a/modules/__pycache__/screens.cpython-314.pyc b/modules/__pycache__/screens.cpython-314.pyc new file mode 100644 index 0000000..a967ea5 Binary files /dev/null and b/modules/__pycache__/screens.cpython-314.pyc differ diff --git a/modules/groups.py b/modules/groups.py new file mode 100644 index 0000000..30b3d27 --- /dev/null +++ b/modules/groups.py @@ -0,0 +1,161 @@ +from libqtile.config import Group, Key, Match, DropDown, ScratchPad +from libqtile.lazy import lazy +from .keys import keys, mod +import re + +group_screen_map = { + "0": 0, + "1": 0, + "2": 0, + "3": 0, + "4": 2, + "5": 2, + "6": 2, + "7": 1, + "8": 1, + "9": 1, + "f1": 2, + "f2": 1, + "f3": 2, + "f4": 1, + "f5": 1, + "f6": 1, + "f7": 2, + "f8": 0, + "f9": 0, + "f10": 0, + "f11": 0, + "f12": 0, +} + +groups = [ + Group( + name="0", + label="󰓓", + matches=[ + Match(wm_class=re.compile(r"^(steam)")), + Match(wm_class=re.compile(r"^(Minecraft*)")), + ], + ), + 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="󰲯"), + Group(name="9", label="󰲱"), + # Groups on function-keys + Group( + name="f1", + label="", + matches=[Match(wm_class=re.compile(r"^(firefox)$"))], + ), + Group( + name="f2", + label=" ", + matches=[ + Match(wm_class="discord"), + Match(wm_class=re.compile(r"^(com.github.th_ch.youtube_music)$")), + Match(wm_class="RMPC"), + ], + ), + Group( + name="f3", + label="", + matches=[Match(wm_class=re.compile(r"^(joplin)$"))], + ), + Group( + name="f4", + label="󱊮", + ), + Group( + name="f5", + label="󰝶 ", + matches=[Match(wm_class="AFFiNE")], + ), + Group( + name="f6", + label="󰟵 ", + matches=[Match(wm_class="bitwarden")], + ), + Group( + name="f7", + label="󰣀 ", + matches=[Match(wm_class=re.compile(r"^(XPipe)$"))], + ), + Group(name="f8", label="󱊲"), + Group(name="f9", label="󱊳"), + Group(name="f10", label="󱊴"), + Group(name="f11", label="󱊵"), + Group(name="f12", label="󱊶"), +] + + +def go_to_group(name: str): + def _inner(qtile): + screen = group_screen_map.get(name, 0) + if len(qtile.screens) <= 2: + qtile.groups_map[name].toscreen() + else: + qtile.focus_screen(screen) + qtile.groups_map[name].toscreen() + + return _inner + + +for i in groups: + keys.append(Key([mod], i.name, lazy.function(go_to_group(i.name)))) + + +def go_to_group_and_move_window(name: str): + def _inner(qtile): + screen = group_screen_map.get(name, 0) + if len(qtile.screens) <= 2: + qtile.current_window.togroup(name, switch_group=False) + else: + qtile.current_window.togroup(name, switch_group=False) + qtile.focus_screen(screen) + qtile.groups_map[name].toscreen() + + return _inner + + +for i in groups: + keys.append( + Key([mod, "shift"], i.name, lazy.function(go_to_group_and_move_window(i.name))) + ) + +for i in range(1, 12): + group_name = f"f{i}" + key = f"F{i}" + keys.append + +groups.append( + ScratchPad( + "scratchpad", + [ + DropDown( + "term", + "kitty", + width=0.7, + height=0.9, + x=0.15, + y=0.05, + opacity=0.8, + on_focus_lost_hide=True, + ), + 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..d8a4a87 --- /dev/null +++ b/modules/hooks.py @@ -0,0 +1,46 @@ +# _ _ _ _ _ +# __ _| |_(_) | ___ | |__ ___ ___ | | _____ +# / _` | __| | |/ _ \ | '_ \ / _ \ / _ \| |/ / __| +# | (_| | |_| | | __/ | | | | (_) | (_) | <\__ \ +# \__, |\__|_|_|\___| |_| |_|\___/ \___/|_|\_\___/ +# |_| +# by cerberus +# -------------------------------------------------------------------- + +# -------------------------------------------------------------------- +# Imports +# -------------------------------------------------------------------- +from libqtile import hook, qtile +import subprocess +import os.path +from libqtile.config import Match + +# -------------------------------------------------------------------- +# HOOK startup +# -------------------------------------------------------------------- +# Client management +FULLSCREEN_RULES = [Match(wm_class="flameshot")] + + +@hook.subscribe.startup_once +def autostart(): + autostartscript = "~/.config/qtile/res/scripts/autostart.sh" + home = os.path.expanduser(autostartscript) + subprocess.Popen([home]) + + +@hook.subscribe.startup_complete +def go_to_group1(): + qtile.focus_screen(0) + qtile.groups_map["1"].toscreen() + + +@hook.subscribe.client_managed +def force_fullscreen(client) -> None: + """ + Some clients won't start fullscreen (exclusive to wayland) + With this function we force clients defined in FULLSCREEN_RULES to enter fullscreen + """ + + if any(client.match(rule) for rule in FULLSCREEN_RULES): + client.fullscreen = True diff --git a/modules/keys.py b/modules/keys.py new file mode 100644 index 0000000..5392ebc --- /dev/null +++ b/modules/keys.py @@ -0,0 +1,161 @@ +# _ _ _ _ +# __ _| |_(_) | ___ | | _____ _ _ ___ +# / _` | __| | |/ _ \ | |/ / _ \ | | / __| +# | (_| | |_| | | __/ | < __/ |_| \__ \ +# \__, |\__|_|_|\___| |_|\_\___|\__, |___/ +# |_| |___/ +# by cerberus +# -------------------------------------------------------------------- + +# -------------------------------------------------------------------- +# Imports +# -------------------------------------------------------------------- + +from libqtile.config import Key, Drag, Click +from libqtile.lazy import lazy + +# from 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], "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"), + # 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"), + # MPD player + # NOTE: requires rmpc to be installed + Key(["control"], "XF86AudioPrev", lazy.spawn("rmpc prev"), desc="Previous Media"), + Key( + ["control"], + "XF86AudioPlay", + lazy.spawn("rmpc togglepause"), + desc="Play Pause Media", + ), + Key(["control"], "XF86AudioNext", lazy.spawn("rmpc next"), desc="Next Media"), + # KB_GROUP-Rofi Menus + Key([mod], "r", lazy.spawn("rofi -show drun -show-icons"), desc="Spawn Rofi D-Run"), + # KB_GROUP-ScratchPad + Key( + ["control"], + "1", + lazy.group["scratchpad"].dropdown_toggle("term"), + desc="PopUp Terminal", + ), + Key( + ["control"], + "2", + lazy.group["scratchpad"].dropdown_toggle("calc"), + desc="Calculator", + ), + # 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, "control"], + "a", + lazy.spawn("python /home/cerberus/.config/qtile/res/scripts/autoclicker.py"), + desc="Autoclicker On", + ), + Key( + [mod, "control"], + "s", + lazy.spawn("pkill -f autoclicker.py"), + desc="Autoclicker Off", + ), +] + +# -------------------------------------------------------------------- +# 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()), +] diff --git a/modules/layouts.py b/modules/layouts.py new file mode 100644 index 0000000..37c7a5b --- /dev/null +++ b/modules/layouts.py @@ -0,0 +1,80 @@ +# _ _ _ _ _ +# __ _| |_(_) | ___ | | __ _ _ _ ___ _ _| |_ ___ +# / _` | __| | |/ _ \ | |/ _` | | | |/ _ \| | | | __/ __| +# | (_| | |_| | | __/ | | (_| | |_| | (_) | |_| | |_\__ \ +# \__, |\__|_|_|\___| |_|\__,_|\__, |\___/ \__,_|\__|___/ +# |_| |___/ +# -------------------------------------------------------------------- + +# -------------------------------------------------------------------- +# 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.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, + lower_right=True, + **layout_defaults, + ), + layout.Spiral(**layout_defaults), + # layout.Stack(num_stacks=2), + # layout.MonadWide(**layout_defaults), + # layout.RatioTile(**layout_defaults), + # layout.Tile(name="Tile", **layout_defaults), + # layout.TreeTab(**layout_defaults), + layout.VerticalTile(**layout_defaults), + # 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(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"), + Match(wm_class="pavucontrol"), + Match(wm_class="org.gnome.FileRoller"), + Match(wm_class="lximage-qt"), + ], + **floating_layout_defaults, +) diff --git a/modules/screens.py b/modules/screens.py new file mode 100644 index 0000000..1cd3f73 --- /dev/null +++ b/modules/screens.py @@ -0,0 +1,257 @@ +# _ _ _ +# __ _| |_(_) | ___ ___ ___ _ __ ___ ___ _ __ ___ +# / _` | __| | |/ _ \ / __|/ __| '__/ _ \/ _ \ '_ \/ __| +# | (_| | |_| | | __/ \__ \ (__| | | __/ __/ | | \__ \ +# \__, |\__|_|_|\___| |___/\___|_| \___|\___|_| |_|___/ +# |_| +# -------------------------------------------------------------------- +# Imports +# -------------------------------------------------------------------- +from libqtile.config import Screen +from libqtile import bar +from libqtile.widget import mpd2widget +from libqtile.lazy import lazy +from qtile_extras import widget +from qtile_extras.widget.groupbox2 import GroupBoxRule + +# from plugins.notifications import Notifier +from plugins.graphical_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 res.themes.colors import gruvbox_dark + + +# -------------------------------------------------------- +# GroupBox2 rules +# -------------------------------------------------------- +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), + ] + + # 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="Roboto Flex", + fontsize=20, + foreground=gruvbox_dark["fg1"], +) +extension_defaults = widget_defaults.copy() + +# -------------------------------------------------------- +# Screens +# -------------------------------------------------------- +bar.Bar +screens = [ + Screen( + # Center Screen + top=bar.Bar( + [ + widget.TextBox( + text="", + fontsize=24, + 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", + "0", + "f8", + "f9", + "f10", + "f11", + "f12", + ], + hide_unused=True, + rules=get_groupbox_rules(monitor_specific=False), + ), + widget.Spacer(), + widget.Systray( + icon_size=21, + ), + widget.Spacer(length=6), + widget.Clock(mouse_callbacks={"Button1": lazy.function(calendar)}), + widget.Spacer(length=2), + widget.TextBox( + font="Open Sans", + fontsize=20, + text=" ", + mouse_callbacks={"Button1": lazy.function(power_menu)}, + ), + 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], + ), + ), + Screen( + # Right Screen + top=bar.Bar( + [ + widget.TextBox( + text="", + fontsize=24, + foreground=gruvbox_dark["blue"], + mouse_callbacks={"Button1": lazy.function(start_menu)}, + ), + widget.GroupBox2( + padding=6, + fontsize=22, + margin=7, + font="Open Sans", + center_aligned=True, + visible_groups=["7", "8", "9", "f2", "f4", "f5", "f6"], + hide_unused=True, + rules=get_groupbox_rules(monitor_specific=False), + ), + widget.Spacer(status_format="{play_status} {artist}/{title}"), + widget.WidgetBox( + fontsize=22, + text_closed="󱤟", + text_open="󱤠", + widgets=[ + widget.Memory( + format="󰍛 {MemPercent}%", + font="Open Sans", + ), + widget.CPU( + format="󰻠 {load_percent}%", + font="Open Sans", + ), + ], + ), + widget.Spacer(length=6), + widget.Clock(mouse_callbacks={"Button1": lazy.function(calendar)}), + widget.Spacer(length=2), + widget.TextBox( + font="Open Sans", + fontsize=20, + text=" ", + mouse_callbacks={"Button1": lazy.function(power_menu)}, + ), + ], + background=gruvbox_dark["bg0_hard"], + opacity=0.75, + size=32, + margin=[3, 3, 0, 3], + ), + ), + Screen( + # Left Screen + top=bar.Bar( + [ + widget.TextBox( + text="", + fontsize=24, + foreground=gruvbox_dark["blue"], + mouse_callbacks={"Button1": lazy.function(start_menu)}, + ), + widget.GroupBox2( + padding=6, + fontsize=22, + font="Open Sans", + center_aligned=True, + visible_groups=[ + "4", + "5", + "6", + "f1", + "f7", + "f3", + ], + 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.Clock(mouse_callbacks={"Button1": lazy.function(calendar)}), + widget.Spacer(length=2), + widget.TextBox( + fontsize=20, + text=" ", + mouse_callbacks={"Button1": lazy.function(power_menu)}, + ), + ], + background=gruvbox_dark["bg0_hard"], + opacity=0.75, + size=32, + margin=[3, 3, 0, 3], + ), + ), +] +notifier = Notifier( + x=int((2560 / 2) - (350 / 2)), + y=38, + width=350, + height=80, + 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=10, + vertical_padding=10, + opacity=0.65, + border_width=0, + font="Open Sans", + font_size=16, + overflow="more_width", + fullscreen="show", + screen=2, + actions=True, + wrap=True, +) diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/__pycache__/__init__.cpython-313.pyc b/plugins/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..7fe6595 Binary files /dev/null and b/plugins/__pycache__/__init__.cpython-313.pyc differ diff --git a/plugins/__pycache__/__init__.cpython-314.pyc b/plugins/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..3322986 Binary files /dev/null and b/plugins/__pycache__/__init__.cpython-314.pyc differ diff --git a/plugins/__pycache__/graphical_notifications.cpython-313.pyc b/plugins/__pycache__/graphical_notifications.cpython-313.pyc new file mode 100644 index 0000000..172ff04 Binary files /dev/null and b/plugins/__pycache__/graphical_notifications.cpython-313.pyc differ diff --git a/plugins/__pycache__/graphical_notifications.cpython-314.pyc b/plugins/__pycache__/graphical_notifications.cpython-314.pyc new file mode 100644 index 0000000..2025879 Binary files /dev/null and b/plugins/__pycache__/graphical_notifications.cpython-314.pyc differ diff --git a/plugins/__pycache__/notifications.cpython-313.pyc b/plugins/__pycache__/notifications.cpython-313.pyc new file mode 100644 index 0000000..770ad75 Binary files /dev/null and b/plugins/__pycache__/notifications.cpython-313.pyc differ diff --git a/plugins/graphical_notifications.py b/plugins/graphical_notifications.py new file mode 100644 index 0000000..a899ac8 --- /dev/null +++ b/plugins/graphical_notifications.py @@ -0,0 +1,462 @@ +""" +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", 4, "Padding at sides of text."), + ("vertical_padding", 4, "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() + + # Icon logic + 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 + + # Notification text drawing + 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/plugins/notifications.py b/plugins/notifications.py new file mode 100644 index 0000000..da61987 --- /dev/null +++ b/plugins/notifications.py @@ -0,0 +1,458 @@ +""" +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/plugins/notifications_copy.py b/plugins/notifications_copy.py new file mode 100644 index 0000000..69e9fbe --- /dev/null +++ b/plugins/notifications_copy.py @@ -0,0 +1,456 @@ +""" +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.'), + ('font_size', 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.font_size // 2 + if self.vertical_padding is None: + self.vertical_padding = self.font_size // 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.foreground = 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.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() + popup.win.bring_to_front() # Patch to bring the notification on top of all windows + 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/__init__.py b/popups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/popups/__pycache__/__init__.cpython-313.pyc b/popups/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..9c1715b Binary files /dev/null and b/popups/__pycache__/__init__.cpython-313.pyc differ diff --git a/popups/__pycache__/__init__.cpython-314.pyc b/popups/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..a5719b0 Binary files /dev/null and b/popups/__pycache__/__init__.cpython-314.pyc differ diff --git a/popups/__pycache__/brightness_notification.cpython-313.pyc b/popups/__pycache__/brightness_notification.cpython-313.pyc new file mode 100644 index 0000000..4567d51 Binary files /dev/null and b/popups/__pycache__/brightness_notification.cpython-313.pyc differ diff --git a/popups/__pycache__/calendar.cpython-313.pyc b/popups/__pycache__/calendar.cpython-313.pyc new file mode 100644 index 0000000..e282637 Binary files /dev/null and b/popups/__pycache__/calendar.cpython-313.pyc differ diff --git a/popups/__pycache__/calendar.cpython-314.pyc b/popups/__pycache__/calendar.cpython-314.pyc new file mode 100644 index 0000000..e6a8749 Binary files /dev/null and b/popups/__pycache__/calendar.cpython-314.pyc differ diff --git a/popups/__pycache__/monitor.cpython-313.pyc b/popups/__pycache__/monitor.cpython-313.pyc new file mode 100644 index 0000000..9b7803f Binary files /dev/null and b/popups/__pycache__/monitor.cpython-313.pyc differ diff --git a/popups/__pycache__/monitor.cpython-314.pyc b/popups/__pycache__/monitor.cpython-314.pyc new file mode 100644 index 0000000..31c4aad Binary files /dev/null and b/popups/__pycache__/monitor.cpython-314.pyc differ diff --git a/popups/__pycache__/mpris2_layout.cpython-313.pyc b/popups/__pycache__/mpris2_layout.cpython-313.pyc new file mode 100644 index 0000000..92eaa20 Binary files /dev/null and b/popups/__pycache__/mpris2_layout.cpython-313.pyc differ diff --git a/popups/__pycache__/mpris2_layout.cpython-314.pyc b/popups/__pycache__/mpris2_layout.cpython-314.pyc new file mode 100644 index 0000000..eefa5c4 Binary files /dev/null and b/popups/__pycache__/mpris2_layout.cpython-314.pyc differ diff --git a/popups/__pycache__/network.cpython-313.pyc b/popups/__pycache__/network.cpython-313.pyc new file mode 100644 index 0000000..37bfb98 Binary files /dev/null and b/popups/__pycache__/network.cpython-313.pyc differ diff --git a/popups/__pycache__/network.cpython-314.pyc b/popups/__pycache__/network.cpython-314.pyc new file mode 100644 index 0000000..058f24d Binary files /dev/null and b/popups/__pycache__/network.cpython-314.pyc differ diff --git a/popups/__pycache__/powermenu.cpython-313.pyc b/popups/__pycache__/powermenu.cpython-313.pyc new file mode 100644 index 0000000..385880d Binary files /dev/null and b/popups/__pycache__/powermenu.cpython-313.pyc differ diff --git a/popups/__pycache__/powermenu.cpython-314.pyc b/popups/__pycache__/powermenu.cpython-314.pyc new file mode 100644 index 0000000..63a9031 Binary files /dev/null and b/popups/__pycache__/powermenu.cpython-314.pyc differ diff --git a/popups/__pycache__/powermenu_sub.cpython-313.pyc b/popups/__pycache__/powermenu_sub.cpython-313.pyc new file mode 100644 index 0000000..d80918d Binary files /dev/null and b/popups/__pycache__/powermenu_sub.cpython-313.pyc differ diff --git a/popups/__pycache__/powermenu_sub.cpython-314.pyc b/popups/__pycache__/powermenu_sub.cpython-314.pyc new file mode 100644 index 0000000..d125df6 Binary files /dev/null and b/popups/__pycache__/powermenu_sub.cpython-314.pyc differ diff --git a/popups/__pycache__/settings.cpython-313.pyc b/popups/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..eb866b5 Binary files /dev/null and b/popups/__pycache__/settings.cpython-313.pyc differ diff --git a/popups/__pycache__/settings.cpython-314.pyc b/popups/__pycache__/settings.cpython-314.pyc new file mode 100644 index 0000000..22b2c91 Binary files /dev/null and b/popups/__pycache__/settings.cpython-314.pyc differ diff --git a/popups/__pycache__/start_menu.cpython-313.pyc b/popups/__pycache__/start_menu.cpython-313.pyc new file mode 100644 index 0000000..d2f19fa Binary files /dev/null and b/popups/__pycache__/start_menu.cpython-313.pyc differ diff --git a/popups/__pycache__/start_menu.cpython-314.pyc b/popups/__pycache__/start_menu.cpython-314.pyc new file mode 100644 index 0000000..8f67f6b Binary files /dev/null and b/popups/__pycache__/start_menu.cpython-314.pyc differ diff --git a/popups/__pycache__/volume_notification.cpython-313.pyc b/popups/__pycache__/volume_notification.cpython-313.pyc new file mode 100644 index 0000000..058b48e Binary files /dev/null and b/popups/__pycache__/volume_notification.cpython-313.pyc differ diff --git a/popups/__pycache__/volume_notification.cpython-314.pyc b/popups/__pycache__/volume_notification.cpython-314.pyc new file mode 100644 index 0000000..6f022ed Binary files /dev/null and b/popups/__pycache__/volume_notification.cpython-314.pyc differ 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/monitor.py b/popups/monitor.py new file mode 100644 index 0000000..6ac460e --- /dev/null +++ b/popups/monitor.py @@ -0,0 +1,31 @@ +from libqtile import qtile + +from qtile_extras import widget +from qtile_extras.popup.toolkit import ( + PopupRelativeLayout, + PopupWidget, +) + +from res.themes.colors import gruvbox_dark + + +def monitor(qtile): + layout = PopupRelativeLayout( + qtile, + rows=7, + cols=9, + width=600, + height=420, + opacity=0.8, + hide_on_mouse_leave=True, + close_on_click=False, + border_width=0, + background=gruvbox_dark["bg0_soft"], + controls=[], + ) + layout.show( + relative_to=5, + relative_to_bar=True, + # y=3, + # x=-3, + ) 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..18709bf --- /dev/null +++ b/popups/start_menu.py @@ -0,0 +1,186 @@ +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 +from popups.monitor import monitor + + +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, + pos_y=0, + height=0.3, + width=0.3, + 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) + # pos_x=0, + # pos_y=0, + # width=0.3, + # height=0.3, + # can_focus=True, + # v_align="middle", + # h_align="center", + # background=gruvbox_dark["green"], + # ), + 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 steam://open/bigpicture") + }, + ), + 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=22, + 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.function(monitor)}, + ), + 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")}, + ), + 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) + 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..1e65367 Binary files /dev/null and b/res/images/standby_rotated.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/autoclicker.py b/res/scripts/autoclicker.py new file mode 100644 index 0000000..0caf64d --- /dev/null +++ b/res/scripts/autoclicker.py @@ -0,0 +1,37 @@ +import time +import subprocess +from pynput.keyboard import Controller as KeyboardController, Key +from pynput.mouse import Controller as MouseController, Button + + +keyboard = KeyboardController() +mouse = MouseController() + +# Intervall für Rechtsklick in Sekunden +rechtsklick_intervall = 4.50 + + +def shift_spam_mit_rechtsklick(): + letzter_klick = time.time() + try: + while True: + # Shift drücken und loslassen + # keyboard.press(Key.shift) + # time.sleep(0.05) + # keyboard.release(Key.shift) + + # Kurze Pause zwischen den Shift-Spams + # time.sleep(0.1) + + # Zeit für Rechtsklick? + jetzt = time.time() + if jetzt - letzter_klick >= rechtsklick_intervall: + mouse.click(Button.left) + letzter_klick = jetzt + + except KeyboardInterrupt: + print("Skript beendet.") + + +if __name__ == "__main__": + shift_spam_mit_rechtsklick() diff --git a/res/scripts/autostart.sh b/res/scripts/autostart.sh new file mode 100755 index 0000000..2ffa68a --- /dev/null +++ b/res/scripts/autostart.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# ___ _____ ___ _ _____ ____ _ _ +# / _ \_ _|_ _| | | ____| / ___|| |_ __ _ _ __| |_ +# | | | || | | || | | _| \___ \| __/ _` | '__| __| +# | |_| || | | || |___| |___ ___) | || (_| | | | |_ +# \__\_\|_| |___|_____|_____| |____/ \__\__,_|_| \__| +# +# by cerberus +# ----------------------------------------------------- + +# ----------------------------------------------------- +# Essentials +# ----------------------------------------------------- +# Load polkit agent +gnome-keyring-daemon --start --components=pkcs11,secrets,ssh & +/usr/lib/mate-polkit/polkit-mate-authentication-agent-1 & +mpd & + +# ----------------------------------------------------- +# Configure Screens +# ----------------------------------------------------- +"$HOME"/.screenlayout/screens.sh & +# ----------------------------------------------------- +# Autostart Applications +# ----------------------------------------------------- +picom & # Compositor +nitrogen --restore & # Wallpaper Manager +copyq & # Clipboard Manager +flameshot & # Screenshot Tool +discord & # Discord +steam -silent & # Steam +firefox & # Firefox +youtube-music & # YT-Music +bitwarden-desktop & # Bitwarden Passwordmanager +joplin & # Note Taking +affine & # Hand-Written Notes +"$HOME"/Documents/scripts/wacom_screen_config.sh & # Grpahic tablet +# kitty --app-id "RMPC" --execute rmpc --theme=.config/rmpc/gruvbox.ron & +sleep 1 & +qtile cmd-obj -o cmd -f reload_config diff --git a/res/scripts/keybinds.py b/res/scripts/keybinds.py new file mode 100755 index 0000000..2c0fcf8 --- /dev/null +++ b/res/scripts/keybinds.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import re +from pathlib import Path + +# Define the file path using Path and expand user directory +file_path = Path("~/.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/__pycache__/colors.cpython-313.pyc b/res/themes/__pycache__/colors.cpython-313.pyc new file mode 100644 index 0000000..b5ccfb8 Binary files /dev/null and b/res/themes/__pycache__/colors.cpython-313.pyc differ diff --git a/res/themes/__pycache__/colors.cpython-314.pyc b/res/themes/__pycache__/colors.cpython-314.pyc new file mode 100644 index 0000000..c3169ef Binary files /dev/null and b/res/themes/__pycache__/colors.cpython-314.pyc differ 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