From 7bcaa341b8685ba411d206c1a89dc3ca77aa71bb Mon Sep 17 00:00:00 2001 From: Max Verevkin Date: Fri, 1 Apr 2022 21:50:19 +0300 Subject: initial commit --- src/button_manager.rs | 19 ++ src/color.rs | 85 +++++++ src/config.rs | 114 ++++++++++ src/i3bar_protocol.rs | 150 +++++++++++++ src/lines_buffer.rs | 54 +++++ src/main.rs | 596 +++++++++++++++++++++++++++++++++++++++++++++++++ src/pointer_btn.rs | 62 +++++ src/river_protocols.rs | 35 +++ src/status_cmd.rs | 93 ++++++++ src/tags.rs | 44 ++++ src/text.rs | 149 +++++++++++++ 11 files changed, 1401 insertions(+) create mode 100644 src/button_manager.rs create mode 100644 src/color.rs create mode 100644 src/config.rs create mode 100644 src/i3bar_protocol.rs create mode 100644 src/lines_buffer.rs create mode 100644 src/main.rs create mode 100644 src/pointer_btn.rs create mode 100644 src/river_protocols.rs create mode 100644 src/status_cmd.rs create mode 100644 src/tags.rs create mode 100644 src/text.rs (limited to 'src') diff --git a/src/button_manager.rs b/src/button_manager.rs new file mode 100644 index 0000000..e46a139 --- /dev/null +++ b/src/button_manager.rs @@ -0,0 +1,19 @@ +#[derive(Default)] +pub struct ButtonManager(Vec<(f64, f64, T)>); + +impl ButtonManager { + pub fn push(&mut self, x_offset: f64, width: f64, elem: T) { + self.0.push((x_offset, width, elem)); + } + + pub fn clear(&mut self) { + self.0.clear() + } + + pub fn click(&self, x: f64) -> Option<&T> { + self.0 + .iter() + .find(|(x_off, w, _)| x >= *x_off && x <= *x_off + *w) + .map(|(_, _, e)| e) + } +} diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..7305c24 --- /dev/null +++ b/src/color.rs @@ -0,0 +1,85 @@ +use pangocairo::cairo::Context; +use serde::de; +use std::fmt; +use std::str::FromStr; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Color { + red: f64, + green: f64, + blue: f64, + alpha: f64, +} + +impl Color { + pub fn apply(self, cr: &Context) { + if self.alpha.is_nan() { + cr.set_source_rgb(self.red, self.green, self.blue); + } else { + cr.set_source_rgba(self.red, self.green, self.blue, self.alpha); + } + } + + pub fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { + red: r as f64 / 255.0, + green: g as f64 / 255.0, + blue: b as f64 / 255.0, + alpha: if a == 255 { f64::NAN } else { a as f64 / 255.0 }, + } + } + + pub fn from_rgba_hex(hex: u32) -> Self { + let r = (hex >> 24 & 0xFF) as u8; + let g = (hex >> 16 & 0xFF) as u8; + let b = (hex >> 8 & 0xFF) as u8; + let a = (hex & 0xFF) as u8; + Self::from_rgba(r, g, b, a) + } +} + +impl FromStr for Color { + type Err = (); + + fn from_str(color: &str) -> Result { + let rgb = color.get(1..7).ok_or(())?; + let rgb = u32::from_str_radix(rgb, 16).map_err(|_| ())?; + let r = (rgb >> 16 & 0xFF) as u8; + let g = (rgb >> 8 & 0xFF) as u8; + let b = (rgb & 0xFF) as u8; + + let a = match color.get(7..9) { + Some(a) => u8::from_str_radix(a, 16).map_err(|_| ())?, + None => 255, + }; + + Ok(Self::from_rgba(r, g, b, a)) + } +} + +impl<'de> de::Deserialize<'de> for Color { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + struct ColorVisitor; + + impl<'de> de::Visitor<'de> for ColorVisitor { + type Value = Color; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("RBG or RGBA color (in hex)") + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + s.parse() + .map_err(|_| E::custom(format!("'{s}' is not a valid RGB/RGBA color"))) + } + } + + deserializer.deserialize_str(ColorVisitor) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..21ef9a2 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,114 @@ +use crate::color::Color; +use anyhow::{Context, Result}; +use dirs_next::config_dir; +use pangocairo::pango::FontDescription; +use serde::{de, Deserialize}; +use std::fmt; +use std::fs::read_to_string; +use std::ops::Deref; + +#[derive(Deserialize)] +#[serde(default)] +pub struct Config { + // colors + pub background: Color, + pub color: Color, + pub separator: Color, + pub tag_fg: Color, + pub tag_bg: Color, + pub tag_focused_fg: Color, + pub tag_focused_bg: Color, + pub tag_urgent_fg: Color, + pub tag_urgent_bg: Color, + // font and size + pub font: Font, + pub height: u32, + // command + pub command: Option, +} + +impl Default for Config { + fn default() -> Self { + // A kind of gruvbox theme + Self { + background: Color::from_rgba_hex(0x282828ff), + color: Color::from_rgba_hex(0xffffffff), + separator: Color::from_rgba_hex(0x9a8a62ff), + tag_fg: Color::from_rgba_hex(0xd79921ff), + tag_bg: Color::from_rgba_hex(0x282828ff), + tag_focused_fg: Color::from_rgba_hex(0x1d2021ff), + tag_focused_bg: Color::from_rgba_hex(0x689d68ff), + tag_urgent_fg: Color::from_rgba_hex(0x282828ff), + tag_urgent_bg: Color::from_rgba_hex(0xcc241dff), + font: Font::new("monospace 10"), + height: 24, + command: None, + } + } +} + +impl Config { + pub fn new() -> Result { + Ok(match config_dir() { + Some(mut config_path) => { + config_path.push("i3bar-river"); + config_path.push("config.toml"); + if config_path.exists() { + let config = + read_to_string(config_path).context("Failed to read configuration")?; + toml::from_str(&config).context("Failed to deserialize configuration")? + } else { + info!("Using default configuration"); + Self::default() + } + } + None => { + warn!("Could not find the configuration path"); + info!("Using default configuration"); + Self::default() + } + }) + } +} + +pub struct Font(pub FontDescription); + +impl Font { + pub fn new(desc: &str) -> Self { + Self(FontDescription::from_string(desc)) + } +} + +impl Deref for Font { + type Target = FontDescription; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'de> de::Deserialize<'de> for Font { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + struct FontVisitor; + + impl<'de> de::Visitor<'de> for FontVisitor { + type Value = Font; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("font description") + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + Ok(Font::new(s)) + } + } + + deserializer.deserialize_str(FontVisitor) + } +} diff --git a/src/i3bar_protocol.rs b/src/i3bar_protocol.rs new file mode 100644 index 0000000..d838fb1 --- /dev/null +++ b/src/i3bar_protocol.rs @@ -0,0 +1,150 @@ +use crate::color::Color; +use crate::pointer_btn::PointerBtn; +use crate::text::Align; +use serde::Deserialize; +use serde::Serialize; +use std::io::{Error, ErrorKind, Result}; + +#[derive(Deserialize, Default)] +pub struct Block { + pub full_text: String, + #[serde(default)] + pub short_text: Option, + #[serde(default)] + pub color: Option, + #[serde(default)] + pub background: Option, + #[serde(default)] + pub min_width: Option, + #[serde(default)] + pub align: Option, + #[serde(default)] + pub name: Option, + #[serde(default)] + pub instance: Option, + #[serde(default = "def_sep")] + pub separator: bool, + #[serde(default = "def_sep_width")] + pub separator_block_width: u8, + #[serde(default)] + pub markup: Option, +} + +fn def_sep() -> bool { + true +} + +fn def_sep_width() -> u8 { + 9 +} + +#[derive(Deserialize, Clone)] +#[serde(untagged)] +pub enum MinWidth { + Text(String), + Pixels(u32), +} + +#[derive(Serialize, Default)] +pub struct Event<'a> { + pub name: Option<&'a str>, + pub instance: Option<&'a str>, + pub button: PointerBtn, + // Not available on wayland + pub modifiers: Vec<()>, + // I see no reason to have these in the protocol, as a lot depends on font & pango markup + pub x: u8, + pub y: u8, + pub relative_x: u8, + pub relative_y: u8, + pub output_x: u8, + pub output_y: u8, + pub width: u8, + pub height: u8, +} + +#[derive(Deserialize, Clone, Copy)] +#[serde(deny_unknown_fields)] +pub struct JsonHeader { + version: u8, + #[serde(default)] + #[allow(dead_code)] + stop_signal: i32, + #[serde(default)] + #[allow(dead_code)] + cont_signal: i32, + #[serde(default)] + click_events: bool, +} + +pub enum Protocol { + Unknown, + PlainText(Option), + JsonNotStarted(JsonHeader), + Json(JsonHeader, Option>), +} + +impl Protocol { + pub fn process_line(&mut self, line: String) -> Result<()> { + match self { + Self::Unknown => { + if let Ok(header) = serde_json::from_str::(&line) { + if header.version == 1 { + *self = Protocol::JsonNotStarted(header); + return Ok(()); + } + } + *self = Self::PlainText(Some(line)) + } + Self::PlainText(s) => *s = Some(line), + Self::JsonNotStarted(header) => { + let line = line.trim(); + if !line.is_empty() { + if !line.starts_with('[') && line.is_empty() { + return Err(Error::new(ErrorKind::InvalidData, "Expected '['")); + } + *self = Self::Json(*header, None); + self.process_line(line[1..].to_owned())?; + } + } + Self::Json(_, blocks) => { + let line = line.trim(); + if !line.is_empty() { + if !line.ends_with(',') { + return Err(Error::new(ErrorKind::InvalidData, "Expected ','")); + } + *blocks = match serde_json::from_str::>(&line[..(line.len() - 1)]) { + Ok(b) => Some(b), + Err(e) => { + return Err(Error::new( + ErrorKind::InvalidData, + format!("Invalid JSON: {e}"), + )) + } + }; + } + } + } + Ok(()) + } + + pub fn get_blocks(&mut self) -> Option> { + match self { + Self::Unknown => None, + Self::PlainText(text) => Some(vec![Block { + full_text: text.take()?, + ..Default::default() + }]), + Self::JsonNotStarted(_) => None, + Self::Json(_, blocks) => blocks.take(), + } + } + + pub fn supports_clicks(&self) -> bool { + match self { + Self::JsonNotStarted(h) => h.click_events, + Self::Json(h, _) => h.click_events, + _ => false, + } + } +} diff --git a/src/lines_buffer.rs b/src/lines_buffer.rs new file mode 100644 index 0000000..7813ea2 --- /dev/null +++ b/src/lines_buffer.rs @@ -0,0 +1,54 @@ +use std::borrow::Cow; +use std::io::{BufRead, BufReader, Read, Result}; + +pub struct LinesBuffer { + reader: BufReader, + next_line: Vec, +} + +impl LinesBuffer { + pub fn new(inner: R) -> Self { + Self { + reader: BufReader::new(inner), + next_line: Vec::new(), + } + } + + pub fn fill_buf(&mut self) -> Result<()> { + self.reader.fill_buf()?; + Ok(()) + } + + pub fn inner(&self) -> &R { + self.reader.get_ref() + } +} + +impl Iterator for &mut LinesBuffer { + type Item = String; + + fn next(&mut self) -> Option { + let pending = self.reader.buffer(); + let (result, used) = match memchr::memchr(b'\n', pending) { + None => { + self.next_line.extend_from_slice(pending); + (None, pending.len()) + } + Some(i) => { + self.next_line.extend_from_slice(&pending[..i]); + let line = from_utf8(&self.next_line); + self.next_line.clear(); + (Some(line), i + 1) + } + }; + self.reader.consume(used); + result + } +} + +fn from_utf8(bytes: &[u8]) -> String { + match String::from_utf8_lossy(bytes) { + Cow::Borrowed(s) => s.to_owned(), + Cow::Owned(s) => s, + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cc37aa7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,596 @@ +#[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, 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, + }, + }, + shm::AutoMemPool, + WaylandSource, +}; + +use pangocairo::cairo; + +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +mod button_manager; +mod color; +mod config; +mod i3bar_protocol; +mod lines_buffer; +mod pointer_btn; +mod river_protocols; +mod status_cmd; +mod tags; +mod text; + +use button_manager::ButtonManager; +use config::Config; +use i3bar_protocol::{Block, MinWidth}; +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 = [ + layer_shell: SimpleGlobal, + river_status_manager: SimpleGlobal, + river_control: SimpleGlobal, + ], + singles = [ + zwlr_layer_shell_v1::ZwlrLayerShellV1 => layer_shell, + zriver_status_manager_v1::ZriverStatusManagerV1 => river_status_manager, + zriver_control_v1::ZriverControlV1 => river_control, + ], +); + +fn main() { + env_logger::init(); + + let (env, display, queue) = new_default_environment!( + Env, + fields = [ + layer_shell: SimpleGlobal::new(), + river_status_manager: SimpleGlobal::new(), + river_control: SimpleGlobal::new(), + ] + ) + .expect("Initial roundtrip failed!"); + + let config = Rc::new(RefCell::new(Config::new().unwrap())); + let surfaces = Rc::new(RefCell::new(Vec::::new())); + let blocks = Rc::new(RefCell::new((Vec::::new(), false))); + + let cmd = config.borrow_mut().command.take().unwrap(); + let status_cmd = StatusCmd::new(&cmd, Rc::clone(&blocks)).expect("failed run status command"); + + let layer_shell = env.require_global::(); + let river_status = env.require_global::(); + let river_control = env.require_global::(); + + 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 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); + output.release(); + } else { + info!("Output detected"); + let surface = env_handle.create_surface().detach(); + let pool = env_handle + .create_auto_pool() + .expect("Failed to create a memory pool!"); + (*surfaces_handle.borrow_mut()).push(Surface::new( + &output, + info.id, + surface, + &layer_shell, + &river_status, + river_control.clone(), + pool, + config_handle.clone(), + blocks_handle.clone(), + status_cmd_handle.clone(), + )); + } + }; + + // 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); + } + } + + // 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, _| 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 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); + } + } + } + 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, + ); + } + } + } + _ => (), + } + }); + } + }); + } + + let mut event_loop = calloop::EventLoop::<()>::try_new().unwrap(); + + WaylandSource::new(queue) + .quick_insert(event_loop.handle()) + .unwrap(); + + // Run command + let blocks_handle = Rc::clone(&blocks); + event_loop + .handle() + .insert_source( + calloop::generic::Generic::new( + status_cmd, + calloop::Interest { + readable: true, + writable: false, + }, + calloop::Mode::Edge, + ), + move |ready, cmd, _| { + info!("status command update"); + if ready.readable { + cmd.notify_available()?; + Ok(calloop::PostAction::Continue) + } else { + let mut blocks = blocks_handle.borrow_mut(); + blocks.0 = vec![Block { + full_text: "[error reading from status command]".into(), + ..Default::default() + }]; + blocks.1 = true; + Ok(calloop::PostAction::Remove) + } + }, + ) + .expect("failed to inser calloop source"); + + 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; + } + } + } + + display.flush().unwrap(); + event_loop.dispatch(None, &mut ()).unwrap(); + } +} + +#[derive(PartialEq, Copy, Clone)] +enum RenderEvent { + Configure { width: u32, height: u32 }, + Closed, +} + +pub struct Surface { + output_id: u32, + surface: wl_surface::WlSurface, + layer_surface: Main, + next_render_event: Rc>>, + pool: AutoMemPool, + dimensions: (u32, u32), + config: Rc>, + // river stuff + river_output_status: Main, + river_control: Attached, + // blocks + blocks: Rc, bool)>>, + status_cmd: StatusCmd, + // tags + tags_info: Rc>, + tags_computed: Vec, + // Clicking stuff + pointer: Option<(f64, f64)>, + tags_btns: ButtonManager, + blocks_btns: ButtonManager<(Option, Option)>, +} + +impl Surface { + fn new( + output: &wl_output::WlOutput, + output_id: u32, + surface: wl_surface::WlSurface, + layer_shell: &Attached, + river_status: &Attached, + river_control: Attached, + pool: AutoMemPool, + config: Rc>, + blocks: Rc, bool)>>, + 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 blocks_handle = Rc::clone(&blocks); + 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; + blocks_handle.borrow_mut().1 = true; + } + zriver_output_status_v1::Event::UrgentTags { tags } => { + info!("Urgent tags updated: {tags}"); + tags_info_handle.borrow_mut().urgent = tags; + blocks_handle.borrow_mut().1 = true; + } + _ => (), + }); + + Self { + output_id, + surface, + layer_surface, + next_render_event, + pool, + dimensions: (0, 0), + config, + river_output_status, + river_control, + blocks, + 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; + } + } + _ => (), + } + if self.blocks.borrow().1 { + 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 mut blocks = self.blocks.borrow_mut(); + + 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 + // TODO: handle short_text + let mut offset_right = 0.0; + blocks.1 = false; + self.blocks_btns.clear(); + let mut first_block = true; + for block in blocks.0.iter().rev() { + if !first_block && block.separator && block.separator_block_width > 0 { + let w = block.separator_block_width as f64; + config.separator.apply(&cairo_ctx); + cairo_ctx.set_line_width(2.0); + cairo_ctx.move_to(width_f - offset_right - w * 0.5, height_f * 0.1); + cairo_ctx.line_to(width_f - offset_right - w * 0.5, height_f * 0.9); + cairo_ctx.stroke().unwrap(); + offset_right += w; + } + let markup = block.markup.as_deref() == Some("pango"); + let 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: match &block.min_width { + Some(MinWidth::Pixels(p)) => Some(*p as f64), + Some(MinWidth::Text(t)) => { + Some(text::width_of(t, &cairo_ctx, markup, &config.font.0)) + } + None => None, + }, + align: block.align.unwrap_or_default(), + markup, + }, + }; + let comp = text.compute(&cairo_ctx); + comp.render( + &cairo_ctx, + RenderOptions { + x_offset: width_f - comp.width - offset_right, + bar_height: height_f, + fg_color: block.color.unwrap_or(config.color), + bg_color: block.background, + }, + ); + offset_right += comp.width; + self.blocks_btns.push( + width_f - offset_right, + comp.width, + (block.name.clone(), block.instance.clone()), + ); + first_block = 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(); + } +} diff --git a/src/pointer_btn.rs b/src/pointer_btn.rs new file mode 100644 index 0000000..829fc9c --- /dev/null +++ b/src/pointer_btn.rs @@ -0,0 +1,62 @@ +use serde::{Serialize, Serializer}; + +// From linux/input-event-codes.h +const BTN_LEFT: u32 = 0x110; +const BTN_RIGHT: u32 = 0x111; +const BTN_MIDDLE: u32 = 0x112; +// const BTN_SIDE: u32 = 0x113; +// const BTN_EXTRA: u32 = 0x114; +const BTN_FORWARD: u32 = 0x115; +const BTN_BACK: u32 = 0x116; +// const BTN_TASK: u32 = 0x117; + +#[derive(Debug, Clone, Copy)] +pub enum PointerBtn { + Left, + Middle, + Right, + Forward, + WheelUp, + WheelDown, + Back, + Unknown, +} + +impl Default for PointerBtn { + fn default() -> Self { + Self::Unknown + } +} + +impl From for PointerBtn { + fn from(code: u32) -> Self { + use PointerBtn::*; + match code { + BTN_LEFT => Left, + BTN_MIDDLE => Middle, + BTN_RIGHT => Right, + BTN_FORWARD => Forward, + BTN_BACK => Back, + _ => Unknown, + } + } +} + +impl Serialize for PointerBtn { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let num = match *self { + PointerBtn::Left => 1, + PointerBtn::Middle => 2, + PointerBtn::Right => 3, + PointerBtn::WheelUp => 4, + PointerBtn::WheelDown => 5, + PointerBtn::Forward => 9, + PointerBtn::Back => 8, + PointerBtn::Unknown => 0, + }; + serializer.serialize_u8(num) + } +} diff --git a/src/river_protocols.rs b/src/river_protocols.rs new file mode 100644 index 0000000..0c77823 --- /dev/null +++ b/src/river_protocols.rs @@ -0,0 +1,35 @@ +pub use generated::client1::*; +pub use generated::client2::*; + +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)] + + pub mod client1 { + // These imports are used by the generated code + pub(crate) use smithay_client_toolkit::reexports::client as wayland_client; + pub(crate) use wayland_client::protocol::*; + pub(crate) use wayland_client::sys; + pub(crate) use wayland_client::{AnonymousObject, Attached, Main, Proxy, ProxyMap}; + pub(crate) use wayland_commons::map::{Object, ObjectMetadata}; + pub(crate) use wayland_commons::smallvec; + pub(crate) use wayland_commons::wire::{Argument, ArgumentType, Message, MessageDesc}; + pub(crate) 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 + pub(crate) use smithay_client_toolkit::reexports::client as wayland_client; + pub(crate) use wayland_client::protocol::*; + pub(crate) use wayland_client::sys; + pub(crate) use wayland_client::{AnonymousObject, Attached, Main, Proxy, ProxyMap}; + pub(crate) use wayland_commons::map::{Object, ObjectMetadata}; + pub(crate) use wayland_commons::smallvec; + pub(crate) use wayland_commons::wire::{Argument, ArgumentType, Message, MessageDesc}; + pub(crate) use wayland_commons::{Interface, MessageGroup}; + include!(concat!(env!("OUT_DIR"), "/river-control-unstable-v1.rs")); + } +} diff --git a/src/status_cmd.rs b/src/status_cmd.rs new file mode 100644 index 0000000..1f6231b --- /dev/null +++ b/src/status_cmd.rs @@ -0,0 +1,93 @@ +use std::cell::RefCell; +use std::io::Result; +use std::io::Write; +use std::os::unix::prelude::{AsRawFd, RawFd}; +use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; +use std::rc::Rc; + +use crate::i3bar_protocol::{Block, Event, Protocol}; +use crate::lines_buffer::LinesBuffer; +use crate::pointer_btn::PointerBtn; + +#[derive(Clone)] +pub struct StatusCmd { + inner: Rc>, +} + +struct Inner { + #[allow(dead_code)] + child: Child, + output: LinesBuffer, + input: ChildStdin, + protocol: Protocol, + blocks: Rc, bool)>>, +} + +impl AsRawFd for StatusCmd { + fn as_raw_fd(&self) -> RawFd { + self.inner.borrow().output.inner().as_raw_fd() + } +} + +impl Inner { + pub fn notify_available(&mut self) -> Result<()> { + self.output.fill_buf()?; + for line in &mut self.output { + self.protocol.process_line(line)?; + } + if let Some(new_blocks) = self.protocol.get_blocks() { + let mut blocks = self.blocks.borrow_mut(); + blocks.0 = new_blocks; + blocks.1 = true; + } + Ok(()) + } +} + +impl StatusCmd { + pub fn new(cmd: &str, blocks: Rc, bool)>>) -> Result { + let mut child = Command::new("sh") + .args(["-c", &format!("exec {cmd}")]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + let output = LinesBuffer::new(child.stdout.take().unwrap()); + let input = child.stdin.take().unwrap(); + Ok(Self { + inner: Rc::new(RefCell::new(Inner { + child, + output, + input, + protocol: Protocol::Unknown, + blocks, + })), + }) + } + + pub fn notify_available(&mut self) -> Result<()> { + self.inner.borrow_mut().notify_available() + } + + 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() + )?; + } + Ok(()) + } +} diff --git a/src/tags.rs b/src/tags.rs new file mode 100644 index 0000000..8114a74 --- /dev/null +++ b/src/tags.rs @@ -0,0 +1,44 @@ +use crate::text::{Attributes, ComputedText, Text}; +use cairo::Context; +use pango::FontDescription; +use pangocairo::{cairo, pango}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TagState { + Focused, + Inactive, + Urgent, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct TagsInfo { + pub focused: u32, + pub urgent: u32, +} + +impl TagsInfo { + pub fn get_state(self, tag: usize) -> TagState { + if self.urgent >> tag & 1 == 1 { + TagState::Urgent + } else if self.focused >> tag & 1 == 1 { + TagState::Focused + } else { + TagState::Inactive + } + } +} + +pub fn compute_tag_label(label: String, font: FontDescription, context: &Context) -> ComputedText { + let text = Text { + attr: Attributes { + font, + padding_left: 25.0, + padding_right: 25.0, + min_width: None, + align: Default::default(), + markup: false, + }, + text: label, + }; + text.compute(context) +} diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000..eefbd48 --- /dev/null +++ b/src/text.rs @@ -0,0 +1,149 @@ +use crate::color::Color; +use pango::{EllipsizeMode, FontDescription}; +use pangocairo::{cairo, pango}; +use serde::Deserialize; + +#[derive(Clone, Debug, PartialEq)] +pub struct RenderOptions { + pub x_offset: f64, + pub bar_height: f64, + pub fg_color: Color, + pub bg_color: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Attributes { + pub font: FontDescription, + pub padding_left: f64, + pub padding_right: f64, + pub min_width: Option, + pub align: Align, + pub markup: bool, +} + +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum Align { + Right, + Left, + Center, +} + +impl Default for Align { + fn default() -> Self { + Self::Left + } +} + +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, +} + +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); + } + + 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) + }; + + if let Some(min_width) = self.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, + Align::Center => { + self.attr.padding_left += d * 0.5; + self.attr.padding_right += d * 0.5; + } + } + } + } + + ComputedText { + text: self, + width, + height, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ComputedText { + pub text: Text, + pub width: f64, + pub height: f64, +} + +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, 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); + + if let Some(bg) = options.bg_color { + bg.apply(&context); + context.rectangle(0.0, 0.0, self.width, options.bar_height); + context.fill().unwrap(); + } + + options.fg_color.apply(&context); + context.translate( + text.attr.padding_left, + (options.bar_height - self.height) * 0.5, + ); + show_pango_layout(context, &layout); + context.restore().unwrap(); + } +} + +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(), + padding_left: 0.0, + padding_right: 0.0, + min_width: None, + align: Default::default(), + markup, + }, + }; + text.compute(context).width +} -- cgit v1.3.1