diff --git a/Cargo.toml b/Cargo.toml index f988908..c01836b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swayout" -version = "1.0.0" +version = "1.1.0" edition = "2021" description = "A tool to configure sway outputs" authors = ["Stephen Byrne"] @@ -15,4 +15,4 @@ serde_json = "1.0.133" # for config file kdl = "6.1.0" -xdg = "2.5.2" \ No newline at end of file +xdg = "2.5.2" diff --git a/README.md b/README.md index 1ae876b..849261d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ 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. -layout "two4k" { +layout "two4k" automatic=#true { 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" } @@ -48,16 +48,12 @@ set $select_layout swayout | tofi | xargs swayout bindsym $mod+Shift+m exec $select_layout ``` -## TODO - -Things I might do someday: - -### Automatic - -Add a `automatic` option to layouts and an `--automatic` flag to `swayout`, and have it automatically enable the -first layout with `automatic=true` for which all outputs are available. +If `swayout` is called with the `--automatic` switch, the first layout with `automatic=#true` for which all outputs +are available is enabled. If no automatic layouts are available, `swayout` exits with code 2. This could be run by `bindswitch lid:toggle exec swayout --automatic`. +## TODO + How to call when an output is connected or disconnected? Wayland events via [wayland_client](https://docs.rs/wayland-client/latest/wayland_client/index.html)? \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 09aab00..6b8b001 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,16 +5,23 @@ use kdl::KdlDocument; /// Configuration for swayout. pub struct Config { /// Monitor name is independent of output name. It can be anything. - pub monitors: HashMap, + pub monitors: Vec, /// Definition of layouts: a map of the layout name to the outputs. /// The outputs is a map of the monitor name (in `monitors`) to the configuration /// of that monitor for this layout. /// Available outputs that do not match a monitor in the map are disabled. - pub layouts: HashMap>, + pub layouts: Vec, +} + +pub struct Layout { + pub name: String, + pub automatic: bool, + pub outputs: HashMap, } /// Defines a monitor by make, model, and serial. pub struct Monitor { + pub name: String, pub make: String, pub model: String, pub serial: String, @@ -44,31 +51,31 @@ pub fn get_config() -> Config { } else { // no config files, use an empty rules Config { - monitors: HashMap::new(), - layouts: HashMap::new(), + monitors: Vec::new(), + layouts: Vec::new(), } } } fn parse_config(kdl:String) -> Config { let doc:KdlDocument = kdl.parse().expect("failed to parse config KDL"); - let monitors:HashMap = doc.nodes().iter() + let monitors:Vec = 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 { + Monitor { + name : String::from(monitor_node[0].as_string().unwrap()), 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() + let layouts:Vec = doc.nodes().iter() .filter(|node| node.name().value() == "layout") .map(|layout_node| { let layout_name = String::from(layout_node[0].as_string().unwrap()); + let automatic = layout_node.get("automatic").is_some_and(|v| v.as_bool().is_some_and(|a| a)); let monitor_name_to_output_config:HashMap = layout_node .children().unwrap().nodes().iter() .map(|output_node| { @@ -83,7 +90,11 @@ fn parse_config(kdl:String) -> Config { (output, output_config) }) .collect(); - (layout_name,monitor_name_to_output_config) + Layout { + name : layout_name, + automatic: automatic, + outputs: monitor_name_to_output_config + } }) .collect(); diff --git a/src/lib.rs b/src/lib.rs index 8d5f0c6..065a558 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,10 @@ use std::collections::HashMap; mod config; -use crate::config::{get_config,OutputConfig}; +use crate::config::{get_config, Config, Layout, OutputConfig}; mod sway; -use crate::sway::{apply_outputs,get_outputs}; +use crate::sway::{apply_outputs, get_outputs, Output}; /// Determine the available layout names. /// Return one layout for each layout defined in the configuration file @@ -15,41 +15,27 @@ fn get_layouts() -> Vec { let available_outputs = get_outputs(); let config = get_config(); - // Get the names of monitors that are available (that match an available output) - let available_monitor_names:Vec<&String> = config.monitors.iter() - .filter(|(_monitor_name,monitor)| - available_outputs.iter().any(|output| - monitor.make == output.make - && monitor.model == output.model - && output.serial == monitor.serial - ) - ) - .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 - config.layouts.iter().for_each(|(layout_name, layout)| { - if layout - .iter() - .all(|(output_name, _output)| available_monitor_names.contains(&output_name)) - { - layout_names.push(String::from(layout_name)) - } - }); + get_available_layouts(&config, &available_outputs) + .iter() + .for_each(|layout| { + layout_names.push(String::from(&layout.name)); + }); // 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 = config.monitors.iter() - .find(|(_monitor_name, monitor)| { + .find(|monitor| { monitor.make == output.make && monitor.model == output.model && monitor.serial == output.serial }); - if let Some((monitor_name, _monitor)) = monitor_opt { - layout_names.push(String::from(monitor_name)); + if let Some(monitor) = monitor_opt { + layout_names.push(String::from(&monitor.name)); } else { layout_names.push(String::from(&output.name)); } @@ -58,6 +44,38 @@ fn get_layouts() -> Vec { layout_names } +/// Get the names of monitors that are available (that match an available output) +fn get_available_monitor_names<'a,'b>(config:&'a Config, available_outputs:&'b Vec) -> Vec<&'a String> { + config.monitors.iter() + .filter(|monitor| + available_outputs.iter().any(|output| + monitor.make == output.make + && monitor.model == output.model + && output.serial == monitor.serial + ) + ) + .map(|monitor| &monitor.name) + .collect() +} + +/// Get the layouts for which all monitors are available. +fn get_available_layouts<'a,'b>(config:&'a Config, available_outputs:&'b Vec) -> Vec<&'a Layout> { + let available_monitor_names:Vec<&String> = get_available_monitor_names(&config, available_outputs); + + let mut layouts: Vec<&'a Layout> = Vec::new(); + + // Add each layout defined in the config file for which all outputs are available + config.layouts.iter().for_each(|layout| { + if layout.outputs.iter() + .all(|(output_name, _output)| available_monitor_names.contains(&output_name)) + { + layouts.push(&layout); + } + }); + + layouts +} + /// Print the name of each layout to standard out, one per line pub fn print_layout_names() { get_layouts().iter().for_each(|layout| println!("{layout}")) @@ -75,11 +93,10 @@ pub fn apply_layout(layout_name: &String) { let mut output_config_map: HashMap<&String, &OutputConfig> = HashMap::new(); // First check if the layout is defined in the rules - let opt_layout = rules.layouts.get(layout_name); - if let Some(layout) = opt_layout { + if let Some(layout) = rules.layouts.iter().find(|layout| &layout.name == layout_name) { // The layout is defined in the rules. - layout.iter().for_each(|(monitor_name, output_config)| { - let monitor = &rules.monitors[monitor_name]; + layout.outputs.iter().for_each(|(monitor_name, output_config)| { + let monitor = &rules.monitors.iter().find(|monitor| &monitor.name == monitor_name).unwrap(); // find the output for the monitor to get the output name. let output_opt = outputs.iter().find(|output| { @@ -96,7 +113,7 @@ pub fn apply_layout(layout_name: &String) { } else { // The layout is not defined in the rules. // See if it is a monitor name... - if let Some(monitor) = rules.monitors.get(layout_name) { + if let Some(monitor) = rules.monitors.iter().find(|monitor| &monitor.name == layout_name) { // It is a monitor name. Find the matching output... if let Some(output) = outputs.iter().find(|output| { output.make == monitor.make && output.model == monitor.model @@ -119,3 +136,19 @@ pub fn apply_layout(layout_name: &String) { apply_outputs(&outputs, &output_config_map); } + +/// Apply the first automatic layout for which all outputs are available. +/// Return the name of the layout applied or None if no automatic layout was available. +pub fn apply_automatic() -> Option { + let config = get_config(); + let outputs = get_outputs(); + + if let Some(layout) = get_available_layouts(&config, &outputs) + .iter() + .find(|layout| layout.automatic) { + apply_layout(&layout.name); + Some(String::from(&layout.name)) + } else { + None + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 0e97482..44fa79d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::env; +use std::{env, process}; fn main() { let args: Vec = env::args().collect(); @@ -6,8 +6,19 @@ fn main() { // first arg is executable swayout::print_layout_names(); } else if args.len() == 2 { - swayout::apply_layout(&args[1]); + let arg = &args[1]; + if arg == "--automatic" { + if let Some(layout_name) = swayout::apply_automatic() { + println!("{}", layout_name); + } else { + eprintln!("no automatic layout available"); + process::exit(2) + } + } else { + swayout::apply_layout(&args[1]); + } } else { - panic!("Usage: {} [layout]", args[0].as_str()); + eprintln!("Usage: {} [layout]", args[0].as_str()); + process::exit(1) } } \ No newline at end of file