use miniquad::conf::Icon;
const SMALL: [u8; 16 * 16 * 4] = *include_bytes!("../icon/icon-16.rgba");
const MEDIUM: [u8; 32 * 32 * 4] = *include_bytes!("../icon/icon-32.rgba");
const BIG: [u8; 64 * 64 * 4] = *include_bytes!("../icon/icon-64.rgba");
pub const LINUX_WM_CLASS: &str = "Flatland3";
pub fn window_icon() -> Icon {
Icon {
small: SMALL,
medium: MEDIUM,
big: BIG,
}
}
#[cfg(target_os = "linux")]
pub fn install_linux_desktop() {
let Some(data) = dirs::data_local_dir() else {
return;
};
let apps = data.join("applications");
let icons_root = data.join("icons").join("hicolor");
if std::fs::create_dir_all(&apps).is_err() {
return;
}
const PNGS: &[(u32, &[u8])] = &[
(16, include_bytes!("../icon/icon-16.png")),
(32, include_bytes!("../icon/icon-32.png")),
(48, include_bytes!("../icon/icon-48.png")),
(64, include_bytes!("../icon/icon-64.png")),
(128, include_bytes!("../icon/icon-128.png")),
(256, include_bytes!("../icon/icon-256.png")),
];
let mut icon_256 = None;
for &(size, bytes) in PNGS {
let dir = icons_root.join(format!("{size}x{size}")).join("apps");
if std::fs::create_dir_all(&dir).is_err() {
continue;
}
for name in [LINUX_WM_CLASS, "flatland3"] {
let path = dir.join(format!("{name}.png"));
let _ = write_if_changed(&path, bytes);
if size == 256 && name == LINUX_WM_CLASS {
icon_256 = Some(path);
}
}
}
let exec = desktop_exec();
let icon_key = icon_256
.as_ref()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| LINUX_WM_CLASS.to_string());
let desktop = desktop_entry(&exec, &icon_key);
let desktop_path = apps.join(format!("{LINUX_WM_CLASS}.desktop"));
let _ = write_if_changed(&desktop_path, desktop.as_bytes());
let _ = std::fs::remove_file(apps.join("flatland3.desktop"));
}
#[cfg(target_os = "linux")]
fn write_if_changed(path: &std::path::Path, contents: &[u8]) -> std::io::Result<()> {
if std::fs::read(path).ok().as_deref() == Some(contents) {
return Ok(());
}
std::fs::write(path, contents)
}
#[cfg(target_os = "linux")]
fn desktop_exec() -> String {
std::env::current_exe()
.ok()
.and_then(|p| p.canonicalize().ok())
.map(|p| quote_desktop_exec(&p.to_string_lossy()))
.unwrap_or_else(|| "flatland3-gfx".to_string())
}
fn quote_desktop_exec(path: &str) -> String {
const SPECIAL: &str = " \"'\\><~|&;$*?#()";
if path.chars().any(|c| SPECIAL.contains(c)) {
let escaped = path.replace('\\', r"\\").replace('"', "\\\"");
format!("\"{escaped}\"")
} else {
path.to_string()
}
}
fn desktop_entry(exec: &str, icon: &str) -> String {
let exec_line = format!("Exec={exec}");
let icon_line = format!("Icon={icon}");
let class_line = format!("StartupWMClass={LINUX_WM_CLASS}");
[
"[Desktop Entry]",
"Type=Application",
"Name=Flatland3",
"Comment=Flatland3 graphical play client",
exec_line.as_str(),
icon_line.as_str(),
"Terminal=false",
"Categories=Game;",
"Keywords=MMO;MMORPG;Flatland;",
class_line.as_str(),
"StartupNotify=true",
"",
]
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn icon_rgba_sizes() {
assert_eq!(SMALL.len(), 1024);
assert_eq!(MEDIUM.len(), 4096);
assert_eq!(BIG.len(), 16384);
assert_eq!(SMALL[3], 0, "top-left 16px pixel should be transparent");
assert_eq!(window_icon().big.len(), BIG.len());
}
#[test]
fn desktop_entry_matches_wm_class() {
let text = desktop_entry("/tmp/flatland3-gfx", "/tmp/Flatland3.png");
assert!(text.contains("StartupWMClass=Flatland3"));
assert!(text.contains("Name=Flatland3"));
assert!(text.contains("Exec=/tmp/flatland3-gfx"));
assert_eq!(quote_desktop_exec("/tmp/my game/x"), "\"/tmp/my game/x\"");
}
}