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 @@
+
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 @@
+
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");
+ }
+}