diff --git a/Cargo.lock b/Cargo.lock index fb8211b..9d2f65d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,7 +192,7 @@ dependencies = [ [[package]] name = "swayout" -version = "1.1.1" +version = "1.2.0" dependencies = [ "kdl", "serde", diff --git a/Cargo.toml b/Cargo.toml index c00499c..475e5cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swayout" -version = "1.1.1" +version = "1.2.0" edition = "2021" description = "A tool to configure sway outputs" authors = ["Stephen Byrne"] diff --git a/README.md b/README.md index 849261d..50b5582 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Example `config.kdl`: ```kdl // This is my laptop's built-in screen. -monitor "laptop" make="Unknown" model="0x403D" serial="0x00000000" +monitor "laptop" make="Unknown" model="0x403D" serial="0x00000000" lid="LID" // 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" @@ -35,6 +35,9 @@ Printed layouts include: * 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 a monitor has its `lid` property is set to the name of a directory in `/proc/acpi/button/lid/`, if the `state` file +in that directory indicates that the lid is closed, the monitor will not be considered available. If you pass a layout name (or monitor name, or output name) as a command line argument to `swayout`, it calls `swaymsg` to configure that layout. diff --git a/src/config.rs b/src/config.rs index da1be46..8a84dff 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,6 +16,7 @@ pub struct Config { pub struct Layout { pub name: String, pub automatic: bool, + /// Map of monitor name to OutputConfig pub outputs: HashMap, } @@ -25,6 +26,7 @@ pub struct Monitor { pub make: String, pub model: String, pub serial: String, + pub lid: Option, } /// Configuration for an enabled output. @@ -68,6 +70,11 @@ fn parse_config(kdl: String) -> Config { 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()), + lid: if let Some(lid) = monitor_node.get("lid") { + Some(String::from(lid.as_string().unwrap())) + } else { + None + }, }) .collect(); diff --git a/src/lib.rs b/src/lib.rs index 59d0663..5615ac8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,39 +1,42 @@ use std::collections::{HashMap, HashSet}; mod config; -use crate::config::{get_config, Config, Layout, OutputConfig}; +use crate::config::{get_config, Config, Layout, Monitor, OutputConfig}; + +mod lid; +use crate::lid::is_lid_closed; mod sway; use crate::sway::{apply_outputs, get_outputs, Output}; /// 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), +/// for which all outputs are available (and lids are not closed), +/// one for each monitor defined in the configuration file (for using just that monitor) if the lid is not closed, /// and one for each available output that is not a configured monitor (for using just that output). fn get_layouts() -> HashSet { - let available_outputs = get_outputs(); + let outputs = get_outputs(); + let config = get_config(); + let monitor_states = get_monitor_states(&config, &outputs); let mut layout_names: HashSet = HashSet::new(); // Add each layout defined in the config file for which all outputs are available - get_available_layouts(&config, &available_outputs) + get_available_layouts(&config, &monitor_states) .iter() .for_each(|layout| { layout_names.insert(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| { + 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| { - monitor.make == output.make - && monitor.model == output.model - && monitor.serial == output.serial - }); - if let Some(monitor) = monitor_opt { - layout_names.insert(String::from(&monitor.name)); + if let Some(monitor_state) = get_monitor_state_for_output(&monitor_states, &output) { + // do not include it if the lid is closed + if !monitor_state.is_lid_closed { + layout_names.insert(String::from(&monitor_state.monitor.name)); + } } else { layout_names.insert(String::from(&output.name)); } @@ -42,47 +45,84 @@ fn get_layouts() -> HashSet { layout_names } -/// Get the names of monitors that are available (that match an available output) -fn get_available_monitor_names<'a, 'b>( +/// Monitor and lid state. Created for available monitors. +struct MonitorState<'a> { + monitor: &'a Monitor, + is_lid_closed: bool, +} + +/// Get a MonitorState for each monitor that is available (matches an output). +fn get_monitor_states<'a, 'b>( config: &'a Config, - available_outputs: &'b Vec, -) -> Vec<&'a String> { + outputs: &'b Vec, +) -> Vec> { config .monitors .iter() .filter(|monitor| { - available_outputs.iter().any(|output| { - monitor.make == output.make - && monitor.model == output.model - && output.serial == monitor.serial - }) + outputs + .iter() + .any(|output| monitor_matches_output(&monitor, &output)) + }) + .map(|monitor| MonitorState { + monitor, + is_lid_closed: is_lid_closed(&monitor), }) - .map(|monitor| &monitor.name) .collect() } -/// Get the layouts for which all monitors are available. -fn get_available_layouts<'a, 'b>( +/// Find a monitor state that matches the supplied output. +fn get_monitor_state_for_output<'a, 'b>( + monitor_states: &'a Vec>, + output: &'b &Output, +) -> Option<&'a MonitorState<'a>> { + monitor_states + .iter() + .find(|monitor_state| monitor_matches_output(monitor_state.monitor, output)) +} + +/// Determine if the specified monitor matches the specified output (by make/model/serial). +fn monitor_matches_output(monitor: &Monitor, output: &Output) -> bool { + monitor.make == output.make && monitor.model == output.model && monitor.serial == output.serial +} + +/// Find a monitor state by monitor name. +fn get_monitor_state_by_name<'a, 'b>( + monitor_states: &'a Vec>, + monitor_name: &'b String, +) -> Option<&'a MonitorState<'a>> { + monitor_states + .iter() + .find(|monitor_state| &monitor_state.monitor.name == monitor_name) +} + +/// Determine if the specified monitor name is available: +/// if it is one of the known monitors and the lid is not closed. +fn is_monitor_available<'a, 'b>( + monitor_states: &'a Vec>, + monitor_name: &'b String, +) -> bool { + if let Some(monitor_state) = get_monitor_state_by_name(&monitor_states, monitor_name) { + !monitor_state.is_lid_closed + } else { + false + } +} + +/// Get the layouts for which all monitors are available and lids are not closed. +fn get_available_layouts<'a, 'b, 'c>( config: &'a Config, - available_outputs: &'b Vec, + monitor_states: &'a 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 + config + .layouts + .iter() + .filter(|layout| { + layout.outputs.iter().all(|(monitor_name, _output_config)| { + is_monitor_available(monitor_states, monitor_name) + }) + }) + .collect() } /// Print the name of each layout to standard out, one per line @@ -94,7 +134,7 @@ pub fn print_layout_names() { /// 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 config = get_config(); let outputs = get_outputs(); // Map of output name to config for all outputs to enable. @@ -102,7 +142,7 @@ 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 - if let Some(layout) = rules + if let Some(layout) = config .layouts .iter() .find(|layout| &layout.name == layout_name) @@ -112,18 +152,16 @@ pub fn apply_layout(layout_name: &String) { .outputs .iter() .for_each(|(monitor_name, output_config)| { - let monitor = &rules + let monitor = &config .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| { - output.make == monitor.make - && output.model == monitor.model - && output.serial == monitor.serial - }); + let output_opt = outputs + .iter() + .find(|output| monitor_matches_output(monitor, output)); if let Some(output) = output_opt { output_config_map.insert(&output.name, &output_config); } else { @@ -133,17 +171,16 @@ 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 + if let Some(monitor) = config .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 - && output.serial == monitor.serial - }) { + if let Some(output) = outputs + .iter() + .find(|output| monitor_matches_output(monitor, output)) + { output_config_map.insert(&output.name, &output.output_config); } else { panic!("could not find output for monitor {layout_name}") @@ -164,10 +201,12 @@ pub fn apply_layout(layout_name: &String) { /// 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) + let config = get_config(); + let monitor_states = get_monitor_states(&config, &outputs); + + if let Some(layout) = get_available_layouts(&config, &monitor_states) .iter() .find(|layout| layout.automatic) { diff --git a/src/lid.rs b/src/lid.rs new file mode 100644 index 0000000..2830979 --- /dev/null +++ b/src/lid.rs @@ -0,0 +1,45 @@ +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use crate::config::Monitor; + +pub fn is_lid_closed(monitor: &Monitor) -> bool { + if let Some(lid) = &monitor.lid { + is_acpi_closed(lid) + } else { + false + } +} + +/// Determine if the lid is closed. +/// lid is the name of a directory in /proc/acpi/button/lid/. +fn is_acpi_closed(lid: &String) -> bool { + let mut path_buf = PathBuf::from("/proc/acpi/button/lid"); + path_buf.push(&lid); + path_buf.push("state"); + + if let Ok(ok) = path_buf.try_exists() { + if ok { + File::open(path_buf).is_ok_and(|mut file| { + let mut str = String::new(); + if let Ok(_size) = file.read_to_string(&mut str) { + is_state_closed(str) + } else { + // error reading file + false + } + }) + } else { + // no file + false + } + } else { + // error checking for file + false + } +} + +/// Parse a /proc/acpi/button/lid/*/state file and return true if the lid is closed +fn is_state_closed(str:String) -> bool { + str.contains("closed") +} \ No newline at end of file