From e8b3dfb4ef650e3c9f6dbbf465333de42af69b14 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 26 Nov 2016 13:57:33 +0100 Subject: [PATCH] [modules/xrandr] Add display on/off toggling This one is a bit tricky: * Clicking on an active xrandr output will disable it * Clicking on a disabled xrandr output will enable it -> if it is a left-click, it will put it as the left-most display if it is a right-click, as the right-most display Also, it will reload the i3 bars (using a script that allows you to write custom pieces of an i3 configuration that is applied conditionally depending on the screens you have). It goes like this: * Base config is in ~/.i3/config.template * Output-specific config is in ~/.i3/config. * Output-specific config when other screens are also active is in ~/.i3/config.- For instance: $ ls ~/.i3 config.template config.eDP1 -> will be applied to eDP1 (always) config.VGA1-eDP1 -> will be applied to VGA1, if eDP1 is also active config.VGA1 -> will be applied to VGA1 (if eDP1 is inactive) fixes #19 --- bin/load-i3-bars.sh | 26 +++++++++++++++++++ bin/toggle-display.sh | 12 +++++++++ bumblebee/modules/xrandr.py | 51 +++++++++++++++++++++++++++++++------ bumblebee/output.py | 23 ++++++++++++----- bumblebee/util.py | 11 ++++++++ 5 files changed, 109 insertions(+), 14 deletions(-) create mode 100755 bin/load-i3-bars.sh create mode 100755 bin/toggle-display.sh diff --git a/bin/load-i3-bars.sh b/bin/load-i3-bars.sh new file mode 100755 index 0000000..cbe0e95 --- /dev/null +++ b/bin/load-i3-bars.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +if [ ! -f ~/.i3/config.template ]; then + cp ~/.i3/config ~/.i3/config.template +else + cp ~/.i3/config.template ~/.i3/config +fi + +screens=$(xrandr -q|grep ' connected'| grep -P '\d+x\d+' |cut -d' ' -f1) + +echo "screens: $screens" + +while read -r line; do + screen=$(echo $line | cut -d' ' -f1) + others=$(echo $screens|tr ' ' '\n'|grep -v $screen|tr '\n' '-'|sed 's/.$//') + + if [ -f ~/.i3/config.$screen-$others ]; then + cat ~/.i3/config.$screen-$others >> ~/.i3/config + else + if [ -f ~/.i3/config.$screen ]; then + cat ~/.i3/config.$screen >> ~/.i3/config + fi + fi +done <<< "$screens" + +i3-msg restart diff --git a/bin/toggle-display.sh b/bin/toggle-display.sh new file mode 100755 index 0000000..bd13a29 --- /dev/null +++ b/bin/toggle-display.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +echo $(dirname $(readlink -f "$0")) + +i3bar_update=$(dirname $(readlink -f "$0"))/load-i3-bars.sh + +xrandr "$@" + +if [ -f $i3bar_update ]; then + sleep 1 + $i3bar_update +fi diff --git a/bumblebee/modules/xrandr.py b/bumblebee/modules/xrandr.py index d5e438b..51998a9 100644 --- a/bumblebee/modules/xrandr.py +++ b/bumblebee/modules/xrandr.py @@ -1,5 +1,7 @@ import bumblebee.module +import bumblebee.util import re +import os import sys import subprocess @@ -13,7 +15,30 @@ def parameters(): class Module(bumblebee.module.Module): def __init__(self, output, config, alias): super(Module, self).__init__(output, config, alias) - self._state = "off" + + self._widgets = [] + + def toggle(self, event, widget): + path = os.path.dirname(os.path.abspath(__file__)) + toggle_cmd = "{}/../../bin/toggle-display.sh".format(path) + + if widget.get("state") == "on": + bumblebee.util.execute("{} --output {} --off".format(toggle_cmd, widget.get("display"))) + else: + neighbor = None + for w in self._widgets: + if w.get("state") == "on": + neighbor = w + if event.get("button") == 1: + break + + if neighbor == None: + bumblebee.util.execute("{} --output {} --auto".format(toggle_cmd, + widget.get("display"))) + else: + bumblebee.util.execute("{} --output {} --auto --{}-of {}".format(toggle_cmd, + widget.get("display"), "left" if event.get("button") == 1 else "right", + neighbor.get("display"))) def widgets(self): process = subprocess.Popen([ "xrandr", "-q" ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -21,29 +46,39 @@ class Module(bumblebee.module.Module): widgets = [] - # TODO: sort by position for line in output.split("\n"): if not " connected" in line: continue - screen = line.split(" ", 2)[0] + display = line.split(" ", 2)[0] m = re.search(r'\d+x\d+\+(\d+)\+\d+', line) - widget = bumblebee.output.Widget(self, screen) + widget = bumblebee.output.Widget(self, display, instance=display) + widget.set("display", display) + + # not optimal (add callback once per interval), but since + # add_callback() just returns if the callback has already + # been registered, it should be "ok" + self._output.add_callback(module=display, button=1, + cmd=self.toggle) + self._output.add_callback(module=display, button=3, + cmd=self.toggle) if m: - self._state = "on" + widget.set("state", "on") widget.set("pos", int(m.group(1))) else: - self._state = "off" - widget.set("pos", sys.maxint()); + widget.set("state", "off") + widget.set("pos", sys.maxint) widgets.append(widget) widgets.sort(key=lambda widget : widget.get("pos")) + self._widgets = widgets + return widgets def state(self, widget): - return self._state + return widget.get("state", "off") def warning(self, widget): return False diff --git a/bumblebee/output.py b/bumblebee/output.py index 952a284..d24e0c6 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -15,6 +15,8 @@ class Widget(object): self._store = {} self._instance = instance + obj._output.register_widget(self.instance(), self) + def set(self, key, value): self._store[key] = value @@ -40,17 +42,22 @@ class Widget(object): return self._text class Command(object): - def __init__(self, command): + def __init__(self, command, event, widget): self._command = command + self._event = event + self._widget = widget def __call__(self, *args, **kwargs): if not isinstance(self._command, list): self._command = [ self._command ] for cmd in self._command: - c = cmd.format(*args, **kwargs) - DEVNULL = open(os.devnull, 'wb') - subprocess.Popen(shlex.split(c), stdout=DEVNULL, stderr=DEVNULL).communicate() + if inspect.ismethod(cmd): + cmd(self._event, self._widget) + else: + c = cmd.format(*args, **kwargs) + DEVNULL = open(os.devnull, 'wb') + subprocess.Popen(shlex.split(c), stdout=DEVNULL, stderr=DEVNULL).communicate() class Output(object): def __init__(self, config): @@ -58,6 +65,10 @@ class Output(object): self._callbacks = {} self._wait = threading.Condition() self._wait.acquire() + self._widgets = {} + + def register_widget(self, identity, widget): + self._widgets[identity] = widget def redraw(self): self._wait.acquire() @@ -84,9 +95,9 @@ class Output(object): event.get("button", -1), event.get("instance", event.get("name", None)), ), cb) - if inspect.isfunction(cb) or cb is None: return cb - return Command(cb) + identity = event.get("instance", event.get("name", None)) + return Command(cb, event, self._widgets.get(identity, None)) def wait(self): self._wait.wait(self._config.parameter("interval", 1)) diff --git a/bumblebee/util.py b/bumblebee/util.py index ef74266..15ff146 100644 --- a/bumblebee/util.py +++ b/bumblebee/util.py @@ -1,3 +1,6 @@ +import shlex +import exceptions +import subprocess def bytefmt(num): for unit in [ "", "Ki", "Mi", "Gi" ]: @@ -13,3 +16,11 @@ def durationfmt(duration): if hours > 0: res = "{:02d}:{}".format(hours, res) return res + +def execute(cmd): + args = shlex.split(cmd) + p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out = p.communicate() + + if p.returncode != 0: + raise exceptions.RuntimeError("{} exited with {}".format(cmd, p.returncode))