diff --git a/.gitignore b/.gitignore index c01ebd2..9322969 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,4 @@ ENV/ # Rope project settings .ropeproject +.idea diff --git a/config.py b/config.py new file mode 100644 index 0000000..45cd90a --- /dev/null +++ b/config.py @@ -0,0 +1,94 @@ +# Copyright (c) 2010 Aldo Cortesi +# Copyright (c) 2010, 2014 dequis +# Copyright (c) 2012 Randall Ma +# Copyright (c) 2012-2014 Tycho Andersen +# Copyright (c) 2012 Craig Barnes +# Copyright (c) 2013 horsik +# Copyright (c) 2013 Tao Sauvage +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Import Theme +from libqtile import hook + +try: + from kuro.theme import Kuro + Theme = Kuro() +except ImportError: + from kuro.base import BaseTheme + Kuro = None + Theme = BaseTheme() + +# Import theme configuration +try: + from kuro.config import Config +except ImportError: + try: + from kuro.baseconfig import BaseConfig as Config + except ImportError: + Config = None + raise ImportError("Could not load theme Config or BaseConfig!") + +# Initialize the Theme +Theme.initialize() + +# Hook theme into all hooks we know of +hook.subscribe.startup_once(Theme.callback_startup_once) +hook.subscribe.startup(Theme.callback_startup) +hook.subscribe.startup_complete(Theme.callback_startup_complete) +hook.subscribe.setgroup(Theme.callback_setgroup) +hook.subscribe.addgroup(Theme.callback_addgroup) +hook.subscribe.delgroup(Theme.callback_delgroup) +hook.subscribe.changegroup(Theme.callback_changegroup) +hook.subscribe.focus_change(Theme.callback_focus_change) +hook.subscribe.float_change(Theme.callback_float_change) +hook.subscribe.group_window_add(Theme.callback_group_window_add) +hook.subscribe.window_name_change(Theme.callback_window_name_change) +hook.subscribe.client_new(Theme.callback_client_new) +hook.subscribe.client_managed(Theme.callback_client_managed) +hook.subscribe.client_killed(Theme.callback_client_killed) +hook.subscribe.client_state_changed(Theme.callback_client_state_changed) +hook.subscribe.client_type_changed(Theme.callback_client_type_changed) +hook.subscribe.client_focus(Theme.callback_client_focus) +hook.subscribe.client_mouse_enter(Theme.callback_client_mouse_enter) +hook.subscribe.client_name_updated(Theme.callback_client_name_updated) +hook.subscribe.client_urgent_hint_changed(Theme.callback_client_urgent_hint_changed) +hook.subscribe.layout_change(Theme.callback_layout_change) +hook.subscribe.net_wm_icon_change(Theme.callback_net_wm_icon_change) +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.current_screen_change(Theme.callback_current_screen_change) + +# Initialize variables from theme +keys = Theme.keys +groups = Theme.groups +layouts = Theme.layouts +widget_defaults = Theme.widget_defaults +screens = Theme.screens +dgroups_key_binder = Theme.dgroups_key_binder +dgroups_app_rules = Theme.dgroups_app_rules +main = Theme.main +follow_mouse_focus = Theme.follow_mouse_focus +bring_front_click = Theme.bring_front_click +cursor_warp = Theme.cursor_warp +floating_layout = Theme.floating_layout +auto_fullscreen = Theme.auto_fullscreen +focus_on_window_activation = Theme.focus_on_window_activation +extentions = Theme.extensions diff --git a/kuro/base.py b/kuro/base.py new file mode 100644 index 0000000..cf8b7bf --- /dev/null +++ b/kuro/base.py @@ -0,0 +1,145 @@ +from libqtile import layout as libqtile_layout + + +class BaseConfig: + @classmethod + def get(cls, key, default): + if hasattr(cls, key): + return cls.__dict__[key] + else: + return default + + +class BaseTheme: + # Changing variables initialized by function + keys = None + groups = None + layouts = None + widget_defaults = None + screens = None + + # 'Static' variables + dgroups_key_binder = None + dgroups_app_rules = [] + main = None + follow_mouse_focus = True + bring_front_click = False + cursor_warp = False + floating_layout = libqtile_layout.Floating() + auto_fullscreen = True + focus_on_window_activation = "smart" + extensions = [] + + # XXX: Gasp! We're lying here. In fact, nobody really uses or cares about this + # string besides java UI toolkits; you can see several discussions on the + # mailing lists, github issues, and other WM documentation that suggest setting + # this string if your java app doesn't work correctly. We may as well just lie + # and say that we're a working one by default. + # + # We choose LG3D to maximize irony: it is a 3D non-reparenting WM written in + # java that happens to be on java's whitelist. + wmname = "LG3D" + + def initialize(self): + self.widget_defaults = self.init_widget_defaults() + self.keys = self.init_keys() + self.groups = self.init_groups() + self.layouts = self.init_layouts() + self.screens = self.init_screens() + + def init_keys(self): + return [] + + def init_groups(self): + return [] + + def init_layouts(self): + return [] + + def init_widget_defaults(self): + return {} + + def init_screens(self): + return [] + + def init_mouse(self): + return [] + + # Callbacks + def callback_startup_once(self, *args, **kwargs): + pass + + def callback_startup(self, *args, **kwargs): + pass + + def callback_startup_complete(self, *args, **kwargs): + pass + + def callback_setgroup(self, *args, **kwargs): + pass + + def callback_addgroup(self, *args, **kwargs): + pass + + def callback_delgroup(self, *args, **kwargs): + pass + + def callback_changegroup(self, *args, **kwargs): + pass + + def callback_focus_change(self, *args, **kwargs): + pass + + def callback_float_change(self, *args, **kwargs): + pass + + def callback_group_window_add(self, *args, **kwargs): + pass + + def callback_window_name_change(self, *args, **kwargs): + pass + + def callback_client_new(self, *args, **kwargs): + pass + + def callback_client_managed(self, *args, **kwargs): + pass + + def callback_client_killed(self, *args, **kwargs): + pass + + def callback_client_state_changed(self, *args, **kwargs): + pass + + def callback_client_type_changed(self, *args, **kwargs): + pass + + def callback_client_focus(self, *args, **kwargs): + pass + + def callback_client_mouse_enter(self, *args, **kwargs): + pass + + def callback_client_name_updated(self, *args, **kwargs): + pass + + def callback_client_urgent_hint_changed(self, *args, **kwargs): + pass + + def callback_layout_change(self, *args, **kwargs): + pass + + def callback_net_wm_icon_change(self, *args, **kwargs): + pass + + def callback_selection_notify(self, *args, **kwargs): + pass + + def callback_selection_change(self, *args, **kwargs): + pass + + def callback_screen_change(self, *args, **kwargs): + pass + + def callback_current_screen_change(self, *args, **kwargs): + pass diff --git a/kuro/config.py b/kuro/config.py new file mode 100644 index 0000000..3c36627 --- /dev/null +++ b/kuro/config.py @@ -0,0 +1,58 @@ +from kuro.base import BaseConfig + + +# Config variables used in the main configuration +class Config(BaseConfig): + # Show debug messages + debug = True + + # Default Applications + app_terminal = "terminator" + + # Images + desktop_bg = "/home/kevin/Pictures/wallpapers/desktop.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 + + # Colours + colour_border_normal = "#333333" + colour_border_focus = "#ffffff" + colour_groupbox_border_normal = "#333333" + colour_groupbox_border_focus = "#aaaaaa" + colour_groupbox_icon_active = "#ffffff" + colour_groupbox_icon_inactive = "#777777" + colour_spacer_background = "#777777" + + # Battery variables + battery_theme_path = "/home/kevin/.config/qtile/kuro/resources/battery" + battery_update_delay = 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/battery/battery-caution-charging.png b/kuro/resources/battery/battery-caution-charging.png new file mode 100644 index 0000000..30efa91 Binary files /dev/null and b/kuro/resources/battery/battery-caution-charging.png differ diff --git a/kuro/resources/battery/battery-caution.png b/kuro/resources/battery/battery-caution.png new file mode 100644 index 0000000..40fdef5 Binary files /dev/null and b/kuro/resources/battery/battery-caution.png differ diff --git a/kuro/resources/battery/battery-full-charged.png b/kuro/resources/battery/battery-full-charged.png new file mode 100644 index 0000000..3446740 Binary files /dev/null and b/kuro/resources/battery/battery-full-charged.png differ diff --git a/kuro/resources/battery/battery-full-charging.png b/kuro/resources/battery/battery-full-charging.png new file mode 100644 index 0000000..03262cd Binary files /dev/null and b/kuro/resources/battery/battery-full-charging.png differ diff --git a/kuro/resources/battery/battery-full.png b/kuro/resources/battery/battery-full.png new file mode 100644 index 0000000..81831c8 Binary files /dev/null and b/kuro/resources/battery/battery-full.png differ diff --git a/kuro/resources/battery/battery-good-charging.png b/kuro/resources/battery/battery-good-charging.png new file mode 100644 index 0000000..2a8e115 Binary files /dev/null and b/kuro/resources/battery/battery-good-charging.png differ diff --git a/kuro/resources/battery/battery-good.png b/kuro/resources/battery/battery-good.png new file mode 100644 index 0000000..5883885 Binary files /dev/null and b/kuro/resources/battery/battery-good.png differ diff --git a/kuro/resources/battery/battery-low-charging.png b/kuro/resources/battery/battery-low-charging.png new file mode 100644 index 0000000..0f3962a Binary files /dev/null and b/kuro/resources/battery/battery-low-charging.png differ diff --git a/kuro/resources/battery/battery-low.png b/kuro/resources/battery/battery-low.png new file mode 100644 index 0000000..6eb007d Binary files /dev/null and b/kuro/resources/battery/battery-low.png differ diff --git a/kuro/resources/battery/battery-missing.png b/kuro/resources/battery/battery-missing.png new file mode 100644 index 0000000..a87a68e Binary files /dev/null and b/kuro/resources/battery/battery-missing.png differ diff --git a/kuro/theme.py b/kuro/theme.py new file mode 100644 index 0000000..671f208 --- /dev/null +++ b/kuro/theme.py @@ -0,0 +1,201 @@ +import logging +import os + +from libqtile.config import Key, Screen, Group, Drag, Click +from libqtile.command import lazy +from libqtile import layout, bar, widget + +# Import theme util functions +from kuro import utils + +# Import variables +from kuro.base import BaseTheme + +try: + from kuro.config import Config +except ImportError: + try: + from kuro.baseconfig import BaseConfig as Config + except ImportError: + Config = None + raise ImportError("Could not load theme Config or BaseConfig!") + +# Initialize logging +log = logging.getLogger(__name__) + + +class Kuro(BaseTheme): + # Shorthand for modifier key + mod = Config.get("modifier", "mod4") + + # Show debug messages + debug = Config.get('debug', False) + + def initialize(self): + log.debug("Initializing Kuro Theme...") + + super(Kuro, self).initialize() + + # Update keys with keys for groups + self.update_keys() + + def init_keys(self): + log.debug("Initializing keys") + + return [ + # Switch between windows in current stack pane + Key([self.mod], "k", lazy.layout.down()), + Key([self.mod], "j", lazy.layout.up()), + + # Move windows up or down in current stack + Key([self.mod, "control"], "k", lazy.layout.shuffle_down()), + Key([self.mod, "control"], "j", lazy.layout.shuffle_up()), + + # Switch window focus to other pane(s) of stack + Key([self.mod], "space", lazy.layout.next()), + + # Swap panes of split stack + Key([self.mod, "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([self.mod, "shift"], "Return", lazy.layout.toggle_split()), + Key([self.mod], "Return", lazy.spawn(Config.get('app_terminal', "xterm"))), + + # 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()), + ] + + def init_groups(self): + log.debug("Initializing groups") + + # http://fontawesome.io/cheatsheet + return [Group(i) for i in ""] + + def init_layouts(self): + 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.Max(), + layout.Stack(num_stacks=2) + ] + + def init_widget_defaults(self): + log.debug("Initializing widget_defaults") + + return { + "font": Config.get('font_default', "Sans"), + "fontsize": Config.get('fontsize_default', 16), + "padding": 3, + } + + def init_screens(self): + log.debug("Initializing screens") + + num_screens = utils.get_screen_count() + + screens = [] + for x in range(num_screens): + widgets = [] + widgets.extend([ + utils.bar_separator(Config), + 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) + ), + utils.bar_separator(Config), + widget.Prompt(**self.widget_defaults), + + widget.WindowName(**self.widget_defaults), + + widget.CPUGraph( + width=25, + border_color="#000000", + border_width=0, + line_width=1, + 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'), + update_delay=Config.get('battery_update_delay', 30) + ), + ]) + + # Systray only on first screen + if x == 0: + widgets.append(widget.Systray(**self.widget_defaults)) + + widgets.extend([ + utils.bar_separator(Config), + widget.Clock(format="%a %d %b, %H:%M", **self.widget_defaults), + utils.CheckUpdatesYaourt( + 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), + **self.widget_defaults + ), + widget.TextBox("#{}".format(x), name="default", **self.widget_defaults), + ]) + screens.append(Screen(top=bar.Bar( + widgets=widgets, + size=Config.get('height_groupbox', 30) + ))) + + return screens + + def init_mouse(self): + log.debug("Initializing mouse") + + # Drag floating layouts. + return [ + Drag([self.mod], "Button1", lazy.window.set_position_floating(), + start=lazy.window.get_position()), + Drag([self.mod], "Button3", lazy.window.set_size_floating(), + start=lazy.window.get_size()), + Click([self.mod], "Button2", lazy.window.bring_to_front()) + ] + + def update_keys(self): + log.debug("Updating keys") + + for i, g in enumerate(self.groups): + # mod1 + number = switch to group + self.keys.append( + Key([self.mod], str(i + 1), lazy.group[g.name].toscreen()) + ) + + # mod1 + shift + number = switch to & move focused window to group + self.keys.append( + Key([self.mod, "shift"], str(i + 1), lazy.window.togroup(g.name)) + ) + + def callback_startup(self): + utils.execute("sleep 3") + utils.execute_once("nitrogen --restore") diff --git a/kuro/utils.py b/kuro/utils.py new file mode 100644 index 0000000..ad58367 --- /dev/null +++ b/kuro/utils.py @@ -0,0 +1,50 @@ +import re +import subprocess +from libqtile import widget, bar +from libqtile.widget.check_updates import CheckUpdates + + +def is_running(process): + s = subprocess.Popen(["ps", "axuw"], stdout=subprocess.PIPE) + for x in s.stdout: + if re.search(process, x.decode('utf-8')): + return True + return False + + +def execute(process): + return subprocess.Popen(process.split()) + + +def execute_once(process): + if not is_running(process): + return subprocess.Popen(process.split()) + + +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] + except subprocess.CalledProcessError: + return 1 + + if output: + return len(output) + else: + return 1 + + +def bar_separator(config): + return widget.Sep(foreground=config.get('colour_spacer_background', '#777777'), + linewidth=config.get('width_spacer', 1), + padding=config.get('padding_spacer', 4), + ) + + +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.subtr = 0 +