From 25c5ae46c9bc8b3016e92371658a193179338de0 Mon Sep 17 00:00:00 2001 From: Simone Camito Date: Wed, 15 Jan 2025 14:21:42 +0100 Subject: [PATCH 1/3] show workspace only on related monitor --- src/app.rs | 2 +- src/modules/mod.rs | 2 + src/modules/workspaces.rs | 120 ++++++++++++++++++++++---------------- src/outputs.rs | 81 ++++++++++++++++--------- 4 files changed, 127 insertions(+), 78 deletions(-) diff --git a/src/app.rs b/src/app.rs index e27104f..8eb53a4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -28,7 +28,7 @@ use log::{debug, info, warn}; pub struct App { logger: LoggerHandle, pub config: Config, - outputs: Outputs, + pub outputs: Outputs, pub app_launcher: AppLauncher, pub updates: Updates, pub clipboard: Clipboard, diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 1d95f9a..eb4cd57 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -209,6 +209,8 @@ impl App { ModuleName::Updates => self.updates.view(&self.config.updates), ModuleName::Clipboard => self.clipboard.view(&self.config.clipboard_cmd), ModuleName::Workspaces => self.workspaces.view(( + &self.outputs, + id, &self.config.appearance.workspace_colors, self.config.appearance.special_workspace_colors.as_deref(), )), diff --git a/src/modules/workspaces.rs b/src/modules/workspaces.rs index 69fc18b..b496cea 100644 --- a/src/modules/workspaces.rs +++ b/src/modules/workspaces.rs @@ -1,4 +1,4 @@ -use crate::{app, config::AppearanceColor, style::WorkspaceButtonStyle}; +use crate::{app, config::AppearanceColor, outputs::Outputs, style::WorkspaceButtonStyle}; use hyprland::{ dispatch::MonitorIdentifier, event_listener::AsyncEventListener, @@ -8,6 +8,7 @@ use iced::{ alignment, stream::channel, widget::{button, container, text, Row}, + window::Id, Element, Length, Subscription, }; use log::{debug, error}; @@ -23,6 +24,7 @@ pub struct Workspace { pub id: i32, pub name: String, pub monitor_id: Option, + pub monitor: String, pub active: bool, pub windows: u16, } @@ -52,6 +54,7 @@ fn get_workspaces() -> Vec { .last() .map_or_else(|| "".to_string(), |s| s.to_owned()), monitor_id: Some(w.monitor_id as usize), + monitor: w.monitor, active: monitors.iter().any(|m| m.special_workspace.id == w.id), windows: w.windows, }] @@ -63,6 +66,7 @@ fn get_workspaces() -> Vec { id: (current + i) as i32, name: (current + i).to_string(), monitor_id: None, + monitor: "".to_string(), active: false, windows: 0, }); @@ -72,6 +76,7 @@ fn get_workspaces() -> Vec { id: w.id, name: w.name.clone(), monitor_id: Some(w.monitor_id as usize), + monitor: w.monitor, active: Some(w.id) == active.as_ref().map(|a| a.id), windows: w.windows, }); @@ -151,69 +156,84 @@ impl Workspaces { } impl Module for Workspaces { - type ViewData<'a> = (&'a [AppearanceColor], Option<&'a [AppearanceColor]>); + type ViewData<'a> = ( + &'a Outputs, + Id, + &'a [AppearanceColor], + Option<&'a [AppearanceColor]>, + ); type SubscriptionData<'a> = (); fn view( &self, - (workspace_colors, special_workspace_colors): Self::ViewData<'_>, + (outputs, id, workspace_colors, special_workspace_colors): Self::ViewData<'_>, ) -> Option<(Element, Option)> { + let monitor_name = outputs.get_monitor_name(id); + Some(( Into::>::into( Row::with_children( self.workspaces .iter() - .map(|w| { - let empty = w.windows == 0; - let monitor = w.monitor_id; - - let color = monitor.map(|m| { - if w.id > 0 { - workspace_colors.get(m).copied() - } else { - special_workspace_colors - .unwrap_or(workspace_colors) - .get(m) - .copied() - } - }); - - button( - container( - if w.id < 0 { - text(w.name.as_str()) + .filter_map(|w| { + if w.monitor == monitor_name.unwrap_or_else(|| &w.monitor) + || !outputs.has_name(&w.monitor) + { + let empty = w.windows == 0; + let monitor = w.monitor_id; + + let color = monitor.map(|m| { + if w.id > 0 { + workspace_colors.get(m).copied() } else { - text(w.id) + special_workspace_colors + .unwrap_or(workspace_colors) + .get(m) + .copied() } - .size(10), + }); + + Some( + button( + container( + if w.id < 0 { + text(w.name.as_str()) + } else { + text(w.id) + } + .size(10), + ) + .align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center), + ) + .style(WorkspaceButtonStyle(empty, color).into_style()) + .padding(if w.id < 0 { + if w.active { + [0, 16] + } else { + [0, 8] + } + } else { + [0, 0] + }) + .on_press(if w.id > 0 { + Message::ChangeWorkspace(w.id) + } else { + Message::ToggleSpecialWorkspace(w.id) + }) + .width(if w.id < 0 { + Length::Shrink + } else if w.active { + Length::Fixed(32.) + } else { + Length::Fixed(16.) + }) + .height(16) + .into(), ) - .align_x(alignment::Horizontal::Center) - .align_y(alignment::Vertical::Center), - ) - .style(WorkspaceButtonStyle(empty, color).into_style()) - .padding(if w.id < 0 { - if w.active { - [0, 16] - } else { - [0, 8] - } } else { - [0, 0] - }) - .on_press(if w.id > 0 { - Message::ChangeWorkspace(w.id) - } else { - Message::ToggleSpecialWorkspace(w.id) - }) - .width(if w.id < 0 { - Length::Shrink - } else if w.active { - Length::Fixed(32.) - } else { - Length::Fixed(16.) - }) - .height(16) - .into() + None + } }) .collect::>>(), ) diff --git a/src/outputs.rs b/src/outputs.rs index 463721a..d11c33c 100644 --- a/src/outputs.rs +++ b/src/outputs.rs @@ -16,8 +16,6 @@ use crate::{ HEIGHT, }; -static FALLBACK_LAYER: &str = "fallback"; - #[derive(Debug, Clone)] struct ShellInfo { id: Id, @@ -26,7 +24,7 @@ struct ShellInfo { } #[derive(Debug, Clone)] -pub struct Outputs(Vec<(String, Option, Option)>); +pub struct Outputs(Vec<(Option, Option, Option)>); pub enum HasOutput<'a> { Main, @@ -39,7 +37,7 @@ impl Outputs { ( Self(vec![( - FALLBACK_LAYER.to_owned(), + None, Some(ShellInfo { id, menu: Menu::new(menu_id), @@ -91,13 +89,13 @@ impl Outputs { (id, menu_id, Task::batch(vec![task, menu_task])) } - fn name_in_config(name: &str, outputs: &config::Outputs) -> bool { + fn name_in_config(name: Option<&str>, outputs: &config::Outputs) -> bool { match outputs { config::Outputs::All => true, config::Outputs::Active => false, - config::Outputs::Targets(request_outputs) => { - request_outputs.iter().any(|output| output.as_str() == name) - } + config::Outputs::Targets(request_outputs) => request_outputs + .iter() + .any(|output| Some(output.as_str()) == name), } } @@ -117,6 +115,26 @@ impl Outputs { }) } + pub fn get_monitor_name(&self, id: Id) -> Option<&str> { + self.0.iter().find_map(|(name, info, _)| { + if let Some(info) = info { + if info.id == id { + name.as_ref().map(|n| n.as_str()) + } else { + None + } + } else { + None + } + }) + } + + pub fn has_name(&self, name: &str) -> bool { + self.0 + .iter() + .any(|(n, info, _)| info.is_some() && n.as_ref().map(|n| n.as_str()) == Some(name)) + } + pub fn add( &mut self, request_outputs: &config::Outputs, @@ -124,31 +142,34 @@ impl Outputs { name: &str, wl_output: WlOutput, ) -> Task { - let target = Self::name_in_config(name, request_outputs); + let target = Self::name_in_config(Some(name), request_outputs); if target { debug!("Found target output, creating a new layer surface"); let (id, menu_id, task) = Self::create_output_layers(Some(wl_output.clone()), position); - let destroy_task = - if let Some(index) = self.0.iter().position(|(key, _, _)| key == name) { - let old_output = self.0.swap_remove(index); + let destroy_task = if let Some(index) = self + .0 + .iter() + .position(|(key, _, _)| key.as_ref().map(|k| k.as_str()) == Some(name)) + { + let old_output = self.0.swap_remove(index); - if let Some(shell_info) = old_output.1 { - let destroy_main_task = destroy_layer_surface(shell_info.id); - let destroy_menu_task = destroy_layer_surface(shell_info.menu.id); + if let Some(shell_info) = old_output.1 { + let destroy_main_task = destroy_layer_surface(shell_info.id); + let destroy_menu_task = destroy_layer_surface(shell_info.menu.id); - Task::batch(vec![destroy_main_task, destroy_menu_task]) - } else { - Task::none() - } + Task::batch(vec![destroy_main_task, destroy_menu_task]) } else { Task::none() - }; + } + } else { + Task::none() + }; self.0.push(( - name.to_owned(), + Some(name.to_owned()), Some(ShellInfo { id, menu: Menu::new(menu_id), @@ -159,7 +180,7 @@ impl Outputs { // remove fallback layer surface let destroy_fallback_task = - if let Some(index) = self.0.iter().position(|(key, _, _)| key == FALLBACK_LAYER) { + if let Some(index) = self.0.iter().position(|(key, _, _)| key.is_none()) { let old_output = self.0.swap_remove(index); if let Some(shell_info) = old_output.1 { @@ -176,7 +197,7 @@ impl Outputs { Task::batch(vec![destroy_task, destroy_fallback_task, task]) } else { - self.0.push((name.to_owned(), None, Some(wl_output))); + self.0.push((Some(name.to_owned()), None, Some(wl_output))); Task::none() } @@ -214,7 +235,7 @@ impl Outputs { let (id, menu_id, task) = Self::create_output_layers(None, position); self.0.push(( - FALLBACK_LAYER.to_owned(), + None, Some(ShellInfo { id, menu: Menu::new(menu_id), @@ -246,7 +267,9 @@ impl Outputs { .0 .iter() .filter_map(|(name, shell_info, wl_output)| { - if !Self::name_in_config(name, request_outputs) && shell_info.is_some() { + if !Self::name_in_config(name.as_ref().map(|n| n.as_str()), request_outputs) + && shell_info.is_some() + { Some(wl_output.clone()) } else { None @@ -260,7 +283,9 @@ impl Outputs { .0 .iter() .filter_map(|(name, shell_info, wl_output)| { - if Self::name_in_config(name, request_outputs) && shell_info.is_none() { + if Self::name_in_config(name.as_ref().map(|n| n.as_str()), request_outputs) + && shell_info.is_none() + { Some((name.clone(), wl_output.clone())) } else { None @@ -272,7 +297,9 @@ impl Outputs { let mut tasks = Vec::new(); for (name, wl_output) in to_add { if let Some(wl_output) = wl_output { - tasks.push(self.add(request_outputs, position, &name, wl_output)); + if let Some(name) = name { + tasks.push(self.add(request_outputs, position, name.as_str(), wl_output)); + } } } From b38861b58bedc7bba5b6dbc6cdcaf5c2a735123d Mon Sep 17 00:00:00 2001 From: Simone Camito Date: Sun, 19 Jan 2025 15:51:37 +0100 Subject: [PATCH 2/3] add configuration module --- src/app.rs | 3 +- src/config.rs | 19 +++++++++ src/modules/mod.rs | 3 +- src/modules/workspaces.rs | 82 +++++++++++++++++++++++---------------- 4 files changed, 72 insertions(+), 35 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8eb53a4..39e9a80 100644 --- a/src/app.rs +++ b/src/app.rs @@ -68,6 +68,7 @@ impl App { pub fn new((logger, config): (LoggerHandle, Config)) -> impl FnOnce() -> (Self, Task) { || { let (outputs, task) = Outputs::new(config.position); + let enable_workspace_filling = config.workspaces.enable_workspace_filling; ( App { logger, @@ -76,7 +77,7 @@ impl App { app_launcher: AppLauncher, updates: Updates::default(), clipboard: Clipboard, - workspaces: Workspaces::default(), + workspaces: Workspaces::new(enable_workspace_filling), window_title: WindowTitle::default(), system_info: SystemInfo::default(), keyboard_layout: KeyboardLayout::default(), diff --git a/src/config.rs b/src/config.rs index fb3de7d..4772305 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,6 +21,22 @@ pub struct UpdatesModuleConfig { pub update_cmd: String, } +#[derive(Deserialize, Clone, Default, PartialEq, Eq, Debug)] +pub enum WorkspaceVisibilityMode { + #[default] + All, + MonitorSpecific, +} + +#[derive(Deserialize, Clone, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct WorkspacesModuleConfig { + #[serde(default)] + pub visibility_mode: WorkspaceVisibilityMode, + #[serde(default)] + pub enable_workspace_filling: bool, +} + #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct SystemModuleConfig { @@ -342,6 +358,8 @@ pub struct Config { #[serde(default)] pub updates: Option, #[serde(default)] + pub workspaces: WorkspacesModuleConfig, + #[serde(default)] pub system: SystemModuleConfig, #[serde(default)] pub clock: ClockModuleConfig, @@ -370,6 +388,7 @@ impl Default for Config { clipboard_cmd: None, truncate_title_after_length: default_truncate_title_after_length(), updates: None, + workspaces: WorkspacesModuleConfig::default(), system: SystemModuleConfig::default(), clock: ClockModuleConfig::default(), settings: SettingsModuleConfig::default(), diff --git a/src/modules/mod.rs b/src/modules/mod.rs index eb4cd57..021dc62 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -211,6 +211,7 @@ impl App { ModuleName::Workspaces => self.workspaces.view(( &self.outputs, id, + &self.config.workspaces, &self.config.appearance.workspace_colors, self.config.appearance.special_workspace_colors.as_deref(), )), @@ -234,7 +235,7 @@ impl App { .as_ref() .and_then(|updates_config| self.updates.subscription(updates_config)), ModuleName::Clipboard => self.clipboard.subscription(()), - ModuleName::Workspaces => self.workspaces.subscription(()), + ModuleName::Workspaces => self.workspaces.subscription(&self.config.workspaces), ModuleName::WindowTitle => self.window_title.subscription(()), ModuleName::SystemInfo => self.system_info.subscription(()), ModuleName::KeyboardLayout => self.keyboard_layout.subscription(()), diff --git a/src/modules/workspaces.rs b/src/modules/workspaces.rs index b496cea..806d499 100644 --- a/src/modules/workspaces.rs +++ b/src/modules/workspaces.rs @@ -1,4 +1,10 @@ -use crate::{app, config::AppearanceColor, outputs::Outputs, style::WorkspaceButtonStyle}; +use super::{Module, OnModulePress}; +use crate::{ + app, + config::{AppearanceColor, WorkspaceVisibilityMode, WorkspacesModuleConfig}, + outputs::Outputs, + style::WorkspaceButtonStyle, +}; use hyprland::{ dispatch::MonitorIdentifier, event_listener::AsyncEventListener, @@ -17,8 +23,6 @@ use std::{ sync::{Arc, RwLock}, }; -use super::{Module, OnModulePress}; - #[derive(Debug, Clone)] pub struct Workspace { pub id: i32, @@ -29,7 +33,7 @@ pub struct Workspace { pub windows: u16, } -fn get_workspaces() -> Vec { +fn get_workspaces(enable_workspace_filling: bool) -> Vec { let active = hyprland::data::Workspace::get_active().ok(); let monitors = hyprland::data::Monitors::get() .map(|m| m.to_vec()) @@ -61,17 +65,21 @@ fn get_workspaces() -> Vec { } else { let missing: usize = w.id as usize - current; let mut res = Vec::with_capacity(missing + 1); - for i in 0..missing { - res.push(Workspace { - id: (current + i) as i32, - name: (current + i).to_string(), - monitor_id: None, - monitor: "".to_string(), - active: false, - windows: 0, - }); + + if enable_workspace_filling { + for i in 0..missing { + res.push(Workspace { + id: (current + i) as i32, + name: (current + i).to_string(), + monitor_id: None, + monitor: "".to_string(), + active: false, + windows: 0, + }); + } + current += missing + 1; } - current += missing + 1; + res.push(Workspace { id: w.id, name: w.name.clone(), @@ -91,10 +99,10 @@ pub struct Workspaces { workspaces: Vec, } -impl Default for Workspaces { - fn default() -> Self { +impl Workspaces { + pub fn new(enable_workspace_filling: bool) -> Self { Self { - workspaces: get_workspaces(), + workspaces: get_workspaces(enable_workspace_filling), } } } @@ -159,14 +167,15 @@ impl Module for Workspaces { type ViewData<'a> = ( &'a Outputs, Id, + &'a WorkspacesModuleConfig, &'a [AppearanceColor], Option<&'a [AppearanceColor]>, ); - type SubscriptionData<'a> = (); + type SubscriptionData<'a> = &'a WorkspacesModuleConfig; fn view( &self, - (outputs, id, workspace_colors, special_workspace_colors): Self::ViewData<'_>, + (outputs, id, config, workspace_colors, special_workspace_colors): Self::ViewData<'_>, ) -> Option<(Element, Option)> { let monitor_name = outputs.get_monitor_name(id); @@ -176,7 +185,8 @@ impl Module for Workspaces { self.workspaces .iter() .filter_map(|w| { - if w.monitor == monitor_name.unwrap_or_else(|| &w.monitor) + if config.visibility_mode == WorkspaceVisibilityMode::All + || w.monitor == monitor_name.unwrap_or_else(|| &w.monitor) || !outputs.has_name(&w.monitor) { let empty = w.windows == 0; @@ -245,13 +255,17 @@ impl Module for Workspaces { )) } - fn subscription(&self, _: Self::SubscriptionData<'_>) -> Option> { + fn subscription( + &self, + config: Self::SubscriptionData<'_>, + ) -> Option> { let id = TypeId::of::(); + let enable_workspace_filling = config.enable_workspace_filling; Some( Subscription::run_with_id( - id, - channel(10, |output| async move { + format!("{:?}-{}", id, enable_workspace_filling), + channel(10, move |output| async move { let output = Arc::new(RwLock::new(output)); loop { let mut event_listener = AsyncEventListener::new(); @@ -264,7 +278,9 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces( + enable_workspace_filling, + ))) .expect( "error getting workspaces: workspace added event", ); @@ -281,7 +297,7 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces(enable_workspace_filling))) .expect( "error getting workspaces: workspace change event", ); @@ -298,7 +314,7 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces(enable_workspace_filling))) .expect( "error getting workspaces: workspace destroy event", ); @@ -315,7 +331,7 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces(enable_workspace_filling))) .expect( "error getting workspaces: workspace moved event", ); @@ -332,7 +348,7 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces(enable_workspace_filling))) .expect( "error getting workspaces: special workspace change event", ); @@ -349,7 +365,7 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces(enable_workspace_filling))) .expect( "error getting workspaces: special workspace removed event", ); @@ -365,7 +381,7 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces(enable_workspace_filling))) .expect("error getting workspaces: window close event"); } }) @@ -379,7 +395,7 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces(enable_workspace_filling))) .expect("error getting workspaces: window open event"); } }) @@ -393,7 +409,7 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces(enable_workspace_filling))) .expect("error getting workspaces: window moved event"); } }) @@ -407,7 +423,7 @@ impl Module for Workspaces { Box::pin(async move { if let Ok(mut output) = output.write() { output - .try_send(Message::WorkspacesChanged(get_workspaces())) + .try_send(Message::WorkspacesChanged(get_workspaces(enable_workspace_filling))) .expect( "error getting workspaces: active monitor change event", ); From da21b51d9af7becb58e0170d66a3367db46d60ce Mon Sep 17 00:00:00 2001 From: Simone Camito Date: Sun, 19 Jan 2025 16:01:54 +0100 Subject: [PATCH 3/3] update changelog and readme --- CHANGELOG.md | 1 + README.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c2a38d..920ee5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Multi monitor support - Tray module - Dynamic modules system configuration +- New workspace module configuration ### Changed diff --git a/README.md b/README.md index b439cf2..1fc7b39 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,18 @@ updates: # optional, default None # Maximum number of chars that can be present in the window title # after that the title will be truncated truncateTitleAfterLength: 150 # optional, default 150 +# Workspaces module configuration, optional +workspaces: + # The visibility mode of the workspaces, possible values are: + # All: all the workspaces will be displayed + # MonitorSpecific: only the workspaces of the related monitor will be displayed + visibilityMode: All # optional, default All + # Enable filling with empty workspaces + # For example: + # With this flag set to true if there are only 2 workspaces, + # the workspace 1 and the workspace 4, the module will show also + # two more workspaces, the workspace 2 and the workspace 3 + enableWorkspaceFilling: false # optional, default false # The system module configuration system: cpuWarnThreshold: 6O # cpu indicator warning level (default 60)