diff --git a/config.py b/config.py index 45cd90a..3638a27 100644 --- a/config.py +++ b/config.py @@ -26,24 +26,30 @@ # Import Theme from libqtile import hook +from libqtile.log_utils import logger try: from kuro.theme import Kuro Theme = Kuro() -except ImportError: - from kuro.base import BaseTheme - Kuro = None - Theme = BaseTheme() +except ImportError as e: + logger.error("Could not load Kuro Theme. Trying to load BaseTheme. Error: {}".format(e)) + try: + from kuro.base import BaseTheme as Kuro + Theme = Kuro() + except ImportError as e: + Kuro = None + raise ImportError("Could not load theme Config or BaseTheme! Error: {}".format(e)) # Import theme configuration try: from kuro.config import Config -except ImportError: +except ImportError as e: + logger.error("Could not load Kuro Config. Trying to load BaseConfig. Error: {}".format(e)) try: from kuro.baseconfig import BaseConfig as Config - except ImportError: + except ImportError as e: Config = None - raise ImportError("Could not load theme Config or BaseConfig!") + raise ImportError("Could not load theme Config or BaseConfig! Error: {}".format(e)) # Initialize the Theme Theme.initialize() @@ -92,3 +98,14 @@ floating_layout = Theme.floating_layout auto_fullscreen = Theme.auto_fullscreen focus_on_window_activation = Theme.focus_on_window_activation extentions = Theme.extensions + + +def main(qtile): + # set logging level + if Config.get('debug', False): + if Config.get('verbose', False): + qtile.cmd_debug() + else: + qtile.cmd_info() + else: + qtile.cmd_warning() diff --git a/kuro/base.py b/kuro/base.py index cf8b7bf..5886634 100644 --- a/kuro/base.py +++ b/kuro/base.py @@ -1,4 +1,6 @@ -from libqtile import layout as libqtile_layout +from libqtile import layout as libqtile_layout, layout, bar, widget +from libqtile.command import lazy +from libqtile.config import Key, Group, Screen, Drag, Click class BaseConfig: @@ -25,7 +27,22 @@ class BaseTheme: follow_mouse_focus = True bring_front_click = False cursor_warp = False - floating_layout = libqtile_layout.Floating() + floating_layout = libqtile_layout.Floating(float_rules=[ + {'wmclass': 'confirm'}, + {'wmclass': 'dialog'}, + {'wmclass': 'download'}, + {'wmclass': 'error'}, + {'wmclass': 'file_progress'}, + {'wmclass': 'notification'}, + {'wmclass': 'splash'}, + {'wmclass': 'toolbar'}, + {'wmclass': 'confirmreset'}, # gitk + {'wmclass': 'makebranch'}, # gitk + {'wmclass': 'maketag'}, # gitk + {'wname': 'branchdialog'}, # gitk + {'wname': 'pinentry'}, # GPG key password entry + {'wmclass': 'ssh-askpass'}, # ssh-askpass + ]) auto_fullscreen = True focus_on_window_activation = "smart" extensions = [] @@ -48,22 +65,88 @@ class BaseTheme: self.screens = self.init_screens() def init_keys(self): - return [] + return [ + # Switch between windows in current stack pane + Key(["mod4"], "k", lazy.layout.down()), + Key(["mod4"], "j", lazy.layout.up()), + + # Move windows up or down in current stack + Key(["mod4", "control"], "k", lazy.layout.shuffle_down()), + Key(["mod4", "control"], "j", lazy.layout.shuffle_up()), + + # Switch window focus to other pane(s) of stack + Key(["mod4"], "space", lazy.layout.next()), + + # Swap panes of split stack + Key(["mod4", "shift"], "space", lazy.layout.rotate()), + + # Toggle between split and unsplit sides of stack. + # Split = all windows displayed + # Unsplit = 1 window displayed, like Max layout, but still with + # multiple stack panes + Key(["mod4", "shift"], "Return", lazy.layout.toggle_split()), + Key(["mod4"], "Return", lazy.spawn("xterm")), + + # Toggle between different layouts as defined below + Key(["mod4"], "Tab", lazy.next_layout()), + Key(["mod4"], "w", lazy.window.kill()), + + Key(["mod4", "control"], "r", lazy.restart()), + Key(["mod4", "control"], "q", lazy.shutdown()), + Key(["mod4"], "r", lazy.spawncmd()), + ] def init_groups(self): - return [] + groups = [Group(i) for i in "asdfuiop"] + for i in groups: + self.keys.extend([ + # mod1 + letter of group = switch to group + Key(["mod4"], i.name, lazy.group[i.name].toscreen()), + + # mod1 + shift + letter of group = switch to & move focused window to group + Key(["mod4", "shift"], i.name, lazy.window.togroup(i.name)), + ]) + return groups def init_layouts(self): - return [] + return [ + layout.Max(), + layout.Stack(num_stacks=2) + ] def init_widget_defaults(self): - return {} + return dict( + font='sans', + fontsize=12, + padding=3, + ) def init_screens(self): - return [] + # noinspection PyUnresolvedReferences + return [ + Screen( + bottom=bar.Bar( + [ + widget.GroupBox(), + widget.Prompt(), + widget.WindowName(), + widget.TextBox("Kuro BaseConfig", foreground="#ff0000", name="default"), + widget.Systray(), + widget.Clock(format='%Y-%m-%d %a %I:%M %p'), + ], + 24, + ), + ), + ] def init_mouse(self): - return [] + 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): diff --git a/kuro/config.py b/kuro/config.py index 3c36627..9ec79b0 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -3,56 +3,110 @@ from kuro.base import BaseConfig # Config variables used in the main configuration class Config(BaseConfig): - # Show debug messages + # Show debug bar and messages debug = True + verbose = False # Default Applications app_terminal = "terminator" + app_launcher = "dmenu_run -i -p '»' -nb '#000000' -fn 'Noto Sans-11' -nf '#777777' -sb '#1793d0' -sf '#ffffff'" # Images desktop_bg = "/home/kevin/Pictures/wallpapers/desktop.png" + applauncher_image = "/home/kevin/.config/qtile/kuro/resources/arch.png" # Fonts font_default = "Noto Sans" font_topbar = "Noto Sans" font_clock = "Noto Sans" font_titlebar = "Noto Sans" - font_groupbox = "FontAwesome" # Font sizes fontsize_default = 11 fontsize_topbar = 11 fontsize_clock = 11 fontsize_titlebar = 11 - fontsize_groupbox = 15 # Sizes width_border = 1 margin_layout = 4 - width_groupbox_border = 1 - height_groupbox = 24 - margin_groupbox = 0 width_spacer = 1 padding_spacer = 4 - - # Variables - bool_groupbox_disable_drag = True - bool_groupbox_rounded_borders = True + grow_amount = 5 + width_zoomy_column = 300 # Colours colour_border_normal = "#333333" colour_border_focus = "#ffffff" + colour_border_urgent = "#774400" + colour_spacer_background = "#777777" + + # Groupbox variables + font_groupbox = "FontAwesome" + fontsize_groupbox = 15 + width_groupbox_border = 1 + height_groupbox = 24 + margin_groupbox = 0 + bool_groupbox_disable_drag = True + bool_groupbox_rounded_borders = True colour_groupbox_border_normal = "#333333" colour_groupbox_border_focus = "#aaaaaa" colour_groupbox_icon_active = "#ffffff" colour_groupbox_icon_inactive = "#777777" - colour_spacer_background = "#777777" + + # Tasklist variables + tasklist_border = "#ffffff" + tasklist_urgent_border = "#774400" + tasklist_font = "Noto Sans" + tasklist_fontsize = 11 + + # Thermal indicator variables + thermal_threshold = 75 + thermal_sensor = "Package id 0" + thermal_chip = "coretemp-isa-0000" + + # CPU graph variables + cpu_graph_colour = '#ff0000' + + # Memory graph variables + mem_graph_colour = '#ff00ff' + + # HDD graph variables + hdd_graph_colour = '#ffff00' # Battery variables battery_theme_path = "/home/kevin/.config/qtile/kuro/resources/battery" - battery_update_delay = 2 + battery_update_delay = 5 + + # Wifi variables + wifi_interface = "wlp4s0" + wifi_theme_path = "/home/kevin/.config/qtile/kuro/resources/wifi" + wifi_update_interval = 5 + + # Normal volume icon variables + volume_font = "Noto Sans" + volume_fontsize = 11 + volume_theme_path = "/home/kevin/.config/qtile/kuro/resources/volume" + volume_get_command = "pamixer --sink 0 --get-volume".split() + volume_mute_command = "pamixer --sink 0 -t".split() + volume_up_command = "pamixer --sink 0 -i 2".split() + volume_down_command = "pamixer --sink 0 -d 2".split() + volume_is_bluetooth_icon = False + volume_update_interval = 0.2 + + # Bluetooth volume icon variables + bluevol_font = "Noto Sans" + bluevol_fontsize = 11 + bluevol_theme_path = "/home/kevin/.config/qtile/kuro/resources/bluetooth_volume" + bluevol_get_command = "pamixer --sink {bsink} --get-volume".split() + bluevol_mute_command = "pamixer --sink {bsink} -t".split() + bluevol_up_command = "pamixer --sink {bsink} -i 2".split() + bluevol_down_command = "pamixer --sink {bsink} -d 2".split() + bluevol_is_bluetooth_icon = True + bluevol_update_interval = 0.2 # CheckUpdates variables updates_display_format = "{updates}" updates_execute_command = "terminator -e 'echo Updating\ via\ yaourt\ -Sayu...; yaourt -Sayu'" updates_colour_available = '#f4d742' + diff --git a/kuro/resources/arch.png b/kuro/resources/arch.png new file mode 100644 index 0000000..362be65 Binary files /dev/null and b/kuro/resources/arch.png differ diff --git a/kuro/resources/bluetooth_volume/audio-volume-high.png b/kuro/resources/bluetooth_volume/audio-volume-high.png new file mode 100644 index 0000000..a714794 Binary files /dev/null and b/kuro/resources/bluetooth_volume/audio-volume-high.png differ diff --git a/kuro/resources/bluetooth_volume/audio-volume-low.png b/kuro/resources/bluetooth_volume/audio-volume-low.png new file mode 100644 index 0000000..3810a3a Binary files /dev/null and b/kuro/resources/bluetooth_volume/audio-volume-low.png differ diff --git a/kuro/resources/bluetooth_volume/audio-volume-medium.png b/kuro/resources/bluetooth_volume/audio-volume-medium.png new file mode 100644 index 0000000..16f932b Binary files /dev/null and b/kuro/resources/bluetooth_volume/audio-volume-medium.png differ diff --git a/kuro/resources/bluetooth_volume/audio-volume-muted-blocked.png b/kuro/resources/bluetooth_volume/audio-volume-muted-blocked.png new file mode 100644 index 0000000..5d049ce Binary files /dev/null and b/kuro/resources/bluetooth_volume/audio-volume-muted-blocked.png differ diff --git a/kuro/resources/bluetooth_volume/audio-volume-muted.png b/kuro/resources/bluetooth_volume/audio-volume-muted.png new file mode 100644 index 0000000..b6b276b Binary files /dev/null and b/kuro/resources/bluetooth_volume/audio-volume-muted.png differ diff --git a/kuro/resources/bluetooth_volume/audio-volume-off.png b/kuro/resources/bluetooth_volume/audio-volume-off.png new file mode 100644 index 0000000..8a6d8c5 Binary files /dev/null and b/kuro/resources/bluetooth_volume/audio-volume-off.png differ diff --git a/kuro/resources/volume/audio-volume-high.png b/kuro/resources/volume/audio-volume-high.png new file mode 100644 index 0000000..a714794 Binary files /dev/null and b/kuro/resources/volume/audio-volume-high.png differ diff --git a/kuro/resources/volume/audio-volume-low.png b/kuro/resources/volume/audio-volume-low.png new file mode 100644 index 0000000..3810a3a Binary files /dev/null and b/kuro/resources/volume/audio-volume-low.png differ diff --git a/kuro/resources/volume/audio-volume-medium.png b/kuro/resources/volume/audio-volume-medium.png new file mode 100644 index 0000000..16f932b Binary files /dev/null and b/kuro/resources/volume/audio-volume-medium.png differ diff --git a/kuro/resources/volume/audio-volume-muted-blocked.png b/kuro/resources/volume/audio-volume-muted-blocked.png new file mode 100644 index 0000000..5d049ce Binary files /dev/null and b/kuro/resources/volume/audio-volume-muted-blocked.png differ diff --git a/kuro/resources/volume/audio-volume-muted.png b/kuro/resources/volume/audio-volume-muted.png new file mode 100644 index 0000000..b6b276b Binary files /dev/null and b/kuro/resources/volume/audio-volume-muted.png differ diff --git a/kuro/resources/volume/audio-volume-off.png b/kuro/resources/volume/audio-volume-off.png new file mode 100644 index 0000000..8a6d8c5 Binary files /dev/null and b/kuro/resources/volume/audio-volume-off.png differ diff --git a/kuro/resources/wifi/wireless-disconnected.png b/kuro/resources/wifi/wireless-disconnected.png new file mode 100644 index 0000000..8a2bc24 Binary files /dev/null and b/kuro/resources/wifi/wireless-disconnected.png differ diff --git a/kuro/resources/wifi/wireless-full.png b/kuro/resources/wifi/wireless-full.png new file mode 100644 index 0000000..29d40a6 Binary files /dev/null and b/kuro/resources/wifi/wireless-full.png differ diff --git a/kuro/resources/wifi/wireless-high.png b/kuro/resources/wifi/wireless-high.png new file mode 100644 index 0000000..b3622ec Binary files /dev/null and b/kuro/resources/wifi/wireless-high.png differ diff --git a/kuro/resources/wifi/wireless-low.png b/kuro/resources/wifi/wireless-low.png new file mode 100644 index 0000000..1d06946 Binary files /dev/null and b/kuro/resources/wifi/wireless-low.png differ diff --git a/kuro/resources/wifi/wireless-medium.png b/kuro/resources/wifi/wireless-medium.png new file mode 100644 index 0000000..6bd0ec5 Binary files /dev/null and b/kuro/resources/wifi/wireless-medium.png differ diff --git a/kuro/resources/wifi/wireless-none.png b/kuro/resources/wifi/wireless-none.png new file mode 100644 index 0000000..0d43617 Binary files /dev/null and b/kuro/resources/wifi/wireless-none.png differ diff --git a/kuro/theme.py b/kuro/theme.py index 671f208..57e458d 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -1,6 +1,3 @@ -import logging -import os - from libqtile.config import Key, Screen, Group, Drag, Click from libqtile.command import lazy from libqtile import layout, bar, widget @@ -21,7 +18,7 @@ except ImportError: raise ImportError("Could not load theme Config or BaseConfig!") # Initialize logging -log = logging.getLogger(__name__) +from libqtile.log_utils import logger as log class Kuro(BaseTheme): @@ -36,7 +33,7 @@ class Kuro(BaseTheme): super(Kuro, self).initialize() - # Update keys with keys for groups + # Update keys with keys for groups and layouts self.update_keys() def init_keys(self): @@ -57,20 +54,31 @@ class Kuro(BaseTheme): # Swap panes of split stack Key([self.mod, "shift"], "space", lazy.layout.rotate()), + # Fullscreen toggle + Key([self.mod], 'f', lazy.window.toggle_fullscreen()), + # Toggle between split and unsplit sides of stack. # Split = all windows displayed # Unsplit = 1 window displayed, like Max layout, but still with # multiple stack panes Key([self.mod, "shift"], "Return", lazy.layout.toggle_split()), + + # Super-Enter to start terminal Key([self.mod], "Return", lazy.spawn(Config.get('app_terminal', "xterm"))), + # Super-R to start dmenu_run + Key([self.mod], "r", lazy.spawn(Config.get('app_launcher', "dmenu_run"))), + + # Super-Shift-R to start spawncmd + Key([self.mod, "shift"], "r", lazy.spawncmd()), + + # Toggle between different layouts as defined below Key([self.mod], "Tab", lazy.next_layout()), Key([self.mod], "w", lazy.window.kill()), Key([self.mod, "control"], "r", lazy.restart()), Key([self.mod, "control"], "q", lazy.shutdown()), - Key([self.mod], "r", lazy.spawncmd()), # Key([self.mod, "shift"], "e", self.evaluate()), ] @@ -84,22 +92,28 @@ class Kuro(BaseTheme): log.debug("Initializing layouts") return [ - layout.Matrix(columns=2, - border_focus=Config.get('colour_border_focus', "#ffffff"), - border_normal=Config.get('colour_border_normal', "#777777"), - border_width=Config.get('width_border', "1"), - margin=Config.get('margin_layout', "0"), - ), + layout.Wmii( + border_focus=Config.get('colour_border_focus', "#ffffff"), + border_focus_stack=Config.get('colour_border_normal', "#777777"), + border_normal=Config.get('colour_border_normal', "#777777"), + border_normal_stack=Config.get('colour_border_normal', "#777777"), + border_width=Config.get('width_border', "1"), + grow_amount=Config.get('grow_amount', "5"), + margin=Config.get('margin_layout', "0"), + ), layout.Max(), - layout.Stack(num_stacks=2) + layout.Zoomy( + columnwidth=Config.get('width_zoomy_column', 150), + margin=Config.get('margin_layout', "0"), + ) ] def init_widget_defaults(self): log.debug("Initializing widget_defaults") return { - "font": Config.get('font_default', "Sans"), - "fontsize": Config.get('fontsize_default', 16), + "font": Config.get('font_topbar', "Sans"), + "fontsize": Config.get('fontsize_topbar', 16), "padding": 3, } @@ -107,11 +121,16 @@ class Kuro(BaseTheme): log.debug("Initializing screens") num_screens = utils.get_screen_count() + if num_screens == 0: + num_screens = 1 screens = [] for x in range(num_screens): widgets = [] widgets.extend([ + utils.AppLauncherIcon( + filename=Config.get('applauncher_image', 'apps.png') + ), utils.bar_separator(Config), widget.GroupBox( active=Config.get('colour_groupbox_icon_active', '#ffffff'), @@ -129,22 +148,114 @@ class Kuro(BaseTheme): utils.bar_separator(Config), widget.Prompt(**self.widget_defaults), - widget.WindowName(**self.widget_defaults), + 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) + ), + + utils.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) + ), widget.CPUGraph( - width=25, - border_color="#000000", - border_width=0, - line_width=1, - samples=10, + width=Config.get('cpu_width', 25), + border_color=Config.get('cpu_border_colour', "#000000"), + graph_color=Config.get('cpu_graph_colour', "#00ffff"), + border_width=Config.get('cpu_graph_width', 0), + line_width=Config.get('cpu_line_width', 1), + samples=Config.get('cpu_samples', 10), ), + + widget.MemoryGraph( + width=Config.get('mem_width', 25), + border_color=Config.get('mem_border_colour', "#000000"), + graph_color=Config.get('mem_graph_colour', "#00ffff"), + border_width=Config.get('mem_graph_width', 0), + line_width=Config.get('mem_line_width', 1), + samples=Config.get('mem_samples', 10), + ), + + widget.HDDBusyGraph( + width=Config.get('hdd_width', 25), + border_color=Config.get('hdd_border_colour', "#000000"), + graph_color=Config.get('hdd_graph_colour', "#00ffff"), + border_width=Config.get('hdd_border_width', 0), + line_width=Config.get('hdd_line_width', 1), + samples=Config.get('hdd_samples', 10), + ), + + widget.NetGraph( + width=Config.get('net_width', 25), + border_color=Config.get('net_border_colour', "#000000"), + graph_color=Config.get('net_graph_colour', "#00ffff"), + border_width=Config.get('net_border_width', 0), + line_width=Config.get('net_line_width', 1), + samples=Config.get('net_samples', 10), + ), + widget.BatteryIcon( battery_name=Config.get('battery_name', 'BAT0'), energy_full_file=Config.get('battery_energy_full_file', 'charge_full'), energy_now_file=Config.get('battery_energy_now_file', 'charge_now'), - theme_path=Config.get('battery_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile/checkouts/latest/libqtile/resources/battery-icons'), + theme_path=Config.get('battery_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile' + '/checkouts/latest/libqtile/resources/battery-icons'), update_delay=Config.get('battery_update_delay', 30) ), + + utils.WifiIconWidget( + interface=Config.get('wifi_interface', 'wlp4s0'), + theme_path=Config.get('wifi_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile' + '/checkouts/latest/libqtile/resources/battery-icons'), + update_interval=Config.get('wifi_update_interval', 30) + ), + + utils.PulseVolumeWidget( + cardid=Config.get('volume_cardid', None), + channel=Config.get('volume_channel', 'Master'), + device=Config.get('volume_device', None), + font=Config.get('volume_font', 'Arial'), + fontsize=Config.get('volume_fontsize', 15), + foreground=Config.get('volume_foreground', '#ffffff'), + get_volume_command=Config.get('volume_get_command', None), + mute_command=Config.get('volume_mute_command', None), + theme_path=Config.get('volume_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile' + '/checkouts/latest/libqtile/resources/volume-icons'), + volume_down_command=Config.get('volume_down_command', None), + volume_up_command=Config.get('volume_up_command', None), + is_bluetooth_icon=Config.get('volume_is_bluetooth_icon', False), + update_interval=Config.get('volume_update_interval', 0.2) + ), + + utils.PulseVolumeWidget( + cardid=Config.get('bluevol_cardid', None), + channel=Config.get('bluevol_channel', 'Master'), + device=Config.get('bluevol_device', None), + font=Config.get('bluevol_font', 'Arial'), + fontsize=Config.get('bluevol_fontsize', 15), + foreground=Config.get('bluevol_foreground', '#ffffff'), + get_volume_command=Config.get('bluevol_get_command', None), + mute_command=Config.get('bluevol_mute_command', None), + theme_path=Config.get('bluevol_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile' + '/checkouts/latest/libqtile/resources/volume-icons'), + volume_down_command=Config.get('bluevol_down_command', None), + volume_up_command=Config.get('bluevol_up_command', None), + is_bluetooth_icon=Config.get('bluevol_is_bluetooth_icon', False), + update_interval=Config.get('bluevol_update_interval', 0.2) + ) ]) # Systray only on first screen @@ -152,7 +263,7 @@ class Kuro(BaseTheme): widgets.append(widget.Systray(**self.widget_defaults)) widgets.extend([ - utils.bar_separator(Config), + widget.CurrentLayoutIcon(), widget.Clock(format="%a %d %b, %H:%M", **self.widget_defaults), utils.CheckUpdatesYaourt( colour_no_updates=Config.get('updates_colour_none', '#ffffff'), @@ -168,13 +279,27 @@ class Kuro(BaseTheme): size=Config.get('height_groupbox', 30) ))) + # Add debug bars on each window if debugging is enabled + if Config.get('debug', False): + for x in range(num_screens): + widgets = [] + widgets.extend([ + widget.TextBox(" Debugging bar ", name="default", **self.widget_defaults), + widget.Notify(), + widget.DebugInfo() + ]) + screens[x].bottom = bar.Bar( + widgets=widgets, + size=Config.get('height_debugbar', 30) + ) + return screens def init_mouse(self): log.debug("Initializing mouse") # Drag floating layouts. - return [ + mouse = [ Drag([self.mod], "Button1", lazy.window.set_position_floating(), start=lazy.window.get_position()), Drag([self.mod], "Button3", lazy.window.set_size_floating(), @@ -182,6 +307,8 @@ class Kuro(BaseTheme): Click([self.mod], "Button2", lazy.window.bring_to_front()) ] + return mouse + def update_keys(self): log.debug("Updating keys") @@ -196,6 +323,30 @@ class Kuro(BaseTheme): Key([self.mod, "shift"], str(i + 1), lazy.window.togroup(g.name)) ) + # Keys for the Wmii layout + self.keys.extend([ + Key( + [self.mod, "shift", "control"], "l", + lazy.layout.grow_right() + ), + Key( + [self.mod, "shift"], "l", + lazy.layout.shuffle_right() + ), + Key( + [self.mod, "shift", "control"], "h", + lazy.layout.grow_left() + ), + Key( + [self.mod, "shift"], "h", + lazy.layout.shuffle_left() + ), + Key( + [self.mod], "s", + lazy.layout.toggle_split() + ) + ]) + def callback_startup(self): utils.execute("sleep 3") utils.execute_once("nitrogen --restore") diff --git a/kuro/utils.py b/kuro/utils.py index ad58367..adfd838 100644 --- a/kuro/utils.py +++ b/kuro/utils.py @@ -1,7 +1,27 @@ +import os import re import subprocess + +import cairocffi +import notify2 from libqtile import widget, bar +from libqtile.utils import catch_exception_and_warn, UnixCommandNotFound +from libqtile.widget import base +from libqtile.widget.battery import default_icon_path from libqtile.widget.check_updates import CheckUpdates +from libqtile.widget.image import Image +from libqtile.widget.sensors import ThermalSensor +from libqtile.widget.volume import Volume +from libqtile.widget.wlan import get_status +from libqtile.log_utils import logger +from notify2 import Notification, URGENCY_NORMAL +notify2.init("QTileWM") + +BUTTON_LEFT = 1 +BUTTON_MIDDLE = 2 +BUTTON_RIGHT = 3 +BUTTON_SCROLL_UP = 4 +BUTTON_SCROLL_DOWN = 5 def is_running(process): @@ -41,10 +61,355 @@ def bar_separator(config): ) +def notify(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None): + if image is not None: + notification = Notification( + summary=title, message=content, + icon=image + ) + else: + notification = Notification( + summary=title, message=content + ) + notification.set_timeout(timeout) + notification.set_urgency(urgency) + + return notification.show() + + +def bluetooth_audio_sink(): + try: + output = subprocess.check_output("pamixer --list-sinks".split()).decode("utf-8") + output = [x for x in output.split('\n') if "blue" in x.lower()] + except subprocess.CalledProcessError: + return -1 + + sink = -1 + try: + sink = int(output[0].split()[0]) + except IndexError: + pass + except AttributeError: + pass + except ValueError: + pass + + return sink + + +def bluetooth_audio_connected(): + return bluetooth_audio_sink() != -1 + + +class AppLauncherIcon(Image): + def button_press(self, x, y, button): + if button == BUTTON_LEFT: + execute("dmenu_run -i -p '»' -nb '#000000' -fn 'Noto Sans-11' -nf '#777777' -sb '#1793d0' -sf '#ffffff'") + + class CheckUpdatesYaourt(CheckUpdates): def __init__(self, **config): super(CheckUpdatesYaourt, self).__init__(**config) # Override command and output with yaourt command self.cmd = "yaourt -Qua".split() + self.status_cmd = "yaourt -Qua".split() + self.update_cmd = "yaourt -Sy" self.subtr = 0 + def _check_updates(self): + subprocess.check_output(self.update_cmd) + super(CheckUpdatesYaourt, self)._check_updates() + + 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 PulseVolumeWidget(Volume): + + defaults = [ + ("cardid", None, "Card Id"), + ("device", "default", "Device Name"), + ("channel", "Master", "Channel"), + ("padding", 3, "Padding left and right. Calculated if None."), + ("theme_path", None, "Path of the icons"), + ("update_interval", 0.2, "Update time in seconds."), + ("emoji", False, "Use emoji to display volume states, only if ``theme_path`` is not set." + "The specified font needs to contain the correct unicode characters."), + ("mute_command", None, "Mute command"), + ("volume_up_command", None, "Volume up command"), + ("volume_down_command", None, "Volume down command"), + ("get_volume_command", None, "Command to get the current volume"), + ("is_bluetooth_icon", False, "Is this icon for a Bluetooth Audio device?"), + ] + + _old_length = 0 + + def __init__(self, **config): + super(PulseVolumeWidget, self).__init__(**config) + self._old_length = self._length + + # Augment commands with bluetooth sink ID if this is a bluetooth icon + if self.is_bluetooth_icon and bluetooth_audio_connected(): + bsink = bluetooth_audio_sink() + self.mute_command = " ".join(self._user_config['mute_command']).format(bsink=bsink).split() + self.volume_up_command = " ".join(self._user_config['volume_up_command']).format(bsink=bsink).split() + self.volume_down_command = " ".join(self._user_config['volume_down_command']).format(bsink=bsink).split() + self.get_volume_command = " ".join(self._user_config['get_volume_command']).format(bsink=bsink).split() + logger.info("Updated bluetooth commands with bluetooth sink {}".format(bsink)) + self._length = self._old_length + self.commands_need_reset = False + elif self.is_bluetooth_icon: + self.commands_need_reset = True + else: + self.commands_need_reset = False + + self._old_length = self._length + + def reset_bluetooth_commands(self): + if self.is_bluetooth_icon and bluetooth_audio_connected(): + bsink = 0 if bluetooth_audio_sink() == -1 else bluetooth_audio_sink() + self.mute_command = " ".join(self._user_config['mute_command']).format(bsink=bsink).split() + self.volume_up_command = " ".join(self._user_config['volume_up_command']).format(bsink=bsink).split() + self.volume_down_command = " ".join(self._user_config['volume_down_command']).format(bsink=bsink).split() + self.get_volume_command = " ".join(self._user_config['get_volume_command']).format(bsink=bsink).split() + logger.info("Updated bluetooth commands with bluetooth sink {}".format(bsink)) + self._length = self._old_length + self.commands_need_reset = False + + def get_volume(self): + try: + get_volume_cmd = "echo 0".split() + + if self.get_volume_command: + if self.is_bluetooth_icon and bluetooth_audio_sink() == -1: + pass + else: + get_volume_cmd = self.get_volume_command + + mixer_out = self.call_process(get_volume_cmd) + except subprocess.CalledProcessError: + return -1 + + try: + return int(mixer_out.strip()) + except ValueError: + return -1 + + def _update_drawer(self): + super(PulseVolumeWidget, self)._update_drawer() + self.text = "" + if self.is_bluetooth_icon and not bluetooth_audio_connected(): + self._length = 0 + + def draw(self): + if self.is_bluetooth_icon and not bluetooth_audio_connected(): + if not self.commands_need_reset: + logger.info("Bluetooth device disconnected. Hiding bluetooth audio mixer") + self.commands_need_reset = True + base._TextBox.draw(self) + else: + if self.commands_need_reset: + self.reset_bluetooth_commands() + if self.theme_path: + self.drawer.draw(offsetx=self.offset, width=self.length) + else: + base._TextBox.draw(self) + + def button_press(self, x, y, button): + if button == BUTTON_LEFT: + volume = self.get_volume() + + width = 15 + + if volume >= 0: + volume_amount = round((volume/100)*width) + else: + volume_amount = 0 + + msg = "[{}{}]".format( + "".join(["#" for x in range(volume_amount)]), + "".join(["-" for x in range(width-volume_amount)]) + ) + + notify( + "{}Volume : {}%".format("Bluetooth " if self.is_bluetooth_icon else "", volume), + msg + ) + else: + super(PulseVolumeWidget, self).button_press(x, y, button) + + +class WifiIconWidget(base._TextBox): + """WiFi connection strength indicator widget.""" + + orientations = base.ORIENTATION_HORIZONTAL + defaults = [ + ('interface', 'wlan0', 'The interface to monitor'), + ('update_interval', 1, 'The update interval.'), + ('theme_path', default_icon_path(), 'Path of the icons'), + ('custom_icons', {}, 'dict containing key->filename icon map'), + ] + + def __init__(self, **config): + super(WifiIconWidget, self).__init__("WLAN", bar.CALCULATED, **config) + self.add_defaults(WifiIconWidget.defaults) + + if self.theme_path: + self.length_type = bar.STATIC + self.length = 0 + self.surfaces = {} + self.current_icon = 'wireless-disconnected' + self.icons = dict([(x, '{0}.png'.format(x)) for x in ( + 'wireless-disconnected', + 'wireless-none', + 'wireless-low', + 'wireless-medium', + 'wireless-high', + 'wireless-full', + )]) + self.icons.update(self.custom_icons) + + def _get_info(self): + try: + essid, quality = get_status(self.interface) + disconnected = essid is None + if disconnected: + return self.disconnected_message + + return { + 'error': False, + 'essid': essid, + 'quality': quality, + 'percent': (quality / 70) + } + except EnvironmentError: + logger.error( + '%s: Probably your wlan device is switched off or ' + ' otherwise not present in your system.', + self.__class__.__name__) + + return {'error': True} + + def timer_setup(self): + self.update() + self.timeout_add(self.update_interval, self.timer_setup) + + def _configure(self, qtile, bar): + super(WifiIconWidget, self)._configure(qtile, bar) + self.setup_images() + + def _get_icon_key(self): + key = 'wireless' + info = self._get_info() + if info is False or info.get('error'): + key += '-none' + elif info.get('essid') is None: + key += '-disconnected' + else: + percent = info['percent'] + if percent < 0.2: + key += '-low' + elif percent < 0.4: + key += '-medium' + elif percent < 0.8: + key += '-high' + else: + key += '-full' + + return key + + def update(self): + icon = self._get_icon_key() + if icon != self.current_icon: + self.current_icon = icon + self.draw() + + def draw(self): + if self.theme_path: + self.drawer.clear(self.background or self.bar.background) + self.drawer.ctx.set_source(self.surfaces[self.current_icon]) + self.drawer.ctx.paint() + self.drawer.draw(offsetx=self.offset, width=self.length) + else: + self.text = self.current_icon[8:] + base._TextBox.draw(self) + + def setup_images(self): + for key, name in self.icons.items(): + try: + path = os.path.join(self.theme_path, name) + img = cairocffi.ImageSurface.create_from_png(path) + except cairocffi.Error: + self.theme_path = None + logger.warning('Wireless Icon switching to text mode') + return + input_width = img.get_width() + input_height = img.get_height() + + sp = input_height / (self.bar.height - 1) + + width = input_width / sp + if width > self.length: + # cast to `int` only after handling all potentially-float values + self.length = int(width + self.actual_padding * 2) + + imgpat = cairocffi.SurfacePattern(img) + + scaler = cairocffi.Matrix() + + scaler.scale(sp, sp) + scaler.translate(self.actual_padding * -1, 0) + imgpat.set_matrix(scaler) + + imgpat.set_filter(cairocffi.FILTER_BEST) + self.surfaces[key] = imgpat + + +class ThermalSensorWidget(ThermalSensor): + defaults = [ + ('metric', True, 'True to use metric/C, False to use imperial/F'), + ('show_tag', False, 'Show tag sensor'), + ('update_interval', 2, 'Update interval in seconds'), + ('tag_sensor', None, + 'Tag of the temperature sensor. For example: "temp1" or "Core 0"'), + ('chip', None, 'Chip argument for sensors command'), + ( + 'threshold', + 70, + 'If the current temperature value is above, ' + 'then change to foreground_alert colour' + ), + ('foreground_alert', 'ff0000', 'Foreground colour alert'), + ] + + @catch_exception_and_warn(warning=UnixCommandNotFound, excepts=OSError) + def get_temp_sensors(self): + """calls the unix `sensors` command with `-f` flag if user has specified that + the output should be read in Fahrenheit. + """ + command = ["sensors", ] + if self.chip: + command.append(self.chip) + if not self.metric: + command.append("-f") + sensors_out = self.call_process(command) + return self._format_sensors_output(sensors_out)