diff --git a/Cargo.lock b/Cargo.lock index 610d646..9739073 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,7 @@ dependencies = [ "anyhow", "gdk", "gio", + "glib", "gtk", "hocon", "regex", diff --git a/Cargo.toml b/Cargo.toml index e809d6d..17f666b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" gtk = { version = "0.9", features = [ "v3_16" ] } gdk = { version = "", features = ["v3_16"] } gio = { version = "", features = ["v2_44"] } +glib = { version = "", features = ["v2_44"] } regex = "1" diff --git a/src/config/mod.rs b/src/config/mod.rs index 3bfe970..5c88b62 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,27 +1,125 @@ use anyhow::*; +use glib::{types, value}; use hocon::*; use hocon_ext::HoconExt; -use std::{collections::HashMap, convert::TryFrom}; +use std::collections::HashMap; use try_match::try_match; pub mod hocon_ext; -#[derive(Debug, PartialEq, Eq)] -pub struct AttrValue(pub String); +#[derive(Debug, Clone)] +pub struct EwwConfig { + widgets: HashMap, + windows: HashMap, +} -#[derive(Debug, PartialEq)] +impl EwwConfig { + pub fn from_hocon(hocon: &Hocon) -> Result { + let data = hocon + .as_hash() + .context("eww config has to be a map structure")?; + + Ok(EwwConfig { + widgets: data + .get("widgets") + .context("widgets need to be provided")? + .as_hash() + .context("widgets need to be a map")? + .iter() + .map(|(name, def)| Ok((name.clone(), parse_widget_definition(name.clone(), def)?))) + .collect::>>()?, + windows: data + .get("windows") + .context("windows need to be provided")? + .as_hash() + .context("windows need to be a map")? + .iter() + .map(|(name, def)| Ok((name.clone(), EwwWindowDefinition::from_hocon(def)?))) + .collect::>>()?, + }) + } + + pub fn widgets(&self) -> &HashMap { + &self.widgets + } + pub fn windows(&self) -> &HashMap { + &self.windows + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct EwwWindowDefinition { + pub position: (i32, i32), + pub size: (i32, i32), + pub widget: ElementUse, +} + +impl EwwWindowDefinition { + pub fn from_hocon(hocon: &Hocon) -> Result { + let data = hocon + .as_hash() + .context("window config has to be a map structure")?; + let position: Option<_> = try { + ( + data.get("pos")?.as_hash()?.get("x")?.as_i64()? as i32, + data.get("pos")?.as_hash()?.get("y")?.as_i64()? as i32, + ) + }; + let size: Option<_> = try { + ( + data.get("size")?.as_hash()?.get("x")?.as_i64()? as i32, + data.get("size")?.as_hash()?.get("y")?.as_i64()? as i32, + ) + }; + + let element = + parse_element_use(data.get("widget").context("no widget use given")?.clone())?; + + Ok(EwwWindowDefinition { + position: position.context("pos.x and pos.y need to be set")?, + size: size.context("size.x and size.y need to be set")?, + widget: element, + }) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum AttrValue { + String(String), + Number(f64), + Boolean(bool), + VarRef(String), +} + +impl AttrValue { + pub fn as_string(&self) -> Option<&String> { + try_match!(AttrValue::String(x) = self).ok() + } + pub fn as_f64(&self) -> Option { + try_match!(AttrValue::Number(x) = self => *x).ok() + } + pub fn as_bool(&self) -> Option { + try_match!(AttrValue::Boolean(x) = self => *x).ok() + } + pub fn as_var_ref(&self) -> Option<&String> { + try_match!(AttrValue::VarRef(x) = self).ok() + } +} + +#[derive(Debug, Clone, PartialEq)] pub struct WidgetDefinition { pub name: String, pub structure: ElementUse, + pub size: Option<(i32, i32)>, } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum ElementUse { Widget(WidgetUse), Text(String), } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct WidgetUse { pub name: String, pub children: Vec, @@ -44,18 +142,25 @@ impl From for ElementUse { } } -pub fn parse_widget_definition(text: &str) -> Result { - let hocon = parse_hocon(text)?; - +pub fn parse_widget_definition(name: String, hocon: &Hocon) -> Result { let definition = hocon .as_hash() - .ok_or_else(|| anyhow!("{:?} is not a hash", text))?; + .context("widget definition was not a hash")?; + let structure = definition + .get("structure") + .cloned() + .context("structure needs to be set") + .and_then(parse_element_use)?; Ok(WidgetDefinition { - name: definition["name"] - .as_string() - .context("name was not a string")?, - structure: parse_element_use(definition.get("structure").unwrap().clone())?, + name, + structure, + size: try { + ( + definition.get("size_x")?.as_i64()? as i32, + definition.get("size_y")?.as_i64()? as i32, + ) + }, }) } @@ -90,12 +195,13 @@ pub fn parse_widget_use(data: HashMap) -> Result { .into_iter() .filter_map(|(key, value)| { Some(( - key.clone(), + key.to_lowercase(), match value { - Hocon::String(s) => AttrValue(s.to_string()), - Hocon::Integer(n) => AttrValue(format!("{}", n)), - Hocon::Real(n) => AttrValue(format!("{}", n)), - Hocon::Boolean(b) => AttrValue(format!("{}", b)), + Hocon::String(s) if s.starts_with("$$") => AttrValue::String(s.to_string()), + Hocon::String(s) => AttrValue::String(s.to_string()), + Hocon::Integer(n) => AttrValue::Number(*n as f64), + Hocon::Real(n) => AttrValue::Number(*n as f64), + Hocon::Boolean(b) => AttrValue::Boolean(*b), _ => return None, }, )) @@ -137,31 +243,31 @@ mod test { ); } - #[test] - fn test_parse_widget_definition() { - let expected = WidgetDefinition { - name: "example_widget".to_string(), - structure: ElementUse::Widget(WidgetUse { - name: "layout_horizontal".to_string(), - attrs: HashMap::new(), - children: vec![ - ElementUse::Widget(WidgetUse::new( - "text".to_string(), - vec![ElementUse::Text("hi".to_string())], - )), - ElementUse::Widget(WidgetUse::new("text".to_string(), vec![])), - ], - }), - }; + // #[test] + // fn test_parse_widget_definition() { + // let expected = WidgetDefinition { + // name: "example_widget".to_string(), + // structure: ElementUse::Widget(WidgetUse { + // name: "layout_horizontal".to_string(), + // attrs: HashMap::new(), + // children: vec![ + // ElementUse::Widget(WidgetUse::new( + // "text".to_string(), + // vec![ElementUse::Text("hi".to_string())], + // )), + // ElementUse::Widget(WidgetUse::new("text".to_string(), vec![])), + // ], + // }), + // }; - let parsed_hocon = parse_hocon("{ text: { children: \"hi\" } }").unwrap(); - assert_eq!( - parse_element_use(parsed_hocon).unwrap(), - ElementUse::Widget(WidgetUse::new( - "text".to_string(), - vec![ElementUse::Text("hi".to_string())] - )) - ); - assert_eq!(parse_widget_definition(EXAMPLE_CONFIG).unwrap(), expected); - } + // let parsed_hocon = parse_hocon("{ text: { children: \"hi\" } }").unwrap(); + // assert_eq!( + // parse_element_use(parsed_hocon).unwrap(), + // ElementUse::Widget(WidgetUse::new( + // "text".to_string(), + // vec![ElementUse::Text("hi".to_string())] + // )) + // ); + // assert_eq!(parse_widget_definition(EXAMPLE_CONFIG).unwrap(), expected); + // } } diff --git a/src/main.rs b/src/main.rs index e4ff820..f0aef4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,51 +2,95 @@ extern crate gio; extern crate gtk; -use anyhow::{self, Context, Result}; +use anyhow::{self, Result}; use gdk::*; use gio::prelude::*; use gtk::prelude::*; -use gtk::{Adjustment, Application, ApplicationWindow, Button, Scale}; -use regex::Regex; -use std::{collections::HashMap, str::FromStr}; +use gtk::{Application, ApplicationWindow}; +use std::{collections::HashMap, process::Command}; pub mod config; pub mod widgets; +const CMD_STRING_PLACEHODLER: &str = "{}"; + +const EXAMPLE_CONFIG: &str = r#"{ + widgets: { + some_widget: { + structure: { + layout_horizontal: { + class: "container", + children: [ + "hi", + { button: { children: "click me you" } } + { slider: { value: 12, min: 0, max: 50, onchange: "notify-send 'changed' {}" } } + "hu" + ] + } + } + } + }, + windows: { + main_window: { + pos.x: 200 + pos.y: 1550 + size.x: 500 + size.y: 50 + widget: { + some_widget: {} + } + } + }, + +}"#; + fn main() -> Result<()> { - let application = Application::new(Some("de.elkowar.eww"), Default::default()) + let eww_config = config::EwwConfig::from_hocon(&config::parse_hocon(EXAMPLE_CONFIG)?)?; + + let application = Application::new(Some("de.elkowar.eww"), gio::ApplicationFlags::FLAGS_NONE) .expect("failed to initialize GTK application"); - application.connect_activate(|app| { - let window = ApplicationWindow::new(app); - window.set_title("Eww"); - window.set_wmclass("noswallow", "noswallow"); - window.set_type_hint(gdk::WindowTypeHint::Dock); - window.set_position(gtk::WindowPosition::Center); - window.set_keep_above(true); + let window_def = eww_config.windows()["main_window"].clone(); - let element = config::parse_element_use( - config::parse_hocon( - r#"{ - layout_horizontal: { - children: [ - "hi", - { button: { children: "click me you" } } - { slider: {} } - "hu" - ] - } - }"#, - ) - .unwrap(), - ) - .unwrap(); + application.connect_activate(move |app| { + let app_window = ApplicationWindow::new(app); + app_window.set_title("Eww"); + app_window.set_wmclass("noswallow", "noswallow"); + app_window.set_type_hint(gdk::WindowTypeHint::Dock); + app_window.set_position(gtk::WindowPosition::Center); + app_window.set_keep_above(true); + app_window.set_default_size(window_def.size.0, window_def.size.1); + app_window.set_visual( + app_window + .get_display() + .get_default_screen() + .get_rgba_visual() + .or_else(|| { + app_window + .get_display() + .get_default_screen() + .get_system_visual() + }) + .as_ref(), + ); + + app_window.fullscreen(); let widget_state = WidgetState(HashMap::new()); - window.add(&element_to_gtk_widget(&widget_state, &element).unwrap()); + app_window.add( + &element_to_gtk_widget(&eww_config.widgets(), &widget_state, &window_def.widget) + .unwrap(), + ); - window.show_all(); + app_window.show_all(); + + let window = app_window.get_window().unwrap(); + + window.set_override_redirect(true); + window.move_(window_def.position.0, window_def.position.1); + window.show(); + window.raise(); }); application.run(&[]); @@ -54,17 +98,32 @@ fn main() -> Result<()> { } fn element_to_gtk_widget( + widget_definitions: &HashMap, widget_state: &WidgetState, element: &config::ElementUse, ) -> Option { match element { config::ElementUse::Text(text) => Some(gtk::Label::new(Some(&text)).upcast()), - config::ElementUse::Widget(widget) => widget_use_to_gtk_container(widget_state, &widget) - .or(widget_use_to_gtk_widget(widget_state, &widget)), + config::ElementUse::Widget(widget) => { + let gtk_widget = + widget_use_to_gtk_container(widget_definitions, widget_state, &widget).or( + widget_use_to_gtk_widget(widget_definitions, widget_state, &widget), + )?; + if let Some(css_class) = widget + .attrs + .get("class") + .and_then(config::AttrValue::as_string) + { + gtk_widget.get_style_context().add_class(css_class); + } + + Some(gtk_widget) + } } } fn widget_use_to_gtk_container( + widget_definitions: &HashMap, widget_state: &WidgetState, widget: &config::WidgetUse, ) -> Option { @@ -75,33 +134,63 @@ fn widget_use_to_gtk_container( }; for child in &widget.children { - container_widget.add(&element_to_gtk_widget(widget_state, child)?); + container_widget.add(&element_to_gtk_widget( + widget_definitions, + widget_state, + child, + )?); } Some(container_widget.upcast()) } fn widget_use_to_gtk_widget( - widget_state: &WidgetState, + widget_definitions: &HashMap, + state: &WidgetState, widget: &config::WidgetUse, ) -> Option { let new_widget: gtk::Widget = match widget.name.as_str() { "slider" => { - let slider_value: f64 = widget_state.resolve(widget.attrs.get("value")?)?; + let slider_value: f64 = state.resolve(widget.attrs.get("value")?)?.as_f64()?; + let slider_min: Option = + try { state.resolve(widget.attrs.get("min")?)?.as_f64()? }; + let slider_min = slider_min.unwrap_or(0f64); + let slider_max: Option = + try { state.resolve(widget.attrs.get("max")?)?.as_f64()? }; + let slider_max = slider_max.unwrap_or(100f64); - gtk::Scale::new( + let on_change: Option = try { + state + .resolve(widget.attrs.get("onchange")?)? + .as_string()? + .clone() + }; + + let scale = gtk::Scale::new( gtk::Orientation::Horizontal, Some(>k::Adjustment::new( slider_value, - 0.0, - 100.0, + slider_min, + slider_max, 1.0, 1.0, 1.0, )), - ) - .upcast() + ); + scale.set_property("draw-value", &false.to_value()).ok()?; + + if let Some(on_change) = on_change { + scale.connect_value_changed(move |scale| { + run_command(&on_change, scale.get_value()); + }); + } + + scale.upcast() } + name if widget_definitions.contains_key(name) => { + let def = &widget_definitions[name]; + element_to_gtk_widget(widget_definitions, state, &def.structure)? + } _ => return None, }; Some(new_widget) @@ -110,31 +199,20 @@ fn widget_use_to_gtk_widget( struct WidgetState(HashMap); impl WidgetState { - pub fn resolve(&self, value: &config::AttrValue) -> Option - where - T: FromStr, - { - let var_pattern: Regex = Regex::new(r"\$\$\{(.*)\}").unwrap(); - let config::AttrValue(value) = value; - - let mut missing_var: Option = None; - var_pattern.replace_all(value, |caps: ®ex::Captures| { - self.lookup_full::(&caps[1]).unwrap_or_else(|| { - missing_var = Some(caps[1].to_string()); - "missing".to_string() - }) - }); - - // TODO REEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEe - - unimplemented!(); + pub fn resolve(&self, value: &config::AttrValue) -> Option { + if let config::AttrValue::VarRef(name) = value { + // TODO REEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE + self.0.get(name).cloned() + } else { + Some(value.clone()) + } } +} - pub fn lookup_full(&self, key: &str) -> Option - where - T: FromStr, - { - self.resolve::(self.0.get(key)?) +fn run_command(cmd: &str, arg: T) { + let cmd = cmd.replace(CMD_STRING_PLACEHODLER, &format!("{}", arg)); + if let Err(e) = Command::new("bash").arg("-c").arg(cmd).output() { + eprintln!("{}", e); } }