summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMax Verevkin <34583604+MaxVerevkin@users.noreply.github.com>2022-11-27 21:25:15 +0200
committerGitHub <noreply@github.com>2022-11-27 21:25:15 +0200
commitd28693c9d293ea7c348c0e9e1a4e89ccff6dcae4 (patch)
tree0794e56251e223388463a84d09616ef47a918bb4 /src
parent9ddde2d2ee9833412e39afbd1ed907cdea22f08f (diff)
parentd45a4ef408c0f5a2071194d4c3285f0b7cb27dd0 (diff)
downloadi3bar-mango-d28693c9d293ea7c348c0e9e1a4e89ccff6dcae4.tar.gz
Merge pull request #1 from MaxVerevkin/next
Rewrite using latest walayd libs & implement HiDPI
Diffstat (limited to 'src')
-rw-r--r--src/bar.rs412
-rw-r--r--src/bar_state.rs651
-rw-r--r--src/config.rs14
-rw-r--r--src/i3bar_protocol.rs3
-rw-r--r--src/main.rs200
-rw-r--r--src/river_protocols.rs38
-rw-r--r--src/river_protocols/control.rs112
-rw-r--r--src/river_protocols/status.rs197
-rw-r--r--src/shared_state.rs30
-rw-r--r--src/state.rs479
-rw-r--r--src/status_cmd.rs71
-rw-r--r--src/tags.rs20
-rw-r--r--src/text.rs117
13 files changed, 1375 insertions, 969 deletions
diff --git a/src/bar.rs b/src/bar.rs
new file mode 100644
index 0000000..dfa63a3
--- /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 configured: bool,
+ pub width: u32,
+ pub height: u32,
+ pub scale: i32,
+ pub layer: LayerSurface,
+ pub blocks_btns: ButtonManager<(Option<String>, Option<String>)>,
+ pub river_output_status: Option<RiverOutputStatus>,
+ pub river_control: Option<RiverControlState>,
+ pub tags_btns: ButtonManager,
+ pub tags_info: TagsInfo,
+ pub tags_computed: Vec<ComputedText>,
+}
+
+impl Bar {
+ pub fn configure(&mut self, ss: &mut SharedState, width: u32) {
+ if self.width != width && width != 0 {
+ self.width = width;
+ self.layer.set_exclusive_zone(self.height as i32);
+ }
+
+ self.configured = true;
+ 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 let Some(river_control) = &self.river_control {
+ let cmd = match button {
+ PointerBtn::Left => "set-focused-tags",
+ PointerBtn::Right => "toggle-focused-tags",
+ _ => return Ok(()),
+ };
+ 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.configured {
+ 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
+ .get_pool(height as usize * stride as usize)
+ .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
+ cairo_ctx.save().unwrap();
+ cairo_ctx.set_operator(cairo::Operator::Source);
+ ss.config.background.apply(&cairo_ctx);
+ cairo_ctx.paint().unwrap();
+ cairo_ctx.restore().unwrap();
+
+ // Display tags
+ let mut offset_left = 0.0;
+ if self.has_tags_provider() {
+ if self.tags_computed.is_empty() {
+ // TODO make configurable
+ for text in ["1", "2", "3", "4", "5", "6", "7", "8", "9"] {
+ self.tags_computed
+ .push(compute_tag_label(text, &ss.config.font, &cairo_ctx));
+ }
+ }
+ self.tags_btns.clear();
+ for (i, label) in self.tags_computed.iter().enumerate() {
+ let state = self.tags_info.get_state(i);
+ let (bg, fg) = match state {
+ TagState::Urgent => (ss.config.tag_urgent_bg, ss.config.tag_urgent_fg),
+ TagState::Focused => (ss.config.tag_focused_bg, ss.config.tag_focused_fg),
+ TagState::Active => (ss.config.tag_bg, ss.config.tag_fg),
+ TagState::Inactive => {
+ if ss.config.hide_inactive_tags {
+ continue;
+ }
+ (ss.config.tag_inactive_bg, ss.config.tag_inactive_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,
+ },
+ );
+ self.tags_btns.push(offset_left, label.width, i);
+ 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<ComputedBlock>,
+ buttons: &mut ButtonManager<(Option<String>, Option<String>)>,
+ 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<f64>| {
+ let markup = block.markup.as_deref() == Some("pango");
+ text::ComputedText::new(
+ &block.full_text,
+ text::Attributes {
+ font: &config.font,
+ padding_left: 0.0,
+ padding_right: 0.0,
+ min_width,
+ align: block.align.unwrap_or_default(),
+ markup,
+ },
+ context,
+ )
+ };
+ let comp_short = |block: &Block, min_width: Option<f64>| {
+ let markup = block.markup.as_deref() == Some("pango");
+ block.short_text.as_ref().map(|short_text| {
+ text::ComputedText::new(
+ short_text,
+ text::Attributes {
+ font: &config.font,
+ padding_left: 0.0,
+ padding_right: 0.0,
+ min_width,
+ align: block.align.unwrap_or_default(),
+ markup,
+ },
+ 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<StatusCmd>,
- config: Rc<RefCell<Config>>,
- blocks: Vec<Block>,
- blocks_cache: Vec<ComputedBlock>,
- surfaces: Vec<Surface>,
- layer_shell: Attached<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
- river_status: Option<Attached<zriver_status_manager_v1::ZriverStatusManagerV1>>,
- river_control: Option<Attached<zriver_control_v1::ZriverControlV1>>,
-}
-
-impl BarState {
- pub fn new(env: &Environment<Env>) -> 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::<zwlr_layer_shell_v1::ZwlrLayerShellV1>(),
- river_status: env.get_global::<zriver_status_manager_v1::ZriverStatusManagerV1>(),
- river_control: env.get_global::<zriver_control_v1::ZriverControlV1>(),
- };
-
- if let Err(e) = error {
- s.set_error(e.to_string());
- }
-
- s
- }
-
- pub fn set_blocks(&mut self, blocks: Vec<Block>) {
- self.blocks = blocks;
- for s in &mut self.surfaces {
- s.dirty = true;
- }
- }
-
- pub fn set_error(&mut self, error: impl Into<String>) {
- 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<Env>) {
- 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<WlSurface>,
- layer_surface: Main<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>,
- pool: AutoMemPool,
- dimensions: (u32, u32),
- config: Rc<RefCell<Config>>,
- dirty: bool,
- frame_scheduled: bool,
- // river stuff
- river_output_status: Option<Main<zriver_output_status_v1::ZriverOutputStatusV1>>,
- river_control: Option<Attached<zriver_control_v1::ZriverControlV1>>,
- // tags
- tags_info: Option<TagsInfo>,
- tags_computed: Vec<ComputedText>,
- // buttons
- tags_btns: ButtonManager,
- blocks_btns: ButtonManager<(Option<String>, Option<String>)>,
-}
-
-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<i3bar_protocol::Event> {
- 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<ComputedBlock>) {
- 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<ComputedBlock>,
- buttons: &mut ButtonManager<(Option<String>, Option<String>)>,
- 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<f64>| {
- 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<f64>| {
- 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<ComputedText>,
- min_width: Option<f64>,
-}
diff --git a/src/config.rs b/src/config.rs
index 66b8c8a..2af9af0 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,
@@ -20,6 +20,8 @@ pub struct Config {
pub tag_focused_bg: Color,
pub tag_urgent_fg: Color,
pub tag_urgent_bg: Color,
+ pub tag_inactive_fg: Color,
+ pub tag_inactive_bg: Color,
// font and size
pub font: Font,
pub height: u32,
@@ -29,6 +31,9 @@ pub struct Config {
pub blocks_overlap: f64,
// command
pub command: Option<String>,
+ // misc
+ pub hide_inactive_tags: bool,
+ pub invert_touchpad_scrolling: bool,
}
impl Default for Config {
@@ -44,6 +49,8 @@ impl Default for Config {
tag_focused_bg: Color::from_rgba_hex(0x689d68ff),
tag_urgent_fg: Color::from_rgba_hex(0x282828ff),
tag_urgent_bg: Color::from_rgba_hex(0xcc241dff),
+ tag_inactive_fg: Color::from_rgba_hex(0xd79921ff),
+ tag_inactive_bg: Color::from_rgba_hex(0x282828ff),
font: Font::new("monospace 10"),
height: 24,
separator_width: 2.0,
@@ -51,6 +58,8 @@ impl Default for Config {
blocks_r: 0.0,
blocks_overlap: 0.0,
command: None,
+ hide_inactive_tags: true,
+ invert_touchpad_scrolling: true,
}
}
}
@@ -79,6 +88,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..8ae31aa 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,55 @@ 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 std::os::unix::io::AsRawFd;
-default_environment!(Env,
- fields = [
- layer_shell: SimpleGlobal<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
- river_status_manager: SimpleGlobal<zriver_status_manager_v1::ZriverStatusManagerV1>,
- river_control: SimpleGlobal<zriver_control_v1::ZriverControlV1>,
- ],
- singles = [
- zwlr_layer_shell_v1::ZwlrLayerShellV1 => layer_shell,
- zriver_status_manager_v1::ZriverStatusManagerV1 => river_status_manager,
- zriver_control_v1::ZriverControlV1 => river_control,
- ],
-);
+use smithay_client_toolkit::reexports::client::Connection;
+use tokio::io::{unix::AsyncFd, Interest};
-fn main() {
- env_logger::init();
+use state::State;
+use wayland_client::globals::registry_queue_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!");
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> anyhow::Result<()> {
+ env_logger::init();
- let mut bar_state = BarState::new(&env);
+ let conn = Connection::connect_to_env()?;
+ let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
+ let mut state = State::new(&mut event_queue, &globals);
- 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);
- }
- };
+ let async_fd = AsyncFd::with_interest(
+ event_queue.prepare_read()?.connection_fd().as_raw_fd(),
+ Interest::READABLE,
+ )?;
- // 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);
- }
- }
+ event_queue.roundtrip(&mut state)?;
- // 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())
- });
-
- // 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.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::<BarState>::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..7cce23d
--- /dev/null
+++ b/src/river_protocols/control.rs
@@ -0,0 +1,112 @@
+use wayland_client::globals::{BindError, GlobalList};
+use wayland_client::{protocol::wl_seat, Connection, Dispatch, QueueHandle};
+
+use protocol::{zriver_command_callback_v1, zriver_control_v1};
+
+#[derive(Debug, Clone)]
+pub struct RiverControlState {
+ control: zriver_control_v1::ZriverControlV1,
+}
+
+impl RiverControlState {
+ pub fn new<D>(globals: &GlobalList, qh: &QueueHandle<D>) -> Result<Self, BindError>
+ where
+ D: Dispatch<zriver_control_v1::ZriverControlV1, (), D> + 'static,
+ {
+ let control = globals.bind(qh, 1..=1, ())?;
+ Ok(Self { control })
+ }
+
+ pub fn run_command<D, Args, Arg>(&self, qh: &QueueHandle<D>, seat: &wl_seat::WlSeat, args: Args)
+ where
+ D: Dispatch<zriver_command_callback_v1::ZriverCommandCallbackV1, RiverCommandCallbackData>
+ + 'static,
+ Args: IntoIterator<Item = Arg>,
+ Arg: Into<String>,
+ {
+ for arg in args {
+ self.control.add_argument(arg.into());
+ }
+ self.control
+ .run_command(seat, qh, RiverCommandCallbackData {});
+ }
+}
+
+pub trait RiverControlHandler: Sized {
+ fn river_control_state(&mut self) -> &mut RiverControlState;
+
+ fn command_failure(&mut self, conn: &Connection, qh: &QueueHandle<Self>, message: String);
+
+ fn command_success(&mut self, conn: &Connection, qh: &QueueHandle<Self>, 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: ()
+ ] => $crate::river_protocols::control::RiverControlState);
+ ::smithay_client_toolkit::reexports::client::delegate_dispatch!($ty: [
+ $crate::river_protocols::control::protocol::zriver_command_callback_v1::ZriverCommandCallbackV1: $crate::river_protocols::control::RiverCommandCallbackData
+ ] => $crate::river_protocols::control::RiverControlState);
+ };
+}
+
+impl<D> Dispatch<zriver_control_v1::ZriverControlV1, (), D> for RiverControlState
+where
+ D: Dispatch<zriver_control_v1::ZriverControlV1, ()> + RiverControlHandler + 'static,
+{
+ fn event(
+ _: &mut D,
+ _: &zriver_control_v1::ZriverControlV1,
+ _: zriver_control_v1::Event,
+ _: &(),
+ _: &Connection,
+ _: &QueueHandle<D>,
+ ) {
+ unreachable!("zriver_control_v1 has no events")
+ }
+}
+
+impl<D> Dispatch<zriver_command_callback_v1::ZriverCommandCallbackV1, RiverCommandCallbackData, D>
+ for RiverControlState
+where
+ D: Dispatch<zriver_command_callback_v1::ZriverCommandCallbackV1, RiverCommandCallbackData>
+ + 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<D>,
+ ) {
+ 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),
+ }
+ }
+}
+
+pub mod protocol {
+ #![allow(non_upper_case_globals)]
+
+ 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");
+}
diff --git a/src/river_protocols/status.rs b/src/river_protocols/status.rs
new file mode 100644
index 0000000..1779719
--- /dev/null
+++ b/src/river_protocols/status.rs
@@ -0,0 +1,197 @@
+use std::sync::{Arc, Weak};
+
+use wayland_client::{
+ globals::{BindError, GlobalList},
+ protocol::wl_output,
+ Connection, Dispatch, QueueHandle,
+};
+
+use protocol::{zriver_output_status_v1, zriver_status_manager_v1};
+
+#[derive(Debug, Clone)]
+pub struct RiverStatusState {
+ status_manager: zriver_status_manager_v1::ZriverStatusManagerV1,
+ output_statuses: Vec<Weak<OutputStatusInner>>,
+}
+
+impl RiverStatusState {
+ pub fn new<D>(globals: &GlobalList, qh: &QueueHandle<D>) -> Result<Self, BindError>
+ where
+ D: Dispatch<zriver_status_manager_v1::ZriverStatusManagerV1, (), D> + 'static,
+ {
+ let status_manager = globals.bind(qh, 1..=1, ())?;
+ Ok(Self {
+ status_manager,
+ output_statuses: Vec::new(),
+ })
+ }
+
+ pub fn get_output_status(
+ &self,
+ status: &zriver_output_status_v1::ZriverOutputStatusV1,
+ ) -> Option<RiverOutputStatus> {
+ 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<D>(
+ &mut self,
+ qh: &QueueHandle<D>,
+ output: &wl_output::WlOutput,
+ ) -> RiverOutputStatus
+ where
+ D: Dispatch<zriver_status_manager_v1::ZriverStatusManagerV1, ()>
+ + Dispatch<zriver_output_status_v1::ZriverOutputStatusV1, RiverStatusData>
+ + 'static,
+ {
+ let output_status =
+ self.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));
+
+ output_status
+ }
+}
+
+pub trait RiverStatusHandler: Sized {
+ fn river_status_state(&mut self) -> &mut RiverStatusState;
+
+ fn focused_tags_updated(
+ &mut self,
+ conn: &Connection,
+ qh: &QueueHandle<Self>,
+ output_status: &RiverOutputStatus,
+ focused: u32,
+ );
+
+ fn urgent_tags_updated(
+ &mut self,
+ conn: &Connection,
+ qh: &QueueHandle<Self>,
+ output_status: &RiverOutputStatus,
+ urgent: u32,
+ );
+
+ fn views_tags_updated(
+ &mut self,
+ conn: &Connection,
+ qh: &QueueHandle<Self>,
+ output_status: &RiverOutputStatus,
+ tags: Vec<u32>,
+ );
+}
+
+#[derive(Debug, Clone)]
+pub struct RiverOutputStatus(Arc<OutputStatusInner>);
+
+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: ()
+ ] => $crate::river_protocols::status::RiverStatusState);
+ ::smithay_client_toolkit::reexports::client::delegate_dispatch!($ty: [
+ $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();
+ }
+}
+
+impl<D> Dispatch<zriver_status_manager_v1::ZriverStatusManagerV1, (), D> for RiverStatusState
+where
+ D: Dispatch<zriver_status_manager_v1::ZriverStatusManagerV1, ()> + RiverStatusHandler + 'static,
+{
+ fn event(
+ _: &mut D,
+ _: &zriver_status_manager_v1::ZriverStatusManagerV1,
+ _: zriver_status_manager_v1::Event,
+ _: &(),
+ _: &Connection,
+ _: &QueueHandle<D>,
+ ) {
+ unreachable!("zriver_status_manager_v1 has no events")
+ }
+}
+
+impl<D> Dispatch<zriver_output_status_v1::ZriverOutputStatusV1, RiverStatusData, D>
+ for RiverStatusState
+where
+ D: Dispatch<zriver_output_status_v1::ZriverOutputStatusV1, RiverStatusData>
+ + 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<D>,
+ ) {
+ 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 } => {
+ let tags = tags
+ .chunks_exact(4)
+ .map(|bytes| u32::from_ne_bytes(bytes.try_into().unwrap()))
+ .collect();
+ data.views_tags_updated(conn, qh, &status, tags);
+ }
+ }
+ }
+ }
+}
+
+pub mod protocol {
+ #![allow(non_upper_case_globals)]
+
+ 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");
+}
diff --git a/src/shared_state.rs b/src/shared_state.rs
new file mode 100644
index 0000000..554d856
--- /dev/null
+++ b/src/shared_state.rs
@@ -0,0 +1,30 @@
+use smithay_client_toolkit::{
+ reexports::client::QueueHandle,
+ shm::{slot::SlotPool, ShmState},
+};
+
+use crate::{
+ config::Config,
+ i3bar_protocol::Block,
+ state::{ComputedBlock, State},
+ status_cmd::StatusCmd,
+};
+
+#[derive(Debug)]
+pub struct SharedState {
+ pub qh: QueueHandle<State>,
+ pub shm_state: ShmState,
+ pub pool: Option<SlotPool>,
+ pub config: Config,
+ pub status_cmd: Option<StatusCmd>,
+ pub blocks: Vec<Block>,
+ pub blocks_cache: Vec<ComputedBlock>,
+}
+
+impl SharedState {
+ pub fn get_pool(&mut self, len: usize) -> &mut SlotPool {
+ self.pool.get_or_insert_with(|| {
+ SlotPool::new(len, &self.shm_state).expect("Failed to create pool")
+ })
+ }
+}
diff --git a/src/state.rs b/src/state.rs
new file mode 100644
index 0000000..ed6c3d1
--- /dev/null
+++ b/src/state.rs
@@ -0,0 +1,479 @@
+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, ThemeSpec, ThemedPointer},
+ Capability, SeatHandler, SeatState,
+ },
+ shell::layer::{
+ Anchor, Layer, LayerShell, LayerShellHandler, LayerSurface, LayerSurfaceConfigure,
+ },
+ shm::{ShmHandler, ShmState},
+};
+use tokio::io::unix::AsyncFdReadyGuard;
+use wayland_client::globals::GlobalList;
+
+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,
+ layer_state: LayerShell,
+ river_status_state: Option<RiverStatusState>,
+ river_control_state: Option<RiverControlState>,
+
+ seats: Vec<Seat>,
+ bars: Vec<Bar>,
+ pub shared_state: SharedState,
+}
+
+struct Seat {
+ seat: wl_seat::WlSeat,
+ pointer: wl_pointer::WlPointer,
+ themed_pointer: ThemedPointer,
+ pointer_surface: wl_surface::WlSurface,
+ finger_scroll: f64,
+}
+
+impl State {
+ pub fn new(event_queue: &mut EventQueue<Self>, globals: &GlobalList) -> Self {
+ let mut error = Ok(());
+
+ let qh = event_queue.handle();
+
+ 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(globals),
+ seat_state: SeatState::new(globals, &qh),
+ output_state: OutputState::new(globals, &qh),
+ compositor_state: CompositorState::bind(globals, &qh).unwrap(),
+ layer_state: LayerShell::bind(globals, &qh).unwrap(),
+ river_status_state: RiverStatusState::new(globals, &qh).ok(),
+ river_control_state: RiverControlState::new(globals, &qh).ok(),
+
+ seats: Vec::new(),
+ bars: Vec::new(),
+ shared_state: SharedState {
+ qh: event_queue.handle(),
+ shm_state: ShmState::bind(globals, &qh).unwrap(),
+ pool: None,
+ 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<Block>) {
+ self.shared_state.blocks = blocks;
+ self.draw_all();
+ }
+
+ pub fn set_error(&mut self, error: impl Into<String>) {
+ 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.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(&mut self.shared_state);
+ }
+ }
+
+ pub async fn wait_for_status_cmd(&self) -> io::Result<AsyncFdReadyGuard<i32>> {
+ match &self.shared_state.status_cmd {
+ Some(cmd) => cmd.async_fd.readable().await,
+ None => {
+ pending::<()>().await;
+ unreachable!()
+ }
+ }
+ }
+}
+
+impl CompositorHandler for State {
+ fn scale_factor_changed(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ 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(&mut self.shared_state);
+ }
+ }
+
+ fn frame(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _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<Self>,
+ output: wl_output::WlOutput,
+ ) {
+ let height = self.shared_state.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, &self.layer_state, surface, Layer::Top)
+ .expect("layer surface creation");
+ let river_output_status = self
+ .river_status_state
+ .as_mut()
+ .map(|s| s.new_output_status(qh, &output));
+ self.bars.push(Bar {
+ configured: false,
+ 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<Self>,
+ _output: wl_output::WlOutput,
+ ) {
+ }
+
+ fn output_destroyed(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _output: wl_output::WlOutput,
+ ) {
+ }
+}
+
+impl LayerShellHandler for State {
+ fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, layer: &LayerSurface) {
+ self.bars.retain(|b| &b.layer != layer)
+ }
+
+ fn configure(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ layer: &LayerSurface,
+ configure: LayerSurfaceConfigure,
+ _serial: u32,
+ ) {
+ self.bars
+ .iter_mut()
+ .find(|b| &b.layer == layer)
+ .unwrap()
+ .configure(&mut self.shared_state, 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<Self>, _: wl_seat::WlSeat) {}
+
+ fn new_capability(
+ &mut self,
+ _conn: &Connection,
+ qh: &QueueHandle<Self>,
+ seat: wl_seat::WlSeat,
+ capability: Capability,
+ ) {
+ if capability == Capability::Pointer {
+ let (pointer, themed_pointer) = self
+ .seat_state
+ .get_pointer_with_theme(qh, &seat, ThemeSpec::System, 1)
+ .expect("Failed to create pointer");
+ self.seats.push(Seat {
+ seat,
+ pointer,
+ themed_pointer,
+ pointer_surface: self.compositor_state.create_surface(qh).unwrap(), // TODO: make a PR to remove this unwrap
+ finger_scroll: 0.0,
+ });
+ }
+ }
+
+ fn remove_capability(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ seat: wl_seat::WlSeat,
+ capability: Capability,
+ ) {
+ if capability == Capability::Pointer {
+ self.seats.retain(|p| p.seat == seat);
+ }
+ }
+
+ fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
+}
+
+impl PointerHandler for State {
+ fn pointer_frame(
+ &mut self,
+ conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ pointer: &wl_pointer::WlPointer,
+ events: &[PointerEvent],
+ ) {
+ let seat = self
+ .seats
+ .iter_mut()
+ .find(|p| &p.pointer == pointer)
+ .unwrap();
+ 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 { .. } => {
+ seat.themed_pointer
+ .set_cursor(
+ conn,
+ "default",
+ self.shared_state.shm_state.wl_shm(),
+ &seat.pointer_surface,
+ )
+ .unwrap();
+ }
+ PointerEventKind::Press { button, .. } => {
+ bar.click(
+ &mut self.shared_state,
+ button.into(),
+ &seat.seat,
+ event.position.0,
+ event.position.1,
+ )
+ .unwrap();
+ }
+ PointerEventKind::Axis {
+ vertical, source, ..
+ } => {
+ if source == Some(wl_pointer::AxisSource::Finger) {
+ if self.shared_state.config.invert_touchpad_scrolling {
+ seat.finger_scroll -= vertical.absolute;
+ } else {
+ seat.finger_scroll += vertical.absolute;
+ }
+ if vertical.stop {
+ seat.finger_scroll = 0.0;
+ }
+ }
+
+ let btn = if vertical.discrete > 0 {
+ PointerBtn::WheelDown
+ } else if vertical.discrete < 0 {
+ PointerBtn::WheelUp
+ } else if seat.finger_scroll >= 15.0 {
+ seat.finger_scroll = 0.0;
+ PointerBtn::WheelDown
+ } else if seat.finger_scroll <= -15.0 {
+ seat.finger_scroll = 0.0;
+ PointerBtn::WheelUp
+ } else {
+ continue;
+ };
+
+ bar.click(
+ &mut self.shared_state,
+ btn,
+ &seat.seat,
+ event.position.0,
+ event.position.1,
+ )
+ .unwrap();
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+}
+
+impl ShmHandler for State {
+ fn shm_state(&mut self) -> &mut ShmState {
+ &mut self.shared_state.shm_state
+ }
+}
+
+impl RiverStatusHandler for State {
+ fn river_status_state(&mut self) -> &mut RiverStatusState {
+ self.river_status_state.as_mut().unwrap()
+ }
+
+ fn focused_tags_updated(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ 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(&mut self.shared_state);
+ }
+
+ fn urgent_tags_updated(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ 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(&mut self.shared_state);
+ }
+
+ fn views_tags_updated(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ output_status: &RiverOutputStatus,
+ tags: Vec<u32>,
+ ) {
+ let bar = self
+ .bars
+ .iter_mut()
+ .find(|b| b.river_output_status.as_ref() == Some(output_status))
+ .unwrap();
+ bar.tags_info.active = tags.into_iter().fold(0, |a, b| a | b);
+ bar.draw(&mut self.shared_state);
+ }
+}
+
+impl RiverControlHandler for State {
+ fn river_control_state(&mut self) -> &mut RiverControlState {
+ self.river_control_state.as_mut().unwrap()
+ }
+
+ fn command_failure(&mut self, _: &Connection, _: &QueueHandle<Self>, message: String) {
+ self.set_error(format!("[river_control] {message}"));
+ }
+
+ fn command_success(&mut self, _: &Connection, _: &QueueHandle<Self>, 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![OutputState,];
+}
+
+#[derive(Debug)]
+pub struct ComputedBlock {
+ pub block: Block,
+ pub full: ComputedText,
+ pub short: Option<ComputedText>,
+ pub min_width: Option<f64>,
+}
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<ChildStdout>,
input: ChildStdin,
protocol: Protocol,
buf: Vec<u8>,
+ pub async_fd: AsyncFd<i32>,
}
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<Option<Vec<Block>>> {
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<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, _, 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<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, _, 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");
+ // }
}
diff --git a/src/tags.rs b/src/tags.rs
index 8114a74..d22853d 100644
--- a/src/tags.rs
+++ b/src/tags.rs
@@ -1,19 +1,21 @@
-use crate::text::{Attributes, ComputedText, Text};
+use crate::text::{Attributes, ComputedText};
use cairo::Context;
use pango::FontDescription;
use pangocairo::{cairo, pango};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TagState {
+ Urgent,
Focused,
+ Active,
Inactive,
- Urgent,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct TagsInfo {
pub focused: u32,
pub urgent: u32,
+ pub active: u32,
}
impl TagsInfo {
@@ -22,15 +24,18 @@ impl TagsInfo {
TagState::Urgent
} else if self.focused >> tag & 1 == 1 {
TagState::Focused
+ } else if self.active >> tag & 1 == 1 {
+ TagState::Active
} else {
TagState::Inactive
}
}
}
-pub fn compute_tag_label(label: String, font: FontDescription, context: &Context) -> ComputedText {
- let text = Text {
- attr: Attributes {
+pub fn compute_tag_label(label: &str, font: &FontDescription, context: &Context) -> ComputedText {
+ ComputedText::new(
+ label,
+ Attributes {
font,
padding_left: 25.0,
padding_right: 25.0,
@@ -38,7 +43,6 @@ pub fn compute_tag_label(label: String, font: FontDescription, context: &Context
align: Default::default(),
markup: false,
},
- text: label,
- };
- text.compute(context)
+ context,
+ )
}
diff --git a/src/text.rs b/src/text.rs
index 60ed707..9b27a11 100644
--- a/src/text.rs
+++ b/src/text.rs
@@ -1,9 +1,8 @@
use crate::color::Color;
-use pango::{EllipsizeMode, FontDescription};
+use pango::FontDescription;
use pangocairo::{cairo, pango};
use serde::Deserialize;
use std::f64::consts::{FRAC_PI_2, PI, TAU};
-use std::ops::Deref;
#[derive(Clone, Debug, PartialEq)]
pub struct RenderOptions {
@@ -17,8 +16,8 @@ pub struct RenderOptions {
}
#[derive(Clone, Debug, PartialEq)]
-pub struct Attributes {
- pub font: FontDescription,
+pub struct Attributes<'a> {
+ pub font: &'a FontDescription,
pub padding_left: f64,
pub padding_right: f64,
pub min_width: Option<f64>,
@@ -40,96 +39,55 @@ impl Default for Align {
}
}
-fn create_pango_layout(cairo_context: &cairo::Context) -> pango::Layout {
- pangocairo::create_layout(cairo_context).unwrap()
-}
-
-fn show_pango_layout(cairo_context: &cairo::Context, layout: &pango::Layout) {
- pangocairo::show_layout(cairo_context, layout);
-}
-
#[derive(Clone, Debug, PartialEq)]
-pub struct Text {
- pub attr: Attributes,
- pub text: String,
+pub struct ComputedText {
+ pub width: f64,
+ layout: pango::Layout,
+ height: f64,
+ padding_left: f64,
}
-impl Text {
- pub fn compute(mut self, context: &cairo::Context) -> ComputedText {
- let (mut width, height) = {
- let layout = create_pango_layout(context);
- layout.set_font_description(Some(&self.attr.font));
- if self.attr.markup {
- layout.set_markup(&self.text);
- } else {
- layout.set_text(&self.text);
- }
+impl ComputedText {
+ pub fn new(text: &str, mut attr: Attributes, context: &cairo::Context) -> Self {
+ let layout = pangocairo::create_layout(context);
+ layout.set_font_description(Some(attr.font));
+ if attr.markup {
+ layout.set_markup(text);
+ } else {
+ layout.set_text(text);
+ }
- let (text_width, text_height) = layout.pixel_size();
- let width = f64::from(text_width) + self.attr.padding_right + self.attr.padding_right;
- let height = f64::from(text_height);
- (width, height)
- };
+ let (text_width, text_height) = layout.pixel_size();
+ let mut width = f64::from(text_width) + attr.padding_right + attr.padding_right;
+ let height = f64::from(text_height);
- if let Some(min_width) = self.attr.min_width {
+ if let Some(min_width) = attr.min_width {
if width < min_width {
let d = min_width - width;
width = min_width;
- match self.attr.align {
- Align::Right => self.attr.padding_left += d,
- Align::Left => self.attr.padding_right += d,
+ match attr.align {
+ Align::Right => attr.padding_left += d,
+ Align::Left => attr.padding_right += d,
Align::Center => {
- self.attr.padding_left += d * 0.5;
- self.attr.padding_right += d * 0.5;
+ attr.padding_left += d * 0.5;
+ attr.padding_right += d * 0.5;
}
}
}
}
- ComputedText {
- text: self,
+ Self {
width,
+ layout,
height,
+ padding_left: attr.padding_left,
}
}
-}
-#[derive(Clone, Debug, PartialEq)]
-pub struct ComputedText {
- pub text: Text,
- pub width: f64,
- pub height: f64,
-}
-
-impl Deref for ComputedText {
- type Target = str;
-
- fn deref(&self) -> &Self::Target {
- &self.text.text
- }
-}
-
-impl ComputedText {
pub fn render(&self, context: &cairo::Context, options: RenderOptions) {
- let text = &self.text;
- let layout = create_pango_layout(context);
- layout.set_font_description(Some(&text.attr.font));
- if text.attr.markup {
- layout.set_markup(&text.text);
- } else {
- layout.set_text(&text.text);
- }
-
context.save().unwrap();
context.translate(options.x_offset - options.overlap, 0.0);
- // Set the width/height on the Pango layout so that it word-wraps/ellipises.
- let text_width = self.width - text.attr.padding_left - text.attr.padding_right;
- let text_height = self.height;
- layout.set_ellipsize(EllipsizeMode::End);
- layout.set_width(text_width as i32 * pango::SCALE);
- layout.set_height(text_height as i32 * pango::SCALE);
-
// Draw background
if let Some(bg) = options.bg_color {
bg.apply(context);
@@ -147,10 +105,10 @@ impl ComputedText {
options.fg_color.apply(context);
context.translate(
- text.attr.padding_left + options.overlap,
+ self.padding_left + options.overlap,
(options.bar_height - self.height) * 0.5,
);
- show_pango_layout(context, &layout);
+ pangocairo::show_layout(context, &self.layout);
context.restore().unwrap();
}
}
@@ -177,16 +135,17 @@ fn rounded_rectangle(
}
pub fn width_of(text: &str, context: &cairo::Context, markup: bool, font: &FontDescription) -> f64 {
- let text = Text {
- text: text.into(),
- attr: Attributes {
- font: font.clone(),
+ ComputedText::new(
+ text,
+ Attributes {
+ font,
padding_left: 0.0,
padding_right: 0.0,
min_width: None,
align: Default::default(),
markup,
},
- };
- text.compute(context).width
+ context,
+ )
+ .width
}