Skip to content

Commit d2485cc

Browse files
committed
Improve icon finding
1 parent 3e74943 commit d2485cc

4 files changed

Lines changed: 99 additions & 15 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ edition = "2024"
66
[dependencies]
77
anyhow = "1.0.100"
88
arboard = "3.6.1"
9+
block2 = "0.6.2"
910
emojis = "0.8.0"
1011
global-hotkey = "0.7.0"
1112
iced = { version = "0.14.0", features = ["image", "tokio"] }

src/platform/macos/discovery.rs

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,24 @@ use core::{
1616
};
1717
use std::{
1818
env,
19+
io::Cursor,
1920
path::{Path, PathBuf},
2021
sync::LazyLock,
2122
};
2223

24+
use iced::widget::image::Handle;
2325
use log::error;
26+
use objc2::{Message, rc::Retained};
27+
use objc2_app_kit::{NSBitmapImageFileType, NSBitmapImageRep, NSImage, NSImageRep, NSWorkspace};
2428
use objc2_core_foundation::{CFArray, CFRetained, CFURL};
25-
use objc2_foundation::{NSBundle, NSNumber, NSString, NSURL, ns_string};
29+
use objc2_foundation::{
30+
NSBundle, NSData, NSDictionary, NSNumber, NSSize, NSString, NSURL, ns_string,
31+
};
2632
use rayon::iter::{IntoParallelIterator, ParallelIterator as _};
2733

2834
use crate::{
2935
app::apps::{App, AppCommand},
3036
commands::Function,
31-
utils::handle_from_icns,
3237
};
3338

3439
use super::super::cross;
@@ -238,18 +243,17 @@ fn query_app(url: impl AsRef<NSURL>, store_icons: bool) -> Option<App> {
238243
.map(|stem| stem.to_string_lossy().into_owned())
239244
})?;
240245

241-
let icons = store_icons
242-
.then(|| {
243-
get_string(ns_string!("CFBundleIconFile")).and_then(|icon| {
244-
let mut path = path.join("Contents/Resources").join(&icon);
245-
if path.extension().is_none() {
246-
path.set_extension("icns");
247-
}
248-
249-
handle_from_icns(&path)
250-
})
251-
})
252-
.flatten();
246+
let icon = icon_of_path_ns(path.to_str().unwrap_or(&name)).unwrap_or(vec![]);
247+
let icons = if store_icons {
248+
image::ImageReader::new(Cursor::new(icon))
249+
.with_guessed_format()
250+
.unwrap()
251+
.decode()
252+
.ok()
253+
.map(|img| Handle::from_rgba(img.width(), img.height(), img.into_bytes()))
254+
} else {
255+
None
256+
};
253257

254258
Some(App {
255259
ranking: 0,
@@ -307,3 +311,82 @@ fn is_helper_location(path: &Path) -> bool {
307311
|| s.contains("/Contents/Frameworks/")
308312
|| s.contains("/Library/PrivilegedHelperTools/")
309313
}
314+
315+
/// https://github.com/cardisoft/cardinal/blob/339b27c3c6abaf94405a9ab09ec39296baba4f91/fs-icon/src/lib.rs#L37
316+
pub fn icon_of_path_ns(path: &str) -> Option<Vec<u8>> {
317+
objc2::rc::autoreleasepool(|_| -> Option<Vec<u8>> {
318+
let path_ns = NSString::from_str(path);
319+
let image = NSWorkspace::sharedWorkspace().iconForFile(&path_ns);
320+
321+
// Choose what you consider "high quality" output.
322+
// 256 is a good default; you can bump to 512 if you want.
323+
let target: f64 = 256.0;
324+
325+
let png_data: Retained<NSData> = (|| -> Option<_> {
326+
unsafe {
327+
// Pick the best representation:
328+
// - Prefer the smallest rep that is >= target (avoids upscaling)
329+
// - Otherwise pick the largest available rep
330+
let mut best_rep = None::<Retained<NSImageRep>>;
331+
let mut best_w = 0.0;
332+
let mut best_h = 0.0;
333+
334+
let mut largest_rep = None::<Retained<NSImageRep>>;
335+
let mut largest_area = 0.0;
336+
let mut largest_w = 0.0;
337+
let mut largest_h = 0.0;
338+
339+
for rep in image.representations().iter() {
340+
let s = rep.size();
341+
let w = s.width;
342+
let h = s.height;
343+
344+
// Track largest (fallback)
345+
let area = w * h;
346+
if area > largest_area {
347+
largest_area = area;
348+
largest_rep = Some(rep.retain());
349+
largest_w = w;
350+
largest_h = h;
351+
}
352+
353+
// Track best rep for target (no upscale if possible)
354+
if w >= target && h >= target {
355+
let best_area = best_w * best_h;
356+
if best_rep.is_none() || area < best_area {
357+
best_rep = Some(rep.retain());
358+
best_w = w;
359+
best_h = h;
360+
}
361+
}
362+
}
363+
364+
let (rep, out_w, out_h) = if let Some(rep) = best_rep {
365+
(rep, target, target)
366+
} else if let Some(rep) = largest_rep {
367+
// If nothing reaches target, use largest and render at its native size
368+
(rep, largest_w, largest_h)
369+
} else {
370+
return None;
371+
};
372+
373+
let new_image = NSImage::imageWithSize_flipped_drawingHandler(
374+
NSSize::new(out_w, out_h),
375+
false,
376+
&block2::RcBlock::new(move |rect| {
377+
rep.drawInRect(rect);
378+
true.into()
379+
}),
380+
);
381+
382+
NSBitmapImageRep::imageRepWithData(&*new_image.TIFFRepresentation()?)?
383+
.representationUsingType_properties(
384+
NSBitmapImageFileType::PNG,
385+
&NSDictionary::new(),
386+
)
387+
}
388+
})()?;
389+
390+
Some(png_data.to_vec())
391+
})
392+
}

src/utils.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use image::RgbaImage;
77
use objc2_app_kit::NSWorkspace;
88
use objc2_foundation::NSURL;
99

10-
/// This logs an error to the error log file
1110
pub fn icns_data_to_handle(data: Vec<u8>) -> Option<Handle> {
1211
let family = IconFamily::read(std::io::Cursor::new(&data)).ok()?;
1312

0 commit comments

Comments
 (0)