use crate::bar::apply_theme; use crate::config::KomobarTheme; use crate::ui::CustomUi; use crate::widget::BarWidget; use crate::MAX_LABEL_WIDTH; use crate::WIDGET_SPACING; use crossbeam_channel::Receiver; use crossbeam_channel::TryRecvError; use eframe::egui::text::LayoutJob; use eframe::egui::Color32; use eframe::egui::ColorImage; use eframe::egui::Context; use eframe::egui::FontId; use eframe::egui::Image; use eframe::egui::Label; use eframe::egui::SelectableLabel; use eframe::egui::Sense; use eframe::egui::TextStyle; use eframe::egui::TextureHandle; use eframe::egui::TextureOptions; use eframe::egui::Ui; use eframe::egui::Vec2; use image::RgbaImage; use komorebi_client::CycleDirection; use komorebi_client::NotificationEvent; use komorebi_client::Rect; use komorebi_client::SocketMessage; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::cell::RefCell; use std::collections::BTreeMap; use std::fmt::Display; use std::fmt::Formatter; use std::path::PathBuf; use std::rc::Rc; use std::sync::atomic::Ordering; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KomorebiConfig { /// Configure the Workspaces widget pub workspaces: KomorebiWorkspacesConfig, /// Configure the Layout widget pub layout: Option, /// Configure the Focused Window widget pub focused_window: Option, /// Configure the Configuration Switcher widget pub configuration_switcher: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KomorebiWorkspacesConfig { /// Enable the Komorebi Workspaces widget pub enable: bool, /// Hide workspaces without any windows pub hide_empty_workspaces: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KomorebiLayoutConfig { /// Enable the Komorebi Layout widget pub enable: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KomorebiFocusedWindowConfig { /// Enable the Komorebi Focused Window widget pub enable: bool, /// Show the icon of the currently focused window pub show_icon: bool, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KomorebiConfigurationSwitcherConfig { /// Enable the Komorebi Configurations widget pub enable: bool, /// A map of display friendly name => path to configuration.json pub configurations: BTreeMap, } impl From<&KomorebiConfig> for Komorebi { fn from(value: &KomorebiConfig) -> Self { let configuration_switcher = if let Some(configuration_switcher) = &value.configuration_switcher { let mut configuration_switcher = configuration_switcher.clone(); for (_, location) in configuration_switcher.configurations.iter_mut() { *location = dunce::simplified(&PathBuf::from(location.clone())) .to_string_lossy() .to_string(); } Some(configuration_switcher) } else { None }; Self { komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState { selected_workspace: String::new(), layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP), workspaces: vec![], hide_empty_workspaces: value.workspaces.hide_empty_workspaces, mouse_follows_focus: true, work_area_offset: None, focused_container_information: (vec![], vec![], 0), stack_accent: None, })), workspaces: value.workspaces, layout: value.layout, focused_window: value.focused_window, configuration_switcher, } } } #[derive(Clone, Debug)] pub struct Komorebi { pub komorebi_notification_state: Rc>, pub workspaces: KomorebiWorkspacesConfig, pub layout: Option, pub focused_window: Option, pub configuration_switcher: Option, } impl BarWidget for Komorebi { fn render(&mut self, ctx: &Context, ui: &mut Ui) { let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut(); if self.workspaces.enable { let mut update = None; for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() { if ui .add(SelectableLabel::new( komorebi_notification_state.selected_workspace.eq(ws), ws.to_string(), )) .clicked() { update = Some(ws.to_string()); let mut proceed = true; if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false)) .is_err() { tracing::error!("could not send message to komorebi: MouseFollowsFocus"); proceed = false; } if proceed && komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i)) .is_err() { tracing::error!("could not send message to komorebi: FocusWorkspaceNumber"); proceed = false; } if proceed && komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( komorebi_notification_state.mouse_follows_focus, )) .is_err() { tracing::error!("could not send message to komorebi: MouseFollowsFocus"); proceed = false; } if proceed && komorebi_client::send_message(&SocketMessage::Retile).is_err() { tracing::error!("could not send message to komorebi: Retile"); } } } if let Some(update) = update { komorebi_notification_state.selected_workspace = update; } ui.add_space(WIDGET_SPACING); } if let Some(layout) = self.layout { if layout.enable { if ui .add( Label::new(komorebi_notification_state.layout.to_string()) .selectable(false) .sense(Sense::click()), ) .clicked() { match komorebi_notification_state.layout { KomorebiLayout::Default(_) => { if komorebi_client::send_message(&SocketMessage::CycleLayout( CycleDirection::Next, )) .is_err() { tracing::error!("could not send message to komorebi: CycleLayout"); } } KomorebiLayout::Floating => { if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() { tracing::error!("could not send message to komorebi: ToggleTiling"); } } KomorebiLayout::Paused => { if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() { tracing::error!("could not send message to komorebi: TogglePause"); } } KomorebiLayout::Custom => {} } } ui.add_space(WIDGET_SPACING); } } if let Some(configuration_switcher) = &self.configuration_switcher { if configuration_switcher.enable { for (name, location) in configuration_switcher.configurations.iter() { let path = PathBuf::from(location); if path.is_file() && ui .add(Label::new(name).selectable(false).sense(Sense::click())) .clicked() { let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path); let mut proceed = true; if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration( canonicalized, )) .is_err() { tracing::error!( "could not send message to komorebi: ReplaceConfiguration" ); proceed = false; } if let Some(rect) = komorebi_notification_state.work_area_offset { if proceed { match komorebi_client::send_query(&SocketMessage::Query( komorebi_client::StateQuery::FocusedMonitorIndex, )) { Ok(idx) => { if let Ok(monitor_idx) = idx.parse::() { if komorebi_client::send_message( &SocketMessage::MonitorWorkAreaOffset( monitor_idx, rect, ), ) .is_err() { tracing::error!( "could not send message to komorebi: MonitorWorkAreaOffset" ); } } } Err(_) => { tracing::error!( "could not send message to komorebi: Query" ); } } } } } } ui.add_space(WIDGET_SPACING); } } if let Some(focused_window) = self.focused_window { if focused_window.enable { let titles = &komorebi_notification_state.focused_container_information.0; let icons = &komorebi_notification_state.focused_container_information.1; let focused_window_idx = komorebi_notification_state.focused_container_information.2; let iter = titles.iter().zip(icons.iter()); for (i, (title, icon)) in iter.enumerate() { if focused_window.show_icon { if let Some(img) = icon { ui.add( Image::from(&img_to_texture(ctx, img)) .maintain_aspect_ratio(true) .max_height(15.0), ); } } if i == focused_window_idx { let font_id = ctx .style() .text_styles .get(&TextStyle::Body) .cloned() .unwrap_or_else(FontId::default); let layout_job = LayoutJob::simple( title.to_string(), font_id.clone(), komorebi_notification_state .stack_accent .unwrap_or(ctx.style().visuals.selection.stroke.color), 100.0, ); if titles.len() > 1 { let available_height = ui.available_height(); let mut custom_ui = CustomUi(ui); custom_ui.add_sized_left_to_right( Vec2::new( MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, available_height, ), Label::new(layout_job).selectable(false).truncate(), ); } else { let available_height = ui.available_height(); let mut custom_ui = CustomUi(ui); custom_ui.add_sized_left_to_right( Vec2::new( MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, available_height, ), Label::new(title).selectable(false).truncate(), ); } } else { let available_height = ui.available_height(); let mut custom_ui = CustomUi(ui); if custom_ui .add_sized_left_to_right( Vec2::new( MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32, available_height, ), Label::new(title) .selectable(false) .sense(Sense::click()) .truncate(), ) .clicked() { if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( false, )) .is_err() { tracing::error!( "could not send message to komorebi: MouseFollowsFocus" ); } if komorebi_client::send_message(&SocketMessage::FocusStackWindow(i)) .is_err() { tracing::error!( "could not send message to komorebi: FocusStackWindow" ); } if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( komorebi_notification_state.mouse_follows_focus, )) .is_err() { tracing::error!( "could not send message to komorebi: MouseFollowsFocus" ); } } } ui.add_space(WIDGET_SPACING); } } ui.add_space(WIDGET_SPACING); } } } fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle { let size = [rgba_image.width() as usize, rgba_image.height() as usize]; let pixels = rgba_image.as_flat_samples(); let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()); ctx.load_texture("icon", color_image, TextureOptions::default()) } #[derive(Clone, Debug)] pub struct KomorebiNotificationState { pub workspaces: Vec, pub selected_workspace: String, pub focused_container_information: (Vec, Vec>, usize), pub layout: KomorebiLayout, pub hide_empty_workspaces: bool, pub mouse_follows_focus: bool, pub work_area_offset: Option, pub stack_accent: Option, } #[derive(Copy, Clone, Debug)] pub enum KomorebiLayout { Default(komorebi_client::DefaultLayout), Floating, Paused, Custom, } impl Display for KomorebiLayout { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { KomorebiLayout::Default(layout) => write!(f, "{layout}"), KomorebiLayout::Floating => write!(f, "Floating"), KomorebiLayout::Paused => write!(f, "Paused"), KomorebiLayout::Custom => write!(f, "Custom"), } } } impl KomorebiNotificationState { pub fn update_from_config(&mut self, config: &Self) { self.hide_empty_workspaces = config.hide_empty_workspaces; } pub fn handle_notification( &mut self, ctx: &Context, monitor_index: usize, rx_gui: Receiver, bg_color: Rc>, ) { match rx_gui.try_recv() { Err(error) => match error { TryRecvError::Empty => {} TryRecvError::Disconnected => { tracing::error!( "failed to receive komorebi notification on gui thread: {error}" ); } }, Ok(notification) => { if let NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(path)) = notification.event { if let Ok(config) = komorebi_client::StaticConfig::read(&path) { if let Some(theme) = config.theme { apply_theme(ctx, KomobarTheme::from(theme), bg_color); tracing::info!("applied theme from updated komorebi.json"); } } } self.mouse_follows_focus = notification.state.mouse_follows_focus; let monitor = ¬ification.state.monitors.elements()[monitor_index]; self.work_area_offset = notification.state.monitors.elements()[monitor_index].work_area_offset(); let focused_workspace_idx = monitor.focused_workspace_idx(); let mut workspaces = vec![]; self.selected_workspace = monitor.workspaces()[focused_workspace_idx] .name() .to_owned() .unwrap_or_else(|| format!("{}", focused_workspace_idx + 1)); for (i, ws) in monitor.workspaces().iter().enumerate() { let should_add = if self.hide_empty_workspaces { focused_workspace_idx == i || !ws.containers().is_empty() } else { true }; if should_add { workspaces .push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1))); } } self.workspaces = workspaces; self.layout = match monitor.workspaces()[focused_workspace_idx].layout() { komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout), komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom, }; if !*monitor.workspaces()[focused_workspace_idx].tile() { self.layout = KomorebiLayout::Floating; } if notification.state.is_paused { self.layout = KomorebiLayout::Paused; } if let Some(container) = monitor.workspaces()[focused_workspace_idx].monocle_container() { self.focused_container_information = ( container .windows() .iter() .map(|w| w.title().unwrap_or_default()) .collect::>(), container .windows() .iter() .map(|w| windows_icons::get_icon_by_process_id(w.process_id())) .collect::>(), container.focused_window_idx(), ); } else if let Some(container) = monitor.workspaces()[focused_workspace_idx].focused_container() { self.focused_container_information = ( container .windows() .iter() .map(|w| w.title().unwrap_or_default()) .collect::>(), container .windows() .iter() .map(|w| windows_icons::get_icon_by_process_id(w.process_id())) .collect::>(), container.focused_window_idx(), ); } else { self.focused_container_information.0.clear(); self.focused_container_information.1.clear(); self.focused_container_information.2 = 0; } } } } }