Tray-Menü: Aktionswörter raus — Status-Punkt (grün = läuft) ganz links im Menü-Icon, daneben Projekt-Icon, Label nur der Name

This commit is contained in:
marcus.hinz
2026-07-05 13:13:44 +02:00
parent e21db6845c
commit ddd8984462
+57 -31
View File
@@ -598,28 +598,59 @@ fn project_icon(project: String) -> Result<Option<String>, String> {
Ok(Some(format!("data:image/png;base64,{}", STANDARD.encode(png)))) Ok(Some(format!("data:image/png;base64,{}", STANDARD.encode(png))))
} }
/// Projekt-Icon fürs Tray-Menü: sips konvertiert (auch ICNS) und skaliert auf /// Menü-Icon fürs Tray: ganz links der Status-Punkt (grün = läuft), daneben
/// 36 px; muda rendert Menü-Icons auf macOS mit 18 pt. /// das Projekt-Icon (sips konvertiert, auch ICNS). Ein RGBA-Bild 56×36 px;
fn menu_icon(paths: &Paths, project: &str) -> Result<Option<tauri::image::Image<'static>>, String> { /// muda rendert Menü-Icons mit 18 pt Höhe und proportionaler Breite.
let Some(icon) = read_project_config_in(paths, project)?.terminal.icon else { fn menu_icon(
return Ok(None); paths: &Paths,
}; project: &str,
let src = resolve_icon_path(paths, project, &icon); running: bool,
let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png")); ) -> Result<tauri::image::Image<'static>, String> {
let out = Command::new("sips") const H: usize = 36;
.args(["-s", "format", "png", "-z", "36", "36"]) const DOT_W: usize = 20;
.arg(&src) const W: usize = DOT_W + H;
.arg("--out") let mut canvas = vec![0u8; W * H * 4];
.arg(&tmp) if running {
.output() // macOS-Systemgrün, weicher Rand über Alpha.
.map_err(|e| format!("sips: {e}"))?; let (cx, cy, r) = (10.0f32, 18.0f32, 7.0f32);
if !out.status.success() { for y in 0..H {
return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr))); for x in 0..DOT_W {
let d = ((x as f32 + 0.5 - cx).powi(2) + (y as f32 + 0.5 - cy).powi(2)).sqrt();
let a = (r - d + 0.5).clamp(0.0, 1.0);
if a > 0.0 {
let i = (y * W + x) * 4;
canvas[i..i + 4].copy_from_slice(&[52, 199, 89, (a * 255.0) as u8]);
}
}
}
} }
let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?; if let Some(icon) = read_project_config_in(paths, project)?.terminal.icon {
let _ = fs::remove_file(&tmp); let src = resolve_icon_path(paths, project, &icon);
let img = tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string())?; let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));
Ok(Some(img)) let out = Command::new("sips")
.args(["-s", "format", "png", "-z", "36", "36"])
.arg(&src)
.arg("--out")
.arg(&tmp)
.output()
.map_err(|e| format!("sips: {e}"))?;
if !out.status.success() {
return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr)));
}
let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;
let _ = fs::remove_file(&tmp);
let img = tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string())?;
let (iw, ih) = (img.width() as usize, img.height() as usize);
let rgba = img.rgba();
for y in 0..ih.min(H) {
for x in 0..iw.min(H) {
let src_i = (y * iw + x) * 4;
let dst_i = (y * W + DOT_W + x) * 4;
canvas[dst_i..dst_i + 4].copy_from_slice(&rgba[src_i..src_i + 4]);
}
}
}
Ok(tauri::image::Image::new_owned(canvas, W as u32, H as u32))
} }
/// Terminal-Einstellungen eines Projekts, für den Terminal-Prozess. /// Terminal-Einstellungen eines Projekts, für den Terminal-Prozess.
@@ -1511,8 +1542,8 @@ pub fn run() {
} }
} }
/// Tray-Menü: pro Projekt ein Eintrag mit Icon; das Label nennt die Aktion — /// Tray-Menü: pro Projekt ein Eintrag — Status-Punkt, Projekt-Icon, Name.
/// „starten" öffnet das Terminal, „zeigen" holt das laufende nach vorn. /// Klick startet das Projekt bzw. holt das laufende Terminal nach vorn.
fn tray_menu( fn tray_menu(
app: &tauri::AppHandle, app: &tauri::AppHandle,
) -> Result<tauri::menu::Menu<tauri::Wry>, Box<dyn std::error::Error>> { ) -> Result<tauri::menu::Menu<tauri::Wry>, Box<dyn std::error::Error>> {
@@ -1524,18 +1555,13 @@ fn tray_menu(
let mut projects = list_projects_in(&paths)?; let mut projects = list_projects_in(&paths)?;
projects.sort_by(|a, b| a.name.cmp(&b.name)); projects.sort_by(|a, b| a.name.cmp(&b.name));
for p in projects { for p in projects {
let label = if p.running { let icon = menu_icon(&paths, &p.name, p.running)?;
format!("{} zeigen", p.name)
} else {
format!("{} starten", p.name)
};
let icon = menu_icon(&paths, &p.name)?;
menu.append(&IconMenuItem::with_id( menu.append(&IconMenuItem::with_id(
app, app,
format!("project:{}", p.name), format!("project:{}", p.name),
label, &p.name,
true, true,
icon, Some(icon),
None::<&str>, None::<&str>,
)?)?; )?)?;
} }