git.lucas.co / cce-status-interface
status bar
git clone https://git.lucas.co/cce-status-interface.git

commit2ac764fd161fb52a3e243847a7f11916555e9154
parent754818d738
authorLucas Galante <[email protected]>
date2026-06-01 11:49
Refactor status-interface to use clear-ui's strongly-typed LogicalPosition and LogicalSize

 src/main.rs | 1212 +++++++++++++++++------------------------------------------
 1 file changed, 341 insertions(+), 871 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 4427fc4..8a554b5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,39 +1,13 @@
 use std::collections::HashMap;
 use std::sync::Arc;
 use glyphon::{
-    Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea,
-    TextAtlas, TextBounds, TextRenderer, Viewport,
+    Attrs, Buffer, FontSystem, Metrics,
 };
 use clear_ui::color;
-use clear_ui::widget::{StyledLabel as Label, TextItem, Separator, Widget};
-
-use smithay_client_toolkit::{
-    compositor::{CompositorHandler, CompositorState},
-    delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
-    delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
-    registry::{ProvidesRegistryState, RegistryState},
-    output::{OutputHandler, OutputState},
-    seat::{
-        keyboard::KeyboardHandler,
-        pointer::PointerHandler,
-        Capability, SeatHandler, SeatState,
-    },
-    shell::{
-        xdg::{
-            window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
-            XdgShell,
-        },
-        WaylandSurface,
-    },
-    shm::{Shm, ShmHandler},
+use clear_ui::widget::{
+    StyledLabel as Label, TextItem, Separator, Widget,
+    MouseButton, ElementState, MouseScrollDelta, KeyEvent,
 };
-use wayland_client::{
-    globals::registry_queue_init,
-    protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface},
-    Connection, QueueHandle, Proxy,
-};
-use calloop::EventLoop;
-use calloop_wayland_source::WaylandSource;
 
 #[derive(Debug, Clone)]
 struct TrayPixmap {
@@ -71,6 +45,14 @@ struct TagBounds {
     h: f32,
 }
 
+#[derive(Debug, Clone)]
+struct LayoutBounds {
+    x: f32,
+    y: f32,
+    w: f32,
+    h: f32,
+}
+
 #[derive(Debug, Clone)]
 struct SystemStats {
     clock: String,
@@ -90,43 +72,7 @@ enum CustomEvent {
     SystemStatsUpdated(SystemStats),
     TrayUpdated(TrayItem),
     TrayRemoved(String),
-}
-
-#[repr(C)]
-#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-struct Vertex {
-    position: [f32; 2],
-    color: [f32; 4],
-}
-
-impl Vertex {
-    const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
-        0 => Float32x2,
-        1 => Float32x4,
-    ];
-
-    fn desc() -> wgpu::VertexBufferLayout<'static> {
-        wgpu::VertexBufferLayout {
-            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
-            step_mode: wgpu::VertexStepMode::Vertex,
-            attributes: &Self::ATTRIBS,
-        }
-    }
-}
-
-fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4]) -> [Vertex; 6] {
-    let x0 = (x / sw) * 2.0 - 1.0;
-    let y0 = 1.0 - (y / sh) * 2.0;
-    let x1 = ((x + w) / sw) * 2.0 - 1.0;
-    let y1 = 1.0 - ((y + h) / sh) * 2.0;
-    [
-        Vertex { position: [x0, y0], color: c },
-        Vertex { position: [x1, y0], color: c },
-        Vertex { position: [x0, y1], color: c },
-        Vertex { position: [x1, y0], color: c },
-        Vertex { position: [x1, y1], color: c },
-        Vertex { position: [x0, y1], color: c },
-    ]
+    LayoutMenuClosed(u32),
 }
 
 fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font_family: &str) -> Buffer {
@@ -263,16 +209,6 @@ struct RectWidget {
 
 
 struct StatusApp {
-    window: XdgWindow,
-    surface: wl_surface::WlSurface,
-    wgpu_surface: wgpu::Surface<'static>,
-    device: wgpu::Device,
-    queue: wgpu::Queue,
-    config: wgpu::SurfaceConfiguration,
-    render_pipeline: wgpu::RenderPipeline,
-    vertex_buffer: wgpu::Buffer,
-    vertex_count: u32,
-
     // Status State
     tags: String,
     layout: String,
@@ -283,18 +219,14 @@ struct StatusApp {
     hovered_tray_item: Option<String>,
     tray_item_bounds: Vec<TrayIconBounds>,
     tag_bounds: Vec<TagBounds>,
+    layout_bounds: Option<LayoutBounds>,
+    layout_menu_pid: Option<u32>,
 
     font_system: FontSystem,
-    swash_cache: SwashCache,
-    text_atlas: TextAtlas,
-    text_renderer: TextRenderer,
-    text_viewport: Viewport,
 
     rects: Vec<RectWidget>,
     overlay_rects: Vec<RectWidget>,
     separators: Vec<Separator>,
-    overlay_vertex_buffer: wgpu::Buffer,
-    overlay_vertex_count: u32,
     text_items: Vec<TextItem>,
 
     scale_factor: f64,
@@ -302,158 +234,21 @@ struct StatusApp {
     height: u32,
     needs_rebuild: bool,
     current_bg_color: [f32; 4],
+    sender: calloop::channel::Sender<CustomEvent>,
 }
 
 impl StatusApp {
-    async fn new(
-        conn: &Connection,
-        qh: &QueueHandle<AppState>,
-        compositor_state: &CompositorState,
-        xdg_shell_state: &XdgShell,
-        width: u32,
-        height: u32,
-        scale: f64,
-    ) -> Self {
-        let surface = compositor_state.create_surface(qh);
-        surface.set_buffer_scale(scale as i32);
-        let window = xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, qh);
-        window.set_title("Clear Status Interface");
-        window.set_app_id("clear-status-interface");
-        window.set_fullscreen(None);
-        window.commit();
-
-        let wayland_handle = Box::leak(Box::new(clear_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 instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
-            backends: wgpu::Backends::VULKAN,
-            ..Default::default()
-        });
-        let wgpu_surface = instance.create_surface(wayland_handle).expect("surface");
-        let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
-            power_preference: wgpu::PowerPreference::LowPower,
-            compatible_surface: Some(&wgpu_surface),
-            force_fallback_adapter: false,
-        }).await.expect("adapter");
-        let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
-            label: Some("GPU Device"),
-            required_features: wgpu::Features::empty(),
-            required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
-            memory_hints: wgpu::MemoryHints::MemoryUsage,
-        }, None).await.expect("device");
-        let config = wgpu_surface.get_default_config(&adapter, width.max(1), height.max(1)).expect("config");
-        // Do not configure surface here before the wayland window configure event has been received.
-        // It will be configured inside the WindowHandler::configure callback via state.resize().
-        // wgpu_surface.configure(&device, &config);
-
-        let shader_code = clear_ui::SHADER;
-        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("Shader"),
-            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader_code)),
-        });
-        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-            label: Some("Pipeline Layout"),
-            bind_group_layouts: &[],
-            push_constant_ranges: &[],
-        });
-        let render_pipeline = 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: config.format,
-                    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 font_system = FontSystem::new();
-        let swash_cache = SwashCache::new();
-        let cache = Cache::new(&device);
-        let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
-        let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
-        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 overlay_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
-            label: Some("Overlay Vertex Buffer"),
-            size: 1,
-            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-            mapped_at_creation: false,
-        });
-
-        let scale_factor = scale;
-
-        let mut app = Self {
-            window, surface, wgpu_surface, device, queue, config, render_pipeline,
-            vertex_buffer, vertex_count: 0,
-            overlay_vertex_buffer, overlay_vertex_count: 0,
-            tags: String::new(),
-            layout: String::new(),
-            title: String::new(),
-            stats: None,
-            tray_items: HashMap::new(),
-            cursor_pos: (0.0, 0.0),
-            hovered_tray_item: None,
-            tray_item_bounds: Vec::new(),
-            tag_bounds: Vec::new(),
-            font_system, swash_cache, text_atlas, text_renderer, text_viewport,
-            rects: Vec::new(), overlay_rects: Vec::new(), separators: Vec::new(), text_items: Vec::new(),
-            scale_factor,
-            width, height,
-            needs_rebuild: true,
-            current_bg_color: color::STATUS_BG,
-        };
-
-        app.rebuild_layout();
-        app
-    }
-
     fn rebuild_layout(&mut self) {
         let font_family = read_status_font_from_config();
         let font_size = read_status_font_size_from_config();
         let show_separators = read_status_separators_from_config();
         let padding = read_status_padding_from_config();
         let separator_color = read_separator_color_from_config().unwrap_or(color::STATUS_ACCENT);
-        let sw = self.width as f32;
-        let sh = self.height as f32;
-        let s = self.scale_factor as f32;
-        let sw_logical = sw / s;
-        let sh_logical = sh / s;
+        let sw_logical = self.width as f32;
+        let sh_logical = self.height as f32;
+        let _s = 1.0f32;
         let bar_h = 28.0;
-        eprintln!("[rebuild_layout] sw={}, sh={}, s={}, sw_logical={}, sh_logical={}, font_family={}, font_size={}", sw, sh, s, sw_logical, sh_logical, font_family, font_size);
+        eprintln!("[rebuild_layout] sw_logical={}, sh_logical={}, font_family={}, font_size={}", sw_logical, sh_logical, font_family, font_size);
 
         self.current_bg_color = read_bg_color_from_config().unwrap_or(color::STATUS_BG);
         eprintln!("[rebuild_layout] Using background color: {:?}", self.current_bg_color);
@@ -479,6 +274,7 @@ impl StatusApp {
         }
 
         self.tag_bounds.clear();
+        self.layout_bounds = None;
         let mut left_x = 12.0;
 
         // 2. Tags
@@ -499,21 +295,16 @@ impl StatusApp {
 
         // Spacing/separator before Layout
         if !self.layout.is_empty() {
-            left_x += padding;
             if show_separators {
                 self.separators.push(Separator::new(
-                    left_x,
+                    left_x + padding,
                     0.0,
                     1.0,
                     bar_h,
                     separator_color,
                 ));
-                left_x += padding + 1.0;
-            } else {
-                left_x += padding;
             }
-        } else {
-            left_x += padding;
+            left_x += padding * 2.0;
         }
 
         // 3. Layout Mode
@@ -521,24 +312,28 @@ impl StatusApp {
             let label_str = self.layout.clone();
             let label = Label::new_with_family(&mut self.font_system, &label_str, font_size, color::TEXT_ACCENT, &font_family);
             let line_w = label.draw(&mut self.text_items, left_x, (bar_h - font_size * 1.4) / 2.0);
+            eprintln!("[rebuild_layout] Layout Mode: '{}', x={}, y={}, w={}, h={}", self.layout, left_x, 0.0, line_w, bar_h);
+            self.layout_bounds = Some(LayoutBounds {
+                x: left_x,
+                y: 0.0,
+                w: line_w,
+                h: bar_h,
+            });
             left_x += line_w;
         }
 
         // Spacing/separator before Title
         if !self.title.is_empty() && self.title != "(none)" {
-            left_x += padding;
             if show_separators {
                 self.separators.push(Separator::new(
-                    left_x,
+                    left_x + padding,
                     0.0,
                     1.0,
                     bar_h,
                     separator_color,
                 ));
-                left_x += padding + 1.0;
-            } else {
-                left_x += padding;
             }
+            left_x += padding * 2.0;
         }
 
         // 4. Focused Title
@@ -879,588 +674,361 @@ impl StatusApp {
 
         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();
-        for r in &self.rects {
-            verts.extend(quad_vertices(r.x * s, r.y * s, r.w * s, r.h * s, sw, sh, r.color));
-        }
-        for sep in &self.separators {
-            let (x, y, w, h) = sep.rect();
-            verts.extend(quad_vertices(x * s, y * s, w * s, h * s, sw, sh, sep.color()));
-        }
-        verts
-    }
-
-    fn collect_overlay_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();
-        for r in &self.overlay_rects {
-            let q = quad_vertices(r.x * s, r.y * s, r.w * s, r.h * s, sw, sh, r.color);
-            verts.extend(q);
+fn get_active_tag_index() -> u32 {
+    let path = if let Ok(display) = std::env::var("WAYLAND_DISPLAY") {
+        format!("/tmp/ccec-tags-{}", display)
+    } else {
+        "/tmp/ccec-tags".to_string()
+    };
+    if let Ok(content) = std::fs::read_to_string(path) {
+        let parts: Vec<&str> = content.split_whitespace().collect();
+        if parts.len() >= 2 {
+            if let Ok(focused_tags) = parts[1].parse::<u32>() {
+                if focused_tags > 0 {
+                    return focused_tags.trailing_zeros() + 1;
+                }
+            }
         }
-        verts
     }
+    1
+}
 
-    fn upload_vertices(&mut self) {
-        // Base/Background rects
-        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);
-
-        // Overlay/Foreground rects
-        let overlay_verts = self.collect_overlay_vertices();
-        self.overlay_vertex_count = overlay_verts.len() as u32;
-        let overlay_data = bytemuck::cast_slice(&overlay_verts);
-        let overlay_needed = overlay_data.len() as wgpu::BufferAddress;
-        if overlay_needed > self.overlay_vertex_buffer.size() {
-            self.overlay_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
-                label: Some("Overlay Vertex Buffer"),
-                size: overlay_needed,
-                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-                mapped_at_creation: false,
-            });
-        }
-        self.queue.write_buffer(&self.overlay_vertex_buffer, 0, overlay_data);
-    }
+impl clear_ui::engine::Application for StatusApp {
+    type Message = CustomEvent;
 
-    fn prepare_text(&mut self) {
-        let w = self.width as f32;
-        let h = self.height as f32;
-        let s = self.scale_factor 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 * s, top: ti.y * s, scale: s, bounds,
-            default_color: ti.color,
-            custom_glyphs: &[],
-        }).collect();
-        self.text_renderer.prepare(
-            &self.device, &self.queue, &mut self.font_system,
-            &mut self.text_atlas, &self.text_viewport, areas, &mut self.swash_cache
-        ).unwrap();
-    }
+    fn new(_qh: &wayland_client::QueueHandle<clear_ui::engine::EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self {
+        let sender_tags = sender.clone();
+        let sender_layout = sender.clone();
+        let sender_title = sender.clone();
+        let sender_stats = sender.clone();
+        let sender_tray = sender.clone();
 
-    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;
-        }
-    }
+        tokio::spawn(spawn_status_listener("tags", sender_tags));
+        tokio::spawn(spawn_status_listener("layout", sender_layout));
+        tokio::spawn(spawn_status_listener("title", sender_title));
+        tokio::spawn(spawn_system_stats(sender_stats));
+        tokio::spawn(spawn_status_tray(sender_tray));
 
-    fn render(&mut self) {
-        if self.needs_rebuild {
-            self.rebuild_layout();
-            self.upload_vertices();
-        }
-        self.prepare_text();
+        let font_system = FontSystem::new();
 
-        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) => { eprintln!("Surface error: {e:?}"); return; }
+        let mut app = Self {
+            tags: String::new(),
+            layout: String::new(),
+            title: String::new(),
+            stats: None,
+            tray_items: HashMap::new(),
+            cursor_pos: (0.0, 0.0),
+            hovered_tray_item: None,
+            tray_item_bounds: Vec::new(),
+            tag_bounds: Vec::new(),
+            layout_bounds: None,
+            layout_menu_pid: None,
+            font_system,
+            rects: Vec::new(),
+            overlay_rects: Vec::new(),
+            separators: Vec::new(),
+            text_items: Vec::new(),
+            scale_factor: 1.0,
+            width: 1920,
+            height: 28,
+            needs_rebuild: true,
+            current_bg_color: color::STATUS_BG,
+            sender,
         };
 
-        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: self.current_bg_color[0] as f64,
-                            g: self.current_bg_color[1] as f64,
-                            b: self.current_bg_color[2] as f64,
-                            a: self.current_bg_color[3] as f64,
-                        }),
-                        store: wgpu::StoreOp::Store,
-                    },
-                })],
-                depth_stencil_attachment: None,
-                timestamp_writes: None,
-                occlusion_query_set: None,
-            });
-
-            pass.set_pipeline(&self.render_pipeline);
-            pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
-            pass.draw(0..self.vertex_count, 0..1);
-
-            self.text_renderer.render(&self.text_atlas, &self.text_viewport, &mut pass).unwrap();
-        }
-
-        if self.overlay_vertex_count > 0 {
-            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("Overlay Render Pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: &view,
-                    resolve_target: None,
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Load,
-                        store: wgpu::StoreOp::Store,
-                    },
-                })],
-                depth_stencil_attachment: None,
-                timestamp_writes: None,
-                occlusion_query_set: None,
-            });
+        app.rebuild_layout();
+        app
+    }
 
-            pass.set_pipeline(&self.render_pipeline);
-            pass.set_vertex_buffer(0, self.overlay_vertex_buffer.slice(..));
-            pass.draw(0..self.overlay_vertex_count, 0..1);
+    fn settings(&self) -> clear_ui::engine::WindowSettings {
+        clear_ui::engine::WindowSettings {
+            title: "Clear Status Interface".to_string(),
+            app_id: "clear-status-interface".to_string(),
+            width: 1920,
+            height: 28,
+            fullscreen: true,
+            min_size: None,
         }
-
-        self.queue.submit(std::iter::once(encoder.finish()));
-        output.present();
     }
-}
 
-// ── CustomEvent & AppState ──
-
-struct AppState {
-    registry_state: RegistryState,
-    compositor_state: CompositorState,
-    xdg_shell_state: XdgShell,
-    shm_state: Shm,
-    seat_state: SeatState,
-    output_state: OutputState,
-
-    seats: Vec<wl_seat::WlSeat>,
-    pointer: Option<wl_pointer::WlPointer>,
-    keyboard: Option<wl_keyboard::WlKeyboard>,
-
-    window: Option<XdgWindow>,
-    surface: Option<wl_surface::WlSurface>,
-
-    state: Option<StatusApp>,
-    exit: bool,
-    redraw: bool,
-    first_configure_received: bool,
-}
-
-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;
-            if self.first_configure_received {
-                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 update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, _exit: &mut bool) {
+        match msg {
+            CustomEvent::TagsUpdated(t) => {
+                self.tags = t;
+            }
+            CustomEvent::LayoutUpdated(l) => {
+                self.layout = l;
+            }
+            CustomEvent::TitleUpdated(t) => {
+                self.title = t;
+            }
+            CustomEvent::SystemStatsUpdated(s) => {
+                self.stats = Some(s);
+            }
+            CustomEvent::TrayUpdated(item) => {
+                self.tray_items.insert(item.id.clone(), item);
+            }
+            CustomEvent::TrayRemoved(id) => {
+                self.tray_items.remove(&id);
+            }
+            CustomEvent::LayoutMenuClosed(pid) => {
+                if self.layout_menu_pid == Some(pid) {
+                    eprintln!("[layout-click] CustomEvent: clear-cloud (PID {}) closed, clearing tracking PID", pid);
+                    self.layout_menu_pid = None;
+                }
             }
         }
-        self.redraw = true;
+        self.needs_rebuild = true;
+        *needs_rebuild = 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,
-    ) {}
-}
+    fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
 
-impl OutputHandler for AppState {
-    fn output_state(&mut self) -> &mut OutputState {
-        &mut self.output_state
+    fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: clear_ui::engine::LogicalSize, scale: f64) {
+        if self.needs_rebuild || self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale {
+            self.width = size.width as u32;
+            self.height = size.height as u32;
+            self.scale_factor = scale;
+            self.rebuild_layout();
+        }
+        for r in &self.rects {
+            quads.push((r.x, r.y, r.w, r.h, r.color));
+        }
+        for sep in &self.separators {
+            let (x, y, w, h) = sep.rect();
+            quads.push((x, y, w, h, sep.color()));
+        }
     }
 
-    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 overlay_quads(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _size: clear_ui::engine::LogicalSize, _scale: f64) {
+        for r in &self.overlay_rects {
+            quads.push((r.x, r.y, r.w, r.h, r.color));
+        }
     }
 
-    fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
-        self.seats.push(seat);
+    fn text_items(&self) -> &[TextItem] {
+        &self.text_items
     }
 
-    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 clear_color(&self) -> [f32; 4] {
+        [
+            self.current_bg_color[0].powf(1.0 / 2.2),
+            self.current_bg_color[1].powf(1.0 / 2.2),
+            self.current_bg_color[2].powf(1.0 / 2.2),
+            self.current_bg_color[3],
+        ]
     }
 
-    fn remove_capability(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _seat: wl_seat::WlSeat,
-        capability: Capability,
-    ) {
-        if capability == Capability::Pointer {
-            self.pointer = None;
+    fn handle_pointer_move(&mut self, pos: clear_ui::engine::LogicalPosition, needs_rebuild: &mut bool) {
+        let (lx, ly) = (pos.x, pos.y);
+        self.cursor_pos = (lx as f64, ly as f64);
+        let mut newly_hovered = None;
+        for bound in &self.tray_item_bounds {
+            if lx >= bound.x && lx <= (bound.x + bound.w)
+                && ly >= bound.y && ly <= (bound.y + bound.h) {
+                newly_hovered = Some(bound.id.clone());
+                break;
+            }
         }
-        if capability == Capability::Keyboard {
-            self.keyboard = None;
+        if self.hovered_tray_item != newly_hovered {
+            self.hovered_tray_item = newly_hovered;
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
         }
     }
 
-    fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
-        self.seats.retain(|s| s != &seat);
-    }
-}
+    fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: clear_ui::engine::LogicalPosition, _needs_rebuild: &mut bool) -> Option<Self::Message> {
+        if state == ElementState::Pressed {
+            let (lx, ly) = (pos.x, pos.y);
+            let cx = lx as f64;
+            let cy = ly as f64;
 
-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],
-    ) {
-        use smithay_client_toolkit::seat::pointer::PointerEventKind;
-        for event in events {
-            let (x, y) = event.position;
-            eprintln!("[pointer] Event: pos=({:.1}, {:.1}), kind={:?}", x, y, event.kind);
-            if let Some(ref mut st) = self.state {
-                st.cursor_pos = (x, y);
+            // Check if tray icon was clicked
+            let mut clicked_tray = None;
+            for bound in &self.tray_item_bounds {
+                if cx >= bound.x as f64 && cx <= (bound.x + bound.w) as f64
+                    && cy >= bound.y as f64 && cy <= (bound.y + bound.h) as f64 {
+                    clicked_tray = Some(bound.id.clone());
+                    break;
+                }
             }
 
-            match &event.kind {
-                PointerEventKind::Motion { .. } => {
-                    if let Some(st) = &mut self.state {
-                        let cx = x;
-                        let cy = y;
-                        
-                        let mut newly_hovered = None;
-                        for bound in &st.tray_item_bounds {
-                            if cx >= bound.x as f64 && cx <= (bound.x + bound.w) as f64
-                                && cy >= bound.y as f64 && cy <= (bound.y + bound.h) as f64 {
-                                newly_hovered = Some(bound.id.clone());
-                                break;
+            if let Some(id) = clicked_tray {
+                let btn_code = match button {
+                    MouseButton::Left => 272,
+                    MouseButton::Right => 273,
+                    _ => 0,
+                };
+                let cx_i = cx as i32;
+                let cy_i = cy as i32;
+                std::thread::spawn(move || {
+                    let rt = tokio::runtime::Builder::new_current_thread()
+                        .enable_all()
+                        .build()
+                        .unwrap();
+                    rt.block_on(async move {
+                        if let Some((destination, path_part)) = id.split_once('/') {
+                            let path = format!("/{}", path_part);
+                            if let Ok(conn) = zbus::Connection::session().await {
+                                if let Ok(proxy) = StatusNotifierItemProxy::builder(&conn)
+                                    .destination(destination.to_string())
+                                    .unwrap()
+                                    .path(path)
+                                    .unwrap()
+                                    .build()
+                                    .await
+                                {
+                                    let is_menu = proxy.item_is_menu().await.unwrap_or(false);
+                                    let menu_path = proxy.menu().await.ok();
+
+                                    let should_show_menu = (btn_code == 273 && menu_path.is_some())
+                                        || (btn_code == 272 && is_menu && menu_path.is_some());
+
+                                    if should_show_menu {
+                                        if let Some(menu_p) = menu_path {
+                                            eprintln!("[tray-click] Displaying menu for {} at path {}", id, menu_p.as_str());
+                                            if let Err(e) = show_clear_cloud_menu(&conn, destination, menu_p.as_str()).await {
+                                                eprintln!("[tray-click] show_clear_cloud_menu failed: {:?}", e);
+                                            }
+                                        }
+                                    } else if btn_code == 272 {
+                                        eprintln!("[tray-click] Calling Activate on {} at ({}, {})", id, cx_i, cy_i);
+                                        if let Err(e) = proxy.activate(cx_i, cy_i).await {
+                                            eprintln!("[tray-click] Activate failed: {:?}", e);
+                                            if let Some(menu_p) = menu_path {
+                                                eprintln!("[tray-click] Fallback: Displaying menu for {}", id);
+                                                if let Err(e) = show_clear_cloud_menu(&conn, destination, menu_p.as_str()).await {
+                                                    eprintln!("[tray-click] Fallback show_clear_cloud_menu failed: {:?}", e);
+                                                }
+                                            }
+                                        }
+                                    } else if btn_code == 273 {
+                                        eprintln!("[tray-click] Calling ContextMenu on {} at ({}, {})", id, cx_i, cy_i);
+                                        let _ = proxy.context_menu(cx_i, cy_i).await;
+                                    }
+                                }
                             }
                         }
-                        if st.hovered_tray_item != newly_hovered {
-                            st.hovered_tray_item = newly_hovered;
-                            st.needs_rebuild = true;
-                            self.redraw = true;
-                        }
+                    });
+                });
+                return None;
+            }
+
+            if button == MouseButton::Left {
+                eprintln!("[tags-click] Mouse left click at logical: ({}, {})", cx, cy);
+                
+                // Check if layout mode was clicked
+                let mut clicked_layout = false;
+                if let Some(ref bounds) = self.layout_bounds {
+                    if cx >= bounds.x as f64 && cx <= (bounds.x + bounds.w) as f64
+                        && cy >= bounds.y as f64 && cy <= (bounds.y + bounds.h) as f64 {
+                        clicked_layout = true;
                     }
                 }
-                PointerEventKind::Press { button, .. } => {
-                    if *button != 272 && *button != 273 {
-                        continue;
-                    }
-                    if let Some(st) = &mut self.state {
-                        let (cx, cy) = st.cursor_pos;
-
-                        // Check if tray icon was clicked
-                        let mut clicked_tray = None;
-                        for bound in &st.tray_item_bounds {
-                            if cx >= bound.x as f64 && cx <= (bound.x + bound.w) as f64
-                                && cy >= bound.y as f64 && cy <= (bound.y + bound.h) as f64 {
-                                clicked_tray = Some(bound.id.clone());
-                                break;
+
+                if clicked_layout {
+                    eprintln!("[layout-click] Layout mode clicked!");
+                    
+                    // Check if there is an existing, running clear-cloud instance
+                    let mut menu_already_running = false;
+                    if let Some(pid) = self.layout_menu_pid {
+                        if std::path::Path::new(&format!("/proc/{}", pid)).exists() {
+                            if let Ok(comm) = std::fs::read_to_string(format!("/proc/{}/comm", pid)) {
+                                if comm.trim() == "clear-cloud" {
+                                    menu_already_running = true;
+                                }
                             }
                         }
+                    }
 
-                        if let Some(id) = clicked_tray {
-                            let btn = *button;
-                            let cx_i = cx as i32;
-                            let cy_i = cy as i32;
+                    if menu_already_running {
+                        if let Some(pid) = self.layout_menu_pid {
+                            eprintln!("[layout-click] clear-cloud (PID {}) is already running, killing it to close the menu", pid);
+                            let _ = std::process::Command::new("kill").arg(pid.to_string()).status();
+                        }
+                        self.layout_menu_pid = None;
+                    } else {
+                        let x_pos = self.layout_bounds.as_ref().map(|b| b.x as i32).unwrap_or(0);
+                        let y_pos = self.layout_bounds.as_ref().map(|b| b.h as i32).unwrap_or(28);
+                        let current_layout = self.layout.clone();
+                        
+                        // Spawn the child on the main thread so we can capture its PID
+                        if let Ok(mut child) = std::process::Command::new("clear-cloud")
+                            .args([
+                                "--dmenu",
+                                "-p",
+                                "Window Mode:",
+                                "-x",
+                                &x_pos.to_string(),
+                                "-y",
+                                &y_pos.to_string(),
+                                "--select",
+                                &current_layout,
+                            ])
+                            .stdin(std::process::Stdio::piped())
+                            .stdout(std::process::Stdio::piped())
+                            .stderr(std::process::Stdio::piped())
+                            .spawn()
+                        {
+                            let pid = child.id();
+                            self.layout_menu_pid = Some(pid);
+                            eprintln!("[layout-click] Spawned clear-cloud with PID {}", pid);
+                            
+                            let thread_sender = self.sender.clone();
                             std::thread::spawn(move || {
-                                let rt = tokio::runtime::Builder::new_current_thread()
-                                    .enable_all()
-                                    .build()
-                                    .unwrap();
-                                rt.block_on(async move {
-                                    if let Some((destination, path_part)) = id.split_once('/') {
-                                        let path = format!("/{}", path_part);
-                                        if let Ok(conn) = zbus::Connection::session().await {
-                                            if let Ok(proxy) = StatusNotifierItemProxy::builder(&conn)
-                                                .destination(destination.to_string())
-                                                .unwrap()
-                                                .path(path)
-                                                .unwrap()
-                                                .build()
-                                                .await
-                                            {
-                                                let is_menu = proxy.item_is_menu().await.unwrap_or(false);
-                                                let menu_path = proxy.menu().await.ok();
-
-                                                let should_show_menu = (btn == 273 && menu_path.is_some())
-                                                    || (btn == 272 && is_menu && menu_path.is_some());
-
-                                                if should_show_menu {
-                                                    if let Some(menu_p) = menu_path {
-                                                        eprintln!("[tray-click] Displaying menu for {} at path {}", id, menu_p.as_str());
-                                                        if let Err(e) = show_clear_cloud_menu(&conn, destination, menu_p.as_str()).await {
-                                                            eprintln!("[tray-click] show_clear_cloud_menu failed: {:?}", e);
-                                                        }
-                                                    }
-                                                } else if btn == 272 {
-                                                    eprintln!("[tray-click] Calling Activate on {} at ({}, {})", id, cx_i, cy_i);
-                                                    if let Err(e) = proxy.activate(cx_i, cy_i).await {
-                                                        eprintln!("[tray-click] Activate failed: {:?}", e);
-                                                        if let Some(menu_p) = menu_path {
-                                                            eprintln!("[tray-click] Fallback: Displaying menu for {}", id);
-                                                            if let Err(e) = show_clear_cloud_menu(&conn, destination, menu_p.as_str()).await {
-                                                                eprintln!("[tray-click] Fallback show_clear_cloud_menu failed: {:?}", e);
-                                                            }
-                                                        }
-                                                    }
-                                                } else if btn == 273 {
-                                                    eprintln!("[tray-click] Calling ContextMenu on {} at ({}, {})", id, cx_i, cy_i);
-                                                    let _ = proxy.context_menu(cx_i, cy_i).await;
-                                                }
-                                            }
+                                let active_tag = get_active_tag_index();
+                                eprintln!("[layout-click] Active tag is {}", active_tag);
+                                if let Some(mut stdin) = child.stdin.take() {
+                                    use std::io::Write;
+                                    let _ = stdin.write_all(b"Cascade\nGrid\nFullscreen\nFloating\nPopup\n");
+                                }
+                                if let Ok(output) = child.wait_with_output() {
+                                    let err_str = String::from_utf8_lossy(&output.stderr);
+                                    if !err_str.is_empty() {
+                                        eprintln!("[clear-cloud stderr] {}", err_str);
+                                    }
+                                    if output.status.success() {
+                                        let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
+                                        if !selected.is_empty() {
+                                            let selected_lower = selected.to_lowercase();
+                                            eprintln!("[layout-click] Selected mode: {}, setting for tag {}", selected_lower, active_tag);
+                                            let _ = std::process::Command::new("clearctl")
+                                                .args(["tag-layout", &active_tag.to_string(), &selected_lower])
+                                                .spawn();
                                         }
                                     }
-                                });
+                                }
+                                let _ = thread_sender.send(CustomEvent::LayoutMenuClosed(pid));
                             });
-                            continue;
                         }
-
-                        if *button == 272 {
-                            eprintln!("[tags-click] Mouse left click at logical: ({}, {})", cx, cy);
-                            for bound in &st.tag_bounds {
-                                eprintln!("[tags-click] Checking Tag '{}' bounds: x=[{}..{}], y=[{}..{}]", 
-                                    bound.name, bound.x, bound.x + bound.w, bound.y, bound.y + bound.h);
-                                if cx >= bound.x as f64 && cx <= (bound.x + bound.w) as f64
-                                    && cy >= bound.y as f64 && cy <= (bound.y + bound.h) as f64 {
-                                    eprintln!("[tags-click] Tag matched: {}", bound.name);
-                                    let name = bound.name.clone();
-                                    std::thread::spawn(move || {
-                                        let _ = std::process::Command::new("clearctl")
-                                            .args(["view", &name])
-                                            .spawn();
-                                    });
-                                    break;
-                                }
-                            }
+                    }
+                } else {
+                    for bound in &self.tag_bounds {
+                        eprintln!("[tags-click] Checking Tag '{}' bounds: x=[{}..{}], y=[{}..{}]", 
+                            bound.name, bound.x, bound.x + bound.w, bound.y, bound.y + bound.h);
+                        if cx >= bound.x as f64 && cx <= (bound.x + bound.w) as f64
+                            && cy >= bound.y as f64 && cy <= (bound.y + bound.h) as f64 {
+                            eprintln!("[tags-click] Tag matched: {}", bound.name);
+                            let name = bound.name.clone();
+                            std::thread::spawn(move || {
+                                let _ = std::process::Command::new("clearctl")
+                                    .args(["view", &name])
+                                    .spawn();
+                            });
+                            break;
                         }
                     }
                 }
-                _ => {}
             }
         }
+        None
     }
-}
-
-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 WindowHandler for AppState {
-    fn configure(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _window: &XdgWindow,
-        configure: WindowConfigure,
-        _serial: u32,
-    ) {
-        self.first_configure_received = true;
-        let (w, h) = configure.new_size;
-        if let (Some(w), Some(h)) = (w, h) {
-            let width = w.get();
-            let height = h.get();
-            if let Some(state) = &mut self.state {
-                let pw = (width as f64 * state.scale_factor) as u32;
-                let ph = (height as f64 * state.scale_factor) as u32;
-                state.resize(pw, ph);
-            }
-        }
-        self.redraw = true;
-    }
-
-    fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
-        self.exit = true;
-    }
-}
 
-impl ProvidesRegistryState for AppState {
-    fn registry(&mut self) -> &mut RegistryState {
-        &mut self.registry_state
-    }
-    
-    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,
-    ) {}
-}
+    fn handle_mouse_wheel(&mut self, _delta: &MouseScrollDelta, _pos: clear_ui::engine::LogicalPosition, _needs_rebuild: &mut bool) {}
 
-delegate_compositor!(AppState);
-delegate_xdg_shell!(AppState);
-delegate_xdg_window!(AppState);
-delegate_shm!(AppState);
-delegate_seat!(AppState);
-delegate_pointer!(AppState);
-delegate_keyboard!(AppState);
-delegate_registry!(AppState);
-delegate_output!(AppState);
-
-impl AppState {
-    fn handle_custom_event(&mut self, event: CustomEvent) {
-        if let Some(ref mut st) = self.state {
-            match event {
-                CustomEvent::TagsUpdated(t) => {
-                    st.tags = t;
-                }
-                CustomEvent::LayoutUpdated(l) => {
-                    st.layout = l;
-                }
-                CustomEvent::TitleUpdated(t) => {
-                    st.title = t;
-                }
-                CustomEvent::SystemStatsUpdated(s) => {
-                    st.stats = Some(s);
-                }
-                CustomEvent::TrayUpdated(item) => {
-                    st.tray_items.insert(item.id.clone(), item);
-                }
-                CustomEvent::TrayRemoved(id) => {
-                    st.tray_items.remove(&id);
-                }
-            }
-            st.needs_rebuild = true;
-            self.redraw = true;
-        }
-    }
+    fn handle_key_input(&mut self, _event: &KeyEvent, _needs_rebuild: &mut bool) -> Option<Self::Message> { None }
 }
 
 async fn spawn_status_listener(sub: &'static str, sender: calloop::channel::Sender<CustomEvent>) {
@@ -1472,16 +1040,18 @@ async fn spawn_status_listener(sub: &'static str, sender: calloop::channel::Send
             Err(_) => "/tmp/ccec-status.sock".to_string(),
         };
         if let Ok(mut stream) = UnixStream::connect(&socket_path).await {
+            eprintln!("[status-listener] connected to {} for sub '{}'", socket_path, sub);
             if stream.write_all(format!("{}\n", sub).as_bytes()).await.is_ok() {
                 let mut reader = BufReader::new(stream);
                 let mut line = String::new();
                 while reader.read_line(&mut line).await.unwrap_or(0) > 0 {
                     let val = line.trim().to_string();
+                    eprintln!("[status-listener] received '{}' update: '{}'", sub, val);
                     if !val.is_empty() {
                         let ev = match sub {
-                            "tags" => CustomEvent::TagsUpdated(val),
-                            "layout" => CustomEvent::LayoutUpdated(val),
-                            "title" => CustomEvent::TitleUpdated(val),
+                            "tags" => CustomEvent::TagsUpdated(val.clone()),
+                            "layout" => CustomEvent::LayoutUpdated(val.clone()),
+                            "title" => CustomEvent::TitleUpdated(val.clone()),
                             _ => unreachable!(),
                         };
                         let _ = sender.send(ev);
@@ -2246,110 +1816,10 @@ async fn spawn_status_tray(sender: calloop::channel::Sender<CustomEvent>) {
 }
 
 fn main() {
-    let conn = Connection::connect_to_env().unwrap();
-    let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
-    let qh = event_queue.handle();
-
-    let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
-    let xdg_shell_state = XdgShell::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);
-
-    let (sender, channel) = calloop::channel::channel::<CustomEvent>();
-
-    let sender_tags = sender.clone();
-    let sender_layout = sender.clone();
-    let sender_title = sender.clone();
-    let sender_stats = sender.clone();
-    let sender_tray = sender.clone();
-
-    // Spawn Tokio Runtime for async listeners
-    std::thread::spawn(move || {
-        let rt = tokio::runtime::Runtime::new().unwrap();
-        rt.block_on(async {
-            tokio::spawn(spawn_status_listener("tags", sender_tags));
-            tokio::spawn(spawn_status_listener("layout", sender_layout));
-            tokio::spawn(spawn_status_listener("title", sender_title));
-            tokio::spawn(spawn_system_stats(sender_stats));
-            tokio::spawn(spawn_status_tray(sender_tray));
-            
-            // Keep the runtime thread alive
-            loop {
-                tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
-            }
-        });
-    });
-
-    let mut app = AppState {
-        registry_state: RegistryState::new(&globals),
-        compositor_state,
-        xdg_shell_state,
-        shm_state,
-        seat_state,
-        output_state,
-        seats: Vec::new(),
-        pointer: None,
-        keyboard: None,
-        window: None,
-        surface: None,
-        state: None,
-        exit: false,
-        redraw: false,
-        first_configure_received: false,
-    };
-
-    // Perform a roundtrip to populate output_state with active output scales
-    event_queue.roundtrip(&mut app).unwrap();
-
-    let scale = clear_ui::wayland::detect_scale_factor(&app.output_state);
-
-    let pw = (1920.0 * scale) as u32;
-    let ph = (28.0 * scale) as u32;
-
-    let state = pollster::block_on(StatusApp::new(
-        &conn,
-        &qh,
-        &app.compositor_state,
-        &app.xdg_shell_state,
-        pw,
-        ph,
-        scale,
-    ));
-
-    app.window = Some(state.window.clone());
-    app.surface = Some(state.surface.clone());
-    app.state = Some(state);
-
-    let mut event_loop = EventLoop::try_new().unwrap();
-    let loop_handle = event_loop.handle();
-    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_custom_event(msg);
-        }
-    }).unwrap();
-
-    loop {
-        event_loop
-            .dispatch(std::time::Duration::from_millis(16), &mut app)
-            .unwrap();
-        if app.exit {
-            break;
-        }
-        if app.redraw {
-            app.redraw = false;
-            if app.first_configure_received {
-                if let Some(state) = &mut app.state {
-                    state.render();
-                }
-            }
-        }
-    }
+    let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
+    let _guard = rt.enter();
 
-    // Drop app explicitly while Wayland connection is still open to prevent SIGSEGV in wgpu drop/unconfigure
-    drop(app);
+    clear_ui::engine::run::<StatusApp>();
 }
 
 fn read_disabled_color_from_config() -> Option<[f32; 4]> {