From 89c33e229745e6300a67e642eeaef896f9794f73 Mon Sep 17 00:00:00 2001
From: Matheus Quost <19558461+Quost@users.noreply.github.com>
Date: Thu, 12 Nov 2026 11:28:20 -0300
Subject: [PATCH 1/2] feat(dev): render the promo video off-screen from demo
data
Co-Authored-By: Claude Opus 5.5
---
CONTRIBUTING.md | 13 +++
Sources/Menew/App.swift | 2 +
Sources/Menew/VideoFrames.swift | 80 +++++++++++++++
scripts/make-video.swift | 176 ++++++++++++++++++++++++++++++++
4 files changed, 271 insertions(+)
create mode 100644 Sources/Menew/VideoFrames.swift
create mode 100644 scripts/make-video.swift
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ddeca82..030fe16 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -164,3 +164,16 @@ Downloads per version, update checks and repo traffic, all from GitHub's public
## Reporting bugs
Use **Report a problem…** in Menew's right-click menu: it opens a GitHub issue with your versions filled in. Or open an [issue](https://github.com/Quost/menew/issues/new/choose) directly.
+
+## Promo video
+
+The promo video is rendered off-screen from demo data, so it never shows your real screen and needs no screen recording permission:
+
+```bash
+./build.sh
+build/Menew.app/Contents/MacOS/Menew --video-frames /tmp/frames -language en
+swift scripts/make-video.swift /tmp/frames /tmp/video-out
+ffmpeg -framerate 30 -i /tmp/video-out/%04d.jpg -c:v libx264 -crf 26 -pix_fmt yuv420p -movflags +faststart menew.mp4
+```
+
+The video has no burned-in text besides shortcuts and the address, so one file serves every language.
diff --git a/Sources/Menew/App.swift b/Sources/Menew/App.swift
index f891f4b..af93e99 100644
--- a/Sources/Menew/App.swift
+++ b/Sources/Menew/App.swift
@@ -17,6 +17,8 @@ enum MenewMain {
if !DemoMode.isOn { Migration.run() }
if let i = CommandLine.arguments.firstIndex(of: "--snapshot"), CommandLine.arguments.count > i + 1 {
Snapshot.run(into: URL(fileURLWithPath: CommandLine.arguments[i + 1]))
+ } else if let i = CommandLine.arguments.firstIndex(of: VideoFrames.flag), CommandLine.arguments.count > i + 1 {
+ VideoFrames.run(into: URL(fileURLWithPath: CommandLine.arguments[i + 1]))
} else if let i = CommandLine.arguments.firstIndex(of: "--preview"), CommandLine.arguments.count > i + 1 {
Snapshot.preview(tab: CommandLine.arguments[i + 1])
} else if CommandLine.arguments.contains("--hardware") {
diff --git a/Sources/Menew/VideoFrames.swift b/Sources/Menew/VideoFrames.swift
new file mode 100644
index 0000000..e2435ed
--- /dev/null
+++ b/Sources/Menew/VideoFrames.swift
@@ -0,0 +1,80 @@
+import AppKit
+import SwiftUI
+
+/// `Menew --demo --video-frames `: renderiza, com os dados fictícios, os quadros-chave do vídeo de
+/// divulgação (abas do menu, busca no clipboard, palette sendo digitada, CPU e notch) e sai.
+/// `scripts/make-video.swift` monta esses quadros no vídeo. Nada vem da tela real, então não precisa de
+/// permissão de gravação e funciona em qualquer idioma (`-language`).
+@MainActor
+enum VideoFrames {
+ static let flag = "--video-frames"
+
+ static func run(into dir: URL) {
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
+ Theme.flatGlass = true
+ let monitor = SystemMonitor()
+ let clipboard = ClipboardManager.demo()
+ MeetingStore.shared.showDemo()
+ NetUsage.shared.showDemo()
+ SystemHistory.shared.showDemo()
+ Sensors.shared.showDemo()
+ let toolbox = Toolbox()
+ toolbox.keepAwake = true
+ DemoMode.fakeDisplays(toolbox)
+ let internet = InternetInfo()
+ DemoMode.fakeInternet(internet)
+ let notes = NotesStore.demo()
+ WorldClock.shared.demo()
+ ProfileStore.shared.demo()
+ AutomationStore.shared.demo()
+ monitor.start()
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 6.5) {
+ DemoMode.fakeMachine(monitor)
+ func out(_ name: String) -> URL { dir.appendingPathComponent(name + ".png") }
+ @MainActor func popover(_ tab: Tab, query: String = "") -> some View {
+ let nav = PopoverState()
+ nav.tab = tab
+ let cs = ClipListState()
+ cs.query = query
+ return MainView(nav: nav, monitor: monitor, clipboard: clipboard, toolbox: toolbox, internet: internet,
+ notes: notes, cleaner: Cleaner(), updater: UpdaterModel(enabled: false), clipState: cs, onPickClip: { _, _ in })
+ }
+ let pop = NSSize(width: 400, height: 600)
+ for tab in [Tab.now, .clipboard, .actions, .system] {
+ Snapshot.render(popover(tab), size: pop, dark: true, to: out("popover-\(tab)"))
+ }
+ // Busca no clipboard, letra a letra
+ let search = "github"
+ for n in 1...search.count {
+ Snapshot.render(popover(.clipboard, query: String(search.prefix(n))), size: pop, dark: true,
+ to: out("clip-search-\(n)"))
+ }
+ // Palette: duas consultas digitadas letra a letra
+ let palette = PaletteModel(toolbox: toolbox, clipboard: clipboard, notes: notes)
+ palette.isDemo = true
+ palette.update()
+ Snapshot.render(PaletteController.chrome(PaletteView(model: palette)), size: PaletteController.size, dark: true,
+ background: .clear, to: out("palette-0-0"))
+ for (q, text) in ["5 km to mi", "timer 10m tea"].enumerated() {
+ for n in 1...text.count {
+ palette.query = String(text.prefix(n))
+ Snapshot.render(PaletteController.chrome(PaletteView(model: palette)), size: PaletteController.size, dark: true,
+ background: .clear, to: out("palette-\(q + 1)-\(n)"))
+ }
+ }
+ // Notch ao passar o mouse: próxima reunião e atalhos
+ let hm = NotchHoverModel()
+ hm.shown = true
+ hm.meeting = Meeting(id: "demo", title: L("Alinhamento semanal"), start: Date().addingTimeInterval(14 * 60),
+ end: Date().addingTimeInterval(44 * 60), link: URL(string: "https://meet.example.com/abc"), color: .orange)
+ hm.focusEnd = Date().addingTimeInterval(18 * 60)
+ hm.after = Meeting(id: "d3", title: L("Revisão do trimestre"), start: Date().addingTimeInterval(3 * 3600),
+ end: Date().addingTimeInterval(4 * 3600), link: nil, color: .purple)
+ Snapshot.render(NotchHoverView(model: hm, notch: CGSize(width: 200, height: 37)).frame(width: 480, height: 172),
+ size: NSSize(width: 480, height: 172), dark: true, background: .clear, to: out("notch"))
+ print("Video frames saved to \(dir.path)")
+ exit(0)
+ }
+ }
+}
diff --git a/scripts/make-video.swift b/scripts/make-video.swift
new file mode 100644
index 0000000..5899c6a
--- /dev/null
+++ b/scripts/make-video.swift
@@ -0,0 +1,176 @@
+// Monta o vídeo de divulgação a partir dos quadros de `Menew --video-frames `.
+//
+// build/Menew.app/Contents/MacOS/Menew --video-frames /tmp/frames -language en
+// swift scripts/make-video.swift /tmp/frames /tmp/video-out
+// ffmpeg -framerate 30 -i /tmp/video-out/%04d.jpg -c:v libx264 -crf 26 -pix_fmt yuv420p -movflags +faststart menew.mp4
+//
+// Sem texto gravado na imagem (só atalhos e o endereço), para servir nos 5 idiomas do site.
+import AppKit
+
+let args = CommandLine.arguments
+guard args.count > 2 else { print("usage: make-video.swift "); exit(1) }
+let frames = URL(fileURLWithPath: args[1]), out = URL(fileURLWithPath: args[2])
+let repo = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent()
+try? FileManager.default.removeItem(at: out)
+try! FileManager.default.createDirectory(at: out, withIntermediateDirectories: true)
+
+let W: CGFloat = 1200, H: CGFloat = 900, fps = 30.0
+let barH: CGFloat = 36
+
+func img(_ name: String) -> NSImage { NSImage(contentsOf: frames.appendingPathComponent(name + ".png"))! }
+let statusbar = NSImage(contentsOf: repo.appendingPathComponent("docs/assets/statusbar-all.png"))!
+let icon = NSImage(contentsOf: repo.appendingPathComponent("docs/assets/icon.png"))!
+
+// Itens na barra: o Menew à esquerda do grupo, depois CPU (posições na imagem statusbar-all, 632×96)
+let sbH: CGFloat = 28, sbW = 632 * sbH / 96
+let sbX = W - sbW - 110
+let menewItem = CGPoint(x: sbX + 30 * sbH / 96, y: H - barH / 2)
+let cpuItem = CGPoint(x: sbX + 180 * sbH / 96, y: H - barH / 2)
+let notchPoint = CGPoint(x: W / 2, y: H - 14)
+let popScale: CGFloat = 1.3
+let popW = 400 * popScale
+func under(_ x: CGFloat) -> CGFloat { min(max(x - popW / 2, 20), W - 20 - popW) }
+let popTopLeft = CGPoint(x: under(menewItem.x), y: H - barH - 6)
+
+// MARK: Linha do tempo (segundos)
+
+func ease(_ t: Double) -> Double { t < 0.5 ? 2 * t * t : 1 - pow(-2 * t + 2, 2) / 2 }
+func clamp(_ x: Double) -> Double { min(max(x, 0), 1) }
+/// 0→1 entre `a` e `a+d`, 1→0 entre `b` e `b+d`
+func window(_ t: Double, _ a: Double, _ b: Double, _ d: Double = 0.25) -> Double { min(clamp((t - a) / d), 1 - clamp((t - b) / d)) }
+
+struct Move { let t0, t1: Double; let to: CGPoint }
+let start = CGPoint(x: 560, y: 420)
+let moves = [
+ Move(t0: 0.2, t1: 1.0, to: menewItem),
+ Move(t0: 2.6, t1: 3.1, to: CGPoint(x: popTopLeft.x + 0.37 * 520, y: H - barH - 8 - 0.215 * 780)), // aba Clipboard
+ Move(t0: 6.2, t1: 6.7, to: CGPoint(x: popTopLeft.x + 0.6 * 520, y: H - barH - 8 - 0.215 * 780)), // aba Actions
+ Move(t0: 8.6, t1: 9.2, to: CGPoint(x: 420, y: 300)),
+ Move(t0: 16.2, t1: 16.9, to: cpuItem),
+ Move(t0: 19.6, t1: 20.3, to: notchPoint),
+ Move(t0: 23.4, t1: 24.0, to: CGPoint(x: 700, y: 330)),
+]
+func cursor(_ t: Double) -> CGPoint {
+ var p = start
+ for m in moves {
+ if t >= m.t1 { p = m.to; continue }
+ if t > m.t0 { let k = ease((t - m.t0) / (m.t1 - m.t0)); return CGPoint(x: p.x + (m.to.x - p.x) * k, y: p.y + (m.to.y - p.y) * k) }
+ break
+ }
+ return p
+}
+
+let total = 27.0
+func typed(_ t: Double, from a: Double, count: Int, step: Double = 0.12) -> Int { max(0, min(count, Int((t - a) / step) + 1)) }
+
+// MARK: Desenho
+
+func draw(_ i: NSImage, center: CGPoint? = nil, topLeft: CGPoint? = nil, scale: CGFloat, alpha: Double, radius: CGFloat = 16, shadow: Bool = true) {
+ guard alpha > 0.001 else { return }
+ let rep = i.representations[0]
+ let w = CGFloat(rep.pixelsWide) / 2 * scale, h = CGFloat(rep.pixelsHigh) / 2 * scale
+ let r: NSRect = if let c = center { NSRect(x: c.x - w / 2, y: c.y - h / 2, width: w, height: h) }
+ else { NSRect(x: topLeft!.x, y: topLeft!.y - h, width: w, height: h) }
+ NSGraphicsContext.saveGraphicsState()
+ if shadow {
+ let s = NSShadow(); s.shadowBlurRadius = 28; s.shadowOffset = NSSize(width: 0, height: -10)
+ s.shadowColor = NSColor.black.withAlphaComponent(0.4 * alpha); s.set()
+ NSColor.black.withAlphaComponent(alpha).setFill(); NSBezierPath(roundedRect: r.insetBy(dx: 2, dy: 2), xRadius: radius, yRadius: radius).fill()
+ }
+ NSGraphicsContext.restoreGraphicsState()
+ NSGraphicsContext.saveGraphicsState()
+ NSBezierPath(roundedRect: r, xRadius: radius, yRadius: radius).addClip()
+ i.draw(in: r, from: .zero, operation: .sourceOver, fraction: alpha)
+ NSGraphicsContext.restoreGraphicsState()
+}
+
+func keycap(_ text: String, alpha: Double) {
+ guard alpha > 0.001 else { return }
+ let a = NSAttributedString(string: text, attributes: [.font: NSFont.systemFont(ofSize: 34, weight: .semibold),
+ .foregroundColor: NSColor.white.withAlphaComponent(alpha)])
+ let s = a.size(), r = NSRect(x: W / 2 - s.width / 2 - 22, y: 110, width: s.width + 44, height: s.height + 20)
+ NSColor.black.withAlphaComponent(0.55 * alpha).setFill()
+ NSBezierPath(roundedRect: r, xRadius: 14, yRadius: 14).fill()
+ a.draw(at: NSPoint(x: r.minX + 22, y: r.minY + 10))
+}
+
+let background: NSImage = {
+ let im = NSImage(size: NSSize(width: W, height: H))
+ im.lockFocus()
+ NSGradient(colors: [NSColor(srgbRed: 0.20, green: 0.13, blue: 0.45, alpha: 1), NSColor(srgbRed: 0.49, green: 0.36, blue: 1.0, alpha: 1),
+ NSColor(srgbRed: 0.93, green: 0.55, blue: 0.75, alpha: 1)])!.draw(in: NSRect(x: 0, y: 0, width: W, height: H), angle: -35)
+ NSColor(white: 0.93, alpha: 0.92).setFill(); NSRect(x: 0, y: H - barH, width: W, height: barH).fill()
+ NSAttributedString(string: " Finder File Edit View Go Window Help",
+ attributes: [.font: NSFont.systemFont(ofSize: 15, weight: .medium), .foregroundColor: NSColor.black])
+ .draw(at: NSPoint(x: 14, y: H - barH + 9))
+ statusbar.draw(in: NSRect(x: sbX, y: H - barH + (barH - sbH) / 2, width: sbW, height: sbH))
+ NSAttributedString(string: "Thu 9:41", attributes: [.font: NSFont.systemFont(ofSize: 15, weight: .medium), .foregroundColor: NSColor.black])
+ .draw(at: NSPoint(x: W - 84, y: H - barH + 9))
+ NSColor.black.setFill() // notch
+ NSBezierPath(roundedRect: NSRect(x: W / 2 - 100, y: H - 34, width: 200, height: 46), xRadius: 14, yRadius: 14).fill()
+ im.unlockFocus()
+ return im
+}()
+let arrow = NSCursor.arrow.image
+
+
+let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(W), pixelsHigh: Int(H), bitsPerSample: 8, samplesPerPixel: 4,
+ hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)!
+let cache = NSCache()
+func cached(_ n: String) -> NSImage { if let i = cache.object(forKey: n as NSString) { return i }; let i = img(n); cache.setObject(i, forKey: n as NSString); return i }
+
+for f in 0.. 0 {
+ let name: String
+ if t < 3.2 { name = "popover-now" }
+ else if t < 6.8 {
+ let n = typed(t, from: 3.9, count: 6, step: 0.14)
+ name = t < 3.9 ? "popover-clipboard" : "clip-search-\(n)"
+ } else { name = "popover-actions" }
+ draw(cached(name), topLeft: popTopLeft, scale: popScale, alpha: popA, radius: 14)
+ }
+ // 2. Palette: ⌃⌥Space, "5 km to mi", depois "timer 10m tea"
+ keycap("⌃ ⌥ Space", alpha: window(t, 9.2, 10.6, 0.2))
+ let palA = window(t, 9.6, 15.9)
+ if palA > 0 {
+ let name: String
+ if t < 10.4 { name = "palette-0-0" }
+ else if t < 13.0 { name = "palette-1-\(typed(t, from: 10.4, count: 10, step: 0.11))" }
+ else if t < 13.3 { name = "palette-0-0" }
+ else { name = "palette-2-\(typed(t, from: 13.3, count: 13, step: 0.11))" }
+ draw(cached(name), center: CGPoint(x: W / 2, y: H / 2 - 10), scale: 1.35, alpha: palA, radius: 18)
+ }
+ // 3. CPU na barra
+ draw(cached("popover-system"), topLeft: CGPoint(x: under(cpuItem.x), y: H - barH - 6), scale: popScale, alpha: window(t, 17.0, 19.5), radius: 14)
+ // 4. Notch
+ draw(cached("notch"), center: CGPoint(x: W / 2, y: H - 172 * 0.95), scale: 1.9, alpha: window(t, 20.4, 23.3), radius: 30, shadow: false)
+ // 5. Fecho: ícone, nome e endereço
+ let endA = clamp((t - 24.0) / 0.5)
+ if endA > 0 {
+ NSColor.black.withAlphaComponent(0.35 * endA).setFill(); NSRect(x: 0, y: 0, width: W, height: H - barH).fill()
+ icon.draw(in: NSRect(x: W / 2 - 80, y: H / 2 - 10, width: 160, height: 160), from: .zero, operation: .sourceOver, fraction: endA)
+ let p = NSMutableParagraphStyle(); p.alignment = .center
+ NSAttributedString(string: "Menew", attributes: [.font: NSFont.systemFont(ofSize: 64, weight: .bold), .paragraphStyle: p,
+ .foregroundColor: NSColor.white.withAlphaComponent(endA)])
+ .draw(in: NSRect(x: 0, y: H / 2 - 100, width: W, height: 80))
+ NSAttributedString(string: "menew.web.app", attributes: [.font: NSFont.systemFont(ofSize: 30, weight: .medium), .paragraphStyle: p,
+ .foregroundColor: NSColor.white.withAlphaComponent(0.85 * endA)])
+ .draw(in: NSRect(x: 0, y: H / 2 - 150, width: W, height: 40))
+ }
+ // Ponteiro (some no fecho)
+ if t < 24.0 {
+ let c = cursor(t), s = arrow.size
+ arrow.draw(in: NSRect(x: c.x - 4, y: c.y - s.height * 1.5 + 4, width: s.width * 1.5, height: s.height * 1.5))
+ }
+ NSGraphicsContext.restoreGraphicsState()
+ let jpg = rep.representation(using: .jpeg, properties: [.compressionFactor: 0.92])!
+ try! jpg.write(to: out.appendingPathComponent(String(format: "%04d.jpg", f)))
+}
+print("\(Int(total * fps)) frames in \(out.path)")
From 01e407e37f2dcc600fcdda93f931f53fe0bfce10 Mon Sep 17 00:00:00 2001
From: Matheus Quost <19558461+Quost@users.noreply.github.com>
Date: Thu, 12 Nov 2026 11:30:44 -0300
Subject: [PATCH 2/2] feat(site): add a 30-second demo video to the website and
README
Co-Authored-By: Claude Opus 5.5
---
README.md | 2 ++
docs/assets/menew-demo-poster.jpg | Bin 0 -> 44670 bytes
docs/assets/menew-demo.mp4 | Bin 0 -> 750905 bytes
docs/assets/menew-demo.webm | Bin 0 -> 610773 bytes
docs/de/index.html | 16 +++++++++++++++-
docs/es/index.html | 16 +++++++++++++++-
docs/index.html | 16 +++++++++++++++-
docs/llms.txt | 2 +-
docs/pt/index.html | 16 +++++++++++++++-
docs/sitemap.xml | 24 ++++++++++++------------
docs/vs/cleanshot/index.html | 2 +-
docs/vs/istat-menus/index.html | 2 +-
docs/vs/maccy/index.html | 2 +-
docs/vs/otter/index.html | 2 +-
docs/vs/raycast/index.html | 2 +-
docs/vs/rectangle/index.html | 2 +-
docs/zh/index.html | 16 +++++++++++++++-
site/i18n/de.json | 5 ++++-
site/i18n/en.json | 5 ++++-
site/i18n/es.json | 5 ++++-
site/i18n/pt.json | 5 ++++-
site/i18n/zh.json | 5 ++++-
site/template.html | 14 ++++++++++++++
23 files changed, 130 insertions(+), 29 deletions(-)
create mode 100644 docs/assets/menew-demo-poster.jpg
create mode 100644 docs/assets/menew-demo.mp4
create mode 100644 docs/assets/menew-demo.webm
diff --git a/README.md b/README.md
index a597af8..9487e7c 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,8 @@
+▶ Watch the 30-second demo
+
## Highlights
### 🧠 Memory — ⌃⌥⌘K
diff --git a/docs/assets/menew-demo-poster.jpg b/docs/assets/menew-demo-poster.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3cc6009c2a964383e7c56c9fe4f2ef5905b6d3dd
GIT binary patch
literal 44670
zcmc$`cU)6V*C@J?2T)Y7fD|bzA_N4a_o6<41PKtDQdOi&?;Q&ksz3lKkrF}(Aw+r?
zMd?Key>}!ODbhhWJAlvkzTbD=d+xpeoUOk-vuD<ln
zuo3L|x1M8El+-k5fMXPtAk?YjfBi)bmLP@4D9`?N?y|Np)%lwbof)n?6=~PWgNwQl
z8Ka`b(sa{fE^yt-|4fZ|H1=$K>FVt-uIA#;-D{TrJ_`<|1gNQLDJcIsB!c4DSxVu*
zE}uIu;;fza@Ma!Ey9-<=>hqPQr_XMsM~_p9GCmqxCgzKA?SBH!9;bZq*9S_9E5Na{
z0OeU=zaKaS5(8p53n&5$qZFl-C;7^2>v1L+ZfJ5QaeNim$3d+q(nL7zKL(GLRO<{^
z>qes*x-Rw=BZ}?qEY_c{1+838xv7njS+mwgVCyna9XHW9w_F6uKSbSJB-_HAh+&Pv
zL(jw(w&{d~5z0!e#wo=(sD(r@QEb>|WhwI>B)fy%T?E``P4+7Co3h0OG%*_ocN-vP
zV%X`7icEXvDWzB;dKgm~QkyuJ2O_mIuHLHXNpI^Y#V;0PBAV^v8tRR~HUD!ttFUAN
zCLYc?yPPqjgzmHxFt6p8VTQ3&FugP)_LhEorewLDLy=o4#hTJD*&PnCtTw!%u8FmZ
zcGwT*5jd=Ay^8R4wF%%9ifNf8HpdCD$&`tPOLfJ(AbsY(MqKHqoTZrdoRV>~>1=fR
z>be(FbSW{Qb