├── tools ├── generate-rhai-docs │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs └── README.md ├── .gitignore ├── .github ├── EwwiiLogo.png ├── EwwiiGithubPreview.png ├── ISSUE_TEMPLATE │ ├── module-request.yml │ ├── feature_request.yml │ ├── widget-request.yml │ └── bug_report.yml ├── pull_request_template.md └── workflows │ └── build.yml ├── crates ├── ewwii │ ├── src │ │ ├── config │ │ │ ├── mod.rs │ │ │ ├── scss.rs │ │ │ └── ewwii_config.rs │ │ ├── window │ │ │ ├── mod.rs │ │ │ ├── window_definition.rs │ │ │ ├── monitor.rs │ │ │ ├── coords.rs │ │ │ └── window_geometry.rs │ │ ├── widgets │ │ │ ├── mod.rs │ │ │ ├── build_widget.rs │ │ │ ├── circular_progressbar.rs │ │ │ └── transform.rs │ │ ├── diag_error.rs │ │ ├── gen_diagnostic_macro.rs │ │ ├── application_lifecycle.rs │ │ ├── client.rs │ │ ├── plugin.rs │ │ ├── daemon_response.rs │ │ ├── ipc_server.rs │ │ ├── error_handling_ctx.rs │ │ ├── window_arguments.rs │ │ ├── paths.rs │ │ ├── file_database.rs │ │ ├── util.rs │ │ └── window_initiator.rs │ ├── build.rs │ └── Cargo.toml ├── shared_utils │ ├── src │ │ ├── lib.rs │ │ ├── span.rs │ │ └── extract_props.rs │ └── Cargo.toml ├── rhai_impl │ ├── src │ │ ├── lib.rs │ │ ├── providers │ │ │ ├── apilib │ │ │ │ └── mod.rs │ │ │ ├── stdlib │ │ │ │ ├── mod.rs │ │ │ │ ├── command.rs │ │ │ │ ├── env.rs │ │ │ │ ├── regex.rs │ │ │ │ └── text.rs │ │ │ └── mod.rs │ │ ├── updates │ │ │ ├── mod.rs │ │ │ ├── poll.rs │ │ │ ├── localsignal.rs │ │ │ └── listen.rs │ │ ├── error.rs │ │ ├── helper.rs │ │ ├── module_resolver.rs │ │ ├── builtins.rs │ │ ├── parser.rs │ │ └── dyn_id.rs │ └── Cargo.toml └── ewwii_plugin_api │ ├── src │ ├── example.rs │ ├── rhai_backend.rs │ ├── widget_backend.rs │ ├── export_macros.rs │ └── lib.rs │ ├── Cargo.toml │ └── README.md ├── examples ├── ewwii-bar │ ├── ewwii-bar.png │ ├── scripts │ │ └── getvol │ ├── ewwii.scss │ └── ewwii.rhai ├── data-structures │ ├── data-structures-preview.png │ ├── scripts │ │ └── readlast_and_truncate.sh │ ├── ewwii.scss │ └── ewwii.rhai └── activateLinux │ ├── ewwii.scss │ └── ewwii.rhai ├── rust-toolchain.toml ├── .editorconfig ├── default.nix ├── shell.nix ├── licenses ├── README.md └── eww-MIT.txt ├── CONTRIBUTING.md ├── rustfmt.toml ├── proc_macros └── scan_prop_proc │ ├── Cargo.toml │ └── src │ └── lib.rs ├── Cargo.toml ├── flake.lock ├── README.md └── flake.nix /tools/generate-rhai-docs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /auto_gen -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /**/target 3 | /result 4 | /result-* 5 | -------------------------------------------------------------------------------- /.github/EwwiiLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buzz/ewwii/main/.github/EwwiiLogo.png -------------------------------------------------------------------------------- /crates/ewwii/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ewwii_config; 2 | pub mod scss; 3 | pub use ewwii_config::*; 4 | -------------------------------------------------------------------------------- /crates/shared_utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod extract_props; 2 | pub mod span; 3 | 4 | pub use span::*; 5 | -------------------------------------------------------------------------------- /.github/EwwiiGithubPreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buzz/ewwii/main/.github/EwwiiGithubPreview.png -------------------------------------------------------------------------------- /examples/ewwii-bar/ewwii-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buzz/ewwii/main/examples/ewwii-bar/ewwii-bar.png -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | # channel = "1.81.0" 3 | channel = "1.89.0" 4 | components = [ "rust-src" ] 5 | profile = "default" 6 | -------------------------------------------------------------------------------- /examples/data-structures/data-structures-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buzz/ewwii/main/examples/data-structures/data-structures-preview.png -------------------------------------------------------------------------------- /examples/activateLinux/ewwii.scss: -------------------------------------------------------------------------------- 1 | * { 2 | all: unset; 3 | } 4 | 5 | 6 | .activate { 7 | background: transparent; 8 | color: gray; 9 | } 10 | -------------------------------------------------------------------------------- /crates/ewwii/src/window/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod backend_window_options; 2 | pub mod coords; 3 | pub mod monitor; 4 | pub mod window_definition; 5 | pub mod window_geometry; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /crates/ewwii/src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod build_widget; 2 | pub mod circular_progressbar; 3 | pub mod graph; 4 | pub mod transform; 5 | pub mod widget_definitions; 6 | pub mod widget_definitions_helper; 7 | -------------------------------------------------------------------------------- /tools/generate-rhai-docs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "generate-rhai-docs" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | rhai_impl.workspace = true 8 | rhai.workspace = true 9 | rhai-autodocs = "0.9.0" 10 | -------------------------------------------------------------------------------- /examples/data-structures/scripts/readlast_and_truncate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | while true; do 3 | # read the last line 4 | if [ -s /tmp/selected_emoji.txt ]; then 5 | tail -n 1 /tmp/selected_emoji.txt 6 | # truncate the file after reading 7 | : > /tmp/selected_emoji.txt 8 | fi 9 | sleep 0.1 10 | done 11 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Ewwii tools 2 | 3 | ## 1. generate-rhai-docs 4 | 5 | Generate rhai docs is a tool that will generate the documentation of rhai's builtin stdlib. 6 | 7 | ### How to run 8 | 9 | Run the following command in the root of the repository to run `generate-rhai-docs`: 10 | 11 | ```bash 12 | $ cargo run --release -p generate-rhai-docs 13 | ``` 14 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import ( 2 | let 3 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 4 | in 5 | fetchTarball { 6 | url = 7 | lock.nodes.flake-compat.locked.url 8 | or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 9 | sha256 = lock.nodes.flake-compat.locked.narHash; 10 | } 11 | ) { src = ./.; }).defaultNix 12 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import ( 2 | let 3 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 4 | in 5 | fetchTarball { 6 | url = 7 | lock.nodes.flake-compat.locked.url 8 | or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 9 | sha256 = lock.nodes.flake-compat.locked.narHash; 10 | } 11 | ) { src = ./.; }).shellNix 12 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! rhai_impl is a simple crate which configures rhai for the `ewwii` widget system. 2 | //! 3 | //! This crate supports parsing, error handling, and has a custom module_resolver. 4 | 5 | pub mod ast; 6 | pub mod builtins; 7 | mod dyn_id; 8 | pub mod error; 9 | pub mod helper; 10 | pub mod module_resolver; 11 | pub mod parser; 12 | pub mod providers; 13 | pub mod updates; 14 | -------------------------------------------------------------------------------- /licenses/README.md: -------------------------------------------------------------------------------- 1 | # Licenses 2 | 3 | This folder contains the license of [Eww](https://github.com/elkowar/eww), the project from which this project was originally forked. 4 | 5 | Modifications and additions in this fork are licensed under **GPL-3.0** (see `../LICENSE`). 6 | 7 | For details: 8 | 9 | - This fork: see `../LICENSE` (GPL-3.0) 10 | - Original Eww code: see `eww-MIT.txt` (MIT License) 11 | -------------------------------------------------------------------------------- /crates/ewwii_plugin_api/src/example.rs: -------------------------------------------------------------------------------- 1 | //! A module providing example implementations of plugins 2 | 3 | /// An example plugin that can be exported directly 4 | pub struct ExamplePlugin; 5 | 6 | impl crate::Plugin for ExamplePlugin { 7 | /// Example code that initalizes the plugin 8 | fn init(&self, host: &dyn crate::EwwiiAPI) { 9 | host.log("Example plugin says Hello!"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/ewwii_plugin_api/src/rhai_backend.rs: -------------------------------------------------------------------------------- 1 | //! Module exposing extra utilities for rhai. 2 | 3 | #[cfg(feature = "include-rhai")] 4 | mod rhai_included { 5 | /// _(include-rhai)_ An enumrate providing options for 6 | /// function registaration namespaces. 7 | pub enum RhaiFnNamespace { 8 | Custom(String), 9 | Global, 10 | } 11 | } 12 | 13 | #[cfg(feature = "include-rhai")] 14 | pub use rhai_included::*; 15 | -------------------------------------------------------------------------------- /examples/ewwii-bar/scripts/getvol: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | get_vol() { 4 | if command -v pamixer &>/dev/null; then 5 | if [ "$(pamixer --get-mute)" = "true" ]; then 6 | echo 0 7 | else 8 | pamixer --get-volume 9 | fi 10 | else 11 | amixer -D pulse sget Master | awk -F '[^0-9]+' '/Left:/{print $3}' 12 | fi 13 | } 14 | 15 | get_vol 16 | 17 | pactl subscribe | while read -r line; do 18 | if echo "$line" | grep -q "sink"; then 19 | get_vol 20 | fi 21 | done 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ewwii 2 | 3 | **General things to keep in mind:** 4 | 5 | - Run `cargo fmt` for formatting your code. 6 | - Always do PR (Pull Request) to iidev branch if it is a new feature. 7 | 8 | ## Codebase 9 | 10 | - `crates/ewwii`: Core of ewwii (ipc, daemon, options, rt engine, gtk, etc.) 11 | - `crates/rhai_impl`: Rhai implementation (parsing, modules, poll/listen handlers) 12 | - `crates/shared_utils`: Utility functions shared between rhai and ewwii (spans, helpers) 13 | -------------------------------------------------------------------------------- /crates/shared_utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared_utils" 3 | version = "0.1.0" 4 | authors = ["byson94 "] 5 | edition = "2021" 6 | license = "GPL-3.0-or-later" 7 | description = "Utility crate used in ewwii" 8 | repository = "https://github.com/ewwii-sh/ewwii" 9 | homepage = "https://github.com/ewwii-sh/ewwii" 10 | 11 | [dependencies] 12 | serde.workspace = true 13 | rhai = { workspace = true, features = ["internals"] } 14 | anyhow.workspace = true 15 | once_cell.workspace = true -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | max_width = 100 3 | use_field_init_shorthand = true 4 | 5 | # these where set when we where still on nightly 6 | 7 | # unstable_features = true 8 | # fn_single_line = false 9 | # reorder_impl_items = true 10 | # imports_granularity = "Crate" 11 | # normalize_comments = true 12 | # wrap_comments = true 13 | # combine_control_expr = false 14 | # condense_wildcard_suffixes = true 15 | # format_code_in_doc_comments = true 16 | # format_macro_matchers = true 17 | # format_strings = true 18 | -------------------------------------------------------------------------------- /proc_macros/scan_prop_proc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scan_prop_proc" 3 | version = "0.1.0" 4 | authors = ["byson94 "] 5 | edition = "2021" 6 | license = "GPL-3.0-or-later" 7 | description = "A procedual macro for generating properties on a WidgetNode" 8 | repository = "https://github.com/byson94/ewwii" 9 | homepage = "https://github.com/byson94/ewwii" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | syn.workspace = true 16 | quote.workspace = true 17 | proc-macro2.workspace = true -------------------------------------------------------------------------------- /crates/rhai_impl/src/providers/apilib/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod linux; 2 | pub mod wifi; 3 | 4 | use rhai::exported_module; 5 | use rhai::module_resolvers::StaticModuleResolver; 6 | 7 | pub fn register_apilib(resolver: &mut StaticModuleResolver) { 8 | use crate::providers::apilib::{linux::linux, wifi::wifi}; 9 | 10 | // adding modules 11 | let wifi_mod = exported_module!(wifi); 12 | let linux_mod = exported_module!(linux); 13 | 14 | // inserting modules 15 | resolver.insert("api::wifi", wifi_mod); 16 | resolver.insert("api::linux", linux_mod); 17 | } 18 | -------------------------------------------------------------------------------- /examples/data-structures/ewwii.scss: -------------------------------------------------------------------------------- 1 | * { 2 | all: unset; 3 | } 4 | 5 | .layout { 6 | padding: 8px; 7 | border: 1px solid black; 8 | border-radius: 4px; 9 | background-color: bisque; 10 | font-size: 16px; 11 | color: black; 12 | } 13 | 14 | .animalLayout { 15 | margin: 0 4px; 16 | } 17 | 18 | .animal { 19 | font-size: 24px; 20 | transition: 0.2s; 21 | border-radius: 4px; 22 | background-color: rgba(0, 0, 0, 0); 23 | border: 0 solid lightcoral; 24 | } 25 | 26 | .animal.selected { 27 | background-color: rgba(0, 0, 0, 0.2); 28 | border-width: 2px; 29 | } 30 | -------------------------------------------------------------------------------- /examples/activateLinux/ewwii.rhai: -------------------------------------------------------------------------------- 1 | enter([ 2 | defwindow( 3 | "activate linux", 4 | #{ 5 | monitor: 0, 6 | focusable: "none", 7 | stacking: "overlay", 8 | wm_ignore: false, 9 | geometry: #{ 10 | x: "50px", 11 | y: "20px", 12 | width: "50px", 13 | height: "30px", 14 | anchor: "bottom right", 15 | }, 16 | reserve: #{ distance: "40px", side: "top" }, 17 | }, 18 | label(#{ markup: "Activate linux\nGo to Settings to activate Linux", justify: "left",class: "activate"}) 19 | ), 20 | ]); 21 | -------------------------------------------------------------------------------- /crates/ewwii/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | fn main() { 3 | let output = Command::new("git").args(["rev-parse", "HEAD"]).output(); 4 | if let Ok(output) = output { 5 | if let Ok(hash) = String::from_utf8(output.stdout) { 6 | println!("cargo:rustc-env=GIT_HASH={}", hash); 7 | println!("cargo:rustc-env=CARGO_PKG_VERSION={} {}", env!("CARGO_PKG_VERSION"), hash); 8 | } 9 | } 10 | let output = Command::new("git").args(["show", "-s", "--format=%ci"]).output(); 11 | if let Ok(output) = output { 12 | if let Ok(date) = String::from_utf8(output.stdout) { 13 | println!("cargo:rustc-env=GIT_COMMIT_DATE={}", date); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/ewwii_plugin_api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ewwii_plugin_api" 3 | version = "0.7.0" 4 | authors = ["byson94 "] 5 | edition = "2021" 6 | license = "GPL-3.0-or-later" 7 | description = "A shared library for building plugins for ewwii" 8 | repository = "https://github.com/byson94/ewwii" 9 | homepage = "https://github.com/byson94/ewwii" 10 | 11 | [features] 12 | default = ["include-gtk4", "include-rhai"] 13 | 14 | ## Include the gtk4 dependency 15 | include-gtk4 = ["dep:gtk4"] 16 | 17 | ## Include the rhai dependency 18 | include-rhai = ["dep:rhai"] 19 | 20 | [dependencies] 21 | # rhai crate features should exactly match that of rhai_impl 22 | rhai = { workspace = true, optional = true, features = ["internals"] } 23 | gtk4 = { workspace = true, optional = true } -------------------------------------------------------------------------------- /crates/ewwii_plugin_api/src/widget_backend.rs: -------------------------------------------------------------------------------- 1 | //! Module exposing structures and types from the 2 | //! Widget rendering and definition backend in ewwii. 3 | 4 | #[cfg(feature = "include-gtk4")] 5 | mod gtk4_included { 6 | use gtk4::Widget as GtkWidget; 7 | use std::collections::HashMap; 8 | 9 | /// _(include-gtk4)_ A representation of widget registry which holds all the 10 | /// information needed for the dynamic runtime engine in ewwii. 11 | /// 12 | /// Not every change in this structure will be represented in the 13 | /// original WidgetRegistry in ewwii. Only the change on gtk4::Widget 14 | /// is reflected back. 15 | pub struct WidgetRegistryRepr<'a> { 16 | pub widgets: HashMap, 17 | } 18 | } 19 | 20 | #[cfg(feature = "include-gtk4")] 21 | pub use gtk4_included::*; 22 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/providers/stdlib/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | pub mod env; 3 | pub mod regex; 4 | pub mod text; 5 | 6 | use rhai::exported_module; 7 | use rhai::module_resolvers::StaticModuleResolver; 8 | 9 | pub fn register_stdlib(resolver: &mut StaticModuleResolver) { 10 | use crate::providers::stdlib::{command::command, env::env, regex::regex_lib, text::text}; 11 | 12 | // adding modules 13 | let text_mod = exported_module!(text); 14 | let env_mod = exported_module!(env); 15 | let command_mod = exported_module!(command); 16 | let regex_mod = exported_module!(regex_lib); 17 | 18 | // inserting modules 19 | resolver.insert("std::text", text_mod); 20 | resolver.insert("std::env", env_mod); 21 | resolver.insert("std::command", command_mod); 22 | resolver.insert("std::regex", regex_mod); 23 | } 24 | -------------------------------------------------------------------------------- /crates/ewwii_plugin_api/README.md: -------------------------------------------------------------------------------- 1 | # ewwii_plugin_api 2 | 3 | A shared interface providing traits for building plugins for ewwii. 4 | 5 | ## Example 6 | 7 | A simple example showing how to use this interface is provided below: 8 | 9 | ```rust 10 | use ewwii_plugin_api::{EwwiiAPI, Plugin, export_plugin}; 11 | 12 | pub struct DummyStructure; 13 | 14 | impl Plugin for DummyStructure { 15 | // critical for ewwii to launch the plugin 16 | fn init(&self, host: &dyn EwwiiAPI) { 17 | // will be printed by the host 18 | host.log("Plugin says Hello!"); 19 | host.rhai_engine_action(Box::new(|engine| { 20 | let ast = engine.compile("1+1"); 21 | println!("Compiled AST: {:#?}", ast); 22 | })); 23 | } 24 | } 25 | 26 | // Critical for ewwii to see the plugin 27 | export_plugin!(DummyStructure); 28 | ``` -------------------------------------------------------------------------------- /examples/ewwii-bar/ewwii.scss: -------------------------------------------------------------------------------- 1 | * { 2 | all: unset; // Unsets everything so you can style everything from scratch 3 | } 4 | 5 | // Global Styles 6 | .bar { 7 | all: unset; 8 | background-color: rgb(30, 30, 30); 9 | color: #cfd3da; 10 | padding: 10px; 11 | } 12 | 13 | // Styles on classes (see ewwii.rhai for more information) 14 | 15 | .sidestuff slider { 16 | all: unset; 17 | color: #ffcab1; 18 | } 19 | 20 | .metric scale trough highlight { 21 | all: unset; 22 | background-color: #e07a5f; 23 | color: #ffffff; 24 | border-radius: 10px; 25 | } 26 | 27 | .metric scale trough { 28 | all: unset; 29 | background-color: #3d3d3d; 30 | border-radius: 50px; 31 | min-height: 8px; 32 | min-width: 50px; 33 | margin-left: 10px; 34 | margin-right: 20px; 35 | } 36 | 37 | .workspaces button:hover { 38 | color: #e07a5f; 39 | } 40 | -------------------------------------------------------------------------------- /crates/rhai_impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rhai_impl" 3 | version = "0.1.0" 4 | authors = ["byson94 "] 5 | edition = "2021" 6 | license = "GPL-3.0-or-later" 7 | description = "The rhai embedded language implementation of ewwii" 8 | repository = "https://github.com/byson94/ewwii" 9 | homepage = "https://github.com/byson94/ewwii" 10 | 11 | [dependencies] 12 | shared_utils.workspace = true 13 | scan_prop_proc.workspace = true 14 | 15 | rhai = { workspace = true, features = ["internals"] } 16 | anyhow.workspace = true 17 | tokio = { workspace = true, features = ["full"] } 18 | log.workspace = true 19 | once_cell.workspace = true 20 | serde = { workspace = true, features = ["derive"] } 21 | ahash.workspace = true 22 | nix = { workspace = true, features = ["process", "fs", "signal"] } 23 | libc.workspace = true 24 | # error handling 25 | rhai_trace = "0.3.1" 26 | codespan-reporting.workspace = true 27 | regex.workspace = true 28 | gtk4.workspace = true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/module-request.yml: -------------------------------------------------------------------------------- 1 | name: Module request 2 | description: Suggest an new Rhai Module 3 | title: "[Module] " 4 | labels: [module-request] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: "Description of the module" 9 | description: "Provide an explanation of the module." 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: "Example usage" 15 | description: "Provide an example snippet of configuration showcasing how the module could be used, including the functions the module should support. For anything non-obvious, include an explanation of how the functions should behave." 16 | validations: 17 | required: false 18 | - type: textarea 19 | attributes: 20 | label: "Additional context" 21 | description: "Provide any additional context if applicable." 22 | validations: 23 | required: false 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: "[FEATURE] " 4 | labels: [enhancement] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: "Description of the requested feature" 9 | description: "Give a clear and concise description of the feature you are proposing, including examples for when it would be useful." 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: "Proposed configuration syntax" 15 | description: "If the feature you are requesting would add or change something to the Ewwii configuration, please provide an example for how the feature could be used." 16 | validations: 17 | required: false 18 | - type: textarea 19 | attributes: 20 | label: "Additional context" 21 | description: "If applicable, provide additional context or screenshots here." 22 | validations: 23 | required: false 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Please follow this template, if applicable. 2 | 3 | ## Description 4 | 5 | Provide a short description of the changes your PR introduces. 6 | This includes the actual feature you are adding, 7 | as well as any other relevant additions that were necessary to implement your feature. 8 | 9 | ## Usage 10 | 11 | When adding a widget or anything else that affects the configuration, 12 | please provide a minimal example configuration snippet showcasing how to use it and 13 | 14 | ### Showcase 15 | 16 | When adding widgets, please provide screenshots showcasing how your widget looks. 17 | This is not strictly required, but strongly appreciated. 18 | 19 | ## Additional Notes 20 | 21 | Anything else you want to add, such as remaining questions or explanations. 22 | 23 | ## Checklist 24 | 25 | Please make sure you can check all the boxes that apply to this PR. 26 | 27 | - [ ] All widgets I've added are correctly documented. 28 | - [ ] I added my changes to CHANGELOG.md, if appropriate. 29 | - [ ] I used `cargo fmt` to automatically format all code before committing 30 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/providers/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Rhai providers. 3 | 4 | This directory contains the code of all non-widget related functions and modules 5 | made to make the configuration better for users. 6 | It is not related to widgets at all and are just there 7 | for providing data or doing certain actions. 8 | */ 9 | 10 | mod apilib; 11 | mod stdlib; 12 | 13 | use crate::module_resolver::{ChainedResolver, SimpleFileResolver}; 14 | use crate::updates::ReactiveVarStore; 15 | use rhai::module_resolvers::StaticModuleResolver; 16 | 17 | // expose the api's publically 18 | pub use apilib::register_apilib; 19 | pub use stdlib::register_stdlib; 20 | 21 | pub fn register_all_providers(engine: &mut rhai::Engine, plhs: Option) { 22 | let mut resolver = StaticModuleResolver::new(); 23 | 24 | // modules 25 | register_stdlib(&mut resolver); 26 | register_apilib(&mut resolver); 27 | 28 | let chained = ChainedResolver { 29 | first: SimpleFileResolver { pl_handler_store: plhs.clone() }, 30 | second: resolver.clone(), 31 | }; 32 | engine.set_module_resolver(chained); 33 | } 34 | -------------------------------------------------------------------------------- /crates/ewwii/src/diag_error.rs: -------------------------------------------------------------------------------- 1 | use codespan_reporting::diagnostic; 2 | // use shared_utils::{Span, Spanned}; 3 | use crate::dynval; 4 | use thiserror::Error; 5 | 6 | // pub type DiagResult = Result; 7 | 8 | #[derive(Debug, Error)] 9 | // #[error("{}", .0.to_message())] // old one 10 | #[error("{:?}", .0)] 11 | pub struct DiagError(pub diagnostic::Diagnostic); 12 | 13 | static_assertions::assert_impl_all!(DiagError: Send, Sync); 14 | static_assertions::assert_impl_all!(dynval::ConversionError: Send, Sync); 15 | 16 | // /// Code used by yuck I suppose. 17 | // impl From for DiagError { 18 | // fn from(x: T) -> Self { 19 | // Self(x.to_diagnostic()) 20 | // } 21 | // } 22 | 23 | // impl DiagError { 24 | // pub fn note(self, note: &str) -> Self { 25 | // DiagError(self.0.with_notes(vec![note.to_string()])) 26 | // } 27 | // } 28 | 29 | // pub trait DiagResultExt { 30 | // fn note(self, note: &str) -> DiagResult; 31 | // } 32 | 33 | // impl DiagResultExt for DiagResult { 34 | // fn note(self, note: &str) -> DiagResult { 35 | // self.map_err(|e| e.note(note)) 36 | // } 37 | // } 38 | -------------------------------------------------------------------------------- /crates/ewwii/src/gen_diagnostic_macro.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! gen_diagnostic { 3 | ( $(kind = $kind:expr,)? 4 | $(msg = $msg:expr)? 5 | $(, label = $span:expr $(=> $label:expr)?)? 6 | $(, note = $note:expr)? $(,)? 7 | ) => { 8 | ::codespan_reporting::diagnostic::Diagnostic::new(gen_diagnostic! { 9 | @macro_fallback $({$kind})? {::codespan_reporting::diagnostic::Severity::Error} 10 | }) 11 | $(.with_message($msg.to_string()))? 12 | $(.with_labels(vec![ 13 | ::codespan_reporting::diagnostic::Label::primary($span.2, $span.0..$span.1) 14 | $(.with_message($label))? 15 | ]))? 16 | $(.with_notes(vec![$note.to_string()]))? 17 | }; 18 | ($msg:expr $(, $span:expr $(,)?)?) => {{ 19 | ::codespan_reporting::diagnostic::Diagnostic::error() 20 | .with_message($msg.to_string()) 21 | $(.with_labels(vec![::codespan_reporting::diagnostic::Label::primary($span.2, $span.0..$span.1)]))? 22 | }}; 23 | 24 | 25 | (@macro_fallback { $value:expr } { $fallback:expr }) => { 26 | $value 27 | }; 28 | (@macro_fallback { $fallback:expr }) => { 29 | $fallback 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/widget-request.yml: -------------------------------------------------------------------------------- 1 | name: Widget request 2 | description: Suggest an new Widget 3 | title: "[WIDGET] " 4 | labels: [widget-request] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: "Description of the widget" 9 | description: "Provide an explanation of the widget." 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: "Implementation proposal" 15 | description: "If applicable, describe which GTK-widgets this widget would be based on. A gallery of GTK widgets can be found at https://docs.gtk.org/gtk3. Please include links to the respective GTK documentation pages." 16 | validations: 17 | required: false 18 | - type: textarea 19 | attributes: 20 | label: "Example usage" 21 | description: "Provide an example snippet of configuration showcasing how the widget could be used, including the attributes the widget should support. For anything non-obvious, include an explanation of how the properties should behave." 22 | validations: 23 | required: false 24 | - type: textarea 25 | attributes: 26 | label: "Additional context" 27 | description: "Provide any additional context if applicable." 28 | validations: 29 | required: false 30 | -------------------------------------------------------------------------------- /crates/ewwii/src/window/window_definition.rs: -------------------------------------------------------------------------------- 1 | use crate::enum_parse; 2 | use std::fmt::Display; 3 | 4 | #[derive(Debug, thiserror::Error)] 5 | pub struct EnumParseError { 6 | pub input: String, 7 | pub expected: Vec<&'static str>, 8 | } 9 | impl Display for EnumParseError { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 11 | write!(f, "Failed to parse `{}`, must be one of {}", self.input, self.expected.join(", ")) 12 | } 13 | } 14 | 15 | #[derive( 16 | Debug, 17 | Clone, 18 | Copy, 19 | PartialEq, 20 | Eq, 21 | derive_more::Display, 22 | smart_default::SmartDefault, 23 | serde::Serialize, 24 | )] 25 | pub enum WindowStacking { 26 | #[default] 27 | Foreground, 28 | Background, 29 | Bottom, 30 | Overlay, 31 | } 32 | 33 | impl std::str::FromStr for WindowStacking { 34 | type Err = EnumParseError; 35 | 36 | fn from_str(s: &str) -> Result { 37 | enum_parse! { "WindowStacking", s, 38 | "foreground" | "fg" => WindowStacking::Foreground, 39 | "background" | "bg" => WindowStacking::Background, 40 | "bottom" | "bt" => WindowStacking::Bottom, 41 | "overlay" | "ov" => WindowStacking::Overlay, 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /licenses/eww-MIT.txt: -------------------------------------------------------------------------------- 1 | The following code is from Eww (MIT License). Only the original Eww code remains MIT. 2 | All modifications and additions in this project are licensed under GPL-3 (see LICENSE). 3 | 4 | === Original Eww (MIT License) === 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2020 ElKowar 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining 11 | a copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | container: 19 | image: archlinux:latest 20 | 21 | steps: 22 | - name: Install dependencies 23 | run: | 24 | pacman -Syu --noconfirm 25 | pacman -S --noconfirm base-devel gtk4 gtk4-layer-shell pkgconf git 26 | 27 | - uses: actions/checkout@v4 28 | 29 | - name: Setup rust 30 | uses: dtolnay/rust-toolchain@stable 31 | with: 32 | components: clippy,rustfmt 33 | 34 | - name: Load rust cache 35 | uses: Swatinem/rust-cache@v2 36 | 37 | - name: Setup problem matchers 38 | uses: r7kamura/rust-problem-matchers@v1 39 | 40 | - name: Check formatting 41 | run: cargo fmt -- --check 42 | 43 | - name: Check with default features 44 | run: cargo check 45 | 46 | - name: Run tests 47 | run: cargo test 48 | 49 | - name: Check x11 only 50 | run: cargo check --no-default-features --features=x11 51 | 52 | - name: Check wayland only 53 | run: cargo check --no-default-features --features=wayland 54 | 55 | - name: Check no-backend 56 | run: cargo check --no-default-features 57 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*", "tools/*", "proc_macros/*"] 3 | resolver = "2" 4 | 5 | [workspace.dependencies] 6 | 7 | shared_utils = { version = "0.1.0", path = "crates/shared_utils" } 8 | rhai_impl = { version = "0.1.0", path = "crates/rhai_impl" } 9 | scan_prop_proc = { version = "0.1.0", path = "proc_macros/scan_prop_proc" } 10 | ewwii_plugin_api = { version = "0.7.0", path = "crates/ewwii_plugin_api" } 11 | 12 | anyhow = "1.0.86" 13 | ahash = "0.8.12" 14 | bincode = "1.3.3" 15 | bytesize = "2.0.1" 16 | clap = { version = "4.5.1", features = ["derive"] } 17 | clap_complete = "4.5.12" 18 | codespan-reporting = "0.11" 19 | derive_more = { version = "1", features = [ 20 | "as_ref", 21 | "debug", 22 | "display", 23 | "from", 24 | "from_str", 25 | ] } 26 | extend = "1.2" 27 | futures = "0.3.30" 28 | grass = "0.13.4" 29 | gtk4 = "0.10.1" 30 | itertools = "0.13.0" 31 | libc = "0.2" 32 | log = "0.4" 33 | nix = "0.29.0" 34 | notify = "6.1.1" 35 | once_cell = "1.19" 36 | pretty_assertions = "1.4.0" 37 | pretty_env_logger = "0.5.0" 38 | regex = "1.10.5" 39 | rhai = "1.23.6" 40 | serde_json = "1.0" 41 | serde = { version = "1.0", features = ["derive"] } 42 | simple-signal = "1.1" 43 | smart-default = "0.7.1" 44 | static_assertions = "1.1.0" 45 | thiserror = "1.0" 46 | tokio = { version = "1.39.2", features = ["full"] } 47 | unescape = "0.1" 48 | wait-timeout = "0.2" 49 | syn = "2.0.107" 50 | quote = "1.0.41" 51 | proc-macro2 = "1.0.101" 52 | shell-words = "1.1.0" 53 | 54 | [profile.dev] 55 | split-debuginfo = "unpacked" 56 | -------------------------------------------------------------------------------- /crates/ewwii/src/application_lifecycle.rs: -------------------------------------------------------------------------------- 1 | //! Module concerned with handling the global application lifecycle of eww. 2 | //! Currently, this only means handling application exit by providing a global 3 | //! `recv_exit()` function which can be awaited to receive an event in case of application termination. 4 | 5 | use anyhow::{Context, Result}; 6 | use once_cell::sync::Lazy; 7 | use tokio::sync::broadcast; 8 | 9 | pub static APPLICATION_EXIT_SENDER: Lazy> = 10 | Lazy::new(|| broadcast::channel(2).0); 11 | 12 | /// Notify all listening tasks of the termination of the eww application process. 13 | pub fn send_exit() -> Result<()> { 14 | (APPLICATION_EXIT_SENDER).send(()).context("Failed to send exit lifecycle event")?; 15 | Ok(()) 16 | } 17 | 18 | /// Yields Ok(()) on application termination. Await on this in all long-running tasks 19 | /// and perform any cleanup if necessary. 20 | pub async fn recv_exit() -> Result<()> { 21 | (APPLICATION_EXIT_SENDER).subscribe().recv().await.context("Failed to receive lifecycle event") 22 | } 23 | 24 | /// Select in a loop, breaking once a application termination event (see `crate::application_lifecycle`) is received. 25 | #[macro_export] 26 | macro_rules! loop_select_exiting { 27 | ($($content:tt)*) => { 28 | loop { 29 | tokio::select! { 30 | Ok(()) = $crate::application_lifecycle::recv_exit() => { 31 | break; 32 | } 33 | $($content)* 34 | } 35 | } 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /proc_macros/scan_prop_proc/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{parse_macro_input, Data, DeriveInput, Fields}; 4 | 5 | #[proc_macro_attribute] 6 | pub fn scan_prop(_attr: TokenStream, item: TokenStream) -> TokenStream { 7 | let input = parse_macro_input!(item as DeriveInput); 8 | let name = &input.ident; 9 | 10 | let props_matches = if let Data::Enum(data_enum) = &input.data { 11 | data_enum 12 | .variants 13 | .iter() 14 | .filter_map(|v| match &v.fields { 15 | Fields::Named(fields) => { 16 | for f in &fields.named { 17 | if f.ident.as_ref().map(|id| id == "props").unwrap_or(false) { 18 | let vname = &v.ident; 19 | return Some(quote! { 20 | #name::#vname { props, .. } => Some(props) 21 | }); 22 | } 23 | } 24 | None 25 | } 26 | _ => None, 27 | }) 28 | .collect::>() 29 | } else { 30 | vec![] 31 | }; 32 | 33 | let expanded = quote! { 34 | #input 35 | 36 | impl #name { 37 | pub fn props(&self) -> Option<&Map> { 38 | match self { 39 | #(#props_matches),*, 40 | _ => None 41 | } 42 | } 43 | } 44 | }; 45 | 46 | TokenStream::from(expanded) 47 | } 48 | -------------------------------------------------------------------------------- /crates/ewwii/src/config/scss.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::{anyhow, Context}; 4 | 5 | use crate::{error_handling_ctx, util::replace_env_var_references}; 6 | 7 | /// read an (s)css file, replace all environment variable references within it and 8 | /// then parse it into css. 9 | /// Also adds the CSS to the [`crate::file_database::FileDatabase`] 10 | pub fn parse_scss_from_config(path: &Path) -> anyhow::Result<(usize, String)> { 11 | let css_file = path.join("ewwii.css"); 12 | let scss_file = path.join("ewwii.scss"); 13 | if css_file.exists() && scss_file.exists() { 14 | return Err(anyhow!( 15 | "Encountered both an SCSS and CSS file. Only one of these may exist at a time" 16 | )); 17 | } 18 | 19 | let (s_css_path, css) = if css_file.exists() { 20 | let css_file_content = std::fs::read_to_string(&css_file) 21 | .with_context(|| format!("Given CSS file doesn't exist: {}", css_file.display()))?; 22 | let css = replace_env_var_references(css_file_content); 23 | (css_file, css) 24 | } else { 25 | let scss_file_content = std::fs::read_to_string(&scss_file) 26 | .with_context(|| format!("Given SCSS file doesn't exist! {}", path.display()))?; 27 | let file_content = replace_env_var_references(scss_file_content); 28 | let grass_config = grass::Options::default().load_path(path); 29 | let css = grass::from_string(file_content, &grass_config) 30 | .map_err(|err| anyhow!("SCSS parsing error: {}", err))?; 31 | (scss_file, css) 32 | }; 33 | 34 | let mut file_db = error_handling_ctx::FILE_DATABASE.write().unwrap(); 35 | let file_id = file_db.insert_string(s_css_path.display().to_string(), css.clone())?; 36 | Ok((file_id, css)) 37 | } 38 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "locked": { 5 | "lastModified": 1709944340, 6 | "narHash": "sha256-xr54XK0SjczlUxRo5YwodibUSlpivS9bqHt8BNyWVQA=", 7 | "owner": "edolstra", 8 | "repo": "flake-compat", 9 | "rev": "baa7aa7bd0a570b3b9edd0b8da859fee3ffaa4d4", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "edolstra", 14 | "ref": "refs/pull/65/head", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "nixpkgs": { 20 | "locked": { 21 | "lastModified": 1725534445, 22 | "narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=", 23 | "owner": "nixos", 24 | "repo": "nixpkgs", 25 | "rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "nixos", 30 | "ref": "nixpkgs-unstable", 31 | "repo": "nixpkgs", 32 | "type": "github" 33 | } 34 | }, 35 | "root": { 36 | "inputs": { 37 | "flake-compat": "flake-compat", 38 | "nixpkgs": "nixpkgs", 39 | "rust-overlay": "rust-overlay" 40 | } 41 | }, 42 | "rust-overlay": { 43 | "inputs": { 44 | "nixpkgs": [ 45 | "nixpkgs" 46 | ] 47 | }, 48 | "locked": { 49 | "lastModified": 1725675754, 50 | "narHash": "sha256-hXW3csqePOcF2e/PYnpXj72KEYyNj2HzTrVNmS/F7Ug=", 51 | "owner": "oxalica", 52 | "repo": "rust-overlay", 53 | "rev": "8cc45e678e914a16c8e224c3237fb07cf21e5e54", 54 | "type": "github" 55 | }, 56 | "original": { 57 | "owner": "oxalica", 58 | "repo": "rust-overlay", 59 | "type": "github" 60 | } 61 | } 62 | }, 63 | "root": "root", 64 | "version": 7 65 | } 66 | -------------------------------------------------------------------------------- /crates/ewwii/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ewwii" 3 | version = "0.4.0" 4 | authors = ["byson94 "] 5 | description = "Widgets for everyone made better!" 6 | license = "GPL-3.0-or-later" 7 | repository = "https://github.com/byson94/ewwii" 8 | homepage = "https://github.com/byson94/ewwii" 9 | edition = "2021" 10 | 11 | 12 | [features] 13 | default = ["x11", "wayland"] 14 | x11 = ["gdk4-x11", "x11rb"] 15 | wayland = ["gtk4-layer-shell"] 16 | 17 | [dependencies] 18 | shared_utils.workspace = true 19 | rhai_impl.workspace = true 20 | ewwii_plugin_api.workspace = true 21 | 22 | gtk4-layer-shell = { version = "0.6.3", optional = true } 23 | gdk4-x11 = { version = "0.10.1", optional = true } 24 | x11rb = { version = "0.13.1", features = ["randr"], optional = true } 25 | 26 | grass.workspace = true 27 | thiserror.workspace = true 28 | anyhow.workspace = true 29 | bincode.workspace = true 30 | static_assertions.workspace = true 31 | clap = { workspace = true, features = ["derive"] } 32 | clap_complete.workspace = true 33 | codespan-reporting.workspace = true 34 | derive_more.workspace = true 35 | extend.workspace = true 36 | futures.workspace = true 37 | smart-default.workspace = true 38 | gtk4.workspace = true 39 | itertools.workspace = true 40 | log.workspace = true 41 | nix = { workspace = true, features = ["process", "fs", "signal"] } 42 | notify.workspace = true 43 | once_cell.workspace = true 44 | pretty_env_logger.workspace = true 45 | regex.workspace = true 46 | serde_json.workspace = true 47 | serde = { workspace = true, features = ["derive"] } 48 | simple-signal.workspace = true 49 | tokio = { workspace = true, features = ["full"] } 50 | unescape.workspace = true 51 | wait-timeout.workspace = true 52 | rhai = { workspace = true, features = ["internals"] } 53 | shell-words.workspace = true 54 | # Plugin loading 55 | libloading = "0.8.9" 56 | 57 | [dev-dependencies] 58 | pretty_assertions.workspace = true 59 | -------------------------------------------------------------------------------- /crates/ewwii_plugin_api/src/export_macros.rs: -------------------------------------------------------------------------------- 1 | //! Module implementing macros 2 | 3 | /// Macro to implement and export a plugin in a single step. 4 | /// With this macro, users can write their plugin code directly 5 | /// without having to manually implement each trait. 6 | /// 7 | /// ## Example 8 | /// 9 | /// The following example shows how you can use this macro to 10 | /// easily make plugins in a single step. 11 | /// 12 | /// ```rust,ignore 13 | /// use ewwii_plugin_api::auto_plugin; 14 | /// 15 | /// auto_plugin!(MyPluginName, { 16 | /// // host variable is passed in automatically 17 | /// host.log("Easy, huh?"); 18 | /// }); 19 | /// ``` 20 | /// 21 | /// ## When not to use it 22 | /// 23 | /// This macro shall not be used if you prefer flexibility and safety. 24 | /// The manual approach is verbose, but is way safer and flexible than using this macro. 25 | #[macro_export] 26 | macro_rules! auto_plugin { 27 | ($struct_name:ident, $init_block:block) => { 28 | pub struct $struct_name; 29 | 30 | // Implement the Plugin trait 31 | impl $crate::Plugin for $struct_name { 32 | fn init(&self, host: &dyn $crate::EwwiiAPI) { 33 | $init_block 34 | } 35 | } 36 | 37 | $crate::export_plugin!($struct_name); 38 | }; 39 | } 40 | 41 | /// Automatically implements `create_plugin` for a given fieldless structure 42 | #[macro_export] 43 | macro_rules! export_plugin { 44 | ($plugin_struct:path) => { 45 | #[unsafe(no_mangle)] 46 | pub extern "C" fn create_plugin() -> Box { 47 | Box::new($plugin_struct) 48 | } 49 | }; 50 | } 51 | 52 | /// Automatically implements `create_plugin` for a given structure that has fields. 53 | /// 54 | /// This macro expects the structure to have fields and also implement a `default()` method. 55 | #[macro_export] 56 | macro_rules! export_stateful_plugin { 57 | ($plugin_struct:path) => { 58 | #[unsafe(no_mangle)] 59 | pub extern "C" fn create_plugin() -> Box { 60 | Box::new(<$plugin_struct>::default()) 61 | } 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug you have encountered 3 | title: "[BUG] " 4 | labels: bug 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Checklist before submitting an issue 9 | options: 10 | - label: I have searched through the existing [closed and open issues](https://github.com/Ewwii-sh/ewwii/issues?q=is%3Aissue) for ewwii and made sure this is not a duplicate 11 | required: true 12 | - label: I have specifically verified that this bug is not a common [user error](https://github.com/Ewwii-sh/ewwii/issues?q=is%3Aissue+label%3Ano-actual-bug+is%3Aclosed) 13 | required: true 14 | - label: I am providing as much relevant information as I am able to in this bug report (Minimal config to reproduce the issue for example, if applicable) 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: "Description of the bug" 19 | description: "A clear an concise description of what the bug is." 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: "Reproducing the issue" 25 | description: "Please provide a clear and and minimal description of how to reproduce the bug. If possible, provide a simple example configuration that shows the issue." 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: "Expected behaviour" 31 | description: "A clear and concise description of what you expected to happen." 32 | validations: 33 | required: false 34 | - type: textarea 35 | attributes: 36 | label: "Additional context" 37 | description: "If applicable, provide additional context or screenshots here." 38 | validations: 39 | required: false 40 | - type: textarea 41 | attributes: 42 | label: "Platform and environment" 43 | description: "Does this happen on wayland, X11, or on both? What WM/Compositor are you using? Which version of ewwii are you using? (when using a git version, optimally provide the exact commit ref)." 44 | validations: 45 | required: true 46 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/providers/stdlib/command.rs: -------------------------------------------------------------------------------- 1 | use rhai::plugin::*; 2 | use rhai::EvalAltResult; 3 | use std::process::Command; 4 | 5 | #[export_module] 6 | pub mod command { 7 | /// Executes a shell command without capturing the output. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `cmd` - The shell command to execute as a string. 12 | /// 13 | /// # Returns 14 | /// 15 | /// This function returns nothing if the command executes successfully. If there is an error 16 | /// running the command, it returns the error. 17 | /// 18 | /// # Example 19 | /// 20 | /// ```javascript 21 | /// import "std::command" as cmd; 22 | /// 23 | /// // Run a shell command (e.g., list directory contents) 24 | /// cmd::run("ls -l"); 25 | /// ``` 26 | #[rhai_fn(return_raw)] 27 | pub fn run(cmd: &str) -> Result<(), Box> { 28 | Command::new("sh") 29 | .arg("-c") 30 | .arg(cmd) 31 | .status() 32 | .map_err(|e| format!("Failed to run command: {}", e))?; 33 | Ok(()) 34 | } 35 | 36 | /// Executes a shell command and captures its output. 37 | /// 38 | /// # Arguments 39 | /// 40 | /// * `cmd` - The shell command to execute as a string. 41 | /// 42 | /// # Returns 43 | /// 44 | /// This function returns the standard output of the command as a `string`. If the command fails, 45 | /// it returns the error. 46 | /// 47 | /// # Example 48 | /// 49 | /// ```javascript 50 | /// import "std::command" as cmd; 51 | /// 52 | /// // Run a shell command and capture its output 53 | /// let output = cmd::run_and_read("echo 'Hello, world!'"); 54 | /// print(output); // output: Hello, world! 55 | /// ``` 56 | #[rhai_fn(return_raw)] 57 | pub fn run_and_read(cmd: &str) -> Result> { 58 | let output = Command::new("sh") 59 | .arg("-c") 60 | .arg(cmd) 61 | .output() 62 | .map_err(|e| format!("Failed to run command: {}", e))?; 63 | 64 | Ok(String::from_utf8_lossy(&output.stdout).to_string()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/ewwii/src/client.rs: -------------------------------------------------------------------------------- 1 | use std::process::Stdio; 2 | 3 | use crate::{ 4 | daemon_response::DaemonResponse, 5 | opts::{self, ActionClientOnly}, 6 | paths::EwwiiPaths, 7 | }; 8 | use anyhow::{Context, Result}; 9 | use std::{ 10 | io::{Read, Write}, 11 | os::unix::net::UnixStream, 12 | }; 13 | 14 | pub fn handle_client_only_action(paths: &EwwiiPaths, action: ActionClientOnly) -> Result<()> { 15 | match action { 16 | ActionClientOnly::Logs => { 17 | std::process::Command::new("tail") 18 | .args(["-f", paths.get_log_file().to_string_lossy().as_ref()].iter()) 19 | .stdin(Stdio::null()) 20 | .spawn()? 21 | .wait()?; 22 | } 23 | } 24 | Ok(()) 25 | } 26 | 27 | /// Connect to the daemon and send the given request. 28 | /// Returns the response from the daemon, or None if the daemon did not provide any useful response. An Ok(None) response does _not_ indicate failure. 29 | pub fn do_server_call( 30 | stream: &mut UnixStream, 31 | action: &opts::ActionWithServer, 32 | ) -> Result> { 33 | log::debug!("Forwarding options to server"); 34 | stream.set_nonblocking(false).context("Failed to set stream to non-blocking")?; 35 | 36 | let message_bytes = bincode::serialize(&action)?; 37 | 38 | stream 39 | .write(&(message_bytes.len() as u32).to_be_bytes()) 40 | .context("Failed to send command size header to IPC stream")?; 41 | 42 | stream.write_all(&message_bytes).context("Failed to write command to IPC stream")?; 43 | 44 | let mut buf = Vec::new(); 45 | // NO TIMEOUT!!!!!! 46 | // Why is timeout even needed here? 47 | // it just breaks stuff if didnt read anything in 100 ms 48 | // thats crazy.... I ain't gonna add it. 49 | // stream 50 | // .set_read_timeout(Some(std::time::Duration::from_millis(100))) 51 | // .context("Failed to set read timeout")?; 52 | stream.read_to_end(&mut buf).context("Error reading response from server")?; 53 | 54 | Ok(if buf.is_empty() { 55 | None 56 | } else { 57 | let buf = bincode::deserialize(&buf)?; 58 | Some(buf) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /examples/data-structures/ewwii.rhai: -------------------------------------------------------------------------------- 1 | // Array of emoji 2 | let stringArray = [ 3 | "🦝", "🐱", "🐵", "🦁", "🐹", "🦊" 4 | ]; 5 | 6 | // Mapping from emoji to names 7 | let object = #{ 8 | "🦝": "racoon", 9 | "🐱": "cat", 10 | "🐵": "ape", 11 | "🦁": "lion", 12 | "🐹": "hamster", 13 | "🦊": "fox" 14 | }; 15 | 16 | 17 | // Widget: a single animal button 18 | fn animalButton(emoji, selected) { 19 | let class = "animal"; 20 | 21 | if selected == emoji { 22 | class = "animal selected" 23 | } 24 | 25 | return box(#{ class: "animalLayout" }, [ 26 | eventbox(#{ 27 | class: class, 28 | cursor: "pointer", 29 | onclick: "echo " + emoji + " >> /tmp/selected_emoji.txt", 30 | }, [ 31 | label(#{ text: emoji }) // unique per emoji 32 | ]) 33 | ]); 34 | } 35 | 36 | // Widget: the row of animal buttons 37 | fn animalRow(stringArray, selected) { 38 | let buttons = []; 39 | for emoji in stringArray { 40 | buttons.push(animalButton(emoji, selected)); 41 | } 42 | return box(#{ 43 | class: "animals", 44 | orientation: "h", 45 | halign: "center" 46 | }, buttons); 47 | } 48 | 49 | // Widget: currently selected animal display 50 | fn currentAnimal(object, selected) { 51 | return box(#{}, [ 52 | label(#{ text: object[selected] + " " + selected }) 53 | ]); 54 | } 55 | 56 | // Layout widget 57 | fn layout(stringArray, object, selected) { 58 | return box(#{ 59 | class: "layout", 60 | orientation: "v", 61 | halign: "center" 62 | }, [ 63 | animalRow(stringArray, selected), 64 | currentAnimal(object, selected) 65 | ]); 66 | } 67 | 68 | enter([ 69 | // Track the selected emoji 70 | listen("selected", #{ 71 | cmd: "scripts/readlast_and_truncate.sh", 72 | initial: "🦝", 73 | }), 74 | 75 | defwindow("data-structures", #{ 76 | monitor: 0, 77 | exclusive: false, 78 | focusable: "none", 79 | windowtype: "normal", 80 | geometry: #{ anchor: "center", x: "0px", y: "0px", width: "100px", height: "10px" }, 81 | }, layout(stringArray, object, selected)) 82 | ]) -------------------------------------------------------------------------------- /crates/shared_utils/src/span.rs: -------------------------------------------------------------------------------- 1 | /// A span is made up of 2 | /// - the start location 3 | /// - the end location 4 | /// - the file id 5 | #[derive(Eq, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)] 6 | pub struct Span(pub usize, pub usize, pub usize); 7 | 8 | impl Span { 9 | pub const DUMMY: Span = Span(usize::MAX, usize::MAX, usize::MAX); 10 | 11 | pub fn point(loc: usize, file_id: usize) -> Self { 12 | Span(loc, loc, file_id) 13 | } 14 | 15 | /// Get the span that includes this and the other span completely. 16 | /// Will panic if the spans are from different file_ids. 17 | pub fn to(mut self, other: Span) -> Self { 18 | assert!(other.2 == self.2); 19 | self.1 = other.1; 20 | self 21 | } 22 | 23 | pub fn ending_at(mut self, end: usize) -> Self { 24 | self.1 = end; 25 | self 26 | } 27 | 28 | /// Turn this span into a span only highlighting the point it starts at, setting the length to 0. 29 | pub fn point_span(mut self) -> Self { 30 | self.1 = self.0; 31 | self 32 | } 33 | 34 | /// Turn this span into a span only highlighting the point it ends at, setting the length to 0. 35 | pub fn point_span_at_end(mut self) -> Self { 36 | self.0 = self.1; 37 | self 38 | } 39 | 40 | pub fn shifted(mut self, n: isize) -> Self { 41 | self.0 = isize::max(0, self.0 as isize + n) as usize; 42 | self.1 = isize::max(0, self.0 as isize + n) as usize; 43 | self 44 | } 45 | 46 | pub fn new_relative(mut self, other_start: usize, other_end: usize) -> Self { 47 | self.0 += other_start; 48 | self.1 += other_end; 49 | self 50 | } 51 | 52 | pub fn is_dummy(&self) -> bool { 53 | *self == Self::DUMMY 54 | } 55 | } 56 | 57 | impl std::fmt::Display for Span { 58 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 59 | if self.is_dummy() { 60 | write!(f, "DUMMY") 61 | } else { 62 | write!(f, "{}..{}", self.0, self.1) 63 | } 64 | } 65 | } 66 | 67 | impl std::fmt::Debug for Span { 68 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 69 | write!(f, "{}", self) 70 | } 71 | } 72 | 73 | pub trait Spanned { 74 | fn span(&self) -> Span; 75 | } 76 | -------------------------------------------------------------------------------- /examples/ewwii-bar/ewwii.rhai: -------------------------------------------------------------------------------- 1 | fn bar(music_var, volume, time) { 2 | return box(#{ orientation: "h" }, [ 3 | workspaces(), 4 | music(music_var), 5 | sidestuff(volume, time), 6 | ]); 7 | } 8 | 9 | fn sidestuff(volume, time) { 10 | return box(#{ class: "sidestuff", orientation: "h", space_evenly: false, halign: "end"}, [ 11 | metric(#{ 12 | label: "🔊", 13 | value: volume, 14 | onchange: "pamixer --set-volume {}", 15 | }), 16 | label(#{ text: time }) 17 | ]); 18 | } 19 | 20 | fn workspaces() { 21 | let buttons = []; 22 | 23 | // looping and creating buttons 24 | for n in 0..9 { 25 | buttons.push( 26 | button(#{ 27 | onclick: "i3-msg workspace " + (n + 1), 28 | label: (n + 1).to_string() 29 | }) 30 | ); 31 | } 32 | 33 | return box(#{ 34 | class: "workspaces", 35 | orientation: "h", 36 | space_evenly: true, 37 | halign: "start", 38 | spacing: 10, 39 | }, buttons); 40 | } 41 | 42 | fn music(music_var) { 43 | let label_text = if music_var != "" { 44 | "🎵" + music_var 45 | } else { 46 | "" 47 | }; 48 | return box(#{ 49 | class: "music", 50 | orientation: "h", 51 | space_evenly: false, 52 | halign: "center" 53 | }, [ 54 | label(#{ text: label_text }), 55 | ]); 56 | } 57 | 58 | fn metric(props) { 59 | let label_prop = props.label; 60 | let value_prop = props.value; 61 | let onchange_prop = props.onchange; 62 | 63 | return box(#{ 64 | orientation: "h", 65 | class: "metric", 66 | space_evenly: false, 67 | }, [ 68 | box(#{ class: "label" }, [ label(#{ text: label_prop }) ]), 69 | scale(#{ 70 | min: 0, 71 | max: 101, 72 | active: onchange_prop != "", 73 | value: value_prop, 74 | onchange: onchange_prop, 75 | }), 76 | ]); 77 | } 78 | 79 | enter([ 80 | listen("music_var", #{ 81 | initial: "", 82 | cmd: "echo Playing Ewwii", 83 | }), 84 | 85 | listen("volume", #{ 86 | cmd: "scripts/getvol", 87 | initial: "" 88 | }), 89 | 90 | poll("time", #{ 91 | interval: "10s", 92 | cmd: "date '+%H:%M %b %d, %Y'", 93 | initial: "" 94 | }), 95 | 96 | defwindow("bar", #{ 97 | monitor: 0, 98 | windowtype: "dock", 99 | geometry: #{ 100 | x: "0%", 101 | y: "0%", 102 | width: "90%", 103 | height: "10px", 104 | anchor: "top center", 105 | }, 106 | reserve: #{ side: "top", distance: "4%" } 107 | }, bar(music_var, volume, time)), 108 | ]); -------------------------------------------------------------------------------- /crates/ewwii/src/window/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::Infallible, 3 | fmt, 4 | str::{self}, 5 | }; 6 | 7 | use crate::dynval::DynVal; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | /// The type of the identifier used to select a monitor 11 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 12 | pub enum MonitorIdentifier { 13 | List(Vec), 14 | Numeric(i32), 15 | Name(String), 16 | Primary, 17 | } 18 | 19 | // impl MonitorIdentifier { 20 | // pub fn from_dynval(val: &DynVal) -> Result { 21 | // match val.as_json_array() { 22 | // Ok(arr) => Ok(MonitorIdentifier::List( 23 | // arr.iter().map(|x| MonitorIdentifier::from_dynval(&x.into())).collect::>()?, 24 | // )), 25 | // Err(_) => match val.as_i32() { 26 | // Ok(x) => Ok(MonitorIdentifier::Numeric(x)), 27 | // Err(_) => Ok(MonitorIdentifier::from_str(&val.as_string().unwrap()).unwrap()), 28 | // }, 29 | // } 30 | // } 31 | 32 | // pub fn is_numeric(&self) -> bool { 33 | // matches!(self, Self::Numeric(_)) 34 | // } 35 | // } 36 | 37 | impl From<&MonitorIdentifier> for DynVal { 38 | fn from(val: &MonitorIdentifier) -> Self { 39 | match val { 40 | MonitorIdentifier::List(l) => l.iter().map(|x| x.into()).collect::>().into(), 41 | MonitorIdentifier::Numeric(n) => DynVal::from(*n), 42 | MonitorIdentifier::Name(n) => DynVal::from(n.clone()), 43 | MonitorIdentifier::Primary => DynVal::from(""), 44 | } 45 | } 46 | } 47 | 48 | impl fmt::Display for MonitorIdentifier { 49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 50 | match self { 51 | Self::List(l) => { 52 | write!(f, "[{}]", l.iter().map(|x| x.to_string()).collect::>().join(" ")) 53 | } 54 | Self::Numeric(n) => write!(f, "{}", n), 55 | Self::Name(n) => write!(f, "{}", n), 56 | Self::Primary => write!(f, ""), 57 | } 58 | } 59 | } 60 | 61 | impl str::FromStr for MonitorIdentifier { 62 | type Err = Infallible; 63 | 64 | fn from_str(s: &str) -> Result { 65 | match s.parse::() { 66 | Ok(n) => Ok(Self::Numeric(n)), 67 | Err(_) => { 68 | if &s.to_lowercase() == "" { 69 | Ok(Self::Primary) 70 | } else { 71 | Ok(Self::Name(s.to_owned())) 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/updates/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This is where we update the variables in ewwii 3 | 4 | Since we can poll and listen in ewwii, it is what we will 5 | use to get variable updates in ewwii. 6 | 7 | Updates are important because it helps keeping rhai dynamic. 8 | Even though rhai is static by nature (only evaluates once), 9 | we can trigger a re-evaluation every time a variable updates 10 | as a workaround to this limitation. 11 | 12 | Other than the poll and listen, we also handle the updates of 13 | the internal built in signals (the functions that return data) 14 | which is also known as "magic variables" in eww. 15 | */ 16 | 17 | mod listen; 18 | mod localsignal; 19 | mod poll; 20 | 21 | pub use localsignal::*; 22 | 23 | use crate::ast::WidgetNode; 24 | use listen::handle_listen; 25 | use once_cell::sync::Lazy; 26 | use poll::handle_poll; 27 | use std::process::Command; 28 | use std::sync::Mutex; 29 | use std::{collections::HashMap, sync::Arc, sync::RwLock}; 30 | use tokio::sync::mpsc::UnboundedSender; 31 | use tokio::sync::watch; 32 | 33 | pub type ReactiveVarStore = Arc>>; 34 | pub static SHUTDOWN_REGISTRY: Lazy>>> = 35 | Lazy::new(|| Mutex::new(Vec::new())); 36 | 37 | pub fn get_prefered_shell() -> String { 38 | // Check Dash and prefer if dash is installed. 39 | let dash_installed: bool = 40 | Command::new("which").arg("dash").output().map(|o| o.status.success()).unwrap_or(false); 41 | 42 | let shell = if dash_installed { String::from("/bin/dash") } else { String::from("/bin/sh") }; 43 | 44 | shell 45 | } 46 | 47 | pub fn handle_state_changes( 48 | root_node: &WidgetNode, 49 | tx: UnboundedSender, 50 | store: ReactiveVarStore, 51 | ) { 52 | let shell = get_prefered_shell(); 53 | if let WidgetNode::Enter(children) = root_node { 54 | for child in children { 55 | match child { 56 | WidgetNode::Poll { var, props } => { 57 | handle_poll(var.to_string(), props, shell.clone(), store.clone(), tx.clone()); 58 | } 59 | WidgetNode::Listen { var, props } => { 60 | handle_listen(var.to_string(), props, shell.clone(), store.clone(), tx.clone()); 61 | } 62 | _ => {} 63 | } 64 | } 65 | } else { 66 | log::warn!("Expected Enter() as root node for config"); 67 | } 68 | } 69 | 70 | pub fn kill_state_change_handler() { 71 | let registry = SHUTDOWN_REGISTRY.lock().unwrap(); 72 | for sender in registry.iter() { 73 | let _ = sender.send(true); 74 | } 75 | log::debug!("All state change handlers requested to stop"); 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![dependency status](https://deps.rs/repo/github/byson94/ewwii/status.svg)](https://deps.rs/repo/github/byson94/ewwii) 2 | [![docs link](https://img.shields.io/badge/documentation-link-blue)](https://ewwii-sh.github.io/docs) 3 | 4 | # Ewwii 5 | 6 | 7 | 8 | Elkowars Wacky Widgets Imporved Interface is a fork of Elkowars Wacky Widgets which is a standalone widget system made in Rust that allows you to implement your own, custom widgets in any window manager. 9 | 10 | ## Examples 11 | 12 | Examples of projects powered by ewwii. 13 | 14 | | Project | Preview | 15 | |---------|---------| 16 | | **Basic Bar**
[- View Example](./examples/ewwii-bar) | [![Basic Bar](./examples/ewwii-bar/ewwii-bar.png)](./examples/ewwii-bar) | 17 | | **Data Structures**
[- View Example](./examples/data-structures) | [![Data Structures](./examples/data-structures/data-structures-preview.png)](./examples/data-structures) | 18 | | **Wi-Fi GUI Template**
[- View on GitHub](https://github.com/Ewwii-sh/ewifi_gui_template) | ![Wi-Fi GUI Template](https://raw.githubusercontent.com/Ewwii-sh/ewifi_gui_template/main/.github/wifi_manager_template.png) | 19 | | **Obsidian Bar Template**
[- View on GitHub](https://github.com/Ewwii-sh/obsidian-bar) | [![Obsidian Bar](https://raw.githubusercontent.com/Ewwii-sh/obsidian-bar/main/.github/screenshot.png)](https://github.com/Ewwii-sh/obsidian-bar) | 20 | | **Binary Dots by [@BinaryHarbinger](https://github.com/BinaryHarbinger)**
[- View on GitHub](https://github.com/BinaryHarbinger/binarydots/) | [![Binary Dots](https://raw.githubusercontent.com/BinaryHarbinger/binarydots/main/preview/desktop.png)](https://github.com/BinaryHarbinger/binarydots) 21 | | **Astatine Dots (Linux Rice with Ewwii)**
[- View on GitHub](https://github.com/Ewwii-sh/astatine-dots) | [![Astatine Dots](https://github.com/user-attachments/assets/f028ca1f-e403-476d-a7d9-cadce47691b7)](https://github.com/Ewwii-sh/astatine-dots) | 22 | 23 | ## Features 24 | 25 | - Powered by Gtk4 26 | - Supports Hot reload 27 | - Extensibility via plugins and rhai modules 28 | - X11 + Wayland support 29 | 30 | ## Contribewwtiing 31 | 32 | If you want to contribute anything, like adding new widgets, features, or subcommands (including sample configs), you should definitely do so. 33 | 34 | ### Steps 35 | 36 | 1. Fork this repository 37 | 2. Read `CONTRIBUTING.md` 38 | 3. Install dependencies 39 | 4. Write down your changes in CHANGELOG.md 40 | 5. Open a pull request once you're finished 41 | 42 | ## Licensing 43 | 44 | This project is a fork of [Eww](https://github.com/elkowar/eww) (MIT License). 45 | 46 | - Original Eww code remains under MIT License (see `licenses/eww-MIT.txt`). 47 | - Modifications and additions in this fork are licensed under GPL-3.0 (see `LICENSE`). 48 | 49 | ## Widget 50 | 51 | https://en.wikipedia.org/wiki/Wikipedia:Widget 52 | -------------------------------------------------------------------------------- /crates/ewwii/src/plugin.rs: -------------------------------------------------------------------------------- 1 | use ewwii_plugin_api::{rhai_backend, widget_backend, EwwiiAPI}; 2 | use rhai::{Array, Dynamic, Engine, EvalAltResult}; 3 | use std::sync::mpsc::{channel as mpsc_channel, Receiver, Sender}; 4 | 5 | pub(crate) struct EwwiiImpl { 6 | pub(crate) requestor: Sender, 7 | } 8 | 9 | impl EwwiiAPI for EwwiiImpl { 10 | // General 11 | // "PCL = Plugin Controlled Log" 12 | fn print(&self, msg: &str) { 13 | println!("[PCL] {}", msg); 14 | } 15 | 16 | fn log(&self, msg: &str) { 17 | log::info!("[PCL] {}", msg); 18 | } 19 | 20 | fn warn(&self, msg: &str) { 21 | log::warn!("[PCL] {}", msg); 22 | } 23 | 24 | fn error(&self, msg: &str) { 25 | log::error!("[PCL] {}", msg); 26 | } 27 | 28 | // Rhai Manipulation Stuff 29 | fn rhai_engine_action(&self, f: Box) -> Result<(), String> { 30 | self.requestor 31 | .send(PluginRequest::RhaiEngineAct(f)) 32 | .map_err(|_| "Failed to send request to host".to_string())?; 33 | Ok(()) 34 | } 35 | 36 | fn register_function( 37 | &self, 38 | name: String, 39 | namespace: rhai_backend::RhaiFnNamespace, 40 | f: Box Result> + Send + Sync>, 41 | ) -> Result<(), String> { 42 | let func_info = (name, namespace, f); 43 | 44 | self.requestor 45 | .send(PluginRequest::RegisterFunc(func_info)) 46 | .map_err(|_| "Failed to send request to host".to_string())?; 47 | Ok(()) 48 | } 49 | 50 | // Widget Rendering & Logic 51 | fn list_widget_ids(&self) -> Result, String> { 52 | let (tx, rx): (Sender>, Receiver>) = mpsc_channel(); 53 | 54 | self.requestor 55 | .send(PluginRequest::ListWidgetIds(tx)) 56 | .map_err(|_| "Failed to send request to host".to_string())?; 57 | 58 | match rx.recv() { 59 | Ok(r) => Ok(r), 60 | Err(e) => Err(e.to_string()), 61 | } 62 | } 63 | 64 | fn widget_reg_action( 65 | &self, 66 | f: Box, 67 | ) -> Result<(), String> { 68 | self.requestor 69 | .send(PluginRequest::WidgetRegistryAct(f)) 70 | .map_err(|_| "Failed to send request to host".to_string())?; 71 | Ok(()) 72 | } 73 | } 74 | 75 | pub(crate) enum PluginRequest { 76 | RhaiEngineAct(Box), 77 | RegisterFunc( 78 | ( 79 | String, 80 | rhai_backend::RhaiFnNamespace, 81 | Box Result> + Send + Sync>, 82 | ), 83 | ), 84 | ListWidgetIds(Sender>), 85 | WidgetRegistryAct(Box), 86 | } 87 | -------------------------------------------------------------------------------- /crates/ewwii/src/daemon_response.rs: -------------------------------------------------------------------------------- 1 | //! Types to manage messages that notify the ewwii client over the result of a command 2 | //! 3 | //! Communcation between the daemon and ewwii client happens via IPC. 4 | //! If the daemon needs to send messages back to the client as a response to a command (mostly for CLI output), 5 | //! this happens via the DaemonResponse types 6 | 7 | use anyhow::{Context, Result}; 8 | use itertools::Itertools; 9 | use tokio::sync::mpsc; 10 | 11 | use crate::error_handling_ctx; 12 | 13 | /// Response that the app may send as a response to a event. 14 | /// This is used in `DaemonCommand`s that contain a response sender. 15 | #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, derive_more::Display)] 16 | pub enum DaemonResponse { 17 | Success(String), 18 | Failure(String), 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct DaemonResponseSender(mpsc::UnboundedSender); 23 | 24 | pub fn create_pair() -> (DaemonResponseSender, mpsc::UnboundedReceiver) { 25 | let (sender, recv) = mpsc::unbounded_channel(); 26 | (DaemonResponseSender(sender), recv) 27 | } 28 | 29 | impl DaemonResponseSender { 30 | pub fn send_success(&self, s: String) -> Result<()> { 31 | self.0 32 | .send(DaemonResponse::Success(s)) 33 | .context("Failed to send success response from application thread") 34 | } 35 | 36 | pub fn send_failure(&self, s: String) -> Result<()> { 37 | self.0 38 | .send(DaemonResponse::Failure(s)) 39 | .context("Failed to send failure response from application thread") 40 | } 41 | 42 | /// Given a list of errors, respond with an error value if there are any errors, and respond with success otherwise. 43 | pub fn respond_with_error_list( 44 | &self, 45 | errors: impl IntoIterator, 46 | ) -> Result<()> { 47 | let errors = errors.into_iter().map(|e| error_handling_ctx::format_error(&e)).join("\n"); 48 | if errors.is_empty() { 49 | self.send_success(String::new()) 50 | } else { 51 | self.respond_with_error_msg(errors) 52 | } 53 | } 54 | 55 | /// In case of an Err, send the error message to a sender. 56 | pub fn respond_with_result(&self, result: Result) -> Result<()> { 57 | match result { 58 | Ok(_) => self.send_success(String::new()), 59 | Err(e) => { 60 | let formatted = error_handling_ctx::format_error(&e); 61 | self.respond_with_error_msg(formatted) 62 | } 63 | } 64 | .context("sending response from main thread") 65 | } 66 | 67 | fn respond_with_error_msg(&self, msg: String) -> Result<()> { 68 | println!("Action failed with error: {}", msg); 69 | self.send_failure(msg) 70 | } 71 | } 72 | 73 | pub type DaemonResponseReceiver = mpsc::UnboundedReceiver; 74 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | flake-compat.url = "github:edolstra/flake-compat/refs/pull/65/head"; 4 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 5 | rust-overlay = { 6 | url = "github:oxalica/rust-overlay"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | }; 10 | 11 | outputs = 12 | { 13 | self, 14 | nixpkgs, 15 | rust-overlay, 16 | flake-compat, 17 | }: 18 | let 19 | overlays = [ 20 | (import rust-overlay) 21 | self.overlays.default 22 | ]; 23 | pkgsFor = system: import nixpkgs { inherit system overlays; }; 24 | 25 | targetSystems = [ 26 | "aarch64-linux" 27 | "x86_64-linux" 28 | ]; 29 | mkRustToolchain = pkgs: pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 30 | in 31 | { 32 | overlays.default = final: prev: { inherit (self.packages.${prev.system}) ewwii ewwii-wayland; }; 33 | 34 | packages = nixpkgs.lib.genAttrs targetSystems ( 35 | system: 36 | let 37 | pkgs = pkgsFor system; 38 | rust = mkRustToolchain pkgs; 39 | rustPlatform = pkgs.makeRustPlatform { 40 | cargo = rust; 41 | rustc = rust; 42 | }; 43 | version = (builtins.fromTOML (builtins.readFile ./crates/ewwii/Cargo.toml)).package.version; 44 | in 45 | rec { 46 | ewwii = rustPlatform.buildRustPackage { 47 | version = "${version}-dirty"; 48 | pname = "ewwii"; 49 | 50 | src = ./.; 51 | cargoLock.lockFile = ./Cargo.lock; 52 | cargoBuildFlags = [ 53 | "--bin" 54 | "ewwii" 55 | ]; 56 | 57 | nativeBuildInputs = with pkgs; [ 58 | pkg-config 59 | wrapGAppsHook 60 | ]; 61 | buildInputs = with pkgs; [ 62 | gtk3 63 | librsvg 64 | gtk-layer-shell 65 | libdbusmenu-gtk3 66 | ]; 67 | }; 68 | 69 | ewwii-wayland = nixpkgs.lib.warn "`ewwii-wayland` is deprecated due to ewwii building with both X11 and wayland support by default. Use `ewwii` instead." ewwii; 70 | default = ewwii; 71 | } 72 | ); 73 | 74 | devShells = nixpkgs.lib.genAttrs targetSystems ( 75 | system: 76 | let 77 | pkgs = pkgsFor system; 78 | rust = mkRustToolchain pkgs; 79 | in 80 | { 81 | default = pkgs.mkShell { 82 | inputsFrom = [ self.packages.${system}.ewwii ]; 83 | packages = with pkgs; [ 84 | deno 85 | mdbook 86 | zbus-xmlgen 87 | ]; 88 | 89 | RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library"; 90 | }; 91 | } 92 | ); 93 | 94 | formatter = nixpkgs.lib.genAttrs targetSystems (system: (pkgsFor system).nixfmt-rfc-style); 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/error.rs: -------------------------------------------------------------------------------- 1 | use codespan_reporting::diagnostic::{Diagnostic, Label}; 2 | use codespan_reporting::files::SimpleFiles; 3 | use codespan_reporting::term::{self, termcolor::Buffer}; 4 | use rhai::{Engine, EvalAltResult, ParseError}; 5 | use rhai_trace::{BetterError, Span}; 6 | 7 | /// Return a formatted Rhai evaluation error. 8 | pub fn format_eval_error( 9 | error: &EvalAltResult, 10 | code: &str, 11 | engine: &Engine, 12 | file_id: Option<&str>, 13 | ) -> String { 14 | let error_str = error.to_string(); 15 | 16 | if error_str == "" || error_str == "module_eval_failed" || error_str == "module_parse_failed" { 17 | return String::new(); 18 | } 19 | 20 | let better_error = 21 | BetterError::improve_eval_error(error, code, engine, None).unwrap_or(BetterError { 22 | message: error_str, 23 | help: None, 24 | hint: None, 25 | note: None, 26 | span: Span::new(0, 0, 0, 0), 27 | }); 28 | format_codespan_error(better_error, code, file_id) 29 | } 30 | 31 | /// Return a formatted Rhai parse error. 32 | pub fn format_parse_error(error: &ParseError, code: &str, file_id: Option<&str>) -> String { 33 | let error_str = error.to_string(); 34 | 35 | if error_str == "" || error_str == "module_eval_failed" || error_str == "module_parse_failed" { 36 | return String::new(); 37 | } 38 | 39 | let better_error = BetterError::improve_parse_error(error, code).unwrap_or(BetterError { 40 | message: error_str, 41 | help: None, 42 | hint: None, 43 | note: None, 44 | span: Span::new(0, 0, 0, 0), 45 | }); 46 | format_codespan_error(better_error, code, file_id) 47 | } 48 | 49 | /// Return a formatted error as a String 50 | pub fn format_codespan_error(be: BetterError, code: &str, file_id: Option<&str>) -> String { 51 | let mut files = SimpleFiles::new(); 52 | let file_id = files.add(file_id.unwrap_or(""), code); 53 | 54 | // build the notes 55 | let mut notes = Vec::new(); 56 | if let Some(help) = &be.help { 57 | notes.push(format!("help: {}", help)); 58 | } 59 | if let Some(hint) = &be.hint { 60 | notes.push(format!("hint: {}", hint)); 61 | } 62 | if let Some(note) = &be.note { 63 | notes.push(format!("note: {}", note)); 64 | } 65 | 66 | // build the diagnostic error 67 | let mut labels = Vec::new(); 68 | if be.span.start() != be.span.end() { 69 | labels.push( 70 | Label::primary(file_id, be.span.start()..be.span.end()).with_message(&be.message), 71 | ); 72 | } 73 | 74 | let diagnostic = 75 | Diagnostic::error().with_message(&be.message).with_labels(labels).with_notes(notes); 76 | 77 | let mut buffer = Buffer::ansi(); 78 | let config = term::Config::default(); 79 | 80 | term::emit(&mut buffer, &config, &files, &diagnostic).unwrap(); 81 | 82 | // Convert buffer to string 83 | String::from_utf8(buffer.into_inner()).unwrap() 84 | } 85 | -------------------------------------------------------------------------------- /crates/ewwii/src/ipc_server.rs: -------------------------------------------------------------------------------- 1 | use crate::{app, opts}; 2 | use anyhow::{Context, Result}; 3 | // use std::path::PathBuf; 4 | use std::time::Duration; 5 | use tokio::{ 6 | io::{AsyncReadExt, AsyncWriteExt}, 7 | sync::mpsc::*, 8 | }; 9 | 10 | /// ewwii ipc 11 | 12 | pub async fn run_ewwii_server>( 13 | evt_send: UnboundedSender, 14 | socket_path: P, 15 | ) -> Result<()> { 16 | let socket_path = socket_path.as_ref(); 17 | let listener = { tokio::net::UnixListener::bind(socket_path)? }; 18 | log::info!("Ewwii IPC server initialized"); 19 | crate::loop_select_exiting! { 20 | connection = listener.accept() => match connection { 21 | Ok((stream, _addr)) => { 22 | let evt_send = evt_send.clone(); 23 | tokio::spawn(async move { 24 | let result = handle_connection(stream, evt_send.clone()).await; 25 | crate::print_result_err!("while handling IPC connection with client", result); 26 | }); 27 | }, 28 | Err(e) => eprintln!("Failed to connect to client: {:?}", e), 29 | } 30 | } 31 | Ok(()) 32 | } 33 | 34 | /// Handle a single IPC connection from start to end. 35 | async fn handle_connection( 36 | mut stream: tokio::net::UnixStream, 37 | evt_send: UnboundedSender, 38 | ) -> Result<()> { 39 | let (mut stream_read, mut stream_write) = stream.split(); 40 | 41 | let action: opts::ActionWithServer = read_ewwii_action_from_stream(&mut stream_read).await?; 42 | 43 | log::debug!("received command from IPC: {:?}", &action); 44 | 45 | let (command, maybe_response_recv) = action.into_daemon_command(); 46 | 47 | evt_send.send(command)?; 48 | 49 | if let Some(mut response_recv) = maybe_response_recv { 50 | log::debug!("Waiting for response for IPC client"); 51 | if let Ok(Some(response)) = 52 | tokio::time::timeout(Duration::from_millis(100), response_recv.recv()).await 53 | { 54 | let response = bincode::serialize(&response)?; 55 | let result = &stream_write.write_all(&response).await; 56 | crate::print_result_err!("sending text response to ipc client", &result); 57 | } 58 | } 59 | stream_write.shutdown().await?; 60 | Ok(()) 61 | } 62 | 63 | /// Read a single message from a unix stream, and parses it into a `ActionWithServer` 64 | /// The format here requires the first 4 bytes to be the size of the rest of the message (in big-endian), followed by the rest of the message. 65 | async fn read_ewwii_action_from_stream( 66 | stream_read: &'_ mut tokio::net::unix::ReadHalf<'_>, 67 | ) -> Result { 68 | let mut message_byte_length = [0u8; 4]; 69 | stream_read 70 | .read_exact(&mut message_byte_length) 71 | .await 72 | .context("Failed to read message size header in IPC message")?; 73 | let message_byte_length = u32::from_be_bytes(message_byte_length); 74 | let mut raw_message = Vec::::with_capacity(message_byte_length as usize); 75 | while raw_message.len() < message_byte_length as usize { 76 | stream_read 77 | .read_buf(&mut raw_message) 78 | .await 79 | .context("Failed to read actual IPC message")?; 80 | } 81 | 82 | bincode::deserialize(&raw_message).context("Failed to parse client message") 83 | } 84 | -------------------------------------------------------------------------------- /crates/ewwii/src/error_handling_ctx.rs: -------------------------------------------------------------------------------- 1 | //! Disgusting global state. 2 | //! I hate this, but [buffet](https://github.com/buffet) told me that this is what I should do for peak maintainability! 3 | 4 | use std::sync::{Arc, RwLock}; 5 | 6 | use crate::diag_error::DiagError; 7 | use crate::dynval::ConversionError; 8 | use crate::file_database::FileDatabase; 9 | use codespan_reporting::{ 10 | diagnostic::Diagnostic, 11 | term::{self, Chars}, 12 | }; 13 | use once_cell::sync::Lazy; 14 | use shared_utils::Span; 15 | 16 | pub static FILE_DATABASE: Lazy>> = 17 | Lazy::new(|| Arc::new(RwLock::new(FileDatabase::new()))); 18 | 19 | // pub fn clear_files() { 20 | // *FILE_DATABASE.write().unwrap() = FileDatabase::new(); 21 | // } 22 | 23 | pub fn print_error(err: anyhow::Error) { 24 | match anyhow_err_to_diagnostic(&err) { 25 | Some(diag) => match stringify_diagnostic(diag) { 26 | Ok(diag) => eprintln!("{}", diag), 27 | Err(_) => log::error!("{:?}", err), 28 | }, 29 | None => log::error!("{:?}", err), 30 | } 31 | } 32 | 33 | pub fn format_error(err: &anyhow::Error) -> String { 34 | anyhow_err_to_diagnostic(err) 35 | .and_then(|diag| stringify_diagnostic(diag).ok()) 36 | .unwrap_or_else(|| format!("{:?}", err)) 37 | } 38 | 39 | // * OLD 40 | // pub fn anyhow_err_to_diagnostic(err: &anyhow::Error) -> Option> { 41 | // #[allow(clippy::manual_map)] 42 | // if let Some(err) = err.downcast_ref::() { 43 | // Some(err.0.clone()) 44 | // } else if let Some(err) = err.downcast_ref::() { 45 | // Some(err.to_diagnostic()) 46 | // } else if let Some(err) = err.downcast_ref::() { 47 | // Some(err.to_diagnostic()) 48 | // } else if let Some(err) = err.downcast_ref::() { 49 | // Some(err.to_diagnostic()) 50 | // } else { 51 | // None 52 | // } 53 | // } 54 | 55 | pub fn anyhow_err_to_diagnostic(err: &anyhow::Error) -> Option> { 56 | if let Some(err) = err.downcast_ref::() { 57 | Some(err.0.clone()) 58 | } else if let Some(err) = err.downcast_ref::() { 59 | Some(Diagnostic::error().with_message(format!("conversion error: {}", err))) 60 | // } else if let Some(err) = err.downcast_ref::() { 61 | // Some(Diagnostic::error().with_message(format!("validation error: {}", err))) 62 | // } else if let Some(err) = err.downcast_ref::() { 63 | // Some(Diagnostic::error().with_message(format!("evaluation error: {}", err))) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | pub fn stringify_diagnostic( 70 | mut diagnostic: codespan_reporting::diagnostic::Diagnostic, 71 | ) -> anyhow::Result { 72 | diagnostic 73 | .labels 74 | .retain(|label| !Span(label.range.start, label.range.end, label.file_id).is_dummy()); 75 | 76 | let mut config = term::Config::default(); 77 | let mut chars = Chars::box_drawing(); 78 | chars.single_primary_caret = '─'; 79 | config.chars = chars; 80 | config.chars.note_bullet = '→'; 81 | let mut buf = Vec::new(); 82 | let mut writer = term::termcolor::Ansi::new(&mut buf); 83 | let files = FILE_DATABASE.read().unwrap(); 84 | term::emit(&mut writer, &config, &*files, &diagnostic)?; 85 | Ok(String::from_utf8(buf)?) 86 | } 87 | -------------------------------------------------------------------------------- /crates/ewwii/src/window_arguments.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | window::coords::Coords, window::monitor::MonitorIdentifier, 3 | window::window_geometry::AnchorPoint, 4 | }; 5 | 6 | /// This stores the arguments given in the command line to create a window 7 | /// While creating a window, we combine this with information from the 8 | /// [`WindowDefinition`] to create a [WindowInitiator](`crate::window_initiator::WindowInitiator`), which stores all the 9 | /// information required to start a window 10 | #[derive(Debug, Clone)] 11 | pub struct WindowArguments { 12 | /// Name of the window as defined in the eww config 13 | pub window_name: String, 14 | /// Instance ID of the window 15 | pub instance_id: String, 16 | pub anchor: Option, 17 | pub duration: Option, 18 | pub monitor: Option, 19 | pub pos: Option, 20 | pub size: Option, 21 | } 22 | 23 | impl WindowArguments { 24 | // pub fn new_from_args(id: String, config_name: String, mut args: HashMap) -> Result { 25 | // let initiator = WindowArguments { 26 | // window_name: config_name, 27 | // instance_id: id, 28 | // pos: parse_value_from_args::("pos", &mut args)?, 29 | // size: parse_value_from_args::("size", &mut args)?, 30 | // monitor: parse_value_from_args::("screen", &mut args)?, 31 | // anchor: parse_value_from_args::("anchor", &mut args)?, 32 | // duration: parse_value_from_args::("duration", &mut args)? 33 | // .map(|x| x.as_duration()) 34 | // .transpose() 35 | // .context("Not a valid duration")?, 36 | // }; 37 | 38 | // Ok(initiator) 39 | // } 40 | 41 | // /// Return a hashmap of all arguments the window was passed and expected, returning 42 | // /// an error in case required arguments are missing or unexpected arguments are passed. 43 | // pub fn get_local_window_variables(&self, window_def: &WindowDefinition) -> Result> { 44 | // let expected_args: HashSet<&String> = window_def.expected_args.iter().map(|x| &x.name.0).collect(); 45 | // let mut local_variables: HashMap = HashMap::new(); 46 | 47 | // // Ensure that the arguments passed to the window that are already interpreted by eww (id, screen) 48 | // // are set to the correct values 49 | // if expected_args.contains(&String::from("id")) { 50 | // local_variables.insert(VarName::from("id"), DynVal::from(self.instance_id.clone())); 51 | // } 52 | // if self.monitor.is_some() && expected_args.contains(&String::from("screen")) { 53 | // let mon_dyn = DynVal::from(&self.monitor.clone().unwrap()); 54 | // local_variables.insert(VarName::from("screen"), mon_dyn); 55 | // } 56 | 57 | // local_variables.extend(self.args.clone()); 58 | 59 | // for attr in &window_def.expected_args { 60 | // let name = VarName::from(attr.name.clone()); 61 | // if !local_variables.contains_key(&name) && !attr.optional { 62 | // bail!("Error, missing argument '{}' when creating window with id '{}'", attr.name, self.instance_id); 63 | // } 64 | // } 65 | 66 | // if local_variables.len() != window_def.expected_args.len() { 67 | // let unexpected_vars: Vec<_> = local_variables.keys().filter(|&n| !expected_args.contains(&n.0)).cloned().collect(); 68 | // bail!( 69 | // "variables {} unexpectedly defined when creating window with id '{}'", 70 | // unexpected_vars.join(", "), 71 | // self.instance_id, 72 | // ); 73 | // } 74 | 75 | // Ok(local_variables) 76 | // } 77 | } 78 | -------------------------------------------------------------------------------- /crates/ewwii/src/paths.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::hash_map::DefaultHasher, 3 | hash::{Hash, Hasher}, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use anyhow::{bail, Result}; 8 | 9 | /// Stores references to all the paths relevant to ewwii, and abstracts access to these files and directories 10 | #[derive(Debug, Clone)] 11 | pub struct EwwiiPaths { 12 | pub log_file: PathBuf, 13 | pub log_dir: PathBuf, 14 | pub ipc_socket_file: PathBuf, 15 | pub config_dir: PathBuf, 16 | } 17 | 18 | impl EwwiiPaths { 19 | pub fn from_config_dir>(config_dir: P) -> Result { 20 | let config_dir = config_dir.as_ref(); 21 | if config_dir.is_file() { 22 | bail!("Please provide the path to the config directory, not a file within it") 23 | } 24 | 25 | if !config_dir.exists() { 26 | bail!("Configuration directory {} does not exist", config_dir.display()); 27 | } 28 | 29 | let config_dir = config_dir.canonicalize()?; 30 | 31 | let mut hasher = DefaultHasher::new(); 32 | format!("{}", config_dir.display()).hash(&mut hasher); 33 | // daemon_id is a hash of the config dir path to ensure that, given a normal XDG_RUNTIME_DIR, 34 | // the absolute path to the socket stays under the 108 bytes limit. (see #387, man 7 unix) 35 | let daemon_id = format!("{:x}", hasher.finish()); 36 | 37 | let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR") 38 | .map(std::path::PathBuf::from) 39 | .unwrap_or_else(|_| std::path::PathBuf::from("/tmp")) 40 | .join(format!("ewwii-server_{}", daemon_id)); 41 | 42 | // 100 as the limit isn't quite 108 everywhere (i.e 104 on BSD or mac) 43 | if format!("{}", ipc_socket_file.display()).len() > 100 { 44 | log::warn!("The IPC socket file's absolute path exceeds 100 bytes, the socket may fail to create."); 45 | } 46 | 47 | let log_dir = std::env::var("XDG_CACHE_HOME") 48 | .map(PathBuf::from) 49 | .unwrap_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap()).join(".cache")) 50 | .join("ewwii"); 51 | 52 | if !log_dir.exists() { 53 | log::info!("Creating log dir"); 54 | std::fs::create_dir_all(&log_dir)?; 55 | } 56 | 57 | Ok(Self { 58 | config_dir, 59 | log_file: log_dir.join(format!("ewwii_{}.log", daemon_id)), 60 | log_dir, 61 | ipc_socket_file, 62 | }) 63 | } 64 | 65 | pub fn default() -> Result { 66 | let config_dir = std::env::var("XDG_CONFIG_HOME") 67 | .map(PathBuf::from) 68 | .unwrap_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap()).join(".config")) 69 | .join("ewwii"); 70 | 71 | Self::from_config_dir(config_dir) 72 | } 73 | 74 | pub fn get_log_file(&self) -> &Path { 75 | self.log_file.as_path() 76 | } 77 | 78 | pub fn get_log_dir(&self) -> &Path { 79 | self.log_dir.as_path() 80 | } 81 | 82 | pub fn get_ipc_socket_file(&self) -> &Path { 83 | self.ipc_socket_file.as_path() 84 | } 85 | 86 | pub fn get_config_dir(&self) -> &Path { 87 | self.config_dir.as_path() 88 | } 89 | 90 | // Modified this code with rhai (the new yuck replacer in ewwii) 91 | pub fn get_rhai_path(&self) -> PathBuf { 92 | self.config_dir.join("ewwii.rhai") 93 | } 94 | } 95 | 96 | impl std::fmt::Display for EwwiiPaths { 97 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 98 | write!( 99 | f, 100 | "config-dir: {}, ipc-socket: {}, log-file: {}", 101 | self.config_dir.display(), 102 | self.ipc_socket_file.display(), 103 | self.log_file.display() 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/providers/stdlib/env.rs: -------------------------------------------------------------------------------- 1 | use rhai::plugin::*; 2 | use rhai::EvalAltResult; 3 | 4 | #[export_module] 5 | pub mod env { 6 | /// Get the value of an environment variable. 7 | /// 8 | /// # Arguments 9 | /// 10 | /// * `var` - The name of the environment variable to retrieve. 11 | /// 12 | /// # Returns 13 | /// 14 | /// This function returns the value of the environment variable as a `String`. 15 | /// If the variable is not found or there is an error, it returns a `Result::Err` with the error message. 16 | /// 17 | /// # Example 18 | /// 19 | /// ```javascript 20 | /// import "std::env" as env; 21 | /// 22 | /// // Get the value of the "HOME" environment variable 23 | /// let home_dir = env::get_env("HOME"); 24 | /// print(home_dir); // output: /home/username 25 | /// ``` 26 | #[rhai_fn(return_raw)] 27 | pub fn get_env(var: &str) -> Result> { 28 | std::env::var(var).map_err(|e| format!("Failed to get env: {e}").into()) 29 | } 30 | 31 | /// Set the value of an environment variable. 32 | /// 33 | /// # Arguments 34 | /// 35 | /// * `var` - The name of the environment variable to set. 36 | /// * `value` - The value to assign to the environment variable. 37 | /// 38 | /// # Returns 39 | /// 40 | /// This function does not return a value. 41 | /// 42 | /// # Example 43 | /// 44 | /// ```javascript 45 | /// import "std::env" as env; 46 | /// 47 | /// // Set the value of the "MY_VAR" environment variable 48 | /// env::set_env("MY_VAR", "SomeValue"); 49 | /// ``` 50 | pub fn set_env(var: &str, value: &str) { 51 | std::env::set_var(var, value); 52 | } 53 | 54 | /// Get the path to the home directory. 55 | /// 56 | /// # Returns 57 | /// 58 | /// This function returns the value of the "HOME" environment variable as a `String`. 59 | /// If the variable is not found or there is an error, it returns a `Result::Err` with the error message. 60 | /// 61 | /// # Example 62 | /// 63 | /// ```javascript 64 | /// import "std::env" as env; 65 | /// 66 | /// // Get the home directory 67 | /// let home_dir = env::get_home_dir(); 68 | /// print(home_dir); // output: /home/username 69 | /// ``` 70 | #[rhai_fn(return_raw)] 71 | pub fn get_home_dir() -> Result> { 72 | std::env::var("HOME").map_err(|e| format!("Failed to get home directory: {e}").into()) 73 | } 74 | 75 | /// Get the current working directory. 76 | /// 77 | /// # Returns 78 | /// 79 | /// This function returns the current working directory as a `String`. If there is an error 80 | /// (e.g., if the path cannot be retrieved), it returns a `Result::Err` with the error message. 81 | /// 82 | /// # Example 83 | /// 84 | /// ```javascript 85 | /// import "std::env" as env; 86 | /// 87 | /// // Get the current working directory 88 | /// let current_dir = env::get_current_dir(); 89 | /// print(current_dir); // output: /home/username/project 90 | /// ``` 91 | #[rhai_fn(return_raw)] 92 | pub fn get_current_dir() -> Result> { 93 | std::env::current_dir() 94 | .map_err(|e| format!("Failed to get current directory: {e}").into()) 95 | .and_then(|p| { 96 | p.into_os_string().into_string().map_err(|_| "Invalid path encoding".into()) 97 | }) 98 | } 99 | 100 | /// Get the current username. 101 | /// 102 | /// # Returns 103 | /// 104 | /// This function returns the value of the "USER" environment variable as a `String`. 105 | /// If the variable is not found or there is an error, it returns a `Result::Err` with the error message. 106 | /// 107 | /// # Example 108 | /// 109 | /// ```javascript 110 | /// import "std::env" as env; 111 | /// 112 | /// // Get the username of the current user 113 | /// let username = env::get_username(); 114 | /// print(username); // output: username 115 | /// ``` 116 | #[rhai_fn(return_raw)] 117 | pub fn get_username() -> Result> { 118 | std::env::var("USER").map_err(|e| format!("Failed to get USER: {e}").into()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/providers/stdlib/regex.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use rhai::plugin::*; 3 | use rhai::{Array, Dynamic}; 4 | 5 | /// A module providing regular expression support. 6 | #[export_module] 7 | pub mod regex_lib { 8 | /// Checks if a regex pattern matches the given text. 9 | /// 10 | /// # Arguments 11 | /// 12 | /// * `text` - A string to be matched with the pattern. 13 | /// * `pattern` - The pattern to match on the string. 14 | /// 15 | /// # Returns 16 | /// 17 | /// Returns a boolean (true/false) based on if the pattern is matched on the text provided. 18 | /// 19 | /// # Example 20 | /// 21 | /// ```javascript 22 | /// import "std::regex" as regex; 23 | /// 24 | /// let result = regex::is_match("This is an example!", "example"); 25 | /// if result == true { 26 | /// print("The pattern is matched!"); 27 | /// } 28 | /// ``` 29 | #[rhai_fn(return_raw)] 30 | pub fn is_match(text: &str, pattern: &str) -> Result> { 31 | let re = Regex::new(pattern).map_err(|e| format!("Failed to read regex pattern: {}", e))?; 32 | Ok(re.is_match(text)) 33 | } 34 | 35 | /// Returns the first match of a regex pattern in the text. 36 | /// 37 | /// # Arguments 38 | /// 39 | /// * `text` - A string to be matched with the pattern. 40 | /// * `pattern` - The pattern to match on the string. 41 | /// 42 | /// # Returns 43 | /// 44 | /// Returns a string which is the first match of a regex pattern. 45 | /// 46 | /// # Example 47 | /// 48 | /// ```javascript 49 | /// import "std::regex" as regex; 50 | /// 51 | /// let result = regex::find("This is an example!", `\be\w*\b`); 52 | /// print(result); // output: "example" 53 | /// ``` 54 | #[rhai_fn(return_raw)] 55 | pub fn find(text: &str, pattern: &str) -> Result> { 56 | let re = Regex::new(pattern).map_err(|e| format!("Failed to read regex pattern: {}", e))?; 57 | match re.find(text).map(|m| m.as_str().to_string()) { 58 | Some(s) => Ok(s), 59 | None => Ok(String::new()), 60 | } 61 | } 62 | 63 | /// Returns all matches of a regex pattern in the text. 64 | /// 65 | /// # Arguments 66 | /// 67 | /// * `text` - A string to be matched with the pattern. 68 | /// * `pattern` - The pattern to match on the string. 69 | /// 70 | /// # Returns 71 | /// 72 | /// Returns aan array of strings containing the all the things that match the regex pattern in the provided text. 73 | /// 74 | /// # Example 75 | /// 76 | /// ```javascript 77 | /// import "std::regex" as regex; 78 | /// 79 | /// let result = regex::find("This is an example!", `\be\w*\b`); 80 | /// print(result); // output: ["example"] 81 | /// ``` 82 | #[rhai_fn(return_raw)] 83 | pub fn find_all(text: &str, pattern: &str) -> Result> { 84 | let re = Regex::new(pattern).map_err(|e| format!("Failed to read regex pattern: {}", e))?; 85 | let results: Array = 86 | re.find_iter(text).map(|m| Dynamic::from(m.as_str().to_string())).collect(); 87 | 88 | Ok(results) 89 | } 90 | 91 | /// Replaces all matches of a regex pattern with a replacement string. 92 | /// 93 | /// # Arguments 94 | /// 95 | /// * `text` - A string to be matched with the pattern. 96 | /// * `pattern` - The pattern to match on the string. 97 | /// * `replacement` - A string that the matched pattern will get replaced with. 98 | /// 99 | /// # Returns 100 | /// 101 | /// Returns the provided text with the matches of the regex pattern replaced with the provided replacement argument. 102 | /// 103 | /// # Example 104 | /// 105 | /// ```javascript 106 | /// import "std::regex" as regex; 107 | /// 108 | /// let result = regex::replace("This is an example!", "an example", "a test"); 109 | /// print(result); // output: "This is a test!" 110 | /// ``` 111 | #[rhai_fn(return_raw)] 112 | pub fn replace( 113 | text: &str, 114 | pattern: &str, 115 | replacement: &str, 116 | ) -> Result> { 117 | let re = Regex::new(pattern).map_err(|e| format!("Failed to read regex pattern: {}", e))?; 118 | Ok(re.replace_all(text, replacement).to_string()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /crates/ewwii/src/window/coords.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Debug, *}; 2 | use once_cell::sync::Lazy; 3 | use serde::{Deserialize, Serialize}; 4 | use smart_default::SmartDefault; 5 | use std::{fmt, str::FromStr}; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum Error { 9 | #[error("Failed to parse \"{0}\" as a length value")] 10 | NumParseFailed(String), 11 | #[error("Invalid unit \"{0}\", must be either % or px")] 12 | InvalidUnit(String), 13 | #[error("Invalid format. Coordinates must be formated like 200x100")] 14 | MalformedCoords, 15 | } 16 | 17 | #[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, Debug, SmartDefault)] 18 | pub enum NumWithUnit { 19 | #[display("{}%", _0)] 20 | #[debug("{}%", _0)] 21 | Percent(f32), 22 | #[display("{}px", _0)] 23 | #[debug("{}px", _0)] 24 | #[default] 25 | Pixels(i32), 26 | } 27 | 28 | impl NumWithUnit { 29 | pub fn pixels_relative_to(&self, max: i32) -> i32 { 30 | match *self { 31 | NumWithUnit::Percent(n) => ((max as f64 / 100.0) * n as f64) as i32, 32 | NumWithUnit::Pixels(n) => n, 33 | } 34 | } 35 | 36 | // add back when needed 37 | // pub fn perc_relative_to(&self, max: i32) -> f32 { 38 | // match *self { 39 | // NumWithUnit::Percent(n) => n, 40 | // NumWithUnit::Pixels(n) => ((n as f64 / max as f64) * 100.0) as f32, 41 | // } 42 | // } 43 | } 44 | 45 | impl FromStr for NumWithUnit { 46 | type Err = Error; 47 | 48 | fn from_str(s: &str) -> Result { 49 | static PATTERN: Lazy = 50 | Lazy::new(|| regex::Regex::new("^(-?\\d+(?:.\\d+)?)(.*)$").unwrap()); 51 | 52 | let captures = PATTERN.captures(s).ok_or_else(|| Error::NumParseFailed(s.to_string()))?; 53 | let value = captures 54 | .get(1) 55 | .unwrap() 56 | .as_str() 57 | .parse::() 58 | .map_err(|_| Error::NumParseFailed(s.to_string()))?; 59 | match captures.get(2).unwrap().as_str() { 60 | "px" | "" => Ok(NumWithUnit::Pixels(value.floor() as i32)), 61 | "%" => Ok(NumWithUnit::Percent(value)), 62 | unit => Err(Error::InvalidUnit(unit.to_string())), 63 | } 64 | } 65 | } 66 | 67 | #[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, Default)] 68 | #[display("{}*{}", x, y)] 69 | pub struct Coords { 70 | pub x: NumWithUnit, 71 | pub y: NumWithUnit, 72 | } 73 | 74 | impl FromStr for Coords { 75 | type Err = Error; 76 | 77 | fn from_str(s: &str) -> Result { 78 | let (x, y) = s 79 | .split_once(|x: char| x.to_ascii_lowercase() == 'x' || x.to_ascii_lowercase() == '*') 80 | .ok_or(Error::MalformedCoords)?; 81 | Coords::from_strs(x, y) 82 | } 83 | } 84 | 85 | impl fmt::Debug for Coords { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | write!(f, "CoordsWithUnits({}, {})", self.x, self.y) 88 | } 89 | } 90 | 91 | impl Coords { 92 | /// parse a string for x and a string for y into a [`Coords`] object. 93 | pub fn from_strs(x: &str, y: &str) -> Result { 94 | Ok(Coords { x: x.parse()?, y: y.parse()? }) 95 | } 96 | 97 | // /// resolve the possibly relative coordinates relative to a given containers size 98 | // pub fn relative_to(&self, width: i32, height: i32) -> (i32, i32) { 99 | // (self.x.pixels_relative_to(width), self.y.pixels_relative_to(height)) 100 | // } 101 | } 102 | 103 | #[cfg(test)] 104 | mod test { 105 | use super::*; 106 | use pretty_assertions::assert_eq; 107 | 108 | #[test] 109 | fn test_parse_num_with_unit() { 110 | assert_eq!(NumWithUnit::Pixels(55), NumWithUnit::from_str("55").unwrap()); 111 | assert_eq!(NumWithUnit::Pixels(55), NumWithUnit::from_str("55px").unwrap()); 112 | assert_eq!(NumWithUnit::Percent(55.0), NumWithUnit::from_str("55%").unwrap()); 113 | assert_eq!(NumWithUnit::Percent(55.5), NumWithUnit::from_str("55.5%").unwrap()); 114 | assert!(NumWithUnit::from_str("55pp").is_err()); 115 | } 116 | 117 | #[test] 118 | fn test_parse_coords() { 119 | assert_eq!( 120 | Coords { x: NumWithUnit::Pixels(50), y: NumWithUnit::Pixels(60) }, 121 | Coords::from_str("50x60").unwrap() 122 | ); 123 | assert!(Coords::from_str("5060").is_err()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/ewwii/src/file_database.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use codespan_reporting::files::Files; 4 | // use shared_utils::Span; 5 | use crate::diag_error::DiagError; 6 | 7 | #[derive(Debug, Clone, Default)] 8 | pub struct FileDatabase { 9 | files: HashMap, 10 | latest_id: usize, 11 | } 12 | 13 | // #[derive(thiserror::Error, Debug)] 14 | // pub enum FilesError { 15 | // #[error(transparent)] 16 | // IoError(#[from] std::io::Error), 17 | 18 | // #[error(transparent)] 19 | // DiagError(#[from] DiagError), 20 | 21 | // #[error(transparent)] 22 | // ParserError(#[from] anyhow::Error), 23 | // } 24 | 25 | impl FileDatabase { 26 | pub fn new() -> Self { 27 | Self::default() 28 | } 29 | 30 | fn get_file(&self, id: usize) -> Result<&CodeFile, codespan_reporting::files::Error> { 31 | self.files.get(&id).ok_or(codespan_reporting::files::Error::FileMissing) 32 | } 33 | 34 | fn insert_code_file(&mut self, file: CodeFile) -> usize { 35 | let file_id = self.latest_id; 36 | self.files.insert(file_id, file); 37 | self.latest_id += 1; 38 | file_id 39 | } 40 | 41 | pub fn insert_string(&mut self, name: String, content: String) -> Result { 42 | let line_starts = codespan_reporting::files::line_starts(&content).collect(); 43 | let code_file = CodeFile { 44 | name, 45 | line_starts, 46 | source_len_bytes: content.len(), 47 | source: CodeSource::Literal(content), 48 | }; 49 | let file_id = self.insert_code_file(code_file); 50 | Ok(file_id) 51 | } 52 | } 53 | 54 | impl<'a> Files<'a> for FileDatabase { 55 | type FileId = usize; 56 | type Name = &'a str; 57 | type Source = String; 58 | 59 | fn name(&'a self, id: Self::FileId) -> Result { 60 | Ok(&self.get_file(id)?.name) 61 | } 62 | 63 | fn source( 64 | &'a self, 65 | id: Self::FileId, 66 | ) -> Result { 67 | self.get_file(id)?.source.read_content().map_err(codespan_reporting::files::Error::Io) 68 | } 69 | 70 | fn line_index( 71 | &self, 72 | id: Self::FileId, 73 | byte_index: usize, 74 | ) -> Result { 75 | Ok(self 76 | .get_file(id)? 77 | .line_starts 78 | .binary_search(&byte_index) 79 | .unwrap_or_else(|next_line| next_line - 1)) 80 | } 81 | 82 | fn line_range( 83 | &self, 84 | id: Self::FileId, 85 | line_index: usize, 86 | ) -> Result, codespan_reporting::files::Error> { 87 | let file = self.get_file(id)?; 88 | let line_start = file.line_start(line_index)?; 89 | let next_line_start = file.line_start(line_index + 1)?; 90 | Ok(line_start..next_line_start) 91 | } 92 | } 93 | 94 | #[derive(Clone, Debug)] 95 | struct CodeFile { 96 | name: String, 97 | line_starts: Vec, 98 | source: CodeSource, 99 | source_len_bytes: usize, 100 | } 101 | 102 | impl CodeFile { 103 | /// Return the starting byte index of the line with the specified line index. 104 | /// Convenience method that already generates errors if necessary. 105 | fn line_start(&self, line_index: usize) -> Result { 106 | use std::cmp::Ordering; 107 | 108 | match line_index.cmp(&self.line_starts.len()) { 109 | Ordering::Less => Ok(self 110 | .line_starts 111 | .get(line_index) 112 | .cloned() 113 | .expect("failed despite previous check")), 114 | Ordering::Equal => Ok(self.source_len_bytes), 115 | Ordering::Greater => Err(codespan_reporting::files::Error::LineTooLarge { 116 | given: line_index, 117 | max: self.line_starts.len() - 1, 118 | }), 119 | } 120 | } 121 | } 122 | 123 | #[derive(Clone, Debug)] 124 | enum CodeSource { 125 | // File(std::path::PathBuf), 126 | Literal(String), 127 | } 128 | 129 | impl CodeSource { 130 | fn read_content(&self) -> std::io::Result { 131 | match self { 132 | // CodeSource::File(path) => Ok(std::fs::read_to_string(path)?), 133 | CodeSource::Literal(x) => Ok(x.to_string()), 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/providers/stdlib/text.rs: -------------------------------------------------------------------------------- 1 | use rhai::plugin::*; 2 | 3 | /// A module providing utility functions for string manipulation. 4 | #[export_module] 5 | pub mod text { 6 | 7 | /// Converts a string to a slug (lowercase words joined by hyphens). 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `text` - A string to be converted to a slug. 12 | /// 13 | /// # Returns 14 | /// 15 | /// Returns the `text` as a slug. 16 | /// 17 | /// # Example 18 | /// 19 | /// ```javascript 20 | /// import "std::text" as text; 21 | /// 22 | /// let result = text::to_slug("Hello World!"); 23 | /// print(result); // output: "hello-world" 24 | /// ``` 25 | pub fn to_slug(text: &str) -> String { 26 | let lower = text.to_lowercase(); 27 | 28 | let sanitized: String = lower 29 | .chars() 30 | .map(|c| if c.is_alphanumeric() || c.is_whitespace() { c } else { ' ' }) 31 | .collect(); 32 | 33 | let words = sanitized.split_whitespace(); 34 | let slug = words.collect::>().join("-"); 35 | 36 | slug 37 | } 38 | 39 | /// Converts a string to camel case. 40 | /// 41 | /// # Arguments 42 | /// 43 | /// * `text` - A string to be converted to camel case. 44 | /// 45 | /// # Returns 46 | /// 47 | /// Returns the `text` in camel case format. 48 | /// 49 | /// # Example 50 | /// 51 | /// ```javascript 52 | /// import "std::text" as text; 53 | /// 54 | /// let result = text::to_camel_case("hello world example"); 55 | /// print(result); // output: "helloWorldExample" 56 | /// ``` 57 | pub fn to_camel_case(text: &str) -> String { 58 | let cleaned: String = text 59 | .chars() 60 | .map(|c| if c.is_alphanumeric() || c.is_whitespace() { c } else { ' ' }) 61 | .collect(); 62 | 63 | let words = cleaned.split_whitespace(); 64 | 65 | let camel = words 66 | .enumerate() 67 | .map(|(i, word)| { 68 | if i == 0 { 69 | word.to_lowercase() 70 | } else { 71 | let mut chars = word.chars(); 72 | match chars.next() { 73 | Some(f) => f.to_uppercase().collect::() + chars.as_str(), 74 | None => String::new(), 75 | } 76 | } 77 | }) 78 | .collect::(); 79 | 80 | camel 81 | } 82 | 83 | /// Truncates a string to the specified number of characters. 84 | /// 85 | /// # Arguments 86 | /// 87 | /// * `text` - A string to be truncated. 88 | /// * `max_chars` - The maximum number of characters to keep in the string. 89 | /// 90 | /// # Returns 91 | /// 92 | /// Returns a truncated string. 93 | /// 94 | /// # Example 95 | /// 96 | /// ```javascript 97 | /// import "std::text" as text; 98 | /// 99 | /// let result = text::truncate_chars("Hello World!", 5); 100 | /// print(result); // output: "Hello" 101 | /// ``` 102 | pub fn truncate_chars(text: String, max_chars: i64) -> String { 103 | match text.char_indices().nth(max_chars.try_into().unwrap()) { 104 | None => text, 105 | Some((idx, _)) => text[..idx].to_string(), 106 | } 107 | } 108 | 109 | /// Converts a string to uppercase. 110 | /// 111 | /// # Arguments 112 | /// 113 | /// * `s` - A string to be converted to uppercase. 114 | /// 115 | /// # Returns 116 | /// 117 | /// Returns the string in uppercase. 118 | /// 119 | /// # Example 120 | /// 121 | /// ```javascript 122 | /// import "std::text" as text; 123 | /// 124 | /// let result = text::to_upper("hello"); 125 | /// print(result); // output: "HELLO" 126 | /// ``` 127 | pub fn to_upper(s: &str) -> String { 128 | s.to_uppercase() 129 | } 130 | 131 | /// Converts a string to lowercase. 132 | /// 133 | /// # Arguments 134 | /// 135 | /// * `s` - A string to be converted to lowercase. 136 | /// 137 | /// # Returns 138 | /// 139 | /// Returns the string in lowercase. 140 | /// 141 | /// # Example 142 | /// 143 | /// ```javascript 144 | /// import "std::text" as text; 145 | /// 146 | /// let result = text::to_lower("HELLO"); 147 | /// print(result); // output: "hello" 148 | /// ``` 149 | pub fn to_lower(s: &str) -> String { 150 | s.to_lowercase() 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/updates/poll.rs: -------------------------------------------------------------------------------- 1 | /* 2 | ┏━━━━━━━━━━━━━━━━━━━━━┓ 3 | ┃ Reference structure ┃ 4 | ┗━━━━━━━━━━━━━━━━━━━━━┛ 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] 7 | pub struct PollScriptVar { 8 | pub name: VarName, 9 | pub run_while_expr: SimplExpr, 10 | pub command: VarSource, 11 | pub initial_value: Option, 12 | pub interval: std::time::Duration, 13 | pub name_span: Span, 14 | } 15 | */ 16 | 17 | use super::{ReactiveVarStore, SHUTDOWN_REGISTRY}; 18 | use rhai::Map; 19 | use shared_utils::extract_props::*; 20 | use std::time::Duration; 21 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; 22 | use tokio::process::Command; 23 | use tokio::sync::watch; 24 | use tokio::time::sleep; 25 | 26 | pub fn handle_poll( 27 | var_name: String, 28 | props: &Map, 29 | shell: String, 30 | store: ReactiveVarStore, 31 | tx: tokio::sync::mpsc::UnboundedSender, 32 | ) { 33 | // Parse polling interval 34 | let interval = get_duration_prop(props, "interval", Some(Duration::from_secs(1))); 35 | let interval = interval.expect("Error parsing interval property of poll"); 36 | 37 | let cmd = match get_string_prop(props, "cmd", Some("")) { 38 | Ok(c) => c, 39 | Err(e) => { 40 | log::warn!("Poll {} missing cmd property: {}", var_name, e); 41 | return; 42 | } 43 | }; 44 | 45 | // No need to do this as we apply the initial value before parsing 46 | // Handle initial value 47 | // if let Ok(initial) = get_string_prop(&props, "initial", None) { 48 | // log::debug!("[{}] initial value: {}", var_name, initial); 49 | // store.write().unwrap().insert(var_name.clone(), initial.clone()); 50 | // let _ = tx.send(var_name.clone()); 51 | // } 52 | 53 | let store = store.clone(); 54 | let tx = tx.clone(); 55 | 56 | let (shutdown_tx, mut shutdown_rx) = watch::channel(false); 57 | SHUTDOWN_REGISTRY.lock().unwrap().push(shutdown_tx.clone()); 58 | 59 | tokio::spawn(async move { 60 | // Spawn a persistent shell 61 | let mut child = match Command::new(&shell) 62 | .stdin(std::process::Stdio::piped()) 63 | .stdout(std::process::Stdio::piped()) 64 | .spawn() 65 | { 66 | Ok(c) => c, 67 | Err(err) => { 68 | log::error!("[{}] failed to spawn shell: {}", var_name, err); 69 | return; 70 | } 71 | }; 72 | 73 | let mut stdin = child.stdin.take().expect("Failed to open stdin"); 74 | let stdout = child.stdout.take().expect("Failed to open stdout"); 75 | let mut reader = BufReader::new(stdout).lines(); 76 | 77 | let mut last_value: Option = None; 78 | 79 | loop { 80 | // Send command 81 | if let Err(err) = stdin.write_all(cmd.as_bytes()).await { 82 | log::error!("[{}] failed to write to shell stdin: {}", var_name, err); 83 | break; 84 | } 85 | if let Err(err) = stdin.write_all(b"\n").await { 86 | log::error!("[{}] failed to write newline to shell stdin: {}", var_name, err); 87 | break; 88 | } 89 | if let Err(err) = stdin.flush().await { 90 | log::error!("[{}] failed to flush shell stdin: {}", var_name, err); 91 | break; 92 | } 93 | 94 | // Read single line output 95 | let output_line = reader.next_line().await; 96 | if let Ok(Some(stdout_line)) = output_line { 97 | let stdout_trimmed = stdout_line.trim().to_string(); 98 | if Some(&stdout_trimmed) != last_value.as_ref() { 99 | last_value = Some(stdout_trimmed.clone()); 100 | log::debug!("[{}] polled value: {}", var_name, stdout_trimmed); 101 | store.write().unwrap().insert(var_name.clone(), stdout_trimmed); 102 | let _ = tx.send(var_name.clone()); 103 | } else { 104 | log::trace!("[{}] value unchanged, skipping tx", var_name); 105 | } 106 | } else { 107 | log::warn!("[{}] shell output ended or failed: {:?}", var_name, output_line); 108 | break; 109 | } 110 | 111 | tokio::select! { 112 | _ = sleep(interval) => {} 113 | _ = shutdown_rx.changed() => { 114 | if *shutdown_rx.borrow() { 115 | let _ = child.kill().await; 116 | break; 117 | } 118 | } 119 | } 120 | } 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /crates/ewwii/src/widgets/build_widget.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use gtk4::gdk::prelude::Cast; 3 | 4 | use crate::config::WindowDefinition; 5 | use crate::widgets::widget_definitions::*; 6 | 7 | use rhai_impl::ast::WidgetNode; 8 | 9 | /// Widget input allows us to pass either a widgetnode or a window_def 10 | /// this is important to make build_gtk_widget standalone without having to 11 | /// make build_gtk_widget_from_node public 12 | pub enum WidgetInput<'a> { 13 | Node(WidgetNode), 14 | BorrowedNode(&'a WidgetNode), 15 | Window(WindowDefinition), 16 | } 17 | 18 | pub fn build_gtk_widget<'a>( 19 | input: &'a WidgetInput<'a>, 20 | widget_reg: &mut WidgetRegistry, 21 | ) -> Result { 22 | let node: &'a WidgetNode = match input { 23 | WidgetInput::Node(n) => n, 24 | WidgetInput::BorrowedNode(n) => n, 25 | WidgetInput::Window(w) => w.root_widget.as_ref(), 26 | }; 27 | build_gtk_widget_from_node(node, widget_reg) 28 | } 29 | 30 | // TODO: implement the commented lines 31 | fn build_gtk_widget_from_node( 32 | root_node: &WidgetNode, 33 | widget_reg: &mut WidgetRegistry, 34 | ) -> Result { 35 | /* 36 | When a a new widget is added to the build process, 37 | make sure to update get_id_to_props_map() found in 38 | `iirhai/widgetnode.rs`. It is crutial to presrve 39 | dynamic update system in ewwii. 40 | */ 41 | 42 | let gtk_widget = match root_node { 43 | WidgetNode::Box { props, children } => build_gtk_box(props, children, widget_reg)?.upcast(), 44 | WidgetNode::FlowBox { props, children } => { 45 | build_gtk_flowbox(props, children, widget_reg)?.upcast() 46 | } 47 | WidgetNode::EventBox { props, children } => { 48 | build_event_box(props, children, widget_reg)?.upcast() 49 | } 50 | WidgetNode::ToolTip { props, children } => { 51 | build_tooltip(props, children, widget_reg)?.upcast() 52 | } 53 | WidgetNode::LocalBind { props, children } => { 54 | build_localbind_util(props, children, widget_reg)?.upcast() 55 | } 56 | WidgetNode::WidgetAction { props, children } => { 57 | build_widgetaction_util(props, children, widget_reg)?.upcast() 58 | } 59 | WidgetNode::CircularProgress { props } => { 60 | build_circular_progress_bar(props, widget_reg)?.upcast() 61 | } 62 | WidgetNode::GtkUI { props } => build_gtk_ui_file(props)?.upcast(), 63 | // WidgetNode::Graph { props } => build_graph(props, widget_reg)?.upcast(), 64 | // WidgetNode::Transform { props } => build_transform(props, widget_reg)?.upcast(), 65 | WidgetNode::Scale { props } => build_gtk_scale(props, widget_reg)?.upcast(), 66 | WidgetNode::Progress { props } => build_gtk_progress(props, widget_reg)?.upcast(), 67 | WidgetNode::Image { props } => build_image(props, widget_reg)?.upcast(), 68 | WidgetNode::Icon { props } => build_icon(props, widget_reg)?.upcast(), 69 | WidgetNode::Button { props } => build_gtk_button(props, widget_reg)?.upcast(), 70 | WidgetNode::Label { props } => build_gtk_label(props, widget_reg)?.upcast(), 71 | // WIDGET_NAME_LITERAL => build_gtk_literal(node)?.upcast(), 72 | WidgetNode::Input { props } => build_gtk_input(props, widget_reg)?.upcast(), 73 | WidgetNode::Calendar { props } => build_gtk_calendar(props, widget_reg)?.upcast(), 74 | WidgetNode::ColorButton { props } => build_gtk_color_button(props, widget_reg)?.upcast(), 75 | WidgetNode::Expander { props, children } => { 76 | build_gtk_expander(props, children, widget_reg)?.upcast() 77 | } 78 | WidgetNode::ColorChooser { props } => build_gtk_color_chooser(props, widget_reg)?.upcast(), 79 | WidgetNode::ComboBoxText { props } => build_gtk_combo_box_text(props, widget_reg)?.upcast(), 80 | WidgetNode::Checkbox { props } => build_gtk_checkbox(props, widget_reg)?.upcast(), 81 | WidgetNode::Revealer { props, children } => { 82 | build_gtk_revealer(props, children, widget_reg)?.upcast() 83 | } 84 | WidgetNode::Scroll { props, children } => { 85 | build_gtk_scrolledwindow(props, children, widget_reg)?.upcast() 86 | } 87 | WidgetNode::OverLay { props, children } => { 88 | build_gtk_overlay(props, children, widget_reg)?.upcast() 89 | } 90 | WidgetNode::Stack { props, children } => { 91 | build_gtk_stack(props, children, widget_reg)?.upcast() 92 | } 93 | // WIDGET_NAME_SYSTRAY => build_systray(node)?.upcast(), 94 | unknown => { 95 | return Err(anyhow::anyhow!("Cannot build GTK widget from node: {:?}", unknown)); 96 | } 97 | }; 98 | 99 | Ok(gtk_widget) 100 | } 101 | -------------------------------------------------------------------------------- /crates/ewwii/src/config/ewwii_config.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | // ipc_server, 3 | // error_handling_ctx, 4 | paths::EwwiiPaths, 5 | window::backend_window_options::BackendWindowOptionsDef, 6 | }; 7 | use anyhow::{bail, Context, Result}; 8 | use std::cell::RefCell; 9 | use std::collections::HashMap; 10 | use std::rc::Rc; 11 | 12 | use rhai::{Map, AST}; 13 | use rhai_impl::{ast::WidgetNode, parser::ParseConfig}; 14 | 15 | // use tokio::{net::UnixStream, runtime::Runtime, sync::mpsc}; 16 | 17 | /// Load an [`EwwiiConfig`] from the config dir of the given [`crate::EwwiiPaths`], 18 | /// resetting and applying the global YuckFiles object in [`crate::error_handling_ctx`]. 19 | pub fn read_from_ewwii_paths( 20 | eww_paths: &EwwiiPaths, 21 | parser: &mut ParseConfig, 22 | ) -> Result { 23 | EwwiiConfig::read_from_dir(eww_paths, parser) 24 | } 25 | 26 | /// Ewwii configuration structure. 27 | #[derive(Debug, Clone, Default)] 28 | pub struct EwwiiConfig { 29 | windows: HashMap, 30 | root_node: Option>, 31 | compiled_ast: Option>>, 32 | } 33 | 34 | #[derive(Debug, Clone)] 35 | pub struct WindowDefinition { 36 | pub name: String, 37 | pub props: Map, 38 | pub backend_options: BackendWindowOptionsDef, 39 | pub root_widget: Rc, 40 | } 41 | 42 | impl EwwiiConfig { 43 | /// Load an [`EwwiiConfig`] from the config dir of the given [`crate::EwwiiPaths`], reading the main config file. 44 | pub fn read_from_dir(eww_paths: &EwwiiPaths, config_parser: &mut ParseConfig) -> Result { 45 | let rhai_path = eww_paths.get_rhai_path(); 46 | if !rhai_path.exists() { 47 | bail!("The configuration file `{}` does not exist", rhai_path.display()); 48 | } 49 | 50 | // get code from file 51 | let rhai_code = config_parser.code_from_file(&rhai_path)?; 52 | 53 | // Get Option<&str> form of rhai_path 54 | let rhai_path_opt_str = rhai_path.to_str(); 55 | 56 | // get the iirhai widget tree 57 | let compiled_ast = 58 | config_parser.compile_code(&rhai_code, rhai_path_opt_str.unwrap_or(""))?; 59 | let poll_listen_scope = ParseConfig::initial_poll_listen_scope(&rhai_code)?; 60 | let config_tree = config_parser.eval_code_with( 61 | &rhai_code, 62 | Some(poll_listen_scope), 63 | Some(&compiled_ast), 64 | rhai_path_opt_str, 65 | )?; 66 | 67 | let mut window_definitions = HashMap::new(); 68 | let config_tree_clone = config_tree.clone(); 69 | 70 | if let WidgetNode::Enter(children) = config_tree_clone { 71 | for node in children { 72 | if let WidgetNode::DefWindow { name, props, node } = node { 73 | let backend_options = BackendWindowOptionsDef::from_map(&props)?; 74 | 75 | let win_def = WindowDefinition { 76 | name, 77 | props, 78 | backend_options, 79 | root_widget: Rc::new(*node), 80 | }; 81 | window_definitions.insert(win_def.name.clone(), win_def); 82 | } 83 | } 84 | } else { 85 | bail!("Expected root node to be `Enter`, but got something else."); 86 | } 87 | 88 | Ok(EwwiiConfig { 89 | windows: window_definitions, 90 | root_node: Some(Rc::new(config_tree)), 91 | compiled_ast: Some(Rc::new(RefCell::new(compiled_ast))), 92 | }) 93 | } 94 | 95 | pub fn get_windows(&self) -> &HashMap { 96 | &self.windows 97 | } 98 | 99 | pub fn get_window(&self, name: &str) -> Result<&WindowDefinition> { 100 | self.windows.get(name).with_context(|| { 101 | format!( 102 | "No window named '{}' exists in config.\nThis may also be caused by your config failing to load properly, \ 103 | please check for any other errors in that case.", 104 | name 105 | ) 106 | }) 107 | } 108 | 109 | pub fn get_root_node(&self) -> Result> { 110 | self.root_node.clone().ok_or_else(|| anyhow::anyhow!("root_node is missing")) 111 | } 112 | 113 | pub fn get_owned_compiled_ast(&self) -> Option>> { 114 | self.compiled_ast.clone() 115 | } 116 | 117 | pub fn replace_data(&mut self, new_dat: Self) { 118 | if let (Some(old_ast_rc), Some(new_ast_rc)) = 119 | (self.compiled_ast.as_ref(), new_dat.compiled_ast.as_ref()) 120 | { 121 | *old_ast_rc.borrow_mut() = new_ast_rc.borrow().clone(); 122 | } 123 | 124 | self.windows = new_dat.windows; 125 | self.root_node = new_dat.root_node; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /crates/ewwii/src/window/window_geometry.rs: -------------------------------------------------------------------------------- 1 | // use anyhow::anyhow; 2 | use derive_more::{Debug, Display}; 3 | // use once_cell::sync::Lazy; 4 | // use regex::Regex; 5 | use serde::{Deserialize, Serialize}; 6 | use smart_default::SmartDefault; 7 | use std::{fmt, str::FromStr}; 8 | 9 | use super::window_definition::EnumParseError; 10 | use crate::enum_parse; 11 | use crate::window::coords::{Error, NumWithUnit}; 12 | 13 | /// A pair of [NumWithUnit] values for x and y 14 | #[derive(Clone, Copy, PartialEq, Deserialize, Serialize, Display, Debug, Default)] 15 | #[display("{}x{}", x, y)] 16 | pub struct Coords { 17 | pub x: NumWithUnit, 18 | pub y: NumWithUnit, 19 | } 20 | 21 | impl FromStr for Coords { 22 | type Err = Error; 23 | 24 | fn from_str(s: &str) -> Result { 25 | let (sx, sy) = 26 | s.split_once(|c: char| c == 'x' || c == '*').ok_or(Error::MalformedCoords)?; 27 | Ok(Coords { x: sx.parse()?, y: sy.parse()? }) 28 | } 29 | } 30 | 31 | impl From for Coords { 32 | fn from(c: crate::window::coords::Coords) -> Self { 33 | Self { x: c.x, y: c.y } 34 | } 35 | } 36 | 37 | impl Coords { 38 | /// Create from absolute pixel values 39 | pub fn from_pixels((x, y): (i32, i32)) -> Self { 40 | Coords { x: NumWithUnit::Pixels(x), y: NumWithUnit::Pixels(y) } 41 | } 42 | /// Resolve relative or absolute coordinates against container size 43 | pub fn relative_to(&self, width: i32, height: i32) -> (i32, i32) { 44 | (self.x.to_pixels(width), self.y.to_pixels(height)) 45 | } 46 | } 47 | 48 | impl NumWithUnit { 49 | pub fn to_pixels(&self, max: i32) -> i32 { 50 | match *self { 51 | NumWithUnit::Percent(n) => ((max as f64 / 100.0) * n as f64).round() as i32, 52 | NumWithUnit::Pixels(n) => n, 53 | } 54 | } 55 | } 56 | 57 | /// Alignment options for anchoring 58 | #[derive(Debug, Clone, Copy, Eq, PartialEq, SmartDefault, Serialize, Deserialize, Display)] 59 | pub enum AnchorAlignment { 60 | #[display("start")] 61 | #[default] 62 | START, 63 | #[display("center")] 64 | CENTER, 65 | #[display("end")] 66 | END, 67 | } 68 | 69 | impl AnchorAlignment { 70 | pub fn from_x_alignment(s: &str) -> Result { 71 | enum_parse! { "x-alignment", s, 72 | "l" | "left" => AnchorAlignment::START, 73 | "c" | "center" => AnchorAlignment::CENTER, 74 | "r" | "right" => AnchorAlignment::END, 75 | } 76 | } 77 | pub fn from_y_alignment(s: &str) -> Result { 78 | enum_parse! { "y-alignment", s, 79 | "t" | "top" => AnchorAlignment::START, 80 | "c" | "center" => AnchorAlignment::CENTER, 81 | "b" | "bottom" => AnchorAlignment::END, 82 | } 83 | } 84 | pub fn alignment_to_coordinate(&self, inner: i32, outer: i32) -> i32 { 85 | match self { 86 | AnchorAlignment::START => 0, 87 | AnchorAlignment::CENTER => (outer - inner) / 2, 88 | AnchorAlignment::END => outer - inner, 89 | } 90 | } 91 | } 92 | 93 | /// A pair of horizontal and vertical anchor alignments 94 | #[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] 95 | pub struct AnchorPoint { 96 | pub x: AnchorAlignment, 97 | pub y: AnchorAlignment, 98 | } 99 | 100 | impl std::str::FromStr for AnchorPoint { 101 | type Err = EnumParseError; 102 | 103 | fn from_str(s: &str) -> Result { 104 | let (x_str, y_str) = s.split_once(' ').ok_or_else(|| EnumParseError { 105 | input: s.to_string(), 106 | expected: vec![" "], 107 | })?; 108 | 109 | let x = AnchorAlignment::from_x_alignment(x_str)?; 110 | let y = AnchorAlignment::from_y_alignment(y_str)?; 111 | 112 | Ok(AnchorPoint { x, y }) 113 | } 114 | } 115 | 116 | impl fmt::Display for AnchorPoint { 117 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 118 | match (self.x, self.y) { 119 | (AnchorAlignment::CENTER, AnchorAlignment::CENTER) => write!(f, "center"), 120 | (x, y) => write!(f, "{} {}", x, y), 121 | } 122 | } 123 | } 124 | 125 | /// Final window geometry with anchor, offset, and size 126 | #[derive(Clone, Copy, Debug, PartialEq, Default, Serialize)] 127 | pub struct WindowGeometry { 128 | pub anchor_point: AnchorPoint, 129 | pub offset: Coords, 130 | pub size: Coords, 131 | } 132 | 133 | impl WindowGeometry { 134 | pub fn override_with( 135 | &self, 136 | anchor_point: Option, 137 | offset: Option, 138 | size: Option, 139 | ) -> Self { 140 | WindowGeometry { 141 | anchor_point: anchor_point.unwrap_or(self.anchor_point), 142 | offset: offset.unwrap_or(self.offset), 143 | size: size.unwrap_or(self.size), 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /crates/ewwii/src/util.rs: -------------------------------------------------------------------------------- 1 | use extend::ext; 2 | use itertools::Itertools; 3 | use std::fmt::Write; 4 | // use anyhow::anyhow; 5 | 6 | #[macro_export] 7 | macro_rules! print_result_err { 8 | ($context:expr, $result:expr $(,)?) => {{ 9 | if let Err(err) = $result { 10 | log::error!("[{}:{}] Error {}: {:?}", ::std::file!(), ::std::line!(), $context, err); 11 | } 12 | }}; 13 | } 14 | 15 | #[macro_export] 16 | macro_rules! loop_select { 17 | ($($body:tt)*) => { 18 | loop { 19 | ::tokio::select! { 20 | $($body)* 21 | }; 22 | } 23 | } 24 | } 25 | 26 | #[macro_export] 27 | macro_rules! regex { 28 | ($re:literal $(,)?) => {{ 29 | static RE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); 30 | RE.get_or_init(|| regex::Regex::new($re).unwrap()) 31 | }}; 32 | } 33 | 34 | /// Parse a string with a concrete set of options into some data-structure, 35 | /// and return a nicely formatted error message on invalid values. I.e.: 36 | /// ```rs 37 | /// let input = "up"; 38 | /// enum_parse { "direction", input, 39 | /// "up" => Direction::Up, 40 | /// "down" => Direction::Down, 41 | /// } 42 | /// ``` 43 | #[macro_export] 44 | macro_rules! enum_parse { 45 | ($name:literal, $input:expr, $($($s:literal)|* => $val:expr),* $(,)?) => { 46 | let input = $input.to_lowercase(); 47 | match input.as_str() { 48 | $( $( $s )|* => Ok($val) ),*, 49 | _ => Err(EnumParseError { 50 | input, 51 | expected: vec![$($($s),*),*], 52 | }) 53 | } 54 | }; 55 | } 56 | 57 | /// Compute the difference of two lists, returning a tuple of 58 | /// ( 59 | /// elements that where in a but not in b, 60 | /// elements that where in b but not in a 61 | /// ). 62 | // pub fn list_difference<'a, 'b, T: PartialEq>(a: &'a [T], b: &'b [T]) -> (Vec<&'a T>, Vec<&'b T>) { 63 | // let mut missing = Vec::new(); 64 | // for elem in a { 65 | // if !b.contains(elem) { 66 | // missing.push(elem); 67 | // } 68 | // } 69 | 70 | // let mut new = Vec::new(); 71 | // for elem in b { 72 | // if !a.contains(elem) { 73 | // new.push(elem); 74 | // } 75 | // } 76 | // (missing, new) 77 | // } 78 | 79 | #[ext(pub, name = StringExt)] 80 | impl> T { 81 | /// check if the string is empty after removing all linebreaks and trimming 82 | /// whitespace 83 | fn is_blank(self) -> bool { 84 | self.as_ref().replace('\n', "").trim().is_empty() 85 | } 86 | 87 | /// trim all lines in a string 88 | fn trim_lines(self) -> String { 89 | self.as_ref().lines().map(|line| line.trim()).join("\n") 90 | } 91 | } 92 | 93 | // pub trait IterAverage { 94 | // fn avg(self) -> f32; 95 | // } 96 | 97 | // impl> IterAverage for I { 98 | // fn avg(self) -> f32 { 99 | // let mut total = 0f32; 100 | // let mut cnt = 0f32; 101 | // for value in self { 102 | // total += value; 103 | // cnt += 1f32; 104 | // } 105 | // total / cnt 106 | // } 107 | // } 108 | 109 | /// Replace all env-var references of the format `"something ${foo}"` in a string 110 | /// by the actual env-variables. If the env-var isn't found, will replace the 111 | /// reference with an empty string. 112 | pub fn replace_env_var_references(input: String) -> String { 113 | regex!(r"\$\{([^\s]*)\}") 114 | .replace_all(&input, |var_name: ®ex::Captures| { 115 | std::env::var(var_name.get(1).unwrap().as_str()).unwrap_or_default() 116 | }) 117 | .into_owned() 118 | } 119 | 120 | pub fn unindent(text: &str) -> String { 121 | // take all the lines of our text and skip over the first empty ones 122 | let lines = text.lines().skip_while(|x| x.is_empty()); 123 | // find the smallest indentation 124 | let min = lines 125 | .clone() 126 | .fold(None, |min, line| { 127 | let min = min.unwrap_or(usize::MAX); 128 | Some(min.min(line.chars().take(min).take_while(|&c| c == ' ').count())) 129 | }) 130 | .unwrap_or(0); 131 | 132 | let mut result = String::new(); 133 | for i in lines { 134 | writeln!(result, "{}", &i[min..]).expect("Something went wrong unindenting the string"); 135 | } 136 | result.pop(); 137 | result 138 | } 139 | 140 | #[cfg(test)] 141 | mod test { 142 | use super::{replace_env_var_references, unindent}; 143 | 144 | #[test] 145 | fn test_replace_env_var_references() { 146 | let scss = "$test: ${USER};"; 147 | 148 | assert_eq!( 149 | replace_env_var_references(String::from(scss)), 150 | format!("$test: {};", std::env::var("USER").unwrap_or_default()) 151 | ) 152 | } 153 | 154 | #[test] 155 | fn test_unindent() { 156 | let indented = " 157 | line one 158 | line two"; 159 | assert_eq!("line one\nline two", unindent(indented)); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /crates/ewwii/src/window_initiator.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | // use shared_utils::{AttrName, VarName}; 3 | // use std::collections::HashMap; 4 | // use rhai::Map; 5 | 6 | use crate::{ 7 | config::WindowDefinition, 8 | window::{ 9 | backend_window_options::BackendWindowOptions, 10 | // coords::Coords, 11 | coords::NumWithUnit, 12 | monitor::MonitorIdentifier, 13 | window_definition::WindowStacking, 14 | window_geometry::{AnchorAlignment, AnchorPoint}, 15 | // monitor, 16 | window_geometry::{Coords, WindowGeometry}, 17 | }, 18 | window_arguments::WindowArguments, 19 | }; 20 | 21 | use rhai::Dynamic; 22 | use std::str::FromStr; 23 | 24 | /// This stores all the information required to create a window and is created 25 | /// via combining information from the [`WindowDefinition`] and the [`WindowInitiator`] 26 | #[derive(Debug, Clone)] 27 | pub struct WindowInitiator { 28 | pub backend_options: BackendWindowOptions, 29 | pub geometry: Option, 30 | pub monitor: Option, 31 | pub name: String, 32 | pub resizable: bool, 33 | pub stacking: WindowStacking, 34 | } 35 | 36 | impl WindowInitiator { 37 | pub fn new(window_def: &WindowDefinition, args: &WindowArguments) -> Result { 38 | let properties = &window_def.props; 39 | let geometry = match properties.get("geometry") { 40 | Some(val) => Some(parse_geometry(val, args, true)?), 41 | // Some(geo) => Some(geo.eval(&vars)?.override_if_given(args.anchor, args.pos, args.size)), 42 | None => None, 43 | }; 44 | let monitor = args.monitor.clone().or_else(|| { 45 | properties 46 | .get("monitor")? 47 | .clone() 48 | .try_cast::() 49 | .map(|n| MonitorIdentifier::Numeric(n as i32)) 50 | }); 51 | Ok(WindowInitiator { 52 | backend_options: window_def.backend_options.eval(properties.clone())?, 53 | geometry, 54 | monitor, 55 | name: window_def.name.clone(), 56 | resizable: properties.get("resizable").map(|d| d.clone_cast::()).unwrap_or(true), 57 | stacking: match properties.get("stacking") { 58 | Some(d) => WindowStacking::from_str(&d.clone_cast::())?, 59 | None => WindowStacking::Foreground, // or error 60 | }, 61 | }) 62 | } 63 | 64 | // pub fn get_scoped_vars(&self) -> HashMap { 65 | // self.local_variables.iter().map(|(k, v)| (AttrName::from(k.clone()), v.clone())).collect() 66 | // } 67 | } 68 | 69 | fn parse_geometry( 70 | val: &Dynamic, 71 | args: &WindowArguments, 72 | override_geom: bool, 73 | ) -> Result { 74 | let map = val.clone().cast::(); 75 | 76 | let anchor = map 77 | .get("anchor") 78 | .map(|dyn_value| anchor_point_from_str(&dyn_value.to_string())) 79 | .transpose()?; 80 | 81 | let mut geom = WindowGeometry { 82 | offset: get_coords_from_map(&map, "x", "y")?, 83 | size: get_coords_from_map(&map, "width", "height")?, 84 | anchor_point: anchor 85 | .unwrap_or(AnchorPoint { x: AnchorAlignment::CENTER, y: AnchorAlignment::START }), 86 | }; 87 | 88 | if override_geom { 89 | geom = geom.override_with( 90 | args.anchor, 91 | // both are converted into window_geometry::Coords from coords::Coords 92 | args.pos.map(Into::into), 93 | args.size.map(Into::into), 94 | ); 95 | } 96 | 97 | Ok(geom) 98 | } 99 | 100 | fn get_coords_from_map(map: &rhai::Map, x_key: &str, y_key: &str) -> Result { 101 | let key1 = map 102 | .get(x_key) 103 | .and_then(|v| v.clone().into_string().ok()) 104 | .map(|s| NumWithUnit::from_str(&s)) 105 | .transpose()? 106 | .unwrap_or_else(NumWithUnit::default); 107 | 108 | let key2 = map 109 | .get(y_key) 110 | .and_then(|v| v.clone().into_string().ok()) 111 | .map(|s| NumWithUnit::from_str(&s)) 112 | .transpose()? 113 | .unwrap_or_else(NumWithUnit::default); 114 | 115 | Ok(Coords { x: key1, y: key2 }) 116 | } 117 | 118 | fn anchor_point_from_str(s: &str) -> Result { 119 | let binding = s.trim().to_lowercase(); 120 | let parts: Vec<_> = binding.split_whitespace().collect(); 121 | 122 | match parts.as_slice() { 123 | [single] => { 124 | // Apply to both x and y 125 | let alignment = AnchorAlignment::from_x_alignment(single) 126 | .or_else(|_| AnchorAlignment::from_y_alignment(single))?; 127 | Ok(AnchorPoint { x: alignment, y: alignment }) 128 | } 129 | [y_part, x_part] => { 130 | let y = AnchorAlignment::from_y_alignment(y_part)?; 131 | let x = AnchorAlignment::from_x_alignment(x_part)?; 132 | Ok(AnchorPoint { x, y }) 133 | } 134 | _ => Err(anyhow!("Expected 1 or 2 words like 'center' or 'top left'")), 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/helper.rs: -------------------------------------------------------------------------------- 1 | use crate::error::format_eval_error; 2 | use anyhow::Result; 3 | use rhai::Engine; 4 | use std::{ 5 | collections::HashSet, 6 | fs, 7 | path::{Path, PathBuf}, 8 | }; 9 | 10 | pub fn extract_poll_and_listen_vars(code: &str) -> Result)>> { 11 | extract_poll_and_listen_vars_inner(code, &mut HashSet::new()) 12 | } 13 | 14 | fn extract_poll_and_listen_vars_inner( 15 | code: &str, 16 | visited: &mut HashSet, 17 | ) -> Result)>> { 18 | let mut results = Vec::new(); 19 | let mut engine = Engine::new(); 20 | register_temp_poll_listen(&mut engine); 21 | 22 | // Handle imports manually 23 | for import_path in extract_import_paths(code)? { 24 | let resolved = resolve_import_path(&import_path)?; 25 | 26 | // Prevent infinite recursion 27 | let canonical = fs::canonicalize(&resolved).unwrap_or(resolved.clone()); 28 | if visited.contains(&canonical) { 29 | continue; 30 | } 31 | 32 | visited.insert(canonical.clone()); 33 | 34 | if resolved.exists() { 35 | let imported_code = fs::read_to_string(&resolved)?; 36 | let inner = extract_poll_and_listen_vars_inner(&imported_code, visited)?; 37 | results.extend(inner); 38 | } 39 | } 40 | 41 | // Process this file’s own poll/listen calls 42 | for expr in extract_poll_listen_exprs(code) { 43 | match engine.eval_expression::(&expr) { 44 | Ok(sig) => { 45 | let initial = sig.props.get("initial").and_then(|v| v.clone().try_cast::()); 46 | results.push((sig.var, initial)); 47 | } 48 | Err(e) => { 49 | return Err(anyhow::anyhow!(format_eval_error(&e, code, &engine, None))); 50 | } 51 | } 52 | } 53 | 54 | Ok(results) 55 | } 56 | 57 | /// Extract import paths from the Rhai source code 58 | fn extract_import_paths(code: &str) -> Result> { 59 | let mut imports = Vec::new(); 60 | 61 | for line in code.lines() { 62 | let trimmed = line.trim_start(); 63 | 64 | if trimmed.starts_with("import ") { 65 | if let Some(start) = trimmed.find('"') { 66 | if let Some(end_rel) = trimmed[start + 1..].find('"') { 67 | let end = start + 1 + end_rel; 68 | let path = &trimmed[start + 1..end]; 69 | imports.push(path.to_string()); 70 | } 71 | } 72 | } 73 | } 74 | 75 | Ok(imports) 76 | } 77 | 78 | /// Resolve relative and absolute import paths. 79 | fn resolve_import_path(import_path: &str) -> Result { 80 | let path = Path::new(import_path); 81 | let abs = 82 | if path.is_absolute() { path.to_path_buf() } else { std::env::current_dir()?.join(path) }; 83 | 84 | let abs = if abs.extension().is_none() { abs.with_extension("rhai") } else { abs }; 85 | 86 | Ok(abs) 87 | } 88 | 89 | pub fn extract_poll_listen_exprs(code: &str) -> Vec { 90 | let mut exprs = Vec::new(); 91 | let mut i = 0; 92 | let code_bytes = code.as_bytes(); 93 | let len = code.len(); 94 | 95 | while i < len { 96 | // skipping comments 97 | if code[i..].starts_with("//") { 98 | while i < len && code_bytes[i] as char != '\n' { 99 | i += 1; 100 | } 101 | i += 1; // skip a full line 102 | continue; 103 | } 104 | 105 | if code[i..].starts_with("/*") { 106 | i += 2; 107 | while i + 1 < len && &code[i..i + 2] != "*/" { 108 | i += 1; 109 | } 110 | i += 2; // skip till `*/` closing 111 | continue; 112 | } 113 | 114 | if code[i..].starts_with("poll(") || code[i..].starts_with("listen(") { 115 | let start = i; 116 | let mut depth = 0; 117 | let mut j = i; 118 | 119 | while j < len { 120 | match code.as_bytes()[j] as char { 121 | '(' => depth += 1, 122 | ')' => { 123 | depth -= 1; 124 | if depth == 0 { 125 | j += 1; 126 | break; 127 | } 128 | } 129 | _ => {} 130 | } 131 | j += 1; 132 | } 133 | 134 | let end = j; 135 | if let Some(expr) = code.get(start..end) { 136 | exprs.push(expr.to_string()); 137 | } 138 | i = j; 139 | } else { 140 | i += code[i..].chars().next().unwrap().len_utf8(); 141 | } 142 | } 143 | 144 | exprs 145 | } 146 | 147 | #[derive(Debug, Clone)] 148 | struct TempSignal { 149 | pub var: String, 150 | pub props: rhai::Map, 151 | } 152 | 153 | fn register_temp_poll_listen(engine: &mut rhai::Engine) { 154 | engine.register_type::(); 155 | 156 | engine.register_fn("poll", |var: &str, props: rhai::Map| TempSignal { 157 | var: var.to_string(), 158 | props, 159 | }); 160 | 161 | engine.register_fn("listen", |var: &str, props: rhai::Map| TempSignal { 162 | var: var.to_string(), 163 | props, 164 | }); 165 | } 166 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/module_resolver.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{format_eval_error, format_parse_error}; 2 | use crate::parser::ParseConfig; 3 | use crate::updates::ReactiveVarStore; 4 | use rhai::Scope; 5 | use rhai::{Dynamic, Engine, EvalAltResult, Module, ModuleResolver, Position, AST}; 6 | use std::collections::HashMap; 7 | use std::fs; 8 | use std::path::PathBuf; 9 | use std::rc::Rc; 10 | 11 | pub struct SimpleFileResolver { 12 | pub pl_handler_store: Option, 13 | } 14 | 15 | impl ModuleResolver for SimpleFileResolver { 16 | fn resolve( 17 | &self, 18 | engine: &Engine, 19 | source_path: Option<&str>, 20 | path: &str, 21 | _pos: Position, 22 | ) -> Result, Box> { 23 | let mut file_path = PathBuf::from(path); 24 | 25 | if file_path.extension().is_none() { 26 | file_path.set_extension("rhai"); 27 | } 28 | 29 | let base_dir = if let Some(src) = source_path { 30 | PathBuf::from(src).parent().map(|p| p.to_path_buf()).unwrap_or( 31 | std::env::current_dir().map_err(|e| { 32 | EvalAltResult::ErrorSystem("getting current_dir".into(), e.into()) 33 | })?, 34 | ) 35 | } else { 36 | std::env::current_dir() 37 | .map_err(|e| EvalAltResult::ErrorSystem("getting current_dir".into(), e.into()))? 38 | }; 39 | 40 | if !file_path.is_absolute() { 41 | file_path = base_dir.join(file_path); 42 | } 43 | 44 | let full_path = file_path 45 | .canonicalize() 46 | .map_err(|e| EvalAltResult::ErrorSystem(format!("resolving path: {path}"), e.into()))?; 47 | 48 | let script = fs::read_to_string(&full_path).map_err(|e| { 49 | EvalAltResult::ErrorSystem(format!("reading file: {full_path:?}"), e.into()) 50 | })?; 51 | 52 | let ast: AST = engine.compile(&script).map_err(|e| { 53 | Box::new(EvalAltResult::ErrorSystem( 54 | "module_parse_failed".into(), 55 | format_parse_error(&e, &script, full_path.to_str()).into(), 56 | )) 57 | })?; 58 | 59 | let parent_script: Option = if let Some(parent_path) = source_path { 60 | match fs::read_to_string(parent_path) { 61 | Ok(s) => Some(s), 62 | Err(err) => { 63 | log::error!("Could not read parent script {parent_path:?}: {err}"); 64 | None 65 | } 66 | } 67 | } else { 68 | None 69 | }; 70 | 71 | let mut scope = if let Some(ref script) = parent_script { 72 | ParseConfig::initial_poll_listen_scope(script).map_err(|e| { 73 | EvalAltResult::ErrorSystem( 74 | format!("error setting up default variables from {source_path:?}"), 75 | e.into(), 76 | ) 77 | })? 78 | } else { 79 | Scope::new() 80 | }; 81 | 82 | match &self.pl_handler_store { 83 | Some(val) => { 84 | let name_to_val: &HashMap = &*val.read().unwrap(); 85 | 86 | for (name, val) in name_to_val { 87 | scope.set_value(name.clone(), Dynamic::from(val.clone())); 88 | } 89 | } 90 | None => {} 91 | } 92 | 93 | let mut module = Module::eval_ast_as_new(scope, &ast, engine).map_err(|e| { 94 | Box::new(EvalAltResult::ErrorSystem( 95 | "module_eval_failed".into(), 96 | format_eval_error(&e, &script, engine, full_path.to_str()).into(), 97 | )) 98 | })?; 99 | 100 | module.build_index(); 101 | Ok(Rc::new(module)) 102 | } 103 | } 104 | 105 | pub struct ChainedResolver { 106 | pub first: Res1, 107 | pub second: Res2, 108 | } 109 | 110 | impl ModuleResolver for ChainedResolver { 111 | fn resolve( 112 | &self, 113 | engine: &Engine, 114 | source_path: Option<&str>, 115 | path: &str, 116 | pos: Position, 117 | ) -> Result, Box> { 118 | match self.first.resolve(engine, source_path, path, pos) { 119 | Ok(m) => Ok(m), 120 | Err(e1) => { 121 | if let EvalAltResult::ErrorSystem(msg, _) = e1.as_ref() { 122 | if msg == "module_eval_failed" || msg == "module_parse_failed" { 123 | return Err(e1); 124 | } 125 | } 126 | 127 | log::trace!( 128 | "Error executing resolver 1, falling back to resolver 2. Error details: {}", 129 | e1 130 | ); 131 | match self.second.resolve(engine, source_path, path, pos) { 132 | Ok(m) => Ok(m), 133 | Err(e2) => Err(Box::new(EvalAltResult::ErrorSystem( 134 | format!( 135 | "Both resolvers failed; first: {}, second (possibly unrelated): {}", 136 | e1, e2 137 | ), 138 | Box::new(e2), 139 | ))), 140 | } 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tools/generate-rhai-docs/src/main.rs: -------------------------------------------------------------------------------- 1 | use rhai::{Engine, module_resolvers::StaticModuleResolver}; 2 | use rhai_autodocs::{export::options, generate::docusaurus}; 3 | use rhai_impl::providers; 4 | use std::{env, fs, path::Path}; 5 | 6 | fn generate_docs( 7 | engine: &Engine, 8 | path: &str, 9 | filename: &str, 10 | include_std: bool, 11 | pre_description: &str, 12 | ) { 13 | let docs = options() 14 | .include_standard_packages(include_std) 15 | .export(engine) 16 | .expect("failed to generate documentation"); 17 | 18 | let docs_content = docusaurus().generate(&docs).unwrap(); 19 | 20 | if docs_content.is_empty() { 21 | eprintln!("No documentation generated for {}.", filename); 22 | return; 23 | } 24 | 25 | // Combine all module docs into one 26 | let full_docs = 27 | docs_content.into_iter().map(|(_, doc)| doc).collect::>().join("\n"); 28 | 29 | let mut lines = full_docs.lines(); 30 | 31 | let mut filtered_lines = Vec::new(); 32 | let mut in_frontmatter = false; 33 | let mut frontmatter_title: Option = None; 34 | 35 | for line in &mut lines { 36 | let trimmed = line.trim(); 37 | 38 | if trimmed == "---" { 39 | in_frontmatter = !in_frontmatter; 40 | 41 | if !in_frontmatter { 42 | if let Some(title) = &frontmatter_title { 43 | // The following will be generated: 44 | // 45 | // :::note Module 46 | // # title 47 | // ::: 48 | filtered_lines.push(format!(":::note Module\n# {}\n:::", title)); 49 | } 50 | frontmatter_title = None; 51 | } 52 | 53 | continue; 54 | } 55 | 56 | if in_frontmatter { 57 | if let Some(title) = trimmed.strip_prefix("title: ") { 58 | frontmatter_title = Some(title.to_string()); 59 | } 60 | continue; 61 | } 62 | 63 | if trimmed == "import Tabs from '@theme/Tabs';" 64 | || trimmed == "import TabItem from '@theme/TabItem';" 65 | { 66 | continue; 67 | } 68 | 69 | filtered_lines.push(line.to_string()); 70 | } 71 | 72 | // combine 73 | let all_imports = "import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';"; 74 | let final_docs = 75 | format!("{}\n\n{}\n\n{}", all_imports, pre_description, filtered_lines.join("\n")); 76 | 77 | // Write to file 78 | let file_path = Path::new(path).join(format!("{}.md", filename)); 79 | fs::write(&file_path, final_docs).expect("failed to write documentation"); 80 | println!("Documentation generated at: {}", file_path.display()); 81 | } 82 | 83 | fn main() { 84 | let args: Vec = env::args().collect(); 85 | let path = if args.len() > 1 { &args[1] } else { "./tools/generate-rhai-docs/auto_gen" }; 86 | 87 | // engine/resolver 88 | let engine = Engine::new(); 89 | let mut resolver = StaticModuleResolver::new(); 90 | 91 | // Generate global.md (full docs) 92 | generate_docs( 93 | &engine, 94 | path, 95 | "global", 96 | true, 97 | r#" 98 | # Global Builtin Rhai Functions 99 | 100 | These functions are built-in and available globally, meaning they can be used directly without any import. 101 | 102 | For example, to get the value of PI, you can simply write: 103 | 104 | ```javascript 105 | let x = PI(); 106 | ``` 107 | 108 | This section covers all the core functions provided by Rhai that are ready to use out of the box. 109 | "#, 110 | ); 111 | 112 | // Generate stdlib.md docs (custom stdlib) 113 | resolver.clear(); 114 | let mut engine = Engine::new(); // recreate engine to reset state 115 | providers::register_stdlib(&mut resolver); 116 | for (module_name, module) in resolver.iter() { 117 | engine.register_static_module(module_name, module.clone()); 118 | } 119 | generate_docs( 120 | &engine, 121 | path, 122 | "stdlib", 123 | false, 124 | r#" 125 | # Std Library Module 126 | 127 | These are all the standard modules in ewwii. 128 | 129 | Each library in this module is under `std::`, where `` is the name of the specific module. 130 | These modules provide essential functionalities that are will be useful for making widgets. 131 | They cover tasks like string manipulation, environmental variable manipuation, running shell commands, and more. 132 | "#, 133 | ); 134 | 135 | // Generate apilib.md docs 136 | resolver.clear(); 137 | let mut engine = Engine::new(); // recreate engine to reset state 138 | providers::register_apilib(&mut resolver); 139 | for (module_name, module) in resolver.iter() { 140 | engine.register_static_module(module_name, module.clone()); 141 | } 142 | generate_docs( 143 | &engine, 144 | path, 145 | "apilib", 146 | false, 147 | r#" 148 | # API Library Module 149 | 150 | These are all the API modules available in ewwii. 151 | 152 | Each library in this module is under `api::`, where `` is the name of the specific module. 153 | 154 | The API library provides system-level functionality, allowing you to interact with external resources and perform advanced operations. Examples include interacting with Wi-Fi, networking, and more. 155 | "#, 156 | ); 157 | 158 | println!("Docs generation completed."); 159 | } 160 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/builtins.rs: -------------------------------------------------------------------------------- 1 | use crate::ast::{hash_props, WidgetNode}; 2 | use crate::updates::{register_signal, LocalDataBinder, LocalSignal}; 3 | use rhai::{Array, Engine, EvalAltResult, Map, NativeCallContext}; 4 | use std::cell::RefCell; 5 | use std::rc::Rc; 6 | use std::sync::Arc; 7 | 8 | /// Converts a Dynamic array into a Vec, returning proper errors with position. 9 | fn children_to_vec( 10 | children: Array, 11 | ctx: &NativeCallContext, 12 | ) -> Result, Box> { 13 | children 14 | .into_iter() 15 | .map(|v| { 16 | let type_name = v.type_name(); 17 | v.try_cast::().ok_or_else(|| { 18 | Box::new(EvalAltResult::ErrorRuntime( 19 | format!("Expected WidgetNode in children array, found {}", type_name).into(), 20 | ctx.call_position(), 21 | )) 22 | }) 23 | }) 24 | .collect() 25 | } 26 | 27 | pub fn register_all_widgets( 28 | engine: &mut Engine, 29 | all_nodes: &Rc>>, 30 | keep_signal: &Rc>>, 31 | ) { 32 | engine.register_type::(); 33 | engine.register_type::(); 34 | 35 | // == Primitive widgets == 36 | macro_rules! register_primitive { 37 | ($name:expr, $variant:ident) => { 38 | engine.register_fn($name, |props: Map| -> Result> { 39 | Ok(WidgetNode::$variant { props }) 40 | }); 41 | }; 42 | } 43 | 44 | register_primitive!("label", Label); 45 | register_primitive!("button", Button); 46 | register_primitive!("image", Image); 47 | register_primitive!("icon", Icon); 48 | register_primitive!("input", Input); 49 | register_primitive!("progress", Progress); 50 | register_primitive!("combo_box_text", ComboBoxText); 51 | register_primitive!("scale", Scale); 52 | register_primitive!("checkbox", Checkbox); 53 | register_primitive!("calendar", Calendar); 54 | register_primitive!("graph", Graph); 55 | register_primitive!("transform", Transform); 56 | register_primitive!("circular_progress", CircularProgress); 57 | register_primitive!("color_button", ColorButton); 58 | register_primitive!("color_chooser", ColorChooser); 59 | 60 | // == Widgets with children == 61 | macro_rules! register_with_children { 62 | ($name:expr, $variant:ident) => { 63 | engine.register_fn( 64 | $name, 65 | |ctx: NativeCallContext, 66 | props: Map, 67 | children: Array| 68 | -> Result> { 69 | let children_vec = children_to_vec(children, &ctx)?; 70 | Ok(WidgetNode::$variant { props, children: children_vec }) 71 | }, 72 | ); 73 | }; 74 | } 75 | 76 | register_with_children!("box", Box); 77 | register_with_children!("flowbox", FlowBox); 78 | register_with_children!("expander", Expander); 79 | register_with_children!("revealer", Revealer); 80 | register_with_children!("scroll", Scroll); 81 | register_with_children!("overlay", OverLay); 82 | register_with_children!("stack", Stack); 83 | register_with_children!("eventbox", EventBox); 84 | register_with_children!("tooltip", ToolTip); 85 | register_with_children!("localbind", LocalBind); 86 | register_with_children!("widget_action", WidgetAction); 87 | 88 | // == Special widget 89 | engine.register_fn( 90 | "gtk_ui", 91 | |path: &str, load: &str| -> Result> { 92 | let mut props = Map::new(); 93 | props.insert("file".into(), path.into()); 94 | props.insert("id".into(), load.into()); 95 | Ok(WidgetNode::GtkUI { props }) 96 | }, 97 | ); 98 | 99 | // == Special signal 100 | let keep_signal_clone = keep_signal.clone(); 101 | engine.register_fn( 102 | "localsignal", 103 | move |props: Map| -> Result> { 104 | let id = hash_props(&props); 105 | let signal = Rc::new(LocalSignal { id, props, data: Arc::new(LocalDataBinder::new()) }); 106 | 107 | let signal_rc = register_signal(id, signal); 108 | 109 | keep_signal_clone.borrow_mut().push(id); 110 | 111 | Ok((*signal_rc).clone()) 112 | }, 113 | ); 114 | 115 | // == Top-level macros == 116 | engine.register_fn( 117 | "defwindow", 118 | |name: &str, props: Map, node: WidgetNode| -> Result> { 119 | Ok(WidgetNode::DefWindow { name: name.to_string(), props, node: Box::new(node) }) 120 | }, 121 | ); 122 | 123 | engine.register_fn("poll", |var: &str, props: Map| -> Result> { 124 | Ok(WidgetNode::Poll { var: var.to_string(), props }) 125 | }); 126 | 127 | engine.register_fn( 128 | "listen", 129 | |var: &str, props: Map| -> Result> { 130 | Ok(WidgetNode::Listen { var: var.to_string(), props }) 131 | }, 132 | ); 133 | 134 | let all_nodes_clone = all_nodes.clone(); 135 | engine.register_fn( 136 | "enter", 137 | move |ctx: NativeCallContext, children: Array| -> Result<(), Box> { 138 | let children_vec = children_to_vec(children, &ctx)?; 139 | let node = WidgetNode::Enter(children_vec); 140 | 141 | all_nodes_clone.borrow_mut().push(node); 142 | 143 | Ok(()) 144 | }, 145 | ); 146 | } 147 | -------------------------------------------------------------------------------- /crates/ewwii_plugin_api/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # ewwii_plugin_api - A plugin interface for ewwii 2 | //! 3 | //! `ewwii_plguin_api` is a shared list of traits 4 | //! that both ewwii and its plugins can use. 5 | //! This crate simplifies and provides a safe way for building 6 | //! plugins for ewwii. 7 | //! 8 | //! ## Example 9 | //! 10 | //! The following example shows how this crate shall be used to build ewwii plugins: 11 | //! 12 | //! ```rust 13 | //! use ewwii_plugin_api::{EwwiiAPI, Plugin, export_plugin}; 14 | //! 15 | //! pub struct DummyStructure; 16 | //! 17 | //! impl Plugin for DummyStructure { 18 | //! // critical for ewwii to launch the plugin 19 | //! fn init(&self, host: &dyn EwwiiAPI) { 20 | //! // will be printed by the host 21 | //! host.log("Plugin says Hello!"); 22 | //! } 23 | //! } 24 | //! 25 | //! // Critical for ewwii to load the plugin 26 | //! export_plugin!(DummyStructure); 27 | //! ``` 28 | 29 | mod export_macros; 30 | 31 | pub mod example; 32 | pub mod rhai_backend; 33 | pub mod widget_backend; 34 | 35 | #[cfg(feature = "include-rhai")] 36 | pub use rhai; 37 | 38 | #[cfg(feature = "include-gtk4")] 39 | pub use gtk4; 40 | 41 | /// The shared trait defining the Ewwii plugin API 42 | pub trait EwwiiAPI: Send + Sync { 43 | // == General Stuff == // 44 | /// Print a message from the host 45 | fn print(&self, msg: &str); 46 | /// Log a message from the host 47 | fn log(&self, msg: &str); 48 | /// Log a warning from the host 49 | fn warn(&self, msg: &str); 50 | /// Log an error from the host 51 | fn error(&self, msg: &str); 52 | 53 | // == Rhai Manipulation Stuff == // 54 | /// _(include-rhai)_ Perform actions on the latest rhai engine. 55 | /// 56 | /// # Example 57 | /// 58 | /// ```rust 59 | /// use ewwii_plugin_api::{EwwiiAPI, Plugin}; 60 | /// 61 | /// pub struct DummyStructure; 62 | /// 63 | /// impl Plugin for DummyStructure { 64 | /// fn init(&self, host: &dyn EwwiiAPI) { 65 | /// host.rhai_engine_action(Box::new(|eng| { 66 | /// // eng = rhai::Engine 67 | /// eng.set_max_expr_depths(128, 128); 68 | /// })); 69 | /// } 70 | /// } 71 | /// ``` 72 | #[cfg(feature = "include-rhai")] 73 | fn rhai_engine_action( 74 | &self, 75 | f: Box, 76 | ) -> Result<(), String>; 77 | 78 | /// _(include-rhai)_ Expose a function that rhai configuration can call. 79 | /// 80 | /// **NOTE:*** 81 | /// 82 | /// Due to TypeID mismatches, methods like `register_type`, `register_fn`, 83 | /// etc. won't work on the engine and may cause a crash. It is recommended 84 | /// to use the `register_function` API to register a funtion which `api::slib` 85 | /// can call to in rhai. 86 | /// 87 | /// # Example 88 | /// 89 | /// ```rust 90 | /// use ewwii_plugin_api::{EwwiiAPI, Plugin, rhai_backend::RhaiFnNamespace}; 91 | /// use rhai::Dynamic; 92 | /// 93 | /// pub struct DummyStructure; 94 | /// 95 | /// impl Plugin for DummyStructure { 96 | /// fn init(&self, host: &dyn EwwiiAPI) { 97 | /// host.register_function( 98 | /// "my_func".to_string(), 99 | /// RhaiFnNamespace::Global, 100 | /// Box::new(|args| { 101 | /// // Do stuff 102 | /// // - Perform things on the args (if needed) 103 | /// // - And return a value 104 | /// 105 | /// Ok(Dynamic::default()) // return empty 106 | /// })); 107 | /// } 108 | /// } 109 | /// ``` 110 | /// 111 | /// This example will register a function with signature "my_func(Array)" in rhai. 112 | /// 113 | /// ## Example use in rhai 114 | /// 115 | /// ```js 116 | /// print(my_func(["param1", "param2"])); 117 | /// ``` 118 | #[cfg(feature = "include-rhai")] 119 | fn register_function( 120 | &self, 121 | name: String, 122 | namespace: rhai_backend::RhaiFnNamespace, 123 | f: Box< 124 | dyn Fn(rhai::Array) -> Result> + Send + Sync, 125 | >, 126 | ) -> Result<(), String>; 127 | 128 | // == Widget Rendering & Logic == // 129 | /// Get the list of all widget id's 130 | fn list_widget_ids(&self) -> Result, String>; 131 | 132 | /// _(include-gtk4)_ Perform actions on the latest widget registry. 133 | /// 134 | /// # Example 135 | /// 136 | /// ```rust 137 | /// use ewwii_plugin_api::{EwwiiAPI, Plugin}; 138 | /// 139 | /// pub struct DummyStructure; 140 | /// 141 | /// impl Plugin for DummyStructure { 142 | /// fn init(&self, host: &dyn EwwiiAPI) { 143 | /// host.widget_reg_action(Box::new(|wrg| { 144 | /// // wrg = widget_backend::WidgetRegistryRepr 145 | /// // The gtk4::Widget can be modified here. 146 | /// })); 147 | /// } 148 | /// } 149 | /// ``` 150 | #[cfg(feature = "include-gtk4")] 151 | fn widget_reg_action( 152 | &self, 153 | f: Box, 154 | ) -> Result<(), String>; 155 | } 156 | 157 | /// The API format that the plugin should follow. 158 | /// This trait should be implemented for a structure and 159 | /// that structure should be exported via FFI. 160 | /// 161 | /// ## Example 162 | /// 163 | /// ```rust 164 | /// use ewwii_plugin_api::{Plugin, EwwiiAPI, export_plugin}; 165 | /// 166 | /// struct MyStruct; 167 | /// 168 | /// impl Plugin for MyStruct { 169 | /// fn init(&self, host: &dyn EwwiiAPI) { 170 | /// /* Implementation Skipped */ 171 | /// } 172 | /// } 173 | /// 174 | /// // Automatically does all the FFI related exports 175 | /// export_plugin!(MyStruct); 176 | /// ``` 177 | pub trait Plugin: Send + Sync { 178 | /// Function ran by host to startup plugin (and its a must-have for plugin loading) 179 | fn init(&self, host: &dyn EwwiiAPI); 180 | } 181 | -------------------------------------------------------------------------------- /crates/shared_utils/src/extract_props.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use rhai::{Dynamic, Map}; 3 | use std::time::Duration; 4 | 5 | /// General purpose helpers 6 | pub fn get_string_prop(props: &Map, key: &str, default: Option<&str>) -> Result { 7 | if let Some(value) = props.get(key) { 8 | value 9 | .clone() 10 | .try_cast::() 11 | .ok_or_else(|| anyhow!("Expected property `{}` to be a string", key)) 12 | } else { 13 | default 14 | .map(|s| s.to_string()) 15 | .ok_or_else(|| anyhow!("Missing required string property `{}`", key)) 16 | } 17 | } 18 | 19 | pub fn get_bool_prop(props: &Map, key: &str, default: Option) -> Result { 20 | if let Some(value) = props.get(key) { 21 | value 22 | .clone() 23 | .try_cast::() 24 | .ok_or_else(|| anyhow!("Expected property `{}` to be a bool", key)) 25 | } else { 26 | default.map(|s| s).ok_or_else(|| anyhow!("Missing required bool property `{}`", key)) 27 | } 28 | } 29 | pub fn get_i64_prop(props: &Map, key: &str, default: Option) -> Result { 30 | if let Some(value) = props.get(key) { 31 | if let Some(v) = value.clone().try_cast::() { 32 | Ok(v) 33 | } else if let Some(s) = value.clone().try_cast::() { 34 | s.parse::() 35 | .map_err(|_| anyhow!("Expected property `{}` to be an i64 or numeric string", key)) 36 | } else { 37 | Err(anyhow!("Expected property `{}` to be an i64 or numeric string", key)) 38 | } 39 | } else { 40 | default.ok_or_else(|| anyhow!("Missing required i64 property `{}`", key)) 41 | } 42 | } 43 | 44 | pub fn get_f64_prop(props: &Map, key: &str, default: Option) -> Result { 45 | if let Some(value) = props.get(key) { 46 | if let Some(v) = value.clone().try_cast::() { 47 | Ok(v) 48 | } else if let Some(v) = value.clone().try_cast::() { 49 | Ok(v as f64) 50 | } else if let Some(s) = value.clone().try_cast::() { 51 | s.parse::().map_err(|_| { 52 | anyhow!("Expected property `{}` to be an f64, i64, or numeric string", key) 53 | }) 54 | } else { 55 | Err(anyhow!("Expected property `{}` to be an f64, i64, or numeric string", key)) 56 | } 57 | } else { 58 | default.ok_or_else(|| anyhow!("Missing required f64 property `{}`", key)) 59 | } 60 | } 61 | 62 | pub fn get_i32_prop(props: &Map, key: &str, default: Option) -> Result { 63 | if let Some(value) = props.get(key) { 64 | if let Some(v) = value.clone().try_cast::() { 65 | Ok(v) 66 | } else if let Some(v) = value.clone().try_cast::() { 67 | if v >= i32::MIN as i64 && v <= i32::MAX as i64 { 68 | Ok(v as i32) 69 | } else { 70 | Err(anyhow!("Value for `{}` is out of range for i32", key)) 71 | } 72 | } else if let Some(s) = value.clone().try_cast::() { 73 | s.parse::() 74 | .map_err(|_| anyhow!("Expected property `{}` to be an i32 or numeric string", key)) 75 | } else { 76 | Err(anyhow!("Expected property `{}` to be an i32 or numeric string", key)) 77 | } 78 | } else { 79 | default.ok_or_else(|| anyhow!("Missing required i32 property `{}`", key)) 80 | } 81 | } 82 | 83 | pub fn get_vec_string_prop( 84 | props: &Map, 85 | key: &str, 86 | default: Option>, 87 | ) -> Result> { 88 | if let Some(value) = props.get(key) { 89 | let array = value 90 | .clone() 91 | .try_cast::>() 92 | .ok_or_else(|| anyhow!("Expected property `{}` to be a vec", key))?; 93 | 94 | array 95 | .into_iter() 96 | .map(|d| { 97 | d.try_cast::() 98 | .ok_or_else(|| anyhow!("Expected all elements of `{}` to be strings", key)) 99 | }) 100 | .collect() 101 | } else { 102 | default.ok_or_else(|| anyhow!("Missing required vec property `{}`", key)) 103 | } 104 | } 105 | 106 | pub fn get_duration_prop(props: &Map, key: &str, default: Option) -> Result { 107 | if let Ok(raw) = get_string_prop(props, key, None) { 108 | let key_str = raw.trim().to_ascii_lowercase(); 109 | if key_str.ends_with("ms") { 110 | let num = &key_str[..key_str.len() - 2]; 111 | let ms = num.parse::().map_err(|_| anyhow!("Invalid ms value: '{}'", key_str))?; 112 | Ok(Duration::from_millis(ms)) 113 | } else if key_str.ends_with("s") { 114 | let num = &key_str[..key_str.len() - 1]; 115 | let s = num.parse::().map_err(|_| anyhow!("Invalid s value: '{}'", key_str))?; 116 | Ok(Duration::from_secs(s)) 117 | } else if key_str.ends_with("min") { 118 | let num = &key_str[..key_str.len() - 3]; 119 | let mins = 120 | num.parse::().map_err(|_| anyhow!("Invalid min value: '{}'", key_str))?; 121 | Ok(Duration::from_secs(mins * 60)) 122 | } else if key_str.ends_with("m") { 123 | let num = &key_str[..key_str.len() - 1]; 124 | let mins = num.parse::().map_err(|_| anyhow!("Invalid m value: '{}'", key_str))?; 125 | Ok(Duration::from_secs(mins * 60)) 126 | } else if key_str.ends_with("h") { 127 | let num = &key_str[..key_str.len() - 1]; 128 | let hrs = num.parse::().map_err(|_| anyhow!("Invalid h value: '{}'", key_str))?; 129 | Ok(Duration::from_secs(hrs * 3600)) 130 | } else { 131 | default.ok_or_else(|| { 132 | anyhow!("Unsupported duration format: '{}', and no default provided", key_str) 133 | }) 134 | } 135 | } else { 136 | default.ok_or_else(|| anyhow!("No value for duration and no default provided")) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/updates/localsignal.rs: -------------------------------------------------------------------------------- 1 | use super::{get_prefered_shell, handle_listen, handle_poll}; 2 | use gtk4::glib; 3 | use gtk4::prelude::*; 4 | use gtk4::subclass::prelude::*; 5 | use once_cell::sync::Lazy; 6 | use rhai::Map; 7 | use std::cell::RefCell; 8 | use std::collections::HashMap; 9 | use std::rc::Rc; 10 | use std::sync::{Arc, RwLock}; 11 | 12 | mod imp { 13 | use super::*; 14 | 15 | #[derive(Default)] 16 | pub struct LocalDataBinder { 17 | pub value: RefCell, 18 | } 19 | 20 | #[glib::object_subclass] 21 | impl ObjectSubclass for LocalDataBinder { 22 | const NAME: &'static str = "LocalDataBinder"; 23 | type Type = super::LocalDataBinder; 24 | type ParentType = glib::Object; 25 | } 26 | 27 | impl ObjectImpl for LocalDataBinder { 28 | fn properties() -> &'static [glib::ParamSpec] { 29 | static PROPERTIES: once_cell::sync::Lazy> = 30 | once_cell::sync::Lazy::new(|| { 31 | vec![glib::ParamSpecString::builder("value") 32 | .nick("Value") 33 | .blurb("The bound value") 34 | .default_value(None) 35 | .build()] 36 | }); 37 | PROPERTIES.as_ref() 38 | } 39 | 40 | fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { 41 | match pspec.name() { 42 | "value" => self.value.borrow().to_value(), 43 | _ => unimplemented!(), 44 | } 45 | } 46 | 47 | fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 48 | match pspec.name() { 49 | "value" => { 50 | let val: Option = value.get().unwrap(); 51 | self.set_value(&self.obj(), val.unwrap_or_default()); 52 | } 53 | _ => unimplemented!(), 54 | } 55 | } 56 | } 57 | 58 | impl LocalDataBinder { 59 | pub fn set_value(&self, obj: &super::LocalDataBinder, val: String) { 60 | *self.value.borrow_mut() = val; 61 | obj.notify("value"); 62 | } 63 | } 64 | } 65 | 66 | glib::wrapper! { 67 | pub struct LocalDataBinder(ObjectSubclass); 68 | } 69 | 70 | impl LocalDataBinder { 71 | pub fn new() -> Self { 72 | glib::Object::new::() 73 | } 74 | 75 | pub fn value(&self) -> String { 76 | self.imp().value.borrow().clone() 77 | } 78 | 79 | pub fn set_value(&self, val: &str) { 80 | self.set_property("value", val); 81 | } 82 | } 83 | 84 | #[derive(Debug, Clone)] 85 | pub struct LocalSignal { 86 | pub id: u64, 87 | pub props: Map, 88 | pub data: Arc, 89 | } 90 | 91 | thread_local! { 92 | pub static LOCAL_SIGNALS: Lazy>>> = 93 | Lazy::new(|| RefCell::new(HashMap::new())); 94 | } 95 | 96 | pub fn register_signal(id: u64, signal: Rc) -> Rc { 97 | LOCAL_SIGNALS.with(|registry| { 98 | let mut map = registry.borrow_mut(); 99 | map.entry(id).or_insert_with(|| signal.clone()).clone() 100 | }) 101 | } 102 | 103 | pub fn retain_signals(ids: &Vec) { 104 | LOCAL_SIGNALS.with(|registry| { 105 | let mut map = registry.borrow_mut(); 106 | map.retain(|id, _| ids.contains(id)); 107 | }); 108 | } 109 | 110 | pub fn notify_all_localsignals() { 111 | LOCAL_SIGNALS.with(|registry| { 112 | let registry_ref = registry.borrow(); 113 | 114 | for (_, signal) in registry_ref.iter() { 115 | signal.data.notify("value"); 116 | } 117 | }); 118 | } 119 | 120 | pub fn handle_localsignal_changes() { 121 | let shell = get_prefered_shell(); 122 | let get_string_fn = shared_utils::extract_props::get_string_prop; 123 | let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); 124 | let store = Arc::new(RwLock::new(HashMap::new())); 125 | 126 | LOCAL_SIGNALS.with(|registry| { 127 | let registry_ref = registry.borrow(); 128 | 129 | for (id, signal) in registry_ref.iter() { 130 | let props = &signal.props; 131 | 132 | if let Ok(initial_str) = get_string_fn(&props, "initial", None) { 133 | signal.data.set_value(&initial_str); 134 | } 135 | 136 | match get_string_fn(&props, "type", None) { 137 | Ok(signal_type) => match signal_type.to_ascii_lowercase().as_str() { 138 | "poll" => handle_poll( 139 | id.to_string(), 140 | &props, 141 | shell.clone(), 142 | store.clone(), 143 | tx.clone(), 144 | ), 145 | "listen" => handle_listen( 146 | id.to_string(), 147 | &props, 148 | shell.clone(), 149 | store.clone(), 150 | tx.clone(), 151 | ), 152 | o => log::error!("Invalid type: '{}'", o), 153 | }, 154 | Err(_) => { 155 | log::error!( 156 | "Unable to handle localsignal {}: 'type' property missing or invalid.", 157 | id 158 | ); 159 | } 160 | } 161 | } 162 | }); 163 | 164 | glib::MainContext::default().spawn_local(async move { 165 | while let Some(id_str) = rx.recv().await { 166 | let value_opt = { 167 | let guard = store.read().unwrap(); 168 | guard.get(&id_str).cloned() 169 | }; 170 | 171 | if let Some(value) = value_opt { 172 | if let Ok(id) = id_str.parse::() { 173 | LOCAL_SIGNALS.with(|registry| { 174 | let mut registry_ref = registry.borrow_mut(); 175 | 176 | if let Some(signal) = registry_ref.get_mut(&id) { 177 | signal.data.set_value(&value); 178 | } else { 179 | log::warn!("No LocalSignal found for id {}", id); 180 | } 181 | }); 182 | } else { 183 | log::error!("Invalid id_str '{}': cannot parse to u64", id_str); 184 | } 185 | } else { 186 | log::warn!("No value found in store for id '{}'", id_str); 187 | } 188 | } 189 | }); 190 | } 191 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | ast::WidgetNode, 3 | builtins::register_all_widgets, 4 | error::{format_eval_error, format_parse_error}, 5 | helper::extract_poll_and_listen_vars, 6 | module_resolver::SimpleFileResolver, 7 | providers::register_all_providers, 8 | updates::ReactiveVarStore, 9 | }; 10 | use anyhow::{anyhow, Result}; 11 | use rhai::{Dynamic, Engine, ImmutableString, OptimizationLevel, Scope, AST}; 12 | use std::cell::RefCell; 13 | use std::fs; 14 | use std::path::Path; 15 | use std::rc::Rc; 16 | 17 | pub struct ParseConfig { 18 | pub engine: Engine, 19 | all_nodes: Rc>>, 20 | keep_signal: Rc>>, 21 | } 22 | 23 | impl ParseConfig { 24 | pub fn new(pl_handler_store: Option) -> Self { 25 | let mut engine = Engine::new(); 26 | let all_nodes = Rc::new(RefCell::new(Vec::new())); 27 | let keep_signal = Rc::new(RefCell::new(Vec::new())); 28 | 29 | engine.set_max_expr_depths(128, 128); 30 | engine 31 | .set_module_resolver(SimpleFileResolver { pl_handler_store: pl_handler_store.clone() }); 32 | 33 | register_all_widgets(&mut engine, &all_nodes, &keep_signal); 34 | register_all_providers(&mut engine, pl_handler_store); 35 | 36 | Self { engine, all_nodes, keep_signal } 37 | } 38 | 39 | pub fn compile_code(&mut self, code: &str, file_path: &str) -> Result { 40 | let mut ast = self 41 | .engine 42 | .compile(code) 43 | .map_err(|e| anyhow!(format_parse_error(&e, code, Some(file_path))))?; 44 | 45 | ast.set_source(ImmutableString::from(file_path)); 46 | Ok(ast) 47 | } 48 | 49 | pub fn eval_code_with( 50 | &mut self, 51 | code: &str, 52 | rhai_scope: Option, 53 | compiled_ast: Option<&AST>, 54 | file_id: Option<&str>, 55 | ) -> Result { 56 | let mut scope = match rhai_scope { 57 | Some(s) => s, 58 | None => Scope::new(), 59 | }; 60 | 61 | // Just eval as node will be in `all_nodes` 62 | if let Some(ast) = compiled_ast { 63 | let _ = self 64 | .engine 65 | .eval_ast_with_scope::(&mut scope, &ast) 66 | .map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine, file_id)))?; 67 | } else { 68 | let _ = self 69 | .engine 70 | .eval_with_scope::(&mut scope, code) 71 | .map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine, file_id)))?; 72 | }; 73 | 74 | // Retain signals 75 | crate::updates::retain_signals(&self.keep_signal.borrow()); 76 | 77 | // Merge all nodes in all_nodes (`enter([])`) into a single root node 78 | let merged_node = { 79 | let mut all_nodes_vec = self.all_nodes.borrow_mut(); 80 | 81 | let mut merged_children = Vec::new(); 82 | for node in all_nodes_vec.drain(..) { 83 | match node { 84 | WidgetNode::Enter(children) => merged_children.extend(children), 85 | // I think that the following line is redundant as 86 | // it will be enter(..) 100% of the time. But, what if it 87 | // is an empty enter or smth? Idk.. it works... so I'll keep it. 88 | other => merged_children.push(other), 89 | } 90 | } 91 | 92 | WidgetNode::Enter(merged_children) 93 | }; 94 | 95 | Ok(merged_node.setup_dyn_ids("root")) 96 | } 97 | 98 | pub fn eval_code_snippet(&mut self, code: &str) -> Result { 99 | let mut scope = Scope::new(); 100 | 101 | // Just eval as node will be in `all_nodes` 102 | let node = self 103 | .engine 104 | .eval_with_scope::(&mut scope, code) 105 | .map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine, Some(""))))?; 106 | 107 | // Retain signals 108 | crate::updates::retain_signals(&self.keep_signal.borrow()); 109 | 110 | // Clear all nodes 111 | self.all_nodes.borrow_mut().clear(); 112 | 113 | Ok(node) 114 | } 115 | 116 | pub fn code_from_file>(&mut self, file_path: P) -> Result { 117 | Ok(fs::read_to_string(&file_path) 118 | .map_err(|e| anyhow!("Failed to read {:?}: {}", file_path.as_ref(), e))?) 119 | } 120 | 121 | pub fn initial_poll_listen_scope(code: &str) -> Result> { 122 | // Setting the initial value of poll/listen 123 | let mut scope = Scope::new(); 124 | for (var, initial) in extract_poll_and_listen_vars(code)? { 125 | let value = match initial { 126 | Some(val) => Dynamic::from(val), 127 | None => Dynamic::UNIT, 128 | }; 129 | scope.set_value(var, value); 130 | } 131 | 132 | Ok(scope) 133 | } 134 | 135 | pub fn call_rhai_fn(&self, ast: &AST, expr: &str, scope: Option<&mut Scope>) -> Result<()> { 136 | // very naive split 137 | let (fn_name, args_str) = 138 | expr.split_once('(').ok_or_else(|| anyhow::anyhow!("Invalid expression: {}", expr))?; 139 | let fn_name = fn_name.trim(); 140 | let args_str = args_str.trim_end_matches(')'); 141 | 142 | // parse args into Dynamics 143 | let args: Vec = args_str 144 | .split(',') 145 | .filter(|s| !s.trim().is_empty()) 146 | .map(|s| { 147 | let s = s.trim(); 148 | if let Ok(i) = s.parse::() { 149 | rhai::Dynamic::from(i) 150 | } else if let Ok(f) = s.parse::() { 151 | rhai::Dynamic::from(f) 152 | } else { 153 | rhai::Dynamic::from(s.to_string()) 154 | } 155 | }) 156 | .collect(); 157 | 158 | let mut scope = match scope { 159 | Some(s) => s, 160 | None => &mut Scope::new(), 161 | }; 162 | 163 | match self.engine.call_fn::(&mut scope, ast, fn_name, args) { 164 | Ok(result) => { 165 | log::debug!("Call `{}` returned {:?}", fn_name, result); 166 | Ok(()) 167 | } 168 | Err(e) => { 169 | log::error!("Call `{}` failed: {}", fn_name, e); 170 | Err(anyhow::anyhow!(e.to_string())) 171 | } 172 | } 173 | } 174 | 175 | pub fn set_opt_level(&mut self, opt_lvl: OptimizationLevel) { 176 | self.engine.set_optimization_level(opt_lvl); 177 | } 178 | 179 | pub fn action_with_engine(&mut self, f: F) -> R 180 | where 181 | F: FnOnce(&mut Engine) -> R, 182 | { 183 | f(&mut self.engine) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/updates/listen.rs: -------------------------------------------------------------------------------- 1 | /* 2 | ┏━━━━━━━━━━━━━━━━━━━━━┓ 3 | ┃ Reference structure ┃ 4 | ┗━━━━━━━━━━━━━━━━━━━━━┛ 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] 7 | pub struct ListenScriptVar { 8 | pub name: VarName, 9 | pub command: String, 10 | pub initial_value: DynVal, 11 | pub command_span: Span, 12 | pub name_span: Span, 13 | } 14 | */ 15 | 16 | use super::{ReactiveVarStore, SHUTDOWN_REGISTRY}; 17 | use nix::{ 18 | sys::signal, 19 | unistd::{setpgid, Pid}, 20 | }; 21 | use rhai::Map; 22 | use shared_utils::extract_props::*; 23 | use std::process::Stdio; 24 | use tokio::io::AsyncBufReadExt; 25 | use tokio::io::BufReader; 26 | use tokio::process::Command; 27 | use tokio::signal as tokio_signal; 28 | use tokio::sync::watch; 29 | 30 | pub fn handle_listen( 31 | var_name: String, 32 | props: &Map, 33 | shell: String, 34 | store: ReactiveVarStore, 35 | tx: tokio::sync::mpsc::UnboundedSender, 36 | ) { 37 | let cmd = match get_string_prop(props, "cmd", Some("")) { 38 | Ok(c) => c, 39 | Err(e) => { 40 | log::warn!("Listen {} missing cmd property: {}", var_name, e); 41 | return; 42 | } 43 | }; 44 | 45 | // No need to do this as we apply the initial value before parsing 46 | // Handle initial value 47 | // if let Ok(initial) = get_string_prop(&props, "initial", None) { 48 | // log::debug!("[{}] initial value: {}", var_name, initial); 49 | // store.write().unwrap().insert(var_name.clone(), initial.clone()); 50 | // let _ = tx.send(var_name.clone()); 51 | // } 52 | 53 | let store = store.clone(); 54 | let tx = tx.clone(); 55 | 56 | let (shutdown_tx, mut shutdown_rx) = watch::channel(false); 57 | SHUTDOWN_REGISTRY.lock().unwrap().push(shutdown_tx.clone()); 58 | 59 | // Task to catch SIGINT and SIGTERM 60 | tokio::spawn({ 61 | let shutdown_tx = shutdown_tx.clone(); 62 | async move { 63 | let mut sigterm_stream = 64 | tokio_signal::unix::signal(tokio_signal::unix::SignalKind::terminate()).unwrap(); 65 | 66 | tokio::select! { 67 | _ = tokio_signal::ctrl_c() => { 68 | log::trace!("Received SIGINT"); 69 | } 70 | _ = sigterm_stream.recv() => { 71 | log::trace!("Received SIGTERM"); 72 | } 73 | } 74 | let _ = shutdown_tx.send(true); 75 | } 76 | }); 77 | 78 | tokio::spawn(async move { 79 | let mut child = unsafe { 80 | Command::new(&shell) 81 | .arg("-c") 82 | .arg(&cmd) 83 | // .kill_on_drop(true) 84 | .stdout(Stdio::piped()) 85 | .stderr(Stdio::piped()) 86 | .stdin(Stdio::null()) 87 | .pre_exec(|| { 88 | let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); 89 | 90 | #[cfg(target_os = "linux")] 91 | { 92 | if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0 { 93 | log::error!( 94 | "prctl PR_SET_PDEATHSIG failed: {}", 95 | std::io::Error::last_os_error() 96 | ); 97 | } 98 | } 99 | 100 | #[cfg(target_os = "freebsd")] 101 | { 102 | use libc::{c_int, c_void}; 103 | 104 | const PROC_PDEATHSIG_CTL: c_int = 11; 105 | let sig: c_int = libc::SIGTERM; 106 | if libc::procctl( 107 | libc::P_PID, 108 | 0, 109 | PROC_PDEATHSIG_CTL, 110 | &sig as *const _ as *mut c_void, 111 | ) != 0 112 | { 113 | log::error!( 114 | "procctl PROC_PDEATHSIG_CTL failed: {}", 115 | std::io::Error::last_os_error() 116 | ); 117 | } 118 | } 119 | 120 | #[cfg(target_os = "macos")] 121 | { 122 | // Perhaps make it a TODO? 123 | log::warn!("Parent-death signal is not supported on macOS system"); 124 | } 125 | 126 | Ok(()) 127 | }) 128 | .spawn() 129 | .expect("failed to start listener process") 130 | }; 131 | 132 | let mut stdout_lines = BufReader::new(child.stdout.take().unwrap()).lines(); 133 | let mut stderr_lines = BufReader::new(child.stderr.take().unwrap()).lines(); 134 | 135 | let mut last_value: Option = None; 136 | 137 | loop { 138 | tokio::select! { 139 | maybe_line = stdout_lines.next_line() => { 140 | match maybe_line { 141 | Ok(Some(line)) => { 142 | let val = line.trim().to_string(); 143 | if Some(&val) != last_value.as_ref() { 144 | last_value = Some(val.clone()); 145 | log::debug!("[{}] listened value: {}", var_name, val); 146 | store.write().unwrap().insert(var_name.clone(), val); 147 | let _ = tx.send(var_name.clone()); 148 | } else { 149 | log::trace!("[{}] value unchanged, skipping tx", var_name); 150 | } 151 | } 152 | Ok(None) => break, // EOF 153 | Err(e) => { 154 | log::error!("[{}] error reading line: {}", var_name, e); 155 | break; 156 | } 157 | } 158 | } 159 | maybe_err_line = stderr_lines.next_line() => { 160 | if let Ok(Some(line)) = maybe_err_line { 161 | log::warn!("stderr of `{}`: {}", var_name, line); 162 | } 163 | } 164 | _ = shutdown_rx.changed() => { 165 | if *shutdown_rx.borrow() { 166 | break; 167 | } 168 | } 169 | } 170 | } 171 | 172 | let _ = terminate_child(child).await; 173 | }); 174 | } 175 | 176 | async fn terminate_child(mut child: tokio::process::Child) { 177 | if let Some(id) = child.id() { 178 | log::debug!("Killing process with id {}", id); 179 | let _ = signal::killpg(Pid::from_raw(id as i32), signal::SIGTERM); 180 | tokio::select! { 181 | _ = child.wait() => { }, 182 | _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => { 183 | let _ = child.kill().await; 184 | } 185 | }; 186 | } else { 187 | let _ = child.kill().await; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /crates/rhai_impl/src/dyn_id.rs: -------------------------------------------------------------------------------- 1 | use crate::ast::WidgetNode; 2 | use rhai::{Dynamic, Map}; 3 | 4 | impl WidgetNode { 5 | /// A very important implementation of [`WidgetNode`]. 6 | /// This function implements dyn_id property to widgets. 7 | pub fn setup_dyn_ids(&self, parent_path: &str) -> Self { 8 | // fn to assign dyn_id to a node 9 | fn with_dyn_id(mut props: Map, dyn_id: &str) -> Map { 10 | props.insert("dyn_id".into(), Dynamic::from(dyn_id.to_string())); 11 | props 12 | } 13 | 14 | // fn to process children of a container node 15 | fn process_children( 16 | children: &[WidgetNode], 17 | parent_path: &str, 18 | kind: &str, 19 | ) -> Vec { 20 | children 21 | .iter() 22 | .enumerate() 23 | .map(|(idx, child)| { 24 | let child_path = format!("{}_{}_{}", parent_path, kind, idx); 25 | child.setup_dyn_ids(&child_path) 26 | }) 27 | .collect() 28 | } 29 | 30 | match self { 31 | WidgetNode::DefWindow { name, props, node } => WidgetNode::DefWindow { 32 | name: name.clone(), 33 | props: props.clone(), 34 | node: Box::new(node.setup_dyn_ids(name)), 35 | }, 36 | 37 | // == Containers with children == 38 | WidgetNode::Box { props, children } => WidgetNode::Box { 39 | props: with_dyn_id(props.clone(), parent_path), 40 | children: process_children(children, parent_path, "box"), 41 | }, 42 | WidgetNode::FlowBox { props, children } => WidgetNode::FlowBox { 43 | props: with_dyn_id(props.clone(), parent_path), 44 | children: process_children(children, parent_path, "flowbox"), 45 | }, 46 | WidgetNode::Expander { props, children } => WidgetNode::Expander { 47 | props: with_dyn_id(props.clone(), parent_path), 48 | children: process_children(children, parent_path, "expander"), 49 | }, 50 | WidgetNode::Revealer { props, children } => WidgetNode::Revealer { 51 | props: with_dyn_id(props.clone(), parent_path), 52 | children: process_children(children, parent_path, "revealer"), 53 | }, 54 | WidgetNode::Scroll { props, children } => WidgetNode::Scroll { 55 | props: with_dyn_id(props.clone(), parent_path), 56 | children: process_children(children, parent_path, "scroll"), 57 | }, 58 | WidgetNode::OverLay { props, children } => WidgetNode::OverLay { 59 | props: with_dyn_id(props.clone(), parent_path), 60 | children: process_children(children, parent_path, "overlay"), 61 | }, 62 | WidgetNode::Stack { props, children } => WidgetNode::Stack { 63 | props: with_dyn_id(props.clone(), parent_path), 64 | children: process_children(children, parent_path, "stack"), 65 | }, 66 | WidgetNode::EventBox { props, children } => WidgetNode::EventBox { 67 | props: with_dyn_id(props.clone(), parent_path), 68 | children: process_children(children, parent_path, "eventbox"), 69 | }, 70 | WidgetNode::ToolTip { props, children } => WidgetNode::ToolTip { 71 | props: with_dyn_id(props.clone(), parent_path), 72 | children: process_children(children, parent_path, "tooltip"), 73 | }, 74 | WidgetNode::LocalBind { props, children } => WidgetNode::LocalBind { 75 | props: with_dyn_id(props.clone(), parent_path), 76 | children: process_children(children, parent_path, "localbind"), 77 | }, 78 | WidgetNode::WidgetAction { props, children } => WidgetNode::WidgetAction { 79 | props: with_dyn_id(props.clone(), parent_path), 80 | children: process_children(children, parent_path, "widget_action"), 81 | }, 82 | 83 | // == Top-level container for multiple widgets == 84 | WidgetNode::Enter(children) => { 85 | WidgetNode::Enter(process_children(children, parent_path, "enter")) 86 | } 87 | 88 | // == Poll/Listen nodes == 89 | WidgetNode::Poll { var, props } => WidgetNode::Poll { 90 | var: var.clone(), 91 | props: with_dyn_id(props.clone(), &format!("{}_poll_{}", parent_path, var)), 92 | }, 93 | WidgetNode::Listen { var, props } => WidgetNode::Listen { 94 | var: var.clone(), 95 | props: with_dyn_id(props.clone(), &format!("{}_listen_{}", parent_path, var)), 96 | }, 97 | 98 | // == Leaf nodes == 99 | node @ WidgetNode::Label { props } 100 | | node @ WidgetNode::Button { props } 101 | | node @ WidgetNode::Image { props } 102 | | node @ WidgetNode::Icon { props } 103 | | node @ WidgetNode::Input { props } 104 | | node @ WidgetNode::Progress { props } 105 | | node @ WidgetNode::ComboBoxText { props } 106 | | node @ WidgetNode::Scale { props } 107 | | node @ WidgetNode::Checkbox { props } 108 | | node @ WidgetNode::Calendar { props } 109 | | node @ WidgetNode::ColorButton { props } 110 | | node @ WidgetNode::ColorChooser { props } 111 | | node @ WidgetNode::CircularProgress { props } 112 | | node @ WidgetNode::Graph { props } 113 | | node @ WidgetNode::GtkUI { props } 114 | | node @ WidgetNode::Transform { props } => { 115 | let new_props = with_dyn_id(props.clone(), parent_path); 116 | match node { 117 | WidgetNode::Label { .. } => WidgetNode::Label { props: new_props }, 118 | WidgetNode::Button { .. } => WidgetNode::Button { props: new_props }, 119 | WidgetNode::Image { .. } => WidgetNode::Image { props: new_props }, 120 | WidgetNode::Icon { .. } => WidgetNode::Icon { props: new_props }, 121 | WidgetNode::Input { .. } => WidgetNode::Input { props: new_props }, 122 | WidgetNode::Progress { .. } => WidgetNode::Progress { props: new_props }, 123 | WidgetNode::ComboBoxText { .. } => { 124 | WidgetNode::ComboBoxText { props: new_props } 125 | } 126 | WidgetNode::Scale { .. } => WidgetNode::Scale { props: new_props }, 127 | WidgetNode::Checkbox { .. } => WidgetNode::Checkbox { props: new_props }, 128 | WidgetNode::Calendar { .. } => WidgetNode::Calendar { props: new_props }, 129 | WidgetNode::ColorButton { .. } => WidgetNode::ColorButton { props: new_props }, 130 | WidgetNode::ColorChooser { .. } => { 131 | WidgetNode::ColorChooser { props: new_props } 132 | } 133 | WidgetNode::CircularProgress { .. } => { 134 | WidgetNode::CircularProgress { props: new_props } 135 | } 136 | WidgetNode::Graph { .. } => WidgetNode::Graph { props: new_props }, 137 | WidgetNode::GtkUI { .. } => WidgetNode::GtkUI { props: new_props }, 138 | WidgetNode::Transform { .. } => WidgetNode::Transform { props: new_props }, 139 | _ => unreachable!(), 140 | } 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/ewwii/src/widgets/circular_progressbar.rs: -------------------------------------------------------------------------------- 1 | use glib::Object; 2 | use gtk4::glib; 3 | use gtk4::prelude::*; 4 | use gtk4::subclass::prelude::*; 5 | use gtk4::{cairo, gdk, graphene}; 6 | use std::cell::Cell; 7 | 8 | mod imp { 9 | use super::*; 10 | 11 | pub struct CircProg { 12 | pub value: Cell, 13 | pub start_at: Cell, 14 | pub thickness: Cell, 15 | pub clockwise: Cell, 16 | pub fg_color: Cell, 17 | pub bg_color: Cell, 18 | } 19 | 20 | impl Default for CircProg { 21 | fn default() -> Self { 22 | Self { 23 | value: Cell::new(0.0), 24 | start_at: Cell::new(0.0), 25 | thickness: Cell::new(8.0), 26 | clockwise: Cell::new(true), 27 | fg_color: Cell::new(gdk::RGBA::new(1.0, 0.0, 0.0, 1.0)), 28 | bg_color: Cell::new(gdk::RGBA::new(0.0, 0.0, 0.0, 0.1)), 29 | } 30 | } 31 | } 32 | 33 | #[glib::object_subclass] 34 | impl ObjectSubclass for CircProg { 35 | const NAME: &'static str = "CircProg"; 36 | type Type = super::CircProg; 37 | type ParentType = gtk4::Widget; 38 | } 39 | 40 | impl ObjectImpl for CircProg { 41 | fn constructed(&self) { 42 | self.parent_constructed(); 43 | 44 | let obj = self.obj(); 45 | obj.add_css_class("circular-progress"); 46 | } 47 | 48 | fn properties() -> &'static [glib::ParamSpec] { 49 | use once_cell::sync::Lazy; 50 | static PROPERTIES: Lazy> = Lazy::new(|| { 51 | vec![ 52 | glib::ParamSpecDouble::builder("value") 53 | .minimum(0.0) 54 | .maximum(100.0) 55 | .default_value(0.0) 56 | .build(), 57 | glib::ParamSpecDouble::builder("start-at") 58 | .minimum(0.0) 59 | .maximum(100.0) 60 | .default_value(0.0) 61 | .build(), 62 | glib::ParamSpecDouble::builder("thickness") 63 | .minimum(1.0) 64 | .maximum(50.0) 65 | .default_value(8.0) 66 | .build(), 67 | glib::ParamSpecBoolean::builder("clockwise").default_value(true).build(), 68 | glib::ParamSpecBoxed::builder::("fg-color").build(), 69 | glib::ParamSpecBoxed::builder::("bg-color").build(), 70 | ] 71 | }); 72 | PROPERTIES.as_ref() 73 | } 74 | 75 | fn set_property(&self, _: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 76 | match pspec.name() { 77 | "value" => self.value.set(value.get().unwrap()), 78 | "start-at" => self.start_at.set(value.get().unwrap()), 79 | "thickness" => self.thickness.set(value.get().unwrap()), 80 | "clockwise" => self.clockwise.set(value.get().unwrap()), 81 | "fg-color" => self.fg_color.set(value.get().unwrap()), 82 | "bg-color" => self.bg_color.set(value.get().unwrap()), 83 | x => panic!("Tried to set inexistant property of CircProg: {}", x,), 84 | } 85 | self.obj().queue_draw(); 86 | } 87 | 88 | fn property(&self, _: usize, pspec: &glib::ParamSpec) -> glib::Value { 89 | match pspec.name() { 90 | "value" => self.value.get().to_value(), 91 | "start-at" => self.start_at.get().to_value(), 92 | "thickness" => self.thickness.get().to_value(), 93 | "clockwise" => self.clockwise.get().to_value(), 94 | "fg-color" => self.fg_color.get().to_value(), 95 | "bg-color" => self.bg_color.get().to_value(), 96 | x => panic!("Tried to get inexistant property of CircProg: {}", x,), 97 | } 98 | } 99 | } 100 | 101 | impl WidgetImpl for CircProg { 102 | fn measure(&self, _orientation: gtk4::Orientation, _for_size: i32) -> (i32, i32, i32, i32) { 103 | let min_size = 32; 104 | let natural_size = 64; 105 | (min_size, natural_size, -1, -1) 106 | } 107 | 108 | fn snapshot(&self, snapshot: >k4::Snapshot) { 109 | let value = self.value.get(); 110 | let start_at = self.start_at.get(); 111 | let thickness = self.thickness.get(); 112 | let clockwise = self.clockwise.get(); 113 | let fg_color = self.fg_color.get(); 114 | let bg_color = self.bg_color.get(); 115 | 116 | let margin_start = self.obj().margin_start() as f64; 117 | let margin_end = self.obj().margin_end() as f64; 118 | let margin_top = self.obj().margin_top() as f64; 119 | let margin_bottom = self.obj().margin_bottom() as f64; 120 | // Padding is not supported yet 121 | 122 | let (start_angle, end_angle) = if clockwise { 123 | (0.0, perc_to_rad(value)) 124 | } else { 125 | (perc_to_rad(100.0 - value), 2f64 * std::f64::consts::PI) 126 | }; 127 | 128 | let total_width = self.obj().allocated_width() as f64; 129 | let total_height = self.obj().allocated_height() as f64; 130 | let center = (total_width / 2.0, total_height / 2.0); 131 | 132 | let circle_width = total_width - margin_start - margin_end; 133 | let circle_height = total_height - margin_top - margin_bottom; 134 | let outer_ring = f64::min(circle_width, circle_height) / 2.0; 135 | let inner_ring = (f64::min(circle_width, circle_height) / 2.0) - thickness; 136 | 137 | // Snapshot Cairo node 138 | let cr = snapshot.append_cairo(&graphene::Rect::new( 139 | 0.0_f32, 140 | 0.0_f32, 141 | total_width as f32, 142 | total_height as f32, 143 | )); 144 | 145 | cr.save().unwrap(); 146 | 147 | // Centering 148 | cr.translate(center.0, center.1); 149 | cr.rotate(perc_to_rad(start_at)); 150 | cr.translate(-center.0, -center.1); 151 | 152 | // Background Ring 153 | cr.move_to(center.0, center.1); 154 | cr.arc(center.0, center.1, outer_ring, 0.0, perc_to_rad(100.0)); 155 | cr.set_source_rgba( 156 | bg_color.red().into(), 157 | bg_color.green().into(), 158 | bg_color.blue().into(), 159 | bg_color.alpha().into(), 160 | ); 161 | cr.move_to(center.0, center.1); 162 | cr.arc(center.0, center.1, inner_ring, 0.0, perc_to_rad(100.0)); 163 | cr.set_fill_rule(cairo::FillRule::EvenOdd); // Substract one circle from the other 164 | cr.fill().unwrap(); 165 | 166 | // Foreground Ring 167 | cr.move_to(center.0, center.1); 168 | cr.arc(center.0, center.1, outer_ring, start_angle, end_angle); 169 | cr.set_source_rgba( 170 | fg_color.red().into(), 171 | fg_color.green().into(), 172 | fg_color.blue().into(), 173 | fg_color.alpha().into(), 174 | ); 175 | cr.move_to(center.0, center.1); 176 | cr.arc(center.0, center.1, inner_ring, start_angle, end_angle); 177 | cr.set_fill_rule(cairo::FillRule::EvenOdd); // Substract one circle from the other 178 | cr.fill().unwrap(); 179 | 180 | cr.restore().unwrap(); 181 | } 182 | } 183 | } 184 | 185 | glib::wrapper! { 186 | pub struct CircProg(ObjectSubclass) 187 | @extends gtk4::Widget, 188 | @implements gtk4::Accessible, gtk4::Actionable, gtk4::Buildable, gtk4::ConstraintTarget; 189 | } 190 | 191 | impl CircProg { 192 | pub fn new() -> Self { 193 | Object::builder().build() 194 | } 195 | } 196 | 197 | fn perc_to_rad(n: f64) -> f64 { 198 | (n / 100f64) * 2f64 * std::f64::consts::PI 199 | } 200 | -------------------------------------------------------------------------------- /crates/ewwii/src/widgets/transform.rs: -------------------------------------------------------------------------------- 1 | // use crate::window::coords::NumWithUnit; 2 | // use anyhow::{anyhow, Result}; 3 | // use gtk4::glib::{self, object_subclass, wrapper, Properties}; 4 | // use gtk4::{prelude::*, subclass::prelude::*}; 5 | // use std::{cell::RefCell, str::FromStr}; 6 | 7 | // use crate::error_handling_ctx; 8 | 9 | // wrapper! { 10 | // pub struct Transform(ObjectSubclass) 11 | // @extends gtk4::Bin, gtk4::Container, gtk4::Widget; 12 | // } 13 | 14 | // #[derive(Properties)] 15 | // #[properties(wrapper_type = Transform)] 16 | // pub struct TransformPriv { 17 | // #[property(get, set, nick = "Rotate", blurb = "The Rotation", minimum = f64::MIN, maximum = f64::MAX, default = 0f64)] 18 | // rotate: RefCell, 19 | 20 | // #[property(get, set, nick = "Transform-Origin X", blurb = "X coordinate (%/px) for the Transform-Origin", default = None)] 21 | // transform_origin_x: RefCell>, 22 | 23 | // #[property(get, set, nick = "Transform-Origin Y", blurb = "Y coordinate (%/px) for the Transform-Origin", default = None)] 24 | // transform_origin_y: RefCell>, 25 | 26 | // #[property(get, set, nick = "Translate x", blurb = "The X Translation", default = None)] 27 | // translate_x: RefCell>, 28 | 29 | // #[property(get, set, nick = "Translate y", blurb = "The Y Translation", default = None)] 30 | // translate_y: RefCell>, 31 | 32 | // #[property(get, set, nick = "Scale x", blurb = "The amount to scale in x", default = None)] 33 | // scale_x: RefCell>, 34 | 35 | // #[property(get, set, nick = "Scale y", blurb = "The amount to scale in y", default = None)] 36 | // scale_y: RefCell>, 37 | 38 | // content: RefCell>, 39 | // } 40 | 41 | // // This should match the default values from the ParamSpecs 42 | // impl Default for TransformPriv { 43 | // fn default() -> Self { 44 | // TransformPriv { 45 | // rotate: RefCell::new(0.0), 46 | // transform_origin_x: RefCell::new(None), 47 | // transform_origin_y: RefCell::new(None), 48 | // translate_x: RefCell::new(None), 49 | // translate_y: RefCell::new(None), 50 | // scale_x: RefCell::new(None), 51 | // scale_y: RefCell::new(None), 52 | // content: RefCell::new(None), 53 | // } 54 | // } 55 | // } 56 | 57 | // impl ObjectImpl for TransformPriv { 58 | // fn properties() -> &'static [glib::ParamSpec] { 59 | // Self::derived_properties() 60 | // } 61 | 62 | // fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 63 | // match pspec.name() { 64 | // "rotate" => { 65 | // self.rotate.replace(value.get().unwrap()); 66 | // self.obj().queue_draw(); // Queue a draw call with the updated value 67 | // } 68 | // "transform-origin-x" => { 69 | // self.transform_origin_x.replace(value.get().unwrap()); 70 | // self.obj().queue_draw(); // Queue a draw call with the updated value 71 | // } 72 | // "transform-origin-y" => { 73 | // self.transform_origin_y.replace(value.get().unwrap()); 74 | // self.obj().queue_draw(); // Queue a draw call with the updated value 75 | // } 76 | // "translate-x" => { 77 | // self.translate_x.replace(value.get().unwrap()); 78 | // self.obj().queue_draw(); // Queue a draw call with the updated value 79 | // } 80 | // "translate-y" => { 81 | // self.translate_y.replace(value.get().unwrap()); 82 | // self.obj().queue_draw(); // Queue a draw call with the updated value 83 | // } 84 | // "scale-x" => { 85 | // self.scale_x.replace(value.get().unwrap()); 86 | // self.obj().queue_draw(); // Queue a draw call with the updated value 87 | // } 88 | // "scale-y" => { 89 | // self.scale_y.replace(value.get().unwrap()); 90 | // self.obj().queue_draw(); // Queue a draw call with the updated value 91 | // } 92 | // x => panic!("Tried to set inexistant property of Transform: {}", x,), 93 | // } 94 | // } 95 | 96 | // fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 97 | // self.derived_property(id, pspec) 98 | // } 99 | // } 100 | 101 | // #[object_subclass] 102 | // impl ObjectSubclass for TransformPriv { 103 | // type ParentType = gtk4::Bin; 104 | // type Type = Transform; 105 | 106 | // const NAME: &'static str = "Transform"; 107 | 108 | // fn class_init(klass: &mut Self::Class) { 109 | // klass.set_css_name("transform"); 110 | // } 111 | // } 112 | 113 | // impl Default for Transform { 114 | // fn default() -> Self { 115 | // Self::new() 116 | // } 117 | // } 118 | 119 | // impl Transform { 120 | // pub fn new() -> Self { 121 | // glib::Object::new::() 122 | // } 123 | // } 124 | 125 | // impl ContainerImpl for TransformPriv { 126 | // fn add(&self, widget: >k4::Widget) { 127 | // if let Some(content) = &*self.content.borrow() { 128 | // // TODO: Handle this error when populating children widgets instead 129 | // error_handling_ctx::print_error(anyhow!( 130 | // "Error, trying to add multiple children to a circular-progress widget" 131 | // )); 132 | // self.parent_remove(content); 133 | // } 134 | // self.parent_add(widget); 135 | // self.content.replace(Some(widget.clone())); 136 | // } 137 | // } 138 | 139 | // impl BinImpl for TransformPriv {} 140 | // impl WidgetImpl for TransformPriv { 141 | // fn draw(&self, cr: >k4::cairo::Context) -> glib::Propagation { 142 | // let res: Result<()> = (|| { 143 | // let rotate = *self.rotate.borrow(); 144 | // let total_width = self.obj().allocated_width() as f64; 145 | // let total_height = self.obj().allocated_height() as f64; 146 | 147 | // cr.save()?; 148 | 149 | // let transform_origin_x = match &*self.transform_origin_x.borrow() { 150 | // Some(rcx) => { 151 | // NumWithUnit::from_str(rcx)?.pixels_relative_to(total_width as i32) as f64 152 | // } 153 | // None => 0.0, 154 | // }; 155 | // let transform_origin_y = match &*self.transform_origin_y.borrow() { 156 | // Some(rcy) => { 157 | // NumWithUnit::from_str(rcy)?.pixels_relative_to(total_height as i32) as f64 158 | // } 159 | // None => 0.0, 160 | // }; 161 | 162 | // let translate_x = match &*self.translate_x.borrow() { 163 | // Some(tx) => { 164 | // NumWithUnit::from_str(tx)?.pixels_relative_to(total_width as i32) as f64 165 | // } 166 | // None => 0.0, 167 | // }; 168 | 169 | // let translate_y = match &*self.translate_y.borrow() { 170 | // Some(ty) => { 171 | // NumWithUnit::from_str(ty)?.pixels_relative_to(total_height as i32) as f64 172 | // } 173 | // None => 0.0, 174 | // }; 175 | 176 | // let scale_x = match &*self.scale_x.borrow() { 177 | // Some(sx) => { 178 | // NumWithUnit::from_str(sx)?.perc_relative_to(total_width as i32) as f64 / 100.0 179 | // } 180 | // None => 1.0, 181 | // }; 182 | 183 | // let scale_y = match &*self.scale_y.borrow() { 184 | // Some(sy) => { 185 | // NumWithUnit::from_str(sy)?.perc_relative_to(total_height as i32) as f64 / 100.0 186 | // } 187 | // None => 1.0, 188 | // }; 189 | 190 | // cr.translate(transform_origin_x, transform_origin_y); 191 | // cr.rotate(perc_to_rad(rotate)); 192 | // cr.translate(translate_x - transform_origin_x, translate_y - transform_origin_y); 193 | // cr.scale(scale_x, scale_y); 194 | 195 | // // Children widget 196 | // if let Some(child) = &*self.content.borrow() { 197 | // self.obj().propagate_draw(child, cr); 198 | // } 199 | 200 | // cr.restore()?; 201 | // Ok(()) 202 | // })(); 203 | 204 | // if let Err(error) = res { 205 | // error_handling_ctx::print_error(error) 206 | // }; 207 | 208 | // glib::Propagation::Proceed 209 | // } 210 | // } 211 | 212 | // fn perc_to_rad(n: f64) -> f64 { 213 | // (n / 100f64) * 2f64 * std::f64::consts::PI 214 | // } 215 | --------------------------------------------------------------------------------