diff --git a/config.py b/config.py
index 7d5f49f..2959508 100644
--- a/config.py
+++ b/config.py
@@ -53,12 +53,12 @@ except ImportError as e:
try:
- logger.info("Initializing theme...")
+ logger.warning("Initializing theme...")
# Initialize the Theme
Theme.initialize()
- logger.info("Initialize done")
+ logger.warning("Initialize done")
- logger.info("Hooking theme into callbacks...")
+ logger.warning("Hooking theme into callbacks...")
# Hook theme into all hooks we know of
hook.subscribe.startup_once(Theme.callback_startup_once)
hook.subscribe.startup(Theme.callback_startup)
@@ -82,10 +82,11 @@ try:
hook.subscribe.selection_notify(Theme.callback_selection_notify)
hook.subscribe.selection_change(Theme.callback_selection_change)
hook.subscribe.screen_change(Theme.callback_screen_change)
+ hook.subscribe.screens_reconfigured(Theme.callback_screens_reconfigured)
hook.subscribe.current_screen_change(Theme.callback_current_screen_change)
- logger.info("Hooking done")
+ logger.warning("Hooking done")
- logger.info("Initializing theme variables")
+ logger.warning("Initializing theme variables")
# Initialize variables from theme
keys = Theme.keys
mouse = Theme.mouse
@@ -104,7 +105,8 @@ try:
focus_on_window_activation = Theme.focus_on_window_activation
extensions = Theme.extensions
wmname = Theme.wmname
- logger.info("Variable initialization done")
+ reconfigure_screens = Theme.reconfigure_screens
+ logger.warning("Variable initialization done")
except Exception as e:
Theme = None
Config = None
diff --git a/kuro/base.py b/kuro/base.py
index 221d1fd..15ce41d 100644
--- a/kuro/base.py
+++ b/kuro/base.py
@@ -2,6 +2,9 @@ from libqtile import layout as libqtile_layout, layout, bar, widget
from libqtile.lazy import lazy
from libqtile.config import Key, Group, Screen, Drag, Click, Match
+# Initialize logging
+from libqtile.log_utils import logger
+
class BaseConfig:
@classmethod
@@ -15,10 +18,11 @@ class BaseConfig:
class BaseTheme:
# Changing variables initialized by function
keys = None
+ mouse = None
groups = None
layouts = None
widget_defaults = None
- screens = None
+ screens = []
qtile = None
# 'Static' variables
@@ -41,6 +45,7 @@ class BaseTheme:
auto_fullscreen = True
focus_on_window_activation = "smart"
extensions = []
+ reconfigure_screens = True
# XXX: Gasp! We're lying here. In fact, nobody really uses or cares about this
# string besides java UI toolkits; you can see several discussions on the
@@ -56,11 +61,17 @@ class BaseTheme:
wmname = "LG3D"
def initialize(self):
+ logger.info("Initializing widget defaults...")
self.widget_defaults = self.init_widget_defaults()
+ logger.info("Initializing keys...")
self.keys = self.init_keys()
+ logger.info("Initializing groups...")
self.groups = self.init_groups()
+ logger.info("Initializing layouts...")
self.layouts = self.init_layouts()
+ logger.info("Initializing screens...")
self.screens = self.init_screens()
+ logger.info("Initializing mouse...")
self.mouse = self.init_mouse()
def init_keys(self):
@@ -95,6 +106,15 @@ class BaseTheme:
Key(["mod4"], "r", lazy.spawncmd()),
]
+ def init_mouse(self):
+ return [
+ Drag(["mod4"], "Button1", lazy.window.set_position_floating(),
+ start=lazy.window.get_position()),
+ Drag(["mod4"], "Button3", lazy.window.set_size_floating(),
+ start=lazy.window.get_size()),
+ Click(["mod4"], "Button2", lazy.window.bring_to_front())
+ ]
+
def init_groups(self):
groups = [Group(i) for i in "asdfuiop"]
for i in groups:
@@ -138,15 +158,6 @@ class BaseTheme:
),
]
- def init_mouse(self):
- return [
- Drag(["mod4"], "Button1", lazy.window.set_position_floating(),
- start=lazy.window.get_position()),
- Drag(["mod4"], "Button3", lazy.window.set_size_floating(),
- start=lazy.window.get_size()),
- Click(["mod4"], "Button2", lazy.window.bring_to_front())
- ]
-
# Callbacks
def callback_startup_once(self, *args, **kwargs):
pass
@@ -220,5 +231,8 @@ class BaseTheme:
def callback_screen_change(self, *args, **kwargs):
pass
+ def callback_screens_reconfigured(self, *args, **kwargs):
+ pass
+
def callback_current_screen_change(self, *args, **kwargs):
pass
diff --git a/kuro/config.py b/kuro/config.py
index 5bc2ce7..43ff6e5 100644
--- a/kuro/config.py
+++ b/kuro/config.py
@@ -14,22 +14,39 @@ class Config(BaseConfig):
inactive_light = "#777777"
inactive_dark = "#333333"
- # Default Applications
- app_terminal = "ghostty"
- app_terminal_init = "ghostty --gtk-single-instance=true --quit-after-last-window-close=false --initial-window=true"
- app_launcher = "/home/kevin/bin/dmenu_wal.sh"
- web_browser = "firefox"
- file_manager = "thunar"
- app_chat = "/usr/bin/rambox"
- app_irc = ""
- app_mail = "thunderbird"
- app_music = "spotify"
+ # Predefined commands
cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10"
cmd_brightness_down = "sudo /usr/bin/xbacklight -dec 10"
+ cmd_screenshot = "/home/kevin/bin/screenshot.sh"
+ cmd_alt_screenshot = "/home/kevin/bin/screenshot.sh"
lock_command = "bash /home/kevin/bin/lock.sh"
+ cliphistory_command = "/home/kevin/bin/cliphistory.sh"
+
+ # Default Applications
+ app_terminal = "ghostty"
+ app_launcher = "wofi --show drun,run"
+ file_manager = "thunar"
visualizer_app = "glava"
- cmd_screenshot = "xfce4-screenshooter -r -c -d 1"
- cmd_alt_screenshot = "xfce4-screenshooter -w -c -d 0"
+ web_browser = "firefox"
+
+ # Autostart applications
+ apps_autostart_group = [
+ {'group': "", 'command': ["firefox"]},
+ {'group': "", 'command': ["ghostty", "--gtk-single-instance=true", "--quit-after-last-window-close=false", "--initial-window=true"]},
+ {'group': "", 'command': ["/usr/bin/rambox"]},
+ {'group': "", 'command': ["thunar"]},
+ {'group': "", 'command': ["thunderbird"]},
+ {'group': "", 'command': ["spotify"]},
+ ]
+ apps_autostart = [
+ # ["ulauncher", "--hide-window", "--no-window-shadow"], # App launcher background daemon
+ ["mako"], # Notification daemon
+ ["kanshi"], # Display hotplug
+ ["wl-paste", "--watch", "cliphist", "store"], # Clipboard manager
+ ["/usr/lib/kdeconnectd"], # KDE Connect daemon
+ ["kdeconnect-indicator"], # KDE Connect tray
+ ["vorta"], # Vorta backup scheduler
+ ]
# Keyboard commands
cmd_media_play = "playerctl -i kdeconnect play-pause"
@@ -44,9 +61,10 @@ class Config(BaseConfig):
cmd_monitor_mode_day = "bash /home/kevin/bin/monitor_day.sh"
cmd_monitor_mode_night = "bash /home/kevin/bin/monitor_night.sh"
cmd_monitor_mode_alt = "bash /home/kevin/bin/monitor_gamenight.sh"
+ cmd_reconfigure_screens = "kanshictl reload"
# Commands
- wallpaper_config_command = "/home/kevin/bin/wal-nitrogen-noupdate"
+ wallpaper_config_command = "/bin/true" # TODO: Remove
# Images
desktop_bg = "/home/kevin/Pictures/wallpapers/desktop.png"
@@ -85,7 +103,8 @@ class Config(BaseConfig):
# Bar variables
bar_background = background
- bar_opacity = 0.8
+ bar_rgba_opacity = "AA"
+ bar_opacity = 1.0
bar_hover_opacity = 1
# Groupbox variables
@@ -106,6 +125,7 @@ class Config(BaseConfig):
tasklist_urgent_border = highlight
tasklist_font = "Noto Sans"
tasklist_fontsize = 11
+ tasklist_rounded = False
# Thermal indicator variables
thermal_threshold = 75
@@ -125,10 +145,11 @@ class Config(BaseConfig):
battery_theme_path = "/home/kevin/.config/qtile/kuro/resources/battery"
battery_update_delay = 5
- # Wifi variables
+ # Network variables
wifi_interface = "wifi0"
wifi_theme_path = "/home/kevin/.config/qtile/kuro/resources/wifi"
wifi_update_interval = 5
+ wired_interface = "br1"
# GPU variables
gpu_theme_path = "/home/kevin/.config/qtile/kuro/resources/gpu"
@@ -137,9 +158,10 @@ class Config(BaseConfig):
volume_font = "Noto Sans"
volume_fontsize = 11
volume_theme_path = "/home/kevin/.config/qtile/kuro/resources/volume"
- volume_pulse_sink = "alsa_output.usb-Burr-Brown_from_TI_USB_Audio_CODEC-00.analog-stereo-output"
- volume_pulse_sink2 = "alsa_output.pci-0000_0d_00.4.analog-stereo"
-
+ volume_pulse_sinks = [
+ "alsa_output.usb-Burr-Brown_from_TI_USB_Audio_CODEC-00.analog-stereo-output",
+ "alsa_output.pci-0000_0d_00.4.analog-stereo",
+ ]
volume_is_bluetooth_icon = False
volume_update_interval = 0.2
@@ -163,9 +185,6 @@ class Config(BaseConfig):
laptop_screen_nvidia = "eDP-1-1"
laptop_screen_intel = "eDP1"
- # Keyboard colors
- do_keyboard_updates = False
-
# Show audio visualizer
show_audio_visualizer = False
kill_unnecessary_glava_processes = True
@@ -179,9 +198,5 @@ class Config(BaseConfig):
# Show battery widget
show_battery_widget = False
- # Audio control applications
- # apps_audio = ["pavucontrol"]
- apps_audio_afterstart = []
-
# Comma-separated list of ignored players in the media widget
media_ignore_players = "kdeconnect"
diff --git a/kuro/theme.py b/kuro/theme.py
index b83e0d6..8bafb1d 100644
--- a/kuro/theme.py
+++ b/kuro/theme.py
@@ -2,37 +2,42 @@ import json
import os
import random
import datetime
+import socket
+import subprocess
+from typing import Optional
+from libqtile.backend.base import Window
+from libqtile.backend.wayland.layer import LayerStatic
+from libqtile.backend.wayland.xwindow import XWindow as WaylandXWindow, XStatic as WaylandXStatic
+from libqtile.backend.x11.window import XWindow as XorgXWindow
# Initialize logging
from libqtile.log_utils import logger
-logger.error("Importing qtile theme requirements...")
+logger.warning("Importing qtile theme requirements...")
from libqtile.config import Key, Screen, Group, Drag, Click, Match
-from libqtile.command.base import expose_command
from libqtile.lazy import lazy
from libqtile import layout, bar, widget, qtile
+from qtile_extras import widget as extra_widget
-logger.error("Importing theme util functions...")
+logger.warning("Importing theme util functions...")
# Import theme util functions
from xcffib.xproto import WindowError
-logger.error("Importing kuro utils...")
+logger.warning("Importing kuro utils...")
import kuro.utils.widgets
from kuro.utils import general as utils
-logger.error("Importing variables and other utils...")
+logger.warning("Importing variables and other utils...")
# Import variables
from kuro.base import BaseTheme
from kuro.utils.general import display_wm_class, test_popups
-from kuro.utils.kb_backlight import handle_focus_change as kb_handle_focus_change
from kuro.utils import layouts as kuro_layouts
-from kuro.utils.windows import KuroStatic
-logger.error("Importing configuration...")
+logger.warning("Importing configuration...")
try:
from kuro.config import Config
@@ -43,88 +48,69 @@ except ImportError:
Config = None
raise ImportError("Could not load theme Config or BaseConfig!")
-logger.error("Imports done")
+logger.warning("Imports done")
+
class Kuro(BaseTheme):
# Shorthand for modifier key
mod = Config.get("modifier", "mod4")
- # Show debug messages
- debug = Config.get('debug', False)
- debug_textfields = []
- debug_bars = []
-
# Screen count
num_screens = 0
# Top bars
topbars = []
- # Visualizers
- audio_visualizers = []
-
# Static windows
static_windows = []
# Current wallpaper path
current_wallpaper = None
- # Whether or not to perform keyboard backlight updates
- do_keyboard_updates = True
-
# Window manager name
wmname = "qtile"
+ startup_completed = False
+ autostart_app_rules = {}
+
# Floating layout override
- floating_layout = kuro_layouts.KuroFloating(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(title='branchdialog'), # gitk
- Match(title='pinentry'), # GPG key password entry
- Match(title='origin.exe', wm_class='Wine'), # Wine Origin game launcher
- ])
+ floating_layout = kuro_layouts.KuroFloating(
+ border_width=0,
+ border_focus="#000000",
+ border_normal="#000000",
+ 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(title='branchdialog'), # gitk
+ Match(title='pinentry'), # GPG key password entry
+ Match(title='origin.exe', wm_class='Wine'), # Wine Origin game launcher
- def set_debug_text(self, text):
- for field in self.debug_textfields:
- field.text = text
- for bar in self.debug_bars:
- if qtile is not None:
- bar.draw()
-
- def log_debug(self, text):
- if Config.get('verbose', False):
- self.set_debug_text(text)
- logger.debug(text)
-
- def log_info(self, text):
- self.set_debug_text(text)
- logger.info(text)
+ # Homebank popups
+ Match(title='Add transaction', wm_class='homebank'),
+ Match(title='Edit transaction', wm_class='homebank'),
+ Match(title='Inherit transaction', wm_class='homebank'),
+ Match(title='Multiple edit transactions', wm_class='homebank'),
+ Match(title='Transaction splits', wm_class='homebank'),
+ ]
+ )
def initialize(self):
- logger.error("Initializing Kuro theme...")
- self.log_debug("Initializing Kuro Theme...")
-
# Update color scheme
+ logger.warning("Initializing colorscheme...")
self.initialize_colorscheme()
- # Set settings
- self.do_keyboard_updates = Config.get("do_keyboard_updates", True)
-
+ logger.warning("Initializing superclass...")
super(Kuro, self).initialize()
- self.update()
-
- def update(self):
- # Update keys with keys for groups and layouts
+ logger.warning("Updating keys for groups and layouts...")
self.update_keys()
def init_keys(self):
- self.log_debug("Initializing keys")
-
+ logger.warning("Initializing keys")
return [
# Switch between windows in current stack pane
Key([self.mod], "k", lazy.layout.down()),
@@ -146,9 +132,6 @@ class Kuro(BaseTheme):
# Floating toggle
Key([self.mod, "shift"], 'f', lazy.window.toggle_floating()),
- # Pinned toggle
- Key([self.mod, "shift"], 'p', lazy.function(self.toggle_pinned)),
-
# Toggle between split and unsplit sides of stack.
# Split = all windows displayed
# Unsplit = 1 window displayed, like Max layout, but still with
@@ -164,8 +147,8 @@ class Kuro(BaseTheme):
# Super-B to start webbrowser
Key([self.mod], "b", lazy.spawn(Config.get('web_browser', "xterm links"))),
- # Super-F to start file manager
-# Key([self.mod], "f", lazy.spawn(Config.get('file_m4anager', "thunar"))),
+ # Super-T to start file manager
+ Key([self.mod], "t", lazy.spawn(Config.get('file_manager', "thunar"))),
# Super-Shift-R to start spawncmd
Key([self.mod, "shift"], "r", lazy.spawncmd()),
@@ -173,10 +156,6 @@ class Kuro(BaseTheme):
# Lock shortcut
Key([self.mod], "l", lazy.spawn(Config.get('lock_command', "i3lock"))),
- # Backlight keys
- Key([], "XF86MonBrightnessUp", lazy.spawn(Config.get('cmd_brightness_up', 'xbacklight -inc 10'))),
- Key([], "XF86MonBrightnessDown", lazy.spawn(Config.get('cmd_brightness_down', 'xbacklight -dec 10'))),
-
# Display modes
Key([self.mod], "Prior", lazy.spawn(Config.get('cmd_monitor_mode_3s144', 'true'))),
Key([self.mod], "Next", lazy.spawn(Config.get('cmd_monitor_mode_3s60', 'true'))),
@@ -184,6 +163,10 @@ class Kuro(BaseTheme):
Key([self.mod], "End", lazy.spawn(Config.get('cmd_monitor_mode_night', 'true'))),
Key([self.mod], "Insert", lazy.spawn(Config.get('cmd_monitor_mode_alt', 'true'))),
+ # Backlight keys
+ Key([], "XF86MonBrightnessUp", lazy.spawn(Config.get('cmd_brightness_up', 'xbacklight -inc 10'))),
+ Key([], "XF86MonBrightnessDown", lazy.spawn(Config.get('cmd_brightness_down', 'xbacklight -dec 10'))),
+
# Media keys
Key([], "XF86AudioPlay", lazy.spawn(Config.get('cmd_media_play', 'true'))),
Key([], "XF86AudioNext", lazy.spawn(Config.get('cmd_media_next', 'true'))),
@@ -200,27 +183,31 @@ class Kuro(BaseTheme):
# Alt screenshot
Key([self.mod], "Print", lazy.spawn(Config.get('cmd_alt_screenshot', 'xfce4-screenshooter'))),
+ # Copy from clipboard history
+ Key([self.mod, "control"], "c", lazy.spawn(Config.get('cliphistory_command', 'true'))),
+
# Toggle between different layouts as defined below
Key([self.mod], "Tab", lazy.next_layout()),
# Kill the current window
Key([self.mod], "w", lazy.window.kill()),
- # Restart QTile
- Key([self.mod, "control"], "r", lazy.restart()),
+ # Restart/reload QTile (Restart only available in X11 backend)
+ Key([self.mod, "control"], "r", lazy.restart() if qtile.core.name == "x11" else lazy.reload_config()),
# Shutdown QTile
Key([self.mod, "control"], "q", lazy.shutdown()),
# Update wallpaper
- Key([self.mod, "control"], "w", lazy.function(self.update_wallpaper)),
+ Key([self.mod, "control"], "w",
+ lazy.function(self.set_random_wallpaper), lazy.function(self.update_colorscheme)),
+
+ # Reload screen configuration
+ Key([self.mod, "control"], "s", lazy.spawn(Config.get('cmd_reconfigure_screens', 'true'))),
# Reload colorscheme
Key([self.mod, "control"], "t", lazy.function(self.update_colorscheme)),
- # Reorganize screens
- Key([self.mod, "control"], "s", lazy.function(self.update_screens)),
-
# Toggle static windows
Key([self.mod], "p", lazy.function(self.toggle_window_static)),
@@ -228,13 +215,7 @@ class Kuro(BaseTheme):
##
# Debug keyboard shortcuts
##
- Key([self.mod, "control"], "c", lazy.function(display_wm_class)),
-
- # Redraw the top bar
- Key([self.mod, "shift", "control"], "r", lazy.function(self.redraw_bar)),
-
- # Update visualizer widgets
- Key([self.mod, "shift", "control"], "v", lazy.function(self.reinitialize_visualizers)),
+ Key([self.mod, "shift", "control"], "w", lazy.function(display_wm_class)),
# Show extensive window info
Key([self.mod, "shift", "control"], "i", lazy.function(self.show_window_info)),
@@ -244,34 +225,29 @@ class Kuro(BaseTheme):
]
def init_groups(self):
- self.log_debug("Initializing groups")
-
- groups = []
-
+ logger.warning("Initializing groups")
# http://fontawesome.io/cheatsheet
- groups.append(Group("", spawn=Config.get('web_browser', "true")))
- groups.append(Group("", spawn=Config.get('app_terminal_init', "true")))
- groups.append(Group(""))
- groups.append(Group("", spawn=Config.get('app_chat', "true")))
- groups.append(Group("", spawn=Config.get('app_irc', "true")))
- groups.append(Group("", spawn=Config.get('file_manager', "true")))
- groups.append(Group("", spawn=Config.get('app_mail', "true")))
- groups.append(Group(""))
- groups.append(Group("", spawn=Config.get('app_music', "true")))
- groups.append(Group(""))
- groups.append(Group("", spawn=Config.get('apps_audio', "true")))
- groups.append(Group("", layout='floating', layouts=[
- layout.Floating(
- border_focus="#990000",
- border_normal="#440000"
- )
- ]))
-
+ groups = [
+ Group(""),
+ Group(""),
+ Group(""),
+ Group(""),
+ Group(""),
+ Group(""),
+ Group(""),
+ Group(""),
+ Group(""),
+ Group("", layout='floating', layouts=[
+ layout.Floating(
+ border_focus="#990000",
+ border_normal="#440000"
+ )
+ ])
+ ]
return groups
def init_layouts(self):
- self.log_debug("Initializing layouts")
-
+ logger.warning("Initializing layouts")
return [
kuro_layouts.KuroWmii(
theme=self,
@@ -291,8 +267,7 @@ class Kuro(BaseTheme):
]
def init_widget_defaults(self):
- self.log_debug("Initializing widget_defaults")
-
+ logger.warning("Initializing widget_defaults")
return {
"font": Config.get('font_topbar', "Sans"),
"fontsize": Config.get('fontsize_topbar', 16),
@@ -300,158 +275,12 @@ class Kuro(BaseTheme):
}
def init_screens(self):
- self.log_debug("Initializing screens")
-
- self.num_screens = utils.get_screen_count()
- if self.num_screens <= 0:
- self.num_screens = 1
-
- screens = []
- for x in range(self.num_screens):
- self.log_info("Initializing bars for screen {}".format(x))
- widgets = []
- widgets.extend([
- widget.GroupBox(
- active=Config.get('colour_groupbox_icon_active', '#ffffff'),
- borderwidth=Config.get('width_groupbox_border', 1),
- disable_drag=Config.get('bool_groupbox_disable_drag', False),
- font=Config.get('font_groupbox', 'Arial'),
- fontsize=Config.get('fontsize_groupbox', 15),
- highlight_color=Config.get("colour_groupbox_border_normal", '#444444'),
- inactive=Config.get('colour_groupbox_icon_inactive', '#444444'),
- rounded=Config.get('bool_groupbox_rounded_borders', True),
- this_current_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'),
- this_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'),
- margin=Config.get('margin_groupbox', 0)
- ),
- widget.Prompt(**self.widget_defaults),
-
- kuro.utils.widgets.KuroTaskList(
- border=Config.get('tasklist_border', '#ffffff'),
- borderwidth=Config.get('tasklist_borderwidth', 1),
- font=Config.get('tasklist_font', 'Arial'),
- fontsize=Config.get('tasklist_fontsize', 15),
- highlight_method=Config.get('tasklist_highlight_method', 'border'),
- max_title_width=Config.get('tasklist_max_title_width', 200),
- rounded=Config.get('tasklist_rounded', True),
- urgent_alert_method=Config.get('tasklist_urgent_alert_method', 'border'),
- urgent_border=Config.get('tasklist_urgent_border', '#ff0000'),
- margin=Config.get('margin_groupbox', 0)
- )
- ])
-
- if Config.get('show_audio_visualizer', False):
- widgets.append(kuro.utils.widgets.AudioVisualizerWidget(margin_x=0, margin_y=0))
-
- widgets.extend([
- kuro.utils.widgets.MediaWidget(ignore_players=Config.get('media_ignore_players', '')),
- kuro.utils.widgets.TextSpacerWidget(fontsize=14),
- ])
-
- if Config.get('show_temperature', False):
- widgets.extend([
- kuro.utils.widgets.ThermalSensorWidget(
- font=Config.get('font_topbar', 'Arial'),
- fontsize=Config.get('fontsize_topbar', 16),
- foreground=Config.get('thermal_colour', '#ffffff'),
- foreground_alert=Config.get('thermal_colour_alert', '#ff0000'),
- tag_sensor=Config.get('thermal_sensor', 'temp1'),
- chip=Config.get('thermal_chip', None),
- threshold=Config.get('thermal_threshold', 70),
- update_interval=5,
- fontsize_left=18, fontsize_right=11
- ),
- ])
-
- widgets.extend([
- kuro.utils.widgets.CPUInfoWidget(fontsize_left=16, fontsize_right=11),
- kuro.utils.widgets.MemoryInfoWidget(fontsize_left=18, fontsize_right=11),
- kuro.utils.widgets.DiskIOInfoWidget(fontsize_left=16, fontsize_right=11),
- ])
- if Config.get('show_battery_widget', False):
- widgets.extend([
- kuro.utils.widgets.BatteryInfoWidget(fontsize_left=16, fontsize_right=11),
- ])
- widgets.extend([
- kuro.utils.widgets.VolumeInfoWidget(
- pulse_sink=Config.get('volume_pulse_sink', None),
- fontsize_left=18,
- fontsize_right=11,
- font_left=Config.get('font_groupbox', None),
- ),
- kuro.utils.widgets.VolumeInfoWidget(
- pulse_sink=Config.get('volume_pulse_sink2', None),
- fontsize_left=18,
- fontsize_right=11,
- font_left=Config.get('font_groupbox', None),
- ),
- kuro.utils.widgets.TextSpacerWidget(fontsize=14),
- kuro.utils.widgets.NetworkInfoWidget(fontsize_left=16, fontsize_right=14),
- ])
- if Config.get('show_gpu_widget', False):
- widgets.extend([
- kuro.utils.widgets.GPUStatusWidget(
- theme_path=Config.get('gpu_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile'
- '/checkouts/latest/libqtile/resources/battery-icons'),
- padding=0,
- )
- ])
- widgets.extend([
- kuro.utils.widgets.TextSpacerWidget(fontsize=14),
- ])
-
- # Systray can only be on one screen, so put it on the first
- if x == 0:
- widgets.append(widget.Systray(**self.widget_defaults))
-
- widgets.extend([
- kuro.utils.widgets.KuroCurrentLayoutIcon(custom_icon_paths=Config.get('custom_layout_icon_paths', [])),
- widget.Clock(format="%a %d %b, %H:%M", **self.widget_defaults),
- kuro.utils.widgets.CheckUpdatesYay(
- colour_no_updates=Config.get('updates_colour_none', '#ffffff'),
- colour_have_updates=Config.get('updates_colour_available', '#ff0000'),
- display_format=Config.get('updates_display_format', 'Updates: {updates}'),
- execute=Config.get('updates_execute_command', None),
- update_interval=Config.get('updates_interval', 600),
- **self.widget_defaults
- ),
- widget.TextBox("#{}".format(x), name="default", **self.widget_defaults),
- ])
-
- topbar = utils.KuroTopBar(
- theme=self,
- background=Config.get('bar_background', '#000000'),
- opacity=Config.get('bar_opacity', 1.0),
- widgets=widgets,
- size=Config.get('height_groupbox', 30)
- )
-
- self.topbars.append(topbar)
-
- screens.append(Screen(top=topbar))
-
- # Add debug bars on each window if debugging is enabled
- if Config.get('debug', False):
- self.debug_textfields = []
- for x in range(self.num_screens):
- textfield = widget.TextBox("...", name="debugtext", **self.widget_defaults)
- self.debug_textfields.append(textfield)
- widgets = []
- widgets.extend([
- widget.TextBox(" Debugging bar ", name="default", **self.widget_defaults),
- textfield,
- ])
- screens[x].bottom = bar.Bar(
- widgets=widgets,
- size=Config.get('height_debugbar', 30)
- )
- self.debug_bars.append(screens[x].bottom)
-
- return screens
+ logger.warning("Initializing screens")
+ self.reinit_screens()
+ return self.screens
def init_mouse(self):
- self.log_debug("Initializing mouse")
-
+ logger.warning("Initializing mouse")
# Drag floating layouts.
mouse = [
Drag([self.mod], "Button1", lazy.window.set_position_floating(),
@@ -460,12 +289,52 @@ class Kuro(BaseTheme):
start=lazy.window.get_size()),
Click([self.mod], "Button2", lazy.window.bring_to_front())
]
-
return mouse
- def update_keys(self):
- self.log_debug("Updating keys")
+ def initialize_colorscheme(self):
+ colors = None
+ if os.path.isfile("/home/kevin/.cache/wal/colors.json"):
+ with open("/home/kevin/.cache/wal/colors.json", 'r') as f:
+ try:
+ colors = json.load(f)['colors']
+ except KeyError:
+ colors = None
+ if colors:
+ # Update Config
+ Config.foreground = colors['color15']
+ Config.background = colors['color0']
+ Config.highlight = colors['color3']
+ Config.inactive_light = colors['color4']
+ Config.inactive_dark = colors['color5']
+ Config.bar_background = colors['color1']
+
+ def reinit_screens(self):
+ # Re-initalize bars
+ self.topbars.clear()
+
+ # TODO: Move backend check into utils method
+ if qtile.core.name == "x11":
+ self.num_screens = max(1, utils.get_screen_count())
+ else:
+ self.num_screens = max(1, len(qtile.core.get_screen_info()))
+ logger.warning(f"Detected {self.num_screens} screens.")
+
+ # TODO: If i get the double topbar issue, this might be the
+ # cause; creating new screens on reinit...
+ screens = []
+ for x in range(self.num_screens):
+ logger.warning("Initializing bars for screen {}".format(x))
+ topbar = self.build_bar_for_screen(x)
+ self.topbars.append(topbar)
+ screens.append(Screen(top=topbar))
+
+ self.screens.clear()
+ for s in screens:
+ self.screens.append(s)
+
+ def update_keys(self):
+ logger.warning("Updating keys")
for i, g in enumerate(self.groups):
if i == 9:
i = -1
@@ -497,124 +366,229 @@ class Kuro(BaseTheme):
Key([self.mod], "n", lazy.layout.normalize()),
])
- # Util functions
- @staticmethod
- def redraw_bar(qtile):
- for s in qtile.screens:
- s.top.draw()
+ def build_bar_for_screen(self, screen_num):
+ widgets = [
+ # Workspaces
+ kuro.utils.widgets.KuroGroupBox(
+ active=Config.get('colour_groupbox_icon_active', '#ffffff'),
+ borderwidth=Config.get('width_groupbox_border', 1),
+ disable_drag=Config.get('bool_groupbox_disable_drag', False),
+ font=Config.get('font_groupbox', 'Arial'),
+ fontsize=Config.get('fontsize_groupbox', 15),
+ highlight_color=Config.get("colour_groupbox_border_normal", '#444444'),
+ inactive=Config.get('colour_groupbox_icon_inactive', '#444444'),
+ rounded=Config.get('bool_groupbox_rounded_borders', True),
+ this_current_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'),
+ this_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'),
+ margin=Config.get('margin_groupbox', 0)
+ ),
- @staticmethod
- def update_screens(qtile):
- out = utils.call_process(["xrandr", "--current"])
- video_mode = "nvidia"
- #if "nvidia" in mode_out:
- # video_mode = "nvidia"
- #elif "intel" in mode_out:
- # video_mode = "intel"
- laptop_screen = None
- screens = []
- for x in out.split("\n"):
- if " connected " in x:
- if Config.get("laptop_screen_nvidia", None) is not None \
- and Config.get("laptop_screen_intel", None) is not None:
- if video_mode == "nvidia" and Config.get("laptop_screen_nvidia", None) in x:
- laptop_screen = x
- elif video_mode == "intel" and Config.get("laptop_screen_intel", None) in x:
- laptop_screen = x
- else:
- screens.append(x)
- else:
- screens.append(x)
+ # Spawn prompt (only shown if activated)
+ widget.Prompt(**self.widget_defaults),
- # Only configure two screens. Open arandr if more screens are present.
- if laptop_screen is not None and len(screens) == 1:
- laptop = laptop_screen.split()[0]
- other = screens[0].split()[0]
- utils.call_process(["xrandr", "--output", laptop, "--below", other])
- qtile.cmd_restart()
- else:
- utils.execute("arandr")
+ # Open window list
+ widget.TaskList(
+ border=Config.get('tasklist_border', '#ffffff'),
+ borderwidth=Config.get('tasklist_borderwidth', 1),
+ font=Config.get('tasklist_font', 'Arial'),
+ fontsize=Config.get('tasklist_fontsize', 15),
+ highlight_method=Config.get('tasklist_highlight_method', 'border'),
+ max_title_width=Config.get('tasklist_max_title_width', 200),
+ rounded=Config.get('tasklist_rounded', True),
+ urgent_alert_method=Config.get('tasklist_urgent_alert_method', 'border'),
+ urgent_border=Config.get('tasklist_urgent_border', '#ff0000'),
+ margin=Config.get('margin_groupbox', 0),
+ icon_size=Config.get('tasklist_iconsize', 17),
+ theme_mode=Config.get('tasklist_thememode', 'preferred'),
+ theme_path=Config.get('tasklist_themepath', '/usr/share/icons/Papirus-Dark'),
+ txt_floating=" ", txt_maximized=" ", txt_minimized=" ",
+ )
+ ]
- def reinitialize_visualizers(self):
- if Config.get("show_audio_visualizer", False):
- logger.warning("Reinitializing visualizers...")
- for screen in qtile.screens:
- for widget in screen.top.widgets:
- if isinstance(widget, kuro.utils.widgets.AudioVisualizerWidget):
- if widget.client is not None:
- widget.client.kill()
- widget.client = None
- widget.screen = None
- self.update_visualizers()
+ # Media widget(s)
+ widgets.extend([
+ # An MPRIS widget that shows the media play status as an icon.
+ widget.Mpris2(
+ font=Config.get('font_groupbox', 'Arial'),
+ fontsize=Config.get('fontsize_groupbox', 15),
+ format="",
+ scroll=False,
+ playing_text="",
+ paused_text="",
+ stopped_text="",
+ no_metadata_text="",
+ name=f"media_icon{screen_num}",
+ mouse_callbacks={
+ "Button1": lazy.widget[f"media_icon{screen_num}"].play_pause(),
+ "Button3": lazy.widget[f"media_icon{screen_num}"].next(),
+ "Button4": lambda: None,
+ "Button5": lambda: None,
+ }
+ ),
+ # An MPRIS widget that shows the currently playing song information in a nice format.
+ widget.Mpris2(
+ font=Config.get('font_topbar', 'Arial'),
+ fontsize=Config.get('fontsize_topbar', 15),
+ format="{xesam:title} - {xesam:artist} - {xesam:album}",
+ scroll=True,
+ width=300, # Maximum width before widget starts scrolling
+ playing_text="{track}",
+ paused_text="{track}",
+ stopped_text="",
+ no_metadata_text="No metadata available",
+ name=f"media_text{screen_num}",
+ mouse_callbacks={
+ "Button1": lazy.widget[f"media_icon{screen_num}"].play_pause(),
+ "Button3": lazy.widget[f"media_icon{screen_num}"].next(),
+ "Button4": lambda: None,
+ "Button5": lambda: None,
+ }
+ ),
+ # An MPRIS widget masquerading as a text widget, that only shows "|" when media is playing or paused.
+ widget.Mpris2(
+ fontsize=14,
+ format="",
+ scroll=False,
+ playing_text="|",
+ paused_text="|",
+ stopped_text="",
+ no_metadata_text="",
+ mouse_callbacks={
+ "Button1": lambda: None,
+ "Button4": lambda: None,
+ "Button5": lambda: None,
+ }
+ )
+ ])
- def update_visualizers(self):
- if Config.get("show_audio_visualizer", False):
- logger.warning("Updating visualizers..")
- for screen in qtile.screens:
- for widget in screen.top.widgets:
- if isinstance(widget, kuro.utils.widgets.AudioVisualizerWidget):
- if widget.client is None:
- logger.warning("Spawning for screen {}".format(screen))
- utils.execute(Config.get('visualizer_app', "glava"))
- else:
- widget.update_graph()
+ # Sensor widgets
+ sensor_widgets = []
- def show_window_info(self, qtile):
- window = qtile.current_window if qtile else None
+ # Temperature sensor
+ if Config.get('show_temperature', False):
+ sensor_widgets.append(kuro.utils.widgets.ThermalSensorWidget(
+ font=Config.get('font_topbar', 'Arial'),
+ fontsize=Config.get('fontsize_topbar', 16),
+ foreground=Config.get('thermal_colour', '#ffffff'),
+ foreground_alert=Config.get('thermal_colour_alert', '#ff0000'),
+ tag_sensor=Config.get('thermal_sensor', 'temp1'),
+ chip=Config.get('thermal_chip', None),
+ threshold=Config.get('thermal_threshold', 70),
+ update_interval=5,
+ fontsize_left=18, fontsize_right=11
+ ))
- import pprint
- if window:
- info = window.cmd_inspect() or None
- name = window.name
+ # CPU/Memory/Disk sensors
+ sensor_widgets.extend([
+ kuro.utils.widgets.CPUInfoWidget(fontsize_left=16, fontsize_right=11),
+ kuro.utils.widgets.MemoryInfoWidget(fontsize_left=18, fontsize_right=11),
+ kuro.utils.widgets.DiskIOInfoWidget(fontsize_left=16, fontsize_right=11),
+ ])
- utils.notify(title="Window properties {}".format(name),
- content="{}".format(pprint.pformat(vars(window))))
- logger.warning("{}".format(pprint.pformat(vars(window))))
+ widgets.extend([
+ widget.WidgetBox(
+ font=Config.get('font_groupbox', 'Arial'),
+ fontsize=Config.get('fontsize_groupbox', 15),
+ text_open="[>]",
+ text_closed="[]",
+ widgets=sensor_widgets
+ ),
+ widget.TextBox(fontsize=14, text="|")
+ ])
- if info:
- info = pprint.pformat(info)
- utils.notify(title="Window info of {}".format(name),
- content="{}".format(info))
- logger.warning("{}".format(info))
+ # Battery level
+ if Config.get('show_battery_widget', False):
+ widgets.extend([
+ kuro.utils.widgets.BatteryInfoWidget(fontsize_left=16, fontsize_right=11),
+ ])
- # @staticmethod
- def toggle_window_static(self, qtile):
- window = qtile.current_window
- if window in self.static_windows:
- utils.notify("Unpinned {}".format(window.name), "{} has been unpinned".format(window.name))
- self.static_windows.remove(window)
- del window.is_static_window
- else:
- utils.notify("Pinned {}".format(window.name), "{} has been pinned".format(window.name))
- self.static_windows.append(window)
- window.is_static_window = True
+ # Volume widget(s)
+ for sink_name in Config.get('volume_pulse_sinks', []):
+ widgets.append(
+ kuro.utils.widgets.VolumeInfoWidget(
+ pulse_sink=sink_name,
+ fontsize_left=18,
+ fontsize_right=11,
+ font_left=Config.get('font_groupbox', None),
+ )
+ )
- window.floating = True
+ # Network information
+ widgets.extend([
+ widget.TextBox(fontsize=14, text="|"),
+ kuro.utils.widgets.NetworkInfoWidget(
+ fontsize_left=16, fontsize_right=14,
+ wireless_interface=Config.get('wifi_interface', None),
+ wired_interface=Config.get('wired_interface', None)
+ ),
+ ])
- # Pinned toggle function
- @staticmethod
- def toggle_pinned(qtile):
- windows = qtile.cmd_windows()
- print(windows)
+ # GPU widget
+ if Config.get('show_gpu_widget', False):
+ widgets.extend([
+ kuro.utils.widgets.GPUStatusWidget(
+ theme_path=Config.get(
+ 'gpu_theme_path',
+ '/usr/lib/python3.11/site-packages/libqtile/resources/battery-icons'),
+ padding=0,
+ )
+ ])
+ widgets.append(widget.TextBox(fontsize=14, text="|"))
+
+ # Determine systray to use (X or Wayland)
+ # X system tray can only be on one screen, so only put it on the first
+ if qtile.core.name == "x11" and screen_num == 0:
+ widgets.append(widget.Systray(**self.widget_defaults))
+ elif qtile.core.name != "x11":
+ widgets.append(extra_widget.StatusNotifier(**self.widget_defaults))
+
+ # Layout switcher, clock and Screen ID
+ widgets.extend([
+ widget.CurrentLayoutIcon(custom_icon_paths=Config.get('custom_layout_icon_paths', [])),
+ widget.Clock(format="%a %d %b ", **self.widget_defaults),
+ widget.Clock(
+ format="%H:%M",
+ font=Config.get('font_topbar', "Sans"),
+ fontsize=16,
+ padding=0,
+ ),
+ widget.Clock(
+ format=":%S ",
+ font=Config.get('font_topbar', "Sans"),
+ fontsize=11,
+ padding=0,
+ ),
+ widget.CurrentScreen(
+ active_color="66FF66", inactive_color="FFFFFF",
+ active_text=f"#{screen_num}", inactive_text=f"#{screen_num}",
+ **self.widget_defaults
+ )
+ ])
+
+ # Build the bar
+ return bar.Bar(
+ background=f"{Config.get('bar_background', '#000000')}{Config.get('bar_rgba_opacity', 'AA')}",
+ opacity=Config.get('bar_opacity', 1.0),
+ widgets=widgets,
+ size=Config.get('height_groupbox', 30)
+ )
# QTile base callbacks
def callback_startup_once(self, *args, **kwargs):
+ logger.warning("Callback Startup Once")
if not hasattr(qtile, 'theme_instance'):
# Save theme instance in qtile
qtile.theme_instance = self
- self.update_wallpaper(qtile)
-
- # Setup audio
- # p = utils.execute_once(["qjackctl"])
- # p.wait()
+ self.set_random_wallpaper()
def callback_startup(self):
+ logger.warning("Callback Startup")
if not hasattr(qtile, 'theme_instance'):
# Save theme instance in qtile
qtile.theme_instance = self
+ logger.warning("Restoring wallpaper...")
if self.current_wallpaper:
- #p = utils.execute_once(["wal", "-n", "-i", "{}".format(self.current_wallpaper)])
p = utils.execute_once(["wallust", "run", "{}".format(self.current_wallpaper)])
p.wait()
else:
@@ -626,38 +600,78 @@ class Kuro(BaseTheme):
except KeyError:
wallpaper = None
if wallpaper:
- Kuro.set_wallpaper(qtile, wallpaper)
+ self.set_wallpaper(wallpaper)
else:
- p = utils.execute_once("nitrogen --restore")
- p.wait()
-
- self.log_info("Starting compositor...")
- utils.execute_once("picom -b")
-
- self.log_info("Starting clipboard manager...")
- utils.execute_once("xfce4-clipman")
-
- self.log_info("Starting notification daemon...")
- utils.execute_once("dunst")
-
- self.log_info("Starting xiccd color profile manager...")
- utils.execute_once("xiccd")
-
- #self.log_info("Starting KDE connect daemon...")
- #utils.execute_once("/usr/lib/kdeconnectd")
-
- self.log_info("Starting KDE connect indicator...")
- utils.execute_once("/usr/bin/kdeconnect-indicator")
-
- self.log_info("Starting automatic backup scheduler...")
- utils.execute_once("/usr/bin/vorta")
+ logger.warning("No wallpaper to restore.")
# Update color scheme
- self.initialize_colorscheme()
+ self.update_colorscheme()
+ self.startup_completed = True
- # def callback_screen_change(self, *args, **kwargs):
- # for window in self.static_windows:
- # window.togroup()
+ def callback_startup_complete(self, *args, **kwargs):
+ logger.warning("Callback Startup Complete")
+ if not hasattr(qtile, 'theme_instance'):
+ # Save theme instance in qtile
+ qtile.theme_instance = self
+
+ # Update color scheme
+ self.update_colorscheme()
+
+ # Setup XDG Desktop Portal
+ self.setup_xdg_desktop_portal()
+
+ # After first startup is complete, autostart configured apps
+ logger.warning("Autostarting apps...")
+ for app in Config.get("apps_autostart", []):
+ logger.warning(f"Starting '{app}'...")
+ utils.execute_once(app)
+
+ for app in Config.get("apps_autostart_group", []):
+ if all(x in app.keys() for x in ["group", "command"]):
+ logger.warning(f"Starting '{app['command']}' in group {app['group']}...")
+ utils.start_in_group_once(theme=self, qtile=qtile, **app)
+ else:
+ logger.warning(f"Invalid app in 'apps_autostart_group', "
+ f"must have 'group' and 'command' keys: {app}...")
+ logger.warning("Autostart complete")
+
+ def callback_client_managed(self, *args, **kwargs):
+ client: Optional[Window] = args[0] if len(args) > 0 else None
+
+ # TODO: Move get_pid to an utility function
+ w_pid = None
+ try:
+ w_pid = client.get_pid()
+ except AttributeError: # Some windows might not have this .get_pid method. Try other ways
+ if isinstance(client, WaylandXWindow) or isinstance(client, WaylandXStatic):
+ w_pid = client.surface.pid
+ elif isinstance(client, XorgXWindow):
+ w_pid = client.get_net_wm_pid()
+ elif isinstance(client, LayerStatic):
+ pass # Wayland background layer 'window'
+ else:
+ logger.error(f"Unknown window type {client.__class__.__name__}")
+
+ if w_pid is not None and w_pid in self.autostart_app_rules.keys():
+ rule_id = self.autostart_app_rules[w_pid]
+ logger.warning(f"Removing rule {rule_id} for PID {w_pid}, client {client.name}")
+ lazy.remove_rule(rule_id)
+
+ def callback_client_killed(self, *args, **kwargs):
+ client = args[0]
+ logger.warning("Client {} Killed".format(client))
+
+ # If this window was static, remove it from the static window list
+ if hasattr(client, "is_static_window") and client.is_static_window:
+ logger.warning("Removing static window {}".format(client.name))
+ del client.is_static_window
+ self.static_windows.remove(client)
+
+ def callback_screens_reconfigured(self, *args, **kwargs):
+ logger.warning(f"Re-configuring screens!")
+ self.reinit_screens()
+ self.set_wallpaper(self.current_wallpaper)
+ self.update_colorscheme()
def callback_setgroup(self, *args, **kwargs):
for window in self.static_windows:
@@ -670,121 +684,28 @@ class Kuro(BaseTheme):
del window.is_static_window
self.static_windows.remove(window)
- def callback_focus_change(self, *args, **kwargs):
- if self.do_keyboard_updates:
- kb_handle_focus_change(self)
+ def show_window_info(self, *args, **kwargs):
+ import pprint
+ window = qtile.current_window
+ if window:
+ logger.warning(f"Window properties {window.name}\n{pprint.pformat(vars(window))}")
+ if window.info():
+ logger.warning(f"Window info of {window.name}\n{pprint.pformat(window.info())}")
- initial_windows = []
+ def toggle_window_static(self, *args, **kwargs):
+ window = qtile.current_window
+ if window in self.static_windows:
+ utils.notify(qtile, "Unpinned {}".format(window.name), "{} has been unpinned".format(window.name))
+ self.static_windows.remove(window)
+ del window.is_static_window
+ else:
+ utils.notify(qtile, "Pinned {}".format(window.name), "{} has been pinned".format(window.name))
+ self.static_windows.append(window)
+ window.is_static_window = True
- def callback_startup_complete(self, *args, **kwargs):
- if not hasattr(qtile, 'theme_instance'):
- # Save theme instance in qtile
- qtile.theme_instance = self
-
- # Only run on first startup
- if not qtile.no_spawn:
- dg = qtile.dgroups
- for r in dg.rules:
- pid = -1
- # noinspection PyProtectedMember
- for m in r.matchlist:
- if m._rules.get('net_wm_pid', None) is not None:
- pid = m._rules.get('net_wm_pid')
- break
- if pid != -1:
- self.initial_windows.append((pid, r.group))
+ window.floating = True
- self.callback_client_new()
-
- # After first startup is complete, start the audio apps that can only be started after boot is complete
- if not qtile.no_spawn:
- for app in Config.get("apps_audio_afterstart", []):
- utils.execute_once(app)
-
- # Update color scheme
- Kuro.update_colorscheme(qtile)
-
- def callback_client_new(self, *args, **kwargs):
- client = args[0] if len(args) > 0 else None
-
- if len(self.initial_windows) > 0:
- init = self.initial_windows.copy()
- for pid, gname in init:
- for group in qtile.groups:
- if len(group.windows) > 0:
- for window in group.windows:
- w_pid = window.window.get_net_wm_pid()
- self.log_info("Comparing pid {} with window PID {}, window {}".format(pid, w_pid,
- window.name))
- if pid == w_pid:
- c = qtile.dgroups.rules_map.copy()
- for rid, r in c.items():
- if r.matches(window):
- qtile.dgroups.remove_rule(rid)
- self.initial_windows.remove((pid, gname))
- self.log_info("Removed group rule for PID {}, window {}".format(pid,
- window.name))
- self.log_info(str(qtile.dgroups.rules_map))
-
- # Check if it is a visualizer
- if Config.get("show_audio_visualizer", False):
- if client is not None and client.window.get_name() == "GLava":
- placed = False
- for screen in qtile.screens:
- for widget in screen.top.widgets:
- if not placed and isinstance(widget, kuro.utils.widgets.AudioVisualizerWidget):
- if widget.client is None:
- viz_info = widget.info()
- pos_x = viz_info['offset'] + widget.margin_x
- pos_y = 0 + widget.margin_y
- width = viz_info['width'] - (2 * widget.margin_x)
- height = viz_info['height'] - (2 * widget.margin_y)
- screen_index = qtile.screens.index(screen)
- logger.warning("Attaching {} {} to {} on screen {}".format(client, client.window.wid, type(widget).__name__, screen_index))
- c = KuroStatic.create(client, screen, x=pos_x, y=pos_y, width=width, height=height)
- c.opacity = Config.get("bar_opacity", 1.0)
- widget.set_client(c, screen)
- placed = True
- if not placed:
- if Config.get("kill_unnecessary_glava_processes", False):
- logger.warning("Killing GLava {} because there is no widget where it can fit".format(client))
- utils.notify("Glava", "Killing new GLava process because there is no screen without a visualizer")
- client.kill()
- else:
- logger.warning("Not repositioning GLava {} because there is no widget where it can fit".format(client))
- utils.notify("Glava", "Not repisitioning new GLava process because there is no screen without a visualizer")
-
- # If this is Non-Mixer, move it to the audio group
- logger.warning("Processing window {}".format(client))
- if client is not None and client.window.get_wm_class() == ('Non-Mixer', 'Non-Mixer'):
- logger.warning("Moving to correct group!")
- client.window.togroup("")
-
-
-
- def callback_client_killed(self, *args, **kwargs):
- client = args[0]
- logger.warning("Client {} Killed".format(client))
-
- # Detach visualizer from widget if it was a visualizer window
- if isinstance(client, KuroStatic):
- for screen in qtile.screens:
- for widget in screen.top.widgets:
- if isinstance(widget, kuro.utils.widgets.AudioVisualizerWidget):
- if widget.client == client:
- screen_index = qtile.screens.index(screen)
- logger.warning("Detaching {} {} from widget {} on screen {}".format(client, client.window.wid, type(widget).__name__, screen_index))
- widget.client = None
- widget.screen = None
-
- # If this window was static, remove it from the static window list
- if hasattr(client, "is_static_window") and client.is_static_window:
- logger.warning("Removing static window {}".format(client.name))
- del client.is_static_window
- self.static_windows.remove(client)
-
- @staticmethod
- def update_wallpaper(qtile):
+ def set_random_wallpaper(self, *args, **kwargs):
wallpapers = []
wallpaper_dir = Config.get("desktop_bg_folder", "")
@@ -800,59 +721,39 @@ class Kuro(BaseTheme):
except os.error as e:
logger.warning("Could not load wallpapers from directory: {}".format(e))
-
if wallpapers:
if Config.get("desktop_bg_override", False):
- qtile.theme_instance.current_wallpaper = Config.get("desktop_bg_override", "")
+ wallpaper_file = Config.get("desktop_bg_override", "")
else:
- qtile.theme_instance.current_wallpaper = os.path.join(wallpaper_dir, random.choice(wallpapers))
- logger.warning("Selected new wallpaper: {}".format(qtile.theme_instance.current_wallpaper))
- Kuro.set_wallpaper(qtile, qtile.theme_instance.current_wallpaper)
+ wallpaper_file = os.path.join(wallpaper_dir, random.choice(wallpapers))
+ logger.warning(f"Selected new wallpaper: {wallpaper_file}")
+ self.set_wallpaper(wallpaper_file)
else:
- utils.execute_once("nitrogen --restore")
+ logger.warning("Random wallpaper requested but no wallpapers are available.")
+ def set_wallpaper(self, filename):
+ if qtile.core.name == "x11":
+ p = utils.execute_once(f"{Config.get('wallpaper_config_command', 'wal-nitrogen-noupdate')} {filename}")
+ p.wait()
+ else:
+ # Wayland can set wallpaper in qtile directly per screen
+ for screen_i, screen in enumerate(qtile.screens):
+ sinfo = screen.info()
+ sfilename = filename
+ if sinfo.get('width', 100) < sinfo.get('height', 10):
+ # Vertical screen, see if there is a vertical alt wallpaper
+ basename, ext = os.path.splitext(filename)
+ new_filename = f"{basename}.vertical{ext}"
+ if os.path.isfile(new_filename):
+ sfilename = new_filename
+ logger.warning(f"Setting Screen#{screen_i} wallpaper to {sfilename}.")
+ screen.set_wallpaper(sfilename, "fill")
+ self.current_wallpaper = filename
- @staticmethod
- def set_wallpaper(qtile, filename):
- p = utils.execute_once("{} {}".format(Config.get('wallpaper_config_command', 'wal-nitrogen-noupdate'),
- filename))
- p.wait()
- qtile.theme_instance.current_wallpaper = filename
- Kuro.update_colorscheme(qtile)
-
-
- @staticmethod
- def update_mediaclients(*args, **kwargs):
- return str(str(args) + " " + str(kwargs))
-
-
- @staticmethod
- def initialize_colorscheme():
- colors = None
- if os.path.isfile("/home/kevin/.cache/wal/colors.json"):
- with open("/home/kevin/.cache/wal/colors.json", 'r') as f:
- try:
- colors = json.load(f)['colors']
- except KeyError:
- colors = None
-
- if colors:
- # Update Config
- Config.foreground = colors['color15']
- Config.background = colors['color0']
- Config.highlight = colors['color3']
- Config.inactive_light = colors['color4']
- Config.inactive_dark = colors['color5']
- Config.bar_background = colors['color1']
-
- @staticmethod
- def update_colorscheme(qtile):
- """
- :type qtile: libqtile.manager.Qtile
- """
- if qtile.theme_instance.current_wallpaper:
- #p = utils.execute(["wal", "-n", "-i", "{}".format(qtile.theme_instance.current_wallpaper)])
- p = utils.execute(["wallust", "run", "{}".format(qtile.theme_instance.current_wallpaper)])
+ def update_colorscheme(self, *args, **kwargs):
+ if self.current_wallpaper:
+ logger.warning(f"Updating wal colors for wallpaper {self.current_wallpaper}")
+ p = utils.execute_once(["wallust", "run", "{}".format(self.current_wallpaper)])
p.wait()
colors = None
@@ -881,62 +782,81 @@ class Kuro(BaseTheme):
layout.border_normal = colors['color1']
layout.border_normal_stack = colors['color1']
- for screen in qtile.screens:
+ for screen_i, screen in enumerate(qtile.screens):
bar = screen.top
- bar.background = colors['color1']
- bar.drawer.clear(bar.background)
- for w in bar.widgets:
- if hasattr(w, '_update_drawer'):
- try:
- w._update_drawer()
- except Exception as e:
- logger.error("Error while updating drawer for widget {}: {}".format(w, e))
+ logger.warning(f"Updating colorscheme for screen {screen_i}")
+ if bar:
+ bar.background = f"{colors['color1']}{Config.get('bar_rgba_opacity', 'AA')}"
+ bar.drawer.clear(bar.background)
- if hasattr(w, 'foreground'):
- w.foreground = colors['color15']
+ def update_widget(w):
+ if hasattr(w, '_update_drawer'):
+ try:
+ w._update_drawer()
+ except Exception as e:
+ logger.error("Error while updating drawer for widget {}: {}".format(w, e))
- if hasattr(w, 'foreground_normal'):
- w.foreground_normal = colors['color15']
+ if hasattr(w, 'foreground'):
+ w.foreground = colors['color15']
- if hasattr(w, 'foreground_alert'):
- w.foreground_alert = colors['color3']
+ if hasattr(w, 'foreground_normal'):
+ w.foreground_normal = colors['color15']
- if hasattr(w, 'border'):
- w.border = colors['color15']
+ if hasattr(w, 'foreground_alert'):
+ w.foreground_alert = colors['color3']
- if hasattr(w, 'active'):
- w.active = colors['color15']
+ if hasattr(w, 'border'):
+ w.border = colors['color15']
- if hasattr(w, 'highlight_color'):
- w.highlight_color = colors['color3']
+ if hasattr(w, 'active'):
+ w.active = colors['color15']
- if hasattr(w, 'inactive'):
- w.inactive = colors['color8']
+ if hasattr(w, 'highlight_color'):
+ w.highlight_color = colors['color3']
- if hasattr(w, 'this_current_screen_border'):
- w.this_current_screen_border = colors['color15']
+ if hasattr(w, 'inactive'):
+ w.inactive = colors['color8']
- if hasattr(w, 'this_screen_border'):
- w.this_screen_border = colors['color15']
+ if hasattr(w, 'this_current_screen_border'):
+ w.this_current_screen_border = colors['color15']
- if hasattr(w, 'other_current_screen_border'):
- w.other_current_screen_border = colors['color8']
+ if hasattr(w, 'this_screen_border'):
+ w.this_screen_border = colors['color15']
- if hasattr(w, 'other_screen_border'):
- w.other_screen_border = colors['color8']
+ if hasattr(w, 'other_current_screen_border'):
+ w.other_current_screen_border = colors['color8']
- if isinstance(w, kuro.utils.widgets.AudioVisualizerWidget):
- w.graph_color = colors['color15']
- w.fill_color = colors['color8']
+ if hasattr(w, 'other_screen_border'):
+ w.other_screen_border = colors['color8']
- bar.draw()
+ if isinstance(w, widget.WidgetBox):
+ for subw in w.widgets:
+ update_widget(subw)
- # Update colors in visualizers and restart visualizers
- with open(Config.get("glava_color_file_path", "~/.config/glava/kurobars_color.glsl"), 'w') as f:
- f.write("#define COLOR {}\n#request setbg {}00".format(colors['color15'], colors['color1'][1:]))
- qtile.theme_instance.reinitialize_visualizers()
+ for w in bar.widgets:
+ update_widget(w)
- utils.notify(
- "Updated colorscheme!",
- "active: {}, inactive: {}".format(colors['color15'], colors['color1'])
- )
+ bar.draw()
+ else:
+ logger.warning(f"Screen {screen_i} has no bar?")
+
+ # Attempt to call pywalfox to update firefox/thunderbird colors
+ try:
+ logger.warning(f"Calling 'pywalfox update'...")
+ p = utils.execute(["pywalfox", "update"])
+ p.wait()
+ except subprocess.SubprocessError as e:
+ logger.error(f"Error running 'pywalfox update': {e}")
+
+ utils.notify(qtile,
+ "Updated colorscheme!",
+ f"active: {colors['color15']}, inactive: {colors['color1']}")
+
+ def setup_xdg_desktop_portal(self):
+ # XDG Desktop portal is used for screensharing, screenshots and filepickers in wayland.
+ # To work correctly, it needs to have two env variables set in the systemd user session
+ logger.warning(f"Setting XDG_CURRENT_DESKTOP env and updating XDG Desktop Portal configuration...")
+ os.environ["XDG_CURRENT_DESKTOP"] = "qtile"
+ subprocess.Popen(["systemctl", "--user", "import-environment", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP"])
+ subprocess.Popen(["dbus-update-activation-environment", "--systemd", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP=qtile"])
+ subprocess.Popen(["systemctl", "--user", "restart", "xdg-desktop-portal"])
diff --git a/kuro/utils/general.py b/kuro/utils/general.py
index f63e1ad..c8d6a8c 100644
--- a/kuro/utils/general.py
+++ b/kuro/utils/general.py
@@ -2,20 +2,14 @@ import re
import subprocess
import traceback
from time import sleep
+from typing import List
import notify2
-import six
from dbus import DBusException
from libqtile import widget
-from libqtile.backend.x11.window import Internal
-from libqtile.bar import Bar
from notify2 import Notification, URGENCY_NORMAL
from libqtile.log_utils import logger
-
-try:
- notify2.init("QTileWM")
-except DBusException as e:
- logger.error("Could not initialize notify2: {}".format(e))
+from libqtile import qtile
BUTTON_LEFT = 1
BUTTON_MIDDLE = 2
@@ -38,40 +32,68 @@ def is_running(process):
def execute(process):
- if isinstance(process, list):
- return subprocess.Popen(process)
- elif isinstance(process, str):
- return subprocess.Popen(process.split())
- else:
- pass
-
-
-def execute_once(process):
- if not is_running(process):
+ try:
if isinstance(process, list):
return subprocess.Popen(process)
elif isinstance(process, str):
return subprocess.Popen(process.split())
else:
- pass
+ logger.info(f"Failed to execute_once")
+ except FileNotFoundError as e:
+ logger.error(f"Could not execute {process}, FileNotFoundError - {e}")
+
+
+def execute_once(process):
+ logger.info(f"Attempting to execute_once: {process}")
+ if not is_running(process):
+ return execute(process)
+ logger.info(f"Process was already running: {process}")
+
+
+def start_in_group(theme, qtile, group: str, command: List[str], floating: bool = False,
+ intrusive: bool = False, dont_break: bool = False):
+ try:
+ proc = subprocess.Popen(command)
+ match_args = {"net_wm_pid": proc.pid}
+ rule_args = {
+ "float": floating,
+ "intrusive": intrusive,
+ "group": group,
+ "break_on_match": not dont_break,
+ }
+ rule_id = qtile.add_rule(match_args, rule_args)
+ theme.autostart_app_rules[proc.pid] = rule_id
+ return proc
+ except FileNotFoundError as e:
+ logger.error(f"Could not execute {process}, FileNotFoundError - {e}")
+
+
+def start_in_group_once(theme, qtile, group: str, command: List[str], floating: bool = False,
+ intrusive: bool = False, dont_break: bool = False):
+ logger.info(f"Attempting to start_in_group_once: {command}")
+ if not is_running(command):
+ return start_in_group(theme=theme, qtile=qtile, group=group, command=command,
+ floating=floating, intrusive=intrusive, dont_break=dont_break)
+ logger.info(f"Process was already running: {command}")
def call_process(command, **kwargs):
"""
- This method uses `subprocess.check_output` to run the given command
- and return the string from stdout, which is decoded when using
- Python 3.
+ Run the given command and return the string from stdout.
"""
- output = subprocess.check_output(command, **kwargs)
- if six.PY3:
- output = output.decode()
- return output
+ return subprocess.check_output(command, **kwargs).decode()
def get_screen_count():
try:
- output = subprocess.check_output("xrandr -q".split()).decode('utf-8')
- output = [x for x in output.split("\n") if " connected" in x]
+ if qtile.core.name == "x11":
+ logger.info("Using xrandr to detect screen count")
+ output = subprocess.check_output("xrandr -q".split()).decode('utf-8')
+ output = [x for x in output.split("\n") if " connected" in x]
+ else:
+ logger.info("Using lsmon (wallutils) to detect screen count")
+ output = subprocess.check_output(["lsmon"]).decode('utf-8')
+ output = output.split("\n")
except subprocess.CalledProcessError:
return 1
@@ -87,8 +109,21 @@ def bar_separator(config):
padding=config.get('padding_spacer', 4),
)
+def init_notify(qtile):
+ if qtile and qtile.theme_instance and qtile.theme_instance.startup_completed:
+ try:
+ if not notify2.is_initted():
+ logger.warning("Initializing Notify2")
+ notify2.init("QTileWM")
+ except DBusException:
+ logger.error(f"Failed to initialize Notify2 (DBus error), retrying later.")
+ except Exception:
+ logger.error(f"Failed to initialize Notify2 (Generic error), retrying later.")
+ else:
+ logger.warning(f"Not initializing Notify2 yet, QTile startup not completed.")
-def notify(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None):
+
+def notify(qtile, title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None):
if image is not None:
notification = Notification(
summary=title, message=content,
@@ -101,13 +136,14 @@ def notify(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None):
notification.set_timeout(timeout)
notification.set_urgency(urgency)
+ init_notify(qtile)
+
try:
- return notification.show()
- except notify2.UninittedError:
- logger.warning("Notify2 was uninitialized, initializing...")
- notify2.init("qtile")
- return notification.show()
- except DBusException as e:
+ try:
+ return notification.show()
+ except notify2.UninittedError:
+ logger.warning("Notify2 is not initialized")
+ except Exception as e:
logger.warning("Showing notification failed: {}".format(e))
logger.warning(traceback.format_exc())
@@ -125,6 +161,10 @@ def spawn_popup(qtile, x, y, text):
:return: The popup instance
:rtype: Internal
"""
+ if qtile.core.name == "x11":
+ from libqtile.backend.x11.window import Internal
+ else:
+ from libqtile.backend.wayland.window import Internal
popup = Internal.create(
qtile, x, y, 100, 100, opacity=1
)
@@ -155,14 +195,14 @@ def test_popups(qtile):
def display_wm_class(qtile):
- window = qtile.currentWindow if qtile else None
+ window = qtile.current_window if qtile else None
if window:
- wm_class = window.window.get_wm_class() or None
+ wm_class = window.get_wm_class() or None
name = window.name
if wm_class:
- notify(title="WM_Class of {}".format(name),
+ notify(qtile=qtile, title="WM_Class of {}".format(name),
content="{}".format(wm_class),
urgency=notify2.URGENCY_CRITICAL)
@@ -189,76 +229,3 @@ def bluetooth_audio_sink():
def bluetooth_audio_connected():
return bluetooth_audio_sink() != -1
-
-
-class KuroTopBar(Bar):
- def __init__(self, theme, widgets, size, **config):
- self.theme = theme
- super(KuroTopBar, self).__init__(widgets, size, **config)
-
- def _configure(self, qtile, screen, *args, **kwargs):
- super(KuroTopBar, self)._configure(qtile, screen)
- self.window.handle_EnterNotify = self.handle_enter_notify
- self.window.handle_LeaveNotify = self.handle_leave_notify
- self.window.window.set_property("_NET_WM_NAME", "KuroTopBar")
- self.window.update_name()
-
- def draw(self):
- if not self.widgets:
- return
- if not self._draw_queued:
- self.future = self.qtile.call_soon(self._actual_draw)
- self._draw_queued = True
-
- def _actual_draw(self):
- self._draw_queued = False
- self._resize(self._length, self.widgets)
- for i in self.widgets:
- i.draw()
- if self.widgets:
- end = i.offset + i.length
- if end < self._length:
- if self.horizontal:
- self.drawer.draw(offsetx=end, width=self._length - end)
- else:
- self.drawer.draw(offsety=end, height=self._length - end)
-
- self.theme.update_visualizers()
-
- def handle_enter_notify(self, e):
- # self.theme.log_debug("Bar HandleEnterNotify")
- #
- # self.window.opacity = Config.get('bar_hover_opacity', 1.0)
- # print("Bar Hover Enter")
- #
- # try:
- # hovered_widget = [x for x in self.widgets if (x.offsetx + x.width) >= e.event_x][0]
- # except IndexError:
- # hovered_widget = None
- #
- # self.theme.log_debug("Hovered over {}".format(hovered_widget))
- #
- # if hasattr(hovered_widget, "handle_hover_enter"):
- # hovered_widget.handle_hover_enter(e)
-
- self.draw()
-
- def handle_leave_notify(self, e):
- # self.theme.log_debug("Bar HandleLeaveNotify")
- #
- # self.window.opacity = Config.get('bar_opacity', 1.0)
- # print("Bar Hover Leave")
- #
- # try:
- # hovered_widget = [x for x in self.widgets if (x.offsetx + x.width) >= e.event_x][0]
- # except IndexError:
- # hovered_widget = None
- #
- # self.theme.log_debug("Hovered over {}".format(hovered_widget))
- #
- # if hasattr(hovered_widget, "handle_hover_leave"):
- # hovered_widget.handle_hover_leave(e)
-
- self.draw()
-
-
diff --git a/kuro/utils/kb_backlight.py b/kuro/utils/kb_backlight.py
index b818ec4..e684735 100644
--- a/kuro/utils/kb_backlight.py
+++ b/kuro/utils/kb_backlight.py
@@ -55,7 +55,7 @@ def handle_focus_change(theme):
window = qtile.currentWindow if qtile else None
if window:
- wm_class = window.window.get_wm_class() or None
+ wm_class = window.get_wm_class() or None
name = window.name
if wm_class:
diff --git a/kuro/utils/layouts.py b/kuro/utils/layouts.py
index e74dee6..719b526 100644
--- a/kuro/utils/layouts.py
+++ b/kuro/utils/layouts.py
@@ -32,29 +32,25 @@ class KuroFloating(Floating):
# 'sun-awt-X11-XWindowPeer' is a dropdown used in Java application,
# don't reposition it anywhere, let Java app to control it
- cls = client.window.get_wm_class() or ''
- is_java_dropdown = 'sun-awt-X11-XWindowPeer' in cls
+ cls = client.get_wm_class() or ""
+ is_java_dropdown = "sun-awt-X11-XWindowPeer" in cls
if is_java_dropdown:
client.paint_borders(bc, bw)
- client.cmd_bring_to_front()
+ client.bring_to_front()
# alternatively, users may have asked us explicitly to leave the client alone
elif any(m.compare(client) for m in self.no_reposition_rules):
client.paint_borders(bc, bw)
- client.cmd_bring_to_front()
+ client.bring_to_front()
else:
above = False
# We definitely have a screen here, so let's be sure we'll float on screen
- try:
- client.float_x
- client.float_y
- except AttributeError:
+ if client.float_x is None or client.float_y is None:
# this window hasn't been placed before, let's put it in a sensible spot
above = self.compute_client_position(client, screen_rect)
-
client.place(
client.x,
client.y,
@@ -63,5 +59,6 @@ class KuroFloating(Floating):
bw,
bc,
above,
+ respect_hints=True,
)
client.unhide()
diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py
index 442595c..ff95975 100644
--- a/kuro/utils/widgets.py
+++ b/kuro/utils/widgets.py
@@ -7,62 +7,19 @@ import cairocffi
import iwlib
import netifaces
import psutil
-import six
-import unicodedata
-from libqtile import bar, pangocffi
+from libqtile import bar, qtile
from libqtile.log_utils import logger
-from libqtile.command.base import expose_command
from libqtile.widget import base
from libqtile.widget.base import ORIENTATION_HORIZONTAL
from libqtile.widget.battery import default_icon_path, load_battery, BatteryState
-from libqtile.widget.check_updates import CheckUpdates
-from libqtile.widget.currentlayout import CurrentLayoutIcon
-from libqtile.widget.graph import _Graph
-from libqtile.widget.tasklist import TaskList
from libqtile.widget.wlan import get_status
-from libqtile.backend.x11.window import Window
+from libqtile.widget.groupbox import GroupBox
+from libqtile.command.base import expose_command
from kuro.utils.general import notify, BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT, BUTTON_DOWN, BUTTON_UP, BUTTON_MUTE, \
call_process
-class CheckUpdatesYay(CheckUpdates):
- def __init__(self, **config):
- super(CheckUpdatesYay, self).__init__(**config)
- # Override command and output with yay command
- self.cmd = "yay -Qu".split()
- self.status_cmd = "yay -Qu --color never".split()
- self.update_cmd = "sudo yay".split()
- self.subtr = 0
-
- def _check_updates(self):
- #subprocess.check_output(self.update_cmd)
- res = super(CheckUpdatesYay, self)._check_updates()
- return res
-
- def button_press(self, x, y, button):
- if button == BUTTON_LEFT:
- output = subprocess.check_output(self.status_cmd).decode('utf-8').split('\n')
-
- num_updates = len(output)-1
- msg = "{} updates available.".format(num_updates)
-
- if num_updates > 0:
- msg += "\n\n"
- for x in range(min(num_updates, 9)):
- msg += output[x] + "\n"
- if num_updates > 9:
- msg += "and {} more...".format(num_updates-9)
-
- notify(
- "System updates",
- msg
- )
-
- elif button == BUTTON_MIDDLE and self.execute is not None:
- subprocess.Popen(self.execute, shell=True)
-
-
class DualPaneTextboxBase(base._Widget):
"""
Base class for widgets that are two boxes next to each other both containing text.
@@ -267,364 +224,6 @@ class DualPaneTextboxBase(base._Widget):
return d
-class MediaWidget(base.InLoopPollText):
- """Media Status Widget"""
-
- class Status:
- OFFLINE = 0
- PLAYING = 1
- PAUSED = 2
- STOPPED = 3
-
- orientations = base.ORIENTATION_HORIZONTAL
- defaults = [
- ('off_text', '', 'The pattern for the text if no players are found.'),
- ('on_text_play', ' {}', 'The pattern for the text if music is playing.'),
- ('on_text_pause', ' {}', 'The pattern for the text if music is paused.'),
- ('on_text_stop', ' {}', 'The pattern for the text if music is stopped.'),
- ('update_interval', 1, 'The update interval.'),
- ('max_chars_per_player', 50, 'Maximum characters of text per player.'),
- ('ignore_players', '', 'Comma-separated list of players to ignore.')
- ]
-
- player_icons = {
- 'spotify': '',
- 'vlc': '',
- 'firefox': '',
- 'mpv': '',
- }
-
- custom_player_data = {
- 'firefox': {
- 'showing': False,
- 'title': '',
- 'state': Status.STOPPED,
- }
- }
-
- image_urls = {}
- current_image_url = None
- player_to_control = None
-
- def __init__(self, **config):
- super(MediaWidget, self).__init__(**config)
- self.add_defaults(MediaWidget.defaults)
- self.surfaces = {}
- self.player_to_control = None
-
- def _player_to_control(self):
- info = self._get_info()
- players = {}
- for player in info.keys():
- if player not in self.custom_player_data.keys():
- if info[player][0] in [MediaWidget.Status.PLAYING, MediaWidget.Status.PAUSED]:
- players[player] = info[player]
-
- if self.player_to_control is not None and self.player_to_control not in players.keys():
- self.player_to_control = None
-
- if self.player_to_control is not None:
- players = {self.player_to_control: players[self.player_to_control]}
-
- if len(players.keys()) == 1:
- player = list(players.keys())[0]
- self.player_to_control = player
- return player
-
- elif len(players) == 0:
- notify("MediaWidget", "Nothing to control!")
- else:
- notify("MediaWidget", "Multiple players to control, I don't know what you want to do!")
-
- return None
-
- def button_press(self, x, y, button):
- if button == BUTTON_LEFT:
- player = self._player_to_control()
- if player is not None:
- command = ["playerctl", "-i", self.ignore_players, "-p", player, "play-pause"]
- _ = self.call_process(command)
- notify("MediaWidget", "Toggled {}".format(player))
- if button == BUTTON_RIGHT:
- player = self._player_to_control()
- if player is not None:
- command = ["playerctl", "-i", self.ignore_players, "-p", player, "next"]
- _ = self.call_process(command)
- if button == BUTTON_MIDDLE:
- # Jump to the screen that the player is on
- # clients = list(self.bar.qtile.windows_map.values())
- # logger.warning("{}")
- pass
-
- @expose_command()
- def update_custom_player(self, player_name, data):
- # Update firefox player
- if player_name.startswith("firefox"):
- if data['playing'] and data['muted']:
- self.custom_player_data['firefox']['showing'] = True
- self.custom_player_data['firefox']['state'] = MediaWidget.Status.PAUSED
- self.custom_player_data['firefox']['title'] = data['title']
- elif data['playing'] and not data['muted']:
- self.custom_player_data['firefox']['showing'] = True
- self.custom_player_data['firefox']['state'] = MediaWidget.Status.PLAYING
- self.custom_player_data['firefox']['title'] = data['title']
- elif not data['playing'] and data['muted']:
- self.custom_player_data['firefox']['showing'] = True
- self.custom_player_data['firefox']['state'] = MediaWidget.Status.STOPPED
- self.custom_player_data['firefox']['title'] = data['title']
- elif not data['playing'] and not data['muted']:
- self.custom_player_data['firefox']['showing'] = False
- self.custom_player_data['firefox']['state'] = MediaWidget.Status.OFFLINE
- self.custom_player_data['firefox']['title'] = data['title']
-
- def _get_players(self):
- players = []
-
- # Playerctl players
- try:
- result = self.call_process(["playerctl", "-i", self.ignore_players, "-l"])
- except subprocess.CalledProcessError:
- result = None
-
- if result:
- players.extend([x for x in result.split("\n") if x])
-
- # Custom players - Firefox
- if self.custom_player_data['firefox']['showing']:
- players.append('firefox')
-
- if players:
- return players
- else:
- return None
-
- def _get_info(self):
- players = self._get_players()
-
- if not players:
- return {}
- else:
- result = {}
-
- for player in players:
- if player in self.custom_player_data.keys():
- # Custom player -- Firefox
- if player == "firefox":
- result[player] = [self.custom_player_data['firefox']['state'], self.custom_player_data['firefox']['title']]
-
- # Other custom players -- generic attempt with error catching
- else:
- try:
- result[player] = [self.custom_player_data[player]['state'],
- self.custom_player_data[player]['title']]
- except KeyError:
- pass
-
- else:
- # PlayerCtl player
- command = ["playerctl", "-i", self.ignore_players, "-p", player, "status"]
- cmd_result = self.call_process(command).strip()
-
- text = "Unknown"
- if cmd_result in ["Playing", "Paused"]:
- try:
- artist = self.call_process(['playerctl', "-i", self.ignore_players, '-p', player, 'metadata', 'artist']).strip()
- except subprocess.CalledProcessError:
- artist = None
- try:
- title = self.call_process(['playerctl', "-i", self.ignore_players, '-p', player, 'metadata', 'title']).strip()
- except subprocess.CalledProcessError:
- title = None
-
- if artist and title:
- text = "{} - {}".format(artist, title)
- elif artist:
- text = artist
- elif title:
- text = title
-
- if cmd_result == "Playing":
- result[player] = [MediaWidget.Status.PLAYING, text]
- elif cmd_result == "Paused":
- result[player] = [MediaWidget.Status.PAUSED, text]
- elif cmd_result == "Stopped":
- result[player] = [MediaWidget.Status.STOPPED, ""]
-
- return result
-
- def _get_formatted_text(self, status):
- if status[0] == MediaWidget.Status.PLAYING:
- res = self.on_text_play.format(status[1])
- elif status[0] == MediaWidget.Status.PAUSED:
- res = self.on_text_pause.format(status[1])
- elif status[0] == MediaWidget.Status.STOPPED:
- res = self.on_text_stop.format(status[1])
- else:
- res = "Unknown"
- res = pangocffi.markup_escape_text(res)
- res = unicodedata.normalize('NFKD', res)
- if len(res) > self.max_chars_per_player:
- res = res[:self.max_chars_per_player] + "..."
- return res
-
- def draw(self):
- super(MediaWidget, self).draw()
-
- def poll(self):
- text = []
- status = self._get_info()
- if not status:
- return self.off_text
- else:
- for player in status.keys():
- # Shorten firefox.instance[0-9]+ to just firefox for icon finding
- if player.startswith("firefox"):
- player_icon = "firefox"
- else:
- player_icon = player
- icon = self.player_icons.get(player_icon, player_icon)
- text.append("{} {}".format(icon, self._get_formatted_text(status[player])))
-
- return " | ".join(text) if text else self.off_text
-
-
-class AudioVisualizerWidget(_Graph):
- """Display Audio Visualization graph"""
- orientations = base.ORIENTATION_HORIZONTAL
- fixed_upper_bound = True
- defaults = [
- ("graph_color", "FFFFFF.0", "Graph color"),
- ("fill_color", "FFFFFF.0", "Fill color for linefill graph"),
- ("border_color", "FFFFFF.0", "Widget border color"),
- ("border_width", 0, "Widget border width"),
- ("line_width", 0, "Line width"),
- ]
-
- def __init__(self, **config):
- _Graph.__init__(self, **config)
- self.add_defaults(AudioVisualizerWidget.defaults)
-
- self.client = None
- self.screen = None
-
- self.old_position = None
-
- def set_client(self, c, s):
- self.client = c
- self.screen = s
-
- def update_graph(self):
- if self.client is not None:
- viz_info = self.info()
- pos_x = viz_info['offset'] + self.margin_x + self.screen.x
- pos_y = 0 + self.margin_y + self.screen.y
- if self.old_position != (pos_x, pos_y):
- self.old_position = (pos_x, pos_y)
-
- # Check if a window on this screen is full-screen
- fullscreen = False
- for window in self.screen.group.windows:
- if isinstance(window, Window):
- if window.fullscreen:
- fullscreen = True
- break
-
- logger.debug("Repositioning {} {} to {}x{}".format(self.client, self.client.window.wid, pos_x, pos_y))
- self.client.reposition(pos_x, pos_y, above=not fullscreen)
-
- self.draw()
-
- def draw(self):
- self.drawer.clear(self.background or self.bar.background)
- self.drawer.draw(offsetx=self.offset, width=self.width)
-
-
-class KuroCurrentLayoutIcon(CurrentLayoutIcon):
- def _get_layout_names(self):
- names = list(super(KuroCurrentLayoutIcon, self)._get_layout_names())
-
- from kuro.utils import layouts as kuro_layouts
- from libqtile.layout.base import Layout
- klayouts = [
- layout_class_name.lower()
- for layout_class, layout_class_name
- in map(lambda x: (getattr(kuro_layouts, x), x), dir(kuro_layouts))
- if isinstance(layout_class, six.class_types) and issubclass(layout_class, Layout)
- ]
- names.extend(klayouts)
-
- return set(names)
-
-
-class KuroTaskList(TaskList):
- defaults = [
- (
- 'txt_pinned',
- 'P ',
- 'Text representation of the pinned window state. '
- 'e.g., "P " or "\U0001F5D7 "'
- ),
- (
- 'markup_pinned',
- None,
- 'Text markup of the pinned window state. Supports pangomarkup with markup=True.'
- 'e.g., "{}" or "{}"'
- ),
- ]
-
- def __init__(self, *args, **kwargs):
- super(KuroTaskList, self).__init__(*args, **kwargs)
- self.add_defaults(KuroTaskList.defaults)
-
- def get_taskname(self, window):
- """
- Get display name for given window.
- Depending on its state minimized, maximized and floating
- appropriate characters are prepended.
- """
- state = ''
- markup_str = self.markup_normal
-
- # Enforce markup and new string format behaviour when
- # at least one markup_* option is used.
- # Mixing non markup and markup may cause problems.
- if self.markup_minimized or self.markup_maximized\
- or self.markup_floating or self.markup_focused or self.markup_pinned:
- enforce_markup = True
- else:
- enforce_markup = False
-
- if window is None:
- pass
- elif hasattr(window, "is_static_window") and window.is_static_window:
- state = self.txt_pinned
- markup_str = self.markup_pinned
- elif window.minimized:
- state = self.txt_minimized
- markup_str = self.markup_minimized
- elif window.maximized:
- state = self.txt_maximized
- markup_str = self.markup_maximized
- elif window.floating:
- state = self.txt_floating
- markup_str = self.markup_floating
- elif window is window.group.current_window:
- markup_str = self.markup_focused
-
- window_name = window.name if window and window.name else "?"
-
- # Emulate default widget behavior if markup_str is None
- if enforce_markup and markup_str is None:
- markup_str = "%s{}" % (state)
-
- if markup_str is not None:
- self.markup = True
- window_name = pangocffi.markup_escape_text(window_name)
- return markup_str.format(window_name)
-
- return "%s%s" % (state, window_name)
-
-
class GPUStatusWidget(base._TextBox):
"""Displays the currently used GPU."""
@@ -655,7 +254,11 @@ class GPUStatusWidget(base._TextBox):
self.icons.update(self.custom_icons)
def _get_info(self):
- output = self.call_process(self.check_command, shell=True)
+ try:
+ output = self.call_process(self.check_command, shell=True)
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Error while calling {self.check_command} - {e}")
+ output = None
mode = "nvidia" if "nvidia" in output else "intel" if "intel" in output else "unknown"
return {'error': False, 'mode': mode}
@@ -731,14 +334,16 @@ class GPUStatusWidget(base._TextBox):
if button == BUTTON_LEFT:
try:
next_gpu = self.call_process(self.next_command, shell=True).split(":")[1].strip()
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Error while calling {self.next_command} - {e}")
except IndexError:
next_gpu = "Unknown"
if self.current_status == "Unknown":
- notify("GPU Status", "The currently used GPU is unknown.\n\nAfter the next login it will be the {} GPU.".format(next_gpu),
+ notify(None, "GPU Status", "The currently used GPU is unknown.\n\nAfter the next login it will be the {} GPU.".format(next_gpu),
image=os.path.join(self.theme_path, "gpu-unknown.png"))
else:
- notify("GPU Status", "The system is currently running on the {} GPU. Press the middle mouse "
+ notify(None, "GPU Status", "The system is currently running on the {} GPU. Press the middle mouse "
"button on this icon to switch GPUs.\n\nAfter the next login it will be the {} GPU.".format(
self.current_status, next_gpu
),
@@ -747,38 +352,25 @@ class GPUStatusWidget(base._TextBox):
if button == BUTTON_MIDDLE:
command = ["optimus-manager", "--no-confirm", "--switch", "auto"]
- output = self.call_process(command)
+ try:
+ output = self.call_process(command)
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Error while calling {command} - {e}")
+ output = ""
if "nvidia" in output:
- notify("GPU Switched", "The GPU has been switched from Intel to NVidia.\n"
+ notify(None, "GPU Switched", "The GPU has been switched from Intel to NVidia.\n"
"Please log out and log back in to apply the changes to the session.",
image=os.path.join(self.theme_path, "gpu-nvidia.png"))
elif "intel" in output:
- notify("GPU Switched", "The GPU has been switched from NVidia to Intel.\n"
+ notify(None, "GPU Switched", "The GPU has been switched from NVidia to Intel.\n"
"Please log out and log back in to apply the changes to the session.",
image=os.path.join(self.theme_path, "gpu-intel.png"))
else:
- notify("GPU Switch Error", "I could not determine if the GPU was switched successfully.\n"
+ notify(None, "GPU Switch Error", "I could not determine if the GPU was switched successfully.\n"
"Please log out and log back in to clear up the inconsistency.",
image=os.path.join(self.theme_path, "gpu-unknown.png"))
-class TextSpacerWidget(base._TextBox):
- """Displays a text separator"""
- orientations = base.ORIENTATION_HORIZONTAL
- defaults = [
- ('spacer', None, 'The character/text to use as separator. Default "|" if None.'),
- ('color', "#ffffff", "Color of the text."),
- ]
-
- def __init__(self, **config):
- super(TextSpacerWidget, self).__init__("Separator", bar.CALCULATED, **config)
- self.add_defaults(TextSpacerWidget.defaults)
- self.text = self.spacer or "|"
-
- def draw(self):
- base._TextBox.draw(self)
-
-
class ThermalSensorWidget(DualPaneTextboxBase):
defaults = [
('show_tag', False, 'Show tag sensor'),
@@ -816,7 +408,11 @@ class ThermalSensorWidget(DualPaneTextboxBase):
self.timeout_add(self.update_interval, self.timer_setup)
def _update_values(self):
- sensors_out = self.call_process(self.get_command())
+ try:
+ sensors_out = self.call_process(self.get_command())
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Error while calling {self.get_command()} - {e}")
+ return
temperature_values = {}
for name, temp, symbol in self.sensors_temp.findall(sensors_out):
name = name.strip()
@@ -846,7 +442,7 @@ class ThermalSensorWidget(DualPaneTextboxBase):
def button_press(self, x, y, button):
if button == BUTTON_LEFT:
- notify("Temperature Information", "\n".join(
+ notify(None, "Temperature Information", "\n".join(
"{}: {}{}".format(name, *values) for name, values in self.values.items()
))
@@ -899,7 +495,7 @@ class CPUInfoWidget(DualPaneTextboxBase):
def button_press(self, x, y, button):
if button == BUTTON_LEFT:
total = sum([self.cpu_old[0], self.cpu_old[1], self.cpu_old[2], self.cpu_old[3]])
- notify("CPU Information", "user: {} %\nnice: {} %\nsys: {} %\nidle: {} %\ntotal: {} %".format(
+ notify(None, "CPU Information", "user: {} %\nnice: {} %\nsys: {} %\nidle: {} %\ntotal: {} %".format(
math.ceil((self.cpu_old[0] / total) * 100),
math.ceil((self.cpu_old[1] / total) * 100),
math.ceil((self.cpu_old[2] / total) * 100),
@@ -952,7 +548,7 @@ class MemoryInfoWidget(DualPaneTextboxBase):
val['SwapUsed'] = swap.used // 1024 // 1024
if button == BUTTON_LEFT:
- notify("Memory Information", "Memory: {}MB / {}MB\n {}%\nSwap: {}MB / {}MB\n {}%".format(
+ notify(None, "Memory Information", "Memory: {}MB / {}MB\n {}%\nSwap: {}MB / {}MB\n {}%".format(
val['MemUsed'], val['MemTotal'],
math.ceil((mem.used / mem.total) * 100),
val['SwapUsed'], val['SwapTotal'],
@@ -1008,7 +604,7 @@ class DiskIOInfoWidget(DualPaneTextboxBase):
def button_press(self, x, y, button):
if button == BUTTON_LEFT:
- notify("Disk IO Information",
+ notify(None, "Disk IO Information",
"Time that there were IO requests queued for /dev/{}: {} ms".format(self.hdd_device, self.io))
@@ -1058,7 +654,7 @@ class NetworkInfoWidget(DualPaneTextboxBase):
status = iwlib.get_iwconfig(self.wireless_interface)
self.wireless_ips = netifaces.ifaddresses(self.wireless_interface)
disconnected = essid is None
- percent = math.ceil((quality / 70) * 100)
+ percent = math.ceil(((quality or 0) / 70) * 100)
self.wireless_quality = quality
self.wireless_signal = percent
self.wireless_name = essid
@@ -1077,7 +673,12 @@ class NetworkInfoWidget(DualPaneTextboxBase):
self.wired_ipv4 = self.wired_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr']
self.wired_ipv6 = self.wired_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr']
self.wired_mac = self.wired_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr']
- eth_status = call_process(["ip", "link", "show", "{}".format(self.wired_interface)])
+ command = ["ip", "link", "show", "{}".format(self.wired_interface)]
+ try:
+ eth_status = call_process(command)
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Error while calling {command} - {e}")
+ return
m = self.wired_up_regex.search(eth_status)
if m:
self.wired_connected = "UP" in m.group(1)
@@ -1135,9 +736,9 @@ class NetworkInfoWidget(DualPaneTextboxBase):
wired_text = "Wired: Not connected"
if wifi_text:
- notify(title, "{}\n\n{}".format(wifi_text, wired_text))
+ notify(None, title, "{}\n\n{}".format(wifi_text, wired_text))
else:
- notify(title, "\n{}".format(wired_text))
+ notify(None, title, "\n{}".format(wired_text))
class BatteryInfoWidget(DualPaneTextboxBase):
@@ -1213,7 +814,7 @@ class BatteryInfoWidget(DualPaneTextboxBase):
def button_press(self, x, y, button):
if button == BUTTON_LEFT:
output = subprocess.check_output(self.status_cmd).decode('utf-8')
- notify("Battery Status", output)
+ notify(None, "Battery Status", output)
class VolumeInfoWidget(DualPaneTextboxBase):
@@ -1248,7 +849,8 @@ class VolumeInfoWidget(DualPaneTextboxBase):
else:
cmd = self.status_cmd
mixer_out = self.call_process(cmd.split(" "))
- except subprocess.CalledProcessError:
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Error while calling {cmd} - {e}")
return -1
try:
return int(mixer_out)
@@ -1292,7 +894,7 @@ class VolumeInfoWidget(DualPaneTextboxBase):
output = subprocess.check_output(cmd.split(" ")).decode('utf-8')
sink = "Sink {}\n".format(self.pulse_sink) if self.pulse_sink else ""
- notify("Volume Status", sink+output)
+ notify(None, "Volume Status", sink+output)
elif button == BUTTON_RIGHT:
if "{sink}" in self.volume_app:
@@ -1343,3 +945,16 @@ class VolumeInfoWidget(DualPaneTextboxBase):
def run_app(self):
# Emulate button press.
self.button_press(0, 0, BUTTON_RIGHT)
+
+
+class KuroGroupBox(GroupBox):
+ @property
+ def length(self):
+ try:
+ return super(KuroGroupBox, self).length
+ except AttributeError:
+ return 1
+
+ @length.setter
+ def length(self, length):
+ logger.warning(f"Setting groupbox length to {length}")
diff --git a/kuro/utils/windows.py b/kuro/utils/windows.py
index 6bfd997..7370274 100644
--- a/kuro/utils/windows.py
+++ b/kuro/utils/windows.py
@@ -1,6 +1,11 @@
from cairocffi.test_xcb import xcffib
-from libqtile import hook
-from libqtile.backend.x11.window import Window, Static
+from libqtile import hook, qtile
+
+if qtile.core.name == "x11":
+ from libqtile.backend.x11.window import Window, Static
+else:
+ from libqtile.backend.wayland.window import Window, Static
+
class KuroStatic(Static):
diff --git a/required_packages.txt b/required_packages.txt
index ff3bd88..cf0478e 100644
--- a/required_packages.txt
+++ b/required_packages.txt
@@ -10,3 +10,26 @@ playerctl
xfce4-screenshooter
xfce4-clipman-plugin
wireless_tools
+
+# Utilities
+kdeconnect # KDE Connect
+vorta # Backup scheduler
+
+# Xorg-only
+picom # Compositor
+xfce4-clipman # Clipboard manager
+dunst # Notification daemon
+xiccd # Color profile manager
+
+# Wayland-only
+xorg-xwayland
+python-pywlroots
+wofi # dmenu replacement
+grim # Screenshot utility
+swappy # Screenshot editor
+slurp # Region selector
+cliphist # Clipboard history
+mako # Notifications daemon
+kanshi # Display hotplugging
+wallutils # Display/wallpaper utilities (lsmon)
+papirus-icon-theme # Icon theme