├── rust-toolchain.toml ├── src ├── layout_manager │ ├── mod.rs │ ├── config.rs │ ├── window_position.rs │ └── fallback_layout.rs ├── lib.rs ├── cli.rs ├── config.rs ├── ipc.rs ├── main.rs ├── module_loading.rs └── app.rs ├── .gitignore ├── .gitmodules ├── LICENSE ├── .github └── workflows │ └── release.yml ├── Cargo.toml ├── default.scss └── README.md /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85.0" 3 | profile = "default" -------------------------------------------------------------------------------- /src/layout_manager/mod.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | pub mod fallback_layout; 3 | mod window_position; 4 | pub const NAME: &str = "FallbackLayout"; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /target 3 | perf.* 4 | disabled-rust-toolchain.toml 5 | *tmp.md 6 | *tmp.txt 7 | heaptrack.* 8 | *.sh 9 | /*.txt 10 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate manages the dynisland window(s), 2 | //! brings together the modules, 3 | //! handles the configuration file 4 | //! and manages the app lifecycle 5 | //! 6 | 7 | pub mod app; 8 | pub mod cli; 9 | pub mod config; 10 | pub mod ipc; 11 | pub mod layout_manager; 12 | pub mod module_loading; 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dynisland-modules"] 2 | path = dynisland-modules 3 | url = https://github.com/cr3eperall/dynisland-modules 4 | [submodule "dynisland-core"] 5 | path = dynisland-core 6 | url = https://github.com/cr3eperall/dynisland-core 7 | [submodule "dynisland-abi"] 8 | path = dynisland-abi 9 | url = https://github.com/cr3eperall/dynisland-abi 10 | [submodule "dynisland-macro"] 11 | path = dynisland-macro 12 | url = https://github.com/cr3eperall/dynisland-macro 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 cr3eperall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use bincode::{Decode, Encode}; 4 | use clap::{Parser, Subcommand}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Parser, Debug, Serialize, Deserialize)] 8 | #[command(arg_required_else_help(true), version, about, long_about = None)] 9 | pub struct Cli { 10 | #[command(subcommand)] 11 | pub command: SubCommands, 12 | 13 | #[arg(long, short)] 14 | pub config_path: Option, 15 | } 16 | 17 | #[derive(Subcommand, Debug, Serialize, Deserialize, PartialEq, Eq, Encode, Decode)] 18 | pub enum SubCommands { 19 | Daemon { 20 | #[arg(short, long, required = false, default_value_t = false)] 21 | no_daemonize: bool, 22 | }, 23 | Reload, 24 | Inspector, 25 | HealthCheck, 26 | ActivityNotification { 27 | activity_identifier: String, 28 | #[arg(help = "0: Minimal, 1: Compact, 2: Expanded, 3: Overlay")] 29 | mode: u8, 30 | duration: Option, 31 | }, 32 | Kill, 33 | Restart { 34 | #[arg(short, long, required = false, default_value_t = false)] 35 | no_daemonize: bool, 36 | }, 37 | DefaultConfig { 38 | // #[arg(short, long, required = false, default_value_t = false)] 39 | #[arg(skip = false)] 40 | replace_current_config: bool, 41 | }, 42 | ListActivities, 43 | ListLoadedModules, 44 | Module { 45 | module_name: String, 46 | // #[arg(required = true, value_delimiter = ' ', num_args = 1..)] 47 | args: Vec, 48 | }, 49 | Layout { 50 | args: Vec, 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-upload-assets: 14 | permissions: 15 | contents: write 16 | runs-on: ubuntu-latest 17 | env: 18 | DEBIAN_FRONTEND: noninteractive 19 | TZ: Etc/UTC 20 | container: ubuntu:24.10 21 | steps: 22 | - name: Install dependencies 23 | run: apt update -y && apt install -y curl git nodejs libdbus-1-dev pkg-config build-essential libssl-dev libgtk-4-dev libgtk4-layer-shell-dev libmimalloc-dev 24 | - uses: actions/checkout@v4 25 | - name: Checkout submodules 26 | run: | 27 | git config --global --add safe.directory /__w/dynisland/dynisland 28 | git submodule update --init --recursive 29 | - name: Setup rust 30 | uses: dtolnay/rust-toolchain@stable 31 | with: 32 | components: clippy,rustfmt 33 | - name: Load rust cache 34 | uses: Swatinem/rust-cache@v2 35 | 36 | - name: Setup problem matchers 37 | uses: r7kamura/rust-problem-matchers@v1 38 | 39 | - name: Create target directory 40 | run: mkdir -p target 41 | 42 | - name: Build 43 | run: cargo build --release --target-dir ./target --features embed_modules,completions 44 | 45 | - name: Release 46 | uses: softprops/action-gh-release@v2 47 | with: 48 | token: "${{ secrets.GITHUB_TOKEN }}" 49 | prerelease: false 50 | draft: true 51 | files: | 52 | target/release/dynisland 53 | target/_dynisland 54 | target/dynisland.bash 55 | target/dynisland.fish 56 | target/dynisland.elv 57 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "dynisland-abi", 5 | "dynisland-core", 6 | "dynisland-macro", 7 | "dynisland-modules/[!.]*", 8 | ] 9 | exclude = ["dynisland-modules/dynisland-module-template"] 10 | # exclude = ["dynisland-modules/dynisland-module-template", "dynisland-modules/clock-module", "dynisland-modules/example-module", "dynisland-modules/music-module", "dynisland-modules/script-module"] 11 | 12 | [workspace.dependencies] 13 | dynisland-core = { path="./dynisland-core", version="=0.1.3" } 14 | dynisland-macro = { path="./dynisland-macro", version="=0.1.0" } 15 | dynisland-abi = { path="./dynisland-abi", version="=0.1.3" } 16 | 17 | [profile.release] 18 | strip = false 19 | opt-level = "z" 20 | lto=true 21 | 22 | [package] 23 | name = "dynisland" 24 | version = "0.1.4" 25 | authors = ["cr3eperall"] 26 | description = "A Dynamic Island bar" 27 | license = "MIT" 28 | repository = "https://github.com/cr3eperall/dynisland" 29 | edition = "2021" 30 | build = "build.rs" 31 | readme = "README.md" 32 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 33 | 34 | [dependencies] 35 | 36 | dynisland-core = { workspace=true, version="=0.1.3" } 37 | json-strip-comments = "1.0.4" 38 | serde_json = "1.0.127" 39 | gtk = { version = "0.8.2", package = "gtk4", features = ["v4_12"] } 40 | linkme = { version = "0.3.17" } 41 | tokio = { version = "1.46.0", features = ["rt", "time", "sync", "macros", "io-util", "net"] } 42 | anyhow = "1.0.86" 43 | serde = { version = "1.0.188", features = ["serde_derive"] } 44 | env_logger = "0.11.8" 45 | abi_stable = "0.11.3" 46 | grass={version = "0.13.4", default-features = false, features = ["macro"]} 47 | 48 | notify = { version = "8.0.0", features = ["fsevent-sys"] } 49 | # colored = "2.1.0" 50 | clap = { version = "4.5.15", features = ["derive"]} 51 | nix = { version = "0.30.1", features = ["process", "fs"]} 52 | bincode = { version = "2.0.1"} 53 | 54 | dynisland_clock_module = { path="./dynisland-modules/clock-module", version="0.1.1", features = ["embedded"], optional = true} 55 | dynisland_dynamic_layoutmanager ={ path="./dynisland-modules/dynamic-layout", version="0.1.1", features = ["embedded"], optional = true} 56 | dynisland_music_module = { path="./dynisland-modules/music-module", version="0.1.2", features = ["embedded"], optional = true} 57 | dynisland_script_module = { path="./dynisland-modules/script-module", version="0.1.1", features = ["embedded"], optional = true} 58 | dynisland_systray_module = { path="./dynisland-modules/systray-module", version="0.1.0", features = ["embedded"], optional = true} 59 | dynisland_power_module = { path="./dynisland-modules/power-module", version="0.1.0", features = ["embedded"], optional = true} 60 | system-mimalloc = "1.0.1" 61 | 62 | [build-dependencies] 63 | clap = {version = "4.5.15", features = ["derive"] } 64 | clap_complete = "4.5.54" 65 | serde = { version = "1.0.188", features = ["serde_derive"] } 66 | bincode = { version = "2.0.1"} 67 | 68 | [features] 69 | default = ["embed_modules"] 70 | completions = [] 71 | embed_modules = ["dynisland_clock_module", "dynisland_dynamic_layoutmanager", "dynisland_music_module", "dynisland_script_module", "dynisland_systray_module", "dynisland_power_module"] 72 | 73 | -------------------------------------------------------------------------------- /default.scss: -------------------------------------------------------------------------------- 1 | $transition-duration: 600ms; 2 | $border-radius: 50px; 3 | 4 | window { 5 | background-color: transparent; 6 | } 7 | 8 | activity-widget { 9 | margin-left: 2.5px; 10 | margin-right: 2.5px; 11 | 12 | .activity-background, 13 | .activity-background * { 14 | transition-property: min-width, min-height; 15 | transition-duration: $transition-duration, $transition-duration; 16 | transition-timing-function: cubic-bezier(0.2, 0.55, 0.25, 1), 17 | cubic-bezier(0.2, 0.55, 0.25, 1); 18 | } 19 | 20 | .mode-minimal, 21 | .mode-compact, 22 | .mode-expanded, 23 | .mode-overlay { 24 | // border-radius: $border-radius; 25 | transition-property: transform, opacity, filter; 26 | transition-duration: $transition-duration, $transition-duration, 27 | $transition-duration; 28 | transition-timing-function: cubic-bezier(0.2, 0.55, 0.25, 1), 29 | cubic-bezier(0.6, 0.6, 0.2, 0.8), cubic-bezier(0.5, 0.5, 0, 0.7); 30 | } 31 | 32 | background-color: rgb(0, 0, 0); 33 | // border-top: 10px inset rgba(0, 160, 204, 0.5); 34 | // border-right: 6px inset rgba(184, 204, 0, 0.5); 35 | // border-bottom: 10px inset rgba(204, 0, 0, 0.5); 36 | // border-left: 6px inset rgba(204, 0, 177, 0.5); 37 | border: 2px solid rgba(69, 69, 69, 0.69); 38 | 39 | border-radius: $border-radius; 40 | } 41 | 42 | activity-widget.dragging { 43 | 44 | .mode-minimal, 45 | .mode-compact, 46 | .mode-expanded, 47 | .mode-overlay { 48 | transition-property: transform, opacity, filter; 49 | transition-duration: 0ms, 0ms, 0ms; 50 | } 51 | 52 | .activity-background, 53 | .activity-background * { 54 | transition-property: min-width, min-height; 55 | transition-duration: 0ms, 0ms; 56 | transition-timing-function: linear, linear, linear; 57 | } 58 | } 59 | 60 | activity-widget.hidden{ 61 | opacity: 0; 62 | border: none; 63 | } 64 | 65 | //rolling char 66 | @keyframes in { 67 | from { 68 | transform: translateY(15px) scale(0.2); 69 | /* should be translateY(100%) but gtk doesn't support that */ 70 | opacity: 0.5; 71 | } 72 | 73 | 65% { 74 | transform: translateY(-2px) scale(1); 75 | opacity: 1; 76 | } 77 | 78 | 80% { 79 | transform: translateY(-0.7px) scale(1); 80 | opacity: 1; 81 | } 82 | 83 | 93% { 84 | transform: translateY(0.2px) scale(1); 85 | opacity: 1; 86 | } 87 | 88 | 100% { 89 | transform: translateY(0px) scale(1); 90 | opacity: 1; 91 | } 92 | } 93 | 94 | @keyframes out { 95 | 0% { 96 | transform: translateY(0px) scale(1); 97 | opacity: 1; 98 | } 99 | 100 | 100% { 101 | transform: translateY(-15px) scale(0.2); 102 | /* should be translateY(-100%) but gtk doesn't support that */ 103 | opacity: 0.2; 104 | } 105 | } 106 | 107 | rolling-char .in { 108 | animation-name: in; 109 | animation-duration: 450ms; 110 | } 111 | 112 | rolling-char .out { 113 | opacity: 0; 114 | animation-name: out; 115 | animation-duration: 300ms; 116 | } 117 | 118 | scrolling-label { 119 | box{ 120 | margin-right: 10px; 121 | } 122 | 123 | .inner-label { 124 | margin-left: 10px; 125 | margin-right: 30px; 126 | margin-top: 1px; 127 | } 128 | 129 | // box { 130 | // animation-timing-function: ease-in-out; 131 | // } 132 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynisland 2 | 3 | A dynamic and extensible GTK4 bar for compositors implementing wlr-layer-shell, written in Rust. 4 | 5 | Dynisland is designed to look and feel like Apple's Dynamic Island. 6 | 7 | ## Demo 8 | 9 | 10 | 11 | ## Status 12 | 13 | This project is still in early development; There will likely be bugs and breaking changes, including changes to the config format. 14 | 15 | ## Features 16 | 17 | - Easy to configure with a dynamically generated default config 18 | - Animated transitions 19 | - Themeable with hot loaded css 20 | - Extensible with third party Rust modules and layout managers 21 | - multi-monitor support 22 | 23 | ## Usage 24 | 25 | ### Start/restart the daemon 26 | 27 | ```bash 28 | dynisland daemon 29 | # or 30 | dynisland restart 31 | ``` 32 | 33 | ### Open the gtk debugger 34 | 35 | ```bash 36 | dynisland inspector 37 | ``` 38 | 39 | - this can be useful for css theming 40 | 41 | ## Dependencies 42 | 43 | - gtk4 44 | - gtk4-layer-shell 45 | 46 | ## Installation 47 | 48 | ### Using cargo 49 | 50 | ```bash 51 | cargo install dynisland 52 | ``` 53 | 54 | ### Arch Linux 55 | 56 | ```bash 57 | yay -S dynisland-git 58 | ``` 59 | 60 | ## Configuration 61 | 62 | ### Create the directory structure 63 | 64 | ```bash 65 | mkdir -p ~/.config/dynisland/{modules,layouts} 66 | ``` 67 | 68 | ### Generate the default config file 69 | 70 | ```bash 71 | dynisland default-config >> ~/.config/dynisland/dynisland.ron 72 | touch ~/.config/dynisland/dynisland.scss 73 | ``` 74 | 75 | ### See the [Wiki](https://github.com/cr3eperall/dynisland/wiki) for the main config options 76 | 77 | ### See [dynisland-modules](https://github.com/cr3eperall/dynisland-modules) for the module specific configs 78 | 79 | Then edit the configs and scss to your liking. 80 | 81 | ## Building 82 | 83 | ### Building with all the modules included 84 | 85 | ```bash 86 | git clone --recursive https://github.com/cr3eperall/dynisland 87 | cd dynisland 88 | cargo build --release --features completions 89 | cd target/release 90 | install -Dm755 dynisland ~/.local/bin/dynisland 91 | ``` 92 | 93 | ### Install shell completions 94 | 95 | ```bash 96 | install -Dm644 "target/_dynisland" "/usr/share/zsh/site-functions/_dynisland" 97 | 98 | install -Dm644 "target/dynisland.bash" "/usr/share/bash-completion/completions/dynisland.bash" 99 | 100 | install -Dm644 "target/dynisland.fish" "/usr/share/fish/vendor_completions.d/dynisland.fish" 101 | ``` 102 | 103 | --- 104 | 105 | ### Building without including the modules 106 | 107 | ```bash 108 | git clone https://github.com/cr3eperall/dynisland 109 | cd dynisland 110 | cargo build --release --no-default-features --features completions 111 | cd target/release 112 | install dynisland ~/.local/bin/dynisland 113 | ``` 114 | 115 | > [!NOTE] 116 | > When compiled with the `embed_modules` or `default` feature the [official](https://github.com/cr3eperall/dynisland-modules) modules are already included in the binary and this part can be skipped 117 | 118 | #### Download or compile the external modules and put them in the modules directory 119 | 120 | Download the precompiled modules from the [Release page](https://github.com/cr3eperall/dynisland-modules/releases/latest) 121 | 122 | ```bash 123 | mv Download/libmusic_module.so Download/libscript_module.so Download/libclock_module.so ~/.config/dynisland/modules 124 | mv Download/libdynamic_layoutmanager.so ~/.config/dynisland/layouts 125 | ``` 126 | 127 | Or build the modules from source 128 | 129 | ```bash 130 | git clone --recursive https://github.com/cr3eperall/dynisland 131 | cargo build --release --no-default-features --package dynisland_clock_module --package dynisland_dynamic_layoutmanager --package dynisland_music_module --package dynisland_script_module 132 | mv target/release/libmusic_module.so target/release/libscript_module.so target/release/libclock_module.so ~/.config/dynisland/modules 133 | mv target/release/libdynamic_layoutmanager.so ~/.config/dynisland/layouts 134 | ``` 135 | 136 | ## Acknowledgements 137 | 138 | - [eww](https://github.com/elkowar/eww) - For reference on how to do IPC, custom gtk widgets and some of the systray code 139 | - [Nullderef](https://nullderef.com/) - For a deep dive on how to implement a plugin system in rust 140 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt::Display, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use dynisland_core::{ 8 | abi::{glib, log}, 9 | ron, 10 | }; 11 | use ron::{extensions::Extensions, ser::PrettyConfig, Value}; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | pub const CONFIG_REL_PATH: &str = "dynisland/"; 15 | 16 | // ron sucks, ~~i need to switch to pkl~~ 17 | // nvm, there are no good pkl crates 18 | 19 | #[derive(Debug, Serialize, Deserialize, Clone)] 20 | #[serde(default)] 21 | pub struct Config { 22 | pub loaded_modules: Vec, 23 | pub layout: Option, 24 | pub general_style_config: GeneralConfig, 25 | pub layout_configs: HashMap, 26 | pub module_config: HashMap, 27 | pub debug: Option, 28 | } 29 | #[derive(Debug, Serialize, Deserialize, Clone)] 30 | #[serde(default)] 31 | pub struct DebugConfig { 32 | pub runtime_path: String, 33 | pub open_debugger_at_start: bool, 34 | } 35 | 36 | impl Default for DebugConfig { 37 | fn default() -> Self { 38 | Self { 39 | runtime_path: get_default_runtime_path().to_str().unwrap().to_string(), 40 | open_debugger_at_start: false, 41 | } 42 | } 43 | } 44 | 45 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 46 | #[serde(default)] 47 | pub struct GeneralConfig { 48 | pub minimal_height: u32, 49 | pub minimal_width: u32, 50 | pub blur_radius: f64, 51 | pub enable_drag_stretch: bool, 52 | // pub hide_widget_timeout_ms: u32, 53 | } 54 | 55 | impl Default for GeneralConfig { 56 | fn default() -> Self { 57 | Self { 58 | minimal_height: 40, 59 | minimal_width: 60, 60 | blur_radius: 6.0, 61 | enable_drag_stretch: false, // whether to enable stretching widgets by dragging 62 | // hide_widget_timeout_ms: 1000, 63 | //TODO find a way to add scrolling label to settings 64 | } 65 | } 66 | } 67 | 68 | impl Default for Config { 69 | fn default() -> Self { 70 | let module_map = HashMap::::new(); 71 | let layout_map = HashMap::::new(); 72 | Self { 73 | module_config: module_map, 74 | layout_configs: layout_map, 75 | layout: Some("FallbackLayout".to_string()), 76 | general_style_config: GeneralConfig::default(), 77 | loaded_modules: vec!["all".to_string()], 78 | debug: None, 79 | } 80 | } 81 | } 82 | 83 | impl Display for Config { 84 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 85 | let options = ron::Options::default().with_default_extension(Extensions::IMPLICIT_SOME); 86 | let res = options 87 | .to_string_pretty(self, PrettyConfig::default()) 88 | .unwrap_or("unable to parse config".to_string()); 89 | write!(f, "{}", res) 90 | } 91 | } 92 | 93 | impl Config { 94 | pub fn get_runtime_dir(&self) -> PathBuf { 95 | self.debug 96 | .clone() 97 | .map(|debug| PathBuf::from(debug.runtime_path)) 98 | .unwrap_or(get_default_runtime_path()) 99 | } 100 | } 101 | 102 | pub fn get_default_config_path() -> PathBuf { 103 | glib::user_config_dir().join(CONFIG_REL_PATH) 104 | } 105 | fn get_default_runtime_path() -> PathBuf { 106 | glib::user_runtime_dir().join(CONFIG_REL_PATH) 107 | } 108 | 109 | pub fn get_config(config_dir: &Path) -> Config { 110 | let config_path = config_dir.join("dynisland.ron"); 111 | let content = std::fs::read_to_string(config_path); 112 | let options = ron::Options::default().with_default_extension(Extensions::IMPLICIT_SOME); 113 | 114 | let ron: Config = match content { 115 | Ok(content) => options.from_str(&content).unwrap_or_else(|err| { 116 | log::warn!( 117 | "failed to parse config, using default. Err:{}", 118 | err.to_string() 119 | ); 120 | Config::default() 121 | }), 122 | Err(err) => { 123 | log::warn!("failed to parse config file, using default: {err}"); 124 | Config::default() 125 | } 126 | }; 127 | ron 128 | } 129 | -------------------------------------------------------------------------------- /src/layout_manager/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::window_position::{DeWindowPosition, WindowPosition}; 6 | 7 | pub const DEFAULT_AUTO_MINIMIZE_TIMEOUT: i32 = 5000; 8 | 9 | // TODO cleanup 10 | 11 | #[derive(Debug, Serialize, Clone)] 12 | #[serde(default)] 13 | pub struct FallbackLayoutConfigMain { 14 | pub(crate) orientation_horizontal: bool, 15 | pub(crate) window_position: WindowPosition, 16 | pub(crate) auto_minimize_timeout: i32, 17 | pub(crate) windows: HashMap, 18 | } 19 | 20 | impl Default for FallbackLayoutConfigMain { 21 | fn default() -> Self { 22 | let mut map = HashMap::new(); 23 | map.insert("".to_string(), FallbackLayoutConfig::default()); 24 | Self { 25 | orientation_horizontal: true, 26 | window_position: WindowPosition::default(), 27 | auto_minimize_timeout: DEFAULT_AUTO_MINIMIZE_TIMEOUT, 28 | windows: map, 29 | } 30 | } 31 | } 32 | impl FallbackLayoutConfigMain { 33 | pub fn default_conf(&self) -> FallbackLayoutConfig { 34 | FallbackLayoutConfig { 35 | orientation_horizontal: self.orientation_horizontal, 36 | window_position: self.window_position.clone(), 37 | auto_minimize_timeout: self.auto_minimize_timeout, 38 | } 39 | } 40 | pub fn get_for_window(&self, window: &str) -> FallbackLayoutConfig { 41 | match self.windows.get(window) { 42 | Some(conf) => conf.clone(), 43 | None => self.default_conf(), 44 | } 45 | } 46 | } 47 | 48 | #[derive(Debug, Serialize, Clone)] 49 | #[serde(default)] 50 | pub struct FallbackLayoutConfig { 51 | pub(crate) orientation_horizontal: bool, 52 | pub(crate) window_position: WindowPosition, 53 | pub(crate) auto_minimize_timeout: i32, 54 | } 55 | impl Default for FallbackLayoutConfig { 56 | fn default() -> Self { 57 | Self { 58 | orientation_horizontal: true, 59 | window_position: WindowPosition::default(), 60 | auto_minimize_timeout: DEFAULT_AUTO_MINIMIZE_TIMEOUT, 61 | } 62 | } 63 | } 64 | #[derive(Debug, Deserialize, Clone)] 65 | #[serde(default)] 66 | pub struct DeFallbackLayoutConfigMain { 67 | orientation_horizontal: bool, 68 | window_position: WindowPosition, 69 | auto_minimize_timeout: i32, 70 | windows: HashMap, 71 | } 72 | 73 | impl Default for DeFallbackLayoutConfigMain { 74 | fn default() -> Self { 75 | Self { 76 | orientation_horizontal: true, 77 | window_position: WindowPosition::default(), 78 | auto_minimize_timeout: DEFAULT_AUTO_MINIMIZE_TIMEOUT, 79 | windows: HashMap::new(), 80 | } 81 | } 82 | } 83 | impl DeFallbackLayoutConfigMain { 84 | pub fn into_main_config(self) -> FallbackLayoutConfigMain { 85 | let mut windows = HashMap::new(); 86 | for (name, opt_conf) in self.windows { 87 | let window_pos = match opt_conf.window_position { 88 | Some(opt_window_pos) => WindowPosition { 89 | layer: opt_window_pos 90 | .layer 91 | .unwrap_or(self.window_position.layer.clone()), 92 | h_anchor: opt_window_pos 93 | .h_anchor 94 | .unwrap_or(self.window_position.h_anchor.clone()), 95 | v_anchor: opt_window_pos 96 | .v_anchor 97 | .unwrap_or(self.window_position.v_anchor.clone()), 98 | margin_x: opt_window_pos 99 | .margin_x 100 | .unwrap_or(self.window_position.margin_x), 101 | margin_y: opt_window_pos 102 | .margin_y 103 | .unwrap_or(self.window_position.margin_y), 104 | exclusive_zone: opt_window_pos 105 | .exclusive_zone 106 | .unwrap_or(self.window_position.exclusive_zone), 107 | monitor: opt_window_pos 108 | .monitor 109 | .unwrap_or(self.window_position.monitor.clone()), 110 | layer_shell: opt_window_pos 111 | .layer_shell 112 | .unwrap_or(self.window_position.layer_shell), 113 | }, 114 | None => self.window_position.clone(), 115 | }; 116 | let conf = FallbackLayoutConfig { 117 | orientation_horizontal: opt_conf 118 | .orientation_horizontal 119 | .unwrap_or(self.orientation_horizontal), 120 | window_position: window_pos, 121 | auto_minimize_timeout: opt_conf 122 | .auto_minimize_timeout 123 | .unwrap_or(self.auto_minimize_timeout), 124 | }; 125 | 126 | windows.insert(name, conf); 127 | } 128 | let mut main_conf = FallbackLayoutConfigMain { 129 | orientation_horizontal: self.orientation_horizontal, 130 | window_position: self.window_position, 131 | auto_minimize_timeout: self.auto_minimize_timeout, 132 | windows, 133 | }; 134 | if main_conf.windows.is_empty() { 135 | let default = main_conf.default_conf(); 136 | main_conf.windows.insert("".to_string(), default); 137 | } 138 | main_conf 139 | } 140 | } 141 | 142 | #[derive(Debug, Deserialize, Clone, Default)] 143 | #[serde(default)] 144 | pub struct DeFallbackLayoutConfig { 145 | orientation_horizontal: Option, 146 | window_position: Option, 147 | auto_minimize_timeout: Option, 148 | } 149 | -------------------------------------------------------------------------------- /src/layout_manager/window_position.rs: -------------------------------------------------------------------------------- 1 | use dynisland_core::abi::{gdk, gtk, gtk_layer_shell, log}; 2 | use gdk::prelude::*; 3 | use gtk::{prelude::*, Window}; 4 | use gtk_layer_shell::LayerShell; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Serialize, Deserialize, Clone)] 8 | #[serde(tag = "Alignment")] 9 | pub enum Alignment { 10 | #[serde(alias = "start")] 11 | Start, 12 | #[serde(alias = "center")] 13 | Center, 14 | #[serde(alias = "end")] 15 | End, 16 | } 17 | 18 | impl Alignment { 19 | pub fn map_gtk(&self) -> gtk::Align { 20 | match self { 21 | Alignment::Start => gtk::Align::Start, 22 | Alignment::Center => gtk::Align::Center, 23 | Alignment::End => gtk::Align::End, 24 | } 25 | } 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Default, Debug, Clone)] 29 | #[serde(tag = "Layer")] 30 | pub enum Layer { 31 | #[serde(alias = "background")] 32 | Background, 33 | #[serde(alias = "bottom")] 34 | Bottom, 35 | #[default] 36 | #[serde(alias = "top")] 37 | Top, 38 | #[serde(alias = "overlay")] 39 | Overlay, 40 | } 41 | 42 | impl Layer { 43 | pub fn map_gtk(&self) -> gtk_layer_shell::Layer { 44 | match self { 45 | Layer::Background => gtk_layer_shell::Layer::Background, 46 | Layer::Bottom => gtk_layer_shell::Layer::Bottom, 47 | Layer::Top => gtk_layer_shell::Layer::Top, 48 | Layer::Overlay => gtk_layer_shell::Layer::Overlay, 49 | } 50 | } 51 | } 52 | 53 | #[derive(Debug, Serialize, Deserialize, Clone)] 54 | #[serde(default)] 55 | pub struct WindowPosition { 56 | pub(crate) layer: Layer, 57 | pub(crate) h_anchor: Alignment, 58 | pub(crate) v_anchor: Alignment, 59 | pub(crate) margin_x: i32, 60 | pub(crate) margin_y: i32, 61 | pub(crate) exclusive_zone: i32, 62 | pub(crate) monitor: String, 63 | pub(crate) layer_shell: bool, 64 | } 65 | 66 | impl Default for WindowPosition { 67 | fn default() -> Self { 68 | Self { 69 | layer: Layer::Top, 70 | h_anchor: Alignment::Center, 71 | v_anchor: Alignment::Start, 72 | margin_x: 0, 73 | margin_y: 0, 74 | exclusive_zone: -1, 75 | monitor: String::from(""), 76 | layer_shell: true, 77 | } 78 | } 79 | } 80 | 81 | #[derive(Debug, Deserialize, Clone, Default)] 82 | #[serde(default)] 83 | pub struct DeWindowPosition { 84 | pub(crate) layer: Option, 85 | pub(crate) h_anchor: Option, 86 | pub(crate) v_anchor: Option, 87 | pub(crate) margin_x: Option, 88 | pub(crate) margin_y: Option, 89 | pub(crate) exclusive_zone: Option, 90 | pub(crate) monitor: Option, 91 | pub(crate) layer_shell: Option, 92 | } 93 | 94 | impl WindowPosition { 95 | pub fn config_layer_shell_for(&self, window: &Window) { 96 | window.set_layer(self.layer.map_gtk()); 97 | match self.v_anchor { 98 | Alignment::Start => { 99 | window.set_anchor(gtk_layer_shell::Edge::Top, true); 100 | window.set_anchor(gtk_layer_shell::Edge::Bottom, false); 101 | window.set_margin(gtk_layer_shell::Edge::Top, self.margin_y); 102 | } 103 | Alignment::Center => { 104 | window.set_anchor(gtk_layer_shell::Edge::Top, false); 105 | window.set_anchor(gtk_layer_shell::Edge::Bottom, false); 106 | } 107 | Alignment::End => { 108 | window.set_anchor(gtk_layer_shell::Edge::Top, false); 109 | window.set_anchor(gtk_layer_shell::Edge::Bottom, true); 110 | window.set_margin(gtk_layer_shell::Edge::Bottom, self.margin_y); 111 | } 112 | } 113 | match self.h_anchor { 114 | Alignment::Start => { 115 | window.set_anchor(gtk_layer_shell::Edge::Left, true); 116 | window.set_anchor(gtk_layer_shell::Edge::Right, false); 117 | window.set_margin(gtk_layer_shell::Edge::Left, self.margin_x); 118 | } 119 | Alignment::Center => { 120 | window.set_anchor(gtk_layer_shell::Edge::Left, false); 121 | window.set_anchor(gtk_layer_shell::Edge::Right, false); 122 | } 123 | Alignment::End => { 124 | window.set_anchor(gtk_layer_shell::Edge::Left, false); 125 | window.set_anchor(gtk_layer_shell::Edge::Right, true); 126 | window.set_margin(gtk_layer_shell::Edge::Right, self.margin_x); 127 | } 128 | } 129 | let mut monitor = None; 130 | for mon in gdk::Display::default() 131 | .unwrap() 132 | .monitors() 133 | .iter::() 134 | { 135 | let mon = match mon { 136 | Ok(monitor) => monitor, 137 | Err(_) => { 138 | continue; 139 | } 140 | }; 141 | if mon.connector().unwrap().eq_ignore_ascii_case(&self.monitor) { 142 | monitor = Some(mon); 143 | break; 144 | } 145 | } 146 | if let Some(monitor) = monitor { 147 | window.set_monitor(&monitor); 148 | } 149 | window.set_namespace("dynisland"); 150 | window.set_exclusive_zone(self.exclusive_zone); 151 | window.set_resizable(false); 152 | window.queue_resize(); 153 | } 154 | 155 | pub fn init_window(&self, window: &Window) { 156 | if self.layer_shell { 157 | window.init_layer_shell(); 158 | self.config_layer_shell_for(window.upcast_ref()); 159 | window.connect_destroy(|_| log::debug!("LayerShell window was destroyed")); 160 | } else { 161 | window.connect_destroy(|_| std::process::exit(0)); 162 | } 163 | } 164 | pub fn reconfigure_window(&self, window: &Window) { 165 | if self.layer_shell { 166 | if !window.is_layer_window() { 167 | window.init_layer_shell(); 168 | } 169 | self.config_layer_shell_for(window); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/ipc.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | path::Path, 4 | time::Duration, 5 | }; 6 | 7 | use anyhow::{anyhow, Result}; 8 | use dynisland_core::{ 9 | abi::{log, module::ActivityIdentifier}, 10 | graphics::activity_widget::boxed_activity_mode::ActivityMode, 11 | }; 12 | use tokio::{ 13 | io::{AsyncReadExt, AsyncWriteExt}, 14 | net::{UnixListener, UnixStream}, 15 | sync::mpsc::UnboundedSender, 16 | }; 17 | 18 | use crate::{app::BackendServerCommand, cli::SubCommands}; 19 | 20 | pub async fn open_socket( 21 | runtime_path: &Path, 22 | server_send: UnboundedSender, 23 | server_response_recv: &mut tokio::sync::mpsc::UnboundedReceiver>, 24 | ) -> Result<()> { 25 | let _ = std::fs::remove_file(runtime_path.join("dynisland.sock")); 26 | let listener = UnixListener::bind(runtime_path.join("dynisland.sock"))?; 27 | loop { 28 | let (mut stream, _socket) = listener.accept().await?; 29 | let message = read_message(&mut stream).await?; 30 | log::debug!("IPC message received: {message:?}"); 31 | match message { 32 | SubCommands::Reload => { 33 | server_send.send(BackendServerCommand::ReloadConfig)?; 34 | } 35 | SubCommands::Inspector => { 36 | server_send.send(BackendServerCommand::OpenInspector)?; 37 | if let Ok(Some(response)) = 38 | tokio::time::timeout(Duration::from_millis(800), server_response_recv.recv()) 39 | .await 40 | { 41 | let _ = send_response(&mut stream, response).await; 42 | } 43 | } 44 | SubCommands::Kill => { 45 | server_send.send(BackendServerCommand::Stop)?; 46 | if let Ok(Some(response)) = 47 | tokio::time::timeout(Duration::from_millis(800), server_response_recv.recv()) 48 | .await 49 | { 50 | let _ = send_response(&mut stream, response).await; 51 | } 52 | break; 53 | } 54 | SubCommands::HealthCheck => { 55 | log::info!("received HealthCheck, Everything OK"); 56 | let _ = send_response(&mut stream, None).await; 57 | } 58 | SubCommands::ActivityNotification { 59 | activity_identifier, 60 | mode, 61 | duration, 62 | } => { 63 | let components: Vec<&str> = activity_identifier.split('@').collect(); 64 | if components.len() != 2 { 65 | log::error!("invalid activity identifier: {activity_identifier}"); 66 | continue; 67 | } 68 | let id = ActivityIdentifier::new(components[1], components[0]); 69 | let mode = ActivityMode::try_from(mode).map_err(|e| anyhow!(e))?; 70 | server_send.send(BackendServerCommand::ActivityNotification( 71 | id, mode, duration, 72 | ))?; 73 | if let Ok(Some(response)) = 74 | tokio::time::timeout(Duration::from_millis(800), server_response_recv.recv()) 75 | .await 76 | { 77 | let _ = send_response(&mut stream, response).await; 78 | } 79 | } 80 | SubCommands::ListActivities => { 81 | server_send.send(BackendServerCommand::ListActivities)?; 82 | if let Ok(Some(response)) = 83 | tokio::time::timeout(Duration::from_millis(800), server_response_recv.recv()) 84 | .await 85 | { 86 | let _ = send_response(&mut stream, response).await; 87 | } 88 | } 89 | SubCommands::ListLoadedModules => { 90 | server_send.send(BackendServerCommand::ListLoadedModules)?; 91 | if let Ok(Some(response)) = 92 | tokio::time::timeout(Duration::from_millis(800), server_response_recv.recv()) 93 | .await 94 | { 95 | let _ = send_response(&mut stream, response).await; 96 | } 97 | } 98 | SubCommands::Module { module_name, args } => { 99 | server_send.send(BackendServerCommand::ModuleCliCommand( 100 | module_name, 101 | args.join(" "), 102 | ))?; 103 | if let Ok(Some(response)) = 104 | tokio::time::timeout(Duration::from_millis(800), server_response_recv.recv()) 105 | .await 106 | { 107 | let _ = send_response(&mut stream, response).await; 108 | } 109 | } 110 | SubCommands::Layout { args } => { 111 | server_send.send(BackendServerCommand::LayoutCliCommand(args.join(" ")))?; 112 | if let Ok(Some(response)) = 113 | tokio::time::timeout(Duration::from_millis(800), server_response_recv.recv()) 114 | .await 115 | { 116 | let _ = send_response(&mut stream, response).await; 117 | } 118 | } 119 | SubCommands::DefaultConfig { 120 | replace_current_config: _, 121 | } 122 | | SubCommands::Daemon { no_daemonize: _ } 123 | | SubCommands::Restart { no_daemonize: _ } => { 124 | log::error!("invalid message passed to ipc"); 125 | } 126 | } 127 | stream.shutdown().await?; 128 | } 129 | 130 | Ok(()) 131 | } 132 | 133 | pub async fn read_message(stream: &mut UnixStream) -> Result { 134 | let mut message_len_bytes = [0u8; 4]; 135 | stream.read_exact(&mut message_len_bytes).await?; 136 | let message_len = u32::from_be_bytes(message_len_bytes) as usize; 137 | let mut message: Vec = Vec::with_capacity(message_len); 138 | while message.len() < message_len { 139 | stream.read_buf(&mut message).await?; 140 | } 141 | 142 | Ok(bincode::decode_from_slice(&message, bincode::config::standard())?.0) 143 | } 144 | 145 | pub async fn send_response(stream: &mut UnixStream, message: Option) -> Result<()> { 146 | let response = bincode::encode_to_vec( 147 | &message.unwrap_or("OK".to_string()), 148 | bincode::config::standard(), 149 | )?; 150 | stream.write_all(&response).await?; 151 | Ok(()) 152 | } 153 | 154 | pub fn send_recv_message( 155 | mut stream: std::os::unix::net::UnixStream, 156 | message: &SubCommands, 157 | ) -> Result> { 158 | stream.set_nonblocking(false)?; 159 | 160 | let message = bincode::encode_to_vec(&message, bincode::config::standard())?; 161 | let message_len_bytes = (message.len() as u32).to_be_bytes(); 162 | stream.write_all(&message_len_bytes)?; 163 | stream.write_all(&message)?; 164 | let mut buf = Vec::new(); 165 | stream.set_read_timeout(Some(Duration::from_millis(1000)))?; 166 | stream.read_to_end(&mut buf)?; 167 | 168 | Ok(if buf.is_empty() { 169 | None 170 | } else { 171 | let (buf, _) = bincode::decode_from_slice(&buf, bincode::config::standard())?; 172 | Some(buf) 173 | }) 174 | } 175 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::ErrorKind, 3 | os::{fd::AsFd, unix::net::UnixStream}, 4 | path::Path, 5 | thread, 6 | time::Duration, 7 | }; 8 | 9 | use anyhow::{Context, Result}; 10 | use clap::Parser; 11 | use dynisland::{ 12 | app::App, 13 | cli::{ 14 | Cli, 15 | SubCommands::{self, *}, 16 | }, 17 | config, ipc, 18 | }; 19 | use dynisland_core::abi::{abi_stable, log, module::UIServerCommand}; 20 | use env_logger::Env; 21 | use log::Level; 22 | use nix::unistd::Pid; 23 | 24 | // [ ] TODO remove some unnecessary arc and mutexes 25 | // [ ] TODO remove some unwraps and handle errors better 26 | // [x] TODO add docs 27 | // [x] TODO remove some unnecessary clones 28 | 29 | // [ ] TODO detect nvidia gpu and display warning (if dynisland uses too much ram, use GSK_RENDERER=vulkan or GSK_RENDERER=gl) 30 | 31 | // FIXME Gsk-WARNING **: 13:09:06.082: Clipping is broken, everything is clipped, but we didn't early-exit. 32 | // maybe it's in ScrollingLabel 33 | 34 | fn main() -> Result<()> { 35 | system_mimalloc::use_mimalloc!(); 36 | env_logger::Builder::new() 37 | // .filter_module("dynisland", log::LevelFilter::Debug) 38 | // .filter_module("dynisland_core", log::LevelFilter::Debug) 39 | // .filter_module("dynisland_modules", log::LevelFilter::Debug) 40 | .filter(Some("reqwest"), log::LevelFilter::Warn) 41 | .parse_env(Env::default().default_filter_or(Level::Info.as_str())) 42 | .init(); 43 | 44 | let cli = Cli::parse(); 45 | let config_dir = cli 46 | .config_path 47 | .clone() 48 | .unwrap_or(config::get_default_config_path()); 49 | let config = config::get_config(&config_dir); 50 | log::debug!("{cli:?}"); 51 | match cli.command { 52 | Daemon { no_daemonize } => { 53 | let runtime_dir = config.get_runtime_dir(); 54 | if let Ok(stream) = UnixStream::connect(runtime_dir.join("dynisland.sock")) { 55 | match ipc::send_recv_message(stream, &HealthCheck) { 56 | Ok(_) => { 57 | //app is already runnig 58 | log::error!("Application is already running"); 59 | } 60 | Err(_) => { 61 | log::error!("Error sending HealthCheck"); 62 | } 63 | }; 64 | return Ok(()); 65 | } else { 66 | let _ = std::fs::remove_file(runtime_dir.join("dynisland.sock")); 67 | } 68 | let pid = if !no_daemonize { 69 | let log_path = runtime_dir.join("dynisland.log"); 70 | detach(&log_path)? 71 | } else { 72 | Pid::from_raw(std::process::id() as i32) 73 | }; 74 | //init GTK 75 | gtk::init().with_context(|| "failed to init gtk")?; 76 | let app = App::default(); 77 | log::info!("pid: {pid}"); 78 | app.run(&config_dir)?; 79 | } 80 | Reload 81 | | Inspector 82 | | HealthCheck 83 | | ActivityNotification { 84 | activity_identifier: _, 85 | mode: _, 86 | duration: _, 87 | } 88 | | Module { 89 | module_name: _, 90 | args: _, 91 | } 92 | | Layout { args: _ } 93 | | ListActivities 94 | | ListLoadedModules => { 95 | let socket_path = config.get_runtime_dir().join("dynisland.sock"); 96 | match UnixStream::connect(socket_path.clone()) { 97 | Ok(stream) => { 98 | if let Some(response) = ipc::send_recv_message(stream, &cli.command)? { 99 | println!("Response: \n{response}"); 100 | } 101 | // if cli.command == HealthCheck { 102 | // println!("OK"); 103 | // } 104 | } 105 | Err(err) => { 106 | log::error!("Error opening dynisland socket: {err}"); 107 | if matches!(err.kind(), ErrorKind::ConnectionRefused) { 108 | log::info!("Connection refused, deleting old socket file"); 109 | std::fs::remove_file(socket_path.clone())?; 110 | } 111 | } 112 | }; 113 | } 114 | Kill => { 115 | let socket_path = config.get_runtime_dir().join("dynisland.sock"); 116 | match UnixStream::connect(socket_path.clone()) { 117 | Ok(stream) => { 118 | let response = ipc::send_recv_message(stream, &cli.command)?; 119 | println!("Kill message sent"); 120 | let has_responded = if let Some(response) = response { 121 | println!("Response: \n{response}"); 122 | true 123 | } else { 124 | false 125 | }; 126 | let mut tries = 0; 127 | while socket_path.exists() && tries < 10 { 128 | thread::sleep(Duration::from_millis(500)); 129 | if !has_responded { 130 | print!("."); 131 | } 132 | tries += 1; 133 | } 134 | println!(); 135 | if tries == 10 { 136 | log::error!("Failed to stop the old instance, manual kill needed"); 137 | } else if !has_responded { 138 | println!("OK"); 139 | } 140 | } 141 | Err(err) => { 142 | if matches!(err.kind(), ErrorKind::ConnectionRefused) { 143 | log::info!("Connection refused, deleting old socket file"); 144 | std::fs::remove_file(socket_path.clone())?; 145 | } else { 146 | log::warn!( 147 | "Error connecting to socket, app is probably not running: {err}" 148 | ); 149 | } 150 | } 151 | }; 152 | } 153 | Restart { no_daemonize } => { 154 | let socket_path = config.get_runtime_dir().join("dynisland.sock"); 155 | match UnixStream::connect(socket_path.clone()) { 156 | Ok(stream) => { 157 | let response = ipc::send_recv_message(stream, &SubCommands::Kill)?; 158 | let has_responded = if let Some(response) = response { 159 | log::info!("Response: \n{response}"); 160 | true 161 | } else { 162 | false 163 | }; 164 | log::info!("Waiting for daemon to die"); 165 | let mut tries = 0; 166 | while socket_path.exists() && tries < 10 { 167 | thread::sleep(Duration::from_millis(500)); 168 | if !has_responded { 169 | print!("."); 170 | } 171 | tries += 1; 172 | } 173 | println!(); 174 | if tries == 10 { 175 | log::error!("failed to stop the old instance, manual kill needed"); 176 | } else if !has_responded { 177 | println!("OK"); 178 | } 179 | } 180 | Err(err) => { 181 | log::error!("Error opening dynisland socket: {err}"); 182 | if matches!(err.kind(), ErrorKind::ConnectionRefused) { 183 | log::info!("Connection refused, trying to delete old socket file"); 184 | std::fs::remove_file(socket_path.clone())?; 185 | } 186 | } 187 | }; 188 | 189 | let pid = if !no_daemonize { 190 | let path = config.get_runtime_dir().join("dynisland.log"); 191 | detach(&path)? 192 | } else { 193 | Pid::from_raw(std::process::id() as i32) 194 | }; 195 | //init GTK 196 | gtk::init().with_context(|| "failed to init gtk")?; 197 | let app = App::default(); 198 | log::info!("pid: {pid}"); 199 | app.run(&config_dir)?; 200 | } 201 | DefaultConfig { 202 | replace_current_config, 203 | } => { 204 | gtk::init().with_context(|| "failed to init gtk")?; 205 | let mut app = App { 206 | config_dir, 207 | ..Default::default() 208 | }; 209 | let (abi_app_send, _abi_app_recv) = 210 | abi_stable::external_types::crossbeam_channel::unbounded::(); 211 | app.app_send = Some(abi_app_send); 212 | let (_conf, conf_str) = app.get_default_config(); 213 | println!("{conf_str}"); 214 | if replace_current_config { 215 | todo!(); 216 | } 217 | } 218 | } 219 | Ok(()) 220 | } 221 | 222 | fn detach(log_file_path: &Path) -> Result { 223 | std::fs::create_dir_all(log_file_path.parent().expect("invalid log path"))?; 224 | 225 | // detach from terminal 226 | let pid = match unsafe { nix::unistd::fork()? } { 227 | nix::unistd::ForkResult::Child => nix::unistd::setsid(), 228 | nix::unistd::ForkResult::Parent { .. } => { 229 | // nix::unistd::daemon(false, false); 230 | std::process::exit(0); 231 | } 232 | }?; 233 | 234 | let file = std::fs::OpenOptions::new() 235 | .create(true) 236 | .write(true) 237 | .append(false) 238 | .truncate(true) 239 | .open(log_file_path) 240 | .unwrap_or_else(|err| { 241 | panic!( 242 | "Error opening log file ({}), for writing: {err}", 243 | log_file_path.to_string_lossy() 244 | ) 245 | }); 246 | let fd = file.as_fd(); 247 | 248 | if nix::unistd::isatty(std::io::stdout().as_fd())? { 249 | nix::unistd::dup2_stdout(fd)?; 250 | } 251 | if nix::unistd::isatty(std::io::stderr().as_fd())? { 252 | nix::unistd::dup2_stderr(fd)?; 253 | } 254 | Ok(pid) 255 | } 256 | -------------------------------------------------------------------------------- /src/module_loading.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(debug_assertions, not(feature = "embed_modules")))] 2 | use std::path::PathBuf; 3 | use std::{collections::HashMap, path::Path, rc::Rc}; 4 | 5 | use abi_stable::{ 6 | external_types::crossbeam_channel::RSender, 7 | library::{lib_header_from_path, LibraryError}, 8 | std_types::{ 9 | RBoxError, RResult, 10 | RResult::{RErr, ROk}, 11 | }, 12 | type_layout::TypeLayout, 13 | StableAbi, 14 | }; 15 | use dynisland_core::abi::{ 16 | abi_stable, 17 | layout::{LayoutManagerBuilderRef, LayoutManagerType}, 18 | log, 19 | module::{ModuleBuilderRef, ModuleType, UIServerCommand}, 20 | SabiApplication, 21 | }; 22 | use tokio::sync::Mutex; 23 | 24 | use crate::{ 25 | app::App, 26 | layout_manager::{self, fallback_layout}, 27 | }; 28 | 29 | impl App { 30 | pub(crate) fn load_modules(&mut self, config_dir: &Path) -> Vec { 31 | let mut module_order = vec![]; 32 | let module_def_map = crate::module_loading::get_module_definitions(config_dir); 33 | 34 | if self.config.loaded_modules.contains(&"all".to_string()) { 35 | //load all modules available in order of hash (random order) 36 | for module_def in module_def_map { 37 | let module_name = module_def.0; 38 | let module_constructor = module_def.1; 39 | 40 | let built_module = match module_constructor(self.app_send.clone().unwrap()) { 41 | ROk(x) => x, 42 | RErr(e) => { 43 | log::error!("error during creation of {module_name}: {e:#?}"); 44 | continue; 45 | } 46 | }; 47 | 48 | module_order.push(module_name.to_string()); 49 | self.module_map 50 | .blocking_lock() 51 | .insert(module_name.to_string(), built_module); 52 | } 53 | } else { 54 | //load only modules in the config in order of definition 55 | for module_name in self.config.loaded_modules.iter() { 56 | let module_constructor = module_def_map.get(module_name); 57 | let module_constructor = match module_constructor { 58 | None => { 59 | log::warn!("module {} not found, skipping", module_name); 60 | continue; 61 | } 62 | Some(x) => x, 63 | }; 64 | 65 | let built_module = match module_constructor(self.app_send.clone().unwrap()) { 66 | ROk(x) => x, 67 | RErr(e) => { 68 | log::error!("error during creation of {module_name}: {e:#?}"); 69 | continue; 70 | } 71 | }; 72 | module_order.push(module_name.to_string()); 73 | // log::info!("loading module {}", module.get_name()); 74 | self.module_map 75 | .blocking_lock() 76 | .insert(module_name.to_string(), built_module); 77 | } 78 | } 79 | 80 | log::info!("loaded modules: {:?}", module_order); 81 | module_order 82 | } 83 | 84 | pub(crate) fn load_layout_manager(&mut self, config_dir: &Path) { 85 | let layout_manager_definitions = crate::module_loading::get_lm_definitions(config_dir); 86 | 87 | if self.config.layout.is_none() { 88 | log::info!("no layout manager in config, using default: FallbackLayout"); 89 | self.load_fallback_layout(); 90 | return; 91 | } 92 | let lm_name = self.config.layout.as_ref().unwrap(); 93 | if lm_name == layout_manager::NAME { 94 | log::info!("using layout manager: FallbackLayout"); 95 | self.load_fallback_layout(); 96 | return; 97 | } 98 | let lm_constructor = layout_manager_definitions.get(lm_name); 99 | let lm_constructor = match lm_constructor { 100 | None => { 101 | log::warn!( 102 | "layout manager {} not found, using default: FallbackLayout", 103 | lm_name 104 | ); 105 | self.load_fallback_layout(); 106 | return; 107 | } 108 | Some(x) => x, 109 | }; 110 | 111 | let built_lm = match lm_constructor(self.application.clone().into()) { 112 | ROk(x) => x, 113 | RErr(e) => { 114 | log::error!("error during creation of {lm_name}: {e:#?}"); 115 | log::info!("using default layout manager FallbackLayout"); 116 | self.load_fallback_layout(); 117 | return; 118 | } 119 | }; 120 | log::info!("using layout manager: {lm_name}"); 121 | self.layout = Some(Rc::new(Mutex::new((lm_name.clone(), built_lm)))); 122 | } 123 | 124 | pub(crate) fn load_fallback_layout(&mut self) { 125 | let layout_builder = fallback_layout::new(self.application.clone().into()); 126 | let layout = layout_builder.unwrap(); 127 | self.layout = Some(Rc::new(Mutex::new(( 128 | layout_manager::NAME.to_string(), 129 | layout, 130 | )))); 131 | } 132 | } 133 | 134 | pub fn get_module_definitions( 135 | _config_dir: &Path, 136 | ) -> HashMap) -> RResult> { 137 | let mut module_def_map = HashMap::< 138 | String, 139 | extern "C" fn(RSender) -> RResult, 140 | >::new(); 141 | 142 | let module_path = { 143 | #[cfg(all(debug_assertions, not(feature = "embed_modules")))] 144 | { 145 | PathBuf::from("./target/debug/") 146 | } 147 | #[cfg(any(not(debug_assertions), feature = "embed_modules"))] 148 | { 149 | _config_dir.join("modules") 150 | } 151 | }; 152 | 153 | #[cfg(feature = "embed_modules")] 154 | { 155 | let clock_module = clock_module::instantiate_root_module(); 156 | module_def_map.insert(clock_module.name().into(), clock_module.new()); 157 | 158 | let music_module = music_module::instantiate_root_module(); 159 | module_def_map.insert(music_module.name().into(), music_module.new()); 160 | 161 | let script_module = script_module::instantiate_root_module(); 162 | module_def_map.insert(script_module.name().into(), script_module.new()); 163 | 164 | let systray_module = systray_module::instantiate_root_module(); 165 | module_def_map.insert(systray_module.name().into(), systray_module.new()); 166 | 167 | let power_module = power_module::instantiate_root_module(); 168 | module_def_map.insert(power_module.name().into(), power_module.new()); 169 | } 170 | 171 | let files = match std::fs::read_dir(&module_path) { 172 | Ok(files) => files, 173 | Err(err) => { 174 | log::error!("failed to read module directory ({module_path:?}): {err}"); 175 | return module_def_map; 176 | } 177 | }; 178 | for file in files { 179 | let file = file.unwrap(); 180 | let path = file.path(); 181 | if !path.is_file() { 182 | continue; 183 | } 184 | match file 185 | .file_name() 186 | .to_str() 187 | .unwrap() 188 | .to_lowercase() 189 | .strip_suffix(".so") 190 | { 191 | Some(name) => { 192 | if !name.ends_with("module") { 193 | continue; 194 | } 195 | } 196 | None => continue, 197 | } 198 | log::debug!("loading module file: {:#?}", path); 199 | 200 | let res = (|| { 201 | let header = lib_header_from_path(&path)?; 202 | // header.init_root_module::() 203 | let layout1 = ModuleBuilderRef::LAYOUT; 204 | let layout2 = header.layout().unwrap(); 205 | ensure_compatibility(layout1, layout2).and_then(|_| unsafe { 206 | header 207 | .unchecked_layout::() 208 | .map_err(|err| err.into_library_error::()) 209 | }) 210 | })(); 211 | 212 | let module_builder = match res { 213 | Ok(x) => x, 214 | Err(e) => { 215 | log::error!( 216 | "error while loading {}: {e:#?}", 217 | path.file_name().unwrap().to_str().unwrap() 218 | ); 219 | continue; 220 | } 221 | }; 222 | let name = module_builder.name(); 223 | let constructor = module_builder.new(); 224 | 225 | module_def_map.insert(name.into(), constructor); 226 | } 227 | module_def_map 228 | } 229 | 230 | pub fn get_lm_definitions( 231 | _config_dir: &Path, 232 | ) -> HashMap< 233 | String, 234 | extern "C" fn(SabiApplication) -> RResult, 235 | > { 236 | let mut lm_def_map = HashMap::< 237 | String, 238 | extern "C" fn(SabiApplication) -> RResult, 239 | >::new(); 240 | 241 | let lm_path = { 242 | #[cfg(all(debug_assertions, not(feature = "embed_modules")))] 243 | { 244 | PathBuf::from("./target/debug/") 245 | } 246 | #[cfg(any(not(debug_assertions), feature = "embed_modules"))] 247 | { 248 | _config_dir.join("layouts") 249 | } 250 | }; 251 | 252 | #[cfg(feature = "embed_modules")] 253 | { 254 | let dynamic_layout = dynamic_layoutmanager::instantiate_root_module(); 255 | lm_def_map.insert(dynamic_layout.name().into(), dynamic_layout.new()); 256 | } 257 | 258 | let files = match std::fs::read_dir(&lm_path) { 259 | Ok(files) => files, 260 | Err(err) => { 261 | log::error!("failed to read layout manager directory ({lm_path:?}): {err}"); 262 | return lm_def_map; 263 | } 264 | }; 265 | for file in files { 266 | let file = file.unwrap(); 267 | let path = file.path(); 268 | if !path.is_file() { 269 | continue; 270 | } 271 | match file 272 | .file_name() 273 | .to_str() 274 | .unwrap() 275 | .to_lowercase() 276 | .strip_suffix(".so") 277 | { 278 | Some(name) => { 279 | if !name.ends_with("layoutmanager") { 280 | continue; 281 | } 282 | } 283 | None => continue, 284 | } 285 | log::debug!("loading layout manager file: {:#?}", path); 286 | 287 | let res = (|| { 288 | let header = lib_header_from_path(&path)?; 289 | // header.init_root_module::() 290 | let layout1 = LayoutManagerBuilderRef::LAYOUT; 291 | let layout2 = header.layout().unwrap(); 292 | ensure_compatibility(layout1, layout2).and_then(|_| unsafe { 293 | header 294 | .unchecked_layout::() 295 | .map_err(|err| err.into_library_error::()) 296 | }) 297 | })(); 298 | 299 | let lm_builder = match res { 300 | Ok(x) => x, 301 | Err(e) => { 302 | log::error!( 303 | "error while loading {}: {e:#?}", 304 | path.file_name().unwrap().to_str().unwrap() 305 | ); 306 | continue; 307 | } 308 | }; 309 | let name = lm_builder.name(); 310 | let constructor = lm_builder.new(); 311 | 312 | lm_def_map.insert(name.into(), constructor); 313 | } 314 | lm_def_map 315 | } 316 | 317 | pub fn ensure_compatibility( 318 | interface: &'static TypeLayout, 319 | implementation: &'static TypeLayout, 320 | ) -> Result<(), abi_stable::library::LibraryError> { 321 | let compatibility = abi_stable::abi_stability::abi_checking::check_layout_compatibility( 322 | interface, 323 | implementation, 324 | ); 325 | if let Err(err) = compatibility { 326 | let incompatibilities = err.errors.iter().filter(|e| !e.errs.is_empty()); 327 | let fatal_incompatibilities = incompatibilities.filter(|err| { 328 | err.errs.iter().any(|err| { 329 | !matches!( 330 | err, 331 | abi_stable::abi_stability::abi_checking::AbiInstability::FieldCountMismatch(assert) if assert.expected > assert.found 332 | ) 333 | }) 334 | }); 335 | if fatal_incompatibilities.count() > 0 { 336 | return Err(LibraryError::AbiInstability(RBoxError::new(err))); 337 | } 338 | } 339 | Ok(()) 340 | } 341 | -------------------------------------------------------------------------------- /src/layout_manager/fallback_layout.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap, rc::Rc, time::Duration}; 2 | 3 | use abi_stable::{ 4 | sabi_extern_fn, 5 | sabi_trait::TD_CanDowncast, 6 | std_types::{ 7 | RBoxError, ROption, 8 | RResult::{self, RErr, ROk}, 9 | RString, RVec, 10 | }, 11 | }; 12 | use anyhow::Result; 13 | use dynisland_core::{ 14 | abi::{ 15 | abi_stable, gdk, glib, gtk, 16 | layout::{LayoutManagerType, SabiLayoutManager, SabiLayoutManager_TO}, 17 | log, 18 | module::ActivityIdentifier, 19 | SabiApplication, SabiWidget, 20 | }, 21 | graphics::activity_widget::{boxed_activity_mode::ActivityMode, ActivityWidget}, 22 | ron, 23 | }; 24 | use gdk::prelude::*; 25 | use glib::SourceId; 26 | use gtk::{prelude::*, ApplicationWindow, EventController, StateFlags}; 27 | use ron::ser::PrettyConfig; 28 | 29 | use crate::layout_manager::{ 30 | self, 31 | config::{DeFallbackLayoutConfigMain, FallbackLayoutConfigMain}, 32 | }; 33 | 34 | pub struct FallbackLayout { 35 | app: gtk::Application, 36 | windows_containers: HashMap, 37 | widget_map: HashMap, 38 | cancel_minimize: Rc>>, 39 | config: FallbackLayoutConfigMain, 40 | } 41 | 42 | #[sabi_extern_fn] 43 | pub extern "C" fn new(app: SabiApplication) -> RResult { 44 | let app = app.try_into().unwrap(); 45 | let this = FallbackLayout { 46 | app, 47 | windows_containers: HashMap::new(), 48 | widget_map: HashMap::new(), 49 | cancel_minimize: Rc::new(RefCell::new(HashMap::new())), 50 | config: FallbackLayoutConfigMain::default(), 51 | }; 52 | ROk(SabiLayoutManager_TO::from_value(this, TD_CanDowncast)) 53 | } 54 | 55 | impl SabiLayoutManager for FallbackLayout { 56 | fn init(&mut self) { 57 | self.update_windows(); 58 | } 59 | 60 | fn update_config(&mut self, config: RString) -> RResult<(), RBoxError> { 61 | match serde_json::from_str::(&config) { 62 | Ok(conf) => { 63 | self.config = conf.into_main_config(); 64 | } 65 | Err(err) => { 66 | log::warn!( 67 | "Failed to parse config into struct, using default: {:#?}", 68 | err 69 | ); 70 | } 71 | } 72 | 73 | log::trace!("current config: {:#?}", self.config); 74 | 75 | if self.app.windows().first().is_some() { 76 | self.update_windows(); 77 | for (window_name, window_config) in self.config.windows.iter() { 78 | let window = &self.windows_containers.get(window_name).unwrap().0; 79 | window_config 80 | .window_position 81 | .reconfigure_window(&window.clone().upcast()); 82 | } 83 | } 84 | self.configure_containers(); 85 | 86 | for (id, widget) in self.widget_map.iter() { 87 | self.configure_widget(id, widget); 88 | } 89 | 90 | ROk(()) 91 | } 92 | fn default_config(&self) -> RResult { 93 | let mut conf = FallbackLayoutConfigMain::default(); 94 | conf.windows.clear(); 95 | match ron::ser::to_string_pretty(&conf, PrettyConfig::default()) { 96 | Ok(map) => ROk(RString::from(map)), 97 | Err(err) => RErr(RBoxError::new(err)), 98 | } 99 | } 100 | 101 | fn add_activity(&mut self, activity_id: &ActivityIdentifier, widget: SabiWidget) { 102 | let widget: gtk::Widget = widget.try_into().unwrap(); 103 | let widget = match widget.downcast::() { 104 | Ok(widget) => widget, 105 | Err(_) => { 106 | log::error!("widget {} is not an ActivityWidget", activity_id); 107 | return; 108 | } 109 | }; 110 | self.configure_widget(activity_id, &widget); 111 | self.add_activity_to_container(activity_id, &widget); 112 | self.widget_map.insert(activity_id.clone(), widget); 113 | } 114 | 115 | fn get_activity(&self, activity: &ActivityIdentifier) -> ROption { 116 | self.widget_map 117 | .get(activity) 118 | .map(|wid| SabiWidget::from(wid.clone().upcast::())) 119 | .into() 120 | } 121 | 122 | fn remove_activity(&mut self, activity: &ActivityIdentifier) { 123 | if let Some(widget) = self.widget_map.remove(activity) { 124 | if self 125 | .remove_activity_from_container(activity, widget) 126 | .is_err() 127 | { 128 | return; 129 | } 130 | } 131 | } 132 | fn list_activities(&self) -> RVec { 133 | self.widget_map.keys().cloned().collect() 134 | } 135 | fn list_windows(&self) -> RVec { 136 | self.windows_containers 137 | .keys() 138 | .map(|s| RString::from(s.clone())) 139 | .collect() 140 | } 141 | fn activity_notification( 142 | &self, 143 | activity: &ActivityIdentifier, 144 | mode_id: u8, 145 | duration: ROption, 146 | ) { 147 | if let Some(widget) = self.widget_map.get(activity) { 148 | let mode = ActivityMode::try_from(mode_id).unwrap(); 149 | widget.set_mode(mode); 150 | if matches!(mode, ActivityMode::Minimal | ActivityMode::Compact) { 151 | return; 152 | } 153 | let default_timeout = self 154 | .config 155 | .get_for_window(&activity.metadata().window_name().unwrap_or_default()) 156 | .auto_minimize_timeout; 157 | let timeout = duration.unwrap_or(default_timeout as u64); 158 | let widget = widget.clone(); 159 | glib::timeout_add_local_once( 160 | Duration::from_millis(timeout.try_into().unwrap()), 161 | move || { 162 | if !widget.state_flags().contains(StateFlags::PRELIGHT) && widget.mode() == mode 163 | { 164 | //mouse is not on widget and mode hasn't changed 165 | widget.set_mode(ActivityMode::Compact); 166 | } 167 | }, 168 | ); 169 | } 170 | } 171 | } 172 | 173 | impl FallbackLayout { 174 | fn get_window_name(&self, activity_id: &ActivityIdentifier) -> String { 175 | let requested_window = activity_id.metadata().window_name().unwrap_or_default(); 176 | if self.windows_containers.contains_key(&requested_window) { 177 | requested_window 178 | } else { 179 | "".to_string() 180 | } 181 | } 182 | fn configure_widget(&self, id: &ActivityIdentifier, widget: &ActivityWidget) { 183 | let config = self 184 | .config 185 | .get_for_window(&id.metadata().window_name().unwrap_or_default()); 186 | widget.set_valign(config.window_position.v_anchor.map_gtk()); 187 | widget.set_halign(config.window_position.h_anchor.map_gtk()); 188 | // remove old controllers 189 | let mut controllers = vec![]; 190 | for controller in widget 191 | .observe_controllers() 192 | .iter::() 193 | .flatten() 194 | .flat_map(|c| c.downcast::()) 195 | { 196 | if let Some(name) = controller.name() { 197 | if name == "press_gesture" || name == "focus_controller" { 198 | controllers.push(controller); 199 | } 200 | } 201 | } 202 | for controller in controllers.iter() { 203 | widget.remove_controller(controller); 204 | } 205 | 206 | let press_gesture = gtk::GestureClick::new(); 207 | press_gesture.set_name(Some("press_gesture")); 208 | 209 | let focus_in = gtk::EventControllerMotion::new(); 210 | focus_in.set_name(Some("focus_controller")); 211 | 212 | // Minimal mode to Compact mode controller 213 | press_gesture.set_button(gdk::BUTTON_PRIMARY); 214 | press_gesture.connect_released(|gest, _, x, y| { 215 | let aw = gest.widget().downcast::().unwrap(); 216 | if x < 0.0 217 | || y < 0.0 218 | || x > aw.size(gtk::Orientation::Horizontal).into() 219 | || y > aw.size(gtk::Orientation::Vertical).into() 220 | { 221 | return; 222 | } 223 | if let ActivityMode::Minimal = aw.mode() { 224 | aw.set_mode(ActivityMode::Compact); 225 | gest.set_state(gtk::EventSequenceState::Claimed); 226 | } 227 | }); 228 | widget.add_controller(press_gesture); 229 | 230 | // auto minimize (to Compact mode) controller 231 | if config.auto_minimize_timeout >= 0 { 232 | let cancel_minimize = self.cancel_minimize.clone(); 233 | let timeout = config.auto_minimize_timeout; 234 | let activity_id = id.clone(); 235 | focus_in.connect_leave(move |evt| { 236 | let aw = evt.widget().downcast::().unwrap(); 237 | let mode = aw.mode(); 238 | if matches!(mode, ActivityMode::Minimal | ActivityMode::Compact) { 239 | return; 240 | } 241 | let id = glib::timeout_add_local_once( 242 | Duration::from_millis(timeout.try_into().unwrap()), 243 | move || { 244 | if !aw.state_flags().contains(StateFlags::PRELIGHT) && aw.mode() == mode { 245 | //mouse is not on widget and mode hasn't changed 246 | aw.set_mode(ActivityMode::Compact); 247 | } 248 | }, 249 | ); 250 | let mut cancel_minimize = cancel_minimize.borrow_mut(); 251 | if let Some(source) = cancel_minimize.remove(&activity_id) { 252 | if glib::MainContext::default() 253 | .find_source_by_id(&source) 254 | .is_some() 255 | { 256 | source.remove(); 257 | } 258 | } 259 | 260 | cancel_minimize.insert(activity_id.clone(), id); 261 | }); 262 | widget.add_controller(focus_in); 263 | } 264 | } 265 | 266 | fn configure_containers(&self) { 267 | for (window_name, (_, container)) in self.windows_containers.iter() { 268 | let config = self.config.get_for_window(window_name); 269 | if config.orientation_horizontal { 270 | container.set_orientation(gtk::Orientation::Horizontal); 271 | } else { 272 | container.set_orientation(gtk::Orientation::Vertical); 273 | } 274 | if !config.window_position.layer_shell { 275 | container.set_halign(config.window_position.h_anchor.map_gtk()); 276 | container.set_valign(config.window_position.v_anchor.map_gtk()); 277 | } 278 | container.set_spacing(0); 279 | } 280 | } 281 | // FIXME: this is terribly inefficient 282 | fn update_windows(&mut self) { 283 | let mut orphan_widgets: Vec = Vec::new(); 284 | // remove orphaned windows 285 | for window in self.app.windows() { 286 | let window_name = window.title().unwrap_or_default(); 287 | if !self 288 | .windows_containers 289 | .contains_key(&window_name.to_string()) 290 | { 291 | for child in window 292 | .child() 293 | .unwrap() 294 | .observe_children() 295 | .iter::() 296 | .flatten() 297 | { 298 | let widget = child.downcast::().unwrap(); 299 | if let Some((id, _)) = self.widget_map.iter().find(|(_, w)| *w == &widget) { 300 | orphan_widgets.push(id.clone()); 301 | } 302 | } 303 | window.close(); 304 | log::warn!("removing orphaned window {}", window_name); 305 | } 306 | } 307 | // remove windows that are no longer in the config 308 | let mut windows_to_remove: Vec = Vec::new(); 309 | for (window_name, (window, _)) in self.windows_containers.iter() { 310 | if !self.config.windows.contains_key(&window_name.to_string()) { 311 | for child in window 312 | .child() 313 | .unwrap() 314 | .observe_children() 315 | .iter::() 316 | .flatten() 317 | { 318 | let widget = child.downcast::().unwrap(); 319 | if let Some((id, _)) = self.widget_map.iter().find(|(_, w)| *w == &widget) { 320 | orphan_widgets.push(id.clone()); 321 | } 322 | } 323 | windows_to_remove.push(window_name.clone()); 324 | window.close(); 325 | } 326 | } 327 | for window_name in windows_to_remove { 328 | self.windows_containers.remove(&window_name); 329 | log::trace!("removing window no longer in config {}", window_name); 330 | } 331 | // create new windows 332 | let existing_windows: Vec = self.windows_containers.keys().cloned().collect(); 333 | let mut windows_to_create: Vec = Vec::new(); 334 | for window_name in self.config.windows.keys() { 335 | if !existing_windows.contains(window_name) { 336 | windows_to_create.push(window_name.clone()); 337 | } 338 | } 339 | for window_name in windows_to_create { 340 | log::trace!("creating new window {}", window_name); 341 | self.create_new_window(&window_name); 342 | } 343 | for widget_id in orphan_widgets { 344 | let widget = self.widget_map.get(&widget_id).unwrap().clone(); 345 | self.add_activity_to_container(&ActivityIdentifier::new("", ""), &widget); 346 | log::trace!("readding orphaned widget {}", widget_id); 347 | } 348 | let mut to_update = Vec::new(); 349 | for (id, widget) in self.widget_map.iter() { 350 | let parent = widget.parent().unwrap().downcast::().unwrap(); 351 | if let Some((current_window, (_, _))) = self 352 | .windows_containers 353 | .iter() 354 | .find(move |(_, (_, container))| &parent == container) 355 | { 356 | if let Some(desired_window) = id.metadata().window_name() { 357 | if desired_window != *current_window 358 | && self.config.windows.contains_key(&desired_window) 359 | { 360 | to_update.push(id.clone()); 361 | } 362 | } 363 | } 364 | } 365 | for id in to_update { 366 | let widget = self.widget_map.get(&id).unwrap().clone(); 367 | self.remove_activity_from_container(&id, widget.clone()) 368 | .unwrap(); 369 | self.add_activity_to_container(&id, &widget); 370 | log::trace!("moving widget {} to correct window", id); 371 | } 372 | log::debug!("updated windows"); 373 | } 374 | 375 | fn create_new_window(&mut self, window_name: &str) { 376 | if self.windows_containers.contains_key(window_name) { 377 | return; 378 | } 379 | if !self.config.windows.contains_key(window_name) { 380 | return; 381 | } 382 | let window = gtk::ApplicationWindow::new(&self.app); 383 | window.set_title(Some(window_name)); 384 | let container = gtk::Box::new(gtk::Orientation::Horizontal, 5); 385 | container.add_css_class("activity-container"); 386 | window.set_child(Some(&container)); 387 | self.config 388 | .get_for_window(window_name) 389 | .window_position 390 | .init_window(&window.clone().upcast()); 391 | //show window 392 | window.present(); 393 | self.windows_containers 394 | .insert(window_name.to_string(), (window, container)); 395 | self.configure_containers(); 396 | log::trace!("created new window {}", window_name); 397 | } 398 | 399 | fn add_activity_to_container( 400 | &mut self, 401 | activity_id: &ActivityIdentifier, 402 | widget: &ActivityWidget, 403 | ) { 404 | let window_name = self.get_window_name(activity_id); 405 | let container = &self 406 | .windows_containers 407 | .get(&window_name) 408 | .expect(&format!( 409 | "there should be a default container for {}", 410 | window_name 411 | )) 412 | .1; 413 | container.append(widget); 414 | } 415 | 416 | fn remove_activity_from_container( 417 | &mut self, 418 | activity: &ActivityIdentifier, 419 | widget: ActivityWidget, 420 | ) -> Result<()> { 421 | let widget_container = match widget.parent().unwrap().downcast::() { 422 | Ok(parent) => parent, 423 | Err(_) => { 424 | log::warn!( 425 | "Error removing {activity:?} from {}: parent is not a Box", 426 | layout_manager::NAME 427 | ); 428 | anyhow::bail!( 429 | "Error removing {activity:?} from {}: parent is not a Box", 430 | layout_manager::NAME 431 | ); 432 | } 433 | }; 434 | let name = if let Some((name, (window, container))) = self 435 | .windows_containers 436 | .iter() 437 | .find(move |(_, (_, container))| &widget_container == container) 438 | { 439 | container.remove(&widget); 440 | if container.first_child().is_some() { 441 | return Ok(()); 442 | } 443 | window.close(); 444 | name.clone() 445 | } else { 446 | return Ok(()); 447 | }; 448 | self.windows_containers.remove(&name.clone()); 449 | log::debug!("removing empty window {}", name); 450 | self.create_new_window(&name.clone()); 451 | Ok(()) 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | io::ErrorKind, 4 | path::{Path, PathBuf}, 5 | rc::Rc, 6 | thread, 7 | }; 8 | 9 | use abi_stable::{ 10 | external_types::crossbeam_channel::RSender, 11 | std_types::{ 12 | RBoxError, ROption, 13 | RResult::{self, RErr, ROk}, 14 | RString, 15 | }, 16 | }; 17 | use anyhow::Result; 18 | use dynisland_core::{ 19 | abi::{ 20 | abi_stable, gdk, glib, 21 | layout::LayoutManagerType, 22 | log, 23 | module::{ActivityIdentifier, ModuleType, UIServerCommand}, 24 | }, 25 | graphics::activity_widget::boxed_activity_mode::ActivityMode, 26 | ron, 27 | }; 28 | use gtk::{prelude::*, CssProvider, Widget}; 29 | use notify::{RecommendedWatcher, Watcher}; 30 | use ron::{extensions::Extensions, ser::PrettyConfig}; 31 | use tokio::sync::{mpsc::unbounded_channel, Mutex}; 32 | 33 | use crate::{ 34 | config::{self, Config, GeneralConfig}, 35 | ipc::open_socket, 36 | layout_manager::{self, fallback_layout}, 37 | }; 38 | 39 | pub enum BackendServerCommand { 40 | ReloadConfig, 41 | Stop, 42 | OpenInspector, 43 | ActivityNotification(ActivityIdentifier, ActivityMode, Option), 44 | ListActivities, 45 | ListLoadedModules, 46 | ModuleCliCommand(String, String), 47 | LayoutCliCommand(String), 48 | } 49 | 50 | pub struct App { 51 | pub application: gtk::Application, 52 | // pub window: gtk::Window, 53 | pub module_map: Rc>>, 54 | pub layout: Option>>, 55 | // pub producers_handle: Handle, 56 | // pub producers_shutdown: tokio::sync::mpsc::Sender<()>, 57 | pub app_send: Option>, 58 | pub config: Config, 59 | pub css_provider: CssProvider, 60 | pub config_dir: PathBuf, 61 | } 62 | 63 | impl App { 64 | pub fn run(mut self, config_dir: &Path) -> Result<()> { 65 | self.config = config::get_config(config_dir); 66 | self.config_dir = config_dir.to_path_buf(); 67 | 68 | let (server_send, server_recv) = unbounded_channel::(); 69 | let (server_response_send, server_response_recv) = unbounded_channel::>(); 70 | let runtime_path = self.config.get_runtime_dir(); 71 | 72 | let mut app_recv_async = self.init_abi_app_channel(); 73 | 74 | // load layout manager and init modules 75 | self.load_layout_manager(config_dir); 76 | self.load_layout_config(); 77 | 78 | let module_order = self.load_modules(config_dir); 79 | self.load_configs(config_dir); 80 | self.init_loaded_modules(&module_order); 81 | 82 | // init layout manager and send start signal 83 | let (start_signal_tx, start_signal_rx) = tokio::sync::broadcast::channel::<()>(1); 84 | let open_debugger = self 85 | .config 86 | .debug 87 | .clone() 88 | .map(|d| d.open_debugger_at_start) 89 | .unwrap_or(false); 90 | let layout = self.layout.clone().unwrap(); 91 | self.application.connect_activate(move |_app| { 92 | log::info!("Loading LayoutManager"); 93 | layout.blocking_lock().1.init(); 94 | start_signal_tx.send(()).unwrap(); 95 | gtk::Window::set_interactive_debugging(open_debugger); 96 | }); 97 | 98 | //UI command consumer 99 | let mut start_signal = start_signal_rx.resubscribe(); 100 | let layout = self.layout.clone().unwrap(); 101 | let module_map = self.module_map.clone(); 102 | glib::MainContext::default().spawn_local(async move { 103 | start_signal.recv().await.unwrap(); 104 | 105 | // TODO check if there are too many tasks on the UI thread and it begins to lag 106 | while let Some(command) = app_recv_async.recv().await { 107 | match command { 108 | UIServerCommand::AddActivity{activity_id, widget} => { 109 | let activity: Widget = match widget.try_into() { 110 | Ok(act) => {act}, 111 | Err(err) => { 112 | log::error!("error while converting SabiWidget to Widget, maybe it was deallocated after UIServerCommand::AddActivity was sent: {err:#?}"); 113 | continue; 114 | }, 115 | }; 116 | 117 | Self::update_general_configs_on_activity( 118 | &self.config.general_style_config, 119 | &activity, 120 | ); 121 | 122 | if layout 123 | .lock() 124 | .await 125 | .1 126 | .get_activity(&activity_id) 127 | .is_some() 128 | { 129 | log::debug!("activity already registered on {}", activity_id.module()); 130 | continue; 131 | } 132 | 133 | layout 134 | .lock() 135 | .await 136 | .1 137 | .add_activity(&activity_id, activity.into()); 138 | log::info!("registered activity on {}", activity_id.module()); 139 | } 140 | UIServerCommand::RemoveActivity { activity_id } => { 141 | let mut layout = layout.lock().await; 142 | if layout.1.get_activity(&activity_id).is_some(){ 143 | layout.1.remove_activity(&activity_id); 144 | log::info!("unregistered activity on {}", activity_id.module()); 145 | }else{ 146 | log::warn!("error removing activity, not found: {:?}", activity_id); 147 | } 148 | } 149 | UIServerCommand::RestartProducers { module_name } => { 150 | if let Some(module) = module_map.lock().await.get(module_name.as_str()) { 151 | module.restart_producers(); 152 | } 153 | } 154 | UIServerCommand::RequestNotification { activity_id, mode, duration} => { 155 | if mode>3{ 156 | continue; 157 | } 158 | let layout = layout.lock().await; 159 | if layout.1.get_activity(&activity_id).is_none(){ 160 | continue; 161 | } 162 | layout.1.activity_notification(&activity_id, mode, duration); 163 | } 164 | } 165 | } 166 | }); 167 | 168 | let app = self.application.clone(); 169 | let mut start_signal = start_signal_rx.resubscribe(); 170 | let conf_dir = config_dir.to_path_buf(); 171 | //server command consumer 172 | glib::MainContext::default().spawn_local(async move { 173 | start_signal.recv().await.unwrap(); 174 | 175 | let renderer_name = match self.application.windows()[0] 176 | .native() 177 | .expect("Layout manager has no windows") 178 | .renderer() 179 | { 180 | Some(renderer_type) => renderer_type.type_().name(), 181 | None => "no renderer found", 182 | }; 183 | 184 | log::info!("Using renderer: {}", renderer_name); 185 | 186 | //init css providers 187 | let fallback_provider = gtk::CssProvider::new(); 188 | let css = 189 | grass::from_string(include_str!("../default.scss"), &grass::Options::default()) 190 | .unwrap(); 191 | fallback_provider.load_from_string(&css); 192 | gtk::style_context_add_provider_for_display( 193 | &gdk::Display::default().unwrap(), 194 | &fallback_provider, 195 | gtk::STYLE_PROVIDER_PRIORITY_SETTINGS, 196 | ); 197 | 198 | gtk::style_context_add_provider_for_display( 199 | &gdk::Display::default().unwrap(), 200 | &self.css_provider, 201 | gtk::STYLE_PROVIDER_PRIORITY_USER, 202 | ); 203 | self.load_css(&conf_dir); //load user's scss 204 | 205 | self.restart_producer_runtimes(); // start producers 206 | 207 | self.start_backend_server(server_recv, server_response_send, conf_dir) 208 | .await; 209 | }); 210 | 211 | let _wathcer = start_config_dir_watcher(server_send.clone(), &config_dir); 212 | 213 | //start application 214 | app.register(None as Option<>k::gio::Cancellable>)?; 215 | let running = app.is_remote(); 216 | if running { 217 | log::error!("dynisland is already running"); 218 | } else { 219 | start_ipc_server(runtime_path.clone(), server_send, server_response_recv); 220 | } 221 | app.run_with_args::(&[]); 222 | if !running { 223 | std::fs::remove_file(runtime_path.join("dynisland.sock"))?; 224 | } 225 | Ok(()) 226 | } 227 | 228 | fn init_abi_app_channel(&mut self) -> tokio::sync::mpsc::UnboundedReceiver { 229 | let (abi_app_send, abi_app_recv) = 230 | abi_stable::external_types::crossbeam_channel::unbounded::(); 231 | self.app_send = Some(abi_app_send); 232 | let (app_send_async, app_recv_async) = unbounded_channel::(); 233 | 234 | //forward message to app receiver 235 | thread::Builder::new() 236 | .name("abi-app-forwarder".to_string()) 237 | .spawn(move || { 238 | while let Ok(msg) = abi_app_recv.recv() { 239 | app_send_async.send(msg).expect("failed to send message"); 240 | } 241 | }) 242 | .expect("failed to spawn abi-app-forwarder thread"); 243 | app_recv_async 244 | } 245 | 246 | async fn start_backend_server( 247 | mut self, 248 | mut server_recv: tokio::sync::mpsc::UnboundedReceiver, 249 | server_response_send: tokio::sync::mpsc::UnboundedSender>, 250 | config_dir: std::path::PathBuf, 251 | ) { 252 | while let Some(command) = server_recv.recv().await { 253 | match command { 254 | BackendServerCommand::ReloadConfig => { 255 | log::info!("Reloading Config"); 256 | //TODO split config and css reload (producers don't need to be restarted if only css changed) 257 | 258 | // without this sleep, reading the config file sometimes gives an empty file. 259 | glib::timeout_future(std::time::Duration::from_millis(50)).await; 260 | self.load_configs(&config_dir); 261 | self.update_general_configs(); 262 | self.load_layout_config(); 263 | self.load_css(&config_dir); 264 | 265 | self.restart_producer_runtimes(); 266 | } 267 | BackendServerCommand::Stop => { 268 | log::info!("Quitting"); 269 | let _ = server_response_send.send(None); 270 | self.application.quit(); 271 | } 272 | BackendServerCommand::OpenInspector => { 273 | log::info!("Opening inspector"); 274 | let _ = server_response_send.send(None); 275 | gtk::Window::set_interactive_debugging(true); 276 | } 277 | BackendServerCommand::ActivityNotification(id, mode, duration) => { 278 | if let Err(err) = 279 | self.app_send 280 | .clone() 281 | .unwrap() 282 | .send(UIServerCommand::RequestNotification { 283 | activity_id: id, 284 | mode: mode as u8, 285 | duration: ROption::from(duration), 286 | }) 287 | { 288 | let _ = server_response_send.send(Some(err.to_string())); 289 | log::error!("{err}"); 290 | } else { 291 | let _ = server_response_send.send(None); 292 | } 293 | } 294 | BackendServerCommand::ListActivities => match self.layout.clone() { 295 | Some(layout) => { 296 | let activities = layout.lock().await.1.list_activities(); 297 | let mut response = String::new(); 298 | for activity in activities { 299 | response += &activity.to_string(); 300 | response += "\n"; 301 | } 302 | let _ = server_response_send.send(Some(response)); 303 | } 304 | None => { 305 | let _ = server_response_send.send(Some("no layout loaded".to_string())); 306 | } 307 | }, 308 | BackendServerCommand::ListLoadedModules => { 309 | let mut response = String::new(); 310 | let mod_map = self.module_map.lock().await; 311 | for module in mod_map.keys() { 312 | response += &format!("{module}\n"); 313 | } 314 | let _ = server_response_send.send(Some(response)); 315 | } 316 | BackendServerCommand::ModuleCliCommand(module_name, args) => { 317 | match self.module_map.lock().await.get(&module_name) { 318 | Some(module) => { 319 | let response = match module.cli_command(args.into()) { 320 | ROk(response) => response.into_string(), 321 | RErr(err) => format!("Error:\n{err}"), 322 | }; 323 | let _ = server_response_send.send(Some(response)); 324 | } 325 | None => { 326 | let _ = server_response_send.send(Some("module not found".to_string())); 327 | } 328 | } 329 | } 330 | BackendServerCommand::LayoutCliCommand(args) => { 331 | let layout = self.layout.clone().unwrap(); 332 | let response = match layout.lock().await.1.cli_command(RString::from(args)) { 333 | ROk(response) => response.into_string(), 334 | RErr(err) => format!("Error:\n{err}"), 335 | }; 336 | let _ = server_response_send.send(Some(response)); 337 | } 338 | } 339 | } 340 | } 341 | 342 | pub fn load_css(&mut self, config_dir: &Path) { 343 | let css_content = grass::from_path( 344 | config_dir.join("dynisland.scss"), 345 | &grass::Options::default(), 346 | ); 347 | match css_content { 348 | Ok(content) => { 349 | self.css_provider //TODO maybe save previous state before trying to update 350 | .load_from_string(&content); 351 | } 352 | Err(err) => { 353 | log::warn!("failed to parse css: {}", err.to_string()); 354 | } 355 | } 356 | } 357 | 358 | fn load_configs(&mut self, config_dir: &Path) { 359 | self.config = config::get_config(config_dir); 360 | log::debug!("general_config: {:#?}", self.config.general_style_config); 361 | for (module_name, module) in self.module_map.blocking_lock().iter_mut() { 362 | log::info!("loading config for module: {:#?}", module_name); 363 | let config_to_parse = self.config.module_config.get(module_name); 364 | let config_parsed = match config_to_parse { 365 | Some(conf) => { 366 | let confs: String = ron::ser::to_string_pretty(&conf, PrettyConfig::default()) 367 | .unwrap() 368 | .into(); 369 | log::trace!("{module_name} config before strip comments: {}", confs); 370 | let mut confs = confs.replace("\\'", "\'"); 371 | if let Err(err) = json_strip_comments::strip(&mut confs) { 372 | log::warn!("failed to strip trailing commas from {module_name} err: {err}"); 373 | }; 374 | log::trace!("{module_name} config: {}", confs); 375 | module.update_config(confs.into()) 376 | } 377 | None => { 378 | log::debug!("no config for module: {:#?}", module_name); 379 | ROk(()) 380 | } 381 | }; 382 | match config_parsed { 383 | RErr(err) => { 384 | log::error!("failed to parse config for module {}: {err:?}", module_name) 385 | } 386 | ROk(()) => { 387 | // log::debug!("{}: {:#?}", module_name, config_to_parse); 388 | } 389 | } 390 | } 391 | } 392 | 393 | //TODO let the modules handle this, something like module.update_general_config or module.update_config itself 394 | fn update_general_configs(&self) { 395 | let layout = self.layout.clone().unwrap(); 396 | let layout = layout.blocking_lock(); 397 | let activities = layout.1.list_activities(); 398 | for activity in activities { 399 | let activity: Widget = layout 400 | .1 401 | .get_activity(&activity) 402 | .unwrap() 403 | .try_into() 404 | .unwrap(); 405 | Self::update_general_configs_on_activity(&self.config.general_style_config, &activity); 406 | } 407 | } 408 | 409 | fn update_general_configs_on_activity(config: &GeneralConfig, activity: &Widget) { 410 | //TODO define property names as constants 411 | activity.set_property("config-minimal-height", config.minimal_height as i32); 412 | activity.set_property("config-minimal-width", config.minimal_width as i32); 413 | activity.set_property("config-blur-radius", config.blur_radius); 414 | activity.set_property("config-enable-drag-stretch", config.enable_drag_stretch); 415 | // activity.set_property("config-transition-duration", config.hide_widget_timeout_ms); 416 | // update widget size 417 | activity.set_property("mode", activity.property::("mode")); 418 | } 419 | 420 | fn init_loaded_modules(&self, order: &Vec) { 421 | let module_map = self.module_map.blocking_lock(); 422 | for module_name in order { 423 | let module = module_map.get(module_name).unwrap(); 424 | module.init(); 425 | } 426 | } 427 | 428 | fn load_layout_config(&self) { 429 | let layout = self.layout.clone().unwrap(); 430 | let mut layout = layout.blocking_lock(); 431 | let layout_name = layout.0.clone(); 432 | if let Some(config) = self.config.layout_configs.get(&layout_name) { 433 | let mut confs: String = ron::ser::to_string_pretty(&config, PrettyConfig::default()) 434 | .unwrap() 435 | .into(); 436 | 437 | if let Err(err) = json_strip_comments::strip(&mut confs) { 438 | log::warn!("failed to strip trailing commas from {layout_name} err: {err}"); 439 | }; 440 | log::debug!("{layout_name} config: {}", confs); 441 | match layout.1.update_config(confs.into()) { 442 | ROk(()) => { 443 | log::info!("loaded layout config for {layout_name}"); 444 | } 445 | RErr(err) => { 446 | log::error!("failed to parse layout config for {layout_name}, {err}"); 447 | } 448 | } 449 | } else { 450 | log::info!("no layout config found for {layout_name}, using Default"); 451 | } 452 | } 453 | 454 | fn restart_producer_runtimes(&self) { 455 | for module in self.module_map.blocking_lock().values_mut() { 456 | module.restart_producers(); 457 | } 458 | } 459 | 460 | pub fn get_default_config(self) -> (Config, String) { 461 | let mut base_conf = Config::default(); 462 | 463 | // get all the loadable LayoutManager configs 464 | let lm_defs = crate::module_loading::get_lm_definitions(&self.config_dir); 465 | let mut layout_configs: Vec<(String, RResult)> = Vec::new(); 466 | for (lm_name, lm_constructor) in lm_defs { 467 | let built_lm = match lm_constructor(self.application.clone().into()) { 468 | ROk(x) => x, 469 | RErr(e) => { 470 | log::error!("error during creation of {lm_name}: {e:#?}"); 471 | continue; 472 | } 473 | }; 474 | layout_configs.push((lm_name, built_lm.default_config())); 475 | } 476 | layout_configs.push(( 477 | layout_manager::NAME.to_owned(), 478 | fallback_layout::new(self.application.clone().into()) 479 | .unwrap() 480 | .default_config(), 481 | )); 482 | 483 | base_conf.layout = Some(layout_configs.first().unwrap().0.clone()); 484 | 485 | // get all the loadable Module configs 486 | let mod_defs = crate::module_loading::get_module_definitions(&self.config_dir); 487 | let mut module_configs: Vec<(String, RResult)> = Vec::new(); 488 | for (mod_name, mod_constructor) in mod_defs { 489 | match mod_constructor(self.app_send.clone().unwrap()) { 490 | ROk(built_mod) => { 491 | module_configs.push((mod_name, built_mod.default_config())); 492 | } 493 | RErr(e) => log::error!("error during creation of {mod_name}: {e:#?}"), 494 | }; 495 | } 496 | base_conf.loaded_modules = module_configs.iter().map(|v| v.0.to_owned()).collect(); 497 | let conf_str = "Config".to_owned() + &base_conf.to_string(); 498 | 499 | // put the LayoutManager configs into base_conf and a string 500 | let mut lm_config_str = String::from("{\n"); 501 | for (lm_name, lm_config) in layout_configs { 502 | match lm_config { 503 | RErr(err) => log::debug!("cannot get default config: {err}"), 504 | ROk(lm_config) => { 505 | lm_config_str += &format!("\"{lm_name}\": {lm_config},\n") 506 | .lines() 507 | .map(|l| " ".to_owned() + l + "\n") 508 | .collect::(); 509 | match ron::de::from_str(lm_config.as_str()) { 510 | Err(err) => log::warn!("cannot get default config for {lm_name}: {err}"), 511 | Ok(value) => { 512 | base_conf.layout_configs.insert(lm_name, value); 513 | } 514 | } 515 | } 516 | } 517 | } 518 | lm_config_str += " },"; 519 | 520 | // put all the Module configs into base_conf and a string 521 | let mut mod_config_str = String::from("{\n"); 522 | for (mod_name, mod_config) in module_configs { 523 | match mod_config { 524 | ROk(mod_config) => { 525 | log::debug!("string config for {mod_name}: {}", mod_config.as_str()); 526 | mod_config_str += &format!("\"{mod_name}\": {mod_config},\n") 527 | .lines() 528 | .map(|l| " ".to_owned() + l + "\n") 529 | .collect::(); 530 | match ron::de::from_str(mod_config.as_str()) { 531 | Err(err) => log::warn!("cannot get default config for {mod_name}: {err}"), 532 | Ok(value) => { 533 | base_conf.module_config.insert(mod_name, value); 534 | } 535 | } 536 | } 537 | RErr(err) => { 538 | log::warn!("cannot get default config for {mod_name}: {err}"); 539 | } 540 | } 541 | } 542 | mod_config_str += " },"; 543 | let conf_str = conf_str 544 | .replace( 545 | "layout_configs: {},", 546 | &("layout_configs: ".to_owned() + &lm_config_str), 547 | ) 548 | .replace( 549 | "module_config: {},", 550 | &("module_config: ".to_owned() + &mod_config_str), 551 | ); 552 | // check that the generated config is valid 553 | let options = ron::Options::default().with_default_extension(Extensions::IMPLICIT_SOME); 554 | if options.from_str::(&conf_str).is_ok() { 555 | (base_conf, conf_str) 556 | } else { 557 | ( 558 | base_conf.clone(), 559 | "Config".to_owned() + &base_conf.to_string(), 560 | ) 561 | } 562 | } 563 | } 564 | 565 | impl Default for App { 566 | fn default() -> Self { 567 | // let (hdl, shutdown) = get_new_tokio_rt(); 568 | let flags = gtk::gio::ApplicationFlags::default(); 569 | let app = gtk::Application::new(Some("com.github.cr3eperall.dynisland"), flags); 570 | App { 571 | application: app, 572 | module_map: Rc::new(Mutex::new(HashMap::new())), 573 | layout: None, 574 | // producers_handle: hdl, 575 | // producers_shutdown: shutdown, 576 | app_send: None, 577 | config: config::Config::default(), 578 | css_provider: gtk::CssProvider::new(), 579 | config_dir: config::get_default_config_path(), 580 | } 581 | } 582 | } 583 | 584 | fn start_config_dir_watcher( 585 | server_send: tokio::sync::mpsc::UnboundedSender, 586 | config_dir: &Path, 587 | ) -> RecommendedWatcher { 588 | log::info!("starting config watcher"); 589 | let mut watcher = 590 | notify::recommended_watcher(move |res: notify::Result| match res { 591 | Ok(evt) => { 592 | // log::info!("config event: {:?}",evt.kind); 593 | match evt.kind { 594 | notify::EventKind::Modify(notify::event::ModifyKind::Data(_)) => { 595 | log::debug!("Config change detected"); 596 | server_send 597 | .send(BackendServerCommand::ReloadConfig) 598 | .expect("Failed to send notification") 599 | } 600 | notify::EventKind::Create(_) => { 601 | // log::info!("file create event"); 602 | } 603 | _ => {} 604 | } 605 | // log::debug!("{evt:?}"); 606 | } 607 | Err(err) => { 608 | log::error!("Notify watcher error: {err}") 609 | } 610 | }) 611 | .expect("Failed to get file watcher"); 612 | if let Err(err) = watcher.watch(&config_dir, notify::RecursiveMode::NonRecursive) { 613 | log::warn!("Failed to start config file watcher, restart dynisland to get automatic config updates: {err}") 614 | } 615 | watcher 616 | } 617 | 618 | fn start_ipc_server( 619 | runtime_path: std::path::PathBuf, 620 | server_send: tokio::sync::mpsc::UnboundedSender, 621 | mut server_response_recv: tokio::sync::mpsc::UnboundedReceiver>, 622 | ) { 623 | let thread = thread::Builder::new().name("ipc-server".to_string()); 624 | thread 625 | .spawn(move || { 626 | let rt = tokio::runtime::Builder::new_current_thread() 627 | .enable_all() 628 | .build() 629 | .unwrap(); 630 | rt.block_on(async move { 631 | loop { 632 | std::fs::create_dir_all(&runtime_path).expect("invalid runtime path"); 633 | log::info!( 634 | "starting ipc socket at {}", 635 | runtime_path.canonicalize().unwrap().to_str().unwrap() 636 | ); 637 | if let Err(err) = open_socket( 638 | &runtime_path, 639 | server_send.clone(), 640 | &mut server_response_recv, 641 | ) 642 | .await 643 | { 644 | log::error!("socket closed: {err}"); 645 | if matches!( 646 | err.downcast::().unwrap().kind(), 647 | ErrorKind::AddrInUse 648 | ) { 649 | log::error!("app was already started"); 650 | break; 651 | } 652 | } else { 653 | log::info!("kill message received"); 654 | break; 655 | } 656 | } 657 | }); 658 | }) 659 | .expect("failed to spawn file-watcher thread"); 660 | } 661 | --------------------------------------------------------------------------------