1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// monitor switches between the monitor layouts of this laptop.
//
// It replaces a pipeline that used to be spread over node (to parse xrandr),
// dhall (to type-check the parser's own JSON) and python (for the layout
// arithmetic), glued together with execline.
//
// Usage:
//
//	monitor home         # external monitor primary, laptop panel to its left
//	monitor laptop-only  # just the built-in panel
//	monitor parse        # the parsed xrandr output as JSON, for debugging
//
// `--dry-run` prints the xrandr invocation instead of running it.
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"strings"
)

func usage() {
	fmt.Fprint(os.Stderr, `usage: monitor [--dry-run] <command>

commands:
  home         external monitor primary, laptop panel to its left, bottom-aligned
  laptop-only  only the built-in laptop panel, at its native resolution
  parse        print the parsed xrandr output as JSON

options:
  --dry-run    print the xrandr command instead of running it
`)
}

func main() {
	dryRun := flag.Bool("dry-run", false, "print the xrandr command instead of running it")
	flag.Usage = usage
	flag.Parse()

	if flag.NArg() != 1 {
		usage()
		os.Exit(1)
	}

	if err := run(flag.Arg(0), *dryRun); err != nil {
		fmt.Fprintf(os.Stderr, "monitor: %s\n", err)
		os.Exit(1)
	}
}

func run(command string, dryRun bool) error {
	outputs, err := queryXrandr()
	if err != nil {
		return err
	}

	var args []string
	switch command {
	case "parse":
		return printJSON(outputs)
	case "home":
		args, err = TwoMonitorArgs(outputs)
	case "laptop-only":
		args, err = LaptopOnlyArgs(outputs)
	default:
		usage()
		return fmt.Errorf("unknown command %q", command)
	}
	if err != nil {
		return err
	}

	if dryRun {
		fmt.Printf("xrandr %s\n", strings.Join(args, " "))
		return nil
	}

	return applyXrandr(args)
}

// queryXrandr runs `xrandr` and parses its output.
func queryXrandr() ([]Output, error) {
	out, err := exec.Command("xrandr").Output()
	if err != nil {
		return nil, fmt.Errorf("running xrandr: %w", err)
	}
	return Parse(string(out)), nil
}

// applyXrandr runs `xrandr` with the computed arguments, passing its output
// through. A non-zero exit status is propagated to our own, because the callers
// of this tool -- the xbindkeys bindings -- chain further commands with `&&`.
func applyXrandr(args []string) error {
	cmd := exec.Command("xrandr", args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("xrandr %s: %w", strings.Join(args, " "), err)
	}
	return nil
}

// printJSON writes the outputs as a JSON object keyed by connector name.
//
// The shape matches what the node parser used to emit, so that the two could be
// compared directly while this tool was replacing it.
func printJSON(outputs []Output) error {
	byName := make(map[string]Output, len(outputs))
	for _, o := range outputs {
		byName[o.Name] = o
	}

	encoded, err := json.MarshalIndent(byName, "", "  ")
	if err != nil {
		return fmt.Errorf("encoding JSON: %w", err)
	}
	fmt.Println(string(encoded))
	return nil
}