diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8af32f7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,319 @@ +use kdl::KdlDocument; +use serde_json::{from_str, Value}; +use std::collections::HashMap; +use std::process::Command; +use std::{fs, str}; + +/// Configuration for swayout. +struct Config { + /// Monitor name is independent of output name. It can be anything. + monitors: HashMap, + /// 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. + layouts: HashMap>, +} + +/// Defines a monitor by make, model, and serial. +struct Monitor { + make: String, + model: String, + serial: String, +} + +/// Configuration for an enabled output. +struct OutputConfig { + /// mode. See man 5 sway-output + mode: String, + /// scale. See man 5 sway-output + scale: String, + /// x value for position. See man 5 sway-output + x: u16, + /// y value for position. See man 5 sway-output + y: u16, + /// transform. See man 5 sway-output + transform: String, +} + +/// An output, as returned by `swaymsg -t get_outputs`. +struct Output { + /// output name, according to sway + name: String, + make: String, + model: String, + serial: String, + output_config: OutputConfig, +} + +/// Get all outputs currently available, according to `swaymsg -t get_outputs`. +fn get_outputs() -> Vec { + let output = Command::new("swaymsg") + .arg("-t") + .arg("get_outputs") + .output() + .expect("swaymsg -t get_outputs failed"); + let json_string = str::from_utf8(&output.stdout).unwrap(); + let json = from_str(json_string).unwrap(); + + if let Value::Array(outputs) = json { + outputs + .iter() + .map(|output_json| Output { + name: String::from(output_json["name"].as_str().unwrap()), + make: String::from(output_json["make"].as_str().unwrap()), + model: String::from(output_json["model"].as_str().unwrap()), + serial: String::from(output_json["serial"].as_str().unwrap()), + output_config: OutputConfig { + mode: get_mode(output_json), + scale: String::from("1.0"), + x: 0, + y: 0, + transform: String::from("normal"), + }, + }) + .collect() + } else { + panic!("outputs json was not an array"); + } +} + +/// Parse the mode from the output JSON into a string suitable for the mode param for sway-output. +/// This will go into `OutputConfig.mode`. +fn get_mode(output: &Value) -> String { + if let Value::Array(modes) = &output["modes"] { + if let Some(mode) = modes.first() { + let width = mode["width"].as_i64().unwrap().to_string(); + let height = mode["height"].as_i64().unwrap().to_string(); + let mut out: String = String::new(); + out.push_str(&width); + out.push_str("x"); + out.push_str(&height); + out + } else { + panic!("no modes") + } + } else { + panic!("modes not an array") + } +} + +/// 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.kdl") { + let kdl = fs::read_to_string(path).expect("error reading file"); + parse_config(kdl) + } else { + // no config files, use an empty rules + Config { + monitors: HashMap::new(), + layouts: HashMap::new(), + } + } +} +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 +/// for which all outputs are available, +/// one for each monitor defined in the configuration file (for using just that monitor), +/// 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 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)) + } + }); + + // 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)| { + 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)); + } else { + layout_names.push(String::from(&output.name)); + } + }); + + layout_names +} + +/// 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}")) +} + +/// Apply the specified layout. +/// layout_name may be the name of a layout in the config, the name of a monitor, +/// or the name of an output. +pub fn apply_layout(layout_name: &String) { + let rules = get_config(); + let outputs = get_outputs(); + + // Map of output name to config for all outputs to enable. + // (All outputs not in this map will be disabled.) + 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 { + // The layout is defined in the rules. + layout.iter().for_each(|(monitor_name, output_config)| { + let monitor = &rules.monitors[monitor_name]; + + // find the output for the monitor to get the output name. + let output_opt = outputs.iter().find(|output| { + output.make == monitor.make + && output.model == monitor.model + && output.serial == monitor.serial + }); + if let Some(output) = output_opt { + output_config_map.insert(&output.name, &output_config); + } else { + panic!("Missing output for monitor {monitor_name}"); + } + }); + } 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) { + // 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 + && output.serial == monitor.serial + }) { + output_config_map.insert(&output.name, &output.output_config); + } else { + panic!("could not find output for monitor {layout_name}") + } + } else { + // See if it is an output name... + if let Some(output) = outputs.iter() + .find(|output| &output.name == layout_name) { + output_config_map.insert(&output.name, &output.output_config); + } else { + panic!("could not find layout, monitor, or output {layout_name}") + } + } + } + + apply_outputs(&outputs, &output_config_map); +} + +/// Apply the specified outputs. Enable all outputs in [outputs], disable others. +fn apply_outputs(all_outputs: &Vec, outputs: &HashMap<&String, &OutputConfig>) { + + // set enabled outputs first, then set disabled outputs. + // That way if some work before an error, you have at least one output enabled. + + // for outputs to be enabled: map of output name to config + let mut enabled: HashMap<&String,&OutputConfig> = HashMap::new(); + // for outputs to be disabled: output names + let mut disabled: Vec<&String> = Vec::new(); + + all_outputs.iter().for_each(|output| { + if let Some(output_config) = outputs.get(&output.name) { + enabled.insert(&output.name, &output_config); + } else { + disabled.push(&output.name); + } + }); + + let mut cmd = Command::new("swaymsg"); + enabled.iter().for_each(|(output_name,output_config)| { + cmd.arg("output"); + cmd.arg(&output_name); + cmd.arg("enable"); + cmd.arg("mode"); + cmd.arg(&output_config.mode); + cmd.arg("scale"); + cmd.arg(&output_config.scale); + cmd.arg("pos"); + cmd.arg(&output_config.x.to_string()); + cmd.arg(&output_config.y.to_string()); + cmd.arg("transform"); + cmd.arg(&output_config.transform); + cmd.arg(","); + }); + + disabled.iter().for_each(|output_name| { + cmd.arg("output"); + cmd.arg(&output_name); + cmd.arg("disable"); + cmd.arg(","); + }); + + // print what we are about to do. + cmd.get_args() + .for_each(|arg| print!("{} ", arg.to_str().unwrap())); + + cmd.output().expect("swaymsg output failed"); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 35db049..0e97482 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,332 +1,13 @@ -use kdl::KdlDocument; -use serde_json::{from_str, Value}; -use std::collections::HashMap; use std::env; -use std::process::Command; -use std::{fs, str}; - -/// Configuration for swayout. -struct Config { - /// Monitor name is independent of output name. It can be anything. - monitors: HashMap, - /// 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. - layouts: HashMap>, -} - -/// Defines a monitor by make, model, and serial. -struct Monitor { - make: String, - model: String, - serial: String, -} - -/// Configuration for an enabled output. -struct OutputConfig { - /// mode. See man 5 sway-output - mode: String, - /// scale. See man 5 sway-output - scale: String, - /// x value for position. See man 5 sway-output - x: u16, - /// y value for position. See man 5 sway-output - y: u16, - /// transform. See man 5 sway-output - transform: String, -} - -/// An output, as returned by `swaymsg -t get_outputs`. -struct Output { - /// output name, according to sway - name: String, - make: String, - model: String, - serial: String, - output_config: OutputConfig, -} fn main() { let args: Vec = env::args().collect(); if args.len() == 1 { // first arg is executable - print_layout_names(); + swayout::print_layout_names(); } else if args.len() == 2 { - apply_layout(&args[1]); + swayout::apply_layout(&args[1]); } else { panic!("Usage: {} [layout]", args[0].as_str()); } -} - -/// Get all outputs currently available, according to `swaymsg -t get_outputs`. -fn get_outputs() -> Vec { - let output = Command::new("swaymsg") - .arg("-t") - .arg("get_outputs") - .output() - .expect("swaymsg -t get_outputs failed"); - let json_string = str::from_utf8(&output.stdout).unwrap(); - let json = from_str(json_string).unwrap(); - - if let Value::Array(outputs) = json { - outputs - .iter() - .map(|output_json| Output { - name: String::from(output_json["name"].as_str().unwrap()), - make: String::from(output_json["make"].as_str().unwrap()), - model: String::from(output_json["model"].as_str().unwrap()), - serial: String::from(output_json["serial"].as_str().unwrap()), - output_config: OutputConfig { - mode: get_mode(output_json), - scale: String::from("1.0"), - x: 0, - y: 0, - transform: String::from("normal"), - }, - }) - .collect() - } else { - panic!("outputs json was not an array"); - } -} - -/// Parse the mode from the output JSON into a string suitable for the mode param for sway-output. -/// This will go into `OutputConfig.mode`. -fn get_mode(output: &Value) -> String { - if let Value::Array(modes) = &output["modes"] { - if let Some(mode) = modes.first() { - let width = mode["width"].as_i64().unwrap().to_string(); - let height = mode["height"].as_i64().unwrap().to_string(); - let mut out: String = String::new(); - out.push_str(&width); - out.push_str("x"); - out.push_str(&height); - out - } else { - panic!("no modes") - } - } else { - panic!("modes not an array") - } -} - -/// 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.kdl") { - let kdl = fs::read_to_string(path).expect("error reading file"); - parse_config(kdl) - } else { - // no config files, use an empty rules - Config { - monitors: HashMap::new(), - layouts: HashMap::new(), - } - } -} -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 -/// for which all outputs are available, -/// one for each monitor defined in the configuration file (for using just that monitor), -/// 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 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)) - } - }); - - // 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)| { - 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)); - } else { - layout_names.push(String::from(&output.name)); - } - }); - - layout_names -} - -/// Print the name of each layout to standard out, one per line -fn print_layout_names() { - get_layouts().iter().for_each(|layout| println!("{layout}")) -} - -/// Apply the specified layout. -/// layout_name may be the name of a layout in the config, the name of a monitor, -/// or the name of an output. -fn apply_layout(layout_name: &String) { - let rules = get_config(); - let outputs = get_outputs(); - - // Map of output name to config for all outputs to enable. - // (All outputs not in this map will be disabled.) - 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 { - // The layout is defined in the rules. - layout.iter().for_each(|(monitor_name, output_config)| { - let monitor = &rules.monitors[monitor_name]; - - // find the output for the monitor to get the output name. - let output_opt = outputs.iter().find(|output| { - output.make == monitor.make - && output.model == monitor.model - && output.serial == monitor.serial - }); - if let Some(output) = output_opt { - output_config_map.insert(&output.name, &output_config); - } else { - panic!("Missing output for monitor {monitor_name}"); - } - }); - } 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) { - // 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 - && output.serial == monitor.serial - }) { - output_config_map.insert(&output.name, &output.output_config); - } else { - panic!("could not find output for monitor {layout_name}") - } - } else { - // See if it is an output name... - if let Some(output) = outputs.iter() - .find(|output| &output.name == layout_name) { - output_config_map.insert(&output.name, &output.output_config); - } else { - panic!("could not find layout, monitor, or output {layout_name}") - } - } - } - - apply_outputs(&outputs, &output_config_map); -} - -/// Apply the specified outputs. Enable all outputs in [outputs], disable others. -fn apply_outputs(all_outputs: &Vec, outputs: &HashMap<&String, &OutputConfig>) { - - // set enabled outputs first, then set disabled outputs. - // That way if some work before an error, you have at least one output enabled. - - // for outputs to be enabled: map of output name to config - let mut enabled: HashMap<&String,&OutputConfig> = HashMap::new(); - // for outputs to be disabled: output names - let mut disabled: Vec<&String> = Vec::new(); - - all_outputs.iter().for_each(|output| { - if let Some(output_config) = outputs.get(&output.name) { - enabled.insert(&output.name, &output_config); - } else { - disabled.push(&output.name); - } - }); - - let mut cmd = Command::new("swaymsg"); - enabled.iter().for_each(|(output_name,output_config)| { - cmd.arg("output"); - cmd.arg(&output_name); - cmd.arg("enable"); - cmd.arg("mode"); - cmd.arg(&output_config.mode); - cmd.arg("scale"); - cmd.arg(&output_config.scale); - cmd.arg("pos"); - cmd.arg(&output_config.x.to_string()); - cmd.arg(&output_config.y.to_string()); - cmd.arg("transform"); - cmd.arg(&output_config.transform); - cmd.arg(","); - }); - - disabled.iter().for_each(|output_name| { - cmd.arg("output"); - cmd.arg(&output_name); - cmd.arg("disable"); - cmd.arg(","); - }); - - // print what we are about to do. - cmd.get_args() - .for_each(|arg| print!("{} ", arg.to_str().unwrap())); - - cmd.output().expect("swaymsg output failed"); } \ No newline at end of file