1052 lines
44 KiB
Bash
1052 lines
44 KiB
Bash
#!/bin/sh
|
||
# Makepad commercial app installer and launcher for macOS: one shell script,
|
||
# nothing compiled for itself. Rust is installed only to build the apps.
|
||
#
|
||
# PREVIEW: the backend is simulated (no downloads, no writes). The screens,
|
||
# keys and flow are the real design. Coding agents are really detected.
|
||
#
|
||
# Every run is a restart: the script reads what earlier runs left in the
|
||
# folder and continues from there, so re-downloading and re-running it is
|
||
# always safe. It refreshes your licenses whenever it opens.
|
||
#
|
||
# sh makepad-mac.sh first run
|
||
# sh makepad-mac.sh --installed an existing installation
|
||
# sh makepad-mac.sh --resume an earlier run stopped mid-download
|
||
# sh makepad-mac.sh --no-rust no system Rust found
|
||
# sh makepad-mac.sh --free an email without licenses
|
||
# sh makepad-mac.sh --email a@b.c
|
||
# sh makepad-mac.sh --linux the same script on Linux: GPU notice + packages
|
||
# sh makepad-mac.sh --no-xcode macOS without Apple's developer tools
|
||
# sh makepad-mac.sh --xcode-license Xcode installed, license not accepted
|
||
#
|
||
# The installed copy (~/makepad-commercial/makepad) is also the command
|
||
# coding agents use; without a terminal UI it takes a command:
|
||
# makepad build APP | run APP | list | env | agent-context
|
||
set -eu
|
||
|
||
demo=first demo_rust=found demo_free=no email= command= plat=mac gpu=ok pkgs=ok xcode=ok
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--installed) demo=installed email=rik2@n4.io ;;
|
||
--resume) demo=resume email=rik2@n4.io ;;
|
||
--no-rust) demo_rust=none ;;
|
||
--linux) plat=linux ;;
|
||
--no-xcode) xcode=missing ;;
|
||
--xcode-license) xcode=license ;;
|
||
--free) demo_free=yes ;;
|
||
--email) shift; email=$1 ;;
|
||
-h|--help) sed -n '2,24p' "$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="$HOME/makepad-commercial"
|
||
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() { case "$1" in "$HOME"/*) printf '~/%s' "${1#"$HOME"/}" ;; *) printf '%s' "$1" ;; esac; }
|
||
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
|
||
[ "$rust" != local ] || kb_rust=820000
|
||
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."
|
||
}
|
||
|
||
gpu_row() {
|
||
[ "$plat" = linux ] || return 0
|
||
gs="${ok}✓$r0 driver notice read" ga=
|
||
[ "$gpu" = ok ] || gs="${warn}read the driver notice$r0" ga=read
|
||
printf '%s' "item|gpu|Graphics||$gs|$ga"
|
||
}
|
||
tools_row() {
|
||
if [ "$plat" != linux ]; then
|
||
case "$xcode" in
|
||
ok) printf '%s' "item|tools|Xcode tools||${ok}✓$r0 clang, SDK, git|recheck" ;;
|
||
missing) printf '%s' "item|tools|Xcode tools||${warn}not installed$r0|install" ;;
|
||
license) printf '%s' "item|tools|Xcode tools||${warn}license not accepted$r0|accept" ;;
|
||
esac
|
||
return
|
||
fi
|
||
n=$(printf '%s\n' $packages | wc -l | tr -d ' ')
|
||
ps="${ok}✓$r0 $distro development packages" pa=recheck
|
||
[ "$pkgs" = ok ] || ps="${warn}$n missing$r0" pa=install
|
||
printf '%s' "item|tools|System packages||$ps|$pa"
|
||
}
|
||
# 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 set up$r0" ra='set up' ;;
|
||
installing) rs="${acc}installing…$r0" ra= ;;
|
||
local) rs="${ok}✓$r0 local $rust_pin ${dim}in $(short "$root")$r0" ra=change ;;
|
||
system) rs="${ok}✓$r0 system $system_rust" ra=change ;;
|
||
esac
|
||
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)" \
|
||
"$(gpu_row)" \
|
||
"item|terms|Agreements||$(if [ "$accepted" = yes ]; then printf '%s' "${ok}✓$r0 accepted ${dim}· Makepad, Rust$r0"; else printf '%s' "${dim}Makepad, Rust$r0"; fi)|read" \
|
||
"item|rust|Rust||$rs|$ra" \
|
||
"$(tools_row)" \
|
||
"$(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.' ;;
|
||
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.' ;;
|
||
xcode) rows_out=$(xcode_rows); crumb=' › Apple developer tools' sub='Needed to compile on macOS.' ;;
|
||
gpu) rows_out=$(gpu_rows); crumb=' › Graphics' sub='Before you continue.' ;;
|
||
consent) rows_out=$(consent_rows); crumb=" › $consent_title" sub='Please read what you are agreeing to.' ;;
|
||
packages) rows_out=$(packages_rows); crumb=' › System packages' sub='Installed outside this folder, so please check them.' ;;
|
||
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" = gpu ] || [ "$screen" = packages ] || [ "$screen" = consent ] || [ "$screen" = xcode ] || 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" = gpu ] || [ "$screen" = packages ] || [ "$screen" = consent ] || [ "$screen" = xcode ]; 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: one list, one acceptance. Return on a row opens it in
|
||
# the browser. Apple's developer tools carry their own license, which Apple's
|
||
# installer shows; it is not repeated here.
|
||
makepad_license_url=https://makepad.nl/commercial-license
|
||
agreements="makepad|Makepad commercial license|$makepad_license_url
|
||
rust|Rust licenses|https://www.rust-lang.org/policies/licenses"
|
||
accepted=no
|
||
|
||
terms_rows() {
|
||
printf '%s\n' "$agreements" | while IFS='|' read -r id title url; do
|
||
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)
|
||
url=$(printf '%s\n' "$agreements" | sed -n "$((sel + 1))p" | cut -d'|' -f3)
|
||
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
|
||
}
|
||
|
||
# One question covers every agreement; accept is the default.
|
||
# 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|||"
|
||
}
|
||
|
||
# First run: the Makepad and Rust terms, on the same screen Windows uses.
|
||
accept_terms() {
|
||
[ "$accepted" != yes ] || return 0
|
||
consent_what='agree and continue'
|
||
consent 'License agreements' "Makepad apps are licensed to you, and they are compiled with Rust." "Rust $rust_pin is installed in this folder only if you choose a local Rust." makepad rust || return 1
|
||
accepted=yes
|
||
}
|
||
|
||
# Linux only: the GPU notice (a screen before anything is probed, downloaded
|
||
# or compiled) and the distribution's development packages, which are the
|
||
# one thing installed outside this folder, system-wide through sudo.
|
||
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|||"
|
||
}
|
||
|
||
distro='Ubuntu 24.04'
|
||
packages='build-essential clang cmake pkg-config git curl libssl-dev libx11-dev libxcursor-dev libxkbcommon-dev libxrandr-dev libxi-dev libasound2-dev libpulse-dev libwayland-dev libegl-dev libgl-dev libdrm-dev libgbm-dev'
|
||
setup_packages() {
|
||
p_screen=$screen p_sel=$sel p_message=$message
|
||
screen=packages sel=0 message=
|
||
while :; do
|
||
draw
|
||
key
|
||
case "$key" in
|
||
up) sel=0 ;;
|
||
down) sel=1 ;;
|
||
enter) break ;;
|
||
esc|q) sel=1; break ;;
|
||
esac
|
||
done
|
||
r_sel=$sel
|
||
screen=$p_screen sel=$p_sel message=$p_message
|
||
draw
|
||
[ "$r_sel" -eq 0 ] || return 1
|
||
# The live version leaves this screen for sudo's password prompt, then
|
||
# turns the package manager's output into this one bar.
|
||
spin 'sudo apt-get install (asks for your password)' 0.8
|
||
bar Packages 0 "$(printf '%s\n' $packages | wc -l | tr -d ' ')" packages 2.5 apt-get
|
||
pkgs=ok
|
||
message="${ok}✓$r0 Development packages installed."
|
||
}
|
||
packages_rows() {
|
||
n=$(printf '%s\n' $packages | wc -l | tr -d ' ')
|
||
printf '%s\n' "note|" \
|
||
"note|Makepad compiles from source and needs $n development packages from" \
|
||
"note|$distro. They are installed system-wide by your package manager:" \
|
||
"note|"
|
||
printf '%s\n' $packages | paste -d' ' - - - - | while IFS= read -r l; do
|
||
printf 'note|%s\n' "$dim $l$r0"
|
||
done
|
||
printf '%s\n' "note|" \
|
||
"item|install|Install with sudo|||sudo apt-get install" \
|
||
"item|cancel|Cancel|||"
|
||
}
|
||
|
||
# macOS: Apple's command line developer tools (clang, the macOS SDK, git).
|
||
# Makepad never installs or accepts them itself: it opens Apple's installer,
|
||
# or hands the terminal to Apple's own license prompt, then checks again.
|
||
xcode_rows() {
|
||
case "$xcode" in
|
||
missing) printf '%s\n' "note|" \
|
||
"note|Makepad compiles with Apple's command line developer tools: clang," \
|
||
"note|the macOS SDK and git. They are not installed on this Mac yet." \
|
||
"note|" \
|
||
"note|${dim}Apple's installer opens in its own window (about 1 GB); come back$r0" \
|
||
"note|${dim}here when it has finished.$r0" \
|
||
"note|" \
|
||
"item|install|$(printf '%-34s' 'Install the developer tools')|||opens Apple's installer" \
|
||
"item|check|Check again|||" \
|
||
"item|cancel|Cancel|||" ;;
|
||
license) printf '%s\n' "note|" \
|
||
"note|Xcode is installed, but its license has not been accepted yet, so" \
|
||
"note|Apple's compiler will not run." \
|
||
"note|" \
|
||
"note|${dim}Apple shows the license here in the terminal and asks for your$r0" \
|
||
"note|${dim}password; type agree at the end to accept it.$r0" \
|
||
"note|" \
|
||
"item|license|$(printf '%-34s' 'Read and accept the Xcode license')|||sudo xcodebuild -license" \
|
||
"item|check|Check again|||" \
|
||
"item|cancel|Cancel|||" ;;
|
||
esac
|
||
}
|
||
setup_xcode() {
|
||
[ "$xcode" != ok ] || return 0
|
||
x_screen=$screen x_sel=$sel x_message=$message
|
||
screen=xcode sel=0 message=
|
||
while [ "$xcode" != ok ]; do
|
||
draw
|
||
key
|
||
message=
|
||
case "$key" in
|
||
up) sel=$(( (sel + items - 1) % items )) ;;
|
||
down) sel=$(( (sel + 1) % items )) ;;
|
||
esc|q) break ;;
|
||
enter)
|
||
select_row
|
||
case "$id" in
|
||
install)
|
||
# Live: xcode-select --install, then poll the check.
|
||
spin "Waiting for Apple's installer to finish" 3
|
||
xcode=ok ;;
|
||
license)
|
||
# Live: leave the screen, run sudo xcodebuild -license
|
||
# with the real terminal, then redraw and check.
|
||
spin "Apple's license prompt runs here (simulated)" 2
|
||
xcode=ok ;;
|
||
check)
|
||
spin 'Checking clang, the macOS SDK, the linker and git' 0.8
|
||
message="${warn}Still not ready.$r0" ;;
|
||
cancel) break ;;
|
||
esac ;;
|
||
esac
|
||
done
|
||
screen=$x_screen sel=$x_sel message=$x_message
|
||
if [ "$xcode" = ok ]; then draw; message="${ok}✓$r0 Apple developer tools are ready."; return 0; fi
|
||
return 1
|
||
}
|
||
|
||
# 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() {
|
||
if [ -n "$system_rust" ]; then
|
||
choose "Use your system Rust $system_rust, or a local $rust_pin that keeps your system clean?" local system local || return 0
|
||
if [ "$chosen" = system ]; then
|
||
rust=system
|
||
message="${ok}✓$r0 Using your system Rust $system_rust. Caches and builds stay in $(short "$root")."
|
||
return
|
||
fi
|
||
else
|
||
choose "Install a local Rust $rust_pin into $(short "$root")? It keeps your system clean." install install cancel || return 0
|
||
[ "$chosen" = install ] || return 0
|
||
fi
|
||
work_begin 'Install Rust' 'About 70 MB, into this folder only.' Rust
|
||
work_step Rust; bar Rust 0 66.4 MB 2.5 static.rust-lang.org
|
||
rust=local
|
||
work_end "${ok}✓$r0 Local Rust $rust_pin installed. Nothing outside $(short "$root") changed."
|
||
select_id rust
|
||
}
|
||
|
||
# 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 [ "$plat" = mac ] && [ "$xcode" != ok ]; then setup_xcode || return 0; fi
|
||
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
|
||
if [ "$plat" = linux ]; then message="${ok}✓$r0 $title is running. ${dim}It is also in your app launcher.$r0"; else message="${ok}✓$r0 $title.app is running. ${dim}Right-click its Dock icon › Options › Keep in Dock.$r0"; fi
|
||
[ "$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
|
||
# Linux: the GPU notice comes before anything else, then the packages.
|
||
if [ "$plat" = linux ] && [ "$demo" = first ]; then
|
||
gpu= pkgs=
|
||
setup_gpu || exit 0
|
||
fi
|
||
if [ "$demo" = first ]; then
|
||
draw
|
||
choose "Install into $(short "$root")?" install install 'other folder' quit || exit 0
|
||
case "$chosen" in
|
||
quit) exit 0 ;;
|
||
'other folder')
|
||
edit "Folder:"
|
||
case "$edited" in '') ;; '~') root=$HOME ;; '~/'*) root=$HOME/${edited#'~/'} ;; *) root=$edited ;; esac ;;
|
||
esac
|
||
fi
|
||
if [ "$demo" = first ]; then accept_terms || exit 0; else accepted=yes; fi
|
||
if [ "$plat" = linux ] && [ "$pkgs" != ok ]; then setup_packages || exit 0; fi
|
||
if [ "$plat" = mac ] && [ "$xcode" != ok ]; then setup_xcode || :; 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) setup_rust ;;
|
||
main/tools) if [ "$plat" = linux ]; then if [ "$pkgs" = ok ]; then spin 'Checking packages' 0.5; message="${ok}✓$r0 All development packages are installed."; else setup_packages || :; fi; elif [ "$xcode" != ok ]; then setup_xcode || :; else spin 'Checking Xcode tools' 0.5; message="${ok}✓$r0 clang, macOS SDK, linker and git are ready."; fi ;;
|
||
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
|