Complete reference for building, installing, and maintaining Purple Computer.
Table of Contents:
- Overview
- Installer Architecture
- Ask Mode (F1)
- Build Process
- Installation
- Customization
- Power Management
- Troubleshooting
Purple Computer turns old laptops into calm, creative tools for kids. The installer boots from USB, writes a pre-built Ubuntu disk image to the internal drive, and reboots into a minimal TUI environment.
Key Facts:
- Installed OS: Ubuntu 24.04 LTS (Noble Numbat) with custom TUI application
- Installer Method: Direct disk imaging (no apt, no package installation during setup)
- Boot Medium: USB stick (hybrid ISO, BIOS and UEFI compatible)
- Installation Time: 10-20 minutes (mostly disk write time)
- Secure Boot: Supported out of the box
Important: There are two separate systems involved:
- USB Installer: A temporary Ubuntu live environment that copies the system to disk, then is never used again
- Installed System: A normal Ubuntu 24.04 system that kids use every day
These are built differently and serve different purposes. See guides/architecture-overview.md for a detailed explanation.
The PurpleOS installer is built for simplicity, reliability, and broad hardware compatibility. It uses Ubuntu's stock kernel and signed boot chain to work across diverse laptop hardware including Surface, Dell, HP, and ThinkPad devices.
Installation requires passing two independent safety gates:
| Gate | When | What | Purpose |
|---|---|---|---|
| Gate 1 | Initramfs (early boot) | Check purple.install=1 in cmdline |
Design-time arming |
| Gate 2 | Userspace (systemd) | Show confirmation, require ENTER | Runtime user consent |
Boot Flow:
- Boot: USB stick loads Ubuntu's signed boot chain (shim → GRUB → kernel)
- Initramfs: Kernel loads initramfs with our injected hook script
- Gate 1 (Hook Runs): Our script in
/scripts/init-top/checks forpurple.install=1- If NOT armed → exit, normal Ubuntu boot
- If ARMED → find payload, write runtime artifacts to
/run/, continue to casper
- Casper: Ubuntu's casper mounts squashfs, systemd starts
- Gate 2 (Confirmation): Runtime systemd service shows confirmation screen
- User presses ENTER → run installer
- User presses ESC or timeout → reboot (no install)
- Write: Decompress and write pre-built Ubuntu image to internal disk
- Bootloader: Setup UEFI boot entries with multi-layer fallback
- Reboot: System boots into installed Ubuntu + Purple TUI
Key insight: The squashfs is never modified. Gate 2's systemd service is written to /run/ by the initramfs hook—systemd automatically loads units from /run/systemd/system/.
No package manager runs during installation. The installer writes a complete, pre-built Ubuntu system image directly to disk.
The installer intercepts boot BEFORE Ubuntu's live system starts:
USB Boot Flow (Two-Gate Model)
═══════════════════════════════════════════════════════════
UEFI Firmware
│
▼
shimx64.efi (Microsoft-signed)
│
▼
grubx64.efi (Canonical-signed)
│
▼
GRUB menu:
• "Install Purple Computer" (default) → purple.install=1
• "Debug Mode (no install)" → no arming flag
│
▼
vmlinuz + initrd (Ubuntu kernel + modified initramfs)
│
▼
═══════════════════════════════════════════════════════════
GATE 1: DESIGN-TIME ARMING
═══════════════════════════════════════════════════════════
initramfs runs init-top scripts
│
├── [Purple hook] Check cmdline for purple.install=1
│ │
│ ├── NOT ARMED → Gate 1 CLOSED → normal Ubuntu boot
│ │
│ └── ARMED → scan for payload
│ │
│ ├── Found → write /run/purple/armed marker
│ │ write /run/systemd/system/purple-confirm.service
│ │ Gate 1 PASSED → continue to casper
│ │
│ └── Not found → Gate 1 CLOSED → normal Ubuntu boot
│
▼
casper mounts squashfs → systemd starts
│
▼
═══════════════════════════════════════════════════════════
GATE 2: RUNTIME USER CONFIRMATION
═══════════════════════════════════════════════════════════
purple-confirm.service runs (only if /run/purple/armed exists)
│
├── Shows large warning: "This will set up Purple Computer"
│
├── Waits for user input:
│ │
│ ├── ENTER → Gate 2 PASSED → run installer
│ │
│ ├── ESC → Gate 2 CLOSED → reboot (no install)
│ │
│ └── Timeout → Gate 2 CLOSED → reboot (no install)
│
▼
(Only reaches here if both gates passed)
install.sh writes golden image to disk
│
▼
Reboot into installed Purple Computer
Key insight: Our hook runs in initramfs but does NOT run the installer. It writes runtime artifacts to /run/ and lets casper continue. The confirmation screen (Gate 2) runs in userspace via systemd.
Safety: Installation requires BOTH gates to pass:
purple.install=1in kernel cmdline (Gate 1, design-time arming)- User presses ENTER on confirmation screen (Gate 2, runtime consent)
Selecting "Debug Mode" from the GRUB menu fails Gate 1, booting into normal Ubuntu Server live environment.
Previous versions used a custom Linux kernel with all drivers built-in. While elegant, this approach failed on diverse hardware due to:
- Platform-Specific Quirks: Modern laptops require ACPI quirks and platform drivers beyond basic storage support
- Secure Boot: Custom kernels aren't signed by Microsoft's UEFI CA
- Maintenance Burden: Debugging kernel configs per laptop model is not sustainable
Ubuntu's stock kernel handles these automatically:
- Surface: ACPI quirks, Type Cover drivers
- Dell/HP: WMI drivers, Thunderbolt quirks
- ThinkPad: ThinkPad ACPI, TrackPoint drivers
- Generic: EFI framebuffer, NVMe APST quirks
Purple Computer displays a 100×28 character viewport (plus header and footer) requiring a 104×37 character terminal grid. The font size is calculated to fill 80% of the screen, with a cap to prevent huge viewports on large displays.
Minimum supported resolution: 1024×768
At X11 startup, calc_font_size.py calculates the optimal font size:
- Get resolution from xrandr (fallback: 1366×768)
- Probe cell size by launching Alacritty at 18pt (fallback: 11×22 pixels)
- Calculate font to fill 80% of screen
- Clamp to 12-24pt range
That's it. No EDID/physical size detection (too unreliable), no caching (fast enough), no validation loops (calculation just works).
| Screen | Resolution | Font | Viewport Fill |
|---|---|---|---|
| 11" laptop | 1366×768 | 14pt | 80% |
| 13" laptop | 1920×1080 | 19pt | 80% |
| 15" laptop | 1920×1080 | 19pt | 80% |
| 17" laptop | 2560×1440 | 24pt | ~60% (capped) |
| 4K monitor | 3840×2160 | 24pt | ~40% (capped) |
The 24pt cap prevents oversized viewports on large screens while keeping the UI proportional on typical donated laptops (11-15").
Run inside the VM to see what's happening:
python3 /opt/purple/calc_font_size.py --infoOutput:
Screen: 1920x1080
Cell: 11x22 (at 18pt)
Grid: 104x37
Fill: 80%
Font: 19.1pt
Startup log: cat /tmp/xinitrc.log
Edit xinitrc to force a specific font size:
FONT_SIZE=20 # Override calculated sizeThe installed Purple Computer uses X11 with a deliberately minimal graphics configuration designed for maximum hardware compatibility.
┌─────────────────────────────────────────────────────────┐
│ Purple TUI │
│ (Alacritty terminal) │
├─────────────────────────────────────────────────────────┤
│ Matchbox Window Manager │
├─────────────────────────────────────────────────────────┤
│ X.Org Server │
│ (modesetting driver only) │
├─────────────────────────────────────────────────────────┤
│ Mesa + Glamor (OpenGL acceleration) │
├─────────────────────────────────────────────────────────┤
│ Linux DRM/KMS (kernel) │
│ (i915, amdgpu, nouveau, etc.) │
└─────────────────────────────────────────────────────────┘
We deliberately do not install xserver-xorg-video-all (which includes legacy drivers like vesa, fbdev, intel DDX). Instead, we rely solely on the modesetting driver built into xserver-xorg-core.
Reasons:
-
Avoids I/O port errors: Legacy drivers (vesa, fbdev) attempt to access VGA I/O ports (
0000-03ff), which fails under rootless X withxf86EnableIO: Operation not permitted -
Works everywhere KMS works: The modesetting driver supports all hardware with a working kernel DRM/KMS driver—which includes Intel (2008+), AMD (2012+), NVIDIA via nouveau, and VM graphics (QXL, virtio-gpu)
-
Simpler, more maintainable: Legacy DDX drivers are effectively unmaintained; modesetting is what modern Xorg development targets
-
No privilege escalation needed: Modesetting uses only DRM/KMS ioctls, never raw I/O—works cleanly with logind/rootless X
| GPU | Kernel Driver | Support |
|---|---|---|
| Intel HD/UHD (2008+) | i915 | ✅ Full |
| AMD Radeon (2012+) | amdgpu | ✅ Full |
| AMD Radeon (older) | radeon | ✅ Full |
| NVIDIA (via nouveau) | nouveau | ✅ Basic |
| NVIDIA proprietary | nvidia | |
| QEMU/KVM VMs | virtio-gpu, qxl | ✅ Full |
| VirtualBox | vboxvideo | ✅ Full |
Note: NVIDIA proprietary drivers install their own X driver and config, which takes precedence. The modesetting approach doesn't interfere with this.
Purple Computer is keyboard-only. Mouse and trackpad input is disabled at multiple layers:
| Layer | How | Config |
|---|---|---|
| X.Org | MatchIsPointer "on" → Ignore "true" |
40-disable-pointer.conf |
| Textual | app.run(mouse=False) |
purple_tui.py |
The X.Org config (/usr/share/X11/xorg.conf.d/40-disable-pointer.conf) completely ignores all pointer and touchpad devices:
Section "InputClass"
Identifier "Disable all pointer devices"
MatchIsPointer "on"
Option "Ignore" "true"
EndSection
Section "InputClass"
Identifier "Disable touchpads"
MatchIsTouchpad "on"
Option "Ignore" "true"
EndSection
This ensures kids can't accidentally click around or get confused by trackpad gestures.
Hold Escape for ~1 second to open the Parent Menu. Alternatively, hold the backslash key (\) for 3 seconds, which works on all keyboards including Macs where Escape may be unreliable.
Navigation uses explicit key handling (no focus system):
| Key | Action |
|---|---|
| ↑/↓ | Move selection |
| Enter | Activate selected item |
| Escape | Close menu |
Ask mode is a calculator that understands math, emojis, and colors. It's designed to be maximally permissive: always try to do something meaningful.
| Input | Output |
|---|---|
2 + 2 |
4 with dot visualization |
3 * 4 + 2 |
14 (operator precedence preserved) |
cat |
🐱 |
3 cats |
🐱🐱🐱 |
cat * 5 |
5 cats + 🐱🐱🐱🐱🐱 (with label) |
cat + dog |
🐱 🐶 (space between different types) |
cats |
🐱🐱 (bare plural = 2) |
red + blue |
Purple swatch (color mixing) |
3 + 4 + 2 bananas |
9 bananas (numbers attach to emoji) |
apple + red + green |
Two lines: inputs then mixed result |
- Operator precedence preserved:
3 * 4 + 2 = 14, not 18 - Numbers attach to emojis:
3 + 4 + 2 bananas = 9 bananas - Labels for computed expressions: Shows "14 dogs" above the emojis when math was involved
- Spaces between different emoji types:
cat + dog→🐱 🐶 - Colors mix like paint: Even with non-colors between them
- Typo tolerance: Long math expressions (3+ operators, 60%+ valid symbols) tolerate accidental keystrokes like
=(replaced with+) - Unknown text passes through:
my name is tavi apple→my name is tavi 🍎
Add ! anywhere in your input to hear it spoken aloud:
| Input | Behavior |
|---|---|
cat! |
Shows 🐱 and speaks "cat" |
2+2! |
Shows 4 and speaks "2 plus 2 equals 4" |
!red + blue |
Shows purple swatch and speaks "red plus blue equals purple" |
The ! is stripped from display. Works anywhere in the text (beginning, middle, end).
Other speech triggers:
sayortalkprefix:say catspeaks "cat"- Enter on empty input: repeats the last result ("say it again")
- Triggers at 2+ characters
- Common 2-letter words excluded (by, my, go, to, etc.) to prevent unwanted completions
- Space accepts the suggestion
For the full design philosophy, see guides/ask-mode-design.md.
Build machine:
- Docker installed and running
- 20GB free disk space
- Internet connection (for downloads)
- Any OS (Linux, macOS, NixOS)
Time estimate:
- Golden image: 10-15 minutes
- Ubuntu ISO download: 5-10 minutes (first build only)
- ISO remaster: 5-10 minutes
- Total: 20-35 minutes (first build), 15-25 minutes (subsequent)
cd build-scripts
./build-in-docker.shThis builds everything in Docker and outputs the ISO to /opt/purple-installer/output/.
Resume from a specific step:
./build-in-docker.sh 1 # Skip golden image, start from ISO remasterThe build uses an initramfs injection approach: we download the official Ubuntu Server ISO and inject a hook script into the initramfs. The squashfs is left completely untouched.
Script: 00-build-golden-image.sh
Creates a complete Ubuntu Noble Numbat system as a disk image using debootstrap.
Output: purple-os.img.zst (~1.5 GB compressed)
Contents:
- Ubuntu 24.04 minimal base
- Standard Ubuntu kernel (linux-image-generic)
- GRUB bootloader
- X11 + Alacritty + Purple TUI
- Python dependencies
This is the system that gets written to the target laptop's internal disk.
Script: 01-remaster-iso.sh
Downloads official Ubuntu Server 24.04 ISO and injects our hook into the initramfs.
Process:
- Download Ubuntu Server ISO (cached for subsequent builds)
- Mount and extract ISO contents
- Extract initramfs (using
unmkinitramfs) - Add hook script to
/scripts/init-top/ - Repack initramfs (maintaining concatenated cpio structure)
- Add payload files to ISO root (
/purple/) - Rebuild ISO with xorriso
Output: purple-installer-YYYYMMDD.iso (~4-5 GB)
Key insight: We only modify the initramfs. The squashfs, kernel, and boot stack remain completely untouched.
ISO structure (after remaster):
purple-installer.iso
├── casper/
│ ├── vmlinuz # Ubuntu kernel (untouched)
│ ├── initrd # MODIFIED: has our hook script
│ └── *.squashfs # Untouched (never mounted)
├── purple/ # NEW: our payload
│ ├── install.sh # Installer script
│ └── purple-os.img.zst # Golden image
├── boot/grub/
│ └── grub.cfg # Ubuntu's GRUB config (untouched)
├── [BOOT]/ # UEFI boot partition (untouched)
└── isolinux/ # BIOS boot (untouched)
The ISO is hybrid—bootable from USB stick or optical media, BIOS or UEFI, with Secure Boot support.
Every ISO is stamped with a version in /etc/purple-version, visible in the Parent Menu (hold Escape).
Version formats:
| Type | Example | Parent Menu shows | When |
|---|---|---|---|
| Semver | v1.0 |
Version 1.0 | Major releases (./release-iso.sh v1.0) |
| Date-time | v2026.03.30-1430 |
Build: Mar 30, 2026 | Regular releases (./release-iso.sh) |
| Dev build | build-abc1234-20260330 |
Dev build: abc1234 | No PURPLE_VERSION set at build time |
To stamp a specific version at build time:
PURPLE_VERSION=v1.0 ./build-in-docker.shIf PURPLE_VERSION is not set, the build uses the git short hash and date as a fallback.
Script: release-iso.sh
Uploads ISOs to Cloudflare R2 and updates redirect rules so /download.iso points to the new version.
./release-iso.sh # date-time version (v2026.03.30-1430)
./release-iso.sh v1.0 # semver for major releasesWhat it does:
- Generates SHA-256 checksums
- Uploads standard + debug ISOs to
releases/{version}/ - Updates Cloudflare redirect rules (
/download.iso→ versioned path) - Writes
latest.jsonwith version, checksums, and sizes
Required .env values (see .env.template): R2_BUCKET, R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, CF_API_TOKEN, CF_ZONE_ID, R2_CUSTOM_DOMAIN.
Cloudflare setup (one-time): setup-cloudflare-rules.sh configures cache rules (aggressive caching for /releases/*, bypass for /download*) and redirect rules. It runs automatically as part of release-iso.sh, or can be run standalone to set up cache rules without releasing.
Scripts: upload-early-access.sh, upload-pdfs.sh
The download page (early-access.html) and guide PDFs are uploaded separately from ISO releases.
just upload-early-access # Upload page + PDFs, purge cache
just upload-pdfs # Upload just the PDFs, purge cacheupload-early-access extracts installation (pages 1-2) and guide (page 3) PDFs from cards/purple.pdf, uploads everything, then purges the Cloudflare cache for index.html and both PDFs.
upload-pdfs does the same extraction and upload for just the PDFs, for when you update the cards without touching the page.
ISOs use versioned paths (releases/{version}/standard.iso), so each release has a unique URL that can be cached aggressively (1 day edge TTL). The /download.iso shortcut is a Cloudflare 302 redirect to the current version, with cache bypassed so it always resolves to the latest release.
index.html and PDFs don't have versioned filenames, so the upload scripts purge the Cloudflare cache after each upload. If the purge fails (e.g., missing CF credentials), a warning is printed but the upload still succeeds. Visitors may see stale content briefly until the cache expires naturally.
Both upload scripts share common helpers from r2-helpers.sh (env loading, R2 upload, PDF extraction, cache purge).
Recommended: Use the flash script (Linux)
The flash-to-usb.sh script provides safe USB writing with verification:
# One-time setup: whitelist your USB drive
./build-scripts/flash-to-usb.sh --list # Find your drive's serial/vendor/model
echo 'YOUR_SERIAL' >> .flash-drives.conf # Add to whitelist
# Or whitelist a whole vendor+model (handy when you have several of the same kind):
echo 'model:Verbatim/STORE N GO max=20G' >> .flash-drives.conf
# Flash the latest ISO
./build-scripts/flash-to-usb.shFeatures:
- Only writes to whitelisted drives (prevents accidental data loss)
- Rejects drives over 256GB (safety limit for flash drives)
- Verifies write by reading back and comparing SHA256 checksums
- Auto-detects most recent ISO in
/opt/purple-installer/output/
Manual method (Linux/macOS):
sudo dd if=/opt/purple-installer/output/purple-installer-*.iso \
of=/dev/sdX bs=4M status=progress conv=fsyncReplace /dev/sdX with your USB device (check with lsblk).
Windows: Use balenaEtcher or Rufus.
- Insert USB stick into target laptop
- Enter BIOS/UEFI boot menu (usually F12, F2, Del, or Esc during startup)
- Select USB device
- System boots into installer automatically
Note: Secure Boot can remain enabled on most systems.
Two-gate installation (10-20 minutes):
- USB boots (Ubuntu kernel loads initramfs)
- Gate 1: Hook script checks for
purple.install=1in kernel cmdline - Hook finds payload on boot device, writes runtime artifacts to
/run/ - Casper mounts squashfs, systemd starts
- Gate 2: Confirmation screen appears: "This will set up Purple Computer"
- User presses ENTER to confirm (ESC or timeout cancels)
- Installer detects internal disk (first non-USB, non-removable disk)
- Wipes disk and writes golden image via
zstdcat | dd - Sets up UEFI boot with 3-layer fallback strategy
- User removes USB and presses ENTER to reboot
User confirmation required. Gate 2 ensures explicit consent before any disk writes.
The system logs in automatically as the purple user. No password is needed. Purple Computer is an offline appliance, so there is no login screen or password prompt.
The golden image is built in step 0. To customize the installed OS:
Edit: build-scripts/00-build-golden-image.sh
Examples:
Add packages during debootstrap:
debootstrap \
--include=linux-image-generic,grub-efi-amd64,systemd,sudo,vim-tiny,neofetch \
noble "$MOUNT_DIR" http://archive.ubuntu.com/ubuntuCreate additional users:
chroot "$MOUNT_DIR" useradd -m -s /bin/bash myuser
echo "myuser:password" | chroot "$MOUNT_DIR" chpasswdInstall custom software:
cp -r /path/to/purple_tui "$MOUNT_DIR/opt/"
chroot "$MOUNT_DIR" systemctl enable purple-tuiRebuild:
./build-in-docker.sh 0 # Full rebuildThe golden image creates a simple two-partition layout (EFI + root). To customize:
Edit: build-scripts/00-build-golden-image.sh
Example—add swap partition:
# In partition creation section
parted -s "$GOLDEN_IMAGE" mkpart primary linux-swap 513MiB 4GiB
parted -s "$GOLDEN_IMAGE" mkpart primary ext4 4GiB 100%Purple Computer includes automatic power management to save energy. Two power states: awake and sleep face (no DPMS screen-off). Timers adapt to charger status and lid position.
The power button is kid-proofed to prevent accidental shutdowns:
| Gesture | What Happens |
|---|---|
| Tap (< 3s) | Shows shutdown confirmation with countdown. Tap again to confirm, or wait to cancel. |
| Hold (3s) | Shows "Bye!" and shuts down. Like a phone. |
| Hold (10s) | Hardware forced off (ACPI, always available). |
logind is configured to ignore the power button (HandlePowerKey=ignore). The TUI reads the power button via a separate evdev device (PowerButtonReader) and handles all the UX.
Closing the lid shows the sleep screen immediately and starts a 10-minute shutdown countdown. Opening the lid cancels the countdown but keeps the sleep screen visible, showing parents how long the lid was closed and when the computer will turn off. Any key press wakes back to normal.
Shuts down after 10 minutes of lid-closed time regardless of charger state.
Timings depend on charger state:
| Condition | Sleep Face | Auto Shutdown |
|---|---|---|
| On charger | 5 min idle | 60 min idle |
| On battery | 2 min idle | 10 min idle |
The sleep screen shows a power status message so parents understand what's happening: charger state, elapsed time, and when auto-shutdown will occur. On live boot (USB), it also notes that the USB is needed to restart.
Key files:
purple_tui/power_manager.py: Idle tracking, charger detection, shutdown timingspurple_tui/rooms/sleep_screen.py: Sleep UI (SleepScreen), shutdown confirm (ShutdownConfirmScreen), bye (ByeScreen)purple_tui/input.py:PowerButtonReaderandLidSwitchReader(evdev)purple_tui/purple_tui.py: Activity recording, lid/power button integration
Use demo mode for accelerated timings:
PURPLE_SLEEP_DEMO=1 python -m purple_tui.purple_tuiDemo timings: sleep face in 2-3 seconds, shutdown in 8-10 seconds (prints message instead of actual shutdown).
"debootstrap: error retrieving packages"
Network connectivity issue during build. Ensure Docker has internet access.
"No space left on device"
Free up space or use larger disk:
df -h /opt/purple-installer # Check usage
du -sh /opt/purple-installer/build/* # Find large files"Boot device not found" / No USB boot option
- Ensure USB was written correctly (use
flash-to-usb.shwhich verifies the write) - Try different USB port (USB 2.0 ports often more reliable)
- Check BIOS boot order includes USB
Installer hangs at boot (black screen)
Ubuntu's kernel should handle most hardware, but try:
- Wait longer (some hardware takes 30+ seconds to initialize)
- Try debug mode from boot menu
- Check BIOS settings for legacy boot options
"No target disk found"
The installer couldn't find an internal disk. Causes:
- Disk is USB-connected (shows as removable)
- NVMe not detected (rare with Ubuntu kernel)
Solution: Switch to tty2 for an emergency shell (see below), then:
lsblk # List all disks
cat /sys/block/*/removable # Check removable flagsInstallation hangs during disk write
- Bad disk sectors or failing hard drive
- USB stick interference
Check:
# Boot Ubuntu live USB separately
# Run disk check
sudo badblocks -v /dev/sda
# Check SMART status
sudo smartctl -a /dev/sdaPurple Computer runs on tty1. To get a root shell on tty2 for debugging:
From the keyboard (emergency escape hatch): Two options:
- Ctrl+Alt+F2: Immediate switch (standard Linux VT switching, reimplemented via evdev since the kernel's built-in version is disabled by Alacritty's K_OFF mode)
- Ctrl+\ (Ctrl+Backslash) held for 3 seconds: Same effect, alternative combo
Both release the evdev grab so tty2 receives keyboard input. Both work even when the TUI is completely frozen. Note: these only work once the app's evdev handler has started (a few seconds after the purple screen appears).
From SSH: sudo chvt 2 (note: this does not release the evdev grab, so keyboard input will still go to the app. Use the keyboard methods instead.)
Returning to Purple Computer: Either press Ctrl+Alt+F1 (works because tty2 is in normal K_UNICODE mode), or hold Ctrl+Backslash for 3 seconds again. Both methods reacquire the evdev grab automatically when the app detects it is back on tty1.
Checking VT keyboard mode: sudo kbd_mode -C /dev/tty1 (will show "unknown" due to K_OFF).
System boots to grub rescue prompt
UEFI boot entries not created correctly.
Solution:
# Boot from Ubuntu live USB
# Mount target system
sudo mount /dev/sdX2 /mnt
sudo mount /dev/sdX1 /mnt/boot/efi
# Check if bootloader exists
ls /mnt/boot/efi/EFI/BOOT/
# If missing, copy from installer or reinstallScreen resolution wrong
X11 may not detect native resolution. See Graphics Stack.
Fix:
# List available modes
xrandr
# Set resolution
xrandr --output HDMI-1 --mode 1920x1080
# Make permanent (add to ~/.xprofile)
echo "xrandr --output HDMI-1 --mode 1920x1080" >> ~/.xprofileCan't open parent menu
Hold Escape for 1 second. If Escape doesn't work (e.g., Touch Bar Macs), try the backtick key (`, top-left corner) which acts as an Escape alias. As a last resort, hold the backslash key (\) for 3 seconds.
Build system:
/opt/purple-installer/
├── build/
│ ├── purple-os.img.zst # Golden image (compressed)
│ ├── ubuntu-24.04.1-live-server-amd64.iso # Cached Ubuntu ISO
│ └── remaster/ # Remaster working directory
│ ├── iso-contents/ # Extracted ISO
│ └── initrd-work/ # Extracted initramfs
└── output/
└── purple-installer-YYYYMMDD.iso # Final ISO
Source files:
build-scripts/
├── 00-build-golden-image.sh # Ubuntu base system (debootstrap)
├── 01-remaster-iso.sh # Remaster Ubuntu Server ISO (initramfs injection)
├── build-all.sh # Orchestrator (2 steps)
├── build-in-docker.sh # Docker wrapper
├── clean.sh # Clean all build artifacts
├── validate-build.sh # Pre-build validation
├── flash-to-usb.sh # Safe USB writing with verification
└── install.sh # Installation script (runs in initramfs)
.flash-drives.conf # USB drive whitelist (gitignored, user-specific)
.flash-drives.conf.example # Template for whitelist
Installed system:
/boot/
├── vmlinuz-* # Ubuntu kernel
├── initrd.img-* # Ubuntu initramfs
└── grub/ # GRUB config
/home/purple/ # User home directory
/etc/hostname # purplecomputer
Tested configurations:
- ThinkPad T/X/L series (2010-2020)
- Dell Latitude (2012+)
- HP EliteBook (2013+)
- Microsoft Surface Laptop (2017+)
- MacBook Air/Pro (2013+, Intel models)
Supported storage:
- SATA (AHCI controller)
- NVMe (modern SSDs)
- USB (for installer boot)
Secure Boot:
- Works on most systems: shim + GRUB + mmx64.efi signed boot chain
- Binaries updated to latest Ubuntu versions at build time (avoids SBAT revocation)
- No MOK enrollment required
| Component | Size |
|---|---|
| Golden image (compressed) | ~1.5 GB |
| Live filesystem (squashfs) | ~2.5 GB |
| Final ISO | ~3-4 GB |
| Installed system | ~5-6 GB |
USB Boot (BIOS/UEFI)
↓
Bootloader (ISOLINUX/GRUB+Shim)
├─ "Install Purple Computer" → purple.install=1 (default)
└─ "Debug Mode (no install)" → no arming flag
↓
Load Ubuntu Kernel + Initramfs
├─ MODULES=most initramfs
├─ Comprehensive driver support
└─ Modified initramfs with Purple hook
↓
═══════════════════════════════════════════════
GATE 1: DESIGN-TIME ARMING
═══════════════════════════════════════════════
Initramfs init-top scripts run
├─ udev starts (devices available)
└─ Purple hook: /scripts/init-top/01_purple_installer
├─ Check cmdline for purple.install=1
├─ If NOT ARMED: exit, Gate 1 closed
├─ If ARMED: scan for /purple/payload
│ ├─ If found: write /run/purple/armed
│ │ write /run/systemd/system/purple-confirm.service
│ │ Gate 1 passed → continue to casper
│ └─ If not found: exit, Gate 1 closed
↓
Casper mounts squashfs → systemd starts
↓
═══════════════════════════════════════════════
GATE 2: RUNTIME USER CONFIRMATION
═══════════════════════════════════════════════
purple-confirm.service runs (if /run/purple/armed exists)
├─ Shows confirmation screen
├─ ENTER → Gate 2 passed → run install.sh
├─ ESC → Gate 2 closed → reboot
└─ Timeout → Gate 2 closed → reboot
↓
(Only if both gates passed)
install.sh writes golden image to disk
↓
First Boot (Installed System)
└─ Ubuntu 24.04 + Purple TUI
- README.md: Quick start and overview
- MANUAL.md: This file (complete reference)
- guides/architecture-overview.md: High-level explanation of the two-system design and design rationale
- guides/ubuntu-live-installer.md: Technical deep-dive on ISO remaster architecture
- guides/ask-mode-design.md: Ask mode evaluation philosophy and behavior reference
💜