diff options
30 files changed, 1416 insertions, 110 deletions
diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..d046680 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[build] +rustflags = ["-C", "prefer-dynamic"] @@ -1,2 +1,3 @@ /target *~ +*.swp @@ -35,44 +35,51 @@ checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - -[[package]] -name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "crossbeam-channel" -version = "0.4.4" +name = "chrono" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ - "crossbeam-utils", - "maybe-uninit", + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi", ] [[package]] name = "crossbeam-utils" -version = "0.7.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" dependencies = [ "autocfg", - "cfg-if 0.1.10", + "cfg-if", "lazy_static", ] [[package]] +name = "erased-serde" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0465971a8cc1fa2455c8465aaa377131e1f1cf4983280f474a13e68793aa770c" +dependencies = [ + "serde", +] + +[[package]] name = "getrandom" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi", ] @@ -105,9 +112,19 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" + +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if", + "winapi", +] [[package]] name = "locale_config" @@ -132,18 +149,31 @@ dependencies = [ ] [[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - -[[package]] name = "memchr" version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] name = "objc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -238,9 +268,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" dependencies = [ "bitflags", ] @@ -272,6 +302,30 @@ dependencies = [ ] [[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] name = "serde" version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -292,20 +346,62 @@ dependencies = [ ] [[package]] +name = "signal-hook" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef33d6d0cd06e0840fba9985aab098c147e67e05cee14d412d3345ed14ff30ac" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +dependencies = [ + "libc", +] + +[[package]] name = "swaystatus" version = "0.1.0" dependencies = [ - "crossbeam-channel", + "crossbeam-utils", + "erased-serde", "gettext-rs", + "libloading", + "rustc_version", "serde", + "signal-hook", + "swaystatus-plugin", "toml", ] [[package]] +name = "swaystatus-clock" +version = "0.1.0" +dependencies = [ + "chrono", + "erased-serde", + "serde", + "swaystatus-plugin", +] + +[[package]] +name = "swaystatus-plugin" +version = "0.1.0" +dependencies = [ + "erased-serde", + "rustc_version", +] + +[[package]] name = "syn" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87" +checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" dependencies = [ "proc-macro2", "quote", @@ -318,7 +414,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "rand", "redox_syscall", @@ -327,6 +423,16 @@ dependencies = [ ] [[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] name = "toml" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1,13 +1,7 @@ -[package] -name = "swaystatus" -version = "0.1.0" -authors = ["Andreas Grois <andi@grois.info>"] -edition = "2018" +[workspace] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -crossbeam-channel = "0.4" -gettext-rs = "0.6.0" -serde = { version = "1.0", features = ["derive"] } -toml = "0.5" +members = [ + "swaystatus", + "swaystatus-plugin", + "clock", +] diff --git a/clock/.cargo/config b/clock/.cargo/config new file mode 100644 index 0000000..d046680 --- /dev/null +++ b/clock/.cargo/config @@ -0,0 +1,2 @@ +[build] +rustflags = ["-C", "prefer-dynamic"] diff --git a/clock/.gitignore b/clock/.gitignore new file mode 100644 index 0000000..db08ac5 --- /dev/null +++ b/clock/.gitignore @@ -0,0 +1,4 @@ +/target +Cargo.lock +*~ +*.swp diff --git a/clock/Cargo.toml b/clock/Cargo.toml new file mode 100644 index 0000000..2215005 --- /dev/null +++ b/clock/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "swaystatus-clock" +version = "0.1.0" +authors = ["Andreas Grois <andi@grois.info>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +swaystatus-plugin = { path = '../swaystatus-plugin', version = '*'} +serde = { version = "1.0", features = ["derive"] } +erased-serde = "0.3" +chrono = { version = "0.4", features = ["serde"] } + +[lib] +crate-type = ["dylib"] diff --git a/clock/src/lib.rs b/clock/src/lib.rs new file mode 100644 index 0000000..b3ad0e8 --- /dev/null +++ b/clock/src/lib.rs @@ -0,0 +1,87 @@ +use serde::{Serialize, Deserialize}; +use swaystatus_plugin::*; +use std::sync::mpsc::*; + +pub struct ClockPlugin; +pub struct ClockRunnable<'c> { + config : &'c ClockConfig, + from_main : Receiver<MessagesFromMain>, + to_main : Box<dyn MsgModuleToMain +'c> +} + +impl<'c> SwayStatusModuleRunnable for ClockRunnable<'c> { + fn run(&self) { + for i in 0..4 { + println!("Sending Error {}",i); + self.to_main.send_update(Err(PluginError::PrintToStdErr(format!("Hello {}", i)))).unwrap(); + std::thread::sleep(std::time::Duration::from_secs(2)); + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "PascalCase",default)] +struct ClockConfig { + format : String, + refresh_rate : f32 +} + +impl Default for ClockConfig { + fn default() -> Self { + ClockConfig { + format : String::from("%R"), + refresh_rate : 1.0 + } + } +} + +impl SwayStatusModuleInstance for ClockConfig { + fn make_runnable<'p>(&'p self, to_main : Box<dyn MsgModuleToMain + 'p>) -> (Box<dyn SwayStatusModuleRunnable + 'p>, Box<dyn MsgMainToModule + 'p>) { + let (sender_from_main, from_main) = channel(); + let runnable = ClockRunnable { + config : &self, + from_main, + to_main + }; + let s = SenderForMain(sender_from_main); + (Box::new(runnable), Box::new(s)) + } +} + +impl SwayStatusModule for ClockPlugin { + fn get_name(&self) -> &str { + "ClockPlugin" + } + fn deserialize_config<'de>(&self, deserializer : &mut (dyn erased_serde::Deserializer + 'de)) -> Result<Box<dyn SwayStatusModuleInstance>, erased_serde::Error> { + let result : ClockConfig = erased_serde::deserialize(deserializer)?; + Ok(Box::new(result)) + } + fn get_default_config(&self) -> Box<dyn SwayStatusModuleInstance> { + let config = ClockConfig::default(); + Box::new(config) + } +} + +impl ClockPlugin { + fn new() -> ClockPlugin { + ClockPlugin + } +} + +enum MessagesFromMain { + Quit, + Refresh +} + +struct SenderForMain(Sender<MessagesFromMain>); + +impl MsgMainToModule for SenderForMain { + fn send_quit(&self) -> Result<(),()> { + self.0.send(MessagesFromMain::Quit).map_err(|_| ()) + } + fn send_refresh(&self) -> Result<(),()> { + self.0.send(MessagesFromMain::Refresh).map_err(|_| ()) + } +} + +declare_swaystatus_module!(ClockPlugin, ClockPlugin::new); diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 2c44978..0000000 --- a/src/config.rs +++ /dev/null @@ -1,45 +0,0 @@ -use serde::{Serialize, Deserialize}; - -#[derive(Serialize, Deserialize, Debug)] -pub struct SwaystatusConfig { - separator : Option<String>, //Separator character between elements. - plugin_path : Option<String>, //path to load plugins from. If unset, hardcoded value is used. - elements : Option<Vec<(String,String)>>, //plugins to display and their config section. -} - -fn create_default_config() -> SwaystatusConfig { - return SwaystatusConfig{ - separator : Some(String::from(", ")), - plugin_path : Some(String::from("")), - //elements : Some(Vec::new())}; - elements : Some(vec!((String::from("time"), String::from("format = \"yyyy-mm-dd hh-mm-ss\"")))) - }; -} - -pub enum SwaystatusConfigErrors -{ - FileNotFound, - ParsingError { - message : String - } -} - -pub fn print_sample_config() { - let default_config = create_default_config(); - let output = toml::to_string(&default_config).unwrap(); - print!("{}", output); -} - -pub fn read_config(path : &std::path::Path) -> Result<SwaystatusConfig,SwaystatusConfigErrors> { - let config_file = match std::fs::read_to_string(path) { - Ok(x) => x, - Err (_) => return Err(SwaystatusConfigErrors::FileNotFound) - }; - let result = match toml::from_str(&config_file) { - Ok(x) => x, - Err(e) => return Err(SwaystatusConfigErrors::ParsingError{message: e.to_string()}) - }; - - return Ok(result); - -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index c85aaa1..0000000 --- a/src/main.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod module_communication; -mod config; - -fn main() { - config::print_sample_config(); -} diff --git a/src/module_communication.rs b/src/module_communication.rs deleted file mode 100644 index 3d4aefe..0000000 --- a/src/module_communication.rs +++ /dev/null @@ -1,18 +0,0 @@ -pub enum MsgMainToModule { - Quit, -} -pub enum MsgModuleToMain { - UpdateText { - text : Result<String, String> - } -} - -pub trait SwayStatusModule { - fn new(from_main : crossbeam_channel::Receiver<MsgMainToModule>, - to_main : crossbeam_channel::Sender<MsgModuleToMain>, - module_settings : &str) -> Result<Box<Self>,String>; - - fn get_name(&self) -> &'static str; - - fn run(&self); -} diff --git a/swaystatus-plugin/.cargo/config b/swaystatus-plugin/.cargo/config new file mode 100644 index 0000000..d046680 --- /dev/null +++ b/swaystatus-plugin/.cargo/config @@ -0,0 +1,2 @@ +[build] +rustflags = ["-C", "prefer-dynamic"] diff --git a/swaystatus-plugin/.gitignore b/swaystatus-plugin/.gitignore new file mode 100644 index 0000000..d4790ef --- /dev/null +++ b/swaystatus-plugin/.gitignore @@ -0,0 +1,4 @@ +/target +*~ +*.swp +Cargo.lock diff --git a/swaystatus-plugin/Cargo.toml b/swaystatus-plugin/Cargo.toml new file mode 100644 index 0000000..c521baa --- /dev/null +++ b/swaystatus-plugin/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "swaystatus-plugin" +version = "0.1.0" +authors = ["Andreas Grois <andi@grois.info>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +erased-serde = "0.3" + +[build-dependencies] +rustc_version = "0.2.3" diff --git a/swaystatus-plugin/build.rs b/swaystatus-plugin/build.rs new file mode 100644 index 0000000..a38022f --- /dev/null +++ b/swaystatus-plugin/build.rs @@ -0,0 +1,4 @@ +fn main() { + let version = rustc_version::version().unwrap(); + println!("cargo:rustc-env=RUSTC_VERSION={}", version); +} diff --git a/swaystatus-plugin/src/lib.rs b/swaystatus-plugin/src/lib.rs new file mode 100644 index 0000000..3e27d19 --- /dev/null +++ b/swaystatus-plugin/src/lib.rs @@ -0,0 +1,163 @@ +#![warn(missing_docs)] +//! Interface definitions for swaystatus plugins. +//! +//! A plugin is a shared library that provides the following types: +//! - `SwayStatusModule`, the main entry point of the module, used to create SwayStatusModuleInstance +//! - `SwayStatusModuleInstance`, one instance of the module. Gets initialized with settings and creates SwayStatusModuleRunnable. +//! - `SwayStatusModuleRunnable`, created by SwayStatusModuleInstance, the plugin's main loop +//! +//! In addition the plugin must have a constructor function that returns a valid SwayStatusModule. +//! This can then be exported using the `declare_swaystatus_module()` macro. +//! +//! The swaystatus main program will call methods on your `SwayStatusModule` during initialization. +//! All calls to this interface will come from the main thread. +//! +//! The `make_runnable()` method should prepare a runnable object, which will then be run in a +//! separate worker thread. You have full control over that thread, meaning you can just sleep +//! until an update is required. The make_runnable() is still called from the main thread. +//! However the runnable's run() method is called from a worker thread. +//! +//! Communication between plugin and main module is routed through special trait objects instead +//! of directly moving channel endpoints. This is done for two reasons. The first is that it allows +//! to abstract away the need to use a given synchronization framework. That way for instance the +//! communication from main to plugin could use crossbeam, while the answers from the plugin are +//! sent using the standard library's mpsc channels. +//! The second, and more important reason is that (at least crossbeam's) channels use thread-local +//! storage, which would only work properly between different dynamic libraries, if they also +//! dynamically link against the library that supplies the channel implementation. Since linkage +//! with Cargo is defined by the dependency, not by the user, that's not really an option... +//! +//! ## Note on lifetimes: +//! There are relatively strict lifetimes imposed. No created trait object may outlive its creator. +//! This is because loading plugins is by definition unsafe, and we need to make sure that nothing +//! can exist beyond the symbols from the dynamic library. +//! +//! ## Note on linkage: +//! While you can't easily change how dependencies are linked into your plugin, you can choose to +//! dynamically link against the Rust standard library. For memory reasons I'd strongly recommend +//! to do so. The easiest way is to set rustc compiler flags using .cargo/config in the project. + +use erased_serde::serialize_trait_object; + +#[doc(hidden)] +pub static RUSTC_VERSION : &str = env!("RUSTC_VERSION"); +#[doc(hidden)] +pub static MODULE_VERSION : &str = env!("CARGO_PKG_VERSION"); + +/// Declares a public export C function that creates your plugin's main object. +/// parameters are: The plugin's concrete type, and the constructor function for it. +/// This is blatantly stolen from +/// https://michael-f-bryan.github.io/rust-ffi-guide/dynamic_loading.html +#[macro_export] +macro_rules! declare_swaystatus_module { + ($plugin_type:ty, $constructor:path) => { + #[no_mangle] + pub extern "C" fn _swaystatus_module_create() -> *mut dyn $crate::SwayStatusModule { + // make sure the constructor is the correct type. + let constructor: fn() -> $plugin_type = $constructor; + let object = constructor(); + let boxed: Box<$crate::SwayStatusModule> = Box::new(object); + Box::into_raw(boxed) + } + #[no_mangle] + pub extern "C" fn _swaystatus_module_version() -> *const str { + $crate::MODULE_VERSION + } + #[no_mangle] + pub extern "C" fn _swaystatus_rustc_version() -> *const str { + $crate::RUSTC_VERSION + } + }; +} + +/// You need to implement this trait, as creating a runnable needs to return this type as well. +/// A typical implementation would be a thin wrapper around a channel's sender end. +/// Please don't make this blocking to prevent deadlocks. +pub trait MsgMainToModule { + /// Implement this in such a way, that when it's called from the main thread, your module will + /// soon-ish return from it's main function, after cleaning up it's resources and after joining + /// all threads it spawns. + fn send_quit(&self) -> Result<(),()>; + + /// Implement this in such a way, that when it's called from the main thread, your module will + /// soon-ish send an updated text. The main module does not really wait for updates, so + /// ignoring this or implementing it empty is perfectly fine if you know that your module's + /// output cannot possibly change between updates it sends anyhow. + fn send_refresh(&self) -> Result<(),()>; +} + +/// When communicating an error to the main program, this allows to choose an appropriate handling +/// method. If the error does not prevent text updates, you likely just want to print it to stderr. +/// If it makes further processing impossible but doesn't cause an outright crash, consider +/// showing it instead of the usual text instead. +#[derive(Debug)] +pub enum PluginError { + /// Use this variant if your error is not critical for the plugin's operation, but should still + /// be communicated to the main program. The main program currently just calls eprintln! with + /// it, but this might change if it gets more features (like logging). If you want a + /// verbose/short type of error, send two errors, first one with this variant that holds the + /// verbose error, afterwards one with ShowInsteadOfText that just replaces the text with a + /// short error. + PrintToStdErr(String), + /// This notifies the main program that an error was encountered and no future text updates + /// from this plugin are expected. The main program will replace the last text output by this + /// error message. If you want to send both, a verbose error for the terminal/log and a short + /// one to be displayed in the status bar, first send a PrintToStdErr variant with a verbose + /// error, and then the short text as this variant. + ShowInsteadOfText(String) +} +impl std::error::Error for PluginError {} +impl std::fmt::Display for PluginError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + PluginError::PrintToStdErr(x) => x, + PluginError::ShowInsteadOfText(x) => x + }) + } +} + +/// This is implemented by the main program and passed to your module when creating the runnable. +/// Please note that the functions on this might eventually block until the main module finished +/// processing them. +pub trait MsgModuleToMain : Send { + /// Invoke this to update your text. This triggers the main program to build a new line of + /// stdout output. While this can in theory return an error, that should practically never + /// happen. If this errors, you should probably clean up your resources and return from the + /// run() function. In other words, act as if main had sent you a quit command. + fn send_update(&self, text : Result<String, PluginError>) -> Result<(),()>; +} + +/// Interface your module should implement. All functions of this will be called in the main thread. +pub trait SwayStatusModule { + ///The plugin's name. Must be globally unique. Shown to users in the config file. + fn get_name(&self) -> &str; + + ///Called by the main program with a deserializer that holds the config for an instance of this + ///module. + ///In almost all cases it's enough to just call `erased_serde::deserialize(deserializer)?` in + ///this function. + fn deserialize_config<'de, 'p>(&'p self, deserializer : &mut (dyn erased_serde::Deserializer + 'de)) -> Result<Box<dyn SwayStatusModuleInstance + 'p>,erased_serde::Error>; + + ///This is used for the command line option to print a default configuration. + ///Let it return your config with all defaults (including optional fields). + fn get_default_config<'p>(&'p self) -> Box<dyn SwayStatusModuleInstance + 'p>; +} + +///This is what `SwayStatusModuleInstance::make_runnable()` returns. The main function of your module. +///Will be called in a worker thread. +pub trait SwayStatusModuleRunnable : Send { + ///Starts executing this module. + fn run(&self); +} + +///Implement this trait on a struct that holds the configuration for a single instance of your +///plugin. The make_runnable then creates a runnable that gets moved to a different thread. +///In addition to making the runnable, the `make_runnable()` method also needs to return a +///MsgMainToModule trait object. +pub trait SwayStatusModuleInstance : erased_serde::Serialize { + ///The main initialization function. Takes the 2 communication channels and a configuration. + ///The config is a trait object of the same type you provide in `get_default_config()` and + ///`deserialize_config()`. + fn make_runnable<'p>(&'p self, to_main : Box<dyn MsgModuleToMain + 'p>) -> (Box<dyn SwayStatusModuleRunnable + 'p>, Box<dyn MsgMainToModule + 'p>); +} +serialize_trait_object!(SwayStatusModuleInstance); diff --git a/swaystatus/.gitignore b/swaystatus/.gitignore new file mode 100644 index 0000000..b0c2531 --- /dev/null +++ b/swaystatus/.gitignore @@ -0,0 +1,3 @@ +/target +*~ +*.swp diff --git a/swaystatus/Cargo.toml b/swaystatus/Cargo.toml new file mode 100644 index 0000000..20677f6 --- /dev/null +++ b/swaystatus/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "swaystatus" +version = "0.1.0" +authors = ["Andreas Grois <andi@grois.info>"] +edition = "2018" +description = "Fully modular status bar text updater, similar to i3bar." + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +crossbeam-utils = "0.8" +gettext-rs = "0.6.0" +serde = { version = "1.0", features = ["derive"] } +erased-serde = "0.3" +toml = "0.5" +libloading = "0.7" +signal-hook = { version = "0.3", default-features = false, features = ["iterator"]} + +swaystatus-plugin = { path = '../swaystatus-plugin', version = '*'} + +[build-dependencies] +rustc_version = "0.2.3" + +#[dev-dependencies] +#mockall = "0.9.1" diff --git a/swaystatus/build.rs b/swaystatus/build.rs new file mode 100644 index 0000000..a38022f --- /dev/null +++ b/swaystatus/build.rs @@ -0,0 +1,4 @@ +fn main() { + let version = rustc_version::version().unwrap(); + println!("cargo:rustc-env=RUSTC_VERSION={}", version); +} diff --git a/swaystatus/src/communication/mod.rs b/swaystatus/src/communication/mod.rs new file mode 100644 index 0000000..c0deff9 --- /dev/null +++ b/swaystatus/src/communication/mod.rs @@ -0,0 +1,53 @@ +use swaystatus_plugin as plugin; +use std::sync::mpsc::Sender; + +/// Used for internal communication. At the moment only from the signal handler to the main thread. +pub enum InternalMessage { + ///Exit gracefully. + Quit, + ///Refresh all text. + Refresh, + ///Reload everything. Plugins, config, basically exit and restart. + Reload +} + +pub enum Message { + Internal(InternalMessage), + External{ + text :Result<String,plugin::PluginError>, + element_number : usize + }, + ThreadCrash{ + element_number : usize + } +} + +/// Sender we give to our plugins. The same message type is used by our message handler internally, +/// but the plugins needn't know. That's why we hide it behind a trait object. Also, vtables rock +/// for going across dynlib boundaries, because that way we can make sure we're actually calling +/// our main-application's symbols, not those of the plugins. Yes, that's an issue. Initially we +/// sent crossbeam channels to plugins directly, without any trait object wrapping them. That +/// didn't work well, because crossbeam uses thread-local storage, which was a different object in +/// the main program and the plugins, as both linked statically against crossbeam... +pub struct SenderToMain { + pub sender : Sender<Message>, + pub element_number : usize, +} + +impl Drop for SenderToMain { + fn drop(&mut self) { + if std::thread::panicking() { + let message = Message::ThreadCrash { element_number : self.element_number}; + if let Err(_e) = self.sender.send(message) { + eprintln!("{}", super::gettext!("I, element {}, tried to inform the main thread that I crashed. However the main thread isn't listening any more. This should be impossible, but well... Also, it's not critical enough to halt the whole program...", self.element_number)); + } + } + } +} + +impl plugin::MsgModuleToMain for SenderToMain { + fn send_update(&self, text : Result<String, plugin::PluginError>) -> Result<(),()> { + let message = Message::External { text , element_number : self.element_number }; + self.sender.send(message).map_err(|_| {}) + } +} diff --git a/swaystatus/src/config/mod.rs b/swaystatus/src/config/mod.rs new file mode 100644 index 0000000..ad1d471 --- /dev/null +++ b/swaystatus/src/config/mod.rs @@ -0,0 +1,276 @@ +use std::fmt; +use serde::{Serialize, Deserialize, Deserializer}; +use serde::de::{self, Visitor, DeserializeSeed, MapAccess, SeqAccess, Error}; +use super::plugin_database::PluginDatabase; +use super::plugin; + +#[cfg(test)] +mod tests; + +/** + * Struct that holds configuration options specific to the main program. + * This is where new config options should go, because it inherits Serialize/Deserialize. + */ +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields, default)] +pub struct SwaystatusMainConfig { + pub separator : String +} +/** + * Helper struct for global configuration. Holds a list of element configurations. + * This is what goes into the config file or is read from it. Needs a manual deserialize + * implementation, to allow routing a seed through. + */ +#[derive(Serialize)] +#[serde(deny_unknown_fields)] +pub struct SwaystatusConfig<'p> { + ///Settings for the main part of the program. + #[serde(rename = "Settings")] + pub settings : Option<SwaystatusMainConfig>, + ///Settings for each part of the output sting. + #[serde(rename = "Elements")] + pub elements : Option<Vec<SwaystatusPluginConfig<'p>>>, +} + +/** + * Helper struct with custom deserializer. Holds config for a single element. + * This is its own struct to make serialization/deserialization easier to maintain. + */ +#[derive(Serialize)] +#[serde(deny_unknown_fields)] +pub struct SwaystatusPluginConfig<'p> { + #[serde(rename = "Plugin")] + plugin : String, + #[serde(rename = "Config")] + config : Box<dyn plugin::SwayStatusModuleInstance + 'p> +} + +impl<'p> SwaystatusConfig<'p> { + fn serialize(&self) -> Result<String, toml::ser::Error> { + toml::to_string(self) + } + fn deserialize(serialized : &str, plugins : &'p PluginDatabase) -> Result<SwaystatusConfig<'p>, toml::de::Error> { + let seed = SwaystatusConfigDeserializeSeed(plugins); + let mut deserializer = toml::Deserializer::new(serialized); + seed.deserialize(&mut deserializer) + } + + fn create_default(plugins : &'p PluginDatabase) -> SwaystatusConfig<'p> { + SwaystatusConfig { + settings : Some(SwaystatusMainConfig::default()), + elements : { + let v : Vec<SwaystatusPluginConfig> = + plugins.get_name_and_plugin_iterator().map(|(name, object)| { + SwaystatusPluginConfig{ + plugin : name.clone(), + config : object.get_default_config() + } + }).collect(); + if v.is_empty() { None } else { Some(v) } + } + } + } + + pub fn print_sample_config(plugins : &PluginDatabase) { + let output = SwaystatusConfig::create_default(plugins).serialize().unwrap(); + print!("{}", output); + } + pub fn read_config<'d>(path : &'d std::path::Path, plugins : &'p PluginDatabase) -> Result<SwaystatusConfig<'p>,SwaystatusConfigErrors> { + let config_file = match std::fs::read_to_string(path) { + Ok(x) => x, + Err (_) => return Err(SwaystatusConfigErrors::FileNotFound) + }; + let result = match SwaystatusConfig::deserialize(&config_file, plugins) { + Ok(x) => x, + Err(e) => return Err(SwaystatusConfigErrors::ParsingError{message: e.to_string()}) + }; + + Ok(result) + } +} + +impl<'p> SwaystatusPluginConfig<'p> { + pub fn get_instance(&'p self) -> &(dyn plugin::SwayStatusModuleInstance + 'p) { + &*self.config + } + pub fn get_name(&'p self) -> &'p str { + &self.plugin + } +} + +pub enum SwaystatusConfigErrors +{ + FileNotFound, + ParsingError { + message : String + } +} + +impl Default for SwaystatusMainConfig { + fn default() -> Self { + SwaystatusMainConfig { separator : String::from(", ")} + } +} + +//----------------------------------------------------------------------------- +//Private stuff below, implementation details for serialization. + + +#[derive(Deserialize)] +#[serde(field_identifier)] +#[allow(non_camel_case_types)] +enum SwaystatusConfigField { Settings, Elements, settings, elements } +struct SwaystatusConfigVisitor<'a>(&'a PluginDatabase<'a>) ; +impl<'de, 'a> Visitor<'de> for SwaystatusConfigVisitor<'a> { + type Value = SwaystatusConfig<'a>; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct SwaystatusConfig") + } + fn visit_map<V>(self, mut map: V) -> Result<SwaystatusConfig<'a>, V::Error> + where V: MapAccess<'de>, { + let mut sett = None; + let mut elem = None; + while let Some(key) = map.next_key()? { + match key { + SwaystatusConfigField::Settings | SwaystatusConfigField::settings => { + if sett.is_some() { + return Err(de::Error::duplicate_field("Settings")); + } + sett = Some(map.next_value()?); + } + SwaystatusConfigField::Elements | SwaystatusConfigField::elements => { + if elem.is_some() { + return Err(de::Error::duplicate_field("Elements")); + } + elem = map.next_value_seed(ElementsOptionDeserialize(self.0))?; + } + } + } + Ok(SwaystatusConfig { + settings : sett, + elements : elem + }) + } +} +struct SwaystatusConfigDeserializeSeed<'a>(&'a PluginDatabase<'a>); +impl<'de, 'a> DeserializeSeed<'de> for SwaystatusConfigDeserializeSeed<'a> { + type Value = SwaystatusConfig<'a>; + fn deserialize<D>(self, deserializer : D) -> Result<Self::Value, D::Error> + where D: Deserializer<'de> { + const FIELDS: &[&str] = &["settings", "elements"]; + deserializer.deserialize_struct("SwaystatusConfig", FIELDS, SwaystatusConfigVisitor(self.0)) + } +} + +struct ElementsOptionVisitor<'a>(&'a PluginDatabase<'a>); +impl<'de, 'a> Visitor<'de> for ElementsOptionVisitor<'a> { + type Value = Option<Vec<SwaystatusPluginConfig<'a>>>; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Option<Vec<struct SwaystatusPluginConfig>>") + } + fn visit_none<E>(self) -> Result<Self::Value, E> + where E: Error, { + Ok(None) + } + fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error> + where D: Deserializer<'de>, { + Ok(Some(deserializer.deserialize_seq(ElementsVisitor(self.0))?)) + } +} + +struct ElementsOptionDeserialize<'a>(&'a PluginDatabase<'a>); +impl<'de, 'a> DeserializeSeed<'de> for ElementsOptionDeserialize<'a> { + type Value = Option<Vec<SwaystatusPluginConfig<'a>>>; + fn deserialize<D>(self, deserializer : D) -> Result<Self::Value, D::Error> + where D: Deserializer<'de> { + deserializer.deserialize_option(ElementsOptionVisitor(self.0)) + } +} + +struct ElementsVisitor<'a>(&'a PluginDatabase<'a>); +impl<'de, 'a> Visitor<'de> for ElementsVisitor<'a> { + type Value = Vec<SwaystatusPluginConfig<'a>>; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Vec<struct SwaystatusPluginConfig>") + } + + fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> + where A: SeqAccess<'de>, { + let mut res : Self::Value = Vec::new(); + while let Some(next_elem) = seq.next_element_seed(SwaystatusPluginConfigSeed(self.0))? { + res.push(next_elem); + } + Ok(res) + } +} + +/** + * Visitor for config deserialization. Forwards the deserialization request to the + * respective plugin. + */ +struct PluginConfigDeserializeSeed<'a, 'b>(&'b PluginDatabase<'b>, &'a String); +impl<'de, 'a, 'b> DeserializeSeed<'de> for PluginConfigDeserializeSeed<'a, 'b> { + type Value = Box<dyn plugin::SwayStatusModuleInstance + 'b>; + fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error> + where D: Deserializer<'de> { + let optplugin = &self.0.get_plugin(self.1); + let plugin = match &optplugin { + Some(x) => x, + None => return Err(de::Error::custom("Plugin not found")) + }; + let mut erased_deserializer = erased_serde::Deserializer::erase(deserializer); + plugin.deserialize_config(&mut erased_deserializer).map_err(Error::custom) + } +} +#[derive(Deserialize)] +#[serde(field_identifier)] +#[allow(non_camel_case_types)] +enum PluginConfigField { Plugin, Config, plugin, config } +struct PluginConfigVisitor<'a>(&'a PluginDatabase<'a>); +impl<'de, 'a> Visitor<'de> for PluginConfigVisitor<'a> { + type Value = SwaystatusPluginConfig<'a>; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct SwaystatusPluginConfig") + } + + fn visit_map<V>(self, mut map: V) -> Result<SwaystatusPluginConfig<'a>, V::Error> + where V: MapAccess<'de>, { + let mut plug = None; + let mut conf = None; + while let Some(key) = map.next_key()? { + match key { + PluginConfigField::Plugin | PluginConfigField::plugin => { + if plug.is_some() { + return Err(de::Error::duplicate_field("Plugin")); + } + plug = Some(map.next_value()?); + } + PluginConfigField::Config | PluginConfigField::config => { + if conf.is_some(){ + return Err(de::Error::duplicate_field("Config")); + } + if let Some(plugin) = plug.as_ref() { + conf = Some(map.next_value_seed(PluginConfigDeserializeSeed(self.0, plugin))?); + } + else { + return Err(de::Error::missing_field("Plugin")); + } + } + } + } + Ok(SwaystatusPluginConfig{ + plugin: plug.ok_or_else(|| de::Error::missing_field("Plugin"))?, + config : conf.ok_or_else(|| de::Error::missing_field("Config"))? + }) + } +} +struct SwaystatusPluginConfigSeed<'a>(&'a PluginDatabase<'a>); +impl<'de, 'a> DeserializeSeed<'de> for SwaystatusPluginConfigSeed<'a> { + type Value = SwaystatusPluginConfig<'a>; + fn deserialize<D>(self, deserializer: D) -> Result<SwaystatusPluginConfig<'a>, D::Error> + where D: Deserializer<'de> { + const FIELDS: &[&str] = &["plugin", "config"]; + deserializer.deserialize_struct("SwaystatusPluginConfig", FIELDS, PluginConfigVisitor(self.0)) + } +} + diff --git a/swaystatus/src/config/tests.rs b/swaystatus/src/config/tests.rs new file mode 100644 index 0000000..1d3782d --- /dev/null +++ b/swaystatus/src/config/tests.rs @@ -0,0 +1,130 @@ +use super::*; +use crate::plugin::*; +use crate::test_plugin::TestPlugin; + +use crate::plugin_database::test_helper::*; + +//to test the internals of the custom deserialize implementation, we need to check how it +//interacts with the deserializer and the map access. To the mock-mobile! +//However, mockall can't be used to mock a Deserializer, becauseit doesn't support lifetimes +//on return types. That's why we mock by hand :-( +#[derive(Debug)] +struct MockError(String); +impl std::fmt::Display for MockError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} +impl std::error::Error for MockError {} +impl serde::de::Error for MockError { + fn custom<T: fmt::Display>(msg: T) -> MockError { + MockError(msg.to_string()) + } +} + +/** + * Deserializer that only exists to check if ConfigVisitor calls the correct function. + * Returns Err(MockError(String::from("Correct"))); on _success_, asserts on failure. + * The reason for not returning Ok is simple: supporting it would bloat this mock beyond + * reason, as we would need to supply data to the erased_serde visitor... + */ +struct MockDeserializerForPluginConfigDeserializeSeed; +impl<'de> serde::Deserializer<'de> for MockDeserializerForPluginConfigDeserializeSeed { + type Error = MockError; + fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, MockError> + where V: de::Visitor<'de>, { + assert!(false); + return Err(MockError(String::from("Unexpected Type"))); + } + fn deserialize_struct<V>(self, name: &'static str, fields: &'static [&'static str], _visitor: V) -> Result<V::Value, MockError> + where V: de::Visitor<'de>, { + //The line below would be awesome, but Visitor doesn't need static lifetime. + //Instead we now just assert on the struct and field names. + //assert_eq!(std::any::TypeId::of::<test_plugin::TestConfig>(), std::any::TypeId::of::<V::Value>()); + assert_eq!(name, "TestConfig"); + assert_eq!(fields[0], "lines"); + assert_eq!(fields[1], "skull"); + //without actual data to deserialize, it's hard to fake an OK... + //For testing we just return an error here. + return Err(MockError(String::from("Correct"))); + } + serde::forward_to_deserialize_any! { + enum bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq + bytes byte_buf map unit newtype_struct + ignored_any unit_struct tuple_struct tuple option identifier + } +} + +#[test] +fn plugin_config_deserialize_seed_calls_correct_plugin() { + let p = get_plugin_database_with_test_plugin(); + + let plugin_name = String::from(TestPlugin.get_name()); + let v = super::PluginConfigDeserializeSeed(&p, &plugin_name); + let result = v.deserialize(MockDeserializerForPluginConfigDeserializeSeed); + assert!(result.is_err()); + assert_eq!(result.err().unwrap().0, "Correct"); +} + +#[test] +fn plugin_config_deserialize_seed_correct_plugin_not_found_error() +{ + let p = get_plugin_database_empty(); + let plugin_name = String::from(TestPlugin.get_name()); + let v = super::PluginConfigDeserializeSeed(&p, &plugin_name); + let result = v.deserialize(MockDeserializerForPluginConfigDeserializeSeed); + assert!(result.is_err()); + assert_eq!(result.err().unwrap().0, "Plugin not found"); +} + +#[test] +fn custom_deserialize_optional_field_settings() +{ + let p = get_plugin_database_with_test_plugin(); + let test_config = String::from( + "[[Elements]]\nPlugin = \"TestPlugin\"\n\n[Elements.Config]\nlines = 2\nskull = \"skully\"\n"); + let deserialized = SwaystatusConfig::deserialize(&test_config, &p).unwrap(); + let serialized = toml::to_string(&deserialized).unwrap(); + //println!("{}", serialized); + assert_eq!(test_config, serialized); +} + +#[test] +fn custom_deserialize_optional_field_elements() +{ + let p = get_plugin_database_with_test_plugin(); + let test_config = String::from( + "[Settings]\nseparator = \"Kisses!\"\n" + ); + let deserialized = SwaystatusConfig::deserialize(&test_config, &p).unwrap(); + let serialized = toml::to_string(&deserialized).unwrap(); + //println!("{}", serialized); + assert_eq!(test_config, serialized); +} + + +#[test] +fn custom_deserialize_multiple_plugins() +{ + let p = get_plugin_database_with_test_plugin(); + let test_config = String::from( + "[[Elements]]\nPlugin = \"TestPlugin\"\n\n[Elements.Config]\nlines = 2\nskull = \"bones\"\n\n[[Elements]]\nPlugin = \"TestPlugin\"\n\n[Elements.Config]\nlines = 5\nskull = \"pirate\"\n"); + let deserialized = SwaystatusConfig::deserialize(&test_config, &p).unwrap(); + let serialized = toml::to_string(&deserialized).unwrap(); + //println!("{}", serialized); + assert_eq!(test_config, serialized); +} + +//this is strictly speaking not a unit test, and more a test of how the custom deserialization +//integrates with serde. But it's trivial to do, and tests an important aspect of the code. +#[test] +fn self_consistency(){ + let p = get_plugin_database_with_test_plugin(); + let def = SwaystatusConfig::create_default(&p); + let serialized = def.serialize().unwrap(); + let deserialized = SwaystatusConfig::deserialize(&serialized, &p).unwrap(); + let serialized2 = toml::to_string(&deserialized).unwrap(); + //println!("{}", serialized2); + assert_eq!(serialized, serialized2); +} + diff --git a/swaystatus/src/main.rs b/swaystatus/src/main.rs new file mode 100644 index 0000000..dc9213c --- /dev/null +++ b/swaystatus/src/main.rs @@ -0,0 +1,163 @@ +pub use swaystatus_plugin as plugin; +mod config; +mod plugin_database; +mod communication; +mod signalhandler; + +extern crate gettextrs; +use gettextrs::*; +use crossbeam_utils::thread; +use std::sync::mpsc; + +#[cfg(test)] +pub mod test_plugin; + +fn main() { + if let Err(_e) = TextDomain::new("swaystatus").prepend("target").init() { + eprintln!("Localization could not be loaded. Will use English instead."); + } + //TODO: Read those from command line + let plugin_path = std::path::Path::new("/home/andi/Dokumente/Rust-Projects/swaystatus/target/debug/"); + let config_path = std::path::Path::new("/home/andi/Dokumente/Rust-Projects/swaystatus/testconfig"); + while !core_loop(plugin_path, config_path) {} +} + +/// Actually the main() function. Factored out so we can restart without actually restaring. +/// Because some people might expect that SIGHUP triggers a reload, and it's trivial to implement. +fn core_loop(plugin_path : &std::path::Path, config_path : &std::path::Path) -> bool { + //Read plugins first (needed for config deserialization, given the config files has + //plugin config as well... + let libraries = plugin_database::Libraries::load_from_folder(plugin_path).unwrap(); + let plugins = plugin_database::PluginDatabase::new(&libraries); + + //TODO: Remove once command line handling is implemented. + //config::SwaystatusConfig::print_sample_config(&plugins); + + + let (elements, main_config) = match config::SwaystatusConfig::read_config(config_path, &plugins) { + Ok(x) => (x.elements.unwrap_or_default(), x.settings.unwrap_or_default()), + Err(e) => { print_config_error(e); return true;} + }; + + if elements.is_empty() { + eprintln!("{}", gettext("No elements set up in configuration. Nothing to display.")); + return true; + } + + let (sender_from_plugins, receiver_from_plugins) = mpsc::channel(); + + let (runnables, senders_to_plugins) : (Vec<_>, Vec<_>) = elements.iter().enumerate().map(|(i,x)| { + let s = communication::SenderToMain { + sender : sender_from_plugins.clone(), + element_number : i, + }; + x.get_instance().make_runnable(Box::new(s)) + }).unzip(); + + //mutable array into which we store our updated texts. + let mut texts = Vec::with_capacity(elements.len()); + texts.resize(elements.len(),String::new()); + assert_eq!(texts.len(), runnables.len()); + assert_eq!(texts.len(), senders_to_plugins.len()); + assert_eq!(elements.len(), runnables.len()); + + let mut should_restart = false; + + // Main everything is ready for the big main loop. Let's spawn the threads! + if let Err(_e) = thread::scope(|s| { + signalhandler::handle_signals(s, sender_from_plugins); + for runnable in runnables { + s.spawn(move |_| { + runnable.run(); + }); + } + + while let Ok(msg) = receiver_from_plugins.recv() { + match msg { + communication::Message::Internal(i) => { + if let communication::InternalMessage::Reload = i { + should_restart = true; + } + forward_to_all_plugins(&senders_to_plugins,&elements, i); + }, + communication::Message::External{text, element_number} => { + handle_message_from_element(&mut texts, &elements[element_number].get_name(), element_number, text); + print_texts(&texts, &main_config); + }, + communication::Message::ThreadCrash{element_number} => { + handle_crash_from_element(&mut texts, &elements[element_number].get_name(), element_number); + print_texts(&texts, &main_config); + } + } + } + + + }) { + //unwinding across plugin boundaries is a _bad_ idea. Unless we want our core dumped, that + //is. The documentation only mentions that unwinding across C functions doesn't work, but + //that seems to also be true for dynamically loaded Rust libs... That's why we can only + //print a general error here. + eprintln!("{}", gettext("At least one of the plugins panicked. For details please check the (hopefully existing) previous error messages.")); + } + return !should_restart; +} + +//----------------------------------------------------------------------------- +//Helpers + +fn print_config_error(e : config::SwaystatusConfigErrors) { + match e { + config::SwaystatusConfigErrors::FileNotFound => { + eprintln!("{}", gettext("The configuration file could not be read. Nothing to do.")); + }, + config::SwaystatusConfigErrors::ParsingError {message} => { + eprintln!("{}", gettext!("The parser for the config file returned an error: {}", message)); + } + } +} + +fn forward_to_all_plugins<'p>(senders : &[Box<dyn plugin::MsgMainToModule + 'p>], elements : &[config::SwaystatusPluginConfig], message : communication::InternalMessage) { + match message { + communication::InternalMessage::Quit | communication::InternalMessage::Reload => { + for (i, sender) in senders.iter().enumerate() { + if sender.send_quit().is_err() { + eprintln!("{}", gettext!("Tried to tell a plugin to quit, but that plugin seems to no longer listen to messages. Either that plugin has already terminated, or it's stuck. In the latter case a clean exit is impossible, you'll need to kill this process. The offending element is element number {} from plugin {}.", i, elements[i].get_name())); + } + } + }, + communication::InternalMessage::Refresh => { + for (i,sender) in senders.iter().enumerate() { + if sender.send_refresh().is_err() { + eprintln!("{}", gettext!("Tried to tell a plugin to refresh, but it doesn't listen any more. Either the plugin already terminated, or it is stuck. The offending element is element number {} from plugin {}.", i, elements[i].get_name())); + } + } + } + } +} + +fn handle_message_from_element(texts : &mut Vec<String>, plugin : &str, element_number : usize, message : Result<String, plugin::PluginError>) { + match message { + Ok(t) => texts[element_number] = t, + Err(e) => match e { + plugin::PluginError::PrintToStdErr(t) => eprintln!("{}", gettext!("Element number {} (plugin: {}) sent an error message: {}",element_number, plugin,t)), + plugin::PluginError::ShowInsteadOfText(t) => { + eprintln!("{}", gettext!("Element number {} (plugin: {}) sent an error message: {}",element_number, plugin,t)); + texts[element_number] = t; + } + } + } +} + +fn print_texts(texts : &[String], settings : &config::SwaystatusMainConfig) { + //Once we do more than just printing, we might want a more advanced code here... + let separators = std::iter::once("").chain(std::iter::repeat(&settings.separator[..])); + for (separator, text) in texts.iter().zip(separators) { + print!("{}{}",separator,text); + } + println!(); //Previosly there was an explicit flush here, but printnl should do that for us. +} + +fn handle_crash_from_element(texts : &mut Vec<String>, name : &str, element_number : usize) { + texts[element_number] = gettext("<plugin crashed>"); + eprintln!("{}", gettext!("The plugin {} crashed while displaying element number {}. Please see the plugin's panic message above for details.",name, element_number)); +} diff --git a/swaystatus/src/plugin_database/mod.rs b/swaystatus/src/plugin_database/mod.rs new file mode 100644 index 0000000..a4693ed --- /dev/null +++ b/swaystatus/src/plugin_database/mod.rs @@ -0,0 +1,104 @@ +use std::collections::HashMap; +use super::plugin; +use libloading::{Library}; +use gettextrs::*; + +pub struct PluginDatabase<'p> { + plugins : HashMap<String, Box<dyn plugin::SwayStatusModule + 'p>> +} + +impl<'a> PluginDatabase<'a> { + pub fn get_plugin<'b>(&'a self, name : &'b str) -> Option<&(dyn plugin::SwayStatusModule + 'a)> { + self.plugins.get(name).map(|x| &**x) + } + pub fn get_name_and_plugin_iterator(&'a self) -> impl Iterator<Item = (&'a String,&'a Box<dyn plugin::SwayStatusModule +'a>)> + 'a { + self.plugins.iter() + } + pub fn new<'b : 'a>(libs : &'b Libraries) -> PluginDatabase<'a> { + PluginDatabase { + plugins : libs.libs.iter().filter_map(|lib| { + match get_plugin_from_library(lib) { + Ok(x) => Some((String::from(x.get_name()), x)), + Err(y) => { + let lib_name = format!("{:?}", lib); + match y { + PluginLoadingError::MissingVersionInformation => { + eprintln!("{}", gettext!("Failed to load library {}, no version information found.", lib_name)); + }, + PluginLoadingError::WrongPluginVersion { expected, version } => { + eprintln!("{}", gettext!("Failed to load library {}, it was built with the wrong plugin version. Expected version {}, found version {}", lib_name, expected, version)); + } + PluginLoadingError::WrongRustcVersion { expected, version } => { + eprintln!("{}", gettext!("Failed to load library {}, it was built with a different Rust version. Since there is no ABI stability guaranteed, this safeguard is required. Please make sure this program and all plugins use the same compiler version. Expected the Rust version {}, found version {}", lib_name, expected, version)); + } + PluginLoadingError::NoConstructor => { + + eprintln!("{}", gettext!("Failed to load library {}, it does not export the _swaystatus_module_create() function.", lib_name)); + } + } + None + } + } + + }).collect() + } + } +} + +pub struct Libraries { + libs : Vec<Library> +} +impl Libraries { + pub fn load_from_folder(path : &std::path::Path) -> Result<Libraries, std::io::Error> { + Ok(Libraries { + libs : path.read_dir()?.filter_map(|f| { + match f { + Err(e) => { + eprintln!("{}", gettext!("File I/O error while iterating libraries: {}", e.to_string())); + None + }, + Ok(d) => unsafe { + match libloading::Library::new(d.path()) { + Ok(x) => Some(x), + Err(_) => { + eprintln!("{}", gettext!("Failed to load as library: {}", d.path().display())); + None + } + } + } + } + }).collect() + }) + } +} +enum PluginLoadingError { + MissingVersionInformation, + WrongPluginVersion { expected: &'static str, version : String }, + WrongRustcVersion { expected: &'static str, version : String }, + NoConstructor +} + +fn get_plugin_from_library<'p>(lib : &'p Library) -> Result<Box<dyn plugin::SwayStatusModule + 'p>,PluginLoadingError> { + unsafe { + let version_getter = lib.get::<unsafe extern fn() -> *const str>(b"_swaystatus_module_version"); + let rustc_version_getter = lib.get::<unsafe extern fn() -> *const str>(b"_swaystatus_rustc_version"); + if version_getter.is_err() || rustc_version_getter.is_err() { + return Err(PluginLoadingError::MissingVersionInformation); + } + let found_version = &*(version_getter.unwrap())(); + let found_rustc_version = &*(rustc_version_getter.unwrap())(); + if found_version != swaystatus_plugin::MODULE_VERSION { + return Err(PluginLoadingError::WrongPluginVersion { expected: swaystatus_plugin::MODULE_VERSION, version : String::from(found_version) }); + } + if found_rustc_version != swaystatus_plugin::RUSTC_VERSION { + return Err(PluginLoadingError::WrongRustcVersion { expected : swaystatus_plugin::RUSTC_VERSION, version : String::from(found_rustc_version)}); + } + if let Ok(constructor) = lib.get::<unsafe extern fn() ->*mut dyn swaystatus_plugin::SwayStatusModule>(b"_swaystatus_module_create") { + return Ok(Box::from_raw(constructor())); + } + Err(PluginLoadingError::NoConstructor) + } +} + +#[cfg(test)] +pub mod test_helper; diff --git a/swaystatus/src/plugin_database/test_helper.rs b/swaystatus/src/plugin_database/test_helper.rs new file mode 100644 index 0000000..8b41cdd --- /dev/null +++ b/swaystatus/src/plugin_database/test_helper.rs @@ -0,0 +1,16 @@ +use std::collections::HashMap; +use crate::test_plugin::TestPlugin; +use swaystatus_plugin::SwayStatusModule; +use super::PluginDatabase; + +pub fn get_plugin_database_with_test_plugin() -> PluginDatabase<'static> { + PluginDatabase { plugins : { + let mut m = HashMap::new(); + m.insert(String::from(TestPlugin.get_name()), + Box::new(TestPlugin) as Box<dyn SwayStatusModule>); + m + }} +} +pub fn get_plugin_database_empty() -> PluginDatabase<'static> { + PluginDatabase { plugins : HashMap::new() } +} diff --git a/swaystatus/src/signalhandler/mod.rs b/swaystatus/src/signalhandler/mod.rs new file mode 100644 index 0000000..0f8696c --- /dev/null +++ b/swaystatus/src/signalhandler/mod.rs @@ -0,0 +1,35 @@ +use crate::communication; +use std::sync::mpsc; +use signal_hook::iterator::Signals; +use signal_hook::consts::*; +use crossbeam_utils::thread::Scope; + +/// This function starts an endless loop, waiting for signals. The only ones that we explicitly +/// handle are USR1 (immediate update), SIGPIPE (because that indicates nobody is listening to us +/// any more), SIGHUP to trigger a reload, and the usual term signals. +pub fn handle_signals(scope : &Scope, sender : mpsc::Sender<communication::Message>) { + //we mustn't forget that upon any terminating signals (including PIPE) and HUP we need to exit. + let mut signals = Signals::new(&[ + signal::SIGTERM, //quit + signal::SIGINT, //quit as well + //signal::SIGQUIT, //we don't do anything special here. Users _expect_ QUIT to make a dump. + signal::SIGPIPE, //quit, because nobody's listening + signal::SIGHUP, //quit, but send the Reload message instead of the Quit one. + signal::SIGUSR1, //trigger a refresh. The ONLY one upon which we dont break the loop! + ]).expect(&gettextrs::gettext("Failed to register signal handler. Since without signal handler there's no proper way to cleanly exit any plugins, we bail now.")); + + scope.spawn(move |_| { + for signal in &mut signals { + match signal { + signal::SIGUSR1 => send(&sender, communication::InternalMessage::Refresh), + signal::SIGHUP => { send(&sender, communication::InternalMessage::Reload); break; } + _=> { send(&sender, communication::InternalMessage::Quit); break;} + + } + } + }); +} + +fn send(sender : &mpsc::Sender<communication::Message>, message : communication::InternalMessage) { + sender.send(communication::Message::Internal(message)).expect(&gettextrs::gettext("Message handler failed to send a message to main thread. This is supposed to be impossible. In any case it's a critical error.")); +} diff --git a/swaystatus/src/test_plugin.rs b/swaystatus/src/test_plugin.rs new file mode 100644 index 0000000..d8f1270 --- /dev/null +++ b/swaystatus/src/test_plugin.rs @@ -0,0 +1,58 @@ +/** + * Plugin implementation for testing purposes of general plugin handling. + * + * Has a few extra functions that assist in testing. + * Does not actually do anything. + */ + +use serde::{Serialize, Deserialize}; +use crate::plugin::*; + +pub struct TestPlugin; +pub struct TestRunnable; +pub struct DeadEndSend; + +impl MsgMainToModule for DeadEndSend { + fn send_quit(&self) -> Result<(),()> { + Err(()) + } + fn send_refresh(&self) -> Result<(),()> { + Err(()) + } +} + +#[derive(Serialize, Deserialize)] +pub struct TestConfig { + lines : u32, + skull : String, +} + +impl SwayStatusModuleRunnable for TestRunnable { + fn run(&self) { + println!("Running!"); + } +} + +impl SwayStatusModuleInstance for TestConfig { + fn make_runnable<'p>(&'p self, to_main : Box<dyn MsgModuleToMain + 'p>) -> (Box<dyn SwayStatusModuleRunnable + 'p>, Box<dyn MsgMainToModule + 'p>) { + return (Box::new(TestRunnable), Box::new(DeadEndSend)); + } +} + +impl SwayStatusModule for TestPlugin { + fn get_name(&self) -> &str { + return "TestPlugin"; + } + fn deserialize_config<'de>(&self, deserializer : &mut (dyn erased_serde::Deserializer + 'de)) -> Result<Box<dyn SwayStatusModuleInstance>, erased_serde::Error> { + let result : TestConfig = erased_serde::deserialize(deserializer)?; + return Ok(Box::new(result)); + } + fn get_default_config(&self) -> Box<dyn SwayStatusModuleInstance> { + let config = TestConfig{ + lines : 3, + skull : String::from("☠"), + }; + return Box::new(config); + } +} + diff --git a/swaystatus/translations/de.po b/swaystatus/translations/de.po new file mode 100644 index 0000000..0ca2e5b --- /dev/null +++ b/swaystatus/translations/de.po @@ -0,0 +1,59 @@ +# German translations for swaystatus package. +# Copyright (C) 2021 Andreas Grois +# This file is distributed under the same license as the swaystatus package. +# Andreas Grois <andi@grois.info>, 2021. +# +msgid "" +msgstr "" +"Project-Id-Version: swaystatus 0.1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-15 07:42+0000\n" +"PO-Revision-Date: 2021-04-15 09:43+0200\n" +"Last-Translator: Andreas Grois <andi@grois.info>\n" +"Language-Team: German <translation-team-de@lists.sourceforge.net>\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/plugin_database/mod.rs:26 +msgid "Failed to load library {}, no version information found." +msgstr "Bibliothek {} konnte nicht geladen werden, keine Versionsinformation enthalten." + +#: src/plugin_database/mod.rs:29 +msgid "" +"Failed to load library {}, it was built with the wrong plugin version. " +"Expected version {}, found version {}" +msgstr "" +"Bibliothek {} konnte nicht geladen werden, sie wurde mit einer anderen Plugin-Version erstellt. " +"Erwartete Version {}, fand Version {}" + +#: src/plugin_database/mod.rs:32 +msgid "" +"Failed to load library {}, it was built with a different Rust version. Since " +"there is no ABI stability guaranteed, this safeguard is required. Please " +"make sure this program and all plugins use the same compiler version. " +"Expected the Rust version {}, found version {}" +msgstr "" +"Bibliothek {} konnte nicht geladen werden, sie wurde mit einer anderen Rust-Version " +"übersetzt. Diese Sicherheitsmaßnahme ist nögit, weil Rust nicht garantiert, dass die " +"ABI über Versionsgrenzen kompatibel ist. Stellen Sie bitte sicher, dass sowohl das " +"Hauptprogramm, als auch alle Bibliotheken mit dem gleichen Compiler übersetzt werden. " +"Erwartete Version {}, fand Version {}" + +#: src/plugin_database/mod.rs:36 +msgid "" +"Failed to load library {}, it does not export the " +"_swaystatus_module_create() function." +msgstr "" +"Bibliothek {} konnte nicht geladen werden, sie enthält nicht die benötigte " +"_swaystatus_module_create() Funktion." + +#: src/plugin_database/mod.rs:57 +msgid "File I/O error while iterating libraries: {}" +msgstr "Ein-/Ausgabefehler beim Auflisten der Bibliotheken: {}" + +#: src/plugin_database/mod.rs:64 +msgid "Failed to load as library: {}" +msgstr "Eintrag im Plugins-Ordner kann nicht als dynamische Bibliothek geladen werden: {}" diff --git a/swaystatus/translations/messages.po b/swaystatus/translations/messages.po new file mode 100644 index 0000000..7ac1c28 --- /dev/null +++ b/swaystatus/translations/messages.po @@ -0,0 +1,42 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Andreas Grois +# This file is distributed under the same license as the swaystatus package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: swaystatus 0.1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-15 07:42+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/plugin_database/mod.rs:26 +msgid "Failed to load library {}, no version information found." +msgstr "" + +#: src/plugin_database/mod.rs:29 +msgid "Failed to load library {}, it was built with the wrong plugin version. Expected version {}, found version {}" +msgstr "" + +#: src/plugin_database/mod.rs:32 +msgid "Failed to load library {}, it was built with a different Rust version. Since there is no ABI stability guaranteed, this safeguard is required. Please make sure this program and all plugins use the same compiler version. Expected the Rust version {}, found version {}" +msgstr "" + +#: src/plugin_database/mod.rs:36 +msgid "Failed to load library {}, it does not export the _swaystatus_module_create() function." +msgstr "" + +#: src/plugin_database/mod.rs:57 +msgid "File I/O error while iterating libraries: {}" +msgstr "" + +#: src/plugin_database/mod.rs:64 +msgid "Failed to load as library: {}" +msgstr "" diff --git a/testconfig b/testconfig new file mode 100644 index 0000000..06d0716 --- /dev/null +++ b/testconfig @@ -0,0 +1,9 @@ +[Settings] +separator = ", " + +[[Elements]] +Plugin = "ClockPlugin" + +[Elements.Config] +Format = "%R" +RefreshRate = 1.0 |
