knoll 0.3.0

A command-line tool for configuring macOS displays
Documentation
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# knoll

<p>
<a href="https://crates.io/crates/knoll"><img src="https://img.shields.io/crates/v/knoll?style=flat-square" alt="Crates.io version" /></a>
<img src="https://github.com/gawashburn/knoll/actions/workflows/tests.yml/badge.svg" alt="Testing action" />
<a href='https://coveralls.io/github/gawashburn/knoll?branch=main'><img src='https://coveralls.io/repos/github/gawashburn/knoll/badge.svg?branch=main' alt='Coverage Status' /></a>
<img src="https://img.shields.io/github/license/gawashburn/knoll" alt="MIT License" />
</p>

A simple command-line tool for manipulating the configuration of macOS displays.

## Table of contents

- [Installation]#installation
    - [Cargo]#cargo
    - [launchd]#launchd
    - [Nix]#nix
- [Usage]#usage
    - [Pipeline mode]#pipeline-mode
    - [Listing mode]#listing-mode
    - [Daemon mode]#daemon-mode
- [Configuration reference]#configuration-reference
- [Future work]#future-work
- [Development]#development
- [What's in a name?]#whats-in-a-name

## Installation

Until someone creates packages for knoll, probably the most common way to
install it will be to use cargo or Nix.

### Cargo

If you already have a Rust environment set up, you can use the
`cargo install` command:

```bash
cargo install knoll
```

### launchd

The recommended solution for running knoll as a daemon is to make use of
[
`launchd`](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html).
Choose a service name unique to your host using
the [reverse domain name](https://en.wikipedia.org/wiki/Reverse_domain_name_notation)
convention and create a `.plist` file in `~/Library/LaunchAgents`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
        "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>EnvironmentVariables</key>
        <dict>
            <key>PATH</key>
            <string>...</string>
        </dict>
        <key>KeepAlive</key>
        <true/>
        <key>Label</key>
        <string>my.service.knoll</string>
        <key>ProgramArguments</key>
        <array>
            <string>/path/to/knoll</string>
            <string>daemon</string>
            <string>-vvv</string>
            <string>--input=/path/to/config-file</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
        <key>StandardErrorPath</key>
        <string>/tmp/knoll.err</string>
        <key>StandardOutPath</key>
        <string>/tmp/knoll.out</string>
    </dict>
</plist>
```

You can then enable and start service using

```bash
launchctl enable gui/$(id -u)/my.service.knoll`
launchctl start gui/$(id -u)/my.service.knoll`
````

### Nix

The knoll repository contains a [Nix Flake](https://nixos.wiki/wiki/Flakes)
that can be used to integrate knoll into your
[nix-darwin](https://github.com/LnL7/nix-darwin/) configuration. I currently
use the following `launchd` definition like:

```nix
  launchd.user.agent = {
    knoll = {
      path = [ "/run/current-system/sw/bin/" ];
      serviceConfig = {
        ProgramArguments = let
          configFile = pkgs.writeText "knoll-config.json"
            (builtins.toJSON [
              [
                # MacBook Pro display
                {
                  uuid = "8684ad81e3ea92cb14f43eb88b97a3f7";
                  enabled = true;
                  origin = [ (-1792) 453 ];
                  extents = [ 1792 1120 ];
                  scaled = true;
                  frequency = 59;
                  color_depth = 8;
                  rotation = 0;
                }
                ...
              ]
            ]);
        in
          [
            "/run/current-system/sw/bin/knoll" "daemon" "-vvv" "--format=json"
            "--input=${configFile}"
          ];
        KeepAlive = true;
        RunAtLoad = true;
        StandardErrorPath = "/tmp/knoll.err";
        StandardOutPath = "/tmp/knoll.out";
      };
    };
  };
```

## Usage

knoll has three primary usage modes: pipeline mode, listing mode, and
daemon mode.

### Pipeline mode

knoll's default mode supports reporting and updating the current display
configuration. In the simplest case, you can just run it with no argument:

```bash
host$ knoll
[
  [
    {
      "uuid": "b00184f4c1ee4cdf8ccfea3fca2f93b2",
      "enabled": true,
      "origin": [
        0,
        0
      ],
      "extents": [
        2560,
        1440
      ],
      "scaled": true,
      "frequency": 60,
      "color_depth": 8,
      "rotation": 0
    }
  ]
]
```

The output here is the current display configuration
in [JSON](https://www.json.org/)
format. It says that there is a single enabled display placed at (0,0) with a
scaled resolution of 2560x1440. The display is not rotated and has a refresh
frequency of 60Hz and a color depth of 8-bits.

knoll also supports
[Rusty Object Notation (RON)](https://github.com/ron-rs/ron).

```bash
host$ knoll --format=ron
[
    [
        (
            uuid: "b00184f4c1ee4cdf8ccfea3fca2f93b2",
            enabled: true,
            origin: (0, 0),
            extents: (2560, 1440),
            scaled: true,
            frequency: 60,
            color_depth: 8,
            rotation: 0,
        ),
    ],
]
```

There are two primary benefits of using RON over JSON. One is that it is a
slightly more compact. Second, and more importantly, it supports comments. This
way you can annotate your configurations if you like. JSON was chosen as the
default as it makes it easier to interface knoll with all the tooling available
as part of the JSON ecosystem.

You may have noticed that the display configuration is nested two levels deep.
knolls output consists of an outermost list of *configuration groups*. Each
configuration group in turn consists of a list of display configurations.

By default, knoll will read a list of configuration groups from standard
input and apply the most specific configuration group that is applicable.

As the output of knoll is a configuration group, piping
knoll to itself is an idempotent operation:

```bash
host$ knoll | knoll --quiet
# Should not change anything.
```

Note that because the operating system may accept some configuration changes
without failure, but modifying them to satisfy certain constraints, providing
knoll with a configuration is not an identity:

```bash
host$ cat my_config.json | knoll > out_config.json 
# my_config.json and out_config.json may differ.
```

The most common case where this might happen is that `my_config.json` omits
some fields we are not interested in adjusting. Another case where this
might happen would be if a configuration group has displays that overlap or
have gaps. We will call these *unstable* configurations.

As just mentioned, display configurations can omit any fields that you do not
want to alter. For example, if you just wanted to rotate your display to be
upside-down, you could write the following:

```bash
host$ cat my_config.ron
[
    [
        (
            uuid: "b00184f4c1ee4cdf8ccfea3fca2f93b2",
            rotation: 180,
        ),
    ],
]
host$ knoll --quiet --format=ron --input=my_config.ron
```

The resolution, location, etc. of the display will all remain unchanged.

The only required field is `uuid`. If just the `uuid` field
is provided the configuration is effectively a no-op.

Earlier I glossed over what it means for knoll to choose a "most specific"
configuration group. A valid configuration group consists of one or more
display configurations with unique UUIDs:

```bash
[   // This is an invalid configuration group because
    // there are duplicate UUIDs.
    (   // First configuration
        uuid: "b00184f4c1ee4cdf8ccfea3fca2f93b2",
    ),
    (   // Second configuration
        uuid: "b00184f4c1ee4cdf8ccfea3fca2f93b2",
    )
]
```

A valid list of configuration groups must contain only groups that do not have
the same set of UUIDs.

```bash
[   // This is an invalid list of configuration groups because 
    // there are two groups with the same set of UUIDs.
    [ // First group
        (
            uuid: "b00184f4c1ee4cdf8ccfea3fca2f93b2",
        ),
    ],
    [   // Second group
        (
            uuid: "b00184f4c1ee4cdf8ccfea3fca2f93b2",
        )
    ],
]
```

Given these restrictions on validity, when run, knoll will determine all the
UUID of all attached displays. It will then choose the configuration group
where its UUIDs are the largest subset of the attached displays. The intent is
here is two-fold:

* Attaching a new display to the computer will not cause an existing
  configuration to become invalid.
* It is possible to provide configurations with and without this new display.

If there is no applicable display group in the provided configuration,
knoll will exit with an error message and error code:

```bash
host$ cat bogus.ron
[
    [
        ( // Improbable display UUID.
          uuid: "11111111111111111111111111111111",
        ),
    ],
]
host$ knoll --quiet --format=ron --input=bogus.ron
No configuration group matches the currently attached displays: 
37d8832a2d6602cab9f78f30a301b230, 94226c6fcef04e9b8503ffa88fedba08,
f3def94a9fbd4de79a432d9d0bc7b4ce.
host$ echo $?
1
```

### Listing mode

knoll's second mode of operation allows inspecting the allowed display mode of
attached displays:

```bash
host$ knoll list
[
  {
    "uuid": "37d8832a2d6602cab9f78f30a301b230",
    "modes": [
      {
        "scaled": true,
        "color_depth": 8,
        "frequency": 59,
        "extents": [
          1280,
          800
        ]
      },

      {
        "scaled": true,
        "color_depth": 8,
        "frequency": 60,
        "extents": [
          1024,
          768
        ]
      }
    ]
  }
]
```

This is useful for determining which display configurations may successfully be
used in an input to knoll.

### Daemon mode

Finally, knoll also supports a "daemon" mode.

```bash
host$ knoll daemon --input=my_config.json
```

When in this mode, knoll wait until a display configuration event occurs. At
that time, if provided an input file, it will (re)load the configuration from
the file specified in the input argument. It will then choose an applicable
configuration group, should one exist, and apply it. However, if no
applicable group is found, it will not exit with an error.

Either way, knoll will continue to run and wait for a display reconfiguration
event from the operating system. At that point it will wait a few seconds for
the configuration to settle, and then attempt to find a matching configuration
and apply it.

Note, that while knoll can still accept a piped configuration, because of the
nature of pipes, it will not be able to reload the configuration upon a
reconfiguration event.

This quiescence period is to avoid knoll from triggering during some fumbling
with cables, quickly opening and closing a laptop lid, or displays taking some
time to awaken from sleep. If the default period is too long for your desired
level of responsiveness, it can be configured:

```bash
host$ knoll daemon --wait=500ms --input=my_config.json
```

## Configuration reference

A configuration may contain the following fields:

* `uuid`
    * This is used to uniquely identify a given display. This is the only
      required field.
        * JSON syntax: `"uuid": "b00184f4c1ee4cdf8ccfea3fca2f93b2"`.
        * RON syntax `uuid: "b00184f4c1ee4cdf8ccfea3fca2f93b2"`.
        * Nix syntax `uuid = "b00184f4c1ee4cdf8ccfea3fca2f93b2"`.
* `enabled`
    * In knolls output this indicates whether display is enabled, and in the
      input
      indicates whether it should remain enabled. Due to limitations in the APIs
      knoll uses at present, disabling a display will remove it from the
      computer's
      configuration. So once disabled, it can only be re-enabled by unplugging
      the display, restarting, etc.
        * JSON syntax: `"enabled": true`.
        * RON syntax: `enabled: true`.
        * Nix syntax: `enabled = true`.
* `origin`
    * This specifies the current or requested location of the display's upper
      left
      corner. Displays may not overlap and all displays must touch.
        * JSON syntax: `"origin": [ -100, 100 ]`.
        * RON syntax: `origin: (-100, 100)`.
        * Nix syntax: `origin = [ (-100) 100 ]`.
* `extents`
    * This specifies either the current or requested resolution of the display.
        * JSON syntax: `"extents": [ 2560, 1440 ]`
        * RON syntax: `extends: (2560, 1440)`.
        * Nix syntax: `extents = [ 2560 1440 ]`.
* `scaled`
    * This specifies whether the current or requested display mode should use
      one-to-one pixels or a "scaled" ("Retina") mode.
        * JSON syntax: `"scaled": true`.
        * RON syntax: `scaled: true`.
        * Nix syntax: `scaled = true`.
* `frequency`
    * This specifies the current or requested refresh frequency for the display
      in Hertz.
        * JSON syntax: `"frequency": 60`.
        * RON syntax: `frequency: 60`.
        * Nix syntax: `frequency = 60`.
* `color_depth`
    * This specifies the current or requested color depth of the display.
        * JSON syntax: `"color_depth": 8`.
        * RON syntax: `color_depth: 8`.
        * Ni syntax: `color_depth = 8`.
* `rotation`
    * This specifies the current or requested rotation of the display in
      degrees.
      At present, only 0, 90, 180, and 270 degree rotations are supported.
        * JSON syntax: `"rotation": 90`.
        * RON syntax: `rotation: 90`.
        * Nix syntax: `rotation = 90`.

## Future work

So far knoll has been working successfully for my specific use cases. However,
there is still room for additional improvements:

* Bug fixing. There remain many strange new displays to explore.
* Writing more tests.
* Support for display mirroring. I only ever mirror displays for presentations,
  so I opted to punt on this for the initial release. There is already some
  initial internals in place to support mirroring, but plumbing and testing is
  still needed.
* Find a better API for enabling/disabling displays. Most users would expect
  this feature to put the display to sleep rather than detach it from the
  computer.
* Detect display configurations with overlapping displays or gaps to warn
  that the configuration is not stable.
* Support UUID abbreviations similar to git hash abbreviations.
* Support configuring the brightness, gamma function, etc. for a display.
* It seems plausible that knoll could be extended to support Windows, XOrg,
  Wayland, etc. It is just a matter of finding the appropriate APIs and perhaps
  making some additional generalizations to the configuration data structures.

## Development

<p>
<a href="https://blog.rust-lang.org/2023/01/10/Rust-1.83.0.html"><img src="https://img.shields.io/badge/rustc-1.83.0+-lightgray.svg" alt="Rust 1.83.0+" /></a>
<a href="https://github.com/gawashburn/knoll/blob/master/LICENCE"><img src="https://img.shields.io/badge/licence-MIT-green" alt="MIT Licence" /></a>
</p>

knoll is written in [Rust](https://www.rust-lang.org/). I have not attempted
cross-compilation, but at present it seems unlikely that knoll could be compiled
successfully on another operating system other than macOS. That said, knoll
does not actually depend on any macOS headers, etc. so it should be possible
to compile it without installing
[XCode](https://developer.apple.com/xcode/).

Pull requests are definitely welcome. I am still a relative Rust novice, so it
also entirely possible there are better or more idiomatic ways to write some of
this code. I have endeavoured to write knoll in a way that is conducive to
unit testing. So please try to add appropriate tests for submitted changes.

## What's in a name?

knoll's name derives from the term
[knolling](https://en.wikipedia.org/wiki/|knolling):
> Kromelow would arrange any displaced tools at right angles on all surfaces,
> and called this routine knolling, in that the tools were arranged in right
> angles ... The result was an organized surface that allowed the user
> to see all objects at once.

It seemed apt as macOS does not currently support placing displays at arbitrary
angles and most users will want to organize their displays to all be clearly
visible.