mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-14 21:04:04 +01:00
feat(wm): JSON schemas, and the module's typed options
komorebi.json is described as a JSON Schema, made from the Rust types that read it: komorebic prints the schema of komorebi.json, of an applications.json, of a notification and of a message, and the daemon answers the same four questions on its socket. schema.json and schema.asc.json are committed (just jsonschema). The module's config option is typed from the schema: schemars-nixgen makes nix/komorebi-options.nix from schema.json (just nixgen), so a layout komorebi does not have, or a value of the wrong kind, does not evaluate. The module writes config as a komorebi.json and gives it to the daemon with --config. configFile and the file the module writes are checked against the schema when the system is built. applicationSpecificConfiguration takes files from GitHub, and there are none by default, since no community file for Plasma is known. Two checks keep the generated files honest without the tools that make them. schema-fresh compares the schemas with what komorebic prints, and the hash on the first line of the options with the schema. nix-options-validation evaluates the options against the example configuration, written in Nix and in JSON. nixos-module-eval also evaluates the module with the sources turned off: with applicationSpecificConfiguration.enable = false the sources are left out of the config that is written, and with no config either there is nothing to write and the unit passes no --config. ARCHITECTURE has the guarantee row that names the two checks.
This commit is contained in:
1 parent
ed34286bca
commit
3a3a0f8f24
29 files changed
+2754
-49
No files matched your search
@@ -72,6 +72,14 @@ session also changes the VM suite `module`, which logs in to Plasma on a machine
|
||||
with the module and checks the daemon the unit runs. Every other VM suite runs
|
||||
on machines built with the module too.
|
||||
|
||||
Three files are generated from the Rust types and committed: `schema.json` and
|
||||
`schema.asc.json` (`just jsonschema`), and `nix/komorebi-options.nix`, which types the
|
||||
module's `config` (`just nixgen`, which needs `schemars-nixgen`, `cargo install
|
||||
schemars-nixgen`). A change to what `komorebi.json` or an `applications.json` can
|
||||
say regenerates them in the same commit, and the check `schema-fresh` fails when
|
||||
it does not. The first line of the options file is the hash of the schema it was
|
||||
made from, which is how the check tells without running `schemars-nixgen`.
|
||||
|
||||
## VM tests
|
||||
|
||||
A change that needs a real KWin to be checked comes with a VM test, next to
|
||||
|
||||
Generated
+2
@@ -1099,6 +1099,8 @@ dependencies = [
|
||||
"komorebi-client",
|
||||
"lazy_static",
|
||||
"pastey",
|
||||
"schemars 1.2.3",
|
||||
"serde_json_lenient",
|
||||
"sysinfo",
|
||||
"version",
|
||||
]
|
||||
|
||||
@@ -99,7 +99,9 @@ package.json: `tsc` and `node` come from the dev shell), inside `nix develop`.
|
||||
`nix flake check` runs the same checks as derivations, plus `cargo deny`, the
|
||||
TOML and Nix formatting, the tests under `cargo nextest`, and
|
||||
`nixos-module-eval`, which evaluates the NixOS module in real NixOS systems, on
|
||||
Linux and on an Apple silicon Mac. On Linux it also boots every VM test, which
|
||||
Linux and on an Apple silicon Mac, `schema-fresh`, which compares the schemas and
|
||||
the options made from them with the Rust types, and `nix-options-validation`,
|
||||
which checks the example configuration, in Nix and in JSON, against the schema. On Linux it also boots every VM test, which
|
||||
needs KVM. A Mac runs the tests against the fake desktop, since KWin only exists
|
||||
on Linux. `nix fmt` formats every file the checks look at.
|
||||
|
||||
@@ -204,7 +206,10 @@ can be in files of their own, which `app_specific_configuration_path` names.
|
||||
`workspace-layout-rule` makes a
|
||||
workspace change layout with the number of windows it has, as the file's
|
||||
`layout_rules` do, and `generate-static-config` prints the settings the daemon
|
||||
has as a `komorebi.json`.
|
||||
has as a `komorebi.json`. The Rust types that read the file describe it as a JSON Schema
|
||||
(`schema.json`, written by `just jsonschema` and printed by `static-config-schema`), and the
|
||||
module's `config` option is typed from that schema (`nix/komorebi-options.nix`, written by
|
||||
`just nixgen`), so a configuration written in Nix is checked when it is evaluated.
|
||||
|
||||
A program such as a status bar can subscribe to the daemon's notifications: it
|
||||
listens on a socket of its own, and after each event and each message, the
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
```text
|
||||
Generate a JSON Schema for applications.json
|
||||
|
||||
Usage: application-specific-configuration-schema
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
```text
|
||||
Generate a JSON Schema of subscription notifications
|
||||
|
||||
Usage: notification-schema
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
```text
|
||||
Generate a JSON Schema of socket messages
|
||||
|
||||
Usage: socket-schema
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
```text
|
||||
Generate a JSON Schema of the static configuration file
|
||||
|
||||
Usage: static-config-schema
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -683,6 +683,19 @@ defaults. A **rule** is an entry of one of the process-wide lists. A workspace's
|
||||
`layout_rules`, `work_area_offset_rules` and `window_container_behaviour_rules` are fields of the
|
||||
[`Workspace`], which [`Workspace::update`] reads each time it tiles.
|
||||
|
||||
The same types describe the file as a JSON Schema, which `schemars` derives: `komorebic
|
||||
static-config-schema` prints the schema of [`StaticConfig`], `application-specific-configuration-schema`
|
||||
that of an applications file, and `notification-schema` and `socket-schema` those of a
|
||||
[`Notification`] and of a [`SocketMessage`]; the daemon answers the same four questions on its
|
||||
socket (the fourth is the schema of a list of applications files, where `komorebic` prints the
|
||||
schema of one).
|
||||
The schemas of the first two are committed (`schema.json`, `schema.asc.json`, written by `just
|
||||
jsonschema`), and so are the options the module's `config` is typed with
|
||||
(`nix/komorebi-options.nix`, which `schemars-nixgen` makes from `schema.json` and `just nixgen`
|
||||
gives the hash of the schema as its first line). The check `schema-fresh` compares the schemas with
|
||||
what the Rust types print and the hash with the schema, so a change to a key that leaves one of
|
||||
them behind fails.
|
||||
|
||||
One `komorebi.json` is meant for every platform komorebi runs on, so [`StaticConfig`] has no
|
||||
`deny_unknown_fields`: a key it does not read is dropped, and the keys of other platforms that
|
||||
mean nothing on KDE Plasma are fields that nothing reads. `StaticConfig::from(&WindowManager)`
|
||||
@@ -695,6 +708,9 @@ goes the other way, for `GenerateStaticConfig`.
|
||||
| The example in the docs is the example file, which loads | test | `the_example_in_the_docs_is_the_example_file`, `fixtures_load_round_trip_and_only_lose_windows_only_keys` | none |
|
||||
| A path to an applications file that is not there is skipped, and one that is there and is broken is an error | framework path: `handle_asc_file` returns early when the path does not exist, and parses the file otherwise | `a_missing_application_specific_configuration_is_skipped_not_fatal`, `an_application_specific_configuration_that_exists_but_is_broken_is_still_an_error`, `a_file_that_is_not_there_is_skipped_and_one_that_is_broken_is_an_error` | none |
|
||||
| The rules of an applications file and of the configuration add up | framework path: both are appended to the process-wide lists | `the_rules_of_the_configuration_and_of_the_file_add_up`, `every_file_of_a_list_is_read` | none |
|
||||
| `schema.json` and `schema.asc.json` are what the Rust types print | Nix check `schema-fresh`, and a test that compares them with `komorebic`'s output | `schema-fresh`, `the_schema_files_of_the_repository_are_what_the_subcommands_print` | `just jsonschema` |
|
||||
| The options the module is typed with are made from the committed schema | the hash of `schema.json` in the first line of `nix/komorebi-options.nix`, compared by `schema-fresh` | `schema-fresh` | `just nixgen` |
|
||||
| The example, in Nix and in JSON, follows the schema, and says the same in both | Nix check `nix-options-validation`: the options evaluated against the Nix example, both examples checked with `check-jsonschema`, and one contained in the other | `nix-options-validation` | none |
|
||||
| A file that cannot be read changes nothing | framework path: [`StaticConfig::reload`] reads and parses the file before it applies anything | `a_configuration_that_cannot_be_read_changes_nothing`, `the_daemon_follows_the_file_it_is_given_when_the_file_changes` | none |
|
||||
| The daemon applies a change to its file, written in place or replaced by a rename | framework path: the watcher sends `ReloadStaticConfiguration` to the daemon's own socket | `the_daemon_follows_the_file_it_is_given_when_the_file_changes`, `vm-module` | none: the file has to be the one the daemon was started with |
|
||||
| A monitor takes the entry its display index preference gives it, else the first nobody has | framework path: [`StaticConfig::postload`] and [`StaticConfig::reload`] choose the entry by serial number id first | `a_monitor_takes_the_configuration_its_display_index_preference_gives_it` | none |
|
||||
@@ -711,9 +727,16 @@ The flake exports one module under two names, `nixosModules.default` and
|
||||
`nixosModules.komorebi`: [`nixos-module.nix`] and the flake's overlay, which builds
|
||||
`komorebi-full` (both programs) with the flake's nixpkgs and Rust toolchain. Its options are
|
||||
`enable`, `package` (by default `pkgs.komorebi-full`, from the overlay), `logLevel`,
|
||||
`configFile`, and `execStart`, which is read-only: the module sets it to `bin/komorebi` of the
|
||||
package with `--log-level`, and `--config` with `configFile` when that is set. With
|
||||
`services.komorebi.enable`, the module:
|
||||
`configFile`, `config`, `applicationSpecificConfiguration`, and `execStart`, which is read-only: the
|
||||
module sets it to `bin/komorebi` of the package with `--log-level`, and `--config` with a
|
||||
`komorebi.json` when `configFile` or `config` is set. `config` is typed from
|
||||
`nix/komorebi-options.nix`, the options `schemars-nixgen` makes from the schema of `komorebi.json`;
|
||||
the module writes it as JSON, with no null in it, and `configFile` and the written file both go
|
||||
through `check-jsonschema` against `schema.json` when the system is built (comment lines taken out
|
||||
first, since the checker reads plain JSON). `applicationSpecificConfiguration.sources` are files
|
||||
fetched from GitHub that become `app_specific_configuration_path`; the module has none by default.
|
||||
`internal.configAttrs` is the configuration as the attribute set the options make, for the tests.
|
||||
With `services.komorebi.enable`, the module:
|
||||
|
||||
- installs the package in the system profile;
|
||||
- defines the user unit `komorebi.service`, which every user of the machine has. Plasma's
|
||||
@@ -734,7 +757,9 @@ module's unit.
|
||||
| komorebi is enabled only on a machine with Plasma 6 | evaluation error: an assertion of the module | `nixos-module-eval` ("without Plasma 6, an assertion fails") | none |
|
||||
| The log level is one the daemon accepts | evaluation error: `logLevel` is an enum of the daemon's `--log-level` values | `nixos-module-eval` ("a log level the daemon does not have does not evaluate") | none |
|
||||
| The unit runs `execStart`, and nothing but the module sets it | evaluation error: the option is read-only | `nixos-module-eval` ("execStart cannot be set", "the unit runs execStart") | a unit of your own |
|
||||
| The daemon is configured by `configFile` when it is set | framework path: `execStart` passes it as `--config`, which comes before the config home | `nixos-module-eval` ("configFile reaches execStart and the unit as --config"), `vm-module` | none: with no `configFile` the daemon reads the file of the config home |
|
||||
| The daemon is configured by `configFile` when it is set, else by `config` | framework path: `execStart` passes the checked file as `--config`, which comes before the config home | `nixos-module-eval` ("configFile reaches execStart and the unit as --config", "config reaches execStart ..."), `vm-module` | none: with neither the daemon reads the file of the config home |
|
||||
| A configuration with a layout or a value komorebi does not have does not build | evaluation error: `config` is typed from the schema; and `check-jsonschema` fails the build for a file that does not follow it | `nixos-module-eval` ("a layout komorebi does not have does not evaluate"), `nix-options-validation` | `configFile` with a file the schema accepts |
|
||||
| The sources of application specific configuration reach the daemon only while `applicationSpecificConfiguration.enable` is set | framework path: the module adds `app_specific_configuration_path` to the file it writes only when `enable` is set and there are sources | `nixos-module-eval` ("with applicationSpecificConfiguration.enable = false, the sources are left out of config", "with the sources turned off and no config, there is nothing to write") | none |
|
||||
| `komorebi` and `komorebic` are on every user's `PATH` | the module installs the package in the system profile | `nixos-module-eval`; every VM suite runs `komorebic` | none |
|
||||
| Every Plasma session starts komorebi, once KWin's service has started | systemd: the user unit is wanted by `plasma-workspace.target`, after `plasma-core.target` | `nixos-module-eval`, `vm-module` | `systemctl --user mask komorebi` |
|
||||
| A daemon that fails or is killed is started again | systemd: `Restart=on-failure` | `vm-module` (`SIGKILL`) | none |
|
||||
@@ -996,7 +1021,13 @@ A window is shown, and ends up on a tile:
|
||||
with several monitors, [`komorebi/tests/hot_reload.rs`](../../komorebi/tests/hot_reload.rs) the
|
||||
daemon following a file, and
|
||||
[`komorebi/tests/config_parity.rs`](../../komorebi/tests/config_parity.rs) the files of other
|
||||
platforms and the example, which load; [`komorebi/tests/asc.rs`](../../komorebi/tests/asc.rs)
|
||||
platforms and the example, which load;
|
||||
[`komorebi/tests/schemas.rs`](../../komorebi/tests/schemas.rs) the schemas the daemon answers
|
||||
with; [`schema.json`](../../schema.json), [`schema.asc.json`](../../schema.asc.json) and
|
||||
[`nix/komorebi-options.nix`](../../nix/komorebi-options.nix), which are generated;
|
||||
[`nix/tests/default.nix`](../../nix/tests/default.nix) and
|
||||
[`nix/tests/komorebi-example.nix`](../../nix/tests/komorebi-example.nix), the check of the options
|
||||
and the example it evaluates them against; [`komorebi/tests/asc.rs`](../../komorebi/tests/asc.rs)
|
||||
the applications files; [`komorebi/src/core/asc.rs`](../../komorebi/src/core/asc.rs) their
|
||||
format.
|
||||
- [`komorebi/src/process_event.rs`](../../komorebi/src/process_event.rs): what each event
|
||||
|
||||
@@ -354,3 +354,12 @@ Because one `komorebi.json` is meant to work on every platform, and the file it
|
||||
one that another platform has: a configuration written for macOS names `applications.mac.json`,
|
||||
which a KDE desktop does not have. The daemon warns and goes on. A file that exists and is not an
|
||||
applications.json is a mistake in something the user wrote, and is an error.
|
||||
|
||||
## Why does `nix/komorebi-options.nix` start with the hash of the schema?
|
||||
|
||||
Because the check that the options are fresh cannot make them again. `schemars-nixgen` makes
|
||||
`nix/komorebi-options.nix` from `schema.json`, and it is not in the dev shell or in CI (it is
|
||||
installed with `cargo install`). `just nixgen` writes the hash of `schema.json` as the first line
|
||||
of the file, and the check `schema-fresh` compares that with the hash of the committed schema, and
|
||||
the schema with what `komorebic static-config-schema` prints. A change to a key that leaves the
|
||||
schema or the options behind fails the check, with no tool but the build.
|
||||
+30
-5
@@ -72,21 +72,46 @@ the repository's `rust-toolchain.toml` selects, whatever nixpkgs your system use
|
||||
| `services.komorebi.enable` | `false` | Installs komorebi and starts it with every Plasma session. |
|
||||
| `services.komorebi.package` | `pkgs.komorebi-full` | The package with `bin/komorebi` and `bin/komorebic`; the unit runs the first, and both are installed. |
|
||||
| `services.komorebi.logLevel` | `"info"` | The daemon's `--log-level`: `"error"`, `"warn"`, `"info"`, `"debug"` or `"trace"`. |
|
||||
| `services.komorebi.configFile` | `null` | A `komorebi.json` for the daemon. The unit passes it to komorebi as `--config`, so the daemon reads it and not the `komorebi.json` of `~/.config/komorebi`. The file is in the Nix store: komorebi reads a new one when it starts again. |
|
||||
| `services.komorebi.execStart` | read only | The command line the unit starts komorebi with, built from `package`, `logLevel` and `configFile`. A unit of your own that runs komorebi uses it to start komorebi the same way. |
|
||||
| `services.komorebi.config` | `null` | The configuration, as Nix: every key of `komorebi.json`, typed from the schema. The module writes it as a `komorebi.json`, checks that against the schema, and the unit passes it to komorebi as `--config`. Ignored if `configFile` is set. |
|
||||
| `services.komorebi.configFile` | `null` | A `komorebi.json` for the daemon, checked against the schema when the system is built. The unit passes it to komorebi as `--config`, so the daemon reads it and not the `komorebi.json` of `~/.config/komorebi`. The file is in the Nix store: komorebi reads a new one when it starts again. |
|
||||
| `services.komorebi.applicationSpecificConfiguration` | `enable = true`, `sources = [ ]` | Files of [application specific configuration](usage/application-specific-configuration.md) fetched from GitHub (`owner`, `repo`, `rev`, `path` and `hash` each), which become the `app_specific_configuration_path` of the configuration `config` makes. There are none by default. |
|
||||
| `services.komorebi.execStart` | read only | The command line the unit starts komorebi with, built from `package`, `logLevel` and `configFile` or `config`. A unit of your own that runs komorebi uses it to start komorebi the same way. |
|
||||
|
||||
The file for `configFile` is written like any other `komorebi.json`
|
||||
([Configuration](usage/configuration.md)):
|
||||
`config` takes the keys of `komorebi.json` ([Configuration](usage/configuration.md)) as Nix, and a
|
||||
value of the wrong kind, or a layout that does not exist, is an error when the configuration is
|
||||
evaluated:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.komorebi = {
|
||||
enable = true;
|
||||
configFile = ./komorebi.json;
|
||||
config = {
|
||||
default_workspace_padding = 15;
|
||||
monitors = [
|
||||
{
|
||||
workspaces = [
|
||||
{ name = "web"; layout = "BSP"; }
|
||||
{ name = "code"; layout = "Columns"; }
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`configFile` takes a `komorebi.json` you wrote:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.komorebi.configFile = ./komorebi.json;
|
||||
}
|
||||
```
|
||||
|
||||
Comment lines (starting with `//`) are taken out of the file before it is checked, which is why
|
||||
the file can have them. With both, `configFile` wins, and the module warns that `config` is
|
||||
ignored.
|
||||
|
||||
### Running it
|
||||
|
||||
`plasma-workspace.target` starts komorebi when you log in. In a session that was already
|
||||
|
||||
@@ -187,3 +187,32 @@ index of the workspace, the number of containers and the layout;
|
||||
[`named-workspace-layout-rule`](../cli/named-workspace-layout-rule.md) takes the name of the
|
||||
workspace instead. A rule for a count that has one replaces it. The clear commands take every
|
||||
rule of the workspace away and leave it with the layout it has.
|
||||
|
||||
## The schema
|
||||
|
||||
The Rust types that read `komorebi.json` also describe it as a JSON Schema, which an editor can
|
||||
check the file against, and which the NixOS module is typed from. [`schema.json`](../../schema.json)
|
||||
in the repository is the schema of `komorebi.json`, [`schema.asc.json`](../../schema.asc.json) the
|
||||
schema of an [`applications.json`](application-specific-configuration.md), and the commands print
|
||||
them (and two more), without a daemon:
|
||||
|
||||
```sh
|
||||
komorebic static-config-schema
|
||||
komorebic application-specific-configuration-schema
|
||||
komorebic notification-schema
|
||||
komorebic socket-schema
|
||||
```
|
||||
|
||||
[`notification-schema`](../cli/notification-schema.md) is the schema of what a
|
||||
[subscriber](subscribing-to-notifications.md) is sent, and
|
||||
[`socket-schema`](../cli/socket-schema.md) the schema of a message. To check a file by hand:
|
||||
|
||||
```sh
|
||||
komorebic static-config-schema > schema.json
|
||||
check-jsonschema --schemafile schema.json ~/.config/komorebi/komorebi.json
|
||||
```
|
||||
|
||||
`check-jsonschema` reads plain JSON, so comments in the file have to come out first. The schema
|
||||
does not stop komorebi from reading a file that does not follow it: a key it does not have is
|
||||
dropped, as [above](#one-file-for-every-platform).
|
||||
|
||||
@@ -99,6 +99,15 @@
|
||||
;
|
||||
komorebi = extractBinary "komorebi";
|
||||
komorebic = extractBinary "komorebic";
|
||||
# komorebic with the constant commit of the checks, whose output the schemas are
|
||||
# checked against: it is the same for every commit
|
||||
komorebicForChecks = craneLib.buildPackage (
|
||||
individualCrateArgs
|
||||
// {
|
||||
pname = "komorebic-for-checks";
|
||||
cargoExtraArgs = "--package komorebic --bin komorebic";
|
||||
}
|
||||
);
|
||||
};
|
||||
in
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
@@ -216,6 +225,30 @@
|
||||
|
||||
vm-lib = vm.lib;
|
||||
|
||||
# The schemas, the options made from one of them and the example are what the Rust types
|
||||
# say: `just jsonschema` and `just nixgen` write them
|
||||
schema-fresh =
|
||||
pkgs.runCommand "komorebi-schema-fresh"
|
||||
{
|
||||
nativeBuildInputs = [
|
||||
build.komorebicForChecks
|
||||
pkgs.diffutils
|
||||
];
|
||||
}
|
||||
''
|
||||
komorebic static-config-schema | diff -u ${./schema.json} - \
|
||||
|| { echo "schema.json is stale: run just jsonschema"; exit 1; }
|
||||
komorebic application-specific-configuration-schema | diff -u ${./schema.asc.json} - \
|
||||
|| { echo "schema.asc.json is stale: run just jsonschema"; exit 1; }
|
||||
expected=$(sha256sum ${./schema.json} | cut -d' ' -f1)
|
||||
head -n 1 ${./nix/komorebi-options.nix} | grep -qx "# schema-sha256: $expected" \
|
||||
|| { echo "nix/komorebi-options.nix is stale: run just nixgen"; exit 1; }
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# the options the module is typed with, evaluated against the example configuration
|
||||
nix-options-validation = import ./nix/tests/default.nix { inherit pkgs; };
|
||||
|
||||
# The NixOS module in real NixOS systems, evaluated and never built, so a Mac checks it
|
||||
# too. A VM suite, `vm-module`, runs it.
|
||||
nixos-module-eval = import ./nix/tests/nixos-module.nix {
|
||||
@@ -282,6 +315,7 @@
|
||||
|
||||
pkgs.cargo-deny
|
||||
pkgs.cargo-nextest
|
||||
pkgs.check-jsonschema
|
||||
pkgs.jq
|
||||
pkgs.just
|
||||
pkgs.ripgrep
|
||||
|
||||
@@ -23,6 +23,21 @@ lint:
|
||||
test -z "$(rg 'dbg!' --type rust .)" || (echo "dbg! macro not allowed"; false)
|
||||
test -z "$(rg 'println!' --type rust ./komorebi/src)" || (echo "println! macro not allowed"; false)
|
||||
|
||||
# the JSON Schemas of komorebi.json and of applications.json, from the Rust types
|
||||
jsonschema:
|
||||
cargo run --package komorebic --locked -- static-config-schema > schema.json
|
||||
cargo run --package komorebic --locked -- application-specific-configuration-schema > schema.asc.json
|
||||
|
||||
# the options of the module's `config`, from schema.json, with the hash of the schema they were
|
||||
# made from in the first line (the check `schema-fresh` compares it); needs schemars-nixgen
|
||||
# (`cargo install schemars-nixgen`), which the dev shell does not have
|
||||
nixgen:
|
||||
schemars-nixgen schema.json -o nix/komorebi-options.nix --name komorebi --description "komorebi for KDE Plasma configuration"
|
||||
printf '# schema-sha256: %s\n' "$(nix hash file --type sha256 --base16 schema.json)" | cat - nix/komorebi-options.nix > nix/komorebi-options.nix.new
|
||||
mv nix/komorebi-options.nix.new nix/komorebi-options.nix
|
||||
nix fmt nix/komorebi-options.nix
|
||||
nix build -L --no-link ".#checks.$(nix eval --raw --impure --expr builtins.currentSystem).nix-options-validation"
|
||||
|
||||
# one VM test, on this machine: `just vm kwin`. Linux boots the guest under KVM through the
|
||||
# flake check; a Mac builds the guest on a builder and runs the driver (see docs/dev/vm-tests.md)
|
||||
vm name:
|
||||
|
||||
@@ -496,6 +496,10 @@ pub enum SocketMessage {
|
||||
RemoveSubscriberSocket(String),
|
||||
// AddSubscriberPipe(String),
|
||||
// RemoveSubscriberPipe(String),
|
||||
ApplicationSpecificConfigurationSchema,
|
||||
NotificationSchema,
|
||||
SocketSchema,
|
||||
StaticConfigSchema,
|
||||
GenerateStaticConfig,
|
||||
DebugWindow(u32),
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
//! ([`RuleDebug`]), as JSON, or nothing for a window that is not there.
|
||||
//! - **GenerateStaticConfig**: the window manager's settings as a `komorebi.json`
|
||||
//! ([`StaticConfig`]), as JSON.
|
||||
//! - **StaticConfigSchema**, **ApplicationSpecificConfigurationSchema**, **NotificationSchema**
|
||||
//! and **SocketSchema**: the JSON Schema of `komorebi.json`, of a list of applications files,
|
||||
//! of a [`Notification`] and of a [`SocketMessage`]. They answer nothing when the daemon is
|
||||
//! built without the `schemars` feature.
|
||||
//!
|
||||
//! The others answer nothing:
|
||||
//!
|
||||
@@ -1625,6 +1629,44 @@ impl WindowManager {
|
||||
SocketMessage::UnmanagedWindowOperationBehaviour(behaviour) => {
|
||||
self.unmanaged_window_operation_behaviour = behaviour;
|
||||
}
|
||||
SocketMessage::ApplicationSpecificConfigurationSchema => {
|
||||
#[cfg(feature = "schemars")]
|
||||
{
|
||||
let asc = schemars::schema_for!(
|
||||
Vec<crate::core::asc::ApplicationSpecificConfiguration>
|
||||
);
|
||||
let schema = serde_json::to_string_pretty(&asc)?;
|
||||
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
SocketMessage::NotificationSchema => {
|
||||
#[cfg(feature = "schemars")]
|
||||
{
|
||||
let notification = schemars::schema_for!(Notification);
|
||||
let schema = serde_json::to_string_pretty(¬ification)?;
|
||||
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
SocketMessage::SocketSchema => {
|
||||
#[cfg(feature = "schemars")]
|
||||
{
|
||||
let socket_message = schemars::schema_for!(SocketMessage);
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
SocketMessage::StaticConfigSchema => {
|
||||
#[cfg(feature = "schemars")]
|
||||
{
|
||||
let static_config = schemars::schema_for!(StaticConfig);
|
||||
let schema = serde_json::to_string_pretty(&static_config)?;
|
||||
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
SocketMessage::AddSubscriberSocket(ref socket) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
//! The JSON Schemas the daemon answers with: `StaticConfigSchema`,
|
||||
//! `ApplicationSpecificConfigurationSchema`, `NotificationSchema` and `SocketSchema`, against an
|
||||
//! in-memory desktop. `komorebic` prints the same schemas without the daemon; the daemon answers
|
||||
//! on its socket for a client that has none.
|
||||
|
||||
use komorebi::backend::fake::FakeBackend;
|
||||
use komorebi::backend::with_backend;
|
||||
use komorebi::core::SocketMessage;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn answer(message: SocketMessage) -> serde_json::Value {
|
||||
let fake = Arc::new(FakeBackend::with_one_monitor());
|
||||
let (_sender, receiver) = crossbeam_channel::bounded(1);
|
||||
let socket = std::path::PathBuf::from(format!("/tmp/kms-{}.sock", std::process::id()));
|
||||
|
||||
let mut reply = Vec::new();
|
||||
with_backend(fake, || {
|
||||
let mut wm = WindowManager::new(receiver, Some(socket.clone())).unwrap();
|
||||
wm.init().unwrap();
|
||||
wm.process_command(message, &mut reply)
|
||||
})
|
||||
.unwrap();
|
||||
let _ = std::fs::remove_file(&socket);
|
||||
|
||||
serde_json::from_slice(&reply).expect("the answer is JSON")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_daemon_answers_with_the_schema_of_komorebi_json() {
|
||||
let schema = answer(SocketMessage::StaticConfigSchema);
|
||||
|
||||
assert_eq!(schema["title"], "StaticConfig");
|
||||
assert!(schema["properties"]["monitors"].is_object());
|
||||
assert!(schema["properties"]["app_specific_configuration_path"].is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_daemon_answers_with_the_schema_of_a_list_of_applications_files() {
|
||||
let schema = answer(SocketMessage::ApplicationSpecificConfigurationSchema);
|
||||
|
||||
assert_eq!(schema["type"], "array");
|
||||
assert!(schema["$defs"]["AscApplicationRules"].is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_daemon_answers_with_the_schemas_of_what_it_sends_and_of_what_it_takes() {
|
||||
assert!(answer(SocketMessage::NotificationSchema)["properties"]["state"].is_object());
|
||||
assert!(answer(SocketMessage::SocketSchema)["oneOf"].is_array());
|
||||
}
|
||||
@@ -16,11 +16,13 @@ dirs = { workspace = true }
|
||||
fs-tail = "0.1"
|
||||
lazy_static = { workspace = true }
|
||||
pastey = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["komorebi-client/default"]
|
||||
schemars = ["dep:schemars", "komorebi-client/default"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)'] }
|
||||
+45
-1
@@ -84,7 +84,10 @@
|
||||
//! The others work without the daemon: `start` runs `komorebi` in the background, with the
|
||||
//! `komorebi.json` it is given with `--config`, `configuration` shows the path of the
|
||||
//! `komorebi.json` the daemon reads, `log` follows its log, `data-directory` shows where its
|
||||
//! socket is, and `completions` prints a completion script for a shell. The hidden `docgen` writes the help of every subcommand as Markdown, wrapped
|
||||
//! socket is, `completions` prints a completion script for a shell, and `static-config-schema`,
|
||||
//! `application-specific-configuration-schema`, `notification-schema` and `socket-schema` print
|
||||
//! the JSON Schema of `komorebi.json`, of an `applications.json`, of a notification and of a
|
||||
//! message. The hidden `docgen` writes the help of every subcommand as Markdown, wrapped
|
||||
//! at 100 columns whatever the width of the terminal, which is how `docs/cli` is made.
|
||||
|
||||
use chrono::Utc;
|
||||
@@ -1029,6 +1032,15 @@ enum SubCommand {
|
||||
MouseFollowsFocus(MouseFollowsFocus),
|
||||
/// Toggle mouse follows focus on all workspaces
|
||||
ToggleMouseFollowsFocus,
|
||||
/// Generate a JSON Schema for applications.json
|
||||
#[clap(alias = "asc-schema")]
|
||||
ApplicationSpecificConfigurationSchema,
|
||||
/// Generate a JSON Schema of subscription notifications
|
||||
NotificationSchema,
|
||||
/// Generate a JSON Schema of socket messages
|
||||
SocketSchema,
|
||||
/// Generate a JSON Schema of the static configuration file
|
||||
StaticConfigSchema,
|
||||
/// Generates a static configuration JSON file based on the current window manager state
|
||||
GenerateStaticConfig,
|
||||
}
|
||||
@@ -1622,6 +1634,38 @@ fn main() -> eyre::Result<()> {
|
||||
SubCommand::MouseFollowsFocus(args) => {
|
||||
send_message(&SocketMessage::MouseFollowsFocus(args.boolean_state.into()))?;
|
||||
}
|
||||
SubCommand::ApplicationSpecificConfigurationSchema => {
|
||||
#[cfg(feature = "schemars")]
|
||||
{
|
||||
let asc = schemars::schema_for!(komorebi_client::ApplicationSpecificConfiguration);
|
||||
let schema = serde_json::to_string_pretty(&asc)?;
|
||||
println!("{schema}");
|
||||
}
|
||||
}
|
||||
SubCommand::NotificationSchema => {
|
||||
#[cfg(feature = "schemars")]
|
||||
{
|
||||
let notification = schemars::schema_for!(komorebi_client::Notification);
|
||||
let schema = serde_json::to_string_pretty(¬ification)?;
|
||||
println!("{schema}");
|
||||
}
|
||||
}
|
||||
SubCommand::SocketSchema => {
|
||||
#[cfg(feature = "schemars")]
|
||||
{
|
||||
let socket_message = schemars::schema_for!(SocketMessage);
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
println!("{schema}");
|
||||
}
|
||||
}
|
||||
SubCommand::StaticConfigSchema => {
|
||||
#[cfg(feature = "schemars")]
|
||||
{
|
||||
let socket_message = schemars::schema_for!(komorebi_client::StaticConfig);
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
println!("{schema}");
|
||||
}
|
||||
}
|
||||
SubCommand::SubscribeSocket(args) => {
|
||||
send_message(&SocketMessage::AddSubscriberSocket(args.socket))?;
|
||||
}
|
||||
|
||||
@@ -692,6 +692,50 @@ fn a_config_home_that_is_not_a_directory_is_refused() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The JSON `komorebic` prints for a subcommand that needs no daemon
|
||||
fn printed(args: &[&str]) -> serde_json::Value {
|
||||
let runtime = Runtime::new();
|
||||
|
||||
let output = runtime.komorebic(args);
|
||||
|
||||
assert!(output.status.success(), "{args:?}: {output:?}");
|
||||
serde_json::from_str(&stdout(&output)).unwrap_or_else(|error| panic!("{args:?}: {error}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_schema_subcommands_print_a_json_schema_without_a_daemon() {
|
||||
let configuration = printed(&["static-config-schema"]);
|
||||
assert!(configuration["properties"]["resize_delta"].is_object());
|
||||
assert!(configuration["$defs"]["MonitorConfig"].is_object());
|
||||
|
||||
// `asc-schema` is another name for it
|
||||
let applications = printed(&["application-specific-configuration-schema"]);
|
||||
assert_eq!(applications, printed(&["asc-schema"]));
|
||||
assert!(applications["$defs"]["AscApplicationRules"]["properties"]["floating"].is_object());
|
||||
|
||||
assert!(printed(&["notification-schema"])["$defs"]["WindowManagerEvent"].is_object());
|
||||
assert!(printed(&["socket-schema"])["oneOf"].is_array());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_schema_files_of_the_repository_are_what_the_subcommands_print() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
|
||||
for (file, subcommand) in [
|
||||
("schema.json", "static-config-schema"),
|
||||
(
|
||||
"schema.asc.json",
|
||||
"application-specific-configuration-schema",
|
||||
),
|
||||
] {
|
||||
let runtime = Runtime::new();
|
||||
let printed = stdout(&runtime.komorebic(&[subcommand]));
|
||||
let committed = std::fs::read_to_string(root.join(file)).unwrap();
|
||||
|
||||
assert_eq!(printed, committed, "{file} is stale: run `just jsonschema`");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_monitor_subcommands_send_what_they_are_given() {
|
||||
sends(
|
||||
@@ -813,6 +857,7 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
||||
[
|
||||
"adjust-container-padding.md",
|
||||
"adjust-workspace-padding.md",
|
||||
"application-specific-configuration-schema.md",
|
||||
"cancel-preselect.md",
|
||||
"change-layout.md",
|
||||
"clear-named-workspace-layout-rules.md",
|
||||
@@ -881,6 +926,7 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
||||
"named-workspace-padding.md",
|
||||
"named-workspace-tiling.md",
|
||||
"new-workspace.md",
|
||||
"notification-schema.md",
|
||||
"preselect-direction.md",
|
||||
"promote-focus.md",
|
||||
"promote-swap.md",
|
||||
@@ -902,10 +948,12 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
||||
"send-to-workspace.md",
|
||||
"session-float-rule.md",
|
||||
"session-float-rules.md",
|
||||
"socket-schema.md",
|
||||
"stack-all.md",
|
||||
"stack.md",
|
||||
"start.md",
|
||||
"state.md",
|
||||
"static-config-schema.md",
|
||||
"stop.md",
|
||||
"subscribe-socket.md",
|
||||
"swap-workspaces-with-monitor.md",
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
# schema-sha256: 2bb6a47d645d5027d15ea70545473ac3dd417e8a2d7ac932d3c593d71a539dbd
|
||||
{ lib, ... }:
|
||||
let
|
||||
defaultLayout = (
|
||||
lib.types.enum [
|
||||
"BSP"
|
||||
"Columns"
|
||||
"Rows"
|
||||
"VerticalStack"
|
||||
"HorizontalStack"
|
||||
"UltrawideVerticalStack"
|
||||
"Grid"
|
||||
"RightMainVerticalStack"
|
||||
"Scrolling"
|
||||
]
|
||||
);
|
||||
floatingLayerBehaviour = (
|
||||
lib.types.enum [
|
||||
"Tile"
|
||||
"Float"
|
||||
]
|
||||
);
|
||||
idWithIdentifier = (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
id = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Target identifier";
|
||||
};
|
||||
kind = lib.mkOption {
|
||||
type = (
|
||||
lib.types.enum [
|
||||
"Exe"
|
||||
"Class"
|
||||
"Title"
|
||||
"Path"
|
||||
]
|
||||
);
|
||||
description = "Kind of identifier to target";
|
||||
};
|
||||
matching_strategy = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"Legacy"
|
||||
"Equals"
|
||||
"StartsWith"
|
||||
"EndsWith"
|
||||
"Contains"
|
||||
"Regex"
|
||||
"DoesNotEndWith"
|
||||
"DoesNotStartWith"
|
||||
"DoesNotEqual"
|
||||
"DoesNotContain"
|
||||
]
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
description = "Matching strategy to use";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
layoutOptions = (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
column_ratios = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.listOf (lib.types.nullOr lib.types.number)));
|
||||
default = null;
|
||||
description = "Column width ratios (up to MAX_RATIOS values between 0.1 and 0.9)\n\n- Used by Columns layout: ratios for each column width\n- Used by Grid layout: ratios for column widths\n- Used by BSP, VerticalStack, RightMainVerticalStack: column_ratios[0] as primary split ratio\n- Used by HorizontalStack: column_ratios[0] as primary split ratio (top area height)\n- Used by UltrawideVerticalStack: column_ratios[0] as center ratio, column_ratios[1] as left ratio\n\nColumns without a ratio share remaining space equally.\nExample: `[0.3, 0.4, 0.3]` for 30%-40%-30% columns";
|
||||
};
|
||||
grid = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
rows = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = "Maximum number of rows per grid column";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
description = "Options related to the Grid layout";
|
||||
};
|
||||
row_ratios = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.listOf (lib.types.nullOr lib.types.number)));
|
||||
default = null;
|
||||
description = "Row height ratios (up to MAX_RATIOS values between 0.1 and 0.9)\n\n- Used by Rows layout: ratios for each row height\n- Used by Grid layout: ratios for row heights\n\nRows without a ratio share remaining space equally.\nExample: `[0.5, 0.5]` for 50%-50% rows";
|
||||
};
|
||||
scrolling = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
center_focused_column = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.bool);
|
||||
default = null;
|
||||
description = "With an odd number of visible columns, keep the focused window column centered";
|
||||
};
|
||||
columns = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = "Desired number of visible columns (default: 3)";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
description = "Options related to the Scrolling layout";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
matchingRule = (
|
||||
lib.types.oneOf [
|
||||
idWithIdentifier
|
||||
(lib.types.listOf idWithIdentifier)
|
||||
]
|
||||
);
|
||||
pathBuf = lib.types.str;
|
||||
placement = (
|
||||
lib.types.enum [
|
||||
"None"
|
||||
"Center"
|
||||
"CenterAndResize"
|
||||
]
|
||||
);
|
||||
rect = (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
bottom = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = "Height of the rectangle (from the top point)";
|
||||
};
|
||||
left = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = "Left point of the rectangle";
|
||||
};
|
||||
right = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = "Width of the recentangle (from the left point)";
|
||||
};
|
||||
top = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = "Top point of the rectangle";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
windowContainerBehaviour = (
|
||||
lib.types.enum [
|
||||
"Create"
|
||||
"Append"
|
||||
]
|
||||
);
|
||||
in
|
||||
{
|
||||
options.komorebi = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
app_specific_configuration_path = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.oneOf [
|
||||
pathBuf
|
||||
(lib.types.listOf pathBuf)
|
||||
]
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
description = "Path to an applications.json, or a list of them, with rules for the applications that\nneed them: which windows to ignore, to manage and to float";
|
||||
};
|
||||
cross_boundary_behaviour = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"Workspace"
|
||||
"Monitor"
|
||||
]
|
||||
)
|
||||
);
|
||||
default = "Monitor";
|
||||
description = "Determine what happens when an action is called on a window at a monitor boundary";
|
||||
};
|
||||
cross_monitor_move_behaviour = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"Swap"
|
||||
"Insert"
|
||||
"NoOp"
|
||||
]
|
||||
)
|
||||
);
|
||||
default = "Swap";
|
||||
description = "Determine what happens when a window is moved across a monitor boundary";
|
||||
};
|
||||
default_container_padding = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.int);
|
||||
default = 5;
|
||||
description = "Global default container padding";
|
||||
};
|
||||
default_workspace_padding = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.int);
|
||||
default = 5;
|
||||
description = "Global default workspace padding";
|
||||
};
|
||||
display_index_preferences = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.attrsOf lib.types.str));
|
||||
default = null;
|
||||
description = "Set display index preferences";
|
||||
};
|
||||
float_override = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.bool);
|
||||
default = false;
|
||||
description = "Enable or disable float override, which makes it so every new window opens in floating mode";
|
||||
};
|
||||
float_override_placement = lib.mkOption {
|
||||
type = (lib.types.nullOr placement);
|
||||
default = null;
|
||||
description = "Determines the `Placement` to be used when spawning a window with float override active";
|
||||
};
|
||||
float_rule_placement = lib.mkOption {
|
||||
type = (lib.types.nullOr placement);
|
||||
default = null;
|
||||
description = "Determines the `Placement` to be used when spawning a window that matches a\n'floating_applications' rule";
|
||||
};
|
||||
floating_applications = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.listOf matchingRule));
|
||||
default = null;
|
||||
description = "Identify applications which should be managed as floating windows";
|
||||
};
|
||||
floating_layer_behaviour = lib.mkOption {
|
||||
type = (lib.types.nullOr floatingLayerBehaviour);
|
||||
default = "Tile";
|
||||
description = "Determines what happens on a new window when on the `FloatingLayer`";
|
||||
};
|
||||
floating_layer_placement = lib.mkOption {
|
||||
type = (lib.types.nullOr placement);
|
||||
default = "Center";
|
||||
description = "Determines the `Placement` to be used when spawning a window on the floating layer with the\n`FloatingLayerBehaviour` set to `FloatingLayerBehaviour::Float`";
|
||||
};
|
||||
floating_window_aspect_ratio = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.oneOf [
|
||||
(lib.types.enum [
|
||||
"Ultrawide"
|
||||
"Widescreen"
|
||||
"Standard"
|
||||
])
|
||||
(lib.types.listOf lib.types.int)
|
||||
]
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
description = "Aspect ratio to resize with when toggling floating mode for a window";
|
||||
};
|
||||
global_work_area_offset = lib.mkOption {
|
||||
type = (lib.types.nullOr rect);
|
||||
default = null;
|
||||
description = "Global work area (space used for tiling) offset";
|
||||
};
|
||||
ignore_rules = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.listOf matchingRule));
|
||||
default = null;
|
||||
description = "Individual window floating rules";
|
||||
};
|
||||
layout_defaults = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
layout_options = lib.mkOption {
|
||||
type = (lib.types.nullOr layoutOptions);
|
||||
default = null;
|
||||
description = "Default layout options for this layout";
|
||||
};
|
||||
layout_options_rules = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.attrsOf layoutOptions));
|
||||
default = null;
|
||||
description = "Threshold-based layout options rules in the format of threshold => options.\nWhen container count >= threshold, the highest matching threshold's options\nfully replace the base `layout_options`.";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
description = "Per-layout default options and rules, keyed by layout name.\nApplied as fallback when a workspace does not define its own layout_options or layout_options_rules.\nIf a workspace defines either setting, all global defaults for that layout are completely replaced.";
|
||||
};
|
||||
manage_rules = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.listOf matchingRule));
|
||||
default = null;
|
||||
description = "Individual window force-manage rules";
|
||||
};
|
||||
monitors = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.listOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
container_padding = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.int);
|
||||
default = null;
|
||||
description = "Container padding";
|
||||
};
|
||||
floating_layer_behaviour = lib.mkOption {
|
||||
type = (lib.types.nullOr floatingLayerBehaviour);
|
||||
default = "Tile";
|
||||
description = "Determine what happens to a new window when the Floating workspace layer is active";
|
||||
};
|
||||
window_based_work_area_offset = lib.mkOption {
|
||||
type = (lib.types.nullOr rect);
|
||||
default = null;
|
||||
description = "Window based work area offset";
|
||||
};
|
||||
window_based_work_area_offset_limit = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.int);
|
||||
default = 1;
|
||||
description = "Open window limit after which the window based work area offset will no longer be applied";
|
||||
};
|
||||
window_hiding_position = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"BottomLeft"
|
||||
"BottomRight"
|
||||
]
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
description = "Accepted so that a configuration written for another platform loads, and ignored: there is\nno hiding position on KDE Plasma";
|
||||
};
|
||||
work_area_offset = lib.mkOption {
|
||||
type = (lib.types.nullOr rect);
|
||||
default = null;
|
||||
description = "Monitor-specific work area offset";
|
||||
};
|
||||
workspace_padding = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.int);
|
||||
default = null;
|
||||
description = "Workspace padding (default: global)";
|
||||
};
|
||||
workspaces = lib.mkOption {
|
||||
type = (
|
||||
lib.types.listOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
apply_window_based_work_area_offset = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.bool);
|
||||
default = true;
|
||||
description = "Apply this monitor's window-based work area offset";
|
||||
};
|
||||
container_padding = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.int);
|
||||
default = null;
|
||||
description = "Container padding (default: global)";
|
||||
};
|
||||
float_override = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.bool);
|
||||
default = false;
|
||||
description = "Enable or disable float override, which makes it so every new window opens in floating mode";
|
||||
};
|
||||
floating_layer_behaviour = lib.mkOption {
|
||||
type = (lib.types.nullOr floatingLayerBehaviour);
|
||||
default = "Tile";
|
||||
description = "Determine what happens to a new window when the Floating workspace layer is active";
|
||||
};
|
||||
layout = lib.mkOption {
|
||||
type = (lib.types.nullOr defaultLayout);
|
||||
default = "BSP";
|
||||
description = "Layout";
|
||||
};
|
||||
layout_flip = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"Horizontal"
|
||||
"Vertical"
|
||||
"HorizontalAndVertical"
|
||||
]
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
description = "Specify an axis on which to flip the selected layout";
|
||||
};
|
||||
layout_options = lib.mkOption {
|
||||
type = (lib.types.nullOr layoutOptions);
|
||||
default = null;
|
||||
description = "Layout-specific options";
|
||||
};
|
||||
layout_options_rules = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.attrsOf layoutOptions));
|
||||
default = null;
|
||||
description = "Threshold-based layout options rules in the format of threshold => options.\nWhen container count >= threshold, the highest matching threshold's options\nfully replace the base `layout_options`.\nThis follows the same threshold logic as `layout_rules`.";
|
||||
};
|
||||
layout_rules = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.attrsOf defaultLayout));
|
||||
default = null;
|
||||
description = "Layout rules in the format of threshold => layout";
|
||||
};
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Name";
|
||||
};
|
||||
tile = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.bool);
|
||||
default = true;
|
||||
description = "Enable or disable tiling for the workspace";
|
||||
};
|
||||
window_container_behaviour = lib.mkOption {
|
||||
type = (lib.types.nullOr windowContainerBehaviour);
|
||||
default = "Create";
|
||||
description = "Determine what happens when a new window is opened";
|
||||
};
|
||||
window_container_behaviour_rules = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.attrsOf windowContainerBehaviour));
|
||||
default = null;
|
||||
description = "Window container behavior rules in the format of threshold => behavior";
|
||||
};
|
||||
work_area_offset = lib.mkOption {
|
||||
type = (lib.types.nullOr rect);
|
||||
default = null;
|
||||
description = "Workspace specific work area offset";
|
||||
};
|
||||
work_area_offset_rules = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.attrsOf rect));
|
||||
default = null;
|
||||
description = "Work area offset rules in the format of threshold => Rect (default: None)";
|
||||
};
|
||||
workspace_padding = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.int);
|
||||
default = null;
|
||||
description = "Workspace padding (default: global)";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
description = "Workspace configurations";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
description = "Monitor and workspace configurations";
|
||||
};
|
||||
monocle_focus_behaviour = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"Cycle"
|
||||
"NoOp"
|
||||
]
|
||||
)
|
||||
);
|
||||
default = "NoOp";
|
||||
description = "Determine what happens when focusing in a direction while a monocle container is active";
|
||||
};
|
||||
mouse_follows_focus = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.bool);
|
||||
default = true;
|
||||
description = "Enable or disable mouse follows focus";
|
||||
};
|
||||
resize_delta = lib.mkOption {
|
||||
type = (lib.types.nullOr lib.types.int);
|
||||
default = 50;
|
||||
description = "Delta to resize windows by";
|
||||
};
|
||||
tabbed_applications = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.listOf lib.types.str));
|
||||
default = null;
|
||||
description = "Accepted so that a configuration written for another platform loads, and ignored: a KWin\nwindow has no native tabs";
|
||||
};
|
||||
titleless_applications = lib.mkOption {
|
||||
type = (lib.types.nullOr (lib.types.listOf lib.types.str));
|
||||
default = null;
|
||||
description = "Accepted so that a configuration written for another platform loads, and ignored: a KWin\nwindow is always a window";
|
||||
};
|
||||
toggle_float_placement = lib.mkOption {
|
||||
type = (lib.types.nullOr placement);
|
||||
default = "CenterAndResize";
|
||||
description = "Determines the placement of a new window when toggling to float";
|
||||
};
|
||||
unmanaged_window_operation_behaviour = lib.mkOption {
|
||||
type = (
|
||||
lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"Op"
|
||||
"NoOp"
|
||||
]
|
||||
)
|
||||
);
|
||||
default = "Op";
|
||||
description = "Determine what happens when commands are sent while an unmanaged window is in the foreground";
|
||||
};
|
||||
window_container_behaviour = lib.mkOption {
|
||||
type = (lib.types.nullOr windowContainerBehaviour);
|
||||
default = "Create";
|
||||
description = "Determine what happens when a new window is opened";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = { };
|
||||
description = "komorebi for KDE Plasma configuration";
|
||||
};
|
||||
}
|
||||
+191
-7
@@ -11,6 +11,83 @@
|
||||
}:
|
||||
let
|
||||
cfg = config.services.komorebi;
|
||||
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
|
||||
# The options `just nixgen` makes from `schema.json`, which type `config`
|
||||
komorebiOptionsModule = import ./komorebi-options.nix { inherit lib; };
|
||||
|
||||
# the Nix module system uses null for an optional field that is not set, and the schema wants
|
||||
# the field to be absent
|
||||
filterNulls =
|
||||
value:
|
||||
if value == null then
|
||||
null
|
||||
else if builtins.isList value then
|
||||
map filterNulls (builtins.filter (x: x != null) value)
|
||||
else if builtins.isAttrs value then
|
||||
lib.filterAttrs (_n: v: v != null) (builtins.mapAttrs (_n: v: filterNulls v) value)
|
||||
else
|
||||
value;
|
||||
|
||||
# A komorebi.json, checked against the schema the Rust types make (`schema.json`). Comment
|
||||
# lines are taken out first: komorebi reads JSON with comments, and the schema checker does not.
|
||||
validateJsonFile =
|
||||
{
|
||||
name,
|
||||
jsonFile,
|
||||
}:
|
||||
pkgs.runCommand "validated-${name}" { nativeBuildInputs = [ pkgs.check-jsonschema ]; } ''
|
||||
grep -v '^\s*//' ${jsonFile} > clean.json
|
||||
check-jsonschema --schemafile ${../schema.json} clean.json
|
||||
cp clean.json $out
|
||||
'';
|
||||
|
||||
fetchAppSpecificSource =
|
||||
source:
|
||||
pkgs.fetchurl {
|
||||
url = "https://raw.githubusercontent.com/${source.owner}/${source.repo}/${source.rev}/${source.path}";
|
||||
inherit (source) hash;
|
||||
};
|
||||
|
||||
applicationSpecificConfigFiles = map fetchAppSpecificSource cfg.applicationSpecificConfiguration.sources;
|
||||
|
||||
# one file is a path, several are a list of paths
|
||||
appSpecificPaths =
|
||||
if builtins.length applicationSpecificConfigFiles == 1 then
|
||||
"${builtins.head applicationSpecificConfigFiles}"
|
||||
else
|
||||
map (f: "${f}") applicationSpecificConfigFiles;
|
||||
|
||||
shouldUseAppSpecific =
|
||||
cfg.applicationSpecificConfiguration.enable && applicationSpecificConfigFiles != [ ];
|
||||
|
||||
# What `config` and `applicationSpecificConfiguration` make: the komorebi.json as an attribute
|
||||
# set, with no null in it, or null when there is nothing to write
|
||||
configAttrs =
|
||||
if cfg.config == null && !shouldUseAppSpecific then
|
||||
null
|
||||
else
|
||||
filterNulls (
|
||||
(if cfg.config == null then { } else cfg.config)
|
||||
// lib.optionalAttrs shouldUseAppSpecific { app_specific_configuration_path = appSpecificPaths; }
|
||||
);
|
||||
|
||||
# the file the daemon is given: the one `configFile` names, or the one the options make, each
|
||||
# checked against the schema
|
||||
komorebiConfigFile =
|
||||
if cfg.configFile != null then
|
||||
validateJsonFile {
|
||||
name = "komorebi.json";
|
||||
jsonFile = cfg.configFile;
|
||||
}
|
||||
else if configAttrs != null then
|
||||
validateJsonFile {
|
||||
name = "komorebi.json";
|
||||
jsonFile = jsonFormat.generate "komorebi.json" configAttrs;
|
||||
}
|
||||
else
|
||||
null;
|
||||
in
|
||||
{
|
||||
options.services.komorebi = {
|
||||
@@ -44,12 +121,110 @@ in
|
||||
default = null;
|
||||
example = lib.literalExpression "./komorebi.json";
|
||||
description = ''
|
||||
The `komorebi.json` the daemon is configured by. The unit starts komorebi with it, as
|
||||
`--config`, in place of the `komorebi.json` the daemon looks for in `~/.config/komorebi`
|
||||
The `komorebi.json` the daemon is configured by, checked against the schema of
|
||||
`komorebi.json` when the system is built. The unit starts komorebi with it, as `--config`,
|
||||
in place of the `komorebi.json` the daemon looks for in `~/.config/komorebi`
|
||||
(`$KOMOREBI_CONFIG_HOME`). The file is in the Nix store, so it does not change under the
|
||||
daemon: a new file is read when the daemon starts again. `komorebic configuration` still
|
||||
shows the file of the config home, which the daemon does not read while this is set. With
|
||||
no `configFile`, the daemon reads the file of the config home, if there is one.
|
||||
neither `configFile` nor `config`, the daemon reads the file of the config home, if there
|
||||
is one. Ignored by `config`, which is ignored when this is set.
|
||||
'';
|
||||
};
|
||||
|
||||
config = lib.mkOption {
|
||||
type = lib.types.nullOr komorebiOptionsModule.options.komorebi.type;
|
||||
default = null;
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
default_workspace_padding = 15;
|
||||
monitors = [
|
||||
{
|
||||
workspaces = [
|
||||
{ name = "web"; layout = "BSP"; }
|
||||
{ name = "code"; layout = "Columns"; }
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
The configuration of komorebi, written as Nix: every key of `komorebi.json`, typed from
|
||||
the schema, so a layout that does not exist or a value of the wrong kind does not
|
||||
evaluate. The module writes it as a `komorebi.json`, checks that against the schema, and
|
||||
starts komorebi with it as `--config`. Ignored if `configFile` is set. A key you leave
|
||||
out is the default the schema gives it, if it gives one, and the daemon's own otherwise.
|
||||
'';
|
||||
};
|
||||
|
||||
applicationSpecificConfiguration = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Whether to use the sources of application specific configuration.";
|
||||
};
|
||||
|
||||
sources = lib.mkOption {
|
||||
type = lib.types.listOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
owner = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "GitHub repository owner.";
|
||||
example = "LGUG2Z";
|
||||
};
|
||||
repo = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "GitHub repository name.";
|
||||
example = "komorebi-application-specific-configuration";
|
||||
};
|
||||
rev = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Git revision (commit hash, tag, or branch).";
|
||||
example = "09ebdcd95780b168cecde2f2d49096a58e91365f";
|
||||
};
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "applications.json";
|
||||
description = "Path to the JSON file within the repository.";
|
||||
};
|
||||
hash = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "SHA256 hash of the file, for reproducibility.";
|
||||
example = "sha256-gfYubJpVEU6vxdd/vqujQhQX8mdAEE9ImugqdIyTQKk=";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = [ ];
|
||||
description = ''
|
||||
Files of application specific configuration, each fetched from GitHub, which the module
|
||||
gives the daemon as `app_specific_configuration_path` of the configuration it writes
|
||||
(it replaces the value `config` has for that key). There are none by default: no
|
||||
community file for KDE Plasma is known. Ignored when `configFile` is set.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
{
|
||||
owner = "myuser";
|
||||
repo = "my-komorebi-rules";
|
||||
rev = "0123456789abcdef0123456789abcdef01234567";
|
||||
path = "applications.json";
|
||||
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||
}
|
||||
]
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
internal.configAttrs = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.attrs;
|
||||
readOnly = true;
|
||||
internal = true;
|
||||
description = ''
|
||||
What `config` and `applicationSpecificConfiguration` make, before it is written as JSON:
|
||||
the `komorebi.json` as an attribute set with no null in it, or null when neither is set.
|
||||
The tests of the module read it, so that they need not build the file.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -58,8 +233,8 @@ in
|
||||
readOnly = true;
|
||||
description = ''
|
||||
The command line the unit starts komorebi with: `bin/komorebi` of `package`, with
|
||||
`logLevel`, and with `--config` and `configFile` when that is set. The module sets it,
|
||||
and setting it anywhere else is an error. A unit of your own that runs komorebi somewhere
|
||||
`logLevel`, and with `--config` and the checked `komorebi.json` when `configFile` or
|
||||
`config` is set. The module sets it, and setting it anywhere else is an error. A unit of your own that runs komorebi somewhere
|
||||
else, as the VM tests do in their headless sessions, starts it with this, the way the
|
||||
module does.
|
||||
'';
|
||||
@@ -74,11 +249,12 @@ in
|
||||
"--log-level"
|
||||
cfg.logLevel
|
||||
]
|
||||
++ lib.optionals (cfg.configFile != null) [
|
||||
++ lib.optionals (komorebiConfigFile != null) [
|
||||
"--config"
|
||||
"${cfg.configFile}"
|
||||
"${komorebiConfigFile}"
|
||||
]
|
||||
);
|
||||
services.komorebi.internal.configAttrs = configAttrs;
|
||||
}
|
||||
|
||||
(lib.mkIf cfg.enable {
|
||||
@@ -92,6 +268,14 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
warnings =
|
||||
lib.optional (
|
||||
cfg.configFile != null && cfg.config != null
|
||||
) "services.komorebi.config is ignored: services.komorebi.configFile is set."
|
||||
++
|
||||
lib.optional (cfg.configFile != null && cfg.applicationSpecificConfiguration.sources != [ ])
|
||||
"services.komorebi.applicationSpecificConfiguration is ignored: services.komorebi.configFile is set.";
|
||||
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
|
||||
# A user unit, so it runs for every user who logs in to Plasma. plasma-workspace.target is
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# `nix-options-validation`: the options `just nixgen` makes from `schema.json`, evaluated against
|
||||
# the example configuration, whose JSON is checked against the schema, and against the example
|
||||
# file of the docs. Run with `nix build -f nix/tests/default.nix`, or as a check of the flake.
|
||||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
}:
|
||||
let
|
||||
lib = pkgs.lib;
|
||||
|
||||
# the Nix module system uses null for an optional field that is not set, and the schema wants
|
||||
# the field to be absent
|
||||
filterNulls =
|
||||
value:
|
||||
if value == null then
|
||||
null
|
||||
else if builtins.isList value then
|
||||
map filterNulls (builtins.filter (x: x != null) value)
|
||||
else if builtins.isAttrs value then
|
||||
lib.filterAttrs (_n: v: v != null) (builtins.mapAttrs (_n: v: filterNulls v) value)
|
||||
else
|
||||
value;
|
||||
|
||||
evaluated =
|
||||
(lib.evalModules {
|
||||
modules = [
|
||||
(import ../komorebi-options.nix)
|
||||
(import ./komorebi-example.nix)
|
||||
];
|
||||
}).config.komorebi;
|
||||
|
||||
evaluatedJson = pkgs.writeText "komorebi-example-from-nix.json" (
|
||||
builtins.toJSON (filterNulls evaluated)
|
||||
);
|
||||
|
||||
exampleJson = ../../docs/komorebi.example.json;
|
||||
schema = ../../schema.json;
|
||||
in
|
||||
pkgs.runCommand "nix-options-validation"
|
||||
{
|
||||
nativeBuildInputs = [
|
||||
pkgs.check-jsonschema
|
||||
pkgs.jq
|
||||
];
|
||||
}
|
||||
''
|
||||
echo "Validating the example, written in Nix, against the schema..."
|
||||
check-jsonschema --schemafile ${schema} ${evaluatedJson}
|
||||
|
||||
echo "Validating the example file of the docs against the schema..."
|
||||
check-jsonschema --schemafile ${schema} ${exampleJson}
|
||||
|
||||
echo "Checking that the two examples say the same..."
|
||||
jq -e --slurpfile example ${exampleJson} 'contains($example[0])' ${evaluatedJson} > /dev/null
|
||||
|
||||
echo "All validations passed!"
|
||||
touch $out
|
||||
''
|
||||
@@ -0,0 +1,55 @@
|
||||
# Nix equivalent of docs/komorebi.example.json
|
||||
# This tests that users can write the exact same config in Nix
|
||||
{ ... }:
|
||||
{
|
||||
komorebi = {
|
||||
app_specific_configuration_path = "$HOME/.config/komorebi/applications.json";
|
||||
cross_monitor_move_behaviour = "Insert";
|
||||
default_workspace_padding = 15;
|
||||
default_container_padding = 15;
|
||||
floating_window_aspect_ratio = "Widescreen";
|
||||
floating_layer_behaviour = "Float";
|
||||
resize_delta = 100;
|
||||
ignore_rules = [ ];
|
||||
floating_applications = [ ];
|
||||
manage_rules = [ ];
|
||||
monitors = [
|
||||
{
|
||||
workspaces = [
|
||||
{
|
||||
name = "I";
|
||||
layout = "BSP";
|
||||
}
|
||||
{
|
||||
name = "II";
|
||||
layout = "VerticalStack";
|
||||
}
|
||||
{
|
||||
name = "III";
|
||||
layout = "HorizontalStack";
|
||||
}
|
||||
{
|
||||
name = "IV";
|
||||
layout = "UltrawideVerticalStack";
|
||||
}
|
||||
{
|
||||
name = "V";
|
||||
layout = "Rows";
|
||||
}
|
||||
{
|
||||
name = "VI";
|
||||
layout = "Grid";
|
||||
}
|
||||
{
|
||||
name = "VII";
|
||||
layout = "RightMainVerticalStack";
|
||||
}
|
||||
{
|
||||
name = "VIII";
|
||||
layout = "Scrolling";
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
+203
-2
@@ -83,6 +83,131 @@ let
|
||||
}
|
||||
);
|
||||
|
||||
withConfig = eval {
|
||||
imports = [ plasma ];
|
||||
services.komorebi = {
|
||||
enable = true;
|
||||
config = {
|
||||
default_workspace_padding = 20;
|
||||
window_container_behaviour = "Append";
|
||||
float_override_placement = "Center";
|
||||
monocle_focus_behaviour = "Cycle";
|
||||
monitors = [
|
||||
{
|
||||
floating_layer_behaviour = "Tile";
|
||||
workspaces = [
|
||||
{
|
||||
name = "web";
|
||||
layout = "BSP";
|
||||
}
|
||||
{
|
||||
name = "code";
|
||||
layout = "Columns";
|
||||
window_container_behaviour_rules."2" = "Append";
|
||||
float_override = false;
|
||||
floating_layer_behaviour = "Float";
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# a layout komorebi does not have
|
||||
badLayout = eval {
|
||||
imports = [ plasma ];
|
||||
services.komorebi = {
|
||||
enable = true;
|
||||
config.monitors = [
|
||||
{
|
||||
workspaces = [
|
||||
{
|
||||
name = "x";
|
||||
layout = "Nope";
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
withSource = eval (
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [ plasma ];
|
||||
services.komorebi = {
|
||||
enable = true;
|
||||
config.resize_delta = 70;
|
||||
applicationSpecificConfiguration.sources = [
|
||||
{
|
||||
owner = "someone";
|
||||
repo = "komorebi-rules";
|
||||
rev = "0123456789abcdef0123456789abcdef01234567";
|
||||
hash = pkgs.lib.fakeHash;
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
# the same source and config, with the sources turned off
|
||||
withSourceOff = eval (
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [ plasma ];
|
||||
services.komorebi = {
|
||||
enable = true;
|
||||
config.resize_delta = 70;
|
||||
applicationSpecificConfiguration = {
|
||||
enable = false;
|
||||
sources = [
|
||||
{
|
||||
owner = "someone";
|
||||
repo = "komorebi-rules";
|
||||
rev = "0123456789abcdef0123456789abcdef01234567";
|
||||
hash = pkgs.lib.fakeHash;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
# the sources turned off, and no config: nothing is left to write
|
||||
onlySourceOff = eval (
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [ plasma ];
|
||||
services.komorebi = {
|
||||
enable = true;
|
||||
applicationSpecificConfiguration = {
|
||||
enable = false;
|
||||
sources = [
|
||||
{
|
||||
owner = "someone";
|
||||
repo = "komorebi-rules";
|
||||
rev = "0123456789abcdef0123456789abcdef01234567";
|
||||
hash = pkgs.lib.fakeHash;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
bothConfigs = eval (
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [ plasma ];
|
||||
services.komorebi = {
|
||||
enable = true;
|
||||
configFile = pkgs.writeText "komorebi.json" "{}";
|
||||
config.resize_delta = 70;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
unknownLevel = eval {
|
||||
imports = [ plasma ];
|
||||
services.komorebi = {
|
||||
@@ -196,11 +321,87 @@ let
|
||||
{
|
||||
name = "configFile reaches execStart and the unit as --config, after the log level";
|
||||
ok =
|
||||
withConfigFile.services.komorebi.execStart
|
||||
== "${withConfigFile.services.komorebi.package}/bin/komorebi --log-level info --config ${withConfigFile.services.komorebi.configFile}"
|
||||
lib.hasPrefix "${withConfigFile.services.komorebi.package}/bin/komorebi --log-level info --config /nix/store/" withConfigFile.services.komorebi.execStart
|
||||
&& (unit withConfigFile).serviceConfig.ExecStart == withConfigFile.services.komorebi.execStart;
|
||||
got = (unit withConfigFile).serviceConfig.ExecStart;
|
||||
}
|
||||
{
|
||||
name = "with neither a configFile nor a config, there is nothing to write";
|
||||
ok = enabled.services.komorebi.internal.configAttrs == null;
|
||||
}
|
||||
{
|
||||
name = "config is the komorebi.json the daemon is given, with no null left in it";
|
||||
ok =
|
||||
withConfig.services.komorebi.internal.configAttrs.default_workspace_padding == 20
|
||||
&&
|
||||
map (w: w.name) (lib.head withConfig.services.komorebi.internal.configAttrs.monitors).workspaces
|
||||
== [
|
||||
"web"
|
||||
"code"
|
||||
]
|
||||
&& !(lib.hasInfix "null" (builtins.toJSON withConfig.services.komorebi.internal.configAttrs));
|
||||
got = withConfig.services.komorebi.internal.configAttrs;
|
||||
}
|
||||
{
|
||||
name = "the keys of the stacks, the float override, monocle and the floating layer are typed options";
|
||||
ok =
|
||||
withConfig.services.komorebi.internal.configAttrs.window_container_behaviour == "Append"
|
||||
&& withConfig.services.komorebi.internal.configAttrs.float_override_placement == "Center"
|
||||
&& withConfig.services.komorebi.internal.configAttrs.monocle_focus_behaviour == "Cycle"
|
||||
&&
|
||||
(lib.head withConfig.services.komorebi.internal.configAttrs.monitors).floating_layer_behaviour
|
||||
== "Tile"
|
||||
&&
|
||||
(lib.elemAt (lib.head withConfig.services.komorebi.internal.configAttrs.monitors).workspaces 1)
|
||||
.window_container_behaviour_rules."2" == "Append";
|
||||
got = withConfig.services.komorebi.internal.configAttrs;
|
||||
}
|
||||
{
|
||||
name = "config reaches execStart as a komorebi.json that was checked against the schema";
|
||||
ok =
|
||||
lib.hasInfix " --config " withConfig.services.komorebi.execStart
|
||||
&& lib.hasSuffix "-validated-komorebi.json" withConfig.services.komorebi.execStart
|
||||
&& (unit withConfig).serviceConfig.ExecStart == withConfig.services.komorebi.execStart;
|
||||
got = withConfig.services.komorebi.execStart;
|
||||
}
|
||||
{
|
||||
name = "configFile is checked against the schema too";
|
||||
ok = lib.hasSuffix "-validated-komorebi.json" withConfigFile.services.komorebi.execStart;
|
||||
got = withConfigFile.services.komorebi.execStart;
|
||||
}
|
||||
{
|
||||
name = "a layout komorebi does not have does not evaluate";
|
||||
ok = !(evaluates (builtins.deepSeq badLayout.services.komorebi.internal.configAttrs true));
|
||||
}
|
||||
{
|
||||
name = "a source of application specific configuration is the daemon's app_specific_configuration_path";
|
||||
ok =
|
||||
lib.hasSuffix "-applications.json" withSource.services.komorebi.internal.configAttrs.app_specific_configuration_path
|
||||
&& withSource.services.komorebi.internal.configAttrs.resize_delta == 70;
|
||||
got = withSource.services.komorebi.internal.configAttrs.app_specific_configuration_path;
|
||||
}
|
||||
{
|
||||
name = "with applicationSpecificConfiguration.enable = false, the sources are left out of config";
|
||||
ok =
|
||||
withSource.services.komorebi.internal.configAttrs ? app_specific_configuration_path
|
||||
&& !(withSourceOff.services.komorebi.internal.configAttrs ? app_specific_configuration_path)
|
||||
&& withSourceOff.services.komorebi.internal.configAttrs.resize_delta == 70;
|
||||
got = withSourceOff.services.komorebi.internal.configAttrs;
|
||||
}
|
||||
{
|
||||
name = "with the sources turned off and no config, there is nothing to write and no --config";
|
||||
ok =
|
||||
onlySourceOff.services.komorebi.internal.configAttrs == null
|
||||
&& !(lib.hasInfix "--config" onlySourceOff.services.komorebi.execStart);
|
||||
got = onlySourceOff.services.komorebi.execStart;
|
||||
}
|
||||
{
|
||||
name = "with a configFile, config is ignored, and the module says so";
|
||||
ok =
|
||||
lib.any (lib.hasInfix "services.komorebi.config is ignored") bothConfigs.warnings
|
||||
&& lib.hasSuffix "-validated-komorebi.json" bothConfigs.services.komorebi.execStart;
|
||||
got = bothConfigs.warnings;
|
||||
}
|
||||
{
|
||||
name = "a log level the daemon does not have does not evaluate";
|
||||
ok = !(evaluates unknownLevel.services.komorebi.logLevel);
|
||||
|
||||
@@ -426,6 +426,10 @@ for args in [
|
||||
"data-directory",
|
||||
"configuration",
|
||||
"generate-static-config",
|
||||
"static-config-schema",
|
||||
"application-specific-configuration-schema",
|
||||
"notification-schema",
|
||||
"socket-schema",
|
||||
"session-float-rules",
|
||||
]:
|
||||
checks.check(f"komorebic {args}", kde.run(f"komorebic {args}")[0] == 0)
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
tier = "fast";
|
||||
kind = "session";
|
||||
timeoutMinutes = 10;
|
||||
# the unit starts the daemon with this file, as `--config`: two named workspaces, and a padding
|
||||
# that moves the tile of the first window away from the edge of the screen
|
||||
node =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
services.komorebi.configFile = pkgs.writeText "komorebi.json" ''
|
||||
{
|
||||
"default_workspace_padding": 40,
|
||||
"default_container_padding": 0,
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{ "name": "web", "layout": "BSP" },
|
||||
{ "name": "code", "layout": "Columns" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
'';
|
||||
};
|
||||
# the module writes this as a komorebi.json, checks it against the schema, and the unit starts
|
||||
# the daemon with it, as `--config`: two named workspaces, and a padding that moves the tile of
|
||||
# the first window away from the edge of the screen
|
||||
node.services.komorebi.config = {
|
||||
default_workspace_padding = 40;
|
||||
default_container_padding = 0;
|
||||
monitors = [
|
||||
{
|
||||
workspaces = [
|
||||
{
|
||||
name = "web";
|
||||
layout = "BSP";
|
||||
}
|
||||
{
|
||||
name = "code";
|
||||
layout = "Columns";
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
# The NixOS module runs komorebi in a real Plasma session: nothing in this test starts the daemon.
|
||||
# What only a real session can tell: that logging in starts the module's user unit through
|
||||
# `plasma-workspace.target`, that the daemon it runs reaches KWin from there and tiles a window
|
||||
# opened in the session, that it runs with the module's log level, its `configFile` and the PATH
|
||||
# of the user's session, that systemd starts it again when it is killed, and that a clean stop is
|
||||
# not a failure. Once the unit is stopped, the daemon is started with a `komorebi.json` of the
|
||||
# user's, which it follows when the file changes.
|
||||
# The options and the unit the module writes are checked without a machine by `nixos-module-eval`
|
||||
# opened in the session, that it runs with the module's log level, the `komorebi.json` the module
|
||||
# wrote from its `config`, and the PATH of the user's session, that systemd starts it again when
|
||||
# it is killed, and that a clean stop is not a failure. Once the unit is stopped, the daemon is
|
||||
# started with a `komorebi.json` of the user's, which it follows when the file changes. The
|
||||
# options and the unit the module writes are checked without a machine by `nixos-module-eval`
|
||||
# (`nix/tests/nixos-module.nix`).
|
||||
import json
|
||||
import shlex
|
||||
@@ -65,7 +65,7 @@ print(f" the unit runs: {' '.join(command)}")
|
||||
checks.check("the unit runs bin/komorebi", command[0].endswith("/bin/komorebi"))
|
||||
checks.check("... at the module's log level", command[1:3] == ["--log-level", "debug"])
|
||||
checks.check(
|
||||
"... with the module's configFile as its --config",
|
||||
"... with the komorebi.json the module wrote as its --config",
|
||||
len(command) == 5 and command[3] == "--config" and command[4].endswith("-komorebi.json"),
|
||||
)
|
||||
environment = kde.sh(f"tr '\\0' '\\n' < /proc/{pid}/environ").splitlines()
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "ApplicationSpecificConfiguration",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/AscApplicationRulesOrSchema"
|
||||
},
|
||||
"$defs": {
|
||||
"ApplicationIdentifier": {
|
||||
"description": "Application identifier",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Executable name",
|
||||
"type": "string",
|
||||
"const": "Exe"
|
||||
},
|
||||
{
|
||||
"description": "Role or subrole",
|
||||
"type": "string",
|
||||
"const": "Class"
|
||||
},
|
||||
{
|
||||
"description": "Window title",
|
||||
"type": "string",
|
||||
"const": "Title"
|
||||
},
|
||||
{
|
||||
"description": "Executable path",
|
||||
"type": "string",
|
||||
"const": "Path"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AscApplicationRules": {
|
||||
"description": "Rules that determine how an application is handled",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"floating": {
|
||||
"description": "Rules to manage specific windows as floating windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/MatchingRule"
|
||||
}
|
||||
},
|
||||
"ignore": {
|
||||
"description": "Rules to ignore specific windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/MatchingRule"
|
||||
}
|
||||
},
|
||||
"manage": {
|
||||
"description": "Rules to forcibly manage specific windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/MatchingRule"
|
||||
}
|
||||
},
|
||||
"tabbed": {
|
||||
"description": "Rules to identify applications which are title-less - only accepts Simple Exe rules",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/MatchingRule"
|
||||
}
|
||||
},
|
||||
"titleless": {
|
||||
"description": "Rules to identify applications which are title-less - only accepts Simple Exe rules",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/MatchingRule"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AscApplicationRulesOrSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/AscApplicationRules"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"IdWithIdentifier": {
|
||||
"description": "Rule for matching applications",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Target identifier",
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"description": "Kind of identifier to target",
|
||||
"$ref": "#/$defs/ApplicationIdentifier"
|
||||
},
|
||||
"matching_strategy": {
|
||||
"description": "Matching strategy to use",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/MatchingStrategy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"MatchingRule": {
|
||||
"description": "Rule for matching applications",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Simple matching rule which must evaluate to true",
|
||||
"$ref": "#/$defs/IdWithIdentifier"
|
||||
},
|
||||
{
|
||||
"description": "Composite matching rule where all conditions must evaluate to true",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/IdWithIdentifier"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"MatchingStrategy": {
|
||||
"description": "Strategy for matching identifiers",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Should not be used, only kept for backward compatibility",
|
||||
"type": "string",
|
||||
"const": "Legacy"
|
||||
},
|
||||
{
|
||||
"description": "Equals",
|
||||
"type": "string",
|
||||
"const": "Equals"
|
||||
},
|
||||
{
|
||||
"description": "Starts With",
|
||||
"type": "string",
|
||||
"const": "StartsWith"
|
||||
},
|
||||
{
|
||||
"description": "Ends With",
|
||||
"type": "string",
|
||||
"const": "EndsWith"
|
||||
},
|
||||
{
|
||||
"description": "Contains",
|
||||
"type": "string",
|
||||
"const": "Contains"
|
||||
},
|
||||
{
|
||||
"description": "Regex",
|
||||
"type": "string",
|
||||
"const": "Regex"
|
||||
},
|
||||
{
|
||||
"description": "Does not end with",
|
||||
"type": "string",
|
||||
"const": "DoesNotEndWith"
|
||||
},
|
||||
{
|
||||
"description": "Does not start with",
|
||||
"type": "string",
|
||||
"const": "DoesNotStartWith"
|
||||
},
|
||||
{
|
||||
"description": "Does not equal",
|
||||
"type": "string",
|
||||
"const": "DoesNotEqual"
|
||||
},
|
||||
{
|
||||
"description": "Does not contain",
|
||||
"type": "string",
|
||||
"const": "DoesNotContain"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+1100
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user