notification daemon
git clone https://git.lucas.co/cce-notifier.git
refactor: reimplement cce-notifier on the cce-ui Application engine
Drop the self-rolled wayland/wgpu/event-loop stack (~1000 lines) in favor of
cce_ui::engine::run with the new opt-in layer-shell support. Notification
content renders via view()/text_items(); the auto-dismiss uses the engine tick
timer (a new notification resets it); the org.freedesktop.Notifications D-Bus
server runs from register_sources and drives update() over the channel.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
src/main.rs | 1194 ++++++++++++-----------------------------------------------
1 file changed, 234 insertions(+), 960 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 3e893aa..798604f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,829 +1,293 @@
use std::collections::HashMap;
-use zbus::{interface, connection};
-use zbus::zvariant::Value;
-use glyphon::{
- Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
- TextBounds, TextRenderer, Viewport,
-};
-use smithay_client_toolkit::{
- compositor::{CompositorHandler, CompositorState},
- delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
- delegate_seat, delegate_shm, delegate_layer, delegate_output,
- registry::{ProvidesRegistryState, RegistryState},
- output::{OutputHandler, OutputState},
- seat::{
- keyboard::KeyboardHandler,
- pointer::PointerHandler,
- Capability, SeatHandler, SeatState,
- },
- shell::{
- wlr_layer::{
- Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler,
- LayerSurface, LayerSurfaceConfigure,
- },
- },
- shm::{Shm, ShmHandler},
-};
-use wayland_client::{
- globals::registry_queue_init,
- protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
- Connection, QueueHandle, Proxy,
-};
-use calloop::EventLoop;
-use calloop_wayland_source::WaylandSource;
-
-// ── Vertices and rendering structures ──
-
-// Vertex and quad_vertices are shared from the cce-ui engine.
-use cce_ui::engine::{quad_vertices, Vertex};
-fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
- let metrics = Metrics::new(size, size * 1.4);
- let mut buf = Buffer::new(fs, metrics);
- buf.set_text(fs, text, Attrs::new(), glyphon::Shaping::Advanced);
- buf.shape_until_scroll(fs, true);
- buf
-}
+use zbus::zvariant::Value;
+use zbus::{connection, interface};
-struct RectWidget {
- x: f32,
- y: f32,
- w: f32,
- h: f32,
- color: [f32; 4],
-}
+use glyphon::FontSystem;
+use wayland_client::QueueHandle;
-struct TextItem {
- buffer: Buffer,
- x: f32,
- y: f32,
- color: glyphon::Color,
-}
+use cce_ui::engine::{
+ Application, EngineState, LayerAnchor, LayerKeyboardInteractivity, LayerKind, LayerSettings,
+ LogicalPosition, LogicalSize, WindowSettings,
+};
+use cce_ui::widget::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, TextItem};
-// ── NotificationApp ──
+const NOTIF_WIDTH: u32 = 360;
+const NOTIF_HEIGHT: u32 = 100;
-struct RendererResources {
- render_pipeline: wgpu::RenderPipeline,
- font_system: FontSystem,
- swash_cache: SwashCache,
- text_atlas: TextAtlas,
- text_renderer: TextRenderer,
- cache: Cache,
+#[derive(Debug, Clone)]
+enum UserEvent {
+ NewNotification {
+ app_name: String,
+ summary: String,
+ body: String,
+ },
+ CloseNotification,
}
-#[allow(dead_code)]
-struct NotificationApp {
- window: LayerSurface,
- surface: wl_surface::WlSurface,
- wgpu_surface: wgpu::Surface<'static>,
- device: wgpu::Device,
- queue: wgpu::Queue,
- config: wgpu::SurfaceConfiguration,
- vertex_buffer: wgpu::Buffer,
- vertex_count: u32,
-
- text_viewport: Viewport,
-
- rects: Vec<RectWidget>,
- text_items: Vec<TextItem>,
-
- scale_factor: f64,
- width: u32,
- height: u32,
- needs_rebuild: bool,
- configured: bool,
- opacity: f32,
- bg_color: [f32; 4],
+// ── Config accessors ──────────────────────────────────────────────────────
- app_name: String,
- summary: String,
- body: String,
+fn read_duration() -> f32 {
+ cce_ui::config::get_i64("/notifications/duration", 5) as f32
}
-impl NotificationApp {
- fn new(
- conn: &Connection,
- qh: &QueueHandle<AppState>,
- compositor_state: &CompositorState,
- layer_shell_state: &LayerShell,
- width: u32,
- height: u32,
- scale: f64,
- instance: &wgpu::Instance,
- adapter: &wgpu::Adapter,
- device: wgpu::Device,
- queue: wgpu::Queue,
- cache: &Cache,
- opacity: f32,
- bg_color: [f32; 4],
- ) -> Self {
- let surface = compositor_state.create_surface(qh);
- surface.set_buffer_scale(scale as i32);
-
- let logical_w = (width as f64 / scale) as u32;
- let logical_h = (height as f64 / scale) as u32;
-
- let window = layer_shell_state.create_layer_surface(
- qh,
- surface.clone(),
- Layer::Overlay,
- Some("cce-notifier".to_string()),
- None,
- );
- window.set_size(logical_w, logical_h);
- window.set_keyboard_interactivity(KeyboardInteractivity::None);
- window.set_anchor(Anchor::TOP | Anchor::RIGHT);
- window.set_margin(20, 20, 0, 0); // 20px margin from top and right
- surface.commit();
-
- let wayland_handle = Box::leak(Box::new(cce_ui::wayland::WaylandSurfaceHandle {
- display_ptr: conn.backend().display_id().as_ptr() as *mut std::ffi::c_void,
- surface_ptr: surface.id().as_ptr() as *mut std::ffi::c_void,
- }));
-
- let wgpu_surface = instance.create_surface(wayland_handle).expect("surface");
- let mut config = wgpu_surface.get_default_config(adapter, width.max(1), height.max(1)).expect("config");
- config.format = wgpu::TextureFormat::Bgra8Unorm;
-
- let capabilities = wgpu_surface.get_capabilities(adapter);
- let alpha_mode = if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
- wgpu::CompositeAlphaMode::PreMultiplied
- } else if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
- wgpu::CompositeAlphaMode::PostMultiplied
- } else {
- capabilities.alpha_modes[0]
- };
- config.alpha_mode = alpha_mode;
- wgpu_surface.configure(&device, &config);
-
- let mut text_viewport = Viewport::new(&device, cache);
- text_viewport.update(&queue, Resolution { width, height });
-
- let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Vertex Buffer"),
- size: 1,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let scale_factor = scale;
-
- Self {
- window,
- surface,
- wgpu_surface,
- device,
- queue,
- config,
- vertex_buffer,
- vertex_count: 0,
- text_viewport,
- rects: Vec::new(),
- text_items: Vec::new(),
- scale_factor,
- width,
- height,
- needs_rebuild: true,
- configured: false,
- opacity,
- bg_color,
- app_name: String::new(),
- summary: String::new(),
- body: String::new(),
- }
- }
-
- fn rebuild_layout(&mut self, font_system: &mut FontSystem) {
- let sh = self.height as f32;
- let s = self.scale_factor as f32;
-
- self.rects.clear();
- self.text_items.clear();
- // 2. Bright Green Left accent border
- self.rects.push(RectWidget {
- x: 0.0,
- y: 0.0,
- w: 6.0 * s,
- h: sh,
- color: cce_ui::colors::TOGGLE_ON,
- });
-
- // 3. Text content
- let app_name_buf = make_text_buffer(font_system, &self.app_name, 10.0 * s);
- self.text_items.push(TextItem {
- buffer: app_name_buf,
- x: 18.0 * s,
- y: 12.0 * s,
- color: {
- let srgb = cce_ui::colors::to_srgb(cce_ui::colors::TEXT_DIM);
- glyphon::Color::rgb(
- (srgb[0] * 255.0) as u8,
- (srgb[1] * 255.0) as u8,
- (srgb[2] * 255.0) as u8,
- )
- },
- });
-
- let summary_buf = make_text_buffer(font_system, &self.summary, 13.0 * s);
- self.text_items.push(TextItem {
- buffer: summary_buf,
- x: 18.0 * s,
- y: 28.0 * s,
- color: {
- let srgb = cce_ui::colors::to_srgb(cce_ui::colors::TEXT_HEADER);
- glyphon::Color::rgb(
- (srgb[0] * 255.0) as u8,
- (srgb[1] * 255.0) as u8,
- (srgb[2] * 255.0) as u8,
- )
- },
- });
-
- let body_buf = make_text_buffer(font_system, &self.body, 11.0 * s);
- self.text_items.push(TextItem {
- buffer: body_buf,
- x: 18.0 * s,
- y: 48.0 * s,
- color: {
- let srgb = cce_ui::colors::to_srgb(cce_ui::colors::TEXT_FG);
- glyphon::Color::rgb(
- (srgb[0] * 255.0) as u8,
- (srgb[1] * 255.0) as u8,
- (srgb[2] * 255.0) as u8,
- )
- },
- });
-
- self.needs_rebuild = false;
- }
-
- fn collect_vertices(&self) -> Vec<Vertex> {
- let sw = self.width as f32;
- let sh = self.height as f32;
- let s = self.scale_factor as f32;
- let mut verts = Vec::new();
-
- // 1. Render Backplate (Background)
- let backplate = cce_ui::widget::container::Backplate::new(0.0, 0.0, sw, sh)
- .with_background([
- self.bg_color[0],
- self.bg_color[1],
- self.bg_color[2],
- self.opacity,
- ]);
-
- let bp_verts = cce_ui::engine::widget_vertices(&backplate, sw, sh, [0.0; 3]);
- verts.extend(bp_verts.into_iter().map(|v| Vertex {
- position: v.position,
- color: v.color,
- clip_circle: v.clip_circle,
- }));
-
- // 2. Render other rects (with rounded left accent border if applicable)
- for r in &self.rects {
- if r.x == 0.0 && r.y == 0.0 && r.w == 6.0 * s {
- let radius = cce_ui::colors::backplate_corner_radius();
- let mut border_verts = Vec::new();
- cce_ui::engine::push_rounded_rect_vertices_corners(
- r.x, r.y, r.w, r.h,
- cce_ui::widget::CornerRadii::new(radius, 0.0, 0.0, radius),
- sw, sh,
- r.color,
- [0.0; 3],
- None,
- &mut border_verts,
- );
- verts.extend(border_verts.into_iter().map(|v| Vertex {
- position: v.position,
- color: v.color,
- clip_circle: v.clip_circle,
- }));
- } else {
- verts.extend(quad_vertices(r.x, r.y, r.w, r.h, sw, sh, r.color));
- }
- }
- verts
- }
-
- fn upload_vertices(&mut self) {
- let verts = self.collect_vertices();
- self.vertex_count = verts.len() as u32;
- let data = bytemuck::cast_slice(&verts);
- let needed = data.len() as wgpu::BufferAddress;
- if needed > self.vertex_buffer.size() {
- self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Vertex Buffer"),
- size: needed,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
- }
- self.queue.write_buffer(&self.vertex_buffer, 0, data);
- }
-
- fn prepare_text(
- &mut self,
- font_system: &mut FontSystem,
- swash_cache: &mut SwashCache,
- text_atlas: &mut TextAtlas,
- text_renderer: &mut TextRenderer,
- ) {
- let w = self.width as f32;
- let h = self.height as f32;
- let viewport = Resolution { width: w as u32, height: h as u32 };
- self.text_viewport.update(&self.queue, viewport);
- let bounds = TextBounds { left: 0, top: 0, right: w as i32, bottom: h as i32 };
- let areas: Vec<TextArea> = self.text_items.iter().map(|ti| TextArea {
- buffer: &ti.buffer,
- left: ti.x.round(), top: ti.y.round(), scale: 1.0, bounds,
- default_color: ti.color,
- custom_glyphs: &[],
- }).collect();
- text_renderer.prepare(
- &self.device, &self.queue, font_system,
- text_atlas, &self.text_viewport, areas, swash_cache
- ).unwrap();
- }
-
- fn resize(&mut self, width: u32, height: u32) {
- if width > 0 && height > 0 {
- self.width = width;
- self.height = height;
- self.config.width = width;
- self.config.height = height;
- self.wgpu_surface.configure(&self.device, &self.config);
- self.needs_rebuild = true;
- }
- }
-
- fn render(
- &mut self,
- render_pipeline: &wgpu::RenderPipeline,
- font_system: &mut FontSystem,
- swash_cache: &mut SwashCache,
- text_atlas: &mut TextAtlas,
- text_renderer: &mut TextRenderer,
- ) {
- if self.needs_rebuild {
- self.rebuild_layout(font_system);
- self.upload_vertices();
- }
- self.prepare_text(font_system, swash_cache, text_atlas, text_renderer);
-
- let output = match self.wgpu_surface.get_current_texture() {
- Ok(t) => t,
- Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
- self.wgpu_surface.configure(&self.device, &self.config);
- return;
- }
- Err(wgpu::SurfaceError::Timeout) => return,
- Err(e) => { log::error!("Surface error: {:?}", e); return; }
- };
-
- let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
- let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
- label: Some("Encoder"),
- });
-
- {
- let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("Render Pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: &view,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: None,
- timestamp_writes: None,
- occlusion_query_set: None,
- });
-
- pass.set_pipeline(render_pipeline);
- pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
- pass.draw(0..self.vertex_count, 0..1);
-
- text_renderer.render(text_atlas, &self.text_viewport, &mut pass).unwrap();
- }
-
- self.queue.submit(std::iter::once(encoder.finish()));
- output.present();
- }
+fn read_opacity() -> f32 {
+ cce_ui::config::get_f32("/notifications/opacity", 0.9)
}
-fn load_config() -> serde_json::Value {
- cce_ui::config::cached_config()
+fn read_bg_color() -> [f32; 4] {
+ cce_ui::config::get_string("/notifications/bg_color")
+ .as_deref()
+ .and_then(cce_ui::color::parse_hex_rgba_linear)
+ .map(|[r, g, b, _]| [r, g, b, 1.0])
+ .unwrap_or([
+ cce_ui::colors::srgb_to_linear(0.08),
+ cce_ui::colors::srgb_to_linear(0.08),
+ cce_ui::colors::srgb_to_linear(0.12),
+ 1.0,
+ ])
}
fn play_bell_if_configured() {
- let val = load_config();
- let sound_type = val.pointer("/notifications/bell").and_then(|v| v.as_str()).unwrap_or("none");
-
- let sound_event = match sound_type {
+ let sound_type = cce_ui::config::get_string("/notifications/bell").unwrap_or_default();
+ let sound_event = match sound_type.as_str() {
"bell" => Some("bell"),
"dialog" => Some("dialog-information"),
"message" => Some("message"),
_ => None,
};
-
if let Some(event) = sound_event {
log::info!("Playing notification sound ({})...", sound_type);
- if std::process::Command::new("canberra-gtk-play")
+ let _ = std::process::Command::new("canberra-gtk-play")
.arg("-i")
.arg(event)
- .spawn()
- .is_err()
- {
- let sound_file = match sound_type {
- "bell" => "/usr/share/sounds/freedesktop/stereo/bell.oga",
- "dialog" => "/usr/share/sounds/freedesktop/stereo/dialog-information.oga",
- "message" => "/usr/share/sounds/freedesktop/stereo/message.oga",
- _ => "",
- };
- if !sound_file.is_empty() {
- if let Err(e) = std::process::Command::new("pw-play")
- .arg(sound_file)
- .spawn()
- {
- log::error!("Failed to spawn pw-play: {}", e);
- }
- }
- }
+ .spawn();
}
}
-fn read_duration_if_configured() -> u64 {
- cce_ui::config::get_i64("/notifications/duration", 5) as u64
+fn glyphon_color(linear: [f32; 4]) -> glyphon::Color {
+ let srgb = cce_ui::colors::to_srgb(linear);
+ glyphon::Color::rgb(
+ (srgb[0] * 255.0) as u8,
+ (srgb[1] * 255.0) as u8,
+ (srgb[2] * 255.0) as u8,
+ )
}
-fn read_opacity_if_configured() -> f32 {
- cce_ui::config::get_f32("/notifications/opacity", 0.9)
+// ── Application ───────────────────────────────────────────────────────────
+
+struct NotifierApp {
+ font_system: FontSystem,
+ app_name: String,
+ summary: String,
+ body: String,
+ visible: bool,
+ dismiss_timer: f32,
+ opacity: f32,
+ bg_color: [f32; 4],
+ scale_factor: f64,
+ needs_rebuild: bool,
+ text_items: Vec<TextItem>,
+ sender: calloop::channel::Sender<UserEvent>,
}
-fn read_bg_color_if_configured() -> [f32; 4] {
- let val = load_config();
-
- if let Some(hex_str) = val.pointer("/notifications/bg_color").and_then(|v| v.as_str()) {
- if let Some([r, g, b, _]) = cce_ui::color::parse_hex_rgba_linear(hex_str) {
- return [r, g, b, 1.0];
+impl NotifierApp {
+ fn rebuild_layout(&mut self) {
+ self.text_items.clear();
+ if self.visible {
+ // app name, summary, body (logical px; the engine applies HiDPI scale)
+ self.text_items.push(TextItem::new(
+ &mut self.font_system,
+ &self.app_name,
+ 10.0,
+ 18.0,
+ 12.0,
+ glyphon_color(cce_ui::colors::TEXT_DIM),
+ None,
+ None,
+ ));
+ self.text_items.push(TextItem::new(
+ &mut self.font_system,
+ &self.summary,
+ 13.0,
+ 18.0,
+ 28.0,
+ glyphon_color(cce_ui::colors::TEXT_HEADER),
+ None,
+ None,
+ ));
+ self.text_items.push(TextItem::new(
+ &mut self.font_system,
+ &self.body,
+ 11.0,
+ 18.0,
+ 48.0,
+ glyphon_color(cce_ui::colors::TEXT_FG),
+ None,
+ None,
+ ));
}
+ self.needs_rebuild = false;
}
- // Default notification background color: srgb [0.08, 0.08, 0.12]
- [
- cce_ui::colors::srgb_to_linear(0.08),
- cce_ui::colors::srgb_to_linear(0.08),
- cce_ui::colors::srgb_to_linear(0.12),
- 1.0,
- ]
-}
-
-// ── D-Bus Events & AppState ──
-
-#[derive(Debug, Clone)]
-enum UserEvent {
- NewNotification {
- app_name: String,
- summary: String,
- body: String,
- },
- CloseNotification {
- notification_id: u32,
- },
}
-struct AppState {
- registry_state: RegistryState,
- compositor_state: CompositorState,
- layer_shell_state: LayerShell,
- shm_state: Shm,
- seat_state: SeatState,
- output_state: OutputState,
-
- seats: Vec<wl_seat::WlSeat>,
- pointer: Option<wl_pointer::WlPointer>,
- keyboard: Option<wl_keyboard::WlKeyboard>,
+impl Application for NotifierApp {
+ type Message = UserEvent;
- state: Option<NotificationApp>,
- current_id: u32,
- exit: bool,
- redraw: bool,
-
- conn: Connection,
- qh: QueueHandle<AppState>,
- rt_handle: tokio::runtime::Handle,
- sender: calloop::channel::Sender<UserEvent>,
-
- wgpu_instance: wgpu::Instance,
- wgpu_adapter: wgpu::Adapter,
- wgpu_device: wgpu::Device,
- wgpu_queue: wgpu::Queue,
- renderer_resources: RendererResources,
-}
-
-impl CompositorHandler for AppState {
- fn scale_factor_changed(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- scale_factor: i32,
- ) {
- _surface.set_buffer_scale(scale_factor);
- if let Some(state) = &mut self.state {
- let old_scale = state.scale_factor;
- state.scale_factor = scale_factor as f64;
- let logical_w = state.width as f64 / old_scale;
- let logical_h = state.height as f64 / old_scale;
- let pw = (logical_w * state.scale_factor) as u32;
- let ph = (logical_h * state.scale_factor) as u32;
- state.resize(pw, ph);
+ fn new(_qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self {
+ Self {
+ font_system: cce_ui::create_font_system_with_system_fonts(),
+ app_name: String::new(),
+ summary: String::new(),
+ body: String::new(),
+ visible: false,
+ dismiss_timer: 0.0,
+ opacity: read_opacity(),
+ bg_color: read_bg_color(),
+ scale_factor: 1.0,
+ needs_rebuild: false,
+ text_items: Vec::new(),
+ sender,
}
- self.redraw = true;
}
- fn transform_changed(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _new_transform: wl_output::Transform,
- ) {}
-
- fn frame(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _time: u32,
- ) {}
-
- fn surface_enter(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _output: &wl_output::WlOutput,
- ) {}
-
- fn surface_leave(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _output: &wl_output::WlOutput,
- ) {}
-}
-
-impl OutputHandler for AppState {
- fn output_state(&mut self) -> &mut OutputState {
- &mut self.output_state
+ fn settings(&self) -> WindowSettings {
+ WindowSettings {
+ title: "cce-notifier".to_string(),
+ app_id: "cce-notifier".to_string(),
+ width: NOTIF_WIDTH,
+ height: NOTIF_HEIGHT,
+ fullscreen: false,
+ min_size: None,
+ }
}
- fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
- 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 SeatHandler for AppState {
- fn seat_state(&mut self) -> &mut SeatState {
- &mut self.seat_state
+ fn layer(&self) -> Option<LayerSettings> {
+ Some(LayerSettings {
+ layer: LayerKind::Overlay,
+ anchor: LayerAnchor::TOP | LayerAnchor::RIGHT,
+ exclusive_zone: 0,
+ keyboard_interactivity: LayerKeyboardInteractivity::None,
+ margin: (20, 20, 0, 0),
+ namespace: "cce-notifier".to_string(),
+ })
}
- fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
- self.seats.push(seat);
+ fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, _exit: &mut bool) {
+ match msg {
+ UserEvent::NewNotification { app_name, summary, body } => {
+ play_bell_if_configured();
+ self.app_name = app_name;
+ self.summary = summary;
+ self.body = body;
+ self.opacity = read_opacity();
+ self.bg_color = read_bg_color();
+ self.visible = true;
+ self.dismiss_timer = read_duration();
+ self.needs_rebuild = true;
+ *needs_rebuild = true;
+ }
+ UserEvent::CloseNotification => {
+ self.visible = false;
+ self.needs_rebuild = true;
+ *needs_rebuild = true;
+ }
+ }
}
- fn new_capability(
- &mut self,
- _conn: &Connection,
- qh: &QueueHandle<Self>,
- seat: wl_seat::WlSeat,
- capability: Capability,
- ) {
- if capability == Capability::Pointer && self.pointer.is_none() {
- let pointer = self.seat_state.get_pointer(qh, &seat).unwrap();
- self.pointer = Some(pointer);
- }
- if capability == Capability::Keyboard && self.keyboard.is_none() {
- let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
- self.keyboard = Some(keyboard);
+ fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
+ if self.visible {
+ self.dismiss_timer -= dt;
+ if self.dismiss_timer <= 0.0 {
+ self.visible = false;
+ self.needs_rebuild = true;
+ *needs_rebuild = true;
+ }
}
}
- fn remove_capability(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _seat: wl_seat::WlSeat,
- capability: Capability,
- ) {
- if capability == Capability::Pointer {
- self.pointer = None;
+ fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64) {
+ if self.needs_rebuild || (self.scale_factor - scale).abs() > f64::EPSILON {
+ self.scale_factor = scale;
+ self.rebuild_layout();
}
- if capability == Capability::Keyboard {
- self.keyboard = None;
+ if self.visible {
+ // Bright green left accent border.
+ quads.push((0.0, 0.0, 6.0, size.height, cce_ui::colors::TOGGLE_ON));
}
}
- fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
- self.seats.retain(|s| s != &seat);
- }
-}
-
-impl ShmHandler for AppState {
- fn shm_state(&mut self) -> &mut Shm {
- &mut self.shm_state
- }
-}
-
-impl PointerHandler for AppState {
- fn pointer_frame(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _pointer: &wl_pointer::WlPointer,
- _events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
- ) {}
-}
-
-impl KeyboardHandler for AppState {
- fn enter(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _surface: &wl_surface::WlSurface,
- _serial: u32,
- _raw_modifiers: &[u32],
- _keysyms: &[xkeysym::Keysym],
- ) {}
-
- fn leave(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _surface: &wl_surface::WlSurface,
- _serial: u32,
- ) {}
-
- fn press_key(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _serial: u32,
- _event: smithay_client_toolkit::seat::keyboard::KeyEvent,
- ) {}
-
- fn release_key(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _serial: u32,
- _event: smithay_client_toolkit::seat::keyboard::KeyEvent,
- ) {}
-
- fn update_modifiers(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _serial: u32,
- _modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
- _layout: u32,
- ) {}
-}
-
-impl LayerShellHandler for AppState {
- fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {
- self.state = None;
- self.redraw = true;
+ fn text_items(&self) -> &[TextItem] {
+ &self.text_items
}
- fn configure(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _layer: &LayerSurface,
- configure: LayerSurfaceConfigure,
- _serial: u32,
- ) {
- let (w, h) = configure.new_size;
- if let Some(state) = &mut self.state {
- state.configured = true;
- if w > 0 && h > 0 {
- let pw = (w as f64 * state.scale_factor) as u32;
- let ph = (h as f64 * state.scale_factor) as u32;
- state.resize(pw, ph);
- }
+ fn clear_color(&self) -> [f32; 4] {
+ if self.visible {
+ [self.bg_color[0], self.bg_color[1], self.bg_color[2], self.opacity]
+ } else {
+ [0.0, 0.0, 0.0, 0.0]
}
- self.redraw = true;
}
-}
-impl ProvidesRegistryState for AppState {
- fn registry(&mut self) -> &mut RegistryState {
- &mut self.registry_state
+ /// When hidden, drop the input region so the transparent overlay is
+ /// click-through; when visible, take input over the notification area.
+ fn input_regions(&self) -> Option<Vec<(i32, i32, i32, i32)>> {
+ if self.visible {
+ None
+ } else {
+ Some(Vec::new())
+ }
}
-
- fn runtime_add_global(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _name: u32,
- _interface: &str,
- _version: u32,
- ) {}
-
- fn runtime_remove_global(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _name: u32,
- _interface: &str,
- ) {}
-}
-
-delegate_compositor!(AppState);
-delegate_layer!(AppState);
-delegate_shm!(AppState);
-delegate_seat!(AppState);
-delegate_pointer!(AppState);
-delegate_keyboard!(AppState);
-delegate_registry!(AppState);
-delegate_output!(AppState);
-
-impl AppState {
- fn handle_user_event(&mut self, event: UserEvent) {
- match event {
- UserEvent::NewNotification { app_name, summary, body } => {
- play_bell_if_configured();
- self.current_id += 1;
- let active_id = self.current_id;
- let opacity = read_opacity_if_configured();
- let bg_color = read_bg_color_if_configured();
- if self.state.is_none() {
- log::info!("Opening notification window: {} - {}", summary, body);
- let scale = cce_ui::wayland::detect_scale_factor(&self.output_state);
- let pw = (360.0 * scale) as u32;
- let ph = (100.0 * scale) as u32;
- let mut state = NotificationApp::new(
- &self.conn,
- &self.qh,
- &self.compositor_state,
- &self.layer_shell_state,
- pw,
- ph,
- scale,
- &self.wgpu_instance,
- &self.wgpu_adapter,
- self.wgpu_device.clone(),
- self.wgpu_queue.clone(),
- &self.renderer_resources.cache,
- opacity,
- bg_color,
- );
- state.app_name = app_name;
- state.summary = summary;
- state.body = body;
- state.needs_rebuild = true;
- self.state = Some(state);
- } else if let Some(ref mut state) = self.state {
- log::info!("Updating active notification window: {} - {}", summary, body);
- state.app_name = app_name;
- state.summary = summary;
- state.body = body;
- state.opacity = opacity;
- state.bg_color = bg_color;
- state.needs_rebuild = true;
+ fn register_sources(&mut self, _handle: &calloop::LoopHandle<'_, EngineState<Self>>) {
+ // Run the org.freedesktop.Notifications D-Bus server on a background
+ // thread; incoming Notify calls are forwarded to update() via the channel.
+ let sender = self.sender.clone();
+ std::thread::spawn(move || {
+ let rt = match tokio::runtime::Runtime::new() {
+ Ok(rt) => rt,
+ Err(e) => {
+ log::error!("cce-notifier: failed to start tokio runtime: {e}");
+ return;
}
- self.redraw = true;
-
- // Schedule closing the window using the configured duration
- let duration_secs = read_duration_if_configured();
- let sender_clone = self.sender.clone();
- self.rt_handle.spawn(async move {
- tokio::time::sleep(tokio::time::Duration::from_secs(duration_secs)).await;
- let _ = sender_clone.send(UserEvent::CloseNotification { notification_id: active_id });
- });
- }
- UserEvent::CloseNotification { notification_id } => {
- if notification_id == self.current_id {
- log::info!("Closing notification window (ID: {})...", notification_id);
- self.state = None; // Dropping the window and resources
- self.redraw = true;
+ };
+ rt.block_on(async move {
+ let dbus_impl = DbusInterface { sender };
+ match connection::Builder::session()
+ .and_then(|b| b.name("org.freedesktop.Notifications"))
+ .and_then(|b| b.serve_at("/org/freedesktop/Notifications", dbus_impl))
+ {
+ Ok(builder) => match builder.build().await {
+ Ok(_conn) => {
+ log::info!("cce-notifier: D-Bus listener registered.");
+ std::future::pending::<()>().await;
+ }
+ Err(e) => log::error!("cce-notifier: failed to build D-Bus connection: {e}"),
+ },
+ Err(e) => log::error!("cce-notifier: failed to register D-Bus name/path: {e}"),
}
- }
- }
+ });
+ });
+ }
+
+ // Notifications are non-interactive.
+ fn handle_pointer_move(&mut self, _pos: LogicalPosition, _needs_rebuild: &mut bool) {}
+ fn handle_mouse_input(
+ &mut self,
+ _button: MouseButton,
+ _state: ElementState,
+ _pos: LogicalPosition,
+ _needs_rebuild: &mut bool,
+ ) -> Option<Self::Message> {
+ None
+ }
+ fn handle_mouse_wheel(&mut self, _delta: &MouseScrollDelta, _pos: LogicalPosition, _needs_rebuild: &mut bool) {}
+ fn handle_key_input(&mut self, _event: &KeyEvent, _needs_rebuild: &mut bool) -> Option<Self::Message> {
+ None
}
}
-// ── D-Bus zbus implementation ──
+// ── D-Bus (org.freedesktop.Notifications) ─────────────────────────────────
struct DbusInterface {
sender: calloop::channel::Sender<UserEvent>,
@@ -832,13 +296,10 @@ struct DbusInterface {
#[interface(name = "org.freedesktop.Notifications")]
impl DbusInterface {
async fn get_capabilities(&self) -> Vec<String> {
- vec![
- "body".to_string(),
- "actions".to_string(),
- "icon-static".to_string(),
- ]
+ vec!["body".to_string(), "actions".to_string(), "icon-static".to_string()]
}
+ #[allow(clippy::too_many_arguments)]
async fn notify(
&self,
app_name: String,
@@ -850,16 +311,13 @@ impl DbusInterface {
_hints: HashMap<String, Value<'_>>,
_expire_timeout: i32,
) -> u32 {
- let _ = self.sender.send(UserEvent::NewNotification {
- app_name,
- summary,
- body,
- });
-
- 1 // zbus notification ID
+ let _ = self.sender.send(UserEvent::NewNotification { app_name, summary, body });
+ 1
}
- async fn close_notification(&self, _id: u32) {}
+ async fn close_notification(&self, _id: u32) {
+ let _ = self.sender.send(UserEvent::CloseNotification);
+ }
async fn get_server_information(&self) -> (String, String, String, String) {
(
@@ -871,191 +329,7 @@ impl DbusInterface {
}
}
-// ── main ──
-
-fn main() -> Result<(), Box<dyn std::error::Error>> {
+fn main() {
env_logger::init();
- let conn = Connection::connect_to_env().unwrap();
- let (globals, event_queue) = registry_queue_init(&conn).unwrap();
- let qh = event_queue.handle();
-
- let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
- let layer_shell_state = LayerShell::bind(&globals, &qh).unwrap();
- let shm_state = Shm::bind(&globals, &qh).unwrap();
- let seat_state = SeatState::new(&globals, &qh);
- let output_state = OutputState::new(&globals, &qh);
-
- // Initialize wgpu graphics context once on startup
- let wgpu_instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
- backends: wgpu::Backends::VULKAN,
- ..Default::default()
- });
- let wgpu_adapter = pollster::block_on(wgpu_instance.request_adapter(&wgpu::RequestAdapterOptions {
- power_preference: wgpu::PowerPreference::LowPower,
- compatible_surface: None,
- force_fallback_adapter: false,
- })).expect("Failed to find wgpu adapter");
- let (wgpu_device, wgpu_queue) = pollster::block_on(wgpu_adapter.request_device(&wgpu::DeviceDescriptor {
- label: Some("GPU Device"),
- required_features: wgpu::Features::empty(),
- required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(wgpu_adapter.limits()),
- memory_hints: wgpu::MemoryHints::MemoryUsage,
- }, None)).expect("Failed to request wgpu device");
-
- // Initialize renderer resources once
- let font_system = cce_ui::create_font_system_with_system_fonts();
- let swash_cache = SwashCache::new();
- let cache = Cache::new(&wgpu_device);
- let mut text_atlas = TextAtlas::new(&wgpu_device, &wgpu_queue, &cache, wgpu::TextureFormat::Bgra8Unorm);
- let text_renderer = TextRenderer::new(&mut text_atlas, &wgpu_device, wgpu::MultisampleState::default(), None);
-
- let shader_code = cce_ui::SHADER;
- let shader = wgpu_device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("Shader"),
- source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader_code)),
- });
- let pipeline_layout = wgpu_device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("Pipeline Layout"),
- bind_group_layouts: &[],
- push_constant_ranges: &[],
- });
- let render_pipeline = wgpu_device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some("Render Pipeline"),
- layout: Some(&pipeline_layout),
- vertex: wgpu::VertexState {
- module: &shader,
- entry_point: Some("vs_main"),
- buffers: &[Vertex::desc()],
- compilation_options: Default::default(),
- },
- fragment: Some(wgpu::FragmentState {
- module: &shader,
- entry_point: Some("fs_main"),
- targets: &[Some(wgpu::ColorTargetState {
- format: wgpu::TextureFormat::Bgra8Unorm,
- blend: Some(wgpu::BlendState::ALPHA_BLENDING),
- write_mask: wgpu::ColorWrites::ALL,
- })],
- compilation_options: Default::default(),
- }),
- primitive: wgpu::PrimitiveState {
- topology: wgpu::PrimitiveTopology::TriangleList,
- front_face: wgpu::FrontFace::Ccw,
- cull_mode: None,
- polygon_mode: wgpu::PolygonMode::Fill,
- unclipped_depth: false,
- conservative: false,
- strip_index_format: None,
- },
- depth_stencil: None,
- multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
- multiview: None,
- cache: None,
- });
-
- let renderer_resources = RendererResources {
- render_pipeline,
- font_system,
- swash_cache,
- text_atlas,
- text_renderer,
- cache,
- };
-
- // Create the tokio runtime
- let rt = tokio::runtime::Builder::new_multi_thread()
- .enable_all()
- .build()
- .unwrap();
- let handle = rt.handle().clone();
-
- let mut event_loop = EventLoop::try_new().unwrap();
- let loop_handle = event_loop.handle();
-
- let (sender, channel) = calloop::channel::channel::<UserEvent>();
-
- // Start D-Bus listener inside a background tokio thread pool
- let sender_clone = sender.clone();
- std::thread::spawn(move || {
- rt.block_on(async {
- let dbus_impl = DbusInterface { sender: sender_clone };
- let _connection = connection::Builder::session()
- .expect("Failed to connect to session bus")
- .name("org.freedesktop.Notifications")
- .expect("Failed to claim name org.freedesktop.Notifications")
- .serve_at("/org/freedesktop/Notifications", dbus_impl)
- .expect("Failed to register D-Bus path")
- .build()
- .await
- .expect("Failed to build D-Bus connection");
-
- log::info!("D-Bus listener registered. Running...");
-
- // Keep background runtime alive
- loop {
- tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
- }
- });
- });
-
- let mut app = AppState {
- registry_state: RegistryState::new(&globals),
- compositor_state,
- layer_shell_state,
- shm_state,
- seat_state,
- output_state,
- seats: Vec::new(),
- pointer: None,
- keyboard: None,
- state: None,
- current_id: 0,
- exit: false,
- redraw: true,
- conn: conn.clone(),
- qh,
- rt_handle: handle,
- sender,
- wgpu_instance,
- wgpu_adapter,
- wgpu_device: wgpu_device.clone(),
- wgpu_queue: wgpu_queue.clone(),
- renderer_resources,
- };
-
- WaylandSource::new(conn, event_queue).insert(loop_handle.clone()).unwrap();
-
- loop_handle.insert_source(channel, |event, _metadata, app_state: &mut AppState| {
- if let calloop::channel::Event::Msg(msg) = event {
- app_state.handle_user_event(msg);
- }
- }).unwrap();
-
- log::info!("Wayland event loop starting...");
- loop {
- if let Err(err) = event_loop.dispatch(std::time::Duration::from_millis(16), &mut app) {
- log::error!("Event loop error (exiting): {:?}", err);
- break;
- }
- if app.exit {
- break;
- }
- if app.redraw {
- app.redraw = false;
- if let Some(state) = &mut app.state {
- if state.configured {
- let rr = &mut app.renderer_resources;
- state.render(
- &rr.render_pipeline,
- &mut rr.font_system,
- &mut rr.swash_cache,
- &mut rr.text_atlas,
- &mut rr.text_renderer,
- );
- }
- }
- }
- }
-
- Ok(())
+ cce_ui::engine::run::<NotifierApp>();
}