diff options
| author | Max Verevkin <maxxverrr@gmail.com> | 2022-04-06 18:47:13 +0300 |
|---|---|---|
| committer | Max Verevkin <maxxverrr@gmail.com> | 2022-04-06 18:49:52 +0300 |
| commit | 9009579fd9328c4beaea7daa47533aa6dc4df11b (patch) | |
| tree | 2e47121f887efc39ae968cdcd0e66b58a7a0f655 | |
| parent | b759c7ec2ac5411f7dd254bb19a20c27a166e7e2 (diff) | |
| download | i3bar-mango-9009579fd9328c4beaea7daa47533aa6dc4df11b.tar.gz | |
heavy refacotring, improve error handling, make `command` optional
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | src/bar_state.rs | 523 | ||||
| -rw-r--r-- | src/main.rs | 642 | ||||
| -rw-r--r-- | src/status_cmd.rs | 119 |
4 files changed, 655 insertions, 633 deletions
@@ -40,7 +40,7 @@ riverctl spawn i3bar-river The configuration file should be stored in `$XDG_CONFIG_HOME/i3bar-river/config.toml` or `~/.config/i3bar-river/config.toml`. -Example configuration (everything except for `command` is optional): +Example configuration (every parameter is optional): ```toml background = "#282828" @@ -54,5 +54,5 @@ tag_urgent_fg = "#282828" tag_urgent_bg = "#cc241d" font = "JetBrainsMono Nerd Font 10" height = 20 -command = "~/i3status-rust/target/debug/i3status-rs" +command = "i3status-rs" ``` diff --git a/src/bar_state.rs b/src/bar_state.rs new file mode 100644 index 0000000..c9c43f4 --- /dev/null +++ b/src/bar_state.rs @@ -0,0 +1,523 @@ +use std::cell::{Cell, RefCell}; +use std::collections::{BinaryHeap, HashMap}; +use std::io; +use std::rc::Rc; + +use smithay_client_toolkit::{ + 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}; + +#[derive(PartialEq, Copy, Clone)] +enum RenderEvent { + Configure { width: u32, height: u32 }, + TagsUpdated, + Closed, +} + +pub struct BarState { + pub status_cmd: Option<StatusCmd>, + config: Rc<RefCell<Config>>, + is_error: bool, + blocks: Vec<Block>, + blocks_updated: bool, + surfaces: Vec<Surface>, +} + +impl BarState { + pub fn new() -> 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)), + is_error: false, + blocks: Vec::new(), + blocks_updated: false, + surfaces: Default::default(), + }; + + if let Err(e) = error { + s.set_error(e.to_string()); + } + + s + } + + pub fn set_blocks(&mut self, blocks: Vec<Block>) { + self.blocks = blocks; + self.blocks_updated = true; + } + + pub fn set_error(&mut self, error: impl Into<String>) { + self.is_error = true; + 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 == 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()); + } + } + } + } + } + + #[allow(clippy::too_many_arguments)] + pub fn add_surface( + &mut self, + output: &WlOutput, + output_id: u32, + surface: WlSurface, + layer_shell: &Attached<zwlr_layer_shell_v1::ZwlrLayerShellV1>, + river_status: &Attached<zriver_status_manager_v1::ZriverStatusManagerV1>, + river_control: Attached<zriver_control_v1::ZriverControlV1>, + pool: AutoMemPool, + ) { + let layer_surface = 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, + ); + + let next_render_event = Rc::new(Cell::new(None)); + let next_render_event_handle = Rc::clone(&next_render_event); + layer_surface.quick_assign(move |layer_surface, event, _| { + match (event, next_render_event_handle.get()) { + (zwlr_layer_surface_v1::Event::Closed, _) => { + next_render_event_handle.set(Some(RenderEvent::Closed)); + } + ( + zwlr_layer_surface_v1::Event::Configure { + serial, + width, + height, + }, + next, + ) if next != Some(RenderEvent::Closed) => { + layer_surface.ack_configure(serial); + next_render_event_handle.set(Some(RenderEvent::Configure { width, height })); + } + (_, _) => (), + } + }); + + // Commit so that the server will send a configure event + surface.commit(); + + let river_output_status = river_status.get_river_output_status(output); + let tags_info = Rc::new(RefCell::new(TagsInfo::default())); + let tags_info_handle = Rc::clone(&tags_info); + let next_render_event_handle = Rc::clone(&next_render_event); + river_output_status.quick_assign(move |_, event, _| { + match event { + zriver_output_status_v1::Event::FocusedTags { tags } => { + tags_info_handle.borrow_mut().focused = tags; + } + zriver_output_status_v1::Event::UrgentTags { tags } => { + tags_info_handle.borrow_mut().urgent = tags; + } + _ => (), + } + if next_render_event_handle.get().is_none() { + next_render_event_handle.set(Some(RenderEvent::TagsUpdated)); + } + }); + + self.surfaces.push(Surface { + output_id, + surface, + layer_surface, + next_render_event, + pool, + dimensions: (0, 0), + config: self.config.clone(), + river_output_status, + river_control, + tags_info, + tags_computed: Vec::with_capacity(9), + tags_btns: Default::default(), + blocks_btns: Default::default(), + }); + } + + pub fn handle_events(&mut self) { + // This is ugly, let's hope that some version of drain_filter() gets stabilized soon + // https://github.com/rust-lang/rust/issues/43244 + let mut i = 0; + while i != self.surfaces.len() { + if self.surfaces[i].handle_events(&self.blocks, self.blocks_updated) { + self.surfaces.remove(i); + } else { + i += 1; + } + } + self.blocks_updated = false; + } +} + +pub struct Surface { + output_id: u32, + surface: WlSurface, + layer_surface: Main<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>, + next_render_event: Rc<Cell<Option<RenderEvent>>>, + pool: AutoMemPool, + dimensions: (u32, u32), + config: Rc<RefCell<Config>>, + // river stuff + river_output_status: Main<zriver_output_status_v1::ZriverOutputStatusV1>, + river_control: Attached<zriver_control_v1::ZriverControlV1>, + // tags + tags_info: Rc<RefCell<TagsInfo>>, + tags_computed: Vec<ComputedText>, + // buttons + tags_btns: ButtonManager, + blocks_btns: ButtonManager<(Option<String>, Option<String>)>, +} + +impl Surface { + /// Handles any events that have occurred since the last call, redrawing if needed. + /// Returns true if the surface should be dropped. + fn handle_events(&mut self, blocks: &[Block], blocks_updated: bool) -> bool { + match self.next_render_event.take() { + Some(RenderEvent::Closed) => return true, + Some(RenderEvent::Configure { width, height }) => { + if self.dimensions != (width, height) { + self.dimensions = (width, height); + self.layer_surface.set_exclusive_zone(height as _); + self.draw(blocks); + return false; + } + } + Some(RenderEvent::TagsUpdated) => { + self.draw(blocks); + return false; + } + _ => (), + } + if blocks_updated { + self.draw(blocks); + return false; + } + false + } + + fn click( + &self, + button: PointerBtn, + seat: &WlSeat, + x: f64, + _y: f64, + ) -> Option<i3bar_protocol::Event> { + if let Some(id) = self.tags_btns.click(x) { + let cmd = match button { + PointerBtn::Left => "set-focused-tags", + PointerBtn::Right => "toggle-focused-tags", + _ => return None, + }; + self.river_control.add_argument(cmd.into()); + self.river_control.add_argument((1u32 << id).to_string()); + let result = self.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]) { + 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"); + + // Compute tags + 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); + } + } + + // Display tags + let mut offset_left = 0.0; + let tags_info = self.tags_info.borrow(); + for (i, label) in self.tags_computed.iter().enumerate() { + let (bg, fg) = match tags_info.get_state(i) { + 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), + }, + ); + offset_left += label.width; + } + + // Display the blocks + render_blocks( + &cairo_ctx, + &*config, + blocks, + &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); + + // Finally, commit the surface + self.surface.commit(); + } +} + +impl Drop for Surface { + fn drop(&mut self) { + self.layer_surface.destroy(); + self.surface.destroy(); + self.river_output_status.destroy(); + } +} + +fn render_blocks( + context: &cairo::Context, + config: &Config, + blocks: &[Block], + buttons: &mut ButtonManager<(Option<String>, Option<String>)>, + offset_left: f64, + full_width: f64, + full_height: f64, +) { + let mut blocks_computed = Vec::with_capacity(blocks.len()); + let mut blocks_width = 0.0; + let mut deltas = HashMap::<&str, f64>::new(); + for (i, block) in blocks.iter().enumerate() { + let markup = block.markup.as_deref() == Some("pango"); + let min_width = 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 full_text = 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 short_text = block.short_text.clone().map(|short_text| { + text::Text { + text: short_text, + 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) + }); + blocks_width += full_text.width; + if i + 1 != blocks.len() { + blocks_width += block.separator_block_width as f64; + } + if let Some(short) = &short_text { + if let Some(name) = &block.name { + *deltas.entry(name).or_insert(0.0) += full_text.width - short.width; + } + } + blocks_computed.push((block.name.as_deref(), full_text, short_text)); + } + + // Progressively switch to short mode + if offset_left + blocks_width > full_width { + let mut heap = BinaryHeap::new(); + for (name, delta) in deltas { + if delta > 0.0 { + heap.push((DefaultLess(delta), name)); + } + } + while let Some((DefaultLess(delta), to_switch)) = heap.pop() { + for (name, full, short) in &mut blocks_computed { + if *name == Some(to_switch) { + if let Some(short) = short { + std::mem::swap(full, short); + } + } + } + blocks_width -= delta; + if offset_left + blocks_width <= full_width { + break; + } + } + } + + buttons.clear(); + for (i, (block, computed)) in blocks + .iter() + .zip(blocks_computed.iter().map(|(_, full, _)| full)) + .enumerate() + { + computed.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, + }, + ); + buttons.push( + full_width - blocks_width, + computed.width, + (block.name.clone(), block.instance.clone()), + ); + blocks_width -= computed.width; + if i + 1 != blocks.len() && block.separator_block_width > 0 { + let w = block.separator_block_width as f64; + if block.separator { + config.separator.apply(context); + context.set_line_width(2.0); + 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/main.rs b/src/main.rs index 48542d9..09795d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ #[macro_use] extern crate log; +#[macro_use] +extern crate anyhow; use smithay_client_toolkit::{ self as sctk, default_environment, @@ -8,22 +10,16 @@ use smithay_client_toolkit::{ output::{with_output_info, OutputInfo}, reexports::{ calloop, - client::protocol::{wl_output, wl_pointer, wl_seat, wl_shm, wl_surface}, - client::{Attached, Main}, - protocols::wlr::unstable::layer_shell::v1::client::{ - zwlr_layer_shell_v1, zwlr_layer_surface_v1, - }, + client::protocol::{wl_output, wl_pointer}, + protocols::wlr::unstable::layer_shell::v1::client::zwlr_layer_shell_v1, }, - shm::AutoMemPool, WaylandSource, }; -use pangocairo::cairo; - -use std::cell::{Cell, RefCell}; -use std::collections::{BinaryHeap, HashMap}; +use std::cell::RefCell; use std::rc::Rc; +mod bar_state; mod button_manager; mod color; mod config; @@ -36,18 +32,10 @@ mod tags; mod text; mod utils; -use button_manager::ButtonManager; -use config::Config; -use i3bar_protocol::{Block, MinWidth}; -use ord_adaptor::DefaultLess; +use bar_state::BarState; use pointer_btn::PointerBtn; -use river_protocols::zriver_command_callback_v1; use river_protocols::zriver_control_v1; -use river_protocols::zriver_output_status_v1; use river_protocols::zriver_status_manager_v1; -use status_cmd::StatusCmd; -use tags::{compute_tag_label, TagState, TagsInfo}; -use text::{ComputedText, RenderOptions}; default_environment!(Env, fields = [ @@ -75,29 +63,18 @@ fn main() { ) .expect("Initial roundtrip failed!"); - let config = Rc::new(RefCell::new(Config::new().unwrap())); - let surfaces = Rc::new(RefCell::new(Vec::<Surface>::new())); - let blocks = Rc::new(RefCell::new(Vec::<Block>::new())); - - let cmd = config.borrow_mut().command.take().unwrap(); - let status_cmd = StatusCmd::new(&cmd, Rc::clone(&blocks), Rc::clone(&surfaces)) - .expect("failed run status command"); + let bar_state = Rc::new(RefCell::new(BarState::new())); let layer_shell = env.require_global::<zwlr_layer_shell_v1::ZwlrLayerShellV1>(); let river_status = env.require_global::<zriver_status_manager_v1::ZriverStatusManagerV1>(); let river_control = env.require_global::<zriver_control_v1::ZriverControlV1>(); let env_handle = env.clone(); - let surfaces_handle = Rc::clone(&surfaces); - let config_handle = Rc::clone(&config); - let blocks_handle = Rc::clone(&blocks); - let status_cmd_handle = status_cmd.clone(); + let bar_state_handle = Rc::clone(&bar_state); let output_handler = move |output: wl_output::WlOutput, info: &OutputInfo| { if info.obsolete { info!("Output removed"); - surfaces_handle - .borrow_mut() - .retain(|s| s.output_id != info.id); + bar_state_handle.borrow_mut().remove_surface(info.id); output.release(); } else { info!("Output detected"); @@ -105,7 +82,7 @@ fn main() { let pool = env_handle .create_auto_pool() .expect("Failed to create a memory pool!"); - (*surfaces_handle.borrow_mut()).push(Surface::new( + bar_state_handle.borrow_mut().add_surface( &output, info.id, surface, @@ -113,10 +90,7 @@ fn main() { &river_status, river_control.clone(), pool, - config_handle.clone(), - blocks_handle.clone(), - status_cmd_handle.clone(), - )); + ); } }; @@ -133,97 +107,73 @@ fn main() { env.listen_for_outputs(move |output, info, _| output_handler(output, info)); // Right now river only supports one seat: default - let pointer_output_id = Rc::new(Cell::new(None)); for seat in env.get_all_seats() { sctk::seat::with_seat_data(&seat, |seat_data| { if seat_data.has_pointer && !seat_data.defunct && seat_data.name == "default" { let pointer = seat.get_pointer(); - let surfaces_handle = Rc::clone(&surfaces); - let pointer_output_id_handle = Rc::clone(&pointer_output_id); + let bar_state_handle = bar_state.clone(); let seat = seat.clone(); - pointer.quick_assign(move |_, event, _| { - info!("pointer event"); - match event { - wl_pointer::Event::Enter { - serial: _, - surface, - surface_x: y, - surface_y: x, - } => { - if let Some(surf) = surfaces_handle - .borrow_mut() - .iter_mut() - .find(|s| s.surface == surface) - { - surf.pointer = Some((x, y)); - pointer_output_id_handle.set(Some(surf.output_id)); - } - } - wl_pointer::Event::Leave { serial: _, surface } => { - if let Some(surf) = surfaces_handle - .borrow_mut() - .iter_mut() - .find(|s| s.surface == surface) - { - surf.pointer = None; - pointer_output_id_handle.set(None); - } - } - wl_pointer::Event::Motion { - time: _, - surface_x: x, - surface_y: y, - } => { - if let Some(output_id) = pointer_output_id_handle.get() { - if let Some(surf) = surfaces_handle - .borrow_mut() - .iter_mut() - .find(|s| s.output_id == output_id) - { - surf.pointer = Some((x, y)); - } - } - } - wl_pointer::Event::Button { - serial: _, - time: _, - button, - state, - } if state == wl_pointer::ButtonState::Pressed => { - if let Some(output_id) = pointer_output_id_handle.get() { - if let Some(surf) = surfaces_handle - .borrow_mut() - .iter_mut() - .find(|s| s.output_id == output_id) - { - surf.click(button.into(), &seat); - } - } + let mut pos = (0.0, 0.0); + let mut cur_surface = None; + pointer.quick_assign(move |_, event, _| match event { + wl_pointer::Event::Enter { + serial: _, + surface, + surface_x: y, + surface_y: x, + } => { + 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.borrow_mut().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(output_id) = pointer_output_id_handle.get() { - if let Some(surf) = surfaces_handle - .borrow_mut() - .iter_mut() - .find(|s| s.output_id == output_id) - { - surf.click( - if value > 0.0 { - PointerBtn::WheelDown - } else { - PointerBtn::WheelUp - }, - &seat, - ); - } - } + } + wl_pointer::Event::Axis { + time: _, + axis, + value, + } if axis == wl_pointer::Axis::VerticalScroll => { + if let Some(cur_surface) = &cur_surface { + bar_state_handle.borrow_mut().handle_click( + cur_surface, + &seat, + pos.0, + pos.1, + if value > 0.0 { + PointerBtn::WheelDown + } else { + PointerBtn::WheelUp + }, + ); } - _ => (), } + _ => (), }); } }); @@ -235,452 +185,20 @@ fn main() { .quick_insert(event_loop.handle()) .unwrap(); - // Run command - let blocks_handle = Rc::clone(&blocks); - let surfaces_handle = Rc::clone(&surfaces); - event_loop - .handle() - .insert_source( - calloop::generic::Generic::new( - status_cmd, - calloop::Interest { - readable: true, - writable: false, - }, - calloop::Mode::Level, - ), - move |ready, cmd, _| { - info!("status command update"); - if ready.readable { - cmd.notify_available()?; - Ok(calloop::PostAction::Continue) - } else { - *blocks_handle.borrow_mut() = vec![Block { - full_text: "[error reading from status command]".into(), - ..Default::default() - }]; - for s in &mut *surfaces_handle.borrow_mut() { - s.blocks_need_update = true; - } - Ok(calloop::PostAction::Remove) - } - }, - ) - .expect("failed to inser calloop source"); + if let Some(cmd) = &bar_state.borrow().status_cmd { + let bar_state_handle = Rc::clone(&bar_state); + cmd.quick_insert(event_loop.handle(), bar_state_handle); + } loop { - // This is ugly, let's hope that some version of drain_filter() gets stabilized soon - // https://github.com/rust-lang/rust/issues/43244 { - let mut surfaces = surfaces.borrow_mut(); - let mut i = 0; - while i != surfaces.len() { - if surfaces[i].handle_events() { - surfaces.remove(i); - } else { - i += 1; - } - } + // Using a new scope in order to be sure that `bar_state` will not be borrowed while + // dispatching the events + bar_state.borrow_mut().handle_events(); } - display.flush().unwrap(); - event_loop.dispatch(None, &mut ()).unwrap(); - } -} - -#[derive(PartialEq, Copy, Clone)] -enum RenderEvent { - Configure { width: u32, height: u32 }, - TagsUpdated, - Closed, -} - -pub struct Surface { - output_id: u32, - surface: wl_surface::WlSurface, - layer_surface: Main<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>, - next_render_event: Rc<Cell<Option<RenderEvent>>>, - pool: AutoMemPool, - dimensions: (u32, u32), - config: Rc<RefCell<Config>>, - // river stuff - river_output_status: Main<zriver_output_status_v1::ZriverOutputStatusV1>, - river_control: Attached<zriver_control_v1::ZriverControlV1>, - // blocks - blocks: Rc<RefCell<Vec<Block>>>, - blocks_need_update: bool, - status_cmd: StatusCmd, - // tags - tags_info: Rc<RefCell<TagsInfo>>, - tags_computed: Vec<ComputedText>, - // Clicking stuff - pointer: Option<(f64, f64)>, - tags_btns: ButtonManager, - blocks_btns: ButtonManager<(Option<String>, Option<String>)>, -} - -impl Surface { - #[allow(clippy::too_many_arguments)] - fn new( - output: &wl_output::WlOutput, - output_id: u32, - surface: wl_surface::WlSurface, - layer_shell: &Attached<zwlr_layer_shell_v1::ZwlrLayerShellV1>, - river_status: &Attached<zriver_status_manager_v1::ZriverStatusManagerV1>, - river_control: Attached<zriver_control_v1::ZriverControlV1>, - pool: AutoMemPool, - config: Rc<RefCell<Config>>, - blocks: Rc<RefCell<Vec<Block>>>, - status_cmd: StatusCmd, - ) -> Self { - let layer_surface = 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, 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, - ); - - let next_render_event = Rc::new(Cell::new(None)); - let next_render_event_handle = Rc::clone(&next_render_event); - layer_surface.quick_assign(move |layer_surface, event, _| { - info!("layer_surface event"); - match (event, next_render_event_handle.get()) { - (zwlr_layer_surface_v1::Event::Closed, _) => { - next_render_event_handle.set(Some(RenderEvent::Closed)); - } - ( - zwlr_layer_surface_v1::Event::Configure { - serial, - width, - height, - }, - next, - ) if next != Some(RenderEvent::Closed) => { - layer_surface.ack_configure(serial); - next_render_event_handle.set(Some(RenderEvent::Configure { width, height })); - } - (_, _) => (), - } - }); - - // Commit so that the server will send a configure event - surface.commit(); - - let river_output_status = river_status.get_river_output_status(output); - let tags_info = Rc::new(RefCell::new(TagsInfo::default())); - let tags_info_handle = Rc::clone(&tags_info); - let next_render_event_handle = Rc::clone(&next_render_event); - river_output_status.quick_assign(move |_, event, _| { - match event { - zriver_output_status_v1::Event::FocusedTags { tags } => { - info!("Focused tags updated: {tags}"); - tags_info_handle.borrow_mut().focused = tags; - } - zriver_output_status_v1::Event::UrgentTags { tags } => { - info!("Urgent tags updated: {tags}"); - tags_info_handle.borrow_mut().urgent = tags; - } - _ => (), - } - if next_render_event_handle.get().is_none() { - next_render_event_handle.set(Some(RenderEvent::TagsUpdated)); - } - }); - - Self { - output_id, - surface, - layer_surface, - next_render_event, - pool, - dimensions: (0, 0), - config, - river_output_status, - river_control, - blocks, - blocks_need_update: false, - status_cmd, - tags_info, - tags_computed: Vec::with_capacity(9), - pointer: None, - tags_btns: Default::default(), - blocks_btns: Default::default(), - } - } - - /// Handles any events that have occurred since the last call, redrawing if needed. - /// Returns true if the surface should be dropped. - fn handle_events(&mut self) -> bool { - match self.next_render_event.take() { - Some(RenderEvent::Closed) => return true, - Some(RenderEvent::Configure { width, height }) => { - if self.dimensions != (width, height) { - self.dimensions = (width, height); - self.layer_surface.set_exclusive_zone(height as _); - self.draw(); - return false; - } - } - Some(RenderEvent::TagsUpdated) => { - self.draw(); - return false; - } - _ => (), - } - if self.blocks_need_update { - self.draw(); - return false; - } - false - } - - fn click(&mut self, button: PointerBtn, seat: &wl_seat::WlSeat) { - if let Some((x, _)) = self.pointer { - if let Some(id) = self.tags_btns.click(x) { - let cmd = match button { - PointerBtn::Left => "set-focused-tags", - PointerBtn::Right => "toggle-focused-tags", - _ => return, - }; - self.river_control.add_argument(cmd.into()); - self.river_control.add_argument((1 << id).to_string()); - let result = self.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) { - let _ = - self.status_cmd - .send_click_event(button, name.as_deref(), instance.as_deref()); - } - } - } - - fn draw(&mut self) { - 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"); - - // Compute tags - 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); - } - } - - // Display tags - let mut offset_left = 0.0; - let tags_info = self.tags_info.borrow(); - for (i, label) in self.tags_computed.iter().enumerate() { - let (bg, fg) = match tags_info.get_state(i) { - 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), - }, - ); - offset_left += label.width; - } - - // Display the blocks - render_blocks( - &cairo_ctx, - &*config, - &*self.blocks.borrow(), - &mut self.blocks_btns, - offset_left, - width_f, - height_f, - ); - self.blocks_need_update = false; - - // 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); - - // Finally, commit the surface - self.surface.commit(); - } -} - -impl Drop for Surface { - fn drop(&mut self) { - self.layer_surface.destroy(); - self.surface.destroy(); - self.river_output_status.destroy(); - } -} - -fn render_blocks( - context: &cairo::Context, - config: &Config, - blocks: &[Block], - buttons: &mut ButtonManager<(Option<String>, Option<String>)>, - offset_left: f64, - full_width: f64, - full_height: f64, -) { - let mut blocks_computed = Vec::with_capacity(blocks.len()); - let mut blocks_width = 0.0; - let mut deltas = HashMap::<&str, f64>::new(); - for (i, block) in blocks.iter().enumerate() { - let markup = block.markup.as_deref() == Some("pango"); - let min_width = 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 full_text = 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 short_text = block.short_text.clone().map(|short_text| { - text::Text { - text: short_text, - 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) - }); - blocks_width += full_text.width; - if i + 1 != blocks.len() { - blocks_width += block.separator_block_width as f64; - } - if let Some(short) = &short_text { - if let Some(name) = &block.name { - *deltas.entry(name).or_insert(0.0) += full_text.width - short.width; - } - } - blocks_computed.push((block.name.as_deref(), full_text, short_text)); - } - - // Progressively switch to short mode - if offset_left + blocks_width > full_width { - let mut heap = BinaryHeap::new(); - for (name, delta) in deltas { - if delta > 0.0 { - heap.push((DefaultLess(delta), name)); - } - } - while let Some((DefaultLess(delta), to_switch)) = heap.pop() { - for (name, full, short) in &mut blocks_computed { - if *name == Some(to_switch) { - if let Some(short) = short { - std::mem::swap(full, short); - } - } - } - blocks_width -= delta; - if offset_left + blocks_width <= full_width { - break; - } - } - } - - buttons.clear(); - for (i, (block, computed)) in blocks - .iter() - .zip(blocks_computed.iter().map(|(_, full, _)| full)) - .enumerate() - { - computed.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, - }, - ); - buttons.push( - full_width - blocks_width, - computed.width, - (block.name.clone(), block.instance.clone()), - ); - blocks_width -= computed.width; - if i + 1 != blocks.len() && block.separator_block_width > 0 { - let w = block.separator_block_width as f64; - if block.separator { - config.separator.apply(context); - context.set_line_width(2.0); - 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; + if let Err(e) = event_loop.dispatch(None, &mut ()) { + bar_state.borrow_mut().set_error(e.to_string()); } } } diff --git a/src/status_cmd.rs b/src/status_cmd.rs index e77e4b2..9824be4 100644 --- a/src/status_cmd.rs +++ b/src/status_cmd.rs @@ -1,97 +1,78 @@ -use crate::Surface; use std::cell::RefCell; -use std::io::Result; -use std::io::{BufRead, BufReader, Write}; -use std::os::unix::prelude::{AsRawFd, RawFd}; -use std::process::{ChildStdin, ChildStdout, Command, Stdio}; +use std::io::{BufRead, BufReader, Result, Write}; +use std::os::unix::prelude::AsRawFd; +use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; use std::rc::Rc; +use smithay_client_toolkit::reexports::calloop::{self, LoopHandle}; + use crate::i3bar_protocol::{Block, Event, Protocol}; -use crate::pointer_btn::PointerBtn; +use crate::BarState; -#[derive(Clone)] pub struct StatusCmd { - inner: Rc<RefCell<Inner>>, -} - -struct Inner { + child: Child, output: BufReader<ChildStdout>, input: ChildStdin, protocol: Protocol, - blocks: Rc<RefCell<Vec<Block>>>, - surfaces: Rc<RefCell<Vec<Surface>>>, -} - -impl AsRawFd for StatusCmd { - fn as_raw_fd(&self) -> RawFd { - self.inner.borrow().output.get_ref().as_raw_fd() - } -} - -impl Inner { - pub fn notify_available(&mut self) -> Result<()> { - let buf = self.output.fill_buf()?; - self.protocol.process_new_bytes(buf)?; - let consumed = buf.len(); - self.output.consume(consumed); - if let Some(new_blocks) = self.protocol.get_blocks() { - *self.blocks.borrow_mut() = new_blocks; - for s in &mut *self.surfaces.borrow_mut() { - s.blocks_need_update = true; - } - } - Ok(()) - } } impl StatusCmd { - pub fn new( - cmd: &str, - blocks: Rc<RefCell<Vec<Block>>>, - surfaces: Rc<RefCell<Vec<Surface>>>, - ) -> Result<Self> { + pub fn new(cmd: &str) -> Result<Self> { let mut child = Command::new("sh") .args(["-c", &format!("exec {cmd}")]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; - let output = BufReader::new(child.stdout.take().unwrap()); + let output = child.stdout.take().unwrap(); let input = child.stdin.take().unwrap(); Ok(Self { - inner: Rc::new(RefCell::new(Inner { - output, - input, - protocol: Protocol::Unknown, - blocks, - surfaces, - })), + child, + output: BufReader::new(output), + input, + protocol: Protocol::Unknown, }) } - pub fn notify_available(&mut self) -> Result<()> { - self.inner.borrow_mut().notify_available() + pub fn notify_available(&mut self) -> Result<Option<Vec<Block>>> { + let buf = self.output.fill_buf()?; + self.protocol.process_new_bytes(buf)?; + let consumed = buf.len(); + self.output.consume(consumed); + Ok(self.protocol.get_blocks()) } - pub fn send_click_event( - &mut self, - button: PointerBtn, - name: Option<&str>, - instance: Option<&str>, - ) -> Result<()> { - let mut inner = self.inner.borrow_mut(); - if inner.protocol.supports_clicks() { - writeln!( - inner.input, - "{}", - serde_json::to_string(&Event { - name, - instance, - button, - ..Default::default() - }) - .unwrap() - )?; + pub fn send_click_event(&mut self, event: &Event) -> Result<()> { + if self.protocol.supports_clicks() { + writeln!(self.input, "{}", serde_json::to_string(event).unwrap(),)?; } Ok(()) } + + pub fn quick_insert(&self, handle: LoopHandle<()>, bar_state: Rc<RefCell<BarState>>) { + 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, _, _| { + if ready.readable { + bar_state.borrow_mut().notify_available()?; + Ok(calloop::PostAction::Continue) + } else { + let mut bar_state = bar_state.borrow_mut(); + 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"); + } } |
