use crate::config::{Config, GlobalConfig};
use crate::error::{Error, Result};
use crate::notification::{Manager, Notification, NOTIFICATION_MESSAGE_TEMPLATE};
use cairo::{
Context as CairoContext, XCBConnection as CairoXCBConnection, XCBDrawable, XCBSurface,
XCBVisualType,
};
use colorsys::ColorAlpha;
use pango::{Context as PangoContext, FontDescription, Layout as PangoLayout};
use pangocairo::functions as pango_functions;
use std::collections::HashMap;
use std::error::Error as StdError;
use std::sync::Arc;
use std::time::Duration;
use tera::{Result as TeraResult, Tera, Value};
use x11rb::connection::Connection;
use x11rb::protocol::{xproto::*, Event};
use x11rb::xcb_ffi::XCBConnection;
use x11rb::COPY_DEPTH_FROM_PARENT;
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct xcb_visualtype_t {
visual_id: u32,
class: u8,
bits_per_rgb_value: u8,
colormap_entries: u16,
red_mask: u32,
green_mask: u32,
blue_mask: u32,
pad0: [u8; 4],
}
impl From<Visualtype> for xcb_visualtype_t {
fn from(value: Visualtype) -> xcb_visualtype_t {
xcb_visualtype_t {
visual_id: value.visual_id,
class: value.class.into(),
bits_per_rgb_value: value.bits_per_rgb_value,
colormap_entries: value.colormap_entries,
red_mask: value.red_mask,
green_mask: value.green_mask,
blue_mask: value.blue_mask,
pad0: [0; 4],
}
}
}
pub struct X11 {
connection: XCBConnection,
cairo: CairoXCBConnection,
screen: Screen,
}
unsafe impl Send for X11 {}
unsafe impl Sync for X11 {}
impl X11 {
pub fn init(screen_num: Option<usize>) -> Result<Self> {
let (connection, default_screen_num) = XCBConnection::connect(None)?;
tracing::trace!("Default screen num: {:?}", default_screen_num);
let setup_info = connection.setup();
tracing::trace!("Setup info status: {:?}", setup_info.status);
let screen = setup_info.roots[screen_num.unwrap_or(default_screen_num)].clone();
tracing::trace!("Screen root: {:?}", screen.root);
let cairo =
unsafe { CairoXCBConnection::from_raw_none(connection.get_raw_xcb_connection() as _) };
Ok(Self {
connection,
screen,
cairo,
})
}
pub fn create_window(&mut self, config: &GlobalConfig) -> Result<X11Window> {
let visual_id = self.screen.root_visual;
let mut visual_type = self
.find_xcb_visualtype(visual_id)
.ok_or_else(|| Error::X11Other(String::from("cannot find a XCB visual type")))?;
let visual = unsafe { XCBVisualType::from_raw_none(&mut visual_type as *mut _ as _) };
let window_id = self.connection.generate_id()?;
tracing::trace!("Window ID: {:?}", window_id);
self.connection.create_window(
COPY_DEPTH_FROM_PARENT,
window_id,
self.screen.root,
config.geometry.x.try_into()?,
config.geometry.y.try_into()?,
config.geometry.width.try_into()?,
config.geometry.height.try_into()?,
0,
WindowClass::INPUT_OUTPUT,
visual_id,
&CreateWindowAux::new()
.border_pixel(self.screen.white_pixel)
.override_redirect(1)
.event_mask(EventMask::EXPOSURE | EventMask::BUTTON_PRESS),
)?;
let surface = XCBSurface::create(
&self.cairo,
&XCBDrawable(window_id),
&visual,
config.geometry.width.try_into()?,
config.geometry.height.try_into()?,
)?;
let context = CairoContext::new(&surface)?;
X11Window::new(
window_id,
context,
&config.font,
Box::leak(config.template.to_string().into_boxed_str()),
)
}
fn find_xcb_visualtype(&self, visual_id: u32) -> Option<xcb_visualtype_t> {
for root in &self.connection.setup().roots {
for depth in &root.allowed_depths {
for visual in &depth.visuals {
if visual.visual_id == visual_id {
return Some((*visual).into());
}
}
}
}
None
}
pub fn show_window(&self, window: &X11Window) -> Result<()> {
window.show(&self.connection)?;
self.connection.flush()?;
Ok(())
}
pub fn hide_window(&self, window: &X11Window) -> Result<()> {
window.hide(&self.connection)?;
self.connection.flush()?;
Ok(())
}
pub fn handle_events<F>(
&self,
window: Arc<X11Window>,
manager: Manager,
config: Arc<Config>,
on_press: F,
) -> Result<()>
where
F: Fn(&Notification),
{
loop {
self.connection.flush()?;
let event = self.connection.wait_for_event()?;
let mut event_opt = Some(event);
while let Some(event) = event_opt {
tracing::trace!("New event: {:?}", event);
match event {
Event::Expose(_) => {
let notification = manager.get_last_unread();
let unread_count = manager.get_unread_count();
window.draw(&self.connection, notification, unread_count, &config)?;
}
Event::ButtonPress(_) => {
let notification = manager.get_last_unread();
manager.mark_last_as_read();
on_press(¬ification);
}
_ => {}
}
event_opt = self.connection.poll_for_event()?;
}
}
}
}
pub struct X11Window {
pub id: u32,
pub cairo_context: CairoContext,
pub pango_context: PangoContext,
pub layout: PangoLayout,
pub template: Tera,
}
unsafe impl Send for X11Window {}
unsafe impl Sync for X11Window {}
impl X11Window {
pub fn new(
id: u32,
cairo_context: CairoContext,
font: &str,
raw_template: &'static str,
) -> Result<Self> {
let pango_context = pango_functions::create_context(&cairo_context);
let layout = PangoLayout::new(&pango_context);
let font_description = FontDescription::from_string(font);
pango_context.set_font_description(Some(&font_description));
let mut template = Tera::default();
if let Err(e) =
template.add_raw_template(NOTIFICATION_MESSAGE_TEMPLATE, raw_template.trim())
{
return if let Some(error_source) = e.source() {
Err(Error::TemplateParse(error_source.to_string()))
} else {
Err(Error::Template(e))
};
}
template.register_filter(
"humantime",
|value: &Value, _: &HashMap<String, Value>| -> TeraResult<Value> {
let value = tera::try_get_value!("humantime_filter", "value", u64, value);
let value = humantime::format_duration(Duration::new(value, 0)).to_string();
Ok(tera::to_value(value)?)
},
);
Ok(Self {
id,
cairo_context,
pango_context,
layout,
template,
})
}
fn show(&self, connection: &impl Connection) -> Result<()> {
connection.map_window(self.id)?;
Ok(())
}
fn hide(&self, connection: &impl Connection) -> Result<()> {
connection.unmap_window(self.id)?;
Ok(())
}
fn draw(
&self,
connection: &XCBConnection,
notification: Notification,
unread_count: usize,
config: &Config,
) -> Result<()> {
let urgency_config = config.get_urgency_config(¬ification.urgency);
urgency_config.run_commands(¬ification)?;
let message =
notification.render_message(&self.template, urgency_config.text, unread_count)?;
let background_color = urgency_config.background;
self.cairo_context.set_source_rgba(
background_color.red() / 255.0,
background_color.green() / 255.0,
background_color.blue() / 255.0,
background_color.alpha(),
);
self.cairo_context.fill()?;
self.cairo_context.paint()?;
let foreground_color = urgency_config.foreground;
self.cairo_context.set_source_rgba(
foreground_color.red() / 255.0,
foreground_color.green() / 255.0,
foreground_color.blue() / 255.0,
foreground_color.alpha(),
);
self.cairo_context.move_to(0., 0.);
self.layout.set_markup(&message);
if config.global.wrap_content {
let (width, height) = self.layout.pixel_size();
let values = ConfigureWindowAux::default()
.width(width.try_into().ok())
.height(height.try_into().ok());
connection.configure_window(self.id, &values)?;
}
pango_functions::show_layout(&self.cairo_context, &self.layout);
Ok(())
}
}