Software project

Kairo application icon — five overlapping petals forming a circular shutter

Kairo

Automatic launcher artwork for Linux.

Linux already lets you change an application's icon by hand. Kairo's point is that you shouldn't have to — it finds what's on your machine, finds artwork that matches, and applies it without you ever opening a .desktop file. It started as a fix for Steam's launcher entries and grew into a general tool for Steam games, installed applications, and emulator ROM libraries.

01 The problem

KDE, GNOME, and effectively every other Linux desktop follow the same freedesktop spec for how an application appears in the launcher: a .desktop file with an Icon= key that either points straight at a file or names an icon to resolve through the current theme. That shared interface cuts both ways — a broken or generic icon looks exactly as broken on KDE as it does on GNOME, because both read the same file the same way.

Fixing one by hand isn't hard. Fixing a launcher full of them is tedious in a specific way: find the application, search for artwork, download an image, locate the right .desktop entry, edit it without breaking it, and repeat. Steam generates entries you can't control the icon of. Emulator ROMs have no launcher presence at all unless you write the entries yourself. Flatpaks and AppImages land wherever their packaging puts them.

Kairo collapses that loop into scan, review, apply — and, just as importantly, into an undo.

02 Providers

Everything Kairo can see comes from a provider. A provider knows how to enumerate one kind of thing and hand back a uniform entry; nothing downstream cares where an entry came from. That's what stops "support one more source" from turning into a rewrite.

DISCOVER MODEL WRITE RECORD SteamProvider DesktopEntryProvider EmulatorProvider one per emulator …future sources AppEntry one shape for all GeneratedEntryWriter Kairo made this file OverrideWriter shadows someone else's ledger undo lives here

Three providers ship today. SteamProvider reads Steam's own library data — native and Flatpak installs, plus extra library folders on other drives. DesktopEntryProvider walks the standard application directories. EmulatorProvider is instantiated once per configured emulator, so Dolphin and PCSX2 are separate entries in the sidebar rather than one lumped category.

A provider only has to declare which group it belongs to and yield entries; declaring group = "Emulators" is enough to appear under that heading with no change to the interface at all. Writing back is deliberately narrower than reading: there are exactly two writers, because there are exactly two situations — Kairo created the launcher entry, or Kairo is shadowing one that already existed. Those two cases have different rules for what Reset and Remove mean, and collapsing them would lose that distinction.

03 Artwork

Artwork sources are modular in the same way. SteamGridDB supplies game art; installed icon themes and Iconify cover applications; and any local file works as a fallback. Only SteamGridDB needs an API key — the rest work without one.

Two decisions here were less obvious than they look. The first is that SteamGridDB is queried two different ways: Steam games resolve by appid, which is exact, while emulator ROMs resolve by title search, which is a guess. Those results are scored differently rather than being treated as equally trustworthy. The second is that desktop applications are deliberately excluded from it — searching a game-artwork database for a text editor returns results that are confident and wrong, which is worse than returning nothing.

One bug worth recording: icons looked soft in the browser for a long time. The cause was that loading a multi-size .ico returns its first frame, which is conventionally the smallest. Kairo now walks every frame in the container and keeps the largest, and applies its minimum-size rule to the frame that actually decoded rather than to the dimensions the API reported.

04 Emulators

Emulator ROMs are the case that benefits most, because they have no launcher presence at all by default. Kairo ships a catalogue of systems and their usual emulators, so adding one starts from a list rather than a blank form — file extensions and the expected executable are already known.

Kairo showing a PCSX2 emulator library of 14 PlayStation 2 games with Grand Theft Auto San Andreas selected and 125 artwork options listed from SteamGridDB
A PlayStation 2 library scanned from ROM folders — each game becomes a launcher entry with artwork of its own.

Finding the emulator itself is layered, because Linux users install software in genuinely different ways. Kairo checks PATH first, then the installed .desktop entry — which covers AUR, Flatpak, Snap and AppImage uniformly, since all of them leave one behind — and finally Flatpak's export directories. ROM folders are read from the emulator's own configuration where possible, falling back to conventional layouts like ~/Emulation/roms and ~/ROMs.

One emulator can span several systems: Dolphin contributes a GameCube folder and a Wii folder, each with its own extensions and label. And a ROM's internal id is derived from its path, not its title — so renaming a game in Kairo can't orphan the record of what was done to it.

05 Safe by design

Kairo modifies how your desktop presents itself, which means the interesting engineering isn't the artwork — it's making every change reversible and impossible to do damage with. Four rules hold the whole design together.

Writes are atomic — written to a temporary file in the same directory and moved into place — so an interrupted write can't leave a half-written entry that the desktop then fails to parse. The interface never touches a launcher file directly either; every mutation goes through one module that owns the marker checks and the ledger, and a test fails the build if a back door appears.

06 Interface

The shell is Qt, in three columns: sources on the left, the library in the middle, and the selected entry with its artwork options on the right. The aim is that someone who has never heard of a .desktop file can still use it — the vocabulary is games and applications, not freedesktop specs.

Kairo showing the Steam library with Far Cry 5 selected, listing 37 artwork options from SteamGridDB in a grid
Steam library — artwork resolved by appid, so matches are exact. A dot marks entries Kairo has customised.

Entries carry a dot when Kairo has customised them, and the list filters to Customized or Untouched so it stays useful once a library is largely done. Per-entry actions are deliberately plain: browse for a local file, reset the artwork, remove the shortcut, apply.

Kairo settings showing an optional SteamGridDB API key field with the key masked, configured emulators Dolphin for GameCube and PCSX2 for PlayStation 2, and a list of where settings, cache, artwork and launcher entries are stored on disk
Settings states plainly where everything lives on disk, and the SteamGridDB key is optional — icon themes, Iconify and local files work without one.

07 From Shortcut Forge to Kairo

The project shipped as Steam Shortcut Forge while Steam was the only thing it understood. Once it could see applications and emulator libraries too, the name described a subset of the product and implied a permanent tie to one storefront it no longer had. Kairo is the same project renamed, not a restart — Steam remains the most complete source, and none of that work was thrown away.

The migration mattered more than the name. Renaming an application's identity moves its config directory, its data directory, and the marker written into every launcher entry it has ever created — and a careless rename would have silently orphaned every shortcut an existing user had, leaving Kairo unable to recognise or restore its own past work. So the marker Kairo writes today is the new one, but it still reads the old key permanently, and the old directories are migrated and then left on disk rather than removed, with their location shown in Settings so the user can delete them when they're satisfied nothing was lost.

08 Status & license

StatusPublic · active development
Version2.3.2
LicenseMIT
RoleCreator and developer
Built withPython, Qt (PySide6), SteamGridDB & Iconify APIs, freedesktop desktop entries
Application IDio.github.shadowxskinner.Kairo
RepositoryView repository →

Kairo runs from source today — clone the repository and launch the Qt frontend. Distribution packaging is written but not yet verified end to end, and there's no tagged release, so the honest install story for now is a git clone rather than a package manager. Auto Match is present in the interface but disabled until its confidence scoring is trustworthy enough to apply artwork in bulk unattended.

Built for my own desktop first, MIT-licensed, and public for anyone who wants to use it or read the source.