diff --git a/bumblebee_status/modules/contrib/arandr.py b/bumblebee_status/modules/contrib/arandr.py new file mode 100644 index 0000000..80ba3b5 --- /dev/null +++ b/bumblebee_status/modules/contrib/arandr.py @@ -0,0 +1,177 @@ +# pylint: disable=C0111,R0903 + +"""Enables handy interaction with arandr for display management. Left-clicking +will execute arandr for interactive display management. Right-clicking will +bring up a context- and state-sensitive menu that will allow you to switch to a +saved screen layout as well as toggle on/off individual connected displays. + +Parameters: + * No configuration parameters + +Requires the following executable: + * arandr + * xrandr +""" + +import fnmatch +from functools import partial +import logging +import os +import re + +import core.module +import core.widget +import core.input +import core.decorators +from util import popup +from util.cli import execute + + +log = logging.getLogger(__name__) + +__screenlayout_dir__ = os.path.expanduser("~/.screenlayout") + + +class Module(core.module.Module): + @core.decorators.never + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget('')) + + self.manager = self.parameter("manager", "arandr") + self.toggle_cmd = "xrandr" + core.input.register( + self, + button=core.input.LEFT_MOUSE, + cmd=self.popup, + ) + + core.input.register(self, button=core.input.RIGHT_MOUSE, + cmd=self.popup) + + @staticmethod + def activate_layout(layout_path): + log.debug("activating layout") + log.debug(layout_path) + execute(layout_path) + + def popup(self, widget): + """Create Popup that allows the user to control their displays in one + of three ways: launch arandr, select a pre-set screenlayout, toggle a + display. + """ + menu = popup.menu() + menu.add_menuitem( + "arandr", + callback=partial(execute, self.manager) + ) + menu.add_separator() + + displays = Module._get_displays() + log.debug(displays) + layouts = Module._get_layouts() + available_layouts = Module._prune_layouts(layouts, displays) + log.debug("Available layouts:") + log.debug(available_layouts) + + if len(available_layouts) > 0: + for layout in available_layouts: + sh = os.path.join(__screenlayout_dir__, layout) + sh_name = os.path.splitext(layout)[0] + menu.add_menuitem(sh_name, + callback=partial(self.activate_layout, sh)) + + menu.add_separator() + count_on = 0 + for display, state in displays.items(): + if state[1]: + count_on += 1 + for display, state in displays.items(): + if not state[0]: + continue + on_off = "On" if state[1] else "Off" + menu_line = "{}: {}".format(display, on_off) + menu.add_menuitem(menu_line, + callback=partial(self.toggle_display, display, + state[1], count_on)) + + menu.show(widget, 0, 0) + + def toggle_display(self, display, current_state, count_on): + """Toggle a display on or off based on its current state.""" + if current_state: + log.debug("toggling off {}".format(display)) + if count_on == 1: + log.info("attempted to turn off last display") + return + execute("{} --output {} --off".format(self.toggle_cmd, display)) + else: + log.debug("toggling on {}".format(display)) + execute( + "{} --output {} --auto".format(self.toggle_cmd, display) + ) + + @staticmethod + def _get_displays(): + """Queries xrandr and builds a dict of the displays and their state. + + The dict entries are key by the display and are bools (True if + connected). + """ + displays = {} + for line in execute("xrandr -q").split("\n"): + if "connected" not in line: + continue + is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line)) + parts = line.split(" ", 2) + display = parts[0] + displays[display] = ( + (True, is_on) if parts[1] == "connected" else (False, is_on) + ) + + return displays + + @staticmethod + def _get_layouts(): + """Loads and parses the arandr screen layout scripts.""" + layouts = {} + for filename in os.listdir(__screenlayout_dir__): + if fnmatch.fnmatch(filename, '*.sh'): + fullpath = os.path.join(__screenlayout_dir__, filename) + with open(fullpath, "r") as file: + for line in file: + s_line = line.strip() + if "xrandr" not in s_line: + continue + displays_in_file = Module._parse_layout(line) + layouts[filename] = displays_in_file + return layouts + + @staticmethod + def _parse_layout(line): + """Parses a single xrandr line to find what displays are active in the + command. Returns them as a list. + """ + active_displays = [] + to_check = line[7:].split("--output ") + for check in to_check: + if not check or "off" in check: + continue + active_displays.append(check.split(" ")[0]) + return active_displays + + @staticmethod + def _prune_layouts(layouts, displays): + """Return a list of layouts whose displays are actually connected.""" + available = [] + for layout, needs in layouts.items(): + still_valid = True + for need in needs: + if need not in displays or not displays[need][0]: + still_valid = False + break + if still_valid: + available.append(layout) + return available + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/util/popup.py b/bumblebee_status/util/popup.py index dee67f6..cf69d9a 100644 --- a/bumblebee_status/util/popup.py +++ b/bumblebee_status/util/popup.py @@ -71,6 +71,11 @@ class menu(object): label=menuitem, command=functools.partial(self.__on_click, callback) ) + """Adds a separator to the menu in the current location""" + + def add_separator(self): + self._menu.add_separator() + """Shows this menu :param event: i3wm event that triggered the menu (dict that contains "x" and "y" fields) diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index 01aff79..1913752 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -359,5 +359,8 @@ }, "hddtemp": { "prefix": "hddtemp" + }, + "arandr": { + "prefix": " displays " } } diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 6504ce2..e660c1d 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -271,5 +271,8 @@ "hddtemp": { "prefix": "" }, "octoprint": { "prefix": " " + }, + "arandr": { + "prefix": "" } } diff --git a/themes/icons/ionicons.json b/themes/icons/ionicons.json index cf7813a..4b90133 100644 --- a/themes/icons/ionicons.json +++ b/themes/icons/ionicons.json @@ -193,6 +193,8 @@ "off": { "prefix": "\uf24f" }, "paused": { "prefix": "\uf210" }, "on": { "prefix": "\uf488" } + }, + "arandr": { + "prefix": "\uf465" } - }