Merge pull request #684 from nayaverdier/main

[xrandr] unit tests, exclude parameter, and final display safeguard
This commit is contained in:
tobi-wan-kenobi 2020-07-26 11:25:07 +02:00 committed by GitHub
commit 3fd8c5aaa0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 201 additions and 65 deletions

View file

@ -12,6 +12,7 @@ Parameters:
* vault.location: Location of the password store (defaults to ~/.password-store) * vault.location: Location of the password store (defaults to ~/.password-store)
* vault.offx: x-axis offset of popup menu (defaults to 0) * vault.offx: x-axis offset of popup menu (defaults to 0)
* vault.offy: y-axis offset of popup menu (defaults to 0) * vault.offy: y-axis offset of popup menu (defaults to 0)
* vault.text: Text to display on the widget (defaults to <click-for-password>)
Many thanks to `bbernhard <https://github.com/bbernhard>`_ for the idea! Many thanks to `bbernhard <https://github.com/bbernhard>`_ for the idea!
""" """

View file

@ -8,6 +8,7 @@ Parameters:
and appending a file '~/.config/i3/config.<screen name>' for every screen. and appending a file '~/.config/i3/config.<screen name>' for every screen.
* xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the * xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the
module will only refresh when displays are enabled or disabled (defaults to true) module will only refresh when displays are enabled or disabled (defaults to true)
* xrandr.exclude: Comma-separated list of display name prefixes to exclude
Requires the following python module: Requires the following python module:
* (optional) i3 - if present, the need for updating the widget list is auto-detected * (optional) i3 - if present, the need for updating the widget list is auto-detected
@ -16,7 +17,6 @@ Requires the following executable:
* xrandr * xrandr
""" """
import os
import re import re
import sys import sys
@ -31,7 +31,7 @@ import util.format
try: try:
import i3 import i3
except: except Exception:
pass pass
@ -40,40 +40,50 @@ class Module(core.module.Module):
def __init__(self, config, theme): def __init__(self, config, theme):
super().__init__(config, theme, []) super().__init__(config, theme, [])
self._exclude = tuple(filter(len, self.parameter("exclude", "").split(",")))
self._active_displays = []
self._autoupdate = util.format.asbool(self.parameter("autoupdate", True)) self._autoupdate = util.format.asbool(self.parameter("autoupdate", True))
self._needs_update = True self._needs_update = True
try: try:
i3.Subscription(self._output_update, "output") i3.Subscription(self._output_update, "output")
except: except Exception:
pass pass
def _output_update(self, event, data, _): def _output_update(self, event, data, _):
self._needs_update = True self._needs_update = True
def update(self): def update(self):
self.clear_widgets() if not self._autoupdate and not self._needs_update:
if self._autoupdate == False and self._needs_update == False:
return return
self.clear_widgets()
self._active_displays.clear()
self._needs_update = False self._needs_update = False
for line in util.cli.execute("xrandr -q").split("\n"): for line in util.cli.execute("xrandr -q").split("\n"):
if not " connected" in line: if " connected" not in line:
continue continue
display = line.split(" ", 2)[0] display = line.split(" ", 2)[0]
m = re.search(r"\d+x\d+\+(\d+)\+\d+", line) resolution = re.search(r"\d+x\d+\+(\d+)\+\d+", line)
if resolution:
self._active_displays.append(display)
if display.startswith(self._exclude):
continue
widget = self.widget(display) widget = self.widget(display)
if not widget: if not widget:
widget = self.add_widget(full_text=display, name=display) widget = self.add_widget(full_text=display, name=display)
core.input.register(widget, button=1, cmd=self._toggle) core.input.register(widget, button=1, cmd=self._toggle)
core.input.register(widget, button=3, cmd=self._toggle) core.input.register(widget, button=3, cmd=self._toggle)
widget.set("state", "on" if m else "off") widget.set("state", "on" if resolution else "off")
widget.set("pos", int(m.group(1)) if m else sys.maxsize) widget.set("pos", int(resolution.group(1)) if resolution else sys.maxsize)
if self._autoupdate == False: if not self._autoupdate:
widget = self.add_widget(full_text="") widget = self.add_widget(full_text="")
widget.set("state", "refresh") widget.set("state", "refresh")
core.input.register(widget, button=1, cmd=self._refresh) core.input.register(widget, button=1, cmd=self._refresh)
@ -85,9 +95,7 @@ class Module(core.module.Module):
self._needs_update = True self._needs_update = True
def _toggle(self, event): def _toggle(self, event):
self._refresh(event) if util.format.asbool(self.parameter("overwrite_i3config", False)):
if util.format.asbool(self.parameter("overwrite_i3config", False)) == True:
toggle_cmd = utility("toggle-display.sh") toggle_cmd = utility("toggle-display.sh")
else: else:
toggle_cmd = "xrandr" toggle_cmd = "xrandr"
@ -95,42 +103,23 @@ class Module(core.module.Module):
widget = self.widget(widget_id=event["instance"]) widget = self.widget(widget_id=event["instance"])
if widget.get("state") == "on": if widget.get("state") == "on":
if len(self._active_displays) > 1:
util.cli.execute("{} --output {} --off".format(toggle_cmd, widget.name)) util.cli.execute("{} --output {} --off".format(toggle_cmd, widget.name))
elif not self._active_displays:
util.cli.execute("{} --output {} --auto".format(toggle_cmd, widget.name))
else: else:
first_neighbor = next( if event["button"] == core.input.LEFT_MOUSE:
(widget for widget in self.widgets() if widget.get("state") == "on"), side, neighbor = "left", self._active_displays[0]
None,
)
last_neighbor = next(
(
widget
for widget in reversed(self.widgets())
if widget.get("state") == "on"
),
None,
)
neighbor = (
first_neighbor
if event["button"] == core.input.LEFT_MOUSE
else last_neighbor
)
if neighbor is None:
util.cli.execute(
"{} --output {} --auto".format(toggle_cmd, widget.name)
)
else: else:
side, neighbor = "right", self._active_displays[-1]
util.cli.execute( util.cli.execute(
"{} --output {} --auto --{}-of {}".format( "{} --output {} --auto --{}-of {}".format(
toggle_cmd, toggle_cmd, widget.name, side, neighbor,
widget.name,
"left"
if event.get("button") == core.input.LEFT_MOUSE
else "right",
neighbor.name,
) )
) )
self._refresh(event)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -16,23 +16,19 @@ class menu(object):
def __init__(self, parent=None, leave=True): def __init__(self, parent=None, leave=True):
self.running = True self.running = True
self.parent = None
if not parent: self.parent = parent
self._root = tk.Tk()
self._root = parent.root() if parent else tk.Tk()
self._root.withdraw() self._root.withdraw()
self._menu = tk.Menu(self._root, tearoff=0) self._menu = tk.Menu(self._root, tearoff=0)
self._menu.bind("<FocusOut>", self.__on_focus_out) self._menu.bind("<FocusOut>", self.__on_focus_out)
self.add_menuitem("close", self.__on_focus_out)
self.add_separator()
else:
self._root = parent.root()
self._root.withdraw()
self._menu = tk.Menu(self._root, tearoff=0)
self._menu.bind("<FocusOut>", self.__on_focus_out)
self.parent = parent
if leave: if leave:
self._menu.bind("<Leave>", self.__on_focus_out) self._menu.bind("<Leave>", self.__on_focus_out)
elif not parent:
self.add_menuitem("close", self.__on_focus_out)
self.add_separator()
self._menu.bind("<ButtonRelease-1>", self.release) self._menu.bind("<ButtonRelease-1>", self.release)

View file

@ -1,7 +1,157 @@
import pytest import sys
pytest.importorskip("i3") from core.input import trigger, LEFT_MOUSE, RIGHT_MOUSE
from core.config import Config
from modules.core.xrandr import Module
def test_load_module():
__import__("modules.core.xrandr")
MOCK_EXECUTE = "modules.core.xrandr.util.cli.execute"
def mock_xrandr(mocker, xrandr_output):
return mocker.patch(MOCK_EXECUTE, return_value=xrandr_output)
def assert_widgets(module, *expected_widgets):
assert len(module.widgets()) == len(expected_widgets)
for widget, (name, state, pos) in zip(module.widgets(), expected_widgets):
assert widget.name == name
assert widget.get("state") == state
assert widget.get("pos") == pos
def assert_trigger(mocker, module, widget_index, button, expected_command):
xrandr_cli = mock_xrandr(mocker, "")
widget = module.widgets()[widget_index]
trigger({"button": button, "instance": widget.id, "name": module.id})
if expected_command is None:
xrandr_cli.assert_not_called()
else:
xrandr_cli.assert_called_once_with(expected_command)
def test_autoupdate(mocker):
xrandr_cli = mock_xrandr(mocker, FULL_OUTPUT_TWO_DISPLAYS)
module = Module(Config([]), theme=None)
module.update()
xrandr_cli.assert_called_once_with("xrandr -q")
assert_widgets(module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "on", 1920))
assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output eDP-1-1 --off")
assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output eDP-1-1 --off")
assert_trigger(mocker, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off")
assert_trigger(mocker, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off")
def test_display_off(mocker):
xrandr_cli = mock_xrandr(mocker, TRUNCATED_OUTPUT_DISPLAY_OFF)
module = Module(Config([]), theme=None)
module.update()
xrandr_cli.assert_called_once_with("xrandr -q")
assert_widgets(module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "off", sys.maxsize))
assert_trigger(mocker, module, 0, LEFT_MOUSE, None)
assert_trigger(mocker, module, 0, RIGHT_MOUSE, None)
assert_trigger(mocker, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --auto --left-of eDP-1-1")
assert_trigger(mocker, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --auto --right-of eDP-1-1")
def test_no_autoupdate(mocker):
xrandr_cli = mock_xrandr(mocker, FULL_OUTPUT_TWO_DISPLAYS)
module = Module(Config(["-p", "xrandr.autoupdate=false"]), theme=None)
module.update()
xrandr_cli.assert_called_once_with("xrandr -q")
assert_widgets(
module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "on", 1920), (None, "refresh", None)
)
assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output eDP-1-1 --off")
assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output eDP-1-1 --off")
assert_trigger(mocker, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off")
assert_trigger(mocker, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off")
def test_exclude(mocker):
xrandr_cli = mock_xrandr(mocker, FULL_OUTPUT_TWO_DISPLAYS)
module = Module(Config(["-p", "xrandr.exclude=eDP"]), theme=None)
module.update()
xrandr_cli.assert_called_once_with("xrandr -q")
assert_widgets(module, ("HDMI-1-1", "on", 1920))
assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off")
assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off")
def test_exclude_off(mocker):
xrandr_cli = mock_xrandr(mocker, TRUNCATED_OUTPUT_DISPLAY_OFF)
module = Module(Config(["-p", "xrandr.exclude=eDP"]), theme=None)
module.update()
xrandr_cli.assert_called_once_with("xrandr -q")
assert_widgets(module, ("HDMI-1-1", "off", sys.maxsize))
assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output HDMI-1-1 --auto --left-of eDP-1-1")
assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --auto --right-of eDP-1-1")
# xrandr sample data
FULL_OUTPUT_TWO_DISPLAYS = """Screen 0: minimum 8 x 8, current 4480 x 1440, maximum 32767 x 32767
eDP-1-1 connected primary 1920x1080+0+0 344mm x 193mm
1920x1080 60.00*+ 59.93 48.00
1680x1050 59.95 59.88
1600x1024 60.17
1400x1050 59.98
1280x1024 60.02
1440x900 59.89
1280x960 60.00
1360x768 59.80 59.96
1152x864 60.00
1024x768 60.04 60.00
960x720 60.00
928x696 60.05
896x672 60.01
960x600 60.00
960x540 59.99
800x600 60.00 60.32 56.25
840x525 60.01 59.88
800x512 60.17
700x525 59.98
640x512 60.02
720x450 59.89
640x480 60.00 59.94
680x384 59.80 59.96
576x432 60.06
512x384 60.00
400x300 60.32 56.34
320x240 60.05
HDMI-1-1 connected 2560x1440+1920+0 596mm x 335mm
2560x1440 59.95*+
1920x1080 60.00 50.00 59.94
1920x1080i 60.00 50.00 59.94
1680x1050 59.88
1600x900 60.00
1280x1024 60.02
1280x800 59.91
1280x720 60.00 50.00 59.94
1024x768 60.00
800x600 60.32
720x576 50.00
720x576i 50.00
720x480 60.00 59.94
720x480i 60.00 59.94
640x480 60.00 59.94
"""
TRUNCATED_OUTPUT_DISPLAY_OFF = """eDP-1-1 connected primary 1920x1080+0+0 344mm x 193mm
1920x1080 60.00*+ 59.93 48.00
HDMI-1-1 connected
2560x1440 59.95 +
"""