Give the app an icon, and a script that installs it

The launcher pointed at `Icon=story-editor`, a name that only the Breeze
theme happened to ship, and only at 16 and 22 px -- so anywhere the
desktop wanted a larger icon it fell back to something generic.

assets/md-manuscript.svg is the source: a sheaf of manuscript pages, an
amber chapter-title line over three of prose. Drawn in plain rounded
rectangles because a finer design silts up into a grey smear at 16 px.
It is rasterised to the eight sizes a desktop asks for, and the 256px
PNG is compiled into the binary as the window icon, so the title bar and
the task switcher carry it whether or not anything is installed. That
costs no new dependency: eframe already pulls `image` with png decoding.
A test decodes the bundled PNG, since a rename would otherwise show up
only as a blank icon at runtime.

Installation is now `./assets/install.sh` -- binary, icons, launcher and
both cache refreshes, safe to re-run after a rebuild. The launcher is a
file in the repository rather than a here-document in the instructions:
pasting one into a terminal leaves the shell sat at a `>` prompt waiting
for a terminator that never arrives, which is exactly what happened, and
this way the launcher is versioned along with everything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYSGPDwkSzhm4qLqjCbqxU
This commit is contained in:
2026-08-25 18:15:21 -05:00
parent f35397b49b
commit be6a2d6a8b
14 changed files with 182 additions and 18 deletions
+62 -14
View File
@@ -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`.
<details>
<summary>What it does, if you would rather do it by hand</summary>
```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.
</details>
`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):
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<title>md-manuscript</title>
<!-- App tile: ink -->
<rect width="64" height="64" rx="13" fill="#22304a"/>
<!-- A sheaf of pages: the sheet behind peeks out above and to the right -->
<rect x="19" y="9" width="30" height="40" rx="2.5" fill="#c9c1b0"/>
<rect x="14" y="14" width="30" height="40" rx="2.5" fill="#f6f1e6"/>
<!-- The chapter title, then prose. Few lines, widely spaced: at 16px a
denser block of them silts up into an illegible grey smear. -->
<rect x="19" y="21" width="16" height="3.4" rx="1.7" fill="#c8763c"/>
<rect x="19" y="30" width="20" height="3" rx="1.5" fill="#3f5578"/>
<rect x="19" y="37" width="20" height="3" rx="1.5" fill="#3f5578"/>
<rect x="19" y="44" width="12" height="3" rx="1.5" fill="#3f5578"/>
</svg>

After

Width:  |  Height:  |  Size: 850 B

+43
View File
@@ -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
+9
View File
@@ -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
+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<title>md-manuscript</title>
<!-- App tile: ink -->
<rect width="64" height="64" rx="13" fill="#22304a"/>
<!-- A sheaf of pages: the sheet behind peeks out above and to the right -->
<rect x="19" y="9" width="30" height="40" rx="2.5" fill="#c9c1b0"/>
<rect x="14" y="14" width="30" height="40" rx="2.5" fill="#f6f1e6"/>
<!-- The chapter title, then prose. Few lines, widely spaced: at 16px a
denser block of them silts up into an illegible grey smear. -->
<rect x="19" y="21" width="16" height="3.4" rx="1.7" fill="#c8763c"/>
<rect x="19" y="30" width="20" height="3" rx="1.5" fill="#3f5578"/>
<rect x="19" y="37" width="20" height="3" rx="1.5" fill="#3f5578"/>
<rect x="19" y="44" width="12" height="3" rx="1.5" fill="#3f5578"/>
</svg>

After

Width:  |  Height:  |  Size: 850 B

+40 -4
View File
@@ -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<egui::IconData> {
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");
}
}