This commit is contained in:
Penelope Gwen 2025-07-05 03:27:55 +00:00
commit 48459d6cc8
50 changed files with 2198 additions and 1454 deletions

View file

@ -37,3 +37,9 @@ body:
description: "If applicable, provide additional context or screenshots here." description: "If applicable, provide additional context or screenshots here."
validations: validations:
required: false required: false
- type: textarea
attributes:
label: "Platform and environment"
description: "Does this happen on wayland, X11, or on both? What WM/Compositor are you using? Which version of eww are you using? (when using a git version, optimally provide the exact commit ref)."
validations:
required: true

View file

@ -4,6 +4,45 @@ All notable changes to eww will be listed here, starting at changes since versio
## Unreleased ## Unreleased
### BREAKING CHANGES
- [#1176](https://github.com/elkowar/eww/pull/1176) changed safe access (`?.`) behavior:
Attempting to index in an empty JSON string (`'""'`) is now an error.
### Fixes
- Fix crash on invalid `formattime` format string (By: luca3s)
- Fix crash on NaN or infinite graph value (By: luca3s)
- Re-enable some scss features (By: w-lfchen)
- Fix and refactor nix flake (By: w-lfchen)
- Fix remove items from systray (By: vnva)
- Fix the gtk `stack` widget (By: ovalkonia)
- Fix values in the `EWW_NET` variable (By: mario-kr)
- Fix the gtk `expander` widget (By: ovalkonia)
- Fix wayland monitor names support (By: dragonnn)
- Load systray items that are registered without a path (By: Kage-Yami)
- `get_locale` now follows POSIX standard for locale selection (By: mirhahn, w-lfchen)
- Improve multi-monitor handling under wayland (By: bkueng)
### Features
- Add warning and docs for incompatible `:anchor` and `:exclusive` options
- Add `eww poll` subcommand to force-poll a variable (By: kiana-S)
- Add OnDemand support for focusable on wayland (By: GallowsDove)
- Add jq `raw-output` support (By: RomanHargrave)
- Update rust toolchain to 1.81.0 (By: w-lfchen)
- Add `:fill-svg` and `:preserve-aspect-ratio` properties to images (By: hypernova7, w-lfchen)
- Add `:truncate` property to labels, disabled by default (except in cases where truncation would be enabled in version `0.5.0` and before) (By: Rayzeq).
- Add support for `:hover` css selectors for tray items (By: zeapoz)
- Add scss support for the `:style` widget property (By: ovalkonia)
- Add `min` and `max` function calls to simplexpr (By: ovalkonia)
- Add `flip-x`, `flip-y`, `vertical` options to the graph widget to determine its direction
- Add `transform-origin-x`/`transform-origin-y` properties to transform widget (By: mario-kr)
- Add keyboard support for button presses (By: julianschuler)
- Support empty string for safe access operator (By: ModProg)
- Add `log` function calls to simplexpr (By: topongo)
- Add `:lines` and `:wrap-mode` properties to label widget (By: vaporii)
- Add `value-pos` to scale widget (By: ipsvn)
- Add `floor` and `ceil` function calls to simplexpr (By: wsbankenstein)
- Add `formatbytes` function calls to simplexpr (By: topongo)
## [0.6.0] (21.04.2024) ## [0.6.0] (21.04.2024)
### Fixes ### Fixes
@ -34,6 +73,7 @@ All notable changes to eww will be listed here, starting at changes since versio
- Made `and`, `or` and `?:` lazily evaluated in simplexpr (By: ModProg) - Made `and`, `or` and `?:` lazily evaluated in simplexpr (By: ModProg)
- Add Vanilla CSS support (By: Ezequiel Ramis) - Add Vanilla CSS support (By: Ezequiel Ramis)
- Add `jq` function, offering jq-style json processing - Add `jq` function, offering jq-style json processing
- Add support for the `EWW_BATTERY` magic variable in FreeBSD, OpenBSD, and NetBSD (By: dangerdyke)
- Add `justify` property to the label widget, allowing text justification (By: n3oney) - Add `justify` property to the label widget, allowing text justification (By: n3oney)
- Add `EWW_TIME` magic variable (By: Erenoit) - Add `EWW_TIME` magic variable (By: Erenoit)
- Add trigonometric functions (`sin`, `cos`, `tan`, `cot`) and degree/radian conversions (`degtorad`, `radtodeg`) (By: end-4) - Add trigonometric functions (`sin`, `cos`, `tan`, `cot`) and degree/radian conversions (`degtorad`, `radtodeg`) (By: end-4)

1979
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,37 +9,45 @@ eww_shared_util = { version = "0.1.0", path = "crates/eww_shared_util" }
yuck = { version = "0.1.0", path = "crates/yuck", default-features = false } yuck = { version = "0.1.0", path = "crates/yuck", default-features = false }
notifier_host = { version = "0.1.0", path = "crates/notifier_host" } notifier_host = { version = "0.1.0", path = "crates/notifier_host" }
anyhow = "1.0.79" anyhow = "1.0.86"
bincode = "1.3.3" bincode = "1.3.3"
cached = "0.48.0" bytesize = "2.0.1"
chrono = "0.4.26" cached = "0.53.1"
chrono-tz = "0.8.2" chrono = "0.4.38"
chrono-tz = "0.10.0"
clap = { version = "4.5.1", features = ["derive"] } clap = { version = "4.5.1", features = ["derive"] }
clap_complete = "4.5.1" clap_complete = "4.5.12"
codespan-reporting = "0.11" codespan-reporting = "0.11"
derive_more = "0.99" derive_more = { version = "1", features = [
"as_ref",
"debug",
"display",
"from",
"from_str",
] }
extend = "1.2" extend = "1.2"
futures = "0.3.28" futures = "0.3.30"
grass = {version = "0.13.1", default-features = false} grass = "0.13.4"
gtk = "0.18.1"
insta = "1.7" insta = "1.7"
itertools = "0.12.1" itertools = "0.13.0"
jaq-core = "1.2.1" jaq-core = "1.5.1"
jaq-parse = "1.0.2" jaq-parse = "1.0.3"
jaq-std = {version = "1.2.1", features = ["bincode"]} jaq-std = "1.6.0"
jaq-interpret = "1.2.1" jaq-interpret = "1.5.0"
jaq-syn = "1.1.0" jaq-syn = "1.6.0"
lalrpop = { version = "0.20.0", features = ["unicode"] } lalrpop = { version = "0.21", features = ["unicode"] }
lalrpop-util = { version = "0.20.0", features = ["unicode"] } lalrpop-util = { version = "0.21", features = ["unicode"] }
libc = "0.2" libc = "0.2"
log = "0.4" log = "0.4"
maplit = "1" maplit = "1"
nix = "0.27.1" nix = "0.29.0"
notify = "6.1.1" notify = "6.1.1"
once_cell = "1.19" once_cell = "1.19"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
ref-cast = "1.0.22" ref-cast = "1.0.22"
regex = "1.10.3" regex = "1.10.5"
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
simple-signal = "1.1" simple-signal = "1.1"
@ -47,12 +55,13 @@ smart-default = "0.7.1"
static_assertions = "1.1.0" static_assertions = "1.1.0"
strsim = "0.11" strsim = "0.11"
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"] }
sysinfo = "0.30.5" sysinfo = "0.31.2"
thiserror = "1.0" thiserror = "1.0"
tokio-util = "0.7.8" tokio-util = "0.7.11"
tokio = { version = "1.36.0", features = ["full"] } tokio = { version = "1.39.2", features = ["full"] }
unescape = "0.1" unescape = "0.1"
wait-timeout = "0.2" wait-timeout = "0.2"
zbus = { version = "3.15.2", default-features = false, features = ["tokio"] }
[profile.dev] [profile.dev]
split-debuginfo = "unpacked" split-debuginfo = "unpacked"

View file

@ -11,10 +11,14 @@ Documentation **and instructions on how to install** can be found [here](https:/
Dharmx also wrote a nice, beginner friendly introductory guide for eww [here](https://dharmx.is-a.dev/eww-powermenu/). Dharmx also wrote a nice, beginner friendly introductory guide for eww [here](https://dharmx.is-a.dev/eww-powermenu/).
## Eww needs your opinion! ## Check out another cool project by me
I've hit a bit of a design roadblock for one of the bigger features that are in the works right now.
**Please read through https://github.com/elkowar/eww/discussions/453 and share your thoughts, ideas and opinions!** <img src="https://raw.githubusercontent.com/elkowar/yolk/refs/heads/main/.github/images/yolk_logo.svg" height="100" align="right"/>
I'm currently busy working [yolk](https://github.com/elkowar/yolk),
which is a dotfile management solution that supports a unique spin on templating: *templating without template files*.
To find out more, check out the [website and documentation](https://elkowar.github.io/yolk)!
## Examples ## Examples
@ -59,14 +63,26 @@ I've hit a bit of a design roadblock for one of the bigger features that are in
</div> </div>
* [Activate Linux by Nycta](https://github.com/Nycta-b424b3c7/eww_activate-linux)
<div align="left">
![Activate Linux](https://raw.githubusercontent.com/Nycta-b424b3c7/eww_activate-linux/refs/heads/master/activate-linux.png)
</div>
## Contribewwting ## Contribewwting
If you want to contribute anything, like adding new widgets, features, or subcommands (including sample configs), you should definitely do so. If you want to contribute anything, like adding new widgets, features, or subcommands (including sample configs), you should definitely do so.
### Steps ### Steps
1. Fork this repository 1. Fork this repository
2. Install dependencies 2. Install dependencies
3. Smash your head against the keyboard from frustration (coding is hard) 3. Smash your head against the keyboard from frustration (coding is hard)
4. Write down your changes in CHANGELOG.md 4. Write down your changes in CHANGELOG.md
5. Open a pull request once you're finished 5. Open a pull request once you're finished
## Widget
https://en.wikipedia.org/wiki/Wikipedia:Widget

View file

@ -1,6 +1,6 @@
[package] [package]
name = "eww" name = "eww"
version = "0.5.0" version = "0.6.0"
authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"] authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"]
description = "Widgets for everyone!" description = "Widgets for everyone!"
license = "MIT" license = "MIT"
@ -9,7 +9,6 @@ homepage = "https://github.com/elkowar/eww"
edition = "2021" edition = "2021"
[features] [features]
default = ["x11", "wayland"] default = ["x11", "wayland"]
x11 = ["gdkx11", "x11rb"] x11 = ["gdkx11", "x11rb"]
@ -21,24 +20,15 @@ eww_shared_util.workspace = true
yuck.workspace = true yuck.workspace = true
notifier_host.workspace = true notifier_host.workspace = true
gtk = "0.17.1" gtk-layer-shell = { version = "0.8.1", optional = true, features=["v0_6"] }
gdk = "0.17.1" gdkx11 = { version = "0.18", optional = true }
pango = "0.17.1" x11rb = { version = "0.13.1", features = ["randr"], optional = true }
glib = "0.17.8" gdk-sys = "0.18.0"
glib-macros = "0.17.8"
cairo-rs = "0.17"
cairo-sys-rs = "0.17"
gdk-pixbuf = "0.17"
gtk-layer-shell = { version = "0.6.1", optional = true }
gdkx11 = { version = "0.17", optional = true }
x11rb = { version = "0.11.1", features = ["randr"], optional = true }
zbus = { version = "3.7.0", default-features = false, features = ["tokio"] }
ordered-stream = "0.2.0" ordered-stream = "0.2.0"
grass.workspace = true
anyhow.workspace = true anyhow.workspace = true
bincode.workspace = true bincode.workspace = true
chrono.workspace = true chrono.workspace = true
@ -48,7 +38,7 @@ codespan-reporting.workspace = true
derive_more.workspace = true derive_more.workspace = true
extend.workspace = true extend.workspace = true
futures.workspace = true futures.workspace = true
grass = {workspace = true, default-features = false} gtk.workspace = true
itertools.workspace = true itertools.workspace = true
libc.workspace = true libc.workspace = true
log.workspace = true log.workspace = true
@ -61,11 +51,12 @@ regex.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
simple-signal.workspace = true simple-signal.workspace = true
sysinfo = { workspace = true, features = ["linux-netdevs"] } sysinfo = { workspace = true }
tokio-util.workspace = true tokio-util.workspace = true
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
unescape.workspace = true unescape.workspace = true
wait-timeout.workspace = true wait-timeout.workspace = true
zbus = { workspace = true, default-features = false, features = ["tokio"] }
[package.metadata.deb] [package.metadata.deb]
maintainer = "Penelope Gwen <support@pogmom.me>" maintainer = "Penelope Gwen <support@pogmom.me>"

View file

@ -1,5 +1,4 @@
use crate::{ use crate::{
config,
daemon_response::DaemonResponseSender, daemon_response::DaemonResponseSender,
display_backend::DisplayBackend, display_backend::DisplayBackend,
error_handling_ctx, error_handling_ctx,
@ -17,12 +16,14 @@ use codespan_reporting::files::Files;
use eww_shared_util::{Span, VarName}; use eww_shared_util::{Span, VarName};
use gdk::Monitor; use gdk::Monitor;
use glib::ObjectExt; use glib::ObjectExt;
use gtk::{gdk, glib};
use itertools::Itertools; use itertools::Itertools;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use simplexpr::{dynval::DynVal, SimplExpr}; use simplexpr::{dynval::DynVal, SimplExpr};
use std::{ use std::{
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
marker::PhantomData,
rc::Rc, rc::Rc,
}; };
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
@ -44,6 +45,7 @@ use yuck::{
pub enum DaemonCommand { pub enum DaemonCommand {
NoOp, NoOp,
UpdateVars(Vec<(VarName, DynVal)>), UpdateVars(Vec<(VarName, DynVal)>),
PollVars(Vec<VarName>),
ReloadConfigAndCss(DaemonResponseSender), ReloadConfigAndCss(DaemonResponseSender),
OpenInspector, OpenInspector,
OpenMany { OpenMany {
@ -66,6 +68,7 @@ pub enum DaemonCommand {
}, },
CloseWindows { CloseWindows {
windows: Vec<String>, windows: Vec<String>,
auto_reopen: bool,
sender: DaemonResponseSender, sender: DaemonResponseSender,
}, },
KillServer, KillServer,
@ -87,10 +90,6 @@ pub enum DaemonCommand {
/// An opened window. /// An opened window.
#[derive(Debug)] #[derive(Debug)]
pub struct EwwWindow { pub struct EwwWindow {
/// Every window has an id, uniquely identifying it.
/// If no specific ID was specified whilst starting the window,
/// this will be the same as the window name.
pub instance_id: String,
pub name: String, pub name: String,
pub scope_index: ScopeIndex, pub scope_index: ScopeIndex,
pub gtk_window: Window, pub gtk_window: Window,
@ -111,11 +110,13 @@ impl EwwWindow {
} }
} }
pub struct App<B> { pub struct App<B: DisplayBackend> {
pub display_backend: B,
pub scope_graph: Rc<RefCell<ScopeGraph>>, pub scope_graph: Rc<RefCell<ScopeGraph>>,
pub eww_config: config::EwwConfig, pub eww_config: config::EwwConfig,
/// Map of all currently open windows by their IDs /// Map of all currently open windows to their unique IDs
/// If no specific ID was specified whilst starting the window,
/// it will be the same as the window name.
/// Therefore, only one window of a given name can exist when not using IDs.
pub open_windows: HashMap<String, EwwWindow>, pub open_windows: HashMap<String, EwwWindow>,
pub instance_id_to_args: HashMap<String, WindowArguments>, pub instance_id_to_args: HashMap<String, WindowArguments>,
/// Window names that are supposed to be open, but failed. /// Window names that are supposed to be open, but failed.
@ -131,9 +132,10 @@ pub struct App<B> {
pub window_close_timer_abort_senders: HashMap<String, futures::channel::oneshot::Sender<()>>, pub window_close_timer_abort_senders: HashMap<String, futures::channel::oneshot::Sender<()>>,
pub paths: EwwPaths, pub paths: EwwPaths,
pub phantom: PhantomData<B>,
} }
impl<B> std::fmt::Debug for App<B> { impl<B: DisplayBackend> std::fmt::Debug for App<B> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("App") f.debug_struct("App")
.field("scope_graph", &*self.scope_graph.borrow()) .field("scope_graph", &*self.scope_graph.borrow())
@ -146,16 +148,34 @@ impl<B> std::fmt::Debug for App<B> {
} }
} }
/// Wait until the .model() is available for all monitors (or there is a timeout)
async fn wait_for_monitor_model() {
let display = gdk::Display::default().expect("could not get default display");
let start = std::time::Instant::now();
loop {
let all_monitors_set =
(0..display.n_monitors()).all(|i| display.monitor(i).and_then(|monitor| monitor.model()).is_some());
if all_monitors_set {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
if std::time::Instant::now() - start > Duration::from_millis(500) {
log::warn!("Timed out waiting for monitor model to be set");
break;
}
}
}
impl<B: DisplayBackend> App<B> { impl<B: DisplayBackend> App<B> {
/// Handle a [`DaemonCommand`] event, logging any errors that occur. /// Handle a [`DaemonCommand`] event, logging any errors that occur.
pub fn handle_command(&mut self, event: DaemonCommand) { pub async fn handle_command(&mut self, event: DaemonCommand) {
if let Err(err) = self.try_handle_command(event) { if let Err(err) = self.try_handle_command(event).await {
error_handling_ctx::print_error(err); error_handling_ctx::print_error(err);
} }
} }
/// Try to handle a [`DaemonCommand`] event. /// Try to handle a [`DaemonCommand`] event.
fn try_handle_command(&mut self, event: DaemonCommand) -> Result<()> { async fn try_handle_command(&mut self, event: DaemonCommand) -> Result<()> {
log::debug!("Handling event: {:?}", &event); log::debug!("Handling event: {:?}", &event);
match event { match event {
DaemonCommand::NoOp => {} DaemonCommand::NoOp => {}
@ -167,7 +187,16 @@ impl<B: DisplayBackend> App<B> {
self.update_global_variable(var_name, new_value); self.update_global_variable(var_name, new_value);
} }
} }
DaemonCommand::PollVars(names) => {
for var_name in names {
self.force_poll_variable(var_name);
}
}
DaemonCommand::ReloadConfigAndCss(sender) => { DaemonCommand::ReloadConfigAndCss(sender) => {
// Wait for all monitor models to be set. When a new monitor gets added, this
// might not immediately be the case. And if we were to wait inside the
// connect_monitor_added callback, model() never gets set. So instead we wait here.
wait_for_monitor_model().await;
let mut errors = Vec::new(); let mut errors = Vec::new();
let config_result = config::read_from_eww_paths(&self.paths); let config_result = config::read_from_eww_paths(&self.paths);
@ -194,7 +223,7 @@ impl<B: DisplayBackend> App<B> {
DaemonCommand::CloseAll => { DaemonCommand::CloseAll => {
log::info!("Received close command, closing all windows"); log::info!("Received close command, closing all windows");
for window_name in self.open_windows.keys().cloned().collect::<Vec<String>>() { for window_name in self.open_windows.keys().cloned().collect::<Vec<String>>() {
self.close_window(&window_name)?; self.close_window(&window_name, false)?;
} }
} }
DaemonCommand::OpenMany { windows, args, should_toggle, sender } => { DaemonCommand::OpenMany { windows, args, should_toggle, sender } => {
@ -203,7 +232,7 @@ impl<B: DisplayBackend> App<B> {
.map(|w| { .map(|w| {
let (config_name, id) = w; let (config_name, id) = w;
if should_toggle && self.open_windows.contains_key(id) { if should_toggle && self.open_windows.contains_key(id) {
self.close_window(id) self.close_window(id, false)
} else { } else {
log::debug!("Config: {}, id: {}", config_name, id); log::debug!("Config: {}, id: {}", config_name, id);
let window_args = args let window_args = args
@ -234,7 +263,7 @@ impl<B: DisplayBackend> App<B> {
let is_open = self.open_windows.contains_key(&instance_id); let is_open = self.open_windows.contains_key(&instance_id);
let result = if should_toggle && is_open { let result = if should_toggle && is_open {
self.close_window(&instance_id) self.close_window(&instance_id, false)
} else { } else {
self.open_window(&WindowArguments { self.open_window(&WindowArguments {
instance_id, instance_id,
@ -250,9 +279,10 @@ impl<B: DisplayBackend> App<B> {
sender.respond_with_result(result)?; sender.respond_with_result(result)?;
} }
DaemonCommand::CloseWindows { windows, sender } => { DaemonCommand::CloseWindows { windows, auto_reopen, sender } => {
let errors = windows.iter().map(|window| self.close_window(window)).filter_map(Result::err); let errors = windows.iter().map(|window| self.close_window(window, auto_reopen)).filter_map(Result::err);
sender.respond_with_error_list(errors)?; // Ignore sending errors, as the channel might already be closed
let _ = sender.respond_with_error_list(errors);
} }
DaemonCommand::PrintState { all, sender } => { DaemonCommand::PrintState { all, sender } => {
let scope_graph = self.scope_graph.borrow(); let scope_graph = self.scope_graph.borrow();
@ -336,8 +366,25 @@ impl<B: DisplayBackend> App<B> {
} }
} }
fn force_poll_variable(&mut self, name: VarName) {
match self.eww_config.get_script_var(&name) {
Err(err) => error_handling_ctx::print_error(err),
Ok(var) => {
if let ScriptVarDefinition::Poll(poll_var) = var {
log::debug!("force-polling var {}", &name);
match script_var_handler::run_poll_once(&poll_var) {
Err(err) => error_handling_ctx::print_error(err),
Ok(value) => self.update_global_variable(name, value),
}
} else {
error_handling_ctx::print_error(anyhow!("Script var '{}' is not polling", name))
}
}
}
}
/// Close a window and do all the required cleanups in the scope_graph and script_var_handler /// Close a window and do all the required cleanups in the scope_graph and script_var_handler
fn close_window(&mut self, instance_id: &str) -> Result<()> { fn close_window(&mut self, instance_id: &str, auto_reopen: bool) -> Result<()> {
if let Some(old_abort_send) = self.window_close_timer_abort_senders.remove(instance_id) { if let Some(old_abort_send) = self.window_close_timer_abort_senders.remove(instance_id) {
_ = old_abort_send.send(()); _ = old_abort_send.send(());
} }
@ -357,7 +404,17 @@ impl<B: DisplayBackend> App<B> {
self.script_var_handler.stop_for_variable(unused_var.clone()); self.script_var_handler.stop_for_variable(unused_var.clone());
} }
if auto_reopen {
self.failed_windows.insert(instance_id.to_string());
// There might be an alternative monitor available already, so try to re-open it immediately.
// This can happen for example when a monitor gets disconnected and another connected,
// and the connection event happens before the disconnect.
if let Some(window_arguments) = self.instance_id_to_args.get(instance_id) {
let _ = self.open_window(&window_arguments.clone());
}
} else {
self.instance_id_to_args.remove(instance_id); self.instance_id_to_args.remove(instance_id);
}
Ok(()) Ok(())
} }
@ -369,7 +426,7 @@ impl<B: DisplayBackend> App<B> {
// if an instance of this is already running, close it // if an instance of this is already running, close it
if self.open_windows.contains_key(instance_id) { if self.open_windows.contains_key(instance_id) {
self.close_window(instance_id)?; self.close_window(instance_id, false)?;
} }
self.instance_id_to_args.insert(instance_id.to_string(), window_args.clone()); self.instance_id_to_args.insert(instance_id.to_string(), window_args.clone());
@ -422,9 +479,17 @@ impl<B: DisplayBackend> App<B> {
move |_| { move |_| {
// we don't care about the actual error response from the daemon as this is mostly just a fallback. // we don't care about the actual error response from the daemon as this is mostly just a fallback.
// Generally, this should get disconnected before the gtk window gets destroyed. // Generally, this should get disconnected before the gtk window gets destroyed.
// It serves as a fallback for when the window is closed manually. // This callback is triggered in 2 cases:
// - When the monitor of this window gets disconnected
// - When the window is closed manually.
// We don't distinguish here and assume the window should be reopened once a monitor
// becomes available again
let (response_sender, _) = daemon_response::create_pair(); let (response_sender, _) = daemon_response::create_pair();
let command = DaemonCommand::CloseWindows { windows: vec![instance_id.clone()], sender: response_sender }; let command = DaemonCommand::CloseWindows {
windows: vec![instance_id.clone()],
auto_reopen: true,
sender: response_sender,
};
if let Err(err) = app_evt_sender.send(command) { if let Err(err) = app_evt_sender.send(command) {
log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err); log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err);
} }
@ -443,7 +508,7 @@ impl<B: DisplayBackend> App<B> {
tokio::select! { tokio::select! {
_ = glib::timeout_future(duration) => { _ = glib::timeout_future(duration) => {
let (response_sender, mut response_recv) = daemon_response::create_pair(); let (response_sender, mut response_recv) = daemon_response::create_pair();
let command = DaemonCommand::CloseWindows { windows: vec![instance_id.clone()], sender: response_sender }; let command = DaemonCommand::CloseWindows { windows: vec![instance_id.clone()], auto_reopen: false, sender: response_sender };
if let Err(err) = app_evt_sender.send(command) { if let Err(err) = app_evt_sender.send(command) {
log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err); log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err);
} }
@ -572,7 +637,6 @@ fn initialize_window<B: DisplayBackend>(
window.show_all(); window.show_all();
Ok(EwwWindow { Ok(EwwWindow {
instance_id: window_init.id.clone(),
name: window_init.name.clone(), name: window_init.name.clone(),
gtk_window: window, gtk_window: window,
scope_index: window_scope, scope_index: window_scope,
@ -626,6 +690,18 @@ fn get_gdk_monitor(identifier: Option<MonitorIdentifier>) -> Result<Monitor> {
Ok(monitor) Ok(monitor)
} }
/// Get the name of monitor plug for given monitor number
/// workaround gdk not providing this information on wayland in regular calls
/// gdk_screen_get_monitor_plug_name is deprecated but works fine for that case
fn get_monitor_plug_name(display: &gdk::Display, monitor_num: i32) -> Option<&str> {
unsafe {
use glib::translate::ToGlibPtr;
let plug_name_pointer = gdk_sys::gdk_screen_get_monitor_plug_name(display.default_screen().to_glib_none().0, monitor_num);
use std::ffi::CStr;
CStr::from_ptr(plug_name_pointer).to_str().ok()
}
}
/// Returns the [Monitor][gdk::Monitor] structure corresponding to the identifer. /// Returns the [Monitor][gdk::Monitor] structure corresponding to the identifer.
/// Outside of x11, only [MonitorIdentifier::Numeric] is supported /// Outside of x11, only [MonitorIdentifier::Numeric] is supported
pub fn get_monitor_from_display(display: &gdk::Display, identifier: &MonitorIdentifier) -> Option<gdk::Monitor> { pub fn get_monitor_from_display(display: &gdk::Display, identifier: &MonitorIdentifier) -> Option<gdk::Monitor> {
@ -643,7 +719,7 @@ pub fn get_monitor_from_display(display: &gdk::Display, identifier: &MonitorIden
MonitorIdentifier::Name(name) => { MonitorIdentifier::Name(name) => {
for m in 0..display.n_monitors() { for m in 0..display.n_monitors() {
if let Some(model) = display.monitor(m).and_then(|x| x.model()) { if let Some(model) = display.monitor(m).and_then(|x| x.model()) {
if model == *name { if model == *name || Some(name.as_str()) == get_monitor_plug_name(display, m) {
return display.monitor(m); return display.monitor(m);
} }
} }

View file

@ -30,11 +30,11 @@ macro_rules! define_builtin_vars {
} }
define_builtin_vars! { define_builtin_vars! {
// @desc EWW_TEMPS - Heat of the components in Celcius // @desc EWW_TEMPS - Heat of the components in degree Celsius
// @prop { <name>: temperature } // @prop { <name>: temperature }
"EWW_TEMPS" [2] => || Ok(DynVal::from(get_temperatures())), "EWW_TEMPS" [2] => || Ok(DynVal::from(get_temperatures())),
// @desc EWW_RAM - Information on ram and swap usage in kB. // @desc EWW_RAM - Information on ram and swap usage in bytes.
// @prop { total_mem, free_mem, total_swap, free_swap, available_mem, used_mem, used_mem_perc } // @prop { total_mem, free_mem, total_swap, free_swap, available_mem, used_mem, used_mem_perc }
"EWW_RAM" [2] => || Ok(DynVal::from(get_ram())), "EWW_RAM" [2] => || Ok(DynVal::from(get_ram())),
@ -42,7 +42,7 @@ define_builtin_vars! {
// @prop { <mount_point>: { name, total, free, used, used_perc } } // @prop { <mount_point>: { name, total, free, used, used_perc } }
"EWW_DISK" [2] => || Ok(DynVal::from(get_disks())), "EWW_DISK" [2] => || Ok(DynVal::from(get_disks())),
// @desc EWW_BATTERY - Battery capacity in procent of the main battery // @desc EWW_BATTERY - Battery capacity in percent of the main battery
// @prop { <name>: { capacity, status } } // @prop { <name>: { capacity, status } }
"EWW_BATTERY" [2] => || Ok(DynVal::from( "EWW_BATTERY" [2] => || Ok(DynVal::from(
match get_battery_capacity() { match get_battery_capacity() {

View file

@ -202,8 +202,54 @@ pub fn get_battery_capacity() -> Result<String> {
Ok(serde_json::to_string(&(Data { batteries, total_avg: (current / total) * 100_f64 })).unwrap()) Ok(serde_json::to_string(&(Data { batteries, total_avg: (current / total) * 100_f64 })).unwrap())
} }
#[cfg(any(target_os = "netbsd", target_os = "freebsd", target_os = "openbsd"))]
pub fn get_battery_capacity() -> Result<String> {
let batteries = String::from_utf8(
// I have only tested `apm` on FreeBSD, but it *should* work on all of the listed targets,
// based on what I can tell from their online man pages.
std::process::Command::new("apm")
.output()
.context("\nError while getting the battery values on bsd, with `apm`: ")?
.stdout,
)?;
// `apm` output should look something like this:
// $ apm
// ...
// Remaining battery life: 87%
// Remaining battery time: unknown
// Number of batteries: 1
// Battery 0
// Battery Status: charging
// Remaining battery life: 87%
// Remaining battery time: unknown
// ...
// last 4 lines are repeated for each battery.
// see also:
// https://www.freebsd.org/cgi/man.cgi?query=apm&manpath=FreeBSD+13.1-RELEASE+and+Ports
// https://man.openbsd.org/amd64/apm.8
// https://man.netbsd.org/apm.8
let mut json = String::from('{');
let re_total = regex!(r"(?m)^Remaining battery life: (\d+)%");
let re_single = regex!(r"(?sm)^Battery (\d+):.*?Status: (\w+).*?(\d+)%");
for bat in re_single.captures_iter(&batteries) {
json.push_str(&format!(
r#""BAT{}": {{ "status": "{}", "capacity": {} }}, "#,
bat.get(1).unwrap().as_str(),
bat.get(2).unwrap().as_str(),
bat.get(3).unwrap().as_str(),
))
}
json.push_str(&format!(r#""total_avg": {}}}"#, re_total.captures(&batteries).unwrap().get(1).unwrap().as_str()));
Ok(json)
}
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
#[cfg(not(target_os = "netbsd"))]
#[cfg(not(target_os = "freebsd"))]
#[cfg(not(target_os = "openbsd"))]
pub fn get_battery_capacity() -> Result<String> { pub fn get_battery_capacity() -> Result<String> {
Err(anyhow::anyhow!("Eww doesn't support your OS for getting the battery capacity")) Err(anyhow::anyhow!("Eww doesn't support your OS for getting the battery capacity"))
} }
@ -212,7 +258,6 @@ pub fn net() -> String {
let (ref mut last_refresh, ref mut networks) = &mut *NETWORKS.lock().unwrap(); let (ref mut last_refresh, ref mut networks) = &mut *NETWORKS.lock().unwrap();
networks.refresh_list(); networks.refresh_list();
networks.refresh();
let elapsed = last_refresh.next_refresh(); let elapsed = last_refresh.next_refresh();
networks networks

View file

@ -1,5 +1,7 @@
use crate::{widgets::window::Window, window_initiator::WindowInitiator}; use crate::{widgets::window::Window, window_initiator::WindowInitiator};
use gtk::gdk;
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
pub use platform_wayland::WaylandBackend; pub use platform_wayland::WaylandBackend;
@ -8,6 +10,7 @@ pub use platform_x11::{set_xprops, X11Backend};
pub trait DisplayBackend: Send + Sync + 'static { pub trait DisplayBackend: Send + Sync + 'static {
const IS_X11: bool; const IS_X11: bool;
const IS_WAYLAND: bool;
fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window>; fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window>;
} }
@ -16,6 +19,7 @@ pub struct NoBackend;
impl DisplayBackend for NoBackend { impl DisplayBackend for NoBackend {
const IS_X11: bool = false; const IS_X11: bool = false;
const IS_WAYLAND: bool = false;
fn initialize_window(_window_init: &WindowInitiator, _monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window> { fn initialize_window(_window_init: &WindowInitiator, _monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window> {
Some(Window::new(gtk::WindowType::Toplevel, x, y)) Some(Window::new(gtk::WindowType::Toplevel, x, y))
@ -24,26 +28,29 @@ impl DisplayBackend for NoBackend {
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
mod platform_wayland { mod platform_wayland {
use crate::{widgets::window::Window, window_initiator::WindowInitiator};
use gtk::prelude::*;
use yuck::config::{window_definition::WindowStacking, window_geometry::AnchorAlignment};
use super::DisplayBackend; use super::DisplayBackend;
use crate::{widgets::window::Window, window_initiator::WindowInitiator};
use gtk::gdk;
use gtk::prelude::*;
use gtk_layer_shell::{KeyboardMode, LayerShell};
use yuck::config::backend_window_options::WlWindowFocusable;
use yuck::config::{window_definition::WindowStacking, window_geometry::AnchorAlignment};
pub struct WaylandBackend; pub struct WaylandBackend;
impl DisplayBackend for WaylandBackend { impl DisplayBackend for WaylandBackend {
const IS_X11: bool = false; const IS_X11: bool = false;
const IS_WAYLAND: bool = true;
fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window> { fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window> {
let window = Window::new(gtk::WindowType::Toplevel, x, y); let window = Window::new(gtk::WindowType::Toplevel, x, y);
// Initialising a layer shell surface // Initialising a layer shell surface
gtk_layer_shell::init_for_window(&window); window.init_layer_shell();
// Sets the monitor where the surface is shown // Sets the monitor where the surface is shown
if let Some(ident) = window_init.monitor.clone() { if let Some(ident) = window_init.monitor.clone() {
let display = gdk::Display::default().expect("could not get default display"); let display = gdk::Display::default().expect("could not get default display");
if let Some(monitor) = crate::app::get_monitor_from_display(&display, &ident) { if let Some(monitor) = crate::app::get_monitor_from_display(&display, &ident) {
gtk_layer_shell::set_monitor(&window, &monitor); window.set_monitor(&monitor);
} else { } else {
return None; return None;
} }
@ -52,18 +59,22 @@ mod platform_wayland {
// Sets the layer where the layer shell surface will spawn // Sets the layer where the layer shell surface will spawn
match window_init.stacking { match window_init.stacking {
WindowStacking::Foreground => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Top), WindowStacking::Foreground => window.set_layer(gtk_layer_shell::Layer::Top),
WindowStacking::Background => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Background), WindowStacking::Background => window.set_layer(gtk_layer_shell::Layer::Background),
WindowStacking::Bottom => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Bottom), WindowStacking::Bottom => window.set_layer(gtk_layer_shell::Layer::Bottom),
WindowStacking::Overlay => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Overlay), WindowStacking::Overlay => window.set_layer(gtk_layer_shell::Layer::Overlay),
} }
if let Some(namespace) = &window_init.backend_options.wayland.namespace { if let Some(namespace) = &window_init.backend_options.wayland.namespace {
gtk_layer_shell::set_namespace(&window, namespace); window.set_namespace(namespace);
} }
// Sets the keyboard interactivity // Sets the keyboard interactivity
gtk_layer_shell::set_keyboard_interactivity(&window, window_init.backend_options.wayland.focusable); match window_init.backend_options.wayland.focusable {
WlWindowFocusable::None => window.set_keyboard_mode(KeyboardMode::None),
WlWindowFocusable::Exclusive => window.set_keyboard_mode(KeyboardMode::Exclusive),
WlWindowFocusable::OnDemand => window.set_keyboard_mode(KeyboardMode::OnDemand),
}
if let Some(geometry) = window_init.geometry { if let Some(geometry) = window_init.geometry {
// Positioning surface // Positioning surface
@ -83,27 +94,34 @@ mod platform_wayland {
AnchorAlignment::END => bottom = true, AnchorAlignment::END => bottom = true,
} }
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Left, left); window.set_anchor(gtk_layer_shell::Edge::Left, left);
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Right, right); window.set_anchor(gtk_layer_shell::Edge::Right, right);
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Top, top); window.set_anchor(gtk_layer_shell::Edge::Top, top);
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Bottom, bottom); window.set_anchor(gtk_layer_shell::Edge::Bottom, bottom);
let xoffset = geometry.offset.x.pixels_relative_to(monitor.width()); let xoffset = geometry.offset.x.pixels_relative_to(monitor.width());
let yoffset = geometry.offset.y.pixels_relative_to(monitor.height()); let yoffset = geometry.offset.y.pixels_relative_to(monitor.height());
if left { if left {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Left, xoffset); window.set_layer_shell_margin(gtk_layer_shell::Edge::Left, xoffset);
} else { } else {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Right, xoffset); window.set_layer_shell_margin(gtk_layer_shell::Edge::Right, xoffset);
} }
if bottom { if bottom {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Bottom, yoffset); window.set_layer_shell_margin(gtk_layer_shell::Edge::Bottom, yoffset);
} else { } else {
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Top, yoffset); window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, yoffset);
}
// https://github.com/elkowar/eww/issues/296
if window_init.backend_options.wayland.exclusive
&& geometry.anchor_point.x != AnchorAlignment::CENTER
&& geometry.anchor_point.y != AnchorAlignment::CENTER
{
log::warn!("When ':exclusive true' the anchor has to include 'center', otherwise exlcusive won't work")
} }
} }
if window_init.backend_options.wayland.exclusive { if window_init.backend_options.wayland.exclusive {
gtk_layer_shell::auto_exclusive_zone_enable(&window); window.auto_exclusive_zone_enable();
} }
Some(window) Some(window)
} }
@ -115,6 +133,7 @@ mod platform_x11 {
use crate::{widgets::window::Window, window_initiator::WindowInitiator}; use crate::{widgets::window::Window, window_initiator::WindowInitiator};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use gdk::Monitor; use gdk::Monitor;
use gtk::gdk;
use gtk::{self, prelude::*}; use gtk::{self, prelude::*};
use x11rb::protocol::xproto::ConnectionExt; use x11rb::protocol::xproto::ConnectionExt;
@ -134,6 +153,7 @@ mod platform_x11 {
pub struct X11Backend; pub struct X11Backend;
impl DisplayBackend for X11Backend { impl DisplayBackend for X11Backend {
const IS_X11: bool = true; const IS_X11: bool = true;
const IS_WAYLAND: bool = false;
fn initialize_window(window_init: &WindowInitiator, _monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window> { fn initialize_window(window_init: &WindowInitiator, _monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window> {
let window_type = let window_type =

View file

@ -31,9 +31,6 @@ pub fn print_error(err: anyhow::Error) {
} }
pub fn format_error(err: &anyhow::Error) -> String { 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)) anyhow_err_to_diagnostic(err).and_then(|diag| stringify_diagnostic(diag).ok()).unwrap_or_else(|| format!("{:?}", err))
} }

View file

@ -1,7 +1,7 @@
use derive_more::*; use derive_more::{Debug, *};
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Display)]
#[display(fmt = ".x*.y:.width*.height")] #[display(".x*.y:.width*.height")]
pub struct Rect { pub struct Rect {
pub x: i32, pub x: i32,
pub y: i32, pub y: i32,

View file

@ -53,28 +53,31 @@ fn main() {
return; return;
} }
let detected_wayland = detect_wayland();
#[allow(unused)] #[allow(unused)]
let use_wayland = opts.force_wayland || detect_wayland(); let use_wayland = opts.force_wayland || detected_wayland;
#[cfg(all(feature = "wayland", feature = "x11"))] #[cfg(all(feature = "wayland", feature = "x11"))]
let result = if use_wayland { let result = if use_wayland {
run(opts, eww_binary_name, display_backend::WaylandBackend) log::debug!("Running on wayland. force_wayland={}, detected_wayland={}", opts.force_wayland, detected_wayland);
run::<display_backend::WaylandBackend>(opts, eww_binary_name)
} else { } else {
run(opts, eww_binary_name, display_backend::X11Backend) log::debug!("Running on X11. force_wayland={}, detected_wayland={}", opts.force_wayland, detected_wayland);
run::<display_backend::X11Backend>(opts, eww_binary_name)
}; };
#[cfg(all(not(feature = "wayland"), feature = "x11"))] #[cfg(all(not(feature = "wayland"), feature = "x11"))]
let result = { let result = {
if use_wayland { if use_wayland {
log::warn!("Eww compiled without wayland support. falling back to X11, eventhough wayland was requested."); log::warn!("Eww compiled without wayland support. Falling back to X11, eventhough wayland was requested.");
} }
run(opts, eww_binary_name, display_backend::X11Backend) run::<display_backend::X11Backend>(opts, eww_binary_name)
}; };
#[cfg(all(feature = "wayland", not(feature = "x11")))] #[cfg(all(feature = "wayland", not(feature = "x11")))]
let result = run(opts, eww_binary_name, display_backend::WaylandBackend); let result = run::<display_backend::WaylandBackend>(opts, eww_binary_name);
#[cfg(not(any(feature = "wayland", feature = "x11")))] #[cfg(not(any(feature = "wayland", feature = "x11")))]
let result = run(opts, eww_binary_name, display_backend::NoBackend); let result = run::<display_backend::NoBackend>(opts, eww_binary_name);
if let Err(err) = result { if let Err(err) = result {
error_handling_ctx::print_error(err); error_handling_ctx::print_error(err);
@ -88,7 +91,7 @@ fn detect_wayland() -> bool {
session_type.contains("wayland") || (!wayland_display.is_empty() && !session_type.contains("x11")) session_type.contains("wayland") || (!wayland_display.is_empty() && !session_type.contains("x11"))
} }
fn run<B: DisplayBackend>(opts: opts::Opt, eww_binary_name: String, display_backend: B) -> Result<()> { fn run<B: DisplayBackend>(opts: opts::Opt, eww_binary_name: String) -> Result<()> {
let paths = opts let paths = opts
.config_path .config_path
.map(EwwPaths::from_config_dir) .map(EwwPaths::from_config_dir)
@ -128,7 +131,7 @@ fn run<B: DisplayBackend>(opts: opts::Opt, eww_binary_name: String, display_back
if !opts.show_logs { if !opts.show_logs {
println!("Run `{} logs` to see any errors while editing your configuration.", eww_binary_name); println!("Run `{} logs` to see any errors while editing your configuration.", eww_binary_name);
} }
let fork_result = server::initialize_server(paths.clone(), None, display_backend, !opts.no_daemonize)?; let fork_result = server::initialize_server::<B>(paths.clone(), None, !opts.no_daemonize)?;
opts.no_daemonize || fork_result == ForkResult::Parent opts.no_daemonize || fork_result == ForkResult::Parent
} }
@ -160,7 +163,7 @@ fn run<B: DisplayBackend>(opts: opts::Opt, eww_binary_name: String, display_back
let (command, response_recv) = action.into_daemon_command(); let (command, response_recv) = action.into_daemon_command();
// start the daemon and give it the command // start the daemon and give it the command
let fork_result = server::initialize_server(paths.clone(), Some(command), display_backend, true)?; let fork_result = server::initialize_server::<B>(paths.clone(), Some(command), true)?;
let is_parent = fork_result == ForkResult::Parent; let is_parent = fork_result == ForkResult::Parent;
if let (Some(recv), true) = (response_recv, is_parent) { if let (Some(recv), true) = (response_recv, is_parent) {
listen_for_daemon_response(recv); listen_for_daemon_response(recv);

View file

@ -98,6 +98,16 @@ pub enum ActionWithServer {
mappings: Vec<(VarName, DynVal)>, mappings: Vec<(VarName, DynVal)>,
}, },
/// Update a polling variable using its script.
///
/// This will force the variable to be updated even if its
/// automatic polling is disabled.
#[command(name = "poll")]
Poll {
/// Variables to be polled
names: Vec<VarName>,
},
/// Open the GTK debugger /// Open the GTK debugger
#[command(name = "inspector", alias = "debugger")] #[command(name = "inspector", alias = "debugger")]
OpenInspector, OpenInspector,
@ -254,6 +264,7 @@ impl ActionWithServer {
pub fn into_daemon_command(self) -> (app::DaemonCommand, Option<daemon_response::DaemonResponseReceiver>) { pub fn into_daemon_command(self) -> (app::DaemonCommand, Option<daemon_response::DaemonResponseReceiver>) {
let command = match self { let command = match self {
ActionWithServer::Update { mappings } => app::DaemonCommand::UpdateVars(mappings), ActionWithServer::Update { mappings } => app::DaemonCommand::UpdateVars(mappings),
ActionWithServer::Poll { names } => app::DaemonCommand::PollVars(names),
ActionWithServer::OpenInspector => app::DaemonCommand::OpenInspector, ActionWithServer::OpenInspector => app::DaemonCommand::OpenInspector,
ActionWithServer::KillServer => app::DaemonCommand::KillServer, ActionWithServer::KillServer => app::DaemonCommand::KillServer,
@ -281,7 +292,7 @@ impl ActionWithServer {
}) })
} }
ActionWithServer::CloseWindows { windows } => { ActionWithServer::CloseWindows { windows } => {
return with_response_channel(|sender| app::DaemonCommand::CloseWindows { windows, sender }); return with_response_channel(|sender| app::DaemonCommand::CloseWindows { windows, auto_reopen: false, sender });
} }
ActionWithServer::Reload => return with_response_channel(app::DaemonCommand::ReloadConfigAndCss), ActionWithServer::Reload => return with_response_channel(app::DaemonCommand::ReloadConfigAndCss),
ActionWithServer::ListWindows => return with_response_channel(app::DaemonCommand::ListWindows), ActionWithServer::ListWindows => return with_response_channel(app::DaemonCommand::ListWindows),

View file

@ -196,7 +196,7 @@ impl PollVarHandler {
} }
} }
fn run_poll_once(var: &PollScriptVar) -> Result<DynVal> { pub fn run_poll_once(var: &PollScriptVar) -> Result<DynVal> {
match &var.command { match &var.command {
VarSource::Shell(span, 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()))) script_var::run_command(command).map_err(|e| anyhow!(create_script_var_failed_warn(*span, &var.name, &e.to_string())))

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
app::{self, DaemonCommand}, app::{self, App, DaemonCommand},
config, daemon_response, config, daemon_response,
display_backend::DisplayBackend, display_backend::DisplayBackend,
error_handling_ctx, ipc_server, script_var_handler, error_handling_ctx, ipc_server, script_var_handler,
@ -12,6 +12,7 @@ use std::{
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
io::Write, io::Write,
marker::PhantomData,
os::unix::io::AsRawFd, os::unix::io::AsRawFd,
path::Path, path::Path,
rc::Rc, rc::Rc,
@ -22,7 +23,6 @@ use tokio::sync::mpsc::*;
pub fn initialize_server<B: DisplayBackend>( pub fn initialize_server<B: DisplayBackend>(
paths: EwwPaths, paths: EwwPaths,
action: Option<DaemonCommand>, action: Option<DaemonCommand>,
display_backend: B,
should_daemonize: bool, should_daemonize: bool,
) -> Result<ForkResult> { ) -> Result<ForkResult> {
let (ui_send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel(); let (ui_send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel();
@ -68,6 +68,9 @@ pub fn initialize_server<B: DisplayBackend>(
} }
}); });
if B::IS_WAYLAND {
std::env::set_var("GDK_BACKEND", "wayland")
}
gtk::init()?; gtk::init()?;
log::debug!("Initializing script var handler"); log::debug!("Initializing script var handler");
@ -75,8 +78,7 @@ pub fn initialize_server<B: DisplayBackend>(
let (scope_graph_evt_send, mut scope_graph_evt_recv) = tokio::sync::mpsc::unbounded_channel(); let (scope_graph_evt_send, mut scope_graph_evt_recv) = tokio::sync::mpsc::unbounded_channel();
let mut app = app::App { let mut app: App<B> = app::App {
display_backend,
scope_graph: Rc::new(RefCell::new(ScopeGraph::from_global_vars( scope_graph: Rc::new(RefCell::new(ScopeGraph::from_global_vars(
eww_config.generate_initial_state()?, eww_config.generate_initial_state()?,
scope_graph_evt_send, scope_graph_evt_send,
@ -90,9 +92,10 @@ pub fn initialize_server<B: DisplayBackend>(
app_evt_send: ui_send.clone(), app_evt_send: ui_send.clone(),
window_close_timer_abort_senders: HashMap::new(), window_close_timer_abort_senders: HashMap::new(),
paths, paths,
phantom: PhantomData,
}; };
if let Some(screen) = gdk::Screen::default() { if let Some(screen) = gtk::gdk::Screen::default() {
gtk::StyleContext::add_provider_for_screen(&screen, &app.css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); gtk::StyleContext::add_provider_for_screen(&screen, &app.css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
} }
@ -102,13 +105,15 @@ pub fn initialize_server<B: DisplayBackend>(
} }
} }
connect_monitor_added(ui_send.clone());
// initialize all the handlers and tasks running asyncronously // initialize all the handlers and tasks running asyncronously
let tokio_handle = init_async_part(app.paths.clone(), ui_send); let tokio_handle = init_async_part(app.paths.clone(), ui_send);
glib::MainContext::default().spawn_local(async move { gtk::glib::MainContext::default().spawn_local(async move {
// if an action was given to the daemon initially, execute it first. // if an action was given to the daemon initially, execute it first.
if let Some(action) = action { if let Some(action) = action {
app.handle_command(action); app.handle_command(action).await;
} }
loop { loop {
@ -117,7 +122,7 @@ pub fn initialize_server<B: DisplayBackend>(
app.scope_graph.borrow_mut().handle_scope_graph_event(scope_graph_evt); app.scope_graph.borrow_mut().handle_scope_graph_event(scope_graph_evt);
}, },
Some(ui_event) = ui_recv.recv() => { Some(ui_event) = ui_recv.recv() => {
app.handle_command(ui_event); app.handle_command(ui_event).await;
} }
else => break, else => break,
} }
@ -133,6 +138,29 @@ pub fn initialize_server<B: DisplayBackend>(
Ok(ForkResult::Child) Ok(ForkResult::Child)
} }
fn connect_monitor_added(ui_send: UnboundedSender<DaemonCommand>) {
let display = gtk::gdk::Display::default().expect("could not get default display");
display.connect_monitor_added({
move |_display: &gtk::gdk::Display, _monitor: &gtk::gdk::Monitor| {
log::info!("New monitor connected, reloading configuration");
let _ = reload_config_and_css(&ui_send);
}
});
}
fn reload_config_and_css(ui_send: &UnboundedSender<DaemonCommand>) -> Result<()> {
let (daemon_resp_sender, mut daemon_resp_response) = daemon_response::create_pair();
ui_send.send(DaemonCommand::ReloadConfigAndCss(daemon_resp_sender))?;
tokio::spawn(async move {
match daemon_resp_response.recv().await {
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"),
}
});
Ok(())
}
fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>) -> tokio::runtime::Handle { fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>) -> tokio::runtime::Handle {
let rt = tokio::runtime::Builder::new_multi_thread() let rt = tokio::runtime::Builder::new_multi_thread()
.thread_name("main-async-runtime") .thread_name("main-async-runtime")
@ -213,20 +241,12 @@ async fn run_filewatch<P: AsRef<Path>>(config_dir: P, evt_send: UnboundedSender<
debounce_done.store(true, Ordering::SeqCst); debounce_done.store(true, Ordering::SeqCst);
}); });
let (daemon_resp_sender, mut daemon_resp_response) = daemon_response::create_pair();
// without this sleep, reading the config file sometimes gives an empty file. // without this sleep, reading the config file sometimes gives an empty file.
// This is probably a result of editors not locking the file correctly, // This is probably a result of editors not locking the file correctly,
// and eww being too fast, thus reading the file while it's empty. // and eww being too fast, thus reading the file while it's empty.
// There should be some cleaner solution for this, but this will do for now. // There should be some cleaner solution for this, but this will do for now.
tokio::time::sleep(std::time::Duration::from_millis(50)).await; tokio::time::sleep(std::time::Duration::from_millis(50)).await;
evt_send.send(app::DaemonCommand::ReloadConfigAndCss(daemon_resp_sender))?; reload_config_and_css(&evt_send)?;
tokio::spawn(async move {
match daemon_resp_response.recv().await {
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"),
}
});
} }
}, },
else => break else => break

View file

@ -1,8 +1,8 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use codespan_reporting::diagnostic::Severity; use codespan_reporting::diagnostic::Severity;
use eww_shared_util::{AttrName, Spanned}; use eww_shared_util::{AttrName, Spanned};
use gdk::prelude::Cast;
use gtk::{ use gtk::{
gdk::prelude::Cast,
prelude::{BoxExt, ContainerExt, WidgetExt}, prelude::{BoxExt, ContainerExt, WidgetExt},
Orientation, Orientation,
}; };
@ -306,13 +306,14 @@ fn build_children_special_widget(
.children .children
.get(nth_value as usize) .get(nth_value as usize)
.with_context(|| format!("No child at index {}", nth_value))?; .with_context(|| format!("No child at index {}", nth_value))?;
let new_child_widget = build_gtk_widget( let scope = tree.register_new_scope(
tree, format!("child {nth_value}"),
widget_defs.clone(), Some(custom_widget_invocation.scope),
custom_widget_invocation.scope, calling_scope,
nth_child_widget_use.clone(), HashMap::new(),
None,
)?; )?;
let new_child_widget =
build_gtk_widget(tree, widget_defs.clone(), scope, nth_child_widget_use.clone(), None)?;
child_container.children().iter().for_each(|f| child_container.remove(f)); child_container.children().iter().for_each(|f| child_container.remove(f));
child_container.set_child(Some(&new_child_widget)); child_container.set_child(Some(&new_child_widget));
new_child_widget.show(); new_child_widget.show();
@ -323,7 +324,13 @@ fn build_children_special_widget(
)?; )?;
} else { } else {
for child in &custom_widget_invocation.children { for child in &custom_widget_invocation.children {
let child_widget = build_gtk_widget(tree, widget_defs.clone(), custom_widget_invocation.scope, child.clone(), None)?; let scope = tree.register_new_scope(
String::from("child"),
Some(custom_widget_invocation.scope),
calling_scope,
HashMap::new(),
)?;
let child_widget = build_gtk_widget(tree, widget_defs.clone(), scope, child.clone(), None)?;
gtk_container.add(&child_widget); gtk_container.add(&child_widget);
} }
} }

View file

@ -1,7 +1,6 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use glib::{object_subclass, prelude::*, wrapper}; use gtk::glib::{self, object_subclass, prelude::*, wrapper, Properties};
use glib_macros::Properties; use gtk::{cairo, gdk, prelude::*, subclass::prelude::*};
use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell; use std::cell::RefCell;
use crate::error_handling_ctx; use crate::error_handling_ctx;
@ -154,7 +153,7 @@ impl WidgetImpl for CircProgPriv {
self.preferred_height() self.preferred_height()
} }
fn draw(&self, cr: &cairo::Context) -> Inhibit { fn draw(&self, cr: &cairo::Context) -> glib::Propagation {
let res: Result<()> = (|| { let res: Result<()> = (|| {
let value = *self.value.borrow(); let value = *self.value.borrow();
let start_at = *self.start_at.borrow(); let start_at = *self.start_at.borrow();
@ -226,7 +225,7 @@ impl WidgetImpl for CircProgPriv {
error_handling_ctx::print_error(error) error_handling_ctx::print_error(error)
}; };
gtk::Inhibit(false) glib::Propagation::Proceed
} }
} }

View file

@ -2,9 +2,8 @@ use std::{cell::RefCell, collections::VecDeque};
// https://www.figuiere.net/technotes/notes/tn002/ // https://www.figuiere.net/technotes/notes/tn002/
// https://github.com/gtk-rs/examples/blob/master/src/bin/listbox_model.rs // https://github.com/gtk-rs/examples/blob/master/src/bin/listbox_model.rs
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use glib::{object_subclass, wrapper}; use gtk::glib::{self, object_subclass, wrapper, Properties};
use glib_macros::Properties; use gtk::{cairo, gdk, prelude::*, subclass::prelude::*};
use gtk::{prelude::*, subclass::prelude::*};
use crate::error_handling_ctx; use crate::error_handling_ctx;
@ -39,6 +38,13 @@ pub struct GraphPriv {
#[property(get, set, nick = "Time Range", blurb = "The Time Range", minimum = 0u64, maximum = u64::MAX, default = 10u64)] #[property(get, set, nick = "Time Range", blurb = "The Time Range", minimum = 0u64, maximum = u64::MAX, default = 10u64)]
time_range: RefCell<u64>, time_range: RefCell<u64>,
#[property(get, set, nick = "Flip X", blurb = "Flip the x axis", default = true)]
flip_x: RefCell<bool>,
#[property(get, set, nick = "Flip Y", blurb = "Flip the y axis", default = true)]
flip_y: RefCell<bool>,
#[property(get, set, nick = "Vertical", blurb = "Exchange the x and y axes", default = false)]
vertical: RefCell<bool>,
history: RefCell<VecDeque<(std::time::Instant, f64)>>, history: RefCell<VecDeque<(std::time::Instant, f64)>>,
extra_point: RefCell<Option<(std::time::Instant, f64)>>, extra_point: RefCell<Option<(std::time::Instant, f64)>>,
last_updated_at: RefCell<std::time::Instant>, last_updated_at: RefCell<std::time::Instant>,
@ -54,6 +60,9 @@ impl Default for GraphPriv {
max: RefCell::new(100.0), max: RefCell::new(100.0),
dynamic: RefCell::new(true), dynamic: RefCell::new(true),
time_range: RefCell::new(10), time_range: RefCell::new(10),
flip_x: RefCell::new(true),
flip_y: RefCell::new(true),
vertical: RefCell::new(false),
history: RefCell::new(VecDeque::new()), history: RefCell::new(VecDeque::new()),
extra_point: RefCell::new(None), extra_point: RefCell::new(None),
last_updated_at: RefCell::new(std::time::Instant::now()), last_updated_at: RefCell::new(std::time::Instant::now()),
@ -78,6 +87,16 @@ impl GraphPriv {
} }
history.push_back(v); history.push_back(v);
} }
/**
* Receives normalized (0-1) coordinates `x` and `y` and convert them to the
* point on the widget.
*/
fn value_to_point(&self, width: f64, height: f64, x: f64, y: f64) -> (f64, f64) {
let x = if *self.flip_x.borrow() { 1.0 - x } else { x };
let y = if *self.flip_y.borrow() { 1.0 - y } else { y };
let (x, y) = if *self.vertical.borrow() { (y, x) } else { (x, y) };
(width * x, height * y)
}
} }
impl ObjectImpl for GraphPriv { impl ObjectImpl for GraphPriv {
@ -111,6 +130,15 @@ impl ObjectImpl for GraphPriv {
"line-style" => { "line-style" => {
self.line_style.replace(value.get().unwrap()); self.line_style.replace(value.get().unwrap());
} }
"flip-x" => {
self.flip_x.replace(value.get().unwrap());
}
"flip-y" => {
self.flip_y.replace(value.get().unwrap());
}
"vertical" => {
self.vertical.replace(value.get().unwrap());
}
x => panic!("Tried to set inexistant property of Graph: {}", x,), x => panic!("Tried to set inexistant property of Graph: {}", x,),
} }
} }
@ -170,7 +198,7 @@ impl WidgetImpl for GraphPriv {
(width, width) (width, width)
} }
fn draw(&self, cr: &cairo::Context) -> Inhibit { fn draw(&self, cr: &cairo::Context) -> glib::Propagation {
let res: Result<()> = (|| { let res: Result<()> = (|| {
let history = &*self.history.borrow(); let history = &*self.history.borrow();
let extra_point = *self.extra_point.borrow(); let extra_point = *self.extra_point.borrow();
@ -215,18 +243,15 @@ impl WidgetImpl for GraphPriv {
.iter() .iter()
.map(|(instant, value)| { .map(|(instant, value)| {
let t = last_updated_at.duration_since(*instant).as_millis() as f64; let t = last_updated_at.duration_since(*instant).as_millis() as f64;
let x = width * (1.0 - (t / time_range)); self.value_to_point(width, height, t / time_range, (value - min) / value_range)
let y = height * (1.0 - ((value - min) / value_range));
(x, y)
}) })
.collect::<VecDeque<(f64, f64)>>(); .collect::<VecDeque<(f64, f64)>>();
// Aad an extra point outside of the graph to extend the line to the left // Aad an extra point outside of the graph to extend the line to the left
if let Some((instant, value)) = extra_point { if let Some((instant, value)) = extra_point {
let t = last_updated_at.duration_since(instant).as_millis() as f64; let t = last_updated_at.duration_since(instant).as_millis() as f64;
let x = -width * ((t - time_range) / time_range); let (x, y) = self.value_to_point(width, height, (t - time_range) / time_range, (value - min) / value_range);
let y = height * (1.0 - ((value - min) / value_range)); points.push_front(if *self.vertical.borrow() { (x, -y) } else { (-x, y) });
points.push_front((x, y));
} }
points points
}; };
@ -276,7 +301,7 @@ impl WidgetImpl for GraphPriv {
error_handling_ctx::print_error(error) error_handling_ctx::print_error(error)
}; };
gtk::Inhibit(false) glib::Propagation::Proceed
} }
} }

View file

@ -1,7 +1,11 @@
use crate::widgets::window::Window; use crate::widgets::window::Window;
use futures::StreamExt; use futures::StreamExt;
use gtk::{cairo::Surface, gdk::ffi::gdk_cairo_surface_create_from_pixbuf, prelude::*}; use gtk::{
use notifier_host; cairo::Surface,
gdk::{self, ffi::gdk_cairo_surface_create_from_pixbuf, NotifyType},
glib,
prelude::*,
};
use std::{cell::RefCell, future::Future, rc::Rc}; use std::{cell::RefCell, future::Future, rc::Rc};
// DBus state shared between systray instances, to avoid creating too many connections etc. // DBus state shared between systray instances, to avoid creating too many connections etc.
@ -105,6 +109,7 @@ impl notifier_host::Host for Tray {
fn remove_item(&mut self, id: &str) { fn remove_item(&mut self, id: &str) {
if let Some(item) = self.items.get(id) { if let Some(item) = self.items.get(id) {
self.container.remove(&item.widget); self.container.remove(&item.widget);
self.items.remove(id);
} else { } else {
log::warn!("Tried to remove nonexistent item {:?} from systray", id); log::warn!("Tried to remove nonexistent item {:?} from systray", id);
} }
@ -130,11 +135,27 @@ impl Drop for Item {
impl Item { impl Item {
fn new(id: String, item: notifier_host::Item, icon_size: tokio::sync::watch::Receiver<i32>) -> Self { fn new(id: String, item: notifier_host::Item, icon_size: tokio::sync::watch::Receiver<i32>) -> Self {
let widget = gtk::EventBox::new(); let gtk_widget = gtk::EventBox::new();
let out_widget = widget.clone(); // copy so we can return it
// Support :hover selector
gtk_widget.connect_enter_notify_event(|gtk_widget, evt| {
if evt.detail() != NotifyType::Inferior {
gtk_widget.clone().set_state_flags(gtk::StateFlags::PRELIGHT, false);
}
glib::Propagation::Proceed
});
gtk_widget.connect_leave_notify_event(|gtk_widget, evt| {
if evt.detail() != NotifyType::Inferior {
gtk_widget.clone().unset_state_flags(gtk::StateFlags::PRELIGHT);
}
glib::Propagation::Proceed
});
let out_widget = gtk_widget.clone(); // copy so we can return it
let task = glib::MainContext::default().spawn_local(async move { let task = glib::MainContext::default().spawn_local(async move {
if let Err(e) = Item::maintain(widget.clone(), item, icon_size).await { if let Err(e) = Item::maintain(gtk_widget.clone(), item, icon_size).await {
log::error!("error for systray item {}: {}", id, e); log::error!("error for systray item {}: {}", id, e);
} }
}); });
@ -213,7 +234,7 @@ impl Item {
if let Err(result) = result { if let Err(result) = result {
log::error!("failed to handle mouse click {}: {}", evt.button(), result); log::error!("failed to handle mouse click {}: {}", evt.button(), result);
} }
gtk::Inhibit(true) glib::Propagation::Stop
})); }));
// updates // updates

View file

@ -1,6 +1,5 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use glib::{object_subclass, wrapper}; use gtk::glib::{self, object_subclass, wrapper, Properties};
use glib_macros::Properties;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, str::FromStr}; use std::{cell::RefCell, str::FromStr};
use yuck::value::NumWithUnit; use yuck::value::NumWithUnit;
@ -18,6 +17,12 @@ pub struct TransformPriv {
#[property(get, set, nick = "Rotate", blurb = "The Rotation", minimum = f64::MIN, maximum = f64::MAX, default = 0f64)] #[property(get, set, nick = "Rotate", blurb = "The Rotation", minimum = f64::MIN, maximum = f64::MAX, default = 0f64)]
rotate: RefCell<f64>, rotate: RefCell<f64>,
#[property(get, set, nick = "Transform-Origin X", blurb = "X coordinate (%/px) for the Transform-Origin", default = None)]
transform_origin_x: RefCell<Option<String>>,
#[property(get, set, nick = "Transform-Origin Y", blurb = "Y coordinate (%/px) for the Transform-Origin", default = None)]
transform_origin_y: RefCell<Option<String>>,
#[property(get, set, nick = "Translate x", blurb = "The X Translation", default = None)] #[property(get, set, nick = "Translate x", blurb = "The X Translation", default = None)]
translate_x: RefCell<Option<String>>, translate_x: RefCell<Option<String>>,
@ -38,6 +43,8 @@ impl Default for TransformPriv {
fn default() -> Self { fn default() -> Self {
TransformPriv { TransformPriv {
rotate: RefCell::new(0.0), rotate: RefCell::new(0.0),
transform_origin_x: RefCell::new(None),
transform_origin_y: RefCell::new(None),
translate_x: RefCell::new(None), translate_x: RefCell::new(None),
translate_y: RefCell::new(None), translate_y: RefCell::new(None),
scale_x: RefCell::new(None), scale_x: RefCell::new(None),
@ -58,6 +65,14 @@ impl ObjectImpl for TransformPriv {
self.rotate.replace(value.get().unwrap()); self.rotate.replace(value.get().unwrap());
self.obj().queue_draw(); // Queue a draw call with the updated value self.obj().queue_draw(); // Queue a draw call with the updated value
} }
"transform-origin-x" => {
self.transform_origin_x.replace(value.get().unwrap());
self.obj().queue_draw(); // Queue a draw call with the updated value
}
"transform-origin-y" => {
self.transform_origin_y.replace(value.get().unwrap());
self.obj().queue_draw(); // Queue a draw call with the updated value
}
"translate-x" => { "translate-x" => {
self.translate_x.replace(value.get().unwrap()); self.translate_x.replace(value.get().unwrap());
self.obj().queue_draw(); // Queue a draw call with the updated value self.obj().queue_draw(); // Queue a draw call with the updated value
@ -121,7 +136,7 @@ impl ContainerImpl for TransformPriv {
impl BinImpl for TransformPriv {} impl BinImpl for TransformPriv {}
impl WidgetImpl for TransformPriv { impl WidgetImpl for TransformPriv {
fn draw(&self, cr: &cairo::Context) -> Inhibit { fn draw(&self, cr: &gtk::cairo::Context) -> glib::Propagation {
let res: Result<()> = (|| { let res: Result<()> = (|| {
let rotate = *self.rotate.borrow(); let rotate = *self.rotate.borrow();
let total_width = self.obj().allocated_width() as f64; let total_width = self.obj().allocated_width() as f64;
@ -129,6 +144,15 @@ impl WidgetImpl for TransformPriv {
cr.save()?; cr.save()?;
let transform_origin_x = match &*self.transform_origin_x.borrow() {
Some(rcx) => NumWithUnit::from_str(rcx)?.pixels_relative_to(total_width as i32) as f64,
None => 0.0,
};
let transform_origin_y = match &*self.transform_origin_y.borrow() {
Some(rcy) => NumWithUnit::from_str(rcy)?.pixels_relative_to(total_height as i32) as f64,
None => 0.0,
};
let translate_x = match &*self.translate_x.borrow() { let translate_x = match &*self.translate_x.borrow() {
Some(tx) => NumWithUnit::from_str(tx)?.pixels_relative_to(total_width as i32) as f64, Some(tx) => NumWithUnit::from_str(tx)?.pixels_relative_to(total_width as i32) as f64,
None => 0.0, None => 0.0,
@ -149,9 +173,10 @@ impl WidgetImpl for TransformPriv {
None => 1.0, None => 1.0,
}; };
cr.scale(scale_x, scale_y); cr.translate(transform_origin_x, transform_origin_y);
cr.rotate(perc_to_rad(rotate)); cr.rotate(perc_to_rad(rotate));
cr.translate(translate_x, translate_y); cr.translate(translate_x - transform_origin_x, translate_y - transform_origin_y);
cr.scale(scale_x, scale_y);
// Children widget // Children widget
if let Some(child) = &*self.content.borrow() { if let Some(child) = &*self.content.borrow() {
@ -166,7 +191,7 @@ impl WidgetImpl for TransformPriv {
error_handling_ctx::print_error(error) error_handling_ctx::print_error(error)
}; };
gtk::Inhibit(false) glib::Propagation::Proceed
} }
} }

View file

@ -8,10 +8,11 @@ use crate::{
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use codespan_reporting::diagnostic::Severity; use codespan_reporting::diagnostic::Severity;
use eww_shared_util::Spanned; use eww_shared_util::Spanned;
use gdk::{ModifierType, NotifyType};
use gdk::{ModifierType, NotifyType};
use glib::translate::FromGlib; use glib::translate::FromGlib;
use gtk::{self, glib, prelude::*, DestDefaults, TargetEntry, TargetList}; use gtk::{self, glib, prelude::*, DestDefaults, TargetEntry, TargetList};
use gtk::{gdk, pango};
use itertools::Itertools; use itertools::Itertools;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -37,16 +38,16 @@ use yuck::{
/// thus not connecting a new handler unless the condition is met. /// thus not connecting a new handler unless the condition is met.
macro_rules! connect_signal_handler { macro_rules! connect_signal_handler {
($widget:ident, if $cond:expr, $connect_expr:expr) => {{ ($widget:ident, if $cond:expr, $connect_expr:expr) => {{
const KEY:&str = std::concat!("signal-handler:", std::line!());
unsafe { unsafe {
let key = ::std::concat!("signal-handler:", ::std::line!()); let old = $widget.data::<gtk::glib::SignalHandlerId>(KEY);
let old = $widget.data::<gtk::glib::SignalHandlerId>(key);
if let Some(old) = old { if let Some(old) = old {
let a = old.as_ref().as_raw(); let a = old.as_ref().as_raw();
$widget.disconnect(gtk::glib::SignalHandlerId::from_glib(a)); $widget.disconnect(gtk::glib::SignalHandlerId::from_glib(a));
} }
$widget.set_data::<gtk::glib::SignalHandlerId>(key, $connect_expr); $widget.set_data::<gtk::glib::SignalHandlerId>(KEY, $connect_expr);
} }
}}; }};
($widget:ident, $connect_expr:expr) => {{ ($widget:ident, $connect_expr:expr) => {{
@ -208,10 +209,10 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Wi
prop(visible: as_bool = true) { prop(visible: as_bool = true) {
if visible { gtk_widget.show(); } else { gtk_widget.hide(); } if visible { gtk_widget.show(); } else { gtk_widget.hide(); }
}, },
// @prop style - inline css style applied to the widget // @prop style - inline scss style applied to the widget
prop(style: as_string) { prop(style: as_string) {
gtk_widget.reset_style(); gtk_widget.reset_style();
css_provider.load_from_data(format!("* {{ {} }}", style).as_bytes())?; css_provider.load_from_data(grass::from_string(format!("* {{ {} }}", style), &grass::Options::default())?.as_bytes())?;
gtk_widget.style_context().add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION) gtk_widget.style_context().add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION)
}, },
// @prop css - scss code applied to the widget, i.e.: `button {color: red;}` // @prop css - scss code applied to the widget, i.e.: `button {color: red;}`
@ -232,11 +233,11 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Ran
let is_being_dragged = Rc::new(RefCell::new(false)); let is_being_dragged = Rc::new(RefCell::new(false));
gtk_widget.connect_button_press_event(glib::clone!(@strong is_being_dragged => move |_, _| { gtk_widget.connect_button_press_event(glib::clone!(@strong is_being_dragged => move |_, _| {
*is_being_dragged.borrow_mut() = true; *is_being_dragged.borrow_mut() = true;
gtk::Inhibit(false) glib::Propagation::Proceed
})); }));
gtk_widget.connect_button_release_event(glib::clone!(@strong is_being_dragged => move |_, _| { gtk_widget.connect_button_release_event(glib::clone!(@strong is_being_dragged => move |_, _| {
*is_being_dragged.borrow_mut() = false; *is_being_dragged.borrow_mut() = false;
gtk::Inhibit(false) glib::Propagation::Proceed
})); }));
// We keep track of the last value that has been set via gtk_widget.set_value (by a change in the value property). // We keep track of the last value that has been set via gtk_widget.set_value (by a change in the value property).
@ -311,15 +312,50 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result<gtk::ComboBoxText
const WIDGET_NAME_EXPANDER: &str = "expander"; const WIDGET_NAME_EXPANDER: &str = "expander";
/// @widget expander /// @widget expander
/// @desc A widget that can expand and collapse, showing/hiding it's children. /// @desc A widget that can expand and collapse, showing/hiding it's children. Should contain
/// exactly one child.
fn build_gtk_expander(bargs: &mut BuilderArgs) -> Result<gtk::Expander> { fn build_gtk_expander(bargs: &mut BuilderArgs) -> Result<gtk::Expander> {
let gtk_widget = gtk::Expander::new(None); let gtk_widget = gtk::Expander::new(None);
match bargs.widget_use.children.len().cmp(&1) {
Ordering::Less => {
return Err(DiagError(gen_diagnostic!("expander must contain exactly one element", bargs.widget_use.span)).into());
}
Ordering::Greater => {
let (_, additional_children) = bargs.widget_use.children.split_at(1);
// we know that there is more than one child, so unwrapping on first and last here is fine.
let first_span = additional_children.first().unwrap().span();
let last_span = additional_children.last().unwrap().span();
return Err(DiagError(gen_diagnostic!(
"expander must contain exactly one element, but got more",
first_span.to(last_span)
))
.into());
}
Ordering::Equal => {
let mut children = bargs.widget_use.children.iter().map(|child| {
build_gtk_widget(
bargs.scope_graph,
bargs.widget_defs.clone(),
bargs.calling_scope,
child.clone(),
bargs.custom_widget_invocation.clone(),
)
});
// we have exactly one child, we can unwrap
let child = children.next().unwrap()?;
gtk_widget.add(&child);
child.show();
}
}
def_widget!(bargs, _g, gtk_widget, { def_widget!(bargs, _g, gtk_widget, {
// @prop name - name of the expander // @prop name - name of the expander
prop(name: as_string) { gtk_widget.set_label(Some(&name)); }, prop(name: as_string) { gtk_widget.set_label(Some(&name)); },
// @prop expanded - sets if the tree is expanded // @prop expanded - sets if the tree is expanded
prop(expanded: as_bool) { gtk_widget.set_expanded(expanded); } prop(expanded: as_bool) { gtk_widget.set_expanded(expanded); }
}); });
Ok(gtk_widget) Ok(gtk_widget)
} }
@ -423,6 +459,9 @@ fn build_gtk_scale(bargs: &mut BuilderArgs) -> Result<gtk::Scale> {
// @prop draw-value - draw the value of the property // @prop draw-value - draw the value of the property
prop(draw_value: as_bool = false) { gtk_widget.set_draw_value(draw_value) }, prop(draw_value: as_bool = false) { gtk_widget.set_draw_value(draw_value) },
// @prop value-pos - position of the drawn value. possible values: $position
prop(value_pos: as_string) { gtk_widget.set_value_pos(parse_position_type(&value_pos)?) },
// @prop round-digits - Sets the number of decimals to round the value to when it changes // @prop round-digits - Sets the number of decimals to round the value to when it changes
prop(round_digits: as_i32 = 0) { gtk_widget.set_round_digits(round_digits) } prop(round_digits: as_i32 = 0) { gtk_widget.set_round_digits(round_digits) }
@ -459,7 +498,6 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result<gtk::Entry> {
prop(value: as_string) { prop(value: as_string) {
gtk_widget.set_text(&value); gtk_widget.set_text(&value);
}, },
// @prop onchange - Command to run when the text changes. The placeholder `{}` will be replaced by the value // @prop onchange - Command to run when the text changes. The placeholder `{}` will be replaced by the value
// @prop timeout - timeout of the command. Default: "200ms" // @prop timeout - timeout of the command. Default: "200ms"
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
@ -484,7 +522,7 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result<gtk::Entry> {
const WIDGET_NAME_BUTTON: &str = "button"; const WIDGET_NAME_BUTTON: &str = "button";
/// @widget button /// @widget button
/// @desc A button /// @desc A button containing any widget as it's child. Events are triggered on release.
fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> { fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> {
let gtk_widget = gtk::Button::new(); let gtk_widget = gtk::Button::new();
@ -492,25 +530,42 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> {
prop( prop(
// @prop timeout - timeout of the command. Default: "200ms" // @prop timeout - timeout of the command. Default: "200ms"
timeout: as_duration = Duration::from_millis(200), timeout: as_duration = Duration::from_millis(200),
// @prop onclick - a command that get's run when the button is clicked // @prop onclick - command to run when the button is activated either by leftclicking or keyboard
onclick: as_string = "", onclick: as_string = "",
// @prop onmiddleclick - a command that get's run when the button is middleclicked // @prop onmiddleclick - command to run when the button is middleclicked
onmiddleclick: as_string = "", onmiddleclick: as_string = "",
// @prop onrightclick - a command that get's run when the button is rightclicked // @prop onrightclick - command to run when the button is rightclicked
onrightclick: as_string = "" onrightclick: as_string = ""
) { ) {
gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); // animate button upon right-/middleclick (if gtk theme supports it)
connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { // since we do this, we can't use `connect_clicked` as that would always run `onclick` as well
connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |button, _| {
button.emit_activate();
glib::Propagation::Proceed
}));
let onclick_ = onclick.clone();
// mouse click events
connect_signal_handler!(gtk_widget, gtk_widget.connect_button_release_event(move |_, evt| {
match evt.button() { match evt.button() {
1 => run_command(timeout, &onclick, &[] as &[&str]), 1 => run_command(timeout, &onclick, &[] as &[&str]),
2 => run_command(timeout, &onmiddleclick, &[] as &[&str]), 2 => run_command(timeout, &onmiddleclick, &[] as &[&str]),
3 => run_command(timeout, &onrightclick, &[] as &[&str]), 3 => run_command(timeout, &onrightclick, &[] as &[&str]),
_ => {}, _ => {},
} }
gtk::Inhibit(false) glib::Propagation::Proceed
}));
// keyboard events
connect_signal_handler!(gtk_widget, gtk_widget.connect_key_release_event(move |_, evt| {
match evt.scancode() {
// return
36 => run_command(timeout, &onclick_, &[] as &[&str]),
// space
65 => run_command(timeout, &onclick_, &[] as &[&str]),
_ => {},
}
glib::Propagation::Proceed
})); }));
} }
}); });
Ok(gtk_widget) Ok(gtk_widget)
} }
@ -536,12 +591,35 @@ fn build_gtk_image(bargs: &mut BuilderArgs) -> Result<gtk::Image> {
// @prop path - path to the image file // @prop path - path to the image file
// @prop image-width - width of the image // @prop image-width - width of the image
// @prop image-height - height of the image // @prop image-height - height of the image
prop(path: as_string, image_width: as_i32 = -1, image_height: as_i32 = -1) { // @prop preserve-aspect-ratio - whether to keep the aspect ratio when resizing an image. Default: true, false doesn't work for all image types
// @prop fill-svg - sets the color of svg images
prop(path: as_string, image_width: as_i32 = -1, image_height: as_i32 = -1, preserve_aspect_ratio: as_bool = true, fill_svg: as_string = "") {
if !path.ends_with(".svg") && !fill_svg.is_empty() {
log::warn!("Fill attribute ignored, file is not an svg image");
}
if path.ends_with(".gif") { if path.ends_with(".gif") {
let pixbuf_animation = gtk::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?; let pixbuf_animation = gtk::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?;
gtk_widget.set_from_animation(&pixbuf_animation); gtk_widget.set_from_animation(&pixbuf_animation);
} else { } else {
let pixbuf = gtk::gdk_pixbuf::Pixbuf::from_file_at_size(std::path::PathBuf::from(path), image_width, image_height)?; let pixbuf;
// populate the pixel buffer
if path.ends_with(".svg") && !fill_svg.is_empty() {
let svg_data = std::fs::read_to_string(std::path::PathBuf::from(path.clone()))?;
// The fastest way to add/change fill color
let svg_data = if svg_data.contains("fill=") {
let reg = regex::Regex::new(r#"fill="[^"]*""#)?;
reg.replace(&svg_data, &format!("fill=\"{}\"", fill_svg))
} else {
let reg = regex::Regex::new(r"<svg")?;
reg.replace(&svg_data, &format!("<svg fill=\"{}\"", fill_svg))
};
let stream = gtk::gio::MemoryInputStream::from_bytes(&gtk::glib::Bytes::from(svg_data.as_bytes()));
pixbuf = gtk::gdk_pixbuf::Pixbuf::from_stream_at_scale(&stream, image_width, image_height, preserve_aspect_ratio, None::<&gtk::gio::Cancellable>)?;
stream.close(None::<&gtk::gio::Cancellable>)?;
} else {
pixbuf = gtk::gdk_pixbuf::Pixbuf::from_file_at_scale(std::path::PathBuf::from(path), image_width, image_height, preserve_aspect_ratio)?;
}
gtk_widget.set_from_pixbuf(Some(&pixbuf)); gtk_widget.set_from_pixbuf(Some(&pixbuf));
} }
}, },
@ -727,27 +805,27 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
// Support :hover selector // Support :hover selector
gtk_widget.connect_enter_notify_event(|gtk_widget, evt| { gtk_widget.connect_enter_notify_event(|gtk_widget, evt| {
if evt.detail() != NotifyType::Inferior { if evt.detail() != NotifyType::Inferior {
gtk_widget.clone().set_state_flags(gtk::StateFlags::PRELIGHT, false); gtk_widget.set_state_flags(gtk::StateFlags::PRELIGHT, false);
} }
gtk::Inhibit(false) glib::Propagation::Proceed
}); });
gtk_widget.connect_leave_notify_event(|gtk_widget, evt| { gtk_widget.connect_leave_notify_event(|gtk_widget, evt| {
if evt.detail() != NotifyType::Inferior { if evt.detail() != NotifyType::Inferior {
gtk_widget.clone().unset_state_flags(gtk::StateFlags::PRELIGHT); gtk_widget.unset_state_flags(gtk::StateFlags::PRELIGHT);
} }
gtk::Inhibit(false) glib::Propagation::Proceed
}); });
// Support :active selector // Support :active selector
gtk_widget.connect_button_press_event(|gtk_widget, _| { gtk_widget.connect_button_press_event(|gtk_widget, _| {
gtk_widget.clone().set_state_flags(gtk::StateFlags::ACTIVE, false); gtk_widget.set_state_flags(gtk::StateFlags::ACTIVE, false);
gtk::Inhibit(false) glib::Propagation::Proceed
}); });
gtk_widget.connect_button_release_event(|gtk_widget, _| { gtk_widget.connect_button_release_event(|gtk_widget, _| {
gtk_widget.clone().unset_state_flags(gtk::StateFlags::ACTIVE); gtk_widget.unset_state_flags(gtk::StateFlags::ACTIVE);
gtk::Inhibit(false) glib::Propagation::Proceed
}); });
def_widget!(bargs, _g, gtk_widget, { def_widget!(bargs, _g, gtk_widget, {
@ -761,7 +839,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
if delta != 0f64 { // Ignore the first event https://bugzilla.gnome.org/show_bug.cgi?id=675959 if delta != 0f64 { // Ignore the first event https://bugzilla.gnome.org/show_bug.cgi?id=675959
run_command(timeout, &onscroll, &[if delta < 0f64 { "up" } else { "down" }]); run_command(timeout, &onscroll, &[if delta < 0f64 { "up" } else { "down" }]);
} }
gtk::Inhibit(false) glib::Propagation::Proceed
})); }));
}, },
// @prop timeout - timeout of the command. Default: "200ms" // @prop timeout - timeout of the command. Default: "200ms"
@ -772,7 +850,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
if evt.detail() != NotifyType::Inferior { if evt.detail() != NotifyType::Inferior {
run_command(timeout, &onhover, &[evt.position().0, evt.position().1]); run_command(timeout, &onhover, &[evt.position().0, evt.position().1]);
} }
gtk::Inhibit(false) glib::Propagation::Proceed
})); }));
}, },
// @prop timeout - timeout of the command. Default: "200ms" // @prop timeout - timeout of the command. Default: "200ms"
@ -783,7 +861,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
if evt.detail() != NotifyType::Inferior { if evt.detail() != NotifyType::Inferior {
run_command(timeout, &onhoverlost, &[evt.position().0, evt.position().1]); run_command(timeout, &onhoverlost, &[evt.position().0, evt.position().1]);
} }
gtk::Inhibit(false) glib::Propagation::Proceed
})); }));
}, },
// @prop cursor - Cursor to show while hovering (see [gtk3-cursors](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html) for possible names) // @prop cursor - Cursor to show while hovering (see [gtk3-cursors](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html) for possible names)
@ -799,7 +877,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
gdk_window.set_cursor(gdk::Cursor::from_name(&display, &cursor).as_ref()); gdk_window.set_cursor(gdk::Cursor::from_name(&display, &cursor).as_ref());
} }
} }
gtk::Inhibit(false) glib::Propagation::Proceed
})); }));
connect_signal_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |widget, _evt| { connect_signal_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |widget, _evt| {
if _evt.detail() != NotifyType::Inferior { if _evt.detail() != NotifyType::Inferior {
@ -808,7 +886,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
gdk_window.set_cursor(None); gdk_window.set_cursor(None);
} }
} }
gtk::Inhibit(false) glib::Propagation::Proceed
})); }));
}, },
// @prop timeout - timeout of the command. Default: "200ms" // @prop timeout - timeout of the command. Default: "200ms"
@ -857,32 +935,28 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
}; };
})); }));
}, },
// TODO the fact that we have the same code here as for button is ugly, as we want to keep consistency
prop( prop(
// @prop timeout - timeout of the command. Default: "200ms" // @prop timeout - timeout of the command. Default: "200ms"
timeout: as_duration = Duration::from_millis(200), timeout: as_duration = Duration::from_millis(200),
// @prop onclick - a command that get's run when the button is clicked // @prop onclick - command to run when the widget is clicked
onclick: as_string = "", onclick: as_string = "",
// @prop onmiddleclick - a command that get's run when the button is middleclicked // @prop onmiddleclick - command to run when the widget is middleclicked
onmiddleclick: as_string = "", onmiddleclick: as_string = "",
// @prop onrightclick - a command that get's run when the button is rightclicked // @prop onrightclick - command to run when the widget is rightclicked
onrightclick: as_string = "" onrightclick: as_string = ""
) { ) {
gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK);
connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { connect_signal_handler!(gtk_widget, gtk_widget.connect_button_release_event(move |_, evt| {
match evt.button() { match evt.button() {
1 => run_command(timeout, &onclick, &[] as &[&str]), 1 => run_command(timeout, &onclick, &[] as &[&str]),
2 => run_command(timeout, &onmiddleclick, &[] as &[&str]), 2 => run_command(timeout, &onmiddleclick, &[] as &[&str]),
3 => run_command(timeout, &onrightclick, &[] as &[&str]), 3 => run_command(timeout, &onrightclick, &[] as &[&str]),
_ => {}, _ => {},
} }
gtk::Inhibit(false) glib::Propagation::Proceed
})); }));
} }
}); });
Ok(gtk_widget) Ok(gtk_widget)
} }
@ -894,11 +968,12 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
def_widget!(bargs, _g, gtk_widget, { def_widget!(bargs, _g, gtk_widget, {
// @prop text - the text to display // @prop text - the text to display
// @prop truncate - whether to truncate text (or pango markup). If `show-truncated` is `false`, or if `limit-width` has a value, this property has no effect and truncation is enabled.
// @prop limit-width - maximum count of characters to display // @prop limit-width - maximum count of characters to display
// @prop truncate-left - whether to truncate on the left side // @prop truncate-left - whether to truncate on the left side
// @prop show-truncated - show whether the text was truncated. Disabling it will also disable dynamic truncation (the labels won't be truncated more than `limit-width`, even if there is not enough space for them), and will completly disable truncation on pango markup. // @prop show-truncated - show whether the text was truncated. Disabling it will also disable dynamic truncation (the labels won't be truncated more than `limit-width`, even if there is not enough space for them), and will completly disable truncation on pango markup.
// @prop unindent - whether to remove leading spaces // @prop unindent - whether to remove leading spaces
prop(text: as_string, limit_width: as_i32 = i32::MAX, truncate_left: as_bool = false, show_truncated: as_bool = true, unindent: as_bool = true) { prop(text: as_string, truncate: as_bool = false, limit_width: as_i32 = i32::MAX, truncate_left: as_bool = false, show_truncated: as_bool = true, unindent: as_bool = true) {
let text = if show_truncated { let text = if show_truncated {
// gtk does weird thing if we set max_width_chars to i32::MAX // gtk does weird thing if we set max_width_chars to i32::MAX
if limit_width == i32::MAX { if limit_width == i32::MAX {
@ -906,11 +981,15 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
} else { } else {
gtk_widget.set_max_width_chars(limit_width); gtk_widget.set_max_width_chars(limit_width);
} }
if truncate || limit_width != i32::MAX {
if truncate_left { if truncate_left {
gtk_widget.set_ellipsize(pango::EllipsizeMode::Start); gtk_widget.set_ellipsize(pango::EllipsizeMode::Start);
} else { } else {
gtk_widget.set_ellipsize(pango::EllipsizeMode::End); gtk_widget.set_ellipsize(pango::EllipsizeMode::End);
} }
} else {
gtk_widget.set_ellipsize(pango::EllipsizeMode::None);
}
text text
} else { } else {
@ -918,7 +997,7 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
let limit_width = limit_width as usize; let limit_width = limit_width as usize;
let char_count = text.chars().count(); let char_count = text.chars().count();
if char_count > limit_width && !show_truncated { if char_count > limit_width {
if truncate_left { if truncate_left {
text.chars().skip(char_count - limit_width).collect() text.chars().skip(char_count - limit_width).collect()
} else { } else {
@ -934,11 +1013,12 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
gtk_widget.set_text(&text); gtk_widget.set_text(&text);
}, },
// @prop markup - Pango markup to display // @prop markup - Pango markup to display
// @prop truncate - whether to truncate text (or pango markup). If `show-truncated` is `false`, or if `limit-width` has a value, this property has no effect and truncation is enabled.
// @prop limit-width - maximum count of characters to display // @prop limit-width - maximum count of characters to display
// @prop truncate-left - whether to truncate on the left side // @prop truncate-left - whether to truncate on the left side
// @prop show-truncated - show whether the text was truncatedd. Disabling it will also disable dynamic truncation (the labels won't be truncated more than `limit-width`, even if there is not enough space for them), and will completly disable truncation on pango markup. // @prop show-truncated - show whether the text was truncated. Disabling it will also disable dynamic truncation (the labels won't be truncated more than `limit-width`, even if there is not enough space for them), and will completly disable truncation on pango markup.
prop(markup: as_string, limit_width: as_i32 = i32::MAX, truncate_left: as_bool = false, show_truncated: as_bool = true) { prop(markup: as_string, truncate: as_bool = false, limit_width: as_i32 = i32::MAX, truncate_left: as_bool = false, show_truncated: as_bool = true) {
if show_truncated { if (truncate || limit_width != i32::MAX) && show_truncated {
// gtk does weird thing if we set max_width_chars to i32::MAX // gtk does weird thing if we set max_width_chars to i32::MAX
if limit_width == i32::MAX { if limit_width == i32::MAX {
gtk_widget.set_max_width_chars(-1); gtk_widget.set_max_width_chars(-1);
@ -973,6 +1053,14 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
prop(justify: as_string = "left") { prop(justify: as_string = "left") {
gtk_widget.set_justify(parse_justification(&justify)?); gtk_widget.set_justify(parse_justification(&justify)?);
}, },
// @prop wrap-mode - how text is wrapped. possible options: $wrap_mode
prop(wrap_mode: as_string = "word") {
gtk_widget.set_wrap_mode(parse_wrap_mode(&wrap_mode)?);
},
// @prop lines - maximum number of lines to display (only works when `limit-width` has a value). A value of -1 (default) disables the limit.
prop(lines: as_i32 = -1) {
gtk_widget.set_lines(lines);
}
}); });
Ok(gtk_widget) Ok(gtk_widget)
} }
@ -1082,20 +1170,11 @@ const WIDGET_NAME_STACK: &str = "stack";
/// @desc A widget that displays one of its children at a time /// @desc A widget that displays one of its children at a time
fn build_gtk_stack(bargs: &mut BuilderArgs) -> Result<gtk::Stack> { fn build_gtk_stack(bargs: &mut BuilderArgs) -> Result<gtk::Stack> {
let gtk_widget = gtk::Stack::new(); let gtk_widget = gtk::Stack::new();
def_widget!(bargs, _g, gtk_widget, {
// @prop selected - index of child which should be shown
prop(selected: as_i32) { gtk_widget.set_visible_child_name(&selected.to_string()); },
// @prop transition - the name of the transition. Possible values: $transition
prop(transition: as_string = "crossfade") { gtk_widget.set_transition_type(parse_stack_transition(&transition)?); },
// @prop same-size - sets whether all children should be the same size
prop(same_size: as_bool = false) { gtk_widget.set_homogeneous(same_size); }
});
match bargs.widget_use.children.len().cmp(&1) { if bargs.widget_use.children.is_empty() {
Ordering::Less => { return Err(DiagError(gen_diagnostic!("stack must contain at least one element", bargs.widget_use.span)).into());
Err(DiagError(gen_diagnostic!("stack must contain at least one element", bargs.widget_use.span)).into())
} }
Ordering::Greater | Ordering::Equal => {
let children = bargs.widget_use.children.iter().map(|child| { let children = bargs.widget_use.children.iter().map(|child| {
build_gtk_widget( build_gtk_widget(
bargs.scope_graph, bargs.scope_graph,
@ -1105,25 +1184,37 @@ fn build_gtk_stack(bargs: &mut BuilderArgs) -> Result<gtk::Stack> {
bargs.custom_widget_invocation.clone(), bargs.custom_widget_invocation.clone(),
) )
}); });
for (i, child) in children.enumerate() { for (i, child) in children.enumerate() {
let child = child?; let child = child?;
gtk_widget.add_named(&child, &i.to_string()); gtk_widget.add_named(&child, &i.to_string());
child.show(); child.show();
} }
def_widget!(bargs, _g, gtk_widget, {
// @prop selected - index of child which should be shown
prop(selected: as_i32) { gtk_widget.set_visible_child_name(&selected.to_string()); },
// @prop transition - the name of the transition. Possible values: $transition
prop(transition: as_string = "crossfade") { gtk_widget.set_transition_type(parse_stack_transition(&transition)?); },
// @prop same-size - sets whether all children should be the same size
prop(same_size: as_bool = false) { gtk_widget.set_homogeneous(same_size); }
});
Ok(gtk_widget) Ok(gtk_widget)
} }
}
}
const WIDGET_NAME_TRANSFORM: &str = "transform"; const WIDGET_NAME_TRANSFORM: &str = "transform";
/// @widget transform /// @widget transform
/// @desc A widget that applies transformations to its content. They are applied in the following /// @desc A widget that applies transformations to its content. They are applied in the following order: rotate -> translate -> scale
/// order: rotate->translate->scale)
fn build_transform(bargs: &mut BuilderArgs) -> Result<Transform> { fn build_transform(bargs: &mut BuilderArgs) -> Result<Transform> {
let w = Transform::new(); let w = Transform::new();
def_widget!(bargs, _g, w, { def_widget!(bargs, _g, w, {
// @prop rotate - the percentage to rotate // @prop rotate - the percentage to rotate
prop(rotate: as_f64) { w.set_property("rotate", rotate); }, prop(rotate: as_f64) { w.set_property("rotate", rotate); },
// @prop transform-origin-x - x coordinate of origin of transformation (px or %)
prop(transform_origin_x: as_string) { w.set_property("transform-origin-x", transform_origin_x) },
// @prop transform-origin-y - y coordinate of origin of transformation (px or %)
prop(transform_origin_y: as_string) { w.set_property("transform-origin-y", transform_origin_y) },
// @prop translate-x - the amount to translate in the x direction (px or %) // @prop translate-x - the amount to translate in the x direction (px or %)
prop(translate_x: as_string) { w.set_property("translate-x", translate_x); }, prop(translate_x: as_string) { w.set_property("translate-x", translate_x); },
// @prop translate-y - the amount to translate in the y direction (px or %) // @prop translate-y - the amount to translate in the y direction (px or %)
@ -1161,7 +1252,14 @@ fn build_graph(bargs: &mut BuilderArgs) -> Result<super::graph::Graph> {
let w = super::graph::Graph::new(); let w = super::graph::Graph::new();
def_widget!(bargs, _g, w, { def_widget!(bargs, _g, w, {
// @prop value - the value, between 0 - 100 // @prop value - the value, between 0 - 100
prop(value: as_f64) { w.set_property("value", value); }, prop(value: as_f64) {
if value.is_nan() || value.is_infinite() {
return Err(DiagError(gen_diagnostic!(
format!("Graph's value should never be NaN or infinite")
)).into());
}
w.set_property("value", value);
},
// @prop thickness - the thickness of the line // @prop thickness - the thickness of the line
prop(thickness: as_f64) { w.set_property("thickness", thickness); }, prop(thickness: as_f64) { w.set_property("thickness", thickness); },
// @prop time-range - the range of time to show // @prop time-range - the range of time to show
@ -1182,6 +1280,12 @@ fn build_graph(bargs: &mut BuilderArgs) -> Result<super::graph::Graph> {
// @prop line-style - changes the look of the edges in the graph. Values: "miter" (default), "round", // @prop line-style - changes the look of the edges in the graph. Values: "miter" (default), "round",
// "bevel" // "bevel"
prop(line_style: as_string) { w.set_property("line-style", line_style); }, prop(line_style: as_string) { w.set_property("line-style", line_style); },
// @prop flip-x - whether the x axis should go from high to low
prop(flip_x: as_bool) { w.set_property("flip-x", flip_x); },
// @prop flip-y - whether the y axis should go from high to low
prop(flip_y: as_bool) { w.set_property("flip-y", flip_y); },
// @prop vertical - if set to true, the x and y axes will be exchanged
prop(vertical: as_bool) { w.set_property("vertical", vertical); },
}); });
Ok(w) Ok(w)
} }
@ -1287,6 +1391,16 @@ fn parse_justification(j: &str) -> Result<gtk::Justification> {
} }
} }
/// @var position - "left", "right", "top", "bottom"
fn parse_position_type(g: &str) -> Result<gtk::PositionType> {
enum_parse! { "position", g,
"left" => gtk::PositionType::Left,
"right" => gtk::PositionType::Right,
"top" => gtk::PositionType::Top,
"bottom" => gtk::PositionType::Bottom,
}
}
/// @var gravity - "south", "east", "west", "north", "auto" /// @var gravity - "south", "east", "west", "north", "auto"
fn parse_gravity(g: &str) -> Result<gtk::pango::Gravity> { fn parse_gravity(g: &str) -> Result<gtk::pango::Gravity> {
enum_parse! { "gravity", g, enum_parse! { "gravity", g,
@ -1298,6 +1412,15 @@ fn parse_gravity(g: &str) -> Result<gtk::pango::Gravity> {
} }
} }
/// @var wrap_mode - "word", "char", "wordchar"
fn parse_wrap_mode(w: &str) -> Result<gtk::pango::WrapMode> {
enum_parse! { "wrap-mode", w,
"word" => gtk::pango::WrapMode::Word,
"char" => gtk::pango::WrapMode::Char,
"wordchar" => gtk::pango::WrapMode::WordChar
}
}
/// Connect a function to the first map event of a widget. After that first map, the handler will get disconnected. /// Connect a function to the first map event of a widget. After that first map, the handler will get disconnected.
fn connect_first_map<W: IsA<gtk::Widget>, F: Fn(&W) + 'static>(widget: &W, func: F) { fn connect_first_map<W: IsA<gtk::Widget>, F: Fn(&W) + 'static>(widget: &W, func: F) {
let signal_handler_id = std::rc::Rc::new(std::cell::RefCell::new(None)); let signal_handler_id = std::rc::Rc::new(std::cell::RefCell::new(None));

View file

@ -1,5 +1,4 @@
use glib::{object_subclass, wrapper}; use gtk::glib::{self, object_subclass, wrapper, Properties};
use glib_macros::Properties;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell; use std::cell::RefCell;

View file

@ -59,10 +59,10 @@ impl WindowArguments {
// Ensure that the arguments passed to the window that are already interpreted by eww (id, screen) // Ensure that the arguments passed to the window that are already interpreted by eww (id, screen)
// are set to the correct values // are set to the correct values
if expected_args.contains(&"id".to_string()) { if expected_args.contains(&String::from("id")) {
local_variables.insert(VarName::from("id"), DynVal::from(self.instance_id.clone())); local_variables.insert(VarName::from("id"), DynVal::from(self.instance_id.clone()));
} }
if self.monitor.is_some() && expected_args.contains(&"screen".to_string()) { if self.monitor.is_some() && expected_args.contains(&String::from("screen")) {
let mon_dyn = DynVal::from(&self.monitor.clone().unwrap()); let mon_dyn = DynVal::from(&self.monitor.clone().unwrap());
local_variables.insert(VarName::from("screen"), mon_dyn); local_variables.insert(VarName::from("screen"), mon_dyn);
} }

View file

@ -17,7 +17,6 @@ use crate::window_arguments::WindowArguments;
pub struct WindowInitiator { pub struct WindowInitiator {
pub backend_options: BackendWindowOptions, pub backend_options: BackendWindowOptions,
pub geometry: Option<WindowGeometry>, pub geometry: Option<WindowGeometry>,
pub id: String,
pub local_variables: HashMap<VarName, DynVal>, pub local_variables: HashMap<VarName, DynVal>,
pub monitor: Option<MonitorIdentifier>, pub monitor: Option<MonitorIdentifier>,
pub name: String, pub name: String,
@ -37,7 +36,6 @@ impl WindowInitiator {
Ok(WindowInitiator { Ok(WindowInitiator {
backend_options: window_def.backend_options.eval(&vars)?, backend_options: window_def.backend_options.eval(&vars)?,
geometry, geometry,
id: args.instance_id.clone(),
monitor, monitor,
name: window_def.name.clone(), name: window_def.name.clone(),
resizable: window_def.eval_resizable(&vars)?, resizable: window_def.eval_resizable(&vars)?,

View file

@ -12,3 +12,4 @@ homepage = "https://github.com/elkowar/eww"
serde.workspace = true serde.workspace = true
derive_more.workspace = true derive_more.workspace = true
ref-cast.workspace = true ref-cast.workspace = true
chrono = { workspace = true, features = ["unstable-locales"] }

View file

@ -1,6 +1,8 @@
pub mod locale;
pub mod span; pub mod span;
pub mod wrappers; pub mod wrappers;
pub use locale::*;
pub use span::*; pub use span::*;
pub use wrappers::*; pub use wrappers::*;

View file

@ -0,0 +1,12 @@
use chrono::Locale;
use std::env::var;
/// Returns the `Locale` enum based on the `LC_ALL`, `LC_TIME`, and `LANG` environment variables in
/// that order, which is the precedence order prescribed by Section 8.2 of POSIX.1-2017.
/// If the environment variable is not defined or is malformed use the POSIX locale.
pub fn get_locale() -> Locale {
var("LC_ALL")
.or_else(|_| var("LC_TIME"))
.or_else(|_| var("LANG"))
.map_or(Locale::POSIX, |v| v.split('.').next().and_then(|x| x.try_into().ok()).unwrap_or_default())
}

View file

@ -1,11 +1,11 @@
use derive_more::*; use derive_more::{Debug, *};
use ref_cast::RefCast; use ref_cast::RefCast;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// The name of a variable /// The name of a variable
#[repr(transparent)] #[repr(transparent)]
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom, RefCast)] #[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, Debug, RefCast)]
#[debug(fmt = "VarName({})", .0)] #[debug("VarName({})", _0)]
pub struct VarName(pub String); pub struct VarName(pub String);
impl std::borrow::Borrow<str> for VarName { impl std::borrow::Borrow<str> for VarName {
@ -34,8 +34,8 @@ impl From<AttrName> for VarName {
/// The name of an attribute /// The name of an attribute
#[repr(transparent)] #[repr(transparent)]
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom, RefCast)] #[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, Debug, RefCast)]
#[debug(fmt="AttrName({})", .0)] #[debug("AttrName({})", _0)]
pub struct AttrName(pub String); pub struct AttrName(pub String);
impl AttrName { impl AttrName {

View file

@ -9,11 +9,12 @@ repository = "https://github.com/elkowar/eww"
homepage = "https://github.com/elkowar/eww" homepage = "https://github.com/elkowar/eww"
[dependencies] [dependencies]
gtk = "0.17.1"
gdk = "0.17.1"
zbus = { version = "3.7.0", default-features = false, features = ["tokio"] }
dbusmenu-gtk3 = "0.1.0" dbusmenu-gtk3 = "0.1.0"
quick-xml = { version = "0.37.1", features = ["serialize"] }
serde = "1.0.215"
gtk.workspace = true
log.workspace = true log.workspace = true
thiserror.workspace = true thiserror.workspace = true
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
zbus = { workspace = true, default-features = false, features = ["tokio"] }

View file

@ -105,7 +105,7 @@ fn icon_from_name(
) -> std::result::Result<gtk::gdk_pixbuf::Pixbuf, IconError> { ) -> std::result::Result<gtk::gdk_pixbuf::Pixbuf, IconError> {
let theme = if let Some(path) = theme_path { let theme = if let Some(path) = theme_path {
let theme = gtk::IconTheme::new(); let theme = gtk::IconTheme::new();
theme.prepend_search_path(&path); theme.prepend_search_path(path);
theme theme
} else { } else {
gtk::IconTheme::default().expect("Could not get default gtk theme") gtk::IconTheme::default().expect("Could not get default gtk theme")

View file

@ -1,6 +1,8 @@
use crate::*; use crate::*;
use gtk::{self, prelude::*}; use gtk::{self, prelude::*};
use serde::Deserialize;
use zbus::fdo::IntrospectableProxy;
/// Recognised values of [`org.freedesktop.StatusNotifierItem.Status`]. /// Recognised values of [`org.freedesktop.StatusNotifierItem.Status`].
/// ///
@ -61,7 +63,12 @@ impl Item {
if let Some((addr, path)) = service.split_once('/') { if let Some((addr, path)) = service.split_once('/') {
(addr.to_owned(), format!("/{}", path)) (addr.to_owned(), format!("/{}", path))
} else if service.starts_with(':') { } else if service.starts_with(':') {
(service[0..6].to_owned(), names::ITEM_OBJECT.to_owned()) (
service.to_owned(),
resolve_pathless_address(con, service, "/".to_owned())
.await?
.ok_or_else(|| zbus::Error::Failure(format!("no StatusNotifierItem found for {service}")))?,
)
} else { } else {
return Err(zbus::Error::Address(service.to_owned())); return Err(zbus::Error::Address(service.to_owned()));
} }
@ -88,9 +95,9 @@ impl Item {
Ok(()) Ok(())
} }
pub async fn popup_menu(&self, event: &gdk::EventButton, x: i32, y: i32) -> zbus::Result<()> { pub async fn popup_menu(&self, event: &gtk::gdk::EventButton, x: i32, y: i32) -> zbus::Result<()> {
if let Some(menu) = &self.gtk_menu { if let Some(menu) = &self.gtk_menu {
menu.popup_at_pointer(event.downcast_ref::<gdk::Event>()); menu.popup_at_pointer(event.downcast_ref::<gtk::gdk::Event>());
Ok(()) Ok(())
} else { } else {
self.sni.context_menu(x, y).await self.sni.context_menu(x, y).await
@ -105,3 +112,59 @@ impl Item {
load_icon_from_sni(&self.sni, size, scale).await load_icon_from_sni(&self.sni, size, scale).await
} }
} }
#[derive(Deserialize)]
struct DBusNode {
#[serde(default)]
interface: Vec<DBusInterface>,
#[serde(default)]
node: Vec<DBusNode>,
#[serde(rename = "@name")]
name: Option<String>,
}
#[derive(Deserialize)]
struct DBusInterface {
#[serde(rename = "@name")]
name: String,
}
async fn resolve_pathless_address(con: &zbus::Connection, service: &str, path: String) -> zbus::Result<Option<String>> {
let introspection_xml =
IntrospectableProxy::builder(con).destination(service)?.path(path.as_str())?.build().await?.introspect().await?;
let dbus_node =
quick_xml::de::from_str::<DBusNode>(&introspection_xml).map_err(|err| zbus::Error::Failure(err.to_string()))?;
if dbus_node.interface.iter().any(|interface| interface.name == "org.kde.StatusNotifierItem") {
// This item implements the desired interface, so bubble it back up
Ok(Some(path))
} else {
for node in dbus_node.node {
if let Some(name) = node.name {
if name == "StatusNotifierItem" {
// If this exists, then there's a good chance DBus may not think anything
// implements the desired interface, so just bubble this up instead.
return Ok(Some(join_to_path(&path, name)));
}
let path = Box::pin(resolve_pathless_address(con, service, join_to_path(&path, name))).await?;
if path.is_some() {
// Return the first item found from a child
return Ok(path);
}
}
}
// No children had the item we want...
Ok(None)
}
}
fn join_to_path(path: &str, name: String) -> String {
// Make sure we don't double-up on the leading slash
format!("{path}/{name}", path = if path == "/" { "" } else { path })
}

View file

@ -2,9 +2,15 @@
//! //!
//! The interface XML files were taken from //! The interface XML files were taken from
//! [Waybar](https://github.com/Alexays/Waybar/tree/master/protocol), and the proxies were //! [Waybar](https://github.com/Alexays/Waybar/tree/master/protocol), and the proxies were
//! generated with [zbus-xmlgen](https://docs.rs/crate/zbus_xmlgen/latest) by running `zbus-xmlgen //! generated with [zbus-xmlgen](https://docs.rs/crate/zbus_xmlgen/latest) by running
//! dbus_status_notifier_item.xml` and `zbus-xmlgen dbus_status_notifier_watcher.xml`. At the //! `zbus-xmlgen file crates/notifier_host/src/proxy/dbus_status_notifier_item.xml` and
//! moment, `dbus_menu.xml` isn't used. //! `zbus-xmlgen file crates/notifier_host/src/proxy/dbus_status_notifier_watcher.xml`.
//!
//! Note that the `dbus_status_notifier_watcher.rs` file has been slightly adjusted, the
//! default arguments to the [proxy](https://docs.rs/zbus/4.4.0/zbus/attr.proxy.html)
//! macro need some adjusting.
//!
//! At the moment, `dbus_menu.xml` isn't used.
//! //!
//! For more information, see ["Writing a client proxy" in the zbus //! For more information, see ["Writing a client proxy" in the zbus
//! tutorial](https://dbus2.github.io/zbus/). //! tutorial](https://dbus2.github.io/zbus/).

View file

@ -14,13 +14,14 @@ build = "build.rs"
[dependencies] [dependencies]
eww_shared_util.workspace = true eww_shared_util.workspace = true
bytesize.workspace = true
cached.workspace = true cached.workspace = true
chrono-tz.workspace = true chrono-tz.workspace = true
chrono.workspace = true chrono = { workspace = true, features = ["unstable-locales"] }
itertools.workspace = true itertools.workspace = true
jaq-core.workspace = true jaq-core.workspace = true
jaq-parse.workspace = true jaq-parse.workspace = true
jaq-std = {workspace = true, features = ["bincode"]} jaq-std.workspace = true
jaq-interpret.workspace = true jaq-interpret.workspace = true
jaq-syn.workspace = true jaq-syn.workspace = true
lalrpop-util.workspace = true lalrpop-util.workspace = true

View file

@ -1,3 +1,4 @@
use bytesize::ByteSize;
use cached::proc_macro::cached; use cached::proc_macro::cached;
use chrono::{Local, LocalResult, TimeZone}; use chrono::{Local, LocalResult, TimeZone};
use itertools::Itertools; use itertools::Itertools;
@ -7,7 +8,7 @@ use crate::{
ast::{AccessType, BinOp, SimplExpr, UnaryOp}, ast::{AccessType, BinOp, SimplExpr, UnaryOp},
dynval::{ConversionError, DynVal}, dynval::{ConversionError, DynVal},
}; };
use eww_shared_util::{Span, Spanned, VarName}; use eww_shared_util::{get_locale, Span, Spanned, VarName};
use std::{ use std::{
collections::HashMap, collections::HashMap,
convert::{Infallible, TryFrom, TryInto}, convert::{Infallible, TryFrom, TryInto},
@ -61,6 +62,9 @@ pub enum EvalError {
#[error("Error parsing date: {0}")] #[error("Error parsing date: {0}")]
ChronoError(String), ChronoError(String),
#[error("Error parsing byte format mode: {0}")]
ByteFormatModeError(String),
#[error("{1}")] #[error("{1}")]
Spanned(Span, Box<EvalError>), Spanned(Span, Box<EvalError>),
} }
@ -268,6 +272,10 @@ impl SimplExpr {
let is_safe = *safe == AccessType::Safe; let is_safe = *safe == AccessType::Safe;
// Needs to be done first as `as_json_value` fails on empty string
if is_safe && val.as_string()?.is_empty() {
return Ok(DynVal::from(&serde_json::Value::Null).at(*span));
}
match val.as_json_value()? { match val.as_json_value()? {
serde_json::Value::Array(val) => { serde_json::Value::Array(val) => {
let index = index.as_i32()?; let index = index.as_i32()?;
@ -281,9 +289,6 @@ impl SimplExpr {
.unwrap_or(&serde_json::Value::Null); .unwrap_or(&serde_json::Value::Null);
Ok(DynVal::from(indexed_value).at(*span)) Ok(DynVal::from(indexed_value).at(*span))
} }
serde_json::Value::String(val) if val.is_empty() && is_safe => {
Ok(DynVal::from(&serde_json::Value::Null).at(*span))
}
serde_json::Value::Null if is_safe => Ok(DynVal::from(&serde_json::Value::Null).at(*span)), serde_json::Value::Null if is_safe => Ok(DynVal::from(&serde_json::Value::Null).at(*span)),
_ => Err(EvalError::CannotIndex(format!("{}", val)).at(*span)), _ => Err(EvalError::CannotIndex(format!("{}", val)).at(*span)),
} }
@ -328,6 +333,52 @@ fn call_expr_function(name: &str, args: Vec<DynVal>) -> Result<DynVal, EvalError
} }
_ => Err(EvalError::WrongArgCount(name.to_string())), _ => Err(EvalError::WrongArgCount(name.to_string())),
}, },
"floor" => match args.as_slice() {
[num] => {
let num = num.as_f64()?;
Ok(DynVal::from(num.floor()))
}
_ => Err(EvalError::WrongArgCount(name.to_string())),
},
"ceil" => match args.as_slice() {
[num] => {
let num = num.as_f64()?;
Ok(DynVal::from(num.ceil()))
}
_ => Err(EvalError::WrongArgCount(name.to_string())),
},
"min" => match args.as_slice() {
[a, b] => {
let a = a.as_f64()?;
let b = b.as_f64()?;
Ok(DynVal::from(f64::min(a, b)))
}
_ => Err(EvalError::WrongArgCount(name.to_string())),
},
"max" => match args.as_slice() {
[a, b] => {
let a = a.as_f64()?;
let b = b.as_f64()?;
Ok(DynVal::from(f64::max(a, b)))
}
_ => Err(EvalError::WrongArgCount(name.to_string())),
},
"powi" => match args.as_slice() {
[num, n] => {
let num = num.as_f64()?;
let n = n.as_i32()?;
Ok(DynVal::from(f64::powi(num, n)))
}
_ => Err(EvalError::WrongArgCount(name.to_string())),
},
"powf" => match args.as_slice() {
[num, n] => {
let num = num.as_f64()?;
let n = n.as_f64()?;
Ok(DynVal::from(f64::powf(num, n)))
}
_ => Err(EvalError::WrongArgCount(name.to_string())),
},
"sin" => match args.as_slice() { "sin" => match args.as_slice() {
[num] => { [num] => {
let num = num.as_f64()?; let num = num.as_f64()?;
@ -439,7 +490,9 @@ fn call_expr_function(name: &str, args: Vec<DynVal>) -> Result<DynVal, EvalError
_ => Err(EvalError::WrongArgCount(name.to_string())), _ => Err(EvalError::WrongArgCount(name.to_string())),
}, },
"jq" => match args.as_slice() { "jq" => match args.as_slice() {
[json, code] => run_jaq_function(json.as_json_value()?, code.as_string()?) [json, code] => run_jaq_function(json.as_json_value()?, code.as_string()?, "")
.map_err(|e| EvalError::Spanned(code.span(), Box::new(e))),
[json, code, args] => run_jaq_function(json.as_json_value()?, code.as_string()?, &args.as_string()?)
.map_err(|e| EvalError::Spanned(code.span(), Box::new(e))), .map_err(|e| EvalError::Spanned(code.span(), Box::new(e))),
_ => Err(EvalError::WrongArgCount(name.to_string())), _ => Err(EvalError::WrongArgCount(name.to_string())),
}, },
@ -451,16 +504,68 @@ fn call_expr_function(name: &str, args: Vec<DynVal>) -> Result<DynVal, EvalError
}; };
Ok(DynVal::from(match timezone.timestamp_opt(timestamp.as_i64()?, 0) { Ok(DynVal::from(match timezone.timestamp_opt(timestamp.as_i64()?, 0) {
LocalResult::Single(t) | LocalResult::Ambiguous(t, _) => t.format(&format.as_string()?).to_string(), LocalResult::Single(t) | LocalResult::Ambiguous(t, _) => {
let format = format.as_string()?;
let delayed_format = t.format_localized(&format, get_locale());
let mut buffer = String::new();
if delayed_format.write_to(&mut buffer).is_err() {
return Err(EvalError::ChronoError("Invalid time formatting string: ".to_string() + &format));
}
buffer
}
LocalResult::None => return Err(EvalError::ChronoError("Invalid UNIX timestamp".to_string())), LocalResult::None => return Err(EvalError::ChronoError("Invalid UNIX timestamp".to_string())),
})) }))
} }
[timestamp, format] => Ok(DynVal::from(match Local.timestamp_opt(timestamp.as_i64()?, 0) { [timestamp, format] => Ok(DynVal::from(match Local.timestamp_opt(timestamp.as_i64()?, 0) {
LocalResult::Single(t) | LocalResult::Ambiguous(t, _) => t.format(&format.as_string()?).to_string(), LocalResult::Single(t) | LocalResult::Ambiguous(t, _) => {
let format = format.as_string()?;
let delayed_format = t.format_localized(&format, get_locale());
let mut buffer = String::new();
if delayed_format.write_to(&mut buffer).is_err() {
return Err(EvalError::ChronoError("Invalid time formatting string: ".to_string() + &format));
}
buffer
}
LocalResult::None => return Err(EvalError::ChronoError("Invalid UNIX timestamp".to_string())), LocalResult::None => return Err(EvalError::ChronoError("Invalid UNIX timestamp".to_string())),
})), })),
_ => Err(EvalError::WrongArgCount(name.to_string())), _ => Err(EvalError::WrongArgCount(name.to_string())),
}, },
"log" => match args.as_slice() {
[num, n] => {
let num = num.as_f64()?;
let n = n.as_f64()?;
Ok(DynVal::from(f64::log(num, n)))
}
_ => Err(EvalError::WrongArgCount(name.to_string())),
},
"formatbytes" => {
let (bytes, short, mode) = match args.as_slice() {
[bytes] => (bytes.as_i64()?, false, "iec".to_owned()),
[bytes, short] => (bytes.as_i64()?, short.as_bool()?, "iec".to_owned()),
[bytes, short, mode] => (bytes.as_i64()?, short.as_bool()?, mode.as_string()?),
_ => return Err(EvalError::WrongArgCount(name.to_string())),
};
let neg = bytes < 0;
let disp = ByteSize(bytes.abs() as u64).display();
let disp = match mode.as_str() {
"iec" => {
if short {
disp.iec_short()
} else {
disp.iec()
}
}
"si" => {
if short {
disp.si_short()
} else {
disp.si()
}
}
_ => return Err(EvalError::ByteFormatModeError(mode)),
};
Ok(DynVal::from(if neg { format!("-{disp}") } else { disp.to_string() }))
}
_ => Err(EvalError::UnknownFunction(name.to_string())), _ => Err(EvalError::UnknownFunction(name.to_string())),
} }
@ -485,16 +590,20 @@ fn prepare_jaq_filter(code: String) -> Result<Arc<jaq_interpret::Filter>, EvalEr
Ok(Arc::new(filter)) Ok(Arc::new(filter))
} }
fn run_jaq_function(json: serde_json::Value, code: String) -> Result<DynVal, EvalError> { fn run_jaq_function(json: serde_json::Value, code: String, args: &str) -> Result<DynVal, EvalError> {
let filter: Arc<jaq_interpret::Filter> = prepare_jaq_filter(code)?; use jaq_interpret::{Ctx, RcIter, Val};
let inputs = jaq_interpret::RcIter::new(std::iter::empty()); prepare_jaq_filter(code)?
let out = filter .run((Ctx::new([], &RcIter::new(std::iter::empty())), Val::from(json)))
.run((jaq_interpret::Ctx::new([], &inputs), jaq_interpret::Val::from(json))) .map(|r| r.map(Into::<serde_json::Value>::into))
.map(|x| x.map(Into::<serde_json::Value>::into)) .map(|x| {
.map(|x| x.map(|x| DynVal::from_string(serde_json::to_string(&x).unwrap()))) x.map(|val| match (args, val) {
("r", serde_json::Value::String(s)) => DynVal::from_string(s),
// invalid arguments are silently ignored
(_, v) => DynVal::from_string(serde_json::to_string(&v).unwrap()),
})
})
.collect::<Result<_, _>>() .collect::<Result<_, _>>()
.map_err(|e| EvalError::JaqError(e.to_string()))?; .map_err(|e| EvalError::JaqError(e.to_string()))
Ok(out)
} }
#[cfg(test)] #[cfg(test)]
@ -544,6 +653,8 @@ mod tests {
string_to_string(r#""Hello""#) => Ok(DynVal::from("Hello".to_string())), string_to_string(r#""Hello""#) => Ok(DynVal::from("Hello".to_string())),
safe_access_to_existing(r#"{ "a": { "b": 2 } }.a?.b"#) => Ok(DynVal::from(2)), safe_access_to_existing(r#"{ "a": { "b": 2 } }.a?.b"#) => Ok(DynVal::from(2)),
safe_access_to_missing(r#"{ "a": { "b": 2 } }.b?.b"#) => Ok(DynVal::from(&serde_json::Value::Null)), safe_access_to_missing(r#"{ "a": { "b": 2 } }.b?.b"#) => Ok(DynVal::from(&serde_json::Value::Null)),
safe_access_to_empty(r#"""?.test"#) => Ok(DynVal::from(&serde_json::Value::Null)),
safe_access_to_empty_json_string(r#"'""'?.test"#) => Err(super::EvalError::CannotIndex("\"\"".to_string())),
safe_access_index_to_existing(r#"[1, 2]?.[1]"#) => Ok(DynVal::from(2)), safe_access_index_to_existing(r#"[1, 2]?.[1]"#) => Ok(DynVal::from(2)),
safe_access_index_to_missing(r#""null"?.[1]"#) => Ok(DynVal::from(&serde_json::Value::Null)), safe_access_index_to_missing(r#""null"?.[1]"#) => Ok(DynVal::from(&serde_json::Value::Null)),
safe_access_index_to_non_indexable(r#"32?.[1]"#) => Err(super::EvalError::CannotIndex("32".to_string())), safe_access_index_to_non_indexable(r#"32?.[1]"#) => Err(super::EvalError::CannotIndex("32".to_string())),
@ -553,5 +664,9 @@ mod tests {
lazy_evaluation_or(r#"true || "null".test"#) => Ok(DynVal::from(true)), lazy_evaluation_or(r#"true || "null".test"#) => Ok(DynVal::from(true)),
lazy_evaluation_elvis(r#""test"?: "null".test"#) => Ok(DynVal::from("test")), lazy_evaluation_elvis(r#""test"?: "null".test"#) => Ok(DynVal::from("test")),
jq_basic_index(r#"jq("[7,8,9]", ".[0]")"#) => Ok(DynVal::from(7)), jq_basic_index(r#"jq("[7,8,9]", ".[0]")"#) => Ok(DynVal::from(7)),
jq_raw_arg(r#"jq("[ \"foo\" ]", ".[0]", "r")"#) => Ok(DynVal::from("foo")),
jq_empty_arg(r#"jq("[ \"foo\" ]", ".[0]", "")"#) => Ok(DynVal::from(r#""foo""#)),
jq_invalid_arg(r#"jq("[ \"foo\" ]", ".[0]", "hello")"#) => Ok(DynVal::from(r#""foo""#)),
jq_no_arg(r#"jq("[ \"foo\" ]", ".[0]")"#) => Ok(DynVal::from(r#""foo""#)),
} }
} }

View file

@ -7,6 +7,7 @@ use simplexpr::{
SimplExpr, SimplExpr,
}; };
use super::{attributes::Attributes, window_definition::EnumParseError};
use crate::{ use crate::{
enum_parse, enum_parse,
error::DiagResult, error::DiagResult,
@ -14,8 +15,7 @@ use crate::{
value::{coords, NumWithUnit}, value::{coords, NumWithUnit},
}; };
use eww_shared_util::{Span, VarName}; use eww_shared_util::{Span, VarName};
use simplexpr::dynval::ConversionError;
use super::{attributes::Attributes, window_definition::EnumParseError};
use crate::error::{DiagError, DiagResultExt}; use crate::error::{DiagError, DiagResultExt};
@ -27,6 +27,8 @@ pub enum Error {
CoordsError(#[from] coords::Error), CoordsError(#[from] coords::Error),
#[error(transparent)] #[error(transparent)]
EvalError(#[from] EvalError), EvalError(#[from] EvalError),
#[error(transparent)]
ConversionError(#[from] ConversionError),
} }
/// Backend-specific options of a window /// Backend-specific options of a window
@ -45,6 +47,7 @@ impl BackendWindowOptionsDef {
pub fn from_attrs(attrs: &mut Attributes) -> DiagResult<Self> { pub fn from_attrs(attrs: &mut Attributes) -> DiagResult<Self> {
let struts = attrs.ast_optional("reserve")?; let struts = attrs.ast_optional("reserve")?;
let window_type = attrs.ast_optional("windowtype")?; let window_type = attrs.ast_optional("windowtype")?;
let focusable = attrs.ast_optional("focusable")?;
let x11 = X11BackendWindowOptionsDef { let x11 = X11BackendWindowOptionsDef {
sticky: attrs.ast_optional("sticky")?, sticky: attrs.ast_optional("sticky")?,
struts, struts,
@ -53,7 +56,7 @@ impl BackendWindowOptionsDef {
}; };
let wayland = WlBackendWindowOptionsDef { let wayland = WlBackendWindowOptionsDef {
exclusive: attrs.ast_optional("exclusive")?, exclusive: attrs.ast_optional("exclusive")?,
focusable: attrs.ast_optional("focusable")?, focusable,
namespace: attrs.ast_optional("namespace")?, namespace: attrs.ast_optional("namespace")?,
}; };
@ -109,7 +112,7 @@ impl X11BackendWindowOptionsDef {
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct WlBackendWindowOptions { pub struct WlBackendWindowOptions {
pub exclusive: bool, pub exclusive: bool,
pub focusable: bool, pub focusable: WlWindowFocusable,
pub namespace: Option<String>, pub namespace: Option<String>,
} }
@ -122,10 +125,13 @@ pub struct WlBackendWindowOptionsDef {
} }
impl WlBackendWindowOptionsDef { impl WlBackendWindowOptionsDef {
fn eval(&self, local_variables: &HashMap<VarName, DynVal>) -> Result<WlBackendWindowOptions, EvalError> { fn eval(&self, local_variables: &HashMap<VarName, DynVal>) -> Result<WlBackendWindowOptions, Error> {
Ok(WlBackendWindowOptions { Ok(WlBackendWindowOptions {
exclusive: eval_opt_expr_as_bool(&self.exclusive, false, local_variables)?, exclusive: eval_opt_expr_as_bool(&self.exclusive, false, local_variables)?,
focusable: eval_opt_expr_as_bool(&self.focusable, false, local_variables)?, focusable: match &self.focusable {
Some(expr) => WlWindowFocusable::from_dynval(&expr.eval(local_variables)?)?,
None => WlWindowFocusable::default(),
},
namespace: match &self.namespace { namespace: match &self.namespace {
Some(expr) => Some(expr.eval(local_variables)?.as_string()?), Some(expr) => Some(expr.eval(local_variables)?.as_string()?),
None => None, None => None,
@ -145,6 +151,28 @@ fn eval_opt_expr_as_bool(
}) })
} }
#[derive(Debug, Clone, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)]
pub enum WlWindowFocusable {
#[default]
None,
Exclusive,
OnDemand,
}
impl FromStr for WlWindowFocusable {
type Err = EnumParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
enum_parse! { "focusable", s,
"none" => Self::None,
"exclusive" => Self::Exclusive,
"ondemand" => Self::OnDemand,
// legacy support
"true" => Self::Exclusive,
"false" => Self::None,
}
}
}
/// Window type of an x11 window /// Window type of an x11 window
#[derive(Debug, Clone, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)] #[derive(Debug, Clone, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)]
pub enum X11WindowType { pub enum X11WindowType {
@ -182,7 +210,7 @@ pub enum Side {
Bottom, Bottom,
} }
impl std::str::FromStr for Side { impl FromStr for Side {
type Err = EnumParseError; type Err = EnumParseError;
fn from_str(s: &str) -> Result<Side, Self::Err> { fn from_str(s: &str) -> Result<Side, Self::Err> {

View file

@ -90,7 +90,7 @@ impl<I: Iterator<Item = Ast>> AstIterator<I> {
parse_key_values(self, true) parse_key_values(self, true)
} }
pub fn put_back(&mut self, ast: Ast) { pub fn put_back(&mut self, ast: Ast) -> Option<Ast> {
self.remaining_span.0 = ast.span().0; self.remaining_span.0 = ast.span().0;
self.iter.put_back(ast) self.iter.put_back(ast)
} }
@ -100,9 +100,8 @@ impl<I: Iterator<Item = Ast>> Iterator for AstIterator<I> {
type Item = Ast; type Item = Ast;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|x| { self.iter.next().inspect(|x| {
self.remaining_span.0 = x.span().1; self.remaining_span.0 = x.span().1;
x
}) })
} }
} }

View file

@ -1,4 +1,4 @@
use derive_more::*; use derive_more::{Debug, *};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smart_default::SmartDefault; use smart_default::SmartDefault;
@ -14,13 +14,13 @@ pub enum Error {
MalformedCoords, MalformedCoords,
} }
#[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, DebugCustom, SmartDefault)] #[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, Debug, SmartDefault)]
pub enum NumWithUnit { pub enum NumWithUnit {
#[display(fmt = "{}%", .0)] #[display("{}%", _0)]
#[debug(fmt = "{}%", .0)] #[debug("{}%", _0)]
Percent(f32), Percent(f32),
#[display(fmt = "{}px", .0)] #[display("{}px", _0)]
#[debug(fmt = "{}px", .0)] #[debug("{}px", _0)]
#[default] #[default]
Pixels(i32), Pixels(i32),
} }
@ -58,7 +58,7 @@ impl FromStr for NumWithUnit {
} }
#[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, Default)] #[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, Default)]
#[display(fmt = "{}*{}", x, y)] #[display("{}*{}", x, y)]
pub struct Coords { pub struct Coords {
pub x: NumWithUnit, pub x: NumWithUnit,
pub y: NumWithUnit, pub y: NumWithUnit,

View file

@ -1,6 +1,11 @@
(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock); (import (
in fetchTarball { let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url = url =
"https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; lock.nodes.flake-compat.locked.url
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash; sha256 = lock.nodes.flake-compat.locked.narHash;
}) { src = ./.; }).defaultNix }
) { src = ./.; }).defaultNix

View file

@ -61,7 +61,7 @@ This field can be:
- the string `<primary>`, in which case eww tries to identify the primary display (which may fail, especially on wayland) - the string `<primary>`, in which case eww tries to identify the primary display (which may fail, especially on wayland)
- an integer, declaring the monitor index - an integer, declaring the monitor index
- the name of the monitor - the name of the monitor
- a string containing a JSON-array of monitor matchers, such as: `'["<primary>" "HDMI-A-1" "PHL 345B1C" 0]'`. Eww will try to find a match in order, allowing you to specify fallbacks. - a string containing a JSON-array of monitor matchers, such as: `'["<primary>", "HDMI-A-1", "PHL 345B1C", 0]'`. Eww will try to find a match in order, allowing you to specify fallbacks.
**`geometry`-properties** **`geometry`-properties**
@ -87,10 +87,10 @@ Depending on if you are using X11 or Wayland, some additional properties exist:
#### Wayland #### Wayland
| Property | Description | | Property | Description |
| ----------: | ------------------------------------------------------------ | | ----------: |------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `stacking` | Where the window should appear in the stack. Possible values: `fg`, `bg`, `overlay`, `bottom`. | | `stacking` | Where the window should appear in the stack. Possible values: `fg`, `bg`, `overlay`, `bottom`. |
| `exclusive` | Whether the compositor should reserve space for the window automatically. Either `true` or `false`. | | `exclusive` | Whether the compositor should reserve space for the window automatically. Either `true` or `false`. If `true` `:anchor` has to include `center`. |
| `focusable` | Whether the window should be able to be focused. This is necessary for any widgets that use the keyboard to work. Either `true` or `false`. | | `focusable` | Whether the window should be able to be focused. This is necessary for any widgets that use the keyboard to work. Possible values: `none`, `exclusive` and `ondemand`. |
| `namespace` | Set the wayland layersurface namespace eww uses. Accepts a `string` value. | | `namespace` | Set the wayland layersurface namespace eww uses. Accepts a `string` value. |
@ -206,9 +206,12 @@ This may be the most commonly used type of variable.
They are useful to access any quickly retrieved value repeatedly, They are useful to access any quickly retrieved value repeatedly,
and thus are the perfect choice for showing your time, date, as well as other bits of information such as pending package updates, weather, and battery level. and thus are the perfect choice for showing your time, date, as well as other bits of information such as pending package updates, weather, and battery level.
You can also specify an initial-value. This should prevent eww from waiting for the result of a give command during startup, thus You can also specify an initial-value. This should prevent eww from waiting for the result of a given command during startup, thus
making the startup time faster. making the startup time faster.
To externally update a polling variable, `eww update` can be used like with basic variables to assign a value.
You can also call `eww poll` to poll the variable outside of its usual interval, or even while it isn't running at all.
**Listening variables (`deflisten`)** **Listening variables (`deflisten`)**
```lisp ```lisp
@ -381,6 +384,8 @@ If you want to display a list of values, you can use the `for`-Element to fill a
This can be useful in many situations, for example when generating a workspace list from a JSON representation of your workspaces. This can be useful in many situations, for example when generating a workspace list from a JSON representation of your workspaces.
In many cases, this can be used instead of `literal`, and should most likely be preferred in those cases. In many cases, this can be used instead of `literal`, and should most likely be preferred in those cases.
To see how to declare and use more advanced data structures, check out the [data structures example](/examples/data-structures/eww.yuck).
## Splitting up your configuration ## Splitting up your configuration
As time passes, your configuration might grow larger and larger. Luckily, you can easily split up your configuration into multiple files! As time passes, your configuration might grow larger and larger. Luckily, you can easily split up your configuration into multiple files!

View file

@ -4,3 +4,7 @@ These configurations of eww are available in the `examples/` directory of the [r
An eww bar configuration: An eww bar configuration:
![Example bar](https://github.com/elkowar/eww/raw/master/examples/eww-bar/eww-bar.png) ![Example bar](https://github.com/elkowar/eww/raw/master/examples/eww-bar/eww-bar.png)
A demo on how to declare and use data structures:
![Data structure example](https://github.com/elkowar/eww/raw/master/examples/data-structures/data-structures-preview.png)

View file

@ -24,14 +24,16 @@ Supported currently are the following features:
- comparisons (`==`, `!=`, `>`, `<`, `<=`, `>=`) - comparisons (`==`, `!=`, `>`, `<`, `<=`, `>=`)
- boolean operations (`||`, `&&`, `!`) - boolean operations (`||`, `&&`, `!`)
- regex match operator (`=~`) - regex match operator (`=~`)
- Rust regex style, left hand is regex, right hand is string
- ex: workspace.name =~ '^special:.+$'
- elvis operator (`?:`) - elvis operator (`?:`)
- if the left side is `""` or a JSON `null`, then returns the right side, - if the left side is `""` or a JSON `null`, then returns the right side,
otherwise evaluates to the left side. otherwise evaluates to the left side.
- Safe Access operator (`?.`) or (`?.[index]`) - Safe Access operator (`?.`) or (`?.[index]`)
- if the left side is `""` or a JSON `null`, then return `null`. Otherwise, - if the left side is an empty string or a JSON `null`, then return `null`. Otherwise,
attempt to index. attempt to index. Note that indexing an empty JSON string (`'""'`) is an error.
- This can still cause an error to occur if the left hand side exists but is - This can still cause an error to occur if the left hand side exists but is
not an object. not an object or an array.
(`Number` or `String`). (`Number` or `String`).
- conditionals (`condition ? 'value' : 'other value'`) - conditionals (`condition ? 'value' : 'other value'`)
- numbers, strings, booleans and variable references (`12`, `'hi'`, `true`, `some_variable`) - numbers, strings, booleans and variable references (`12`, `'hi'`, `true`, `some_variable`)
@ -39,7 +41,12 @@ Supported currently are the following features:
- for this, the object/array value needs to refer to a variable that contains a valid json string. - for this, the object/array value needs to refer to a variable that contains a valid json string.
- some function calls: - some function calls:
- `round(number, decimal_digits)`: Round a number to the given amount of decimals - `round(number, decimal_digits)`: Round a number to the given amount of decimals
- `floor(number)`: Round a number down to the nearest integer
- `ceil(number)`: Round a number up to the nearest integer
- `sin(number)`, `cos(number)`, `tan(number)`, `cot(number)`: Calculate the trigonometric value of a given number in **radians** - `sin(number)`, `cos(number)`, `tan(number)`, `cot(number)`: Calculate the trigonometric value of a given number in **radians**
- `min(a, b)`, `max(a, b)`: Get the smaller or bigger number out of two given numbers
- `powi(num, n)`, `powf(num, n)`: Raise number `num` to power `n`. `powi` expects `n` to be of type `i32`
- `log(num, n)`: Calculate the base `n` logarithm of `num`. `num`, `n` and return type are `f64`
- `degtorad(number)`: Converts a number from degrees to radians - `degtorad(number)`: Converts a number from degrees to radians
- `radtodeg(number)`: Converts a number from radians to degrees - `radtodeg(number)`: Converts a number from radians to degrees
- `replace(string, regex, replacement)`: Replace matches of a given regex in a string - `replace(string, regex, replacement)`: Replace matches of a given regex in a string
@ -50,7 +57,10 @@ Supported currently are the following features:
- `substring(string, start, length)`: Return a substring of given length starting at the given index - `substring(string, start, length)`: Return a substring of given length starting at the given index
- `arraylength(value)`: Gets the length of the array - `arraylength(value)`: Gets the length of the array
- `objectlength(value)`: Gets the amount of entries in the object - `objectlength(value)`: Gets the amount of entries in the object
- `jq(value, jq_filter_string)`: run a [jq](https://stedolan.github.io/jq/manual/) style command on a json value. (Uses [jaq](https://crates.io/crates/jaq) internally). - `jq(value, jq_filter_string)`: run a [jq](https://jqlang.github.io/jq/manual/) style command on a json value. (Uses [jaq](https://crates.io/crates/jaq) internally).
- `jq(value, jq_filter_string, args)`: Emulate command line flags for jq, see [the docs](https://jqlang.github.io/jq/manual/#invoking-jq) on invoking jq for details. Invalid flags are silently ignored.
Currently supported flags:
- `"r"`: If the result is a string, it won't be formatted as a JSON string. The equivalent jq flag is `--raw-output`.
- `get_env(string)`: Gets the specified enviroment variable - `get_env(string)`: Gets the specified enviroment variable
- `formattime(unix_timestamp, format_str, timezone)`: Gets the time in a given format from UNIX timestamp. - `formattime(unix_timestamp, format_str, timezone)`: Gets the time in a given format from UNIX timestamp.
Check [chrono's documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) for more Check [chrono's documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) for more
@ -60,3 +70,8 @@ Supported currently are the following features:
Same as other `formattime`, but does not accept timezone. Instead, it uses system's local timezone. Same as other `formattime`, but does not accept timezone. Instead, it uses system's local timezone.
Check [chrono's documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) for more Check [chrono's documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) for more
information about format string. information about format string.
- `formatbytes(bytes, short, format_mode)`: Display bytes in a human-readable format.
Arguments:
- `bytes`: `i64` of bytes, supports negative sizes.
- `short`: set true for a compact version (default: false)
- `format_mode`: set to either to "iec" (eg. `1.0 GiB`) or "si" (eg. `1.2 GB`) (default: "iec")

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

@ -0,0 +1,29 @@
* {
all: unset;
}
.layout {
padding: 8px;
border: 1px solid black;
border-radius: 4px;
background-color: bisque;
font-size: 16px;
color: black;
}
.animalLayout {
margin: 0 4px;
}
.animal {
font-size: 24px;
transition: 0.2s;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0);
border: 0 solid lightcoral;
}
.animal.selected {
background-color: rgba(0, 0, 0, 0.2);
border-width: 2px;
}

View file

@ -0,0 +1,73 @@
(defvar stringArray `[
"🦝",
"🐱",
"🐵",
"🦁",
"🐹",
"🦊"
]`)
(defvar object `{
"🦝": "racoon",
"🐱": "cat",
"🐵": "ape",
"🦁": "lion",
"🐹": "hamster",
"🦊": "fox"
}`)
; You could also create an array of objects:
; (defvar objectArray `[{ "emoji": "🦝", "name": "racoon" }, { "emoji": "🦊", "name": "fox" }]`)
(defvar selected `🦝`)
(defwidget animalButton [emoji]
(box
:class "animalLayout"
(eventbox
:class `animal ${selected == emoji ? "selected" : ""}`
:cursor "pointer"
:onhover "eww update selected=${emoji}"
emoji
)
)
)
(defwidget animalRow []
(box
:class "animals"
:orientation "horizontal"
:halign "center"
(for animal in stringArray
(animalButton
:emoji animal
)
)
)
)
(defwidget currentAnimal []
(box
`${object[selected]} ${selected}`
)
)
(defwidget layout []
(box
:class "layout"
:orientation "vertical"
:halign "center"
(animalRow)
(currentAnimal)
)
)
(defwindow data-structures
:monitor 0
:exclusive false
:focusable none
:geometry (geometry
:anchor "center"
)
(layout)
)

View file

@ -22,6 +22,7 @@
color: #000000; color: #000000;
border-radius: 10px; border-radius: 10px;
} }
.metric scale trough { .metric scale trough {
all: unset; all: unset;
background-color: #4e4e4e; background-color: #4e4e4e;
@ -31,24 +32,11 @@
margin-left: 10px; margin-left: 10px;
margin-right: 20px; margin-right: 20px;
} }
.metric scale trough highlight {
all: unset;
background-color: #D35D6E;
color: #000000;
border-radius: 10px;
}
.metric scale trough {
all: unset;
background-color: #4e4e4e;
border-radius: 50px;
min-height: 3px;
min-width: 50px;
margin-left: 10px;
margin-right: 20px;
}
.label-ram { .label-ram {
font-size: large; font-size: large;
} }
.workspaces button:hover { .workspaces button:hover {
color: #D35D6E; color: #D35D6E;
} }

54
flake.lock generated
View file

@ -1,46 +1,28 @@
{ {
"nodes": { "nodes": {
"flake-compat": { "flake-compat": {
"flake": false,
"locked": { "locked": {
"lastModified": 1696426674, "lastModified": 1709944340,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "narHash": "sha256-xr54XK0SjczlUxRo5YwodibUSlpivS9bqHt8BNyWVQA=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "rev": "baa7aa7bd0a570b3b9edd0b8da859fee3ffaa4d4",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "edolstra", "owner": "edolstra",
"ref": "refs/pull/65/head",
"repo": "flake-compat", "repo": "flake-compat",
"type": "github" "type": "github"
} }
}, },
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1708407374, "lastModified": 1725534445,
"narHash": "sha256-EECzarm+uqnNDCwaGg/ppXCO11qibZ1iigORShkkDf0=", "narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "f33dd27a47ebdf11dc8a5eb05e7c8fbdaf89e73f", "rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -59,17 +41,16 @@
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1708395022, "lastModified": 1725675754,
"narHash": "sha256-pxHZbfDsLAAcyWz+snbudxhQPlAnK2nWGAqRx11veac=", "narHash": "sha256-hXW3csqePOcF2e/PYnpXj72KEYyNj2HzTrVNmS/F7Ug=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "b4ae18c03af976549a0b6e396b2b5be56d275f8b", "rev": "8cc45e678e914a16c8e224c3237fb07cf21e5e54",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -77,21 +58,6 @@
"repo": "rust-overlay", "repo": "rust-overlay",
"type": "github" "type": "github"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

132
flake.nix
View file

@ -1,9 +1,6 @@
{ {
inputs = { inputs = {
flake-compat = { flake-compat.url = "github:edolstra/flake-compat/refs/pull/65/head";
url = "github:edolstra/flake-compat";
flake = false;
};
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
rust-overlay = { rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
@ -11,76 +8,89 @@
}; };
}; };
outputs = { self, nixpkgs, rust-overlay, flake-compat }: outputs =
{
self,
nixpkgs,
rust-overlay,
flake-compat,
}:
let let
pkgsFor = system: overlays = [
import nixpkgs { (import rust-overlay)
inherit system; self.overlays.default
overlays = [ self.overlays.default rust-overlay.overlays.default ]; ];
}; pkgsFor = system: import nixpkgs { inherit system overlays; };
targetSystems = [ "aarch64-linux" "x86_64-linux" ]; targetSystems = [
mkRustToolchain = pkgs: "aarch64-linux"
pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; "x86_64-linux"
in { ];
overlays.default = final: prev: mkRustToolchain = pkgs: pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
let in
rust = mkRustToolchain final; {
overlays.default = final: prev: { inherit (self.packages.${prev.system}) eww eww-wayland; };
rustPlatform = prev.makeRustPlatform { packages = nixpkgs.lib.genAttrs targetSystems (
cargo = rust; system:
rustc = rust;
};
in {
eww = (prev.eww.override { inherit rustPlatform; }).overrideAttrs
(old: {
version = self.rev or "dirty";
src = builtins.path {
name = "eww";
path = prev.lib.cleanSource ./.;
};
cargoDeps =
rustPlatform.importCargoLock { lockFile = ./Cargo.lock; };
patches = [ ];
# remove this when nixpkgs includes it
buildInputs = old.buildInputs ++ [ final.libdbusmenu-gtk3 ];
});
eww-wayland = final.eww;
};
packages = nixpkgs.lib.genAttrs targetSystems (system:
let pkgs = pkgsFor system;
in (self.overlays.default pkgs pkgs) // {
default = self.packages.${system}.eww;
});
devShells = nixpkgs.lib.genAttrs targetSystems (system:
let let
pkgs = pkgsFor system; pkgs = pkgsFor system;
rust = mkRustToolchain pkgs; rust = mkRustToolchain pkgs;
in { rustPlatform = pkgs.makeRustPlatform {
default = pkgs.mkShell { cargo = rust;
packages = with pkgs; [ rustc = rust;
rust };
rust-analyzer-unwrapped version = (builtins.fromTOML (builtins.readFile ./crates/eww/Cargo.toml)).package.version;
gcc in
glib rec {
gdk-pixbuf eww = rustPlatform.buildRustPackage {
librsvg version = "${version}-dirty";
libdbusmenu-gtk3 pname = "eww";
gtk3
gtk-layer-shell src = ./.;
cargoLock.lockFile = ./Cargo.lock;
cargoBuildFlags = [
"--bin"
"eww"
];
nativeBuildInputs = with pkgs; [
pkg-config pkg-config
wrapGAppsHook
];
buildInputs = with pkgs; [
gtk3
librsvg
gtk-layer-shell
libdbusmenu-gtk3
];
};
eww-wayland = nixpkgs.lib.warn "`eww-wayland` is deprecated due to eww building with both X11 and wayland support by default. Use `eww` instead." eww;
default = eww;
}
);
devShells = nixpkgs.lib.genAttrs targetSystems (
system:
let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in
{
default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.eww ];
packages = with pkgs; [
deno deno
mdbook mdbook
zbus-xmlgen
]; ];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library"; RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
}; };
}); }
);
formatter = formatter = nixpkgs.lib.genAttrs targetSystems (system: (pkgsFor system).nixfmt-rfc-style);
nixpkgs.lib.genAttrs targetSystems (system: (pkgsFor system).nixfmt);
}; };
} }

View file

@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "1.76.0" channel = "1.81.0"
components = [ "rust-src" ] components = [ "rust-src" ]
profile = "default" profile = "default"

View file

@ -1,6 +1,11 @@
(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock); (import (
in fetchTarball { let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url = url =
"https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; lock.nodes.flake-compat.locked.url
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash; sha256 = lock.nodes.flake-compat.locked.narHash;
}) { src = ./.; }).shellNix }
) { src = ./.; }).shellNix