diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bfeaff7..31c3148 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -73,7 +73,10 @@ "Bash(rustup target *)", "Bash(echo \"exit code: $?\")", "Read(//home/cbr/.cargo/registry/src/**)", - "Bash(find / -maxdepth 6 -iname \"mistralrs-*\" -type d)" + "Bash(find / -maxdepth 6 -iname \"mistralrs-*\" -type d)", + "Bash(grep -A2 '^name = \"tauri\"$' \"/mnt/c/Users/cbr/Desktop/Neuer Ordner \\(6\\)/src-tauri/Cargo.lock\")", + "Bash(rustfmt --edition 2024 --check src/badge.rs src/main.rs)", + "Bash(rustfmt --edition 2024 --check src/git.rs)" ] } } diff --git a/src-tauri/src/badge.rs b/src-tauri/src/badge.rs new file mode 100644 index 0000000..f59a0b5 --- /dev/null +++ b/src-tauri/src/badge.rs @@ -0,0 +1,122 @@ +//! Renders the "ahead + behind" count as a small circular icon and applies it as the +//! Windows taskbar overlay badge. Windows has no native numeric taskbar badge (unlike +//! macOS/Linux, which support `Window::set_badge_count`), so the number has to be drawn +//! into a plain RGBA icon ourselves and set via `Window::set_overlay_icon`. + +use tauri::Manager; +use tauri::image::Image; + +const GLYPH_WIDTH: usize = 3; +const GLYPH_HEIGHT: usize = 5; + +/// A minimal 3x5 pixel-grid font, just enough to draw digits and "+" legibly at badge size. +fn glyph_rows(ch: char) -> [&'static str; GLYPH_HEIGHT] { + match ch { + '0' => ["111", "101", "101", "101", "111"], + '1' => ["010", "110", "010", "010", "111"], + '2' => ["111", "001", "111", "100", "111"], + '3' => ["111", "001", "111", "001", "111"], + '4' => ["101", "101", "111", "001", "001"], + '5' => ["111", "100", "111", "001", "111"], + '6' => ["111", "100", "111", "101", "111"], + '7' => ["111", "001", "010", "010", "010"], + '8' => ["111", "101", "111", "101", "111"], + '9' => ["111", "101", "111", "001", "111"], + '+' => ["000", "010", "111", "010", "000"], + _ => ["000", "000", "000", "000", "000"], + } +} + +/// Renders `text` (digits / "+" only) centered on a filled circle, at `size`x`size` pixels, +/// scaling each font pixel up by `scale` so it stays legible once Windows shrinks it down +/// for the taskbar. +fn render_circle_with_text(text: &str, size: usize, scale: usize) -> Vec { + let mut rgba = vec![0u8; size * size * 4]; + + // Filled circle background with a slightly darker rim for contrast against any + // taskbar/background color. + let center = size as f32 / 2.0; + let radius = size as f32 / 2.0 - 1.0; + const FILL_COLOR: [u8; 4] = [220, 53, 69, 255]; + const BORDER_COLOR: [u8; 4] = [176, 32, 48, 255]; + for y in 0..size { + for x in 0..size { + let dx = x as f32 + 0.5 - center; + let dy = y as f32 + 0.5 - center; + let dist = (dx * dx + dy * dy).sqrt(); + if dist <= radius { + let color = if dist >= radius - 2.0 { BORDER_COLOR } else { FILL_COLOR }; + let idx = (y * size + x) * 4; + rgba[idx..idx + 4].copy_from_slice(&color); + } + } + } + + let glyphs: Vec<[&'static str; GLYPH_HEIGHT]> = text.chars().map(glyph_rows).collect(); + let glyph_px_w = GLYPH_WIDTH * scale; + let glyph_px_h = GLYPH_HEIGHT * scale; + let gap = scale; + let total_w = glyphs.len() * glyph_px_w + gap * glyphs.len().saturating_sub(1); + let start_x = size.saturating_sub(total_w) / 2; + let start_y = size.saturating_sub(glyph_px_h) / 2; + + for (gi, rows) in glyphs.iter().enumerate() { + let glyph_x = start_x + gi * (glyph_px_w + gap); + for (gy, row) in rows.iter().enumerate() { + for (gx, pixel) in row.chars().enumerate() { + if pixel != '1' { + continue; + } + for py in 0..scale { + for px in 0..scale { + let x = glyph_x + gx * scale + px; + let y = start_y + gy * scale + py; + if x < size && y < size { + let idx = (y * size + x) * 4; + rgba[idx..idx + 4].copy_from_slice(&[255, 255, 255, 255]); + } + } + } + } + } + } + + rgba +} + +/// Renders the badge icon for a given count. Caps the displayed text at "9+" since the +/// badge is only large enough for two characters to stay legible. +fn render_badge_icon(count: u32) -> Image<'static> { + const SIZE: usize = 40; + const SCALE: usize = 4; + let text = if count > 9 { "9+".to_string() } else { count.to_string() }; + let rgba = render_circle_with_text(&text, SIZE, SCALE); + Image::new_owned(rgba, SIZE as u32, SIZE as u32) +} + +/// Sets the taskbar badge to `ahead + behind` (0 clears it). Windows-only: Windows has no +/// native numeric badge API, so this draws and applies a small overlay icon instead. No-op +/// on other platforms — non-Windows desktops should use `Window::set_badge_count` for a +/// real native badge instead, which this app doesn't currently wire up. +#[tauri::command] +pub fn set_sync_badge(app: tauri::AppHandle, ahead: u32, behind: u32) -> Result<(), String> { + let total = ahead + behind; + + #[cfg(target_os = "windows")] + { + let Some(window) = app.get_webview_window("main") else { + return Ok(()); + }; + let icon = if total == 0 { None } else { Some(render_badge_icon(total)) }; + window + .set_overlay_icon(icon) + .map_err(|err| format!("Could not set taskbar badge: {err}"))?; + } + + #[cfg(not(target_os = "windows"))] + { + let _ = (app, total); + } + + Ok(()) +} diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 0ad8215..f0b1070 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -1302,6 +1302,8 @@ pub fn compare_commits( &repo, [ "diff", + "--no-ext-diff", + "--no-textconv", "-M", FULL_FILE_DIFF_CONTEXT, from_hash.as_str(), @@ -1344,7 +1346,14 @@ pub fn diff_file_against_working_tree( )?; let patch_output = run_git_with_paths( &repo, - &["diff", "-M", FULL_FILE_DIFF_CONTEXT, commit_hash.as_str()], + &[ + "diff", + "--no-ext-diff", + "--no-textconv", + "-M", + FULL_FILE_DIFF_CONTEXT, + commit_hash.as_str(), + ], std::slice::from_ref(&file), )?; @@ -1400,6 +1409,8 @@ pub fn compare_file_to_head( &repo, &[ "diff", + "--no-ext-diff", + "--no-textconv", "-M", FULL_FILE_DIFF_CONTEXT, commit_hash.as_str(), @@ -1472,6 +1483,8 @@ pub fn compare_file_to_parent( &repo, &[ "diff", + "--no-ext-diff", + "--no-textconv", "-M", FULL_FILE_DIFF_CONTEXT, from_hash.as_str(), diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1422785..341669f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,7 +1,9 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +mod badge; mod git; +use badge::set_sync_badge; use git::{ SearchCancellationState, apply_file_patch, cancel_code_search, cancel_file_history, checkout_branch, commit, commit_ai_generate, commit_ai_load, commit_ai_local_models, @@ -62,7 +64,8 @@ fn main() { get_remote_url, cred_load, cred_save, - cred_delete + cred_delete, + set_sync_badge ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.svelte b/src/App.svelte index 5f6b11d..b0ce520 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -63,6 +63,7 @@ restoreFiles, restoreToCommit, searchCodeIntroductions, + setSyncBadge, stageFiles, unstageFiles, } from "./lib/git"; @@ -656,6 +657,7 @@ repoPath = ""; status = null; lastStatusFingerprint = ""; + void setSyncBadge(0, 0).catch(() => {}); } branches = []; commits = []; @@ -689,6 +691,7 @@ repoPath = activeRepoPath; lastStatusFingerprint = statusFingerprint(nextStatus); upsertRepoTab(activeRepoPath, nextStatus); + void setSyncBadge(nextStatus.ahead, nextStatus.behind).catch(() => {}); } function errorToMessage(error: unknown): string { diff --git a/src/lib/git.ts b/src/lib/git.ts index efe5c8b..8d7e8fb 100644 --- a/src/lib/git.ts +++ b/src/lib/git.ts @@ -37,6 +37,12 @@ export function getStatus(path: string): Promise { return invoke("get_status", { path }); } +// Sets the taskbar icon badge to ahead + behind (0 clears it). Windows only — a no-op on +// other platforms, since Windows has no native numeric badge to fall back to. +export function setSyncBadge(ahead: number, behind: number): Promise { + return invoke("set_sync_badge", { ahead, behind }); +} + export function listBranches(path: string): Promise { return invoke("list_branches", { path }); }