ob-randr.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env python2
  2. # The script base is taken from http://openbox.org/wiki/Openbox:Pipemenus
  3. # -*- coding: utf-8 -*-
  4. """A small utility to make xrandr adjustments from an OpenBox menu.
  5. To install, put this file somewhere and make sure it is executable.
  6. Edit your $HOME/.config/openbox/menu.xml file. Add something like the following
  7. near the top::
  8. <menu id="randr-menu" label="randr" execute="/path/to/ob-randr.py" />
  9. Then add this in the place you actually want the menu to appear::
  10. <menu id="randr-menu" />
  11. You can easily add custom commands to the menu by creating the file
  12. $HOME/.ob-randrrc. The syntax looks like this::
  13. [Notebook]
  14. portrait: --output LVDS --primary --mode 1366x768 --output VGA-0 --mode 1440x900 --left-of LVDS --rotate left
  15. [Netbook]
  16. zoom out: --output LVDS --scale 1.3x1.3
  17. zoom in: --output LVDS --panning 1280x1024
  18. The idea is that you can create machine-specific shortcuts. For example, with
  19. my laptop at home I frequently connect to an external widescreen display turned
  20. sideways. On my netbook, I frequently 'zoom out' to a higher resolution in
  21. scaled-out mode or 'zoom in' to a higher resolution in panning mode.
  22. TODO:
  23. * Invoking position commands on a monitor that is turned off has no effect
  24. * What other common tasks should be represented?
  25. """
  26. AUTHOR = 'Seth House <seth@eseth.com>, Petr Penzin <penzin.dev@gmail.com>'
  27. VERSION = '0.2'
  28. import ConfigParser
  29. import os
  30. import subprocess
  31. import sys
  32. try:
  33. from xml.etree import cElementTree as etree
  34. except ImportError:
  35. from xml.etree import ElementTree as etree
  36. HOME = os.path.expanduser('~')
  37. RCFILE = '.ob-randrrc'
  38. def mk_exe_node(output, name, command):
  39. """A small helper to speed the three-element PITA that is the OpenBox
  40. execute menu syntax.
  41. """
  42. CMD = 'xrandr --output %s ' % output
  43. item = etree.Element('item', label=name)
  44. action = etree.SubElement(item, 'action', name='execute')
  45. etree.SubElement(action, 'command').text = CMD + command
  46. return item
  47. def get_rc_menu():
  48. """Read the user's rc file and return XML for menu entries."""
  49. config = ConfigParser.ConfigParser()
  50. config.read(os.path.join(HOME, RCFILE))
  51. menus = []
  52. for i in config.sections():
  53. menu = etree.Element('menu', id='shortcut-%s' % i, label=i)
  54. for name in config.options(i):
  55. command = config.get(i, name)
  56. item = etree.SubElement(menu, 'item', label=name)
  57. action = etree.SubElement(item, 'action', name='execute')
  58. etree.SubElement(action, 'command').text = 'xrandr ' + command
  59. menus.append(menu)
  60. return menus
  61. def mk_position_controls(output, name, action, outputs):
  62. """A helper function to generate a menu containing set of positional commands (left of, right of, above, below, etc).
  63. """
  64. menu = etree.Element('menu', id=output+action,
  65. type=action, label=name)
  66. empty = True
  67. # Add --auto to turn the screen on if it is off
  68. if outputs[output]:
  69. extra_action = ''
  70. else:
  71. extra_action = '--auto'
  72. for other in outputs.keys():
  73. # Don't position against itself
  74. if output == other:
  75. continue
  76. # Don't position against an output that is off
  77. if not outputs[other]:
  78. continue
  79. menu.append(mk_exe_node(output, other, ' '.join([extra_action, action, other])))
  80. empty = False
  81. if empty:
  82. etree.SubElement(menu, 'separator', label="<none>")
  83. return menu
  84. def get_xml():
  85. """Run xrandr -q and parse the output for the bits we're interested in,
  86. then build an XML tree suitable for passing to OpenBox.
  87. """
  88. xrandr = subprocess.Popen(['xrandr', '-q'], stdout=subprocess.PIPE)
  89. xrandr_lines = xrandr.stdout.readlines()
  90. root = etree.Element('openbox_pipe_menu')
  91. # Dictionary of connected outputs, key - output name, value - is it on
  92. outputs = {}
  93. actions = (
  94. ('right', '--rotate right'),
  95. ('left', '--rotate left'),
  96. ('inverted', '--rotate inverted'),
  97. ('normal', '--rotate normal'),
  98. (),
  99. ('auto', '--auto'),
  100. ('off', '--off'),
  101. ('reset', ' '.join([
  102. '--auto', '--rotate normal', '--scale 1x1', '--panning 0x0'])))
  103. # The following string processing is far more verbose than necessary but if
  104. # the xrandr output ever changes (or I simply got it wrong to begin with)
  105. # this should make it easier to fix.
  106. for i in xrandr_lines:
  107. if ' current' in i:
  108. # Screen 0: minimum 320 x 200, current 1700 x 1440, maximum 2048 x 2048
  109. text = [j for j in i.split(',') if ' current' in j][0]
  110. text = text.replace(' current ', '')
  111. etree.SubElement(root, 'separator', label="Current: %s" % text)
  112. elif ' connected' in i:
  113. # VGA connected 900x1440+0+0 left (normal left inverted right x axis y axis) 408mm x 255mm
  114. text = i.replace(' connected', '')
  115. text = text.partition('(')[0]
  116. text = text.strip()
  117. try:
  118. output, mode, extra = (lambda x: (x[0], x[1], x[2:]))(text.split(' '))
  119. outputs[output] = True
  120. except IndexError:
  121. # LVDS connected (normal left inverted right x axis y axis)
  122. # Display is connected but off. Is this the best place to check that?
  123. output, mode, extra = text, 'off', ''
  124. outputs[output] = False
  125. node = etree.SubElement(root, 'menu', id=output, type='output',
  126. label=' '.join([output, mode, ' '.join(extra)]))
  127. modes = etree.SubElement(node, 'menu', id='%s-modes' % output,
  128. type='modes', label='modes')
  129. etree.SubElement(node, 'separator')
  130. # Add a position menu, but fill in later
  131. position = etree.SubElement(node, 'menu', id='%s-position' % output,
  132. type='position', label='position')
  133. etree.SubElement(node, 'separator')
  134. # Grab all the available modes (I'm ignoring refresh rates for now)
  135. for j in xrandr_lines[xrandr_lines.index(i) + 1:]:
  136. if not j.startswith(' '):
  137. break
  138. # 1440x900 59.9*+ 59.9*
  139. text = j.strip()
  140. text = text.split(' ')[0]
  141. modes.append(mk_exe_node(output, text, '--mode %s --dpi 96' % text))
  142. for action in actions:
  143. if not action:
  144. etree.SubElement(node, 'separator')
  145. else:
  146. node.append(mk_exe_node(output, *action))
  147. elif ' disconnected' in i:
  148. # TV disconnected (normal left inverted right x axis y axis)
  149. text = i.replace(' disconnected', '')
  150. text = text.partition('(')[0]
  151. name, extra = (lambda x: (x[0], x[1:]))(text.split(' '))
  152. etree.SubElement(root, 'item', label=name)
  153. # Grab the user's rc menu shortcuts
  154. etree.SubElement(root, 'separator', label='Shortcuts')
  155. auto = etree.SubElement(root, 'item', label='auto')
  156. auto_action = etree.SubElement(auto, 'action', name='execute')
  157. etree.SubElement(auto_action, 'command').text = 'xrandr --auto'
  158. # Populate position menus
  159. for output in outputs.keys():
  160. # Find position entry
  161. position = root.find(".//menu[@id=\"%s-position\"]" % output)
  162. # Add position options
  163. position.append(mk_position_controls(output, 'left of', '--left-of', outputs))
  164. position.append(mk_position_controls(output, 'right of', '--right-of', outputs))
  165. position.append(mk_position_controls(output, 'above', '--above', outputs))
  166. position.append(mk_position_controls(output, 'below', '--below', outputs))
  167. position.append(mk_position_controls(output, 'same as', '--same-as', outputs))
  168. for i in get_rc_menu():
  169. root.append(i)
  170. return root
  171. if __name__ == '__main__':
  172. ob_menu = get_xml()
  173. sys.stdout.write(etree.tostring(ob_menu) + '\n')