Merge pull request #221 from elkowar/config_rework
Configuration format rework
This commit is contained in:
commit
9c12a316d6
213 changed files with 9866 additions and 3826 deletions
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/target
|
||||
/**/target
|
||||
|
|
936
Cargo.lock
generated
936
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
81
Cargo.toml
81
Cargo.toml
|
@ -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"
|
||||
|
|
16
README.md
16
README.md
|
@ -4,18 +4,26 @@
|
|||
|
||||
<img src="./.github/EwwLogo.svg" height="100" align="left"/>
|
||||
|
||||
Elkowar’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)
|
||||

|
||||
|
||||
|
|
29
YUCK_MIGRATION.md
Normal file
29
YUCK_MIGRATION.md
Normal 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
71
crates/eww/Cargo.toml
Normal 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}
|
|
@ -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: >k::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: >k::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 }
|
||||
}
|
|
@ -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<()> {
|
|
@ -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)?;
|
90
crates/eww/src/config/eww_config.rs
Normal file
90
crates/eww/src/config/eww_config.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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())),
|
||||
}
|
||||
}
|
8
crates/eww/src/config/mod.rs
Normal file
8
crates/eww/src/config/mod.rs
Normal 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::*;
|
47
crates/eww/src/config/script_var.rs
Normal file
47
crates/eww/src/config/script_var.rs
Normal 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))
|
||||
}
|
|
@ -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
|
39
crates/eww/src/config/window_definition.rs
Normal file
39
crates/eww/src/config/window_definition.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
39
crates/eww/src/daemon_response.rs
Normal file
39
crates/eww/src/daemon_response.rs
Normal 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>;
|
|
@ -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: >k::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
20
crates/eww/src/error.rs
Normal 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)
|
||||
}
|
||||
}
|
88
crates/eww/src/error_handling_ctx.rs
Normal file
88
crates/eww/src/error_handling_ctx.rs
Normal 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)?)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
247
crates/eww/src/main.rs
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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: ®ex::Captures| std::env::var(var_name.get(1).unwrap().as_str()).unwrap_or_default())
|
||||
.into_owned()
|
||||
}
|
|
@ -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,
|
||||
>k_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,
|
||||
>k_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,) => {
|
|
@ -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: >k::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: >k::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: >k::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: >k::Wi
|
|||
}
|
||||
|
||||
/// @widget !container
|
||||
pub(super) fn resolve_container_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::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: >k::Container) {
|
||||
// resolve_block!(bargs, gtk_widget, {});
|
||||
}
|
||||
|
||||
/// @widget !range
|
||||
|
@ -188,13 +198,14 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::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(¢er));
|
||||
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())
|
||||
)
|
147
crates/eww/src/widgets/widget_node.rs
Normal file
147
crates/eww/src/widgets/widget_node.rs
Normal 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<_>>>()?,
|
||||
}))
|
||||
}
|
||||
}
|
8
crates/eww_shared_util/Cargo.toml
Normal file
8
crates/eww_shared_util/Cargo.toml
Normal 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"
|
38
crates/eww_shared_util/src/lib.rs
Normal file
38
crates/eww_shared_util/src/lib.rs
Normal 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);
|
||||
});
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
64
crates/eww_shared_util/src/span.rs
Normal file
64
crates/eww_shared_util/src/span.rs
Normal 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;
|
||||
}
|
|
@ -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
656
crates/simplexpr/Cargo.lock
generated
Normal 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",
|
||||
]
|
30
crates/simplexpr/Cargo.toml
Normal file
30
crates/simplexpr/Cargo.toml
Normal 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"
|
6
crates/simplexpr/README.md
Normal file
6
crates/simplexpr/README.md
Normal 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.
|
4
crates/simplexpr/build.rs
Normal file
4
crates/simplexpr/build.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
extern crate lalrpop;
|
||||
fn main() {
|
||||
lalrpop::Configuration::new().log_verbose().process_current_dir().unwrap();
|
||||
}
|
1
crates/simplexpr/rust-toolchain
Normal file
1
crates/simplexpr/rust-toolchain
Normal file
|
@ -0,0 +1 @@
|
|||
nightly
|
14
crates/simplexpr/rustfmt.toml
Normal file
14
crates/simplexpr/rustfmt.toml
Normal 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
103
crates/simplexpr/src/ast.rs
Normal 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)
|
||||
}
|
||||
}
|
225
crates/simplexpr/src/dynval.rs
Normal file
225
crates/simplexpr/src/dynval.rs
Normal 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] += ⁢
|
||||
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());
|
||||
}
|
||||
}
|
65
crates/simplexpr/src/error.rs
Normal file
65
crates/simplexpr/src/error.rs
Normal 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)
|
||||
}};
|
||||
}
|
247
crates/simplexpr/src/eval.rs
Normal file
247
crates/simplexpr/src/eval.rs
Normal 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())),
|
||||
}
|
||||
}
|
24
crates/simplexpr/src/lib.rs
Normal file
24
crates/simplexpr/src/lib.rs
Normal 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;
|
45
crates/simplexpr/src/parser/lalrpop_helpers.rs
Normal file
45
crates/simplexpr/src/parser/lalrpop_helpers.rs
Normal 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))
|
||||
}
|
||||
}
|
290
crates/simplexpr/src/parser/lexer.rs
Normal file
290
crates/simplexpr/src/parser/lexer.rs
Normal 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, "e])?;
|
||||
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)"#),
|
||||
}
|
||||
}
|
47
crates/simplexpr/src/parser/mod.rs
Normal file
47
crates/simplexpr/src/parser/mod.rs
Normal 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]",
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/lexer.rs
|
||||
expression: "v!(\"foo ; bar\")"
|
||||
|
||||
---
|
||||
(0, Ident("foo"), 3)
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/lexer.rs
|
||||
expression: "v!(r#\"12\"#)"
|
||||
|
||||
---
|
||||
(0, NumLit("12"), 2)
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/lexer.rs
|
||||
expression: "v!(r#\" \"a\\\"b\\{}\" \"#)"
|
||||
|
||||
---
|
||||
(1, StringLit([(1, Literal("a\"b{}"), 10)]), 10)
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/lexer.rs
|
||||
expression: "v!(r#\"foo_1_bar\"#)"
|
||||
|
||||
---
|
||||
(0, Ident("foo_1_bar"), 9)
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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)
|
|
@ -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)
|
|
@ -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"),
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/mod.rs
|
||||
expression: "p.parse(0, Lexer::new(0, 0, \"hi[\\\"ho\\\"]\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
hi["ho"],
|
||||
)
|
|
@ -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"],
|
||||
)
|
|
@ -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"]]),
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/mod.rs
|
||||
expression: "p.parse(0, Lexer::new(0, 0, \"2 + 5\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
("2" + "5"),
|
||||
)
|
|
@ -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"),
|
||||
)
|
|
@ -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"),
|
||||
)
|
|
@ -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"),
|
||||
)
|
|
@ -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")),
|
||||
)
|
|
@ -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"),
|
||||
)
|
|
@ -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"),
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/mod.rs
|
||||
expression: "p.parse(0, Lexer::new(0, 0, \"! false || ! true\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
(!"false" || !"true"),
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/mod.rs
|
||||
expression: "p.parse(0, Lexer::new(0, 0, \"1\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
"1",
|
||||
)
|
112
crates/simplexpr/src/simplexpr_parser.lalrpop
Normal file
112
crates/simplexpr/src/simplexpr_parser.lalrpop
Normal 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>;
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/simplexpr/src/dynval.rs
|
||||
expression: "DynVal::from_string(\"[hi]\".to_string()).as_vec()"
|
||||
|
||||
---
|
||||
Ok(
|
||||
[
|
||||
"hi",
|
||||
],
|
||||
)
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/simplexpr/src/dynval.rs
|
||||
expression: "DynVal::from_string(\"[hi\\\\,ho]\".to_string()).as_vec()"
|
||||
|
||||
---
|
||||
Ok(
|
||||
[
|
||||
"hi,ho",
|
||||
],
|
||||
)
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: crates/simplexpr/src/dynval.rs
|
||||
expression: "DynVal::from_string(\"\".to_string()).as_vec()"
|
||||
|
||||
---
|
||||
Ok(
|
||||
[],
|
||||
)
|
|
@ -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,
|
||||
},
|
||||
)
|
|
@ -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,
|
||||
},
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/simplexpr/src/dynval.rs
|
||||
expression: "DynVal::from_string(\"[]\".to_string()).as_vec()"
|
||||
|
||||
---
|
||||
Ok(
|
||||
[
|
||||
"",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: src/lib.rs
|
||||
expression: "p.parse(Lexer::new(\"\\\"foo\\\" + 12.4\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
("foo" + "12.4"),
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: src/lib.rs
|
||||
expression: "p.parse(Lexer::new(\"hi[\\\"ho\\\"]\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
hi["ho"],
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: src/lib.rs
|
||||
expression: "p.parse(Lexer::new(\"foo.bar.baz\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
foo["bar"]["baz"],
|
||||
)
|
|
@ -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"]]),
|
||||
)
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
)
|
|
@ -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",
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
source: src/lib.rs
|
||||
expression: "p.parse(Lexer::new(\"foo(1, 2)\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
FunctionCall(
|
||||
"foo",
|
||||
[
|
||||
Literal(
|
||||
"1",
|
||||
),
|
||||
Literal(
|
||||
"2",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
|
@ -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",
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
|
@ -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",
|
||||
]
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: src/lib.rs
|
||||
expression: "p.parse(Lexer::new(\"2 + 5\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
("2" + "5"),
|
||||
)
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: src/lib.rs
|
||||
expression: "p.parse(Lexer::new(\"\\\"foo\\\" + 12.4\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
BinOp(
|
||||
Literal(
|
||||
"foo",
|
||||
),
|
||||
Plus,
|
||||
Literal(
|
||||
"12.4",
|
||||
),
|
||||
),
|
||||
)
|
|
@ -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",
|
||||
]
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
source: src/lib.rs
|
||||
expression: "p.parse(Lexer::new(\"hi[\\\"ho\\\"]\"))"
|
||||
|
||||
---
|
||||
Ok(
|
||||
JsonAccess(
|
||||
VarRef(
|
||||
"hi",
|
||||
),
|
||||
Literal(
|
||||
"ho",
|
||||
),
|
||||
),
|
||||
)
|
|
@ -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
Loading…
Add table
Reference in a new issue