Merge pull request #221 from elkowar/config_rework

Configuration format rework
This commit is contained in:
ElKowar 2021-08-18 16:56:16 +02:00 committed by GitHub
commit 9c12a316d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
213 changed files with 9866 additions and 3826 deletions

View file

@ -27,6 +27,6 @@ jobs:
run: cargo check --no-default-features --features=x11
- name: Build wayland
run: cargo check --no-default-features --features=wayland
- name: Build no-x11-wayland
run: cargo check --no-default-features --features=no-x11-wayland
- name: Build no-backend
run: cargo check --no-default-features

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
/**/target

936
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,73 +1,10 @@
[package]
name = "eww"
version = "0.1.0"
authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"]
edition = "2018"
description= "Widget system for everyone!"
license = "MIT"
repository = "https://github.com/elkowar/eww"
homepage = "https://github.com/elkowar/eww"
[workspace]
members = [
"crates/eww",
"crates/simplexpr",
"crates/yuck",
"crates/eww_shared_util"
]
[profile.dev]
split-debuginfo = "unpacked"
[features]
default = ["x11"]
x11 = ["gdkx11", "x11rb"]
wayland = ["gtk-layer-shell", "gtk-layer-shell-sys"]
no-x11-wayland = []
[dependencies.cairo-sys-rs]
version = "0.10.0"
[dependencies]
gtk = { version = "0.9", features = [ "v3_22" ] }
gdk = { version = "*", features = ["v3_22"] }
gio = { version = "*", features = ["v2_44"] }
glib = { version = "*", features = ["v2_44"] }
gdk-pixbuf = "0.9"
gtk-layer-shell = { version="0.2.0", optional=true }
gtk-layer-shell-sys = { version="0.2.0", optional=true }
gdkx11 = { version = "0.9", optional = true }
x11rb = { version = "0.8", features = ["randr"], optional = true }
regex = "1"
bincode = "1.3"
anyhow = "1.0"
derive_more = "0.99"
maplit = "1"
structopt = "0.3"
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
extend = "1"
grass = "0.10"
num = "0.4"
roxmltree = "0.14"
itertools = "0.10"
debug_stub_derive = "0.3"
log = "0.4"
pretty_env_logger = "0.4"
lazy_static = "1.4.0"
libc = "0.2"
nix = "0.20"
smart-default = "0.6"
simple-signal = "1.1"
unescape = "0.1"
tokio = { version = "1.0", features = ["full"] }
tokio-stream = "0.1"
async-stream = "0.3"
futures-core = "0.3"
futures-util = "0.3"
tokio-util = "0.6"
sysinfo = "0.16.1"
nom = "6.1"
dyn-clone = "1.0"
base64 = "0.13"
wait-timeout = "0.2"
notify = "5.0.0-pre.7"
[dev-dependencies]
pretty_assertions = "0.7.1"

View file

@ -4,18 +4,26 @@
<img src="./.github/EwwLogo.svg" height="100" align="left"/>
Elkowar&rsquo;s Wacky Widgets is a standalone widget system made in Rust that allows you to implement
Elkowars Wacky Widgets is a standalone widget system made in Rust that allows you to implement
your own, custom widgets in any window manager.
Documentation **and instructions on how to install** can be found [here](https://elkowar.github.io/eww).
## New configuration language is being made!
## New configuration language!
A new configuration language for Eww is being made! `yuck` is the new name! (as discussed in the [discussion post](https://github.com/elkowar/eww/discussions/206).)
You can check out its development in [this PR](https://github.com/elkowar/eww/pull/221) and maybe contribewwte!
YUCK IS ALIVE! After months of waiting, the new configuration language has now been released!
This also means that XML is no longer supported from this point onwards.
If you want to keep using the latest releases of eww, you'll need to migrate your config over to yuck.
The steps to migrate can be found in [the migration guide](YUCK_MIGRATION.md).
Additionally, a couple _amazing_ people have started to work on an
[automatic converter](https://github.com/undefinedDarkness/ewwxml) that can turn your old eww.xml into the new yuck format!
## Examples
(note that some of these still make use of the old configuration syntax)
* A basic bar, see [examples](./examples/eww-bar)
![Example 1](./examples/eww-bar/eww-bar.png)

29
YUCK_MIGRATION.md Normal file
View file

@ -0,0 +1,29 @@
# Migrating to yuck
Yuck is the new configuration syntax used by eww.
While the syntax has changed dramatically, the general structure of the configuration
has stayed mostly the same.
Most notably, the top-level blocks are now gone.
This means that `defvar`, `defwidget`, etc blocks no longer need to be in separate
sections of the file, but instead can be put wherever you need them.
Explaining the exact syntax of yuck would be significantly less effective than just
looking at an example, as the general syntax is very simple.
Thus, to get a feel for yuck, read through the [example configuration](./examples/eww-bar/eww.yuck).
Additionally, a couple smaller things have been changed.
The fields and structure of the `defwindow` block as been adjusted to better reflect
the options provided by the displayserver that is being used.
The major changes are:
- The `screen` field is now called `monitor`
- `reserve` and `geometry` are now structured slightly differently (see [here](./docs/src/configuration.md#creating-your-first-window))
To see how exactly the configuration now looks, check the [respective documentation](./docs/src/configuration.md#creating-your-first-window)
## Automatically converting your configuration
A couple _amazing_ people have started to work on an [automatic converter](https://github.com/undefinedDarkness/ewwxml) that can turn your
old eww.xml into the new yuck format!

71
crates/eww/Cargo.toml Normal file
View file

@ -0,0 +1,71 @@
[package]
name = "eww"
version = "0.2.0"
authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"]
edition = "2018"
description = "Widget system for everyone!"
license = "MIT"
repository = "https://github.com/elkowar/eww"
homepage = "https://github.com/elkowar/eww"
[features]
default = ["x11"]
x11 = ["gdkx11", "x11rb"]
wayland = ["gtk-layer-shell", "gtk-layer-shell-sys"]
[dependencies.cairo-sys-rs]
version = "0.10.0"
[dependencies]
gtk = { version = "0.9", features = [ "v3_22" ] }
gdk = { version = "*", features = ["v3_22"] }
gio = { version = "*", features = ["v2_44"] }
glib = { version = "*", features = ["v2_44"] }
gdk-pixbuf = "0.9"
gtk-layer-shell = { version="0.2.0", optional=true }
gtk-layer-shell-sys = { version="0.2.0", optional=true }
gdkx11 = { version = "0.9", optional = true }
x11rb = { version = "0.8", features = ["randr"], optional = true }
regex = "1"
bincode = "1.3"
anyhow = "1.0"
derive_more = "0.99"
maplit = "1"
structopt = "0.3"
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
extend = "1"
grass = "0.10"
itertools = "0.10"
debug_stub_derive = "0.3"
log = "0.4"
pretty_env_logger = "0.4"
libc = "0.2"
once_cell = "1.8"
nix = "0.20"
smart-default = "0.6"
simple-signal = "1.1"
unescape = "0.1"
unindent = "0.1"
tokio = { version = "1.0", features = ["full"] }
futures-core = "0.3"
futures-util = "0.3"
tokio-util = "0.6"
sysinfo = "0.16.1"
dyn-clone = "1.0"
base64 = "0.13"
wait-timeout = "0.2"
notify = "5.0.0-pre.7"
codespan-reporting = "0.11"
simplexpr = { path = "../simplexpr" }
eww_shared_util = { path = "../eww_shared_util" }
yuck = { path = "../yuck", default-features = false}

View file

@ -1,61 +1,43 @@
use crate::{
config,
config::{window_definition::WindowName, AnchorPoint},
display_backend, eww_state,
script_var_handler::*,
value::{Coords, NumWithUnit, PrimVal, VarName},
config, daemon_response::DaemonResponseSender, display_backend, error_handling_ctx, eww_state, script_var_handler::*,
EwwPaths,
};
use anyhow::*;
use debug_stub_derive::*;
use eww_shared_util::VarName;
use gdk::WindowExt;
use gtk::{ContainerExt, CssProviderExt, GtkWindowExt, StyleContextExt, WidgetExt};
use itertools::Itertools;
use std::collections::HashMap;
use simplexpr::dynval::DynVal;
use std::collections::{HashMap, HashSet};
use tokio::sync::mpsc::UnboundedSender;
/// Response that the app may send as a response to a event.
/// This is used in `DaemonCommand`s that contain a response sender.
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, derive_more::Display)]
pub enum DaemonResponse {
Success(String),
Failure(String),
}
impl DaemonResponse {
pub fn is_success(&self) -> bool {
matches!(self, DaemonResponse::Success(_))
}
pub fn is_failure(&self) -> bool {
!self.is_success()
}
}
pub type DaemonResponseSender = tokio::sync::mpsc::UnboundedSender<DaemonResponse>;
pub type DaemonResponseReceiver = tokio::sync::mpsc::UnboundedReceiver<DaemonResponse>;
use yuck::{
config::window_geometry::{AnchorPoint, WindowGeometry},
value::Coords,
};
#[derive(Debug)]
pub enum DaemonCommand {
NoOp,
UpdateVars(Vec<(VarName, PrimVal)>),
UpdateVars(Vec<(VarName, DynVal)>),
ReloadConfigAndCss(DaemonResponseSender),
UpdateConfig(config::EwwConfig),
UpdateCss(String),
OpenMany {
windows: Vec<WindowName>,
windows: Vec<String>,
sender: DaemonResponseSender,
},
OpenWindow {
window_name: WindowName,
window_name: String,
pos: Option<Coords>,
size: Option<Coords>,
anchor: Option<AnchorPoint>,
monitor: Option<i32>,
screen: Option<i32>,
should_toggle: bool,
sender: DaemonResponseSender,
},
CloseWindow {
window_name: WindowName,
window_name: String,
sender: DaemonResponseSender,
},
KillServer,
@ -70,7 +52,7 @@ pub enum DaemonCommand {
#[derive(Debug, Clone)]
pub struct EwwWindow {
pub name: WindowName,
pub name: String,
pub definition: config::EwwWindowDefinition,
pub gtk_window: gtk::Window,
}
@ -85,7 +67,10 @@ impl EwwWindow {
pub struct App {
pub eww_state: eww_state::EwwState,
pub eww_config: config::EwwConfig,
pub open_windows: HashMap<WindowName, EwwWindow>,
pub open_windows: HashMap<String, EwwWindow>,
/// Window names that are supposed to be open, but failed.
/// When reloading the config, these should be opened again.
pub failed_windows: HashSet<String>,
pub css_provider: gtk::CssProvider,
#[debug_stub = "ScriptVarHandler(...)"]
@ -111,24 +96,23 @@ impl App {
DaemonCommand::ReloadConfigAndCss(sender) => {
let mut errors = Vec::new();
let config_result = config::RawEwwConfig::read_from_file(&self.paths.get_eww_xml_path())
.and_then(config::EwwConfig::generate);
match config_result {
Ok(new_config) => self.handle_command(DaemonCommand::UpdateConfig(new_config)),
let config_result = config::read_from_file(&self.paths.get_yuck_path());
match config_result.and_then(|new_config| self.load_config(new_config)) {
Ok(()) => {}
Err(e) => errors.push(e),
}
let css_result = crate::util::parse_scss_from_file(&self.paths.get_eww_scss_path());
match css_result {
Ok(new_css) => self.handle_command(DaemonCommand::UpdateCss(new_css)),
match css_result.and_then(|css| self.load_css(&css)) {
Ok(()) => {}
Err(e) => errors.push(e),
}
let errors = errors.into_iter().map(|e| format!("{:?}", e)).join("\n");
let errors = errors.into_iter().map(|e| error_handling_ctx::format_error(&e)).join("\n");
if errors.is_empty() {
sender.send(DaemonResponse::Success(String::new()))?;
sender.send_success(String::new())?;
} else {
sender.send(DaemonResponse::Failure(errors))?;
sender.send_failure(errors)?;
}
}
DaemonCommand::UpdateConfig(config) => {
@ -150,15 +134,19 @@ impl App {
}
DaemonCommand::OpenMany { windows, sender } => {
let result = windows.iter().try_for_each(|w| self.open_window(w, None, None, None, None));
respond_with_error(sender, result)?;
respond_with_result(sender, result)?;
}
DaemonCommand::OpenWindow { window_name, pos, size, anchor, monitor, sender } => {
let result = self.open_window(&window_name, pos, size, monitor, anchor);
respond_with_error(sender, result)?;
DaemonCommand::OpenWindow { window_name, pos, size, anchor, screen: monitor, should_toggle, sender } => {
let result = if should_toggle && self.open_windows.contains_key(&window_name) {
self.close_window(&window_name)
} else {
self.open_window(&window_name, pos, size, monitor, anchor)
};
respond_with_result(sender, result)?;
}
DaemonCommand::CloseWindow { window_name, sender } => {
let result = self.close_window(&window_name);
respond_with_error(sender, result)?;
respond_with_result(sender, result)?;
}
DaemonCommand::PrintState { all, sender } => {
let vars = self.eww_state.get_variables().iter();
@ -169,7 +157,7 @@ impl App {
.map(|(key, value)| format!("{}: {}", key, value))
.join("\n")
};
sender.send(DaemonResponse::Success(output)).context("sending response from main thread")?
sender.send_success(output)?
}
DaemonCommand::PrintWindows(sender) => {
let output = self
@ -181,38 +169,43 @@ impl App {
format!("{}{}", if is_open { "*" } else { "" }, window_name)
})
.join("\n");
sender.send(DaemonResponse::Success(output)).context("sending response from main thread")?
sender.send_success(output)?
}
DaemonCommand::PrintDebug(sender) => {
let output = format!("state: {:#?}\n\nconfig: {:#?}", &self.eww_state, &self.eww_config);
sender.send(DaemonResponse::Success(output)).context("sending response from main thread")?
sender.send_success(output)?
}
}
};
crate::print_result_err!("while handling event", &result);
if let Err(err) = result {
error_handling_ctx::print_error(err);
}
}
fn stop_application(&mut self) {
self.script_var_handler.stop_all();
self.open_windows.drain().for_each(|(_, w)| w.close());
for (_, window) in self.open_windows.drain() {
window.close();
}
gtk::main_quit();
}
fn update_state(&mut self, fieldname: VarName, value: PrimVal) {
fn update_state(&mut self, fieldname: VarName, value: DynVal) {
self.eww_state.update_variable(fieldname, value)
}
fn close_window(&mut self, window_name: &WindowName) -> Result<()> {
fn close_window(&mut self, window_name: &String) -> Result<()> {
for unused_var in self.variables_only_used_in(window_name) {
log::info!("stopping for {}", &unused_var);
log::debug!("stopping for {}", &unused_var);
self.script_var_handler.stop_for_variable(unused_var.clone());
}
let window =
self.open_windows.remove(window_name).context(format!("No window with name '{}' is running.", window_name))?;
self.open_windows
.remove(window_name)
.with_context(|| format!("Tried to close window named '{}', but no such window was open", window_name))?
.close();
window.close();
self.eww_state.clear_window_state(window_name);
Ok(())
@ -220,42 +213,55 @@ impl App {
fn open_window(
&mut self,
window_name: &WindowName,
window_name: &String,
pos: Option<Coords>,
size: Option<Coords>,
monitor: Option<i32>,
anchor: Option<config::AnchorPoint>,
anchor: Option<AnchorPoint>,
) -> Result<()> {
// remove and close existing window with the same name
let _ = self.close_window(window_name);
self.failed_windows.remove(window_name);
log::info!("Opening window {}", window_name);
let mut window_def = self.eww_config.get_window(window_name)?.clone();
window_def.geometry = window_def.geometry.override_if_given(anchor, pos, size);
// if an instance of this is already running, close it
let _ = self.close_window(window_name);
let root_widget =
window_def.widget.render(&mut self.eww_state, window_name, &self.eww_config.get_widget_definitions())?;
root_widget.get_style_context().add_class(&window_name.to_string());
let open_result: Result<_> = try {
let mut window_def = self.eww_config.get_window(window_name)?.clone();
window_def.geometry = window_def.geometry.map(|x| x.override_if_given(anchor, pos, size));
let monitor_geometry =
get_monitor_geometry(monitor.or(window_def.screen_number).unwrap_or_else(get_default_monitor_index));
let eww_window = initialize_window(monitor_geometry, root_widget, window_def)?;
let root_widget =
window_def.widget.render(&mut self.eww_state, window_name, &self.eww_config.get_widget_definitions())?;
self.open_windows.insert(window_name.clone(), eww_window);
root_widget.get_style_context().add_class(&window_name.to_string());
// initialize script var handlers for variables that where not used before opening this window.
// TODO somehow make this less shit
for newly_used_var in
self.variables_only_used_in(window_name).filter_map(|var| self.eww_config.get_script_var(var).ok())
{
self.script_var_handler.add(newly_used_var.clone());
let monitor_geometry = get_monitor_geometry(monitor.or(window_def.monitor_number))?;
let eww_window = initialize_window(monitor_geometry, root_widget, window_def)?;
self.open_windows.insert(window_name.clone(), eww_window);
// initialize script var handlers for variables that where not used before opening this window.
// TODO somehow make this less shit
for newly_used_var in
self.variables_only_used_in(window_name).filter_map(|var| self.eww_config.get_script_var(var).ok())
{
self.script_var_handler.add(newly_used_var.clone());
}
};
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))
} else {
Ok(())
}
Ok(())
}
/// Load the given configuration, reloading all script-vars and reopening all windows that where opened.
/// Load the given configuration, reloading all script-vars and attempting to reopen all windows that where opened.
pub fn load_config(&mut self, config: config::EwwConfig) -> Result<()> {
// TODO the reload procedure is kinda bad.
// It should probably instead prepare a new eww_state and everything, and then swap the instances once everything has worked.
log::info!("Reloading windows");
// refresh script-var poll stuff
self.script_var_handler.stop_all();
@ -263,9 +269,8 @@ impl App {
self.eww_config = config;
self.eww_state.clear_all_window_states();
let windows = self.open_windows.clone();
for (window_name, window) in windows {
window.close();
let window_names: Vec<String> = 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)?;
}
Ok(())
@ -282,7 +287,7 @@ impl App {
}
/// Get all variables mapped to a list of windows they are being used in.
pub fn currently_used_variables<'a>(&'a self) -> HashMap<&'a VarName, Vec<&'a WindowName>> {
pub fn currently_used_variables<'a>(&'a self) -> HashMap<&'a VarName, Vec<&'a String>> {
let mut vars: HashMap<&'a VarName, Vec<_>> = HashMap::new();
for window_name in self.open_windows.keys() {
for var in self.eww_state.vars_referenced_in(window_name) {
@ -293,7 +298,7 @@ impl App {
}
/// Get all variables that are only used in the given window.
pub fn variables_only_used_in<'a>(&'a self, window: &'a WindowName) -> impl Iterator<Item = &'a VarName> {
pub fn variables_only_used_in<'a>(&'a self, window: &'a String) -> impl Iterator<Item = &'a VarName> {
self.currently_used_variables()
.into_iter()
.filter(move |(_, wins)| wins.len() == 1 && wins.contains(&window))
@ -306,55 +311,54 @@ fn initialize_window(
root_widget: gtk::Widget,
window_def: config::EwwWindowDefinition,
) -> Result<EwwWindow> {
let actual_window_rect = window_def.geometry.get_window_rectangle(monitor_geometry);
if let Some(window) = display_backend::initialize_window(&window_def, monitor_geometry) {
window.set_title(&format!("Eww - {}", window_def.name));
let wm_class_name = format!("eww-{}", window_def.name);
window.set_wmclass(&wm_class_name, &wm_class_name);
window.set_position(gtk::WindowPosition::Center);
let window = display_backend::initialize_window(&window_def, monitor_geometry)
.with_context(|| format!("monitor {} is unavailable", window_def.monitor_number.unwrap()))?;
window.set_title(&format!("Eww - {}", window_def.name));
window.set_position(gtk::WindowPosition::None);
window.set_gravity(gdk::Gravity::Center);
if let Some(geometry) = window_def.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);
window.set_decorated(false);
// run on_screen_changed to set the visual correctly initially.
on_screen_changed(&window, None);
window.connect_screen_changed(on_screen_changed);
window.add(&root_widget);
window.show_all();
apply_window_position(window_def.clone(), monitor_geometry, &window)?;
let gdk_window = window.get_window().context("couldn't get gdk window from gtk window")?;
gdk_window.set_override_redirect(!window_def.focusable);
#[cfg(feature = "x11")]
display_backend::set_xprops(&window, monitor_geometry, &window_def)?;
// this should only be required on x11, as waylands layershell should manage the margins properly anways.
#[cfg(feature = "x11")]
window.connect_configure_event({
let window_def = window_def.clone();
move |window, _evt| {
let _ = apply_window_position(window_def.clone(), monitor_geometry, &window);
false
}
});
Ok(EwwWindow { name: window_def.name.clone(), definition: window_def, gtk_window: window })
} else {
Err(anyhow!("monitor {} is unavailable", window_def.screen_number.unwrap()))
}
window.set_decorated(false);
window.set_skip_taskbar_hint(true);
window.set_skip_pager_hint(true);
// run on_screen_changed to set the visual correctly initially.
on_screen_changed(&window, None);
window.connect_screen_changed(on_screen_changed);
window.add(&root_widget);
window.show_all();
#[cfg(feature = "x11")]
{
if let Some(geometry) = window_def.geometry {
let _ = apply_window_position(geometry, monitor_geometry, &window);
window.connect_configure_event(move |window, _| {
let _ = apply_window_position(geometry, monitor_geometry, &window);
false
});
}
display_backend::set_xprops(&window, monitor_geometry, &window_def)?;
}
Ok(EwwWindow { name: window_def.name.clone(), definition: window_def, gtk_window: window })
}
/// Apply the provided window-positioning rules to the window.
#[cfg(feature = "x11")]
fn apply_window_position(
mut window_def: config::EwwWindowDefinition,
mut window_geometry: WindowGeometry,
monitor_geometry: gdk::Rectangle,
window: &gtk::Window,
) -> Result<()> {
let (gtk_window_width, gtk_window_height) = window.get_size();
window_def.geometry.size = Coords { x: NumWithUnit::Pixels(gtk_window_width), y: NumWithUnit::Pixels(gtk_window_height) };
let gdk_window = window.get_window().context("Failed to get gdk window from gtk window")?;
let actual_window_rect = window_def.geometry.get_window_rectangle(monitor_geometry);
window_geometry.size = Coords::from_pixels(window.get_size());
let actual_window_rect = get_window_rectangle(window_geometry, monitor_geometry);
gdk_window.move_(actual_window_rect.x, actual_window_rect.y);
Ok(())
}
@ -366,20 +370,34 @@ fn on_screen_changed(window: &gtk::Window, _old_screen: Option<&gdk::Screen>) {
window.set_visual(visual.as_ref());
}
fn get_default_monitor_index() -> i32 {
gdk::Display::get_default().expect("could not get default display").get_default_screen().get_primary_monitor()
}
/// Get the monitor geometry of a given monitor number
fn get_monitor_geometry(n: i32) -> gdk::Rectangle {
gdk::Display::get_default().expect("could not get default display").get_default_screen().get_monitor_geometry(n)
/// Get the monitor geometry of a given monitor number, or the default if none is given
fn get_monitor_geometry(n: Option<i32>) -> Result<gdk::Rectangle> {
#[allow(deprecated)]
let display = gdk::Display::get_default().expect("could not get default display");
let monitor = match n {
Some(n) => display.get_monitor(n).with_context(|| format!("Failed to get monitor with index {}", n))?,
None => display.get_primary_monitor().context("Failed to get primary monitor from GTK")?,
};
Ok(monitor.get_geometry())
}
/// In case of an Err, send the error message to a sender.
fn respond_with_error<T>(sender: DaemonResponseSender, result: Result<T>) -> Result<()> {
fn respond_with_result<T>(sender: DaemonResponseSender, result: Result<T>) -> Result<()> {
match result {
Ok(_) => sender.send(DaemonResponse::Success(String::new())),
Err(e) => sender.send(DaemonResponse::Failure(format!("{:?}", e))),
Ok(_) => sender.send_success(String::new()),
Err(e) => {
let formatted = error_handling_ctx::format_error(&e);
println!("Action failed with error: {}", formatted);
sender.send_failure(formatted)
},
}
.context("sending response from main thread")
}
pub fn get_window_rectangle(geometry: WindowGeometry, screen_rect: gdk::Rectangle) -> gdk::Rectangle {
let (offset_x, offset_y) = geometry.offset.relative_to(screen_rect.width, screen_rect.height);
let (width, height) = geometry.size.relative_to(screen_rect.width, screen_rect.height);
let x = screen_rect.x + offset_x + geometry.anchor_point.x.alignment_to_coordinate(width, screen_rect.width);
let y = screen_rect.y + offset_y + geometry.anchor_point.y.alignment_to_coordinate(height, screen_rect.height);
gdk::Rectangle { x, y, width, height }
}

View file

@ -3,11 +3,10 @@
//! `recv_exit()` function which can be awaited to receive an event in case of application termination.
use anyhow::*;
use once_cell::sync::Lazy;
use tokio::sync::broadcast;
lazy_static::lazy_static! {
static ref APPLICATION_EXIT_SENDER: broadcast::Sender<()> = broadcast::channel(2).0;
}
pub static APPLICATION_EXIT_SENDER: Lazy<broadcast::Sender<()>> = Lazy::new(|| broadcast::channel(2).0);
/// Notify all listening tasks of the termination of the eww application process.
pub fn send_exit() -> Result<()> {

View file

@ -1,7 +1,7 @@
use std::process::Stdio;
use crate::{
app,
daemon_response::DaemonResponse,
opts::{self, ActionClientOnly},
EwwPaths,
};
@ -24,8 +24,8 @@ pub fn handle_client_only_action(paths: &EwwPaths, action: ActionClientOnly) ->
Ok(())
}
pub fn do_server_call(mut stream: UnixStream, action: opts::ActionWithServer) -> Result<Option<app::DaemonResponse>> {
log::info!("Forwarding options to server");
pub fn do_server_call(stream: &mut UnixStream, action: &opts::ActionWithServer) -> Result<Option<DaemonResponse>> {
log::debug!("Forwarding options to server");
stream.set_nonblocking(false).context("Failed to set stream to non-blocking")?;
let message_bytes = bincode::serialize(&action)?;

View file

@ -0,0 +1,90 @@
use anyhow::*;
use eww_shared_util::VarName;
use std::{collections::HashMap, path::Path};
use yuck::config::{
file_provider::YuckFiles, script_var_definition::ScriptVarDefinition, widget_definition::WidgetDefinition, Config,
};
use simplexpr::dynval::DynVal;
use crate::error_handling_ctx;
use super::{script_var, EwwWindowDefinition};
/// Load an [EwwConfig] from a given file, resetting and applying the global YuckFiles object in [error_handling_ctx].
pub fn read_from_file(path: impl AsRef<Path>) -> Result<EwwConfig> {
error_handling_ctx::clear_files();
EwwConfig::read_from_file(&mut error_handling_ctx::YUCK_FILES.write().unwrap(), path)
}
/// Eww configuration structure.
#[derive(Debug, Clone)]
pub struct EwwConfig {
widgets: HashMap<String, WidgetDefinition>,
windows: HashMap<String, EwwWindowDefinition>,
initial_variables: HashMap<VarName, DynVal>,
script_vars: HashMap<VarName, ScriptVarDefinition>,
}
impl Default for EwwConfig {
fn default() -> Self {
Self { widgets: HashMap::new(), windows: HashMap::new(), initial_variables: HashMap::new(), script_vars: HashMap::new() }
}
}
impl EwwConfig {
pub fn read_from_file(files: &mut YuckFiles, path: impl AsRef<Path>) -> Result<Self> {
if !path.as_ref().exists() {
bail!("The configuration file `{}` does not exist", path.as_ref().display());
}
let config = Config::generate_from_main_file(files, path)?;
// run some validations on the configuration
yuck::config::validate::validate(&config, super::inbuilt::get_inbuilt_vars().keys().cloned().collect())?;
let Config { widget_definitions, window_definitions, var_definitions, mut script_vars } = config;
script_vars.extend(crate::config::inbuilt::get_inbuilt_vars());
Ok(EwwConfig {
windows: window_definitions
.into_iter()
.map(|(name, window)| Ok((name, EwwWindowDefinition::generate(&widget_definitions, window)?)))
.collect::<Result<HashMap<_, _>>>()?,
widgets: widget_definitions,
initial_variables: var_definitions.into_iter().map(|(k, v)| (k, v.initial_value)).collect(),
script_vars,
})
}
// TODO this is kinda ugly
pub fn generate_initial_state(&self) -> Result<HashMap<VarName, DynVal>> {
let mut vars = self
.script_vars
.iter()
.map(|(name, var)| Ok((name.clone(), script_var::initial_value(var)?)))
.collect::<Result<HashMap<_, _>>>()?;
vars.extend(self.initial_variables.clone());
Ok(vars)
}
pub fn get_windows(&self) -> &HashMap<String, EwwWindowDefinition> {
&self.windows
}
pub fn get_window(&self, name: &String) -> Result<&EwwWindowDefinition> {
self.windows.get(name).with_context(|| {
format!(
"No window named '{}' exists in config.\nThis may also be caused by your config failing to load properly, \
please check for any other errors in that case.",
name
)
})
}
pub fn get_script_var(&self, name: &VarName) -> Result<&ScriptVarDefinition> {
self.script_vars.get(name).with_context(|| format!("No script var named '{}' exists", name))
}
pub fn get_widget_definitions(&self) -> &HashMap<String, WidgetDefinition> {
&self.widgets
}
}

View file

@ -1,35 +1,38 @@
use crate::{
config::{system_stats::*, PollScriptVar, ScriptVar, VarSource},
value::{PrimVal as PrimitiveValue, VarName},
};
use std::{collections::HashMap, time::Duration};
use simplexpr::dynval::DynVal;
use yuck::config::script_var_definition::{PollScriptVar, ScriptVarDefinition, VarSource};
use crate::config::system_stats::*;
use eww_shared_util::VarName;
macro_rules! builtin_vars {
($interval:expr, $($name:literal => $fun:expr),*$(,)?) => {{
maplit::hashmap! {
$(
VarName::from($name) => ScriptVar::Poll(PollScriptVar {
VarName::from($name) => ScriptVarDefinition::Poll(PollScriptVar {
name: VarName::from($name),
command: VarSource::Function($fun),
interval: $interval,
name_span: eww_shared_util::span::Span::DUMMY,
})
),*
}
}}}
pub fn get_inbuilt_vars() -> HashMap<VarName, ScriptVar> {
pub fn get_inbuilt_vars() -> HashMap<VarName, ScriptVarDefinition> {
builtin_vars! {Duration::new(2, 0),
// @desc EWW_TEMPS - Heat of the components in Celcius\nExample: `{{(CPU_TEMPS.core_1 + CPU_TEMPS.core_2) / 2}}`
"EWW_TEMPS" => || Ok(PrimitiveValue::from(cores())),
"EWW_TEMPS" => || Ok(DynVal::from(cores())),
// @desc EWW_RAM - The current RAM + Swap usage
"EWW_RAM" => || Ok(PrimitiveValue::from(format!("{:.2}", ram()))),
"EWW_RAM" => || Ok(DynVal::from(format!("{:.2}", ram()))),
// @desc EWW_DISK - Information on on all mounted partitions (Might report inaccurately on some filesystems, like btrfs)\nExample: `{{EWW_DISK["/"]}}`
"EWW_DISK" => || Ok(PrimitiveValue::from(disk())),
"EWW_DISK" => || Ok(DynVal::from(disk())),
// @desc EWW_BATTERY - Battery capacity in procent of the main battery
"EWW_BATTERY" => || Ok(PrimitiveValue::from(
"EWW_BATTERY" => || Ok(DynVal::from(
match get_battery_capacity() {
Err(e) => {
log::error!("Couldn't get the battery capacity: {:?}", e);
@ -40,9 +43,9 @@ pub fn get_inbuilt_vars() -> HashMap<VarName, ScriptVar> {
)),
// @desc EWW_CPU_USAGE - Average CPU usage (all cores) since the last update (No MacOS support)
"EWW_CPU_USAGE" => || Ok(PrimitiveValue::from(get_avg_cpu_usage())),
"EWW_CPU_USAGE" => || Ok(DynVal::from(get_avg_cpu_usage())),
// @desc EWW_NET - Bytes up/down on all interfaces
"EWW_NET" => || Ok(PrimitiveValue::from(net())),
"EWW_NET" => || Ok(DynVal::from(net())),
}
}

View file

@ -0,0 +1,8 @@
pub mod eww_config;
pub mod inbuilt;
pub mod script_var;
pub mod system_stats;
pub mod window_definition;
pub use eww_config::*;
pub use script_var::*;
pub use window_definition::*;

View file

@ -0,0 +1,47 @@
use std::process::Command;
use anyhow::*;
use codespan_reporting::diagnostic::Severity;
use eww_shared_util::{Span, VarName};
use simplexpr::dynval::DynVal;
use yuck::{
config::script_var_definition::{ScriptVarDefinition, VarSource},
gen_diagnostic,
};
use crate::error::DiagError;
pub fn create_script_var_failed_warn(span: Span, var_name: &VarName, error_output: &str) -> DiagError {
DiagError::new(gen_diagnostic! {
kind = Severity::Warning,
msg = format!("The script for the `{}`-variable exited unsuccessfully", var_name),
label = span => "Defined here",
note = error_output,
})
}
pub fn initial_value(var: &ScriptVarDefinition) -> Result<DynVal> {
match var {
ScriptVarDefinition::Poll(x) => match &x.command {
VarSource::Function(f) => {
f().map_err(|err| anyhow!(err)).with_context(|| format!("Failed to compute initial value for {}", &var.name()))
}
VarSource::Shell(span, command) => {
run_command(command).map_err(|e| anyhow!(create_script_var_failed_warn(*span, var.name(), &e.to_string())))
}
},
ScriptVarDefinition::Listen(var) => Ok(var.initial_value.clone()),
}
}
/// Run a command and get the output
pub fn run_command(cmd: &str) -> Result<DynVal> {
log::debug!("Running command: {}", cmd);
let command = Command::new("/bin/sh").arg("-c").arg(cmd).output()?;
if !command.status.success() {
bail!("Failed with output:\n{}", String::from_utf8(command.stderr)?);
}
let output = String::from_utf8(command.stdout)?;
let output = output.trim_matches('\n');
Ok(DynVal::from(output))
}

View file

@ -1,13 +1,11 @@
use crate::util::IterAverage;
use anyhow::*;
use itertools::Itertools;
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use std::{fs::read_to_string, sync::Mutex};
use sysinfo::{ComponentExt, DiskExt, NetworkExt, NetworksExt, ProcessorExt, System, SystemExt};
lazy_static! {
static ref SYSTEM: Mutex<System> = Mutex::new(System::new());
}
static SYSTEM: Lazy<Mutex<System>> = Lazy::new(|| Mutex::new(System::new()));
pub fn disk() -> String {
let mut c = SYSTEM.lock().unwrap();
@ -63,7 +61,6 @@ pub fn get_avg_cpu_usage() -> String {
#[cfg(target_os = "macos")]
pub fn get_battery_capacity() -> Result<String> {
use regex::Regex;
let capacity = String::from_utf8(
std::process::Command::new("pmset")
.args(&["-g", "batt"])
@ -75,7 +72,7 @@ pub fn get_battery_capacity() -> Result<String> {
// Example output of that command:
// Now drawing from 'Battery Power'
//-InternalBattery-0 (id=11403363) 100%; discharging; (no estimate) present: true
let regex = Regex::new(r"[0-9]*%")?;
let regex = regex!(r"[0-9]*%");
let mut number = regex.captures(&capacity).unwrap().get(0).unwrap().as_str().to_string();
// Removes the % at the end

View file

@ -0,0 +1,39 @@
use std::collections::HashMap;
use anyhow::*;
use yuck::config::{
backend_window_options::BackendWindowOptions,
widget_definition::WidgetDefinition,
window_definition::{WindowDefinition, WindowStacking},
window_geometry::WindowGeometry,
};
use crate::widgets::widget_node;
/// Full window-definition containing the fully expanded widget tree.
/// **Use this** rather than `[RawEwwWindowDefinition]`.
#[derive(Debug, Clone)]
pub struct EwwWindowDefinition {
pub name: String,
pub geometry: Option<WindowGeometry>,
pub stacking: WindowStacking,
pub monitor_number: Option<i32>,
pub widget: Box<dyn widget_node::WidgetNode>,
pub resizable: bool,
pub backend_options: BackendWindowOptions,
}
impl EwwWindowDefinition {
pub fn generate(defs: &HashMap<String, WidgetDefinition>, window: WindowDefinition) -> Result<Self> {
Ok(EwwWindowDefinition {
name: window.name,
geometry: window.geometry,
stacking: window.stacking,
monitor_number: window.monitor_number,
resizable: window.resizable,
widget: widget_node::generate_generic_widget_node(defs, &HashMap::new(), window.widget)?,
backend_options: window.backend_options,
})
}
}

View file

@ -0,0 +1,39 @@
use anyhow::*;
/// Response that the app may send as a response to a event.
/// This is used in `DaemonCommand`s that contain a response sender.
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, derive_more::Display)]
pub enum DaemonResponse {
Success(String),
Failure(String),
}
impl DaemonResponse {
pub fn is_success(&self) -> bool {
matches!(self, DaemonResponse::Success(_))
}
pub fn is_failure(&self) -> bool {
!self.is_success()
}
}
#[derive(Debug)]
pub struct DaemonResponseSender(tokio::sync::mpsc::UnboundedSender<DaemonResponse>);
pub fn create_pair() -> (DaemonResponseSender, tokio::sync::mpsc::UnboundedReceiver<DaemonResponse>) {
let (sender, recv) = tokio::sync::mpsc::unbounded_channel();
(DaemonResponseSender(sender), recv)
}
impl DaemonResponseSender {
pub fn send_success(&self, s: String) -> Result<()> {
self.0.send(DaemonResponse::Success(s)).context("Failed to send success response from application thread")
}
pub fn send_failure(&self, s: String) -> Result<()> {
self.0.send(DaemonResponse::Failure(s)).context("Failed to send failure response from application thread")
}
}
pub type DaemonResponseReceiver = tokio::sync::mpsc::UnboundedReceiver<DaemonResponse>;

View file

@ -1,57 +1,38 @@
pub use platform::*;
#[cfg(feature = "no-x11-wayland")]
#[cfg(not(any(feature = "x11", feature = "wayland")))]
mod platform {
use crate::config::{EwwWindowDefinition, StrutDefinition, WindowStacking};
use anyhow::*;
use gtk::{self, prelude::*};
use crate::config::EwwWindowDefinition;
pub fn initialize_window(window_def: &EwwWindowDefinition, _monitor: gdk::Rectangle) -> Option<gtk::Window> {
let window = if window_def.focusable {
gtk::Window::new(gtk::WindowType::Toplevel)
} else {
gtk::Window::new(gtk::WindowType::Popup)
};
window.set_resizable(true);
if !window_def.focusable {
window.set_type_hint(gdk::WindowTypeHint::Dock);
}
if window_def.stacking == WindowStacking::Foreground {
window.set_keep_above(true);
} else {
window.set_keep_below(true);
}
Some(window)
}
pub fn reserve_space_for(_window: &gtk::Window, _monitor: gdk::Rectangle, _strut_def: StrutDefinition) -> Result<()> {
Err(anyhow!("Cannot reserve space on non X11 or and wayland backends"))
pub fn initialize_window(_window_def: &EwwWindowDefinition, _monitor: gdk::Rectangle) -> Option<gtk::Window> {
Some(gtk::Window::new(gtk::WindowType::Toplevel))
}
}
#[cfg(feature = "wayland")]
mod platform {
use crate::config::{AnchorAlignment, EwwWindowDefinition, Side, WindowStacking};
use anyhow::*;
use gdk;
use gtk::prelude::*;
use yuck::config::{window_definition::WindowStacking, window_geometry::AnchorAlignment};
use crate::config::EwwWindowDefinition;
pub fn initialize_window(window_def: &EwwWindowDefinition, monitor: gdk::Rectangle) -> Option<gtk::Window> {
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
match window_def.screen_number {
match window_def.monitor_number {
Some(index) => {
if let Some(monitor) = gdk::Display::get_default().expect("could not get default display").get_monitor(index) {
gtk_layer_shell::set_monitor(&window, &monitor);
} else {
return None
return None;
}
}
None => {},
None => {}
};
window.set_resizable(true);
window.set_resizable(window_def.resizable);
// Sets the layer where the layer shell surface will spawn
match window_def.stacking {
@ -62,44 +43,46 @@ mod platform {
}
// Sets the keyboard interactivity
gtk_layer_shell::set_keyboard_interactivity(&window, window_def.focusable);
// Positioning surface
let mut top = false;
let mut left = false;
let mut right = false;
let mut bottom = false;
gtk_layer_shell::set_keyboard_interactivity(&window, window_def.backend_options.focusable);
match window_def.geometry.anchor_point.x {
AnchorAlignment::START => left = true,
AnchorAlignment::CENTER => {}
AnchorAlignment::END => right = true,
if let Some(geometry) = window_def.geometry {
// Positioning surface
let mut top = false;
let mut left = false;
let mut right = false;
let mut bottom = false;
match geometry.anchor_point.x {
AnchorAlignment::START => left = true,
AnchorAlignment::CENTER => {}
AnchorAlignment::END => right = true,
}
match geometry.anchor_point.y {
AnchorAlignment::START => top = true,
AnchorAlignment::CENTER => {}
AnchorAlignment::END => bottom = true,
}
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Left, left);
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Right, right);
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Top, top);
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Bottom, bottom);
let xoffset = geometry.offset.x.relative_to(monitor.width);
let yoffset = geometry.offset.y.relative_to(monitor.height);
if left {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Left, xoffset);
} else {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Right, xoffset);
}
if bottom {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Bottom, yoffset);
} else {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Top, yoffset);
}
}
match window_def.geometry.anchor_point.y {
AnchorAlignment::START => top = true,
AnchorAlignment::CENTER => {}
AnchorAlignment::END => bottom = true,
}
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Left, left);
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Right, right);
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Top, top);
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Bottom, bottom);
let xoffset = window_def.geometry.offset.x.relative_to(monitor.width);
let yoffset = window_def.geometry.offset.y.relative_to(monitor.height);
if left {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Left, xoffset);
} else {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Right, xoffset);
}
if bottom {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Bottom, yoffset);
} else {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Top, yoffset);
}
if window_def.exclusive {
if window_def.backend_options.exclusive {
gtk_layer_shell::auto_exclusive_zone_enable(&window);
}
Some(window)
@ -108,7 +91,6 @@ mod platform {
#[cfg(feature = "x11")]
mod platform {
use crate::config::{EwwWindowDefinition, EwwWindowType, Side, WindowStacking};
use anyhow::*;
use gdkx11;
use gtk::{self, prelude::*};
@ -120,21 +102,26 @@ mod platform {
protocol::xproto::*,
rust_connection::{DefaultStream, RustConnection},
};
use yuck::config::{
backend_window_options::{Side, WindowType},
window_definition::WindowStacking,
};
use crate::config::EwwWindowDefinition;
pub fn initialize_window(window_def: &EwwWindowDefinition, _monitor: gdk::Rectangle) -> Option<gtk::Window> {
let window = if window_def.focusable {
gtk::Window::new(gtk::WindowType::Toplevel)
let window_type = if window_def.backend_options.wm_ignore { gtk::WindowType::Popup } else { gtk::WindowType::Toplevel };
let window = gtk::Window::new(window_type);
let wm_class_name = format!("eww-{}", window_def.name);
#[allow(deprecated)]
window.set_wmclass(&wm_class_name, &wm_class_name);
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.sticky {
window.stick();
} else {
gtk::Window::new(gtk::WindowType::Popup)
};
window.set_resizable(true);
if !window_def.focusable {
window.set_type_hint(gdk::WindowTypeHint::Dock);
}
if window_def.stacking == WindowStacking::Foreground {
window.set_keep_above(true);
} else {
window.set_keep_below(true);
window.unstick();
}
Some(window)
}
@ -172,7 +159,7 @@ mod platform {
.ok()
.context("Failed to get x11 window for gtk window")?
.get_xid() as u32;
let strut_def = window_def.struts;
let strut_def = window_def.backend_options.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;
@ -225,11 +212,12 @@ mod platform {
win_id,
self.atoms._NET_WM_WINDOW_TYPE,
self.atoms.ATOM,
&[match window_def.window_type {
EwwWindowType::Dock => self.atoms._NET_WM_WINDOW_TYPE_DOCK,
EwwWindowType::Normal => self.atoms._NET_WM_WINDOW_TYPE_NORMAL,
EwwWindowType::Dialog => self.atoms._NET_WM_WINDOW_TYPE_DIALOG,
EwwWindowType::Toolbar => self.atoms._NET_WM_WINDOW_TYPE_TOOLBAR,
&[match window_def.backend_options.window_type {
WindowType::Dock => self.atoms._NET_WM_WINDOW_TYPE_DOCK,
WindowType::Normal => self.atoms._NET_WM_WINDOW_TYPE_NORMAL,
WindowType::Dialog => self.atoms._NET_WM_WINDOW_TYPE_DIALOG,
WindowType::Toolbar => self.atoms._NET_WM_WINDOW_TYPE_TOOLBAR,
WindowType::Utility => self.atoms._NET_WM_WINDOW_TYPE_UTILITY,
}],
)?
.check()?;
@ -245,6 +233,7 @@ mod platform {
_NET_WM_WINDOW_TYPE_DOCK,
_NET_WM_WINDOW_TYPE_DIALOG,
_NET_WM_WINDOW_TYPE_TOOLBAR,
_NET_WM_WINDOW_TYPE_UTILITY,
_NET_WM_STATE,
_NET_WM_STATE_STICKY,
_NET_WM_STATE_ABOVE,

20
crates/eww/src/error.rs Normal file
View file

@ -0,0 +1,20 @@
use codespan_reporting::diagnostic::Diagnostic;
/// An error that contains a [Diagnostic] for ad-hoc creation of diagnostics.
#[derive(Debug)]
pub struct DiagError {
pub diag: Diagnostic<usize>,
}
impl DiagError {
pub fn new(diag: Diagnostic<usize>) -> Self {
Self { diag }
}
}
impl std::error::Error for DiagError {}
impl std::fmt::Display for DiagError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.diag.message)
}
}

View file

@ -0,0 +1,88 @@
//! Disgusting global state.
//! I hate this, but [buffet](https://github.com/buffet) told me that this is what I should do for peak maintainability!
use std::sync::{Arc, RwLock};
use codespan_reporting::{
diagnostic::Diagnostic,
term::{self, Chars},
};
use eww_shared_util::Span;
use once_cell::sync::Lazy;
use simplexpr::{dynval::ConversionError, eval::EvalError};
use yuck::{
config::{file_provider::YuckFiles, validate::ValidationError},
error::AstError,
format_diagnostic::ToDiagnostic,
};
use crate::error::DiagError;
pub static YUCK_FILES: Lazy<Arc<RwLock<YuckFiles>>> = Lazy::new(|| Arc::new(RwLock::new(YuckFiles::new())));
pub fn clear_files() {
*YUCK_FILES.write().unwrap() = YuckFiles::new();
}
pub fn print_error(err: anyhow::Error) {
match anyhow_err_to_diagnostic(&err) {
Some(diag) => match stringify_diagnostic(diag) {
Ok(diag) => {
eprintln!("{}", diag);
}
Err(_) => {
log::error!("{:?}", err);
}
},
None => {
log::error!("{:?}", err);
}
}
}
pub fn format_error(err: &anyhow::Error) -> String {
for err in err.chain() {
format!("chain: {}", err);
}
anyhow_err_to_diagnostic(err).and_then(|diag| stringify_diagnostic(diag).ok()).unwrap_or_else(|| format!("{:?}", err))
}
pub fn anyhow_err_to_diagnostic(err: &anyhow::Error) -> Option<Diagnostic<usize>> {
if let Some(err) = err.downcast_ref::<DiagError>() {
Some(err.diag.clone())
} else if let Some(err) = err.downcast_ref::<AstError>() {
Some(err.to_diagnostic())
} else if let Some(err) = err.downcast_ref::<ConversionError>() {
Some(err.to_diagnostic())
} else if let Some(err) = err.downcast_ref::<ValidationError>() {
Some(err.to_diagnostic())
} else if let Some(err) = err.downcast_ref::<EvalError>() {
Some(err.to_diagnostic())
} else {
None
}
}
// pub fn print_diagnostic(diagnostic: codespan_reporting::diagnostic::Diagnostic<usize>) {
// match stringify_diagnostic(diagnostic.clone()) {
// Ok(diag) => {
// eprintln!("{}", diag);
//}
// Err(_) => {
// log::error!("{:?}", diagnostic);
//}
pub fn stringify_diagnostic(mut diagnostic: codespan_reporting::diagnostic::Diagnostic<usize>) -> anyhow::Result<String> {
diagnostic.labels.drain_filter(|label| Span(label.range.start, label.range.end, label.file_id).is_dummy());
let mut config = term::Config::default();
let mut chars = Chars::box_drawing();
chars.single_primary_caret = '─';
config.chars = chars;
config.chars.note_bullet = '→';
let mut buf = Vec::new();
let mut writer = term::termcolor::Ansi::new(&mut buf);
let files = YUCK_FILES.read().unwrap();
term::emit(&mut writer, &config, &*files, &diagnostic)?;
Ok(String::from_utf8(buf)?)
}

View file

@ -1,39 +1,42 @@
use crate::{
config::window_definition::WindowName,
value::{AttrName, AttrValElement, VarName},
};
use anyhow::*;
use eww_shared_util::{AttrName, VarName};
use std::{collections::HashMap, sync::Arc};
use crate::value::{AttrVal, PrimVal};
use simplexpr::{dynval::DynVal, SimplExpr};
use crate::error_handling_ctx;
/// Handler that gets executed to apply the necessary parts of the eww state to
/// a gtk widget. These are created and initialized in EwwState::resolve.
pub struct StateChangeHandler {
func: Box<dyn Fn(HashMap<AttrName, PrimVal>) -> Result<()> + 'static>,
unresolved_values: HashMap<AttrName, AttrVal>,
func: Box<dyn Fn(HashMap<AttrName, DynVal>) -> Result<()> + 'static>,
unresolved_values: HashMap<AttrName, SimplExpr>,
}
impl StateChangeHandler {
fn used_variables(&self) -> impl Iterator<Item = &VarName> {
self.unresolved_values.iter().flat_map(|(_, value)| value.var_refs())
self.unresolved_values.iter().flat_map(|(_, value)| value.var_refs()).map(|(_, value)| value)
}
/// Run the StateChangeHandler.
/// [`state`] should be the global [EwwState::state].
fn run_with_state(&self, state: &HashMap<VarName, PrimVal>) {
fn run_with_state(&self, state: &HashMap<VarName, DynVal>) {
let resolved_attrs = self
.unresolved_values
.clone()
.into_iter()
.map(|(attr_name, value)| Ok((attr_name, value.resolve_fully(state)?)))
.map(|(attr_name, value)| Ok((attr_name, value.eval(state)?)))
.collect::<Result<_>>();
match resolved_attrs {
Ok(resolved_attrs) => {
crate::print_result_err!("while updating UI based after state change", &(self.func)(resolved_attrs))
if let Err(err) = (self.func)(resolved_attrs).context("Error while updating UI after state change") {
error_handling_ctx::print_error(err);
}
}
Err(err) => {
error_handling_ctx::print_error(err);
}
Err(err) => log::error!("Error while resolving attributes: {:?}", err),
}
}
}
@ -60,8 +63,8 @@ impl EwwWindowState {
/// window-specific state-change handlers.
#[derive(Default)]
pub struct EwwState {
windows: HashMap<WindowName, EwwWindowState>,
variables_state: HashMap<VarName, PrimVal>,
windows: HashMap<String, EwwWindowState>,
variables_state: HashMap<VarName, DynVal>,
}
impl std::fmt::Debug for EwwState {
@ -71,16 +74,16 @@ impl std::fmt::Debug for EwwState {
}
impl EwwState {
pub fn from_default_vars(defaults: HashMap<VarName, PrimVal>) -> Self {
pub fn from_default_vars(defaults: HashMap<VarName, DynVal>) -> Self {
EwwState { variables_state: defaults, ..EwwState::default() }
}
pub fn get_variables(&self) -> &HashMap<VarName, PrimVal> {
pub fn get_variables(&self) -> &HashMap<VarName, DynVal> {
&self.variables_state
}
/// remove all state stored specific to one window
pub fn clear_window_state(&mut self, window_name: &WindowName) {
pub fn clear_window_state(&mut self, window_name: &str) {
self.windows.remove(window_name);
}
@ -91,7 +94,7 @@ impl EwwState {
/// Update the value of a variable, running all registered
/// [StateChangeHandler]s.
pub fn update_variable(&mut self, key: VarName, value: PrimVal) {
pub fn update_variable(&mut self, key: VarName, value: DynVal) {
self.variables_state.insert(key.clone(), value);
// run all of the handlers
@ -103,27 +106,21 @@ impl EwwState {
}
/// Look up a single variable in the eww state, returning an `Err` when the value is not found.
pub fn lookup(&self, var_name: &VarName) -> Result<&PrimVal> {
pub fn lookup(&self, var_name: &VarName) -> Result<&DynVal> {
self.variables_state.get(var_name).with_context(|| format!("Unknown variable '{}' referenced", var_name))
}
/// resolves a value if possible, using the current eww_state.
pub fn resolve_once<'a>(&'a self, value: &'a AttrVal) -> Result<PrimVal> {
value
.iter()
.map(|element| match element {
AttrValElement::Primitive(primitive) => Ok(primitive.clone()),
AttrValElement::Expr(expr) => expr.clone().eval(&self.variables_state),
})
.collect()
pub fn resolve_once<'a>(&'a self, value: &'a SimplExpr) -> Result<DynVal> {
Ok(value.clone().eval(&self.variables_state)?)
}
/// Resolve takes a function that applies a set of fully resolved attribute
/// values to it's gtk widget.
pub fn resolve<F: Fn(HashMap<AttrName, PrimVal>) -> Result<()> + 'static + Clone>(
pub fn resolve<F: Fn(HashMap<AttrName, DynVal>) -> Result<()> + 'static + Clone>(
&mut self,
window_name: &WindowName,
required_attributes: HashMap<AttrName, AttrVal>,
window_name: &str,
required_attributes: HashMap<AttrName, SimplExpr>,
set_value: F,
) {
let handler = StateChangeHandler { func: Box::new(set_value), unresolved_values: required_attributes };
@ -132,7 +129,7 @@ impl EwwState {
// only store the handler if at least one variable is being used
if handler.used_variables().next().is_some() {
self.windows.entry(window_name.clone()).or_insert_with(EwwWindowState::default).put_handler(handler);
self.windows.entry(window_name.to_string()).or_insert_with(EwwWindowState::default).put_handler(handler);
}
}
@ -140,7 +137,7 @@ impl EwwState {
self.windows.values().flat_map(|w| w.state_change_handlers.keys())
}
pub fn vars_referenced_in(&self, window_name: &WindowName) -> std::collections::HashSet<&VarName> {
pub fn vars_referenced_in(&self, window_name: &str) -> std::collections::HashSet<&VarName> {
self.windows.get(window_name).map(|window| window.state_change_handlers.keys().collect()).unwrap_or_default()
}
}

View file

@ -38,7 +38,7 @@ async fn handle_connection(mut stream: tokio::net::UnixStream, evt_send: Unbound
evt_send.send(command)?;
if let Some(mut response_recv) = maybe_response_recv {
log::info!("Waiting for response for IPC client");
log::debug!("Waiting for response for IPC client");
if let Ok(Some(response)) = tokio::time::timeout(Duration::from_millis(100), response_recv.recv()).await {
let response = bincode::serialize(&response)?;
let result = &stream_write.write_all(&response).await;

247
crates/eww/src/main.rs Normal file
View file

@ -0,0 +1,247 @@
#![feature(trace_macros)]
#![feature(drain_filter)]
#![feature(box_syntax)]
#![feature(box_patterns)]
#![feature(slice_concat_trait)]
#![feature(result_cloned)]
#![feature(try_blocks)]
#![feature(nll)]
extern crate gio;
extern crate gtk;
#[cfg(feature = "wayland")]
extern crate gtk_layer_shell as gtk_layer_shell;
use anyhow::*;
use daemon_response::DaemonResponseReceiver;
use opts::ActionWithServer;
use std::{
os::unix::net,
path::{Path, PathBuf},
time::Duration,
};
use crate::server::ForkResult;
pub mod app;
pub mod application_lifecycle;
pub mod client;
pub mod config;
mod daemon_response;
pub mod display_backend;
pub mod error;
mod error_handling_ctx;
pub mod eww_state;
pub mod geometry;
pub mod ipc_server;
pub mod opts;
pub mod script_var_handler;
pub mod server;
pub mod util;
pub mod widgets;
fn main() {
let eww_binary_name = std::env::args().next().unwrap();
let opts: opts::Opt = opts::Opt::from_env();
let log_level_filter = if opts.log_debug { log::LevelFilter::Debug } else { log::LevelFilter::Info };
if std::env::var("RUST_LOG").is_ok() {
pretty_env_logger::init_timed();
} else {
pretty_env_logger::formatted_timed_builder().filter(Some("eww"), log_level_filter).init();
}
let result: Result<()> = try {
let paths = opts
.config_path
.map(EwwPaths::from_config_dir)
.unwrap_or_else(EwwPaths::default)
.context("Failed to initialize eww paths")?;
let would_show_logs = match opts.action {
opts::Action::ClientOnly(action) => {
client::handle_client_only_action(&paths, action)?;
false
}
// a running daemon is necessary for this command
opts::Action::WithServer(action) if action.can_start_daemon() => {
if opts.restart {
let _ = handle_server_command(&paths, &ActionWithServer::KillServer, 1);
std::thread::sleep(std::time::Duration::from_millis(200));
}
// attempt to just send the command to a running daemon
if let Err(err) = handle_server_command(&paths, &action, 5) {
// connecting to the daemon failed. Thus, start the daemon here!
log::warn!("Failed to connect to daemon: {}", err);
log::info!("Initializing eww server. ({})", paths.get_ipc_socket_file().display());
let _ = std::fs::remove_file(paths.get_ipc_socket_file());
if !opts.show_logs {
println!("Run `{} logs` to see any errors while editing your configuration.", eww_binary_name);
}
let (command, response_recv) = action.into_daemon_command();
// start the daemon and give it the command
let fork_result = server::initialize_server(paths.clone(), Some(command))?;
let is_parent = fork_result == ForkResult::Parent;
if let (Some(recv), true) = (response_recv, is_parent) {
listen_for_daemon_response(recv);
}
is_parent
} else {
true
}
}
opts::Action::WithServer(ActionWithServer::KillServer) => {
handle_server_command(&paths, &ActionWithServer::KillServer, 1)?;
false
}
opts::Action::WithServer(action) => {
handle_server_command(&paths, &action, 5)?;
true
}
// make sure that there isn't already a Eww daemon running.
opts::Action::Daemon if check_server_running(paths.get_ipc_socket_file()) => {
eprintln!("Eww server already running.");
true
}
opts::Action::Daemon => {
log::info!("Initializing Eww server. ({})", paths.get_ipc_socket_file().display());
let _ = std::fs::remove_file(paths.get_ipc_socket_file());
if !opts.show_logs {
println!("Run `{} logs` to see any errors while editing your configuration.", eww_binary_name);
}
let fork_result = server::initialize_server(paths.clone(), None)?;
fork_result == ForkResult::Parent
}
};
if would_show_logs && opts.show_logs {
client::handle_client_only_action(&paths, opts::ActionClientOnly::Logs)?;
}
};
if let Err(e) = result {
error_handling_ctx::print_error(e);
std::process::exit(1);
}
}
fn listen_for_daemon_response(mut recv: DaemonResponseReceiver) {
let rt = tokio::runtime::Builder::new_current_thread().enable_time().build().expect("Failed to initialize tokio runtime");
rt.block_on(async {
if let Ok(Some(response)) = tokio::time::timeout(Duration::from_millis(100), recv.recv()).await {
println!("{}", response);
}
})
}
fn handle_server_command(paths: &EwwPaths, action: &ActionWithServer, connect_attempts: usize) -> Result<()> {
log::debug!("Trying to find server process at socket {}", paths.get_ipc_socket_file().display());
let mut stream = attempt_connect(&paths.get_ipc_socket_file(), connect_attempts).context("Failed to connect to daemon")?;
log::debug!("Connected to Eww server ({}).", &paths.get_ipc_socket_file().display());
let response = client::do_server_call(&mut stream, action).context("Error while forwarding command to server")?;
if let Some(response) = response {
println!("{}", response);
}
Ok(())
}
fn attempt_connect(socket_path: impl AsRef<Path>, attempts: usize) -> Option<net::UnixStream> {
for _ in 0..attempts {
if let Ok(mut con) = net::UnixStream::connect(&socket_path) {
if client::do_server_call(&mut con, &opts::ActionWithServer::Ping).is_ok() {
return net::UnixStream::connect(&socket_path).ok();
}
}
std::thread::sleep(Duration::from_millis(200));
}
None
}
/// Check if a eww server is currently running by trying to send a ping message to it.
fn check_server_running(socket_path: impl AsRef<Path>) -> bool {
let response = net::UnixStream::connect(socket_path)
.ok()
.and_then(|mut stream| client::do_server_call(&mut stream, &opts::ActionWithServer::Ping).ok());
response.is_some()
}
#[derive(Debug, Clone)]
pub struct EwwPaths {
log_file: PathBuf,
ipc_socket_file: PathBuf,
config_dir: PathBuf,
}
impl EwwPaths {
pub fn from_config_dir<P: AsRef<Path>>(config_dir: P) -> Result<Self> {
let config_dir = config_dir.as_ref();
if config_dir.is_file() {
bail!("Please provide the path to the config directory, not a file within it")
}
if !config_dir.exists() {
bail!("Configuration directory {} does not exist", config_dir.display());
}
let config_dir = config_dir.canonicalize()?;
let daemon_id = base64::encode(format!("{}", config_dir.display()));
Ok(EwwPaths {
config_dir,
log_file: std::env::var("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap()).join(".cache"))
.join(format!("eww_{}.log", daemon_id)),
ipc_socket_file: std::env::var("XDG_RUNTIME_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("/tmp"))
.join(format!("eww-server_{}", daemon_id)),
})
}
pub fn default() -> Result<Self> {
let config_dir = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap()).join(".config"))
.join("eww");
Self::from_config_dir(config_dir)
}
pub fn get_log_file(&self) -> &Path {
self.log_file.as_path()
}
pub fn get_ipc_socket_file(&self) -> &Path {
self.ipc_socket_file.as_path()
}
pub fn get_config_dir(&self) -> &Path {
self.config_dir.as_path()
}
pub fn get_yuck_path(&self) -> PathBuf {
self.config_dir.join("eww.yuck")
}
pub fn get_eww_scss_path(&self) -> PathBuf {
self.config_dir.join("eww.scss")
}
}
impl std::fmt::Display for EwwPaths {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"config-dir: {}, ipc-socket: {}, log-file: {}",
self.config_dir.display(),
self.ipc_socket_file.display(),
self.log_file.display()
)
}
}

View file

@ -1,17 +1,21 @@
use anyhow::*;
use eww_shared_util::VarName;
use serde::{Deserialize, Serialize};
use simplexpr::dynval::DynVal;
use structopt::StructOpt;
use yuck::{config::window_geometry::AnchorPoint, value::Coords};
use crate::{
app,
config::{AnchorPoint, WindowName},
value::{Coords, PrimVal, VarName},
daemon_response::{self, DaemonResponse, DaemonResponseSender},
};
/// Struct that gets generated from `RawOpt`.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Opt {
pub log_debug: bool,
pub show_logs: bool,
pub restart: bool,
pub config_path: Option<std::path::PathBuf>,
pub action: Action,
}
@ -22,10 +26,18 @@ struct RawOpt {
#[structopt(long = "debug", global = true)]
log_debug: bool,
/// override path to configuration directory (directory that contains eww.xml and eww.scss)
/// override path to configuration directory (directory that contains eww.yuck and eww.scss)
#[structopt(short, long, global = true)]
config: Option<std::path::PathBuf>,
/// Watch the log output after executing the command
#[structopt(long = "logs", global = true)]
show_logs: bool,
/// Restart the daemon completely before running the command
#[structopt(long = "restart", global = true)]
restart: bool,
#[structopt(subcommand)]
action: Action,
}
@ -61,18 +73,18 @@ pub enum ActionWithServer {
Update {
/// variable_name="new_value"-pairs that will be updated
#[structopt(parse(try_from_str = parse_var_update_arg))]
mappings: Vec<(VarName, PrimVal)>,
mappings: Vec<(VarName, DynVal)>,
},
/// open a window
#[structopt(name = "open", alias = "o")]
OpenWindow {
/// Name of the window you want to open.
window_name: WindowName,
window_name: String,
/// Monitor-index the window should open on
#[structopt(short, long)]
monitor: Option<i32>,
#[structopt(long)]
screen: Option<i32>,
/// The position of the window, where it should open.
#[structopt(short, long)]
@ -85,16 +97,20 @@ pub enum ActionWithServer {
/// Sidepoint of the window, formatted like "top right"
#[structopt(short, long)]
anchor: Option<AnchorPoint>,
/// If the window is already open, close it instead
#[structopt(long = "toggle")]
should_toggle: bool,
},
/// Open multiple windows at once.
/// NOTE: This will in the future be part of eww open, and will then be removed.
#[structopt(name = "open-many")]
OpenMany { windows: Vec<WindowName> },
OpenMany { windows: Vec<String> },
/// Close the window with the given name
#[structopt(name = "close", alias = "c")]
CloseWindow { window_name: WindowName },
CloseWindow { window_name: String },
/// Reload the configuration
#[structopt(name = "reload", alias = "r")]
@ -137,40 +153,48 @@ impl Opt {
impl From<RawOpt> for Opt {
fn from(other: RawOpt) -> Self {
let RawOpt { action, log_debug, config } = other;
Opt { action, log_debug, config_path: config }
let RawOpt { action, log_debug, show_logs, config, restart } = other;
Opt { action, log_debug, show_logs, config_path: config, restart }
}
}
fn parse_var_update_arg(s: &str) -> Result<(VarName, PrimVal)> {
fn parse_var_update_arg(s: &str) -> Result<(VarName, DynVal)> {
let (name, value) = s
.split_once('=')
.with_context(|| format!("arguments must be in the shape `variable_name=\"new_value\"`, but got: {}", s))?;
Ok((name.into(), PrimVal::from_string(value.to_owned())))
Ok((name.into(), DynVal::from_string(value.to_owned())))
}
impl ActionWithServer {
pub fn into_daemon_command(self) -> (app::DaemonCommand, Option<app::DaemonResponseReceiver>) {
pub fn can_start_daemon(&self) -> bool {
match self {
ActionWithServer::OpenWindow { .. } | ActionWithServer::OpenMany { .. } => true,
_ => false,
}
}
pub fn into_daemon_command(self) -> (app::DaemonCommand, Option<daemon_response::DaemonResponseReceiver>) {
let command = match self {
ActionWithServer::Update { mappings } => app::DaemonCommand::UpdateVars(mappings.into_iter().collect()),
ActionWithServer::Update { mappings } => app::DaemonCommand::UpdateVars(mappings),
ActionWithServer::KillServer => app::DaemonCommand::KillServer,
ActionWithServer::CloseAll => app::DaemonCommand::CloseAll,
ActionWithServer::Ping => {
let (send, recv) = tokio::sync::mpsc::unbounded_channel();
let _ = send.send(app::DaemonResponse::Success("pong".to_owned()));
let _ = send.send(DaemonResponse::Success("pong".to_owned()));
return (app::DaemonCommand::NoOp, Some(recv));
}
ActionWithServer::OpenMany { windows } => {
return with_response_channel(|sender| app::DaemonCommand::OpenMany { windows, sender });
}
ActionWithServer::OpenWindow { window_name, pos, size, monitor, anchor } => {
ActionWithServer::OpenWindow { window_name, pos, size, screen, anchor, should_toggle } => {
return with_response_channel(|sender| app::DaemonCommand::OpenWindow {
window_name,
pos,
size,
anchor,
monitor,
screen,
should_toggle,
sender,
})
}
@ -188,10 +212,10 @@ impl ActionWithServer {
}
}
fn with_response_channel<T, O, F>(f: F) -> (O, Option<tokio::sync::mpsc::UnboundedReceiver<T>>)
fn with_response_channel<O, F>(f: F) -> (O, Option<tokio::sync::mpsc::UnboundedReceiver<DaemonResponse>>)
where
F: FnOnce(tokio::sync::mpsc::UnboundedSender<T>) -> O,
F: FnOnce(DaemonResponseSender) -> O,
{
let (sender, recv) = tokio::sync::mpsc::unbounded_channel();
let (sender, recv) = daemon_response::create_pair();
(f(sender), Some(recv))
}

View file

@ -1,17 +1,20 @@
use std::collections::HashMap;
use crate::{
app, config,
value::{PrimVal, VarName},
app,
config::{create_script_var_failed_warn, script_var},
};
use anyhow::*;
use app::DaemonCommand;
use eww_shared_util::VarName;
use simplexpr::dynval::DynVal;
use tokio::{
io::{AsyncBufReadExt, BufReader},
sync::mpsc::UnboundedSender,
};
use tokio_util::sync::CancellationToken;
use yuck::config::script_var_definition::{ListenScriptVar, PollScriptVar, ScriptVarDefinition, VarSource};
/// Initialize the script var handler, and return a handle to that handler, which can be used to control
/// the script var execution.
@ -23,7 +26,7 @@ pub fn init(evt_send: UnboundedSender<DaemonCommand>) -> ScriptVarHandlerHandle
rt.block_on(async {
let _: Result<_> = try {
let mut handler = ScriptVarHandler {
tail_handler: TailVarHandler::new(evt_send.clone())?,
listen_handler: ListenVarHandler::new(evt_send.clone())?,
poll_handler: PollVarHandler::new(evt_send)?,
};
crate::loop_select_exiting! {
@ -53,7 +56,7 @@ pub struct ScriptVarHandlerHandle {
impl ScriptVarHandlerHandle {
/// Add a new script-var that should be executed.
pub fn add(&self, script_var: config::ScriptVar) {
pub fn add(&self, script_var: ScriptVarDefinition) {
crate::print_result_err!(
"while forwarding instruction to script-var handler",
self.msg_send.send(ScriptVarHandlerMsg::AddVar(script_var))
@ -80,29 +83,29 @@ impl ScriptVarHandlerHandle {
/// Message enum used by the ScriptVarHandlerHandle to communicate to the ScriptVarHandler
#[derive(Debug, Eq, PartialEq)]
enum ScriptVarHandlerMsg {
AddVar(config::ScriptVar),
AddVar(ScriptVarDefinition),
Stop(VarName),
StopAll,
}
/// Handler that manages running and updating [ScriptVar]s
/// Handler that manages running and updating [ScriptVarDefinition]s
struct ScriptVarHandler {
tail_handler: TailVarHandler,
listen_handler: ListenVarHandler,
poll_handler: PollVarHandler,
}
impl ScriptVarHandler {
async fn add(&mut self, script_var: config::ScriptVar) {
async fn add(&mut self, script_var: ScriptVarDefinition) {
match script_var {
config::ScriptVar::Poll(var) => self.poll_handler.start(var).await,
config::ScriptVar::Tail(var) => self.tail_handler.start(var).await,
ScriptVarDefinition::Poll(var) => self.poll_handler.start(var).await,
ScriptVarDefinition::Listen(var) => self.listen_handler.start(var).await,
};
}
/// Stop the handler that is responsible for a given variable.
fn stop_for_variable(&mut self, name: &VarName) -> Result<()> {
log::debug!("Stopping script var process for variable {}", name);
self.tail_handler.stop_for_variable(name);
self.listen_handler.stop_for_variable(name);
self.poll_handler.stop_for_variable(name);
Ok(())
}
@ -110,7 +113,7 @@ impl ScriptVarHandler {
/// stop all running scripts and schedules
fn stop_all(&mut self) {
log::debug!("Stopping script-var-handlers");
self.tail_handler.stop_all();
self.listen_handler.stop_all();
self.poll_handler.stop_all();
}
}
@ -126,24 +129,29 @@ impl PollVarHandler {
Ok(handler)
}
async fn start(&mut self, var: config::PollScriptVar) {
async fn start(&mut self, var: PollScriptVar) {
log::debug!("starting poll var {}", &var.name);
let cancellation_token = CancellationToken::new();
self.poll_handles.insert(var.name.clone(), cancellation_token.clone());
let evt_send = self.evt_send.clone();
tokio::spawn(async move {
let result: Result<_> = try {
evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), var.run_once()?)]))?;
evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), run_poll_once(&var)?)]))?;
};
crate::print_result_err!("while running script-var command", &result);
if let Err(err) = result {
crate::error_handling_ctx::print_error(err);
}
crate::loop_select_exiting! {
_ = cancellation_token.cancelled() => break,
_ = tokio::time::sleep(var.interval) => {
let result: Result<_> = try {
evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), var.run_once()?)]))?;
evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), run_poll_once(&var)?)]))?;
};
crate::print_result_err!("while running script-var command", &result);
if let Err(err) = result {
crate::error_handling_ctx::print_error(err);
}
}
}
});
@ -161,45 +169,58 @@ impl PollVarHandler {
}
}
fn run_poll_once(var: &PollScriptVar) -> Result<DynVal> {
match &var.command {
VarSource::Shell(span, command) => {
script_var::run_command(command).map_err(|e| anyhow!(create_script_var_failed_warn(*span, &var.name, &e.to_string())))
}
VarSource::Function(x) => x().map_err(|e| anyhow!(e)),
}
}
impl Drop for PollVarHandler {
fn drop(&mut self) {
self.stop_all();
}
}
struct TailVarHandler {
struct ListenVarHandler {
evt_send: UnboundedSender<DaemonCommand>,
tail_process_handles: HashMap<VarName, CancellationToken>,
listen_process_handles: HashMap<VarName, CancellationToken>,
}
impl TailVarHandler {
impl ListenVarHandler {
fn new(evt_send: UnboundedSender<DaemonCommand>) -> Result<Self> {
let handler = TailVarHandler { evt_send, tail_process_handles: HashMap::new() };
let handler = ListenVarHandler { evt_send, listen_process_handles: HashMap::new() };
Ok(handler)
}
async fn start(&mut self, var: config::TailScriptVar) {
log::debug!("starting poll var {}", &var.name);
async fn start(&mut self, var: ListenScriptVar) {
log::debug!("starting listen-var {}", &var.name);
let cancellation_token = CancellationToken::new();
self.tail_process_handles.insert(var.name.clone(), cancellation_token.clone());
self.listen_process_handles.insert(var.name.clone(), cancellation_token.clone());
let evt_send = self.evt_send.clone();
tokio::spawn(async move {
crate::try_logging_errors!(format!("Executing tail var command {}", &var.command) => {
crate::try_logging_errors!(format!("Executing listen var-command {}", &var.command) => {
let mut handle = tokio::process::Command::new("sh")
.args(&["-c", &var.command])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.stderr(std::process::Stdio::piped())
.stdin(std::process::Stdio::null())
.spawn()?;
let mut stdout_lines = BufReader::new(handle.stdout.take().unwrap()).lines();
let mut stderr_lines = BufReader::new(handle.stderr.take().unwrap()).lines();
crate::loop_select_exiting! {
_ = handle.wait() => break,
_ = cancellation_token.cancelled() => break,
Ok(Some(line)) = stdout_lines.next_line() => {
let new_value = PrimVal::from_string(line.to_owned());
let new_value = DynVal::from_string(line.to_owned());
evt_send.send(DaemonCommand::UpdateVars(vec![(var.name.to_owned(), new_value)]))?;
}
Ok(Some(line)) = stderr_lines.next_line() => {
log::warn!("stderr of `{}`: {}", var.name, line);
}
else => break,
}
let _ = handle.kill().await;
@ -208,18 +229,18 @@ impl TailVarHandler {
}
fn stop_for_variable(&mut self, name: &VarName) {
if let Some(token) = self.tail_process_handles.remove(name) {
log::debug!("stopped tail var {}", name);
if let Some(token) = self.listen_process_handles.remove(name) {
log::debug!("stopped listen-var {}", name);
token.cancel();
}
}
fn stop_all(&mut self) {
self.tail_process_handles.drain().for_each(|(_, token)| token.cancel());
self.listen_process_handles.drain().for_each(|(_, token)| token.cancel());
}
}
impl Drop for TailVarHandler {
impl Drop for ListenVarHandler {
fn drop(&mut self) {
self.stop_all();
}

View file

@ -1,10 +1,42 @@
use crate::{app, config, eww_state::*, ipc_server, script_var_handler, util, EwwPaths};
use crate::{
app::{self, DaemonCommand},
config, daemon_response, error_handling_ctx,
eww_state::*,
ipc_server, script_var_handler, util, EwwPaths,
};
use anyhow::*;
use std::{collections::HashMap, os::unix::io::AsRawFd, path::Path};
use std::{
collections::{HashMap, HashSet},
os::unix::io::AsRawFd,
path::Path,
sync::{atomic::Ordering, Arc},
};
use tokio::sync::mpsc::*;
pub fn initialize_server(paths: EwwPaths) -> Result<()> {
do_detach(&paths.get_log_file())?;
pub fn initialize_server(paths: EwwPaths, action: Option<DaemonCommand>) -> Result<ForkResult> {
let (ui_send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel();
std::env::set_current_dir(&paths.get_config_dir())
.with_context(|| format!("Failed to change working directory to {}", paths.get_config_dir().display()))?;
log::info!("Loading paths: {}", &paths);
let read_config = config::read_from_file(&paths.get_yuck_path());
let eww_config = match read_config {
Ok(config) => config,
Err(err) => {
error_handling_ctx::print_error(err);
config::EwwConfig::default()
}
};
let fork_result = do_detach(&paths.get_log_file())?;
if fork_result == ForkResult::Parent {
return Ok(ForkResult::Parent);
}
println!(
r#"
@ -21,23 +53,17 @@ pub fn initialize_server(paths: EwwPaths) -> Result<()> {
std::process::exit(1);
}
});
let (ui_send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel();
std::env::set_current_dir(&paths.get_config_dir())
.with_context(|| format!("Failed to change working directory to {}", paths.get_config_dir().display()))?;
log::info!("Loading paths: {}", &paths);
let eww_config = config::EwwConfig::read_from_file(&paths.get_eww_xml_path())?;
gtk::init()?;
log::info!("Initializing script var handler");
log::debug!("Initializing script var handler");
let script_var_handler = script_var_handler::init(ui_send.clone());
let mut app = app::App {
eww_state: EwwState::from_default_vars(eww_config.generate_initial_state()?),
eww_config,
open_windows: HashMap::new(),
failed_windows: HashSet::new(),
css_provider: gtk::CssProvider::new(),
script_var_handler,
app_evt_send: ui_send.clone(),
@ -56,6 +82,10 @@ pub fn initialize_server(paths: EwwPaths) -> Result<()> {
init_async_part(app.paths.clone(), ui_send);
glib::MainContext::default().spawn_local(async move {
// if an action was given to the daemon initially, execute it first.
if let Some(action) = action {
app.handle_command(action);
}
while let Some(event) = ui_recv.recv().await {
app.handle_command(event);
}
@ -64,7 +94,7 @@ pub fn initialize_server(paths: EwwPaths) -> Result<()> {
gtk::main();
log::info!("main application thread finished");
Ok(())
Ok(ForkResult::Child)
}
fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>) {
@ -87,7 +117,7 @@ fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>
tokio::spawn(async move {
// Wait for application exit event
let _ = crate::application_lifecycle::recv_exit().await;
log::info!("Forward task received exit event");
log::debug!("Forward task received exit event");
// Then forward that to the application
let _ = ui_send.send(app::DaemonCommand::KillServer);
})
@ -104,34 +134,43 @@ fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>
/// Watch configuration files for changes, sending reload events to the eww app when the files change.
async fn run_filewatch<P: AsRef<Path>>(config_dir: P, evt_send: UnboundedSender<app::DaemonCommand>) -> Result<()> {
use notify::Watcher;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let mut watcher: notify::RecommendedWatcher =
notify::Watcher::new_immediate(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
if let Err(err) = tx.send(event.paths) {
let mut watcher: RecommendedWatcher = Watcher::new(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
let relevant_files_changed = event.paths.iter().any(|path| {
let ext = path.extension().unwrap_or_default();
ext == "yuck" || ext == "scss"
});
if !relevant_files_changed {
if let Err(err) = tx.send(()) {
log::warn!("Error forwarding file update event: {:?}", err);
}
}
Err(e) => log::error!("Encountered Error While Watching Files: {}", e),
})?;
watcher.watch(&config_dir, notify::RecursiveMode::Recursive)?;
}
Err(e) => log::error!("Encountered Error While Watching Files: {}", e),
})?;
watcher.watch(&config_dir.as_ref(), RecursiveMode::Recursive)?;
// make sure to not trigger reloads too much by only accepting one reload every 500ms.
let debounce_done = Arc::new(std::sync::atomic::AtomicBool::new(true));
crate::loop_select_exiting! {
Some(paths) = rx.recv() => {
for path in paths {
let extension = path.extension().unwrap_or_default();
if extension != "xml" && extension != "scss" {
continue;
}
Some(()) = rx.recv() => {
let debounce_done = debounce_done.clone();
if debounce_done.swap(false, Ordering::SeqCst) {
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
debounce_done.store(true, Ordering::SeqCst);
});
let (daemon_resp_sender, mut daemon_resp_response) = tokio::sync::mpsc::unbounded_channel();
let (daemon_resp_sender, mut daemon_resp_response) = daemon_response::create_pair();
evt_send.send(app::DaemonCommand::ReloadConfigAndCss(daemon_resp_sender))?;
tokio::spawn(async move {
match daemon_resp_response.recv().await {
Some(app::DaemonResponse::Success(_)) => log::info!("Reloaded config successfully"),
Some(app::DaemonResponse::Failure(e)) => log::error!("Failed to reload config: {}", e),
Some(daemon_response::DaemonResponse::Success(_)) => log::info!("Reloaded config successfully"),
Some(daemon_response::DaemonResponse::Failure(e)) => eprintln!("{}", e),
None => log::error!("No response to reload configuration-reload request"),
}
});
@ -142,14 +181,26 @@ async fn run_filewatch<P: AsRef<Path>>(config_dir: P, evt_send: UnboundedSender<
return Ok(());
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ForkResult {
Parent,
Child,
}
/// detach the process from the terminal, also redirecting stdout and stderr to LOG_FILE
fn do_detach(log_file_path: impl AsRef<Path>) -> Result<()> {
fn do_detach(log_file_path: impl AsRef<Path>) -> Result<ForkResult> {
// detach from terminal
match unsafe { nix::unistd::fork()? } {
nix::unistd::ForkResult::Parent { .. } => {
std::process::exit(0);
nix::unistd::ForkResult::Child => {
nix::unistd::setsid()?;
match unsafe { nix::unistd::fork()? } {
nix::unistd::ForkResult::Parent { .. } => std::process::exit(0),
nix::unistd::ForkResult::Child => {}
}
}
nix::unistd::ForkResult::Parent { .. } => {
return Ok(ForkResult::Parent);
}
nix::unistd::ForkResult::Child => {}
}
let file = std::fs::OpenOptions::new()
@ -166,5 +217,5 @@ fn do_detach(log_file_path: impl AsRef<Path>) -> Result<()> {
nix::unistd::dup2(fd, std::io::stderr().as_raw_fd())?;
}
Ok(())
Ok(ForkResult::Child)
}

View file

@ -3,23 +3,6 @@ use extend::ext;
use itertools::Itertools;
use std::path::Path;
#[macro_export]
macro_rules! impl_try_from {
($typ:ty {
$(
for $for:ty => |$arg:ident| $code:expr
);*;
}) => {
$(impl TryFrom<$typ> for $for {
type Error = anyhow::Error;
fn try_from($arg: $typ) -> Result<Self> {
$code
}
})*
};
}
#[macro_export]
macro_rules! try_logging_errors {
($context:expr => $code:block) => {{
@ -50,6 +33,14 @@ macro_rules! loop_select {
}
}
#[macro_export]
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
RE.get_or_init(|| regex::Regex::new($re).unwrap())
}};
}
/// Parse a string with a concrete set of options into some data-structure,
/// and return a nicely formatted error message on invalid values. I.e.:
/// ```rs
@ -62,8 +53,8 @@ macro_rules! loop_select {
#[macro_export]
macro_rules! enum_parse {
($name:literal, $input:expr, $($($s:literal)|* => $val:expr),* $(,)?) => {
let input = $input;
match input {
let input = $input.to_lowercase();
match input.as_str() {
$( $( $s )|* => Ok($val) ),*,
_ => Err(anyhow!(concat!("Couldn't parse ", $name, ": '{}'. Possible values are ", $($($s),*),*), input))
}
@ -139,21 +130,6 @@ impl<T: AsRef<str>> T {
}
}
pub fn parse_duration(s: &str) -> Result<std::time::Duration> {
use std::time::Duration;
if s.ends_with("ms") {
Ok(Duration::from_millis(s.trim_end_matches("ms").parse()?))
} else if s.ends_with('s') {
Ok(Duration::from_secs(s.trim_end_matches('s').parse()?))
} else if s.ends_with('m') {
Ok(Duration::from_secs(s.trim_end_matches('m').parse::<u64>()? * 60))
} else if s.ends_with('h') {
Ok(Duration::from_secs(s.trim_end_matches('h').parse::<u64>()? * 60 * 60))
} else {
Err(anyhow!("unrecognized time format: {}", s))
}
}
pub trait IterAverage {
fn avg(self) -> f32;
}
@ -174,10 +150,7 @@ impl<I: Iterator<Item = f32>> IterAverage for I {
/// by the actual env-variables. If the env-var isn't found, will replace the
/// reference with an empty string.
pub fn replace_env_var_references(input: String) -> String {
lazy_static::lazy_static! {
static ref ENV_VAR_PATTERN: regex::Regex = regex::Regex::new(r"\$\{([^\s]*)\}").unwrap();
}
ENV_VAR_PATTERN
regex!(r"\$\{([^\s]*)\}")
.replace_all(&input, |var_name: &regex::Captures| std::env::var(var_name.get(1).unwrap().as_str()).unwrap_or_default())
.into_owned()
}

View file

@ -1,12 +1,10 @@
use crate::{
config::{element::WidgetDefinition, window_definition::WindowName},
eww_state::*,
value::AttrName,
};
use crate::eww_state::*;
use anyhow::*;
use eww_shared_util::AttrName;
use gtk::prelude::*;
use itertools::Itertools;
use std::collections::HashMap;
use yuck::config::widget_definition::WidgetDefinition;
use std::process::Command;
use widget_definitions::*;
@ -18,7 +16,7 @@ const CMD_STRING_PLACEHODLER: &str = "{}";
/// Run a command that was provided as an attribute. This command may use a
/// placeholder ('{}') which will be replaced by the value provided as [`arg`]
pub(self) fn run_command<T: 'static + std::fmt::Display + Send + Sync>(cmd: &str, arg: T) {
pub(self) fn run_command<T: 'static + std::fmt::Display + Send + Sync>(timeout: std::time::Duration, cmd: &str, arg: T) {
use wait_timeout::ChildExt;
let cmd = cmd.to_string();
std::thread::spawn(move || {
@ -26,7 +24,7 @@ pub(self) fn run_command<T: 'static + std::fmt::Display + Send + Sync>(cmd: &str
log::debug!("Running command from widget: {}", cmd);
let child = Command::new("/bin/sh").arg("-c").arg(&cmd).spawn();
match child {
Ok(mut child) => match child.wait_timeout(std::time::Duration::from_millis(200)) {
Ok(mut child) => match child.wait_timeout(timeout) {
// child timed out
Ok(None) => {
log::error!("WARNING: command {} timed out", &cmd);
@ -45,7 +43,7 @@ struct BuilderArgs<'a, 'b, 'c, 'd, 'e> {
eww_state: &'a mut EwwState,
widget: &'b widget_node::Generic,
unhandled_attrs: Vec<&'c AttrName>,
window_name: &'d WindowName,
window_name: &'d str,
widget_definitions: &'e HashMap<String, WidgetDefinition>,
}
@ -60,40 +58,31 @@ struct BuilderArgs<'a, 'b, 'c, 'd, 'e> {
/// widget name.
fn build_builtin_gtk_widget(
eww_state: &mut EwwState,
window_name: &WindowName,
window_name: &str,
widget_definitions: &HashMap<String, WidgetDefinition>,
widget: &widget_node::Generic,
) -> Result<Option<gtk::Widget>> {
let mut bargs =
BuilderArgs { eww_state, widget, window_name, unhandled_attrs: widget.attrs.keys().collect(), widget_definitions };
let gtk_widget = match widget_to_gtk_widget(&mut bargs) {
Ok(Some(gtk_widget)) => gtk_widget,
result => {
return result.with_context(|| {
format!(
"{}Error building widget {}",
bargs.widget.text_pos.map(|x| format!("{} |", x)).unwrap_or_default(),
bargs.widget.name,
)
})
}
};
let gtk_widget = widget_to_gtk_widget(&mut bargs)?;
// run resolve functions for superclasses such as range, orientable, and widget
if let Some(gtk_widget) = gtk_widget.dynamic_cast_ref::<gtk::Container>() {
resolve_container_attrs(&mut bargs, gtk_widget);
for child in &widget.children {
let child_widget = child.render(bargs.eww_state, window_name, widget_definitions).with_context(|| {
format!(
"{}error while building child '{:#?}' of '{}'",
widget.text_pos.map(|x| format!("{} |", x)).unwrap_or_default(),
&child,
&gtk_widget.get_widget_name()
)
})?;
gtk_widget.add(&child_widget);
child_widget.show();
if gtk_widget.get_children().is_empty() {
for child in &widget.children {
let child_widget = child.render(bargs.eww_state, window_name, widget_definitions).with_context(|| {
format!(
"{}error while building child '{:#?}' of '{}'",
format!("{} | ", widget.span),
&child,
&gtk_widget.get_widget_name()
)
})?;
gtk_widget.add(&child_widget);
child_widget.show();
}
}
}
@ -108,7 +97,7 @@ fn build_builtin_gtk_widget(
if !bargs.unhandled_attrs.is_empty() {
log::error!(
"{}: Unknown attribute used in {}: {}",
widget.text_pos.map(|x| format!("{} | ", x)).unwrap_or_default(),
format!("{} | ", widget.span),
widget.name,
bargs.unhandled_attrs.iter().map(|x| x.to_string()).join(", ")
)
@ -132,7 +121,7 @@ macro_rules! resolve_block {
let attr_map: Result<_> = try {
::maplit::hashmap! {
$(
crate::value::AttrName(::std::stringify!($attr_name).to_owned()) =>
eww_shared_util::AttrName(::std::stringify!($attr_name).to_owned()) =>
resolve_block!(@get_value $args, &::std::stringify!($attr_name).replace('_', "-"), $(= $default)?)
),*
}
@ -154,7 +143,7 @@ macro_rules! resolve_block {
};
(@get_value $args:ident, $name:expr, = $default:expr) => {
$args.widget.get_attr($name).cloned().unwrap_or(AttrVal::from_primitive($default))
$args.widget.get_attr($name).cloned().unwrap_or(simplexpr::SimplExpr::synth_literal($default))
};
(@get_value $args:ident, $name:expr,) => {

View file

@ -1,25 +1,30 @@
#![allow(clippy::option_map_unit_fn)]
use super::{run_command, BuilderArgs};
use crate::{
config, enum_parse, eww_state, resolve_block,
util::{list_difference, parse_duration},
value::AttrVal,
widgets::widget_node,
enum_parse, error::DiagError, error_handling_ctx, eww_state, resolve_block, util::list_difference, widgets::widget_node,
};
use anyhow::*;
use gdk::WindowExt;
use glib;
use gtk::{self, prelude::*, ImageExt};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use itertools::Itertools;
use std::{cell::RefCell, collections::HashMap, rc::Rc, time::Duration};
use yuck::{
config::validate::ValidationError,
error::{AstError, AstResult, AstResultExt},
gen_diagnostic,
parser::from_ast::FromAst,
};
// TODO figure out how to
// TODO https://developer.gnome.org/gtk3/stable/GtkFixed.html
//// widget definitions
pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result<Option<gtk::Widget>> {
pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result<gtk::Widget> {
let gtk_widget = match bargs.widget.name.as_str() {
"box" => build_gtk_box(bargs)?.upcast(),
"centerbox" => build_center_box(bargs)?.upcast(),
"scale" => build_gtk_scale(bargs)?.upcast(),
"progress" => build_gtk_progress(bargs)?.upcast(),
"image" => build_gtk_image(bargs)?.upcast(),
@ -35,9 +40,11 @@ pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result<Option<gtk
"checkbox" => build_gtk_checkbox(bargs)?.upcast(),
"revealer" => build_gtk_revealer(bargs)?.upcast(),
"if-else" => build_if_else(bargs)?.upcast(),
_ => return Ok(None),
_ => {
Err(AstError::ValidationError(ValidationError::UnknownWidget(bargs.widget.name_span, bargs.widget.name.to_string())))?
}
};
Ok(Some(gtk_widget))
Ok(gtk_widget)
}
/// attributes that apply to all widgets
@ -46,7 +53,9 @@ pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result<Option<gtk
pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Widget) {
let css_provider = gtk::CssProvider::new();
if let Ok(visible) = bargs.widget.get_attr("visible").and_then(|v| bargs.eww_state.resolve_once(v)?.as_bool()) {
if let Ok(visible) =
bargs.widget.get_attr("visible").and_then(|v| bargs.eww_state.resolve_once(v)?.as_bool().map_err(|e| anyhow!(e)))
{
connect_first_map(gtk_widget, move |w| {
if visible {
w.show();
@ -79,6 +88,10 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Wi
prop(valign: as_string) { gtk_widget.set_valign(parse_align(&valign)?) },
// @prop halign - how to align this horizontally. possible values: $alignment
prop(halign: as_string) { gtk_widget.set_halign(parse_align(&halign)?) },
// @prop vexpand - should this container expand vertically. Default: false.
prop(vexpand: as_bool = false) { gtk_widget.set_vexpand(vexpand) },
// @prop hexpand - should this widget expand horizontally. Default: false.
prop(hexpand: as_bool = false) { gtk_widget.set_hexpand(hexpand) },
// @prop width - width of this element. note that this can not restrict the size if the contents stretch it
prop(width: as_f64) { gtk_widget.set_size_request(width as i32, gtk_widget.get_allocated_height()) },
// @prop height - height of this element. note that this can not restrict the size if the contents stretch it
@ -100,24 +113,26 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Wi
css_provider.load_from_data(format!("* {{ {} }}", style).as_bytes())?;
gtk_widget.get_style_context().add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION)
},
// @prop timeout - timeout of the command
// @prop onscroll - event to execute when the user scrolls with the mouse over the widget. The placeholder `{}` used in the command will be replaced with either `up` or `down`.
prop(onscroll: as_string) {
prop(timeout: as_duration = Duration::from_millis(200), onscroll: as_string) {
gtk_widget.add_events(gdk::EventMask::SCROLL_MASK);
gtk_widget.add_events(gdk::EventMask::SMOOTH_SCROLL_MASK);
let old_id = on_scroll_handler_id.replace(Some(
gtk_widget.connect_scroll_event(move |_, evt| {
run_command(&onscroll, if evt.get_delta().1 < 0f64 { "up" } else { "down" });
run_command(timeout, &onscroll, if evt.get_delta().1 < 0f64 { "up" } else { "down" });
gtk::Inhibit(false)
})
));
old_id.map(|id| gtk_widget.disconnect(id));
},
// @prop timeout - timeout of the command
// @prop onhover - event to execute when the user hovers over the widget
prop(onhover: as_string) {
prop(timeout: as_duration = Duration::from_millis(200),onhover: as_string) {
gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK);
let old_id = on_hover_handler_id.replace(Some(
gtk_widget.connect_enter_notify_event(move |_, evt| {
run_command(&onhover, format!("{} {}", evt.get_position().0, evt.get_position().1));
run_command(timeout, &onhover, format!("{} {}", evt.get_position().0, evt.get_position().1));
gtk::Inhibit(false)
})
));
@ -152,13 +167,8 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Wi
}
/// @widget !container
pub(super) fn resolve_container_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Container) {
resolve_block!(bargs, gtk_widget, {
// @prop vexpand - should this container expand vertically
prop(vexpand: as_bool = false) { gtk_widget.set_vexpand(vexpand) },
// @prop hexpand - should this container expand horizontally
prop(hexpand: as_bool = false) { gtk_widget.set_hexpand(hexpand) },
});
pub(super) fn resolve_container_attrs(_bargs: &mut BuilderArgs, _gtk_widget: &gtk::Container) {
// resolve_block!(bargs, gtk_widget, {});
}
/// @widget !range
@ -188,13 +198,14 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Ran
prop(min: as_f64) { gtk_widget.get_adjustment().set_lower(min)},
// @prop max - the maximum value
prop(max: as_f64) { gtk_widget.get_adjustment().set_upper(max)},
// @prop timeout - timeout of the command
// @prop onchange - command executed once the value is changes. The placeholder `{}`, used in the command will be replaced by the new value.
prop(onchange: as_string) {
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
gtk_widget.set_sensitive(true);
gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK);
let old_id = on_change_handler_id.replace(Some(
gtk_widget.connect_value_changed(move |gtk_widget| {
run_command(&onchange, gtk_widget.get_value());
run_command(timeout, &onchange, gtk_widget.get_value());
})
));
old_id.map(|id| gtk_widget.disconnect(id));
@ -249,11 +260,12 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result<gtk::ComboBoxText
gtk_widget.append_text(&i);
}
},
// @prop timeout - timeout of the command
// @prop onchange - runs the code when a item was selected, replacing {} with the item as a string
prop(onchange: as_string) {
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
let old_id = on_change_handler_id.replace(Some(
gtk_widget.connect_changed(move |gtk_widget| {
run_command(&onchange, gtk_widget.get_active_text().unwrap_or_else(|| "".into()));
run_command(timeout, &onchange, gtk_widget.get_active_text().unwrap_or_else(|| "".into()));
})
));
old_id.map(|id| gtk_widget.disconnect(id));
@ -284,7 +296,7 @@ fn build_gtk_revealer(bargs: &mut BuilderArgs) -> Result<gtk::Revealer> {
// @prop reveal - sets if the child is revealed or not
prop(reveal: as_bool) { gtk_widget.set_reveal_child(reveal); },
// @prop duration - the duration of the reveal transition
prop(duration: as_string = "500ms") { gtk_widget.set_transition_duration(parse_duration(&duration)?.as_millis() as u32); },
prop(duration: as_duration = Duration::from_millis(500)) { gtk_widget.set_transition_duration(duration.as_millis() as u32); },
});
Ok(gtk_widget)
}
@ -295,16 +307,13 @@ fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result<gtk::CheckButton> {
let gtk_widget = gtk::CheckButton::new();
let on_change_handler_id: Rc<RefCell<Option<glib::SignalHandlerId>>> = Rc::new(RefCell::new(None));
resolve_block!(bargs, gtk_widget, {
// @prop onchecked - action (command) to be executed when checked by the user
// @prop onunchecked - similar to onchecked but when the widget is unchecked
prop(onchecked: as_string = "", onunchecked: as_string = "") {
// @prop timeout - timeout of the command
// @prop onchecked - action (command) to be executed when checked by the user
// @prop onunchecked - similar to onchecked but when the widget is unchecked
prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_string = "", onunchecked: as_string = "") {
let old_id = on_change_handler_id.replace(Some(
gtk_widget.connect_toggled(move |gtk_widget| {
if gtk_widget.get_active() {
run_command(&onchecked, "");
} else {
run_command(&onunchecked, "");
}
run_command(timeout, if gtk_widget.get_active() { &onchecked } else { &onunchecked }, "");
})
));
old_id.map(|id| gtk_widget.disconnect(id));
@ -324,10 +333,11 @@ fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result<gtk::ColorButton> {
prop(use_alpha: as_bool) {gtk_widget.set_use_alpha(use_alpha);},
// @prop onchange - runs the code when the color was selected
prop(onchange: as_string) {
// @prop timeout - timeout of the command
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
let old_id = on_change_handler_id.replace(Some(
gtk_widget.connect_color_set(move |gtk_widget| {
run_command(&onchange, gtk_widget.get_rgba());
run_command(timeout, &onchange, gtk_widget.get_rgba());
})
));
old_id.map(|id| gtk_widget.disconnect(id));
@ -347,10 +357,11 @@ fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result<gtk::ColorChooserW
prop(use_alpha: as_bool) {gtk_widget.set_use_alpha(use_alpha);},
// @prop onchange - runs the code when the color was selected
prop(onchange: as_string) {
// @prop timeout - timeout of the command
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
let old_id = on_change_handler_id.replace(Some(
gtk_widget.connect_color_activated(move |_a, color| {
run_command(&onchange, *color);
run_command(timeout, &onchange, *color);
})
));
old_id.map(|id| gtk_widget.disconnect(id));
@ -403,10 +414,11 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result<gtk::Entry> {
},
// @prop onchange - Command to run when the text changes. The placeholder `{}` will be replaced by the value
prop(onchange: as_string) {
// @prop timeout - timeout of the command
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
let old_id = on_change_handler_id.replace(Some(
gtk_widget.connect_changed(move |gtk_widget| {
run_command(&onchange, gtk_widget.get_text().to_string());
run_command(timeout, &onchange, gtk_widget.get_text().to_string());
})
));
old_id.map(|id| gtk_widget.disconnect(id));
@ -425,14 +437,20 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> {
// @prop onclick - a command that get's run when the button is clicked
// @prop onmiddleclick - a command that get's run when the button is middleclicked
// @prop onrightclick - a command that get's run when the button is rightclicked
prop(onclick: as_string = "", onmiddleclick: as_string = "", onrightclick: as_string = "") {
// @prop timeout - timeout of the command
prop(
timeout: as_duration = Duration::from_millis(200),
onclick: as_string = "",
onmiddleclick: as_string = "",
onrightclick: as_string = ""
) {
gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK);
let old_id = on_click_handler_id.replace(Some(
gtk_widget.connect_button_press_event(move |_, evt| {
match evt.get_button() {
1 => run_command(&onclick, ""),
2 => run_command(&onmiddleclick, ""),
3 => run_command(&onrightclick, ""),
1 => run_command(timeout, &onclick, ""),
2 => run_command(timeout, &onmiddleclick, ""),
3 => run_command(timeout, &onrightclick, ""),
_ => {},
}
gtk::Inhibit(false)
@ -481,6 +499,39 @@ fn build_gtk_box(bargs: &mut BuilderArgs) -> Result<gtk::Box> {
Ok(gtk_widget)
}
/// @widget centerbox extends container
/// @desc a box that must contain exactly three children, which will be layed out at the start, center and end of the container.
fn build_center_box(bargs: &mut BuilderArgs) -> Result<gtk::Box> {
let gtk_widget = gtk::Box::new(gtk::Orientation::Horizontal, 0);
resolve_block!(bargs, gtk_widget, {
// @prop orientation - orientation of the centerbox. possible values: $orientation
prop(orientation: as_string) { gtk_widget.set_orientation(parse_orientation(&orientation)?) },
});
if bargs.widget.children.len() < 3 {
Err(DiagError::new(gen_diagnostic!("centerbox must contain exactly 3 elements", bargs.widget.span)))?
} else if bargs.widget.children.len() > 3 {
let (_, additional_children) = bargs.widget.children.split_at(3);
// we know that there is more than three children, so unwrapping on first and left here is fine.
let first_span = additional_children.first().unwrap().span();
let last_span = additional_children.last().unwrap().span();
Err(DiagError::new(gen_diagnostic!("centerbox must contain exactly 3 elements, but got more", first_span.to(last_span))))?
}
let mut children =
bargs.widget.children.iter().map(|child| child.render(bargs.eww_state, bargs.window_name, bargs.widget_definitions));
// we know that we have exactly three children here, so we can unwrap here.
let (first, center, end) = children.next_tuple().unwrap();
let (first, center, end) = (first?, center?, end?);
gtk_widget.pack_start(&first, true, true, 0);
gtk_widget.set_center_widget(Some(&center));
gtk_widget.pack_end(&end, true, true, 0);
first.show();
center.show();
end.show();
Ok(gtk_widget)
}
/// @widget label
/// @desc A text widget giving you more control over how the text is displayed
fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
@ -492,43 +543,59 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
prop(text: as_string, limit_width: as_i32 = i32::MAX) {
let text = text.chars().take(limit_width as usize).collect::<String>();
let text = unescape::unescape(&text).context(format!("Failed to unescape label text {}", &text))?;
let text = unindent::unindent(&text);
gtk_widget.set_text(&text);
},
// @prop markup - Pango markup to display
prop(markup: as_string) {
gtk_widget.set_markup(&markup);
},
prop(markup: as_string) { gtk_widget.set_markup(&markup); },
// @prop wrap - Wrap the text. This mainly makes sense if you set the width of this widget.
prop(wrap: as_bool) {
gtk_widget.set_line_wrap(wrap)
},
prop(wrap: as_bool) { gtk_widget.set_line_wrap(wrap) },
// @prop angle - the angle of rotation for the label (between 0 - 360)
prop(angle: as_f64 = 0) {
gtk_widget.set_angle(angle)
}
prop(angle: as_f64 = 0) { gtk_widget.set_angle(angle) }
});
Ok(gtk_widget)
}
/// @widget literal
/// @desc A widget that allows you to render arbitrary XML.
/// @desc A widget that allows you to render arbitrary yuck.
fn build_gtk_literal(bargs: &mut BuilderArgs) -> Result<gtk::Box> {
let gtk_widget = gtk::Box::new(gtk::Orientation::Vertical, 0);
gtk_widget.set_widget_name("literal");
// TODO these clones here are dumdum
let window_name = bargs.window_name.clone();
let window_name = bargs.window_name.to_string();
let widget_definitions = bargs.widget_definitions.clone();
let literal_use_span = bargs.widget.span;
// the file id the literal-content has been stored under, for error reporting.
let literal_file_id: Rc<RefCell<Option<usize>>> = Rc::new(RefCell::new(None));
resolve_block!(bargs, gtk_widget, {
// @prop content - inline Eww XML that will be rendered as a widget.
// @prop content - inline yuck that will be rendered as a widget.
prop(content: as_string) {
gtk_widget.get_children().iter().for_each(|w| gtk_widget.remove(w));
if !content.is_empty() {
let document = roxmltree::Document::parse(&content).map_err(|e| anyhow!("Failed to parse eww xml literal: {:?}", e))?;
let content_widget_use = config::element::WidgetUse::from_xml_node(document.root_element().into())?;
let widget_node_result: AstResult<_> = try {
let ast = {
let mut yuck_files = error_handling_ctx::YUCK_FILES.write().unwrap();
let (span, asts) = yuck_files.load_str("<literal-content>".to_string(), content)?;
if let Some(file_id) = literal_file_id.replace(Some(span.2)) {
yuck_files.unload(file_id);
}
yuck::parser::require_single_toplevel(span, asts)?
};
let widget_node = &*widget_node::generate_generic_widget_node(&widget_definitions, &HashMap::new(), content_widget_use)?;
let child_widget = widget_node.render(&mut eww_state::EwwState::default(), &window_name, &widget_definitions)?;
let content_widget_use = yuck::config::widget_use::WidgetUse::from_ast(ast)?;
widget_node::generate_generic_widget_node(&widget_definitions, &HashMap::new(), content_widget_use)?
};
let widget_node = widget_node_result.context_label(literal_use_span, "Error in the literal used here")?;
let child_widget = widget_node.render(&mut eww_state::EwwState::default(), &window_name, &widget_definitions)
.map_err(|e| AstError::ErrorContext {
label_span: literal_use_span,
context: "Error in the literal used here".to_string(),
main_err: Box::new(error_handling_ctx::anyhow_err_to_diagnostic(&e).unwrap_or_else(|| gen_diagnostic!(e)))
})?;
gtk_widget.add(&child_widget);
child_widget.show();
}
@ -558,10 +625,12 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result<gtk::Calendar> {
// @prop show-week-numbers - show week numbers
prop(show_week_numbers: as_bool) { gtk_widget.set_property_show_week_numbers(show_week_numbers) },
// @prop onclick - command to run when the user selects a date. The `{}` placeholder will be replaced by the selected date.
prop(onclick: as_string) {
// @prop timeout - timeout of the command
prop(timeout: as_duration = Duration::from_millis(200), onclick: as_string) {
let old_id = on_click_handler_id.replace(Some(
gtk_widget.connect_day_selected(move |w| {
run_command(
timeout,
&onclick,
format!("{}.{}.{}", w.get_property_day(), w.get_property_month(), w.get_property_year())
)

View file

@ -0,0 +1,147 @@
use crate::eww_state::EwwState;
use anyhow::*;
use dyn_clone;
use eww_shared_util::{AttrName, Span, Spanned, VarName};
use simplexpr::SimplExpr;
use std::collections::HashMap;
use yuck::{
config::{validate::ValidationError, widget_definition::WidgetDefinition, widget_use::WidgetUse},
error::{AstError, AstResult},
};
pub trait WidgetNode: Spanned + std::fmt::Debug + dyn_clone::DynClone + Send + Sync {
fn get_name(&self) -> &str;
/// Generate a [gtk::Widget] from a [element::WidgetUse].
///
/// Also registers all the necessary state-change handlers in the eww_state.
///
/// This may return `Err` in case there was an actual error while parsing
/// or when the widget_use did not match any widget name
fn render(
&self,
eww_state: &mut EwwState,
window_name: &str,
widget_definitions: &HashMap<String, WidgetDefinition>,
) -> Result<gtk::Widget>;
}
dyn_clone::clone_trait_object!(WidgetNode);
#[derive(Debug, Clone)]
pub struct UserDefined {
name: String,
span: Span,
content: Box<dyn WidgetNode>,
}
impl WidgetNode for UserDefined {
fn get_name(&self) -> &str {
&self.name
}
fn render(
&self,
eww_state: &mut EwwState,
window_name: &str,
widget_definitions: &HashMap<String, WidgetDefinition>,
) -> Result<gtk::Widget> {
self.content.render(eww_state, window_name, widget_definitions)
}
}
impl Spanned for UserDefined {
fn span(&self) -> Span {
self.span
}
}
#[derive(Debug, Clone)]
pub struct Generic {
pub name: String,
pub name_span: Span,
pub span: Span,
pub children: Vec<Box<dyn WidgetNode>>,
pub attrs: HashMap<AttrName, SimplExpr>,
}
impl Generic {
pub fn get_attr(&self, key: &str) -> Result<&SimplExpr> {
Ok(self.attrs.get(key).ok_or_else(|| {
AstError::ValidationError(ValidationError::MissingAttr {
widget_name: self.name.to_string(),
arg_name: AttrName(key.to_string()),
use_span: self.span,
// TODO set this when available
arg_list_span: None,
})
})?)
}
/// returns all the variables that are referenced in this widget
pub fn referenced_vars(&self) -> impl Iterator<Item = &VarName> {
self.attrs.iter().flat_map(|(_, value)| value.var_refs()).map(|(_, value)| value)
}
}
impl WidgetNode for Generic {
fn get_name(&self) -> &str {
&self.name
}
fn render(
&self,
eww_state: &mut EwwState,
window_name: &str,
widget_definitions: &HashMap<String, WidgetDefinition>,
) -> Result<gtk::Widget> {
Ok(crate::widgets::build_builtin_gtk_widget(eww_state, window_name, widget_definitions, self)?.ok_or_else(|| {
AstError::ValidationError(ValidationError::UnknownWidget(self.name_span, self.get_name().to_string()))
})?)
}
}
impl Spanned for Generic {
fn span(&self) -> Span {
self.span
}
}
pub fn generate_generic_widget_node(
defs: &HashMap<String, WidgetDefinition>,
local_env: &HashMap<VarName, SimplExpr>,
w: WidgetUse,
) -> AstResult<Box<dyn WidgetNode>> {
if let Some(def) = defs.get(&w.name) {
if !w.children.is_empty() {
Err(AstError::TooManyNodes(w.children_span(), 0).note("User-defined widgets cannot be given children."))?
}
let new_local_env = w
.attrs
.attrs
.into_iter()
.map(|(name, value)| Ok((VarName(name.0), value.value.as_simplexpr()?.resolve_one_level(local_env))))
.collect::<AstResult<HashMap<VarName, _>>>()?;
let content = generate_generic_widget_node(defs, &new_local_env, def.widget.clone())?;
Ok(Box::new(UserDefined { name: w.name, span: w.span, content }))
} else {
Ok(Box::new(Generic {
name: w.name,
name_span: w.name_span,
span: w.span,
attrs: w
.attrs
.attrs
.into_iter()
.map(|(name, value)| Ok((name, value.value.as_simplexpr()?.resolve_one_level(local_env))))
.collect::<AstResult<HashMap<_, _>>>()?,
children: w
.children
.into_iter()
.map(|child| generate_generic_widget_node(defs, local_env, child))
.collect::<AstResult<Vec<_>>>()?,
}))
}
}

View file

@ -0,0 +1,8 @@
[package]
name = "eww_shared_util"
version = "0.1.0"
edition = "2018"
[dependencies]
serde = {version = "1.0", features = ["derive"]}
derive_more = "0.99"

View file

@ -0,0 +1,38 @@
pub mod span;
pub mod wrappers;
pub use span::*;
pub use wrappers::*;
#[macro_export]
macro_rules! snapshot_debug {
( $($name:ident => $test:expr),* $(,)?) => {
$(
#[test]
fn $name() { ::insta::assert_debug_snapshot!($test); }
)*
};
}
#[macro_export]
macro_rules! snapshot_string {
( $($name:ident => $test:expr),* $(,)?) => {
$(
#[test]
fn $name() { ::insta::assert_snapshot!($test); }
)*
};
}
#[macro_export]
macro_rules! snapshot_ron {
( $($name:ident => $test:expr),* $(,)?) => {
$(
#[test]
fn $name() {
::insta::with_settings!({sort_maps => true}, {
::insta::assert_ron_snapshot!($test);
});
}
)*
};
}

View file

@ -0,0 +1,64 @@
#[derive(Eq, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct Span(pub usize, pub usize, pub usize);
impl Span {
pub const DUMMY: Span = Span(usize::MAX, usize::MAX, usize::MAX);
pub fn point(loc: usize, file_id: usize) -> Self {
Span(loc, loc, file_id)
}
/// Get the span that includes this and the other span completely.
/// Will panic if the spans are from different file_ids.
pub fn to(mut self, other: Span) -> Self {
assert!(other.2 == self.2);
self.1 = other.1;
self
}
pub fn ending_at(mut self, end: usize) -> Self {
self.1 = end;
self
}
/// Turn this span into a span only highlighting the point it starts at, setting the length to 0.
pub fn point_span(mut self) -> Self {
self.1 = self.0;
self
}
/// Turn this span into a span only highlighting the point it ends at, setting the length to 0.
pub fn point_span_at_end(mut self) -> Self {
self.0 = self.1;
self
}
pub fn shifted(mut self, n: isize) -> Self {
self.0 = isize::max(0, self.0 as isize + n) as usize;
self.1 = isize::max(0, self.0 as isize + n) as usize;
self
}
pub fn is_dummy(&self) -> bool {
*self == Self::DUMMY
}
}
impl std::fmt::Display for Span {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_dummy() {
write!(f, "DUMMY")
} else {
write!(f, "{}..{}", self.0, self.1)
}
}
}
impl std::fmt::Debug for Span {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}
pub trait Spanned {
fn span(&self) -> Span;
}

View file

@ -1,17 +1,11 @@
use derive_more::*;
use serde::{Deserialize, Serialize};
pub mod attr_value;
pub mod coords;
pub mod primitive;
pub use attr_value::*;
pub use attr_value_expr::*;
pub use coords::*;
pub use primitive::*;
/// The name of a variable
#[repr(transparent)]
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom)]
#[derive(
Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom,
)]
#[debug(fmt = "VarName({})", .0)]
pub struct VarName(pub String);
@ -29,7 +23,9 @@ impl From<&str> for VarName {
/// The name of an attribute
#[repr(transparent)]
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom)]
#[derive(
Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom,
)]
#[debug(fmt="AttrName({})", .0)]
pub struct AttrName(pub String);

656
crates/simplexpr/Cargo.lock generated Normal file
View file

@ -0,0 +1,656 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "ascii-canvas"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6"
dependencies = [
"term",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "beef"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6736e2428df2ca2848d846c43e88745121a6654696e349ce0054a420815a7409"
[[package]]
name = "bit-set"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"terminal_size",
"winapi",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "diff"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dtoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "ena"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3"
dependencies = [
"log",
]
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "fixedbitset"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "getrandom"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "insta"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1b21a2971cea49ca4613c0e9fe8225ecaf5de64090fddc6002284726e9244"
dependencies = [
"console",
"lazy_static",
"serde",
"serde_json",
"serde_yaml",
"similar",
"uuid",
]
[[package]]
name = "itertools"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "lalrpop"
version = "0.19.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988"
dependencies = [
"ascii-canvas",
"atty",
"bit-set",
"diff",
"ena",
"itertools",
"lalrpop-util",
"petgraph",
"pico-args",
"regex",
"regex-syntax",
"string_cache",
"term",
"tiny-keccak",
"unicode-xid",
]
[[package]]
name = "lalrpop-util"
version = "0.19.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278"
dependencies = [
"regex",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
[[package]]
name = "linked-hash-map"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "logos"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427e2abca5be13136da9afdbf874e6b34ad9001dd70f2b103b083a85daa7b345"
dependencies = [
"logos-derive",
]
[[package]]
name = "logos-derive"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56a7d287fd2ac3f75b11f19a1c8a874a7d55744bd91f7a1b3e7cf87d4343c36d"
dependencies = [
"beef",
"fnv",
"proc-macro2",
"quote",
"regex-syntax",
"syn",
"utf8-ranges",
]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "memchr"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "new_debug_unreachable"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "petgraph"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "proc-macro2"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
]
[[package]]
name = "regex"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "rustversion"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "serde"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
dependencies = [
"dtoa",
"linked-hash-map",
"serde",
"yaml-rust",
]
[[package]]
name = "similar"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
[[package]]
name = "simplexpr"
version = "0.1.0"
dependencies = [
"insta",
"itertools",
"lalrpop",
"lalrpop-util",
"logos",
"maplit",
"regex",
"serde",
"serde_json",
"strum",
"thiserror",
]
[[package]]
name = "siphasher"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27"
[[package]]
name = "string_cache"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a"
dependencies = [
"lazy_static",
"new_debug_unreachable",
"phf_shared",
"precomputed-hash",
]
[[package]]
name = "strum"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "term"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
dependencies = [
"dirs-next",
"rustversion",
"winapi",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "thiserror"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "utf8-ranges"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]

View file

@ -0,0 +1,30 @@
[package]
name = "simplexpr"
version = "0.1.0"
edition = "2018"
authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"]
build = "build.rs"
[dependencies]
lalrpop-util = "0.19.5"
regex = "1"
itertools = "0.10"
thiserror = "1.0"
once_cell = "1.8.0"
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
levenshtein = "1.0"
strum = { version = "0.21", features = ["derive"] }
eww_shared_util = { path = "../eww_shared_util" }
[build-dependencies]
lalrpop = "0.19.5"
[dev-dependencies]
insta = "1.7"

View file

@ -0,0 +1,6 @@
# simplexpr
simplexpr is a parser and interpreter for a simple expression syntax that can be embedded into other applications or crates.
It is being developed to be used in [eww](https://github.com/elkowar/eww), but may also other uses.
For now, this is highly experimental, unstable, and ugly. You most definitely do not want to use this crate.

View file

@ -0,0 +1,4 @@
extern crate lalrpop;
fn main() {
lalrpop::Configuration::new().log_verbose().process_current_dir().unwrap();
}

View file

@ -0,0 +1 @@
nightly

View file

@ -0,0 +1,14 @@
unstable_features = true
fn_single_line = false
max_width = 130
reorder_impl_items = true
merge_imports = true
normalize_comments = true
use_field_init_shorthand = true
#wrap_comments = true
combine_control_expr = false
condense_wildcard_suffixes = true
format_code_in_doc_comments = true
format_macro_matchers = true
format_strings = true
use_small_heuristics = "Max"

103
crates/simplexpr/src/ast.rs Normal file
View file

@ -0,0 +1,103 @@
use crate::dynval::DynVal;
use eww_shared_util::{Span, Spanned};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use eww_shared_util::VarName;
#[rustfmt::skip]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, strum::EnumString, strum::Display)]
pub enum BinOp {
#[strum(serialize = "+") ] Plus,
#[strum(serialize = "-") ] Minus,
#[strum(serialize = "*") ] Times,
#[strum(serialize = "/") ] Div,
#[strum(serialize = "%") ] Mod,
#[strum(serialize = "==")] Equals,
#[strum(serialize = "!=")] NotEquals,
#[strum(serialize = "&&")] And,
#[strum(serialize = "||")] Or,
#[strum(serialize = ">") ] GT,
#[strum(serialize = "<") ] LT,
#[strum(serialize = "?:")] Elvis,
#[strum(serialize = "=~")] RegexMatch,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, strum::EnumString, strum::Display)]
pub enum UnaryOp {
#[strum(serialize = "!")]
Not,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimplExpr {
Literal(DynVal),
Concat(Span, Vec<SimplExpr>),
VarRef(Span, VarName),
BinOp(Span, Box<SimplExpr>, BinOp, Box<SimplExpr>),
UnaryOp(Span, UnaryOp, Box<SimplExpr>),
IfElse(Span, Box<SimplExpr>, Box<SimplExpr>, Box<SimplExpr>),
JsonAccess(Span, Box<SimplExpr>, Box<SimplExpr>),
FunctionCall(Span, String, Vec<SimplExpr>),
}
impl std::fmt::Display for SimplExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SimplExpr::Literal(x) => write!(f, "\"{}\"", x),
SimplExpr::Concat(_, elems) => {
let text = elems
.iter()
.map(|x| match x {
SimplExpr::Literal(lit) => lit.to_string(),
other => format!("${{{}}}", other),
})
.join("");
write!(f, "\"{}\"", text)
}
SimplExpr::VarRef(_, x) => write!(f, "{}", x),
SimplExpr::BinOp(_, l, op, r) => write!(f, "({} {} {})", l, op, r),
SimplExpr::UnaryOp(_, op, x) => write!(f, "{}{}", op, x),
SimplExpr::IfElse(_, a, b, c) => write!(f, "({} ? {} : {})", a, b, c),
SimplExpr::JsonAccess(_, value, index) => write!(f, "{}[{}]", value, index),
SimplExpr::FunctionCall(_, function_name, args) => {
write!(f, "{}({})", function_name, args.iter().join(", "))
}
}
}
}
impl SimplExpr {
pub fn literal(span: Span, s: String) -> Self {
Self::Literal(DynVal(s, span))
}
/// Construct a synthetic simplexpr from a literal string, without adding any relevant span information (uses [DUMMY_SPAN])
pub fn synth_string(s: String) -> Self {
Self::Literal(DynVal(s, Span::DUMMY))
}
/// Construct a synthetic simplexpr from a literal dynval, without adding any relevant span information (uses [DUMMY_SPAN])
pub fn synth_literal<T: Into<DynVal>>(s: T) -> Self {
Self::Literal(s.into())
}
}
impl Spanned for SimplExpr {
fn span(&self) -> Span {
match self {
SimplExpr::Literal(x) => x.span(),
SimplExpr::Concat(span, _) => *span,
SimplExpr::VarRef(span, _) => *span,
SimplExpr::BinOp(span, ..) => *span,
SimplExpr::UnaryOp(span, ..) => *span,
SimplExpr::IfElse(span, ..) => *span,
SimplExpr::JsonAccess(span, ..) => *span,
SimplExpr::FunctionCall(span, ..) => *span,
}
}
}
impl std::fmt::Debug for SimplExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}

View file

@ -0,0 +1,225 @@
use eww_shared_util::{Span, Spanned};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::{fmt, iter::FromIterator, str::FromStr};
pub type Result<T> = std::result::Result<T, ConversionError>;
#[derive(Debug, thiserror::Error)]
#[error("Failed to turn `{value}` into a value of type {target_type}")]
pub struct ConversionError {
pub value: DynVal,
pub target_type: &'static str,
pub source: Option<Box<dyn std::error::Error + Sync + Send + 'static>>,
}
impl ConversionError {
fn new(value: DynVal, target_type: &'static str, source: impl std::error::Error + 'static + Sync + Send) -> Self {
ConversionError { value, target_type, source: Some(Box::new(source)) }
}
}
impl Spanned for ConversionError {
fn span(&self) -> Span {
self.value.1
}
}
#[derive(Clone, Deserialize, Serialize, Eq)]
pub struct DynVal(pub String, pub Span);
impl From<String> for DynVal {
fn from(s: String) -> Self {
DynVal(s, Span::DUMMY)
}
}
impl fmt::Display for DynVal {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Debug for DynVal {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "\"{}\"", self.0)
}
}
/// Manually implement equality, to allow for values in different formats (i.e. "1" and "1.0") to still be considered as equal.
impl std::cmp::PartialEq<Self> for DynVal {
fn eq(&self, other: &Self) -> bool {
if let (Ok(a), Ok(b)) = (self.as_f64(), other.as_f64()) {
a == b
} else {
self.0 == other.0
}
}
}
impl FromIterator<DynVal> for DynVal {
fn from_iter<T: IntoIterator<Item = DynVal>>(iter: T) -> Self {
DynVal(iter.into_iter().join(""), Span::DUMMY)
}
}
impl std::str::FromStr for DynVal {
type Err = ConversionError;
/// parses the value, trying to turn it into a number and a boolean first,
/// before deciding that it is a string.
fn from_str(s: &str) -> Result<Self> {
Ok(DynVal::from_string(s.to_string()))
}
}
pub trait FromDynVal: Sized {
type Err;
fn from_dynval(x: &DynVal) -> std::result::Result<Self, Self::Err>;
}
impl<E, T: FromStr<Err = E>> FromDynVal for T {
type Err = E;
fn from_dynval(x: &DynVal) -> std::result::Result<Self, Self::Err> {
x.0.parse()
}
}
macro_rules! impl_dynval_from {
($($t:ty),*) => {
$(impl From<$t> for DynVal {
fn from(x: $t) -> Self { DynVal(x.to_string(), Span::DUMMY) }
})*
};
}
impl_dynval_from!(bool, i32, u32, f32, u8, f64, &str);
impl From<std::time::Duration> for DynVal {
fn from(d: std::time::Duration) -> Self {
DynVal(format!("{}ms", d.as_millis()), Span::DUMMY)
}
}
impl From<&serde_json::Value> for DynVal {
fn from(v: &serde_json::Value) -> Self {
DynVal(
v.as_str()
.map(|x| x.to_string())
.or_else(|| serde_json::to_string(v).ok())
.unwrap_or_else(|| "<invalid json value>".to_string()),
Span::DUMMY,
)
}
}
impl Spanned for DynVal {
fn span(&self) -> Span {
self.1
}
}
impl DynVal {
pub fn at(mut self, span: Span) -> Self {
self.1 = span;
self
}
pub fn from_string(s: String) -> Self {
DynVal(s, Span::DUMMY)
}
pub fn read_as<E, T: FromDynVal<Err = E>>(&self) -> std::result::Result<T, E> {
T::from_dynval(self)
}
pub fn into_inner(self) -> String {
self.0
}
/// This will never fail
pub fn as_string(&self) -> Result<String> {
Ok(self.0.to_owned())
}
pub fn as_f64(&self) -> Result<f64> {
self.0.parse().map_err(|e| ConversionError::new(self.clone(), "f64", e))
}
pub fn as_i32(&self) -> Result<i32> {
self.0.parse().map_err(|e| ConversionError::new(self.clone(), "i32", e))
}
pub fn as_bool(&self) -> Result<bool> {
self.0.parse().map_err(|e| ConversionError::new(self.clone(), "bool", e))
}
pub fn as_duration(&self) -> Result<std::time::Duration> {
use std::time::Duration;
let s = &self.0;
if s.ends_with("ms") {
Ok(Duration::from_millis(
s.trim_end_matches("ms").parse().map_err(|e| ConversionError::new(self.clone(), "integer", e))?,
))
} else if s.ends_with('s') {
Ok(Duration::from_secs(
s.trim_end_matches('s').parse().map_err(|e| ConversionError::new(self.clone(), "integer", e))?,
))
} else if s.ends_with('m') {
Ok(Duration::from_secs(
s.trim_end_matches('m').parse::<u64>().map_err(|e| ConversionError::new(self.clone(), "integer", e))? * 60,
))
} else if s.ends_with('h') {
Ok(Duration::from_secs(
s.trim_end_matches('h').parse::<u64>().map_err(|e| ConversionError::new(self.clone(), "integer", e))? * 60 * 60,
))
} else {
Err(ConversionError { value: self.clone(), target_type: "duration", source: None })
}
}
pub fn as_vec(&self) -> Result<Vec<String>> {
if self.0.is_empty() {
Ok(Vec::new())
} else {
match self.0.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
Some(content) => {
let mut items: Vec<String> = content.split(',').map(|x: &str| x.to_string()).collect();
let mut removed = 0;
for times_ran in 0..items.len() {
// escapes `,` if there's a `\` before em
if items[times_ran - removed].ends_with('\\') {
items[times_ran - removed].pop();
let it = items.remove((times_ran + 1) - removed);
items[times_ran - removed] += ",";
items[times_ran - removed] += &it;
removed += 1;
}
}
Ok(items)
}
None => Err(ConversionError { value: self.clone(), target_type: "vec", source: None }),
}
}
}
pub fn as_json_value(&self) -> Result<serde_json::Value> {
serde_json::from_str::<serde_json::Value>(&self.0)
.map_err(|e| ConversionError::new(self.clone(), "json-value", Box::new(e)))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_vec() {
insta::assert_debug_snapshot!(DynVal::from_string("[]".to_string()).as_vec());
insta::assert_debug_snapshot!(DynVal::from_string("[hi]".to_string()).as_vec());
insta::assert_debug_snapshot!(DynVal::from_string("[hi,ho,hu]".to_string()).as_vec());
insta::assert_debug_snapshot!(DynVal::from_string("[hi\\,ho]".to_string()).as_vec());
insta::assert_debug_snapshot!(DynVal::from_string("[hi\\,ho,hu]".to_string()).as_vec());
insta::assert_debug_snapshot!(DynVal::from_string("".to_string()).as_vec());
insta::assert_debug_snapshot!(DynVal::from_string("[a,b".to_string()).as_vec());
insta::assert_debug_snapshot!(DynVal::from_string("a]".to_string()).as_vec());
}
}

View file

@ -0,0 +1,65 @@
use crate::{
dynval,
parser::lexer::{self, LexicalError},
};
use eww_shared_util::{Span, Spanned};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Error parsing expression: {source}")]
ParseError { file_id: usize, source: lalrpop_util::ParseError<usize, lexer::Token, lexer::LexicalError> },
#[error(transparent)]
ConversionError(#[from] dynval::ConversionError),
#[error("{1}")]
Spanned(Span, Box<Error>),
#[error(transparent)]
Eval(#[from] crate::eval::EvalError),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl Error {
pub fn from_parse_error(file_id: usize, err: lalrpop_util::ParseError<usize, lexer::Token, lexer::LexicalError>) -> Self {
Error::ParseError { file_id, source: err }
}
pub fn at(self, span: Span) -> Self {
Self::Spanned(span, Box::new(self))
}
}
impl Spanned for Error {
fn span(&self) -> Span {
match self {
Self::ParseError { file_id, source } => get_parse_error_span(*file_id, source),
Self::Spanned(span, _) => *span,
Self::Eval(err) => err.span(),
Self::ConversionError(err) => err.span(),
_ => Span::DUMMY,
}
}
}
fn get_parse_error_span(file_id: usize, err: &lalrpop_util::ParseError<usize, lexer::Token, lexer::LexicalError>) -> Span {
match err {
lalrpop_util::ParseError::InvalidToken { location } => Span(*location, *location, file_id),
lalrpop_util::ParseError::UnrecognizedEOF { location, expected: _ } => Span(*location, *location, file_id),
lalrpop_util::ParseError::UnrecognizedToken { token, expected: _ } => Span(token.0, token.2, file_id),
lalrpop_util::ParseError::ExtraToken { token } => Span(token.0, token.2, file_id),
lalrpop_util::ParseError::User { error: LexicalError(span) } => *span,
}
}
#[macro_export]
macro_rules! spanned {
($err:ty, $span:expr, $block:expr) => {{
let span = $span;
let result: Result<_, $err> = try { $block };
result.at(span)
}};
}

View file

@ -0,0 +1,247 @@
use itertools::Itertools;
use crate::{
ast::{BinOp, SimplExpr, UnaryOp},
dynval::{ConversionError, DynVal},
};
use eww_shared_util::{Span, Spanned, VarName};
use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum EvalError {
#[error("Tried to reference variable `{0}`, but we cannot access variables here")]
NoVariablesAllowed(VarName),
#[error("Invalid regex: {0}")]
InvalidRegex(#[from] regex::Error),
#[error("Unknown variable {0}")]
UnknownVariable(VarName, Vec<VarName>),
#[error(transparent)]
ConversionError(#[from] ConversionError),
#[error("Incorrect number of arguments given to function: {0}")]
WrongArgCount(String),
#[error("Unknown function {0}")]
UnknownFunction(String),
#[error("Unable to index into value {0}")]
CannotIndex(String),
#[error("{1}")]
Spanned(Span, Box<EvalError>),
}
impl EvalError {
pub fn at(self, span: Span) -> Self {
Self::Spanned(span, Box::new(self))
}
pub fn map_in_span(self, f: impl FnOnce(Self) -> Self) -> Self {
match self {
EvalError::Spanned(span, err) => EvalError::Spanned(span, Box::new(err.map_in_span(f))),
other => f(other),
}
}
}
impl Spanned for EvalError {
fn span(&self) -> Span {
match self {
EvalError::Spanned(span, _) => *span,
EvalError::ConversionError(err) => err.span(),
_ => Span::DUMMY,
}
}
}
impl SimplExpr {
/// map over all of the variable references, replacing them with whatever expression the provided function returns.
/// Returns [Err] when the provided function fails with an [Err]
pub fn try_map_var_refs<E, F: Fn(Span, VarName) -> Result<SimplExpr, E> + Copy>(self, f: F) -> Result<Self, E> {
use SimplExpr::*;
Ok(match self {
BinOp(span, box a, op, box b) => BinOp(span, box a.try_map_var_refs(f)?, op, box b.try_map_var_refs(f)?),
Concat(span, elems) => Concat(span, elems.into_iter().map(|x| x.try_map_var_refs(f)).collect::<Result<_, _>>()?),
UnaryOp(span, op, box a) => UnaryOp(span, op, box a.try_map_var_refs(f)?),
IfElse(span, box a, box b, box c) => {
IfElse(span, box a.try_map_var_refs(f)?, box b.try_map_var_refs(f)?, box c.try_map_var_refs(f)?)
}
JsonAccess(span, box a, box b) => JsonAccess(span, box a.try_map_var_refs(f)?, box b.try_map_var_refs(f)?),
FunctionCall(span, name, args) => {
FunctionCall(span, name, args.into_iter().map(|x| x.try_map_var_refs(f)).collect::<Result<_, _>>()?)
}
VarRef(span, name) => f(span, name)?,
x @ Literal(..) => x,
})
}
pub fn map_var_refs(self, f: impl Fn(Span, VarName) -> SimplExpr) -> Self {
self.try_map_var_refs(|span, var| Ok::<_, !>(f(span, var))).into_ok()
}
/// resolve partially.
/// If a var-ref links to another var-ref, that other var-ref is used.
/// If a referenced variable is not found in the given hashmap, returns the var-ref unchanged.
pub fn resolve_one_level(self, variables: &HashMap<VarName, SimplExpr>) -> Self {
self.map_var_refs(|span, name| variables.get(&name).cloned().unwrap_or_else(|| Self::VarRef(span, name)))
}
/// resolve variable references in the expression. Fails if a variable cannot be resolved.
pub fn resolve_refs(self, variables: &HashMap<VarName, DynVal>) -> Result<Self, EvalError> {
use SimplExpr::*;
self.try_map_var_refs(|span, name| match variables.get(&name) {
Some(value) => Ok(Literal(value.clone())),
None => {
let similar_ish =
variables.keys().filter(|key| levenshtein::levenshtein(&key.0, &name.0) < 3).cloned().collect_vec();
Err(EvalError::UnknownVariable(name.clone(), similar_ish).at(span))
}
})
}
pub fn var_refs(&self) -> Vec<(Span, &VarName)> {
use SimplExpr::*;
match self {
Literal(..) => Vec::new(),
VarRef(span, name) => vec![(*span, name)],
Concat(_, elems) => elems.iter().flat_map(|x| x.var_refs().into_iter()).collect(),
BinOp(_, box a, _, box b) | JsonAccess(_, box a, box b) => {
let mut refs = a.var_refs();
refs.extend(b.var_refs().iter());
refs
}
UnaryOp(_, _, box x) => x.var_refs(),
IfElse(_, box a, box b, box c) => {
let mut refs = a.var_refs();
refs.extend(b.var_refs().iter());
refs.extend(c.var_refs().iter());
refs
}
FunctionCall(_, _, args) => args.iter().flat_map(|a| a.var_refs()).collect(),
}
}
pub fn eval_no_vars(&self) -> Result<DynVal, EvalError> {
match self.eval(&HashMap::new()) {
Ok(x) => Ok(x),
Err(x) => Err(x.map_in_span(|err| match err {
EvalError::UnknownVariable(name, _) => EvalError::NoVariablesAllowed(name),
other => other,
})),
}
}
pub fn eval(&self, values: &HashMap<VarName, DynVal>) -> Result<DynVal, EvalError> {
let span = self.span();
let value = match self {
SimplExpr::Literal(x) => Ok(x.clone()),
SimplExpr::Concat(span, elems) => {
let mut output = String::new();
for elem in elems {
let result = elem.eval(values)?;
output.push_str(&result.0);
}
Ok(DynVal(output, *span))
}
SimplExpr::VarRef(span, ref name) => {
let similar_ish =
values.keys().filter(|keys| levenshtein::levenshtein(&keys.0, &name.0) < 3).cloned().collect_vec();
Ok(values
.get(name)
.cloned()
.ok_or_else(|| EvalError::UnknownVariable(name.clone(), similar_ish).at(*span))?
.at(*span))
}
SimplExpr::BinOp(span, a, op, b) => {
let a = a.eval(values)?;
let b = b.eval(values)?;
let dynval = match op {
BinOp::Equals => DynVal::from(a == b),
BinOp::NotEquals => DynVal::from(a != b),
BinOp::And => DynVal::from(a.as_bool()? && b.as_bool()?),
BinOp::Or => DynVal::from(a.as_bool()? || b.as_bool()?),
BinOp::Plus => match (a.as_f64(), b.as_f64()) {
(Ok(a), Ok(b)) => DynVal::from(a + b),
_ => DynVal::from(format!("{}{}", a.as_string()?, b.as_string()?)),
},
BinOp::Minus => DynVal::from(a.as_f64()? - b.as_f64()?),
BinOp::Times => DynVal::from(a.as_f64()? * b.as_f64()?),
BinOp::Div => DynVal::from(a.as_f64()? / b.as_f64()?),
BinOp::Mod => DynVal::from(a.as_f64()? % b.as_f64()?),
BinOp::GT => DynVal::from(a.as_f64()? > b.as_f64()?),
BinOp::LT => DynVal::from(a.as_f64()? < b.as_f64()?),
#[allow(clippy::useless_conversion)]
BinOp::Elvis => DynVal::from(if a.0.is_empty() { b } else { a }),
BinOp::RegexMatch => {
let regex = regex::Regex::new(&b.as_string()?)?;
DynVal::from(regex.is_match(&a.as_string()?))
}
};
Ok(dynval.at(*span))
}
SimplExpr::UnaryOp(span, op, a) => {
let a = a.eval(values)?;
Ok(match op {
UnaryOp::Not => DynVal::from(!a.as_bool()?).at(*span),
})
}
SimplExpr::IfElse(_, cond, yes, no) => {
if cond.eval(values)?.as_bool()? {
yes.eval(values)
} else {
no.eval(values)
}
}
SimplExpr::JsonAccess(span, val, index) => {
let val = val.eval(values)?;
let index = index.eval(values)?;
match val.as_json_value()? {
serde_json::Value::Array(val) => {
let index = index.as_i32()?;
let indexed_value = val.get(index as usize).unwrap_or(&serde_json::Value::Null);
Ok(DynVal::from(indexed_value).at(*span))
}
serde_json::Value::Object(val) => {
let indexed_value = val
.get(&index.as_string()?)
.or_else(|| val.get(&index.as_i32().ok()?.to_string()))
.unwrap_or(&serde_json::Value::Null);
Ok(DynVal::from(indexed_value).at(*span))
}
_ => Err(EvalError::CannotIndex(format!("{}", val)).at(*span)),
}
}
SimplExpr::FunctionCall(span, function_name, args) => {
let args = args.into_iter().map(|a| a.eval(values)).collect::<Result<_, EvalError>>()?;
call_expr_function(&function_name, args).map(|x| x.at(*span)).map_err(|e| e.at(*span))
}
};
Ok(value?.at(span))
}
}
fn call_expr_function(name: &str, args: Vec<DynVal>) -> Result<DynVal, EvalError> {
match name {
"round" => match args.as_slice() {
[num, digits] => {
let num = num.as_f64()?;
let digits = digits.as_i32()?;
Ok(DynVal::from(format!("{:.1$}", num, digits as usize)))
}
_ => Err(EvalError::WrongArgCount(name.to_string())),
},
"replace" => match args.as_slice() {
[string, pattern, replacement] => {
let string = string.as_string()?;
let pattern = regex::Regex::new(&pattern.as_string()?)?;
let replacement = replacement.as_string()?;
Ok(DynVal::from(pattern.replace_all(&string, replacement.replace("$", "$$").replace("\\", "$")).into_owned()))
}
_ => Err(EvalError::WrongArgCount(name.to_string())),
},
_ => Err(EvalError::UnknownFunction(name.to_string())),
}
}

View file

@ -0,0 +1,24 @@
#![feature(box_patterns)]
#![feature(format_args_capture)]
#![feature(pattern)]
#![feature(box_syntax)]
#![feature(try_blocks)]
#![feature(unwrap_infallible)]
#![feature(never_type)]
pub mod ast;
pub mod dynval;
pub mod error;
pub mod eval;
pub mod parser;
pub use ast::SimplExpr;
use lalrpop_util::lalrpop_mod;
lalrpop_mod!(
#[allow(clippy::all)]
pub simplexpr_parser
);
pub use parser::parse_string;

View file

@ -0,0 +1,45 @@
use eww_shared_util::Span;
use crate::{dynval::DynVal, SimplExpr};
use super::lexer::{LexicalError, Sp, StrLitSegment, Token};
pub fn b<T>(x: T) -> Box<T> {
Box::new(x)
}
pub fn parse_stringlit(
span: Span,
mut segs: Vec<Sp<StrLitSegment>>,
) -> Result<SimplExpr, lalrpop_util::ParseError<usize, Token, LexicalError>> {
let file_id = span.2;
let parser = crate::simplexpr_parser::ExprParser::new();
if segs.len() == 1 {
let (lo, seg, hi) = segs.remove(0);
let span = Span(lo, hi, file_id);
match seg {
StrLitSegment::Literal(lit) => Ok(SimplExpr::Literal(DynVal(lit, span))),
StrLitSegment::Interp(toks) => {
let token_stream = toks.into_iter().map(|x| Ok(x));
parser.parse(file_id, token_stream)
}
}
} else {
let elems = segs
.into_iter()
.filter_map(|(lo, segment, hi)| {
let span = Span(lo, hi, file_id);
match segment {
StrLitSegment::Literal(lit) if lit.is_empty() => None,
StrLitSegment::Literal(lit) => Some(Ok(SimplExpr::Literal(DynVal(lit, span)))),
StrLitSegment::Interp(toks) => {
let token_stream = toks.into_iter().map(|x| Ok(x));
Some(parser.parse(file_id, token_stream))
}
}
})
.collect::<Result<Vec<SimplExpr>, _>>()?;
Ok(SimplExpr::Concat(span, elems))
}
}

View file

@ -0,0 +1,290 @@
use std::str::pattern::Pattern;
use eww_shared_util::{Span, Spanned};
use once_cell::sync::Lazy;
use regex::{escape, Regex, RegexSet};
pub type Sp<T> = (usize, T, usize);
#[derive(Debug, PartialEq, Eq, Clone, strum::Display, strum::EnumString)]
pub enum StrLitSegment {
Literal(String),
Interp(Vec<Sp<Token>>),
}
#[derive(Debug, PartialEq, Eq, Clone, strum::Display, strum::EnumString)]
pub enum Token {
Plus,
Minus,
Times,
Div,
Mod,
Equals,
NotEquals,
And,
Or,
GT,
LT,
Elvis,
RegexMatch,
Not,
Comma,
Question,
Colon,
LPren,
RPren,
LBrack,
RBrack,
Dot,
True,
False,
Ident(String),
NumLit(String),
StringLit(Vec<Sp<StrLitSegment>>),
Comment,
Skip,
}
macro_rules! regex_rules {
($( $regex:expr => $token:expr),*) => {
static LEXER_REGEX_SET: Lazy<RegexSet> = Lazy::new(|| { RegexSet::new(&[
$(format!("^{}", $regex)),*
]).unwrap()});
static LEXER_REGEXES: Lazy<Vec<Regex>> = Lazy::new(|| { vec![
$(Regex::new(&format!("^{}", $regex)).unwrap()),*
]});
static LEXER_FNS: Lazy<Vec<Box<dyn Fn(String) -> Token + Sync + Send>>> = Lazy::new(|| { vec![
$(Box::new($token)),*
]});
}
}
static ESCAPE_REPLACE_REGEX: Lazy<regex::Regex> = Lazy::new(|| Regex::new(r"\\(.)").unwrap());
pub static STR_INTERPOLATION_START: &str = "${";
pub static STR_INTERPOLATION_END: &str = "}";
regex_rules! {
escape(r"+") => |_| Token::Plus,
escape(r"-") => |_| Token::Minus,
escape(r"*") => |_| Token::Times,
escape(r"/") => |_| Token::Div,
escape(r"%") => |_| Token::Mod,
escape(r"==") => |_| Token::Equals,
escape(r"!=") => |_| Token::NotEquals,
escape(r"&&") => |_| Token::And,
escape(r"||") => |_| Token::Or,
escape(r">") => |_| Token::GT,
escape(r"<") => |_| Token::LT,
escape(r"?:") => |_| Token::Elvis,
escape(r"=~") => |_| Token::RegexMatch,
escape(r"!" ) => |_| Token::Not,
escape(r",") => |_| Token::Comma,
escape(r"?") => |_| Token::Question,
escape(r":") => |_| Token::Colon,
escape(r"(") => |_| Token::LPren,
escape(r")") => |_| Token::RPren,
escape(r"[") => |_| Token::LBrack,
escape(r"]") => |_| Token::RBrack,
escape(r".") => |_| Token::Dot,
escape(r"true") => |_| Token::True,
escape(r"false") => |_| Token::False,
r"[ \n\n\f]+" => |_| Token::Skip,
r";.*"=> |_| Token::Comment,
r"[a-zA-Z_][a-zA-Z0-9_-]*" => |x| Token::Ident(x.to_string()),
r"[+-]?(?:[0-9]+[.])?[0-9]+" => |x| Token::NumLit(x.to_string())
}
#[derive(Debug)]
pub struct Lexer<'s> {
file_id: usize,
source: &'s str,
pos: usize,
failed: bool,
offset: usize,
}
impl<'s> Lexer<'s> {
pub fn new(file_id: usize, span_offset: usize, source: &'s str) -> Self {
Lexer { source, offset: span_offset, file_id, failed: false, pos: 0 }
}
fn remaining(&self) -> &'s str {
&self.source[self.pos..]
}
pub fn continues_with(&self, pat: impl Pattern<'s>) -> bool {
self.remaining().starts_with(pat)
}
pub fn next_token(&mut self) -> Option<Result<Sp<Token>, LexicalError>> {
loop {
if self.failed || self.pos >= self.source.len() {
return None;
}
let remaining = self.remaining();
if remaining.starts_with(&['"', '\'', '`'][..]) {
return self.string_lit().map(|x| x.map(|(lo, segs, hi)| (lo, Token::StringLit(segs), hi)));
} else {
let match_set = LEXER_REGEX_SET.matches(remaining);
let matched_token = match_set
.into_iter()
.map(|i: usize| {
let m = LEXER_REGEXES[i].find(remaining).unwrap();
(m.end(), i)
})
.min_by_key(|(_, x)| *x);
let (len, i) = match matched_token {
Some(x) => x,
None => {
self.failed = true;
return Some(Err(LexicalError(Span(self.pos + self.offset, self.pos + self.offset, self.file_id))));
}
};
let tok_str = &self.source[self.pos..self.pos + len];
let old_pos = self.pos;
self.advance_by(len);
match LEXER_FNS[i](tok_str.to_string()) {
Token::Skip | Token::Comment => {}
token => {
return Some(Ok((old_pos + self.offset, token, self.pos + self.offset)));
}
}
}
}
}
fn advance_by(&mut self, n: usize) {
self.pos += n;
while self.pos < self.source.len() && !self.source.is_char_boundary(self.pos) {
self.pos += 1;
}
}
fn advance_until_one_of<'a>(&mut self, pat: &[&'a str]) -> Option<&'a str> {
loop {
let remaining = self.remaining();
if remaining.is_empty() {
return None;
} else if let Some(matched) = pat.iter().find(|&&p| remaining.starts_with(p)) {
self.advance_by(matched.len());
return Some(matched);
} else {
self.advance_by(1);
}
}
}
fn advance_until_unescaped_one_of<'a>(&mut self, pat: &[&'a str]) -> Option<&'a str> {
let mut pattern = pat.to_vec();
pattern.push("\\");
match self.advance_until_one_of(pattern.as_slice()) {
Some("\\") => {
self.advance_by(1);
self.advance_until_unescaped_one_of(pat)
}
result => result,
}
}
pub fn string_lit(&mut self) -> Option<Result<Sp<Vec<Sp<StrLitSegment>>>, LexicalError>> {
let quote = self.remaining().chars().next()?.to_string();
let str_lit_start = self.pos;
self.advance_by(quote.len());
let mut elements = Vec::new();
let mut in_string_lit = true;
loop {
if in_string_lit {
let segment_start = self.pos - quote.len();
let segment_ender = self.advance_until_unescaped_one_of(&[STR_INTERPOLATION_START, &quote])?;
let lit_content = &self.source[segment_start + quote.len()..self.pos - segment_ender.len()];
let lit_content = ESCAPE_REPLACE_REGEX.replace_all(lit_content, "$1").to_string();
elements.push((segment_start + self.offset, StrLitSegment::Literal(lit_content), self.pos + self.offset));
if segment_ender == STR_INTERPOLATION_START {
in_string_lit = false;
} else if segment_ender == quote {
return Some(Ok((str_lit_start + self.offset, elements, self.pos + self.offset)));
}
} else {
let segment_start = self.pos;
let mut toks = Vec::new();
while self.pos < self.source.len() && !self.remaining().starts_with(STR_INTERPOLATION_END) {
match self.next_token()? {
Ok(tok) => toks.push(tok),
Err(err) => return Some(Err(err)),
}
}
elements.push((segment_start + self.offset, StrLitSegment::Interp(toks), self.pos + self.offset));
self.advance_by(STR_INTERPOLATION_END.len());
in_string_lit = true;
}
}
}
}
impl<'s> Iterator for Lexer<'s> {
type Item = Result<Sp<Token>, LexicalError>;
fn next(&mut self) -> Option<Self::Item> {
self.next_token()
}
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct LexicalError(pub Span);
impl Spanned for LexicalError {
fn span(&self) -> Span {
self.0
}
}
impl std::fmt::Display for LexicalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Lexical error at {}", self.0)
}
}
#[cfg(test)]
mod test {
use super::*;
use eww_shared_util::snapshot_string;
use itertools::Itertools;
macro_rules! v {
($x:literal) => {
Lexer::new(0, 0, $x)
.map(|x| match x {
Ok((l, x, r)) => format!("({}, {:?}, {})", l, x, r),
Err(err) => format!("{}", err),
})
.join("\n")
};
}
snapshot_string! {
basic => v!(r#"bar "foo""#),
digit => v!(r#"12"#),
number_in_ident => v!(r#"foo_1_bar"#),
interpolation_1 => v!(r#" "foo ${2 * 2} bar" "#),
interpolation_nested => v!(r#" "foo ${(2 * 2) + "${5 + 5}"} bar" "#),
escaping => v!(r#" "a\"b\{}" "#),
comments => v!("foo ; bar"),
weird_char_boundaries => v!(r#"" " + music"#),
symbol_spam => v!(r#"(foo + - "()" "a\"b" true false [] 12.2)"#),
}
}

View file

@ -0,0 +1,47 @@
pub mod lalrpop_helpers;
pub mod lexer;
use crate::{
ast::SimplExpr,
error::{Error, Result},
};
pub fn parse_string(byte_offset: usize, file_id: usize, s: &str) -> Result<SimplExpr> {
let lexer = lexer::Lexer::new(file_id, byte_offset, s);
let parser = crate::simplexpr_parser::ExprParser::new();
parser.parse(file_id, lexer).map_err(|e| Error::from_parse_error(file_id, e))
}
#[cfg(test)]
mod tests {
macro_rules! test_parser {
($($text:literal),* $(,)?) => {{
let p = crate::simplexpr_parser::ExprParser::new();
use crate::parser::lexer::Lexer;
::insta::with_settings!({sort_maps => true}, {
$(
::insta::assert_debug_snapshot!(p.parse(0, Lexer::new(0, 0, $text)));
)*
});
}}
}
#[test]
fn test() {
test_parser!(
"1",
"2 + 5",
"2 * 5 + 1 * 1 + 3",
"(1 + 2) * 2",
"1 + true ? 2 : 5",
"1 + true ? 2 : 5 + 2",
"1 + (true ? 2 : 5) + 2",
"foo(1, 2)",
"! false || ! true",
"\"foo\" + 12.4",
"hi[\"ho\"]",
"foo.bar.baz",
"foo.bar[2 + 2] * asdf[foo.bar]",
);
}
}

View file

@ -0,0 +1,7 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "v!(r#\"bar \"foo\"\"#)"
---
(0, Ident("bar"), 3)
(4, StringLit([(4, Literal("foo"), 9)]), 9)

View file

@ -0,0 +1,6 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "v!(\"foo ; bar\")"
---
(0, Ident("foo"), 3)

View file

@ -0,0 +1,6 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "v!(r#\"12\"#)"
---
(0, NumLit("12"), 2)

View file

@ -0,0 +1,6 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "v!(r#\" \"a\\\"b\\{}\" \"#)"
---
(1, StringLit([(1, Literal("a\"b{}"), 10)]), 10)

View file

@ -0,0 +1,6 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "v!(r#\" \"foo ${2 * 2} bar\" \"#)"
---
(1, StringLit([(1, Literal("foo "), 8), (8, Interp([(8, NumLit("2"), 9), (10, Times, 11), (12, NumLit("2"), 13)]), 13), (13, Literal(" bar"), 19)]), 19)

View file

@ -0,0 +1,6 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "v!(r#\" \"foo ${(2 * 2) + \"${5 + 5}\"} bar\" \"#)"
---
(1, StringLit([(1, Literal("foo "), 8), (8, Interp([(8, LPren, 9), (9, NumLit("2"), 10), (11, Times, 12), (13, NumLit("2"), 14), (14, RPren, 15), (16, Plus, 17), (18, StringLit([(18, Literal(""), 21), (21, Interp([(21, NumLit("5"), 22), (23, Plus, 24), (25, NumLit("5"), 26)]), 26), (26, Literal(""), 28)]), 28)]), 28), (28, Literal(" bar"), 34)]), 34)

View file

@ -0,0 +1,6 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "v!(r#\"foo_1_bar\"#)"
---
(0, Ident("foo_1_bar"), 9)

View file

@ -0,0 +1,33 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "Lexer::new(0, 0, r#\"bar \"foo\"\"#).collect_vec()"
---
[
Ok(
(
0,
Ident(
"bar",
),
3,
),
),
Ok(
(
4,
StringLit(
[
(
4,
Literal(
"foo",
),
9,
),
],
),
9,
),
),
]

View file

@ -0,0 +1,122 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "Lexer::new(0, 0, r#\" \"foo {(2 * 2) + \"{5 + 5}\"} bar\" \"#).collect_vec()"
---
[
Ok(
(
1,
StringLit(
[
(
1,
Literal(
"foo ",
),
7,
),
(
7,
Interp(
[
(
7,
LPren,
8,
),
(
8,
NumLit(
"2",
),
9,
),
(
10,
Times,
11,
),
(
12,
NumLit(
"2",
),
13,
),
(
13,
RPren,
14,
),
(
15,
Plus,
16,
),
(
17,
StringLit(
[
(
17,
Literal(
"",
),
19,
),
(
19,
Interp(
[
(
19,
NumLit(
"5",
),
20,
),
(
21,
Plus,
22,
),
(
23,
NumLit(
"5",
),
24,
),
],
),
24,
),
(
24,
Literal(
"",
),
26,
),
],
),
26,
),
],
),
26,
),
(
26,
Literal(
" bar",
),
32,
),
],
),
32,
),
),
]

View file

@ -0,0 +1,24 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "Lexer::new(0, 0, r#\" \"a\\\"b\\{}\" \"#).collect_vec()"
---
[
Ok(
(
1,
StringLit(
[
(
1,
Literal(
"a\"b{}",
),
10,
),
],
),
10,
),
),
]

View file

@ -0,0 +1,58 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "Lexer::new(0, 0, r#\" \"foo {2 * 2} bar\" \"#).collect_vec()"
---
[
Ok(
(
1,
StringLit(
[
(
1,
Literal(
"foo ",
),
7,
),
(
7,
Interp(
[
(
7,
NumLit(
"2",
),
8,
),
(
9,
Times,
10,
),
(
11,
NumLit(
"2",
),
12,
),
],
),
12,
),
(
12,
Literal(
" bar",
),
18,
),
],
),
18,
),
),
]

View file

@ -0,0 +1,17 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "v!(r#\"(foo + - \"()\" \"a\\\"b\" true false [] 12.2)\"#)"
---
(0, LPren, 1)
(1, Ident("foo"), 4)
(5, Plus, 6)
(7, Minus, 8)
(9, StringLit([(9, Literal("()"), 13)]), 13)
(14, StringLit([(14, Literal("a\"b"), 20)]), 20)
(21, True, 25)
(26, False, 31)
(32, LBrack, 33)
(33, RBrack, 34)
(35, NumLit("12.2"), 39)
(39, RPren, 40)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/lexer.rs
expression: "v!(r#\"\" \" + music\"#)"
---
(0, StringLit([(0, Literal("\u{f001} "), 8)]), 8)
(9, Plus, 10)
(11, Ident("music"), 16)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"\\\"foo\\\" + 12.4\"))"
---
Ok(
("foo" + "12.4"),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"hi[\\\"ho\\\"]\"))"
---
Ok(
hi["ho"],
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"foo.bar.baz\"))"
---
Ok(
foo["bar"]["baz"],
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"foo.bar[2 + 2] * asdf[foo.bar]\"))"
---
Ok(
(foo["bar"][("2" + "2")] * asdf[foo["bar"]]),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"2 + 5\"))"
---
Ok(
("2" + "5"),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"2 * 5 + 1 * 1 + 3\"))"
---
Ok(
((("2" * "5") + ("1" * "1")) + "3"),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"(1 + 2) * 2\"))"
---
Ok(
(("1" + "2") * "2"),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"1 + true ? 2 : 5\"))"
---
Ok(
(("1" + "true") ? "2" : "5"),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"1 + true ? 2 : 5 + 2\"))"
---
Ok(
(("1" + "true") ? "2" : ("5" + "2")),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"1 + (true ? 2 : 5) + 2\"))"
---
Ok(
(("1" + ("true" ? "2" : "5")) + "2"),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"foo(1, 2)\"))"
---
Ok(
foo("1", "2"),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"! false || ! true\"))"
---
Ok(
(!"false" || !"true"),
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/parser/mod.rs
expression: "p.parse(0, Lexer::new(0, 0, \"1\"))"
---
Ok(
"1",
)

View file

@ -0,0 +1,112 @@
use crate::ast::{SimplExpr::{self, *}, BinOp::*, UnaryOp::*};
use eww_shared_util::{Span, VarName};
use crate::parser::lexer::{Token, LexicalError, StrLitSegment, Sp};
use crate::parser::lalrpop_helpers::*;
grammar(fid: usize);
extern {
type Location = usize;
type Error = LexicalError;
enum Token {
"+" => Token::Plus,
"-" => Token::Minus,
"*" => Token::Times,
"/" => Token::Div,
"%" => Token::Mod,
"==" => Token::Equals,
"!=" => Token::NotEquals,
"&&" => Token::And,
"||" => Token::Or,
">" => Token::GT,
"<" => Token::LT,
"?:" => Token::Elvis,
"=~" => Token::RegexMatch,
"!" => Token::Not,
"," => Token::Comma,
"?" => Token::Question,
":" => Token::Colon,
"(" => Token::LPren,
")" => Token::RPren,
"[" => Token::LBrack,
"]" => Token::RBrack,
"." => Token::Dot,
"true" => Token::True,
"false" => Token::False,
"identifier" => Token::Ident(<String>),
"number" => Token::NumLit(<String>),
"string" => Token::StringLit(<Vec<Sp<StrLitSegment>>>),
}
}
Comma<T>: Vec<T> = {
<mut v:(<T> ",")*> <e:T?> => match e {
None => v,
Some(e) => {
v.push(e);
v
}
}
};
pub Expr: SimplExpr = {
#[precedence(level="0")]
//<l:@L> "lexer_error" <r:@R> =>? {
// Err(ParseError::User { error: LexicalError(l, r, fid) })
//},
<l:@L> <x:"string"> <r:@R> =>? parse_stringlit(Span(l, r, fid), x),
<l:@L> <x:"number"> <r:@R> => SimplExpr::literal(Span(l, r, fid), x),
<l:@L> "true" <r:@R> => SimplExpr::literal(Span(l, r, fid), "true".into()),
<l:@L> "false" <r:@R> => SimplExpr::literal(Span(l, r, fid), "false".into()),
<l:@L> <ident:"identifier"> <r:@R> => VarRef(Span(l, r, fid), VarName(ident.to_string())),
"(" <ExprReset> ")",
#[precedence(level="1")] #[assoc(side="right")]
<l:@L> <ident:"identifier"> "(" <args: Comma<ExprReset>> ")" <r:@R> => FunctionCall(Span(l, r, fid), ident, args),
<l:@L> <value:Expr> "[" <index: ExprReset> "]" <r:@R> => JsonAccess(Span(l, r, fid), b(value), b(index)),
<l:@L> <value:Expr> "." <lit_l:@L> <index:"identifier"> <r:@R> => {
JsonAccess(Span(l, r, fid), b(value), b(Literal(index.into())))
},
#[precedence(level="2")] #[assoc(side="right")]
<l:@L> "!" <e:Expr> <r:@R> => UnaryOp(Span(l, r, fid), Not, b(e)),
#[precedence(level="3")] #[assoc(side="left")]
<l:@L> <le:Expr> "*" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), Times, b(re)),
<l:@L> <le:Expr> "/" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), Div, b(re)),
<l:@L> <le:Expr> "%" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), Mod, b(re)),
#[precedence(level="4")] #[assoc(side="left")]
<l:@L> <le:Expr> "+" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), Plus, b(re)),
<l:@L> <le:Expr> "-" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), Minus, b(re)),
#[precedence(level="5")] #[assoc(side="left")]
<l:@L> <le:Expr> "==" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), Equals, b(re)),
<l:@L> <le:Expr> "!=" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), NotEquals, b(re)),
<l:@L> <le:Expr> "<" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), GT, b(re)),
<l:@L> <le:Expr> ">" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), LT, b(re)),
<l:@L> <le:Expr> "=~" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), RegexMatch, b(re)),
#[precedence(level="6")] #[assoc(side="left")]
<l:@L> <le:Expr> "&&" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), And, b(re)),
<l:@L> <le:Expr> "||" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), Or, b(re)),
<l:@L> <le:Expr> "?:" <re:Expr> <r:@R> => BinOp(Span(l, r, fid), b(le), Elvis, b(re)),
#[precedence(level="7")] #[assoc(side="right")]
<l:@L> <cond:Expr> "?" <then:ExprReset> ":" <els:Expr> <r:@R> => {
IfElse(Span(l, r, fid), b(cond), b(then), b(els))
},
};
ExprReset = <Expr>;

View file

@ -0,0 +1,10 @@
---
source: crates/simplexpr/src/dynval.rs
expression: "DynVal::from_string(\"[hi]\".to_string()).as_vec()"
---
Ok(
[
"hi",
],
)

View file

@ -0,0 +1,12 @@
---
source: crates/simplexpr/src/dynval.rs
expression: "DynVal::from_string(\"[hi,ho,hu]\".to_string()).as_vec()"
---
Ok(
[
"hi",
"ho",
"hu",
],
)

View file

@ -0,0 +1,10 @@
---
source: crates/simplexpr/src/dynval.rs
expression: "DynVal::from_string(\"[hi\\\\,ho]\".to_string()).as_vec()"
---
Ok(
[
"hi,ho",
],
)

View file

@ -0,0 +1,11 @@
---
source: crates/simplexpr/src/dynval.rs
expression: "DynVal::from_string(\"[hi\\\\,ho,hu]\".to_string()).as_vec()"
---
Ok(
[
"hi,ho",
"hu",
],
)

View file

@ -0,0 +1,8 @@
---
source: crates/simplexpr/src/dynval.rs
expression: "DynVal::from_string(\"\".to_string()).as_vec()"
---
Ok(
[],
)

View file

@ -0,0 +1,12 @@
---
source: crates/simplexpr/src/dynval.rs
expression: "DynVal::from_string(\"[a,b\".to_string()).as_vec()"
---
Err(
ConversionError {
value: "[a,b",
target_type: "vec",
source: None,
},
)

View file

@ -0,0 +1,12 @@
---
source: crates/simplexpr/src/dynval.rs
expression: "DynVal::from_string(\"a]\".to_string()).as_vec()"
---
Err(
ConversionError {
value: "a]",
target_type: "vec",
source: None,
},
)

View file

@ -0,0 +1,10 @@
---
source: crates/simplexpr/src/dynval.rs
expression: "DynVal::from_string(\"[]\".to_string()).as_vec()"
---
Ok(
[
"",
],
)

View file

@ -0,0 +1,8 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"\\\"foo\\\" + 12.4\"))"
---
Ok(
("foo" + "12.4"),
)

View file

@ -0,0 +1,8 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"hi[\\\"ho\\\"]\"))"
---
Ok(
hi["ho"],
)

View file

@ -0,0 +1,8 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"foo.bar.baz\"))"
---
Ok(
foo["bar"]["baz"],
)

View file

@ -0,0 +1,8 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"foo.bar[2 + 2] * asdf[foo.bar]\"))"
---
Ok(
(foo["bar"][("2" + "2")] * asdf[foo["bar"]]),
)

View file

@ -0,0 +1,30 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"1 + (true ? 2 : 5) + 2\"))"
---
Ok(
BinOp(
BinOp(
Literal(
"1",
),
Plus,
IfElse(
Literal(
"true",
),
Literal(
"2",
),
Literal(
"5",
),
),
),
Plus,
Literal(
"2",
),
),
)

View file

@ -0,0 +1,13 @@
---
source: src/lib.rs
expression: "Lexer::new(\"foo(1, 2)\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x) |\n Token::NumLit(x) |\n Token::StrLit(x) =>\n format!(\"{}\", x),\n x =>\n format!(\"{}\", x),\n }).collect::<Vec<_>>()"
---
[
"foo",
"LPren",
"1",
"Comma",
"2",
"RPren",
]

View file

@ -0,0 +1,18 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"foo(1, 2)\"))"
---
Ok(
FunctionCall(
"foo",
[
Literal(
"1",
),
Literal(
"2",
),
],
),
)

View file

@ -0,0 +1,12 @@
---
source: src/lib.rs
expression: "Lexer::new(\"! false || ! true\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x)\n |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\",\n x),\n x =>\n format!(\"{}\",\n x),\n }).collect::<Vec<_>>()"
---
[
"!",
"False",
"||",
"!",
"True",
]

View file

@ -0,0 +1,22 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"! false || ! true\"))"
---
Ok(
BinOp(
UnaryOp(
Not,
Literal(
"false",
),
),
Or,
UnaryOp(
Not,
Literal(
"true",
),
),
),
)

View file

@ -0,0 +1,10 @@
---
source: src/lib.rs
expression: "Lexer::new(\"\\\"foo\\\" + 12.4\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x)\n |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\",\n x),\n x =>\n format!(\"{}\",\n x),\n }).collect::<Vec<_>>()"
---
[
"\"foo\"",
"+",
"12.4",
]

View file

@ -0,0 +1,8 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"2 + 5\"))"
---
Ok(
("2" + "5"),
)

View file

@ -0,0 +1,16 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"\\\"foo\\\" + 12.4\"))"
---
Ok(
BinOp(
Literal(
"foo",
),
Plus,
Literal(
"12.4",
),
),
)

View file

@ -0,0 +1,11 @@
---
source: src/lib.rs
expression: "Lexer::new(\"hi[\\\"ho\\\"]\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x) |\n Token::NumLit(x) |\n Token::StrLit(x)\n =>\n format!(\"{}\", x),\n x =>\n format!(\"{}\", x),\n }).collect::<Vec<_>>()"
---
[
"hi",
"LBrack",
"\"ho\"",
"RBrack",
]

View file

@ -0,0 +1,15 @@
---
source: src/lib.rs
expression: "p.parse(Lexer::new(\"hi[\\\"ho\\\"]\"))"
---
Ok(
JsonAccess(
VarRef(
"hi",
),
Literal(
"ho",
),
),
)

View file

@ -0,0 +1,12 @@
---
source: src/lib.rs
expression: "Lexer::new(\"foo.bar.baz\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x) |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\", x),\n x =>\n format!(\"{}\", x),\n }).collect::<Vec<_>>()"
---
[
"foo",
"Dot",
"bar",
"Dot",
"baz",
]

Some files were not shown because too many files have changed in this diff Show more