diff --git a/src/cached_broadcast.rs b/src/cached_broadcast.rs new file mode 100644 index 0000000..eefff56 --- /dev/null +++ b/src/cached_broadcast.rs @@ -0,0 +1,145 @@ +use crate::{arc_rw, read_lock, send_async, write_lock}; +use std::fmt::Debug; +use std::ops::Deref; +use std::sync::{Arc, RwLock}; +// use std::thread::sleep; +use std::time::Duration; +use tokio::spawn; +use tokio::sync::mpsc; +use tokio::task::spawn_blocking; +use tokio::time::sleep; +use tracing::trace; + +pub trait Cacheable: Debug + Clone + Send + Sync { + type Key: Debug + Clone + Send + Sync + Eq; + + fn get_key(&self) -> Self::Key; +} + +pub type Sender = mpsc::Sender>; +pub type Receiver = mpsc::Receiver>; + +pub struct CachedBroadcastChannel +where + T: Cacheable, +{ + capacity: usize, + data: Vec, + channels: Arc>>>>, + base_tx: mpsc::Sender>, +} + +#[derive(Debug, Clone)] +pub enum Event +where + T: Cacheable, +{ + Add(T), + Remove(T::Key), + Replace(T::Key, T), +} + +impl CachedBroadcastChannel +where + T: Cacheable + 'static, +{ + pub fn new(capacity: usize) -> Self { + let (tx, rx) = mpsc::channel::>(capacity); + let mut rx = DropDetector(rx); + + // spawn_blocking(move || loop { + // let ev = rx.0.try_recv(); + // println!("{ev:?}"); + // sleep(Duration::from_secs(1)) + // }); + + let channels = arc_rw!(Vec::>::new()); + + let channels = Arc::clone(&channels); + spawn(async move { + println!("hello"); + + while let Some(event) = rx.0.recv().await { + println!("ev"); + // trace!("{event:?}"); + // let iter = read_lock!(channels).clone().into_iter(); + // for channel in iter { + // send_async!(channel, event.clone()); + // } + } + println!("goodbye"); + }); + + Self { + capacity, + data: vec![], + channels, + base_tx: tx, + } + } + + pub async fn send(&mut self, event: Event) { + match event.clone() { + Event::Add(data) => { + self.data.push(data); + } + Event::Remove(key) => { + let Some(index) = self.data.iter().position(|t| t.get_key() == key) else { + return; + }; + self.data.remove(index); + } + Event::Replace(key, data) => { + let Some(index) = self.data.iter().position(|t| t.get_key() == key) else { + return; + }; + let _ = std::mem::replace(&mut self.data[index], data); + } + } + + send_async!(self.base_tx, event); + + // let mut closed = vec![]; + // for (i, channel) in read_lock!(self.channels).iter().enumerate() { + // if channel.is_closed() { + // closed.push(i); + // } else { + // send_async!(channel, event.clone()); + // } + // } + // + // for channel in closed.into_iter().rev() { + // write_lock!(self.channels).remove(channel); + // } + } + + pub fn sender(&self) -> mpsc::Sender> { + self.base_tx.clone() + } + + pub fn receiver(&mut self) -> mpsc::Receiver> { + let (tx, rx) = mpsc::channel(self.capacity); + write_lock!(self.channels).push(tx); + + rx + } + + pub fn data(&self) -> &Vec { + &self.data + } +} + +#[derive(Debug)] +struct DropDetector(T); + +impl Drop for DropDetector { + fn drop(&mut self) { + println!("DROPPED") + } +} + +impl Drop for CachedBroadcastChannel { + fn drop(&mut self) { + println!("Channel DROPPED") + } +} diff --git a/src/clients/wayland/client.rs b/src/clients/wayland/client.rs index 2a1a874..8670e09 100644 --- a/src/clients/wayland/client.rs +++ b/src/clients/wayland/client.rs @@ -2,8 +2,9 @@ use super::wlr_foreign_toplevel::handle::ToplevelHandle; use super::wlr_foreign_toplevel::manager::ToplevelManagerState; use super::wlr_foreign_toplevel::ToplevelEvent; use super::Environment; +use crate::cached_broadcast::CachedBroadcastChannel; use crate::error::ERR_CHANNEL_RECV; -use crate::send; +use crate::{cached_broadcast, send}; use cfg_if::cfg_if; use color_eyre::Report; use smithay_client_toolkit::output::{OutputInfo, OutputState}; @@ -31,11 +32,8 @@ cfg_if! { #[derive(Debug)] pub enum Request { - /// Sends a request for all the outputs. - /// These are then sent on the `output` channel. - Outputs, /// Sends a request for all the seats. - /// These are then sent ont the `seat` channel. + /// These are then sent on the `seat` channel. Seats, /// Sends a request for all the toplevels. /// These are then sent on the `toplevel_init` channel. @@ -53,8 +51,10 @@ pub enum Request { pub struct WaylandClient { // External channels + output_channel: CachedBroadcastChannel, toplevel_tx: broadcast::Sender, _toplevel_rx: broadcast::Receiver, + #[cfg(feature = "clipboard")] clipboard_tx: broadcast::Sender>, #[cfg(feature = "clipboard")] @@ -62,7 +62,6 @@ pub struct WaylandClient { // Internal channels toplevel_init_rx: mpsc::Receiver>, - output_rx: mpsc::Receiver>, seat_rx: mpsc::Receiver>, #[cfg(feature = "clipboard")] clipboard_init_rx: mpsc::Receiver>>, @@ -74,10 +73,14 @@ impl WaylandClient { pub(super) fn new() -> Self { let (toplevel_tx, toplevel_rx) = broadcast::channel(32); + let mut output_channel = CachedBroadcastChannel::new(8); + let output_tx = output_channel.sender(); + + let tx2 = output_tx.clone(); + let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel(); #[cfg(feature = "clipboard")] let (clipboard_init_tx, clipboard_init_rx) = mpsc::channel(); - let (output_tx, output_rx) = mpsc::channel(); let (seat_tx, seat_rx) = mpsc::channel(); let toplevel_tx2 = toplevel_tx.clone(); @@ -99,6 +102,7 @@ impl WaylandClient { let conn = Connection::connect_to_env().expect("Failed to connect to Wayland compositor"); + let (globals, queue) = registry_queue_init(&conn).expect("Failed to retrieve Wayland globals"); @@ -139,6 +143,7 @@ impl WaylandClient { handles: HashMap::new(), #[cfg(feature = "clipboard")] clipboard: crate::arc_mut!(None), + output_tx, toplevel_tx, #[cfg(feature = "clipboard")] clipboard_tx, @@ -156,11 +161,6 @@ impl WaylandClient { trace!("{event:?}"); match event { Event::Msg(Request::Roundtrip) => debug!("Received refresh event"), - Event::Msg(Request::Outputs) => { - trace!("Received get outputs request"); - - send!(output_tx, env.output_info()); - } Event::Msg(Request::Seats) => { trace!("Receive get seats request"); send!(seat_tx, env.seats.clone()); @@ -196,12 +196,12 @@ impl WaylandClient { }); Self { + output_channel, toplevel_tx, _toplevel_rx: toplevel_rx, toplevel_init_rx, #[cfg(feature = "clipboard")] clipboard_init_rx, - output_rx, seat_rx, #[cfg(feature = "clipboard")] clipboard_tx, @@ -242,6 +242,10 @@ impl WaylandClient { (rx, data) } + pub fn subscribe_outputs(&mut self) -> cached_broadcast::Receiver { + self.output_channel.receiver() + } + /// Force a roundtrip on the wayland connection, /// flushing any queued events and immediately receiving any new ones. pub fn roundtrip(&self) { @@ -249,11 +253,13 @@ impl WaylandClient { send!(self.request_tx, Request::Roundtrip); } - pub fn get_outputs(&self) -> Vec { - trace!("Sending get outputs request"); - - send!(self.request_tx, Request::Outputs); - self.output_rx.recv().expect(ERR_CHANNEL_RECV) + /// Gets a list of all outputs. + /// + /// This should only be used in a scenario + /// where you need a snapshot of outputs at the current time. + /// Prefer to listen to output events with `subscribe_output` where possible. + pub fn get_outputs(&self) -> &Vec { + self.output_channel.data() } pub fn get_seats(&self) -> Vec { diff --git a/src/clients/wayland/mod.rs b/src/clients/wayland/mod.rs index 014c786..6661a2b 100644 --- a/src/clients/wayland/mod.rs +++ b/src/clients/wayland/mod.rs @@ -6,10 +6,10 @@ mod wl_seat; mod wlr_foreign_toplevel; use self::wlr_foreign_toplevel::manager::ToplevelManagerState; -use crate::{arc_mut, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager}; +use crate::{arc_mut, cached_broadcast, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager}; use cfg_if::cfg_if; use lazy_static::lazy_static; -use smithay_client_toolkit::output::OutputState; +use smithay_client_toolkit::output::{OutputInfo, OutputState}; use smithay_client_toolkit::reexports::calloop::LoopHandle; use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState}; use smithay_client_toolkit::seat::SeatState; @@ -65,6 +65,7 @@ pub struct Environment { #[cfg(feature = "clipboard")] clipboard: Arc>>>, + output_tx: cached_broadcast::Sender, toplevel_tx: broadcast::Sender, #[cfg(feature = "clipboard")] clipboard_tx: broadcast::Sender>, diff --git a/src/clients/wayland/wl_output.rs b/src/clients/wayland/wl_output.rs index 1b3af8d..e2ce1da 100644 --- a/src/clients/wayland/wl_output.rs +++ b/src/clients/wayland/wl_output.rs @@ -1,4 +1,6 @@ use super::Environment; +use crate::cached_broadcast::Cacheable; +use crate::{cached_broadcast, try_send}; use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState}; use tracing::debug; use wayland_client::protocol::wl_output; @@ -31,9 +33,12 @@ impl OutputHandler for Environment { &mut self, _conn: &Connection, _qh: &QueueHandle, - _output: wl_output::WlOutput, + output: wl_output::WlOutput, ) { debug!("Handler received new output"); + if let Some(info) = self.output_state.info(&output) { + try_send!(self.output_tx, cached_broadcast::Event::Add(info)); + }; } fn update_output( @@ -42,14 +47,26 @@ impl OutputHandler for Environment { _qh: &QueueHandle, _output: wl_output::WlOutput, ) { + debug!("Handle received output update"); } fn output_destroyed( &mut self, _conn: &Connection, _qh: &QueueHandle, - _output: wl_output::WlOutput, + output: wl_output::WlOutput, ) { debug!("Handle received output destruction"); + if let Some(info) = self.output_state.info(&output) { + try_send!(self.output_tx, cached_broadcast::Event::Remove(info.id)); + }; + } +} + +impl Cacheable for OutputInfo { + type Key = u32; + + fn get_key(&self) -> Self::Key { + self.id } } diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 5878030..943ed67 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -18,7 +18,7 @@ use crate::ipc::{Command, Response}; use crate::ironvar::get_variable_manager; use crate::modules::PopupButton; use crate::style::load_css; -use crate::{read_lock, send_async, try_send, write_lock, GlobalState}; +use crate::{await_sync, read_lock, send_async, try_send, write_lock, GlobalState}; use super::Ipc; @@ -123,14 +123,7 @@ impl Ipc { Response::Ok } Command::Reload => { - info!("Closing existing bars"); - let windows = application.windows(); - for window in windows { - window.close(); - } - - crate::load_interface(application, global_state); - + await_sync(async move { crate::reload(application, global_state).await }).unwrap(); Response::Ok } Command::Set { key, value } => { diff --git a/src/main.rs b/src/main.rs index 4c47b28..8d5f3e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,10 +14,12 @@ use clap::Parser; use color_eyre::eyre::Result; use color_eyre::Report; use dirs::config_dir; -use gtk::gdk::Display; +use gtk::gdk::{Display, Monitor}; use gtk::prelude::*; use gtk::Application; +use smithay_client_toolkit::output::OutputInfo; use tokio::runtime::Handle; +use tokio::spawn; use tokio::task::{block_in_place, spawn_blocking}; use tracing::{debug, error, info, warn}; use universal_config::ConfigLoader; @@ -25,6 +27,8 @@ use universal_config::ConfigLoader; use clients::wayland; use crate::bar::create_bar; +use crate::bridge_channel::BridgeChannel; +use crate::cached_broadcast::Event; use crate::config::{Config, MonitorConfig}; use crate::error::ExitCode; use crate::global_state::GlobalState; @@ -32,6 +36,7 @@ use crate::style::load_css; mod bar; mod bridge_channel; +mod cached_broadcast; #[cfg(feature = "cli")] mod cli; mod clients; @@ -84,19 +89,22 @@ async fn run_with_args(global_state: Rc>) { Err(err) => error!("{err:?}"), }; } - None => start_ironbar(global_state), + None => start_ironbar(global_state).await, } } -fn start_ironbar(global_state: Rc>) { +async fn start_ironbar(global_state: Rc>) { info!("Ironbar version {}", VERSION); info!("Starting application"); let app = Application::builder().application_id(GTK_APP_ID).build(); - let _ = wayland::get_client(); // force-init + + let output_bridge = BridgeChannel::>::new(); + let output_tx = output_bridge.create_sender(); let running = Rc::new(Cell::new(false)); + let global_state2 = global_state.clone(); app.connect_activate(move |app| { if running.get() { info!("Ironbar already running, returning"); @@ -107,13 +115,11 @@ fn start_ironbar(global_state: Rc>) { cfg_if! { if #[cfg(feature = "ipc")] { - let ipc = ipc::Ipc::new(global_state.clone()); + let ipc = ipc::Ipc::new(global_state2.clone()); ipc.start(app); } } - load_interface(app, &global_state); - let style_path = env::var("IRONBAR_CSS").ok().map_or_else( || { config_dir().map_or_else( @@ -147,27 +153,131 @@ fn start_ironbar(global_state: Rc>) { exit(0); }); - ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel.")) - .expect("Error setting Ctrl-C handler"); + let wc = wayland::get_client(); + + let output_tx = output_tx.clone(); + let mut output_rx = lock!(wc).subscribe_outputs(); + + spawn(async move { + while let Some(event) = output_rx.recv().await { + try_send!(output_tx.clone(), event); + } + }); + + ctrlc::set_handler(move || send!(tx, ())).expect("Error setting Ctrl-C handler"); }); + let config = load_config(); + + { + let app = app.clone(); + let global_state = global_state.clone(); + + output_bridge.recv(move |event: cached_broadcast::Event<_>| { + let display = get_display(); + match event { + Event::Add(output) => { + debug!("Adding bar(s) for monitor {:?}", &output.name); + create_bars_for_monitor(&app, &display, &output, config.clone(), &global_state) + .unwrap(); + } + // TODO: Implement + Event::Remove(_) => {} + Event::Replace(_, _) => {} + } + Continue(true) + }); + } // Ignore CLI args // Some are provided by swaybar_config but not currently supported app.run_with_args(&Vec::<&str>::new()); } -/// Loads the Ironbar config and interface. -pub fn load_interface(app: &Application, global_state: &Rc>) { - let display = Display::default().map_or_else( +/// Closes all current bars and entirely reloads Ironbar. +/// This re-reads the config file. +pub async fn reload(app: &Application, global_state: &Rc>) -> Result<()> { + info!("Closing existing bars"); + let windows = app.windows(); + for window in windows { + window.close(); + } + + let config = load_config(); + + let wl = wayland::get_client(); + let wl = lock!(wl); + let outputs = wl.get_outputs(); + + let display = get_display(); + for output in outputs.iter() { + create_bars_for_monitor(app, &display, output, config.clone(), global_state)?; + } + + Ok(()) +} +fn create_bars_for_monitor( + app: &Application, + display: &Display, + output: &OutputInfo, + config: Config, + global_state: &Rc>, +) -> Result<()> { + let Some(monitor_name) = &output.name else { + return Ok(()); + }; + + let monitor = match get_monitor(&monitor_name, display) { + Ok(monitor) => monitor, + Err(err) => return Err(err), + }; + + let Some(monitor_config) = config.get_monitor_config(&monitor_name) else { + return Ok(()); + }; + + match monitor_config { + MonitorConfig::Single(config) => { + create_bar(&app, &monitor, &monitor_name, config, global_state) + } + MonitorConfig::Multiple(configs) => configs + .into_iter() + .map(|config| create_bar(&app, &monitor, &monitor_name, config, global_state)) + .collect(), + } +} +fn get_display() -> Display { + Display::default().map_or_else( || { let report = Report::msg("Failed to get default GTK display"); error!("{:?}", report); exit(ExitCode::GtkDisplay as i32) }, |display| display, - ); + ) +} - let mut config = env::var("IRONBAR_CONFIG") +fn get_monitor(name: &str, display: &Display) -> Result { + let wl = wayland::get_client(); + let wl = lock!(wl); + let outputs = wl.get_outputs(); + + let monitor = (0..display.n_monitors()).into_iter().find_map(|i| { + let monitor = display.monitor(i)?; + let output = outputs.get(i as usize)?; + + let is_match = output.name.as_ref().map(|n| n == name).unwrap_or_default(); + if is_match { + Some(monitor) + } else { + None + } + }); + + monitor.ok_or_else(|| Report::msg(error::ERR_OUTPUTS)) +} + +fn load_config() -> Config { + let config = env::var("IRONBAR_CONFIG") .map_or_else( |_| ConfigLoader::new("ironbar").find_and_load(), ConfigLoader::load, @@ -182,79 +292,7 @@ pub fn load_interface(app: &Application, global_state: &Rc> }); debug!("Loaded config file"); - - #[cfg(feature = "ipc")] - if let Some(ironvars) = config.ironvar_defaults.take() { - let variable_manager = ironvar::get_variable_manager(); - for (k, v) in ironvars { - if write_lock!(variable_manager).set(k.clone(), v).is_err() { - warn!("Ignoring invalid ironvar: '{k}'"); - } - } - } - - if let Err(err) = create_bars(app, &display, &config, global_state) { - error!("{:?}", err); - exit(ExitCode::CreateBars as i32); - } - - debug!("Created bars"); -} - -/// Creates each of the bars across each of the (configured) outputs. -fn create_bars( - app: &Application, - display: &Display, - config: &Config, - global_state: &Rc>, -) -> Result<()> { - let wl = wayland::get_client(); - let outputs = lock!(wl).get_outputs(); - - debug!("Received {} outputs from Wayland", outputs.len()); - debug!("Outputs: {:?}", outputs); - - let num_monitors = display.n_monitors(); - - for i in 0..num_monitors { - let monitor = display - .monitor(i) - .ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?; - let output = outputs - .get(i as usize) - .ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?; - - let Some(monitor_name) = &output.name else { - continue; - }; - - config.monitors.as_ref().map_or_else( - || { - info!("Creating bar on '{}'", monitor_name); - create_bar(app, &monitor, monitor_name, config.clone(), global_state) - }, - |config| { - let config = config.get(monitor_name); - match &config { - Some(MonitorConfig::Single(config)) => { - info!("Creating bar on '{}'", monitor_name); - create_bar(app, &monitor, monitor_name, config.clone(), global_state) - } - Some(MonitorConfig::Multiple(configs)) => { - for config in configs { - info!("Creating bar on '{}'", monitor_name); - create_bar(app, &monitor, monitor_name, config.clone(), global_state)?; - } - - Ok(()) - } - _ => Ok(()), - } - }, - )?; - } - - Ok(()) + config } /// Blocks on a `Future` until it resolves.