fix: use Ollama-style DMG background (white, hand-drawn arrow, retina)

Replace procedural dark background with Ollama's MIT-licensed static
background asset: white canvas, hand-drawn curved arrow, yellow bookmark
accents. Window resized to 800×400 with app left / Applications right
matching standard macOS DMG conventions.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Alex Cheema
2026-02-16 06:21:16 -08:00
co-authored by Claude Opus 4.6
parent eff1531534
commit f81488cfc0
3 changed files with 17 additions and 216 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+6 -6
View File
@@ -69,8 +69,8 @@ if [[ -n $BACKGROUND_PNG && -f $BACKGROUND_PNG ]]; then
fi
# ── Step 5: Configure window appearance via AppleScript ──────────────────────
# Window: 660×440, icons at 128px centered at y=130, app on left, Applications on right
# Text size set to 10 (Finder minimum) to minimize icon label visibility on dark background
# Window: 800×400, app icon on left, Applications on right (matches Ollama layout)
# Background image is 1600×740 (2× retina). Text size 12.
APP_NAME="$(basename "$APP_PATH")"
osascript <<APPLESCRIPT
@@ -80,16 +80,16 @@ tell application "Finder"
set current view of container window to icon view
set toolbar visible of container window to false
set statusbar visible of container window to false
set bounds of container window to {200, 200, 860, 640}
set bounds of container window to {200, 120, 1000, 520}
set opts to icon view options of container window
set icon size of opts to 128
set text size of opts to 10
set text size of opts to 12
set arrangement of opts to not arranged
if exists file ".background:background.png" then
set background picture of opts to file ".background:background.png"
end if
set position of item "$APP_NAME" of container window to {155, 130}
set position of item "Applications" of container window to {505, 130}
set position of item "$APP_NAME" of container window to {200, 190}
set position of item "Applications" of container window to {600, 190}
close
open
update without registering applications
+11 -210
View File
@@ -1,11 +1,9 @@
#!/usr/bin/env python3
"""Generate a DMG background image for the EXO installer.
"""Copy the static DMG background image to the specified output path.
Creates a 660x440 PNG with:
- Clean solid dark background
- Bold right-pointing arrow (thick shaft + filled triangle head)
- White "Drag to install" instruction text
- Style inspired by Slack/Discord/VSCode DMGs
The background is a 1600×740 retina PNG (2× for 800×400 logical window) with a
hand-drawn arrow and yellow bookmark accents on a white background. Based on
Ollama's DMG background (MIT-licensed).
Usage:
python3 generate-background.py output.png
@@ -13,216 +11,19 @@ Usage:
from __future__ import annotations
import struct
import shutil
import sys
import zlib
from pathlib import Path
def _png_chunk(chunk_type: bytes, data: bytes) -> bytes:
"""Build a single PNG chunk (type + data + CRC)."""
raw = chunk_type + data
return (
struct.pack(">I", len(data))
+ raw
+ struct.pack(">I", zlib.crc32(raw) & 0xFFFFFFFF)
)
def _create_png(
width: int, height: int, pixels: list[list[tuple[int, int, int, int]]]
) -> bytes:
"""Create a minimal RGBA PNG from pixel data."""
signature = b"\x89PNG\r\n\x1a\n"
# IHDR
ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) # 8-bit RGBA
ihdr = _png_chunk(b"IHDR", ihdr_data)
# IDAT — build raw scanlines then deflate
raw_lines = bytearray()
for row in pixels:
raw_lines.append(0) # filter: None
for r, g, b, a in row:
raw_lines.extend((r, g, b, a))
idat = _png_chunk(b"IDAT", zlib.compress(bytes(raw_lines), 9))
# IEND
iend = _png_chunk(b"IEND", b"")
return signature + ihdr + idat + iend
def _set_pixel(
pixels: list[list[tuple[int, int, int, int]]],
x: int,
y: int,
color: tuple[int, int, int],
alpha: float,
) -> None:
"""Set a pixel with alpha blending."""
height = len(pixels)
width = len(pixels[0]) if height > 0 else 0
if not (0 <= x < width and 0 <= y < height):
return
a = max(0, min(255, int(alpha * 255)))
if a == 0:
return
bg = pixels[y][x]
if a == 255:
pixels[y][x] = (color[0], color[1], color[2], 255)
return
fa = a / 255.0
ba = bg[3] / 255.0
oa = fa + ba * (1 - fa)
if oa == 0:
return
r = int((color[0] * fa + bg[0] * ba * (1 - fa)) / oa)
g = int((color[1] * fa + bg[1] * ba * (1 - fa)) / oa)
b = int((color[2] * fa + bg[2] * ba * (1 - fa)) / oa)
pixels[y][x] = (r, g, b, int(oa * 255))
def _draw_arrow(
pixels: list[list[tuple[int, int, int, int]]],
color: tuple[int, int, int],
) -> None:
"""Draw a bold right-pointing arrow: thick shaft + solid filled triangle head.
Arrow is positioned between the app icon (x=155) and Applications (x=505),
centered vertically at y=130 (icons moved up to reduce top space).
"""
# Arrow geometry
cy = 130 # vertical center — aligned with icon row
# Shaft: solid rectangle
shaft_x1 = 250
shaft_x2 = 380
shaft_half_h = 3 # 6px thick shaft
for y in range(cy - shaft_half_h, cy + shaft_half_h + 1):
for x in range(shaft_x1, shaft_x2 + 1):
# Anti-alias top and bottom edges
dist_from_edge = shaft_half_h - abs(y - cy)
if dist_from_edge >= 1:
_set_pixel(pixels, x, y, color, 1.0)
elif dist_from_edge > 0:
_set_pixel(pixels, x, y, color, dist_from_edge)
# Arrowhead: filled triangle pointing right
# Vertices: left-top (375, 180), left-bottom (375, 220), tip (420, 200)
head_left = 375
head_right = 420
head_half_h = 20 # 40px tall triangle
for x in range(head_left, head_right + 1):
# At this x, how tall is the triangle?
t = (x - head_left) / (head_right - head_left) # 0 at left, 1 at tip
half_height = head_half_h * (1.0 - t)
y_top = cy - half_height
y_bot = cy + half_height
for y in range(int(y_top) - 1, int(y_bot) + 2):
dist_top = y - y_top # positive = inside from top
dist_bot = y_bot - y # positive = inside from bottom
if dist_top >= 1.0 and dist_bot >= 1.0:
_set_pixel(pixels, x, y, color, 1.0)
elif dist_top > 0 and dist_bot > 0:
alpha = min(dist_top, dist_bot)
_set_pixel(pixels, x, y, color, min(1.0, alpha))
def _draw_text(
pixels: list[list[tuple[int, int, int, int]]],
x: int,
y: int,
text: str,
color: tuple[int, int, int],
scale: int = 1,
) -> None:
"""Draw pixel text using a built-in 5x7 bitmap font."""
glyphs: dict[str, list[str]] = {
"A": [" 111 ", "1 1", "1 1", "11111", "1 1", "1 1", "1 1"],
"D": ["1110 ", "1 01", "1 01", "1 01", "1 01", "1 01", "1110 "],
"E": ["11111", "1 ", "1 ", "1111 ", "1 ", "1 ", "11111"],
"O": [" 111 ", "1 1", "1 1", "1 1", "1 1", "1 1", " 111 "],
"X": ["1 1", " 1 1 ", " 1 ", " 1 1 ", "1 1", "1 1", " "],
"a": [" ", " ", " 111 ", " 01", " 1111", "1 01", " 1111"],
"c": [" ", " ", " 111 ", "1 ", "1 ", "1 ", " 111 "],
"g": [" ", " ", " 1111", "1 01", " 1111", " 01", " 110 "],
"i": [" ", " 1 ", " ", " 1 ", " 1 ", " 1 ", " 1 "],
"l": [" ", " 1 ", " 1 ", " 1 ", " 1 ", " 1 ", " 1 "],
"n": [" ", " ", "1 10 ", "11 01", "1 01", "1 01", "1 01"],
"o": [" ", " ", " 110 ", "1 01", "1 01", "1 01", " 110 "],
"p": [" ", " ", "1110 ", "1 01", "1110 ", "1 ", "1 "],
"r": [" ", " ", " 110 ", "1 ", "1 ", "1 ", "1 "],
"s": [" ", " ", " 111 ", "1 ", " 11 ", " 01", "111 "],
"t": [" ", " 1 ", "1111 ", " 1 ", " 1 ", " 1 ", " 11 "],
" ": [" ", " ", " ", " ", " ", " ", " "],
}
cursor_x = x
for ch in text:
glyph = glyphs.get(ch)
if glyph is None:
cursor_x += 6 * scale
continue
for row_idx, row_str in enumerate(glyph):
for col_idx, pixel_ch in enumerate(row_str):
if pixel_ch == "1":
for sy in range(scale):
for sx in range(scale):
py = y + row_idx * scale + sy
px = cursor_x + col_idx * scale + sx
_set_pixel(pixels, px, py, color, 1.0)
cursor_x += (len(glyph[0]) + 1) * scale
BACKGROUND_PNG = Path(__file__).parent / "background.png"
def generate_background(output_path: str) -> None:
"""Generate the DMG background image."""
width, height = 660, 440
# Solid dark background — clean, no gradients or vignettes
bg_color = (22, 22, 24, 255) # macOS dark mode surface
pixels: list[list[tuple[int, int, int, int]]] = [
[bg_color] * width for _ in range(height)
]
# Draw bold white right-pointing arrow between icon positions
# Arrow is vertically centered at icon row (y=130)
_draw_arrow(pixels, (255, 255, 255))
# Draw white icon labels (Finder's labels minimized via text size 10)
# Finder positions icons by center point: x=155 and x=505
# Icon bottom edge is at y=130+64=194, labels start below at y=202
label_color = (255, 255, 255)
label_scale = 2
char_width = 6 * label_scale # 5px glyph + 1px spacing, scaled
# "EXO" — 3 chars, centered under icon at x=155
exo_width = 3 * char_width
_draw_text(pixels, 155 - exo_width // 2, 202, "EXO", label_color, scale=label_scale)
# "Applications" — 12 chars, centered under icon at x=505
apps_width = 12 * char_width
_draw_text(
pixels,
505 - apps_width // 2,
202,
"Applications",
label_color,
scale=label_scale,
)
# Draw "Drag to install" instruction — bright white, scale=3 for readability
# Text is ~15 chars × 18px/char (at scale=3) = ~270px wide
text_x = (width - 270) // 2
_draw_text(pixels, text_x, 310, "Drag to install", (255, 255, 255), scale=3)
# Write PNG
png_data = _create_png(width, height, pixels)
Path(output_path).write_bytes(png_data)
"""Copy the static background image to the output path."""
if not BACKGROUND_PNG.exists():
print(f"Error: {BACKGROUND_PNG} not found", file=sys.stderr)
sys.exit(1)
shutil.copy2(BACKGROUND_PNG, output_path)
if __name__ == "__main__":