Ely Office: six offline-first desktop apps in Rust and GPUI

ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Renders assets/icons/<slug>.{icns,png,ico} from the SVG masters.
set -euo pipefail
root=$(cd "$(dirname "$0")/../.." && pwd)
[ "$(uname)" = Darwin ] || { echo "build.sh: macOS only (AppKit draws the icons)" >&2; exit 1; }
icons=$root/assets/icons
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
swiftc -O -o "$work/icon" "$root/packaging/icons/icon.swift"
# The small cut, where one exists, below 48 px.
master() { # slug, variant ("" or -macos), px
local small=$icons/$1-small$2.svg
if [ "$3" -lt 48 ] && [ -f "$small" ]; then echo "$small"; else echo "$icons/$1$2.svg"; fi
}
cut -f1 "$root/packaging/apps.tsv" | while read -r slug; do
set=$work/$slug.iconset
mkdir "$set"
for px in 16 32 128 256 512; do
"$work/icon" "$px" "$(master "$slug" -macos "$px")" "$set/icon_${px}x${px}.png"
"$work/icon" $((px * 2)) "$(master "$slug" -macos $((px * 2)))" "$set/icon_${px}x${px}@2x.png"
done
iconutil -c icns -o "$icons/$slug.icns" "$set"
"$work/icon" 512 "$icons/$slug.svg" "$icons/$slug.png"
sizes=()
for px in 16 24 32 48 64 128 256; do
"$work/icon" "$px" "$(master "$slug" "" "$px")" "$work/$slug-$px.png"
sizes+=("$work/$slug-$px.png")
done
python3 "$root/packaging/icons/ico.py" "$icons/$slug.ico" "${sizes[@]}"
echo "$icons/$slug.{icns,png,ico}"
done
+16
View File
@@ -0,0 +1,16 @@
"""Packs PNGs into a Windows .ico. Usage: ico.py out.ico in.png..."""
import struct
import sys
out, pngs = sys.argv[1], sys.argv[2:]
images = [open(path, "rb").read() for path in pngs]
header = struct.pack("<HHH", 0, 1, len(images))
offset = len(header) + 16 * len(images)
entries = b""
for data in images:
# Size from the PNG header; 256 is written as 0.
width, height = struct.unpack(">II", data[16:24])
entries += struct.pack("<BBBBHHII", width % 256, height % 256, 0, 0, 1, 32, len(data), offset)
offset += len(data)
with open(out, "wb") as ico:
ico.write(header + entries + b"".join(images))
+22
View File
@@ -0,0 +1,22 @@
// Renders SVGs to transparent PNGs. Usage: icon <px> <in.svg> <out.png>...
import AppKit
let args = CommandLine.arguments
guard args.count >= 4, args.count % 2 == 0, let size = Int(args[1]) else {
FileHandle.standardError.write("usage: icon <px> <in.svg> <out.png>...\n".data(using: .utf8)!)
exit(2)
}
for pair in stride(from: 2, to: args.count, by: 2) {
guard let svg = NSImage(contentsOfFile: args[pair]) else {
FileHandle.standardError.write("icon: cannot read \(args[pair])\n".data(using: .utf8)!)
exit(1)
}
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: size, pixelsHigh: size,
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)!
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
svg.draw(in: NSRect(x: 0, y: 0, width: size, height: size))
NSGraphicsContext.restoreGraphicsState()
try rep.representation(using: .png, properties: [:])!.write(to: URL(fileURLWithPath: args[pair + 1]))
}
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
# Writes the icon masters, drawn on Papirus's 192 grid.
import os
import sys
out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(__file__), '../../assets/icons')
def tint(hex_, amount):
# Mixes a colour towards white.
rgb = [int(hex_[i:i + 2], 16) for i in (1, 3, 5)]
return '#%02X%02X%02X' % tuple(round(c + (255 - c) * amount) for c in rgb)
def rect(x, y, w, h, r):
return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{r}"/>'
def circle(x, y, r):
return f'<circle cx="{x}" cy="{y}" r="{r}"/>'
def path(d):
return f'<path d="{d}"/>'
def line(points, width):
pts = ' '.join(f'{x},{y}' for x, y in points)
return (f'<polyline points="{pts}" fill="none" stroke-width="{width}" '
'stroke-linecap="round" stroke-linejoin="round"/>')
def curve(d, width):
return f'<path d="{d}" fill="none" stroke-width="{width}" stroke-linecap="round"/>'
# The page: 128 by 160, top-right corner folded.
PAGE = 'M40 16H112L160 64V168A8 8 0 0 1 152 176H40A8 8 0 0 1 32 168V24A8 8 0 0 1 40 16Z'
FOLD = 'M112 16V56A8 8 0 0 0 120 64H160Z'
# Part: (svg, colour, shadow, small). Shadow 'body', 'part' or None.
APPS = {}
BLUE = '#3D7FE5'
APPS['ely-doc'] = [
(path(PAGE), BLUE, 'body', True), (path(FOLD), tint(BLUE, .45), 'fold', True),
(rect(56, 84, 64, 12, 4), '#FFFFFF', 'part', True),
(rect(56, 108, 80, 8, 4), '#FFFFFF', 'part', True),
(rect(56, 124, 80, 8, 4), '#FFFFFF', 'part', True),
(rect(56, 140, 80, 8, 4), '#FFFFFF', 'part', False),
(rect(56, 156, 48, 8, 4), tint(BLUE, .55), 'part', False),
]
GREEN = '#2E9E5B'
APPS['ely-table'] = [
(path(PAGE), GREEN, 'body', True), (path(FOLD), tint(GREEN, .45), 'fold', True),
(path('M52 92H140V156H52ZM60 108H92V120H60ZM100 108H132V120H100Z'
'M60 128H92V140H60ZM100 128H132V140H100Z').replace('<path', '<path fill-rule="evenodd"'),
'#FFFFFF', 'part', True),
(rect(100, 128, 32, 12, 0), '#FFD54F', None, False),
]
ORANGE = '#F07B2A'
APPS['ely-show'] = [
(line([(96, 136), (64, 176)], 12), '#5D5D5D', 'part', True),
(line([(96, 136), (128, 176)], 12), '#5D5D5D', 'part', True),
(rect(16, 28, 160, 112, 10), ORANGE, 'body', True),
(rect(16, 28, 160, 16, 8), tint(ORANGE, .3), None, False),
(rect(32, 52, 128, 72, 4), '#FFFFFF', 'part', True),
(rect(48, 96, 16, 16, 2), ORANGE, None, True),
(rect(72, 80, 16, 32, 2), ORANGE, None, True),
(rect(96, 66, 16, 46, 2), ORANGE, None, True),
(path('M136 66A16 16 0 1 0 152 82H136Z'), '#5D7FC9', None, False),
]
RED = '#E0344F'
APPS['ely-pdf'] = [
(path(PAGE), RED, 'body', True), (path(FOLD), tint(RED, .45), 'fold', True),
(rect(52, 88, 88, 60, 6), '#FFFFFF', 'part', True),
(path('M60 140L82 112L98 128L108 118L132 140Z'), RED, None, True),
(circle(112, 104, 7), tint(RED, .4), None, False),
(rect(52, 156, 56, 8, 4), tint(RED, .55), 'part', False),
]
SKY = '#1E8FE0'
APPS['ely-mail'] = [
(rect(16, 40, 160, 112, 12), SKY, 'body', True),
(path('M28 40H164L104 92A12 12 0 0 1 88 92Z'), tint(SKY, .35), None, True),
(line([(30, 54), (96, 108), (162, 54)], 12), '#FFFFFF', 'part', True),
(line([(32, 138), (72, 104)], 8), tint(SKY, .35), None, False),
(line([(160, 138), (120, 104)], 8), tint(SKY, .35), None, False),
]
PURPLE = '#7E57C2'
APPS['ely-tree'] = [
(line([(64, 44), (64, 148)], 20), PURPLE, 'part', True),
(curve('M64 128C64 92 128 104 128 72', 20), PURPLE, 'part', True),
(circle(64, 40, 24), PURPLE, 'part', True), (circle(64, 40, 11), '#FFFFFF', None, True),
(circle(128, 64, 24), PURPLE, 'part', True), (circle(128, 64, 11), '#FFFFFF', None, True),
(circle(64, 152, 24), '#F2A900', 'part', True), (circle(64, 152, 11), '#FFFFFF', None, True),
]
APPS['ely-mark'] = [
(rect(28, 24, 136, 152, 10), '#ECECEC', 'body', True),
(path('M38 24H154A10 10 0 0 1 164 34V52H28V34A10 10 0 0 1 38 24Z'), '#4F4F4F', None, True),
(rect(46, 32, 10, 14, 5), '#FFFFFF', None, False), (rect(70, 32, 10, 14, 5), '#FFFFFF', None, False),
(rect(94, 32, 10, 14, 5), '#FFFFFF', None, False), (rect(118, 32, 10, 14, 5), '#FFFFFF', None, False),
(rect(142, 32, 10, 14, 5), '#FFFFFF', None, False),
(path('M44 156V76H66L82 100L98 76H120V156H100V110L82 136L64 110V156Z'), '#3E4A5C', 'part', True),
(path('M128 76H148V124H164L138 158L112 124H128Z'), '#F2A900', 'part', True),
]
APPS['ely-agent'] = [
(rect(16, 32, 160, 140, 12), '#4F4F4F', 'body', True),
(rect(16, 32, 160, 24, 12), '#5F5F5F', None, False),
(rect(16, 44, 160, 12, 0), '#5F5F5F', None, False),
(circle(32, 44, 4), '#FF6B8B', None, False), (circle(46, 44, 4), '#FFC857', None, False),
(circle(60, 44, 4), '#5FD38D', None, False),
(line([(44, 92), (72, 116)], 16), '#FFFFFF', 'part', True),
(line([(72, 116), (44, 140)], 16), '#A2A2A2', 'part', True),
(rect(84, 132, 48, 16, 4), '#FFFFFF', 'part', True),
(path('M148 64C151 84 156 89 176 92C156 95 151 100 148 120C145 100 140 95 120 92C140 89 145 84 148 64Z'),
'#FF6B8B', 'part', True),
]
# Apple's squircle on the 256 canvas; n = 5.
SQ = ('M231 128C231 229.764 229.764 231 128 231C26.236 231 25 229.764 25 128'
'C25 26.236 26.236 25 128 25C229.764 25 231 26.236 231 128Z')
SHADOWS = {'body': (4, 2.88), 'fold': (2, 0.96), 'part': (2, 1.28)}
def paint(svg, colour):
if 'stroke-width' in svg:
return svg.replace('fill="none"', f'fill="none" stroke="{colour}"', 1)
return svg.replace('/>', f' fill="{colour}"/>', 1)
def draw(parts, small):
body, filters = [], set()
for svg, colour, shadow, keep in parts:
if small and not keep:
continue
if shadow and not small:
dy, _ = SHADOWS[shadow]
filters.add(shadow)
body.append(f'<g transform="translate(0 {dy})" opacity="0.2" filter="url(#{shadow})">'
f'{paint(svg, "#000000")}</g>')
body.append(paint(svg, colour))
return ''.join(body), filters
def svg(parts, macos, small):
body, used = draw(parts, small)
defs = ''.join(f'<filter id="{name}" x="-0.2" y="-0.2" width="1.4" height="1.4">'
f'<feGaussianBlur stdDeviation="{SHADOWS[name][1]}"/></filter>'
for name in sorted(used))
if macos:
# A light squircle; the object fills 86 % of it.
scale = 0.86 * 206 / 192
offset = 128 - 96 * scale
content = (f'<path fill="#F4F4F2" d="{SQ}"/>'
f'<g transform="translate({offset:.3f} {offset + 4:.3f}) scale({scale:.4f})">{body}</g>')
else:
content = f'<g transform="scale({256 / 192:.6f})">{body}</g>'
return ('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">'
f'<defs>{defs}</defs>{content}</svg>\n')
for slug, parts in APPS.items():
for macos in (False, True):
for small in (False, True):
name = f'{out}/{slug}{"-small" if small else ""}{"-macos" if macos else ""}.svg'
with open(name, 'w') as file:
file.write(svg(parts, macos, small))