makepad/tools/makepad_builder/makepad-windows-preview.sh
Admin c27ee1dccb tools, ci, docs, workspace: the Builder ships as source on Windows and is one shell script on macOS and Linux, the CI box, makepad-agents, cargo-makepad, and one workspace for every crate
The Makepad Builder (tools/makepad_builder: the Windows download is makepad-builder.zip with the Builder's source; macOS and Linux run makepad.sh), the CI runner (tools/ci), cargo-makepad, the agents tool, the root workspace and the documents.

Covers work 6f1e44649..2b1a41df4 outside platform, draw, widgets, libs, apps and examples:

- wm: the Android super-app compiles tiles on the phone against the host engine
- cargo-makepad: the super-app APK is packed by `android dyn-pack`, in Rust
- widgets, storybook: the widget catalogue app, some eighty new widgets, a theme store with mixable style sheets, and the rule that a press belongs to
- wm, widgets: the style tween starts with all eight weights, and the new style sheet takes the last number instead of macOS's
- platform: the Android desk swipes at 120 Hz — offscreen render passes and framebuffers cached, SVG meshes kept, font families that complete
- cef: a page's audio can be captured instead of played
- Button: activate from the keyboard when focused (#1241)
- libs/ai: exclude hub_ui and services from the AI workspace (#1242)
- flow: the flow-ui app and the flowgraph canvas return to the workspace, with staged progress in libs/flow
- svg: a stroke that bends tighter than its half width collapses its inner edge to the corner
- the accent a name is written with stops deciding whether search finds it
- macOS: keep the paint clock beating while the window is minimized (#1244)
- Linux: only link libdrm for the direct backend (#1245)
- cargo-makepad: find dependency crate dirs via cargo metadata (#1246)
- app_main!: only ship the fonts an app declares (#1247)
- ai stems: LaneDemixer, VocalsModel and the platform Stage origin/main needs
- platform, widgets, wm: every Vulkan pass renders through a negative-height viewport again, so a Wayland or X11 window stands upright, and no capture
- calendar: its own colour contrast is named explicitly, now that the theme store exports one under the same name through the widgets glob
- widgets: the supersampled resolve and the map's shadow mask sample their textures as stored, like every other render texture
- platform: every OpenGL texture pass stores top-left rows, custom cameras included, and culls with the winding its inverted projection gives
- platform, wm, director: a hosted app's software frame arrives as a top-left texture and the window manager and the director carry no flip term at al
- widgets, platform: the last twelve warnings go
- The Builder compiles itself again from the Makepad tree Scope's release pins and removes the source snapshots nothing is built from any more
- The regex engine accepts \b and \B, reports the earliest match so a scan can resume after it, and can decide word boundaries on ASCII so a code sear
- workspace: Scope's settings carry the code map's tab width and the directories each prepared project hides from its map
- AGENTS.md: a platform change made for one application needs the user's feedback and checks first, and app specifics never leak into the shared layer
- wm: the desktop style menu lists each style once
- cef, video, platform: what Stage's browser needs from the shared layers
- macOS: re-arm the display links when a window leaves the Dock (#1251)
- platform, wm: work builds again on iOS, tvOS and wasm, and the window manager is warning-free on Windows
- ci: tools/ci, a Splash runtime that watches a branch and runs every ci.splash it finds
- stitch: the crate says what the rest of the repository says about its license
- ci: a run is one target dir and one cargo batch per target, and the wall draws
- ci: the wall shows THIS run and is fit for an OLED
- ci: untested is grey and nothing else
- ci: warming fills the cache and blames nobody
- Vulkan: stop printing the loader's startup narration by default (#1252)
- ci: the terminal gets its pty helper, director is a desktop tool, and a refused input is asked again
- platform: no warnings on tvOS and linux_direct, and a missing pty helper says so
- apps: what the CI box found
- ScrollBar: add `show_handle` for a view that scrolls without a grabbable bar (#1254)
- cargo-makepad: keep the android SDK at a stable path (#1253)
- ci: the window is a dashboard
- Remove Flow, asset and VJ applications moved into Stage
- ci: a stop or a restart is not a test result
- ci: a quiet header, a build that visibly moves, and scripts that leave the machine alone
- widgets: children a lookup discovers reach the dump, the snapshot and the flood searches
- tests: the workspace suite, run as a whole for the first time, passes outside the example UI tests
- platform: a hidden window wears no hands-off frame
- examples: the UI tests pass, for the reasons they failed
- ci: a library's warnings turn the workspace block yellow, and a desktop tool is not warmed for the web
- libs: no warnings in the workspace check on any row
- ci: the workspace script gives the wall back after warming
- ci: a judgement is never lost to the way it was phrased, and every "retry" of the bridge is retried
- tests: the last three failures of the CI box's night
- Linux: fix window chrome button hovers and how maximized/fullscreen windows work (#1255)
- ci: the header names the tip being tested
- tools: makepad-screen is makepad-agents, and its binary is `agents`
- gif: the crate's doc examples compile
- ci: the tests are the platform's own, in release, in two minutes
- Tooltip: position anchored tooltips in the same draw (#1256)
- cef: a hosted browser survives its host's exit
- ci: a card is as high as its content
- tests: the three binaries over ten seconds in release come under it
- widgets: let the host install script mods into every Splash isolate
- widgets: a pooled test context forgets the last case's Escape claim
- widgets: the pooled test context's resets replace the globals
- wm: the shell menu says what it did, and the CI waits for that
- platform: the storage module says how much a volume has free
- git: the memory ledger can take what it is asked for
- ci: a driven app takes every key once, and the storage module builds for the browser
- RadioButton: fix its touch hover state and click-off behavior (#1258)
- HtmlLink: fix its hover and pressed states (#1257)
- RadioButton: take key focus on the click, not on the press (#1259)
- script: re-entrant dispatch, thread index validation, Any-based handle downcasts, UTF-8 previews; regex: never_loop fix
- script: port Octoscript WS1 — worklist equality with fuel/deadline/work bail, uncaught-error bail, allow_debug_output
- script: port Octoscript parser/tokenizer/control-flow VM patches (ws2)
- script: port Octoscript heap/string/array/object hardening onto upstream's allocation budget
- script: port orphaned Octoscript VM hardening (execution caps, clear_type_methods, parser diagnostics)
- script: keep silenced streaming evals running past uncaught errors
- script: the Octoscript hardening keeps a Makepad host's semantics and its speed
- fix(text): enable ttf-parser gvar-alloc for many-tuple variable fonts
- feat(text): CoreText outline fallback for hvgl-only fonts (macOS)
- draw text: the CoreText outline fallback is only ever resolved for a face whose outlines live solely in hvgl, and font-family diagnostics are the `f
- mail: the phone layout lines up
- clock: the phone layout fits both orientations
- calendar, reminders, calculator, sheets: the phone layouts fit their space
- platform, widgets, wm: a touch that is taken away is cancelled, never released
- weather, notes, finance, photos, route: the phone layouts tidy up
- files: the phone layout tidies up
- wm: the phone shell's surfaces match the app and the grid
- wm on Android: every app is its own process
- clock: a cancelled release neither opens an alarm nor switches tab
- platform, wm: hosted apps ask the WM for what they cannot do themselves
- wm on Android: apps build on the phone
- wm on Android: app transitions and gestures feel like the phone's
- android: rotated Vulkan windows stop rebuilding their swapchain every frame, and hosted children get touch
- vulkan (android): a window released on suspend is not released again
- android: hosted children hand frames over with GPU fences and draw only when they have work
- wm on Android: gestures and surfaces settle like the phone's
- widgets: touch lists scroll with Android's physics
- wm on Android: the shell follows the Pixel launcher's motion
- wm on Android: Recents shows the apps over a receding home, and the home screen switches looks
- wm on Android: the look switcher stays put, the iOS dock shows its icons, background apps follow a look change
- task, cargo-makepad, widgets, audio: the task manager chooses its columns, graphs every process' network and disk traffic and installs itself as a D
- agents: a user touching an app under test no longer stops the agent
- flowgraph: ordered ports take many wires, sockets can override their icon, and the canvas embeds as a viewport
- cef: captured frames and audio carry callback clocks and a navigation epoch
- git: timings read a portable clock
- ai: the hub's rig-fixture tests find the asset library without the asset client
- ai llm: a Metal main buffer is filled in 32 MB chunks
- diskmap: a scan lists each macOS directory in one getattrlistbulk call and classifies files without allocating or locking
- files: the tile view's three projections are toolbar buttons
- mail: the local view takes a theme-derived palette and line icons, search pages share records
- director: agent lanes form a tree and Grok joins the usage bar
- widgets, draw: skeuomorphic surfaces light each other through an optional relief buffer
- widgets, platform: a texture can light the relief buffer, menus take colour chips, and a style reload recompiles changed shader functions
- widgets: relief surfaces can travel like mechanical keys, and dark surfaces take less neighbour light
- platform, widgets, audio_route, ai: window crossfades and whole-frame presents, caption controls that click, themed menus, a stereo-pair audio tap,
- builder: the Builder TUI matches the new terminal design, Windows executables carry app icons, and every app has one
- platform: Android builds without Vulkan compile again, and the font-selection test counts the hosted entry point
- platform: the pipeline-skip repaint mark exists only on Apple, where Metal reads it
- builder: the terminal scripts delete nothing and read top to bottom, and every Builder delete stays inside its own folder
- files: the Files app deletes nothing and never overwrites
- director: coding agents ask before acting unless you choose otherwise
- sheets, score, git, home: saves cannot cut a file short or overwrite another one, and checkouts cannot write outside the repository
- git: worktree status matches git on real repositories and can be cancelled
- builder: one forward-only bar per setup component, green checks on finished setup rows, a black Builder window and a smaller Windows ZIP
- builder: the GPU notice is remembered
- cuda: builds link only the CUDA toolkit they are given, never a system install
- builder: a refused email opens the editor again with what was typed and says why, and the app section is YOUR APPS
- builder: a refused email says "Email not recognised" and keeps what was typed for fixing
- platform (windows): every window keeps animating with several windows open
- builder previews: long work gets a page of its own (steps as a checklist, the current one carrying its bar, footer working · ctrl+c stops), Account
- builder: long work runs on its own page
- builder: a first build resolves online, the Rust row and bars move when the work does, and the window fits the TUI
- audio_route: one API on macOS, Windows and Linux, monitoring by default and processing only when asked, plus shaders that discard a value compile to
- platform, widgets, ai: a restyle recompiles nothing it already has, shows only complete frames, and a window can own its caption
- builder: Compile shows a real bar
- builder: the compile line is the bar and n / total crates
- builder: apps get the icon their package declares
- builder: no Scope command
- widgets: relief surfaces can swing their light toward a point
- builder: a HEAD request is a HEAD
- builder: the menu waits for a choice
- builder: no menu flash after log-in
- builder: wait out Windows security on fresh Rust
- builder: the menu opens on the first of YOUR APPS
- platform (windows): half-float RGBA textures have four channels
- builder: CUDA whenever the machine has NVIDIA
- audio_route example: print the samples that reached the processor each second
- widgets, builder: the big widget families are features, all on by default, and the Builder takes none of them
- platform (macos): the display link is kept until it is invalidated
- ai hub: a local chat with no model says so once, plainly
- builder: the Windows exe is built for size
- windows: desktop apps open no console window, and still speak through pipes
- ai: Claude Desktop drives any app with the F10 panel
- Constrain Splash external I/O to the host service bridge (#1243)
- PortalList: stop following the end when scrolling to an earlier item (#1261)
- wm: starts on Windows
- ci: a full disk empties the build output before the run, not every row after it
- builder: an incremental compile's bar follows what really compiles
- builder: the compile bar's total is the app's own crates
- builder: menus wrap around
- wm: children open no console windows on Windows
- wm: the whole deck out of a Builder's source, compiled only when the person opens an app
- builder: CUDA crates build in an installation whose path has a space
- ai: Connect hands the .mcpb to Claude Desktop itself
- wm: the clock and calendar know the date on Windows
- platform (windows): a popup opened for the first time draws its rows
- wm: opening an app rebuilds it when its binary is out of date; warm instances never build
- builder: an app edited after its build shows as needing a compile
- civil-time: one local wall clock for every app, and it knows the zone on Windows
- platform + wm: a hosted child redraws at its tile's new size
- platform (windows): a texture pass drawn before its window has a size is skipped, not a crash
- draw: glass of a view that stopped drawing leaves the screen
- calendar: the Calendars sheet is an opaque panel, and the wide layout does not float one over its sidebar
- build: dev keeps line tables only, release is incremental without LTO, the parallel frontend is a documented local opt-in
- cargo-makepad: a binary with several app_main! entry points bundles
- script: #[derive(Script)] emits one helper call per field, and ScriptNew's default methods keep their bodies out of every type
- widgets: the Widget lookup methods and with_script_vm_id are compiled once, not once per widget type
- platform: studio-protocol no longer waits for script, so platform starts ~0.5 s earlier
- ai-speech: makepad-ai-sfx (and with it ai-h3) is built only for IndexTTS
- platform: an app's package dir, icons and bundle name rebuild only that app
- script + wasm_bridge: no build script that reruns on every file
- build: a private app cloned into apps/<name> joins this workspace, Stage first
- build: the AI stack, csg, Scope and Source Library join the one workspace
- builder: every app builds in the one Makepad workspace, one target directory per source snapshot
- ai chat: the chat's backends are the CLI, MCP and cloud providers; the local model is the `localai` feature
- apps: every app that hosts the chat has a `localai` feature for the local model, off unless the app's own job runs one
- builder: time each install component and phase
- builder: Build tools, Windows SDK, Rust and CUDA install side by side, each with its own bar
- builder bootstrap: rustc, rust-std and cargo download and unpack side by side under one Rust bar
- widgets: the library moves to makepad-widgets-core in widgets/core, and makepad-widgets becomes its front crate
- widgets: the Window, widget tree and panel theme reach the tweaker, voice, AI slot and dock through hooks, not through their modules
- widgets: every widget family is a crate of its own under widgets/families, and makepad-widgets links, re-exports and registers the ones its features
- aichat, widgets: the chat reads the design feedback through the tweaker's hook, so linking the chat no longer needs the tweaker
- apps, examples, libs: each crate builds only the widget families it uses, and the design overlay is each app's own default feature
- builder: an app release builds without the app's development defaults, and the catalog names every feature a release ships with
- widgets: the build scripts rerun on their own edits only, and makepad-widgets no longer reads MAKEPAD
- livepipe: takes makepad-widgets without its default families, now that the AI stack is in the one workspace
- builder: the catalog names the new localai defaults of route and ai-hub
- feedback: Send feedback, a caption icon and a small panel that shows exactly what is sent
- builder: an app it launches knows who the person is, for Send feedback
- feedback: the panel's wording reads right and draws in any font
- feedback: takes makepad-widgets without its default families
- builder: clear build data deletes target/ itself and says why when it cannot
- ai chat: a build without local AI starts on the first provider that answers here, not on "No model"
- platform: MAKEPAD=gpusim builds again
- cargo-makepad: a Windows desktop build links the app's icon into its own binary, not into RUSTFLAGS
- thiserror: RUSTC_BOOTSTRAP no longer turns on the unstable generic member access API
- builder: every end-user build uses rustc's parallel frontend, and falls back to one thread when it fails
- builder: the Windows SDK downloads the 25 cabinets its MSIs name instead of all 149, no screen or log shows a \\?\ path, long status messages wrap,
- builder: CUDA kernels compile in any install folder, and when they cannot the app is built without them
- builder: makepad-builder.exe declares itself like a well-formed Windows program and stops a stalled build through a job object instead of taskkill
- platform (macos): a contained panic no longer frees the windows under AppKit or leaves an app that ignores quit
- builder: macOS and Linux run the Builder as one shell script, the same TUI as Windows, and nothing is compiled for it
- builder + ai-cuda: the CUDA kernel progress shows in the Builder's Compile row, not scribbled over its screen
- builder: an app built with CUDA starts on Windows
- builder (windows): the downloaded exe is built large again
- builder (unix): the email check takes real addresses
- repo: no third-party comic archive, generated models or sample asset in the tree
- remove big example media files
- cargo_makepad: no stray KNMI radar frame in the Android Java sources
- widgets: the empty parts of an app's caption bar drag the window again
- ScrollBar: fade out when idle, like macOS overlay scrollers (#1264)
- platform: every app builds the same `windows` crate
- wm: in a Makepad Builder installation apps build through the Builder, however wm was started
- platform, draw: web builds start again
- platform: web text draws again
- widgets: the keyboard reaches boxes and modals
- widgets: a hover tooltip never sits under the mouse cursor
- platform: clipboard_read, reading text or an image from the system clipboard
- feedback: Send clipboard, and the dialog works from the keyboard
- builder: the Windows Builder ships as source, runs in its own console, and apps compile with Rust's GNU toolchain or Microsoft's
- platform: an app also finds its resource map in the builder folder beside it
- win_resource: PNG decode and encode on makepad-fast-inflate instead of zune-png
- builder: one folder layout everywhere, makepad-builder.zip with only the files a Windows build reads, a Rust download over parallel connections, and
- builder (windows): the .bat runs from a folder with spaces and parentheses, and the ZIP always carries it with CRLF
- builder: coding agents start from the macOS/Linux Builder, its compile bar has its end, the app rows say where each app is in a word or two, and Win
- builder (unix): running curl | sh again opens the existing installation with its saved email
- builder: log out from the Account row
- builder: `makepad-builder build APP` compiles an app offline exactly as the Builder does

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 20:11:31 +02:00

932 lines
39 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# PREVIEW of the Windows Builder TUI (makepad-builder.exe), as a shell script
# so it runs here. The real one is Rust (tools/makepad_builder/src/tui.rs);
# this is its visual spec. Everything is simulated: nothing is downloaded,
# installed or written; the License agreements page really opens links.
#
# Windows differences from macOS: the folder is wherever the ZIP was unzipped
# (portable, no question), setup installs the Visual Studio Build Tools,
# Windows SDK and Rust itself (never a system Rust), CUDA is optional for
# NVIDIA GPUs (AI features in Makepad Amp), and apps are .exe files you pin
# to the taskbar.
#
# sh makepad-windows-preview.sh first run
# sh makepad-windows-preview.sh --installed an existing installation
# sh makepad-windows-preview.sh --resume an earlier run stopped mid-download
# sh makepad-windows-preview.sh --no-nvidia a PC without an NVIDIA GPU
# sh makepad-windows-preview.sh --free an email without licenses
set -eu
demo=first demo_rust=none demo_free=no email= command= nvidia=yes
while [ $# -gt 0 ]; do
case "$1" in
--installed) demo=installed email=rik2@n4.io ;;
--resume) demo=resume email=rik2@n4.io ;;
--no-nvidia) nvidia=no ;;
--free) demo_free=yes ;;
--email) shift; email=$1 ;;
-h|--help) sed -n '2,17p' "$0"; exit 0 ;;
build|run|list|env|agent-context) command=$1; shift; break ;;
*) printf 'unknown option %s\n' "$1" >&2; exit 1 ;;
esac
shift
done
# ------------------------------------------------------------------ model ---
# App states, all derived from the folder on every start:
# new nothing downloaded -> download, compile, run
# partial a download was interrupted -> resume, compile, run
# compile source is newer than the build -> compile, run
# ready built from the current source -> run
# The live version reads these markers:
# makepad.json the account email
# selected-rust "local" or the system sysroot
# toolchain/rust/<v>/.ready local Rust completely unpacked
# downloads/<repo>.part an interrupted download (curl -C - resumes)
# sources/<repo>/.verified source checked against the catalog sha256
# <Title>.app/.../build-source the source commit the bundle was built from
# .lock/pid another run is active (stale ones cleared)
root="C:\\Users\\$USER\\Downloads\\makepad-commercial"
cuda=none cuda_pin=12.8 gpu=ok
rust_pin=1.92.0
system_rust=1.93.1
[ "$demo_rust" = found ] || system_rust=
licensed=unknown
checked=
message=
get() { eval "printf '%s' \"\${st_$1:-new}\""; }
put() { eval "st_$1=\$2"; }
# Licensed apps come from the VPS catalog: id|title|license|megabytes|source
fetch_licenses() {
if [ "$demo_free" = yes ]; then licenses=; else licenses='scope|Makepad Scope|commercial|214|sources/scope
amp|Makepad Amp|beta|188|sources/stage/apps/amp'; fi
}
# The free apps in the public Makepad repository (apps.json in the live one).
free_apps='wm|Makepad WM
mail|Mail
notes|Notes
calendar|Calendar
terminal|Terminal
files|Files
photos|Photos
sheets|Sheets
pdf|PDF
mixer|Mixer
clock|Clock
weather|Weather
calculator|Calculator
reminders|Reminders
finance|Finance
video|Video
browser|Browser
fab|Fab
director|Director
route|Route'
scan() {
rust=local
case "$demo" in
first) rust=none ;;
installed) put scope ready; put amp ready; put makepad ready; put wm ready; put mail ready ;;
resume) put scope partial ;;
esac
# Free apps share one Makepad download; built ones are ready, the rest
# compile on first run.
src=$(get makepad)
for id in $(printf '%s\n' "$free_apps" | cut -d'|' -f1); do
if [ "$src" = new ]; then put "$id" new; elif [ "$(get "$id")" != ready ]; then put "$id" compile; fi
done
}
# ------------------------------------------------------- agent interface ---
# The environment every build uses, the TUI's and an agent's alike: the
# selected compiler first on PATH, caches and output inside the folder.
print_env() {
if [ "${rust:-local}" = system ]; then bin="$HOME/.rustup/toolchains/stable-aarch64-apple-darwin/bin"; else bin="$root/toolchain/rust/$rust_pin/bin"; fi
cat <<EOF
export PATH="$bin:\$PATH"
export CARGO_HOME="$root/cargo-home"
export CARGO_TARGET_DIR="$root/target"
export RUSTUP_TOOLCHAIN= RUSTC_WRAPPER= RUSTFLAGS=
# One shared target/ lets every app reuse the crates the others compiled.
# No debug info or separate debug files; stripped binaries.
export CARGO_PROFILE_RELEASE_DEBUG=0 CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off CARGO_PROFILE_RELEASE_STRIP=true
EOF
}
# Written to AGENTS.md (and CLAUDE.md) in the folder before an agent opens,
# so Claude, Codex and Grok all read it on start.
print_agent_context() {
cat <<EOF
# Makepad apps in this folder
Everything here is Makepad applications as Rust source. The person using this
folder wants you to change these apps and rebuild them.
## Build and run
Always use the \`makepad\` command in this folder; do not call cargo directly.
It uses this folder's compiler (a private Rust $rust_pin in toolchain/, or the
system Rust recorded in selected-rust), keeps caches and build output in this
folder, and rebuilds the macOS app bundle so Dock icons keep working.
./makepad build <app> compile after you change its source
./makepad run <app> compile if needed, then open <App>.app
./makepad list every app, its source folder and state
A build prints compiler errors in full; fix them and build again. If you need
cargo itself (tests, clippy), first run: eval "\$(./makepad env)"
## Apps
| app | name | source |
|-----|------|--------|
EOF
printf '%s\n' "$licenses" | while IFS='|' read -r id title _ _ src; do
printf '| %s | %s | %s |\n' "$id" "$title" "$src"
done
printf '%s\n' "$free_apps" | while IFS='|' read -r id title; do
printf '| %s | %s | sources/makepad/apps/%s |\n' "$id" "$title" "$id"
done
cat <<EOF
Makepad itself (widgets, platform, shaders) is in sources/makepad; read its
AGENTS.md before changing it. Each repository keeps its own instructions.
## After an update
Updates never merge. When an app you changed gets a new release, the old
changes are saved first and the app is replaced by the clean new source:
changes/<app>-<date>.diff unified diff against the release you edited
changes/<app>-<date>.files one "M|A|D path" line per changed file
When asked to merge, reapply that diff onto the new source by editing files
(do not rely on git being installed), keep the intent of each change where the
new release moved code, then ./makepad build <app> until it compiles. Report
any change that no longer applies.
## Rules
- Keep edits inside sources/. Built bundles (<App>.app) are regenerated.
- Your licenses do not allow publishing the commercial sources.
EOF
}
if [ -n "$command" ]; then
fetch_licenses
rust=local
case "$command" in
env) print_env ;;
agent-context) print_agent_context ;;
list) { printf '%s\n' "$licenses" | cut -d'|' -f1,2,5; printf '%s\n' "$free_apps" | sed 's#$#|sources/makepad/apps#'; } | awk -F'|' '{ printf "%-12s %-14s %s\n", $1, $2, $3 }' ;;
build|run)
app=${1:?usage: makepad $command APP}
# Plain, line-based output for agents: every compiler message,
# then one result line they can check.
printf 'Compiling %s with Rust %s (preview: simulated)\n' "$app" "$rust_pin"
printf 'Bundled %s/%s.app\n' "$root" "$app"
[ "$command" = build ] || printf 'Opened %s.app\n' "$app" ;;
esac
exit 0
fi
# Coding agents: only what is installed on this machine.
agents=
for a in 'claude|Claude Code' 'codex|Codex' 'grok|Grok'; do
if command -v "${a%%|*}" >/dev/null 2>&1; then agents="$agents${agents:+
}$a"; fi
done
# --------------------------------------------------------------- terminal ---
exec 3<>/dev/tty || { printf 'Makepad needs an interactive terminal.\n' >&2; exit 1; }
e=$(printf '\033')
dim="$e[2m" b="$e[1m" ok="$e[32m" acc="$e[36m" warn="$e[33m" inv="$e[7m" r0="$e[0m"
tty_saved=$(stty -g <&3)
leave() {
stty "$tty_saved" <&3 2>/dev/null || :
printf '%s[?25h%s[?1049l' "$e" "$e"
}
trap leave EXIT
trap 'exit 130' INT TERM HUP
printf '%s[?1049h%s[?25l' "$e" "$e"
stty -icanon -echo min 1 time 0 <&3
short() { printf '%s' "$1"; }
at() { printf '%s[%d;1H%s[2K' "$e" "$1" "$e"; }
line() { printf '%s%s[K\n' "$*" "$e"; y=$((y + 1)); }
size() {
width=$(tput cols 2>/dev/null || printf 80)
height=$(tput lines 2>/dev/null || printf 24)
[ "$width" -le 80 ] || width=80
}
rule() { r=; i=4; while [ "$i" -lt "$width" ]; do r="${r}─"; i=$((i + 1)); done; printf ' %s%s%s' "$dim" "$r" "$r0"; }
key() {
# The trailing x keeps a newline (Return) distinguishable from end of
# input; a closed terminal quits instead of pressing Return forever.
k=$(dd bs=1 count=1 <&3 2>/dev/null; printf x)
k=${k%x}
case "$k" in
'') exit 1 ;;
"$e")
stty min 0 time 1 <&3
s=$(dd bs=2 count=1 <&3 2>/dev/null)
stty min 1 time 0 <&3
case "$s" in '[A'|OA) key=up ;; '[B'|OB) key=down ;; '[C'|OC) key=right ;; '[D'|OD) key=left ;; '') key=esc ;; *) key=other ;; esac ;;
'
'|' ') key=enter ;;
k) key=up ;;
j) key=down ;;
*) key=$k ;;
esac
}
# ------------------------------------------------------------------ rows ---
# A screen is a list of rows: "head|TITLE", "note|TEXT" or
# "item|id|name|license|status|action".
state_cells() { # state_cells ID -> st (status text) and act (action)
case "$(get "$1")" in
new) st="${dim}download 42 MB, compile and run$r0" act== ;;
partial) st="${warn}download stopped at 38% · resume, compile and run$r0" act== ;;
compile) st="${warn}compile and run$r0" act== ;;
ready) st="${ok}✓$r0 ready" act=run ;;
merge) st="${warn}updated · your changes to merge$r0" act='merge with agent' ;;
esac
}
license_rows() {
printf '%s\n' "$licenses" | while IFS='|' read -r id title lic mb _; do
state_cells "$id"
[ "$(get "$id")" != new ] || st="${dim}download $mb MB, compile and run$r0"
printf 'item|%s|%s|%s|%s|%s\n' "$id" "$title" "$lic" "$st" "$act"
done
}
free_rows() {
# One shared download covers every app here, so rows stay short: a mark
# when built, and the action only on the selected row.
if [ "$(get makepad)" = new ]; then
printf '%s\n' "note|" "note|${dim}One 42 MB download covers them all; each compiles on first run.$r0"
else
printf '%s\n' "note|" "note|${dim}Each one compiles the first time you run it.$r0"
fi
printf '%s\n' "$free_apps" | while IFS='|' read -r id title; do
case "$(get "$id")" in
ready) st="${ok}✓$r0 ready" act=run ;;
new) st= act='download source, compile and run' ;;
*) st= act='compile and run' ;;
esac
printf 'item|%s|%s||%s|%s\n' "$id" "$title" "$st" "$act"
done
}
agent_rows() {
if [ -n "$agents" ]; then
printf '%s\n' "$agents" | while IFS='|' read -r id title; do
printf 'item|agent-%s|%s||%s|open\n' "$id" "$title" ""
done
fi
printf 'item|agent-shell|Shell||%s|open\n' "${dim}with this folder's Rust on PATH$r0"
}
# Disk: what this folder uses. The live version sums `du -sk` of target/
# (build data), sources/ + changes/ (source), toolchain/ (Rust) and the
# built <App>.app bundles (apps); the preview derives the split from the app
# states. Clearing build data deletes target/ only: the apps keep running,
# the next compile starts from scratch.
disk_row() {
kb_build=0 kb_src=0 kb_rust=0 kb_apps=0
case "$rust" in none|installing) ;; *) kb_rust=2600000 ;; esac
for id in scope amp makepad; do
case "$(get "$id")" in new) ;; *) kb_src=$((kb_src + 240000)) ;; esac
done
for id in scope amp $(printf '%s\n' "$free_apps" | cut -d'|' -f1); do
if [ "$(get "$id")" = ready ]; then
kb_apps=$((kb_apps + 30000))
[ "${cleared:-}" = yes ] || kb_build=$((kb_build + 380000))
fi
done
total=$((kb_build + kb_src + kb_rust + kb_apps))
free_kb=$(df -k "$HOME" | awk 'NR == 2 { print $4 }')
n() { awk -v k="$1" 'BEGIN { printf "%.1f", k / 1048576 }'; }
# What the folder uses, and the part a clean build can delete (target/).
# Sources, the toolchain and built apps stay.
if [ "$kb_build" -eq 0 ]; then
printf 'item|disk|Disk||%s|\n' "$(n "$total") GB used"
return
fi
printf 'item|disk|Disk||%s|%s\n' "$(n "$total") GB used ${dim}· build $(n "$kb_build") GB$r0" 'clear build data'
}
clear_build() {
[ "${cleared:-}" != yes ] || return 0
choose "Delete the build data in $(short "$root")/target? Your apps keep working; the next compile starts from scratch." keep delete keep || return 0
[ "$chosen" = delete ] || return 0
spin 'Deleting build data' 0.6
cleared=yes
message="${ok}✓$r0 Build data deleted. Built apps are unchanged."
}
# The email lives only in this folder (makepad.json): the download carries
# none, and a reinstall into the same folder remembers it.
account_row() {
if [ -n "$email" ]; then printf '%s' "item|account|Account||${ok}✓$r0 $email|switch email"
else printf '%s' "item|account|Account||${warn}not logged in$r0|log in"; fi
}
# Put the selection on the row a step is working on, so the marker follows
# the work (installing…) instead of staying where the person last was.
select_id() {
n=$(main_rows | grep -v '^$' | grep '^item|' | grep -n "^item|$1|" | cut -d: -f1)
[ -z "$n" ] || sel=$((n - 1))
}
main_rows() {
case "$rust" in
none) rs="${warn}not installed$r0" ra=install ;;
installing) rs="${acc}installing…$r0" ra= ;;
*) rs="${ok}✓$r0 Visual Studio Build Tools, Windows SDK, Rust $rust_pin" ra=recheck ;;
esac
if [ "$nvidia" != yes ]; then cs="${dim}needs an NVIDIA GPU$r0" ca=;
elif [ "$cuda" = installing ]; then cs="${acc}installing…$r0" ca=;
elif [ "$cuda" = ready ]; then cs="${ok}✓$r0 CUDA $cuda_pin ${dim}· AI features in Makepad Amp$r0" ca=;
else cs="${dim}optional · AI features in Makepad Amp$r0" ca=install; fi
ready=0 total=0
for id in $(printf '%s\n' "$free_apps" | cut -d'|' -f1); do
total=$((total + 1))
[ "$(get "$id")" != ready ] || ready=$((ready + 1))
done
printf '%s\n' "head|SETUP" \
"$(account_row)" \
"item|gpu|Graphics||$(if [ "$gpu" = ok ]; then printf '%s' "${ok}✓$r0 driver notice read"; else printf '%s' "${warn}read the driver notice$r0"; fi)|$([ "$gpu" = ok ] || printf read)" \
"item|terms|Agreements||$(if [ "$accepted" = yes ]; then printf '%s' "${ok}✓$r0 accepted ${dim}· Makepad, Microsoft, Rust$([ "$cuda" != ready ] || printf ', NVIDIA')$r0"; else printf '%s' "${dim}Makepad, Microsoft, Rust, NVIDIA$r0"; fi)|read" \
"item|rust|Build tools||$rs|$ra" \
"item|cuda|CUDA||$cs|$ca" \
"$(disk_row)" \
"$(if [ -z "$checked" ]; then printf '%s' "item|updates|Update||${dim}check for updates$r0|="; else printf '%s' "item|updates|Update||$checked|check again"; fi)" \
"head|YOUR APPS"
case "$licensed" in
unknown) printf '%s\n' "note|${dim}checking licenses for ${email}…$r0" ;;
*) if [ -z "$email" ]; then printf '%s\n' "item|login|Log in with your email address|||to see your licenses"; elif [ -n "$licenses" ]; then license_rows; else printf '%s\n' "note|${dim}none on this email yet · buy or request beta access at makepad.nl$r0"; fi ;;
esac
printf '%s\n' "head|MAKEPAD EXPERIMENTS" \
"item|free|Experiments|free|$total · $ready ready|open list" \
"head|CODING AGENTS · each one knows how to change and rebuild these apps"
agent_rows
}
# ---------------------------------------------------------------- screen ---
draw() {
size
case "$screen" in
main) rows_out=$(main_rows | grep -v '^$'); crumb= sub='Makepad apps ship as source, so your own coding agent can customize them.' ;;
free) rows_out=$(free_rows); crumb=' › Makepad experiments' sub='From our open source repository: experiments, not finished applications.' ;;
terms) rows_out=$(terms_rows); crumb=' › License agreements' sub='Return opens an agreement in your browser.' ;;
consent) rows_out=$(consent_rows); crumb=" › $consent_title" sub='Please read what you are agreeing to.' ;;
work) rows_out=$(work_rows); crumb=" › $work_title" sub=$work_sub ;;
signin) rows_out=$(signin_rows); crumb=' › Log in' sub='Welcome to the Makepad Builder.' ;;
gpu) rows_out=$(gpu_rows); crumb=' › Graphics' sub='Before you continue.' ;;
esac
items=$(printf '%s\n' "$rows_out" | grep -c '^item|' || :)
y=0
printf '%s[H' "$e"
line
left="Makepad commercial apps$crumb"
shown_email=${email:-not logged in}
pad=$((width - 4 - ${#left} - ${#shown_email}))
[ "$pad" -gt 1 ] || pad=1
line " ${b}Makepad$r0 commercial apps$crumb$(printf '%*s' "$pad" '')$dim${email:-not logged in}$r0"
line " $dim$sub$r0"
# Long lists scroll: keep the selection inside the visible window.
room=$((height - 11))
[ "$room" -ge 3 ] || room=3
[ "$sel" -ge "$top" ] || top=$sel
[ "$sel" -lt $((top + room)) ] || top=$((sel - room + 1))
[ "$screen" != main ] || top=0
i=0 shown=0
while IFS='|' read -r kind id title lic st act; do
case "$kind" in
head) line; line " $dim$id$r0"; continue ;;
step)
case "$id" in
cur) work_row=$((y + 1)); line " ${acc}●$r0 $title" ;;
done) line " ${ok}✓$r0 $title" ;;
*) line " ${dim}○ $title$r0" ;;
esac
continue ;;
note) line " $id"; continue ;;
esac
if [ "$i" -lt "$top" ] || { [ "$screen" != main ] && [ "$shown" -ge "$room" ]; }; then i=$((i + 1)); continue; fi
[ "$shown" -gt 0 ] || [ "$screen" = main ] || [ "$screen" = consent ] || [ "$screen" = gpu ] || [ "$screen" = signin ] || line
# Pad plain text; escape codes must not count toward column widths.
name=$(printf '%-15s' "$title")
case "$lic" in
commercial) lt="$b$(printf '%-20s' 'commercial license')$r0" ;;
beta) lt="$warn$(printf '%-20s' 'beta access')$r0" ;;
free) lt="$dim$(printf '%-20s' 'free, open source')$r0" ;;
*) lt= ;;
esac
if [ "$i" = "$sel" ]; then
# act "=" means the status already says what Return does.
hint=; [ -z "$act" ] || hint=" $acc$act ⏎$r0"; [ "$act" != = ] || hint=" ${acc}⏎$r0"
# With no status the action sits in the status column itself.
[ -n "$st" ] || hint="${hint# }"
line " ${acc}›$r0 $b$name$r0 $lt$st$hint"
else
line " $name $lt$st"
fi
i=$((i + 1)) shown=$((shown + 1))
done <<ROWS
$rows_out
ROWS
if [ "$screen" != main ]; then
more=$((items - top - shown))
if [ "$more" -gt 0 ]; then line " ${dim}↓ $more more$r0"; else line; fi
fi
line
line "$(rule)"
status_row=$((y + 1))
[ "$screen" != work ] || [ -z "${work_row:-}" ] || status_row=$work_row
line " $message"
line
footer_row=$((y + 1))
if [ "$screen" = work ]; then
line " ${dim}working · ctrl+c stops$r0"
elif [ "$screen" = signin ]; then
line " ${dim}type your email ⏎ continue$r0"
elif [ "$screen" = consent ] || [ "$screen" = gpu ]; then
line " ${dim}↑↓ move ⏎ select esc cancel$r0"
elif [ "$screen" = main ]; then
line " ${dim}↑↓ move ⏎ select q quit$r0"
else
line " ${dim}↑↓ move ⏎ select esc back q quit$r0"
fi
printf '%s[J' "$e"
}
status() {
at "$status_row"
# On a work page the status line is the current step's own line.
if [ "$screen" = work ]; then printf ' %s●%s %s' "$acc" "$r0" "$*"; else printf ' %s' "$*"; fi
}
# While work runs, the key hints give way to a busy line; the next draw
# puts them back.
busy() { at "${footer_row:-$((status_row + 2))}"; printf ' %s' "${dim}working · ctrl+c stops$r0"; }
bar() { # bar LABEL FROM% TOTAL UNIT SECONDS TEXT
busy
label=$1 from=$2 total=$3 unit=$4 secs=$5 text=$6 steps=40
i=$((from * steps / 100))
while [ "$i" -le "$steps" ]; do
fill=$((i * 30 / steps)) s= t= k=0
while [ "$k" -lt 30 ]; do if [ "$k" -lt "$fill" ]; then s="${s}━"; else t="${t}─"; fi; k=$((k + 1)); done
n=$(awk -v t="$total" -v i="$i" -v n="$steps" 'BEGIN { v = t * i / n; if (t >= 100) printf "%d", v; else printf "%.1f", v }')
status "$(printf '%-12s' "$label") $acc$s$dim$t$r0 $(printf '%3d%%' $((i * 100 / steps))) $dim$n / $total $unit · $text$r0"
sleep "$(awk -v s="$secs" -v n="$steps" 'BEGIN { printf "%.3f", s / n }')"
i=$((i + 1))
done
}
spin() { # spin TEXT SECONDS
busy
i=0 n=$(awk -v s="$2" 'BEGIN { printf "%d", s * 12 }')
while [ "$i" -lt "$n" ]; do
case $((i % 10)) in 0) c=⠋ ;; 1) c=⠙ ;; 2) c=⠹ ;; 3) c=⠸ ;; 4) c=⠼ ;; 5) c=⠴ ;; 6) c=⠦ ;; 7) c=⠧ ;; 8) c=⠇ ;; *) c=⠏ ;; esac
status "$acc$c$r0 $1"
sleep 0.08
i=$((i + 1))
done
}
# choose QUESTION DEFAULT OPTION...: ←→ or a first letter picks, ⏎ accepts,
# $note, when set, is a dim remark after the options (cleared on return).
# Escape backs out.
choose() {
q=$1 pick=$2; shift 2
while :; do
o=
for opt in "$@"; do
if [ "$opt" = "$pick" ]; then o="$o $inv $opt $r0"; else o="$o $opt "; fi
done
status "$q"
at $((status_row + 1)); printf ' %s %s' "$o" "$dim${note:-}$r0"
key
case "$key" in
enter) chosen=$pick; at $((status_row + 1)); note=; return 0 ;;
esc|q) chosen=; at $((status_row + 1)); note=; return 1 ;;
left|right)
prev= next= found=
for opt in "$@"; do
if [ -n "$found" ] && [ -z "$next" ]; then next=$opt; fi
[ "$opt" != "$pick" ] || found=1
[ -n "$found" ] || prev=$opt
done
if [ "$key" = left ] && [ -n "$prev" ]; then pick=$prev; fi
if [ "$key" = right ] && [ -n "$next" ]; then pick=$next; fi ;;
*) for opt in "$@"; do case "$(printf '%s' "$opt" | tr '[:upper:]' '[:lower:]')" in "$key"*) chosen=$opt; at $((status_row + 1)); note=; return 0 ;; esac; done ;;
esac
done
}
# edit PROMPT: a normal line editor on the status line; empty keeps nothing.
edit() {
status "$1 "
stty "$tty_saved" <&3
printf '%s[?25h' "$e"
IFS= read -r edited <&3 || edited=
printf '%s[?25l' "$e"
stty -icanon -echo min 1 time 0 <&3
}
# ---------------------------------------------------------------- actions ---
# First screen of a first run: the email that holds the licenses. It is kept
# in this folder only (makepad.json), so a rerun or reinstall here skips this.
signin_rows() {
printf '%s\n' "note|" \
"note|Enter the email address you bought a Makepad app with, such as Scope" \
"note|or Amp, or that has beta access. It shows your licenses and stays in" \
"note|this folder only." \
"note|" \
"note|${dim}Leave it empty to continue with the open source experiments; you can" \
"note|${dim}log in later from the menu.$r0"
}
signin() {
s_screen=$screen s_sel=$sel
screen=signin
while :; do
draw
edit "Email:"
case "$edited" in
'') break ;;
*@*.*) email=$edited; break ;;
*) message="${warn}That does not look like an email address.$r0" ;;
esac
done
message= screen=$s_screen sel=$s_sel
}
switch_account() {
if [ -n "$email" ]; then edit "Email for your licenses ${dim}(⏎ keeps $email)$r0:"; else edit "Email you bought a Makepad app with:"; fi
case "$edited" in
'') return ;;
*@*.*) ;;
*) message="${warn}That does not look like an email address.$r0"; return ;;
esac
email=$edited
refresh
n=0; [ -z "$licenses" ] || n=$(printf '%s\n' "$licenses" | wc -l | tr -d ' ')
message="${ok}✓$r0 Switched to $email · $n licenses."
}
# License agreements. Installing the build tools accepts the Microsoft and
# Rust ones, installing CUDA the NVIDIA one; Return on a row opens it.
makepad_license_url=https://makepad.nl/commercial-license
agreements="makepad|Makepad commercial license|$makepad_license_url
vs|Microsoft Visual Studio Build Tools|https://visualstudio.microsoft.com/license-terms/vs2022-ga-diagnosticbuildtools/
sdk|Microsoft Windows SDK|https://learn.microsoft.com/legal/windows-sdk/windows-sdk-license
rust|Rust|https://www.rust-lang.org/policies/licenses
cuda|NVIDIA CUDA Toolkit|https://docs.nvidia.com/cuda/eula/"
accepted=no
terms_rows() {
printf '%s\n' "$agreements" | while IFS='|' read -r id title url; do
# Opened from a consent screen, the page lists only its agreements.
case " ${terms_only:-$id} " in *" $id "*) ;; *) continue ;; esac
host=${url#https://}; host=${host%%/*}
printf 'item|%s|%-36s||%s|open in browser\n' "$id" "$title" "$dim$host$r0"
done
}
# The agreements screen, entered from the SETUP row or from the acceptance
# question; Escape returns to wherever it was opened from.
terms_loop() {
t_screen=$screen t_sel=$sel t_top=$top t_message=$message
screen=terms sel=0 top=0 message=
while :; do
draw
key
message=
case "$key" in
up) sel=$(( (sel + items - 1) % items )) ;;
down) sel=$(( (sel + 1) % items )) ;;
esc|q) break ;;
enter)
select_row
url=$(printf '%s\n' "$agreements" | awk -F'|' -v id="$id" '$1 == id { print $3 }')
open "$url" 2>/dev/null || :
message="${ok}✓$r0 Opened ${url#https://} in your browser." ;;
esac
done
screen=$t_screen sel=$t_sel top=$t_top message=$t_message
}
# consent TITLE INTRO DETAIL ID...: a screen of its own for the license terms
# a step needs. The agreements are listed by name, Return opens one in the
# browser; "Agree to all" (selected at the start) and Cancel close the list.
consent() {
c_screen=$screen c_sel=$sel c_message=$message
consent_title=$1 consent_intro=$2 consent_detail=$3; shift 3
consent_ids="$*"
screen=consent message=
# Start on "Agree to all", below the agreements.
sel=$(($# + 0))
while :; do
draw
key
message=
case "$key" in
up) sel=$(( (sel + items - 1) % items )) ;;
down) sel=$(( (sel + 1) % items )) ;;
esc|q) id=cancel ;;
enter) select_row ;;
*) continue ;;
esac
case "$key/$id" in
*/agree) screen=$c_screen sel=$c_sel message=$c_message; draw; return 0 ;;
*/cancel) screen=$c_screen sel=$c_sel message=$c_message; return 1 ;;
enter/*)
url=$(printf '%s\n' "$agreements" | awk -F'|' -v id="$id" '$1 == id { print $3 }')
open "$url" 2>/dev/null || :
message="${ok}✓$r0 Opened ${url#https://} in your browser." ;;
esac
done
}
consent_rows() {
printf '%s\n' "note|" "note|$consent_intro" "note|$dim$consent_detail$r0" "head|READ THE AGREEMENTS"
printf '%s\n' "$agreements" | while IFS='|' read -r id title url; do
case " $consent_ids " in *" $id "*) ;; *) continue ;; esac
host=${url#https://}; host=${host%%/*}
printf 'item|%s|%-36s||%s|open in browser\n' "$id" "$title" "$dim$host$r0"
done
printf '%s\n' "note|" "item|agree|Agree to all|||$consent_what" "item|cancel|Cancel|||"
}
# The GPU notice is a setup step with a screen of its own, like the license
# terms; its answer is kept.
setup_gpu() {
g_screen=$screen g_sel=$sel g_message=$message
screen=gpu sel=0 message=
while :; do
draw
key
case "$key" in
up) sel=0 ;;
down) sel=1 ;;
enter) [ "$sel" -eq 0 ] && gpu=ok; break ;;
esc|q) sel=1; break ;;
esac
done
r_sel=$sel
screen=$g_screen sel=$g_sel message=$g_message
[ "$r_sel" -eq 0 ]
}
gpu_rows() {
printf '%s\n' "note|" \
"note|Makepad heavily relies on your GPU to draw its UI and implement AI" \
"note|functionality. Old hardware and broken drivers can cause your computer" \
"note|to reboot unexpectedly." \
"note|" \
"item|continue|I understand|||" \
"item|cancel|Cancel|||"
}
# One question covers every agreement; accept is the default.
accept_terms() {
[ "$accepted" != yes ] || return 0
while :; do
choose "Makepad apps come with the Makepad commercial license and Rust's licenses." accept accept read quit || return 1
case "$chosen" in
accept) accepted=yes; return 0 ;;
read) terms_loop; draw ;;
quit) return 1 ;;
esac
done
}
# A page of its own while something runs: the steps as a checklist, the
# current one carrying its progress bar, then back to the menu with the result.
# work_begin TITLE SUBTITLE STEP... work_step STEP work_end MESSAGE
work_begin() {
w_screen=$screen w_sel=$sel
work_title=$1 work_sub=$2; shift 2
work_steps=$(printf '%s\n' "$@")
work_cur= screen=work message=
}
work_step() { work_cur=$1; draw; }
work_end() { screen=$w_screen sel=$w_sel message=$1; }
work_rows() {
printf '%s\n' "note|"
seen=no
printf '%s\n' "$work_steps" | while IFS= read -r label; do
if [ "$label" = "$work_cur" ]; then state=cur; seen=yes
elif [ "$seen" = yes ] || [ -z "$work_cur" ]; then state=next
else state=done; fi
printf 'step|%s|%s\n' "$state" "$label"
done
}
setup_rust() {
consent_what='install the build tools and Rust'
consent 'Install build tools' "Makepad compiles from source, so it needs a compiler (1.6 GB download)." "Installed in this folder only: Microsoft C++ Build Tools, Windows SDK, Rust $rust_pin" makepad vs sdk rust || return 0
work_begin 'Install build tools' 'This takes a few minutes; everything goes into this folder.' 'Build tools' 'Windows SDK' 'Rust'
work_step 'Build tools'; bar 'Build tools' 0 770 MB 1.5 'Microsoft C++ Build Tools'
work_step 'Windows SDK'; bar 'Windows SDK' 0 710 MB 1.5 'download and unpack'
work_step 'Rust'; bar Rust 0 66.4 MB 1 static.rust-lang.org
rust=local accepted=yes
work_end "${ok}✓$r0 Build tools and Rust $rust_pin installed in this folder. Nothing else on your PC changed."
select_id rust
}
setup_cuda() {
if [ "$nvidia" != yes ]; then
message="${dim}CUDA needs an NVIDIA graphics card; none was found.$r0"
return
fi
if [ "$cuda" = ready ]; then
message="${ok}✓$r0 CUDA $cuda_pin is installed; Makepad Amp builds with its AI features."
return
fi
consent_what='install CUDA'
consent 'Install CUDA' "CUDA adds the AI features in Makepad Amp (2.9 GB download)." "Installed in this folder only: NVIDIA CUDA Toolkit $cuda_pin" cuda || return 0
work_begin 'Install CUDA' 'About 3 GB, into this folder only.' CUDA
work_step CUDA; bar CUDA 0 2940 MB 3 developer.download.nvidia.com
cuda=ready
[ "$(get amp)" != ready ] || put amp compile
work_end "${ok}✓$r0 CUDA $cuda_pin installed. Makepad Amp recompiles with its AI features on its next run."
select_id cuda
}
# Download what is missing, compile when the build is older than the source,
# bundle, then run: every app goes through the same steps from whatever state
# it is in. The result is a real <Title>.app (icon, Info.plist, the binary
# inside), opened with `open`, so its Dock icon can be kept.
open_app() { # open_app ID TITLE MB CRATES
id=$1 title=$2 mb=$3 crates=$4
if [ "$rust" = none ]; then setup_rust; [ "$rust" != none ] || return 0; fi
state=$(get "$id")
if [ "$state" != ready ]; then
case "$state" in
new | partial) work_begin "$title" 'Download the source, compile it, then run it.' 'Download source' Compile Open ;;
*) work_begin "$title" 'Compile the source, then run it.' Compile Open ;;
esac
case "$state" in
new) work_step 'Download source'; bar 'Download source' 0 "$mb" MB 2.5 makepad.nl ;;
partial) work_step 'Download source'; bar 'Download source' 38 "$mb" MB 1.8 resuming ;;
esac
work_step Compile; bar Compile 0 "$crates" crates 3 compiling
put "$id" ready
work_step Open; spin "Opening $title" 0.5
w_open=yes
else
spin "Opening $title" 0.5
w_open=no
fi
message="${ok}✓$r0 $title is running. ${dim}Right-click its taskbar icon › Pin to taskbar.$r0"
[ "$w_open" != yes ] || work_end "$message"
}
open_free() { # a free app: the shared Makepad download first, then its build
id=$1 title=$2
if [ "$(get makepad)" = new ]; then
if [ "$rust" = none ]; then setup_rust; [ "$rust" != none ] || return 0; fi
bar Makepad 0 41.7 MB 2 'open source apps, shared by all of them'
put makepad ready
for f in $(printf '%s\n' "$free_apps" | cut -d'|' -f1); do [ "$(get "$f")" != new ] || put "$f" compile; done
fi
open_app "$id" "$title" 0 96
}
open_agent() { # open_agent TITLE [TASK]
if [ "$rust" = none ]; then setup_rust; [ "$rust" != none ] || return 0; fi
# The live version writes AGENTS.md + CLAUDE.md from print_agent_context,
# leaves this screen, runs the agent in the folder with print_env applied
# (TASK as its first prompt), and redraws when the agent exits.
spin "Writing AGENTS.md for $1" 0.4
message="${ok}✓$r0 $1 would open in $(short "$root") knowing ./makepad build and run. ${dim}Preview: sh makepad-mac.sh agent-context$r0"
[ -z "${2:-}" ] || message="${ok}✓$r0 $1 would open with: ${dim}$2$r0"
}
pick_agent() { # sets agent to the one installed agent, or asks which
agent=
n=$(printf '%s\n' "$agents" | grep -c . || :)
case "$n" in
0) message="${warn}Install Claude Code, Codex or Grok to merge your changes, or apply the diff yourself.$r0"; return 1 ;;
1) agent=$(printf '%s\n' "$agents" | cut -d'|' -f2); return 0 ;;
esac
set -- $(printf '%s\n' "$agents" | cut -d'|' -f1)
choose "Merge your changes with which agent?" "$1" "$@" || return 1
agent=$(printf '%s\n' "$agents" | awk -F'|' -v id="$chosen" '$1 == id { print $2 }')
}
# After an update, local edits are never merged by the installer. Your edited
# tree was diffed against the release it came from (git on macOS/Linux,
# makepad-git in the Windows Builder, so no git is needed there), the diff was
# saved in changes/, and the app now holds the clean new release. Your coding
# agent reapplies the diff; it reads it like any file.
merge_changes() { # merge_changes ID TITLE
pick_agent || return 0
diff="changes/$1-$(date +%Y-%m-%d).diff"
open_agent "$agent" "Reapply my changes in $diff onto the new $2 source, then ./makepad build $1."
put "$1" compile
}
# Updates: licenses first (new purchases appear, expired ones go), then every
# downloaded repository is compared with the newest release on makepad.nl.
# An untouched repository is replaced by a clean fetch of the new release. A
# repository you changed is first saved as a diff against its release:
# changes/<app>-<date>.diff unified diff, new files included
# changes/<app>-<date>.files one "M|A|D path" line per changed file
# then replaced the same way, and the app waits for your agent to merge.
check_updates() {
refresh
got= kept=
if [ "$demo" = installed ] && [ "$(get amp)" = ready ]; then
spin 'Makepad Amp has local changes: saving them as a diff' 0.6
bar 'Makepad Amp' 0 188 MB 2 'clean source for the new release'
put amp merge
got='Makepad Amp' kept=' Your 3 changed files are saved in changes/; select Makepad Amp to merge them.'
fi
if [ "$demo" = installed ] && [ "$(get makepad)" = ready ]; then
bar Makepad 0 41.7 MB 1.5 'clean source for the new release'
put makepad updated
for f in $(printf '%s\n' "$free_apps" | cut -d'|' -f1); do [ "$(get "$f")" != ready ] || put "$f" compile; done
got="$got${got:+ and }Makepad apps"
fi
checked="${ok}✓$r0 up to date · $(date +%H:%M)"
if [ -n "$got" ]; then
checked="updates downloaded · $(date +%H:%M)"
message="${ok}✓$r0 Updated $got.$kept"
else
message="${ok}✓$r0 Licenses checked; everything is up to date."
fi
}
refresh() {
if [ -z "$email" ]; then licenses= licensed=yes; return; fi
licensed=unknown
draw
spin "Checking licenses for $email" 0.8
fetch_licenses
licensed=yes
}
select_row() {
row=$(printf '%s\n' "$rows_out" | grep '^item|' | sed -n "$((sel + 1))p")
IFS='|' read -r _ id title _ _ _ <<EOF
$row
EOF
}
# ------------------------------------------------------------------- run ---
scan
screen=main sel=0 top=0
# The email comes first; everything after it can show the licenses.
[ "$demo" != first ] || [ -n "$email" ] || signin
# First run walks the setup steps in order: the driver notice, then the build
# tools. Each is also a SETUP row to revisit.
# Screens follow each other directly; the menu first appears once the
# person has agreed (or cancelled), never in between.
if [ "$demo" = first ]; then
gpu=
setup_gpu || exit 0
setup_rust
else
gpu=ok accepted=yes
fi
refresh
[ "$(get scope)" != partial ] || message="${warn}An earlier run stopped while downloading Makepad Scope. Select it to resume.$r0"
draw
[ "$rust" != none ] || { setup_rust; draw; }
# Start on the first licensed app.
# Start on the first licensed app, or on logging in.
if [ -z "$email" ] || [ -n "$licenses" ]; then sel=$(main_rows | sed '/^head|YOUR APPS/q' | grep -c '^item|'); fi
while :; do
draw
key
message=
case "$key" in
up) sel=$(( (sel + items - 1) % items )) ;;
down) sel=$(( (sel + 1) % items )) ;;
q) exit 0 ;;
esc) if [ "$screen" = main ]; then exit 0; else screen=main sel=$back top=0; fi ;;
enter)
select_row
case "$screen/$id" in
main/account) switch_account ;;
main/login) switch_account ;;
main/rust) if [ "$rust" = none ]; then setup_rust; else spin 'Checking the build tools' 0.5; message="${ok}✓$r0 Build tools, Windows SDK and Rust are ready."; fi ;;
main/cuda) setup_cuda ;;
main/gpu) setup_gpu || : ;;
main/updates) check_updates ;;
main/disk) clear_build ;;
main/terms) terms_loop ;;
main/free) back=$sel screen=free sel=0 top=0 ;;
main/agent-*) open_agent "$title" ;;
main/*)
if [ "$(get "$id")" = merge ]; then merge_changes "$id" "$title"; continue; fi
mb=$(printf '%s\n' "$licenses" | awk -F'|' -v id="$id" '$1 == id { print $4 }')
open_app "$id" "$title" "$mb" 214 ;;
free/*) open_free "$id" "$title" ;;
esac ;;
esac
done