From ce5bb9f4af0606dc155d07c25408059f3ac9836b Mon Sep 17 00:00:00 2001 From: stephen Date: Wed, 18 Dec 2024 04:35:15 +0000 Subject: [PATCH] use KDL instead of TOML --- Cargo.lock | 132 ++++++++++++++++++++++++++++------------------------ Cargo.toml | 8 ++-- README.md | 58 ++++++++--------------- src/main.rs | 67 ++++++++++++++++++++------ 4 files changed, 146 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ef03c4..5168940 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,40 +2,74 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "indexmap" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "kdl" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e03e2e96c5926fe761088d66c8c2aee3a4352a2573f4eaca50043ad130af9117" +dependencies = [ + "miette", + "nom", + "thiserror", +] + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "proc-macro2" version = "1.0.92" @@ -92,22 +126,13 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - [[package]] name = "swayout" version = "1.0.0" dependencies = [ + "kdl", "serde", "serde_json", - "toml", "xdg", ] @@ -123,37 +148,23 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.8.19" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "thiserror-impl", ] [[package]] -name = "toml_datetime" -version = "0.6.8" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -163,13 +174,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] -name = "winnow" -version = "0.6.20" +name = "unicode-width" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" -dependencies = [ - "memchr", -] +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "xdg" diff --git a/Cargo.toml b/Cargo.toml index b5bcbd2..4d6b7e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,11 @@ authors = ["Stephen Byrne"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -# for config yaml -serde = { version = "1.0", features = ["derive"] } -toml = "0.8.19" - # for json output from swaymsg +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.133" +# for config file +kdl = "4.7.1" + xdg = "2.5.2" \ No newline at end of file diff --git a/README.md b/README.md index 550d204..1ae876b 100644 --- a/README.md +++ b/README.md @@ -5,55 +5,35 @@ [kanshi](https://sr.ht/~emersion/kanshi/) and [way-displays](https://github.com/alex-courtis/way-displays) certainly do a better job of this. I wrote this because I wanted a project to learn Rust. -Monitors and layouts are configured in a toml file -(`~/.config/swayout/config.toml`). +Monitors and layouts are configured in a [KDL](https://kdl.dev) file +(`~/.config/swayout/config.kdl`). -Example `config.toml`: +Example `config.kdl`: -```toml -# This is my laptop's built-in screen. -[monitors.laptop] -make = "Unknown" -model = "0x403D" -serial = "0x00000000" +```kdl +// This is my laptop's built-in screen. +monitor "laptop" make="Unknown" model="0x403D" serial="0x00000000" -# I usually have it hooked up to this monitor, which is the top one on my desk. -[monitors.top] -make = "Dell Inc." -model = "DELL U2415" -serial = "CFV9N99T0Y0U" +// I usually have it hooked up to this monitor, which is the top one on my desk. +monitor "top" make="Dell Inc." model="DELL U2415" serial="CFV9N99T0Y0U" -# Sometimes I hook it up to these monitors, which are left and right. -# The left is rotated 270 degrees. -[monitors.left] -make = "Goldstar Company Ltd" -model = "LG HDR 4K" -serial = "0x0000B9C0" -[monitors.right] -make = "Goldstar Company Ltd" -model = "LG HDR 4K" -serial = "0x0000B9BF" +// Sometimes I hook it up to these monitors, which are left and right. +// The left is in portrait. +monitor "left" make="Goldstar Company Ltd" model="LG HDR 4K" serial="0x0000B9C0" +monitor "right" make="Goldstar Company Ltd" model="LG HDR 4K" serial="0x0000B9BF" -# When I have "left" and "right" hooked up, I want this layout. -[layouts.two4k.left] -mode = "3840x2160" -scale = "1.5" -x = 0 -y = 0 -transform = "270" -[layouts.two4k.right] -mode = "3840x2160" -scale = "1.5" -x = 1440 -y = 1120 -transform = "normal" +// When I have "left" and "right" hooked up, I want this layout. +layout "two4k" { + output "left" mode="3840x2160" scale="1.5" x=0 y=0 transform="270" + output "right" mode="3840x2160" scale="1.5" x=1440 y=1120 transform="normal" +} ``` When you run `swayout`, it prints all available layout names to standard output, one per line. Printed layouts include: -* All layouts defined in `config.toml` for which the monitors are currently available. E.g.: `two4k`. -* All monitors defined in `config.toml` that are currently available. E.g.: `laptop`, `left`, `right`. +* All layouts defined in `config.kdl` for which the monitors are currently available. E.g.: `two4k`. +* All monitors defined in `config.kdl` that are currently available. E.g.: `laptop`, `left`, `right`. * All available outputs that are not matched by a configured monitor. E.g.: `HDMI-2`. If you pass a layout name (or monitor name, or output name) as a command line argument to `swayout`, diff --git a/src/main.rs b/src/main.rs index 8c9a944..35db049 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use kdl::KdlDocument; use serde_json::{from_str, Value}; use std::collections::HashMap; use std::env; @@ -6,7 +6,6 @@ use std::process::Command; use std::{fs, str}; /// Configuration for swayout. -#[derive(Serialize, Deserialize, Debug)] struct Config { /// Monitor name is independent of output name. It can be anything. monitors: HashMap, @@ -16,15 +15,15 @@ struct Config { /// Available outputs that do not match a monitor in the map are disabled. layouts: HashMap>, } + /// Defines a monitor by make, model, and serial. -#[derive(Serialize, Deserialize, Debug)] struct Monitor { make: String, model: String, serial: String, } + /// Configuration for an enabled output. -#[derive(Serialize, Deserialize, Debug)] struct OutputConfig { /// mode. See man 5 sway-output mode: String, @@ -112,13 +111,13 @@ fn get_mode(output: &Value) -> String { } } -/// Get a `Config` from ~/.config/swayout/config.toml. +/// Get a `Config` from ~/.config/swayout/config.kdl. /// (It really uses XDG config dirs.) fn get_config() -> Config { let xdg_dirs = xdg::BaseDirectories::with_prefix("swayout").unwrap(); - if let Some(path) = xdg_dirs.find_config_file("config.toml") { - let toml = fs::read_to_string(path).expect("error reading file"); - toml::from_str(&toml).expect("toml parse error") + if let Some(path) = xdg_dirs.find_config_file("config.kdl") { + let kdl = fs::read_to_string(path).expect("error reading file"); + parse_config(kdl) } else { // no config files, use an empty rules Config { @@ -127,6 +126,46 @@ fn get_config() -> Config { } } } +fn parse_config(kdl:String) -> Config { + let doc:KdlDocument = kdl.parse().expect("failed to parse config KDL"); + + let monitors:HashMap = doc.nodes().iter() + .filter(|node| node.name().value() == "monitor") + .map(|monitor_node| { + let name = String::from(monitor_node[0].as_string().unwrap()); + let monitor = Monitor { + make : String::from(monitor_node["make"].as_string().unwrap()), + model : String::from(monitor_node["model"].as_string().unwrap()), + serial : String::from(monitor_node["serial"].as_string().unwrap()), + }; + (name, monitor) + }) + .collect(); + + let layouts:HashMap> = doc.nodes().iter() + .filter(|node| node.name().value() == "layout") + .map(|layout_node| { + let layout_name = String::from(layout_node[0].as_string().unwrap()); + let monitor_name_to_output_config:HashMap = layout_node + .children().unwrap().nodes().iter() + .map(|output_node| { + let output = String::from(output_node[0].as_string().unwrap()); + let output_config = OutputConfig { + mode : String::from(output_node["mode"].as_string().unwrap()), + scale : String::from(output_node["scale"].as_string().unwrap()), + transform : String::from(output_node["transform"].as_string().unwrap()), + x : output_node["x"].as_i64().unwrap() as u16, + y : output_node["y"].as_i64().unwrap() as u16, + }; + (output, output_config) + }) + .collect(); + (layout_name,monitor_name_to_output_config) + }) + .collect(); + + Config { monitors, layouts } +} /// Determine the available layout names. /// Return one layout for each layout defined in the configuration file @@ -135,12 +174,10 @@ fn get_config() -> Config { /// and one for each available output that is not a configured monitor (for using just that output). fn get_layouts() -> Vec { let available_outputs = get_outputs(); - let rules = get_config(); - - let mut layout_names: Vec = Vec::new(); + let config = get_config(); // Get the names of monitors that are available (that match an available output) - let available_monitor_names:Vec<&String> = rules.monitors.iter() + let available_monitor_names:Vec<&String> = config.monitors.iter() .filter(|(_monitor_name,monitor)| available_outputs.iter().any(|output| monitor.make == output.make @@ -151,8 +188,10 @@ fn get_layouts() -> Vec { .map(|(monitor_name,_monitor)| monitor_name) .collect(); + let mut layout_names: Vec = Vec::new(); + // Add each layout defined in the config file for which all outputs are available - rules.layouts.iter().for_each(|(layout_name, layout)| { + config.layouts.iter().for_each(|(layout_name, layout)| { if layout .iter() .all(|(output_name, _output)| available_monitor_names.contains(&output_name)) @@ -164,7 +203,7 @@ fn get_layouts() -> Vec { // add each individual output (by monitor name if the monitor is defined, else by output name) available_outputs.iter().for_each(|output| { // if there is a monitor for the output, use the monitor name, else use the output name - let monitor_opt = rules.monitors.iter() + let monitor_opt = config.monitors.iter() .find(|(_monitor_name, monitor)| { monitor.make == output.make && monitor.model == output.model