FreeBSD graphical installer

Design study · Ondra Knežour with Claude · July 2026 · rev 8

A native graphical installer for FreeBSD that runs directly on the vt(4) console framebuffer — no Xorg, no Wayland, no ports dependencies. Everything below is renderable with software primitives on a memory-mapped linear framebuffer, using only what the FreeBSD base system provides.

Why this exists

FreeBSD's vt(4) console has no userland graphics story. The old vgl(3) library is hard-wired to the deprecated sc(4) driver — it sets VGA/VESA modes through syscons-specific ioctls that vt never implemented, and sc(4) itself now carries a formal deprecation notice (no UEFI, no UTF-8, Giant-locked). Meanwhile bsdinstall still greets users with a 1980s ncurses dialog.

What vt(4) does offer is enough: the console tty answers fbio ioctls (FBIO_GETINFO, FBIO_GETLINEWIDTH) and lets a process mmap() the framebuffer — exactly the interface Xorg's scfb driver uses. Add KDSETMODE KD_GRAPHICS to stop the console scribbling over your pixels, kbio(4) raw keyboard, and /dev/sysmouse, and you have a complete platform for a fixed-mode graphical application in the base system.

Technical ground rules

These constraints shaped every visual decision in this document.

The video mode is fixed. vt(4) inherits whatever mode the loader set (EFI GOP) and cannot change it — after ExitBootServices() nothing in the base kernel can reprogram the display. The layout therefore targets ~1024×768 minimum and scales its grid to whatever fbio reports. Below that, fall back to dialog-based bsdinstall.

One optional mode change. If the user opts into loading a KMS driver (screen 02), the framebuffer address, stride, and resolution all change once. The renderer is built around "the mode can change exactly once": tear down, re-query fbio, re-mmap, redraw.

Everything is a software primitive. Filled rectangles and horizontal spans are the atoms. Rounded corners are precomputed per-radius span tables. Hard shadows are offset gray rects; soft shadows are a small precomputed 1D blur kernel blended once per redraw. Alpha blending (read-modify-write) is affordable because the screen redraws once per keypress, never per frame.

Assets are embedded at build time. Logos and icons are converted from SVG/PNG to raw alpha bitmaps during build and linked into the binary — zero runtime decoders, zero file I/O. If dynamic images are ever needed, a ~500-line PNG-subset decoder on top of base libz is the fallback plan.

Keyboard first, mouse welcome. Every screen is fully operable with Tab/Space/Enter (kbio raw mode); sysmouse is a bonus. Focus is shown by inversion and accent borders — states that are trivial to redraw.

Text needs a real font. Spacious layouts die with the 8×16 console font. The plan is a bundled bitmap font (Terminus/spleen class, or a pre-rasterized atlas of a permissively-licensed face) at two or three fixed sizes: 22–26px headings, 14px body, 11–12px captions.

Design language

The palette is deliberately small — eight solid colors cover the whole installer. Warm off-whites instead of clinical gray, FreeBSD red as the single accent doing triple duty: brand, selection state, and primary action.

#f7f6f2 canvas #ffffff cards #e2dfd7 borders #a02c2c accent #26221e text #7a746a secondary #e6ede2 success tint #f8ecd9 warning tint

Corner radii are 8px for cards and buttons, 3px for checkboxes, 4px for badges and progress bars — enough to feel modern, square enough to feel like an OS tool (an earlier draft with pill-shaped badges read as a web app and was rejected). Shadows appear in exactly two places: the selected/focused card and the primary button. Icons are Tabler-style outlines (MIT), used only where they speed recognition: device categories, driver sources, navigation arrows. Decoration is limited to the FreeBSD sphere — large on first/last screens, 16px in every header (an earlier draft's faint corner circles were cut: less is more). Every step screen carries a progress strip in the header's top-right corner: the step name over a row of nine small rectangles, filled up to the current step.

The layout contract, held on every screen: one question per screen with a 22px title and one-line explanation; 48px gutters; Back bottom-left, primary action bottom-right, skip/escape paths in the middle as plain text; the primary button always says what it will actually do ("Load from media and download 14 MB", "Erase ada0 and install" — never "OK" or "Finish").

00 · Splash (fallback state, usually never seen)

Honesty pass: by the time the installer process runs, kernel device probing, devd/devmatch, and usually the DHCP lease are already finished — a splash narrating them would be theater. The slow stretch a user actually experiences is earlier (loader + kernel + memory-disk loading from USB), and that is covered by a static image the existing boot chain already provides: the graphics-capable loader with a logo and boot_mute to hide the dmesg scroll. The installer itself therefore boots straight to the welcome screen and runs its own startup work asynchronously — tasting disks, walking pciconf, starting the Wi-Fi scan, waiting out a straggling lease, querying NTP for the clock heuristic. Welcome needs none of those results, and by the time a human picks a language the probes are long done.

This screen survives only as a fallback for the rare case where the installer genuinely cannot proceed yet (slow USB settle, missing lease it must have) — its task line then names the installer's own real work, never the kernel's. The console-map hint (log on console 2 via Alt+F2, emergency shell on console 3; vt(4) keeps console switching alive in KD_GRAPHICS mode via VT_PROCESS) repeats in the welcome footer so it isn't lost when this screen never shows.

FreeBSD 15.0
Installer is starting
Probing hardware…
Detailed log runs on console 2 — press Alt+F2 to view it, Alt+F1 to return here.
Splash — fallback wait state only; most boots never render it.

01 · Welcome

First contact sets the tone: one obvious action and quiet escape hatches to the live system and rescue shell that experienced users look for on install media. Language and keymap moved to the next screen, where location prefills them — welcome stays action-only. The progress strip starts here: nine rectangles, one filled. The small print carries the console map (log on Alt+F2, shell on Alt+F3), since the splash that used to teach it rarely renders at all.

FreeBSD installer
Welcome
Welcome to FreeBSD 15.0
A few questions and you'll be up and running.
Install FreeBSD
Live system  ·  Rescue shell
Log on console 2 (Alt+F2) · shell on console 3 (Alt+F3)
Welcome — action-only, escape hatches visible, progress strip begins.

02 · Location and language

One location choice drives three prefills: time zone, language, and keymap, each marked "auto" until the user overrides it (an override clears the badge and pins the value). The live clock makes the time zone verifiable at a glance. Clock synchronization is no longer asked here — it's simply the ntpd entry on the services screen. This replaces the split welcome-dropdowns + late-timezone arrangement; the split variant is kept at screen 11 for comparison. The language list only offers what the installer can honestly deliver (font coverage ∩ existing catalogs); system locales are a larger set, so when they diverge — Slovak system, no Slovak installer catalog yet — an inline warning appears with a second selector for the installation UI only, shown in the figure.

The hardware-clock row answers bsdinstall's "is the CMOS clock set to UTC?" question without asking it as a riddle. Once NTP time is available (servers courtesy of DHCP option 42 or the pool), the guess is mechanical: RTC within a couple of minutes of UTC means UTC; RTC matching UTC plus the local offset means localtime — the classic dual-boot-with-Windows signature. The offset-zero case is ambiguous and defaults to UTC, as does the no-network case. Like every guessed value on this screen it wears an "auto" badge and is freely editable, because no heuristic survives all the machines people actually own.

FreeBSD installer
Location
Where are you?
Pick your location — time zone, language and keymap follow. Adjust anything we guessed wrong.
Location
Czechia ▾
Time zone auto
Europe/Prague ▾
Language auto
Čeština ▾
Keymap auto
Czech (QWERTZ) ▾
Current timeTue 2026-07-07 14:32 CEST
Hardware clock storesUTC ▾ auto
Slovenčina isn't translated in the installer yet. Your system will use Slovak — choose a language for the installation itself:
Installer language
Čeština ▾
BackContinue
Location — one choice prefills three, "auto" badges show what was guessed.

03 · Hardware drivers

The installer's headline feature — and it builds on machinery the boot already ran. Since FreeBSD 12, devd(8) fires devmatch(8) at boot: it compares each unattached device's PNP info against the linker hints of modules on the media and kldloads whatever matches. That is how em and snd_hda in the last row attached "automatically." The installer's own pass covers the three cases devmatch cannot: kmods that live outside base (drm-kmod from FreeBSD-ports-kmods, built against the RELEASE GENERIC kernel so the ABI matches), devices whose driver needs firmware fetched separately (what fwget(8) does for wifi and GPUs), and devices with no FreeBSD driver at all — listed last, so the user learns now that the fingerprint reader will need extra action or won't work, rather than discovering it post-install. Each actionable card states where the driver comes from and what a download costs; the primary button repeats the plan verbatim. Status lines are written as plain sentences, pluralized at runtime (“this device was” / “these devices were”) — the devmatch machinery stays behind the curtain.

Sequencing is deliberate: this happens before any disk writes, so if a KMS driver hangs the machine the user reboots and declines, having lost nothing. Loading i915kms switches vt to the KMS framebuffer — the one sanctioned mode change. Accepted drivers are also written to the target system's rc.conf (kld_list), so the installed system boots with working graphics and Wi-Fi from the first start. A monitor icon stands for the GPU — people associate graphics with the screen, not with a chip hidden in the case. A text-only variant of the cards is shown below for comparison; the icons are embedded bitmaps either way, so the cost difference is zero.

FreeBSD installer
Hardware
Hardware drivers
Optional drivers found for your hardware. Nothing is written to disk yet.
Intel UHD Graphics 620 Internet · 14 MB
i915kms — resolution switching, smoother rendering. From FreeBSD-ports-kmods repository.
Realtek RTL8821CE Wi-Fi On install media
rtw88 driver and firmware — enables Wi-Fi during installation.
These devices were identified automatically and their drivers are already loaded: Ethernet (em), audio (snd_hda).
2 devices without a driver
Goodix fingerprint reader — USB 27c6:5395
O2 Micro SD/MMC card reader — PCI 1217:8621
FreeBSD has no driver for these devices. Everything else will work; check the wiki for their status.
Skip, use safe defaultsLoad from media and download 14 MB
Hardware drivers — source and cost per driver, action button states the plan.
FreeBSD installer
Hardware
Hardware drivers
Optional drivers found for your hardware. Nothing is written to disk yet.
Intel UHD Graphics 620Internet · 14 MB
i915kms — resolution switching, smoother rendering. From FreeBSD-ports-kmods repository.
Realtek RTL8821CE Wi-FiOn install media
rtw88 driver and firmware — enables Wi-Fi during installation.
✓ These devices were identified automatically and their drivers are already loaded: Ethernet (em), audio (snd_hda).
Skip, use safe defaultsLoad from media and download 14 MB
Variant — the same screen without category icons, for comparison.

03b · Display (only after a KMS driver loads)

This screen exists only on the path where the user opted into a KMS driver — skip the driver and it never appears. It does two jobs: confirms the risky step worked (the green banner is the payoff for the opt-in), and exposes the capability the driver just unlocked — resolution switching, which plain vt/efifb could not do. The mode list comes straight from the DRM connector's mode enumeration. "Native" is the monitor's preferred mode from its EDID block, which also yields the vendor (PNP ID, e.g. GSM → LG), the model-name descriptor, and — via DRM — the connector it hangs on. Hence the monitor line: make "native" mean "native for this LG on HDMI-1", not an abstract label. None of this is visible without KMS — one more quiet argument for the opt-in.

Changing modes uses the monitor-OSD safety pattern: the new mode applies immediately and reverts automatically after 10 seconds unless confirmed, because a failed modeset leaves an unreadable screen and Back buttons don't help when you can't see them. For the renderer this reuses the same teardown/re-query/re-mmap path the driver load already exercised — "the mode can change once" simply generalizes to once per confirmed user action. The chosen mode is also written to the installed system's loader.conf (kern.vt.fb.default_mode), so the console comes up in it from the first boot.

FreeBSD installer
Hardware · Display
Display
Pick a resolution for the installer and the console of the installed system.
i915kms loaded — Intel UHD Graphics 620 is now driving the screen.
LG ULTRAWIDE 29" — connected via HDMI-1GSM · EDID
1920 × 1080
60 Hz · monitor native
1600 × 900
60 Hz
1366 × 768
60 Hz
1280 × 1024
60 Hz
1280 × 720
60 Hz
1024 × 768
60 Hz
A new resolution applies immediately. If the screen stays readable, confirm within 10 seconds — otherwise it reverts on its own.
BackContinue
Display — driver success, the monitor identified from EDID, and “native” anchored to that monitor.

04 · Network — interfaces

Interfaces appear as cards with live state (link, lease) rather than as a bare device list; the common case — wired DHCP — is already done by the time the user arrives, and the screen just confirms it. The interface part is skippable, but the screen itself never disappears: on a machine with no usable NIC it degrades to the hostname question alone, because a hostname is needed with or without a network.

The hostname is prefilled from whatever the environment offers, best first: DHCP option 12 (host-name) if the server assigns one, a reverse-DNS PTR lookup on the leased address, and finally a generated freebsd-XXXX from the machine's hostid — always editable, the "auto" badge clearing on first keystroke. DHCP is generous beyond addresses, and the installer should harvest all of it: DNS servers (option 6), domain and search list (15, 119) to complete the FQDN, and NTP servers (42), which feed the clock display on the location screen and the UTC-vs-localtime detection described there.

FreeBSD installer
Network
Network
Two interfaces found. Pick one to configure now, or skip.
Hostname auto — from DHCP
knezour-nb.local
em0 — EthernetLink up
DHCP · 192.168.1.42 / 24 · gateway 192.168.1.1
Adjust addresses…
wlan0 — Wi-Fi (RTL8821CE)Not connected
Select network…
BackSkip for nowContinue
Interfaces — live state on cards, wired DHCP already done, step skippable.

05 · Network — Wi-Fi scan

Entered from a wireless interface. Networks sort by signal; each row shows security (lock icon + WPA badge, explicit "Open" pill for unsecured) and band. Selecting a row expands it in place for the password — no separate dialog, keeping the one-surface model that both keyboard focus handling and the renderer prefer. Signal bars are four rects; the whole row costs nothing to draw. The regulatory domain — which channels and power levels the radio may legally use — is prefilled from the location screen's country and applied via the wlan regdomain machinery; it stays editable for the traveler installing on hardware bought elsewhere.

FreeBSD installer
Network · Wi-Fi
Wi-Fi networks Rescan
wlan0 (Realtek RTL8821CE) — 5 networks found.
ChalupaWPA2 · 5 GHz
Password••••••••
Connect
TP-LINK_5D42WPA2 · 2.4 GHz
Vodafone-B21AWPA3 · 5 GHz
CafeGuestOpen2.4 GHz
Regulatory domain auto — from location
CZ — Czechia ▾
BackSkip Wi-Fi, use Ethernet only
Wi-Fi scan — inline password on the selected row, no modal dialogs.

06 · Network — addresses

Per-interface IPv4 and IPv6, each independently DHCP/SLAAC, static, or off — both protocols optional, with one guard sentence explaining when at least one is needed. The automatic options show their live result inline, so "DHCP" is a verifiable fact, not a hope. Static fields use the monospace face; addresses are the one place alignment aids reading.

FreeBSD installer
Network · Addresses
Addresses for wlan0
Both are optional, but at least one is needed to download packages.
IPv4
DHCPStaticOff
Lease obtained — 192.168.1.87/24, gateway 192.168.1.1, DNS 192.168.1.1
IPv6
SLAACStaticOff
Address / prefix
2001:db8:2:1::7/64
Gateway
fe80::1
BackContinue
Addresses — v4 and v6 independent, automatic modes show their live result.

07 · Disk setup

Guided ZFS is the recommended default and the selected card carries all the detail: a proportional partition preview (efi / swap / zroot as colored spans — squared corners; an early pill-shaped draft looked like a toy) and the resulting layout in words. UFS and manual partitioning stay one keypress away but visually quiet. Encryption is a single checkbox here; the passphrase is collected on commit.

The target disk is identified by everything CAM/GEOM knows about it — device name, vendor, model, capacity, and serial number (diskinfo -v / CAM inquiry data). On a machine with three visually identical SSDs the serial is the only string the user can physically match against the label on the drive, so it appears here at selection time and is repeated verbatim in the final warning on the summary screen.

FreeBSD installer
Disk
How should the disk be set up?
The guided options handle partitioning for you.
ada0 — Samsung SSD 870 EVO · 238 GB
serial S5Y1NG0N409762K
Change disk…
Guided — ZFS on rootRecommended
Snapshots, checksums, optional encryption.
efi 260 MB · swap 8 GB · zroot 230 GB
Encrypt disk (GELI)
Guided — UFS
Manual partitioning
BackContinue
Disk setup — recommended default carries the detail, alternatives stay quiet.

07b · ZFS pool layout (multiple disks)

Appears only when guided ZFS is chosen and more than one disk exists. The layout choice is the important decision, so each option states its consequence in usable capacity and failure tolerance — computed live from the checked disks — instead of assuming the user knows what raidz1 trades away. Layouts the disk count can't support (RAID-Z2 with three disks) stay visible but disabled, teaching what would unlock them. Every disk row carries the full identity again — model and serial — because this is exactly the screen where picking the wrong physical disk hurts. Pool name, per-disk swap, and 4K-sector forcing map straight to bsdinstall's zfsboot options.

FreeBSD installer
Disk · ZFS pool
ZFS pool layout
Three disks found. Redundancy costs capacity — usable space shown per layout.
Stripe
~714 GB · no safety
Mirror
~238 GB · 2 may fail
RAID-Z1
~476 GB · 1 may fail
RAID-Z2
needs 4 disks
ada0 — Samsung SSD 870 EVO · 238 GBS5Y1NG0N409762K
ada1 — Samsung SSD 870 EVO · 238 GBS5Y1NG0N409881C
ada2 — Samsung SSD 870 EVO · 238 GBS5Y1NG0N410034D
Pool name
zroot
Swap per disk
4 GB ▾
Force 4K sectors
BackContinue
ZFS pool — consequences computed live, impossible layouts teach their requirements.

07c · Manual partitioning

The full gpart editor for people who know exactly what they want: scheme selection (GPT default), a proportional map, and a table where the selected row exposes Edit/Delete inline and free space exposes Add — the same interaction grammar as everywhere else, no modal dialogs. The critical property is staging: every operation edits an in-memory plan, and nothing touches the disk until the "Erase and install" commit at the very end of setup. The screen says so in its second sentence — worded carefully ("changes apply at the very end, after the final review") so nobody goes hunting for a confirm button on this screen. That single sentence turns a terrifying screen into a safe sandbox — with "Undo all" and an "Auto layout instead" escape for the user who wanders in by mistake.

FreeBSD installer
Disk · Manual
Manual partitioning — ada0GPT ▾
238 GB total · 12 GB free. Nothing is written to disk yet — changes apply at the very end, after the final review.
PartitionTypeMountSize
ada0p1efi/boot/efi260 MB
ada0p2freebsd-swap8 GB
ada0p3freebsd-zfszroot · Edit · Delete218 GB
free space+ Add partition…12 GB
Changes are staged: 1 partition resized. Undo all · Auto layout instead…
BackUse this layout
Manual partitioning — staged edits, inline row actions, honest escape hatches.

08 · Optional components

Distribution sets, reorganized by audience. lib32 stands alone; everything developer-facing (src, ports, debug symbols, tests) folds into a Development group with a selection count — progressive disclosure so a first-time user sees one decision, not six, while a developer opens the group once. Base system is shown but locked; hiding it entirely would invite "where's the OS?" doubts.

FreeBSD installer
Components
Optional components
The base system is always installed.
Base system
Required
lib32
32-bit compatibility libraries
Development
Sources, ports, debugging
1 of 4 selected
System sources /usr/src
Ports tree /usr/ports
Debug symbols base-dbg, kernel-dbg
Test suite /usr/tests
BackContinue
Components — developer sets grouped behind one expandable row.

08b · Installation source

Two decisions live here. First, since FreeBSD 15.0 the base system itself comes in two formats: packages (pkgbase), managed entirely by pkg(8) afterwards, or the legacy distribution sets, updated via freebsd-update(8) — kept for the 15.x lifetime and planned for removal in time for FreeBSD 16 (open-source schedules being what they are); either way pkgbase is the honest default for new installs. Second, where the bits come from: memstick and disc1 images don't carry every set, and a desktop choice always means packages from the network. When everything selected is on the media, this screen never appears; when something is missing, it shows exactly what comes from where and what the download costs, with the mirror chosen automatically by probing a handful of candidates and picking the fastest — overridable for the user behind a corporate mirror. Downloads are checksummed against the release manifests and resume on failure.

FreeBSD installer
Components · Source
Installation source
How should the base system be installed and where from?
Packages (pkgbase)Recommended
Updated with pkg(8), component by component
Distribution sets
Legacy base.txz — planned for removal in FreeBSD 16
From install media
base, kernel, lib32
ready
From network mirror
src, ports · 890 MB, checksummed, resumes on failure
download
Mirror auto — fastest of 6 probed
https://download.freebsd.org ▾
BackContinue
Installation source — pkgbase vs distribution sets, and origin and cost of everything.

09 · Graphical environment

Desktop selection during install — the single biggest quality-of-life gap versus Linux installers, and cheap once the network step exists: it's pkg install into the target plus rc.conf wiring (SDDM/GDM, dbus). Real project logos (KDE, GNOME, Xfce — MIT-licensed single-path versions from the simple-icons set) rendered white on each project's brand color: recognizable at a glance but visually uniform, so no card shouts. MATE uses a placeholder leaf in the same treatment until a proper mark is sourced. Console-only is a first-class card, not a buried opt-out, and each choice shows its download cost. MATE and LXQt use placeholder glyphs in the same treatment until proper marks are sourced. Trademark note: shipping project logos in an official installer needs the usual permission round with each project.

FreeBSD installer
Desktop
Graphical environment
Installed from packages over the network. You can always add more later.
>_
None
Console only
KDE Plasma
~2.1 GB
GNOME
~2.4 GB
X
Xfce
~900 MB
MATE
~850 MB
LXQt
~600 MB
Minimal X/Wayland
Pick your own WM
BackContinue
Desktop — real logos, uniform treatment, console-only as an equal citizen.

10 · Accounts

Only the root password is required. Creating a user is an explicit, optional action behind a real button — a deliberate rejection of the Debian-style forced user that gets created, used once, and deleted. The SSH-for-root toggle lives here because it's the first question a headless-server admin asks; it defaults off. The hint names adduser(8) so nobody fears this is the only chance.

FreeBSD installer
Accounts
Set the root password
This is the only account the system needs.
Root password
••••••••••
Confirm
Repeat password
Allow root login over SSH
You can also run adduser(8) any time later. Add a user account
BackContinue
Accounts — root required, user creation an explicit optional button.

11 · Time zone (variant B — split flow)

Kept for comparison: the traditional arrangement where welcome asks language/keymap and time zone is its own late step. If the combined location screen (02) is adopted, this screen disappears entirely and its ntpd checkbox is absorbed by the ntpd entry on the services screen — one fewer step, no lost information.

FreeBSD installer
Time zone
Time zone
Guessed from your keymap and network — change it if we got it wrong.
Region
Europe ▾
Zone
Prague ▾
Current timeTue 2026-07-07 14:32 CEST
Keep clock synchronized over the network (ntpd)
BackContinue
Time zone — guessed defaults, verifiable live clock, NTP on by default.

12 · Services

The four everyday services as cards with plain-language subtitles (sshd, ntpd, powerd, dumpdev — sensible defaults pre-checked; ntpd here is where clock synchronization from the location screen actually lives). The twelve hardening options collapse into a single expandable row with a "2 of 12" count: present and discoverable for operators, invisible noise for everyone else. Same accordion pattern as Development on screen 08 — one interaction model to learn, one widget to implement.

FreeBSD installer
Services
Services at boot
Start these automatically on the installed system.
sshd
Remote access
ntpd
Time sync
powerd
CPU power management
dumpdev
Kernel crash dumps
Security hardening
Hide other users' processes, randomize PIDs, disable core dumps…
2 of 12
BackContinue
Services — everyday choices in the open, hardening behind a counted expander.

13 · Summary

The last stop before anything irreversible. Every decision from the whole flow on one screen, each row selectable to jump back and change it. The warning box is the only red-tinted surface in the entire installer, reserving that alarm for the one moment it's true — and it repeats the disk's full identity (device, vendor and model, capacity, serial number), so on a multi-disk machine the user can physically verify the victim against the drive label. The commit button names it too: "Erase ada0 and install" — a user who clicks it cannot claim they weren't told. The footer carries a third, quieter button — "Save as install script…" — exporting every answer as a bsdinstall(8)-compatible installerconfig script: the graphical flow doubles as a script generator, so the second, third, and fortieth machine install unattended — and conversely the installer honors an existing script on boot, skipping straight to the summary.

FreeBSD installer
Summary
Ready to install
Review your choices. Select any row to change it.
Answers loaded from network — installerconfig for 3c:ec:ef:12:34:56 from pxe01.school.local. Review and confirm, or edit anything first.
Diskada0 Samsung SSD 870 EVO — ZFS on root, GELI encrypted
Driversi915kms, rtw88 — kept on installed system
Networkem0 — DHCP · wlan0 — Chalupa, DHCP + static v6
DesktopKDE Plasma (~2.1 GB download)
Accountsroot only, SSH login disabled
All data will be erased on ada0 — Samsung SSD 870 EVO, 238 GB, serial S5Y1NG0N409762K. Check the serial against the drive label if in doubt. This is the last step where you can turn back.
Back Save as install script…Erase ada0 and install
Summary — every choice editable, the destructive action named in full.

14 · Installing

One honest progress bar with a file count and time estimate, plus a task checklist so a stall is diagnosable at a glance ("stuck on base.txz" beats a frozen percentage). No slideshow marketing — this screen redraws on a timer tick and dtrace-style extraction callbacks, nothing more.

FreeBSD installer
Installing
Installing FreeBSD
Extracting base.txz — 1,204 of 1,946 files
62%about 2 minutes left
Checksums verified
Partitions created, ZFS pool zroot online
kernel.txz extracted
base.txz
· lib32.txz  ·  KDE Plasma packages
Progress — one bar, one task list, no theater.

15 · Done

Closes the loop the welcome screen opened — same centered composition. Two actions: reboot, or drop to a shell first for the people who want to tweak the new system before first boot.

FreeBSD installer
Done
Installation complete
Remove the installation media, then reboot into your new system.
Reboot now Open shell first
Done — mirrors the welcome screen, shell access for the curious.

Internationalization

The only translation system in FreeBSD base is catgets(3), the POSIX message-catalog API with gencat(1)-compiled .msg files. It works, costs nothing — and is why nothing gets translated: messages are addressed as set/number pairs rather than by their English text, there is no plural machinery, and no translator community works in the format. GNU gettext solves all of that but lives in ports as an (L)GPL dependency, which would undercut the installer's base-only premise. The plan therefore splits format from runtime: keep .po as the source format — where translators and the FreeBSD project's own Weblate instance already live — and replace the GNU runtime with a small BSD-licensed one.

The .mo format is genuinely trivial

A compiled .mo catalog is just a string-lookup table: a 28-byte header (magic 0x950412de, revision, string count N, and file offsets of two tables), followed by two parallel arrays of N (length, offset) pairs — the first for original strings, sorted bytewise, the second for their translations — and an optional precomputed hash table a reader is free to ignore. A complete reader is: map the file (or point at an embedded blob), sanity-check the header, binary-search the original-string table, return the translation at the same index. With bounds checking and the plural hook that lands around 200 lines of C, no allocations required. Plural entries add no format complexity: the original is stored as msgid NUL msgid_plural, the translation as all plural forms NUL-separated — the lookup stays identical, the caller just indexes into the result.

The even simpler variant fits this installer better: skip runtime files entirely. A build step (a ~100-line Python script replacing msgfmt, avoiding GPL tools in the build too) parses each .po and emits a sorted C array compiled into the binary — the exact pipeline the fonts and logos already use. Selecting a language on screen 02 then means swapping one pointer and redrawing; fallback is automatic because keys are the English strings themselves. Per-language cost is a few tens of kilobytes.

Prior art: citrus libintl

None of this is speculative. NetBSD has shipped a BSD-licensed libintl for two decades — written from scratch as part of the Citrus project — that reads GNU .mo files with a gettext()-compatible API. It is the existence proof that the format is an interface, not a GPL contract: the file format isn't copyrightable, only GNU's implementation is. Importing a citrus-style libintl into FreeBSD base (or contributing a fresh minimal one) would be a valuable side effect of this project — it would let other base utilities grow translations for free, and it has been idly wished for on the lists for years.

Plural rules — the only real work

Every .po header carries a C-expression describing the language's plural selection, e.g. Czech: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;. Two honest options. (a) Evaluate it: the expression grammar is tiny — integer arithmetic, comparisons, boolean ops, ternary — a recursive-descent evaluator is ~150 lines and handles every language Weblate will ever throw at it. (b) Hard-code a table of plural functions for the launch languages and grow it on demand. Option (a) is barely more work and never needs revisiting, so it wins. Czech is the useful forcing function here: with three forms ("1 soubor, 2 soubory, 5 souborů") it breaks every lazy n == 1 assumption on day one — an installer developed against Czech is plural-correct for most of Europe by construction. English-side discipline matters too: every countable UI string ("N files extracted", "N devices without a driver") goes through the plural API from the start, because retrofitting is miserable.

Fonts and glyph coverage

Because the installer rasterizes text itself, translation coverage is bounded by the font atlas, not the catalog. The base-first answer: FreeBSD's vt(4) console font is a Terminus BSD Console derivative (SIL OFL), shipped in base with Latin, extended Latin, Cyrillic, and Greek coverage — which covers all of Europe, and with it the Americas, Australia, and most of Africa essentially for free. That makes the honest launch scope "every language writable in extended Latin, Cyrillic, or Greek". Scripts absent from base fonts — CJK, Armenian, Georgian, Ethiopic, Arabic (which also needs shaping, a different problem entirely) — are deliberately out of scope for the prototype and are a community-level discussion about what belongs in base, not something one installer should solve alone.

One layer of honesty about these mockups: the HTML previews render in the viewer's system UI font (Segoe UI, Roboto, or similar) — a proportional face that does not exist in FreeBSD base, where console fonts are all monospace bitmaps. The real installer has two options: embrace Terminus everywhere (monospace aesthetic, pure base pedigree), or bundle one permissively-licensed proportional face rasterized into the atlas at build time — DejaVu Sans (Bitstream Vera license, no copyleft, strong Latin/Cyrillic/Greek coverage) is the natural candidate. The mockups' look assumes the second; the first would still work, just with a more terminal flavor. Decision for the prototype: bundle the proportional face first — it's what the mockups promise — and keep the pure-Terminus route open as a documented fallback; the proof render below shows it holds up, with an honest terminal accent. The language list on screen 02 must be generated from the intersection of two sets: what the font atlas can draw, and which installer catalogs actually exist — offering a language the font can't draw or that would silently fall back to English is worse than not offering it.

There is a third set in play: system locales, which are far more numerous than installer translations will ever be. The two choices must be allowed to diverge. If the user picks, say, Slovak — a perfectly good system locale (sk_SK.UTF-8) with no installer catalog yet — the screen says so plainly and offers a second selector for the installation UI only: "Slovak isn't translated in the installer yet. Your system will use Slovak; choose a language for the installation itself." The installed system gets exactly what the user asked for, the installer runs in their second-best language instead of silently defaulting to English, and the warning doubles as a recruitment poster for the translation effort.

Installer UI elements rendered with FreeBSD base console fonts
Proof render — UI elements drawn pixel-by-pixel with fonts from FreeBSD base: Terminus BSD Console 8×16 (the vt(4) kernel default) for body text, Terminus Bold 16×32 for the heading. Czech diacritics, arrows, and the interpunct all come from the base fonts' own coverage. Produced by a bitmap compositor using only filled rectangles, rounded-corner spans, and glyph blits — the exact primitive set the real renderer will have.

Zero-touch and mass deployment

The script story scales past "save my answers". Because the installer both generates and honors installerconfig, the network-boot case composes from existing FreeBSD pieces: the machine PXE-boots (or UEFI-HTTP-boots) the installer image from the deployment server, and the installer then hunts for its configuration in a fixed order — a kernel environment variable set by the loader, a DHCP-supplied URL, then a URL template on the config server tried with progressively less specific names: by MAC (installerconfig-3c-ec-ef-12-34-56), by product serial, then a catch-all default — the same convention PXELINUX taught every lab admin decades ago.

What happens next is one policy knob in the config itself. Review mode boots straight to the summary screen, every answer prefilled and marked as network-loaded (see the summary figure), waiting for a human to press the one button — right for a technician walking a row of machines. Auto mode confirms by itself after a visible countdown and reports progress to a syslog server or HTTP webhook — right for the forty-seat school lab where the configs were generated from a spreadsheet of MACs and hostnames by a fifty-line script. Per-machine differences (hostname, static address, disk layout by machine class) are just fields in the generated files; everything else is identical. The graphical installer's role in this story is mostly to get out of the way — but its summary screen is what makes the semi-attended variant humane, because the technician sees exactly what this machine is about to become before it happens.

FreeBSD installer
Network install
Unattended installation
Configuration for this machine was loaded from pxe01.school.local.
This machine3c:ec:ef:12:34:56 → lab-ucebna2-07
Diskada0 — Kingston A400, 240 GB · will be erased
SystemFreeBSD 15.0 · pkgbase · Xfce · cs_CZ.UTF-8
7
Installation starts automatically
Press any key to pause the countdown.
Cancel — boot from local diskReview settings…Install now
Auto mode — machine identity, the one destructive fact, and a pausable countdown.
FreeBSD installer
Network install · Done
lab-ucebna2-07 installed
Result reported to deploy.school.local · 14 min 12 s · no warnings
Rebooting into the new system in 5 seconds…
Auto mode, finished — hostname as the headline, result reported, reboot self-timed.

Design notes on the countdown screen: it shows only what a passing technician needs — which machine this is (MAC and assigned hostname), the one destructive fact (which disk dies, with model), and how to interrupt. Any key pauses; "Review settings…" opens the ordinary summary in review mode; cancel falls back to local boot. The countdown card reuses the selected-card accent, making it the obvious focal point on a screen someone may glance at from two meters away. The completion screen leads with the hostname — in a room of forty identical boxes, "which one is this" is the only question — and reports success or warnings to the deployment server before rebooting itself.

Parity with bsdinstall

A checklist against everything the current installer can do, so nothing is silently dropped in the name of polish. Covered by design: keymap/language (02), hostname (04 — asked even with no NIC), network including wireless with WPA and regulatory domain (04–06), guided UFS and ZFS with multi-disk topologies, pool options and GELI (07, 07b), manual gpart partitioning (07c), distribution sets and pkgbase (08, 08b), fetching missing sets from a mirror (08b), root password and optional users (10), time zone and UTC/localtime CMOS handling (02), services and hardening options (12), crash-dump device (12), final-stage shell access (15), live system and rescue shell (01), and scripted installs (13 — both generating and honoring installerconfig).

Consciously beyond bsdinstall: driver detection with opt-in kmod loading (03), resolution switching (03b), desktop environment installation (09), and the unmatched-devices report (03). Consciously not replicated: the ncurses aesthetic.

How we got here — decision log

vgl(3) was ruled out early. It only works on deprecated sc(4); vt(4) never implemented its mode-setting interface. Instead: fbio + mmap + KDSETMODE, the scfb approach. A vt/fbio backend for libvgl remains a worthwhile upstream contribution — it would remove the last practical reason to keep sc(4) alive.

Fixed mode, embraced. Runtime mode switching would require KMS drivers that live in ports, not base. Rather than fight this, the design targets the loader-set mode and offers drm-kmod as an opt-in (screen 02), placed before disk writes so a driver hang costs nothing.

Style evolved through three drafts. Draft one was a period-correct beige dialog (rejected: cramped, joyless). Draft two went airy with rounded cards and soft shadows once we established both are cheap in software (corner span tables, precomputed blur kernels). Draft three squared off badges and bars that read as too web-like, swapped abstract DE icons for real logos, made the GPU icon a monitor, and turned implicit actions (add user) into explicit buttons.

Structural choices: one question per screen instead of bsdinstall's modal maze; a single location choice prefilling time zone, language, and keymap (with "auto" badges and a split-flow variant kept for comparison); a nine-segment progress strip in every header; root-only accounts with optional user creation; developer distribution sets and hardening options behind counted accordions; every download labeled with source and size; disks identified by vendor, model, capacity, and serial at selection time and again in the final warning; every destructive or network action named on its button.

Assets: all images (FreeBSD sphere, DE logos, icon glyphs) are compiled into the binary as raw alpha bitmaps at build time — no runtime decoders. Icon set: Tabler (MIT). DE logos: simple-icons single-path versions (the set is CC0; the marks themselves remain each project's trademark and need permission for official use). If runtime image loading is ever needed, the plan is a small PNG-subset decoder (~500 lines) over base libz, or QOI-converted assets.

This document renders each screen in HTML for review convenience; the layouts intentionally use only constructs (solid fills, rounded rects, alpha-blended bitmaps, two font sizes per screen) that map one-to-one onto the planned framebuffer renderer. Icons load from a CDN — with no network they degrade to blank squares, everything else is self-contained.