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

commit7aaa0f0c0bde9b28802d12b4f94050b07bc3dc3b
parentfae9f0c607
authorLucas Galante <[email protected]>
date2026-06-13 21:30
Refactor status interface and UI rendering logic

 src/main.rs | 384 ++++++++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 272 insertions(+), 112 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 7b57b8a..fda88df 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -24,6 +24,7 @@ struct TrayItem {
     icon_theme_path: Option<String>,
     pixmaps: Option<Vec<TrayPixmap>>,
     title: Option<String>,
+    dbus_id: Option<String>,
 }
 
 #[derive(Debug, Clone)]
@@ -34,6 +35,7 @@ struct TrayIconBounds {
     w: f32,
     h: f32,
     title: Option<String>,
+    dbus_id: Option<String>,
 }
 
 #[derive(Debug, Clone)]
@@ -93,7 +95,7 @@ fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font_family: &st
     let mut attrs = Attrs::new();
     if let Some(ref font_name) = family_name {
         let family = match font_name.as_str() {
-            "monospace" => glyphon::Family::Monospace,
+            "monospace" => glyphon::Family::Name(clear_ui::layout::get_system_monospace_font()),
             "sans-serif" => glyphon::Family::SansSerif,
             "serif" => glyphon::Family::Serif,
             name => glyphon::Family::Name(name),
@@ -181,7 +183,7 @@ fn read_memory_usage() -> Option<String> {
     }
     if total > 0.0 {
         let used = total - free - buffers - cached;
-        Some(format!("Mem {:.1}G/{:.1}G", used, total))
+        Some(format!("Mem {:.1}/{:.1}G", used, total))
     } else {
         None
     }
@@ -276,6 +278,9 @@ impl StatusApp {
         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);
+        if let Some(opacity) = clear_ui::color::read_opacity_if_configured() {
+            self.current_bg_color[3] = opacity;
+        }
         eprintln!("[rebuild_layout] Using background color: {:?}", self.current_bg_color);
 
         self.rects.clear();
@@ -437,7 +442,7 @@ impl StatusApp {
                 let draw_x = right_x + (volume_w - label.w) / 2.0; // centered
                 let start_y = (bar_h - font_size * 1.4) / 2.0;
                 eprintln!("[rebuild_layout] Volume: x={}, w={}, is_muted={}", draw_x, label.w, is_muted);
-                if let Some((sx, sy, sw_rect, sh_rect, scol)) = label.strikethrough_rect(draw_x, start_y, 1.0) {
+                if let Some((sx, sy, sw_rect, sh_rect, scol)) = label.strikethrough_rect(draw_x, start_y, self.scale_factor as f32) {
                     eprintln!("[rebuild_layout] Strikethrough rect: sx={}, sy={}, sw={}, sh={}, scol={:?}", sx, sy, sw_rect, sh_rect, scol);
                     self.overlay_rects.push(RectWidget {
                         x: sx,
@@ -537,6 +542,7 @@ impl StatusApp {
                     w: icon_size,
                     h: icon_size,
                     title: item.title.clone(),
+                    dbus_id: item.dbus_id.clone(),
                 });
 
                 // Attempt to draw pixmap
@@ -671,9 +677,58 @@ impl StatusApp {
         // 5c. Tooltip Rendering (if hovered)
         if let Some(ref hovered_id) = self.hovered_tray_item {
             if let Some(bound) = self.tray_item_bounds.iter().find(|b| &b.id == hovered_id) {
-                let tooltip_text = bound.title.as_deref().unwrap_or(bound.id.as_str());
+                let clean_tooltip = |title: Option<&str>, dbus_id: Option<&str>, fallback_id: &str| -> String {
+                    if let Some(t) = title {
+                        let trimmed = t.trim();
+                        if !trimmed.is_empty() {
+                            return trimmed.to_string();
+                        }
+                    }
+                    if let Some(id) = dbus_id {
+                        let trimmed = id.trim();
+                        if !trimmed.is_empty() {
+                            let cleaned = trimmed
+                                .split('_')
+                                .next()
+                                .unwrap_or(trimmed)
+                                .split('-')
+                                .next()
+                                .unwrap_or(trimmed);
+                            if !cleaned.is_empty() && cleaned.chars().any(|c| c.is_alphabetic()) {
+                                let mut chars = cleaned.chars();
+                                if let Some(first) = chars.next() {
+                                    return first.to_uppercase().collect::<String>() + chars.as_str();
+                                }
+                                return cleaned.to_string();
+                            }
+                            return trimmed.to_string();
+                        }
+                    }
+                    let last_segment = fallback_id.split('/').last().unwrap_or(fallback_id);
+                    let cleaned = last_segment
+                        .split('_')
+                        .next()
+                        .unwrap_or(last_segment)
+                        .split('-')
+                        .next()
+                        .unwrap_or(last_segment);
+                    if !cleaned.is_empty() && cleaned.chars().any(|c| c.is_alphabetic()) {
+                        let mut chars = cleaned.chars();
+                        if let Some(first) = chars.next() {
+                            return first.to_uppercase().collect::<String>() + chars.as_str();
+                        }
+                        return cleaned.to_string();
+                    }
+                    last_segment.to_string()
+                };
+
+                let tooltip_text = clean_tooltip(
+                    bound.title.as_deref(),
+                    bound.dbus_id.as_deref(),
+                    bound.id.as_str(),
+                );
                 let tooltip_font_size = font_size - 1.0;
-                let buf = make_text_buffer(&mut self.font_system, tooltip_text, tooltip_font_size, &font_family);
+                let buf = make_text_buffer(&mut self.font_system, &tooltip_text, tooltip_font_size, &font_family);
                 let scale = clear_ui::scale::scale_factor();
                 let text_w = buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0) / scale;
                 let padding = 6.0;
@@ -880,16 +935,7 @@ impl clear_ui::engine::Application for StatusApp {
     }
 
     fn clear_color(&self) -> [f32; 4] {
-        let mut color = [
-            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],
-        ];
-        if let Some(opacity) = clear_ui::color::read_opacity_if_configured() {
-            color[3] = opacity;
-        }
-        color
+        [0.0, 0.0, 0.0, 0.0]
     }
 
     fn handle_pointer_move(&mut self, pos: clear_ui::engine::LogicalPosition, needs_rebuild: &mut bool) {
@@ -921,12 +967,13 @@ impl clear_ui::engine::Application for StatusApp {
             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());
+                    clicked_tray = Some(bound.clone());
                     break;
                 }
             }
 
-            if let Some(id) = clicked_tray {
+            if let Some(bound) = clicked_tray {
+                let id = bound.id.clone();
                 let btn_code = match button {
                     MouseButton::Left => 272,
                     MouseButton::Right => 273,
@@ -934,6 +981,10 @@ impl clear_ui::engine::Application for StatusApp {
                 };
                 let cx_i = cx as i32;
                 let cy_i = cy as i32;
+                let screen_width = self.width as i32;
+                let bar_height = read_status_height_from_config() as i32;
+                let bound_x = bound.x;
+                let bound_w = bound.w;
                 std::thread::spawn(move || {
                     let rt = tokio::runtime::Builder::new_current_thread()
                         .enable_all()
@@ -942,45 +993,56 @@ impl clear_ui::engine::Application for StatusApp {
                     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);
+                            match zbus::Connection::session().await {
+                                Ok(conn) => {
+                                    match StatusNotifierItemProxy::builder(&conn)
+                                        .destination(destination.to_string())
+                                        .unwrap()
+                                        .path(path)
+                                        .unwrap()
+                                        .build()
+                                        .await
+                                    {
+                                        Ok(proxy) => {
+                                            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());
+
+                                            let x_pos = screen_width - (bound_x + bound_w) as i32;
+                                            let y_pos = bar_height;
+
+                                            if should_show_menu {
+                                                if let Some(menu_p) = menu_path {
+                                                    if let Err(e) = show_clear_cloud_menu(&conn, destination, menu_p.as_str(), x_pos, y_pos, true).await {
+                                                        eprintln!("[tray-click] show_clear_cloud_menu failed: {:?}", e);
+                                                    }
+                                                }
+                                            } else if btn_code == 272 {
+                                                if let Err(e) = proxy.activate(cx_i, cy_i).await {
+                                                    eprintln!("[tray-click] Activate failed: {:?}", e);
+                                                    if let Some(menu_p) = menu_path {
+                                                        if let Err(e) = show_clear_cloud_menu(&conn, destination, menu_p.as_str(), x_pos, y_pos, true).await {
+                                                            eprintln!("[tray-click] Fallback show_clear_cloud_menu failed: {:?}", e);
+                                                        }
+                                                    }
                                                 }
+                                            } else if btn_code == 273 {
+                                                let _ = proxy.context_menu(cx_i, cy_i).await;
                                             }
                                         }
-                                    } 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;
+                                        Err(e) => {
+                                            eprintln!("[tray-click] Failed to build proxy: {:?}", e);
+                                        }
                                     }
                                 }
+                                Err(e) => {
+                                    eprintln!("[tray-click] Failed to connect to session bus: {:?}", e);
+                                }
                             }
+                        } else {
+                            eprintln!("[tray-click] Failed to split id: {}", id);
                         }
                     });
                 });
@@ -1039,7 +1101,7 @@ impl clear_ui::engine::Application for StatusApp {
                             ]
                         }).to_string();
 
-                        if let Ok(mut child) = std::process::Command::new("clear-cloud")
+                        if let Ok(mut child) = std::process::Command::new(get_clear_cloud_cmd())
                             .args([
                                 "--json",
                                 "-x",
@@ -1365,25 +1427,35 @@ trait DBusMenu {
     fn about_to_show(&self, id: i32) -> zbus::Result<bool>;
 }
 
-fn flatten_menu(
+struct MenuItem {
+    id: i32,
+    label: String,
+    enabled: bool,
+    is_separator: bool,
+    toggle_state: i32, // -1 if not toggleable, 0 if unchecked, 1 if checked
+    children: Vec<MenuItem>,
+}
+
+fn parse_menu_item(
     id: i32,
     mut properties: std::collections::HashMap<String, zbus::zvariant::OwnedValue>,
-    children: Vec<zbus::zvariant::OwnedValue>,
-    prefix: &str,
-    out: &mut Vec<(i32, String)>
-) {
-    let label: String = properties.remove("label")
+    children_vals: Vec<zbus::zvariant::OwnedValue>,
+) -> Option<MenuItem> {
+    let type_: String = properties.remove("type")
         .and_then(|v| {
             let s: Result<String, _> = v.try_into();
             s.ok()
         })
         .unwrap_or_default();
-    let type_: String = properties.remove("type")
+    let is_separator = type_ == "separator";
+
+    let label: String = properties.remove("label")
         .and_then(|v| {
             let s: Result<String, _> = v.try_into();
             s.ok()
         })
         .unwrap_or_default();
+
     let enabled: bool = properties.remove("enabled")
         .and_then(|v| {
             let b: Result<bool, _> = v.try_into();
@@ -1391,31 +1463,51 @@ fn flatten_menu(
         })
         .unwrap_or(true);
 
-    if type_ == "separator" || !enabled {
-        // Skip
-    } else {
-        let current_path = if prefix.is_empty() {
-            label.clone()
-        } else if !label.is_empty() {
-            format!("{} > {}", prefix, label)
-        } else {
-            prefix.to_string()
-        };
-
-        if !current_path.is_empty() && children.is_empty() {
-            out.push((id, current_path.clone()));
+    let toggle_state: i32 = properties.remove("toggle-state")
+        .and_then(|v| {
+            let i: Result<i32, _> = v.try_into();
+            i.ok()
+        })
+        .unwrap_or(-1);
+
+    let mut children = Vec::new();
+    for child_val in children_vals {
+        let child_val_inner = zbus::zvariant::Value::from(child_val);
+        if let Ok(child) = <(i32, std::collections::HashMap<String, zbus::zvariant::OwnedValue>, Vec<zbus::zvariant::OwnedValue>)>::try_from(child_val_inner) {
+            if let Some(parsed) = parse_menu_item(child.0, child.1, child.2) {
+                children.push(parsed);
+            }
         }
+    }
 
-        for child_val in children {
-            let child_val_inner = zbus::zvariant::Value::from(child_val);
-            if let Ok(child) = <(i32, std::collections::HashMap<String, zbus::zvariant::OwnedValue>, Vec<zbus::zvariant::OwnedValue>)>::try_from(child_val_inner) {
-                flatten_menu(child.0, child.1, child.2, &current_path, out);
-            }
+    Some(MenuItem {
+        id,
+        label,
+        enabled,
+        is_separator,
+        toggle_state,
+        children,
+    })
+}
+
+fn get_clear_cloud_cmd() -> String {
+    if let Ok(home) = std::env::var("HOME") {
+        let path = format!("{}/.local/bin/clear-cloud", home);
+        if std::path::Path::new(&path).exists() {
+            return path;
         }
     }
+    "clear-cloud".to_string()
 }
 
-async fn show_clear_cloud_menu(conn: &zbus::Connection, destination: &str, menu_path: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+async fn show_clear_cloud_menu(
+    conn: &zbus::Connection,
+    destination: &str,
+    menu_path: &str,
+    x_pos: i32,
+    y_pos: i32,
+    align_right: bool,
+) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
     let menu_proxy = DBusMenuProxy::builder(conn)
         .destination(destination)?
         .path(menu_path)?
@@ -1423,51 +1515,117 @@ async fn show_clear_cloud_menu(conn: &zbus::Connection, destination: &str, menu_
         .await?;
 
     let _ = menu_proxy.about_to_show(0).await;
-    let (_, layout) = menu_proxy.get_layout(0, 3, vec![]).await?;
-    eprintln!("[tray-click] show_clear_cloud_menu layout: root_id={}, properties={:?}, children_len={}", layout.0, layout.1, layout.2.len());
+    let (_, layout) = menu_proxy.get_layout(0, 5, vec![]).await?;
 
-    let mut items = Vec::new();
-    flatten_menu(layout.0, layout.1, layout.2, "", &mut items);
-    eprintln!("[tray-click] show_clear_cloud_menu flattened items: {:?}", items);
+    let root_item = match parse_menu_item(layout.0, layout.1, layout.2) {
+        Some(item) => item,
+        None => return Ok(()),
+    };
 
-    if items.is_empty() {
-        eprintln!("[tray-click] show_clear_cloud_menu: items is empty, returning early");
-        return Ok(());
-    }
+    let mut menu_stack: Vec<&MenuItem> = vec![&root_item];
 
-    let mut clear_cloud_input = String::new();
-    for (_, label) in &items {
-        clear_cloud_input.push_str(label);
-        clear_cloud_input.push('\n');
+    enum MenuOption<'a> {
+        Back,
+        Item(&'a MenuItem),
     }
 
-    let mut child = std::process::Command::new("clear-cloud")
-        .args(["--dmenu", "-p", "Tray Menu:"])
-        .stdin(std::process::Stdio::piped())
-        .stdout(std::process::Stdio::piped())
-        .stderr(std::process::Stdio::piped())
-        .spawn()?;
+    loop {
+        let current_item = *menu_stack.last().unwrap();
+        let mut display_list = Vec::new();
 
-    if let Some(mut stdin) = child.stdin.take() {
-        use std::io::Write;
-        stdin.write_all(clear_cloud_input.as_bytes())?;
-    }
+        if menu_stack.len() > 1 {
+            display_list.push(("< Back".to_string(), MenuOption::Back));
+        }
+
+        for child in &current_item.children {
+            if child.is_separator || !child.enabled {
+                continue;
+            }
+
+            let mut display_label = if child.toggle_state == 1 {
+                format!("[x] {}", child.label)
+            } else if child.toggle_state == 0 {
+                format!("[ ] {}", child.label)
+            } else {
+                child.label.clone()
+            };
+
+            if !child.children.is_empty() {
+                display_label = format!("{} >", display_label);
+            }
+
+            display_list.push((display_label, MenuOption::Item(child)));
+        }
+
+        if display_list.is_empty() {
+            break;
+        }
+
+        let mut clear_cloud_input = String::new();
+        for (label, _) in &display_list {
+            clear_cloud_input.push_str(label);
+            clear_cloud_input.push('\n');
+        }
+
+        let mut cmd_args = vec![
+            "--dmenu".to_string(),
+            "-p".to_string(),
+            "Tray Menu:".to_string(),
+            "-x".to_string(),
+            x_pos.to_string(),
+            "-y".to_string(),
+            y_pos.to_string(),
+        ];
+        if align_right {
+            cmd_args.push("--align-right".to_string());
+        }
+
+        let mut child = std::process::Command::new(get_clear_cloud_cmd())
+            .args(&cmd_args)
+            .stdin(std::process::Stdio::piped())
+            .stdout(std::process::Stdio::piped())
+            .stderr(std::process::Stdio::piped())
+            .spawn()?;
+
+        if let Some(mut stdin) = child.stdin.take() {
+            use std::io::Write;
+            stdin.write_all(clear_cloud_input.as_bytes())?;
+        }
+
+        let output = child.wait_with_output()?;
+        if !output.status.success() {
+            break;
+        }
 
-    let output = child.wait_with_output()?;
-    if output.status.success() {
         let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
-        if let Some((id, _)) = items.iter().find(|(_, label)| label == &selected) {
-            let timestamp = std::time::SystemTime::now()
-                .duration_since(std::time::UNIX_EPOCH)
-                .unwrap_or_default()
-                .as_secs() as u32;
-            let val = zbus::zvariant::Value::from("");
-            let _ = menu_proxy.event(*id, "clicked", &val, timestamp).await;
+        if selected.is_empty() {
+            break;
+        }
+
+        if let Some((_, opt)) = display_list.iter().find(|(label, _)| label == &selected) {
+            match opt {
+                MenuOption::Back => {
+                    menu_stack.pop();
+                }
+                MenuOption::Item(menu_item) => {
+                    if !menu_item.children.is_empty() {
+                        menu_stack.push(menu_item);
+                    } else {
+                        let timestamp = std::time::SystemTime::now()
+                            .duration_since(std::time::UNIX_EPOCH)
+                            .unwrap_or_default()
+                            .as_secs() as u32;
+                        let val = zbus::zvariant::Value::from("");
+                        let _ = menu_proxy.event(menu_item.id, "clicked", &val, timestamp).await;
+                        break;
+                    }
+                }
+            }
+        } else {
+            break;
         }
-    } else {
-        let stderr_str = String::from_utf8_lossy(&output.stderr).trim().to_string();
-        eprintln!("[tray-click] clear-cloud failed with status: {:?}, stderr: {:?}", output.status, stderr_str);
     }
+
     Ok(())
 }
 
@@ -1678,6 +1836,7 @@ async fn fetch_tray_item(conn: &zbus::Connection, addr: &NotifierAddress) -> Res
     let icon_name = proxy.icon_name().await.ok();
     let icon_theme_path = proxy.icon_theme_path().await.ok();
     let title = proxy.title().await.ok();
+    let dbus_id = proxy.id().await.ok();
 
     let mut pixmaps = proxy.icon_pixmap().await.ok().and_then(|v| {
         if v.is_empty() || (v.len() == 1 && v[0].0 == 0 && v[0].1 == 0) {
@@ -1724,6 +1883,7 @@ async fn fetch_tray_item(conn: &zbus::Connection, addr: &NotifierAddress) -> Res
         icon_theme_path,
         pixmaps,
         title,
+        dbus_id,
     })
 }