|
| 1 | +const mer = @import("mer"); |
| 2 | + |
| 3 | +pub const meta: mer.Meta = .{ |
| 4 | + .title = "Desktop — merjs as a native macOS app", |
| 5 | + .description = "No Electron. No Tauri. One 5.3MB Zig binary. Native AppKit + WKWebView.", |
| 6 | + .og_title = "merjs Desktop — Native macOS App in Zig", |
| 7 | +}; |
| 8 | + |
| 9 | +pub fn render(req: mer.Request) mer.Response { |
| 10 | + _ = req; |
| 11 | + return mer.html(html); |
| 12 | +} |
| 13 | + |
| 14 | +const html = |
| 15 | + \\<!DOCTYPE html> |
| 16 | + \\<html lang="en"> |
| 17 | + \\<head> |
| 18 | + \\ <meta charset="UTF-8"> |
| 19 | + \\ <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 20 | + \\ <title>Desktop — merjs</title> |
| 21 | + \\ <link rel="preconnect" href="https://fonts.googleapis.com"> |
| 22 | + \\ <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet"> |
| 23 | + \\ <style> |
| 24 | + \\ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| 25 | + \\ :root { --bg:#f0ebe3; --bg2:#e8e2d9; --bg3:#ddd5cc; --text:#252530; --muted:#8a7f78; --border:#d5cdc4; --red:#e8251f; } |
| 26 | + \\ body { background:var(--bg); color:var(--text); font-family:'DM Sans',system-ui,sans-serif; min-height:100vh; } |
| 27 | + \\ a { color:inherit; text-decoration:none; } |
| 28 | + \\ .page { max-width:680px; margin:0 auto; padding:48px 32px 96px; } |
| 29 | + \\ .header { display:flex; align-items:center; justify-content:space-between; margin-bottom:56px; } |
| 30 | + \\ .wordmark { font-family:'DM Serif Display',Georgia,serif; font-size:18px; letter-spacing:-0.02em; } |
| 31 | + \\ .wordmark span { color:var(--red); } |
| 32 | + \\ .back { font-size:13px; color:var(--muted); transition:color 0.15s; } |
| 33 | + \\ .back:hover { color:var(--text); } |
| 34 | + \\ h1 { font-family:'DM Serif Display',Georgia,serif; font-size:38px; letter-spacing:-0.02em; line-height:1.1; margin-bottom:16px; } |
| 35 | + \\ .subtitle { font-size:15px; color:var(--muted); line-height:1.6; margin-bottom:40px; } |
| 36 | + \\ h2 { font-family:'DM Serif Display',Georgia,serif; font-size:22px; letter-spacing:-0.01em; color:var(--text); margin:40px 0 14px; } |
| 37 | + \\ p { font-size:15px; color:var(--muted); line-height:1.75; margin-bottom:16px; } |
| 38 | + \\ p strong { color:var(--text); font-weight:500; } |
| 39 | + \\ code { font-family:'SF Mono','Fira Code',monospace; font-size:13px; background:var(--bg3); border-radius:4px; padding:1px 6px; color:var(--text); } |
| 40 | + \\ pre { background:var(--bg2); border:1px solid var(--border); border-radius:8px; padding:16px; overflow-x:auto; font-family:'SF Mono','Fira Code',monospace; font-size:13px; color:var(--text); margin:16px 0; line-height:1.6; } |
| 41 | + \\ .rule { border:none; border-top:1px solid var(--border); margin:40px 0; } |
| 42 | + \\ .stats { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin:24px 0 40px; } |
| 43 | + \\ .stat { text-align:center; background:var(--bg2); border:1px solid var(--border); border-radius:8px; padding:20px 12px; } |
| 44 | + \\ .stat-num { font-family:'DM Serif Display',Georgia,serif; font-size:28px; color:var(--red); } |
| 45 | + \\ .stat-label { font-size:12px; color:var(--muted); margin-top:4px; } |
| 46 | + \\ .targets { display:flex; flex-direction:column; gap:8px; margin:16px 0; } |
| 47 | + \\ .target { display:flex; align-items:center; gap:16px; background:var(--bg2); border:1px solid var(--border); border-radius:8px; padding:12px 16px; } |
| 48 | + \\ .target-cmd { font-family:'SF Mono','Fira Code',monospace; font-size:13px; color:var(--red); min-width:160px; } |
| 49 | + \\ .target-desc { font-size:14px; color:var(--muted); } |
| 50 | + \\ .links { display:flex; gap:12px; margin-top:40px; flex-wrap:wrap; } |
| 51 | + \\ .btn { display:inline-flex; align-items:center; font-size:14px; font-weight:500; padding:11px 22px; border-radius:6px; transition:opacity 0.15s; } |
| 52 | + \\ .btn-red { background:var(--red); color:var(--bg); } |
| 53 | + \\ .btn-red:hover { opacity:0.88; } |
| 54 | + \\ .btn-outline { border:1px solid var(--border); color:var(--muted); } |
| 55 | + \\ .btn-outline:hover { color:var(--text); border-color:var(--text); } |
| 56 | + \\ .compare { display:flex; flex-direction:column; gap:8px; margin:16px 0; } |
| 57 | + \\ .compare-row { display:grid; grid-template-columns:140px 100px 1fr; gap:12px; align-items:center; padding:10px 0; border-bottom:1px solid var(--border); font-size:14px; } |
| 58 | + \\ .compare-row:last-child { border-bottom:none; } |
| 59 | + \\ .compare-name { font-weight:500; color:var(--text); } |
| 60 | + \\ .compare-size { font-family:'SF Mono',monospace; font-size:13px; } |
| 61 | + \\ .compare-note { color:var(--muted); font-size:13px; } |
| 62 | + \\ .highlight { color:var(--red); font-weight:600; } |
| 63 | + \\ </style> |
| 64 | + \\</head> |
| 65 | + \\<body> |
| 66 | + \\<div class="page"> |
| 67 | + \\ <header class="header"> |
| 68 | + \\ <a href="/" class="wordmark">mer<span>js</span></a> |
| 69 | + \\ <a href="/" class="back">← home</a> |
| 70 | + \\ </header> |
| 71 | + \\ |
| 72 | + \\ <h1>desktop.zig</h1> |
| 73 | + \\ <p class="subtitle"> |
| 74 | + \\ Native macOS app. No Electron. No Tauri. No Node.js.<br> |
| 75 | + \\ One Zig binary. AppKit + WKWebView. Instant launch. |
| 76 | + \\ </p> |
| 77 | + \\ |
| 78 | + \\ <div class="stats"> |
| 79 | + \\ <div class="stat"><div class="stat-num">5.3MB</div><div class="stat-label">binary size</div></div> |
| 80 | + \\ <div class="stat"><div class="stat-num">0ms</div><div class="stat-label">startup overhead</div></div> |
| 81 | + \\ <div class="stat"><div class="stat-num">171</div><div class="stat-label">lines of Zig</div></div> |
| 82 | + \\ </div> |
| 83 | + \\ |
| 84 | + \\ <hr class="rule"> |
| 85 | + \\ <h2>How it works</h2> |
| 86 | + \\ <p> |
| 87 | + \\ The app spawns the merjs HTTP server on a <strong>background thread</strong>, |
| 88 | + \\ waits for it to bind a random port, then opens a native |
| 89 | + \\ <strong>NSWindow</strong> with <strong>WKWebView</strong> pointed at localhost. |
| 90 | + \\ Same server, same routes, same SSR — just wrapped in a native window. |
| 91 | + \\ </p> |
| 92 | + \\ <pre>main thread: NSApplication + NSWindow + WKWebView |
| 93 | + \\bg thread: merjs HTTP server (:random-port) |
| 94 | + \\protocol: plain HTTP (no IPC, no bridge)</pre> |
| 95 | + \\ |
| 96 | + \\ <hr class="rule"> |
| 97 | + \\ <h2>The ObjC bridge</h2> |
| 98 | + \\ <p> |
| 99 | + \\ No <code>@cImport</code>. No headers. No binding generators. |
| 100 | + \\ Three <code>extern fn</code> declarations give Zig full access to AppKit and WebKit: |
| 101 | + \\ </p> |
| 102 | + \\ <pre>extern fn objc_getClass([*:0]const u8) ?*anyopaque; |
| 103 | + \\extern fn sel_registerName([*:0]const u8) *anyopaque; |
| 104 | + \\extern fn objc_msgSend() void;</pre> |
| 105 | + \\ <p> |
| 106 | + \\ Cast <code>objc_msgSend</code> per call site. |
| 107 | + \\ Zig's comptime handles the rest. No Swift. No Objective-C files. |
| 108 | + \\ </p> |
| 109 | + \\ |
| 110 | + \\ <hr class="rule"> |
| 111 | + \\ <h2>vs Electron</h2> |
| 112 | + \\ <div class="compare"> |
| 113 | + \\ <div class="compare-row"> |
| 114 | + \\ <div class="compare-name">Electron</div> |
| 115 | + \\ <div class="compare-size">~200MB</div> |
| 116 | + \\ <div class="compare-note">Chromium + Node.js, ~300ms launch</div> |
| 117 | + \\ </div> |
| 118 | + \\ <div class="compare-row"> |
| 119 | + \\ <div class="compare-name">Tauri</div> |
| 120 | + \\ <div class="compare-size">~10MB</div> |
| 121 | + \\ <div class="compare-note">System webview, still needs JS runtime</div> |
| 122 | + \\ </div> |
| 123 | + \\ <div class="compare-row"> |
| 124 | + \\ <div class="compare-name highlight">merjs</div> |
| 125 | + \\ <div class="compare-size highlight">5.3MB</div> |
| 126 | + \\ <div class="compare-note">Native binary, zero runtime, instant</div> |
| 127 | + \\ </div> |
| 128 | + \\ </div> |
| 129 | + \\ |
| 130 | + \\ <hr class="rule"> |
| 131 | + \\ <h2>One codebase, five targets</h2> |
| 132 | + \\ <div class="targets"> |
| 133 | + \\ <div class="target"><div class="target-cmd">zig build serve</div><div class="target-desc">Native HTTP server</div></div> |
| 134 | + \\ <div class="target"><div class="target-cmd">zig build worker</div><div class="target-desc">Cloudflare Workers (233KB WASM)</div></div> |
| 135 | + \\ <div class="target"><div class="target-cmd">zig build worker</div><div class="target-desc">Vercel Edge (same WASM)</div></div> |
| 136 | + \\ <div class="target"><div class="target-cmd">docker build</div><div class="target-desc">Container (160MB image)</div></div> |
| 137 | + \\ <div class="target"><div class="target-cmd">zig build desktop</div><div class="target-desc">macOS app (5.3MB binary)</div></div> |
| 138 | + \\ </div> |
| 139 | + \\ |
| 140 | + \\ <hr class="rule"> |
| 141 | + \\ <h2>Try it</h2> |
| 142 | + \\ <pre>zig build desktop |
| 143 | + \\open zig-out/MerApp.app</pre> |
| 144 | + \\ <p>Two commands. Native app on your dock.</p> |
| 145 | + \\ |
| 146 | + \\ <div class="links"> |
| 147 | + \\ <a href="https://github.com/justrach/merjs" class="btn btn-red">GitHub</a> |
| 148 | + \\ <a href="/" class="btn btn-outline">Home</a> |
| 149 | + \\ <a href="/about" class="btn btn-outline">Philosophy</a> |
| 150 | + \\ </div> |
| 151 | + \\</div> |
| 152 | + \\</body> |
| 153 | + \\</html> |
| 154 | +; |
0 commit comments