200426.5 · 481b64a667e8d9aca522a0455a00f32dac1f4977
initial commit of ~/bin
This brings over everything that was in my bin-only repository.
I removed scripts that were unused or were no longer understood. I also moved any work-specific scripts to a separate (private) repository.
reading declarations
Changes to bin/x11-gnome-do. All files
@@ -0,0 +1,423 @@+#!/usr/bin/env python3++# This script serves as duct tape to make the multiple monitor experience+# on GNOME 3 just a little bit better. Currently, the only functionality of+# this script is to switch focus from one monitor to the next, while respecting+# the stacking order of windows inside each individual monitor. However, the+# script is designed to make it easy to add more functionality later. Namely,+# when the script is run, it reads the current monitor, desktop and window+# configuration into a convenient in-memory data structure.+#+# This program is made simply by relying on some external commands:+#+# wmctrl+# xprop+# gohead (if not available, then xrandr is used)+#+# gohead can be installed from: https://github.com/BurntSushi/gohead+# The only advantage of gohead is that it can be used to give "nice" names+# to your monitors. For example, on my laptop, I have this:+#+# $ cat ~/.config/gohead/config.ini+# [Monitors]+# laptop = DP-0+# left = DP-2+# right = HDMI-0+#+# You can then run `x11-gnome-do focus-monitor left`. If your monitor+# configuration ever changes, then you just need to update the gohead+# mapping.++import argparse+import os+import re+from collections import OrderedDict, namedtuple+import subprocess+++class Monitor(namedtuple('Monitor', [+ 'index',+ 'nice_name',+ 'output_name',+ 'x',+ 'y',+ 'width',+ 'height',+ 'primary',+])):+ @staticmethod+ def parse_gohead(gohead_line):+ parser = re.compile(r'''(?xm)+ ^+ (?P<index>[0-9]+)+ \s++ (?P<nice>\S+)+ \s++ (?P<output>\S+)+ \s++ (?P<x>[0-9]+)+ \s++ (?P<y>[0-9]+)+ \s++ (?P<width>[0-9]+)+ \s++ (?P<height>[0-9]+)+ \s*+ (?P<primary>primary)?+ \s*+ $+ ''')+ m = parser.match(gohead_line)+ if m is None:+ raise Exception(f'invalid gohead monitor line: "{gohead_line}"')+ return Monitor(+ index=int(m.group('index')),+ nice_name=m.group('nice'),+ output_name=m.group('output'),+ x=int(m.group('x')),+ y=int(m.group('y')),+ width=int(m.group('width')),+ height=int(m.group('height')),+ primary=m.group('primary') == 'primary',+ )++ @staticmethod+ def parse_xrandr(index, xrandr_line):+ '''+ Parses a monitor configuration from a line outputted by xrandr.+ The line must start with "OUTPUT-NAME connected".+ '''+ parser = re.compile(r'''(?xm)+ ^+ (?P<output>\S+)+ \s++ connected+ \s++ (?P<primary>primary)?\s*+ (?P<width>[0-9]+)x(?P<height>[0-9]+)+ \+(?P<x>[0-9]+)\+(?P<y>[0-9]+)+ \s++ ''')+ m = parser.match(xrandr_line)+ if m is None:+ raise Exception(f'invalid xrandr monitor line: "{xrandr_line}"')+ return Monitor(+ index=index,+ nice_name=m.group('output'),+ output_name=m.group('output'),+ x=int(m.group('x')),+ y=int(m.group('y')),+ width=int(m.group('width')),+ height=int(m.group('height')),+ primary=m.group('primary') == 'primary',+ )++ def contains(self, win):+ # For now, we just check which monitor contains the top left corner of+ # the window, even if the window has more overlap with a different+ # monitor.+ return (+ win.x >= self.x+ and win.y >= self.y+ and win.x < (self.x + self.width)+ and win.y < (self.y + self.height)+ )++ def is_name_match(self, name):+ name = name.lower()+ return name in [self.nice_name.lower(), self.output_name.lower()]+++class Desktop(namedtuple('Desktop', [+ 'index',+ 'name',+ 'current',+ 'width',+ 'height',+ 'work_x',+ 'work_y',+ 'work_width',+ 'work_height',+ 'windows',+])):+ @staticmethod+ def parse(wmctrl_line):+ parser = re.compile(r'''(?xm)+ ^+ (?P<index>[0-9]+)+ \s++ (?P<current>-|\*)+ \s++ DG:\s+(?P<width>[0-9]+)x(?P<height>[0-9]+)+ \s++ VP:\s+\S++ \s++ WA:\s+(?P<wx>[0-9]+),(?P<wy>[0-9]+)+ \s+(?P<wwidth>[0-9]+)x(?P<wheight>[0-9]+)+ \s*+ (?P<name>.*)+ \s*+ $+ ''')+ m = parser.match(wmctrl_line)+ if m is None:+ raise Exception(f'invalid wmctrl desktop line: "{wmctrl_line}"')+ return Desktop(+ index=int(m.group('index')),+ name=m.group('name') or None,+ current=m.group('current') == '*',+ width=int(m.group('width')),+ height=int(m.group('height')),+ work_x=int(m.group('wx')),+ work_y=int(m.group('wy')),+ work_width=int(m.group('wwidth')),+ work_height=int(m.group('wheight')),+ windows=[],+ )+++class Window(namedtuple('Window', [+ 'id',+ 'name',+ 'monitor',+ 'desktop_index',+ 'desktop',+ 'x',+ 'y',+ 'width',+ 'height',+ 'client_name',+])):+ @staticmethod+ def parse(wmctrl_line):+ parser = re.compile(r'''(?xm)+ ^+ (?P<id>0x[0-9a-fA-F]+)+ \s++ (?P<desk>[0-9]+)+ \s++ (?P<x>[0-9]+)+ \s++ (?P<y>[0-9]+)+ \s++ (?P<width>[0-9]+)+ \s++ (?P<height>[0-9]+)+ \s++ (?P<client>\S+)+ \s*+ (?P<name>.*)+ \s*+ $+ ''')+ m = parser.match(wmctrl_line)+ if m is None:+ raise Exception(f'invalid wmctrl window line: "{wmctrl_line}"')+ return Window(+ id=int(m.group('id'), base=16),+ name=m.group('name'),+ monitor=None,+ desktop_index=int(m.group('desk')),+ desktop=None,+ x=int(m.group('x')),+ y=int(m.group('y')),+ width=int(m.group('width')),+ height=int(m.group('height')),+ client_name=m.group('client'),+ )++ def is_sticky(self):+ return self.desktop_index == -1+++def read_monitors():+ '''+ Read the existing monitor configuration. This only includes+ monitors that are both connected and active.++ This attempts to use gohead first, which permits associating "nice"+ names with monitors. If gohead isn't installed, then xrandr is+ used.++ The dictionary returned is keyed by the monitor's "nice" name. (Or+ just output name if gohead isn't available.)+ '''+ gohead = os.path.join(os.getenv('HOME'), '.gox11', 'bin', 'gohead')+ if not os.path.exists(gohead):+ return read_monitors_xrandr()++ out = subprocess.run(+ [gohead, 'tabs'],+ encoding='utf-8',+ stdout=subprocess.PIPE,+ stderr=subprocess.PIPE,+ )++ monitors = OrderedDict()+ for line in out.stdout.splitlines():+ mon = Monitor.parse_gohead(line)+ monitors[mon.nice_name] = mon+ return monitors+++def read_monitors_xrandr():+ '''+ Read monitor configuration directly from xrandr.++ This is useful for folks that don't have BurntSushi/gohead+ installed.+ '''+ out = subprocess.run(+ ['xrandr', '-q'],+ encoding='utf-8',+ stdout=subprocess.PIPE,+ stderr=subprocess.PIPE,+ )++ monitors = OrderedDict()+ index = 0+ for line in out.stdout.splitlines():+ # Make sure we only try to parse monitors that are not only+ # connected, but have an active resolution assigned to them.+ if not re.match(r'\S+\s+connected(\s+primary)?\s+[0-9]+', line):+ continue+ mon = Monitor.parse_xrandr(index, line)+ monitors[mon.nice_name] = mon+ index += 1+ return monitors+++def read_desktops():+ '''+ Read in all available desktops.++ The map returned is keyed by desktop index.+ '''+ out = subprocess.run(+ ['wmctrl', '-d'],+ encoding='utf-8',+ stdout=subprocess.PIPE,+ stderr=subprocess.PIPE,+ )++ desktops = OrderedDict()+ for line in out.stdout.splitlines():+ desk = Desktop.parse(line)+ desktops[desk.index] = desk+ return desktops+++def read_windows(mons, desks):+ '''+ Read in all visible windows.++ mons and desks must be the current set of monitors and+ desktops. Each window returned is attached to the+ corresponding monitor and desktop.+ '''+ out = subprocess.run(+ ['wmctrl', '-l', '-G'],+ encoding='utf-8',+ stdout=subprocess.PIPE,+ stderr=subprocess.PIPE,+ )++ unordered = {}+ for line in out.stdout.splitlines():+ win = Window.parse(line)+ unordered[win.id] = win++ windows = OrderedDict()+ for wid in read_stacking_window_ids():+ win = unordered[wid]+ for mon in mons.values():+ if mon.contains(win):+ win = win._replace(monitor=mon)+ break+ if not win.is_sticky():+ win = win._replace(desktop=desks[win.desktop_index])+ win.desktop.windows.append(win)+ windows[win.id] = win+ return windows+++def read_stacking_window_ids():+ '''+ Read a list of window IDs corresponding to the global stacking+ order of all open windows. The list returned is in bottom-to-top+ order.+ '''+ out = subprocess.run(+ ['xprop', '-root', '_NET_CLIENT_LIST_STACKING'],+ encoding='utf-8',+ stdout=subprocess.PIPE,+ stderr=subprocess.PIPE,+ )++ for m in re.finditer('0x[0-9a-fA-F]+', out.stdout):+ yield int(m.group(0), base=16)+++def current_desktop(desks):+ '''+ Find and return the currently active desktop in the given desktops.+ '''+ for d in desks.values():+ if d.current:+ return d+ return None+++def find_monitor(mons, name):+ '''+ Searches for a monitor by matching any one of its names, case+ insensitively, to the name given.+ '''+ for m in mons.values():+ if m.is_name_match(name):+ return m+ return None+++def cmd_focus_monitor(mons, desks, wins, monitor_name):+ '''+ Find the most recently used window on the monitor with the given+ name and focus it. Also, move the mouse to the top-left corner of+ the monitor.+ '''+ mon = find_monitor(mons, monitor_name)+ curdesk = current_desktop(desks)+ for w in reversed(curdesk.windows):+ if w.monitor is not None and w.monitor.index == mon.index:+ subprocess.run(['wmctrl', '-i', '-a', str(w.id)])+ break++ # And also move the mouse to the top-left corner of the+ # corresponding monitor.+ subprocess.run(['xdotool', 'mousemove', str(mon.x), str(mon.y)])+++if __name__ == '__main__':+ p = argparse.ArgumentParser(+ description='miscellaneous X11 commands for use in window managers '+ 'with shitty multi-monitor support'+ )+ subps = p.add_subparsers(dest='cmd')++ subp = subps.add_parser(+ 'focus-monitor',+ description='Focus the most recently used window on the monitor '+ 'provided.',+ )+ subp.add_argument('monitor_name')++ args = p.parse_args()++ mons = read_monitors()+ desks = read_desktops()+ wins = read_windows(mons, desks)++ if args.cmd == 'focus-monitor':+ cmd_focus_monitor(mons, desks, wins, args.monitor_name)+ else:+ raise Exception(f'unrecognized command "{args.cmd}"')