From 1851c56e407cac72ca104cc5e75692cd9c648986 Mon Sep 17 00:00:00 2001 From: MaxVerevkin Date: Mon, 8 Aug 2022 23:12:23 +0300 Subject: rewrite using latest walayd libs & implement HiDPI --- src/bar.rs | 412 ++++++++++++++++++++ src/bar_state.rs | 651 -------------------------------- src/config.rs | 5 +- src/i3bar_protocol.rs | 3 +- src/main.rs | 198 ++-------- src/river_protocols.rs | 38 +- src/river_protocols/control.rs | 89 +++++ src/river_protocols/control/dispatch.rs | 70 ++++ src/river_protocols/status.rs | 134 +++++++ src/river_protocols/status/dispatch.rs | 86 +++++ src/shared_state.rs | 18 + src/state.rs | 430 +++++++++++++++++++++ src/status_cmd.rs | 71 ++-- 13 files changed, 1323 insertions(+), 882 deletions(-) create mode 100644 src/bar.rs delete mode 100644 src/bar_state.rs create mode 100644 src/river_protocols/control.rs create mode 100644 src/river_protocols/control/dispatch.rs create mode 100644 src/river_protocols/status.rs create mode 100644 src/river_protocols/status/dispatch.rs create mode 100644 src/shared_state.rs create mode 100644 src/state.rs (limited to 'src') diff --git a/src/bar.rs b/src/bar.rs new file mode 100644 index 0000000..f3dc95c --- /dev/null +++ b/src/bar.rs @@ -0,0 +1,412 @@ +use std::collections::BinaryHeap; + +use pangocairo::cairo; +use smithay_client_toolkit::{ + reexports::client::protocol::{wl_seat, wl_shm}, + shell::layer::LayerSurface, +}; + +use crate::{ + button_manager::ButtonManager, + config::Config, + i3bar_protocol::{self, Block, MinWidth}, + ord_adaptor::DefaultLess, + pointer_btn::PointerBtn, + river_protocols::{control::RiverControlState, status::RiverOutputStatus}, + shared_state::SharedState, + state::ComputedBlock, + tags::{compute_tag_label, TagState, TagsInfo}, + text::{self, ComputedText, RenderOptions}, +}; + +pub struct Bar { + pub first_configure: bool, + pub width: u32, + pub height: u32, + pub scale: i32, + pub layer: LayerSurface, + pub blocks_btns: ButtonManager<(Option, Option)>, + pub river_output_status: Option, + pub river_control: RiverControlState, + pub tags_btns: ButtonManager, + pub tags_info: TagsInfo, + pub tags_computed: Vec, +} + +impl Bar { + pub fn configure(&mut self, ss: &mut SharedState, width: u32) { + if self.width != width { + self.width = width; + self.layer.set_exclusive_zone(self.height as i32); + } + + self.first_configure = false; + self.draw(ss); + } + + pub fn has_tags_provider(&self) -> bool { + // TODO: add more tags providers + self.river_output_status.is_some() + } + + pub fn click( + &mut self, + ss: &mut SharedState, + button: PointerBtn, + seat: &wl_seat::WlSeat, + x: f64, + _y: f64, + ) -> anyhow::Result<()> { + if let Some(tag) = self.tags_btns.click(x) { + if self.river_control.is_available() { + let cmd = match button { + PointerBtn::Left => "set-focused-tags", + PointerBtn::Right => "toggle-focused-tags", + _ => return Ok(()), + }; + self.river_control.run_command( + &ss.qh, + seat, + [cmd.into(), (1u32 << tag).to_string()], + )?; + } + } else if let Some((name, instance)) = self.blocks_btns.click(x) { + if let Some(cmd) = &mut ss.status_cmd { + cmd.send_click_event(&i3bar_protocol::Event { + name: name.as_deref(), + instance: instance.as_deref(), + button, + ..Default::default() + })?; + } + } + Ok(()) + } + + pub fn draw(&mut self, ss: &mut SharedState) { + if self.first_configure { + return; + } + + let stride = 4 * self.width as i32; + let width = self.width as i32; + let height = self.height as i32; + let width_f = width as f64; + let height_f = height as f64; + + let (buffer, canvas) = ss + .pool + .create_buffer( + width * self.scale, + height * self.scale, + stride * self.scale, + wl_shm::Format::Argb8888, + ) + .expect("create buffer"); + + let cairo_surf = unsafe { + cairo::ImageSurface::create_for_data_unsafe( + canvas.as_mut_ptr(), + cairo::Format::ARgb32, + width * self.scale, + height * self.scale, + stride * self.scale, + ) + .expect("cairo surface") + }; + + let cairo_ctx = cairo::Context::new(&cairo_surf).expect("cairo context"); + cairo_ctx.scale(self.scale as f64, self.scale as f64); + self.layer.wl_surface().set_buffer_scale(self.scale); + + // Background + ss.config.background.apply(&cairo_ctx); + cairo_ctx.paint().expect("cairo paint"); + + // Display tags + let mut offset_left = 0.0; + if self.has_tags_provider() { + if self.tags_computed.is_empty() { + let mut x_offset = 0.0; + // TODO make configurable + for (id, text) in ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + .iter() + .enumerate() + { + let tag = + compute_tag_label(text.to_string(), ss.config.font.clone(), &cairo_ctx); + self.tags_btns.push(x_offset, tag.width, id); + x_offset += tag.width; + self.tags_computed.push(tag); + } + } + for (i, label) in self.tags_computed.iter().enumerate() { + let state = self.tags_info.get_state(i); + let (bg, fg) = match state { + TagState::Focused => (ss.config.tag_focused_bg, ss.config.tag_focused_fg), + TagState::Inactive => (ss.config.tag_bg, ss.config.tag_fg), + TagState::Urgent => (ss.config.tag_urgent_bg, ss.config.tag_urgent_fg), + }; + label.render( + &cairo_ctx, + RenderOptions { + x_offset: offset_left, + bar_height: height_f, + fg_color: fg, + bg_color: Some(bg), + r_left: if i == 0 || self.tags_info.get_state(i.saturating_sub(1)) != state + { + ss.config.tags_r + } else { + 0.0 + }, + r_right: if i == 8 || self.tags_info.get_state(i + 1) != state { + ss.config.tags_r + } else { + 0.0 + }, + overlap: 0.0, + }, + ); + offset_left += label.width; + } + } + + // Display the blocks + render_blocks( + &cairo_ctx, + &ss.config, + &ss.blocks, + &mut ss.blocks_cache, + &mut self.blocks_btns, + offset_left, + width_f, + height_f, + ); + + // Attach the buffer to the surface and mark the entire surface as damaged + buffer + .attach_to(self.layer.wl_surface()) + .expect("attaching buffer"); + self.layer + .wl_surface() + .damage_buffer(0, 0, width * self.scale, height * self.scale); + + // Finally, commit the surface + self.layer.wl_surface().commit(); + } +} + +#[allow(clippy::too_many_arguments)] +fn render_blocks( + context: &cairo::Context, + config: &Config, + blocks: &[Block], + blocks_cache: &mut Vec, + buttons: &mut ButtonManager<(Option, Option)>, + offset_left: f64, + full_width: f64, + full_height: f64, +) { + let comp_min_width = |block: &Block| { + let markup = block.markup.as_deref() == Some("pango"); + match &block.min_width { + Some(MinWidth::Pixels(p)) => Some(*p as f64), + Some(MinWidth::Text(t)) => Some(text::width_of(t, context, markup, &config.font.0)), + None => None, + } + }; + let comp_full = |block: &Block, min_width: Option| { + let markup = block.markup.as_deref() == Some("pango"); + text::Text { + text: block.full_text.clone(), + attr: text::Attributes { + font: config.font.clone(), + padding_left: 0.0, + padding_right: 0.0, + min_width, + align: block.align.unwrap_or_default(), + markup, + }, + } + .compute(context) + }; + let comp_short = |block: &Block, min_width: Option| { + let markup = block.markup.as_deref() == Some("pango"); + block.short_text.as_ref().map(|short_text| { + text::Text { + text: short_text.clone(), + attr: text::Attributes { + font: config.font.clone(), + padding_left: 0.0, + padding_right: 0.0, + min_width, + align: block.align.unwrap_or_default(), + markup, + }, + } + .compute(context) + }) + }; + + // update cashe + if blocks.len() != blocks_cache.len() { + blocks_cache.clear(); + for block in blocks { + let mw = comp_min_width(block); + blocks_cache.push(ComputedBlock { + block: block.clone(), + full: comp_full(block, mw), + short: comp_short(block, mw), + min_width: mw, + }); + } + } else { + for (block, computed) in blocks.iter().zip(blocks_cache.iter_mut()) { + if block.min_width != computed.block.min_width || block.markup != computed.block.markup + { + let mw = comp_min_width(block); + *computed = ComputedBlock { + block: block.clone(), + full: comp_full(block, mw), + short: comp_short(block, mw), + min_width: mw, + }; + } else { + if block.full_text != computed.block.full_text { + computed.block.full_text = block.full_text.clone(); + computed.full = comp_full(block, computed.min_width); + } + if block.short_text != computed.block.short_text { + computed.block.full_text = block.full_text.clone(); + computed.short = comp_short(block, computed.min_width); + } + computed.block.color = block.color; + computed.block.background = block.background; + computed.block.align = block.align; + computed.block.name = block.name.clone(); + computed.block.instance = block.instance.clone(); + computed.block.separator = block.separator; + computed.block.separator_block_width = block.separator_block_width; + } + } + } + + #[derive(Debug)] + struct LogialBlock<'a> { + blocks: Vec<(&'a ComputedBlock, bool)>, + delta: f64, + separator: bool, + separator_block_width: u8, + } + + let mut blocks_computed = Vec::new(); + let mut blocks_width = 0.0; + let mut s_start = 0; + while s_start < blocks.len() { + let mut s_end = s_start + 1; + let series_name = &blocks[s_start].name; + while s_end < blocks.len() + && blocks[s_end - 1].separator_block_width == 0 + && &blocks[s_end].name == series_name + { + s_end += 1; + } + + let mut series = LogialBlock { + blocks: Vec::with_capacity(s_end - s_start), + delta: 0.0, + separator: blocks[s_end - 1].separator, + separator_block_width: blocks[s_end - 1].separator_block_width, + }; + + for comp in &blocks_cache[s_start..s_end] { + blocks_width += comp.full.width; + if let Some(short) = &comp.short { + series.delta += comp.full.width - short.width; + } + series.blocks.push((comp, false)); + } + if s_end != blocks.len() { + blocks_width += series.separator_block_width as f64; + } + blocks_computed.push(series); + s_start = s_end; + } + + // Progressively switch to short mode + if offset_left + blocks_width > full_width { + let mut heap = BinaryHeap::new(); + for (i, b) in blocks_computed.iter().enumerate() { + if b.delta > 0.0 { + heap.push((DefaultLess(b.delta), i)); + } + } + while let Some((DefaultLess(delta), to_switch)) = heap.pop() { + for comp in &mut blocks_computed[to_switch].blocks { + comp.1 = true; + } + blocks_width -= delta; + if offset_left + blocks_width <= full_width { + break; + } + } + } + + // Remove all the empy blocks + for s in &mut blocks_computed { + s.blocks.retain(|(text, is_short)| { + (*is_short + && text + .short + .as_ref() + .map_or(text.full.width > 0.0, |s| s.width > 0.0)) + || (!is_short && text.full.width > 0.0) + }); + } + + // Render blocks + buttons.clear(); + let mut j = 0; + for series in blocks_computed { + let s_len = series.blocks.len(); + for (i, (computed, is_short)) in series.blocks.into_iter().enumerate() { + let block = &computed.block; + let to_render = if is_short { + computed.short.as_ref().unwrap_or(&computed.full) + } else { + &computed.full + }; + j += 1; + to_render.render( + context, + RenderOptions { + x_offset: full_width - blocks_width, + bar_height: full_height, + fg_color: block.color.unwrap_or(config.color), + bg_color: block.background, + r_left: if i == 0 { config.blocks_r } else { 0.0 }, + r_right: if i + 1 == s_len { config.blocks_r } else { 0.0 }, + overlap: config.blocks_overlap, + }, + ); + buttons.push( + full_width - blocks_width, + to_render.width, + (block.name.clone(), block.instance.clone()), + ); + blocks_width -= to_render.width; + } + if j != blocks.len() && series.separator_block_width > 0 { + let w = series.separator_block_width as f64; + if series.separator && config.separator_width > 0.0 { + config.separator.apply(context); + context.set_line_width(config.separator_width); + context.move_to(full_width - blocks_width + w * 0.5, full_height * 0.1); + context.line_to(full_width - blocks_width + w * 0.5, full_height * 0.9); + context.stroke().unwrap(); + } + blocks_width -= w; + } + } +} diff --git a/src/bar_state.rs b/src/bar_state.rs deleted file mode 100644 index 55705da..0000000 --- a/src/bar_state.rs +++ /dev/null @@ -1,651 +0,0 @@ -use std::cell::RefCell; -use std::collections::BinaryHeap; -use std::io; -use std::rc::Rc; - -use smithay_client_toolkit::{ - environment::Environment, - reexports::{ - client::protocol::{wl_output::WlOutput, wl_seat::WlSeat, wl_shm, wl_surface::WlSurface}, - client::{Attached, Main}, - protocols::wlr::unstable::layer_shell::v1::client::{ - zwlr_layer_shell_v1, zwlr_layer_surface_v1, - }, - }, - shm::AutoMemPool, -}; - -use pangocairo::cairo; - -use crate::button_manager::ButtonManager; -use crate::config::Config; -use crate::i3bar_protocol::{self, Block, MinWidth}; -use crate::ord_adaptor::DefaultLess; -use crate::pointer_btn::PointerBtn; -use crate::river_protocols::zriver_command_callback_v1; -use crate::river_protocols::zriver_control_v1; -use crate::river_protocols::zriver_output_status_v1; -use crate::river_protocols::zriver_status_manager_v1; -use crate::status_cmd::StatusCmd; -use crate::tags::{compute_tag_label, TagState, TagsInfo}; -use crate::text; -use crate::text::{ComputedText, RenderOptions}; -use crate::Env; - -pub struct BarState { - pub status_cmd: Option, - config: Rc>, - blocks: Vec, - blocks_cache: Vec, - surfaces: Vec, - layer_shell: Attached, - river_status: Option>, - river_control: Option>, -} - -impl BarState { - pub fn new(env: &Environment) -> Self { - let mut error = Ok(()); - - let config = Config::new() - .map_err(|e| error = Err(e)) - .unwrap_or_default(); - - let status_cmd = match &error { - Err(_) => None, - Ok(()) => config.command.as_deref().and_then(|cmd| { - StatusCmd::new(cmd) - .map_err(|e| error = Err(anyhow!(e))) - .ok() - }), - }; - - let mut s = Self { - status_cmd, - config: Rc::new(RefCell::new(config)), - blocks: Vec::new(), - blocks_cache: Vec::new(), - surfaces: Vec::default(), - layer_shell: env.require_global::(), - river_status: env.get_global::(), - river_control: env.get_global::(), - }; - - if let Err(e) = error { - s.set_error(e.to_string()); - } - - s - } - - pub fn set_blocks(&mut self, blocks: Vec) { - self.blocks = blocks; - for s in &mut self.surfaces { - s.dirty = true; - } - } - - pub fn set_error(&mut self, error: impl Into) { - self.set_blocks(vec![Block { - full_text: error.into(), - ..Default::default() - }]); - } - - pub fn remove_surface(&mut self, output_id: u32) { - self.surfaces.retain(|s| s.output_id != output_id); - } - - pub fn notify_available(&mut self) -> io::Result<()> { - if let Some(cmd) = &mut self.status_cmd { - if let Some(blocks) = cmd.notify_available()? { - self.set_blocks(blocks); - } - } - Ok(()) - } - - pub fn handle_click( - &mut self, - surface: &WlSurface, - seat: &WlSeat, - x: f64, - y: f64, - btn: PointerBtn, - ) { - if let Some(s) = self - .surfaces - .iter() - .find(|s| &s.surface.detach() == surface) - { - if let Some(event) = s.click(btn, seat, x, y) { - if let Some(cmd) = &mut self.status_cmd { - if let Err(e) = cmd.send_click_event(&event) { - self.set_error(e.to_string()); - } - } - } - } - } - - pub fn add_surface(&mut self, output: &WlOutput, output_id: u32, env: &Environment) { - let surface = env.create_surface(); - let pool = env - .create_auto_pool() - .expect("Failed to create a memory pool!"); - - let layer_surface = self.layer_shell.get_layer_surface( - &surface, - Some(output), - zwlr_layer_shell_v1::Layer::Top, - "i3bar-river".to_owned(), - ); - - // Set the height - layer_surface.set_size(0, self.config.borrow().height); - // Anchor to the top - layer_surface.set_anchor( - zwlr_layer_surface_v1::Anchor::Top - | zwlr_layer_surface_v1::Anchor::Left - | zwlr_layer_surface_v1::Anchor::Right, - ); - - layer_surface.quick_assign(move |layer_surface, event, mut data| { - let bar_state: &mut BarState = data.get().unwrap(); - match event { - zwlr_layer_surface_v1::Event::Closed => { - bar_state.remove_surface(output_id); - } - zwlr_layer_surface_v1::Event::Configure { - serial, - width, - height, - } => { - layer_surface.ack_configure(serial); - if let Some(s) = bar_state - .surfaces - .iter_mut() - .find(|s| s.output_id == output_id) - { - if s.configure(width, height) { - s.draw(&bar_state.blocks, &mut bar_state.blocks_cache); - } - } - } - _ => (), - } - }); - - // Commit so that the server will send a configure event - surface.commit(); - - let (river_output_status, tags_info) = if let Some(river_status) = &self.river_status { - let river_output_status = river_status.get_river_output_status(output); - river_output_status.quick_assign(move |_, event, mut data| { - let bar_state: &mut BarState = data.get().unwrap(); - if let Some(s) = bar_state - .surfaces - .iter_mut() - .find(|s| s.output_id == output_id) - { - match event { - zriver_output_status_v1::Event::FocusedTags { tags } => { - s.tags_info.as_mut().unwrap().focused = tags; - } - zriver_output_status_v1::Event::UrgentTags { tags } => { - s.tags_info.as_mut().unwrap().urgent = tags; - } - _ => (), - } - s.dirty = true; - } - }); - (Some(river_output_status), Some(TagsInfo::default())) - } else { - (None, None) - }; - - self.surfaces.push(Surface { - output_id, - surface, - layer_surface, - pool, - dimensions: (0, 0), - config: self.config.clone(), - dirty: true, - frame_scheduled: false, - river_output_status, - river_control: self.river_control.clone(), - tags_info, - tags_computed: Vec::with_capacity(9), - tags_btns: Default::default(), - blocks_btns: Default::default(), - }); - } -} - -pub struct Surface { - output_id: u32, - surface: Attached, - layer_surface: Main, - pool: AutoMemPool, - dimensions: (u32, u32), - config: Rc>, - dirty: bool, - frame_scheduled: bool, - // river stuff - river_output_status: Option>, - river_control: Option>, - // tags - tags_info: Option, - tags_computed: Vec, - // buttons - tags_btns: ButtonManager, - blocks_btns: ButtonManager<(Option, Option)>, -} - -impl Surface { - /// Handle `configure` event. Returns `true` if configuration changed. - fn configure(&mut self, width: u32, height: u32) -> bool { - if self.dimensions != (width, height) { - self.dimensions = (width, height); - self.layer_surface.set_exclusive_zone(height as i32); - self.dirty = true; - true - } else { - false - } - } - - fn click( - &self, - button: PointerBtn, - seat: &WlSeat, - x: f64, - _y: f64, - ) -> Option { - if let Some((id, river_control)) = self.tags_btns.click(x).zip(self.river_control.as_ref()) - { - let cmd = match button { - PointerBtn::Left => "set-focused-tags", - PointerBtn::Right => "toggle-focused-tags", - _ => return None, - }; - river_control.add_argument(cmd.into()); - river_control.add_argument((1u32 << id).to_string()); - let result = river_control.run_command(seat); - result.quick_assign(|_, event, _| match event { - zriver_command_callback_v1::Event::Success { output } => { - info!("River cmd output: '{output}'"); - } - zriver_command_callback_v1::Event::Failure { failure_message: f } => { - error!("River error: '{f}'"); - } - }); - } else if let Some((name, instance)) = self.blocks_btns.click(x) { - return Some(i3bar_protocol::Event { - name: name.as_deref(), - instance: instance.as_deref(), - button, - ..Default::default() - }); - } - None - } - - fn draw(&mut self, blocks: &[Block], blocks_cache: &mut Vec) { - if self.dirty { - let config = self.config.borrow(); - - let stride = 4 * self.dimensions.0 as i32; - let width = self.dimensions.0 as i32; - let height = self.dimensions.1 as i32; - let width_f = width as f64; - let height_f = height as f64; - - let (canvas, buffer) = self - .pool - .buffer(width, height, stride, wl_shm::Format::Argb8888) - .unwrap(); - - let cairo_surf = unsafe { - cairo::ImageSurface::create_for_data_unsafe( - canvas.as_mut_ptr(), - cairo::Format::ARgb32, - width, - height, - stride, - ) - .expect("cairo surface") - }; - - let cairo_ctx = cairo::Context::new(&cairo_surf).expect("cairo context"); - let pango_layout = pangocairo::create_layout(&cairo_ctx).expect("pango layout"); - pango_layout.set_font_description(Some(&config.font)); - pango_layout.set_height(height); - - // Background - config.background.apply(&cairo_ctx); - cairo_ctx.paint().expect("cairo paint"); - - // Display tags - let mut offset_left = 0.0; - if let Some(tags_info) = &self.tags_info { - if self.tags_computed.is_empty() { - let mut x_offset = 0.0; - // TODO make configurable - for (id, text) in ["1", "2", "3", "4", "5", "6", "7", "8", "9"] - .iter() - .enumerate() - { - let tag = - compute_tag_label(text.to_string(), config.font.clone(), &cairo_ctx); - self.tags_btns.push(x_offset, tag.width, id); - x_offset += tag.width; - self.tags_computed.push(tag); - } - } - for (i, label) in self.tags_computed.iter().enumerate() { - let state = tags_info.get_state(i); - let (bg, fg) = match state { - TagState::Focused => (config.tag_focused_bg, config.tag_focused_fg), - TagState::Inactive => (config.tag_bg, config.tag_fg), - TagState::Urgent => (config.tag_urgent_bg, config.tag_urgent_fg), - }; - label.render( - &cairo_ctx, - RenderOptions { - x_offset: offset_left, - bar_height: height_f, - fg_color: fg, - bg_color: Some(bg), - r_left: if i == 0 || tags_info.get_state(i.saturating_sub(1)) != state { - config.tags_r - } else { - 0.0 - }, - r_right: if i == 8 || tags_info.get_state(i + 1) != state { - config.tags_r - } else { - 0.0 - }, - overlap: 0.0, - }, - ); - offset_left += label.width; - } - } - - // Display the blocks - render_blocks( - &cairo_ctx, - &*config, - blocks, - blocks_cache, - &mut self.blocks_btns, - offset_left, - width_f, - height_f, - ); - - // Attach the buffer to the surface and mark the entire surface as damaged - self.surface.attach(Some(&buffer), 0, 0); - self.surface - .damage_buffer(0, 0, width as i32, height as i32); - } - - // Schedule next frame - if !self.frame_scheduled { - self.frame_scheduled = true; - let output_id = self.output_id; - let frame = self.surface.frame(); - frame.quick_assign(move |_callback, _event, mut data| { - let bar_state: &mut BarState = data.get().unwrap(); - if let Some(s) = bar_state - .surfaces - .iter_mut() - .find(|s| s.output_id == output_id) - { - s.frame_scheduled = false; - s.draw(&bar_state.blocks, &mut bar_state.blocks_cache); - } - }); - } - - // Finally, commit the surface - self.surface.commit(); - self.dirty = false; - } -} - -impl Drop for Surface { - fn drop(&mut self) { - self.layer_surface.destroy(); - self.surface.destroy(); - if let Some(ros) = &self.river_output_status { - ros.destroy(); - } - } -} - -#[allow(clippy::too_many_arguments)] -fn render_blocks( - context: &cairo::Context, - config: &Config, - blocks: &[Block], - blocks_cache: &mut Vec, - buttons: &mut ButtonManager<(Option, Option)>, - offset_left: f64, - full_width: f64, - full_height: f64, -) { - let comp_min_width = |block: &Block| { - let markup = block.markup.as_deref() == Some("pango"); - match &block.min_width { - Some(MinWidth::Pixels(p)) => Some(*p as f64), - Some(MinWidth::Text(t)) => Some(text::width_of(t, context, markup, &config.font.0)), - None => None, - } - }; - let comp_full = |block: &Block, min_width: Option| { - let markup = block.markup.as_deref() == Some("pango"); - text::Text { - text: block.full_text.clone(), - attr: text::Attributes { - font: config.font.clone(), - padding_left: 0.0, - padding_right: 0.0, - min_width, - align: block.align.unwrap_or_default(), - markup, - }, - } - .compute(context) - }; - let comp_short = |block: &Block, min_width: Option| { - let markup = block.markup.as_deref() == Some("pango"); - block.short_text.as_ref().map(|short_text| { - text::Text { - text: short_text.clone(), - attr: text::Attributes { - font: config.font.clone(), - padding_left: 0.0, - padding_right: 0.0, - min_width, - align: block.align.unwrap_or_default(), - markup, - }, - } - .compute(context) - }) - }; - - // update cashe - if blocks.len() != blocks_cache.len() { - blocks_cache.clear(); - for block in blocks { - let mw = comp_min_width(block); - blocks_cache.push(ComputedBlock { - block: block.clone(), - full: comp_full(block, mw), - short: comp_short(block, mw), - min_width: mw, - }); - } - } else { - for (block, computed) in blocks.iter().zip(blocks_cache.iter_mut()) { - if block.min_width != computed.block.min_width || block.markup != computed.block.markup - { - let mw = comp_min_width(block); - *computed = ComputedBlock { - block: block.clone(), - full: comp_full(block, mw), - short: comp_short(block, mw), - min_width: mw, - }; - } else { - if block.full_text != computed.block.full_text { - computed.block.full_text = block.full_text.clone(); - computed.full = comp_full(block, computed.min_width); - } - if block.short_text != computed.block.short_text { - computed.block.full_text = block.full_text.clone(); - computed.short = comp_short(block, computed.min_width); - } - computed.block.color = block.color; - computed.block.background = block.background; - computed.block.align = block.align; - computed.block.name = block.name.clone(); - computed.block.instance = block.instance.clone(); - computed.block.separator = block.separator; - computed.block.separator_block_width = block.separator_block_width; - } - } - } - - #[derive(Debug)] - struct LogialBlock<'a> { - blocks: Vec<(&'a ComputedBlock, bool)>, - delta: f64, - separator: bool, - separator_block_width: u8, - } - - let mut blocks_computed = Vec::new(); - let mut blocks_width = 0.0; - let mut s_start = 0; - while s_start < blocks.len() { - let mut s_end = s_start + 1; - let series_name = &blocks[s_start].name; - while s_end < blocks.len() - && blocks[s_end - 1].separator_block_width == 0 - && &blocks[s_end].name == series_name - { - s_end += 1; - } - - let mut series = LogialBlock { - blocks: Vec::with_capacity(s_end - s_start), - delta: 0.0, - separator: blocks[s_end - 1].separator, - separator_block_width: blocks[s_end - 1].separator_block_width, - }; - - for comp in &blocks_cache[s_start..s_end] { - blocks_width += comp.full.width; - if let Some(short) = &comp.short { - series.delta += comp.full.width - short.width; - } - series.blocks.push((comp, false)); - } - if s_end != blocks.len() { - blocks_width += series.separator_block_width as f64; - } - blocks_computed.push(series); - s_start = s_end; - } - - // Progressively switch to short mode - if offset_left + blocks_width > full_width { - let mut heap = BinaryHeap::new(); - for (i, b) in blocks_computed.iter().enumerate() { - if b.delta > 0.0 { - heap.push((DefaultLess(b.delta), i)); - } - } - while let Some((DefaultLess(delta), to_switch)) = heap.pop() { - for comp in &mut blocks_computed[to_switch].blocks { - comp.1 = true; - } - blocks_width -= delta; - if offset_left + blocks_width <= full_width { - break; - } - } - } - - // Remove all the empy blocks - for s in &mut blocks_computed { - s.blocks.retain(|(text, is_short)| { - (*is_short - && text - .short - .as_ref() - .map_or(text.full.width > 0.0, |s| s.width > 0.0)) - || (!is_short && text.full.width > 0.0) - }); - } - - // Render blocks - buttons.clear(); - let mut j = 0; - for series in blocks_computed { - let s_len = series.blocks.len(); - for (i, (computed, is_short)) in series.blocks.into_iter().enumerate() { - let block = &computed.block; - let to_render = if is_short { - computed.short.as_ref().unwrap_or(&computed.full) - } else { - &computed.full - }; - j += 1; - to_render.render( - context, - RenderOptions { - x_offset: full_width - blocks_width, - bar_height: full_height, - fg_color: block.color.unwrap_or(config.color), - bg_color: block.background, - r_left: if i == 0 { config.blocks_r } else { 0.0 }, - r_right: if i + 1 == s_len { config.blocks_r } else { 0.0 }, - overlap: config.blocks_overlap, - }, - ); - buttons.push( - full_width - blocks_width, - to_render.width, - (block.name.clone(), block.instance.clone()), - ); - blocks_width -= to_render.width; - } - if j != blocks.len() && series.separator_block_width > 0 { - let w = series.separator_block_width as f64; - if series.separator && config.separator_width > 0.0 { - config.separator.apply(context); - context.set_line_width(config.separator_width); - context.move_to(full_width - blocks_width + w * 0.5, full_height * 0.1); - context.line_to(full_width - blocks_width + w * 0.5, full_height * 0.9); - context.stroke().unwrap(); - } - blocks_width -= w; - } - } -} - -#[derive(Debug)] -struct ComputedBlock { - block: Block, - full: ComputedText, - short: Option, - min_width: Option, -} diff --git a/src/config.rs b/src/config.rs index 66b8c8a..bd7b91c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,8 +7,8 @@ use std::fmt; use std::fs::read_to_string; use std::ops::Deref; -#[derive(Deserialize)] -#[serde(default)] +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields, default)] pub struct Config { // colors pub background: Color, @@ -79,6 +79,7 @@ impl Config { } } +#[derive(Debug)] pub struct Font(pub FontDescription); impl Font { diff --git a/src/i3bar_protocol.rs b/src/i3bar_protocol.rs index 54ebd93..67e03f1 100644 --- a/src/i3bar_protocol.rs +++ b/src/i3bar_protocol.rs @@ -64,7 +64,7 @@ pub struct Event<'a> { pub height: u8, } -#[derive(Deserialize, Clone, Copy)] +#[derive(Deserialize, Clone, Copy, Debug)] #[serde(deny_unknown_fields)] pub struct JsonHeader { version: u8, @@ -78,6 +78,7 @@ pub struct JsonHeader { click_events: bool, } +#[derive(Debug)] pub enum Protocol { Unknown, PlainText { diff --git a/src/main.rs b/src/main.rs index 730edf6..b158c91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,9 @@ -#![allow(clippy::explicit_auto_deref)] // TODO: remove once https://github.com/rust-lang/rust-clippy/issues/9101 is resloved - -#[macro_use] -extern crate log; #[macro_use] extern crate anyhow; +#[macro_use] +extern crate log; -use smithay_client_toolkit::{ - self as sctk, default_environment, - environment::SimpleGlobal, - new_default_environment, - output::{with_output_info, OutputInfo}, - reexports::{ - calloop, - client::protocol::{wl_output::WlOutput, wl_pointer}, - protocols::wlr::unstable::layer_shell::v1::client::zwlr_layer_shell_v1, - }, - seat::pointer, - WaylandSource, -}; - -mod bar_state; +mod bar; mod button_manager; mod color; mod config; @@ -27,163 +11,53 @@ mod i3bar_protocol; mod ord_adaptor; mod pointer_btn; mod river_protocols; +mod shared_state; +mod state; mod status_cmd; mod tags; mod text; mod utils; -use bar_state::BarState; -use pointer_btn::PointerBtn; -use river_protocols::zriver_control_v1; -use river_protocols::zriver_status_manager_v1; +use smithay_client_toolkit::reexports::client::Connection; +use tokio::io::{unix::AsyncFd, Interest}; -default_environment!(Env, - fields = [ - layer_shell: SimpleGlobal, - river_status_manager: SimpleGlobal, - river_control: SimpleGlobal, - ], - singles = [ - zwlr_layer_shell_v1::ZwlrLayerShellV1 => layer_shell, - zriver_status_manager_v1::ZriverStatusManagerV1 => river_status_manager, - zriver_control_v1::ZriverControlV1 => river_control, - ], -); +use state::State; -fn main() { +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { env_logger::init(); - let (env, _display, queue) = new_default_environment!( - Env, - fields = [ - layer_shell: SimpleGlobal::new(), - river_status_manager: SimpleGlobal::new(), - river_control: SimpleGlobal::new(), - ] - ) - .expect("Initial roundtrip failed!"); - - let mut bar_state = BarState::new(&env); + let conn = Connection::connect_to_env()?; + let mut event_queue = conn.new_event_queue(); + let mut state = State::new(&conn, &mut event_queue); - let env_handle = env.clone(); - let output_handler = move |output: WlOutput, info: &OutputInfo, bar_state: &mut BarState| { - if info.obsolete { - info!("Output {} removed", info.id); - output.release(); - } else { - info!("Output {} detected", info.id); - bar_state.add_surface(&output, info.id, &env_handle); - } - }; - - // Process currently existing outputs - for output in env.get_all_outputs() { - if let Some(info) = with_output_info(&output, Clone::clone) { - output_handler(output, &info, &mut bar_state); - } - } + let async_fd = AsyncFd::with_interest( + event_queue.prepare_read()?.connection_fd(), + Interest::READABLE, + )?; - // Setup a listener for changes - // The listener will live for as long as we keep this handle alive - let _listner_handle = env.listen_for_outputs(move |output, info, mut data| { - output_handler(output, info, data.get().unwrap()) - }); + event_queue.roundtrip(&mut state)?; + event_queue.flush()?; - // Right now river only supports one seat - let cursor_theme = pointer::ThemeManager::init( - pointer::ThemeSpec::System, - env.require_global(), - env.require_global(), - ); - for seat in env.get_all_seats() { - sctk::seat::with_seat_data(&seat, |seat_data| { - if seat_data.has_pointer && !seat_data.defunct { - let pointer = seat.get_pointer(); - let themed_pointer = cursor_theme.theme_pointer(pointer.detach()); - let seat = seat.clone(); - let mut pos = (0.0, 0.0); - let mut cur_surface = None; - pointer.quick_assign(move |_, event, mut data| { - let bar_state: &mut BarState = data.get().unwrap(); - match event { - wl_pointer::Event::Enter { - serial, - surface, - surface_x: y, - surface_y: x, - } => { - let _ = themed_pointer.set_cursor("default", Some(serial)); - cur_surface = Some(surface); - pos = (x, y); - } - wl_pointer::Event::Leave { - serial: _, - surface: _, - } => { - cur_surface = None; - } - wl_pointer::Event::Motion { - time: _, - surface_x: x, - surface_y: y, - } => { - pos = (x, y); - } - wl_pointer::Event::Button { - serial: _, - time: _, - button, - state, - } if state == wl_pointer::ButtonState::Pressed => { - if let Some(cur_surface) = &cur_surface { - bar_state.handle_click( - cur_surface, - &seat, - pos.0, - pos.1, - button.into(), - ); - } - } - wl_pointer::Event::Axis { - time: _, - axis, - value, - } if axis == wl_pointer::Axis::VerticalScroll => { - if let Some(cur_surface) = &cur_surface { - bar_state.handle_click( - cur_surface, - &seat, - pos.0, - pos.1, - if value > 0.0 { - PointerBtn::WheelDown - } else { - PointerBtn::WheelUp - }, - ); - } - } - _ => (), + loop { + tokio::select! { + readable = async_fd.readable() => { + readable?.clear_ready(); + event_queue.prepare_read()?.read()?; + event_queue.dispatch_pending(&mut state)?; + event_queue.flush()?; + } + readable = state.wait_for_status_cmd() => { + readable?.clear_ready(); + if let Err(e) = state.notify_available() { + if let Some(mut status_cmd) = state.shared_state.as_mut().unwrap().status_cmd.take() { + let _ = status_cmd.child.kill(); } - }); + state.set_error(e.to_string()); + state.draw_all(); + } + event_queue.flush()?; } - }); - } - - let mut event_loop = calloop::EventLoop::::try_new().unwrap(); - - WaylandSource::new(queue) - .quick_insert(event_loop.handle()) - .unwrap(); - - if let Some(cmd) = &bar_state.status_cmd { - cmd.quick_insert(event_loop.handle()); - } - - loop { - if let Err(e) = event_loop.dispatch(None, &mut bar_state) { - bar_state.set_error(e.to_string()); } } } diff --git a/src/river_protocols.rs b/src/river_protocols.rs index c81a7f7..0a2cf91 100644 --- a/src/river_protocols.rs +++ b/src/river_protocols.rs @@ -1,36 +1,4 @@ -pub use generated::client1::*; -pub use generated::client2::*; +#![allow(clippy::enum_variant_names)] -mod generated { - // The generated code tends to trigger a lot of warnings - // so we isolate it into a very permissive module - #![allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)] - #![allow(non_upper_case_globals, non_snake_case, unused_imports)] - #![allow(clippy::all)] - - pub mod client1 { - // These imports are used by the generated code - use smithay_client_toolkit::reexports::client as wayland_client; - use wayland_client::protocol::*; - use wayland_client::sys; - use wayland_client::{AnonymousObject, Attached, Main, Proxy, ProxyMap}; - use wayland_commons::map::{Object, ObjectMetadata}; - use wayland_commons::smallvec; - use wayland_commons::wire::{Argument, ArgumentType, Message, MessageDesc}; - use wayland_commons::{Interface, MessageGroup}; - include!(concat!(env!("OUT_DIR"), "/river-status-unstable-v1.rs")); - } - - pub mod client2 { - // These imports are used by the generated code - use smithay_client_toolkit::reexports::client as wayland_client; - use wayland_client::protocol::*; - use wayland_client::sys; - use wayland_client::{AnonymousObject, Attached, Main, Proxy, ProxyMap}; - use wayland_commons::map::{Object, ObjectMetadata}; - use wayland_commons::smallvec; - use wayland_commons::wire::{Argument, ArgumentType, Message, MessageDesc}; - use wayland_commons::{Interface, MessageGroup}; - include!(concat!(env!("OUT_DIR"), "/river-control-unstable-v1.rs")); - } -} +pub mod control; +pub mod status; diff --git a/src/river_protocols/control.rs b/src/river_protocols/control.rs new file mode 100644 index 0000000..0e7d7e4 --- /dev/null +++ b/src/river_protocols/control.rs @@ -0,0 +1,89 @@ +pub mod protocol { + use smithay_client_toolkit::reexports::client as wayland_client; + use smithay_client_toolkit::reexports::client::protocol::*; + + pub mod __interfaces { + use smithay_client_toolkit::reexports::client::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("protocols/river-control-unstable-v1.xml"); + } + use self::__interfaces::*; + + wayland_scanner::generate_client_code!("protocols/river-control-unstable-v1.xml"); +} + +mod dispatch; + +use std::sync::Arc; + +use smithay_client_toolkit::globals::GlobalData; +use wayland_client::{protocol::wl_seat, Connection, Dispatch, QueueHandle}; + +use protocol::{zriver_command_callback_v1, zriver_control_v1}; + +use smithay_client_toolkit::error::GlobalError; +use smithay_client_toolkit::registry::GlobalProxy; + +#[derive(Debug, Clone)] +pub struct RiverControlState { + control: Arc>, +} + +impl RiverControlState { + pub fn new() -> Self { + Self { + control: Arc::new(GlobalProxy::NotReady), + } + } + + pub fn is_available(&self) -> bool { + self.control.get().is_ok() + } + + pub fn control(&self) -> Result<&zriver_control_v1::ZriverControlV1, GlobalError> { + self.control.get() + } + + pub fn run_command( + &self, + qh: &QueueHandle, + seat: &wl_seat::WlSeat, + args: Args, + ) -> Result<(), GlobalError> + where + D: Dispatch + + Dispatch + + 'static, + Args: IntoIterator, + Arg: Into, + { + let control = self.control()?; + for arg in args { + control.add_argument(arg.into()); + } + control.run_command(seat, qh, RiverCommandCallbackData {})?; + Ok(()) + } +} + +pub trait RiverControlHandler: Sized { + fn river_control_state(&mut self) -> &mut RiverControlState; + + fn command_failure(&mut self, conn: &Connection, qh: &QueueHandle, message: String); + + fn command_success(&mut self, conn: &Connection, qh: &QueueHandle, message: String); +} + +#[derive(Debug)] +pub struct RiverCommandCallbackData { + // This is empty right now, but may be populated in the future. +} + +#[macro_export] +macro_rules! delegate_river_control { + ($ty: ty) => { + ::smithay_client_toolkit::reexports::client::delegate_dispatch!($ty: [ + $crate::river_protocols::control::protocol::zriver_control_v1::ZriverControlV1: ::smithay_client_toolkit::globals::GlobalData, + $crate::river_protocols::control::protocol::zriver_command_callback_v1::ZriverCommandCallbackV1: $crate::river_protocols::control::RiverCommandCallbackData, + ] => $crate::river_protocols::control::RiverControlState); + }; +} diff --git a/src/river_protocols/control/dispatch.rs b/src/river_protocols/control/dispatch.rs new file mode 100644 index 0000000..14759e7 --- /dev/null +++ b/src/river_protocols/control/dispatch.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use super::protocol::{zriver_command_callback_v1, zriver_control_v1}; +use wayland_client::{Connection, Dispatch, QueueHandle}; + +use smithay_client_toolkit::{ + error::GlobalError, + globals::{GlobalData, ProvidesBoundGlobal}, + registry::{ProvidesRegistryState, RegistryHandler}, +}; + +use super::{RiverCommandCallbackData, RiverControlHandler, RiverControlState}; + +impl RegistryHandler for RiverControlState +where + D: Dispatch + + RiverControlHandler + + ProvidesRegistryState + + 'static, +{ + fn ready(data: &mut D, _conn: &Connection, qh: &QueueHandle) { + data.river_control_state().control = + Arc::new(data.registry().bind_one(qh, 1..=1, GlobalData).into()); + } +} + +impl ProvidesBoundGlobal for RiverControlState { + fn bound_global(&self) -> Result { + self.control.get().cloned() + } +} + +impl Dispatch for RiverControlState +where + D: Dispatch + RiverControlHandler + 'static, +{ + fn event( + _: &mut D, + _: &zriver_control_v1::ZriverControlV1, + _: zriver_control_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("zriver_control_v1 has no events") + } +} + +impl Dispatch + for RiverControlState +where + D: Dispatch + + RiverControlHandler + + 'static, +{ + fn event( + data: &mut D, + _callback: &zriver_command_callback_v1::ZriverCommandCallbackV1, + event: zriver_command_callback_v1::Event, + _udata: &RiverCommandCallbackData, + conn: &Connection, + qh: &QueueHandle, + ) { + use zriver_command_callback_v1::Event; + match event { + Event::Success { output } => data.command_success(conn, qh, output), + Event::Failure { failure_message } => data.command_success(conn, qh, failure_message), + } + } +} diff --git a/src/river_protocols/status.rs b/src/river_protocols/status.rs new file mode 100644 index 0000000..50c85e0 --- /dev/null +++ b/src/river_protocols/status.rs @@ -0,0 +1,134 @@ +pub mod protocol { + use smithay_client_toolkit::reexports::client as wayland_client; + use smithay_client_toolkit::reexports::client::protocol::*; + + pub mod __interfaces { + use smithay_client_toolkit::reexports::client::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("protocols/river-status-unstable-v1.xml"); + } + use self::__interfaces::*; + + wayland_scanner::generate_client_code!("protocols/river-status-unstable-v1.xml"); +} + +mod dispatch; + +use std::sync::{Arc, Weak}; + +use smithay_client_toolkit::globals::GlobalData; +use wayland_client::{protocol::wl_output, Connection, Dispatch, QueueHandle}; + +use protocol::{zriver_output_status_v1, zriver_status_manager_v1}; + +use smithay_client_toolkit::error::GlobalError; +use smithay_client_toolkit::registry::GlobalProxy; + +#[derive(Debug)] +pub struct RiverStatusState { + status_manager: GlobalProxy, + output_statuses: Vec>, +} + +impl RiverStatusState { + pub fn new() -> Self { + Self { + status_manager: GlobalProxy::NotReady, + output_statuses: Vec::new(), + } + } + + pub fn status_manager( + &self, + ) -> Result<&zriver_status_manager_v1::ZriverStatusManagerV1, GlobalError> { + self.status_manager.get() + } + + pub fn get_output_status( + &self, + status: &zriver_output_status_v1::ZriverOutputStatusV1, + ) -> Option { + self.output_statuses + .iter() + .filter_map(Weak::upgrade) + .find(|inner| &inner.status == status) + .map(RiverOutputStatus) + } + + #[must_use = "The output status is destroyed if dropped"] + pub fn new_output_status( + &mut self, + qh: &QueueHandle, + output: &wl_output::WlOutput, + ) -> Result + where + D: Dispatch + + Dispatch + + 'static, + { + let manager = self.status_manager()?; + let output_status = manager.get_river_output_status(output, qh, RiverStatusData {})?; + + let output_status = RiverOutputStatus(Arc::new(OutputStatusInner { + status: output_status, + })); + + self.output_statuses.push(Arc::downgrade(&output_status.0)); + + Ok(output_status) + } +} + +pub trait RiverStatusHandler: Sized { + fn river_status_state(&mut self) -> &mut RiverStatusState; + + fn focused_tags_updated( + &mut self, + conn: &Connection, + qh: &QueueHandle, + output_status: &RiverOutputStatus, + focused: u32, + ); + + fn urgent_tags_updated( + &mut self, + conn: &Connection, + qh: &QueueHandle, + output_status: &RiverOutputStatus, + urgent: u32, + ); +} + +#[derive(Debug, Clone)] +pub struct RiverOutputStatus(Arc); + +impl PartialEq for RiverOutputStatus { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +#[derive(Debug)] +pub struct RiverStatusData { + // This is empty right now, but may be populated in the future. +} + +#[macro_export] +macro_rules! delegate_river_status { + ($ty: ty) => { + ::smithay_client_toolkit::reexports::client::delegate_dispatch!($ty: [ + $crate::river_protocols::status::protocol::zriver_status_manager_v1::ZriverStatusManagerV1: ::smithay_client_toolkit::globals::GlobalData, + $crate::river_protocols::status::protocol::zriver_output_status_v1::ZriverOutputStatusV1: $crate::river_protocols::status::RiverStatusData, + ] => $crate::river_protocols::status::RiverStatusState); + }; +} + +#[derive(Debug)] +struct OutputStatusInner { + status: zriver_output_status_v1::ZriverOutputStatusV1, +} + +impl Drop for OutputStatusInner { + fn drop(&mut self) { + self.status.destroy(); + } +} diff --git a/src/river_protocols/status/dispatch.rs b/src/river_protocols/status/dispatch.rs new file mode 100644 index 0000000..d38037d --- /dev/null +++ b/src/river_protocols/status/dispatch.rs @@ -0,0 +1,86 @@ +use super::protocol::{zriver_output_status_v1, zriver_status_manager_v1}; +use wayland_client::{Connection, Dispatch, QueueHandle}; + +use smithay_client_toolkit::{ + error::GlobalError, + globals::{GlobalData, ProvidesBoundGlobal}, + registry::{ProvidesRegistryState, RegistryHandler}, +}; + +use super::{RiverStatusData, RiverStatusHandler, RiverStatusState}; + +impl RegistryHandler for RiverStatusState +where + D: Dispatch + + RiverStatusHandler + + ProvidesRegistryState + + 'static, +{ + fn ready(data: &mut D, _conn: &Connection, qh: &QueueHandle) { + data.river_status_state().status_manager = + data.registry().bind_one(qh, 1..=2, GlobalData).into(); + } +} + +impl ProvidesBoundGlobal for RiverStatusState { + fn bound_global(&self) -> Result { + self.status_manager.get().cloned() + } +} + +impl ProvidesBoundGlobal for RiverStatusState { + fn bound_global(&self) -> Result { + self.status_manager.get().cloned() + } +} + +impl Dispatch + for RiverStatusState +where + D: Dispatch + + RiverStatusHandler + + 'static, +{ + fn event( + _: &mut D, + _: &zriver_status_manager_v1::ZriverStatusManagerV1, + _: zriver_status_manager_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("zriver_status_manager_v1 has no events") + } +} + +impl Dispatch + for RiverStatusState +where + D: Dispatch + + RiverStatusHandler + + 'static, +{ + fn event( + data: &mut D, + status: &zriver_output_status_v1::ZriverOutputStatusV1, + event: zriver_output_status_v1::Event, + _udata: &RiverStatusData, + conn: &Connection, + qh: &QueueHandle, + ) { + use zriver_output_status_v1::Event; + + // Remove any statuses that have been dropped + data.river_status_state() + .output_statuses + .retain(|status| status.upgrade().is_some()); + + if let Some(status) = data.river_status_state().get_output_status(status) { + match event { + Event::FocusedTags { tags } => data.focused_tags_updated(conn, qh, &status, tags), + Event::UrgentTags { tags } => data.urgent_tags_updated(conn, qh, &status, tags), + Event::ViewTags { tags: _ } => (), + } + } + } +} diff --git a/src/shared_state.rs b/src/shared_state.rs new file mode 100644 index 0000000..c4ec9d6 --- /dev/null +++ b/src/shared_state.rs @@ -0,0 +1,18 @@ +use smithay_client_toolkit::{reexports::client::QueueHandle, shm::slot::SlotPool}; + +use crate::{ + config::Config, + i3bar_protocol::Block, + state::{ComputedBlock, State}, + status_cmd::StatusCmd, +}; + +#[derive(Debug)] +pub struct SharedState { + pub qh: QueueHandle, + pub pool: SlotPool, + pub config: Config, + pub status_cmd: Option, + pub blocks: Vec, + pub blocks_cache: Vec, +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..ea5ca4e --- /dev/null +++ b/src/state.rs @@ -0,0 +1,430 @@ +use std::{future::pending, io}; + +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState}, + delegate_compositor, delegate_layer, delegate_output, delegate_pointer, delegate_registry, + delegate_seat, delegate_shm, + output::{OutputHandler, OutputState}, + reexports::client::{ + protocol::{wl_output, wl_pointer, wl_seat, wl_surface}, + Connection, EventQueue, QueueHandle, + }, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::{ + pointer::{PointerEvent, PointerEventKind, PointerHandler}, + Capability, SeatHandler, SeatState, + }, + shell::layer::{Anchor, Layer, LayerHandler, LayerState, LayerSurface, LayerSurfaceConfigure}, + shm::{slot::SlotPool, ShmHandler, ShmState}, +}; +use tokio::io::unix::AsyncFdReadyGuard; + +use crate::{ + bar::Bar, + config::Config, + delegate_river_control, delegate_river_status, + i3bar_protocol::Block, + pointer_btn::PointerBtn, + river_protocols::{ + control::{RiverControlHandler, RiverControlState}, + status::{RiverOutputStatus, RiverStatusHandler, RiverStatusState}, + }, + shared_state::SharedState, + status_cmd::StatusCmd, + text::ComputedText, +}; + +pub struct State { + registry_state: RegistryState, + seat_state: SeatState, + output_state: OutputState, + compositor_state: CompositorState, + shm_state: ShmState, + layer_state: LayerState, + river_status_state: RiverStatusState, + river_control_state: RiverControlState, + + pointers: Vec<(wl_pointer::WlPointer, wl_seat::WlSeat)>, + bars: Vec, + pub shared_state: Option, +} + +impl State { + pub fn new(conn: &Connection, event_queue: &mut EventQueue) -> Self { + let mut error = Ok(()); + + let config = Config::new() + .map_err(|e| error = Err(e)) + .unwrap_or_default(); + + let status_cmd = match &error { + Err(_) => None, + Ok(()) => config.command.as_ref().and_then(|cmd| { + StatusCmd::new(cmd) + .map_err(|e| error = Err(anyhow!(e))) + .ok() + }), + }; + + let mut this = Self { + registry_state: RegistryState::new(conn, &event_queue.handle()), + seat_state: SeatState::new(), + output_state: OutputState::new(), + compositor_state: CompositorState::new(), + shm_state: ShmState::new(), + layer_state: LayerState::new(), + river_status_state: RiverStatusState::new(), + river_control_state: RiverControlState::new(), + + pointers: Vec::new(), + bars: Vec::new(), + shared_state: None, + }; + + while !this.registry_state.ready() { + event_queue.blocking_dispatch(&mut this).unwrap(); + } + + this.shared_state = Some(SharedState { + qh: event_queue.handle(), + pool: SlotPool::new(1, &this.shm_state).expect("Failed to create pool"), + config, + status_cmd, + blocks: Vec::new(), + blocks_cache: Vec::new(), + }); + + if let Err(e) = error { + this.set_error(e.to_string()); + } + + this + } + + pub fn set_blocks(&mut self, blocks: Vec) { + self.shared_state.as_mut().unwrap().blocks = blocks; + self.draw_all(); + } + + pub fn set_error(&mut self, error: impl Into) { + self.set_blocks(vec![Block { + full_text: error.into(), + ..Default::default() + }]); + } + + pub fn notify_available(&mut self) -> anyhow::Result<()> { + if let Some(cmd) = &mut self.shared_state.as_mut().unwrap().status_cmd { + if let Some(blocks) = cmd.notify_available()? { + self.set_blocks(blocks); + } + } + Ok(()) + } + + pub fn draw_all(&mut self) { + for bar in &mut self.bars { + bar.draw(self.shared_state.as_mut().unwrap()); + } + } + + pub async fn wait_for_status_cmd(&self) -> io::Result> { + match &self.shared_state.as_ref().unwrap().status_cmd { + Some(cmd) => cmd.async_fd.readable().await, + None => { + pending::<()>().await; + unreachable!() + } + } + } +} + +impl CompositorHandler for State { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } + + fn scale_factor_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + surface: &wl_surface::WlSurface, + scale: i32, + ) { + if let Some(bar) = self + .bars + .iter_mut() + .find(|bar| bar.layer.wl_surface() == surface) + { + bar.scale = scale; + bar.draw(self.shared_state.as_mut().expect("no shared_state?")); + } + } + + fn frame( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _time: u32, + ) { + } +} + +impl OutputHandler for State { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + output: wl_output::WlOutput, + ) { + let height = self.shared_state.as_ref().unwrap().config.height; + let surface = self.compositor_state.create_surface(qh).unwrap(); + let layer = LayerSurface::builder() + .output(&output) + .size((0, height)) + .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT) + .namespace("i3bar-river") + .map(qh, &mut self.layer_state, surface, Layer::Top) + .expect("layer surface creation"); + let river_output_status = self.river_status_state.new_output_status(qh, &output).ok(); + self.bars.push(Bar { + first_configure: true, + width: 0, + height, + scale: 1, + layer, + blocks_btns: Default::default(), + river_output_status, + river_control: self.river_control_state.clone(), + tags_btns: Default::default(), + tags_info: Default::default(), + tags_computed: Vec::new(), + }); + } + + fn update_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn output_destroyed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } +} + +impl LayerHandler for State { + fn layer_state(&mut self) -> &mut LayerState { + &mut self.layer_state + } + + fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle, layer: &LayerSurface) { + self.bars.retain(|b| &b.layer != layer) + } + + fn configure( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + layer: &LayerSurface, + configure: LayerSurfaceConfigure, + _serial: u32, + ) { + self.bars + .iter_mut() + .find(|b| &b.layer == layer) + .unwrap() + .configure(self.shared_state.as_mut().unwrap(), configure.new_size.0); + } +} + +impl SeatHandler for State { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} + + fn new_capability( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Pointer { + let pointer = self + .seat_state + .get_pointer(qh, &seat) + .expect("Failed to create pointer"); + self.pointers.push((pointer, seat)); + } + } + + fn remove_capability( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Pointer { + self.pointers.retain(|p| p.1 == seat); + } + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} +} + +impl PointerHandler for State { + fn pointer_frame( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + pointer: &wl_pointer::WlPointer, + events: &[PointerEvent], + ) { + let seat = &self.pointers.iter().find(|p| &p.0 == pointer).unwrap().1; + for event in events { + if let Some(bar) = self + .bars + .iter_mut() + .find(|b| b.layer.wl_surface() == &event.surface) + { + match event.kind { + PointerEventKind::Enter { .. } => { + // TODO: set_cursor() + } + PointerEventKind::Press { button, .. } => { + bar.click( + self.shared_state.as_mut().unwrap(), + button.into(), + seat, + event.position.0, + event.position.1, + ) + .unwrap(); + } + PointerEventKind::Axis { vertical, .. } => { + bar.click( + self.shared_state.as_mut().unwrap(), + if vertical.discrete > 0 { + PointerBtn::WheelDown + } else { + PointerBtn::WheelUp + }, + seat, + event.position.0, + event.position.1, + ) + .unwrap(); + } + _ => (), + } + } + } + } +} + +impl ShmHandler for State { + fn shm_state(&mut self) -> &mut ShmState { + &mut self.shm_state + } +} + +impl RiverStatusHandler for State { + fn river_status_state(&mut self) -> &mut RiverStatusState { + &mut self.river_status_state + } + + fn focused_tags_updated( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + output_status: &RiverOutputStatus, + focused: u32, + ) { + let bar = self + .bars + .iter_mut() + .find(|b| b.river_output_status.as_ref() == Some(output_status)) + .unwrap(); + bar.tags_info.focused = focused; + bar.draw(self.shared_state.as_mut().unwrap()); + } + + fn urgent_tags_updated( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + output_status: &RiverOutputStatus, + urgent: u32, + ) { + let bar = self + .bars + .iter_mut() + .find(|b| b.river_output_status.as_ref() == Some(output_status)) + .unwrap(); + bar.tags_info.urgent = urgent; + bar.draw(self.shared_state.as_mut().unwrap()); + } +} + +impl RiverControlHandler for State { + fn river_control_state(&mut self) -> &mut RiverControlState { + &mut self.river_control_state + } + + fn command_failure(&mut self, _: &Connection, _: &QueueHandle, message: String) { + self.set_error(format!("[river_control] {message}")); + } + + fn command_success(&mut self, _: &Connection, _: &QueueHandle, message: String) { + info!("river_control: {message}"); + } +} + +delegate_compositor!(State); +delegate_output!(State); +delegate_shm!(State); +delegate_seat!(State); +delegate_pointer!(State); +delegate_layer!(State); +delegate_registry!(State); +delegate_river_status!(State); +delegate_river_control!(State); + +impl ProvidesRegistryState for State { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![ + CompositorState, + OutputState, + ShmState, + SeatState, + LayerState, + RiverStatusState, + RiverControlState, + ]; +} + +#[derive(Debug)] +pub struct ComputedBlock { + pub block: Block, + pub full: ComputedText, + pub short: Option, + pub min_width: Option, +} diff --git a/src/status_cmd.rs b/src/status_cmd.rs index a229200..4d9c453 100644 --- a/src/status_cmd.rs +++ b/src/status_cmd.rs @@ -1,18 +1,22 @@ -use std::io::{BufRead, BufReader, Result, Write}; +use std::io::{BufRead, BufReader, Write}; use std::os::unix::prelude::AsRawFd; use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; -use smithay_client_toolkit::reexports::calloop::{self, LoopHandle}; +use anyhow::Result; + +use tokio::io::unix::AsyncFd; +use tokio::io::Interest; use crate::i3bar_protocol::{Block, Event, Protocol}; -use crate::BarState; +#[derive(Debug)] pub struct StatusCmd { - child: Child, + pub child: Child, output: BufReader, input: ChildStdin, protocol: Protocol, buf: Vec, + pub async_fd: AsyncFd, } impl StatusCmd { @@ -24,17 +28,22 @@ impl StatusCmd { .spawn()?; let output = child.stdout.take().unwrap(); let input = child.stdin.take().unwrap(); + let async_fd = AsyncFd::with_interest(output.as_raw_fd(), Interest::READABLE)?; Ok(Self { child, output: BufReader::new(output), input, protocol: Protocol::Unknown, buf: Vec::new(), + async_fd, }) } pub fn notify_available(&mut self) -> Result>> { let buf = self.output.fill_buf()?; + if buf.is_empty() { + bail!("status command exited"); + } if self.buf.is_empty() { let rem = self.protocol.process_new_bytes(buf)?; if !rem.is_empty() { @@ -57,35 +66,35 @@ impl StatusCmd { pub fn send_click_event(&mut self, event: &Event) -> Result<()> { if self.protocol.supports_clicks() { - writeln!(self.input, "{}", serde_json::to_string(event).unwrap(),)?; + writeln!(self.input, "{}", serde_json::to_string(event).unwrap())?; } Ok(()) } - pub fn quick_insert(&self, handle: LoopHandle) { - handle - .insert_source( - calloop::generic::Generic::new( - self.output.get_ref().as_raw_fd(), - calloop::Interest { - readable: true, - writable: false, - }, - calloop::Mode::Level, - ), - move |ready, _, bar_state| { - if ready.readable { - bar_state.notify_available()?; - Ok(calloop::PostAction::Continue) - } else { - bar_state.set_error("error reading from status command"); - if let Some(mut child) = bar_state.status_cmd.take() { - let _ = child.child.kill(); - } - Ok(calloop::PostAction::Remove) - } - }, - ) - .expect("failed to inser calloop source"); - } + // pub fn quick_insert(&self, handle: LoopHandle) { + // handle + // .insert_source( + // calloop::generic::Generic::new( + // self.output.get_ref().as_raw_fd(), + // calloop::Interest { + // readable: true, + // writable: false, + // }, + // calloop::Mode::Level, + // ), + // move |ready, _, bar_state| { + // if ready.readable { + // bar_state.notify_available()?; + // Ok(calloop::PostAction::Continue) + // } else { + // bar_state.set_error("error reading from status command"); + // if let Some(mut child) = bar_state.status_cmd.take() { + // let _ = child.child.kill(); + // } + // Ok(calloop::PostAction::Remove) + // } + // }, + // ) + // .expect("failed to inser calloop source"); + // } } -- cgit v1.3.1