{"id":25968775,"url":"https://github.com/gawashburn/knoll","last_synced_at":"2025-03-04T22:25:11.983Z","repository":{"id":190701739,"uuid":"668962388","full_name":"gawashburn/knoll","owner":"gawashburn","description":"A simple command-line tool for manipulating the configuration of macOS displays.","archived":false,"fork":false,"pushed_at":"2024-12-03T04:15:51.000Z","size":185,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-04T15:43:38.558Z","etag":null,"topics":["displays","macos","macos-setup","monitors","nix","nix-flake","rust","rust-lang"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gawashburn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-21T02:50:43.000Z","updated_at":"2024-12-03T17:44:32.000Z","dependencies_parsed_at":"2023-08-26T01:32:51.146Z","dependency_job_id":"889361bc-11d0-4460-82d8-adafc9860287","html_url":"https://github.com/gawashburn/knoll","commit_stats":null,"previous_names":["gawashburn/knoll"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gawashburn%2Fknoll","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gawashburn%2Fknoll/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gawashburn%2Fknoll/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gawashburn%2Fknoll/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gawashburn","download_url":"https://codeload.github.com/gawashburn/knoll/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241930232,"owners_count":20044117,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["displays","macos","macos-setup","monitors","nix","nix-flake","rust","rust-lang"],"created_at":"2025-03-04T22:25:11.061Z","updated_at":"2025-03-04T22:25:11.952Z","avatar_url":"https://github.com/gawashburn.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# knoll\n\n\u003cp\u003e\n\u003ca href=\"https://crates.io/crates/knoll\"\u003e\u003cimg src=\"https://img.shields.io/crates/v/knoll?style=flat-square\" alt=\"Crates.io version\" /\u003e\u003c/a\u003e\n\u003cimg src=\"https://github.com/gawashburn/knoll/actions/workflows/tests.yml/badge.svg\" alt=\"Testing action\" /\u003e\n\u003ca href='https://coveralls.io/github/gawashburn/knoll?branch=main'\u003e\u003cimg src='https://coveralls.io/repos/github/gawashburn/knoll/badge.svg?branch=main' alt='Coverage Status' /\u003e\u003c/a\u003e\n\u003cimg src=\"https://img.shields.io/github/license/gawashburn/knoll\" alt=\"MIT License\" /\u003e\n\u003c/p\u003e\n\nA simple command-line tool for manipulating the configuration of macOS displays.\n\n## Table of contents\n\n- [Installation](#installation)\n    - [Cargo](#cargo)\n    - [launchd](#launchd)\n    - [Nix](#nix)\n- [Usage](#usage)\n    - [Pipeline mode](#pipeline-mode)\n    - [Listing mode](#listing-mode)\n    - [Daemon mode](#daemon-mode)\n- [Configuration reference](#configuration-reference)\n- [Future work](#future-work)\n- [Development](#development)\n- [What's in a name?](#whats-in-a-name)\n\n## Installation\n\n### Pre-built binaries\n\nPre-built Intel and Apple Silicon binaries are available from the GitHub\nrepository [releases](https://github.com/gawashburn/knoll/releases/) page.\n\nNote that after downloading and unpacking, as these binaries are not signed\nyou may need to run the following command so that macOS will allow them to\nbe run:\n\n```bash\nxattr -d com.apple.quarantine /path/to/knoll\n```\n\n### Cargo\n\nIf you already have a Rust environment set up, you can use the\n`cargo install` command:\n\n```bash\ncargo install knoll\n```\n\n### Nix\n\nThe knoll repository contains a [Nix Flake](https://nixos.wiki/wiki/Flakes)\nthat can be used to integrate knoll into your\n[nix-darwin](https://github.com/LnL7/nix-darwin/) configuration. I currently\nuse the following `launchd` definition like:\n\n```nix\n  launchd.user.agent = {\n    knoll = {\n      path = [ \"/run/current-system/sw/bin/\" ];\n      serviceConfig = {\n        ProgramArguments = let\n          configFile = pkgs.writeText \"knoll-config.json\"\n            (builtins.toJSON [\n              [\n                # MacBook Pro display\n                {\n                  uuid = \"8684ad81e3ea92cb14f43eb88b97a3f7\";\n                  enabled = true;\n                  origin = [ (-1792) 453 ];\n                  extents = [ 1792 1120 ];\n                  scaled = true;\n                  frequency = 59;\n                  color_depth = 8;\n                  rotation = 0;\n                }\n                ...\n              ]\n            ]);\n        in\n          [\n            \"/run/current-system/sw/bin/knoll\" \"daemon\" \"-vvv\" \"--format=json\"\n            \"--input=${configFile}\"\n          ];\n        KeepAlive = true;\n        RunAtLoad = true;\n        StandardErrorPath = \"/tmp/knoll.err\";\n        StandardOutPath = \"/tmp/knoll.out\";\n      };\n    };\n  };\n```\n\nThe particulars of the configuration file you craft in your Nix definition will\nbe explained in the subsequent sections.\n\n## Usage\n\nknoll has three primary usage modes: pipeline mode, listing mode, and\ndaemon mode.\n\n### Pipeline mode\n\nknoll's default mode supports reporting and updating the current display\nconfiguration. In the simplest case, you can just run it with no argument:\n\n```bash\nhost$ knoll\n[\n  [\n    {\n      \"uuid\": \"b00184f4c1ee4cdf8ccfea3fca2f93b2\",\n      \"enabled\": true,\n      \"origin\": [\n        0,\n        0\n      ],\n      \"extents\": [\n        2560,\n        1440\n      ],\n      \"scaled\": true,\n      \"frequency\": 60,\n      \"color_depth\": 8,\n      \"rotation\": 0\n    }\n  ]\n]\n```\n\nThe output here is the current display configuration\nin [JSON](https://www.json.org/)\nformat. It says that there is a single enabled display placed at (0,0) with a\nscaled resolution of 2560x1440. The display is not rotated and has a refresh\nfrequency of 60Hz and a color depth of 8-bits.\n\nknoll also supports\n[Rusty Object Notation (RON)](https://github.com/ron-rs/ron).\n\n```bash\nhost$ knoll --format=ron\n[\n    [\n        (\n            uuid: \"b00184f4c1ee4cdf8ccfea3fca2f93b2\",\n            enabled: true,\n            origin: (0, 0),\n            extents: (2560, 1440),\n            scaled: true,\n            frequency: 60,\n            color_depth: 8,\n            rotation: 0,\n        ),\n    ],\n]\n```\n\nThere are two primary benefits of using RON over JSON. One is that it is a\nslightly more compact. Second, and more importantly, it supports comments. This\nway you can annotate your configurations if you like. JSON was chosen as the\ndefault as it makes it easier to interface knoll with all the tooling available\nas part of the JSON ecosystem.\n\nYou may have noticed that the display configuration is nested two levels deep.\nknolls output consists of an outermost list of *configuration groups*. Each\nconfiguration group in turn consists of a list of display configurations.\n\nBy default, knoll will read a list of configuration groups from standard\ninput and apply the most specific configuration group that is applicable.\n\nAs the output of knoll is a configuration group, piping\nknoll to itself is an idempotent operation:\n\n```bash\nhost$ knoll | knoll --quiet\n# Should not change anything.\n```\n\nNote that because the operating system may accept some configuration changes\nwithout failure, but modifying them to satisfy certain constraints, providing\nknoll with a configuration is not an identity:\n\n```bash\nhost$ cat my_config.json | knoll \u003e out_config.json \n# my_config.json and out_config.json may differ.\n```\n\nThe most common case where this might happen is that `my_config.json` omits\nsome fields we are not interested in adjusting. Another case where this\nmight happen would be if a configuration group has displays that overlap or\nhave gaps. We will call these *unstable* configurations.\n\nAs just mentioned, display configurations can omit any fields that you do not\nwant to alter. For example, if you just wanted to rotate your display to be\nupside-down, you could write the following:\n\n```bash\nhost$ cat my_config.ron\n[\n    [\n        (\n            uuid: \"b00184f4c1ee4cdf8ccfea3fca2f93b2\",\n            rotation: 180,\n        ),\n    ],\n]\nhost$ knoll --quiet --format=ron --input=my_config.ron\n```\n\nThe resolution, location, etc. of the display will all remain unchanged.\n\nThe only required field is `uuid`. If just the `uuid` field\nis provided the configuration is effectively a no-op.\n\nEarlier I glossed over what it means for knoll to choose a \"most specific\"\nconfiguration group. A valid configuration group consists of one or more\ndisplay configurations with unique UUIDs:\n\n```bash\n[   // This is an invalid configuration group because\n    // there are duplicate UUIDs.\n    (   // First configuration\n        uuid: \"b00184f4c1ee4cdf8ccfea3fca2f93b2\",\n    ),\n    (   // Second configuration\n        uuid: \"b00184f4c1ee4cdf8ccfea3fca2f93b2\",\n    )\n]\n```\n\nA valid list of configuration groups must contain only groups that do not have\nthe same set of UUIDs.\n\n```bash\n[   // This is an invalid list of configuration groups because \n    // there are two groups with the same set of UUIDs.\n    [ // First group\n        (\n            uuid: \"b00184f4c1ee4cdf8ccfea3fca2f93b2\",\n        ),\n    ],\n    [   // Second group\n        (\n            uuid: \"b00184f4c1ee4cdf8ccfea3fca2f93b2\",\n        )\n    ],\n]\n```\n\nGiven these restrictions on validity, when run, knoll will determine all the\nUUID of all attached displays. It will then choose the configuration group\nwhere its UUIDs are the largest subset of the attached displays. The intent is\nhere is two-fold:\n\n* Attaching a new display to the computer will not cause an existing\n  configuration to become invalid.\n* It is possible to provide configurations with and without this new display.\n\nIf there is no applicable display group in the provided configuration,\nknoll will exit with an error message and error code:\n\n```bash\nhost$ cat bogus.ron\n[\n    [\n        ( // Improbable display UUID.\n          uuid: \"11111111111111111111111111111111\",\n        ),\n    ],\n]\nhost$ knoll --quiet --format=ron --input=bogus.ron\nNo configuration group matches the currently attached displays: \n37d8832a2d6602cab9f78f30a301b230, 94226c6fcef04e9b8503ffa88fedba08,\nf3def94a9fbd4de79a432d9d0bc7b4ce.\nhost$ echo $?\n1\n```\n\n### Listing mode\n\nknoll's second mode of operation allows inspecting the allowed display mode of\nattached displays:\n\n```bash\nhost$ knoll list\n[\n  {\n    \"uuid\": \"37d8832a2d6602cab9f78f30a301b230\",\n    \"modes\": [\n      {\n        \"scaled\": true,\n        \"color_depth\": 8,\n        \"frequency\": 59,\n        \"extents\": [\n          1280,\n          800\n        ]\n      },\n\n      {\n        \"scaled\": true,\n        \"color_depth\": 8,\n        \"frequency\": 60,\n        \"extents\": [\n          1024,\n          768\n        ]\n      }\n    ]\n  }\n]\n```\n\nThis is useful for determining which display configurations may successfully be\nused in an input to knoll.\n\n### Daemon mode\n\nFinally, knoll also supports a \"daemon\" mode.\n\n```bash\nhost$ knoll daemon --input=my_config.json\n```\n\nWhen in this mode, knoll wait until a display configuration event occurs. At\nthat time, if provided an input file, it will (re)load the configuration from\nthe file specified in the input argument. It will then choose an applicable\nconfiguration group, should one exist, and apply it. However, if no\napplicable group is found, it will not exit with an error.\n\nEither way, knoll will continue to run and wait for a display reconfiguration\nevent from the operating system. At that point it will wait a few seconds for\nthe configuration to settle, and then attempt to find a matching configuration\nand apply it.\n\nNote, that while knoll can still accept a piped configuration, because of the\nnature of pipes, it will not be able to reload the configuration upon a\nreconfiguration event.\n\nThis quiescence period is to avoid knoll from triggering during some fumbling\nwith cables, quickly opening and closing a laptop lid, or displays taking some\ntime to awaken from sleep. If the default period is too long for your desired\nlevel of responsiveness, it can be configured:\n\n```bash\nhost$ knoll daemon --wait=500ms --input=my_config.json\n```\n\n### launchd\n\nThe recommended solution for running knoll as a daemon is to make use of\n[\n`launchd`](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html).\nIf you are not using nix-darwin as described in\nthe [Installation](#installation)\nsection, you can still configure `launchd` manually.\nChoose a service name unique to your host using\nthe [reverse domain name](https://en.wikipedia.org/wiki/Reverse_domain_name_notation)\nconvention and create a `.plist` file in `~/Library/LaunchAgents`:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003c!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\"\n        \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"\u003e\n\u003cplist version=\"1.0\"\u003e\n    \u003cdict\u003e\n        \u003ckey\u003eEnvironmentVariables\u003c/key\u003e\n        \u003cdict\u003e\n            \u003ckey\u003ePATH\u003c/key\u003e\n            \u003cstring\u003e...\u003c/string\u003e\n        \u003c/dict\u003e\n        \u003ckey\u003eKeepAlive\u003c/key\u003e\n        \u003ctrue/\u003e\n        \u003ckey\u003eLabel\u003c/key\u003e\n        \u003cstring\u003emy.service.knoll\u003c/string\u003e\n        \u003ckey\u003eProgramArguments\u003c/key\u003e\n        \u003carray\u003e\n            \u003cstring\u003e/path/to/knoll\u003c/string\u003e\n            \u003cstring\u003edaemon\u003c/string\u003e\n            \u003cstring\u003e-vvv\u003c/string\u003e\n            \u003cstring\u003e--input=/path/to/config-file\u003c/string\u003e\n        \u003c/array\u003e\n        \u003ckey\u003eRunAtLoad\u003c/key\u003e\n        \u003ctrue/\u003e\n        \u003ckey\u003eStandardErrorPath\u003c/key\u003e\n        \u003cstring\u003e/tmp/knoll.err\u003c/string\u003e\n        \u003ckey\u003eStandardOutPath\u003c/key\u003e\n        \u003cstring\u003e/tmp/knoll.out\u003c/string\u003e\n    \u003c/dict\u003e\n\u003c/plist\u003e\n```\n\nYou can then enable and start service using\n\n```bash\nlaunchctl enable gui/$(id -u)/my.service.knoll`\nlaunchctl start gui/$(id -u)/my.service.knoll`\n````\n\n## Configuration reference\n\nA configuration may contain the following fields:\n\n* `uuid`\n    * This is used to uniquely identify a given display. This is the only\n      required field.\n        * JSON syntax: `\"uuid\": \"b00184f4c1ee4cdf8ccfea3fca2f93b2\"`.\n        * RON syntax `uuid: \"b00184f4c1ee4cdf8ccfea3fca2f93b2\"`.\n        * Nix syntax `uuid = \"b00184f4c1ee4cdf8ccfea3fca2f93b2\"`.\n* `enabled`\n    * In knolls output this indicates whether display is enabled, and in the\n      input\n      indicates whether it should remain enabled. Due to limitations in the APIs\n      knoll uses at present, disabling a display will remove it from the\n      computer's\n      configuration. So once disabled, it can only be re-enabled by unplugging\n      the display, restarting, etc.\n        * JSON syntax: `\"enabled\": true`.\n        * RON syntax: `enabled: true`.\n        * Nix syntax: `enabled = true`.\n* `origin`\n    * This specifies the current or requested location of the display's upper\n      left\n      corner. Displays may not overlap and all displays must touch.\n        * JSON syntax: `\"origin\": [ -100, 100 ]`.\n        * RON syntax: `origin: (-100, 100)`.\n        * Nix syntax: `origin = [ (-100) 100 ]`.\n* `extents`\n    * This specifies either the current or requested resolution of the display.\n        * JSON syntax: `\"extents\": [ 2560, 1440 ]`\n        * RON syntax: `extends: (2560, 1440)`.\n        * Nix syntax: `extents = [ 2560 1440 ]`.\n* `scaled`\n    * This specifies whether the current or requested display mode should use\n      one-to-one pixels or a \"scaled\" (\"Retina\") mode.\n        * JSON syntax: `\"scaled\": true`.\n        * RON syntax: `scaled: true`.\n        * Nix syntax: `scaled = true`.\n* `frequency`\n    * This specifies the current or requested refresh frequency for the display\n      in Hertz.\n        * JSON syntax: `\"frequency\": 60`.\n        * RON syntax: `frequency: 60`.\n        * Nix syntax: `frequency = 60`.\n* `color_depth`\n    * This specifies the current or requested color depth of the display.\n        * JSON syntax: `\"color_depth\": 8`.\n        * RON syntax: `color_depth: 8`.\n        * Ni syntax: `color_depth = 8`.\n* `rotation`\n    * This specifies the current or requested rotation of the display in\n      degrees.\n      At present, only 0, 90, 180, and 270 degree rotations are supported.\n        * JSON syntax: `\"rotation\": 90`.\n        * RON syntax: `rotation: 90`.\n        * Nix syntax: `rotation = 90`.\n\n## Future work\n\nSo far knoll has been working successfully for my specific use cases. However,\nthere is still room for additional improvements:\n\n* Bug fixing. There remain many strange new displays to explore.\n* Writing more tests.\n* Support for display mirroring. I only ever mirror displays for presentations,\n  so I opted to punt on this for the initial release. There is already some\n  initial internals in place to support mirroring, but plumbing and testing is\n  still needed.\n* Find a better API for enabling/disabling displays. Most users would expect\n  this feature to put the display to sleep rather than detach it from the\n  computer.\n* Detect display configurations with overlapping displays or gaps to warn\n  that the configuration is not stable.\n* Support UUID abbreviations similar to git hash abbreviations.\n* Support configuring the brightness, gamma function, etc. for a display.\n* It seems plausible that knoll could be extended to support Windows, XOrg,\n  Wayland, etc. It is just a matter of finding the appropriate APIs and perhaps\n  making some additional generalizations to the configuration data structures.\n\n## Development\n\n\u003cp\u003e\n\u003ca href=\"https://blog.rust-lang.org/2023/01/10/Rust-1.83.0.html\"\u003e\u003cimg src=\"https://img.shields.io/badge/rustc-1.83.0+-lightgray.svg\" alt=\"Rust 1.83.0+\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/gawashburn/knoll/blob/master/LICENCE\"\u003e\u003cimg src=\"https://img.shields.io/badge/licence-MIT-green\" alt=\"MIT Licence\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\nknoll is written in [Rust](https://www.rust-lang.org/). I have not attempted\ncross-compilation, but at present it seems unlikely that knoll could be compiled\nsuccessfully on another operating system other than macOS. That said, knoll\ndoes not actually depend on any macOS headers, etc. so it should be possible\nto compile it without installing\n[XCode](https://developer.apple.com/xcode/).\n\nPull requests are definitely welcome. I am still a relative Rust novice, so it\nalso entirely possible there are better or more idiomatic ways to write some of\nthis code. I have endeavoured to write knoll in a way that is conducive to\nunit testing. So please try to add appropriate tests for submitted changes.\n\n## What's in a name?\n\nknoll's name derives from the term\n[knolling](https://en.wikipedia.org/wiki/|knolling):\n\u003e Kromelow would arrange any displaced tools at right angles on all surfaces,\n\u003e and called this routine knolling, in that the tools were arranged in right\n\u003e angles ... The result was an organized surface that allowed the user\n\u003e to see all objects at once.\n\nIt seemed apt as macOS does not currently support placing displays at arbitrary\nangles and most users will want to organize their displays to all be clearly\nvisible.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgawashburn%2Fknoll","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgawashburn%2Fknoll","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgawashburn%2Fknoll/lists"}