Skip to content

IO-ZetZor/Visor-BootManager

Repository files navigation

Visor

A minimal, fast, modern, graphical UEFI boot manager.

Visor draws an icon-based boot menu which combines the efficiency and speed of grub with the beauty of refind, capable of booting Linux (EFI stub kernels / Unified Kernel Images) or chainloading other efi executables (including Windows Boot Manager) no external dependencies, no scripting bs — just a config and ur imagination.

Visor Menu

Features

  • Graphical, double-buffered menu — flicker-free rendering via the UEFI Graphics Output Protocol (GOP).
  • Fluid Animations — smooth animations for switching between entries.
  • Customizable UI — almost everything you see can be customized.
  • Auto-detection — if boot.conf is missing, Visor scans for common Linux and Windows loaders and builds a menu automatically.
  • Mouse & touch — pointer cursor with single-click-to-boot, when the firmware exposes a pointer device.
  • Secure Boot aware — verifies images through shim's SHIM_LOCK protocol when present, and refuses an unverifiable kernel under Secure Boot.
  • Self-pruning logsboot.log keeps only the last 3 boots, with descriptive messages at every fallible step for easy debugging.

Requirements

  • An x86_64 UEFI system
  • gnu-efi development files.
  • GCC and binutils (objcopy).
  • (Optional) Python 3 + Pillow — only to re-bake a different font; the default font is committed, so normal builds need neither.

Install dependencies

Distro Command
Arch sudo pacman -S gnu-efi base-devel
Debian / Ubuntu sudo apt install gnu-efi build-essential
Fedora sudo dnf install gnu-efi gnu-efi-devel gcc make
openSUSE sudo zypper in gnu-efi-devel gcc make
Void sudo xbps-install gnu-efi-libs gcc make

Quick Install

Installs the build tools for your distro, downloads Visor, builds it, and installs it — all in one go:

sh -c "$(curl -fsSL https://raw.githubusercontent.com/IO-ZetZor/Visor-BootManager/main/get.sh)"

(or with wget)

sh -c "$(wget -qO- https://raw.githubusercontent.com/IO-ZetZor/Visor-BootManager/main/get.sh)"

Building

make

This produces visor_x64.efi in the project root. The Makefile auto-detects the gnu-efi headers and crt0-efi-x86_64.o across distros; if yours installs them somewhere unusual, override the paths:

make GNU_EFI_INC=/path/to/efi CRT0=/path/to/crt0-efi-x86_64.o

If gnu-efi is missing, make stops early with an explanatory message instead of a cryptic compiler error.


Manual install

git clone https://github.com/IO-ZetZor/Visor-BootManager.git
cd Visor-BootManager
sudo ./install.sh

Options:*

Flag Effect
--esp <path> Use this ESP mount point instead of auto-detecting.
--no-build Install the already-built visor_x64.efi.
--boot-entry Register a Visor UEFI boot entry via efibootmgr.
--no-boot-entry Do not ask about creating a UEFI boot entry.
--sign Sign Visor with sbctl after installing.
--no-sign Do not ask about Secure Boot signing.
--fs-driver <path> Install an EFI filesystem driver into \EFI\visor\drivers\.
--no-cli Do not install the host-side visor command.
--cli-dir <path> Install the host-side command somewhere other than /usr/local/bin.
--force-config Overwrite an existing boot.conf with the bundled example.
After installing, **edit `<ESP>/EFI/visor/boot.conf`** to point at your real
kernels and set your root partition.

Visor CLI

The installer also adds a host-side visor command:

visor build
visor install --esp /boot/efi
visor update
visor sign --esp /boot/efi
visor status
visor config validate --esp /boot/efi
visor uninstall --esp /boot/efi
visor clean

visor install passes its arguments through to install.sh. visor sign signs the installed EFI binary. visor status reports the current install state. visor config validate checks the syntax of a config file.

Updating

After installation, update to the latest committed version from GitHub with:

visor update

The updater clones https://github.com/IO-ZetZor/Visor-BootManager into a local source checkout on first run, then uses git pull on later runs. It rebuilds Visor and runs install.sh --no-build without replacing your existing boot.conf. Use visor update --esp /your/esp/mount if ESP auto-detection is wrong, and visor update --sign if you want the updated binary signed with sbctl.

### Manual installation to ESP

```bash
# 1. Mount the ESP (skip if already mounted at /boot/efi).
sudo mount /dev/sdXn /mnt/esp

# 2. Copy the binary and assets.
sudo mkdir -p /mnt/esp/EFI/visor/icons /mnt/esp/EFI/visor/backgrounds
sudo cp visor_x64.efi              /mnt/esp/EFI/visor/
sudo cp assets/icons/*.png         /mnt/esp/EFI/visor/icons/
sudo cp assets/backgrounds/*.png   /mnt/esp/EFI/visor/backgrounds/
sudo cp boot.conf.example          /mnt/esp/EFI/visor/boot.conf

# 3. Register a boot entry (disk = whole disk, part = ESP partition number).
sudo efibootmgr --create --disk /dev/sdX --part n \
                --label "Visor" --loader '\EFI\visor\visor_x64.efi'

Configuration

Config lives at \EFI\visor\boot.conf on the ESP. A fully-commented reference is in boot.conf.example.

Path rules: all paths are relative to the root of the ESP and use back-slashes, e.g. \EFI\visor\icons\arch.png. Colors are #RRGGBB.

Global settings

Key Values / meaning
timeout N = auto-boot the default after N seconds · -1 = wait forever · 0 = boot default instantly (no menu)
default Index of the default entry (0-based).
show_names 1 = show entry names under icons · 0 = icons only.
text_menu 1 = use text-mode instead of a graphical menu. Fallback menu if firmware denies rendering
resolution Resolution at which the menu is rendered. native = default. max = highest resolution firmware offers. WxH = custom resolution, eg resolution=1920x1080.
quiet 1 = black screen during hand-off · 0 = show progress text.
autoboot 1 = skip the menu and boot immediately — the single entry if there's only one, otherwise the remembered/default= entry. Hold any key at power-on to force the menu. 0 = always show the menu.
center_info 1 = show selected entry details near the bottom. Path-only when show_names=1
entries_per_page Entries shown per page. Default 3.
title Menu title. Empty/absent = Visor · none = no title · else verbatim.
font Text font. Currently jetbrains. Empty = default.
theme Load themes/<name>.conf; its UI values override those in boot.conf. random = pick a random theme each boot · cycle = advance to the next theme each boot (saved in NVRAM). See Themes.
remember_last 1 = remember the last-booted entry and preselect it next time (NVRAM); overrides default=. 0 = always use default=.
recovery_entries 1 = auto-add a <name> (recovery) entry per Linux entry, appending systemd.unit=rescue.target nomodeset.
mouse 1 = mouse/touchpad/touchscreen cursor; single click boots an entry or runs a power icon · 0 = off.
editor 1 = allow editing a kernel command line at the menu with e (one-shot) · 0 = disable.
title_color Title text color, #RRGGBB.
name_color Default entry-name color, #RRGGBB.
box_radius Corner radius (in pixels) of the selection highlight / frost box. 0 = Default
highlight_color Selection accent/underline color, #RRGGBB.
blur Blurred-glass highlight that follows the selection (entries and power actions), replacing the flat card. 0/absent = flat card · 1 = frosted (blur + tint) · clear = clear blur (no tint).
blur_title 1 = static blurred panel behind the title. Independent of blur.
blur_color Tint colour for frosted mode, #RRGGBB. Absent = white. Ignored when blur=clear.
fade_speed Full-screen menu fade-in/out speed, 1 (slow) .. 10 (fast). Default 10. Ignored when animation=0.
animation Master animation switch. 1 = fades/slides on (default) · 0 = all GUI animations/fades are instant.
anim_speed Selection animation speed, 1 (slow) .. 10 (fast). Default 8. Entry↔power cross-fades; within a row/column slides.
title_size Title height in pixels. 0/absent = screen_height / 12.
name_size Entry-name height in pixels. 0/absent = 16.
icon_size Icon edge length in pixels (square). 0/absent = 64.
icon_spacing Horizontal gap between icons in pixels. 0/absent = 60.
icon_y Vertical center of the icon row in pixels. 0/absent = screen middle.
underline_color Selection underline color, #RRGGBB. Absent = highlight_color.
underline_thickness Underline height in pixels. 0/absent = 4.
underline_length Underline width in pixels. 0/absent = icon width + margin.
power_position Corner for the power actions: bottomright (default), bottomleft, topright, topleft.
shutdown_color Color of the Shutdown hotkey letter, #RRGGBB. Absent = highlight_color.
reboot_color Color of the Reboot hotkey letter, #RRGGBB. Absent = highlight_color.
firmware_color Color of the Firmware hotkey letter, #RRGGBB. Absent = highlight_color.
power_icons 1 = draw the power actions as icons instead of text (still triggered by S/R/F). Needs the *_icon keys.
power_icon_size Power-icon edge length in pixels. 0/absent = 40.
shutdown_icon PNG for the Shutdown action (used when power_icons=1). Falls back to text if missing.
reboot_icon PNG for the Reboot action (used when power_icons=1). Falls back to text if missing.
firmware_icon PNG for the Firmware action (used when power_icons=1). Falls back to text if missing.
background Full-screen background image (PNG, RGB or RGBA). Falls back to backgrounds/default.png if missing/corrupt.

Boot entries (Examples)

Visor auto-detects how to boot each one from the image itself: a PE image with no initrd/cmdline is chainloaded (e.g. Windows bootmgfw.efi, loaded by device path so its BCD is found); a PE UKI or EFI-stub kernel is started with its cmdline/initrd applied if given; a raw (non-PE) kernel uses the Linux EFI handover path. (The old linux/windows block names still parse as aliases for entry, so existing configs keep working.)

entry {
    name    = "Arch Linux"      # shown under the icon
    icon    = \EFI\visor\icons\arch.png
    color   = #1793D1           # optional: overrides name_color for this entry
    kernel  = \vmlinuz-linux    # EFI stub kernel or UKI .efi
    initrd  = \initramfs-linux.img        # optional (omit for a UKI)
    cmdline = "root=PARTUUID=... rw quiet" # optional (omit for a UKI)
}

entry {
    name   = "Windows 11"
    icon   = \EFI\visor\icons\windows.png
    kernel = \EFI\Microsoft\Boot\bootmgfw.efi
}
Entry key Meaning
name Display name under the icon.
icon PNG icon (square, e.g. 128×128, RGBA recommended).
icon_size Per-entry icon edge length in pixels; overrides global icon_size
kernel EFI stub kernel / UKI (linux) or bootmgfw.efi (windows).
initrd initrd image — Linux only, optional (omit for a UKI).
cmdline Kernel command line — Linux only, optional (omit for a UKI).
color Per-entry name color, #RRGGBB (overrides name_color).
uuid Partition UUID hint for Windows chainloading, optional.
sha256 Pin the image's SHA-256 (64 hex). Boot is refused on mismatch.
encrypted 1 = kernel and initrd point at Visor encrypted containers.
kernel_encrypted 1 = only the kernel/UKI path is encrypted.
initrd_encrypted 1 = only the initrd path is encrypted.
luks 1 = ask for a LUKS password and inject it into the initrd.
luks_key_path Keyfile path to create inside the initramfs archive. Default: /crypto_keyfile.bin.
luks_preset Auto-generate the keyfile option for mkinitcpio/arch, dracut, or systemd.
luks_cmdline Manual kernel options that tell your initramfs to use that keyfile; overrides the preset.

LUKS unlock handoff

For LUKS-root systems, Visor can ask for the passphrase before Linux starts and append that passphrase as a temporary keyfile inside the in-memory initramfs. The actual LUKS unlock is still performed by your initramfs/cryptsetup stack. This keeps Visor small and avoids putting the password on the kernel command line. The temporary keyfile is still present in the initrd memory handed to Linux, so this is a convenience handoff, not a way to hide the passphrase from the booted kernel/initramfs.

For EFI-stub kernels with initrd=, Visor appends the keyfile archive to that initrd. For UKIs, Visor leaves the UKI unchanged and provides a supplemental keyfile-only initrd through the Linux EFI initrd protocol; your UKI's embedded initramfs must support that keyfile option. Raw non-PE kernels still need a separate initrd=.

For common initramfs stacks, set luks_preset and Visor fills in a matching starter keyfile option:

Preset Generated option
mkinitcpio / arch cryptkey=rootfs:<luks_key_path>
dracut / systemd rd.luks.key=<luks_key_path>

If your distro needs UUID binding, a different key source, or different syntax, set luks_cmdline manually; it takes priority over the preset.

entry {
    name      = "Arch LUKS"
    icon      = \EFI\visor\icons\arch.png
    kernel    = \vmlinuz-linux
    initrd    = \initramfs-linux.img
    cmdline   = "root=/dev/mapper/cryptroot rw quiet"
    luks      = 1
    luks_key_path = /crypto_keyfile.bin
    luks_preset = mkinitcpio
}

UKI example:

entry {
    name      = "Arch UKI LUKS"
    kernel    = \EFI\Linux\arch-linux.efi
    cmdline   = "root=/dev/mapper/cryptroot rw quiet"
    luks      = 1
    luks_key_path = /crypto_keyfile.bin
    luks_preset = mkinitcpio
}

When you boot that entry, Visor shows the same overlay used by the boot-options editor. The prompt title and caret use the configured underline color; typed password text stays white.

Encrypted boot artifacts

Visor can password-decrypt the boot files it loads: a kernel, UKI, or initrd can be stored as a Visor encrypted container and decrypted in memory after entry selection. The container is authenticated before decrypting, so modified files are refused. This is for boot artifacts, not full-disk/LUKS volume parsing.

Create encrypted files on the host:

python3 tools/visor_encrypt.py vmlinuz-linux vmlinuz-linux.venc
python3 tools/visor_encrypt.py initramfs-linux.img initramfs-linux.img.venc

Then point the entry at those files:

entry {
    name      = "Encrypted Arch"
    kernel    = \vmlinuz-linux.venc
    initrd    = \initramfs-linux.img.venc
    encrypted = 1
    cmdline   = "root=PARTUUID=... rw quiet"
}

When you boot that entry, Visor asks for the password with the same overlay used by the boot-options editor. The prompt title and caret use the configured underline color; typed password text stays white.

Changing the font size

Set title_size and name_size (pixels) in the global section. For example:

title_size=120
name_size=28

Leave them out (or 0) for the defaults (screen_height/12 and 16).

Using a different font

The font is baked into the binary from a TTF, so a normal build needs no font tooling. To swap it, regenerate the atlas (needs Python 3 + Pillow) and rebuild:

make bakefont FONT=/usr/share/fonts/TTF/YourFont.ttf FONT_PX=64
make

Themes

Instead of scattering UI tweaks through boot.conf, you can keep a whole look in a theme file and switch with a single line.

  1. Create \EFI\visor\themes\<name>.conf on the ESP (a themes/ folder is made for you at install time, with a sample nord.conf).
  2. Put any UI keys in it — the same keys used in boot.conf (colors, sizes, icon_*, underline_*, power_position, background, title, …).
  3. In boot.conf, set theme=<name>.

When a theme is set, its values override whatever boot.conf specifies for those keys. Keys the theme doesn't mention keep their boot.conf/default value. Boot entries (linux {} / windows {}) always live in boot.conf — themes are UI-only.

# boot.conf
theme=zero
# themes/zero.conf  (overrides boot.conf's UI)
title=Visor
title_color=#88C0D0
name_color=#ECEFF4
underline_color=#A3BE8C
underline_thickness=8
icon_size=128
power_position=bottomright
shutdown_color=#BF616A
reboot_color=#EBCB8B
firmware_color=#A3BE8C
background=\EFI\visor\backgrounds\default.png

Controls

Key Action
Left/Right Move between boot entries (wraps)
Down Enter the power column (Shutdown), then move down it
Up Move up the power column; from the top, return to the boot entries
Enter Boot the focused entry, or run the focused power action
e Edit the focused entry's kernel command line, then boot it (one-shot)
v On an OS with multiple deployments (OSTree/BLS), browse its versions in the centre-info
19 Boot entry N directly
Esc Boot the default entry
S Shut down (works from anywhere)
R Reboot (works from anywhere)
F Enter firmware setup (works from anywhere)
Mouse / touch Move to show a cursor; single click an icon to boot it, or a power icon to run it

Immutable distros & OSTree (atomic deployments)

Visor understands the Boot Loader Specification (BLS) entries that OSTree systems — Fedora Silverblue/Kinoite, Fedora CoreOS, bootc images, and anything using kernel-install — write to loader/entries/*.conf. No configuration needed: if Visor finds them (on the ESP, or on a separate /boot reachable via an efifs driver), it builds the menu automatically.

  • One icon per OS. All of an OS's deployments (current + rollback + older) collapse into a single icon. The newest boots by default.
  • Browse versions with v. Press v on the OS to turn the centre-info into a version browser: Left/Right step through deployments (current ↔ rollback ↔ older, with the version, role, and position shown), Enter boots the one shown, Esc backs out. The rollback is tinted amber so it's easy to spot.
  • Automatic rollback safety net. Visor honours boot-counting (…+tries.conf), the same mechanism greenboot uses: it decrements the counter before booting, and if a deployment's tries run out, it automatically defaults to the previous (rollback) deployment. No agent needed in the OS — it piggybacks on greenboot. (On a read-only /boot it simply skips the counter update.)

If /boot is a separate ext4/btrfs partition, drop an efifs driver into \EFI\visor\drivers\ so Visor can read the kernels there (see Booting from non-FAT filesystems).


Linux EFI stub requirements

To boot via the EFI stub, the kernel must be built with:

CONFIG_EFI=y
CONFIG_EFI_STUB=y

Most distributions enable these by default. Unified Kernel Images (e.g. from systemd-ukify or dracut --uefi) already bundle the stub, initrd, and cmdline — point kernel at the .efi and omit initrd/cmdline.

Recommended: Unified Kernel Images (no driver). A UKI on the FAT ESP at \EFI\Linux\*.efi bundles the kernel, initrd and cmdline into one EFI binary and boots with zero extra code. This is the simplest and most portable setup — prefer it unless you specifically want raw vmlinuz+initrd off a Linux filesystem.

Optional: bring-your-own filesystem driver. Visor ships no filesystem drivers Instead it loads whatever EFI driver you place in \EFI\visor\drivers\ — every *.efi there is started at boot and connected to your disks.

Obtain a driver yourself — the open-source efifs set provides btrfs_x64.efi, ext4_x64.efi, etc.:

mkdir -p <ESP>/EFI/visor/drivers
cp <your>/btrfs_x64.efi <ESP>/EFI/visor/drivers/

boot.log reports drivers: started N, connecting controllers when this works. If Secure Boot is on, the driver must be signed/enrolled (e.g. via sbctl) too.


Secure Boot

Visor is Secure Boot aware. Before launching any image it asks shim's SHIM_LOCK protocol to verify it (the same trust path used when Visor is itself started by shimx64.efi):

  • PE images (Windows Boot Manager, UKIs, EFI-stub kernels) — if shim is present and verification fails, the boot is refused. If shim is absent, the firmware's own image verification (via LoadImage) still applies under Secure Boot.
  • Raw (non-PE) kernels booted through the EFI handover protocol bypass firmware verification entirely, so under Secure Boot Visor refuses them unless shim verifies them first.

With Secure Boot off, everything boots as before. For a fully signed chain, sign visor_x64.efi (and any efifs driver) and enrol the keys with sbctl, or boot Visor via shim so shim validates your kernels against the enrolled db/MOK.


Troubleshooting

Visor writes a log to \EFI\visor\boot.log on the ESP. It is overwritten each boot but retains the last 3 boots, and logs every fallible step (config parse, PNG decode, image load, hand-off). Read it first when something fails — most problems are one descriptive line away.


MASSIVELY INSPIRED BY rEFInd!!


About

A minimal, fast, graphical UEFI boot manager written in C. no external dependencies — just a config file :)

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors