commit
5eb930c39b
50 changed files with 2198 additions and 1454 deletions
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -37,3 +37,9 @@ body:
|
|||
description: "If applicable, provide additional context or screenshots here."
|
||||
validations:
|
||||
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
|
||||
|
|
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -4,6 +4,45 @@ All notable changes to eww will be listed here, starting at changes since versio
|
|||
|
||||
## 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)
|
||||
|
||||
### 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)
|
||||
- Add Vanilla CSS support (By: Ezequiel Ramis)
|
||||
- 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 `EWW_TIME` magic variable (By: Erenoit)
|
||||
- Add trigonometric functions (`sin`, `cos`, `tan`, `cot`) and degree/radian conversions (`degtorad`, `radtodeg`) (By: end-4)
|
||||
|
|
1979
Cargo.lock
generated
1979
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
57
Cargo.toml
57
Cargo.toml
|
@ -6,53 +6,62 @@ resolver = "2"
|
|||
|
||||
simplexpr = { version = "0.1.0", path = "crates/simplexpr" }
|
||||
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" }
|
||||
|
||||
anyhow = "1.0.79"
|
||||
anyhow = "1.0.86"
|
||||
bincode = "1.3.3"
|
||||
cached = "0.48.0"
|
||||
chrono = "0.4.26"
|
||||
chrono-tz = "0.8.2"
|
||||
clap = {version = "4.5.1", features = ["derive"] }
|
||||
clap_complete = "4.5.1"
|
||||
bytesize = "2.0.1"
|
||||
cached = "0.53.1"
|
||||
chrono = "0.4.38"
|
||||
chrono-tz = "0.10.0"
|
||||
clap = { version = "4.5.1", features = ["derive"] }
|
||||
clap_complete = "4.5.12"
|
||||
codespan-reporting = "0.11"
|
||||
derive_more = "0.99"
|
||||
derive_more = { version = "1", features = [
|
||||
"as_ref",
|
||||
"debug",
|
||||
"display",
|
||||
"from",
|
||||
"from_str",
|
||||
] }
|
||||
extend = "1.2"
|
||||
futures = "0.3.28"
|
||||
grass = {version = "0.13.1", default-features = false}
|
||||
futures = "0.3.30"
|
||||
grass = "0.13.4"
|
||||
gtk = "0.18.1"
|
||||
insta = "1.7"
|
||||
itertools = "0.12.1"
|
||||
jaq-core = "1.2.1"
|
||||
jaq-parse = "1.0.2"
|
||||
jaq-std = {version = "1.2.1", features = ["bincode"]}
|
||||
jaq-interpret = "1.2.1"
|
||||
jaq-syn = "1.1.0"
|
||||
lalrpop = { version = "0.20.0", features = ["unicode"] }
|
||||
lalrpop-util = { version = "0.20.0", features = ["unicode"] }
|
||||
itertools = "0.13.0"
|
||||
jaq-core = "1.5.1"
|
||||
jaq-parse = "1.0.3"
|
||||
jaq-std = "1.6.0"
|
||||
jaq-interpret = "1.5.0"
|
||||
jaq-syn = "1.6.0"
|
||||
lalrpop = { version = "0.21", features = ["unicode"] }
|
||||
lalrpop-util = { version = "0.21", features = ["unicode"] }
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
maplit = "1"
|
||||
nix = "0.27.1"
|
||||
nix = "0.29.0"
|
||||
notify = "6.1.1"
|
||||
once_cell = "1.19"
|
||||
pretty_assertions = "1.4.0"
|
||||
pretty_env_logger = "0.5.0"
|
||||
ref-cast = "1.0.22"
|
||||
regex = "1.10.3"
|
||||
regex = "1.10.5"
|
||||
serde_json = "1.0"
|
||||
serde = {version = "1.0", features = ["derive"]}
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
simple-signal = "1.1"
|
||||
smart-default = "0.7.1"
|
||||
static_assertions = "1.1.0"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
sysinfo = "0.30.5"
|
||||
sysinfo = "0.31.2"
|
||||
thiserror = "1.0"
|
||||
tokio-util = "0.7.8"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
tokio-util = "0.7.11"
|
||||
tokio = { version = "1.39.2", features = ["full"] }
|
||||
unescape = "0.1"
|
||||
wait-timeout = "0.2"
|
||||
zbus = { version = "3.15.2", default-features = false, features = ["tokio"] }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
|
22
README.md
22
README.md
|
@ -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/).
|
||||
|
||||
## Eww needs your opinion!
|
||||
I've hit a bit of a design roadblock for one of the bigger features that are in the works right now.
|
||||
## Check out another cool project by me
|
||||
|
||||
**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
|
||||
|
||||
|
@ -59,14 +63,26 @@ I've hit a bit of a design roadblock for one of the bigger features that are in
|
|||
|
||||
</div>
|
||||
|
||||
* [Activate Linux by Nycta](https://github.com/Nycta-b424b3c7/eww_activate-linux)
|
||||
|
||||
<div align="left">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Contribewwting
|
||||
|
||||
If you want to contribute anything, like adding new widgets, features, or subcommands (including sample configs), you should definitely do so.
|
||||
|
||||
### Steps
|
||||
|
||||
1. Fork this repository
|
||||
2. Install dependencies
|
||||
3. Smash your head against the keyboard from frustration (coding is hard)
|
||||
4. Write down your changes in CHANGELOG.md
|
||||
5. Open a pull request once you're finished
|
||||
|
||||
## Widget
|
||||
|
||||
https://en.wikipedia.org/wiki/Wikipedia:Widget
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "eww"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"]
|
||||
description = "Widgets for everyone!"
|
||||
license = "MIT"
|
||||
|
@ -9,7 +9,6 @@ homepage = "https://github.com/elkowar/eww"
|
|||
edition = "2021"
|
||||
|
||||
|
||||
|
||||
[features]
|
||||
default = ["x11", "wayland"]
|
||||
x11 = ["gdkx11", "x11rb"]
|
||||
|
@ -21,34 +20,25 @@ eww_shared_util.workspace = true
|
|||
yuck.workspace = true
|
||||
notifier_host.workspace = true
|
||||
|
||||
gtk = "0.17.1"
|
||||
gdk = "0.17.1"
|
||||
pango = "0.17.1"
|
||||
glib = "0.17.8"
|
||||
glib-macros = "0.17.8"
|
||||
gtk-layer-shell = { version = "0.8.1", optional = true, features=["v0_6"] }
|
||||
gdkx11 = { version = "0.18", optional = true }
|
||||
x11rb = { version = "0.13.1", features = ["randr"], optional = true }
|
||||
gdk-sys = "0.18.0"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
grass.workspace = true
|
||||
anyhow.workspace = true
|
||||
bincode.workspace = true
|
||||
chrono.workspace = true
|
||||
clap = {workspace = true, features = ["derive"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
clap_complete.workspace = true
|
||||
codespan-reporting.workspace = true
|
||||
derive_more.workspace = true
|
||||
extend.workspace = true
|
||||
futures.workspace = true
|
||||
grass = {workspace = true, default-features = false}
|
||||
gtk.workspace = true
|
||||
itertools.workspace = true
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
|
@ -59,13 +49,14 @@ once_cell.workspace = true
|
|||
pretty_env_logger.workspace = true
|
||||
regex.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde = {workspace = true, features = ["derive"]}
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
simple-signal.workspace = true
|
||||
sysinfo = { workspace = true, features = ["linux-netdevs"] }
|
||||
sysinfo = { workspace = true }
|
||||
tokio-util.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
unescape.workspace = true
|
||||
wait-timeout.workspace = true
|
||||
zbus = { workspace = true, default-features = false, features = ["tokio"] }
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "Penelope Gwen <support@pogmom.me>"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::{
|
||||
config,
|
||||
daemon_response::DaemonResponseSender,
|
||||
display_backend::DisplayBackend,
|
||||
error_handling_ctx,
|
||||
|
@ -17,12 +16,14 @@ use codespan_reporting::files::Files;
|
|||
use eww_shared_util::{Span, VarName};
|
||||
use gdk::Monitor;
|
||||
use glib::ObjectExt;
|
||||
use gtk::{gdk, glib};
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use simplexpr::{dynval::DynVal, SimplExpr};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
marker::PhantomData,
|
||||
rc::Rc,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
@ -44,6 +45,7 @@ use yuck::{
|
|||
pub enum DaemonCommand {
|
||||
NoOp,
|
||||
UpdateVars(Vec<(VarName, DynVal)>),
|
||||
PollVars(Vec<VarName>),
|
||||
ReloadConfigAndCss(DaemonResponseSender),
|
||||
OpenInspector,
|
||||
OpenMany {
|
||||
|
@ -66,6 +68,7 @@ pub enum DaemonCommand {
|
|||
},
|
||||
CloseWindows {
|
||||
windows: Vec<String>,
|
||||
auto_reopen: bool,
|
||||
sender: DaemonResponseSender,
|
||||
},
|
||||
KillServer,
|
||||
|
@ -87,10 +90,6 @@ pub enum DaemonCommand {
|
|||
/// An opened window.
|
||||
#[derive(Debug)]
|
||||
pub struct EwwWindow {
|
||||
/// Every window has an id, uniquely identifying it.
|
||||
/// If no specific ID was specified whilst starting the window,
|
||||
/// this will be the same as the window name.
|
||||
pub instance_id: String,
|
||||
pub name: String,
|
||||
pub scope_index: ScopeIndex,
|
||||
pub gtk_window: Window,
|
||||
|
@ -111,11 +110,13 @@ impl EwwWindow {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct App<B> {
|
||||
pub display_backend: B,
|
||||
pub struct App<B: DisplayBackend> {
|
||||
pub scope_graph: Rc<RefCell<ScopeGraph>>,
|
||||
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 instance_id_to_args: HashMap<String, WindowArguments>,
|
||||
/// 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 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 {
|
||||
f.debug_struct("App")
|
||||
.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> {
|
||||
/// Handle a [`DaemonCommand`] event, logging any errors that occur.
|
||||
pub fn handle_command(&mut self, event: DaemonCommand) {
|
||||
if let Err(err) = self.try_handle_command(event) {
|
||||
pub async fn handle_command(&mut self, event: DaemonCommand) {
|
||||
if let Err(err) = self.try_handle_command(event).await {
|
||||
error_handling_ctx::print_error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
match event {
|
||||
DaemonCommand::NoOp => {}
|
||||
|
@ -167,7 +187,16 @@ impl<B: DisplayBackend> App<B> {
|
|||
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) => {
|
||||
// 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 config_result = config::read_from_eww_paths(&self.paths);
|
||||
|
@ -194,7 +223,7 @@ impl<B: DisplayBackend> App<B> {
|
|||
DaemonCommand::CloseAll => {
|
||||
log::info!("Received close command, closing all windows");
|
||||
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 } => {
|
||||
|
@ -203,7 +232,7 @@ impl<B: DisplayBackend> App<B> {
|
|||
.map(|w| {
|
||||
let (config_name, id) = w;
|
||||
if should_toggle && self.open_windows.contains_key(id) {
|
||||
self.close_window(id)
|
||||
self.close_window(id, false)
|
||||
} else {
|
||||
log::debug!("Config: {}, id: {}", config_name, id);
|
||||
let window_args = args
|
||||
|
@ -234,7 +263,7 @@ impl<B: DisplayBackend> App<B> {
|
|||
let is_open = self.open_windows.contains_key(&instance_id);
|
||||
|
||||
let result = if should_toggle && is_open {
|
||||
self.close_window(&instance_id)
|
||||
self.close_window(&instance_id, false)
|
||||
} else {
|
||||
self.open_window(&WindowArguments {
|
||||
instance_id,
|
||||
|
@ -250,9 +279,10 @@ impl<B: DisplayBackend> App<B> {
|
|||
|
||||
sender.respond_with_result(result)?;
|
||||
}
|
||||
DaemonCommand::CloseWindows { windows, sender } => {
|
||||
let errors = windows.iter().map(|window| self.close_window(window)).filter_map(Result::err);
|
||||
sender.respond_with_error_list(errors)?;
|
||||
DaemonCommand::CloseWindows { windows, auto_reopen, sender } => {
|
||||
let errors = windows.iter().map(|window| self.close_window(window, auto_reopen)).filter_map(Result::err);
|
||||
// Ignore sending errors, as the channel might already be closed
|
||||
let _ = sender.respond_with_error_list(errors);
|
||||
}
|
||||
DaemonCommand::PrintState { all, sender } => {
|
||||
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
|
||||
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) {
|
||||
_ = old_abort_send.send(());
|
||||
}
|
||||
|
@ -357,7 +404,17 @@ impl<B: DisplayBackend> App<B> {
|
|||
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);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -369,7 +426,7 @@ impl<B: DisplayBackend> App<B> {
|
|||
|
||||
// if an instance of this is already running, close it
|
||||
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());
|
||||
|
@ -422,9 +479,17 @@ impl<B: DisplayBackend> App<B> {
|
|||
move |_| {
|
||||
// we don't care about the actual error response from the daemon as this is mostly just a fallback.
|
||||
// Generally, this should get disconnected before the gtk window gets destroyed.
|
||||
// It serves as a fallback for when the window is closed manually.
|
||||
// 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 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) {
|
||||
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! {
|
||||
_ = glib::timeout_future(duration) => {
|
||||
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) {
|
||||
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();
|
||||
|
||||
Ok(EwwWindow {
|
||||
instance_id: window_init.id.clone(),
|
||||
name: window_init.name.clone(),
|
||||
gtk_window: window,
|
||||
scope_index: window_scope,
|
||||
|
@ -626,6 +690,18 @@ fn get_gdk_monitor(identifier: Option<MonitorIdentifier>) -> Result<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.
|
||||
/// Outside of x11, only [MonitorIdentifier::Numeric] is supported
|
||||
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) => {
|
||||
for m in 0..display.n_monitors() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,11 +30,11 @@ macro_rules! 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 }
|
||||
"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 }
|
||||
"EWW_RAM" [2] => || Ok(DynVal::from(get_ram())),
|
||||
|
||||
|
@ -42,7 +42,7 @@ define_builtin_vars! {
|
|||
// @prop { <mount_point>: { name, total, free, used, used_perc } }
|
||||
"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 } }
|
||||
"EWW_BATTERY" [2] => || Ok(DynVal::from(
|
||||
match get_battery_capacity() {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
#[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 = "linux"))]
|
||||
#[cfg(not(target_os = "netbsd"))]
|
||||
#[cfg(not(target_os = "freebsd"))]
|
||||
#[cfg(not(target_os = "openbsd"))]
|
||||
pub fn get_battery_capacity() -> Result<String> {
|
||||
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();
|
||||
|
||||
networks.refresh_list();
|
||||
networks.refresh();
|
||||
let elapsed = last_refresh.next_refresh();
|
||||
|
||||
networks
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::{widgets::window::Window, window_initiator::WindowInitiator};
|
||||
|
||||
use gtk::gdk;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub use platform_wayland::WaylandBackend;
|
||||
|
||||
|
@ -8,6 +10,7 @@ pub use platform_x11::{set_xprops, X11Backend};
|
|||
|
||||
pub trait DisplayBackend: Send + Sync + 'static {
|
||||
const IS_X11: bool;
|
||||
const IS_WAYLAND: bool;
|
||||
|
||||
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 {
|
||||
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> {
|
||||
Some(Window::new(gtk::WindowType::Toplevel, x, y))
|
||||
|
@ -24,26 +28,29 @@ impl DisplayBackend for NoBackend {
|
|||
|
||||
#[cfg(feature = "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 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;
|
||||
|
||||
impl DisplayBackend for WaylandBackend {
|
||||
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> {
|
||||
let window = Window::new(gtk::WindowType::Toplevel, x, y);
|
||||
// Initialising a layer shell surface
|
||||
gtk_layer_shell::init_for_window(&window);
|
||||
window.init_layer_shell();
|
||||
// Sets the monitor where the surface is shown
|
||||
if let Some(ident) = window_init.monitor.clone() {
|
||||
let display = gdk::Display::default().expect("could not get default display");
|
||||
if let Some(monitor) = crate::app::get_monitor_from_display(&display, &ident) {
|
||||
gtk_layer_shell::set_monitor(&window, &monitor);
|
||||
window.set_monitor(&monitor);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
@ -52,18 +59,22 @@ mod platform_wayland {
|
|||
|
||||
// Sets the layer where the layer shell surface will spawn
|
||||
match window_init.stacking {
|
||||
WindowStacking::Foreground => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Top),
|
||||
WindowStacking::Background => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Background),
|
||||
WindowStacking::Bottom => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Bottom),
|
||||
WindowStacking::Overlay => gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Overlay),
|
||||
WindowStacking::Foreground => window.set_layer(gtk_layer_shell::Layer::Top),
|
||||
WindowStacking::Background => window.set_layer(gtk_layer_shell::Layer::Background),
|
||||
WindowStacking::Bottom => window.set_layer(gtk_layer_shell::Layer::Bottom),
|
||||
WindowStacking::Overlay => window.set_layer(gtk_layer_shell::Layer::Overlay),
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
// Positioning surface
|
||||
|
@ -83,27 +94,34 @@ mod platform_wayland {
|
|||
AnchorAlignment::END => bottom = true,
|
||||
}
|
||||
|
||||
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Left, left);
|
||||
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Right, right);
|
||||
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Top, top);
|
||||
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Bottom, bottom);
|
||||
window.set_anchor(gtk_layer_shell::Edge::Left, left);
|
||||
window.set_anchor(gtk_layer_shell::Edge::Right, right);
|
||||
window.set_anchor(gtk_layer_shell::Edge::Top, top);
|
||||
window.set_anchor(gtk_layer_shell::Edge::Bottom, bottom);
|
||||
|
||||
let xoffset = geometry.offset.x.pixels_relative_to(monitor.width());
|
||||
let yoffset = geometry.offset.y.pixels_relative_to(monitor.height());
|
||||
|
||||
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 {
|
||||
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 {
|
||||
gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Bottom, yoffset);
|
||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Bottom, yoffset);
|
||||
} 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 {
|
||||
gtk_layer_shell::auto_exclusive_zone_enable(&window);
|
||||
window.auto_exclusive_zone_enable();
|
||||
}
|
||||
Some(window)
|
||||
}
|
||||
|
@ -115,6 +133,7 @@ mod platform_x11 {
|
|||
use crate::{widgets::window::Window, window_initiator::WindowInitiator};
|
||||
use anyhow::{Context, Result};
|
||||
use gdk::Monitor;
|
||||
use gtk::gdk;
|
||||
use gtk::{self, prelude::*};
|
||||
use x11rb::protocol::xproto::ConnectionExt;
|
||||
|
||||
|
@ -134,6 +153,7 @@ mod platform_x11 {
|
|||
pub struct X11Backend;
|
||||
impl DisplayBackend for X11Backend {
|
||||
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> {
|
||||
let window_type =
|
||||
|
|
|
@ -31,9 +31,6 @@ pub fn print_error(err: anyhow::Error) {
|
|||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use derive_more::*;
|
||||
use derive_more::{Debug, *};
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display)]
|
||||
#[display(fmt = ".x*.y:.width*.height")]
|
||||
#[display(".x*.y:.width*.height")]
|
||||
pub struct Rect {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
|
|
|
@ -53,28 +53,31 @@ fn main() {
|
|||
return;
|
||||
}
|
||||
|
||||
let detected_wayland = detect_wayland();
|
||||
#[allow(unused)]
|
||||
let use_wayland = opts.force_wayland || detect_wayland();
|
||||
let use_wayland = opts.force_wayland || detected_wayland;
|
||||
#[cfg(all(feature = "wayland", feature = "x11"))]
|
||||
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 {
|
||||
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"))]
|
||||
let result = {
|
||||
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")))]
|
||||
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")))]
|
||||
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 {
|
||||
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"))
|
||||
}
|
||||
|
||||
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
|
||||
.config_path
|
||||
.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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
// 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;
|
||||
if let (Some(recv), true) = (response_recv, is_parent) {
|
||||
listen_for_daemon_response(recv);
|
||||
|
|
|
@ -98,6 +98,16 @@ pub enum ActionWithServer {
|
|||
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
|
||||
#[command(name = "inspector", alias = "debugger")]
|
||||
OpenInspector,
|
||||
|
@ -254,6 +264,7 @@ impl ActionWithServer {
|
|||
pub fn into_daemon_command(self) -> (app::DaemonCommand, Option<daemon_response::DaemonResponseReceiver>) {
|
||||
let command = match self {
|
||||
ActionWithServer::Update { mappings } => app::DaemonCommand::UpdateVars(mappings),
|
||||
ActionWithServer::Poll { names } => app::DaemonCommand::PollVars(names),
|
||||
ActionWithServer::OpenInspector => app::DaemonCommand::OpenInspector,
|
||||
|
||||
ActionWithServer::KillServer => app::DaemonCommand::KillServer,
|
||||
|
@ -281,7 +292,7 @@ impl ActionWithServer {
|
|||
})
|
||||
}
|
||||
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::ListWindows => return with_response_channel(app::DaemonCommand::ListWindows),
|
||||
|
|
|
@ -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 {
|
||||
VarSource::Shell(span, command) => {
|
||||
script_var::run_command(command).map_err(|e| anyhow!(create_script_var_failed_warn(*span, &var.name, &e.to_string())))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
app::{self, DaemonCommand},
|
||||
app::{self, App, DaemonCommand},
|
||||
config, daemon_response,
|
||||
display_backend::DisplayBackend,
|
||||
error_handling_ctx, ipc_server, script_var_handler,
|
||||
|
@ -12,6 +12,7 @@ use std::{
|
|||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
io::Write,
|
||||
marker::PhantomData,
|
||||
os::unix::io::AsRawFd,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
|
@ -22,7 +23,6 @@ use tokio::sync::mpsc::*;
|
|||
pub fn initialize_server<B: DisplayBackend>(
|
||||
paths: EwwPaths,
|
||||
action: Option<DaemonCommand>,
|
||||
display_backend: B,
|
||||
should_daemonize: bool,
|
||||
) -> Result<ForkResult> {
|
||||
let (ui_send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
@ -57,7 +57,7 @@ pub fn initialize_server<B: DisplayBackend>(
|
|||
┏━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃Initializing eww daemon┃
|
||||
┗━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
"#
|
||||
"#
|
||||
);
|
||||
|
||||
simple_signal::set_handler(&[simple_signal::Signal::Int, simple_signal::Signal::Term], move |_| {
|
||||
|
@ -68,6 +68,9 @@ pub fn initialize_server<B: DisplayBackend>(
|
|||
}
|
||||
});
|
||||
|
||||
if B::IS_WAYLAND {
|
||||
std::env::set_var("GDK_BACKEND", "wayland")
|
||||
}
|
||||
gtk::init()?;
|
||||
|
||||
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 mut app = app::App {
|
||||
display_backend,
|
||||
let mut app: App<B> = app::App {
|
||||
scope_graph: Rc::new(RefCell::new(ScopeGraph::from_global_vars(
|
||||
eww_config.generate_initial_state()?,
|
||||
scope_graph_evt_send,
|
||||
|
@ -90,9 +92,10 @@ pub fn initialize_server<B: DisplayBackend>(
|
|||
app_evt_send: ui_send.clone(),
|
||||
window_close_timer_abort_senders: HashMap::new(),
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -102,13 +105,15 @@ pub fn initialize_server<B: DisplayBackend>(
|
|||
}
|
||||
}
|
||||
|
||||
connect_monitor_added(ui_send.clone());
|
||||
|
||||
// initialize all the handlers and tasks running asyncronously
|
||||
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 let Some(action) = action {
|
||||
app.handle_command(action);
|
||||
app.handle_command(action).await;
|
||||
}
|
||||
|
||||
loop {
|
||||
|
@ -117,7 +122,7 @@ pub fn initialize_server<B: DisplayBackend>(
|
|||
app.scope_graph.borrow_mut().handle_scope_graph_event(scope_graph_evt);
|
||||
},
|
||||
Some(ui_event) = ui_recv.recv() => {
|
||||
app.handle_command(ui_event);
|
||||
app.handle_command(ui_event).await;
|
||||
}
|
||||
else => break,
|
||||
}
|
||||
|
@ -133,6 +138,29 @@ pub fn initialize_server<B: DisplayBackend>(
|
|||
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: >k::gdk::Display, _monitor: >k::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 {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.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);
|
||||
});
|
||||
|
||||
let (daemon_resp_sender, mut daemon_resp_response) = daemon_response::create_pair();
|
||||
// without this sleep, reading the config file sometimes gives an empty file.
|
||||
// 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.
|
||||
// There should be some cleaner solution for this, but this will do for now.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
evt_send.send(app::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"),
|
||||
}
|
||||
});
|
||||
reload_config_and_css(&evt_send)?;
|
||||
}
|
||||
},
|
||||
else => break
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use anyhow::{Context, Result};
|
||||
use codespan_reporting::diagnostic::Severity;
|
||||
use eww_shared_util::{AttrName, Spanned};
|
||||
use gdk::prelude::Cast;
|
||||
use gtk::{
|
||||
gdk::prelude::Cast,
|
||||
prelude::{BoxExt, ContainerExt, WidgetExt},
|
||||
Orientation,
|
||||
};
|
||||
|
@ -306,13 +306,14 @@ fn build_children_special_widget(
|
|||
.children
|
||||
.get(nth_value as usize)
|
||||
.with_context(|| format!("No child at index {}", nth_value))?;
|
||||
let new_child_widget = build_gtk_widget(
|
||||
tree,
|
||||
widget_defs.clone(),
|
||||
custom_widget_invocation.scope,
|
||||
nth_child_widget_use.clone(),
|
||||
None,
|
||||
let scope = tree.register_new_scope(
|
||||
format!("child {nth_value}"),
|
||||
Some(custom_widget_invocation.scope),
|
||||
calling_scope,
|
||||
HashMap::new(),
|
||||
)?;
|
||||
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.set_child(Some(&new_child_widget));
|
||||
new_child_widget.show();
|
||||
|
@ -323,7 +324,13 @@ fn build_children_special_widget(
|
|||
)?;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use glib::{object_subclass, prelude::*, wrapper};
|
||||
use glib_macros::Properties;
|
||||
use gtk::{prelude::*, subclass::prelude::*};
|
||||
use gtk::glib::{self, object_subclass, prelude::*, wrapper, Properties};
|
||||
use gtk::{cairo, gdk, prelude::*, subclass::prelude::*};
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crate::error_handling_ctx;
|
||||
|
@ -154,7 +153,7 @@ impl WidgetImpl for CircProgPriv {
|
|||
self.preferred_height()
|
||||
}
|
||||
|
||||
fn draw(&self, cr: &cairo::Context) -> Inhibit {
|
||||
fn draw(&self, cr: &cairo::Context) -> glib::Propagation {
|
||||
let res: Result<()> = (|| {
|
||||
let value = *self.value.borrow();
|
||||
let start_at = *self.start_at.borrow();
|
||||
|
@ -226,7 +225,7 @@ impl WidgetImpl for CircProgPriv {
|
|||
error_handling_ctx::print_error(error)
|
||||
};
|
||||
|
||||
gtk::Inhibit(false)
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,8 @@ use std::{cell::RefCell, collections::VecDeque};
|
|||
// https://www.figuiere.net/technotes/notes/tn002/
|
||||
// https://github.com/gtk-rs/examples/blob/master/src/bin/listbox_model.rs
|
||||
use anyhow::{anyhow, Result};
|
||||
use glib::{object_subclass, wrapper};
|
||||
use glib_macros::Properties;
|
||||
use gtk::{prelude::*, subclass::prelude::*};
|
||||
use gtk::glib::{self, object_subclass, wrapper, Properties};
|
||||
use gtk::{cairo, gdk, prelude::*, subclass::prelude::*};
|
||||
|
||||
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)]
|
||||
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)>>,
|
||||
extra_point: RefCell<Option<(std::time::Instant, f64)>>,
|
||||
last_updated_at: RefCell<std::time::Instant>,
|
||||
|
@ -54,6 +60,9 @@ impl Default for GraphPriv {
|
|||
max: RefCell::new(100.0),
|
||||
dynamic: RefCell::new(true),
|
||||
time_range: RefCell::new(10),
|
||||
flip_x: RefCell::new(true),
|
||||
flip_y: RefCell::new(true),
|
||||
vertical: RefCell::new(false),
|
||||
history: RefCell::new(VecDeque::new()),
|
||||
extra_point: RefCell::new(None),
|
||||
last_updated_at: RefCell::new(std::time::Instant::now()),
|
||||
|
@ -78,6 +87,16 @@ impl GraphPriv {
|
|||
}
|
||||
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 {
|
||||
|
@ -111,6 +130,15 @@ impl ObjectImpl for GraphPriv {
|
|||
"line-style" => {
|
||||
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,),
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +198,7 @@ impl WidgetImpl for GraphPriv {
|
|||
(width, width)
|
||||
}
|
||||
|
||||
fn draw(&self, cr: &cairo::Context) -> Inhibit {
|
||||
fn draw(&self, cr: &cairo::Context) -> glib::Propagation {
|
||||
let res: Result<()> = (|| {
|
||||
let history = &*self.history.borrow();
|
||||
let extra_point = *self.extra_point.borrow();
|
||||
|
@ -215,18 +243,15 @@ impl WidgetImpl for GraphPriv {
|
|||
.iter()
|
||||
.map(|(instant, value)| {
|
||||
let t = last_updated_at.duration_since(*instant).as_millis() as f64;
|
||||
let x = width * (1.0 - (t / time_range));
|
||||
let y = height * (1.0 - ((value - min) / value_range));
|
||||
(x, y)
|
||||
self.value_to_point(width, height, t / time_range, (value - min) / value_range)
|
||||
})
|
||||
.collect::<VecDeque<(f64, f64)>>();
|
||||
|
||||
// Aad an extra point outside of the graph to extend the line to the left
|
||||
if let Some((instant, value)) = extra_point {
|
||||
let t = last_updated_at.duration_since(instant).as_millis() as f64;
|
||||
let x = -width * ((t - time_range) / time_range);
|
||||
let y = height * (1.0 - ((value - min) / value_range));
|
||||
points.push_front((x, y));
|
||||
let (x, y) = self.value_to_point(width, height, (t - time_range) / time_range, (value - min) / value_range);
|
||||
points.push_front(if *self.vertical.borrow() { (x, -y) } else { (-x, y) });
|
||||
}
|
||||
points
|
||||
};
|
||||
|
@ -276,7 +301,7 @@ impl WidgetImpl for GraphPriv {
|
|||
error_handling_ctx::print_error(error)
|
||||
};
|
||||
|
||||
gtk::Inhibit(false)
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
use crate::widgets::window::Window;
|
||||
use futures::StreamExt;
|
||||
use gtk::{cairo::Surface, gdk::ffi::gdk_cairo_surface_create_from_pixbuf, prelude::*};
|
||||
use notifier_host;
|
||||
use gtk::{
|
||||
cairo::Surface,
|
||||
gdk::{self, ffi::gdk_cairo_surface_create_from_pixbuf, NotifyType},
|
||||
glib,
|
||||
prelude::*,
|
||||
};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc};
|
||||
|
||||
// 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) {
|
||||
if let Some(item) = self.items.get(id) {
|
||||
self.container.remove(&item.widget);
|
||||
self.items.remove(id);
|
||||
} else {
|
||||
log::warn!("Tried to remove nonexistent item {:?} from systray", id);
|
||||
}
|
||||
|
@ -130,11 +135,27 @@ impl Drop for Item {
|
|||
|
||||
impl Item {
|
||||
fn new(id: String, item: notifier_host::Item, icon_size: tokio::sync::watch::Receiver<i32>) -> Self {
|
||||
let widget = gtk::EventBox::new();
|
||||
let out_widget = widget.clone(); // copy so we can return it
|
||||
let gtk_widget = gtk::EventBox::new();
|
||||
|
||||
// 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 {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
@ -213,7 +234,7 @@ impl Item {
|
|||
if let Err(result) = result {
|
||||
log::error!("failed to handle mouse click {}: {}", evt.button(), result);
|
||||
}
|
||||
gtk::Inhibit(true)
|
||||
glib::Propagation::Stop
|
||||
}));
|
||||
|
||||
// updates
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use glib::{object_subclass, wrapper};
|
||||
use glib_macros::Properties;
|
||||
use gtk::glib::{self, object_subclass, wrapper, Properties};
|
||||
use gtk::{prelude::*, subclass::prelude::*};
|
||||
use std::{cell::RefCell, str::FromStr};
|
||||
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)]
|
||||
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)]
|
||||
translate_x: RefCell<Option<String>>,
|
||||
|
||||
|
@ -38,6 +43,8 @@ impl Default for TransformPriv {
|
|||
fn default() -> Self {
|
||||
TransformPriv {
|
||||
rotate: RefCell::new(0.0),
|
||||
transform_origin_x: RefCell::new(None),
|
||||
transform_origin_y: RefCell::new(None),
|
||||
translate_x: RefCell::new(None),
|
||||
translate_y: RefCell::new(None),
|
||||
scale_x: RefCell::new(None),
|
||||
|
@ -58,6 +65,14 @@ impl ObjectImpl for TransformPriv {
|
|||
self.rotate.replace(value.get().unwrap());
|
||||
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" => {
|
||||
self.translate_x.replace(value.get().unwrap());
|
||||
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 WidgetImpl for TransformPriv {
|
||||
fn draw(&self, cr: &cairo::Context) -> Inhibit {
|
||||
fn draw(&self, cr: >k::cairo::Context) -> glib::Propagation {
|
||||
let res: Result<()> = (|| {
|
||||
let rotate = *self.rotate.borrow();
|
||||
let total_width = self.obj().allocated_width() as f64;
|
||||
|
@ -129,6 +144,15 @@ impl WidgetImpl for TransformPriv {
|
|||
|
||||
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() {
|
||||
Some(tx) => NumWithUnit::from_str(tx)?.pixels_relative_to(total_width as i32) as f64,
|
||||
None => 0.0,
|
||||
|
@ -149,9 +173,10 @@ impl WidgetImpl for TransformPriv {
|
|||
None => 1.0,
|
||||
};
|
||||
|
||||
cr.scale(scale_x, scale_y);
|
||||
cr.translate(transform_origin_x, transform_origin_y);
|
||||
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
|
||||
if let Some(child) = &*self.content.borrow() {
|
||||
|
@ -166,7 +191,7 @@ impl WidgetImpl for TransformPriv {
|
|||
error_handling_ctx::print_error(error)
|
||||
};
|
||||
|
||||
gtk::Inhibit(false)
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,11 @@ use crate::{
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use codespan_reporting::diagnostic::Severity;
|
||||
use eww_shared_util::Spanned;
|
||||
use gdk::{ModifierType, NotifyType};
|
||||
|
||||
use gdk::{ModifierType, NotifyType};
|
||||
use glib::translate::FromGlib;
|
||||
use gtk::{self, glib, prelude::*, DestDefaults, TargetEntry, TargetList};
|
||||
use gtk::{gdk, pango};
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
|
@ -37,16 +38,16 @@ use yuck::{
|
|||
/// thus not connecting a new handler unless the condition is met.
|
||||
macro_rules! connect_signal_handler {
|
||||
($widget:ident, if $cond:expr, $connect_expr:expr) => {{
|
||||
const KEY:&str = std::concat!("signal-handler:", std::line!());
|
||||
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 {
|
||||
let a = old.as_ref().as_raw();
|
||||
$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) => {{
|
||||
|
@ -208,10 +209,10 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Wi
|
|||
prop(visible: as_bool = true) {
|
||||
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) {
|
||||
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)
|
||||
},
|
||||
// @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: >k::Ran
|
|||
let is_being_dragged = Rc::new(RefCell::new(false));
|
||||
gtk_widget.connect_button_press_event(glib::clone!(@strong is_being_dragged => move |_, _| {
|
||||
*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 |_, _| {
|
||||
*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).
|
||||
|
@ -311,15 +312,50 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result<gtk::ComboBoxText
|
|||
|
||||
const WIDGET_NAME_EXPANDER: &str = "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> {
|
||||
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, {
|
||||
// @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: as_bool) { gtk_widget.set_expanded(expanded); }
|
||||
});
|
||||
|
||||
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: 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: 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) {
|
||||
gtk_widget.set_text(&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: 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";
|
||||
/// @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> {
|
||||
let gtk_widget = gtk::Button::new();
|
||||
|
||||
|
@ -492,25 +530,42 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> {
|
|||
prop(
|
||||
// @prop timeout - timeout of the command. Default: "200ms"
|
||||
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 = "",
|
||||
// @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 = "",
|
||||
// @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 = ""
|
||||
) {
|
||||
gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK);
|
||||
connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| {
|
||||
// animate button upon right-/middleclick (if gtk theme supports it)
|
||||
// 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() {
|
||||
1 => run_command(timeout, &onclick, &[] as &[&str]),
|
||||
2 => run_command(timeout, &onmiddleclick, &[] 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)
|
||||
}
|
||||
|
@ -536,12 +591,35 @@ fn build_gtk_image(bargs: &mut BuilderArgs) -> Result<gtk::Image> {
|
|||
// @prop path - path to the image file
|
||||
// @prop image-width - width 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") {
|
||||
let pixbuf_animation = gtk::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?;
|
||||
gtk_widget.set_from_animation(&pixbuf_animation);
|
||||
} 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(>k::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::<>k::gio::Cancellable>)?;
|
||||
stream.close(None::<>k::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));
|
||||
}
|
||||
},
|
||||
|
@ -727,27 +805,27 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
|
|||
// 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);
|
||||
gtk_widget.set_state_flags(gtk::StateFlags::PRELIGHT, false);
|
||||
}
|
||||
gtk::Inhibit(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);
|
||||
gtk_widget.unset_state_flags(gtk::StateFlags::PRELIGHT);
|
||||
}
|
||||
gtk::Inhibit(false)
|
||||
glib::Propagation::Proceed
|
||||
});
|
||||
|
||||
// Support :active selector
|
||||
gtk_widget.connect_button_press_event(|gtk_widget, _| {
|
||||
gtk_widget.clone().set_state_flags(gtk::StateFlags::ACTIVE, false);
|
||||
gtk::Inhibit(false)
|
||||
gtk_widget.set_state_flags(gtk::StateFlags::ACTIVE, false);
|
||||
glib::Propagation::Proceed
|
||||
});
|
||||
|
||||
gtk_widget.connect_button_release_event(|gtk_widget, _| {
|
||||
gtk_widget.clone().unset_state_flags(gtk::StateFlags::ACTIVE);
|
||||
gtk::Inhibit(false)
|
||||
gtk_widget.unset_state_flags(gtk::StateFlags::ACTIVE);
|
||||
glib::Propagation::Proceed
|
||||
});
|
||||
|
||||
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
|
||||
run_command(timeout, &onscroll, &[if delta < 0f64 { "up" } else { "down" }]);
|
||||
}
|
||||
gtk::Inhibit(false)
|
||||
glib::Propagation::Proceed
|
||||
}));
|
||||
},
|
||||
// @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 {
|
||||
run_command(timeout, &onhover, &[evt.position().0, evt.position().1]);
|
||||
}
|
||||
gtk::Inhibit(false)
|
||||
glib::Propagation::Proceed
|
||||
}));
|
||||
},
|
||||
// @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 {
|
||||
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)
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
gtk::Inhibit(false)
|
||||
glib::Propagation::Proceed
|
||||
}));
|
||||
connect_signal_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |widget, _evt| {
|
||||
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);
|
||||
}
|
||||
}
|
||||
gtk::Inhibit(false)
|
||||
glib::Propagation::Proceed
|
||||
}));
|
||||
},
|
||||
// @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 timeout - timeout of the command. Default: "200ms"
|
||||
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 = "",
|
||||
// @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 = "",
|
||||
// @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 = ""
|
||||
) {
|
||||
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() {
|
||||
1 => run_command(timeout, &onclick, &[] as &[&str]),
|
||||
2 => run_command(timeout, &onmiddleclick, &[] as &[&str]),
|
||||
3 => run_command(timeout, &onrightclick, &[] as &[&str]),
|
||||
_ => {},
|
||||
}
|
||||
gtk::Inhibit(false)
|
||||
glib::Propagation::Proceed
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(gtk_widget)
|
||||
}
|
||||
|
||||
|
@ -894,11 +968,12 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
|
|||
|
||||
def_widget!(bargs, _g, gtk_widget, {
|
||||
// @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 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 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 {
|
||||
// gtk does weird thing if we set max_width_chars to i32::MAX
|
||||
if limit_width == i32::MAX {
|
||||
|
@ -906,11 +981,15 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
|
|||
} else {
|
||||
gtk_widget.set_max_width_chars(limit_width);
|
||||
}
|
||||
if truncate || limit_width != i32::MAX {
|
||||
if truncate_left {
|
||||
gtk_widget.set_ellipsize(pango::EllipsizeMode::Start);
|
||||
} else {
|
||||
gtk_widget.set_ellipsize(pango::EllipsizeMode::End);
|
||||
}
|
||||
} else {
|
||||
gtk_widget.set_ellipsize(pango::EllipsizeMode::None);
|
||||
}
|
||||
|
||||
text
|
||||
} else {
|
||||
|
@ -918,7 +997,7 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
|
|||
|
||||
let limit_width = limit_width as usize;
|
||||
let char_count = text.chars().count();
|
||||
if char_count > limit_width && !show_truncated {
|
||||
if char_count > limit_width {
|
||||
if truncate_left {
|
||||
text.chars().skip(char_count - limit_width).collect()
|
||||
} else {
|
||||
|
@ -934,11 +1013,12 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
|
|||
gtk_widget.set_text(&text);
|
||||
},
|
||||
// @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 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(markup: as_string, limit_width: as_i32 = i32::MAX, truncate_left: as_bool = false, show_truncated: as_bool = true) {
|
||||
if show_truncated {
|
||||
// @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, truncate: as_bool = false, limit_width: as_i32 = i32::MAX, truncate_left: as_bool = false, show_truncated: as_bool = true) {
|
||||
if (truncate || limit_width != i32::MAX) && show_truncated {
|
||||
// gtk does weird thing if we set max_width_chars to i32::MAX
|
||||
if limit_width == i32::MAX {
|
||||
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") {
|
||||
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)
|
||||
}
|
||||
|
@ -1082,20 +1170,11 @@ const WIDGET_NAME_STACK: &str = "stack";
|
|||
/// @desc A widget that displays one of its children at a time
|
||||
fn build_gtk_stack(bargs: &mut BuilderArgs) -> Result<gtk::Stack> {
|
||||
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) {
|
||||
Ordering::Less => {
|
||||
Err(DiagError(gen_diagnostic!("stack must contain at least one element", bargs.widget_use.span)).into())
|
||||
if bargs.widget_use.children.is_empty() {
|
||||
return 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| {
|
||||
build_gtk_widget(
|
||||
bargs.scope_graph,
|
||||
|
@ -1105,25 +1184,37 @@ fn build_gtk_stack(bargs: &mut BuilderArgs) -> Result<gtk::Stack> {
|
|||
bargs.custom_widget_invocation.clone(),
|
||||
)
|
||||
});
|
||||
|
||||
for (i, child) in children.enumerate() {
|
||||
let child = child?;
|
||||
gtk_widget.add_named(&child, &i.to_string());
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const WIDGET_NAME_TRANSFORM: &str = "transform";
|
||||
/// @widget transform
|
||||
/// @desc A widget that applies transformations to its content. They are applied in the following
|
||||
/// order: rotate->translate->scale)
|
||||
/// @desc A widget that applies transformations to its content. They are applied in the following order: rotate -> translate -> scale
|
||||
fn build_transform(bargs: &mut BuilderArgs) -> Result<Transform> {
|
||||
let w = Transform::new();
|
||||
def_widget!(bargs, _g, w, {
|
||||
// @prop rotate - the percentage to 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: as_string) { w.set_property("translate-x", translate_x); },
|
||||
// @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();
|
||||
def_widget!(bargs, _g, w, {
|
||||
// @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: as_f64) { w.set_property("thickness", thickness); },
|
||||
// @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",
|
||||
// "bevel"
|
||||
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)
|
||||
}
|
||||
|
@ -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"
|
||||
fn parse_gravity(g: &str) -> Result<gtk::pango::Gravity> {
|
||||
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.
|
||||
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));
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use glib::{object_subclass, wrapper};
|
||||
use glib_macros::Properties;
|
||||
use gtk::glib::{self, object_subclass, wrapper, Properties};
|
||||
use gtk::{prelude::*, subclass::prelude::*};
|
||||
use std::cell::RefCell;
|
||||
|
||||
|
|
|
@ -59,10 +59,10 @@ impl WindowArguments {
|
|||
|
||||
// Ensure that the arguments passed to the window that are already interpreted by eww (id, screen)
|
||||
// are set to the correct values
|
||||
if expected_args.contains(&"id".to_string()) {
|
||||
if expected_args.contains(&String::from("id")) {
|
||||
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());
|
||||
local_variables.insert(VarName::from("screen"), mon_dyn);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ use crate::window_arguments::WindowArguments;
|
|||
pub struct WindowInitiator {
|
||||
pub backend_options: BackendWindowOptions,
|
||||
pub geometry: Option<WindowGeometry>,
|
||||
pub id: String,
|
||||
pub local_variables: HashMap<VarName, DynVal>,
|
||||
pub monitor: Option<MonitorIdentifier>,
|
||||
pub name: String,
|
||||
|
@ -37,7 +36,6 @@ impl WindowInitiator {
|
|||
Ok(WindowInitiator {
|
||||
backend_options: window_def.backend_options.eval(&vars)?,
|
||||
geometry,
|
||||
id: args.instance_id.clone(),
|
||||
monitor,
|
||||
name: window_def.name.clone(),
|
||||
resizable: window_def.eval_resizable(&vars)?,
|
||||
|
|
|
@ -12,3 +12,4 @@ homepage = "https://github.com/elkowar/eww"
|
|||
serde.workspace = true
|
||||
derive_more.workspace = true
|
||||
ref-cast.workspace = true
|
||||
chrono = { workspace = true, features = ["unstable-locales"] }
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
pub mod locale;
|
||||
pub mod span;
|
||||
pub mod wrappers;
|
||||
|
||||
pub use locale::*;
|
||||
pub use span::*;
|
||||
pub use wrappers::*;
|
||||
|
||||
|
|
12
crates/eww_shared_util/src/locale.rs
Normal file
12
crates/eww_shared_util/src/locale.rs
Normal 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())
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
use derive_more::*;
|
||||
use derive_more::{Debug, *};
|
||||
use ref_cast::RefCast;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The name of a variable
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom, RefCast)]
|
||||
#[debug(fmt = "VarName({})", .0)]
|
||||
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, Debug, RefCast)]
|
||||
#[debug("VarName({})", _0)]
|
||||
pub struct VarName(pub String);
|
||||
|
||||
impl std::borrow::Borrow<str> for VarName {
|
||||
|
@ -34,8 +34,8 @@ impl From<AttrName> for VarName {
|
|||
|
||||
/// The name of an attribute
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom, RefCast)]
|
||||
#[debug(fmt="AttrName({})", .0)]
|
||||
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, Debug, RefCast)]
|
||||
#[debug("AttrName({})", _0)]
|
||||
pub struct AttrName(pub String);
|
||||
|
||||
impl AttrName {
|
||||
|
|
|
@ -9,11 +9,12 @@ repository = "https://github.com/elkowar/eww"
|
|||
homepage = "https://github.com/elkowar/eww"
|
||||
|
||||
[dependencies]
|
||||
gtk = "0.17.1"
|
||||
gdk = "0.17.1"
|
||||
zbus = { version = "3.7.0", default-features = false, features = ["tokio"] }
|
||||
dbusmenu-gtk3 = "0.1.0"
|
||||
quick-xml = { version = "0.37.1", features = ["serialize"] }
|
||||
serde = "1.0.215"
|
||||
|
||||
gtk.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
zbus = { workspace = true, default-features = false, features = ["tokio"] }
|
||||
|
|
|
@ -105,7 +105,7 @@ fn icon_from_name(
|
|||
) -> std::result::Result<gtk::gdk_pixbuf::Pixbuf, IconError> {
|
||||
let theme = if let Some(path) = theme_path {
|
||||
let theme = gtk::IconTheme::new();
|
||||
theme.prepend_search_path(&path);
|
||||
theme.prepend_search_path(path);
|
||||
theme
|
||||
} else {
|
||||
gtk::IconTheme::default().expect("Could not get default gtk theme")
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use crate::*;
|
||||
|
||||
use gtk::{self, prelude::*};
|
||||
use serde::Deserialize;
|
||||
use zbus::fdo::IntrospectableProxy;
|
||||
|
||||
/// Recognised values of [`org.freedesktop.StatusNotifierItem.Status`].
|
||||
///
|
||||
|
@ -61,7 +63,12 @@ impl Item {
|
|||
if let Some((addr, path)) = service.split_once('/') {
|
||||
(addr.to_owned(), format!("/{}", path))
|
||||
} 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 {
|
||||
return Err(zbus::Error::Address(service.to_owned()));
|
||||
}
|
||||
|
@ -88,9 +95,9 @@ impl Item {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn popup_menu(&self, event: &gdk::EventButton, x: i32, y: i32) -> zbus::Result<()> {
|
||||
pub async fn popup_menu(&self, event: >k::gdk::EventButton, x: i32, y: i32) -> zbus::Result<()> {
|
||||
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(())
|
||||
} else {
|
||||
self.sni.context_menu(x, y).await
|
||||
|
@ -105,3 +112,59 @@ impl Item {
|
|||
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 })
|
||||
}
|
||||
|
|
|
@ -2,9 +2,15 @@
|
|||
//!
|
||||
//! The interface XML files were taken from
|
||||
//! [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
|
||||
//! dbus_status_notifier_item.xml` and `zbus-xmlgen dbus_status_notifier_watcher.xml`. At the
|
||||
//! moment, `dbus_menu.xml` isn't used.
|
||||
//! generated with [zbus-xmlgen](https://docs.rs/crate/zbus_xmlgen/latest) by running
|
||||
//! `zbus-xmlgen file crates/notifier_host/src/proxy/dbus_status_notifier_item.xml` and
|
||||
//! `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
|
||||
//! tutorial](https://dbus2.github.io/zbus/).
|
||||
|
|
|
@ -14,20 +14,21 @@ build = "build.rs"
|
|||
[dependencies]
|
||||
eww_shared_util.workspace = true
|
||||
|
||||
bytesize.workspace = true
|
||||
cached.workspace = true
|
||||
chrono-tz.workspace = true
|
||||
chrono.workspace = true
|
||||
chrono = { workspace = true, features = ["unstable-locales"] }
|
||||
itertools.workspace = true
|
||||
jaq-core.workspace = true
|
||||
jaq-parse.workspace = true
|
||||
jaq-std = {workspace = true, features = ["bincode"]}
|
||||
jaq-std.workspace = true
|
||||
jaq-interpret.workspace = true
|
||||
jaq-syn.workspace = true
|
||||
lalrpop-util.workspace = true
|
||||
once_cell.workspace = true
|
||||
regex.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde = {workspace = true, features = ["derive"]}
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
static_assertions.workspace = true
|
||||
strsim.workspace = true
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use bytesize::ByteSize;
|
||||
use cached::proc_macro::cached;
|
||||
use chrono::{Local, LocalResult, TimeZone};
|
||||
use itertools::Itertools;
|
||||
|
@ -7,7 +8,7 @@ use crate::{
|
|||
ast::{AccessType, BinOp, SimplExpr, UnaryOp},
|
||||
dynval::{ConversionError, DynVal},
|
||||
};
|
||||
use eww_shared_util::{Span, Spanned, VarName};
|
||||
use eww_shared_util::{get_locale, Span, Spanned, VarName};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::{Infallible, TryFrom, TryInto},
|
||||
|
@ -61,6 +62,9 @@ pub enum EvalError {
|
|||
#[error("Error parsing date: {0}")]
|
||||
ChronoError(String),
|
||||
|
||||
#[error("Error parsing byte format mode: {0}")]
|
||||
ByteFormatModeError(String),
|
||||
|
||||
#[error("{1}")]
|
||||
Spanned(Span, Box<EvalError>),
|
||||
}
|
||||
|
@ -268,6 +272,10 @@ impl SimplExpr {
|
|||
|
||||
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()? {
|
||||
serde_json::Value::Array(val) => {
|
||||
let index = index.as_i32()?;
|
||||
|
@ -281,9 +289,6 @@ impl SimplExpr {
|
|||
.unwrap_or(&serde_json::Value::Null);
|
||||
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)),
|
||||
_ => 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())),
|
||||
},
|
||||
"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() {
|
||||
[num] => {
|
||||
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())),
|
||||
},
|
||||
"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))),
|
||||
_ => 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) {
|
||||
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())),
|
||||
}))
|
||||
}
|
||||
[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())),
|
||||
})),
|
||||
_ => 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())),
|
||||
}
|
||||
|
@ -485,16 +590,20 @@ fn prepare_jaq_filter(code: String) -> Result<Arc<jaq_interpret::Filter>, EvalEr
|
|||
Ok(Arc::new(filter))
|
||||
}
|
||||
|
||||
fn run_jaq_function(json: serde_json::Value, code: String) -> Result<DynVal, EvalError> {
|
||||
let filter: Arc<jaq_interpret::Filter> = prepare_jaq_filter(code)?;
|
||||
let inputs = jaq_interpret::RcIter::new(std::iter::empty());
|
||||
let out = filter
|
||||
.run((jaq_interpret::Ctx::new([], &inputs), jaq_interpret::Val::from(json)))
|
||||
.map(|x| x.map(Into::<serde_json::Value>::into))
|
||||
.map(|x| x.map(|x| DynVal::from_string(serde_json::to_string(&x).unwrap())))
|
||||
fn run_jaq_function(json: serde_json::Value, code: String, args: &str) -> Result<DynVal, EvalError> {
|
||||
use jaq_interpret::{Ctx, RcIter, Val};
|
||||
prepare_jaq_filter(code)?
|
||||
.run((Ctx::new([], &RcIter::new(std::iter::empty())), Val::from(json)))
|
||||
.map(|r| r.map(Into::<serde_json::Value>::into))
|
||||
.map(|x| {
|
||||
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<_, _>>()
|
||||
.map_err(|e| EvalError::JaqError(e.to_string()))?;
|
||||
Ok(out)
|
||||
.map_err(|e| EvalError::JaqError(e.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -544,6 +653,8 @@ mod tests {
|
|||
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_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_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())),
|
||||
|
@ -553,5 +664,9 @@ mod tests {
|
|||
lazy_evaluation_or(r#"true || "null".test"#) => Ok(DynVal::from(true)),
|
||||
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_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""#)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use simplexpr::{
|
|||
SimplExpr,
|
||||
};
|
||||
|
||||
use super::{attributes::Attributes, window_definition::EnumParseError};
|
||||
use crate::{
|
||||
enum_parse,
|
||||
error::DiagResult,
|
||||
|
@ -14,8 +15,7 @@ use crate::{
|
|||
value::{coords, NumWithUnit},
|
||||
};
|
||||
use eww_shared_util::{Span, VarName};
|
||||
|
||||
use super::{attributes::Attributes, window_definition::EnumParseError};
|
||||
use simplexpr::dynval::ConversionError;
|
||||
|
||||
use crate::error::{DiagError, DiagResultExt};
|
||||
|
||||
|
@ -27,6 +27,8 @@ pub enum Error {
|
|||
CoordsError(#[from] coords::Error),
|
||||
#[error(transparent)]
|
||||
EvalError(#[from] EvalError),
|
||||
#[error(transparent)]
|
||||
ConversionError(#[from] ConversionError),
|
||||
}
|
||||
|
||||
/// Backend-specific options of a window
|
||||
|
@ -45,6 +47,7 @@ impl BackendWindowOptionsDef {
|
|||
pub fn from_attrs(attrs: &mut Attributes) -> DiagResult<Self> {
|
||||
let struts = attrs.ast_optional("reserve")?;
|
||||
let window_type = attrs.ast_optional("windowtype")?;
|
||||
let focusable = attrs.ast_optional("focusable")?;
|
||||
let x11 = X11BackendWindowOptionsDef {
|
||||
sticky: attrs.ast_optional("sticky")?,
|
||||
struts,
|
||||
|
@ -53,7 +56,7 @@ impl BackendWindowOptionsDef {
|
|||
};
|
||||
let wayland = WlBackendWindowOptionsDef {
|
||||
exclusive: attrs.ast_optional("exclusive")?,
|
||||
focusable: attrs.ast_optional("focusable")?,
|
||||
focusable,
|
||||
namespace: attrs.ast_optional("namespace")?,
|
||||
};
|
||||
|
||||
|
@ -109,7 +112,7 @@ impl X11BackendWindowOptionsDef {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct WlBackendWindowOptions {
|
||||
pub exclusive: bool,
|
||||
pub focusable: bool,
|
||||
pub focusable: WlWindowFocusable,
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
|
@ -122,10 +125,13 @@ pub struct 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 {
|
||||
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 {
|
||||
Some(expr) => Some(expr.eval(local_variables)?.as_string()?),
|
||||
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
|
||||
#[derive(Debug, Clone, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)]
|
||||
pub enum X11WindowType {
|
||||
|
@ -182,7 +210,7 @@ pub enum Side {
|
|||
Bottom,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Side {
|
||||
impl FromStr for Side {
|
||||
type Err = EnumParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Side, Self::Err> {
|
||||
|
|
|
@ -90,7 +90,7 @@ impl<I: Iterator<Item = Ast>> AstIterator<I> {
|
|||
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.iter.put_back(ast)
|
||||
}
|
||||
|
@ -100,9 +100,8 @@ impl<I: Iterator<Item = Ast>> Iterator for AstIterator<I> {
|
|||
type Item = Ast;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next().map(|x| {
|
||||
self.iter.next().inspect(|x| {
|
||||
self.remaining_span.0 = x.span().1;
|
||||
x
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use derive_more::*;
|
||||
use derive_more::{Debug, *};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smart_default::SmartDefault;
|
||||
|
@ -14,13 +14,13 @@ pub enum Error {
|
|||
MalformedCoords,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, DebugCustom, SmartDefault)]
|
||||
#[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, Debug, SmartDefault)]
|
||||
pub enum NumWithUnit {
|
||||
#[display(fmt = "{}%", .0)]
|
||||
#[debug(fmt = "{}%", .0)]
|
||||
#[display("{}%", _0)]
|
||||
#[debug("{}%", _0)]
|
||||
Percent(f32),
|
||||
#[display(fmt = "{}px", .0)]
|
||||
#[debug(fmt = "{}px", .0)]
|
||||
#[display("{}px", _0)]
|
||||
#[debug("{}px", _0)]
|
||||
#[default]
|
||||
Pixels(i32),
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ impl FromStr for NumWithUnit {
|
|||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, Default)]
|
||||
#[display(fmt = "{}*{}", x, y)]
|
||||
#[display("{}*{}", x, y)]
|
||||
pub struct Coords {
|
||||
pub x: NumWithUnit,
|
||||
pub y: NumWithUnit,
|
||||
|
|
13
default.nix
13
default.nix
|
@ -1,6 +1,11 @@
|
|||
(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in fetchTarball {
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in
|
||||
fetchTarball {
|
||||
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;
|
||||
}) { src = ./.; }).defaultNix
|
||||
}
|
||||
) { src = ./.; }).defaultNix
|
||||
|
|
|
@ -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)
|
||||
- an integer, declaring the monitor index
|
||||
- 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**
|
||||
|
@ -87,10 +87,10 @@ Depending on if you are using X11 or Wayland, some additional properties exist:
|
|||
#### Wayland
|
||||
|
||||
| Property | Description |
|
||||
| ----------: | ------------------------------------------------------------ |
|
||||
| ----------: |------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `stacking` | Where the window should appear in the stack. Possible values: `fg`, `bg`, `overlay`, `bottom`. |
|
||||
| `exclusive` | Whether the compositor should reserve space for the window automatically. Either `true` or `false`. |
|
||||
| `focusable` | Whether the window should be able to be focused. This is necessary for any widgets that use the keyboard to work. Either `true` or `false`. |
|
||||
| `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. Possible values: `none`, `exclusive` and `ondemand`. |
|
||||
| `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,
|
||||
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.
|
||||
|
||||
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`)**
|
||||
|
||||
```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.
|
||||
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
|
||||
|
||||
As time passes, your configuration might grow larger and larger. Luckily, you can easily split up your configuration into multiple files!
|
||||
|
|
|
@ -4,3 +4,7 @@ These configurations of eww are available in the `examples/` directory of the [r
|
|||
|
||||
An eww bar configuration:
|
||||

|
||||
|
||||
A demo on how to declare and use data structures:
|
||||
|
||||

|
||||
|
|
|
@ -24,14 +24,16 @@ Supported currently are the following features:
|
|||
- comparisons (`==`, `!=`, `>`, `<`, `<=`, `>=`)
|
||||
- boolean operations (`||`, `&&`, `!`)
|
||||
- regex match operator (`=~`)
|
||||
- Rust regex style, left hand is regex, right hand is string
|
||||
- ex: workspace.name =~ '^special:.+$'
|
||||
- elvis operator (`?:`)
|
||||
- if the left side is `""` or a JSON `null`, then returns the right side,
|
||||
otherwise evaluates to the left side.
|
||||
- Safe Access operator (`?.`) or (`?.[index]`)
|
||||
- if the left side is `""` or a JSON `null`, then return `null`. Otherwise,
|
||||
attempt to index.
|
||||
- if the left side is an empty string or a JSON `null`, then return `null`. Otherwise,
|
||||
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
|
||||
not an object.
|
||||
not an object or an array.
|
||||
(`Number` or `String`).
|
||||
- conditionals (`condition ? 'value' : 'other value'`)
|
||||
- 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.
|
||||
- some function calls:
|
||||
- `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**
|
||||
- `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
|
||||
- `radtodeg(number)`: Converts a number from radians to degrees
|
||||
- `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
|
||||
- `arraylength(value)`: Gets the length of the array
|
||||
- `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
|
||||
- `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
|
||||
|
@ -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.
|
||||
Check [chrono's documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) for more
|
||||
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")
|
||||
|
|
BIN
examples/data-structures/data-structures-preview.png
Normal file
BIN
examples/data-structures/data-structures-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
29
examples/data-structures/eww.scss
Normal file
29
examples/data-structures/eww.scss
Normal 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;
|
||||
}
|
73
examples/data-structures/eww.yuck
Normal file
73
examples/data-structures/eww.yuck
Normal 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)
|
||||
)
|
|
@ -1,8 +1,8 @@
|
|||
* {
|
||||
all: unset; //Unsets everything so you can style everything from scratch
|
||||
all: unset; // Unsets everything so you can style everything from scratch
|
||||
}
|
||||
|
||||
//Global Styles
|
||||
// Global Styles
|
||||
.bar {
|
||||
background-color: #3a3a3a;
|
||||
color: #b0b4bc;
|
||||
|
@ -22,6 +22,7 @@
|
|||
color: #000000;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.metric scale trough {
|
||||
all: unset;
|
||||
background-color: #4e4e4e;
|
||||
|
@ -31,24 +32,11 @@
|
|||
margin-left: 10px;
|
||||
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 {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.workspaces button:hover {
|
||||
color: #D35D6E;
|
||||
}
|
||||
|
|
54
flake.lock
generated
54
flake.lock
generated
|
@ -1,46 +1,28 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"lastModified": 1709944340,
|
||||
"narHash": "sha256-xr54XK0SjczlUxRo5YwodibUSlpivS9bqHt8BNyWVQA=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"rev": "baa7aa7bd0a570b3b9edd0b8da859fee3ffaa4d4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"ref": "refs/pull/65/head",
|
||||
"repo": "flake-compat",
|
||||
"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": {
|
||||
"locked": {
|
||||
"lastModified": 1708407374,
|
||||
"narHash": "sha256-EECzarm+uqnNDCwaGg/ppXCO11qibZ1iigORShkkDf0=",
|
||||
"lastModified": 1725534445,
|
||||
"narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f33dd27a47ebdf11dc8a5eb05e7c8fbdaf89e73f",
|
||||
"rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -59,17 +41,16 @@
|
|||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1708395022,
|
||||
"narHash": "sha256-pxHZbfDsLAAcyWz+snbudxhQPlAnK2nWGAqRx11veac=",
|
||||
"lastModified": 1725675754,
|
||||
"narHash": "sha256-hXW3csqePOcF2e/PYnpXj72KEYyNj2HzTrVNmS/F7Ug=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "b4ae18c03af976549a0b6e396b2b5be56d275f8b",
|
||||
"rev": "8cc45e678e914a16c8e224c3237fb07cf21e5e54",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -77,21 +58,6 @@
|
|||
"repo": "rust-overlay",
|
||||
"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",
|
||||
|
|
132
flake.nix
132
flake.nix
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
inputs = {
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
flake-compat.url = "github:edolstra/flake-compat/refs/pull/65/head";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||
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
|
||||
pkgsFor = system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ self.overlays.default rust-overlay.overlays.default ];
|
||||
};
|
||||
overlays = [
|
||||
(import rust-overlay)
|
||||
self.overlays.default
|
||||
];
|
||||
pkgsFor = system: import nixpkgs { inherit system overlays; };
|
||||
|
||||
targetSystems = [ "aarch64-linux" "x86_64-linux" ];
|
||||
mkRustToolchain = pkgs:
|
||||
pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
||||
in {
|
||||
overlays.default = final: prev:
|
||||
let
|
||||
rust = mkRustToolchain final;
|
||||
targetSystems = [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
];
|
||||
mkRustToolchain = pkgs: pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
||||
in
|
||||
{
|
||||
overlays.default = final: prev: { inherit (self.packages.${prev.system}) eww eww-wayland; };
|
||||
|
||||
rustPlatform = prev.makeRustPlatform {
|
||||
cargo = rust;
|
||||
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:
|
||||
packages = nixpkgs.lib.genAttrs targetSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
rust = mkRustToolchain pkgs;
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
rust
|
||||
rust-analyzer-unwrapped
|
||||
gcc
|
||||
glib
|
||||
gdk-pixbuf
|
||||
librsvg
|
||||
libdbusmenu-gtk3
|
||||
gtk3
|
||||
gtk-layer-shell
|
||||
rustPlatform = pkgs.makeRustPlatform {
|
||||
cargo = rust;
|
||||
rustc = rust;
|
||||
};
|
||||
version = (builtins.fromTOML (builtins.readFile ./crates/eww/Cargo.toml)).package.version;
|
||||
in
|
||||
rec {
|
||||
eww = rustPlatform.buildRustPackage {
|
||||
version = "${version}-dirty";
|
||||
pname = "eww";
|
||||
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
cargoBuildFlags = [
|
||||
"--bin"
|
||||
"eww"
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
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
|
||||
mdbook
|
||||
zbus-xmlgen
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
formatter =
|
||||
nixpkgs.lib.genAttrs targetSystems (system: (pkgsFor system).nixfmt);
|
||||
formatter = nixpkgs.lib.genAttrs targetSystems (system: (pkgsFor system).nixfmt-rfc-style);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "1.76.0"
|
||||
channel = "1.81.0"
|
||||
components = [ "rust-src" ]
|
||||
profile = "default"
|
||||
|
|
13
shell.nix
13
shell.nix
|
@ -1,6 +1,11 @@
|
|||
(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in fetchTarball {
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in
|
||||
fetchTarball {
|
||||
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;
|
||||
}) { src = ./.; }).shellNix
|
||||
}
|
||||
) { src = ./.; }).shellNix
|
||||
|
|
Loading…
Add table
Reference in a new issue