Crates.io | knoll |
lib.rs | knoll |
version | |
source | src |
created_at | 2023-08-26 00:35:09.095218 |
updated_at | 2024-12-03 04:06:17.578258 |
description | A command-line tool for configuring macOS displays |
homepage | |
repository | https://github.com/gawashburn/knoll |
max_upload_size | |
id | 955192 |
Cargo.toml error: | TOML parse error at line 19, column 1 | 19 | autolib = false | ^^^^^^^ unknown field `autolib`, expected one of `name`, `version`, `edition`, `authors`, `description`, `readme`, `license`, `repository`, `homepage`, `documentation`, `build`, `resolver`, `links`, `default-run`, `default_dash_run`, `rust-version`, `rust_dash_version`, `rust_version`, `license-file`, `license_dash_file`, `license_file`, `licenseFile`, `license_capital_file`, `forced-target`, `forced_dash_target`, `autobins`, `autotests`, `autoexamples`, `autobenches`, `publish`, `metadata`, `keywords`, `categories`, `exclude`, `include` |
size | 0 |
A simple command-line tool for manipulating the configuration of macOS displays.
Until someone creates packages for knoll, probably the most common way to install it will be to use cargo or Nix.
If you already have a Rust environment set up, you can use the
cargo install
command:
cargo install knoll
The recommended solution for running knoll as a daemon is to make use of
launchd
.
Choose a service name unique to your host using
the reverse domain name
convention and create a .plist
file in ~/Library/LaunchAgents
:
<?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
launchctl enable gui/$(id -u)/my.service.knoll`
launchctl start gui/$(id -u)/my.service.knoll`
The knoll repository contains a Nix Flake
that can be used to integrate knoll into your
nix-darwin configuration. I currently
use the following launchd
definition like:
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";
};
};
};
knoll has three primary usage modes: pipeline mode, listing mode, and daemon 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:
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 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).
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:
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:
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:
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:
[ // 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.
[ // 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:
If there is no applicable display group in the provided configuration, knoll will exit with an error message and error code:
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
knoll's second mode of operation allows inspecting the allowed display mode of attached displays:
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.
Finally, knoll also supports a "daemon" mode.
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:
host$ knoll daemon --wait=500ms --input=my_config.json
A configuration may contain the following fields:
uuid
"uuid": "b00184f4c1ee4cdf8ccfea3fca2f93b2"
.uuid: "b00184f4c1ee4cdf8ccfea3fca2f93b2"
.uuid = "b00184f4c1ee4cdf8ccfea3fca2f93b2"
.enabled
"enabled": true
.enabled: true
.enabled = true
.origin
"origin": [ -100, 100 ]
.origin: (-100, 100)
.origin = [ (-100) 100 ]
.extents
"extents": [ 2560, 1440 ]
extends: (2560, 1440)
.extents = [ 2560 1440 ]
.scaled
"scaled": true
.scaled: true
.scaled = true
.frequency
"frequency": 60
.frequency: 60
.frequency = 60
.color_depth
"color_depth": 8
.color_depth: 8
.color_depth = 8
.rotation
"rotation": 90
.rotation: 90
.rotation = 90
.So far knoll has been working successfully for my specific use cases. However, there is still room for additional improvements:
knoll is written in Rust. 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.
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.
knoll's name derives from the term 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.