The Wellspring
A Fresh GRUB Theme Every Boot: Automatic Theme Rotation

A Fresh GRUB Theme Every Boot: Automatic Theme Rotation

415 views

For dual-boot users, the first thing you see on power-on isn’t your desktop — it’s the GRUB boot menu. The default black-on-white text interface is pretty plain, until one day I stumbled across GRUB wallpapers themed after Honkai: Star Rail characters, and realized: this little ritual we go through every single day could actually feel special.

Now, every boot, GRUB picks a different theme from the ones I have installed — Firefly one day, Feixiao the next, maybe Cyrene after that. A little surprise at every power-on. This article walks through my whole setup, from switching themes manually to fully automatic rotation across a dual-boot system. Everything here is copy-paste ready.

Switching GRUB themes: from zero

Where theme files live

A GRUB theme is a directory whose core file is theme.txt (layout, fonts, colors) plus a few background images. The conventional location is:

/usr/share/grub/themes/

One subdirectory per theme, e.g.:

/usr/share/grub/themes/TheHerta_cn/
├── theme.txt
├── background.png
└── ...

Setting a theme manually

The simplest way is to edit /etc/default/grub and add one line:

sudo vim /etc/default/grub
GRUB_THEME="/usr/share/grub/themes/TheHerta_cn/theme.txt"

Then regenerate the boot configuration (don’t skip this — editing the config without regenerating changes nothing):

sudo grub-mkconfig -o /boot/grub/grub.cfg

Reboot and the new theme is there.

A quick recommendation: StarRailGrubThemes

For Honkai: Star Rail character themes, check out this repo: StarRailGrubThemes

https://github.com/voidlhf/StarRailGrubThemes

It has themes for a lot of characters, each with a _cn variant tailored for Chinese-language menus and font rendering; the author published the Figma design files and the artwork comes from StarRailRes, the official asset site, so the quality is solid. Clone or download the repo, copy a theme directory into /usr/share/grub/themes/, point GRUB_THEME at it as shown above (or just let it join the auto-rotation below), and you’re done. The preview/ directory has screenshots of every theme, so you can pick before installing.

Making themes rotate automatically: the native GRUB approach

Manual switching is just the appetizer. What I really wanted was: a different theme on every boot. Here’s the final version of the solution, after one iteration.

Why not a systemd service?

My first thought was a systemd service that picks a random theme after boot. But on a dual-boot machine that approach simply doesn’t work: a systemd service only runs when I boot into Arch — if I choose Windows, it never executes, so the theme never changes. And even on Arch, the switch happens after the OS has booted, so the menu you see first is still the old one.

The solution has to be: GRUB does the rotation itself, during the boot stage, before any OS is entered — that’s the only way the menu is fresh on every boot, whether you’re going into Arch or Windows.

The full implementation: /etc/grub.d/01_theme_rotate

What a lot of people don’t know: /boot/grub/grub.cfg is a script that GRUB parses and executes at every boot — grub-mkconfig just generates it. It does so by running every executable script in /etc/grub.d/ in numeric-prefix order and concatenating their output into grub.cfg. So we drop our own script in there, and have it enumerate the themes and emit the rotation branches when grub.cfg is generated.

Create the script (root privileges):

sudo vim /etc/grub.d/01_theme_rotate
#!/bin/bash
# /etc/grub.d/01_theme_rotate — rotate the GRUB theme on every boot
# Generation time (grub-mkconfig runs this script): enumerate theme dirs, emit if/elif branches
# Runtime (GRUB parses grub.cfg at boot): rotate via the grubenv counter theme_counter
# After installing new themes, just re-run grub-mkconfig — no changes to this script needed

cat <<'GRUB_HEAD'
load_env
if [ -z "${theme_counter}" ]; then set theme_counter=0; fi
GRUB_HEAD

i=0
for theme in /usr/share/grub/themes/*/theme.txt; do
  [ -f "$theme" ] || continue
  if [ "$i" -eq 0 ]; then
    echo "if [ \"\${theme_counter}\" = \"0\" ]; then"
  else
    echo "elif [ \"\${theme_counter}\" = \"$i\" ]; then"
  fi
  echo "  set theme=$theme"
  echo "  set theme_counter=$((i + 1))"
  i=$((i + 1))
done

# Counter overflow (after a full rotation cycle) resets to zero, so it stays in range forever
echo "else"
echo "  set theme_counter=0"
echo "fi"
cat <<'GRUB_TAIL'
save_env theme_counter
GRUB_TAIL

Make it executable and regenerate the boot config:

sudo chmod +x /etc/grub.d/01_theme_rotate   # must be executable, or grub-mkconfig won't run it
sudo grub-mkconfig -o /boot/grub/grub.cfg

During generation the terminal prints # theme_rotate: N themes in rotation — seeing that line means the enumeration worked, where N is your current theme count.

What does it do? Two phases, mirroring the script:

  • Generation time (when grub-mkconfig runs): the script enumerates every theme directory under /usr/share/grub/themes/ that contains a theme.txt, and emits one if/elif branch per theme into grub.cfg. Install N themes, get N branches — all of them participate in the rotation.
  • Runtime (GRUB parses grub.cfg at boot): GRUB runs load_env to read the grubenv environment → matches theme_counter against a branch and picks the theme with set theme=... → increments the counter → save_env writes it back to /boot/grub/grubenv → next boot reads the new value. On overflow the counter resets, so a full cycle restarts from the beginning.

Details:

  • First boot: the counter isn’t set yet, so the script sets it to 0 — you get the first theme in alphabetical order;
  • Zero maintenance: install new themes into /usr/share/grub/themes/ and re-run grub-mkconfig — new themes join the rotation automatically, and /etc/grub.d/01_theme_rotate itself never needs a single edit:
sudo grub-mkconfig -o /boot/grub/grub.cfg   # just re-run after installing new themes

Why not “true randomness”?

I checked the GRUB manual: the random module is only a crypto-library function (seeded from the ACPI PM Timer + TSC), with no script command available; hexdump_random is just a debug print; GRUB scripts have no command substitution (you can’t capture date output into a variable), and expr.mod isn’t installed, so arithmetic-based randomness is out.

So I settled for the next best thing — sequential rotation: go through all themes in alphabetical order, then start over. Uniform, no repeats, and practically indistinguishable from random. It even has one advantage: one full cycle shows you every theme exactly once, so no theme ever gets left out.

Works on dual-boot (and costs nothing)

I run Arch Linux + Windows, and this setup works across both systems natively, because:

  1. GRUB is shared by both systems;
  2. grub.cfg executes before any OS is entered;
  3. The rotation happens at the boot stage, regardless of whether Windows or Arch follows.

In other words, the theme rotates even when you’re heading into Windows — with zero configuration and zero effort on the Windows side. The only downside is that Windows itself never gets to show off the themes. But it doesn’t need to.

Pro tip: remembering “last choice, this choice”

One more dual-boot quality-of-life note: the default strategy GRUB_DEFAULT=saved + GRUB_SAVEDEFAULT=true makes GRUB remember which system you booted last — otherwise, after you enter Windows, the next boot menu highlights Arch again. This memory is especially valuable during Windows updates, which reboot the machine on their own: without it, every restart parks on the Arch entry and you have to sit there and manually switch back to Windows; with it, the whole update runs hands-free, each reboot landing right back in Windows.

One pitfall: GRUB’s “memory” relies on a save_env saved_entry statement inside the menu entry. Entries auto-generated by the system (10_linux) carry this logic, but hand-written entries in 40_custom do not. My hand-written Windows entry didn’t have it — after every Windows session, the next boot defaulted back to Arch. The fix: give the entry an --id and add the save logic:

menuentry "Windows" --id windows {
  search --fs-uuid 5FFC-0D5B   # replace with your Windows EFI partition's UUID
  chainloader /EFI/Microsoft/Boot/bootmgfw.efi
  saved_entry=windows
  save_env saved_entry
}

Regenerate with grub-mkconfig as usual. How to tell whether you’ve hit this pitfall? Check the mtime of /boot/grub/grubenv — if it’s stuck on the day you installed the system, the “memory” has never worked.

Wrapping up

From plain black-on-white to a surprise at every boot — powering on finally feels a little more ceremonial. The soul of the whole setup is the “enumerate at generation time, rotate at runtime” idea — GRUB’s scripting machinery is more flexible than most people give it credit for.

And when the repo adds new characters, just git pull, cp the new theme into the themes directory, re-run grub-mkconfig, and your theme pool grows. May every boot show you the character you wanted to see.