diff --git a/config.py b/config.py index 3638a27..c2a1cbb 100644 --- a/config.py +++ b/config.py @@ -109,3 +109,6 @@ def main(qtile): qtile.cmd_info() else: qtile.cmd_warning() + + # Save qtile instance in theme + Theme.qtile = qtile diff --git a/config_debug.py b/config_debug.py new file mode 100644 index 0000000..8dbfaff --- /dev/null +++ b/config_debug.py @@ -0,0 +1,191 @@ +# 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. + +from libqtile.config import Key, Screen, Group, Drag, Click +from libqtile.command import lazy +from libqtile import layout, bar, widget + + +class KuroTopBar(bar.Bar): + def __init__(self, widgets, size, **config): + super(KuroTopBar, self).__init__(widgets, size, **config) + + def _configure(self, qtile, screen): + super(KuroTopBar, self)._configure(qtile, screen) + self.window.handle_EnterNotify = self.handle_enter_notify + self.window.handle_LeaveNotify = self.handle_leave_notify + + def handle_enter_notify(self, e): + + self.window.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 + + if hasattr(hovered_widget, "handle_hover_enter"): + hovered_widget.handle_hover_enter(e) + + self.draw() + + def handle_leave_notify(self, e): + self.window.opacity = 0.6 + 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 + + if hasattr(hovered_widget, "handle_hover_leave"): + hovered_widget.handle_hover_leave(e) + + self.draw() + + +mod = "mod4" + +keys = [ + # Switch between windows in current stack pane + Key([mod], "k", lazy.layout.down()), + Key([mod], "j", lazy.layout.up()), + + # Move windows up or down in current stack + Key([mod, "control"], "k", lazy.layout.shuffle_down()), + Key([mod, "control"], "j", lazy.layout.shuffle_up()), + + # Switch window focus to other pane(s) of stack + Key([mod], "space", lazy.layout.next()), + + # Swap panes of split stack + Key([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([mod, "shift"], "Return", lazy.layout.toggle_split()), + Key([mod], "Return", lazy.spawn("xterm")), + + # Toggle between different layouts as defined below + Key([mod], "Tab", lazy.next_layout()), + Key([mod], "w", lazy.window.kill()), + + Key([mod, "control"], "r", lazy.restart()), + Key([mod, "control"], "q", lazy.shutdown()), + Key([mod], "r", lazy.spawncmd()), +] + +groups = [Group(i) for i in "asdfuiop"] + +for i in groups: + keys.extend([ + # mod1 + letter of group = switch to group + Key([mod], i.name, lazy.group[i.name].toscreen()), + + # mod1 + shift + letter of group = switch to & move focused window to group + Key([mod, "shift"], i.name, lazy.window.togroup(i.name)), + ]) + +layouts = [ + layout.Max(), + layout.Stack(num_stacks=2) +] + +widget_defaults = dict( + font='sans', + fontsize=12, + padding=3, +) +extension_defaults = widget_defaults.copy() + + +widgets = [ + widget.GroupBox(), + widget.Prompt(), + widget.WindowName(), + widget.TextBox("default config", name="default"), + widget.Systray(), + widget.Clock(format='%Y-%m-%d %a %I:%M %p'), +] + +topbar = KuroTopBar( + background='#000000', + opacity=0.6, + widgets=widgets, + size=24 +) + +screens = [ + Screen(top=topbar), +] + +# Drag floating layouts. +mouse = [ + Drag([mod], "Button1", lazy.window.set_position_floating(), + start=lazy.window.get_position()), + Drag([mod], "Button3", lazy.window.set_size_floating(), + start=lazy.window.get_size()), + Click([mod], "Button2", lazy.window.bring_to_front()) +] + +dgroups_key_binder = None +dgroups_app_rules = [] +main = None +follow_mouse_focus = True +bring_front_click = False +cursor_warp = False +floating_layout = 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" + +# 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" + diff --git a/kuro/base.py b/kuro/base.py index 5886634..2785e17 100644 --- a/kuro/base.py +++ b/kuro/base.py @@ -19,6 +19,7 @@ class BaseTheme: layouts = None widget_defaults = None screens = None + qtile = None # 'Static' variables dgroups_key_binder = None @@ -55,6 +56,9 @@ class BaseTheme: # # We choose LG3D to maximize irony: it is a 3D non-reparenting WM written in # java that happens to be on java's whitelist. + # + # Alternatively, you could add this to .xinitrc: + # 'export _JAVA_AWT_WM_NONREPARENTING=1' wmname = "LG3D" def initialize(self): diff --git a/kuro/config.py b/kuro/config.py index 9ec79b0..a7c8486 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -10,6 +10,8 @@ class Config(BaseConfig): # Default Applications app_terminal = "terminator" app_launcher = "dmenu_run -i -p '»' -nb '#000000' -fn 'Noto Sans-11' -nf '#777777' -sb '#1793d0' -sf '#ffffff'" + cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10" + cmd_brightness_down = "sudo /usr/bin/xbacklight -dec 10" # Images desktop_bg = "/home/kevin/Pictures/wallpapers/desktop.png" @@ -29,7 +31,7 @@ class Config(BaseConfig): # Sizes width_border = 1 - margin_layout = 4 + margin_layout = 8 width_spacer = 1 padding_spacer = 4 grow_amount = 5 @@ -41,6 +43,11 @@ class Config(BaseConfig): colour_border_urgent = "#774400" colour_spacer_background = "#777777" + # Bar variables + bar_background = "#000000" + bar_opacity = 0.65 + bar_hover_opacity = 1 + # Groupbox variables font_groupbox = "FontAwesome" fontsize_groupbox = 15 diff --git a/kuro/theme.py b/kuro/theme.py index c7364bc..c2d3b1f 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -3,10 +3,12 @@ from libqtile.command import lazy from libqtile import layout, bar, widget # Import theme util functions -from kuro import utils +from kuro.utils import general as utils # Import variables from kuro.base import BaseTheme +from kuro.utils.general import display_wm_class +from kuro.utils.kb_backlight import handle_focus_change as kb_handle_focus_change try: from kuro.config import Config @@ -18,7 +20,7 @@ except ImportError: raise ImportError("Could not load theme Config or BaseConfig!") # Initialize logging -from libqtile.log_utils import logger as log +from libqtile.log_utils import logger class Kuro(BaseTheme): @@ -27,17 +29,46 @@ class Kuro(BaseTheme): # Show debug messages debug = Config.get('debug', False) + debug_textfields = [] + debug_bars = [] + + # Screen count + num_screens = 0 + + # Top bars + topbars = [] + + # Window manager name + wmname = "QTile" + + def set_debug_text(self, text): + for field in self.debug_textfields: + field.text = text + for bar in self.debug_bars: + 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) def initialize(self): - log.debug("Initializing Kuro Theme...") + self.log_debug("Initializing Kuro Theme...") super(Kuro, self).initialize() + self.update() + + def update(self): # Update keys with keys for groups and layouts self.update_keys() def init_keys(self): - log.debug("Initializing keys") + self.log_debug("Initializing keys") return [ # Switch between windows in current stack pane @@ -76,23 +107,42 @@ class Kuro(BaseTheme): Key([self.mod, "shift"], "r", lazy.spawncmd()), + # 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'))), + # 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()), + + # Redraw the top bar + Key([self.mod, "shift", "control"], "r", lazy.function(self.redraw_bar)), + + # Shutdown QTile Key([self.mod, "control"], "q", lazy.shutdown()), - # Key([self.mod, "shift"], "e", self.evaluate()), + + + + + ## + # Debug keyboard shortcuts + ## + Key([self.mod, "control"], "w", lazy.function(display_wm_class)) ] def init_groups(self): - log.debug("Initializing groups") + self.log_debug("Initializing groups") # http://fontawesome.io/cheatsheet return [Group(i) for i in ""] def init_layouts(self): - log.debug("Initializing layouts") + self.log_debug("Initializing layouts") return [ layout.Wmii( @@ -112,7 +162,7 @@ class Kuro(BaseTheme): ] def init_widget_defaults(self): - log.debug("Initializing widget_defaults") + self.log_debug("Initializing widget_defaults") return { "font": Config.get('font_topbar', "Sans"), @@ -121,20 +171,17 @@ class Kuro(BaseTheme): } def init_screens(self): - log.debug("Initializing screens") + self.log_debug("Initializing screens") - num_screens = utils.get_screen_count() - if num_screens == 0: - num_screens = 1 + self.num_screens = utils.get_screen_count() + if self.num_screens == 0: + self.num_screens = 1 screens = [] - for x in range(num_screens): + for x in range(self.num_screens): + self.log_info("Initializing bars for screen {}".format(x)) 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'), borderwidth=Config.get('width_groupbox_border', 1), @@ -148,7 +195,6 @@ class Kuro(BaseTheme): 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.TaskList( @@ -171,7 +217,8 @@ class Kuro(BaseTheme): 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) + threshold=Config.get('thermal_threshold', 70), + update_interval=5, ), widget.CPUGraph( @@ -181,6 +228,7 @@ class Kuro(BaseTheme): border_width=Config.get('cpu_graph_width', 0), line_width=Config.get('cpu_line_width', 1), samples=Config.get('cpu_samples', 10), + frequency=2, ), widget.MemoryGraph( @@ -190,6 +238,7 @@ class Kuro(BaseTheme): border_width=Config.get('mem_graph_width', 0), line_width=Config.get('mem_line_width', 1), samples=Config.get('mem_samples', 10), + frequency=2, ), widget.HDDBusyGraph( @@ -199,6 +248,7 @@ class Kuro(BaseTheme): border_width=Config.get('hdd_border_width', 0), line_width=Config.get('hdd_line_width', 1), samples=Config.get('hdd_samples', 10), + frequency=2, ), widget.NetGraph( @@ -208,6 +258,7 @@ class Kuro(BaseTheme): border_width=Config.get('net_border_width', 0), line_width=Config.get('net_line_width', 1), samples=Config.get('net_samples', 10), + frequency=2, ), widget.BatteryIcon( @@ -277,29 +328,40 @@ class Kuro(BaseTheme): ), widget.TextBox("#{}".format(x), name="default", **self.widget_defaults), ]) - screens.append(Screen(top=bar.Bar( + + 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): - for x in range(num_screens): + 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), - widget.Notify(), - widget.DebugInfo() + textfield, ]) screens[x].bottom = bar.Bar( widgets=widgets, size=Config.get('height_debugbar', 30) ) + self.debug_bars.append(screens[x].bottom) return screens def init_mouse(self): - log.debug("Initializing mouse") + self.log_debug("Initializing mouse") # Drag floating layouts. mouse = [ @@ -313,7 +375,7 @@ class Kuro(BaseTheme): return mouse def update_keys(self): - log.debug("Updating keys") + self.log_debug("Updating keys") for i, g in enumerate(self.groups): # mod1 + number = switch to group @@ -350,6 +412,32 @@ class Kuro(BaseTheme): ) ]) + # Util functions + @staticmethod + def redraw_bar(qtile): + for b in qtile.topbars: + b.draw() + + # QTile base callbacks def callback_startup(self): utils.execute("sleep 3") + + self.log_info("Restoring wallpaper...") utils.execute_once("nitrogen --restore") + # + # display = os.environ['DISPLAY'] + # + # if not display: + # display = ":0" + # + # # Start compton for each screen + # for x in range(self.num_screens): + # self.log_info("Launching compton for screen {}.{}".format(display, x)) + # utils.execute_once("compton --config ~/.config/compton.conf -b -d {}.{}".format(display, x)) + + # def callback_screen_change(self, *args, **kwargs): + # self.num_screens = utils.get_screen_count() + # return True + + def callback_focus_change(self, *args, **kwargs): + kb_handle_focus_change(self) diff --git a/kuro/utils/__init__.py b/kuro/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kuro/utils.py b/kuro/utils/general.py similarity index 84% rename from kuro/utils.py rename to kuro/utils/general.py index adfd838..ab2ff17 100644 --- a/kuro/utils.py +++ b/kuro/utils/general.py @@ -5,6 +5,7 @@ import subprocess import cairocffi import notify2 from libqtile import widget, bar +from libqtile.bar import Bar from libqtile.utils import catch_exception_and_warn, UnixCommandNotFound from libqtile.widget import base from libqtile.widget.battery import default_icon_path @@ -15,6 +16,7 @@ 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 @@ -77,6 +79,29 @@ def notify(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None): return notification.show() +def spawn_popup(qtile, x, y, text): + # Create textwidget for in window + pass + + + # window.Internal.create( + # qtile, x, y, width, height, opacity=1 + # ) + + +def display_wm_class(qtile): + window = qtile.currentWindow if qtile else None + + if window: + wm_class = window.window.get_wm_class() or None + name = window.name + + if wm_class: + notify(title="WM_Class of {}".format(name), + content="{}".format(wm_class), + urgency=notify2.URGENCY_CRITICAL) + + def bluetooth_audio_sink(): try: output = subprocess.check_output("pamixer --list-sinks".split()).decode("utf-8") @@ -101,11 +126,61 @@ 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): + super(KuroTopBar, self)._configure(qtile, screen) + self.window.handle_EnterNotify = self.handle_enter_notify + self.window.handle_LeaveNotify = self.handle_leave_notify + + 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() + + 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'") + def handle_hover(self, event): + spawn_popup(self.qtile, self.offsetx, self.offsety, "Hovered over AppLauncherIcon!") + class CheckUpdatesYaourt(CheckUpdates): def __init__(self, **config): @@ -113,12 +188,13 @@ class CheckUpdatesYaourt(CheckUpdates): # Override command and output with yaourt command self.cmd = "yaourt -Qua".split() self.status_cmd = "yaourt -Qua".split() - self.update_cmd = "yaourt -Sy" + self.update_cmd = "sudo yaourt -Sya".split() self.subtr = 0 def _check_updates(self): - subprocess.check_output(self.update_cmd) - super(CheckUpdatesYaourt, self)._check_updates() + #subprocess.check_output(self.update_cmd) + res = super(CheckUpdatesYaourt, self)._check_updates() + return res def button_press(self, x, y, button): if button == BUTTON_LEFT: diff --git a/kuro/utils/kb_backlight.py b/kuro/utils/kb_backlight.py new file mode 100644 index 0000000..d028aa9 --- /dev/null +++ b/kuro/utils/kb_backlight.py @@ -0,0 +1,356 @@ +import subprocess + +# Initialize logging +from libqtile.log_utils import logger + + +class State: + ON = "on" + OFF = "off" + LIST = ["on", "off"] + + +class Mode: + RANDOM = "random" + CUSTOM = "custom" + BREATHE = "breathe" + CYCLE = "cycle" + WAVE = "wave" + DANCE = "dance" + TEMPO = "tempo" + FLASH = "flash" + LIST = ["random", "custom", "breathe", "cycle", "wave", "dance", "tempo", "flash"] + + +class Brightness: + LOW = 0 + MEDIUM = 1 + HIGH = 2 + FULL = 3 + LIST = [0, 1, 2, 3] + + +class Side: + LEFT = "left" + MIDDLE = "middle" + RIGHT = "right" + ALL = "all" + LIST = ["left", "middle", "right", "all"] + + +class Color: + BLACK = "black" + BLUE = "blue" + RED = "red" + MAGENTA = "magenta" + GREEN = "green" + CYAN = "cyan" + YELLOW = "yellow" + WHITE = "white" + LIST = ["black", "blue", "red", "magenta", "green", "cyan", "yellow", "white"] + + +def handle_focus_change(theme): + qtile = theme.qtile + window = qtile.currentWindow if qtile else None + + if window: + wm_class = window.window.get_wm_class() or None + name = window.name + + if wm_class: + theme.log_info(str(wm_class)) + + # Check which window we entered and do some special effects if it is a special window. + + # Make keyboard red/white (pink) while in Osu! + if "osu!.exe" in wm_class[0]: + BacklightController.reset_backlight(state=KeyboardState(values={ + 'brightness': Brightness.FULL, + 'left': Color.WHITE, + 'middle': Color.RED, + 'right': Color.WHITE, + })) + elif "chromium" in wm_class[0]: + BacklightController.reset_backlight(state=KeyboardState(values={ + 'brightness': Brightness.FULL, + 'left': Color.WHITE, + 'middle': Color.BLUE, + 'right': Color.WHITE, + })) + elif "pycharm" in wm_class[1]: + BacklightController.reset_backlight(state=KeyboardState(values={ + 'brightness': Brightness.MEDIUM, + 'left': Color.WHITE, + 'middle': Color.GREEN, + 'right': Color.WHITE, + })) + elif "franz" in wm_class[0]: + BacklightController.reset_backlight(state=KeyboardState(values={ + 'brightness': Brightness.MEDIUM, + 'left': Color.BLUE, + 'middle': Color.WHITE, + 'right': Color.BLUE, + })) + else: + BacklightController.reset_backlight() + + +class KeyboardState: + _instance = None + + state = State.ON + mode = Mode.CUSTOM + brightness = Brightness.LOW + left = Color.WHITE + middle = Color.WHITE + right = Color.WHITE + + def __init__(self, values=None): + """ + :param values: Default values + :type values: dict + """ + if values is not None: + keys = values.keys() + if 'state' in keys: + self.state = values['state'] + if 'mode' in keys: + self.mode = values['mode'] + if 'brightness' in keys: + self.brightness = values['brightness'] + if 'left' in keys: + self.left = values['left'] + if 'middle' in keys: + self.middle = values['middle'] + if 'right' in keys: + self.right = values['right'] + + def __str__(self): + return "KBState({}, {}, {}, {}, {}, {})".format( + self.state, self.mode, self.brightness, self.left, self.middle, self.right + ) + + def get_copy(self): + c = KeyboardState() + c.state = self.state + c.mode = self.mode + c.brightness = self.brightness + c.left = self.left + c.middle = self.middle + c.right = self.right + return c + + @classmethod + def get_instance(cls): + """ + :rtype: KeyboardState + """ + if cls._instance is None: + cls._instance = KeyboardState() + return cls._instance + + +class BacklightController: + + @staticmethod + def reset_backlight(force=False, state=None): + """ + Resets the keyboard backlight to the default colors / states + :param force: Force the reset + :type force: bool + :param state: A state to reset to + :type state: KeyboardState + """ + if state is None: + # Create state with default values. + state = KeyboardState() + + logger.debug("Resetting KB backlight to {}".format(state)) + + flags = [BacklightController.set_colors([state.left, state.middle, state.right], force), + BacklightController.set_brightness(state.brightness, force), + BacklightController.set_state(state.state, force), + BacklightController.set_mode(state.mode, force)] + + BacklightController.exec_flags(flags) + + @staticmethod + def exec_flags(flags): + """ + Removes duplicate flags and executes the command with the resulting flags, and + updates the current keyboard state. + :param flags: List of list of flags, to be executed. + :return: The return code of the execution + """ + final_flags = {} + changes = {} + for flag in flags: + for (k, v) in flag: + final_flags[k] = v + if k == "-p": + changes['state'] = v + elif k == "-t": + changes['mode'] = v + elif k == "-b": + changes['brightness'] = v + elif k == "-l": + changes['left'] = v + elif k == "-m": + changes['middle'] = v + elif k == "-r": + changes['right'] = v + elif k == "-c": + changes['left'] = v + changes['middle'] = v + changes['right'] = v + + args = [] + for (k, v) in final_flags.items(): + args.append(k) + args.append(v) + + res = BacklightController._call(args) + if res == 0: + # Update state + css = KeyboardState.get_instance() + for (k, v) in changes.items(): + css.__setattr__(k, v) + + @staticmethod + def set_state(state, force=False): + """ + Turns the backlight on or off + :param state: State you want ('on' or 'off') + :type state: str + :param force: Force execution. + :type force: bool + """ + if state not in State.LIST: + return + + logger.debug("Setting KB state to {}".format(state)) + + css = KeyboardState.get_instance() + + if css.state != state or force: + return [('-p', state)] + + return [] + + @staticmethod + def set_mode(mode, force=False): + """ + Set the backlight mode + :param mode: One of "random", "custom", "breathe", "cycle", "wave", "dance", "tempo" or "flash" + :type mode: str + :param force: Force execution. + :type force: bool + """ + if mode not in Mode.LIST: + return + + logger.debug("Setting KB mode to {}".format(mode)) + + css = KeyboardState.get_instance() + if css.mode != mode or force: + return [('-t', mode)] + + return [] + + @staticmethod + def set_brightness(level, force=False): + """ + Set the brightness level + :param level: Brightness (0 to 3) + :type level: int + :param force: Force execution. + :type force: bool + """ + if level not in Brightness.LIST: + return + + logger.debug("Setting KB brightness to {}".format(level)) + + css = KeyboardState.get_instance() + if css.brightness != level or force: + return [('-b', '{}'.format(level))] + + return [] + + @staticmethod + def set_color(side, color, force=False): + """ + Set the backlight color + :param side: Side of backlight to change, from left, middle, right or all. + :type side: str + :param color: The new color, one of "black", "blue", "red", "magenta", "green", "cyan", "yellow" or "white" + :type color: str + :param force: Force execution. + :type force: bool + """ + if side not in Side.LIST: + return + + if color not in Color.LIST: + return + + logger.debug("Setting KB side {} to color {}".format(side, color)) + + css = KeyboardState.get_instance() + + if side == "all": + if css.left != color or css.right != color or css.right != color or force: + return [('-c', color)] + elif side == "left": + if css.left != color or force: + return [('-l', color)] + elif side == "right": + if css.right != color or force: + return [('-r', color)] + elif side == "middle": + if css.middle != color or force: + return [('-m', color)] + + return [] + + @staticmethod + def set_colors(colors, force=False): + """ + Set the backlight colors in one go + :param colors: The new colors, list of three colors, [left, middle, right]. Colors must be one of + "black", "blue", "red", "magenta", "green", "cyan", "yellow" or "white" + :type colors: list + :param force: Force execution. + :type force: bool + """ + if len(colors) != 3: + return + + for color in colors: + if color not in Color.LIST: + return + + logger.debug("Setting KB colors to {}, {}, {}".format(colors[0], colors[1], colors[2])) + + css = KeyboardState.get_instance() + + if css.left != colors[0] or css.middle != colors[1] or css.right != colors[2] or force: + return [('-l', '{}'.format(colors[0])), + ('-m', '{}'.format(colors[1])), + ('-r', '{}'.format(colors[2]))] + + return [] + + @staticmethod + def _call(args): + """ + Call the script. + :param args: Arguments to the script + :type args: list + :return The exit code of the script + :rtype: int + """ + logger.debug("Calling kb_backlight' with args {}".format(args)) + return subprocess.call(["sudo", "/home/kevin/bin/kb_backlight"] + args) +