Merge pull request #632 from zerorust/arandr
Adds a new contributed module `arandr` to complement `xrandr` with fast layout access
This commit is contained in:
commit
1158c57f85
5 changed files with 191 additions and 1 deletions
177
bumblebee_status/modules/contrib/arandr.py
Normal file
177
bumblebee_status/modules/contrib/arandr.py
Normal file
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -359,5 +359,8 @@
|
|||
},
|
||||
"hddtemp": {
|
||||
"prefix": "hddtemp"
|
||||
},
|
||||
"arandr": {
|
||||
"prefix": " displays "
|
||||
}
|
||||
}
|
||||
|
|
|
@ -271,5 +271,8 @@
|
|||
"hddtemp": { "prefix": "" },
|
||||
"octoprint": {
|
||||
"prefix": " "
|
||||
},
|
||||
"arandr": {
|
||||
"prefix": ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -193,6 +193,8 @@
|
|||
"off": { "prefix": "\uf24f" },
|
||||
"paused": { "prefix": "\uf210" },
|
||||
"on": { "prefix": "\uf488" }
|
||||
},
|
||||
"arandr": {
|
||||
"prefix": "\uf465"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue