1
0
Fork 0
forked from wry/wry

config: change default config to use toml-based configuration

This commit is contained in:
Julian Orth 2024-03-13 19:30:34 +01:00
parent e24a61bc62
commit 3cebf651c5
58 changed files with 14093 additions and 145 deletions

View file

@ -6,6 +6,8 @@ tasks:
sudo pacman -Syu --noconfirm sudo pacman -Syu --noconfirm
sudo pacman -S --noconfirm rustup libinput pango mesa libxkbcommon xorg-xwayland adwaita-icon-theme libxcursor cmake sudo pacman -S --noconfirm rustup libinput pango mesa libxkbcommon xorg-xwayland adwaita-icon-theme libxcursor cmake
rustup toolchain install stable rustup toolchain install stable
cd jay
git submodule update --init
- test: | - test: |
cd jay cd jay
cargo test cargo test

31
.github/workflows/toml-spec.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: toml-spec
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Show env
run: |
uname -a
ldd --version
- name: Install
run: |
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly
rustup toolchain install nightly --allow-downgrade -c rustfmt
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Check
run: |
cd toml-spec
cargo run
git diff --exit-code

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
.* .*
!.gitignore !.gitignore
!.gitmodules
!/.cargo !/.cargo
!/.builds !/.builds
!/.github !/.github

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "toml-config/toml-test"]
path = toml-config/toml-test
url = https://github.com/mahkoh/toml-tests.git

245
Cargo.lock generated
View file

@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.7" version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"getrandom", "getrandom",
@ -77,9 +77,9 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.5" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
@ -111,9 +111,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.79" version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
@ -174,9 +174,9 @@ checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "1.9.0" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde", "serde",
@ -292,13 +292,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]] [[package]]
name = "default-config" name = "deranged"
version = "0.1.0" version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [ dependencies = [
"chrono", "powerfmt",
"jay-config",
"log",
"rand",
] ]
[[package]] [[package]]
@ -479,12 +478,13 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.2.2" version = "2.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@ -516,7 +516,6 @@ dependencies = [
"chrono", "chrono",
"clap", "clap",
"clap_complete", "clap_complete",
"default-config",
"dirs", "dirs",
"futures-util", "futures-util",
"gpu-alloc", "gpu-alloc",
@ -525,6 +524,7 @@ dependencies = [
"indexmap", "indexmap",
"isnt", "isnt",
"jay-config", "jay-config",
"jay-toml-config",
"libloading 0.8.1", "libloading 0.8.1",
"log", "log",
"num-derive", "num-derive",
@ -557,6 +557,23 @@ dependencies = [
"uapi", "uapi",
] ]
[[package]]
name = "jay-toml-config"
version = "0.1.0"
dependencies = [
"ahash",
"bstr",
"error_reporter",
"indexmap",
"jay-config",
"log",
"phf",
"serde_json",
"simplelog",
"thiserror",
"walkdir",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.67" version = "0.3.67"
@ -627,9 +644,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.20" version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "memchr" name = "memchr"
@ -646,6 +663,12 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-derive" name = "num-derive"
version = "0.4.1" version = "0.4.1"
@ -666,6 +689,15 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@ -710,6 +742,48 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "phf_shared"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher",
]
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.5" version = "1.1.5"
@ -742,6 +816,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -894,6 +974,15 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -902,18 +991,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.196" version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.196" version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -926,11 +1015,25 @@ version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [ dependencies = [
"indexmap",
"itoa", "itoa",
"ryu", "ryu",
"serde", "serde",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "shaderc" name = "shaderc"
version = "0.8.3" version = "0.8.3"
@ -952,6 +1055,23 @@ dependencies = [
"roxmltree", "roxmltree",
] ]
[[package]]
name = "simplelog"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
dependencies = [
"log",
"termcolor",
"time",
]
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -995,6 +1115,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "terminal_size" name = "terminal_size"
version = "0.3.0" version = "0.3.0"
@ -1025,6 +1154,39 @@ dependencies = [
"syn 2.0.48", "syn 2.0.48",
] ]
[[package]]
name = "time"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.0"
@ -1040,6 +1202,18 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml-spec"
version = "0.1.0"
dependencies = [
"anyhow",
"error_reporter",
"indexmap",
"serde",
"serde_json",
"serde_yaml",
]
[[package]] [[package]]
name = "uapi" name = "uapi"
version = "0.2.13" version = "0.2.13"
@ -1072,6 +1246,12 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unsafe-libyaml"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.1" version = "0.2.1"
@ -1084,6 +1264,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -1160,6 +1350,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"

View file

@ -5,7 +5,7 @@ edition = "2021"
build = "build/build.rs" build = "build/build.rs"
[workspace] [workspace]
members = ["jay-config", "default-config", "algorithms"] members = ["jay-config", "toml-config", "algorithms", "toml-spec"]
[profile.release] [profile.release]
panic = "abort" panic = "abort"
@ -30,7 +30,7 @@ smallvec = { version = "1.11.1", features = ["const_generics", "const_new", "uni
byteorder = "1.5.0" byteorder = "1.5.0"
bincode = "1.3.3" bincode = "1.3.3"
jay-config = { path = "jay-config" } jay-config = { path = "jay-config" }
default-config = { path = "default-config" } jay-toml-config = { path = "toml-config" }
algorithms = { path = "algorithms" } algorithms = { path = "algorithms" }
pin-project = "1.1.4" pin-project = "1.1.4"
clap = { version = "4.4.18", features = ["derive", "wrap_help"] } clap = { version = "4.4.18", features = ["derive", "wrap_help"] }

View file

@ -1,13 +0,0 @@
[package]
name = "default-config"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["lib", "cdylib"]
[dependencies]
jay-config = { path = "../jay-config" }
log = "0.4.14"
rand = "0.8.5"
chrono = "0.4.19"

View file

@ -1,106 +0,0 @@
use {
chrono::{format::StrftimeItems, Local},
jay_config::{
config,
exec::Command,
get_workspace,
input::{get_seat, input_devices, on_new_input_device, InputDevice, Seat},
keyboard::{
mods::{Modifiers, ALT, CTRL, SHIFT},
syms::{
SYM_Super_L, SYM_c, SYM_d, SYM_f, SYM_h, SYM_j, SYM_k, SYM_l, SYM_m, SYM_p, SYM_q,
SYM_r, SYM_t, SYM_u, SYM_v, SYM_F1, SYM_F10, SYM_F11, SYM_F12, SYM_F2, SYM_F3,
SYM_F4, SYM_F5, SYM_F6, SYM_F7, SYM_F8, SYM_F9,
},
},
quit, reload,
status::set_status,
switch_to_vt,
timer::{duration_until_wall_clock_is_multiple_of, get_timer},
video::on_graphics_initialized,
Axis::{Horizontal, Vertical},
Direction::{Down, Left, Right, Up},
},
std::time::Duration,
};
const MOD: Modifiers = ALT;
fn configure_seat(s: Seat) {
s.bind(MOD | SYM_h, move || s.focus(Left));
s.bind(MOD | SYM_j, move || s.focus(Down));
s.bind(MOD | SYM_k, move || s.focus(Up));
s.bind(MOD | SYM_l, move || s.focus(Right));
s.bind(MOD | SHIFT | SYM_h, move || s.move_(Left));
s.bind(MOD | SHIFT | SYM_j, move || s.move_(Down));
s.bind(MOD | SHIFT | SYM_k, move || s.move_(Up));
s.bind(MOD | SHIFT | SYM_l, move || s.move_(Right));
s.bind(MOD | SYM_d, move || s.create_split(Horizontal));
s.bind(MOD | SYM_v, move || s.create_split(Vertical));
s.bind(MOD | SYM_t, move || s.toggle_split());
s.bind(MOD | SYM_m, move || s.toggle_mono());
s.bind(MOD | SYM_u, move || s.toggle_fullscreen());
s.bind(MOD | SYM_f, move || s.focus_parent());
s.bind(MOD | SHIFT | SYM_c, move || s.close());
s.bind(MOD | SHIFT | SYM_f, move || s.toggle_floating());
s.bind(SYM_Super_L, || Command::new("alacritty").spawn());
s.bind(MOD | SYM_p, || Command::new("bemenu-run").spawn());
s.bind(MOD | SYM_q, quit);
s.bind(MOD | SHIFT | SYM_r, reload);
let fnkeys = [
SYM_F1, SYM_F2, SYM_F3, SYM_F4, SYM_F5, SYM_F6, SYM_F7, SYM_F8, SYM_F9, SYM_F10, SYM_F11,
SYM_F12,
];
for (i, sym) in fnkeys.into_iter().enumerate() {
s.bind(CTRL | ALT | sym, move || switch_to_vt(i as u32 + 1));
let ws = get_workspace(&format!("{}", i + 1));
s.bind(MOD | sym, move || s.show_workspace(ws));
s.bind(MOD | SHIFT | sym, move || s.set_workspace(ws));
}
}
fn setup_status() {
let time_format: Vec<_> = StrftimeItems::new("%Y-%m-%d %H:%M:%S").collect();
let update_status = move || {
let status = format!("{}", Local::now().format_with_items(time_format.iter()));
set_status(&status);
};
update_status();
let period = Duration::from_secs(5);
let timer = get_timer("status_timer");
timer.repeated(duration_until_wall_clock_is_multiple_of(period), period);
timer.on_tick(update_status);
}
pub fn configure() {
// Configure seats and input devices
let seat = get_seat("default");
configure_seat(seat);
let handle_input_device = move |device: InputDevice| {
device.set_seat(seat);
};
input_devices().into_iter().for_each(handle_input_device);
on_new_input_device(handle_input_device);
// Configure the status message
setup_status();
// Start programs
on_graphics_initialized(|| {
Command::new("mako").spawn();
});
}
config!(configure);

View file

@ -150,7 +150,7 @@ unsafe extern "C" fn default_client_init(
size: usize, size: usize,
) -> *const u8 { ) -> *const u8 {
extern "C" fn configure() { extern "C" fn configure() {
default_config::configure(); jay_toml_config::configure();
} }
jay_config::_private::client::init(srv_data, srv_unref, srv_handler, msg, size, configure) jay_config::_private::client::init(srv_data, srv_unref, srv_handler, msg, size, configure)
} }

22
toml-config/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "jay-toml-config"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["lib", "cdylib"]
[dependencies]
jay-config = { path = "../jay-config" }
log = "0.4.14"
thiserror = "1.0.57"
error_reporter = "1.0.0"
phf = { version = "0.11.2", features = ["macros"] }
indexmap = "2.2.5"
bstr = { version = "1.9.1", default-features = false }
ahash = "0.8.11"
[dev-dependencies]
simplelog = { version = "0.12.2", features = ["test"] }
serde_json = "1.0.114"
walkdir = "2.5.0"

307
toml-config/src/config.rs Normal file
View file

@ -0,0 +1,307 @@
mod context;
pub mod error;
mod extractor;
mod keysyms;
mod parser;
mod parsers;
mod spanned;
mod value;
use {
crate::{
config::{
context::Context,
parsers::config::{ConfigParser, ConfigParserError},
},
toml::{self},
},
jay_config::{
input::acceleration::AccelProfile,
keyboard::{Keymap, ModifiedKeySym},
logging::LogLevel,
status::MessageFormat,
theme::Color,
video::{GfxApi, Transform},
Axis, Direction,
},
std::{
error::Error,
fmt::{Display, Formatter},
time::Duration,
},
thiserror::Error,
toml::toml_parser,
};
#[derive(Debug, Copy, Clone)]
pub enum SimpleCommand {
Close,
DisablePointerConstraint,
Focus(Direction),
FocusParent,
Move(Direction),
None,
Quit,
ReloadConfigSo,
ReloadConfigToml,
Split(Axis),
ToggleFloating,
ToggleFullscreen,
ToggleMono,
ToggleSplit,
}
#[derive(Debug, Clone)]
pub enum Action {
ConfigureConnector { con: ConfigConnector },
ConfigureDirectScanout { enabled: bool },
ConfigureDrmDevice { dev: ConfigDrmDevice },
ConfigureIdle { idle: Duration },
ConfigureInput { input: Input },
ConfigureOutput { out: Output },
Exec { exec: Exec },
Multi { actions: Vec<Action> },
SetEnv { env: Vec<(String, String)> },
SetGfxApi { api: GfxApi },
SetKeymap { map: ConfigKeymap },
SetLogLevel { level: LogLevel },
SetRenderDevice { dev: DrmDeviceMatch },
SetStatus { status: Option<Status> },
SetTheme { theme: Box<Theme> },
ShowWorkspace { name: String },
SimpleCommand { cmd: SimpleCommand },
SwitchToVt { num: u32 },
UnsetEnv { env: Vec<String> },
}
#[derive(Debug, Clone, Default)]
pub struct Theme {
pub attention_requested_bg_color: Option<Color>,
pub bg_color: Option<Color>,
pub bar_bg_color: Option<Color>,
pub bar_status_text_color: Option<Color>,
pub border_color: Option<Color>,
pub captured_focused_title_bg_color: Option<Color>,
pub captured_unfocused_title_bg_color: Option<Color>,
pub focused_inactive_title_bg_color: Option<Color>,
pub focused_inactive_title_text_color: Option<Color>,
pub focused_title_bg_color: Option<Color>,
pub focused_title_text_color: Option<Color>,
pub separator_color: Option<Color>,
pub unfocused_title_bg_color: Option<Color>,
pub unfocused_title_text_color: Option<Color>,
pub border_width: Option<i32>,
pub title_height: Option<i32>,
pub font: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Status {
pub format: MessageFormat,
pub exec: Exec,
pub separator: Option<String>,
}
#[derive(Debug, Clone)]
pub enum OutputMatch {
Any(Vec<OutputMatch>),
All {
name: Option<String>,
connector: Option<String>,
serial_number: Option<String>,
manufacturer: Option<String>,
model: Option<String>,
},
}
#[derive(Debug, Clone)]
pub enum DrmDeviceMatch {
Any(Vec<DrmDeviceMatch>),
All {
name: Option<String>,
syspath: Option<String>,
vendor: Option<u32>,
vendor_name: Option<String>,
model: Option<u32>,
model_name: Option<String>,
devnode: Option<String>,
},
}
#[derive(Debug, Clone)]
pub struct Mode {
pub width: i32,
pub height: i32,
pub refresh_rate: Option<f64>,
}
impl Display for Mode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} x {}", self.width, self.height)?;
if let Some(rr) = self.refresh_rate {
write!(f, " @ {}", rr)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Output {
pub name: Option<String>,
pub match_: OutputMatch,
pub x: Option<i32>,
pub y: Option<i32>,
pub scale: Option<f64>,
pub transform: Option<Transform>,
pub mode: Option<Mode>,
}
#[derive(Debug, Clone)]
pub enum ConnectorMatch {
Any(Vec<ConnectorMatch>),
All { connector: Option<String> },
}
#[derive(Debug, Clone)]
pub enum InputMatch {
Any(Vec<InputMatch>),
All {
tag: Option<String>,
name: Option<String>,
syspath: Option<String>,
devnode: Option<String>,
is_keyboard: Option<bool>,
is_pointer: Option<bool>,
is_touch: Option<bool>,
is_tablet_tool: Option<bool>,
is_tablet_pad: Option<bool>,
is_gesture: Option<bool>,
is_switch: Option<bool>,
},
}
#[derive(Debug, Clone)]
pub struct Input {
pub tag: Option<String>,
pub match_: InputMatch,
pub accel_profile: Option<AccelProfile>,
pub accel_speed: Option<f64>,
pub tap_enabled: Option<bool>,
pub tap_drag_enabled: Option<bool>,
pub tap_drag_lock_enabled: Option<bool>,
pub left_handed: Option<bool>,
pub natural_scrolling: Option<bool>,
pub px_per_wheel_scroll: Option<f64>,
pub transform_matrix: Option<[[f64; 2]; 2]>,
}
#[derive(Debug, Clone)]
pub struct Exec {
pub prog: String,
pub args: Vec<String>,
pub envs: Vec<(String, String)>,
}
#[derive(Debug, Clone)]
pub struct ConfigConnector {
pub match_: ConnectorMatch,
pub enabled: bool,
}
#[derive(Debug, Clone)]
pub struct ConfigDrmDevice {
pub name: Option<String>,
pub match_: DrmDeviceMatch,
pub gfx_api: Option<GfxApi>,
pub direct_scanout_enabled: Option<bool>,
}
#[derive(Debug, Clone)]
pub enum ConfigKeymap {
Named(String),
Literal(Keymap),
Defined { name: String, map: Keymap },
}
#[derive(Debug, Clone)]
pub struct Config {
pub keymap: Option<ConfigKeymap>,
pub shortcuts: Vec<(ModifiedKeySym, Action)>,
pub on_graphics_initialized: Option<Action>,
pub on_idle: Option<Action>,
pub status: Option<Status>,
pub connectors: Vec<ConfigConnector>,
pub outputs: Vec<Output>,
pub workspace_capture: bool,
pub env: Vec<(String, String)>,
pub on_startup: Option<Action>,
pub keymaps: Vec<ConfigKeymap>,
pub log_level: Option<LogLevel>,
pub theme: Theme,
pub gfx_api: Option<GfxApi>,
pub direct_scanout_enabled: Option<bool>,
pub drm_devices: Vec<ConfigDrmDevice>,
pub render_device: Option<DrmDeviceMatch>,
pub inputs: Vec<Input>,
pub idle: Option<Duration>,
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Could not parse the toml document")]
Toml(#[from] toml_parser::ParserError),
#[error("Could not interpret the toml as a config document")]
Parser(#[from] ConfigParserError),
}
pub fn parse_config<F>(input: &[u8], handle_error: F) -> Option<Config>
where
F: FnOnce(&dyn Error),
{
let cx = Context {
input,
used: Default::default(),
};
macro_rules! fatal {
($e:expr) => {{
let e = ConfigError::from($e.value);
let e = cx.error2($e.span, e);
handle_error(&e);
return None;
}};
}
let toml = match toml_parser::parse(input, &cx) {
Ok(t) => t,
Err(e) => fatal!(e),
};
let config = match toml.parse(&mut ConfigParser(&cx)) {
Ok(c) => c,
Err(e) => fatal!(e),
};
let used = cx.used.take();
macro_rules! check_defined {
($name:expr, $used:ident, $defined:ident) => {
for spanned in &used.$used {
if !used.$defined.contains(spanned) {
log::warn!(
"{} {} used but not defined: {}",
$name,
spanned.value,
cx.error3(spanned.span),
);
}
}
};
}
check_defined!("Keymap", keymaps, defined_keymaps);
check_defined!("DRM device", drm_devices, defined_drm_devices);
check_defined!("Output", outputs, defined_outputs);
check_defined!("Input", inputs, defined_inputs);
Some(config)
}
#[test]
fn default_config_parses() {
let input = include_bytes!("default-config.toml");
parse_config(input, |_| ()).unwrap();
}

View file

@ -0,0 +1,65 @@
use {
crate::{
config::error::SpannedError,
toml::{
toml_parser::{ErrorHandler, ParserError},
toml_span::{Span, Spanned},
},
},
ahash::AHashSet,
error_reporter::Report,
std::{cell::RefCell, convert::Infallible, error::Error},
};
pub struct Context<'a> {
pub input: &'a [u8],
pub used: RefCell<Used>,
}
#[derive(Default)]
pub struct Used {
pub outputs: Vec<Spanned<String>>,
pub inputs: Vec<Spanned<String>>,
pub drm_devices: Vec<Spanned<String>>,
pub keymaps: Vec<Spanned<String>>,
pub defined_outputs: AHashSet<Spanned<String>>,
pub defined_inputs: AHashSet<Spanned<String>>,
pub defined_drm_devices: AHashSet<Spanned<String>>,
pub defined_keymaps: AHashSet<Spanned<String>>,
}
impl<'a> Context<'a> {
pub fn error<E: Error>(&self, cause: Spanned<E>) -> SpannedError<'a, E> {
self.error2(cause.span, cause.value)
}
pub fn error2<E: Error>(&self, span: Span, cause: E) -> SpannedError<'a, E> {
SpannedError {
input: self.input.into(),
span,
cause: Some(cause),
}
}
pub fn error3(&self, span: Span) -> SpannedError<'a, Infallible> {
SpannedError {
input: self.input.into(),
span,
cause: None,
}
}
}
impl<'a> ErrorHandler for Context<'a> {
fn handle(&self, err: Spanned<ParserError>) {
log::warn!("{}", Report::new(self.error(err)));
}
fn redefinition(&self, err: Spanned<ParserError>, prev: Span) {
log::warn!("{}", Report::new(self.error(err)));
log::info!(
"Previous definition here: {}",
Report::new(self.error3(prev))
);
}
}

View file

@ -0,0 +1,89 @@
use {
crate::toml::toml_span::Span,
bstr::ByteSlice,
error_reporter::Report,
std::{
borrow::Cow,
error::Error,
fmt::{Display, Formatter},
ops::Deref,
},
};
#[derive(Debug)]
pub struct SpannedError<'a, E> {
pub input: Cow<'a, [u8]>,
pub span: Span,
pub cause: Option<E>,
}
impl<'a, E: Error> Display for SpannedError<'a, E> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let original = self.input.deref();
let span = self.span;
let (line, column) = translate_position(original, span.lo);
let line_num = line + 1;
let col_num = column + 1;
let gutter = line_num.to_string().len();
let content = original
.split(|c| *c == b'\n')
.nth(line)
.expect("valid line number");
if let Some(cause) = &self.cause {
write!(f, "{}: ", Report::new(cause))?;
}
writeln!(f, "At line {}, column {}:", line_num, col_num)?;
for _ in 0..=gutter {
write!(f, " ")?;
}
writeln!(f, "|")?;
write!(f, "{} | ", line_num)?;
writeln!(f, "{}", content.as_bstr())?;
for _ in 0..=gutter {
write!(f, " ")?;
}
write!(f, "|")?;
for _ in 0..=column {
write!(f, " ")?;
}
write!(f, "^")?;
for _ in (span.lo + 1)..(span.hi.min(span.lo + content.len() - column)) {
write!(f, "^")?;
}
Ok(())
}
}
impl<'a, E: Error> Error for SpannedError<'a, E> {}
fn translate_position(input: &[u8], index: usize) -> (usize, usize) {
if input.is_empty() {
return (0, index);
}
let safe_index = index.min(input.len() - 1);
let column_offset = index - safe_index;
let index = safe_index;
let nl = input[0..index]
.iter()
.rev()
.enumerate()
.find(|(_, b)| **b == b'\n')
.map(|(nl, _)| index - nl - 1);
let line_start = match nl {
Some(nl) => nl + 1,
None => 0,
};
let line = input[0..line_start].iter().filter(|b| **b == b'\n').count();
let column = std::str::from_utf8(&input[line_start..=index])
.map(|s| s.chars().count() - 1)
.unwrap_or_else(|_| index - line_start);
let column = column + column_offset;
(line, column)
}

View file

@ -0,0 +1,281 @@
use {
crate::{
config::context::Context,
toml::{
toml_span::{Span, Spanned, SpannedExt},
toml_value::Value,
},
},
ahash::AHashSet,
error_reporter::Report,
indexmap::IndexMap,
thiserror::Error,
};
pub struct Extractor<'v> {
cx: &'v Context<'v>,
table: &'v IndexMap<Spanned<String>, Spanned<Value>>,
used: Vec<&'static str>,
log_unused: bool,
span: Span,
}
impl<'v> Extractor<'v> {
pub fn new(
cx: &'v Context<'v>,
span: Span,
table: &'v IndexMap<Spanned<String>, Spanned<Value>>,
) -> Self {
Self {
cx,
table,
used: Default::default(),
log_unused: true,
span,
}
}
fn get(&mut self, name: &'static str) -> Option<&'v Spanned<Value>> {
let v = self.table.get(name);
if v.is_some() {
self.used.push(name);
}
v
}
pub fn ignore_unused(&mut self) {
self.log_unused = false;
}
pub fn extract<E: Extractable<'v>, U>(&mut self, e: E) -> Result<E::Output, Spanned<U>>
where
ExtractorError: Into<U>,
{
e.extract(self).map_err(|e| e.map(|e| e.into()))
}
pub fn extract_or_ignore<E: Extractable<'v>, U>(
&mut self,
e: E,
) -> Result<E::Output, Spanned<U>>
where
ExtractorError: Into<U>,
{
let res = self.extract(e);
if res.is_err() {
self.ignore_unused();
}
res
}
}
impl<'v> Drop for Extractor<'v> {
fn drop(&mut self) {
if !self.log_unused {
return;
}
if self.used.len() == self.table.len() {
return;
}
let used: AHashSet<_> = self.used.iter().copied().collect();
for key in self.table.keys() {
if !used.contains(key.value.as_str()) {
#[derive(Debug, Error)]
#[error("Ignoring unknown key {0}")]
struct Err<'a>(&'a str);
let err = self.cx.error2(key.span, Err(&key.value));
log::warn!("{}", Report::new(err));
}
}
}
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum ExtractorError {
#[error("Missing field {0}")]
MissingField(&'static str),
#[error("Expected {0} but found {1}")]
Expected(&'static str, &'static str),
#[error("Value must fit in a u32")]
U32,
#[error("Value must fit in a u64")]
U64,
#[error("Value must fit in a i32")]
I32,
}
pub trait Extractable<'v> {
type Output;
fn extract(
self,
extractor: &mut Extractor<'v>,
) -> Result<Self::Output, Spanned<ExtractorError>>;
}
impl<'v, T, F> Extractable<'v> for F
where
F: FnOnce(&mut Extractor<'v>) -> Result<T, Spanned<ExtractorError>>,
{
type Output = T;
fn extract(
self,
extractor: &mut Extractor<'v>,
) -> Result<Self::Output, Spanned<ExtractorError>> {
self(extractor)
}
}
pub fn val(
name: &'static str,
) -> impl for<'v> FnOnce(&mut Extractor<'v>) -> Result<Spanned<&'v Value>, Spanned<ExtractorError>>
{
move |extractor: &mut Extractor| match extractor.get(name) {
None => Err(ExtractorError::MissingField(name).spanned(extractor.span)),
Some(v) => Ok(v.as_ref()),
}
}
macro_rules! ty {
($f:ident, $lt:lifetime, $ty:ident, $ret:ty, $v:ident, $map:expr, $name:expr) => {
#[allow(dead_code)]
pub fn $f(
name: &'static str,
) -> impl for<$lt> FnOnce(&mut Extractor<$lt>) -> Result<Spanned<$ret>, Spanned<ExtractorError>> {
move |extractor: &mut Extractor| {
val(name)(extractor).and_then(|v| match v.value {
Value::$ty($v) => Ok($map.spanned(v.span)),
_ => Err(ExtractorError::Expected($name, v.value.name()).spanned(v.span)),
})
}
}
};
}
ty!(str, 'a, String, &'a str, v, v.as_str(), "a string");
ty!(int, 'a, Integer, i64, v, *v, "an integer");
ty!(flt, 'a, Float, f64, v, *v, "a float");
ty!(bol, 'a, Boolean, bool, v, *v, "a boolean");
ty!(arr, 'a, Array, &'a [Spanned<Value>], v, &**v, "an array");
ty!(tbl, 'a, Table, &'a IndexMap<Spanned<String>, Spanned<Value>>, v, v, "a table");
pub fn fltorint(
name: &'static str,
) -> impl for<'a> FnOnce(&mut Extractor<'a>) -> Result<Spanned<f64>, Spanned<ExtractorError>> {
move |extractor: &mut Extractor| {
val(name)(extractor).and_then(|v| match *v.value {
Value::Float(f) => Ok(f.spanned(v.span)),
Value::Integer(i) => Ok((i as f64).spanned(v.span)),
_ => Err(
ExtractorError::Expected("a float or an integer", v.value.name()).spanned(v.span),
),
})
}
}
macro_rules! int {
($f:ident, $ty:ident, $err:ident) => {
#[allow(dead_code)]
pub fn $f(
name: &'static str,
) -> impl for<'v> FnOnce(&mut Extractor<'v>) -> Result<Spanned<$ty>, Spanned<ExtractorError>> {
move |extractor: &mut Extractor| {
int(name)(extractor).and_then(|v| {
v.value
.try_into()
.map(|n: $ty| n.spanned(v.span))
.map_err(|_| ExtractorError::$err.spanned(v.span))
})
}
}
};
}
int!(n32, u32, U32);
int!(n64, u64, U64);
int!(s32, i32, I32);
pub fn recover<F>(f: F) -> Recover<F> {
Recover(f)
}
pub struct Recover<E>(E);
impl<'v, E> Extractable<'v> for Recover<E>
where
E: Extractable<'v>,
<E as Extractable<'v>>::Output: Default,
{
type Output = <E as Extractable<'v>>::Output;
fn extract(
self,
extractor: &mut Extractor<'v>,
) -> Result<Self::Output, Spanned<ExtractorError>> {
match self.0.extract(extractor) {
Ok(v) => Ok(v),
Err(e) => {
log::warn!("{}", extractor.cx.error(e));
Ok(<E as Extractable<'v>>::Output::default())
}
}
}
}
pub fn opt<F>(f: F) -> Opt<F> {
Opt(f)
}
pub struct Opt<E>(E);
impl<'v, E> Extractable<'v> for Opt<E>
where
E: Extractable<'v>,
{
type Output = Option<<E as Extractable<'v>>::Output>;
fn extract(
self,
extractor: &mut Extractor<'v>,
) -> Result<Self::Output, Spanned<ExtractorError>> {
match self.0.extract(extractor) {
Ok(v) => Ok(Some(v)),
Err(e) if matches!(e.value, ExtractorError::MissingField(_)) => Ok(None),
Err(e) => Err(e),
}
}
}
macro_rules! tuples {
($($idx:tt: $name:ident,)*) => {
impl<'v, $($name,)*> Extractable<'v> for ($($name,)*)
where $($name: Extractable<'v>,)*
{
type Output = ($($name::Output,)*);
#[allow(non_snake_case)]
fn extract(self, extractor: &mut Extractor<'v>) -> Result<Self::Output, Spanned<ExtractorError>> {
$(
let $name = self.$idx.extract(extractor);
)*
Ok((
$(
$name?,
)*
))
}
}
};
}
tuples!(0:T0,);
tuples!(0:T0,1:T1,);
tuples!(0:T0,1:T1,2:T2,);
tuples!(0:T0,1:T1,2:T2,3:T3,);
tuples!(0:T0,1:T1,2:T2,3:T3,4:T4,);
tuples!(0:T0,1:T1,2:T2,3:T3,4:T4,5:T5,);
tuples!(0:T0,1:T1,2:T2,3:T3,4:T4,5:T5,6:T6,);
tuples!(0:T0,1:T1,2:T2,3:T3,4:T4,5:T5,6:T6,7:T7,);
tuples!(0:T0,1:T1,2:T2,3:T3,4:T4,5:T5,6:T6,7:T7,8:T8,);
tuples!(0:T0,1:T1,2:T2,3:T3,4:T4,5:T5,6:T6,7:T7,8:T8,9:T9,);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,118 @@
use {
crate::toml::{
toml_span::{Span, Spanned, SpannedExt},
toml_value::Value,
},
indexmap::IndexMap,
std::{
error::Error,
fmt::{self, Display, Formatter},
},
};
#[derive(Copy, Clone, Debug)]
pub enum DataType {
String,
Integer,
Float,
Boolean,
Array,
Table,
}
impl Display for DataType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let s = match self {
DataType::String => "a string",
DataType::Integer => "an integer",
DataType::Float => "a float",
DataType::Boolean => "a bool",
DataType::Array => "an array",
DataType::Table => "a table",
};
f.write_str(s)
}
}
pub struct DataTypes(&'static [DataType]);
impl Display for DataTypes {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let d = self.0;
match d.len() {
0 => Ok(()),
1 => d[0].fmt(f),
2 => write!(f, "{} or {}", d[0], d[1]),
_ => {
let mut first = true;
#[allow(clippy::needless_range_loop)]
for i in 0..d.len() - 1 {
if !first {
f.write_str(", ")?;
}
first = false;
d[i].fmt(f)?;
}
write!(f, ", or {}", d[d.len() - 1])
}
}
}
}
#[derive(Debug)]
pub struct UnexpectedDataType(&'static [DataType], DataType);
impl Display for UnexpectedDataType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Expected {} but found {}", DataTypes(self.0), self.1)
}
}
impl Error for UnexpectedDataType {}
pub type ParseResult<P> = Result<<P as Parser>::Value, Spanned<<P as Parser>::Error>>;
pub trait Parser {
type Value;
type Error: From<UnexpectedDataType>;
const EXPECTED: &'static [DataType];
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
let _ = string;
expected(self, span, DataType::String)
}
fn parse_integer(&mut self, span: Span, integer: i64) -> ParseResult<Self> {
let _ = integer;
expected(self, span, DataType::Integer)
}
fn parse_float(&mut self, span: Span, float: f64) -> ParseResult<Self> {
let _ = float;
expected(self, span, DataType::Float)
}
fn parse_bool(&mut self, span: Span, bool: bool) -> ParseResult<Self> {
let _ = bool;
expected(self, span, DataType::Boolean)
}
fn parse_array(&mut self, span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let _ = array;
expected(self, span, DataType::Array)
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let _ = table;
expected(self, span, DataType::Table)
}
}
fn expected<P: Parser + ?Sized>(_p: &P, span: Span, actual: DataType) -> ParseResult<P> {
Err(P::Error::from(UnexpectedDataType(P::EXPECTED, actual)).spanned(span))
}

View file

@ -0,0 +1,48 @@
use {
crate::{
config::parser::{DataType, ParseResult, Parser, UnexpectedDataType},
toml::toml_span::Span,
},
thiserror::Error,
};
pub mod action;
mod color;
pub mod config;
mod connector;
mod connector_match;
mod drm_device;
mod drm_device_match;
mod env;
pub mod exec;
mod gfx_api;
mod idle;
mod input;
mod input_match;
pub mod keymap;
mod log_level;
mod mode;
pub mod modified_keysym;
mod output;
mod output_match;
pub mod shortcuts;
mod status;
mod theme;
#[derive(Debug, Error)]
pub enum StringParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
}
pub struct StringParser;
impl Parser for StringParser {
type Value = String;
type Error = StringParserError;
const EXPECTED: &'static [DataType] = &[DataType::String];
fn parse_string(&mut self, _span: Span, string: &str) -> ParseResult<Self> {
Ok(string.to_string())
}
}

View file

@ -0,0 +1,308 @@
use {
crate::{
config::{
context::Context,
extractor::{arr, bol, n32, opt, str, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{
connector::{ConnectorParser, ConnectorParserError},
drm_device::{DrmDeviceParser, DrmDeviceParserError},
drm_device_match::{DrmDeviceMatchParser, DrmDeviceMatchParserError},
env::{EnvParser, EnvParserError},
exec::{ExecParser, ExecParserError},
gfx_api::{GfxApiParser, GfxApiParserError},
idle::{IdleParser, IdleParserError},
input::{InputParser, InputParserError},
keymap::{KeymapParser, KeymapParserError},
log_level::{LogLevelParser, LogLevelParserError},
output::{OutputParser, OutputParserError},
status::{StatusParser, StatusParserError},
theme::{ThemeParser, ThemeParserError},
StringParser, StringParserError,
},
Action,
},
toml::{
toml_span::{Span, Spanned, SpannedExt},
toml_value::Value,
},
},
indexmap::IndexMap,
jay_config::Axis::{Horizontal, Vertical},
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ActionParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
StringParser(#[from] StringParserError),
#[error("Unknown type {0}")]
UnknownType(String),
#[error("Unknown simple action {0}")]
UnknownSimpleAction(String),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error("Could not parse the exec action")]
Exec(#[from] ExecParserError),
#[error("Could not parse the configure-connector action")]
ConfigureConnector(#[from] ConnectorParserError),
#[error("Could not parse the configure-input action")]
ConfigureInput(#[from] InputParserError),
#[error("Could not parse the configure-output action")]
ConfigureOutput(#[from] OutputParserError),
#[error("Could not parse the environment variables")]
Env(#[from] EnvParserError),
#[error("Could not parse a set-keymap action")]
SetKeymap(#[from] KeymapParserError),
#[error("Could not parse a set-status action")]
Status(#[from] StatusParserError),
#[error("Could not parse a set-theme action")]
Theme(#[from] ThemeParserError),
#[error("Could not parse a set-log-level action")]
SetLogLevel(#[from] LogLevelParserError),
#[error("Could not parse a set-gfx-api action")]
GfxApi(#[from] GfxApiParserError),
#[error("Could not parse a configure-drm-device action")]
DrmDevice(#[from] DrmDeviceParserError),
#[error("Could not parse a set-render-device action")]
SetRenderDevice(#[from] DrmDeviceMatchParserError),
#[error("Could not parse a configure-idle action")]
ConfigureIdle(#[from] IdleParserError),
}
pub struct ActionParser<'a>(pub &'a Context<'a>);
impl ActionParser<'_> {
fn parse_simple_cmd(&self, span: Span, string: &str) -> ParseResult<Self> {
use {crate::config::SimpleCommand::*, jay_config::Direction::*};
let cmd = match string {
"focus-left" => Focus(Left),
"focus-down" => Focus(Down),
"focus-up" => Focus(Up),
"focus-right" => Focus(Right),
"move-left" => Move(Left),
"move-down" => Move(Down),
"move-up" => Move(Up),
"move-right" => Move(Right),
"split-horizontal" => Split(Horizontal),
"split-vertical" => Split(Vertical),
"toggle-split" => ToggleSplit,
"toggle-mono" => ToggleMono,
"toggle-fullscreen" => ToggleFullscreen,
"focus-parent" => FocusParent,
"close" => Close,
"disable-pointer-constraint" => DisablePointerConstraint,
"toggle-floating" => ToggleFloating,
"quit" => Quit,
"reload-config-toml" => ReloadConfigToml,
"reload-config-so" => ReloadConfigSo,
"none" => None,
_ => {
return Err(ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span))
}
};
Ok(Action::SimpleCommand { cmd })
}
fn parse_multi(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut actions = vec![];
for el in array {
actions.push(el.parse(self)?);
}
Ok(Action::Multi { actions })
}
fn parse_exec(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let exec = ext
.extract(val("exec"))?
.parse_map(&mut ExecParser(self.0))?;
Ok(Action::Exec { exec })
}
fn parse_switch_to_vt(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let num = ext.extract(n32("num"))?.value;
Ok(Action::SwitchToVt { num })
}
fn parse_show_workspace(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let name = ext.extract(str("name"))?.value.to_string();
Ok(Action::ShowWorkspace { name })
}
fn parse_move_to_workspace(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let name = ext.extract(str("name"))?.value.to_string();
Ok(Action::ShowWorkspace { name })
}
fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let con = ext
.extract(val("connector"))?
.parse_map(&mut ConnectorParser(self.0))?;
Ok(Action::ConfigureConnector { con })
}
fn parse_configure_input(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let input = ext.extract(val("input"))?.parse_map(&mut InputParser {
cx: self.0,
tag_ok: false,
})?;
Ok(Action::ConfigureInput { input })
}
fn parse_configure_idle(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let idle = ext
.extract(val("idle"))?
.parse_map(&mut IdleParser(self.0))?;
Ok(Action::ConfigureIdle { idle })
}
fn parse_configure_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let out = ext.extract(val("output"))?.parse_map(&mut OutputParser {
cx: self.0,
name_ok: false,
})?;
Ok(Action::ConfigureOutput { out })
}
fn parse_set_env(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let env = ext.extract(val("env"))?.parse_map(&mut EnvParser)?;
Ok(Action::SetEnv { env })
}
fn parse_unset_env(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
struct P;
impl Parser for P {
type Value = Vec<String>;
type Error = ActionParserError;
const EXPECTED: &'static [DataType] = &[DataType::Array, DataType::String];
fn parse_string(&mut self, _span: Span, string: &str) -> ParseResult<Self> {
Ok(vec![string.to_string()])
}
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for v in array {
res.push(v.parse_map(&mut StringParser)?);
}
Ok(res)
}
}
let env = ext.extract(val("env"))?.parse_map(&mut P)?;
Ok(Action::UnsetEnv { env })
}
fn parse_set_keymap(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let map = ext.extract(val("map"))?.parse_map(&mut KeymapParser {
cx: self.0,
definition: false,
})?;
Ok(Action::SetKeymap { map })
}
fn parse_set_status(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let status = match ext.extract(opt(val("status")))? {
None => None,
Some(v) => Some(v.parse_map(&mut StatusParser(self.0))?),
};
Ok(Action::SetStatus { status })
}
fn parse_set_theme(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let theme = ext
.extract(val("theme"))?
.parse_map(&mut ThemeParser(self.0))?;
Ok(Action::SetTheme {
theme: Box::new(theme),
})
}
fn parse_set_log_level(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let level = ext.extract(val("level"))?.parse_map(&mut LogLevelParser)?;
Ok(Action::SetLogLevel { level })
}
fn parse_set_gfx_api(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let api = ext.extract(val("api"))?.parse_map(&mut GfxApiParser)?;
Ok(Action::SetGfxApi { api })
}
fn parse_set_render_device(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let dev = ext
.extract(val("dev"))?
.parse_map(&mut DrmDeviceMatchParser(self.0))?;
Ok(Action::SetRenderDevice { dev })
}
fn parse_configure_direct_scanout(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let enabled = ext.extract(bol("enabled"))?.value;
Ok(Action::ConfigureDirectScanout { enabled })
}
fn parse_configure_drm_device(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let dev = ext.extract(val("dev"))?.parse_map(&mut DrmDeviceParser {
cx: self.0,
name_ok: false,
})?;
Ok(Action::ConfigureDrmDevice { dev })
}
}
impl<'a> Parser for ActionParser<'a> {
type Value = Action;
type Error = ActionParserError;
const EXPECTED: &'static [DataType] = &[DataType::String, DataType::Array, DataType::Table];
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
self.parse_simple_cmd(span, string)
}
fn parse_array(&mut self, span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
self.parse_multi(span, array)
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let ty = ext.extract_or_ignore(str("type"))?;
let res = match ty.value {
"simple" => {
let cmd = ext.extract(str("cmd"))?;
self.parse_simple_cmd(cmd.span, cmd.value)
}
"multi" => {
let actions = ext.extract(arr("actions"))?;
self.parse_multi(actions.span, actions.value)
}
"exec" => self.parse_exec(&mut ext),
"switch-to-vt" => self.parse_switch_to_vt(&mut ext),
"show-workspace" => self.parse_show_workspace(&mut ext),
"move-to-workspace" => self.parse_move_to_workspace(&mut ext),
"configure-connector" => self.parse_configure_connector(&mut ext),
"configure-input" => self.parse_configure_input(&mut ext),
"configure-output" => self.parse_configure_output(&mut ext),
"set-env" => self.parse_set_env(&mut ext),
"unset-env" => self.parse_unset_env(&mut ext),
"set-keymap" => self.parse_set_keymap(&mut ext),
"set-status" => self.parse_set_status(&mut ext),
"set-theme" => self.parse_set_theme(&mut ext),
"set-log-level" => self.parse_set_log_level(&mut ext),
"set-gfx-api" => self.parse_set_gfx_api(&mut ext),
"configure-direct-scanout" => self.parse_configure_direct_scanout(&mut ext),
"configure-drm-device" => self.parse_configure_drm_device(&mut ext),
"set-render-device" => self.parse_set_render_device(&mut ext),
"configure-idle" => self.parse_configure_idle(&mut ext),
v => {
ext.ignore_unused();
return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span));
}
};
drop(ext);
res
}
}

View file

@ -0,0 +1,58 @@
use {
crate::{
config::{
context::Context,
extractor::ExtractorError,
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
},
toml::toml_span::{Span, SpannedExt},
},
jay_config::theme::Color,
std::{num::ParseIntError, ops::Range},
thiserror::Error,
};
pub struct ColorParser<'a>(pub &'a Context<'a>);
#[derive(Debug, Error)]
pub enum ColorParserError {
#[error(transparent)]
DataType(#[from] UnexpectedDataType),
#[error(transparent)]
Extractor(#[from] ExtractorError),
#[error("Color must start with `#`")]
Prefix,
#[error("String must have length 4, 5, 6, or 9")]
Length,
#[error(transparent)]
ParseIntError(#[from] ParseIntError),
}
impl Parser for ColorParser<'_> {
type Value = Color;
type Error = ColorParserError;
const EXPECTED: &'static [DataType] = &[DataType::String];
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
let hex = match string.strip_prefix("#") {
Some(s) => s,
_ => return Err(ColorParserError::Prefix.spanned(span)),
};
let d = |range: Range<usize>| {
u8::from_str_radix(&hex[range], 16)
.map_err(|e| ColorParserError::ParseIntError(e).spanned(span))
};
let s = |range: Range<usize>| {
let v = d(range)?;
Ok((v << 4) | v)
};
let (r, g, b, a) = match hex.len() {
3 => (s(0..1)?, s(1..2)?, s(2..3)?, u8::MAX),
4 => (s(0..1)?, s(1..2)?, s(2..3)?, s(3..4)?),
6 => (d(0..2)?, d(2..4)?, d(4..6)?, u8::MAX),
8 => (d(0..2)?, d(2..4)?, d(4..6)?, d(4..8)?),
_ => return Err(ColorParserError::Length.spanned(span)),
};
Ok(Color::new_straight(r, g, b, a))
}
}

View file

@ -0,0 +1,279 @@
use {
crate::{
config::{
context::Context,
extractor::{arr, bol, opt, recover, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{
action::ActionParser,
connector::ConnectorsParser,
drm_device::DrmDevicesParser,
drm_device_match::DrmDeviceMatchParser,
env::EnvParser,
gfx_api::GfxApiParser,
idle::IdleParser,
input::InputsParser,
keymap::KeymapParser,
log_level::LogLevelParser,
output::OutputsParser,
shortcuts::{ShortcutsParser, ShortcutsParserError},
status::StatusParser,
theme::ThemeParser,
},
spanned::SpannedErrorExt,
Action, Config, Theme,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ConfigParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extractor(#[from] ExtractorError),
#[error("Could not parse the shortcuts")]
ParseShortcuts(#[source] ShortcutsParserError),
}
pub struct ConfigParser<'a>(pub &'a Context<'a>);
impl ConfigParser<'_> {
fn parse_action(&self, name: &str, action: Option<Spanned<&Value>>) -> Option<Action> {
match action {
None => None,
Some(value) => match value.parse(&mut ActionParser(self.0)) {
Ok(v) => Some(v),
Err(e) => {
log::warn!("Could not parse the {name} action: {}", self.0.error(e));
None
}
},
}
}
}
impl Parser for ConfigParser<'_> {
type Value = Config;
type Error = ConfigParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (
(
keymap_val,
shortcuts_val,
on_graphics_init_val,
status_val,
outputs_val,
connectors_val,
workspace_capture,
env_val,
on_startup_val,
keymaps_val,
),
(
log_level_val,
theme_val,
gfx_api_val,
drm_devices_val,
direct_scanout,
render_device_val,
inputs_val,
on_idle_val,
_,
idle_val,
),
) = ext.extract((
(
opt(val("keymap")),
opt(val("shortcuts")),
opt(val("on-graphics-initialized")),
opt(val("status")),
opt(val("outputs")),
opt(val("connectors")),
recover(opt(bol("workspace-capture"))),
opt(val("env")),
opt(val("on-startup")),
recover(opt(arr("keymaps"))),
),
(
opt(val("log-level")),
opt(val("theme")),
opt(val("gfx-api")),
opt(val("drm-devices")),
recover(opt(bol("direct-scanout"))),
opt(val("render-device")),
opt(val("inputs")),
opt(val("on-idle")),
opt(val("$schema")),
opt(val("idle")),
),
))?;
let mut keymap = None;
if let Some(value) = keymap_val {
match value.parse(&mut KeymapParser {
cx: self.0,
definition: false,
}) {
Ok(m) => keymap = Some(m),
Err(e) => {
log::warn!("Could not parse the keymap: {}", self.0.error(e));
}
}
}
let mut shortcuts = vec![];
if let Some(value) = shortcuts_val {
shortcuts = value
.parse(&mut ShortcutsParser(self.0))
.map_spanned_err(ConfigParserError::ParseShortcuts)?;
}
if shortcuts.is_empty() {
log::warn!("Config defines no shortcuts");
}
let on_graphics_initialized =
self.parse_action("on-graphics-initialized", on_graphics_init_val);
let on_idle = self.parse_action("on-idle", on_idle_val);
let on_startup = self.parse_action("on-startup", on_startup_val);
let mut status = None;
if let Some(value) = status_val {
match value.parse(&mut StatusParser(self.0)) {
Ok(v) => status = Some(v),
Err(e) => log::warn!("Could not parse the status config: {}", self.0.error(e)),
}
}
let mut outputs = vec![];
if let Some(value) = outputs_val {
match value.parse(&mut OutputsParser(self.0)) {
Ok(v) => outputs = v,
Err(e) => log::warn!("Could not parse the outputs: {}", self.0.error(e)),
}
}
let mut connectors = vec![];
if let Some(value) = connectors_val {
match value.parse(&mut ConnectorsParser(self.0)) {
Ok(v) => connectors = v,
Err(e) => log::warn!("Could not parse the connectors: {}", self.0.error(e)),
}
}
let mut env = vec![];
if let Some(value) = env_val {
match value.parse(&mut EnvParser) {
Ok(v) => env = v,
Err(e) => log::warn!(
"Could not parse the environment variables: {}",
self.0.error(e)
),
}
}
let mut keymaps = vec![];
if let Some(value) = keymaps_val {
for value in value.value {
match value.parse(&mut KeymapParser {
cx: self.0,
definition: true,
}) {
Ok(m) => keymaps.push(m),
Err(e) => {
log::warn!("Could not parse a keymap: {}", self.0.error(e));
}
}
}
}
let mut log_level = None;
if let Some(value) = log_level_val {
match value.parse(&mut LogLevelParser) {
Ok(v) => log_level = Some(v),
Err(e) => {
log::warn!("Could not parse the log level: {}", self.0.error(e));
}
}
}
let mut theme = Theme::default();
if let Some(value) = theme_val {
match value.parse(&mut ThemeParser(self.0)) {
Ok(v) => theme = v,
Err(e) => {
log::warn!("Could not parse the theme: {}", self.0.error(e));
}
}
}
let mut gfx_api = None;
if let Some(value) = gfx_api_val {
match value.parse(&mut GfxApiParser) {
Ok(v) => gfx_api = Some(v),
Err(e) => {
log::warn!("Could not parse the graphics API: {}", self.0.error(e));
}
}
}
let mut drm_devices = vec![];
if let Some(value) = drm_devices_val {
match value.parse(&mut DrmDevicesParser(self.0)) {
Ok(v) => drm_devices = v,
Err(e) => {
log::warn!("Could not parse the drm devices: {}", self.0.error(e));
}
}
}
let mut render_device = None;
if let Some(value) = render_device_val {
match value.parse(&mut DrmDeviceMatchParser(self.0)) {
Ok(v) => render_device = Some(v),
Err(e) => {
log::warn!("Could not parse the render device: {}", self.0.error(e));
}
}
}
let mut inputs = vec![];
if let Some(value) = inputs_val {
match value.parse(&mut InputsParser(self.0)) {
Ok(v) => inputs = v,
Err(e) => {
log::warn!("Could not parse the inputs: {}", self.0.error(e));
}
}
}
let mut idle = None;
if let Some(value) = idle_val {
match value.parse(&mut IdleParser(self.0)) {
Ok(v) => idle = Some(v),
Err(e) => {
log::warn!("Could not parse the idle timeout: {}", self.0.error(e));
}
}
}
Ok(Config {
keymap,
shortcuts,
on_graphics_initialized,
on_idle,
status,
outputs,
connectors,
workspace_capture: workspace_capture.despan().unwrap_or(true),
env,
on_startup,
keymaps,
log_level,
theme,
gfx_api,
drm_devices,
direct_scanout_enabled: direct_scanout.despan(),
render_device,
inputs,
idle,
})
}
}

View file

@ -0,0 +1,83 @@
use {
crate::{
config::{
context::Context,
extractor::{bol, opt, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::connector_match::{ConnectorMatchParser, ConnectorMatchParserError},
ConfigConnector,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ConnectorParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error(transparent)]
Match(#[from] ConnectorMatchParserError),
}
pub struct ConnectorParser<'a>(pub &'a Context<'a>);
impl<'a> Parser for ConnectorParser<'a> {
type Value = ConfigConnector;
type Error = ConnectorParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (match_val, enabled) = ext.extract((val("match"), opt(bol("enabled"))))?;
Ok(ConfigConnector {
match_: match_val.parse_map(&mut ConnectorMatchParser(self.0))?,
enabled: enabled.despan().unwrap_or(true),
})
}
}
pub struct ConnectorsParser<'a>(pub &'a Context<'a>);
impl<'a> Parser for ConnectorsParser<'a> {
type Value = Vec<ConfigConnector>;
type Error = ConnectorParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(&mut ConnectorParser(self.0)) {
Ok(o) => res.push(o),
Err(e) => {
log::warn!("Could not parse connector: {}", self.0.error(e));
}
}
}
Ok(res)
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
log::warn!(
"`connectors` value should be an array: {}",
self.0.error3(span)
);
ConnectorParser(self.0)
.parse_table(span, table)
.map(|v| vec![v])
}
}

View file

@ -0,0 +1,57 @@
use {
crate::{
config::{
context::Context,
extractor::{opt, str, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
ConnectorMatch,
},
toml::{
toml_span::{Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ConnectorMatchParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
}
pub struct ConnectorMatchParser<'a>(pub &'a Context<'a>);
impl<'a> Parser for ConnectorMatchParser<'a> {
type Value = ConnectorMatch;
type Error = ConnectorMatchParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(self) {
Ok(m) => res.push(m),
Err(e) => {
log::error!("Could not parse match rule: {}", self.0.error(e));
}
}
}
Ok(ConnectorMatch::Any(res))
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (connector,) = ext.extract((opt(str("name")),))?;
Ok(ConnectorMatch::All {
connector: connector.map(|v| v.value.to_owned()),
})
}
}

View file

@ -0,0 +1,126 @@
use {
crate::{
config::{
context::Context,
extractor::{bol, opt, recover, str, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{
drm_device_match::{DrmDeviceMatchParser, DrmDeviceMatchParserError},
gfx_api::GfxApiParser,
},
ConfigDrmDevice,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum DrmDeviceParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error(transparent)]
Match(#[from] DrmDeviceMatchParserError),
}
pub struct DrmDeviceParser<'a> {
pub cx: &'a Context<'a>,
pub name_ok: bool,
}
impl<'a> Parser for DrmDeviceParser<'a> {
type Value = ConfigDrmDevice;
type Error = DrmDeviceParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.cx, span, table);
let (name, match_val, direct_scanout_enabled, gfx_api_val) = ext.extract((
opt(str("name")),
val("match"),
recover(opt(bol("direct-scanout"))),
opt(val("gfx-api")),
))?;
let gfx_api = match gfx_api_val {
Some(api) => match api.parse(&mut GfxApiParser) {
Ok(m) => Some(m),
Err(e) => {
log::warn!("Could not parse graphics API: {}", self.cx.error(e));
None
}
},
None => None,
};
if let Some(name) = name {
if self.name_ok {
self.cx
.used
.borrow_mut()
.defined_drm_devices
.insert(name.into());
} else {
log::warn!(
"DRM device names have no effect in this position (did you mean match.name?): {}",
self.cx.error3(name.span)
);
}
}
Ok(ConfigDrmDevice {
name: name.despan().map(|v| v.to_string()),
match_: match_val.parse_map(&mut DrmDeviceMatchParser(self.cx))?,
direct_scanout_enabled: direct_scanout_enabled.despan(),
gfx_api,
})
}
}
pub struct DrmDevicesParser<'a>(pub &'a Context<'a>);
impl<'a> Parser for DrmDevicesParser<'a> {
type Value = Vec<ConfigDrmDevice>;
type Error = DrmDeviceParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(&mut DrmDeviceParser {
cx: self.0,
name_ok: true,
}) {
Ok(o) => res.push(o),
Err(e) => {
log::warn!("Could not parse drm device: {}", self.0.error(e));
}
}
}
Ok(res)
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
log::warn!(
"`drm-devices` value should be an array: {}",
self.0.error3(span)
);
DrmDeviceParser {
cx: self.0,
name_ok: true,
}
.parse_table(span, table)
.map(|v| vec![v])
}
}

View file

@ -0,0 +1,74 @@
use {
crate::{
config::{
context::Context,
extractor::{n32, opt, recover, str, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
DrmDeviceMatch,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum DrmDeviceMatchParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
}
pub struct DrmDeviceMatchParser<'a>(pub &'a Context<'a>);
impl Parser for DrmDeviceMatchParser<'_> {
type Value = DrmDeviceMatch;
type Error = DrmDeviceMatchParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Table];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(self) {
Ok(m) => res.push(m),
Err(e) => {
log::error!("Could not parse match rule: {}", self.0.error(e));
}
}
}
Ok(DrmDeviceMatch::Any(res))
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (name, syspath, vendor, vendor_name, model, model_name, devnode) = ext.extract((
recover(opt(str("name"))),
recover(opt(str("syspath"))),
recover(opt(n32("pci-vendor"))),
recover(opt(str("vendor"))),
recover(opt(n32("pci-model"))),
recover(opt(str("model"))),
recover(opt(str("devnode"))),
))?;
if let Some(name) = name {
self.0.used.borrow_mut().drm_devices.push(name.into());
}
Ok(DrmDeviceMatch::All {
name: name.despan_into(),
syspath: syspath.despan_into(),
vendor: vendor.despan(),
vendor_name: vendor_name.despan_into(),
model: model.despan(),
model_name: model_name.despan_into(),
devnode: devnode.despan_into(),
})
}
}

View file

@ -0,0 +1,42 @@
use {
crate::{
config::{
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{StringParser, StringParserError},
},
toml::{
toml_span::{Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum EnvParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
String(#[from] StringParserError),
}
pub struct EnvParser;
impl Parser for EnvParser {
type Value = Vec<(String, String)>;
type Error = EnvParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
_span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut envs = vec![];
for (k, v) in table {
envs.push((k.value.to_string(), v.parse_map(&mut StringParser)?));
}
Ok(envs)
}
}

View file

@ -0,0 +1,91 @@
use {
crate::{
config::{
context::Context,
extractor::{arr, opt, str, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{
env::{EnvParser, EnvParserError},
StringParser, StringParserError,
},
Exec,
},
toml::{
toml_span::{Span, Spanned, SpannedExt},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ExecParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extractor(#[from] ExtractorError),
#[error(transparent)]
String(#[from] StringParserError),
#[error(transparent)]
Env(#[from] EnvParserError),
#[error("Array cannot be empty")]
Empty,
}
pub struct ExecParser<'a>(pub &'a Context<'a>);
impl Parser for ExecParser<'_> {
type Value = Exec;
type Error = ExecParserError;
const EXPECTED: &'static [DataType] = &[DataType::String, DataType::Array, DataType::Table];
fn parse_string(&mut self, _span: Span, string: &str) -> ParseResult<Self> {
Ok(Exec {
prog: string.to_string(),
args: vec![],
envs: vec![],
})
}
fn parse_array(&mut self, span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
if array.is_empty() {
return Err(ExecParserError::Empty.spanned(span));
}
let prog = array[0].parse_map(&mut StringParser)?;
let mut args = vec![];
for v in &array[1..] {
args.push(v.parse_map(&mut StringParser)?);
}
Ok(Exec {
prog,
args,
envs: vec![],
})
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (prog, args_val, envs_val) =
ext.extract((str("prog"), opt(arr("args")), opt(val("env"))))?;
let mut args = vec![];
if let Some(args_val) = args_val {
for arg in args_val.value {
args.push(arg.parse_map(&mut StringParser)?);
}
}
let envs = match envs_val {
None => vec![],
Some(e) => e.parse_map(&mut EnvParser)?,
};
Ok(Exec {
prog: prog.value.to_string(),
args,
envs,
})
}
}

View file

@ -0,0 +1,34 @@
use {
crate::{
config::parser::{DataType, ParseResult, Parser, UnexpectedDataType},
toml::toml_span::{Span, SpannedExt},
},
jay_config::video::GfxApi,
thiserror::Error,
};
pub struct GfxApiParser;
#[derive(Debug, Error)]
pub enum GfxApiParserError {
#[error(transparent)]
DataType(#[from] UnexpectedDataType),
#[error("Unknown API {0}")]
Unknown(String),
}
impl Parser for GfxApiParser {
type Value = GfxApi;
type Error = GfxApiParserError;
const EXPECTED: &'static [DataType] = &[DataType::String];
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
use GfxApi::*;
let api = match string.to_ascii_lowercase().as_str() {
"opengl" => OpenGl,
"vulkan" => Vulkan,
_ => return Err(GfxApiParserError::Unknown(string.to_string()).spanned(span)),
};
Ok(api)
}
}

View file

@ -0,0 +1,45 @@
use {
crate::{
config::{
context::Context,
extractor::{n64, opt, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
std::time::Duration,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum IdleParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
}
pub struct IdleParser<'a>(pub &'a Context<'a>);
impl Parser for IdleParser<'_> {
type Value = Duration;
type Error = IdleParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (minutes, seconds) = ext.extract((opt(n64("minutes")), opt(n64("seconds"))))?;
let idle = Duration::from_secs(
minutes.despan().unwrap_or_default() * 60 + seconds.despan().unwrap_or_default(),
);
Ok(idle)
}
}

View file

@ -0,0 +1,205 @@
use {
crate::{
config::{
context::Context,
extractor::{bol, fltorint, opt, recover, str, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::input_match::{InputMatchParser, InputMatchParserError},
Input,
},
toml::{
toml_span::{DespanExt, Span, Spanned, SpannedExt},
toml_value::Value,
},
},
indexmap::IndexMap,
jay_config::input::acceleration::{ACCEL_PROFILE_ADAPTIVE, ACCEL_PROFILE_FLAT},
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum InputParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error(transparent)]
Match(#[from] InputMatchParserError),
#[error("Transform matrix must have exactly two rows")]
TwoRows,
#[error("Transform matrix must have exactly two columns")]
TwoColumns,
#[error("Transform matrix entries must be floats")]
Float,
}
pub struct InputParser<'a> {
pub cx: &'a Context<'a>,
pub tag_ok: bool,
}
impl<'a> Parser for InputParser<'a> {
type Value = Input;
type Error = InputParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.cx, span, table);
let (
(
tag,
match_val,
accel_profile,
accel_speed,
tap_enabled,
tap_drag_enabled,
tap_drag_lock_enabled,
left_handed,
natural_scrolling,
px_per_wheel_scroll,
),
(transform_matrix,),
) = ext.extract((
(
opt(str("tag")),
val("match"),
recover(opt(str("accel-profile"))),
recover(opt(fltorint("accel-speed"))),
recover(opt(bol("tap-enabled"))),
recover(opt(bol("tap-drag-enabled"))),
recover(opt(bol("tap-drag-lock-enabled"))),
recover(opt(bol("left-handed"))),
recover(opt(bol("natural-scrolling"))),
recover(opt(fltorint("px-per-wheel-scroll"))),
),
(recover(opt(val("transform-matrix"))),),
))?;
let accel_profile = match accel_profile {
None => None,
Some(p) => match p.value.to_ascii_lowercase().as_str() {
"flat" => Some(ACCEL_PROFILE_FLAT),
"adaptive" => Some(ACCEL_PROFILE_ADAPTIVE),
v => {
log::warn!("Unknown accel-profile {v}: {}", self.cx.error3(p.span));
None
}
},
};
let transform_matrix = match transform_matrix {
None => None,
Some(matrix) => match matrix.parse(&mut TransformMatrixParser) {
Ok(v) => Some(v),
Err(e) => {
log::warn!("Could not parse transform matrix: {}", self.cx.error(e));
None
}
},
};
if let Some(tag) = tag {
if self.tag_ok {
self.cx.used.borrow_mut().defined_inputs.insert(tag.into());
} else {
log::warn!(
"Input tags have no effect in this position (did you mean match.tag?): {}",
self.cx.error3(tag.span)
);
}
}
Ok(Input {
tag: tag.despan_into(),
match_: match_val.parse_map(&mut InputMatchParser(self.cx))?,
accel_profile,
accel_speed: accel_speed.despan(),
tap_enabled: tap_enabled.despan(),
tap_drag_enabled: tap_drag_enabled.despan(),
tap_drag_lock_enabled: tap_drag_lock_enabled.despan(),
left_handed: left_handed.despan(),
natural_scrolling: natural_scrolling.despan(),
px_per_wheel_scroll: px_per_wheel_scroll.despan(),
transform_matrix,
})
}
}
pub struct InputsParser<'a>(pub &'a Context<'a>);
impl<'a> Parser for InputsParser<'a> {
type Value = Vec<Input>;
type Error = InputParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(&mut InputParser {
cx: self.0,
tag_ok: true,
}) {
Ok(o) => res.push(o),
Err(e) => {
log::warn!("Could not parse output: {}", self.0.error(e));
}
}
}
Ok(res)
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
log::warn!(
"`outputs` value should be an array: {}",
self.0.error3(span)
);
InputParser {
cx: self.0,
tag_ok: true,
}
.parse_table(span, table)
.map(|v| vec![v])
}
}
struct TransformMatrixParser;
impl Parser for TransformMatrixParser {
type Value = [[f64; 2]; 2];
type Error = InputParserError;
const EXPECTED: &'static [DataType] = &[DataType::Array];
fn parse_array(&mut self, span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
if array.len() != 2 {
return Err(InputParserError::TwoRows.spanned(span));
}
Ok([
array[0].parse(&mut TransformMatrixRowParser)?,
array[1].parse(&mut TransformMatrixRowParser)?,
])
}
}
struct TransformMatrixRowParser;
impl Parser for TransformMatrixRowParser {
type Value = [f64; 2];
type Error = InputParserError;
const EXPECTED: &'static [DataType] = &[DataType::Array];
fn parse_array(&mut self, span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
if array.len() != 2 {
return Err(InputParserError::TwoColumns.spanned(span));
}
let extract = |v: &Spanned<Value>| match v.value {
Value::Float(f) => Ok(f),
Value::Integer(f) => Ok(f as _),
_ => Err(InputParserError::Float.spanned(v.span)),
};
Ok([extract(&array[0])?, extract(&array[1])?])
}
}

View file

@ -0,0 +1,98 @@
use {
crate::{
config::{
context::Context,
extractor::{bol, opt, str, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
InputMatch,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum InputMatchParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
}
pub struct InputMatchParser<'a>(pub &'a Context<'a>);
impl<'a> Parser for InputMatchParser<'a> {
type Value = InputMatch;
type Error = InputMatchParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(self) {
Ok(m) => res.push(m),
Err(e) => {
log::error!("Could not parse match rule: {}", self.0.error(e));
}
}
}
Ok(InputMatch::Any(res))
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (
(
tag,
name,
syspath,
devnode,
is_keyboard,
is_pointer,
is_touch,
is_tablet_tool,
is_tablet_pad,
is_gesture,
),
(is_switch,),
) = ext.extract((
(
opt(str("tag")),
opt(str("name")),
opt(str("syspath")),
opt(str("devnode")),
opt(bol("is-keyboard")),
opt(bol("is-pointer")),
opt(bol("is-touch")),
opt(bol("is-tablet-tool")),
opt(bol("is-tablet-pad")),
opt(bol("is-gesture")),
),
(opt(bol("is-switch")),),
))?;
if let Some(tag) = tag {
self.0.used.borrow_mut().inputs.push(tag.into());
}
Ok(InputMatch::All {
tag: tag.despan_into(),
name: name.despan_into(),
syspath: syspath.despan_into(),
devnode: devnode.despan_into(),
is_keyboard: is_keyboard.despan(),
is_pointer: is_pointer.despan(),
is_touch: is_touch.despan(),
is_tablet_tool: is_tablet_tool.despan(),
is_tablet_pad: is_tablet_pad.despan(),
is_gesture: is_gesture.despan(),
is_switch: is_switch.despan(),
})
}
}

View file

@ -0,0 +1,123 @@
use {
crate::{
config::{
context::Context,
extractor::{opt, str, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
ConfigKeymap,
},
toml::{
toml_span::{Span, Spanned, SpannedExt},
toml_value::Value,
},
},
indexmap::IndexMap,
jay_config::{
config_dir,
keyboard::{parse_keymap, Keymap},
},
std::{io, path::PathBuf},
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum KeymapParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extractor(#[from] ExtractorError),
#[error("The keymap is invalid")]
Invalid,
#[error("Keymap table must contain at least one of `name`, `map`")]
MissingField,
#[error("Keymap must have both `name` and `map` fields in this context")]
DefinitionRequired,
#[error("Could not read {0}")]
ReadFile(String, #[source] io::Error),
}
pub struct KeymapParser<'a> {
pub cx: &'a Context<'a>,
pub definition: bool,
}
impl Parser for KeymapParser<'_> {
type Value = ConfigKeymap;
type Error = KeymapParserError;
const EXPECTED: &'static [DataType] = &[DataType::String, DataType::Table];
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
Ok(ConfigKeymap::Literal(parse(span, string)?))
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.cx, span, table);
let (mut name_val, mut map_val, mut path) =
ext.extract((opt(str("name")), opt(str("map")), opt(str("path"))))?;
if map_val.is_some() && path.is_some() {
log::warn!(
"Both `name` and `path` are specified. Ignoring `path`: {}",
self.cx.error3(span)
);
path = None;
}
let file_content;
if let Some(path) = path {
let mut root = PathBuf::from(config_dir());
root.push(path.value);
file_content = match std::fs::read_to_string(&root) {
Ok(c) => c,
Err(e) => {
return Err(KeymapParserError::ReadFile(root.display().to_string(), e)
.spanned(path.span))
}
};
map_val = Some(file_content.as_str().spanned(path.span));
}
if self.definition && (name_val.is_none() || map_val.is_none()) {
return Err(KeymapParserError::DefinitionRequired.spanned(span));
}
if !self.definition && map_val.is_some() {
if let Some(val) = name_val {
log::warn!(
"Cannot use both `name` and `map` in this position. Ignoring `name`: {}",
self.cx.error3(val.span)
);
}
name_val = None;
}
if let Some(name) = name_val {
if self.definition {
self.cx
.used
.borrow_mut()
.defined_keymaps
.insert(name.into());
} else {
self.cx.used.borrow_mut().keymaps.push(name.into());
}
}
let res = match (name_val, map_val) {
(Some(name_val), Some(map_val)) => ConfigKeymap::Defined {
name: name_val.value.to_string(),
map: parse(map_val.span, map_val.value)?,
},
(Some(name_val), None) => ConfigKeymap::Named(name_val.value.to_string()),
(None, Some(map_val)) => ConfigKeymap::Literal(parse(map_val.span, map_val.value)?),
(None, None) => return Err(KeymapParserError::MissingField.spanned(span)),
};
Ok(res)
}
}
fn parse(span: Span, string: &str) -> Result<Keymap, Spanned<KeymapParserError>> {
let map = parse_keymap(string);
match map.is_valid() {
true => Ok(map),
false => Err(KeymapParserError::Invalid.spanned(span)),
}
}

View file

@ -0,0 +1,37 @@
use {
crate::{
config::parser::{DataType, ParseResult, Parser, UnexpectedDataType},
toml::toml_span::{Span, SpannedExt},
},
jay_config::logging::LogLevel,
thiserror::Error,
};
pub struct LogLevelParser;
#[derive(Debug, Error)]
pub enum LogLevelParserError {
#[error(transparent)]
DataType(#[from] UnexpectedDataType),
#[error("Unknown log level {0}")]
Unknown(String),
}
impl Parser for LogLevelParser {
type Value = LogLevel;
type Error = LogLevelParserError;
const EXPECTED: &'static [DataType] = &[DataType::String];
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
use LogLevel::*;
let level = match string.to_ascii_lowercase().as_str() {
"error" => Error,
"warn" | "warning" => Warn,
"info" => Info,
"debug" => Debug,
"trace" => Trace,
_ => return Err(LogLevelParserError::Unknown(string.to_string()).spanned(span)),
};
Ok(level)
}
}

View file

@ -0,0 +1,47 @@
use {
crate::{
config::{
context::Context,
extractor::{flt, opt, s32, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
Mode,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ModeParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
}
pub struct ModeParser<'a>(pub &'a Context<'a>);
impl<'a> Parser for ModeParser<'a> {
type Value = Mode;
type Error = ModeParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (width, height, refresh_rate) =
ext.extract((s32("width"), s32("height"), opt(flt("refresh-rate"))))?;
Ok(Mode {
width: width.value,
height: height.value,
refresh_rate: refresh_rate.despan(),
})
}
}

View file

@ -0,0 +1,71 @@
use {
crate::{
config::{
keysyms::KEYSYMS,
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
},
toml::toml_span::{Span, SpannedExt},
},
jay_config::keyboard::{
mods::{Modifiers, ALT, CAPS, CTRL, LOCK, LOGO, MOD1, MOD2, MOD3, MOD4, MOD5, NUM, SHIFT},
ModifiedKeySym,
},
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ModifiedKeysymParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error("You cannot use more than one non-modifier key")]
MoreThanOneSym,
#[error("You must specify exactly one non-modifier key")]
MissingSym,
#[error("Unknown keysym {0}")]
UnknownKeysym(String),
}
pub struct ModifiedKeysymParser;
impl Parser for ModifiedKeysymParser {
type Value = ModifiedKeySym;
type Error = ModifiedKeysymParserError;
const EXPECTED: &'static [DataType] = &[DataType::String];
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
let mut modifiers = Modifiers(0);
let mut sym = None;
for part in string.split("-") {
let modifier = match part {
"shift" => SHIFT,
"lock" => LOCK,
"ctrl" => CTRL,
"mod1" => MOD1,
"mod2" => MOD2,
"mod3" => MOD3,
"mod4" => MOD4,
"mod5" => MOD5,
"caps" => CAPS,
"alt" => ALT,
"num" => NUM,
"logo" => LOGO,
_ => match KEYSYMS.get(part) {
Some(new) if sym.is_none() => {
sym = Some(*new);
continue;
}
Some(_) => return Err(ModifiedKeysymParserError::MoreThanOneSym.spanned(span)),
_ => {
return Err(ModifiedKeysymParserError::UnknownKeysym(part.to_string())
.spanned(span))
}
},
};
modifiers |= modifier;
}
match sym {
Some(s) => Ok(modifiers | s),
None => Err(ModifiedKeysymParserError::MissingSym.spanned(span)),
}
}
}

View file

@ -0,0 +1,150 @@
use {
crate::{
config::{
context::Context,
extractor::{fltorint, opt, recover, s32, str, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{
mode::ModeParser,
output_match::{OutputMatchParser, OutputMatchParserError},
},
Output,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
jay_config::video::Transform,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum OutputParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error(transparent)]
Match(#[from] OutputMatchParserError),
}
pub struct OutputParser<'a> {
pub cx: &'a Context<'a>,
pub name_ok: bool,
}
impl<'a> Parser for OutputParser<'a> {
type Value = Output;
type Error = OutputParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.cx, span, table);
let (name, match_val, x, y, scale, transform, mode) = ext.extract((
opt(str("name")),
val("match"),
recover(opt(s32("x"))),
recover(opt(s32("y"))),
recover(opt(fltorint("scale"))),
recover(opt(str("transform"))),
opt(val("mode")),
))?;
let transform = match transform {
None => None,
Some(t) => match t.value {
"none" => Some(Transform::None),
"rotate-90" => Some(Transform::Rotate90),
"rotate-180" => Some(Transform::Rotate180),
"rotate-270" => Some(Transform::Rotate270),
"flip" => Some(Transform::Flip),
"flip-rotate-90" => Some(Transform::FlipRotate90),
"flip-rotate-180" => Some(Transform::FlipRotate180),
"flip-rotate-270" => Some(Transform::FlipRotate270),
_ => {
log::warn!("Unknown transform {}: {}", t.value, self.cx.error3(t.span));
None
}
},
};
let mode = match mode {
Some(mode) => match mode.parse(&mut ModeParser(self.cx)) {
Ok(m) => Some(m),
Err(e) => {
log::warn!("Could not parse mode: {}", self.cx.error(e));
None
}
},
None => None,
};
if let Some(name) = name {
if self.name_ok {
self.cx
.used
.borrow_mut()
.defined_outputs
.insert(name.into());
} else {
log::warn!(
"Output names have no effect in this position (did you mean match.name?): {}",
self.cx.error3(name.span)
);
}
}
Ok(Output {
name: name.despan().map(|v| v.to_string()),
match_: match_val.parse_map(&mut OutputMatchParser(self.cx))?,
x: x.despan(),
y: y.despan(),
scale: scale.despan(),
transform,
mode,
})
}
}
pub struct OutputsParser<'a>(pub &'a Context<'a>);
impl<'a> Parser for OutputsParser<'a> {
type Value = Vec<Output>;
type Error = OutputParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(&mut OutputParser {
cx: self.0,
name_ok: true,
}) {
Ok(o) => res.push(o),
Err(e) => {
log::warn!("Could not parse output: {}", self.0.error(e));
}
}
}
Ok(res)
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
log::warn!(
"`outputs` value should be an array: {}",
self.0.error3(span)
);
OutputParser {
cx: self.0,
name_ok: true,
}
.parse_table(span, table)
.map(|v| vec![v])
}
}

View file

@ -0,0 +1,70 @@
use {
crate::{
config::{
context::Context,
extractor::{opt, str, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
OutputMatch,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum OutputMatchParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
}
pub struct OutputMatchParser<'a>(pub &'a Context<'a>);
impl<'a> Parser for OutputMatchParser<'a> {
type Value = OutputMatch;
type Error = OutputMatchParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Table];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(self) {
Ok(m) => res.push(m),
Err(e) => {
log::error!("Could not parse match rule: {}", self.0.error(e));
}
}
}
Ok(OutputMatch::Any(res))
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (name, connector, serial_number, manufacturer, model) = ext.extract((
opt(str("name")),
opt(str("connector")),
opt(str("serial-number")),
opt(str("manufacturer")),
opt(str("model")),
))?;
if let Some(name) = name {
self.0.used.borrow_mut().outputs.push(name.into());
}
Ok(OutputMatch::All {
name: name.despan_into(),
connector: connector.despan_into(),
serial_number: serial_number.despan_into(),
manufacturer: manufacturer.despan_into(),
model: model.despan_into(),
})
}
}

View file

@ -0,0 +1,72 @@
use {
crate::{
config::{
context::Context,
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{action::ActionParser, modified_keysym::ModifiedKeysymParser},
Action,
},
toml::{
toml_span::{Span, Spanned, SpannedExt},
toml_value::Value,
},
},
indexmap::IndexMap,
jay_config::keyboard::ModifiedKeySym,
std::collections::HashSet,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ShortcutsParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
}
pub struct ShortcutsParser<'a>(pub &'a Context<'a>);
impl Parser for ShortcutsParser<'_> {
type Value = Vec<(ModifiedKeySym, Action)>;
type Error = ShortcutsParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
_span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut used_keys = HashSet::<Spanned<ModifiedKeySym>>::new();
let mut res = vec![];
for (key, value) in table.iter() {
let keysym = match ModifiedKeysymParser.parse_string(key.span, &key.value) {
Ok(k) => k,
Err(e) => {
log::warn!("Could not parse keysym: {}", self.0.error(e));
continue;
}
};
let action = match value.parse(&mut ActionParser(self.0)) {
Ok(a) => a,
Err(e) => {
log::warn!(
"Could not parse action for keysym {}: {}",
key.value,
self.0.error(e)
);
continue;
}
};
let spanned = keysym.spanned(key.span);
if let Some(prev) = used_keys.get(&spanned) {
log::warn!(
"Duplicate key overrides previous definition: {}",
self.0.error3(spanned.span)
);
log::info!("Previous definition here: {}", self.0.error3(prev.span));
}
used_keys.insert(spanned);
res.push((keysym, action));
}
Ok(res)
}
}

View file

@ -0,0 +1,81 @@
use {
crate::{
config::{
context::Context,
extractor::{opt, recover, str, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::exec::{ExecParser, ExecParserError},
Status,
},
toml::{
toml_span::{Span, Spanned, SpannedExt},
toml_value::Value,
},
},
indexmap::IndexMap,
jay_config::status::MessageFormat,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum StatusParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Exec(#[from] ExecParserError),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error("Expected `plain`, `pango`, or `i3bar` but found {0}")]
UnknownFormat(String),
}
pub struct StatusParser<'a>(pub &'a Context<'a>);
impl Parser for StatusParser<'_> {
type Value = Status;
type Error = StatusParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (format, exec, separator) = ext.extract((
opt(str("format")),
val("exec"),
recover(opt(str("i3bar-separator"))),
))?;
let format = match format {
Some(f) => match f.value {
"plain" => MessageFormat::Plain,
"pango" => MessageFormat::Pango,
"i3bar" => MessageFormat::I3Bar,
_ => {
return Err(
StatusParserError::UnknownFormat(f.value.to_string()).spanned(f.span)
)
}
},
_ => MessageFormat::Plain,
};
let exec = exec.parse_map(&mut ExecParser(self.0))?;
let separator = match separator {
None => None,
Some(sep) if format == MessageFormat::I3Bar => Some(sep.value.to_string()),
Some(sep) => {
log::warn!(
"Separator has no effect for format {format:?}: {}",
self.0.error3(sep.span)
);
None
}
};
Ok(Status {
format,
exec,
separator,
})
}
}

View file

@ -0,0 +1,119 @@
use {
crate::{
config::{
context::Context,
extractor::{opt, recover, s32, str, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::color::ColorParser,
Theme,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
pub struct ThemeParser<'a>(pub &'a Context<'a>);
#[derive(Debug, Error)]
pub enum ThemeParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extractor(#[from] ExtractorError),
}
impl Parser for ThemeParser<'_> {
type Value = Theme;
type Error = ThemeParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (
(
attention_requested_bg_color,
bg_color,
bar_bg_color,
bar_status_text_color,
border_color,
captured_focused_title_bg_color,
captured_unfocused_title_bg_color,
focused_inactive_title_bg_color,
focused_inactive_title_text_color,
focused_title_bg_color,
),
(
focused_title_text_color,
separator_color,
unfocused_title_bg_color,
unfocused_title_text_color,
border_width,
title_height,
font,
),
) = ext.extract((
(
opt(val("attention-requested-bg-color")),
opt(val("bg-color")),
opt(val("bar-bg-color")),
opt(val("bar-status-text-color")),
opt(val("border-color")),
opt(val("captured-focused-title-bg-color")),
opt(val("captured-unfocused-title-bg-color")),
opt(val("focused-inactive-title-bg-color")),
opt(val("focused-inactive-title-text-color")),
opt(val("focused-title-bg-color")),
),
(
opt(val("focused-title-text-color")),
opt(val("separator-color")),
opt(val("unfocused-title-bg-color")),
opt(val("unfocused-title-text-color")),
recover(opt(s32("border-width"))),
recover(opt(s32("title-height"))),
recover(opt(str("font"))),
),
))?;
macro_rules! color {
($e:expr) => {
match $e {
None => None,
Some(v) => match v.parse(&mut ColorParser(self.0)) {
Ok(v) => Some(v),
Err(e) => {
log::warn!("Could not parse a color: {}", self.0.error(e));
None
}
},
}
};
}
Ok(Theme {
attention_requested_bg_color: color!(attention_requested_bg_color),
bg_color: color!(bg_color),
bar_bg_color: color!(bar_bg_color),
bar_status_text_color: color!(bar_status_text_color),
border_color: color!(border_color),
captured_focused_title_bg_color: color!(captured_focused_title_bg_color),
captured_unfocused_title_bg_color: color!(captured_unfocused_title_bg_color),
focused_inactive_title_bg_color: color!(focused_inactive_title_bg_color),
focused_inactive_title_text_color: color!(focused_inactive_title_text_color),
focused_title_bg_color: color!(focused_title_bg_color),
focused_title_text_color: color!(focused_title_text_color),
separator_color: color!(separator_color),
unfocused_title_bg_color: color!(unfocused_title_bg_color),
unfocused_title_text_color: color!(unfocused_title_text_color),
border_width: border_width.despan(),
title_height: title_height.despan(),
font: font.map(|f| f.value.to_string()),
})
}
}

View file

@ -0,0 +1,63 @@
use crate::{
config::parser::{ParseResult, Parser},
toml::{toml_span::Spanned, toml_value::Value},
};
impl Spanned<&Value> {
pub fn parse<P: Parser>(&self, parser: &mut P) -> ParseResult<P> {
self.value.parse(self.span, parser)
}
pub fn parse_map<P: Parser, E>(
&self,
parser: &mut P,
) -> Result<<P as Parser>::Value, Spanned<E>>
where
<P as Parser>::Error: Into<E>,
{
self.parse(parser).map_spanned_err(|e| e.into())
}
}
impl Spanned<Value> {
pub fn parse<P: Parser>(&self, parser: &mut P) -> ParseResult<P> {
self.as_ref().parse(parser)
}
pub fn parse_map<P: Parser, E>(
&self,
parser: &mut P,
) -> Result<<P as Parser>::Value, Spanned<E>>
where
<P as Parser>::Error: Into<E>,
{
self.as_ref().parse_map(parser)
}
}
pub trait SpannedErrorExt {
type T;
type E;
fn map_spanned_err<U, F>(self, f: F) -> Result<Self::T, Spanned<U>>
where
F: FnOnce(Self::E) -> U;
}
impl<T, E> SpannedErrorExt for Result<T, Spanned<E>> {
type T = T;
type E = E;
fn map_spanned_err<U, F>(self, f: F) -> Result<Self::T, Spanned<U>>
where
F: FnOnce(Self::E) -> U,
{
match self {
Ok(v) => Ok(v),
Err(e) => Err(Spanned {
span: e.span,
value: f(e.value),
}),
}
}
}

View file

@ -0,0 +1,17 @@
use crate::{
config::parser::{ParseResult, Parser},
toml::{toml_span::Span, toml_value::Value},
};
impl Value {
pub fn parse<P: Parser>(&self, span: Span, parser: &mut P) -> ParseResult<P> {
match self {
Value::String(a) => parser.parse_string(span, a),
Value::Integer(a) => parser.parse_integer(span, *a),
Value::Float(a) => parser.parse_float(span, *a),
Value::Boolean(a) => parser.parse_bool(span, *a),
Value::Array(a) => parser.parse_array(span, a),
Value::Table(a) => parser.parse_table(span, a),
}
}
}

View file

@ -0,0 +1,75 @@
keymap = """
xkb_keymap {
xkb_keycodes { include "evdev+aliases(qwerty)" };
xkb_types { include "complete" };
xkb_compat { include "complete" };
xkb_symbols { include "pc+us+inet(evdev)" };
};
"""
on-graphics-initialized = { type = "exec", exec = "mako" }
[shortcuts]
alt-h = "focus-left"
alt-j = "focus-down"
alt-k = "focus-up"
alt-l = "focus-right"
alt-shift-h = "move-left"
alt-shift-j = "move-down"
alt-shift-k = "move-up"
alt-shift-l = "move-right"
alt-d = "split-horizontal"
alt-v = "split-vertical"
alt-t = "toggle-split"
alt-m = "toggle-mono"
alt-u = "toggle-fullscreen"
alt-f = "focus-parent"
alt-shift-c = "close"
alt-shift-f = "toggle-floating"
Super_L = { type = "exec", exec = "alacritty" }
alt-p = { type = "exec", exec = "bemenu-run" }
alt-q = "quit"
alt-shift-r = "reload-config-toml"
ctrl-alt-F1 = { type = "switch-to-vt", num = 1 }
ctrl-alt-F2 = { type = "switch-to-vt", num = 2 }
ctrl-alt-F3 = { type = "switch-to-vt", num = 3 }
ctrl-alt-F4 = { type = "switch-to-vt", num = 4 }
ctrl-alt-F5 = { type = "switch-to-vt", num = 5 }
ctrl-alt-F6 = { type = "switch-to-vt", num = 6 }
ctrl-alt-F7 = { type = "switch-to-vt", num = 7 }
ctrl-alt-F8 = { type = "switch-to-vt", num = 8 }
ctrl-alt-F9 = { type = "switch-to-vt", num = 9 }
ctrl-alt-F10 = { type = "switch-to-vt", num = 10 }
ctrl-alt-F11 = { type = "switch-to-vt", num = 11 }
ctrl-alt-F12 = { type = "switch-to-vt", num = 12 }
alt-F1 = { type = "show-workspace", name = "1" }
alt-F2 = { type = "show-workspace", name = "2" }
alt-F3 = { type = "show-workspace", name = "3" }
alt-F4 = { type = "show-workspace", name = "4" }
alt-F5 = { type = "show-workspace", name = "5" }
alt-F6 = { type = "show-workspace", name = "6" }
alt-F7 = { type = "show-workspace", name = "7" }
alt-F8 = { type = "show-workspace", name = "8" }
alt-F9 = { type = "show-workspace", name = "9" }
alt-F10 = { type = "show-workspace", name = "10" }
alt-F11 = { type = "show-workspace", name = "11" }
alt-F12 = { type = "show-workspace", name = "12" }
alt-shift-F1 = { type = "move-to-workspace", name = "1" }
alt-shift-F2 = { type = "move-to-workspace", name = "2" }
alt-shift-F3 = { type = "move-to-workspace", name = "3" }
alt-shift-F4 = { type = "move-to-workspace", name = "4" }
alt-shift-F5 = { type = "move-to-workspace", name = "5" }
alt-shift-F6 = { type = "move-to-workspace", name = "6" }
alt-shift-F7 = { type = "move-to-workspace", name = "7" }
alt-shift-F8 = { type = "move-to-workspace", name = "8" }
alt-shift-F9 = { type = "move-to-workspace", name = "9" }
alt-shift-F10 = { type = "move-to-workspace", name = "10" }
alt-shift-F11 = { type = "move-to-workspace", name = "11" }
alt-shift-F12 = { type = "move-to-workspace", name = "12" }

819
toml-config/src/lib.rs Normal file
View file

@ -0,0 +1,819 @@
#![allow(clippy::len_zero, clippy::single_char_pattern, clippy::collapsible_if)]
mod config;
mod toml;
use {
crate::config::{
parse_config, Action, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap,
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch,
SimpleCommand, Status, Theme,
},
ahash::{AHashMap, AHashSet},
error_reporter::Report,
jay_config::{
config, config_dir,
exec::{set_env, unset_env, Command},
get_workspace,
input::{get_seat, input_devices, on_new_input_device, InputDevice, Seat},
is_reload,
keyboard::{Keymap, ModifiedKeySym},
logging::set_log_level,
on_devices_enumerated, on_idle, quit, reload, set_default_workspace_capture, set_idle,
status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
switch_to_vt,
theme::{reset_colors, reset_font, reset_sizes, set_font},
video::{
connectors, drm_devices, on_connector_connected, on_graphics_initialized,
on_new_connector, on_new_drm_device, set_direct_scanout_enabled, set_gfx_api,
Connector, DrmDevice,
},
},
std::{cell::RefCell, io::ErrorKind, path::PathBuf, rc::Rc},
};
fn default_seat() -> Seat {
get_seat("default")
}
impl Action {
fn into_fn(self, state: &Rc<State>) -> Box<dyn FnMut()> {
let s = state.persistent.seat;
match self {
Action::SimpleCommand { cmd } => match cmd {
SimpleCommand::Focus(dir) => Box::new(move || s.focus(dir)),
SimpleCommand::Move(dir) => Box::new(move || s.move_(dir)),
SimpleCommand::Split(axis) => Box::new(move || s.create_split(axis)),
SimpleCommand::ToggleSplit => Box::new(move || s.toggle_split()),
SimpleCommand::ToggleMono => Box::new(move || s.toggle_mono()),
SimpleCommand::ToggleFullscreen => Box::new(move || s.toggle_fullscreen()),
SimpleCommand::FocusParent => Box::new(move || s.focus_parent()),
SimpleCommand::Close => Box::new(move || s.close()),
SimpleCommand::DisablePointerConstraint => {
Box::new(move || s.disable_pointer_constraint())
}
SimpleCommand::ToggleFloating => Box::new(move || s.toggle_floating()),
SimpleCommand::Quit => Box::new(quit),
SimpleCommand::ReloadConfigToml => {
let persistent = state.persistent.clone();
Box::new(move || load_config(false, &persistent))
}
SimpleCommand::ReloadConfigSo => Box::new(reload),
SimpleCommand::None => Box::new(|| ()),
},
Action::Multi { actions } => {
let mut actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
Box::new(move || {
for action in &mut actions {
action();
}
})
}
Action::Exec { exec } => Box::new(move || create_command(&exec).spawn()),
Action::SwitchToVt { num } => Box::new(move || switch_to_vt(num)),
Action::ShowWorkspace { name } => {
let workspace = get_workspace(&name);
Box::new(move || s.show_workspace(workspace))
}
Action::ConfigureConnector { con } => Box::new(move || {
for c in connectors() {
if con.match_.matches(c) {
con.apply(c);
}
}
}),
Action::ConfigureInput { input } => {
let state = state.clone();
Box::new(move || {
for c in input_devices() {
if input.match_.matches(c, &state) {
input.apply(c);
}
}
})
}
Action::ConfigureOutput { out } => {
let state = state.clone();
Box::new(move || {
for c in connectors() {
if out.match_.matches(c, &state) {
out.apply(c);
}
}
})
}
Action::SetEnv { env } => Box::new(move || {
for (k, v) in &env {
set_env(k, v);
}
}),
Action::UnsetEnv { env } => Box::new(move || {
for k in &env {
unset_env(k);
}
}),
Action::SetKeymap { map } => {
let state = state.clone();
Box::new(move || state.set_keymap(&map))
}
Action::SetStatus { status } => {
let state = state.clone();
Box::new(move || state.set_status(&status))
}
Action::SetTheme { theme } => {
let state = state.clone();
Box::new(move || state.apply_theme(&theme))
}
Action::SetLogLevel { level } => Box::new(move || set_log_level(level)),
Action::SetGfxApi { api } => Box::new(move || set_gfx_api(api)),
Action::ConfigureDirectScanout { enabled } => {
Box::new(move || set_direct_scanout_enabled(enabled))
}
Action::ConfigureDrmDevice { dev } => {
let state = state.clone();
Box::new(move || {
for d in drm_devices() {
if dev.match_.matches(d, &state) {
dev.apply(d);
}
}
})
}
Action::SetRenderDevice { dev } => {
let state = state.clone();
Box::new(move || {
for d in drm_devices() {
if dev.matches(d, &state) {
d.make_render_device();
}
}
})
}
Action::ConfigureIdle { idle } => Box::new(move || set_idle(Some(idle))),
}
}
}
fn apply_recursive_match<'a, U>(
type_name: &str,
list: &'a AHashMap<String, U>,
active: &mut AHashSet<&'a str>,
name: &'a str,
matches: impl FnOnce(&'a U, &mut AHashSet<&'a str>) -> bool,
) -> bool {
match list.get(name) {
None => {
log::warn!("{type_name} with name {name} does not exist");
false
}
Some(m) => {
if active.insert(name) {
let matches = matches(m, active);
active.remove(name);
matches
} else {
log::warn!("Recursion while evaluating match for {type_name} {name}");
false
}
}
}
}
impl ConfigDrmDevice {
fn apply(&self, d: DrmDevice) {
if let Some(api) = self.gfx_api {
d.set_gfx_api(api);
}
if let Some(dse) = self.direct_scanout_enabled {
d.set_direct_scanout_enabled(dse);
}
}
}
impl DrmDeviceMatch {
fn matches(&self, d: DrmDevice, state: &State) -> bool {
self.matches_(d, state, &mut AHashSet::new())
}
fn matches_<'a>(
&'a self,
d: DrmDevice,
state: &'a State,
active: &mut AHashSet<&'a str>,
) -> bool {
match self {
DrmDeviceMatch::Any(m) => m.iter().any(|m| m.matches_(d, state, active)),
DrmDeviceMatch::All {
name,
syspath,
vendor,
vendor_name,
model,
model_name,
devnode,
} => {
if let Some(name) = name {
let matches = apply_recursive_match(
"drm device",
&state.drm_devices,
active,
name,
|m, active| m.matches_(d, state, active),
);
if !matches {
return false;
}
}
if let Some(syspath) = syspath {
if d.syspath() != *syspath {
return false;
}
}
if let Some(devnode) = devnode {
if d.devnode() != *devnode {
return false;
}
}
if let Some(model) = model_name {
if d.model() != *model {
return false;
}
}
if let Some(vendor) = vendor_name {
if d.vendor() != *vendor {
return false;
}
}
if let Some(vendor) = vendor {
if d.pci_id().vendor != *vendor {
return false;
}
}
if let Some(model) = model {
if d.pci_id().model != *model {
return false;
}
}
true
}
}
}
}
impl InputMatch {
fn matches(&self, d: InputDevice, state: &State) -> bool {
self.matches_(d, state, &mut AHashSet::new())
}
fn matches_<'a>(
&'a self,
d: InputDevice,
state: &'a State,
active: &mut AHashSet<&'a str>,
) -> bool {
match self {
InputMatch::Any(m) => m.iter().any(|m| m.matches_(d, state, active)),
InputMatch::All {
tag,
name,
syspath,
devnode,
is_keyboard,
is_pointer,
is_touch,
is_tablet_tool,
is_tablet_pad,
is_gesture,
is_switch,
} => {
if let Some(name) = name {
if d.name() != *name {
return false;
}
}
if let Some(tag) = tag {
let matches = apply_recursive_match(
"input device",
&state.input_devices,
active,
tag,
|m, active| m.matches_(d, state, active),
);
if !matches {
return false;
}
}
if let Some(syspath) = syspath {
if d.syspath() != *syspath {
return false;
}
}
if let Some(devnode) = devnode {
if d.devnode() != *devnode {
return false;
}
}
macro_rules! check_cap {
($is:expr, $cap:ident) => {
if let Some(is) = *$is {
if d.has_capability(jay_config::input::capability::$cap) != is {
return false;
}
}
};
}
check_cap!(is_keyboard, CAP_KEYBOARD);
check_cap!(is_pointer, CAP_POINTER);
check_cap!(is_touch, CAP_TOUCH);
check_cap!(is_tablet_tool, CAP_TABLET_TOOL);
check_cap!(is_tablet_pad, CAP_TABLET_PAD);
check_cap!(is_gesture, CAP_GESTURE);
check_cap!(is_switch, CAP_SWITCH);
true
}
}
}
}
impl Input {
fn apply(&self, c: InputDevice) {
if let Some(v) = self.accel_profile {
c.set_accel_profile(v);
}
if let Some(v) = self.accel_speed {
c.set_accel_speed(v);
}
if let Some(v) = self.tap_enabled {
c.set_tap_enabled(v);
}
if let Some(v) = self.tap_drag_enabled {
c.set_drag_enabled(v);
}
if let Some(v) = self.tap_drag_lock_enabled {
c.set_drag_lock_enabled(v);
}
if let Some(v) = self.left_handed {
c.set_left_handed(v);
}
if let Some(v) = self.natural_scrolling {
c.set_natural_scrolling_enabled(v);
}
if let Some(v) = self.px_per_wheel_scroll {
c.set_px_per_wheel_scroll(v);
}
if let Some(v) = self.transform_matrix {
c.set_transform_matrix(v);
}
}
}
impl OutputMatch {
fn matches(&self, c: Connector, state: &State) -> bool {
if !c.connected() {
return false;
}
self.matches_(c, state, &mut AHashSet::new())
}
fn matches_<'a>(
&'a self,
c: Connector,
state: &'a State,
active: &mut AHashSet<&'a str>,
) -> bool {
match self {
OutputMatch::Any(m) => m.iter().any(|m| m.matches_(c, state, active)),
OutputMatch::All {
name,
connector,
serial_number,
manufacturer,
model,
} => {
if let Some(name) = name {
let matches = apply_recursive_match(
"output",
&state.outputs,
active,
name,
|m, active| m.matches_(c, state, active),
);
if !matches {
return false;
}
}
if let Some(connector) = &connector {
if c.name() != *connector {
return false;
}
}
if let Some(serial_number) = &serial_number {
if c.serial_number() != *serial_number {
return false;
}
}
if let Some(manufacturer) = &manufacturer {
if c.manufacturer() != *manufacturer {
return false;
}
}
if let Some(model) = &model {
if c.model() != *model {
return false;
}
}
true
}
}
}
}
impl ConnectorMatch {
fn matches(&self, c: Connector) -> bool {
if !c.exists() {
return false;
}
match self {
ConnectorMatch::Any(m) => m.iter().any(|m| m.matches(c)),
ConnectorMatch::All { connector } => {
if let Some(connector) = &connector {
if c.name() != *connector {
return false;
}
}
true
}
}
}
}
impl ConfigConnector {
fn apply(&self, c: Connector) {
c.set_enabled(self.enabled);
}
}
impl Output {
fn apply(&self, c: Connector) {
if self.x.is_some() || self.y.is_some() {
let (old_x, old_y) = c.position();
c.set_position(self.x.unwrap_or(old_x), self.y.unwrap_or(old_y));
}
if let Some(scale) = self.scale {
c.set_scale(scale);
}
if let Some(transform) = self.transform {
c.set_transform(transform);
}
if let Some(mode) = &self.mode {
let modes = c.modes();
let m = modes.iter().find(|m| {
if m.width() != mode.width || m.height() != mode.height {
return false;
}
match mode.refresh_rate {
None => true,
Some(rr) => m.refresh_rate() as f64 / 1000.0 == rr,
}
});
match m {
None => {
log::warn!("Output {} does not support mode {mode}", c.name());
}
Some(m) => c.set_mode(m.width(), m.height(), Some(m.refresh_rate())),
}
}
}
}
struct State {
outputs: AHashMap<String, OutputMatch>,
drm_devices: AHashMap<String, DrmDeviceMatch>,
input_devices: AHashMap<String, InputMatch>,
persistent: Rc<PersistentState>,
keymaps: AHashMap<String, Keymap>,
}
impl Drop for State {
fn drop(&mut self) {
for keymap in self.keymaps.values() {
keymap.destroy();
}
}
}
impl State {
fn unbind_all(&self) {
let mut binds = self.persistent.binds.borrow_mut();
for bind in binds.drain() {
self.persistent.seat.unbind(bind);
}
}
fn apply_shortcuts(
self: &Rc<Self>,
shortcuts: impl IntoIterator<Item = (ModifiedKeySym, Action)>,
) {
let mut binds = self.persistent.binds.borrow_mut();
for (key, value) in shortcuts {
if let Action::SimpleCommand {
cmd: SimpleCommand::None,
} = value
{
self.persistent.seat.unbind(key);
binds.remove(&key);
} else {
self.persistent.seat.bind(key, value.into_fn(self));
binds.insert(key);
}
}
}
fn set_keymap(&self, map: &ConfigKeymap) {
let map = match map {
ConfigKeymap::Named(n) => match self.keymaps.get(n) {
None => {
log::warn!("Unknown keymap {n}");
return;
}
Some(m) => *m,
},
ConfigKeymap::Defined { map, .. } => *map,
ConfigKeymap::Literal(map) => *map,
};
self.persistent.seat.set_keymap(map);
}
fn set_status(&self, status: &Option<Status>) {
set_status("");
match status {
None => unset_status_command(),
Some(s) => {
set_i3bar_separator(s.separator.as_deref().unwrap_or(" | "));
set_status_command(s.format, create_command(&s.exec))
}
}
}
fn apply_theme(&self, theme: &Theme) {
use jay_config::theme::{colors::*, sized::*};
macro_rules! color {
($colorable:ident, $field:ident) => {
if let Some(color) = theme.$field {
$colorable.set_color(color)
}
};
}
color!(
ATTENTION_REQUESTED_BACKGROUND_COLOR,
attention_requested_bg_color
);
color!(BACKGROUND_COLOR, bg_color);
color!(BAR_BACKGROUND_COLOR, bar_bg_color);
color!(BAR_STATUS_TEXT_COLOR, bar_status_text_color);
color!(BORDER_COLOR, border_color);
color!(
CAPTURED_FOCUSED_TITLE_BACKGROUND_COLOR,
captured_focused_title_bg_color
);
color!(
CAPTURED_UNFOCUSED_TITLE_BACKGROUND_COLOR,
captured_unfocused_title_bg_color
);
color!(
FOCUSED_INACTIVE_TITLE_BACKGROUND_COLOR,
focused_inactive_title_bg_color
);
color!(
FOCUSED_INACTIVE_TITLE_TEXT_COLOR,
focused_inactive_title_text_color
);
color!(FOCUSED_TITLE_BACKGROUND_COLOR, focused_title_bg_color);
color!(FOCUSED_TITLE_TEXT_COLOR, focused_title_text_color);
color!(SEPARATOR_COLOR, separator_color);
color!(UNFOCUSED_TITLE_BACKGROUND_COLOR, unfocused_title_bg_color);
color!(UNFOCUSED_TITLE_TEXT_COLOR, unfocused_title_text_color);
macro_rules! size {
($sized:ident, $field:ident) => {
if let Some(size) = theme.$field {
$sized.set(size);
}
};
}
size!(BORDER_WIDTH, border_width);
size!(TITLE_HEIGHT, title_height);
if let Some(font) = &theme.font {
set_font(font);
}
}
}
#[derive(Eq, PartialEq, Hash)]
struct OutputId {
manufacturer: String,
model: String,
serial_number: String,
}
struct PersistentState {
seen_outputs: RefCell<AHashSet<OutputId>>,
default: Config,
seat: Seat,
binds: RefCell<AHashSet<ModifiedKeySym>>,
}
fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
let mut path = PathBuf::from(config_dir());
path.push("config.toml");
let config = match std::fs::read(&path) {
Ok(input) => match parse_config(&input, |e| {
log::warn!("Error while parsing {}: {}", path.display(), Report::new(e))
}) {
None if initial_load => {
log::warn!("Using default config instead");
persistent.default.clone()
}
None => {
log::warn!("Ignoring config reload");
return;
}
Some(c) => c,
},
Err(e) if e.kind() == ErrorKind::NotFound => {
log::info!("{} does not exist. Using default config.", path.display());
persistent.default.clone()
}
Err(e) => {
log::warn!("Could not load {}: {}", path.display(), Report::new(e));
log::warn!("Ignoring config reload");
return;
}
};
let mut outputs = AHashMap::new();
for output in &config.outputs {
if let Some(name) = &output.name {
let prev = outputs.insert(name.clone(), output.match_.clone());
if prev.is_some() {
log::warn!("Duplicate output name {name}");
}
}
}
let mut keymaps = AHashMap::new();
for keymap in config.keymaps {
match keymap {
ConfigKeymap::Defined { name, map } => {
keymaps.insert(name, map);
}
_ => log::warn!("Keymap is not in defined form in top-level context"),
}
}
let mut input_devices = AHashMap::new();
for input in &config.inputs {
if let Some(tag) = &input.tag {
let prev = input_devices.insert(tag.clone(), input.match_.clone());
if prev.is_some() {
log::warn!("Duplicate input tag {tag}");
}
}
}
let mut named_drm_device = AHashMap::new();
for drm_device in &config.drm_devices {
if let Some(name) = &drm_device.name {
let prev = named_drm_device.insert(name.clone(), drm_device.match_.clone());
if prev.is_some() {
log::warn!("Duplicate drm device name {name}");
}
}
}
let state = Rc::new(State {
outputs,
drm_devices: named_drm_device,
input_devices,
persistent: persistent.clone(),
keymaps,
});
state.set_status(&config.status);
match config.on_graphics_initialized {
None => on_graphics_initialized(|| ()),
Some(a) => on_graphics_initialized(a.into_fn(&state)),
}
match config.on_idle {
None => on_idle(|| ()),
Some(a) => on_idle(a.into_fn(&state)),
}
state.unbind_all();
state.apply_shortcuts(config.shortcuts);
if let Some(keymap) = config.keymap {
state.set_keymap(&keymap);
}
on_new_connector(move |c| {
for connector in &config.connectors {
if connector.match_.matches(c) {
connector.apply(c);
}
}
});
on_connector_connected({
let state = state.clone();
move |c| {
let id = OutputId {
manufacturer: c.manufacturer(),
model: c.model(),
serial_number: c.serial_number(),
};
if state.persistent.seen_outputs.borrow_mut().insert(id) {
for output in &config.outputs {
if output.match_.matches(c, &state) {
output.apply(c);
}
}
}
}
});
set_default_workspace_capture(config.workspace_capture);
for (k, v) in config.env {
set_env(&k, &v);
}
if initial_load && !is_reload() {
if let Some(on_startup) = config.on_startup {
on_startup.into_fn(&state)();
}
if let Some(level) = config.log_level {
set_log_level(level);
}
if let Some(idle) = config.idle {
set_idle(Some(idle));
}
}
on_devices_enumerated({
let state = state.clone();
move || {
if let Some(dev) = config.render_device {
for d in drm_devices() {
if dev.matches(d, &state) {
d.make_render_device();
return;
}
}
}
}
});
reset_colors();
reset_font();
reset_sizes();
state.apply_theme(&config.theme);
if let Some(api) = config.gfx_api {
set_gfx_api(api);
}
if let Some(dse) = config.direct_scanout_enabled {
set_direct_scanout_enabled(dse);
}
on_new_drm_device({
let state = state.clone();
move |d| {
for dev in &config.drm_devices {
if dev.match_.matches(d, &state) {
dev.apply(d);
}
}
}
});
on_new_input_device({
let state = state.clone();
move |c| {
for input in &config.inputs {
if input.match_.matches(c, &state) {
input.apply(c);
}
}
}
});
}
fn create_command(exec: &Exec) -> Command {
let mut command = Command::new(&exec.prog);
for arg in &exec.args {
command.arg(arg);
}
for (k, v) in &exec.envs {
command.env(k, v);
}
command
}
const DEFAULT: &[u8] = include_bytes!("default-config.toml");
pub fn configure() {
let default = parse_config(DEFAULT, |e| {
panic!("Could not parse the default config: {}", Report::new(e))
});
let persistent = Rc::new(PersistentState {
seen_outputs: Default::default(),
default: default.unwrap(),
seat: default_seat(),
binds: Default::default(),
});
load_config(true, &persistent);
}
config!(configure);

6
toml-config/src/toml.rs Normal file
View file

@ -0,0 +1,6 @@
#[cfg(test)]
mod tests;
mod toml_lexer;
pub mod toml_parser;
pub mod toml_span;
pub mod toml_value;

View file

@ -0,0 +1,133 @@
use {
crate::{
config::error::SpannedError,
toml::{
toml_parser::{parse, ErrorHandler, ParserError},
toml_span::{Span, Spanned, SpannedExt},
toml_value::Value,
},
},
bstr::{BStr, ByteSlice},
std::{
convert::Infallible,
os::unix::ffi::OsStrExt,
panic::{catch_unwind, AssertUnwindSafe},
str::FromStr,
},
walkdir::WalkDir,
};
#[test]
fn test() {
let mut have_failures = false;
let mut num = 0;
for path in WalkDir::new("./toml-test/tests/valid") {
let path = path.unwrap();
if let Some(prefix) = path.path().as_os_str().as_bytes().strip_suffix(b".toml") {
num += 1;
let res = catch_unwind(AssertUnwindSafe(|| {
have_failures |= run_test(prefix.as_bstr());
}));
if res.is_err() {
eprintln!("panic while running {}", prefix.as_bstr());
}
}
}
if have_failures {
panic!("There were test failures");
}
eprintln!("ran {num} tests");
}
fn run_test(prefix: &BStr) -> bool {
let toml = std::fs::read(&format!("{}.toml", prefix)).unwrap();
let json = std::fs::read_to_string(&format!("{}.json", prefix)).unwrap();
let json: serde_json::Value = serde_json::from_str(&json).unwrap();
let json_as_toml = json_to_value(json);
let toml = match parse(toml.as_bytes(), &NoErrorHandler(prefix, &toml)) {
Ok(t) => t,
Err(e) => {
eprintln!("toml could not be parsed in test {}", prefix);
NoErrorHandler(prefix, &toml).handle(e);
return true;
}
};
if toml != json_as_toml {
eprintln!("toml and json differ in test {}", prefix);
eprintln!("toml: {:#?}", toml);
eprintln!("json: {:#?}", json_as_toml);
true
} else {
false
}
}
fn json_to_value(json: serde_json::Value) -> Spanned<Value> {
let span = Span { lo: 0, hi: 0 };
let val = match json {
serde_json::Value::String(_)
| serde_json::Value::Number(_)
| serde_json::Value::Null
| serde_json::Value::Bool(_) => panic!("Unexpected type"),
serde_json::Value::Array(v) => Value::Array(v.into_iter().map(json_to_value).collect()),
serde_json::Value::Object(v) => {
if v.len() == 2 && v.contains_key("type") && v.contains_key("value") {
let ty = v.get("type").unwrap().as_str().unwrap();
let val = v.get("value").unwrap().as_str().unwrap();
match ty {
"string" => Value::String(val.to_owned()),
"integer" => Value::Integer(i64::from_str(val).unwrap()),
"float" => Value::Float(f64::from_str(val).unwrap()),
"bool" => Value::Boolean(bool::from_str(val).unwrap()),
_ => panic!("unexpected type {}", ty),
}
} else {
Value::Table(
v.into_iter()
.map(|(k, v)| (k.spanned(span), json_to_value(v)))
.collect(),
)
}
}
};
val.spanned(span)
}
struct NoErrorHandler<'a>(&'a BStr, &'a [u8]);
impl<'a> ErrorHandler for NoErrorHandler<'a> {
fn handle(&self, err: Spanned<ParserError>) {
eprintln!(
"{}: An error occurred during validation: {}",
self.0,
SpannedError {
input: self.1.into(),
span: err.span,
cause: Some(err.value),
}
);
}
fn redefinition(&self, err: Spanned<ParserError>, prev: Span) {
eprintln!(
"{}: Redefinition: {}",
self.0,
SpannedError {
input: self.1.into(),
span: err.span,
cause: Some(err.value),
}
);
eprintln!(
"{}: Previous: {}",
self.0,
SpannedError {
input: self.1.into(),
span: prev,
cause: None::<Infallible>,
}
);
}
}

View file

@ -0,0 +1,193 @@
use crate::toml::toml_span::{Span, Spanned, SpannedExt};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Token<'a> {
Dot,
Equals,
Comma,
LeftBracket,
RightBracket,
LeftBrace,
RightBrace,
LiteralString(&'a [u8]),
CookedString(&'a [u8]),
Literal(&'a [u8]),
}
impl<'a> Token<'a> {
pub fn name(self, value_context: bool) -> &'static str {
match self {
Token::Dot => "`.`",
Token::Equals => "`=`",
Token::Comma => "`,`",
Token::LeftBracket => "`[`",
Token::RightBracket => "`]`",
Token::LeftBrace => "`{`",
Token::RightBrace => "`}`",
Token::LiteralString(_) | Token::CookedString(_) => "a string",
Token::Literal(_) if value_context => "a literal",
Token::Literal(_) => "a key",
}
}
}
pub struct Lexer<'a> {
input: &'a [u8],
pos: usize,
peek: Option<Spanned<Token<'a>>>,
peek_value_context: bool,
}
impl<'a> Lexer<'a> {
pub fn new(input: &'a [u8]) -> Self {
Self {
input,
pos: 0,
peek: None,
peek_value_context: false,
}
}
pub fn pos(&mut self) -> usize {
self.skip_ws();
self.pos
}
fn skip_ws(&mut self) {
while let Some(char) = self.input.get(self.pos).copied() {
match char {
b' ' | b'\t' | b'\n' => self.pos += 1,
b'#' => {
self.pos += 1;
while let Some(char) = self.input.get(self.pos).copied() {
self.pos += 1;
if char == b'\n' {
break;
}
}
}
_ => break,
}
}
}
pub fn peek(&mut self, value_context: bool) -> Option<Spanned<Token<'a>>> {
let next = self.next(value_context);
self.peek = next;
self.peek_value_context = value_context;
next
}
pub fn next(&mut self, value_context: bool) -> Option<Spanned<Token<'a>>> {
if let Some(peek) = self.peek.take() {
if self.peek_value_context == value_context {
return Some(peek);
}
self.pos = peek.span.lo;
}
use Token::*;
macro_rules! get {
($off:expr) => {
self.input.get(self.pos + $off).copied()
};
}
self.skip_ws();
let Some(c) = get!(0) else {
return None;
};
let pos = self.pos;
macro_rules! span {
() => {
Span {
lo: pos,
hi: self.pos,
}
};
}
'simple: {
let t = match c {
b'.' => Dot,
b',' => Comma,
b'=' => Equals,
b'[' => LeftBracket,
b']' => RightBracket,
b'{' => LeftBrace,
b'}' => RightBrace,
_ => break 'simple,
};
self.pos += 1;
return Some(t.spanned(span!()));
}
macro_rules! try_string {
($delim:expr, $escaping:expr, $ident:ident) => {
if c == $delim {
'ml_string: {
let delim = ($delim, Some($delim), Some($delim));
if (c, get!(1), get!(2)) != delim {
break 'ml_string;
}
self.pos += 3;
if get!(0) == Some(b'\n') {
self.pos += 1;
}
let start = self.pos;
let end = loop {
let c = match get!(0) {
Some(c) => c,
_ => break self.pos,
};
self.pos += 1;
if $escaping && c == b'\\' {
self.pos += 1;
} else if c == $delim {
if (c, get!(0), get!(1)) == delim && get!(2) != Some($delim) {
self.pos += 2;
break self.pos - 3;
}
}
};
return Some($ident(&self.input[start..end]).spanned(span!()));
}
self.pos += 1;
let start = self.pos;
let end = loop {
let c = match get!(0) {
Some(c) => c,
_ => break self.pos,
};
self.pos += 1;
if $escaping && c == b'\\' {
self.pos += 1;
} else if c == $delim {
break self.pos - 1;
}
};
return Some($ident(&self.input[start..end]).spanned(span!()));
}
};
}
try_string!(b'\'', false, LiteralString);
try_string!(b'"', true, CookedString);
let start = self.pos;
while let Some(c) = get!(0) {
match c {
b' ' | b'\t' | b'\n' | b'#' | b',' | b'=' | b'{' | b'}' | b'[' | b']' => break,
b'.' if !value_context => break,
_ => {}
}
self.pos += 1;
}
let end = self.pos;
Some(Literal(&self.input[start..end]).spanned(span!()))
}
}

View file

@ -0,0 +1,534 @@
use {
crate::toml::{
toml_lexer::{Lexer, Token},
toml_span::{Span, Spanned, SpannedExt},
toml_value::Value,
},
bstr::ByteSlice,
indexmap::{
map::{raw_entry_v1::RawEntryMut, RawEntryApiV1},
IndexMap,
},
std::{collections::VecDeque, mem, str::FromStr},
thiserror::Error,
};
pub trait ErrorHandler {
fn handle(&self, err: Spanned<ParserError>);
fn redefinition(&self, err: Spanned<ParserError>, prev: Span);
}
#[derive(Debug, Error)]
pub enum ParserError {
#[error("Unexpected end of file")]
UnexpectedEof,
#[error("Expected a key")]
MissingKey,
#[error("Expected {0} but found {1}")]
Expected(&'static str, &'static str),
#[error("Duplicate key overwrites the previous definition")]
Redefined,
#[error("Literal is not valid UTF-8")]
NonUtf8Literal,
#[error("Could not parse the literal")]
UnknownLiteral,
#[error("Ignoring key due to following error")]
IgnoringKey,
#[error("Unnecessary comma")]
UnnecessaryComma,
}
pub fn parse(
input: &[u8],
error_handler: &dyn ErrorHandler,
) -> Result<Spanned<Value>, Spanned<ParserError>> {
let parser = Parser {
lexer: Lexer::new(input),
error_handler,
last_span: None,
};
parser.parse()
}
struct Parser<'a, 'b> {
lexer: Lexer<'a>,
error_handler: &'b dyn ErrorHandler,
last_span: Option<Span>,
}
type Key = VecDeque<Spanned<String>>;
impl<'a, 'b> Parser<'a, 'b> {
fn parse(mut self) -> Result<Spanned<Value>, Spanned<ParserError>> {
self.parse_document()
}
fn unexpected_eof(&self) -> Spanned<ParserError> {
let span = self.last_span.unwrap_or(Span { lo: 0, hi: 0 });
ParserError::UnexpectedEof.spanned(span)
}
fn next(&mut self, value_context: bool) -> Result<Spanned<Token<'a>>, Spanned<ParserError>> {
match self.lexer.next(value_context) {
Some(t) => {
self.last_span = Some(t.span);
Ok(t)
}
_ => Err(self.unexpected_eof()),
}
}
fn peek(&mut self, value_context: bool) -> Result<Spanned<Token<'a>>, Spanned<ParserError>> {
match self.lexer.peek(value_context) {
Some(t) => Ok(t),
_ => Err(self.unexpected_eof()),
}
}
fn parse_value(&mut self) -> Result<Spanned<Value>, Spanned<ParserError>> {
let token = self.peek(true)?;
match token.value {
Token::LiteralString(s) => self.parse_literal_string(s),
Token::CookedString(s) => self.parse_cooked_string(s),
Token::LeftBracket => self.parse_array(),
Token::Literal(l) => self.parse_literal_value(l),
Token::LeftBrace => self.parse_inline_table(),
Token::Dot | Token::Equals | Token::Comma | Token::RightBrace | Token::RightBracket => {
Err(ParserError::Expected("a value", token.value.name(true)).spanned(token.span))
}
}
}
fn parse_literal_value(
&mut self,
literal: &[u8],
) -> Result<Spanned<Value>, Spanned<ParserError>> {
let span = self.next(true)?.span;
let Ok(s) = std::str::from_utf8(literal) else {
return Err(ParserError::NonUtf8Literal.spanned(span));
};
if s == "true" {
return Ok(Value::Boolean(true).spanned(span));
}
if s == "false" {
return Ok(Value::Boolean(false).spanned(span));
}
let s = s.replace('_', "");
if let Ok(n) = i64::from_str(&s) {
return Ok(Value::Integer(n).spanned(span));
}
'radix: {
let b = s.as_bytes();
if b.len() >= 2 && b[0] == b'0' {
let radix = match b[1] {
b'x' => 16,
b'o' => 8,
b'b' => 2,
_ => break 'radix,
};
if let Ok(n) = i64::from_str_radix(&s[2..], radix) {
return Ok(Value::Integer(n).spanned(span));
}
}
}
if let Ok(n) = f64::from_str(&s) {
return Ok(Value::Float(n).spanned(span));
}
Err(ParserError::UnknownLiteral.spanned(span))
}
fn parse_literal_string(&mut self, s: &[u8]) -> Result<Spanned<Value>, Spanned<ParserError>> {
let span = self.next(true)?.span;
let s = s.as_bstr().to_string();
Ok(Value::String(s).spanned(span))
}
fn parse_cooked_string(&mut self, s: &[u8]) -> Result<Spanned<Value>, Spanned<ParserError>> {
let span = self.next(true)?.span;
let s = self.cook_string(s);
Ok(Value::String(s).spanned(span))
}
fn cook_string(&self, s: &[u8]) -> String {
use std::io::Write;
if !s.contains(&b'\\') {
return s.as_bstr().to_string();
}
let mut res = vec![];
let mut pos = 0;
while pos < s.len() {
let c = s[pos];
pos += 1;
match c {
b'\\' => {
let c = s[pos];
pos += 1;
match c {
b'\\' => res.push(b'\\'),
b'"' => res.push(b'"'),
b'b' => res.push(0x8),
b'f' => res.push(0xc),
b'n' => res.push(b'\n'),
b'r' => res.push(b'\r'),
b't' => res.push(b'\t'),
b'e' => res.push(0x1b),
b'x' | b'u' | b'U' => 'unicode: {
let len = match c {
b'x' => 2,
b'u' => 4,
_ => 8,
};
if s.len() - pos >= len {
if let Ok(s) = std::str::from_utf8(&s[pos..pos + len]) {
if let Ok(n) = u32::from_str_radix(s, 16) {
if let Some(c) = char::from_u32(n) {
pos += len;
let _ = write!(res, "{}", c);
break 'unicode;
}
}
}
}
res.extend_from_slice(&s[pos - 2..]);
}
b' ' | b'\t' | b'\n' => {
let mut t = pos;
let mut saw_nl = c == b'\n';
while t < s.len() && matches!(s[t], b' ' | b'\t' | b'\n') {
saw_nl |= s[t] == b'\n';
t += 1;
}
if saw_nl {
pos = t;
} else {
res.extend_from_slice(&[b'\\', c]);
}
}
_ => {
res.extend_from_slice(&[b'\\', c]);
}
}
}
_ => res.push(c),
}
}
res.as_bstr().to_string()
}
fn parse_array(&mut self) -> Result<Spanned<Value>, Spanned<ParserError>> {
let lo = self.next(true)?.span.lo;
let mut entries = vec![];
let mut consumed_comma = false;
loop {
if let Some(v) = self.lexer.peek(true) {
if v.value == Token::RightBracket {
let _ = self.next(true);
let hi = v.span.hi;
let span = Span { lo, hi };
return Ok(Value::Array(entries).spanned(span));
}
if entries.len() > 0 && !mem::take(&mut consumed_comma) {
self.error_handler.handle(
ParserError::Expected("`,` or `]`", v.value.name(true)).spanned(v.span),
);
}
}
match self.parse_value() {
Ok(v) => {
entries.push(v);
consumed_comma = self.skip_comma(true);
}
Err(e) => {
self.skip_tree(Token::LeftBracket, Token::RightBracket);
return Err(e);
}
}
}
}
fn parse_inline_table(&mut self) -> Result<Spanned<Value>, Spanned<ParserError>> {
let lo = self.next(true)?.span.lo;
let mut map = IndexMap::new();
let mut consumed_comma = false;
loop {
let token = match self.peek(false) {
Ok(t) => t,
Err(e) => {
self.error_handler.handle(e);
break;
}
};
if token.value == Token::RightBrace {
let _ = self.next(false);
break;
}
if !map.is_empty() && !mem::take(&mut consumed_comma) {
self.error_handler.handle(
ParserError::Expected("`,` or `}`", token.value.name(false))
.spanned(token.span),
);
}
let res = match self.parse_key_value_with_recovery() {
Ok(res) => res,
Err(e) => {
self.skip_tree(Token::LeftBrace, Token::RightBrace);
return Err(e);
}
};
if let Some((mut key, value)) = res {
self.insert(&mut map, &mut key, value, false, false);
};
consumed_comma = self.skip_comma(false);
}
let hi = self.last_span().hi;
let span = Span { lo, hi };
Ok(Value::Table(map).spanned(span))
}
fn skip_comma(&mut self, value_context: bool) -> bool {
if let Some(token) = self.lexer.peek(value_context) {
if token.value != Token::Comma {
return false;
}
let _ = self.next(value_context);
}
while let Some(token) = self.lexer.peek(value_context) {
if token.value != Token::Comma {
break;
}
let _ = self.next(value_context);
self.error_handler
.handle(ParserError::UnnecessaryComma.spanned(token.span));
}
true
}
fn parse_document(&mut self) -> Result<Spanned<Value>, Spanned<ParserError>> {
let mut map = IndexMap::new();
self.parse_table_body(&mut map)?;
while self.lexer.peek(false).is_some() {
let (mut key, append) = self.parse_table_header()?;
let mut inner_map = IndexMap::new();
self.parse_table_body(&mut inner_map)?;
let value = Value::Table(inner_map).spanned(key.span);
self.insert(&mut map, &mut key.value, value, true, append);
}
let hi = self.last_span().hi;
let span = Span { lo: 0, hi };
Ok(Value::Table(map).spanned(span))
}
fn parse_table_header(&mut self) -> Result<(Spanned<Key>, bool), Spanned<ParserError>> {
let lo = self.next(false)?.span.lo;
let mut append = false;
if let Some(token) = self.lexer.peek(false) {
if token.value == Token::LeftBracket {
let _ = self.next(false);
append = true;
}
}
let key = self.parse_key()?;
let mut hi = self.parse_exact(Token::RightBracket, false)?.hi;
if append {
hi = self.parse_exact(Token::RightBracket, false)?.hi;
}
let span = Span { lo, hi };
Ok((key.spanned(span), append))
}
fn parse_table_body(
&mut self,
dst: &mut IndexMap<Spanned<String>, Spanned<Value>>,
) -> Result<(), Spanned<ParserError>> {
while let Some(e) = self.lexer.peek(false) {
if e.value == Token::LeftBracket {
return Ok(());
}
let Some((mut key, value)) = self.parse_key_value_with_recovery()? else {
continue;
};
self.insert(dst, &mut key, value, false, false);
}
Ok(())
}
fn insert(
&self,
dst: &mut IndexMap<Spanned<String>, Spanned<Value>>,
keys: &mut Key,
value: Spanned<Value>,
modify_array_element: bool,
append_last: bool,
) {
let key = keys.pop_front().unwrap();
if keys.is_empty() {
if let RawEntryMut::Occupied(mut old) =
dst.raw_entry_mut_v1().from_key(key.value.as_str())
{
if append_last {
if let Value::Array(array) = &mut old.get_mut().value {
array.push(value);
return;
}
}
if let Value::Table(old) = &mut old.get_mut().value {
if let Value::Table(new) = value.value {
for (k, v) in new {
let mut keys = Key::new();
keys.push_back(k);
self.insert(old, &mut keys, v, false, false);
}
return;
}
}
self.error_handler
.redefinition(ParserError::Redefined.spanned(key.span), old.key().span);
old.shift_remove();
}
let span = value.span;
let value = match append_last {
true => Value::Array(vec![value]).spanned(span),
false => value,
};
dst.insert(key, value);
} else {
if let RawEntryMut::Occupied(mut o) = dst.raw_entry_mut_v1().from_key(&key) {
match &mut o.get_mut().value {
Value::Table(dst) => {
self.insert(dst, keys, value, modify_array_element, append_last);
return;
}
Value::Array(array) if modify_array_element => {
if let Some(Value::Table(dst)) =
array.last_mut().as_mut().map(|v| &mut v.value)
{
self.insert(dst, keys, value, modify_array_element, append_last);
return;
}
}
_ => {}
}
self.error_handler
.redefinition(ParserError::Redefined.spanned(key.span), o.key().span);
o.shift_remove();
}
let mut map = IndexMap::new();
let span = value.span;
self.insert(&mut map, keys, value, modify_array_element, append_last);
dst.insert(key, Value::Table(map).spanned(span));
}
}
fn parse_key_value_with_recovery(
&mut self,
) -> Result<Option<(Key, Spanned<Value>)>, Spanned<ParserError>> {
let pos = self.lexer.pos();
match self.parse_key_value() {
Ok(kv) => Ok(Some(kv)),
Err((e, key)) => {
if let Some(key) = key {
let span = key.back().unwrap().span;
self.error_handler
.handle(ParserError::IgnoringKey.spanned(span));
}
if self.lexer.pos() == pos {
Err(e)
} else {
self.error_handler.handle(e);
Ok(None)
}
}
}
}
#[allow(clippy::type_complexity)]
fn parse_key_value(
&mut self,
) -> Result<(Key, Spanned<Value>), (Spanned<ParserError>, Option<Key>)> {
let key = self.parse_key();
let eq = self.parse_exact(Token::Equals, true);
let value = self.parse_value();
let key = match key {
Ok(k) => k,
Err(e) => return Err((e, None)),
};
if let Err(e) = eq {
return Err((e, Some(key)));
}
let value = match value {
Ok(v) => v,
Err(e) => return Err((e, Some(key))),
};
Ok((key, value))
}
fn parse_key(&mut self) -> Result<Key, Spanned<ParserError>> {
let mut parts = Key::new();
loop {
if parts.len() > 0 {
if self.parse_exact(Token::Dot, false).is_err() {
break;
}
}
let Some(token) = self.lexer.peek(false) else {
break;
};
let s = match token.value {
Token::LiteralString(s) => s.as_bstr().to_string(),
Token::CookedString(s) => self.cook_string(s),
Token::Literal(l) => l.as_bstr().to_string(),
_ => break,
};
parts.push_back(s.spanned(token.span));
let _ = self.next(false);
}
if parts.is_empty() {
Err(ParserError::MissingKey.spanned(self.next_span()))
} else {
Ok(parts)
}
}
fn parse_exact(
&mut self,
token: Token<'a>,
value_context: bool,
) -> Result<Span, Spanned<ParserError>> {
let actual = match self.peek(value_context) {
Ok(t) if t.value == token => {
let _ = self.next(value_context);
return Ok(t.span);
}
Ok(t) => t.value.name(value_context),
Err(_) => "end of file",
};
let span = self.next_span();
Err(ParserError::Expected(token.name(value_context), actual).spanned(span))
}
fn last_span(&self) -> Span {
self.last_span.unwrap_or(Span { lo: 0, hi: 0 })
}
fn next_span(&mut self) -> Span {
self.lexer.peek(false).map(|v| v.span).unwrap_or_else(|| {
let hi = self.last_span().hi;
Span { lo: hi, hi }
})
}
fn skip_tree(&mut self, start: Token, end: Token) {
let mut depth = 1;
while let Ok(next) = self.next(false) {
if next.value == start {
depth += 1;
} else if next.value == end {
depth -= 1;
if depth == 0 {
return;
}
}
}
}
}

View file

@ -0,0 +1,124 @@
use std::{
borrow::Borrow,
fmt::{Debug, Formatter},
hash::{Hash, Hasher},
};
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct Span {
pub lo: usize,
pub hi: usize,
}
impl Debug for Span {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}..{}", self.lo, self.hi)
}
}
#[derive(Copy, Clone, Eq)]
pub struct Spanned<T> {
pub span: Span,
pub value: T,
}
impl<T> Spanned<T> {
pub fn as_ref(&self) -> Spanned<&T> {
Spanned {
span: self.span,
value: &self.value,
}
}
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Spanned<U> {
Spanned {
span: self.span,
value: f(self.value),
}
}
pub fn into<U>(self) -> Spanned<U>
where
T: Into<U>,
{
Spanned {
span: self.span,
value: self.value.into(),
}
}
}
impl<T: Debug> Debug for Spanned<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.value.fmt(f)?;
write!(f, " @ {:?}", self.span)
}
}
impl<T: PartialEq> PartialEq for Spanned<T> {
fn eq(&self, other: &Self) -> bool {
self.value.eq(&other.value)
}
}
impl<T: Hash> Hash for Spanned<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.value.hash(state)
}
}
impl Borrow<str> for Spanned<String> {
fn borrow(&self) -> &str {
&self.value
}
}
pub trait SpannedExt: Sized {
fn spanned(self, span: Span) -> Spanned<Self>;
}
impl<T> SpannedExt for T {
fn spanned(self, span: Span) -> Spanned<Self> {
Spanned { span, value: self }
}
}
pub trait DespanExt: Sized {
type T;
fn despan(self) -> Option<Self::T>;
fn despan_into<U>(self) -> Option<U>
where
Self::T: Into<U>;
}
impl<T> DespanExt for Option<Spanned<T>> {
type T = T;
fn despan(self) -> Option<Self::T> {
self.map(|v| v.value)
}
fn despan_into<U>(self) -> Option<U>
where
Self::T: Into<U>,
{
self.map(|v| v.value.into())
}
}
pub trait SpannedResultExt1: Sized {
type T;
type E;
fn map_spanned<U, F: FnOnce(Self::T) -> U>(self, f: F) -> Result<Spanned<U>, Self::E>;
}
impl<T, E> SpannedResultExt1 for Result<Spanned<T>, E> {
type T = T;
type E = E;
fn map_spanned<U, F: FnOnce(Self::T) -> U>(self, f: F) -> Result<Spanned<U>, Self::E> {
self.map(|v| v.map(f))
}
}

View file

@ -0,0 +1,63 @@
use {
crate::toml::toml_span::Spanned,
indexmap::IndexMap,
std::{
cmp::Ordering,
fmt::{Debug, Formatter},
},
};
pub enum Value {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<Spanned<Value>>),
Table(IndexMap<Spanned<String>, Spanned<Value>>),
}
impl Value {
pub fn name(&self) -> &'static str {
match self {
Value::String(_) => "a string",
Value::Integer(_) => "an integer",
Value::Float(_) => "a float",
Value::Boolean(_) => "a boolean",
Value::Array(_) => "an array",
Value::Table(_) => "a table",
}
}
}
impl Debug for Value {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Value::String(v) => v.fmt(f),
Value::Integer(v) => v.fmt(f),
Value::Float(v) => v.fmt(f),
Value::Boolean(v) => v.fmt(f),
Value::Array(v) => v.fmt(f),
Value::Table(v) => v.fmt(f),
}
}
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Value::String(v1), Value::String(v2)) => v1 == v2,
(Value::Integer(v1), Value::Integer(v2)) => v1 == v2,
(Value::Float(v1), Value::Float(v2)) => {
if v1.is_nan() && v2.is_nan() {
true
} else {
v1.total_cmp(v2) == Ordering::Equal
}
}
(Value::Boolean(v1), Value::Boolean(v2)) => v1 == v2,
(Value::Array(v1), Value::Array(v2)) => v1 == v2,
(Value::Table(v1), Value::Table(v2)) => v1 == v2,
_ => false,
}
}
}

12
toml-spec/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "toml-spec"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.197", features = ["derive"] }
serde_yaml = "0.9.32"
anyhow = "1.0.81"
indexmap = { version = "2.2.5", features = ["serde"] }
error_reporter = "1.0.0"
serde_json = { version = "1.0.114", features = ["preserve_order"] }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

1936
toml-spec/spec/spec.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,10 @@
# Jay TOML Config
This document describes the format of the TOML configuration of the Jay compositor.
A JSON Schema for this format is available at [spec.generated.json](./spec.generated.json). You can include this file in your editor to get auto completion.
Start at the top-level type: [Config](#types-config).
## Types

View file

@ -0,0 +1,194 @@
use {
crate::types::{
ArraySpec, Described, MapSpec, NestableTypesSpec, NumberSpec, RefOrSpec, SingleTableSpec,
StringSpec, TableSpec, TopLevelTypeSpec, VariantSpec,
},
anyhow::Result,
serde_json::{json, Map, Value},
};
pub fn generate_json_schema(
types_sorted: &[(&String, &Described<TopLevelTypeSpec>)],
) -> Result<()> {
let mut types = Map::new();
for (name, ty) in types_sorted {
types.insert(name.to_string(), create_top_level_schema(ty));
}
let json = json!({
"$id": "jay_toml_schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "#/$defs/Config",
"$defs": types,
});
let json = serde_json::to_string_pretty(&json).unwrap();
std::fs::write(
concat!(env!("CARGO_MANIFEST_DIR"), "/spec/spec.generated.json"),
json.as_bytes(),
)?;
Ok(())
}
fn create_top_level_schema(spec: &Described<TopLevelTypeSpec>) -> Value {
match &spec.value {
TopLevelTypeSpec::Variable { variants } => {
let mut cases = vec![];
for variant in variants {
cases.push(create_variant_schema(&variant.description, &variant.value));
}
json!({
"description": spec.description,
"anyOf": cases,
})
}
TopLevelTypeSpec::Single(variant) => create_variant_schema(&spec.description, variant),
}
}
fn create_variant_schema(description: &str, spec: &VariantSpec) -> Value {
macro_rules! spec {
($v:expr) => {
match $v {
RefOrSpec::Ref { name } => return create_ref_spec(description, name),
RefOrSpec::Spec(s) => s,
}
};
}
match spec {
VariantSpec::String(ss) => {
let ss = spec!(ss);
create_string_spec(description, ss)
}
VariantSpec::Number(ns) => {
let ns = spec!(ns);
create_number_spec(description, ns)
}
VariantSpec::Boolean => create_boolean_spec(description),
VariantSpec::Array(s) => {
let s = spec!(s);
create_array_spec(description, s)
}
VariantSpec::Table(ts) => {
let ts = spec!(ts);
match ts {
TableSpec::Tagged { types } => {
let mut variants = vec![];
for (name, ty) in types {
variants.push(create_single_table_spec(
&ty.description,
&ty.value,
Some(name),
));
}
json!({
"description": description,
"anyOf": variants,
})
}
TableSpec::Single(s) => create_single_table_spec(description, s, None),
}
}
}
}
fn create_single_table_spec(
description: &str,
spec: &SingleTableSpec,
type_: Option<&str>,
) -> Value {
let mut properties = Map::new();
let mut required = vec![];
if let Some(type_) = type_ {
properties.insert("type".into(), json!({ "const": type_ }));
required.push("type".into());
}
for (key, val) in &spec.fields {
properties.insert(
key.into(),
create_nestable_type_spec(&val.description, &val.value.kind),
);
if val.value.required {
required.push(key.to_string());
}
}
json!({
"description": description,
"type": "object",
"properties": properties,
"required": required,
})
}
fn create_ref_spec(description: &str, name: &str) -> Value {
let path = format!("#/$defs/{name}");
json!({
"description": description,
"$ref": path,
})
}
fn create_nestable_type_spec(description: &str, spec: &RefOrSpec<NestableTypesSpec>) -> Value {
let spec = match spec {
RefOrSpec::Ref { name } => return create_ref_spec(description, name),
RefOrSpec::Spec(s) => s,
};
match spec {
NestableTypesSpec::String(s) => create_string_spec(description, s),
NestableTypesSpec::Number(s) => create_number_spec(description, s),
NestableTypesSpec::Boolean => create_boolean_spec(description),
NestableTypesSpec::Array(s) => create_array_spec(description, s),
NestableTypesSpec::Map(s) => create_map_spec(description, s),
}
}
fn create_map_spec(description: &str, spec: &MapSpec) -> Value {
json!({
"description": description,
"type": "object",
"additionalProperties": create_nestable_type_spec("", &spec.values),
})
}
fn create_string_spec(description: &str, spec: &StringSpec) -> Value {
let mut res = Map::new();
res.insert("type".into(), json!("string"));
res.insert("description".into(), json!(description));
if let Some(values) = &spec.values {
let strings: Vec<_> = values.iter().map(|v| &v.value.value).collect();
res.insert("enum".into(), json!(strings));
}
res.into()
}
fn create_array_spec(description: &str, spec: &ArraySpec) -> Value {
json!({
"type": "array",
"description": description,
"items": create_nestable_type_spec("", &spec.items),
})
}
fn create_number_spec(description: &str, spec: &NumberSpec) -> Value {
let ty = match spec.integer_only {
true => "integer",
false => "number",
};
let mut res = Map::new();
res.insert("type".into(), json!(ty));
res.insert("description".into(), json!(description));
if let Some(minimum) = spec.minimum {
let key = match spec.exclusive_minimum {
true => "exclusiveMinimum",
false => "minimum",
};
res.insert(key.into(), json!(minimum));
}
res.into()
}
fn create_boolean_spec(description: &str) -> Value {
json!({"type": "boolean", "description": description})
}

27
toml-spec/src/main.rs Normal file
View file

@ -0,0 +1,27 @@
use {
crate::{
json_schema::generate_json_schema,
markdown::generate_markdown,
types::{Described, TopLevelTypeSpec},
},
anyhow::Result,
indexmap::IndexMap,
};
mod json_schema;
mod markdown;
mod types;
fn parse() -> Result<IndexMap<String, Described<TopLevelTypeSpec>>> {
let file = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/spec/spec.yaml"))?;
Ok(serde_yaml::from_str(&file)?)
}
fn main() -> Result<()> {
let types = parse()?;
let mut types_sorted: Vec<_> = types.iter().collect();
types_sorted.sort_by_key(|t| t.0);
generate_markdown(&types_sorted)?;
generate_json_schema(&types_sorted)?;
Ok(())
}

259
toml-spec/src/markdown.rs Normal file
View file

@ -0,0 +1,259 @@
use {
crate::types::{
ArraySpec, Described, NestableTypesSpec, NumberSpec, RefOrSpec, SingleTableSpec,
StringSpec, TableSpec, TopLevelTypeSpec, VariantSpec,
},
anyhow::Result,
std::io::Write,
};
pub fn generate_markdown(types: &[(&String, &Described<TopLevelTypeSpec>)]) -> Result<()> {
const TEMPLATE: &str = include_str!("../spec/template.md");
let mut buf = vec![];
buf.extend_from_slice(TEMPLATE.as_bytes());
for (name, ty) in types {
write_top_level_type_spec(&mut buf, name, ty)?;
}
std::fs::write(
concat!(env!("CARGO_MANIFEST_DIR"), "/spec/spec.generated.md"),
&buf,
)?;
Ok(())
}
fn write_top_level_type_spec(
buf: &mut Vec<u8>,
name: &str,
spec: &Described<TopLevelTypeSpec>,
) -> Result<()> {
writeln!(buf, "<a name=\"types-{name}\"></a>")?;
writeln!(buf, "### `{name}`")?;
writeln!(buf)?;
writeln!(buf, "{}", spec.description.trim())?;
writeln!(buf)?;
match &spec.value {
TopLevelTypeSpec::Variable { variants } => {
writeln!(
buf,
"Values of this type should have one of the following forms:"
)?;
writeln!(buf)?;
for variant in variants {
write!(buf, "#### ")?;
let name = match &variant.value {
VariantSpec::String(_) => "A string",
VariantSpec::Number(_) => "A number",
VariantSpec::Boolean => "A boolean",
VariantSpec::Array(_) => "An array",
VariantSpec::Table(_) => "A table",
};
writeln!(buf, "{name}")?;
writeln!(buf)?;
writeln!(buf, "{}", variant.description.trim())?;
writeln!(buf)?;
write_variant_spec(buf, &variant.value)?;
}
}
TopLevelTypeSpec::Single(variant) => {
let name = match &variant {
VariantSpec::String(_) => "strings",
VariantSpec::Number(_) => "numbers",
VariantSpec::Boolean => "booleans",
VariantSpec::Array(_) => "arrays",
VariantSpec::Table(_) => "tables",
};
writeln!(buf, "Values of this type should be {name}.")?;
writeln!(buf)?;
write_variant_spec(buf, variant)?;
}
}
writeln!(buf)?;
Ok(())
}
fn write_variant_spec(buf: &mut Vec<u8>, spec: &VariantSpec) -> Result<()> {
macro_rules! spec {
($v:expr) => {
match $v {
RefOrSpec::Ref { name } => {
writeln!(buf, "The value should be a [{name}](#types-{name}).")?;
writeln!(buf)?;
return Ok(());
}
RefOrSpec::Spec(s) => s,
}
};
}
match spec {
VariantSpec::String(ss) => {
let ss = spec!(ss);
write_string_spec(buf, ss, "")?;
}
VariantSpec::Number(ns) => {
let ns = spec!(ns);
write_number_spec(buf, ns, "")?;
}
VariantSpec::Boolean => {}
VariantSpec::Array(s) => {
let s = spec!(s);
write_array_spec(buf, s, "")?;
}
VariantSpec::Table(ts) => {
let ts = spec!(ts);
match ts {
TableSpec::Tagged { types } => {
writeln!(buf, "This table is a tagged union. The variant is determined by the `type` field. It takes one of the following values:")?;
writeln!(buf)?;
for (name, spec) in types {
writeln!(buf, "- `{name}`:")?;
writeln!(buf)?;
for line in spec.description.trim().lines() {
writeln!(buf, " {line}")?;
}
writeln!(buf)?;
write_single_table_spec(buf, &spec.value, " ")?;
}
}
TableSpec::Single(s) => {
write_single_table_spec(buf, s, "")?;
}
}
}
}
Ok(())
}
fn write_single_table_spec(buf: &mut Vec<u8>, spec: &SingleTableSpec, pad: &str) -> Result<()> {
writeln!(buf, "{pad}The table has the following fields:")?;
writeln!(buf)?;
for (name, fs) in &spec.fields {
let optional = match fs.value.required {
true => "required",
false => "optional",
};
writeln!(buf, "{pad}- `{name}` ({optional}):")?;
writeln!(buf)?;
for line in fs.description.trim().lines() {
writeln!(buf, "{pad} {line}")?;
}
writeln!(buf)?;
write!(buf, "{pad} The value of this field should be ")?;
let spec = write_nestable_type_spec(buf, &fs.value.kind, false)?;
writeln!(buf, ".")?;
writeln!(buf)?;
if let Some(spec) = spec {
let pad = format!("{pad} ");
write_nestable_type_restrictions(buf, spec, &pad)?;
}
}
Ok(())
}
fn write_nestable_type_spec<'a>(
buf: &mut Vec<u8>,
spec: &'a RefOrSpec<NestableTypesSpec>,
plural: bool,
) -> Result<Option<&'a NestableTypesSpec>> {
let spec = match spec {
RefOrSpec::Ref { name } => {
if plural {
write!(buf, "[{name}s](#types-{name})")?;
} else {
write!(buf, "a [{name}](#types-{name})")?;
}
return Ok(None);
}
RefOrSpec::Spec(s) => s,
};
let name = match (spec, plural) {
(NestableTypesSpec::String(_), false) => "a string",
(NestableTypesSpec::String(_), true) => "strings",
(NestableTypesSpec::Number(_), false) => "a number",
(NestableTypesSpec::Number(_), true) => "numbers",
(NestableTypesSpec::Boolean, false) => "a boolean",
(NestableTypesSpec::Boolean, true) => "booleans",
(NestableTypesSpec::Map(s), _) => {
let name = match plural {
true => "tables",
false => "a table",
};
write!(buf, "{name} whose values are ")?;
return write_nestable_type_spec(buf, &s.values, true);
}
(NestableTypesSpec::Array(s), _) => {
let name = match plural {
true => "arrays",
false => "an array",
};
write!(buf, "{name} of ")?;
return write_nestable_type_spec(buf, &s.items, true);
}
};
write!(buf, "{name}")?;
Ok(Some(spec))
}
fn write_nestable_type_restrictions(
buf: &mut Vec<u8>,
spec: &NestableTypesSpec,
pad: &str,
) -> Result<()> {
match spec {
NestableTypesSpec::String(s) => write_string_spec(buf, s, pad),
NestableTypesSpec::Number(s) => write_number_spec(buf, s, pad),
NestableTypesSpec::Boolean => Ok(()),
NestableTypesSpec::Array(_) => Ok(()),
NestableTypesSpec::Map(_) => Ok(()),
}
}
fn write_string_spec(buf: &mut Vec<u8>, spec: &StringSpec, pad: &str) -> Result<()> {
if let Some(values) = &spec.values {
writeln!(
buf,
"{pad}The string should have one of the following values:"
)?;
writeln!(buf)?;
for value in values {
writeln!(buf, "{pad}- `{}`:", value.value.value)?;
writeln!(buf)?;
for line in value.description.lines() {
writeln!(buf, "{pad} {line}")?;
}
writeln!(buf)?;
}
writeln!(buf)?;
}
Ok(())
}
fn write_array_spec(buf: &mut Vec<u8>, spec: &ArraySpec, pad: &str) -> Result<()> {
write!(buf, "{pad}Each element of this array should be ")?;
let spec = write_nestable_type_spec(buf, &spec.items, false)?;
writeln!(buf, ".")?;
writeln!(buf)?;
if let Some(spec) = spec {
write_nestable_type_restrictions(buf, spec, pad)?;
}
Ok(())
}
fn write_number_spec(buf: &mut Vec<u8>, spec: &NumberSpec, pad: &str) -> Result<()> {
if spec.integer_only {
writeln!(buf, "{pad}The numbers should be integers.")?;
writeln!(buf)?;
}
if let Some(minimum) = spec.minimum {
let greater = match spec.exclusive_minimum {
true => "strictly greater than",
false => "greater than or equal to",
};
writeln!(buf, "{pad}The numbers should be {greater} {minimum}.")?;
writeln!(buf)?;
}
Ok(())
}

184
toml-spec/src/types.rs Normal file
View file

@ -0,0 +1,184 @@
use {
error_reporter::Report,
indexmap::IndexMap,
serde::{
de::{DeserializeOwned, Error},
Deserialize, Deserializer,
},
};
#[derive(Debug, Deserialize)]
pub struct Described<T> {
pub description: String,
#[serde(flatten)]
pub value: T,
}
#[derive(Debug)]
pub enum TopLevelTypeSpec {
Variable {
variants: Vec<Described<VariantSpec>>,
},
Single(VariantSpec),
}
#[derive(Debug)]
pub enum TableSpec {
Tagged {
types: IndexMap<String, Described<SingleTableSpec>>,
},
Single(SingleTableSpec),
}
#[derive(Debug, Deserialize)]
pub struct SingleTableSpec {
pub fields: IndexMap<String, Described<TableFieldSpec>>,
}
#[derive(Debug, Deserialize)]
pub struct TableFieldSpec {
pub required: bool,
#[serde(flatten)]
pub kind: RefOrSpec<NestableTypesSpec>,
}
#[derive(Debug)]
pub enum RefOrSpec<T> {
Ref { name: String },
Spec(T),
}
#[derive(Debug, Deserialize)]
pub struct StringSpec {
pub values: Option<Vec<Described<StringSpecValue>>>,
}
#[derive(Debug, Deserialize)]
pub struct StringSpecValue {
pub value: String,
}
#[derive(Debug, Deserialize)]
pub struct NumberSpec {
#[serde(default)]
pub integer_only: bool,
pub minimum: Option<f64>,
#[serde(default)]
pub exclusive_minimum: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum VariantSpec {
String(RefOrSpec<StringSpec>),
Number(RefOrSpec<NumberSpec>),
Boolean,
Array(RefOrSpec<ArraySpec>),
Table(RefOrSpec<TableSpec>),
}
#[derive(Debug, Deserialize)]
pub struct ArraySpec {
pub items: Box<RefOrSpec<NestableTypesSpec>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum NestableTypesSpec {
String(StringSpec),
Number(NumberSpec),
Boolean,
Array(ArraySpec),
Map(MapSpec),
}
#[derive(Debug, Deserialize)]
pub struct MapSpec {
pub values: Box<RefOrSpec<NestableTypesSpec>>,
}
impl<'de> Deserialize<'de> for TopLevelTypeSpec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v = serde_yaml::Value::deserialize(deserializer)?;
#[derive(Debug, Deserialize)]
struct Variable {
variants: Vec<Described<VariantSpec>>,
}
let variable = Variable::deserialize(&v);
let single = VariantSpec::deserialize(&v);
let res = match (variable, single) {
(Ok(variable), _) => Self::Variable {
variants: variable.variants,
},
(_, Ok(single)) => Self::Single(single),
(Err(e1), Err(e2)) => {
return Err(Error::custom(format!(
"spec must define either variants or a single variant. failures: {} ----- {}",
Report::new(e1),
Report::new(e2)
)))
}
};
Ok(res)
}
}
impl<'de> Deserialize<'de> for TableSpec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v = serde_yaml::Value::deserialize(deserializer)?;
#[derive(Debug, Deserialize)]
struct Tagged {
types: IndexMap<String, Described<SingleTableSpec>>,
}
let tagged = Tagged::deserialize(&v);
let single = SingleTableSpec::deserialize(&v);
let res = match (tagged, single) {
(Ok(tagged), _) => Self::Tagged {
types: tagged.types,
},
(_, Ok(single)) => Self::Single(single),
(Err(e1), Err(e2)) => {
return Err(Error::custom(format!(
"spec must define either types or fields. failures: {} ----- {}",
Report::new(e1),
Report::new(e2)
)))
}
};
Ok(res)
}
}
impl<'de, U: DeserializeOwned> Deserialize<'de> for RefOrSpec<U> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v = serde_yaml::Value::deserialize(deserializer)?;
#[derive(Debug, Deserialize)]
struct Ref {
#[serde(rename = "ref")]
name: String,
}
let name = Ref::deserialize(&v);
let single = U::deserialize(&v);
let res = match (name, single) {
(Ok(name), _) => Self::Ref { name: name.name },
(_, Ok(single)) => Self::Spec(single),
(Err(e1), Err(e2)) => {
return Err(Error::custom(format!(
"spec must define either a ref or a spec. failures: {} ----- {}",
Report::new(e1),
Report::new(e2)
)))
}
};
Ok(res)
}
}