xrandrコマンドの出力結果をもとに、よしなにモニターのミラーリングをする

春に買い替えた個人ノートPCで、Arch Linuxを使ってます。
（ゆっくりと慣らしているので、まだ使いこなせてるとは言い難い

外で何かしらの発表する機会がちょこちょこ増えてきたり、モニター買い替えて積極的に使うようになってきたのですが、そうなると面倒なのがモニターの設定。

自分は`xrandr`コマンドを使っているのですが、いろいろと引数を考えるのが面倒になってきたので、`xrandr`コマンドの出力を加工して、都度いい感じにする`xonsh`コマンドにしてみました。

# TL;DR

* `xrandr`の出力を元に、いい感じに出力設定を変えるコマンドを作ってみた
* `xonsh`だとオブジェクトやPython制御構造を使えて良さげ

# コードに関する前提

* `xonsh`上で使うことを前提としたコードのため、途中からのコードがPythonのような何かになっていることがあります
* 一旦、今使っているコードをなるべくそのまま載せます。そのため、オーバースペック感があります

# `xrandr`のコマンド

## 情報表示だけするケース

何のオプションもなく使うと、次のものがわかります。

* 使えるディスプレイ(出力先)
* 出力先の接続有無
* 接続している出力先で使える画面モード

何も接続していないと、こんな感じになります。

```bash
Screen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192
eDP-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 309mm x 174mm
   2560x1440     60.00*+  59.99    59.99    59.96    59.95
   1920x1440     60.00
   1856x1392     60.01
   1792x1344     60.01
   2048x1152     59.99    59.98    59.90    59.91
   1920x1200     59.88    59.95
   1920x1080     60.01    59.97    59.96    59.93
   1600x1200     60.00
   1680x1050     59.95    59.88
   1400x1050     59.98
   1600x900      59.99    59.94    59.95    59.82
   # モード数がそこそこ多くて長くなったので、略します
   320x240       60.05
   360x202       59.51    59.13
   320x180       59.84    59.32
DP-1 disconnected (normal left inverted right x axis y axis)
HDMI-1 disconnected (normal left inverted right x axis y axis)
DP-2 disconnected (normal left inverted right x axis y axis)
HDMI-2 disconnected (normal left inverted right x axis y axis)
```

※ `eDP-1`がPC本体のディスプレイです


HDMI端子に接続すると、こうなります。
`HDMI-1`の状態が`connected`になり出力が可能な状態になります。[^1]

```
Screen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192
eDP-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 309mm x 174mm
   2560x1440     60.00*+  59.99    59.99    59.96    59.95
   1920x1440     60.00
   1856x1392     60.01
   1792x1344     60.01
   2048x1152     59.99    59.98    59.90    59.91
   1920x1200     59.88    59.95
   1920x1080     60.01    59.97    59.96    59.93
   1600x1200     60.00
   1680x1050     59.95    59.88
   1400x1050     59.98
   1600x900      59.99    59.94    59.95    59.82
   # モード数がそこそこ多くて長くなったので、略します
   320x240       60.05
   360x202       59.51    59.13
   320x180       59.84    59.32
DP-1 disconnected (normal left inverted right x axis y axis)
HDMI-1 connected (normal left inverted right x axis y axis)
   1920x1080     60.00 +  60.00    50.00    50.00    59.94    30.00    25.00    24.00    29.97    23.98
   1920x1080i    60.00    60.00    50.00    50.00    59.94
   1366x768      59.79
   1280x720      60.00    50.00    59.94
   1024x768      60.00
   720x576       50.00
   720x576i      50.00
   720x480       60.00    59.94
   720x480i      60.00    59.94
   640x480       60.00    59.94
DP-2 disconnected (normal left inverted right x axis y axis)
HDMI-2 disconnected (normal left inverted right x axis y axis)
```

## 出力を変えるケース

雑に書くと、`--output [出力先]` を起点にオプションを組み合わせることで、出力先設定を切り替えることができます。

```bash:example
xrandr --auto --output eDP-1 --right-of HDMI-1
```

この場合、

* `--auto`: 画面モードは自動決定
* `--output eDP-1`: メインとして`eDP-1`に出力して
* `--right-of HDMI-1`: メインの左にHDMIモニターのディスプレイを出力 [^2]

と言ったように、「使用する出力先」「出力先ごとの画面モード」「各出力先の配置」を決められます。


# これを使って、どうしたいのか

この情報を元に、こんなケースに対応したくなりました。

* 外付けの出力先が出力されていたときに、自動で見つけて接続したい
* 稀にミラーリングしたいときがあるので、画面モードを揃えて出力したくもなる

## その1: `xrandr` の出力を加工する

まずは、出力内容を分析して、扱いやすくしましょう。

`xrandr`の出力は、分解するとこんな感じになります。（自PCでの挙動）

* Screen~で始まる行
* [出力先名]で始まる行 x 出力先の数だけ
    * 指定できる画面モード x モード数

大まかにはこの3種類しかなく、規則もシンプルかつ文法がわかりやすくなってます。
そのため、こんな理屈で全出力先x画面モードを構造化できます。


```python3:
import re
from subprocess import Popen, PIPE

re_output = re.compile(r'^(?P<name>.+) (?P<status>connected|disconnected).*')
re_mode = re.compile(r'^   (?P<mode>[0-9]+x[0-9]+).*')


def find_outputs():
    # xrandrの出力取ってくる
    proc = Popen('xrandr', stdout=PIPE)
    out, err = proc.communicate()
    displays = DisplayList()
    for line in out.decode().split('\n'):
        # 出力先情報か判定して、該当したらリスト登録
        re_ = re_output.search(line)
        if re_:
            d = Display(re_.group('name'), re_.group('status'))
            displays.append(d)
        # 画面モード情報を判定して、該当したら出力先リストの最後の情報に画面モードを情報を登録
        re_ = re_mode.search(line)
        if re_:
            d = displays.last()
            d.mode_list.append(Size.from_string(re_.group('mode')))
    return displays
```

* `Display`は、出力先名と画面モードのリストを持つだけのクラスです
* `DisplayList`は、`Display`をまとめる`list`よりちょっとだけ便利にしてみたクラスです。


```python3:xsh
def mirror():
    """Slide mode
    """
    # とりあえず、一式設定を取ってくる
    displays = find_outputs()
    # eDP-1がメイン
    main_display = displays.get('eDP-1')
    try:
        # 接続されている出力先でeDP-1以外をサブとする
        sub_display = [d for d in displays.connected() if d.name != 'eDP-1'][0]
        # メインとサブの最大公約数的な最も広い画面モードを探す
        mode = min([main_display.max_size, sub_display.max_size])
        # xrandrコマンドを呼んでミラーリング
        xrandr --output @(main_display.name) --same-as @(sub_display.name) --mode @(mode)
    except IndexError:
        # サブがないならその旨書いて終了
        print('Currently solo display')

aliases['mirror'] = mirror
```

これを、`xonsh`の`rc`あたりで読ませるようにすると、コマンド一発で

> **出力先を探して、複数の出力先があれば1個を選んでミラーリングする**

が実現可能となります。

# おまけ

ミラーリング状態からモニターを外しても、画面モードが元に戻らないため、
こんな感じのコマンドを用意して、ディスプレイが外れたときの最適化にも使ってます。[^3]

```python3
def solo():
    displays = xrandr.find_outputs()
    d = displays.get('eDP-1')
    xrandr --output eDP-1 --mode @(d.max_size)
```


[^1]: この時点では接続のみで、出力はされていません
[^2]: 表現的には「eDP-1 is right side of HDMI-1」みたいな文法になるっぽいです
[^3]: 実は画面モードは`--auto`でもいいし出力先は固定値なので、ここまでしなくても良いです