1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
use std::collections::HashMap;
use std::path::PathBuf;
use pangocairo::cairo;
#[derive(Default)]
pub struct IconCache {
map: HashMap<String, Option<cairo::ImageSurface>>,
}
impl IconCache {
/// Get the icon for an app_id, best suited for the given bar height.
/// Results (including misses) are cached per app_id.
pub fn get(&mut self, app_id: &str, bar_height: u32) -> Option<&cairo::ImageSurface> {
self.map
.entry(app_id.to_string())
.or_insert_with(|| resolve_icon(app_id, bar_height))
.as_ref()
}
}
fn resolve_icon(app_id: &str, bar_height: u32) -> Option<cairo::ImageSurface> {
let mut candidates = vec![app_id.to_string()];
if let Some(desktop_icon) = desktop_file_icon(app_id)
&& !candidates.contains(&desktop_icon)
{
candidates.push(desktop_icon);
}
for name in candidates {
if let Some(path) = find_icon_png(&name, bar_height)
&& let Ok(mut file) = std::fs::File::open(path)
&& let Ok(surface) = cairo::ImageSurface::create_from_png(&mut file)
{
return Some(surface);
}
}
None
}
/// Look up the `Icon=` value of `<app_id>.desktop` in the applications
/// directories of $XDG_DATA_HOME and $XDG_DATA_DIRS.
fn desktop_file_icon(app_id: &str) -> Option<String> {
let file_name = format!("{app_id}.desktop");
for base in std::iter::once(data_home()).chain(data_dirs()) {
let path = base.join("applications").join(&file_name);
if let Ok(contents) = std::fs::read_to_string(path) {
let mut in_desktop_entry = false;
for line in contents.lines() {
let line = line.trim();
if line.starts_with('[') {
in_desktop_entry = line == "[Desktop Entry]";
} else if in_desktop_entry
&& let Some(icon) = line.strip_prefix("Icon=")
{
return Some(icon.trim().to_string());
}
}
}
}
None
}
/// Search for `<name>.png` in the icon themes and pixmaps directories.
/// Picks the smallest icon >= bar_height, else the largest available.
fn find_icon_png(name: &str, bar_height: u32) -> Option<PathBuf> {
let file_name = format!("{name}.png");
let mut best: Option<(u32, PathBuf)> = None;
let base_dirs = std::iter::once(data_home())
.chain(std::iter::once(home_dir().join(".icons")))
.chain(data_dirs());
for base in base_dirs {
let pixmap = base.join("pixmaps").join(&file_name);
if pixmap.is_file() {
consider(&mut best, 0, pixmap, bar_height);
}
let Ok(themes) = std::fs::read_dir(base.join("icons")) else {
continue;
};
for theme in themes.flatten() {
let Ok(size_dirs) = std::fs::read_dir(theme.path()) else {
continue;
};
for size_dir in size_dirs.flatten() {
let dir_name = size_dir.file_name();
let Some(dir_name) = dir_name.to_str() else {
continue;
};
// "48x48" or "48x48@2x"; skip "scalable" (cairo can't load SVG)
let Ok(size) = dir_name.split('x').next().unwrap().parse::<u32>() else {
continue;
};
let path = size_dir.path().join("apps").join(&file_name);
if path.is_file() {
consider(&mut best, size, path, bar_height);
}
}
}
}
best.map(|(_, path)| path)
}
/// Keep the candidate that is the smallest >= bar_height, else the largest.
fn consider(best: &mut Option<(u32, PathBuf)>, size: u32, path: PathBuf, bar_height: u32) {
let better = match best {
None => true,
Some((best_size, _)) => match (size >= bar_height, *best_size >= bar_height) {
(true, true) => size < *best_size,
(true, false) => true,
(false, true) => false,
(false, false) => size > *best_size,
},
};
if better {
*best = Some((size, path));
}
}
fn data_home() -> PathBuf {
match std::env::var_os("XDG_DATA_HOME") {
Some(dir) if !dir.is_empty() => PathBuf::from(dir),
_ => home_dir().join(".local/share"),
}
}
fn data_dirs() -> Vec<PathBuf> {
match std::env::var_os("XDG_DATA_DIRS") {
Some(dirs) if !dirs.is_empty() => std::env::split_paths(&dirs).collect(),
_ => vec!["/usr/local/share".into(), "/usr/share".into()],
}
}
fn home_dir() -> PathBuf {
std::env::var_os("HOME").map_or_else(|| PathBuf::from("/"), PathBuf::from)
}
|