diff --git a/INSTALL-debian12-surface.md b/INSTALL-debian12-surface.md index 6ef7115..7a1a259 100644 --- a/INSTALL-debian12-surface.md +++ b/INSTALL-debian12-surface.md @@ -92,26 +92,74 @@ applications menu (GNOME app grid / XFCE Whisker Menu) and can be pinned for touch: ```sh -# 4a. Binary +./assets/install.sh +``` + +That puts the binary on your `PATH`, installs the icon at every size the desktop +might ask for, and adds the launcher — then refreshes both caches. It is safe to +re-run after every rebuild, which is how you pick up a new version. Installing +somewhere else is `PREFIX=/usr/local sudo ./assets/install.sh`. + +
+What it does, if you would rather do it by hand + +```sh install -Dm755 target/release/md-manuscript ~/.local/bin/md-manuscript -# 4b. Desktop launcher -mkdir -p ~/.local/share/applications -cat > ~/.local/share/applications/md-manuscript.desktop <<'EOF' -[Desktop Entry] -Type=Application -Name=md-manuscript -Comment=Draggable markdown manuscript editor with git sync and ODT export -Exec=md-manuscript -Icon=text-editor -Categories=Office;TextEditor; -Terminal=false -EOF +for size in 16 22 24 32 48 64 128 256; do + install -Dm644 "assets/icons/hicolor/${size}x${size}/apps/md-manuscript.png" \ + ~/.local/share/icons/hicolor/${size}x${size}/apps/md-manuscript.png +done +install -Dm644 assets/icons/hicolor/scalable/apps/md-manuscript.svg \ + ~/.local/share/icons/hicolor/scalable/apps/md-manuscript.svg -# 4c. Refresh the app list +install -Dm644 assets/md-manuscript.desktop \ + ~/.local/share/applications/md-manuscript.desktop + +gtk-update-icon-cache -f -t ~/.local/share/icons/hicolor 2>/dev/null || true update-desktop-database ~/.local/share/applications 2>/dev/null || true ``` +The launcher is `assets/md-manuscript.desktop`, kept in the repository and +copied into place rather than typed out at the prompt — a here-document is an +awkward thing to paste into a terminal, and this way the launcher is versioned +along with everything else. + +
+ +`Icon=md-manuscript` in the launcher is a *name*, not a path: the desktop looks +it up in the icon theme and picks whichever size it wants, which is why the whole +set gets installed rather than one file. `StartupWMClass` matches the running +window to the launcher, so a pinned icon highlights rather than spawning a second +entry. + +The window carries the icon itself as well — the 256px PNG is compiled into the +binary — so the title bar and the task switcher show it even before any of this +is installed. + +### Regenerating the icon + +`assets/md-manuscript.svg` is the source. After editing it, rebuild the PNG set +(ImageMagick rasterises the SVG; Pillow does the downscaling, which is markedly +cleaner at 16 and 24 px): + +```sh +python3 - <<'REGEN' +import os, subprocess +from PIL import Image +subprocess.run(["convert", "-background", "none", "-density", "1200", + "assets/md-manuscript.svg", "-resize", "512x512", + "/tmp/md-manuscript-master.png"], check=True) +im = Image.open("/tmp/md-manuscript-master.png").convert("RGBA") +for size in (16, 22, 24, 32, 48, 64, 128, 256): + d = f"assets/icons/hicolor/{size}x{size}/apps" + os.makedirs(d, exist_ok=True) + im.resize((size, size), Image.LANCZOS).save(f"{d}/md-manuscript.png") +REGEN +cp assets/md-manuscript.svg assets/icons/hicolor/scalable/apps/md-manuscript.svg +cargo build --release # the 256px PNG is baked into the binary +``` + Make sure `~/.local/bin` is on your `PATH` (it is by default on Debian 12 if the directory exists at login — otherwise add this to `~/.bashrc` and re-login): diff --git a/assets/icons/hicolor/128x128/apps/md-manuscript.png b/assets/icons/hicolor/128x128/apps/md-manuscript.png new file mode 100644 index 0000000..3be4b6d Binary files /dev/null and b/assets/icons/hicolor/128x128/apps/md-manuscript.png differ diff --git a/assets/icons/hicolor/16x16/apps/md-manuscript.png b/assets/icons/hicolor/16x16/apps/md-manuscript.png new file mode 100644 index 0000000..6fca5ab Binary files /dev/null and b/assets/icons/hicolor/16x16/apps/md-manuscript.png differ diff --git a/assets/icons/hicolor/22x22/apps/md-manuscript.png b/assets/icons/hicolor/22x22/apps/md-manuscript.png new file mode 100644 index 0000000..1bbdb04 Binary files /dev/null and b/assets/icons/hicolor/22x22/apps/md-manuscript.png differ diff --git a/assets/icons/hicolor/24x24/apps/md-manuscript.png b/assets/icons/hicolor/24x24/apps/md-manuscript.png new file mode 100644 index 0000000..8bcd223 Binary files /dev/null and b/assets/icons/hicolor/24x24/apps/md-manuscript.png differ diff --git a/assets/icons/hicolor/256x256/apps/md-manuscript.png b/assets/icons/hicolor/256x256/apps/md-manuscript.png new file mode 100644 index 0000000..7259eec Binary files /dev/null and b/assets/icons/hicolor/256x256/apps/md-manuscript.png differ diff --git a/assets/icons/hicolor/32x32/apps/md-manuscript.png b/assets/icons/hicolor/32x32/apps/md-manuscript.png new file mode 100644 index 0000000..164cd4c Binary files /dev/null and b/assets/icons/hicolor/32x32/apps/md-manuscript.png differ diff --git a/assets/icons/hicolor/48x48/apps/md-manuscript.png b/assets/icons/hicolor/48x48/apps/md-manuscript.png new file mode 100644 index 0000000..19c2e02 Binary files /dev/null and b/assets/icons/hicolor/48x48/apps/md-manuscript.png differ diff --git a/assets/icons/hicolor/64x64/apps/md-manuscript.png b/assets/icons/hicolor/64x64/apps/md-manuscript.png new file mode 100644 index 0000000..96cee40 Binary files /dev/null and b/assets/icons/hicolor/64x64/apps/md-manuscript.png differ diff --git a/assets/icons/hicolor/scalable/apps/md-manuscript.svg b/assets/icons/hicolor/scalable/apps/md-manuscript.svg new file mode 100644 index 0000000..4d2adcf --- /dev/null +++ b/assets/icons/hicolor/scalable/apps/md-manuscript.svg @@ -0,0 +1,14 @@ + + md-manuscript + + + + + + + + + + + diff --git a/assets/install.sh b/assets/install.sh new file mode 100755 index 0000000..d72b9d4 --- /dev/null +++ b/assets/install.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# Install md-manuscript for the current user: the binary, the icon set and the +# desktop launcher. Safe to re-run; it overwrites what it installed last time. +# +# Everything lands under ~/.local, so no root is needed. Set PREFIX to install +# somewhere else (PREFIX=/usr/local sudo ./assets/install.sh). +set -eu + +# Resolve paths against the repo, not the working directory, so this runs from +# anywhere. +here=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +prefix=${PREFIX:-$HOME/.local} +binary=$here/target/release/md-manuscript + +if [ ! -x "$binary" ]; then + echo "No release binary at $binary" >&2 + echo "Build it first: cargo build --release" >&2 + exit 1 +fi + +install -Dm755 "$binary" "$prefix/bin/md-manuscript" + +for size in 16 22 24 32 48 64 128 256; do + install -Dm644 \ + "$here/assets/icons/hicolor/${size}x${size}/apps/md-manuscript.png" \ + "$prefix/share/icons/hicolor/${size}x${size}/apps/md-manuscript.png" +done +install -Dm644 "$here/assets/icons/hicolor/scalable/apps/md-manuscript.svg" \ + "$prefix/share/icons/hicolor/scalable/apps/md-manuscript.svg" + +install -Dm644 "$here/assets/md-manuscript.desktop" \ + "$prefix/share/applications/md-manuscript.desktop" + +# Best-effort cache refreshes: the launcher works without them, it may just take +# a re-login to show up. +gtk-update-icon-cache -f -t "$prefix/share/icons/hicolor" 2>/dev/null || true +update-desktop-database "$prefix/share/applications" 2>/dev/null || true + +echo "Installed to $prefix" +case ":$PATH:" in + *":$prefix/bin:"*) ;; + *) echo "Note: $prefix/bin is not on your PATH." >&2 ;; +esac diff --git a/assets/md-manuscript.desktop b/assets/md-manuscript.desktop new file mode 100644 index 0000000..ca09718 --- /dev/null +++ b/assets/md-manuscript.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=md-manuscript +Comment=Draggable markdown manuscript editor with git sync and ODT export +Exec=md-manuscript +Icon=md-manuscript +StartupWMClass=md-manuscript +Categories=Office;TextEditor; +Terminal=false diff --git a/assets/md-manuscript.svg b/assets/md-manuscript.svg new file mode 100644 index 0000000..4d2adcf --- /dev/null +++ b/assets/md-manuscript.svg @@ -0,0 +1,14 @@ + + md-manuscript + + + + + + + + + + + diff --git a/src/main.rs b/src/main.rs index d29426e..1dd984e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,12 +18,33 @@ mod spell; use eframe::egui; +/// The window icon, for the title bar, the task switcher and the dock. +/// +/// A desktop usually finds an app's icon by matching the window class against a +/// `.desktop` file, but that only works once one is installed; setting it here +/// means the window carries its own icon whatever it was launched from. +/// +/// The PNG is generated from `assets/md-manuscript.svg` — see the icon section +/// of INSTALL-debian12-surface.md for the command that regenerates the set. +fn window_icon() -> Option { + const PNG: &[u8] = include_bytes!("../assets/icons/hicolor/256x256/apps/md-manuscript.png"); + // A broken icon is not worth refusing to start over. + eframe::icon_data::from_png_bytes(PNG).ok() +} + fn main() -> eframe::Result<()> { + let mut viewport = egui::ViewportBuilder::default() + .with_inner_size([1100.0, 720.0]) + .with_min_inner_size([700.0, 400.0]) + .with_title("md-manuscript") + // Matched against the .desktop file's name by the desktop shell, which + // is what lets a launcher pin and group the window. + .with_app_id("md-manuscript"); + if let Some(icon) = window_icon() { + viewport = viewport.with_icon(icon); + } let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_inner_size([1100.0, 720.0]) - .with_min_inner_size([700.0, 400.0]) - .with_title("md-manuscript"), + viewport, ..Default::default() }; @@ -33,3 +54,18 @@ fn main() -> eframe::Result<()> { Box::new(|cc| Ok(Box::new(app::App::new(cc)))), ) } + +#[cfg(test)] +mod tests { + /// The window icon is an asset loaded by path, so a rename or a bad export + /// would only show up as a missing icon at runtime. Decode it in a test. + #[test] + fn the_window_icon_decodes() { + let icon = super::window_icon().expect("the bundled icon must decode"); + assert_eq!((icon.width, icon.height), (256, 256)); + assert_eq!(icon.rgba.len(), 256 * 256 * 4); + // Not a blank square: the tile has both ink and paper in it. + let opaque = icon.rgba.chunks(4).filter(|p| p[3] > 0).count(); + assert!(opaque > 256 * 256 / 2, "icon looks empty"); + } +}