248 lines
8.5 KiB
Plaintext
248 lines
8.5 KiB
Plaintext
|
#!/usr/bin/env python3
|
||
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
## Copyright (C) 2020-2023 Aditya Shakya <adi1090x@gmail.com>
|
||
|
|
||
|
"""A small utility to make xrandr adjustments from an OpenBox menu.
|
||
|
|
||
|
To install, put this file somewhere and make sure it is executable.
|
||
|
|
||
|
Edit your $HOME/.config/openbox/menu-<style>.xml file. Add something like the following
|
||
|
near the top::
|
||
|
|
||
|
<menu id="XrandrPipeMenu" label="Display/Monitor" execute="/path/to/ac-randr" />
|
||
|
|
||
|
Then add this in the place you actually want the menu to appear::
|
||
|
|
||
|
<menu id="randr-menu" />
|
||
|
|
||
|
You can easily add custom commands to the menu by creating the file
|
||
|
$HOME/.ob-randrrc. The syntax looks like this::
|
||
|
|
||
|
[Notebook]
|
||
|
|
||
|
portrait: --output LVDS --primary --mode 1366x768 --output VGA-0 --mode 1440x900 --left-of LVDS --rotate left
|
||
|
|
||
|
[Netbook]
|
||
|
|
||
|
zoom out: --output LVDS --scale 1.3x1.3
|
||
|
zoom in: --output LVDS --panning 1280x1024
|
||
|
|
||
|
The idea is that you can create machine-specific shortcuts. For example, with
|
||
|
my laptop at home I frequently connect to an external widescreen display turned
|
||
|
sideways. On my netbook, I frequently 'zoom out' to a higher resolution in
|
||
|
scaled-out mode or 'zoom in' to a higher resolution in panning mode.
|
||
|
|
||
|
|
||
|
TODO:
|
||
|
|
||
|
* Invoking position commands on a monitor that is turned off has no effect
|
||
|
* What other common tasks should be represented?
|
||
|
|
||
|
"""
|
||
|
AUTHOR = 'Seth House <seth@eseth.com>, Petr Penzin <penzin.dev@gmail.com>'
|
||
|
VERSION = '0.2'
|
||
|
|
||
|
## Modified for Archcraft by Aditya Shakya <adi1090x@gmail.com>
|
||
|
|
||
|
import configparser
|
||
|
import os
|
||
|
import subprocess
|
||
|
import sys
|
||
|
|
||
|
try:
|
||
|
from xml.etree import cElementTree as etree
|
||
|
except ImportError:
|
||
|
from xml.etree import ElementTree as etree
|
||
|
|
||
|
HOME = os.path.expanduser('~')
|
||
|
RCFILE = '.ob-randrrc'
|
||
|
|
||
|
def mk_exe_node(output, name, command):
|
||
|
"""A small helper to speed the three-element PITA that is the OpenBox
|
||
|
execute menu syntax.
|
||
|
|
||
|
"""
|
||
|
CMD = 'xrandr --output %s ' % output
|
||
|
CMD_1 = 'nitrogen --restore'
|
||
|
CMD_2 = '~/.config/openbox/themes/launch-bar.sh'
|
||
|
|
||
|
# Apply Mode
|
||
|
item = etree.Element('item', label=name)
|
||
|
action = etree.SubElement(item, 'action', name='execute')
|
||
|
etree.SubElement(action, 'command').text = CMD + command
|
||
|
|
||
|
# Fix Wallpaper : Restore
|
||
|
action = etree.SubElement(item, 'action', name='execute')
|
||
|
etree.SubElement(action, 'command').text = CMD_1
|
||
|
|
||
|
# Fix Polybar : Relaunch
|
||
|
action = etree.SubElement(item, 'action', name='execute')
|
||
|
etree.SubElement(action, 'command').text = CMD_2
|
||
|
|
||
|
return item
|
||
|
|
||
|
def get_rc_menu():
|
||
|
"""Read the user's rc file and return XML for menu entries."""
|
||
|
config = configparser.ConfigParser()
|
||
|
config.read(os.path.join(HOME, RCFILE))
|
||
|
|
||
|
menus = []
|
||
|
|
||
|
for i in config.sections():
|
||
|
menu = etree.Element('menu', id='shortcut-%s' % i, label=i)
|
||
|
|
||
|
for name in config.options(i):
|
||
|
command = config.get(i, name)
|
||
|
|
||
|
item = etree.SubElement(menu, 'item', label=name)
|
||
|
action = etree.SubElement(item, 'action', name='execute')
|
||
|
etree.SubElement(action, 'command').text = 'xrandr ' + command
|
||
|
|
||
|
menus.append(menu)
|
||
|
|
||
|
return menus
|
||
|
|
||
|
def mk_position_controls(output, name, action, outputs):
|
||
|
"""A helper function to generate a menu containing set of positional commands (left of, right of, above, below, etc).
|
||
|
"""
|
||
|
menu = etree.Element('menu', id=output+action,
|
||
|
type=action, label=name)
|
||
|
empty = True
|
||
|
|
||
|
# Add --auto to turn the screen on if it is off
|
||
|
if outputs[output]:
|
||
|
extra_action = ''
|
||
|
else:
|
||
|
extra_action = '--auto'
|
||
|
|
||
|
for other in outputs.keys():
|
||
|
# Don't position against itself
|
||
|
if output == other:
|
||
|
continue
|
||
|
# Don't position against an output that is off
|
||
|
if not outputs[other]:
|
||
|
continue
|
||
|
menu.append(mk_exe_node(output, other, ' '.join([extra_action, action, other])))
|
||
|
empty = False
|
||
|
|
||
|
if empty:
|
||
|
etree.SubElement(menu, 'separator', label="<none>")
|
||
|
|
||
|
return menu
|
||
|
|
||
|
def get_xml():
|
||
|
"""Run xrandr -q and parse the output for the bits we're interested in,
|
||
|
then build an XML tree suitable for passing to OpenBox.
|
||
|
"""
|
||
|
xrandr = subprocess.Popen(['xrandr', '-q'], stdout=subprocess.PIPE)
|
||
|
|
||
|
# Decode the byte data to strings when reading lines
|
||
|
xrandr_lines = [line.decode('utf-8') for line in xrandr.stdout.readlines()]
|
||
|
|
||
|
root = etree.Element('openbox_pipe_menu')
|
||
|
|
||
|
# Dictionary of connected outputs, key - output name, value - is it on
|
||
|
outputs = {}
|
||
|
|
||
|
actions = (
|
||
|
('Right', '--rotate right'),
|
||
|
('Left', '--rotate left'),
|
||
|
('Inverted', '--rotate inverted'),
|
||
|
('Normal', '--rotate normal'),
|
||
|
(),
|
||
|
('Auto', '--auto'),
|
||
|
('Off', '--off'),
|
||
|
('Reset', ' '.join([
|
||
|
'--auto', '--rotate normal', '--scale 1x1', '--panning 0x0'])))
|
||
|
|
||
|
# The following string processing is far more verbose than necessary but if
|
||
|
# the xrandr output ever changes (or I simply got it wrong to begin with)
|
||
|
# this should make it easier to fix.
|
||
|
for i in xrandr_lines:
|
||
|
if ' current' in i:
|
||
|
# Screen 0: minimum 320 x 200, current 1700 x 1440, maximum 2048 x 2048
|
||
|
text = [j for j in i.split(',') if ' current' in j][0]
|
||
|
text = text.replace(' current ', '')
|
||
|
|
||
|
etree.SubElement(root, 'separator', label="Current: %s" % text)
|
||
|
|
||
|
elif ' connected' in i:
|
||
|
# VGA connected 900x1440+0+0 left (normal left inverted right x axis y axis) 408mm x 255mm
|
||
|
text = i.replace(' connected', '')
|
||
|
text = text.partition('(')[0]
|
||
|
text = text.strip()
|
||
|
|
||
|
try:
|
||
|
output, mode, extra = (lambda x: (x[0], x[1], x[2:]))(text.split(' '))
|
||
|
outputs[output] = True
|
||
|
except IndexError:
|
||
|
# LVDS connected (normal left inverted right x axis y axis)
|
||
|
# Display is connected but off. Is this the best place to check that?
|
||
|
output, mode, extra = text, 'off', ''
|
||
|
outputs[output] = False
|
||
|
|
||
|
node = etree.SubElement(root, 'menu', id=output, type='output',
|
||
|
label=' '.join([output, mode, ' '.join(extra)]))
|
||
|
modes = etree.SubElement(node, 'menu', id='%s-modes' % output,
|
||
|
type='modes', label='Modes')
|
||
|
etree.SubElement(node, 'separator')
|
||
|
|
||
|
# Add a position menu, but fill in later
|
||
|
position = etree.SubElement(node, 'menu', id='%s-position' % output,
|
||
|
type='position', label='Position')
|
||
|
|
||
|
etree.SubElement(node, 'separator')
|
||
|
|
||
|
# Grab all the available modes (I'm ignoring refresh rates for now)
|
||
|
for j in xrandr_lines[xrandr_lines.index(i) + 1:]:
|
||
|
if not j.startswith(' '):
|
||
|
break
|
||
|
|
||
|
# 1440x900 59.9*+ 59.9*
|
||
|
text = j.strip()
|
||
|
text = text.split(' ')[0]
|
||
|
|
||
|
modes.append(mk_exe_node(output, text, '--mode %s' % text))
|
||
|
|
||
|
for action in actions:
|
||
|
if not action:
|
||
|
etree.SubElement(node, 'separator')
|
||
|
else:
|
||
|
node.append(mk_exe_node(output, *action))
|
||
|
|
||
|
elif ' disconnected' in i:
|
||
|
# TV disconnected (normal left inverted right x axis y axis)
|
||
|
text = i.replace(' disconnected', '')
|
||
|
text = text.partition('(')[0]
|
||
|
name, extra = (lambda x: (x[0], x[1:]))(text.split(' '))
|
||
|
|
||
|
etree.SubElement(root, 'item', label=name)
|
||
|
|
||
|
# Grab the user's rc menu shortcuts
|
||
|
etree.SubElement(root, 'separator', label='Shortcuts')
|
||
|
|
||
|
auto = etree.SubElement(root, 'item', label='Automatic')
|
||
|
auto_action = etree.SubElement(auto, 'action', name='execute')
|
||
|
etree.SubElement(auto_action, 'command').text = 'xrandr --auto'
|
||
|
|
||
|
# Populate position menus
|
||
|
for output in outputs.keys():
|
||
|
# Find position entry
|
||
|
position = root.find(".//menu[@id=\"%s-position\"]" % output)
|
||
|
# Add position options
|
||
|
position.append(mk_position_controls(output, 'Left of', '--left-of', outputs))
|
||
|
position.append(mk_position_controls(output, 'Right of', '--right-of', outputs))
|
||
|
position.append(mk_position_controls(output, 'Above', '--above', outputs))
|
||
|
position.append(mk_position_controls(output, 'Below', '--below', outputs))
|
||
|
position.append(mk_position_controls(output, 'Same as', '--same-as', outputs))
|
||
|
|
||
|
for i in get_rc_menu():
|
||
|
root.append(i)
|
||
|
|
||
|
return root
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
ob_menu = get_xml()
|
||
|
sys.stdout.write(etree.tostring(ob_menu).decode('utf-8') + '\n')
|