diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cc0df24..ec340e07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to eww will be listed here, starting at changes since versio ## [Unreleased] +### BREAKING CHANGES +- Remove `eww windows` command, replace with `eww active-windows` and `eww list-windows` + ### Features - Add `:namespace` window option - Default to building with x11 and wayland support simultaneously @@ -21,6 +24,8 @@ All notable changes to eww will be listed here, starting at changes since versio - Add trigonometric functions (`sin`, `cos`, `tan`, `cot`) and degree/radian conversions (`degtorad`, `radtodeg`) (By: end-4) - Add `substring` function to simplexpr - Add `--duration` flag to `eww open` +- Add support for referring to monitor with `` +- Add support for multiple matchers in `monitor` field ## [0.4.0] (04.09.2022) @@ -72,6 +77,7 @@ All notable changes to eww will be listed here, starting at changes since versio - Add `:onaccept` to input field, add `:onclick` to eventbox - Add `EWW_CMD`, `EWW_CONFIG_DIR`, `EWW_EXECUTABLE` magic variables - Add `overlay` widget (By: viandoxdev) +- Add arguments option to `defwindow` (By: WilfSilver) ### Notable Internal changes - Rework state management completely, now making local state and dynamic widget hierarchy changes possible. diff --git a/crates/eww/src/app.rs b/crates/eww/src/app.rs index 3b306be7..a3bc918a 100644 --- a/crates/eww/src/app.rs +++ b/crates/eww/src/app.rs @@ -7,15 +7,18 @@ use crate::{ paths::EwwPaths, script_var_handler::ScriptVarHandlerHandle, state::scope_graph::{ScopeGraph, ScopeIndex}, + window_arguments::WindowArguments, + window_initiator::WindowInitiator, *, }; use anyhow::anyhow; use codespan_reporting::files::Files; use eww_shared_util::{Span, VarName}; +use gdk::Monitor; use glib::ObjectExt; use itertools::Itertools; use once_cell::sync::Lazy; -use simplexpr::dynval::DynVal; +use simplexpr::{dynval::DynVal, SimplExpr}; use std::{ cell::RefCell, collections::{HashMap, HashSet}, @@ -26,7 +29,6 @@ use yuck::{ config::{ monitor::MonitorIdentifier, script_var_definition::ScriptVarDefinition, - window_definition::WindowDefinition, window_geometry::{AnchorPoint, WindowGeometry}, }, error::DiagError, @@ -44,12 +46,14 @@ pub enum DaemonCommand { ReloadConfigAndCss(DaemonResponseSender), OpenInspector, OpenMany { - windows: Vec, + windows: Vec<(String, String)>, + args: Vec<(String, VarName, DynVal)>, should_toggle: bool, sender: DaemonResponseSender, }, OpenWindow { window_name: String, + instance_id: Option, pos: Option, size: Option, anchor: Option, @@ -57,6 +61,7 @@ pub enum DaemonCommand { should_toggle: bool, duration: Option, sender: DaemonResponseSender, + args: Option>, }, CloseWindows { windows: Vec, @@ -74,12 +79,17 @@ pub enum DaemonCommand { }, PrintDebug(DaemonResponseSender), PrintGraph(DaemonResponseSender), - PrintWindows(DaemonResponseSender), + ListWindows(DaemonResponseSender), + ListActiveWindows(DaemonResponseSender), } /// An opened window. #[derive(Debug)] pub struct EwwWindow { + /// Every window has an id, uniquely identifying it. + /// If no specific ID was specified whilst starting the window, + /// this will be the same as the window name. + pub instance_id: String, pub name: String, pub scope_index: ScopeIndex, pub gtk_window: gtk::Window, @@ -104,8 +114,9 @@ pub struct App { pub display_backend: B, pub scope_graph: Rc>, pub eww_config: config::EwwConfig, - /// Map of all currently open windows + /// Map of all currently open windows by their IDs pub open_windows: HashMap, + pub instance_id_to_args: HashMap, /// Window names that are supposed to be open, but failed. /// When reloading the config, these should be opened again. pub failed_windows: HashSet, @@ -128,6 +139,7 @@ impl std::fmt::Debug for App { .field("eww_config", &self.eww_config) .field("open_windows", &self.open_windows) .field("failed_windows", &self.failed_windows) + .field("window_arguments", &self.instance_id_to_args) .field("paths", &self.paths) .finish() } @@ -178,14 +190,25 @@ impl App { self.close_window(&window_name)?; } } - DaemonCommand::OpenMany { windows, should_toggle, sender } => { + DaemonCommand::OpenMany { windows, args, should_toggle, sender } => { let errors = windows .iter() .map(|w| { - if should_toggle && self.open_windows.contains_key(w) { - self.close_window(w) + let (config_name, id) = w; + if should_toggle && self.open_windows.contains_key(id) { + self.close_window(id) } else { - self.open_window(w, None, None, None, None, None) + log::debug!("Config: {}, id: {}", config_name, id); + let window_args = args + .iter() + .filter(|(win_id, ..)| win_id.is_empty() || win_id == id) + .map(|(_, n, v)| (n.clone(), v.clone())) + .collect(); + self.open_window(&WindowArguments::new_from_args( + id.to_string(), + config_name.clone(), + window_args, + )?) } }) .filter_map(Result::err); @@ -193,6 +216,7 @@ impl App { } DaemonCommand::OpenWindow { window_name, + instance_id, pos, size, anchor, @@ -200,13 +224,27 @@ impl App { should_toggle, duration, sender, + args, } => { - let is_open = self.open_windows.contains_key(&window_name); + let instance_id = instance_id.unwrap_or_else(|| window_name.clone()); + + let is_open = self.open_windows.contains_key(&instance_id); + let result = if should_toggle && is_open { - self.close_window(&window_name) + self.close_window(&instance_id) } else { - self.open_window(&window_name, pos, size, monitor, anchor, duration) + self.open_window(&WindowArguments { + instance_id, + window_name, + pos, + size, + monitor, + anchor, + duration, + args: args.unwrap_or_default().into_iter().collect(), + }) }; + sender.respond_with_result(result)?; } DaemonCommand::CloseWindows { windows, sender } => { @@ -233,16 +271,12 @@ impl App { None => sender.send_failure(format!("Variable not found \"{}\"", name))?, } } - DaemonCommand::PrintWindows(sender) => { - let output = self - .eww_config - .get_windows() - .keys() - .map(|window_name| { - let is_open = self.open_windows.contains_key(window_name); - format!("{}{}", if is_open { "*" } else { "" }, window_name) - }) - .join("\n"); + DaemonCommand::ListWindows(sender) => { + let output = self.eww_config.get_windows().keys().join("\n"); + sender.send_success(output)? + } + DaemonCommand::ListActiveWindows(sender) => { + let output = self.open_windows.iter().map(|(id, window)| format!("{id}: {}", window.name)).join("\n"); sender.send_success(output)? } DaemonCommand::PrintDebug(sender) => { @@ -304,14 +338,14 @@ impl App { } /// Close a window and do all the required cleanups in the scope_graph and script_var_handler - fn close_window(&mut self, window_name: &str) -> Result<()> { - if let Some(old_abort_send) = self.window_close_timer_abort_senders.remove(window_name) { + fn close_window(&mut self, instance_id: &str) -> Result<()> { + if let Some(old_abort_send) = self.window_close_timer_abort_senders.remove(instance_id) { _ = old_abort_send.send(()); } let eww_window = self .open_windows - .remove(window_name) - .with_context(|| format!("Tried to close window named '{}', but no such window was open", window_name))?; + .remove(instance_id) + .with_context(|| format!("Tried to close window with id '{instance_id}', but no such window was open"))?; let scope_index = eww_window.scope_index; eww_window.close(); @@ -324,52 +358,54 @@ impl App { self.script_var_handler.stop_for_variable(unused_var.clone()); } + self.instance_id_to_args.remove(instance_id); + Ok(()) } - fn open_window( - &mut self, - window_name: &str, - pos: Option, - size: Option, - monitor: Option, - anchor: Option, - duration: Option, - ) -> Result<()> { - self.failed_windows.remove(window_name); - log::info!("Opening window {}", window_name); + fn open_window(&mut self, window_args: &WindowArguments) -> Result<()> { + let instance_id = &window_args.instance_id; + self.failed_windows.remove(instance_id); + log::info!("Opening window {} as '{}'", window_args.window_name, instance_id); // if an instance of this is already running, close it - // TODO make reopening optional via a --no-reopen flag? - if self.open_windows.contains_key(window_name) { - self.close_window(window_name)?; + if self.open_windows.contains_key(instance_id) { + self.close_window(instance_id)?; } + self.instance_id_to_args.insert(instance_id.to_string(), window_args.clone()); + let open_result: Result<_> = try { - let mut window_def = self.eww_config.get_window(window_name)?.clone(); + let window_name: &str = &window_args.window_name; + + let window_def = self.eww_config.get_window(window_name)?.clone(); assert_eq!(window_def.name, window_name, "window definition name did not equal the called window"); - window_def.geometry = window_def.geometry.map(|x| x.override_if_given(anchor, pos, size)); + + let initiator = WindowInitiator::new(&window_def, window_args)?; let root_index = self.scope_graph.borrow().root_index; + let scoped_vars_literal = initiator.get_scoped_vars().into_iter().map(|(k, v)| (k, SimplExpr::Literal(v))).collect(); + let window_scope = self.scope_graph.borrow_mut().register_new_scope( - window_name.to_string(), + instance_id.to_string(), Some(root_index), root_index, - HashMap::new(), + scoped_vars_literal, )?; let root_widget = crate::widgets::build_widget::build_gtk_widget( &mut self.scope_graph.borrow_mut(), Rc::new(self.eww_config.get_widget_definitions().clone()), window_scope, - window_def.widget.clone(), + window_def.widget, None, )?; - let monitor_geometry = get_monitor_geometry(monitor.or_else(|| window_def.monitor.clone()))?; + root_widget.style_context().add_class(window_name); - let mut eww_window = initialize_window::(monitor_geometry, root_widget, window_def, window_scope)?; + let monitor = get_gdk_monitor(initiator.monitor.clone())?; + let mut eww_window = initialize_window::(&initiator, monitor, root_widget, window_scope)?; eww_window.gtk_window.style_context().add_class(window_name); // initialize script var handlers for variables. As starting a scriptvar with the script_var_handler is idempodent, @@ -383,32 +419,32 @@ impl App { eww_window.destroy_event_handler_id = Some(eww_window.gtk_window.connect_destroy({ let app_evt_sender = self.app_evt_send.clone(); - let window_name: String = eww_window.name.to_string(); + let instance_id = instance_id.to_string(); move |_| { // we don't care about the actual error response from the daemon as this is mostly just a fallback. // Generally, this should get disconnected before the gtk window gets destroyed. // It serves as a fallback for when the window is closed manually. let (response_sender, _) = daemon_response::create_pair(); - let command = DaemonCommand::CloseWindows { windows: vec![window_name.clone()], sender: response_sender }; + let command = DaemonCommand::CloseWindows { windows: vec![instance_id.clone()], sender: response_sender }; if let Err(err) = app_evt_sender.send(command) { log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err); } } })); + let duration = window_args.duration; if let Some(duration) = duration { let app_evt_sender = self.app_evt_send.clone(); - let window_name = window_name.to_string(); let (abort_send, abort_recv) = futures::channel::oneshot::channel(); glib::MainContext::default().spawn_local({ - let window_name = window_name.clone(); + let instance_id = instance_id.to_string(); async move { tokio::select! { _ = glib::timeout_future(duration) => { let (response_sender, mut response_recv) = daemon_response::create_pair(); - let command = DaemonCommand::CloseWindows { windows: vec![window_name], sender: response_sender }; + let command = DaemonCommand::CloseWindows { windows: vec![instance_id.clone()], sender: response_sender }; if let Err(err) = app_evt_sender.send(command) { log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err); } @@ -419,17 +455,17 @@ impl App { } }); - if let Some(old_abort_send) = self.window_close_timer_abort_senders.insert(window_name, abort_send) { + if let Some(old_abort_send) = self.window_close_timer_abort_senders.insert(instance_id.to_string(), abort_send) { _ = old_abort_send.send(()); } } - self.open_windows.insert(window_name.to_string(), eww_window); + self.open_windows.insert(instance_id.to_string(), eww_window); }; if let Err(err) = open_result { - self.failed_windows.insert(window_name.to_string()); - Err(err).with_context(|| format!("failed to open window `{}`", window_name)) + self.failed_windows.insert(instance_id.to_string()); + Err(err).with_context(|| format!("failed to open window `{}`", instance_id)) } else { Ok(()) } @@ -448,10 +484,13 @@ impl App { self.eww_config = config; self.scope_graph.borrow_mut().clear(self.eww_config.generate_initial_state()?); - let window_names: Vec = + let open_window_ids: Vec = self.open_windows.keys().cloned().chain(self.failed_windows.iter().cloned()).dedup().collect(); - for window_name in &window_names { - self.open_window(window_name, None, None, None, None, None)?; + for instance_id in &open_window_ids { + let window_arguments = self.instance_id_to_args.get(instance_id).with_context(|| { + format!("Cannot reopen window, initial parameters were not saved correctly for {instance_id}") + })?; + self.open_window(&window_arguments.clone())?; } Ok(()) } @@ -480,19 +519,20 @@ impl App { } fn initialize_window( - monitor_geometry: gdk::Rectangle, + window_init: &WindowInitiator, + monitor: Monitor, root_widget: gtk::Widget, - window_def: WindowDefinition, window_scope: ScopeIndex, ) -> Result { - let window = B::initialize_window(&window_def, monitor_geometry) - .with_context(|| format!("monitor {} is unavailable", window_def.monitor.clone().unwrap()))?; + let monitor_geometry = monitor.geometry(); + let window = B::initialize_window(window_init, monitor_geometry) + .with_context(|| format!("monitor {} is unavailable", window_init.monitor.clone().unwrap()))?; - window.set_title(&format!("Eww - {}", window_def.name)); + window.set_title(&format!("Eww - {}", window_init.name)); window.set_position(gtk::WindowPosition::None); window.set_gravity(gdk::Gravity::Center); - if let Some(geometry) = window_def.geometry { + if let Some(geometry) = window_init.geometry { let actual_window_rect = get_window_rectangle(geometry, monitor_geometry); window.set_size_request(actual_window_rect.width(), actual_window_rect.height()); window.set_default_size(actual_window_rect.width(), actual_window_rect.height()); @@ -511,21 +551,27 @@ fn initialize_window( #[cfg(feature = "x11")] if B::IS_X11 { - if let Some(geometry) = window_def.geometry { + if let Some(geometry) = window_init.geometry { let _ = apply_window_position(geometry, monitor_geometry, &window); - if window_def.backend_options.x11.window_type != yuck::config::backend_window_options::X11WindowType::Normal { + if window_init.backend_options.x11.window_type != yuck::config::backend_window_options::X11WindowType::Normal { window.connect_configure_event(move |window, _| { let _ = apply_window_position(geometry, monitor_geometry, window); false }); } } - display_backend::set_xprops(&window, monitor_geometry, &window_def)?; + display_backend::set_xprops(&window, monitor, window_init)?; } window.show_all(); - Ok(EwwWindow { name: window_def.name, gtk_window: window, scope_index: window_scope, destroy_event_handler_id: None }) + Ok(EwwWindow { + instance_id: window_init.id.clone(), + name: window_init.name.clone(), + gtk_window: window, + scope_index: window_scope, + destroy_event_handler_id: None, + }) } /// Apply the provided window-positioning rules to the window. @@ -555,54 +601,43 @@ fn on_screen_changed(window: >k::Window, _old_screen: Option<&gdk::Screen>) { } /// Get the monitor geometry of a given monitor, or the default if none is given -fn get_monitor_geometry(identifier: Option) -> Result { +fn get_gdk_monitor(identifier: Option) -> Result { let display = gdk::Display::default().expect("could not get default display"); let monitor = match identifier { Some(ident) => { let mon = get_monitor_from_display(&display, &ident); - - #[cfg(feature = "x11")] - { - mon.with_context(|| { - let head = format!("Failed to get monitor {}\nThe available monitors are:", ident); - let mut body = String::new(); - for m in 0..display.n_monitors() { - if let Some(model) = display.monitor(m).and_then(|x| x.model()) { - body.push_str(format!("\n\t[{}] {}", m, model).as_str()); - } - } - format!("{}{}", head, body) - })? - } - - #[cfg(not(feature = "x11"))] - { - mon.with_context(|| { - if ident.is_numeric() { - format!("Failed to get monitor {}", ident) - } else { - format!("Using ouput names (\"{}\" in the configuration) is not supported outside of x11 yet", ident) + mon.with_context(|| { + let head = format!("Failed to get monitor {}\nThe available monitors are:", ident); + let mut body = String::new(); + for m in 0..display.n_monitors() { + if let Some(model) = display.monitor(m).and_then(|x| x.model()) { + body.push_str(format!("\n\t[{}] {}", m, model).as_str()); } - })? - } + } + format!("{}{}", head, body) + })? } None => display .primary_monitor() .context("Failed to get primary monitor from GTK. Try explicitly specifying the monitor on your window.")?, }; - Ok(monitor.geometry()) + Ok(monitor) } /// Returns the [Monitor][gdk::Monitor] structure corresponding to the identifer. /// Outside of x11, only [MonitorIdentifier::Numeric] is supported pub fn get_monitor_from_display(display: &gdk::Display, identifier: &MonitorIdentifier) -> Option { match identifier { + MonitorIdentifier::List(list) => { + for ident in list { + if let Some(monitor) = get_monitor_from_display(display, ident) { + return Some(monitor); + } + } + None + } + MonitorIdentifier::Primary => display.primary_monitor(), MonitorIdentifier::Numeric(num) => display.monitor(*num), - - #[cfg(not(feature = "x11"))] - MonitorIdentifier::Name(_) => return None, - - #[cfg(feature = "x11")] MonitorIdentifier::Name(name) => { for m in 0..display.n_monitors() { if let Some(model) = display.monitor(m).and_then(|x| x.model()) { diff --git a/crates/eww/src/display_backend.rs b/crates/eww/src/display_backend.rs index e1830201..e653123e 100644 --- a/crates/eww/src/display_backend.rs +++ b/crates/eww/src/display_backend.rs @@ -1,4 +1,4 @@ -use yuck::config::window_definition::WindowDefinition; +use crate::window_initiator::WindowInitiator; #[cfg(feature = "wayland")] pub use platform_wayland::WaylandBackend; @@ -8,7 +8,8 @@ pub use platform_x11::{set_xprops, X11Backend}; pub trait DisplayBackend: Send + Sync + 'static { const IS_X11: bool; - fn initialize_window(window_def: &WindowDefinition, monitor: gdk::Rectangle) -> Option; + + fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle) -> Option; } pub struct NoBackend; @@ -16,18 +17,16 @@ pub struct NoBackend; impl DisplayBackend for NoBackend { const IS_X11: bool = false; - fn initialize_window(_window_def: &WindowDefinition, _monitor: gdk::Rectangle) -> Option { + fn initialize_window(_window_init: &WindowInitiator, _monitor: gdk::Rectangle) -> Option { Some(gtk::Window::new(gtk::WindowType::Toplevel)) } } #[cfg(feature = "wayland")] mod platform_wayland { + use crate::window_initiator::WindowInitiator; use gtk::prelude::*; - use yuck::config::{ - window_definition::{WindowDefinition, WindowStacking}, - window_geometry::AnchorAlignment, - }; + use yuck::config::{window_definition::WindowStacking, window_geometry::AnchorAlignment}; use super::DisplayBackend; @@ -36,12 +35,12 @@ mod platform_wayland { impl DisplayBackend for WaylandBackend { const IS_X11: bool = false; - fn initialize_window(window_def: &WindowDefinition, monitor: gdk::Rectangle) -> Option { + fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle) -> Option { let window = gtk::Window::new(gtk::WindowType::Toplevel); // Initialising a layer shell surface gtk_layer_shell::init_for_window(&window); // Sets the monitor where the surface is shown - if let Some(ident) = window_def.monitor.clone() { + if let Some(ident) = window_init.monitor.clone() { let display = gdk::Display::default().expect("could not get default display"); if let Some(monitor) = crate::app::get_monitor_from_display(&display, &ident) { gtk_layer_shell::set_monitor(&window, &monitor); @@ -49,24 +48,24 @@ mod platform_wayland { return None; } }; - window.set_resizable(window_def.resizable); + window.set_resizable(window_init.resizable); // Sets the layer where the layer shell surface will spawn - match window_def.stacking { + match window_init.stacking { WindowStacking::Foreground => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Top), WindowStacking::Background => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Background), WindowStacking::Bottom => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Bottom), WindowStacking::Overlay => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Overlay), } - if let Some(namespace) = &window_def.backend_options.wayland.namespace { + if let Some(namespace) = &window_init.backend_options.wayland.namespace { gtk_layer_shell::set_namespace(&window, namespace); } // Sets the keyboard interactivity - gtk_layer_shell::set_keyboard_interactivity(&window, window_def.backend_options.wayland.focusable); + gtk_layer_shell::set_keyboard_interactivity(&window, window_init.backend_options.wayland.focusable); - if let Some(geometry) = window_def.geometry { + if let Some(geometry) = window_init.geometry { // Positioning surface let mut top = false; let mut left = false; @@ -103,7 +102,7 @@ mod platform_wayland { gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Top, yoffset); } } - if window_def.backend_options.wayland.exclusive { + if window_init.backend_options.wayland.exclusive { gtk_layer_shell::auto_exclusive_zone_enable(&window); } Some(window) @@ -113,7 +112,9 @@ mod platform_wayland { #[cfg(feature = "x11")] mod platform_x11 { + use crate::window_initiator::WindowInitiator; use anyhow::{Context, Result}; + use gdk::Monitor; use gtk::{self, prelude::*}; use x11rb::protocol::xproto::ConnectionExt; @@ -125,7 +126,7 @@ mod platform_x11 { }; use yuck::config::{ backend_window_options::{Side, X11WindowType}, - window_definition::{WindowDefinition, WindowStacking}, + window_definition::WindowStacking, }; use super::DisplayBackend; @@ -134,14 +135,14 @@ mod platform_x11 { impl DisplayBackend for X11Backend { const IS_X11: bool = true; - fn initialize_window(window_def: &WindowDefinition, _monitor: gdk::Rectangle) -> Option { + fn initialize_window(window_init: &WindowInitiator, _monitor: gdk::Rectangle) -> Option { let window_type = - if window_def.backend_options.x11.wm_ignore { gtk::WindowType::Popup } else { gtk::WindowType::Toplevel }; + if window_init.backend_options.x11.wm_ignore { gtk::WindowType::Popup } else { gtk::WindowType::Toplevel }; let window = gtk::Window::new(window_type); - window.set_resizable(window_def.resizable); - window.set_keep_above(window_def.stacking == WindowStacking::Foreground); - window.set_keep_below(window_def.stacking == WindowStacking::Background); - if window_def.backend_options.x11.sticky { + window.set_resizable(window_init.resizable); + window.set_keep_above(window_init.stacking == WindowStacking::Foreground); + window.set_keep_below(window_init.stacking == WindowStacking::Background); + if window_init.backend_options.x11.sticky { window.stick(); } else { window.unstick(); @@ -150,9 +151,9 @@ mod platform_x11 { } } - pub fn set_xprops(window: >k::Window, monitor: gdk::Rectangle, window_def: &WindowDefinition) -> Result<()> { + pub fn set_xprops(window: >k::Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> { let backend = X11BackendConnection::new()?; - backend.set_xprops_for(window, monitor, window_def)?; + backend.set_xprops_for(window, monitor, window_init)?; Ok(()) } @@ -170,24 +171,23 @@ mod platform_x11 { Ok(X11BackendConnection { conn, root_window: screen.root, atoms }) } - fn set_xprops_for( - &self, - window: >k::Window, - monitor_rect: gdk::Rectangle, - window_def: &WindowDefinition, - ) -> Result<()> { + fn set_xprops_for(&self, window: >k::Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> { + let monitor_rect = monitor.geometry(); + let scale_factor = monitor.scale_factor() as u32; let gdk_window = window.window().context("Couldn't get gdk window from gtk window")?; let win_id = gdk_window.downcast_ref::().context("Failed to get x11 window for gtk window")?.xid() as u32; - let strut_def = window_def.backend_options.x11.struts; + let strut_def = window_init.backend_options.x11.struts; let root_window_geometry = self.conn.get_geometry(self.root_window)?.reply()?; - let mon_end_x = (monitor_rect.x() + monitor_rect.width()) as u32 - 1u32; - let mon_end_y = (monitor_rect.y() + monitor_rect.height()) as u32 - 1u32; + let mon_x = scale_factor * monitor_rect.x() as u32; + let mon_y = scale_factor * monitor_rect.y() as u32; + let mon_end_x = scale_factor * (monitor_rect.x() + monitor_rect.width()) as u32 - 1u32; + let mon_end_y = scale_factor * (monitor_rect.y() + monitor_rect.height()) as u32 - 1u32; let dist = match strut_def.side { - Side::Left | Side::Right => strut_def.dist.pixels_relative_to(monitor_rect.width()) as u32, - Side::Top | Side::Bottom => strut_def.dist.pixels_relative_to(monitor_rect.height()) as u32, + Side::Left | Side::Right => strut_def.distance.pixels_relative_to(monitor_rect.width()) as u32, + Side::Top | Side::Bottom => strut_def.distance.pixels_relative_to(monitor_rect.height()) as u32, }; // don't question it,..... @@ -195,10 +195,10 @@ mod platform_x11 { // left, right, top, bottom, left_start_y, left_end_y, right_start_y, right_end_y, top_start_x, top_end_x, bottom_start_x, bottom_end_x #[rustfmt::skip] let strut_list: Vec = match strut_def.side { - Side::Left => vec![dist + monitor_rect.x() as u32, 0, 0, 0, monitor_rect.y() as u32, mon_end_y, 0, 0, 0, 0, 0, 0], - Side::Right => vec![0, root_window_geometry.width as u32 - mon_end_x + dist, 0, 0, 0, 0, monitor_rect.y() as u32, mon_end_y, 0, 0, 0, 0], - Side::Top => vec![0, 0, dist + monitor_rect.y() as u32, 0, 0, 0, 0, 0, monitor_rect.x() as u32, mon_end_x, 0, 0], - Side::Bottom => vec![0, 0, 0, root_window_geometry.height as u32 - mon_end_y + dist, 0, 0, 0, 0, 0, 0, monitor_rect.x() as u32, mon_end_x], + Side::Left => vec![dist + mon_x, 0, 0, 0, mon_x, mon_end_y, 0, 0, 0, 0, 0, 0], + Side::Right => vec![0, root_window_geometry.width as u32 - mon_end_x + dist, 0, 0, 0, 0, mon_x, mon_end_y, 0, 0, 0, 0], + Side::Top => vec![0, 0, dist + mon_y as u32, 0, 0, 0, 0, 0, mon_x, mon_end_x, 0, 0], + Side::Bottom => vec![0, 0, 0, root_window_geometry.height as u32 - mon_end_y + dist, 0, 0, 0, 0, 0, 0, mon_x as u32, mon_end_x], // This should never happen but if it does the window will be anchored on the // right of the screen }.iter().flat_map(|x| x.to_le_bytes().to_vec()).collect(); @@ -233,7 +233,7 @@ mod platform_x11 { win_id, self.atoms._NET_WM_WINDOW_TYPE, self.atoms.ATOM, - &[match window_def.backend_options.x11.window_type { + &[match window_init.backend_options.x11.window_type { X11WindowType::Dock => self.atoms._NET_WM_WINDOW_TYPE_DOCK, X11WindowType::Normal => self.atoms._NET_WM_WINDOW_TYPE_NORMAL, X11WindowType::Dialog => self.atoms._NET_WM_WINDOW_TYPE_DIALOG, diff --git a/crates/eww/src/main.rs b/crates/eww/src/main.rs index 9211d03b..a2fc5282 100644 --- a/crates/eww/src/main.rs +++ b/crates/eww/src/main.rs @@ -4,6 +4,7 @@ #![feature(slice_concat_trait)] #![feature(try_blocks)] #![feature(hash_extract_if)] +#![feature(let_chains)] #![allow(rustdoc::private_intra_doc_links)] extern crate gtk; @@ -36,6 +37,8 @@ mod server; mod state; mod util; mod widgets; +mod window_arguments; +mod window_initiator; fn main() { let eww_binary_name = std::env::args().next().unwrap(); diff --git a/crates/eww/src/opts.rs b/crates/eww/src/opts.rs index 75d08c3b..526aa3c7 100644 --- a/crates/eww/src/opts.rs +++ b/crates/eww/src/opts.rs @@ -101,6 +101,10 @@ pub enum ActionWithServer { /// Name of the window you want to open. window_name: String, + // The id of the window instance + #[arg(long)] + id: Option, + /// The identifier of the monitor the window should open on #[arg(long)] screen: Option, @@ -124,13 +128,23 @@ pub enum ActionWithServer { /// Automatically close the window after a specified amount of time, i.e.: 1s #[arg(long, value_parser=parse_duration)] duration: Option, + + /// Define a variable for the window, i.e.: `--arg "var_name=value"` + #[arg(long = "arg", value_parser = parse_var_update_arg)] + args: Option>, }, /// Open multiple windows at once. /// NOTE: This will in the future be part of eww open, and will then be removed. #[command(name = "open-many")] OpenMany { - windows: Vec, + /// List the windows to open, optionally including their id, i.e.: `--window "window_name:window_id"` + #[arg(value_parser = parse_window_config_and_id)] + windows: Vec<(String, String)>, + + /// Define a variable for the window, i.e.: `--arg "window_id:var_name=value"` + #[arg(long = "arg", value_parser = parse_window_id_args)] + args: Vec<(String, VarName, DynVal)>, /// If a window is already open, close it instead #[arg(long = "toggle")] @@ -165,9 +179,13 @@ pub enum ActionWithServer { #[command(name = "get")] GetVar { name: String }, - /// Print the names of all configured windows. Windows with a * in front of them are currently opened. - #[command(name = "windows")] - ShowWindows, + /// List the names of active windows + #[command(name = "list-windows")] + ListWindows, + + /// Show active window IDs, formatted linewise `: ` + #[command(name = "active-windows")] + ListActiveWindows, /// Print out the widget structure as seen by eww. /// @@ -195,6 +213,25 @@ impl From for Opt { } } +/// Parse a window-name:window-id pair of the form `name:id` or `name` into a tuple of `(name, id)`. +fn parse_window_config_and_id(s: &str) -> Result<(String, String)> { + let (name, id) = s.split_once(':').unwrap_or((s, s)); + + Ok((name.to_string(), id.to_string())) +} + +/// Parse a window-id specific variable value declaration with the syntax `window-id:variable_name="new_value"` +/// into a tuple of `(id, variable_name, new_value)`. +fn parse_window_id_args(s: &str) -> Result<(String, VarName, DynVal)> { + // Parse the = first so we know if an id has not been given + let (name, value) = parse_var_update_arg(s)?; + + let (id, var_name) = name.0.split_once(':').unwrap_or(("", &name.0)); + + Ok((id.to_string(), var_name.into(), value)) +} + +/// Split the input string at `=`, parsing the value into a [`DynVal`]. fn parse_var_update_arg(s: &str) -> Result<(VarName, DynVal)> { let (name, value) = s .split_once('=') @@ -219,12 +256,13 @@ impl ActionWithServer { let _ = send.send(DaemonResponse::Success("pong".to_owned())); return (app::DaemonCommand::NoOp, Some(recv)); } - ActionWithServer::OpenMany { windows, should_toggle } => { - return with_response_channel(|sender| app::DaemonCommand::OpenMany { windows, should_toggle, sender }); + ActionWithServer::OpenMany { windows, args, should_toggle } => { + return with_response_channel(|sender| app::DaemonCommand::OpenMany { windows, args, should_toggle, sender }); } - ActionWithServer::OpenWindow { window_name, pos, size, screen, anchor, should_toggle, duration } => { + ActionWithServer::OpenWindow { window_name, id, pos, size, screen, anchor, should_toggle, duration, args } => { return with_response_channel(|sender| app::DaemonCommand::OpenWindow { window_name, + instance_id: id, pos, size, anchor, @@ -232,13 +270,15 @@ impl ActionWithServer { should_toggle, duration, sender, + args, }) } ActionWithServer::CloseWindows { windows } => { return with_response_channel(|sender| app::DaemonCommand::CloseWindows { windows, sender }); } ActionWithServer::Reload => return with_response_channel(app::DaemonCommand::ReloadConfigAndCss), - ActionWithServer::ShowWindows => return with_response_channel(app::DaemonCommand::PrintWindows), + ActionWithServer::ListWindows => return with_response_channel(app::DaemonCommand::ListWindows), + ActionWithServer::ListActiveWindows => return with_response_channel(app::DaemonCommand::ListActiveWindows), ActionWithServer::ShowState { all } => { return with_response_channel(|sender| app::DaemonCommand::PrintState { all, sender }) } diff --git a/crates/eww/src/server.rs b/crates/eww/src/server.rs index fbe2286a..5a7ecc87 100644 --- a/crates/eww/src/server.rs +++ b/crates/eww/src/server.rs @@ -81,6 +81,7 @@ pub fn initialize_server( eww_config, open_windows: HashMap::new(), failed_windows: HashSet::new(), + instance_id_to_args: HashMap::new(), css_provider: gtk::CssProvider::new(), script_var_handler, app_evt_send: ui_send.clone(), diff --git a/crates/eww/src/window_arguments.rs b/crates/eww/src/window_arguments.rs new file mode 100644 index 00000000..8833bac9 --- /dev/null +++ b/crates/eww/src/window_arguments.rs @@ -0,0 +1,90 @@ +use anyhow::{bail, Context, Result}; +use eww_shared_util::VarName; +use simplexpr::dynval::DynVal; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; +use yuck::{ + config::{monitor::MonitorIdentifier, window_definition::WindowDefinition, window_geometry::AnchorPoint}, + value::Coords, +}; + +fn parse_value_from_args(name: &str, args: &mut HashMap) -> Result, T::Err> { + args.remove(&VarName(name.to_string())).map(|x| FromStr::from_str(&x.as_string().unwrap())).transpose() +} + +/// This stores the arguments given in the command line to create a window +/// While creating a window, we combine this with information from the +/// [`WindowDefinition`] to create a [WindowInitiator](`crate::window_initiator::WindowInitiator`), which stores all the +/// information required to start a window +#[derive(Debug, Clone)] +pub struct WindowArguments { + /// Name of the window as defined in the eww config + pub window_name: String, + /// Instance ID of the window + pub instance_id: String, + pub anchor: Option, + pub args: HashMap, + pub duration: Option, + pub monitor: Option, + pub pos: Option, + pub size: Option, +} + +impl WindowArguments { + pub fn new_from_args(id: String, config_name: String, mut args: HashMap) -> Result { + let initiator = WindowArguments { + window_name: config_name, + instance_id: id, + pos: parse_value_from_args::("pos", &mut args)?, + size: parse_value_from_args::("size", &mut args)?, + monitor: parse_value_from_args::("screen", &mut args)?, + anchor: parse_value_from_args::("anchor", &mut args)?, + duration: parse_value_from_args::("duration", &mut args)? + .map(|x| x.as_duration()) + .transpose() + .context("Not a valid duration")?, + args, + }; + + Ok(initiator) + } + + /// Return a hashmap of all arguments the window was passed and expected, returning + /// an error in case required arguments are missing or unexpected arguments are passed. + pub fn get_local_window_variables(&self, window_def: &WindowDefinition) -> Result> { + let expected_args: HashSet<&String> = window_def.expected_args.iter().map(|x| &x.name.0).collect(); + let mut local_variables: HashMap = HashMap::new(); + + // Ensure that the arguments passed to the window that are already interpreted by eww (id, screen) + // are set to the correct values + if expected_args.contains(&"id".to_string()) { + local_variables.insert(VarName::from("id"), DynVal::from(self.instance_id.clone())); + } + if self.monitor.is_some() && expected_args.contains(&"screen".to_string()) { + let mon_dyn = DynVal::from(&self.monitor.clone().unwrap()); + local_variables.insert(VarName::from("screen"), mon_dyn); + } + + local_variables.extend(self.args.clone()); + + for attr in &window_def.expected_args { + let name = VarName::from(attr.name.clone()); + if !local_variables.contains_key(&name) && !attr.optional { + bail!("Error, missing argument '{}' when creating window with id '{}'", attr.name, self.instance_id); + } + } + + if local_variables.len() != window_def.expected_args.len() { + let unexpected_vars: Vec<_> = local_variables.keys().cloned().filter(|n| !expected_args.contains(&n.0)).collect(); + bail!( + "variables {} unexpectedly defined when creating window with id '{}'", + unexpected_vars.join(", "), + self.instance_id, + ); + } + + Ok(local_variables) + } +} diff --git a/crates/eww/src/window_initiator.rs b/crates/eww/src/window_initiator.rs new file mode 100644 index 00000000..cb2cda00 --- /dev/null +++ b/crates/eww/src/window_initiator.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use eww_shared_util::{AttrName, VarName}; +use simplexpr::dynval::DynVal; +use std::collections::HashMap; +use yuck::config::{ + backend_window_options::BackendWindowOptions, + monitor::MonitorIdentifier, + window_definition::{WindowDefinition, WindowStacking}, + window_geometry::WindowGeometry, +}; + +use crate::window_arguments::WindowArguments; + +/// This stores all the information required to create a window and is created +/// via combining information from the [`WindowDefinition`] and the [`WindowInitiator`] +#[derive(Debug, Clone)] +pub struct WindowInitiator { + pub backend_options: BackendWindowOptions, + pub geometry: Option, + pub id: String, + pub local_variables: HashMap, + pub monitor: Option, + pub name: String, + pub resizable: bool, + pub stacking: WindowStacking, +} + +impl WindowInitiator { + pub fn new(window_def: &WindowDefinition, args: &WindowArguments) -> Result { + let vars = args.get_local_window_variables(window_def)?; + + let geometry = match &window_def.geometry { + Some(geo) => Some(geo.eval(&vars)?.override_if_given(args.anchor, args.pos, args.size)), + None => None, + }; + let monitor = if args.monitor.is_none() { window_def.eval_monitor(&vars)? } else { args.monitor.clone() }; + Ok(WindowInitiator { + backend_options: window_def.backend_options.eval(&vars)?, + geometry, + id: args.instance_id.clone(), + monitor, + name: window_def.name.clone(), + resizable: window_def.eval_resizable(&vars)?, + stacking: window_def.eval_stacking(&vars)?, + local_variables: vars, + }) + } + + pub fn get_scoped_vars(&self) -> HashMap { + self.local_variables.iter().map(|(k, v)| (AttrName::from(k.clone()), v.clone())).collect() + } +} diff --git a/crates/simplexpr/src/dynval.rs b/crates/simplexpr/src/dynval.rs index 2bdc3129..33530fb3 100644 --- a/crates/simplexpr/src/dynval.rs +++ b/crates/simplexpr/src/dynval.rs @@ -106,6 +106,14 @@ impl TryFrom for DynVal { } } +impl From> for DynVal { + fn from(v: Vec) -> Self { + let span = if let (Some(first), Some(last)) = (v.first(), v.last()) { first.span().to(last.span()) } else { Span::DUMMY }; + let elements = v.into_iter().map(|x| x.as_string().unwrap()).collect::>(); + DynVal(serde_json::to_string(&elements).unwrap(), span) + } +} + impl From for DynVal { fn from(d: std::time::Duration) -> Self { DynVal(format!("{}ms", d.as_millis()), Span::DUMMY) diff --git a/crates/yuck/src/config/attributes.rs b/crates/yuck/src/config/attributes.rs index 9a10eb40..15639a54 100644 --- a/crates/yuck/src/config/attributes.rs +++ b/crates/yuck/src/config/attributes.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use simplexpr::{dynval::FromDynVal, eval::EvalError, SimplExpr}; use crate::{ - error::DiagError, + error::{DiagError, DiagResult}, parser::{ast::Ast, from_ast::FromAst}, }; use eww_shared_util::{AttrName, Span, Spanned}; @@ -109,3 +109,20 @@ impl Attributes { self.attrs.into_iter().map(|(k, v)| (v.key_span.to(v.value.span()), k)) } } + +/// Specification of an argument to a widget or window +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct AttrSpec { + pub name: AttrName, + pub optional: bool, + pub span: Span, +} + +impl FromAst for AttrSpec { + fn from_ast(e: Ast) -> DiagResult { + let span = e.span(); + let symbol = e.as_symbol()?; + let (name, optional) = if let Some(name) = symbol.strip_prefix('?') { (name.to_string(), true) } else { (symbol, false) }; + Ok(Self { name: AttrName(name), optional, span }) + } +} diff --git a/crates/yuck/src/config/backend_window_options.rs b/crates/yuck/src/config/backend_window_options.rs index d237a200..1314664a 100644 --- a/crates/yuck/src/config/backend_window_options.rs +++ b/crates/yuck/src/config/backend_window_options.rs @@ -1,45 +1,73 @@ -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use anyhow::Result; +use simplexpr::{ + dynval::{DynVal, FromDynVal}, + eval::EvalError, + SimplExpr, +}; use crate::{ enum_parse, error::DiagResult, parser::{ast::Ast, ast_iterator::AstIterator, from_ast::FromAstElementContent}, - value::NumWithUnit, + value::{coords, NumWithUnit}, }; -use eww_shared_util::Span; +use eww_shared_util::{Span, VarName}; use super::{attributes::Attributes, window_definition::EnumParseError}; use crate::error::{DiagError, DiagResultExt}; -/// Backend-specific options of a window that are backend -#[derive(Debug, Clone, serde::Serialize, PartialEq)] -pub struct BackendWindowOptions { - pub x11: X11BackendWindowOptions, - pub wayland: WlBackendWindowOptions, +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + EnumParseError(#[from] EnumParseError), + #[error(transparent)] + CoordsError(#[from] coords::Error), + #[error(transparent)] + EvalError(#[from] EvalError), } -impl BackendWindowOptions { +/// Backend-specific options of a window +/// Unevaluated form of [`BackendWindowOptions`] +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct BackendWindowOptionsDef { + pub wayland: WlBackendWindowOptionsDef, + pub x11: X11BackendWindowOptionsDef, +} + +impl BackendWindowOptionsDef { + pub fn eval(&self, local_variables: &HashMap) -> Result { + Ok(BackendWindowOptions { wayland: self.wayland.eval(local_variables)?, x11: self.x11.eval(local_variables)? }) + } + pub fn from_attrs(attrs: &mut Attributes) -> DiagResult { let struts = attrs.ast_optional("reserve")?; - let window_type = attrs.primitive_optional("windowtype")?; - let x11 = X11BackendWindowOptions { - wm_ignore: attrs.primitive_optional("wm-ignore")?.unwrap_or(window_type.is_none() && struts.is_none()), - window_type: window_type.unwrap_or_default(), - sticky: attrs.primitive_optional("sticky")?.unwrap_or(true), - struts: struts.unwrap_or_default(), + let window_type = attrs.ast_optional("windowtype")?; + let x11 = X11BackendWindowOptionsDef { + sticky: attrs.ast_optional("sticky")?, + struts, + window_type, + wm_ignore: attrs.ast_optional("wm-ignore")?, }; - let wayland = WlBackendWindowOptions { - exclusive: attrs.primitive_optional("exclusive")?.unwrap_or(false), - focusable: attrs.primitive_optional("focusable")?.unwrap_or(false), - namespace: attrs.primitive_optional("namespace")?, + let wayland = WlBackendWindowOptionsDef { + exclusive: attrs.ast_optional("exclusive")?, + focusable: attrs.ast_optional("focusable")?, + namespace: attrs.ast_optional("namespace")?, }; - Ok(Self { x11, wayland }) + + Ok(Self { wayland, x11 }) } } +/// Backend-specific options of a window that are backend +#[derive(Debug, Clone, serde::Serialize, PartialEq)] +pub struct BackendWindowOptions { + pub x11: X11BackendWindowOptions, + pub wayland: WlBackendWindowOptions, +} + #[derive(Debug, Clone, PartialEq, serde::Serialize)] pub struct X11BackendWindowOptions { pub wm_ignore: bool, @@ -48,6 +76,36 @@ pub struct X11BackendWindowOptions { pub struts: X11StrutDefinition, } +/// Unevaluated form of [`X11BackendWindowOptions`] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct X11BackendWindowOptionsDef { + pub sticky: Option, + pub struts: Option, + pub window_type: Option, + pub wm_ignore: Option, +} + +impl X11BackendWindowOptionsDef { + fn eval(&self, local_variables: &HashMap) -> Result { + Ok(X11BackendWindowOptions { + sticky: eval_opt_expr_as_bool(&self.sticky, true, local_variables)?, + struts: match &self.struts { + Some(expr) => expr.eval(local_variables)?, + None => X11StrutDefinition::default(), + }, + window_type: match &self.window_type { + Some(expr) => X11WindowType::from_dynval(&expr.eval(local_variables)?)?, + None => X11WindowType::default(), + }, + wm_ignore: eval_opt_expr_as_bool( + &self.wm_ignore, + self.window_type.is_none() && self.struts.is_none(), + local_variables, + )?, + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] pub struct WlBackendWindowOptions { pub exclusive: bool, @@ -55,6 +113,38 @@ pub struct WlBackendWindowOptions { pub namespace: Option, } +/// Unevaluated form of [`WlBackendWindowOptions`] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct WlBackendWindowOptionsDef { + pub exclusive: Option, + pub focusable: Option, + pub namespace: Option, +} + +impl WlBackendWindowOptionsDef { + fn eval(&self, local_variables: &HashMap) -> Result { + Ok(WlBackendWindowOptions { + exclusive: eval_opt_expr_as_bool(&self.exclusive, false, local_variables)?, + focusable: eval_opt_expr_as_bool(&self.focusable, false, local_variables)?, + namespace: match &self.namespace { + Some(expr) => Some(expr.eval(local_variables)?.as_string()?), + None => None, + }, + }) + } +} + +fn eval_opt_expr_as_bool( + opt_expr: &Option, + default: bool, + local_variables: &HashMap, +) -> Result { + Ok(match opt_expr { + Some(expr) => expr.eval(local_variables)?.as_bool()?, + None => default, + }) +} + /// Window type of an x11 window #[derive(Debug, Clone, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)] pub enum X11WindowType { @@ -105,18 +195,37 @@ impl std::str::FromStr for Side { } } -#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Serialize)] -pub struct X11StrutDefinition { - pub side: Side, - pub dist: NumWithUnit, +/// Unevaluated form of [`X11StrutDefinition`] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct X11StrutDefinitionExpr { + pub side: Option, + pub distance: SimplExpr, +} + +impl X11StrutDefinitionExpr { + fn eval(&self, local_variables: &HashMap) -> Result { + Ok(X11StrutDefinition { + side: match &self.side { + Some(expr) => Side::from_dynval(&expr.eval(local_variables)?)?, + None => Side::default(), + }, + distance: NumWithUnit::from_dynval(&self.distance.eval(local_variables)?)?, + }) + } } -impl FromAstElementContent for X11StrutDefinition { +impl FromAstElementContent for X11StrutDefinitionExpr { const ELEMENT_NAME: &'static str = "struts"; fn from_tail>(_span: Span, mut iter: AstIterator) -> DiagResult { let mut attrs = iter.expect_key_values()?; iter.expect_done().map_err(DiagError::from).note("Check if you are missing a colon in front of a key")?; - Ok(X11StrutDefinition { side: attrs.primitive_required("side")?, dist: attrs.primitive_required("distance")? }) + Ok(X11StrutDefinitionExpr { side: attrs.ast_optional("side")?, distance: attrs.ast_required("distance")? }) } } + +#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Serialize)] +pub struct X11StrutDefinition { + pub side: Side, + pub distance: NumWithUnit, +} diff --git a/crates/yuck/src/config/monitor.rs b/crates/yuck/src/config/monitor.rs index 53d6a393..46d4ec2a 100644 --- a/crates/yuck/src/config/monitor.rs +++ b/crates/yuck/src/config/monitor.rs @@ -1,25 +1,57 @@ -use std::{convert::Infallible, fmt, str}; +use std::{ + convert::Infallible, + fmt, + str::{self, FromStr}, +}; use serde::{Deserialize, Serialize}; +use simplexpr::dynval::{ConversionError, DynVal}; /// The type of the identifier used to select a monitor #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum MonitorIdentifier { + List(Vec), Numeric(i32), Name(String), + Primary, } impl MonitorIdentifier { + pub fn from_dynval(val: &DynVal) -> Result { + match val.as_json_array() { + Ok(arr) => Ok(MonitorIdentifier::List( + arr.iter().map(|x| MonitorIdentifier::from_dynval(&x.into())).collect::>()?, + )), + Err(_) => match val.as_i32() { + Ok(x) => Ok(MonitorIdentifier::Numeric(x)), + Err(_) => Ok(MonitorIdentifier::from_str(&val.as_string().unwrap()).unwrap()), + }, + } + } + pub fn is_numeric(&self) -> bool { matches!(self, Self::Numeric(_)) } } +impl From<&MonitorIdentifier> for DynVal { + fn from(val: &MonitorIdentifier) -> Self { + match val { + MonitorIdentifier::List(l) => l.iter().map(|x| x.into()).collect::>().into(), + MonitorIdentifier::Numeric(n) => DynVal::from(*n), + MonitorIdentifier::Name(n) => DynVal::from(n.clone()), + MonitorIdentifier::Primary => DynVal::from(""), + } + } +} + impl fmt::Display for MonitorIdentifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::List(l) => write!(f, "[{}]", l.iter().map(|x| x.to_string()).collect::>().join(" ")), Self::Numeric(n) => write!(f, "{}", n), Self::Name(n) => write!(f, "{}", n), + Self::Primary => write!(f, ""), } } } @@ -30,7 +62,13 @@ impl str::FromStr for MonitorIdentifier { fn from_str(s: &str) -> Result { match s.parse::() { Ok(n) => Ok(Self::Numeric(n)), - Err(_) => Ok(Self::Name(s.to_owned())), + Err(_) => { + if &s.to_lowercase() == "" { + Ok(Self::Primary) + } else { + Ok(Self::Name(s.to_owned())) + } + } } } } diff --git a/crates/yuck/src/config/snapshots/yuck__config__test__config.snap b/crates/yuck/src/config/snapshots/yuck__config__test__config.snap index 2440bf03..3a511e1f 100644 --- a/crates/yuck/src/config/snapshots/yuck__config__test__config.snap +++ b/crates/yuck/src/config/snapshots/yuck__config__test__config.snap @@ -41,6 +41,8 @@ Config( window_definitions: { "some-window": WindowDefinition( name: "some-window", + expected_args: [], + args_span: Span(18446744073709551615, 18446744073709551615, 18446744073709551615), geometry: Some(WindowGeometry( anchor_point: AnchorPoint( x: START, @@ -56,7 +58,7 @@ Config( ), )), stacking: Foreground, - monitor_number: Some(12), + monitor_number: Some(Literal(DynVal("12", Span(278, 280, 0)))), widget: Basic(BasicWidgetUse( name: "bar", attrs: Attributes( @@ -83,6 +85,63 @@ Config( ), ), ), + "some-window-with-args": WindowDefinition( + name: "some-window-with-args", + expected_args: [ + AttrSpec( + name: AttrName("arg"), + optional: false, + span: Span(523, 526, 0), + ), + AttrSpec( + name: AttrName("arg2"), + optional: false, + span: Span(527, 531, 0), + ), + ], + args_span: Span(522, 532, 0), + geometry: Some(WindowGeometry( + anchor_point: AnchorPoint( + x: START, + y: START, + ), + offset: Coords( + x: Pixels(0), + y: Pixels(0), + ), + size: Coords( + x: Percent(12), + y: Pixels(20), + ), + )), + stacking: Foreground, + monitor_number: Some(Literal(DynVal("12", Span(595, 597, 0)))), + widget: Basic(BasicWidgetUse( + name: "bar", + attrs: Attributes( + span: Span(784, 795, 0), + attrs: { + AttrName("arg"): AttrEntry( + key_span: Span(785, 789, 0), + value: SimplExpr(Span(790, 795, 0), Literal(DynVal("bla", Span(790, 795, 0)))), + ), + }, + ), + children: [], + span: Span(780, 796, 0), + name_span: Span(781, 784, 0), + )), + resizable: true, + backend_options: BackendWindowOptions( + wm_ignore: false, + sticky: true, + window_type: Dock, + struts: StrutDefinition( + side: Left, + dist: Pixels(30), + ), + ), + ), }, var_definitions: { VarName("some_var"): VarDefinition( diff --git a/crates/yuck/src/config/validate.rs b/crates/yuck/src/config/validate.rs index 5f9b035a..0c20e5c2 100644 --- a/crates/yuck/src/config/validate.rs +++ b/crates/yuck/src/config/validate.rs @@ -33,13 +33,17 @@ impl Spanned for ValidationError { } pub fn validate(config: &Config, additional_globals: Vec) -> Result<(), ValidationError> { - let var_names = std::iter::empty() + let var_names: HashSet = std::iter::empty() .chain(additional_globals.iter().cloned()) .chain(config.script_vars.keys().cloned()) .chain(config.var_definitions.keys().cloned()) .collect(); for window in config.window_definitions.values() { - validate_variables_in_widget_use(&config.widget_definitions, &var_names, &window.widget, false)?; + let local_var_names: HashSet = std::iter::empty() + .chain(var_names.iter().cloned()) + .chain(window.expected_args.iter().map(|x| VarName::from(x.name.clone()))) + .collect(); + validate_variables_in_widget_use(&config.widget_definitions, &local_var_names, &window.widget, false)?; } for def in config.widget_definitions.values() { validate_widget_definition(&config.widget_definitions, &var_names, def)?; diff --git a/crates/yuck/src/config/widget_definition.rs b/crates/yuck/src/config/widget_definition.rs index 8a17233a..093db778 100644 --- a/crates/yuck/src/config/widget_definition.rs +++ b/crates/yuck/src/config/widget_definition.rs @@ -8,25 +8,9 @@ use crate::{ from_ast::{FromAst, FromAstElementContent}, }, }; -use eww_shared_util::{AttrName, Span, Spanned}; +use eww_shared_util::{Span, Spanned}; -use super::widget_use::WidgetUse; - -#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] -pub struct AttrSpec { - pub name: AttrName, - pub optional: bool, - pub span: Span, -} - -impl FromAst for AttrSpec { - fn from_ast(e: Ast) -> DiagResult { - let span = e.span(); - let symbol = e.as_symbol()?; - let (name, optional) = if let Some(name) = symbol.strip_prefix('?') { (name.to_string(), true) } else { (symbol, false) }; - Ok(Self { name: AttrName(name), optional, span }) - } -} +use super::{attributes::AttrSpec, widget_use::WidgetUse}; #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] pub struct WidgetDefinition { diff --git a/crates/yuck/src/config/window_definition.rs b/crates/yuck/src/config/window_definition.rs index afe3fd52..18bd581b 100644 --- a/crates/yuck/src/config/window_definition.rs +++ b/crates/yuck/src/config/window_definition.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{collections::HashMap, fmt::Display}; use crate::{ config::monitor::MonitorIdentifier, @@ -9,19 +9,69 @@ use crate::{ from_ast::{FromAst, FromAstElementContent}, }, }; -use eww_shared_util::Span; +use eww_shared_util::{Span, VarName}; +use simplexpr::{ + dynval::{DynVal, FromDynVal}, + eval::EvalError, + SimplExpr, +}; -use super::{backend_window_options::BackendWindowOptions, widget_use::WidgetUse, window_geometry::WindowGeometry}; +use super::{ + attributes::AttrSpec, backend_window_options::BackendWindowOptionsDef, widget_use::WidgetUse, + window_geometry::WindowGeometryDef, +}; -#[derive(Debug, Clone, serde::Serialize, PartialEq)] +#[derive(Debug, thiserror::Error)] +pub enum WindowStackingConversionError { + #[error(transparent)] + EvalError(#[from] EvalError), + #[error(transparent)] + EnumParseError(#[from] EnumParseError), +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct WindowDefinition { pub name: String, - pub geometry: Option, - pub stacking: WindowStacking, - pub monitor: Option, + pub expected_args: Vec, + pub args_span: Span, + pub geometry: Option, + pub stacking: Option, + pub monitor: Option, pub widget: WidgetUse, - pub resizable: bool, - pub backend_options: BackendWindowOptions, + pub resizable: Option, + pub backend_options: BackendWindowOptionsDef, +} + +impl WindowDefinition { + /// Evaluate the `monitor` field of the window definition + pub fn eval_monitor(&self, local_variables: &HashMap) -> Result, EvalError> { + Ok(match &self.monitor { + Some(monitor_expr) => Some(MonitorIdentifier::from_dynval(&monitor_expr.eval(local_variables)?)?), + None => None, + }) + } + + /// Evaluate the `resizable` field of the window definition + pub fn eval_resizable(&self, local_variables: &HashMap) -> Result { + Ok(match &self.resizable { + Some(expr) => expr.eval(local_variables)?.as_bool()?, + None => true, + }) + } + + /// Evaluate the `stacking` field of the window definition + pub fn eval_stacking( + &self, + local_variables: &HashMap, + ) -> Result { + match &self.stacking { + Some(stacking_expr) => match stacking_expr.eval(local_variables) { + Ok(val) => Ok(WindowStacking::from_dynval(&val)?), + Err(err) => Err(WindowStackingConversionError::EvalError(err)), + }, + None => Ok(WindowStacking::Foreground), + } + } } impl FromAstElementContent for WindowDefinition { @@ -29,15 +79,17 @@ impl FromAstElementContent for WindowDefinition { fn from_tail>(_span: Span, mut iter: AstIterator) -> DiagResult { let (_, name) = iter.expect_symbol()?; + let (args_span, expected_args) = iter.expect_array().unwrap_or((Span::DUMMY, Vec::new())); + let expected_args = expected_args.into_iter().map(AttrSpec::from_ast).collect::>()?; let mut attrs = iter.expect_key_values()?; - let monitor = attrs.primitive_optional("monitor")?; - let resizable = attrs.primitive_optional("resizable")?.unwrap_or(true); - let stacking = attrs.primitive_optional("stacking")?.unwrap_or(WindowStacking::Foreground); + let monitor = attrs.ast_optional("monitor")?; + let resizable = attrs.ast_optional("resizable")?; + let stacking = attrs.ast_optional("stacking")?; let geometry = attrs.ast_optional("geometry")?; - let backend_options = BackendWindowOptions::from_attrs(&mut attrs)?; + let backend_options = BackendWindowOptionsDef::from_attrs(&mut attrs)?; let widget = iter.expect_any().map_err(DiagError::from).and_then(WidgetUse::from_ast)?; iter.expect_done()?; - Ok(Self { name, monitor, resizable, widget, stacking, geometry, backend_options }) + Ok(Self { name, expected_args, args_span, monitor, resizable, widget, stacking, geometry, backend_options }) } } diff --git a/crates/yuck/src/config/window_geometry.rs b/crates/yuck/src/config/window_geometry.rs index a96c256a..07b221d6 100644 --- a/crates/yuck/src/config/window_geometry.rs +++ b/crates/yuck/src/config/window_geometry.rs @@ -1,14 +1,21 @@ +use std::collections::HashMap; + use crate::{ enum_parse, error::DiagResult, format_diagnostic::ToDiagnostic, parser::{ast::Ast, ast_iterator::AstIterator, from_ast::FromAstElementContent}, - value::Coords, + value::{coords, Coords, NumWithUnit}, }; use super::window_definition::EnumParseError; -use eww_shared_util::Span; +use eww_shared_util::{Span, VarName}; use serde::{Deserialize, Serialize}; +use simplexpr::{ + dynval::{DynVal, FromDynVal}, + eval::EvalError, + SimplExpr, +}; #[derive(Debug, Clone, Copy, Eq, PartialEq, smart_default::SmartDefault, Serialize, Deserialize, strum::Display)] pub enum AnchorAlignment { @@ -102,34 +109,86 @@ impl std::str::FromStr for AnchorPoint { } } -#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize)] -pub struct WindowGeometry { - pub anchor_point: AnchorPoint, - pub offset: Coords, - pub size: Coords, +/// Unevaluated variant of [`Coords`] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct CoordsDef { + pub x: Option, + pub y: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + AnchorPointParseError(#[from] AnchorPointParseError), + #[error(transparent)] + CoordsError(#[from] coords::Error), + #[error(transparent)] + EvalError(#[from] EvalError), +} + +impl CoordsDef { + pub fn eval(&self, local_variables: &HashMap) -> Result { + Ok(Coords { + x: convert_to_num_with_unit(&self.x, local_variables)?, + y: convert_to_num_with_unit(&self.y, local_variables)?, + }) + } } -impl FromAstElementContent for WindowGeometry { +fn convert_to_num_with_unit( + opt_expr: &Option, + local_variables: &HashMap, +) -> Result { + Ok(match opt_expr { + Some(expr) => NumWithUnit::from_dynval(&expr.eval(local_variables)?)?, + None => NumWithUnit::default(), + }) +} + +/// Unevaluated variant of [`WindowGeometry`] +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct WindowGeometryDef { + pub anchor_point: Option, + pub offset: CoordsDef, + pub size: CoordsDef, +} + +impl FromAstElementContent for WindowGeometryDef { const ELEMENT_NAME: &'static str = "geometry"; fn from_tail>(_span: Span, mut iter: AstIterator) -> DiagResult { let mut attrs = iter.expect_key_values()?; iter.expect_done() .map_err(|e| e.to_diagnostic().with_notes(vec!["Check if you are missing a colon in front of a key".to_string()]))?; + + Ok(WindowGeometryDef { + anchor_point: attrs.ast_optional("anchor")?, + size: CoordsDef { x: attrs.ast_optional("width")?, y: attrs.ast_optional("height")? }, + offset: CoordsDef { x: attrs.ast_optional("x")?, y: attrs.ast_optional("y")? }, + }) + } +} + +impl WindowGeometryDef { + pub fn eval(&self, local_variables: &HashMap) -> Result { Ok(WindowGeometry { - anchor_point: attrs.primitive_optional("anchor")?.unwrap_or_default(), - size: Coords { - x: attrs.primitive_optional("width")?.unwrap_or_default(), - y: attrs.primitive_optional("height")?.unwrap_or_default(), - }, - offset: Coords { - x: attrs.primitive_optional("x")?.unwrap_or_default(), - y: attrs.primitive_optional("y")?.unwrap_or_default(), + anchor_point: match &self.anchor_point { + Some(expr) => AnchorPoint::from_dynval(&expr.eval(local_variables)?)?, + None => AnchorPoint::default(), }, + size: self.size.eval(local_variables)?, + offset: self.offset.eval(local_variables)?, }) } } +#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize)] +pub struct WindowGeometry { + pub anchor_point: AnchorPoint, + pub offset: Coords, + pub size: Coords, +} + impl WindowGeometry { pub fn override_if_given(&self, anchor_point: Option, offset: Option, size: Option) -> Self { WindowGeometry { diff --git a/crates/yuck/src/parser/ast.rs b/crates/yuck/src/parser/ast.rs index d80d208f..5a901299 100644 --- a/crates/yuck/src/parser/ast.rs +++ b/crates/yuck/src/parser/ast.rs @@ -71,6 +71,8 @@ impl Ast { as_func!(AstType::List, as_list as_list_ref> = Ast::List(_, x) => x); + as_func!(AstType::Array, as_array as_array_ref> = Ast::Array(_, x) => x); + pub fn expr_type(&self) -> AstType { match self { Ast::List(..) => AstType::List, diff --git a/docs/src/configuration.md b/docs/src/configuration.md index d7f62c89..2f62d10c 100644 --- a/docs/src/configuration.md +++ b/docs/src/configuration.md @@ -50,10 +50,20 @@ You can now open your first window by running `eww open example`! Glorious! | Property | Description | | ---------: | ------------------------------------------------------------ | -| `monitor` | Which monitor this window should be displayed on. Can be either a number (X11 and Wayland) or an output name (X11 only). | +| `monitor` | Which monitor this window should be displayed on. See below for details.| | `geometry` | Geometry of the window. | +**`monitor`-property** + +This field can be: + +- the string ``, in which case eww tries to identify the primary display (which may fail, especially on wayland) +- an integer, declaring the monitor index +- the name of the monitor +- an array of monitor matchers, such as: `["" "HDMI-A-1" "PHL 345B1C" 0]`. Eww will try to find a match in order, allowing you to specify fallbacks. + + **`geometry`-properties** | Property | Description | @@ -79,9 +89,9 @@ Depending on if you are using X11 or Wayland, some additional properties exist: | Property | Description | | ----------: | ------------------------------------------------------------ | | `stacking` | Where the window should appear in the stack. Possible values: `fg`, `bg`, `overlay`, `bottom`. | -| `exclusive` | Whether the compositor should reserve space for the window automatically. | -| `focusable` | Whether the window should be able to be focused. This is necessary for any widgets that use the keyboard to work. | -| `namespace` | Set the wayland layersurface namespace eww uses | +| `exclusive` | Whether the compositor should reserve space for the window automatically. Either `true` or `false`. | +| `focusable` | Whether the window should be able to be focused. This is necessary for any widgets that use the keyboard to work. Either `true` or `false`. | +| `namespace` | Set the wayland layersurface namespace eww uses. Accepts a `string` value. | @@ -130,7 +140,7 @@ As you may have noticed, we are using a couple predefined widgets here. These ar ### Rendering children in your widgets -As your configuration grows, you might want to improve the structure of you config by factoring out functionality into basic reusable widgets. +As your configuration grows, you might want to improve the structure of your config by factoring out functionality into basic reusable widgets. Eww allows you to create custom wrapper widgets that can themselves take children, just like some of the built-in widgets like `box` or `button` can. For this, use the `children` placeholder: ```lisp @@ -249,6 +259,112 @@ Eww then reads the provided value and renders the resulting widget. Whenever it Note that this is not all that efficient. Make sure to only use `literal` when necessary! +## Using window arguments and IDs + +In some cases you may want to use the same window configuration for multiple widgets, e.g. for multiple windows. This is where arguments and ids come in. + +### Window ID + +Firstly let us start off with ids. An id can be specified in the `open` command +with `--id`, by default the id will be set to the name of the window +configuration. These ids allow you to spawn multiple of the same windows. So +for example you can do: + +```bash +eww open my_bar --screen 0 --id primary +eww open my_bar --screen 1 --id secondary +``` + +When using `open-many` you can follow the structure below. Again if no id is +given, the id will default to the name of the window configuration. + +```bash +eww open-many my_config:primary my_config:secondary +``` + +You may notice with this we didn't set `screen`, this is set through the +`--arg` system, please see below for more information. + +### Window Arguments + +However this may not be enough and you want to have slight changes for each of +these bars, e.g. having a different class for 1080p displays vs 4k or having +spawning the window in a different size or location. This is where the +arguments come in. + +Please note these arguments are **CONSTANT** and so cannot be update after the +window has been opened. + +Defining arguments in a window is the exact same as in a widget so you can +have: + +```lisp +(defwindow my_bar [arg1 ?arg2] + :geometry (geometry + :x "0%" + :y "6px" + :width "100%" + :height { arg1 == "small" ? "30px" : "40px" } + :anchor "top center") + :stacking "bg" + :windowtype "dock" + :reserve (struts :distance "50px" :side "top") + (my_widget :arg2 arg2)) +``` + +Here we have two arguments, `arg1` and `arg2` (an optional parameter). + +Once we have these parameters, when opening a new window, we must specify them +(unless they are required, like `arg2`), but how? Well, we use the `--arg` +option when running the `open` command: + +```bash +eww open my_bar --id primary --arg arg1=some_value --arg arg2=another_value +``` + +With the `open-many` it looks like this: + +```bash +# Please note that `--arg` option must be given after all the windows names +eww open-many my_bar:primary --arg primary:arg1=some_value --arg primary:arg2=another_value +``` + +Using this method you can define `screen`, `anchor`, `pos`, `size` inside the +args for each window and it will act like giving `--screen`, `--anchor` etc. in +the `open` command. + +So, now you know the basics, I shall introduce you to some of these "special" +parameters, which are set slightly differently. However these can all be +overridden by the `--arg` option. + +- `id` - If `id` is included in the argument list, it will be set to the id + specified by `--id` or will be set to the name of the config. This can be + used when closing the current window through eww commands. +- `screen` - If `screen` is specified it will be set to the value given by + `--screen`, so you can use this in other widgets to access screen specific + information. + +### Further insight into args in `open-many` + +Now due to the system behind processing the `open-many` `--arg` option you +don't have to specify an id for each argument. If you do not, that argument +will be applied across all windows e.g. + +```bash +eww open-many my_bar:primary my_bar:secondary --arg gui_size="small" +``` + +This will mean the config is the same throughout the bars. + +Furthermore if you didn't specify an id for the window, you can still set args +specifically for that window - following the idea that the id will be set to +the window configuration if not given - by just using the name of the window +configuration e.g. + +```bash +eww open-many my_primary_bar --arg my_primary_bar:screen=0 +``` + ## Generating a list of widgets from JSON using `for` If you want to display a list of values, you can use the `for`-Element to fill a container with a list of elements generated from a JSON-array.