Create themes
How to create, test and publish archr-launcher themes - theme.toml colors, fonts, style, images, views, screen sizes, Game Details, achievements, sounds and the Theme store.
A theme is a folder with a theme.toml. It can change colors, fonts, icons, console pictures and also where each element sits on the Systems and Games screens. Everything is optional: whatever the theme does not set comes from the default theme (Grafite), key by key.
To have a coding assistant such as Claude Code write and test a theme from these rules, install the AI skill for themes and modules.

Where themes live
/storage/.config/archr-launcher/themes/<name>/ user themes (take priority)
/usr/share/archr-launcher/themes/<name>/ system themes
theme.toml required
fonts/ .ttf fonts named in theme.toml (optional)
icons/ Lucide .svg icons that replace the built-in ones (optional)
systems/ console pictures: snes.png, gba.webp, app-<id>.png... (optional)
sounds/ interface sounds .ogg/.wav (optional, see Sounds)The theme shows up in Settings › Appearance › Theme when its folder (not a hidden one) contains theme.toml. Any folder the theme does not bring comes from the installed default theme (/usr/share/archr-launcher/themes/default/): without fonts/ and without font keys it uses the default fonts; without a console's picture, the default one; without an icon or a sound, the default one. A small complete example is themes/praia/ in the launcher repository (it appears as "Example"; the full Praia theme is published separately in the Theme store).
The active theme is the folder name saved in launcher.theme (/storage/.config/system/configs/system.cfg, default default).
Name
name = "Praia" # one name for every language
name = { en = "Beach", pt_BR = "Praia", ru = "Пляж" } # or per languageWithout name, Appearance shows the folder name. Languages: en, pt_BR, es, ru; a missing one falls back to en and then to any other.
Colors
#RRGGBB or #RRGGBBAA, all at the top of theme.toml.
| key | used for |
|---|---|
background | background |
surface, surface_elevated | panels and windows |
border, badge | borders and footer buttons |
text, text_secondary, muted, dim | text, from strongest to faintest |
accent, accent_text, accent_text_secondary | highlight and the text on it |
success, warning, danger | positive, attention and failure states |
scrim | veil behind notices and menus |
When the ROMs partition has a problem (read-only or not mounted), a warning replaces the summary in the footer of the Systems and Games lists, with the icon icons/triangle-alert.svg in the warning color. The layout does not change.
accent, accent_text and dim also accept the old names highlight_bg, highlight_text and dim_text; the new names win.
Appearance on top of the theme
In Appearance, the first dot of Accent color is the active theme's accent, with the theme's own accent_text. The six ready-made colors come next (the one equal to the theme's is hidden) and the custom color is last. When the user picks another color, the text on it turns black or white automatically, by contrast.
Background starts at From the theme (the theme's surfaces); Black, Midnight blue and a custom color are alternatives. Switching themes carries along everything left at "from the theme"; what the user chose explicitly stays. Restore defaults brings everything back to the chosen theme.
User options that act over the theme
Appearance has choices saved in system.cfg that apply on top of any theme. None of them is a theme.toml key: a theme can neither declare nor disable them, and they are applied after the theme is read.
option (system.cfg) | values | effect |
|---|---|---|
Font weight (launcher.font_weight) | from the theme (empty) / bold | bold uses the theme's font_semibold (with the mono font, font_mono_medium). A theme without that face keeps the regular one. |
Battery percentage (launcher.battery_percent) | on (default) / off | off hides the number in the status pill; the icon stays. |
Box art (launcher.game_art) | on (default) / off | off removes the games screen's art, meta, backdrop and the panel that coincides with the art. |
Box art width (launcher.art_width) | from the theme / 35, 40, 45, 50, 55 % of the screen width | the art keeps its outer edge (the one away from the list); the list beside it keeps its gap to it; meta follows the art. |
Box art corners (launcher.art_radius) | from the theme / 0, 6, 12, 20 reference px | replaces the radius of the view's art; the panel follows. |
Switcher background (launcher.switcher_curtain) | theme background / 40, 60, 80 % | the selected game's picture fills the screen behind the switcher, under a curtain of the theme's background color at that opacity. A game without a picture keeps the theme background. |
Text color (launcher.text_color) | from the theme (empty) / #RRGGBB | replaces the theme's text; text_secondary is derived (25% toward background). |
Selected text color (launcher.selected_text_color) | from the theme (empty) / #RRGGBB | replaces the theme's highlight_text and the accent_text_secondary that goes with it. |
Secondary text color (launcher.secondary_color) | from the theme (empty) / #RRGGBB | replaces the theme's muted and the dim_text that goes with it. |
Background (launcher.background) | from the theme (empty) / black / midnight / #RRGGBB | with #RRGGBB, it becomes the background; surface, surface_elevated, border and badge are derived by mixing toward white (4, 7, 12 and 12%). |
Text-only button hints (launcher.text_hints) | off (default) / on | footer button hints show only the button letter in the theme's text color, with no badge circle or badge frame; START keeps its word, without the box. |
System background (launcher.system_background) | on (default) / off | on a system's games list and game details, the screen background is that system's picture (see "Per-system backgrounds"). |
What theme authors should know:
- Colors: each of the three text colors opens the color editor (hue, saturation, brightness and hex code). Empty keeps the theme. Since they replace
text,highlight_textandmuted, pick theme values that still work with text of another color; do not depend on the color being exactly yours. - Text-only hints: the theme needs to do nothing; the text uses the theme's
text, so it must contrast with thebackground. - System background: this is a feature a theme can use (next section, "Per-system backgrounds"); the option only turns it on or off.
- Box art off: a list beside the art, on its left or right, takes the whole width, between equal margins (the list's margin on the side away from the art, repeated on both sides). The
panelgoes away only if it has the samexandwas theart; apanelwith another rectangle (a larger card, a decorative frame) is still drawn. - Wider box art: the
artrectangle is rebuilt from the outer edge the theme chose (the right one when the art is right of the list, the left one when it is left of it), and the list beside it steps back, keeping the original gap and its other edge. A list that is not beside the art (above or below it) is left alone. - Corners: they apply to the
artand, when thepanelcoincides with it, to thepanel(1 reference px more, so the frame wraps the art). - Switcher: the curtain uses the theme's
background; pick one that works over both light and dark covers.
To keep a theme good under these choices: place the list beside the art; keep art and panel equal when the panel is the art's card; do not depend on an exact art size or on the art existing (the title and info must work alone). Test the theme with the art off, wider (55 %), with corners 0 and 20, with Bold weight, and with the switcher curtain at 40 and 80 %. Also try text-only hints, custom text and background colors, and a per-system background.
Fonts
font = "fonts/Inter-Regular.ttf"
font_semibold = "fonts/Inter-SemiBold.ttf"
font_mono = "fonts/JetBrainsMono-Regular.ttf"
font_mono_medium = "fonts/JetBrainsMono-Medium.ttf"Paths are relative to the theme folder, without ... Each family comes whole: a theme that names font or font_semibold uses its own (the regular one stands in for semibold if that is missing); a theme that names neither inherits both from the default theme. The same goes for font_mono and font_mono_medium. A named font that does not exist falls back to the system fonts (DejaVu/Liberation).
Text size and sharpness
font_scale = 1.4 # 0.5 to 2: multiplies every text size (together with the user's)
font_grid = 8 # 0 to 32 px: sizes snap to the nearest multiple (0 = free)
font_smoothing = false # no anti-aliasing: hard-edged letters, for pixel fonts
font_min = 12 # 6 to 32 px: minimum text size; what does not fit wraps.
# Start tile labels may go 2 px lower, never below 8 pxfont_grid suits fonts drawn on a grid (Press Start 2P is sharp at multiples of 8 px). It also applies in Settings, where a theme only changes colors, fonts and icons. Defaults: 1.0, 0, true and 8.
font_min and font_grid are reference px (480 px tall) and scale with the screen. On top of them there is a fixed floor of 10 screen px: no text is drawn smaller, at any resolution, whatever font_scale, font_min or the user's text size say (see Screen sizes).
General measures
There are two kinds of number. Sizes (fonts, row, header and footer heights, radii, margin) are fractions of a 480 px reference: 0.035416667 is 17 px on a 640×480 screen and grows or shrinks with the screen like everything else. Rectangles (x y w h) are fractions of the screen: x and w of its width, y and h of its height. On a screen of another shape the rectangles stretch with it and the sizes do not, so a taller screen fits more rows; see Screen sizes.
font_size = 0.035416667 # 17 px
margin = 0.033333333 # 16 px
radius_item = 0.020833333 # 10 px
radius_card = 0.033333333 # 16 px
header_height = 0.091666667 # 44 px
footer_height = 0.1 # 48 px
row_height = 0.079166667 # system rows
selected_height = 0.091666667
game_row_height = 0.075 # game rows
game_selected_height = 0.0875
list_width = 0.590625 # width of the game list
cover = { x = 0.640625, y = 0.1125, w = 0.328125, h = 0.583333333 }These measures build the classic view, lista.
Screen sizes
The launcher runs at 640×480 (4:3: the R36S and most RK3326 handhelds), 720×720 (1:1, R36 Plus) and 480×320 (3:2: Odroid Go Advance, RGB10, RG351M; the panel is portrait and Sway turns it, so the launcher already gets it in landscape). A 16:9 screen (854×480, 1280×720) falls in the same shape as 3:2.
The 640×480 design is scaled up or down until it fits whole: the scale is the smaller of width/640 and height/480, so text keeps its physical size on screens of the same density. The spare room goes to the longer side, in reference px:
| screen | scale | screen in reference px | shape |
|---|---|---|---|
| 640×480 | 1 | 640×480 | classic |
| 720×720 | 1.125 | 640×640 | square |
| 480×320 | 0.667 | 720×480 | wide |
| 1280×720 | 1.5 | 853×480 | wide |
What that does to each number in a theme:
- Sizes keep their reference px. On a square screen the game list has rows of the same size and shows more of them (at least 13 instead of 10 in Grafite); L/R turn the page by the rows the list shows.
- Rectangles (
cover,details_*,switcher_*, thex y w hof views) are screen fractions and stretch with it. A cover that is portrait at 640×480 becomes taller and narrower at 720×720. - The built-in geometry of
lista(the 250 px system panel, margins, the gap under the header) is worked out in reference px for the real screen; it does not stretch.
[aspect.<shape>] tables
To change the layout for one shape, write an [aspect.<shape>] table. The shape comes from the screen's width/height ratio:
| shape | ratio | screens |
|---|---|---|
square | below 1.2 (6:5) | 720×720 |
classic | 1.2 to 1.45 (29:20), inclusive | 640×480, 800×600 |
wide | above 1.45 | 480×320 (3:2), 854×480 and 1280×720 (16:9) |
Any other name ([aspect.tall]) is ignored with a warning in the log.
Everything allowed at the top of theme.toml is allowed inside it, [view.*], frames, background_image and text_outline included, and it is laid over its own file key by key: tables (views, list, art...) merge field by field; any other value replaces. A top-level rectangle (cover, details_*, switcher_*, start_area) comes whole, with its four fields.
# On a square handheld, a narrower game list and a taller cover.
[aspect.square.view.lista.games]
list = { w = 0.5 }
art = { x = 0.55, w = 0.4, h = 0.7 }
[aspect.square]
switcher_card = { x = 0.1875, y = 0.1, w = 0.625, h = 0.46875 }The order is: default theme, the default's [aspect], your theme, your theme's [aspect]. Two things follow from it:
- A theme without any
[aspect]table inherits Grafite's. On a square handheld it already gets the 4:3 Switcher, the centered Start Menu and the wider cover. - What your theme writes at the top applies to every shape and overrides the default's
[aspect]. If your theme setscoverorlist_widthat the top, Grafite's square cover and list no longer apply; write your own[aspect.square]for them.
Like any TOML table, [aspect.square] goes after the top-level keys; a top-level key written after it becomes a square-only key.
Font files (font, font_semibold, font_mono, font_mono_medium), name, sounds, icons and console pictures do not change per shape. font_size, font_scale and font_min do.
What the default theme sets:
[aspect.square]: 4:3 Switcher and resume pictures, the Start Menu centered between header and footer (start_area), a narrower game list and a wider cover (list_width,cover), the Details picture and table sharing the column, the console picture in the middle of the panel and, invitrine, a wider cover.[aspect.wide]: lists in larger text, 19 reference px (font_size = 0.039583333, 13 px on a 480×320 screen), and thevitrinegame list at 18 px.
The Praia example repeats the cover change for its art-on-the-left layout in [aspect.square.view.lista.games].
Text: the 10 px floor
No text is drawn under 10 screen px, at any resolution. The floor is absolute: it applies over font_min, font_scale and the user's text size, and a theme cannot change it. On a small screen text keeps that minimum and the rest of the design shrinks around it; what does not fit wraps, or ends in an ellipsis in Settings. At 480×320 the floor is 15 reference px, so a smaller size (such as the 12 px support lines) is drawn at 10 screen px. The one exception is the MangoHud preview in Settings, a miniature that shrinks like a picture.
Start Menu: start_area
The Start Menu (title, tiles, shortcuts and widgets) is a 640×480 reference composition. start_area ({ x, y, w, h }, screen fractions) is the box it sits in: the composition is 480 reference px tall and as wide as the box, centered in it, and the tiles may run to the box's edges. The footer always stays at the bottom of the screen. The default is the whole screen; on a square screen Grafite puts it between header and footer (0.75 of 640 px is the composition's 480 px):
[aspect.square]
start_area = { x = 0.0, y = 0.1796875, w = 1.0, h = 0.75 }A box shorter than 480 reference px squeezes the composition. start_widgets is measured inside the composition, not on the screen.
Settings do not change with the theme: they are 640 reference px wide, centered on a wider screen, and gain rows on a taller one.
Style
Six keys at the top of theme.toml change the finish of every screen, Settings included. Each inherits from the default on its own; an invalid value goes back to the default with a warning in the log.
| key | values | default | effect |
|---|---|---|---|
corners | 0 to 1 | 1.0 | multiplies every corner radius; 0 makes everything square |
border_width | 1 to 8 | 1 | border thickness, in reference px (480 px height) |
shadow | "soft", "hard", "none" | "soft" | panel shadow: blurred, an offset black block (4 px), or none ("soft" also puts the contact shadow under the console picture) |
art_scaling | "smooth", "pixel" | "smooth" | console pictures: "smooth" shrinks with a filter; "pixel" enlarges by whole multiples without smoothing (grid pixel art). Game covers do not change |
image_scaling | "smooth", "pixel" | "smooth" | the same for the theme images: background, frames and decorations |
easing | "smooth", "steps" | "smooth" | "steps" swaps the deceleration for four equal jumps, the first one at once ("8-bit" movement) |
corners = 0
border_width = 3
shadow = "hard"
art_scaling = "pixel"
easing = "steps"Theme images
On the Systems, Games and Details screens (never in Settings) a theme can replace plain boxes with pictures. All of it is optional; what is missing is drawn as before. Pictures live in the theme folder (.png, .jpg) and load off the drawing thread, so the first frame may show the plain box.
Background
background_image = "images/background.png" # covers the whole screen, cropped at the centerWith image_scaling = "pixel" the picture is enlarged without smoothing (draw it at 320×240 for 2×2 pixels).
Per-system backgrounds
On a system's games list and game details, the launcher uses the system's picture as the screen background when the System background option (launcher.system_background, on by default) is on. A theme can ship these pictures in a backgrounds/ folder:
<theme>/backgrounds/snes.png
<theme>/backgrounds/gba.jpg- The file name is the system id, as in
es_systems(snes,gba,psx...), as.pngor.jpg. - Lookup order: the user folder
/storage/.config/archr-launcher/backgrounds/first, then the theme'sbackgrounds/, then the default theme'sbackgrounds/. - The picture covers the whole screen (cropped to fill) under a curtain of the theme's
backgroundcolor at about 59% opacity (alpha 150), so the text stays readable. It replaces the theme'sbackground_imageon those screens. - Collections (
@favorites,@recent) keep the theme background. - The picture is scaled to the screen when it loads, so any size works; at screen size (640×480, 720×720 or 1280×720) it loads faster on the device.
- With no picture for the system, the theme background is used. Choose a
backgroundthat suits the curtain.
Frames (9-slice)
[frames.row]
image = "frames/row.png"
slice = [6, 5, 6, 7] # image px kept whole: left, top, right, bottom (or one number)
scale = 2 # screen px (480 reference) per image px; 0.25 to 8
pad = 0 # how far the frame goes past the box, in reference px (or [l, t, r, b])
tile = false # true: edges and middle repeat instead of stretchingCorners stay whole; edges and middle stretch (or repeat with tile). Boxes that accept a frame:
| name | where |
|---|---|
header, footer | header and footer bands (height of header_height/footer_height) |
list | behind the list; use pad so the frame sticks out |
row, selected | each list row and the chosen row (in place of the colored band) |
panel | system panel, menus and, in Details, the table and the title-to-buttons column |
art | frame of the cover and of the game screenshot in Details |
button, button_selected | Details buttons |
badge | footer A/B/X/Y buttons (the letter uses the text color) |
tile, tile_selected | blocks of the tile view (mark = "tile") |
Text on frames uses the usual colors: text_secondary on rows, accent_text on the chosen one, text and muted on panels.
Text outline
text_outline = { color = "#3F2214", width = 2, ink = "#FFFFFF" }It applies to text drawn straight on the background: header, footer hints and the system name when the view has no panel (panel = false). width is 0 to 4 reference px; ink (optional) changes the color of that text, for light letters with a dark outline over a picture background.
Decorations
Each screen of each view can hold loose pictures, still or animated:
[view.lista.systems]
decor = [
{ image = "decor/palm.png", x = 0.88, y = 0.02, scale = 2 },
{ image = "decor/wave.png", x = 0.0, y = 0.82, scale = 2, layer = "back" },
{ image = "decor/crab.png", x = 0.02, y = 0.8, scale = 2, frames = 2, fps = 2 },
]x, y: top-left corner as screen fractions. scale: reference px per image px. frames: frames side by side in the image, switched fps times per second (still when Animations is Off). layer = "back" draws behind the list and panels; the default is in front. The list replaces the inherited one; decor = [] clears it. The Details screen uses the decorations of the Games screen.
Views
A view places the elements of the Systems (systems) and Games (games) screens. The user picks among the views the theme declares in Settings › Appearance › View (launcher.view). Every view starts from lista and changes only what it writes; a theme inherits the default's views (today lista and vitrine) and can change them or add new ones.
[view.lado]
name = { en = "Side", pt_BR = "Lado" }
[view.lado.games]
list = { x = 0.4, w = 0.575 } # list on the right
art = { x = 0.025, w = 0.328125 } # cover on the leftThe Praia example only moves the list to the right in the lista view:
[view.lista.systems]
list = { x = 0.4375, w = 0.5375 }
panel = { x = 0.025, w = 0.390625 }
art = { x = 0.05625, w = 0.328125 }
info = { x = 0.05, w = 0.340625 }
[view.lista.games]
list = { x = 0.4, w = 0.575 }
panel = { x = 0.025, w = 0.328125 }
art = { x = 0.025, w = 0.328125 }
meta = { x = 0.025, w = 0.328125 }Elements of each screen ([view.<id>.systems] or [view.<id>.games]):
| key | what it is | fields |
|---|---|---|
header | header | "full" (name and counter) or "compact" (one quiet line: "SNES · 4 of 213") |
title | title of the Systems header and of the Start Menu | "brand" (spaced ARCHR, the default) or "screen" ("Systems"/"Start Menu", in the language; the Start Menu then does not repeat the title in the middle) |
status | clock/battery pill | true/false |
divider | line above the footer | true/false |
summary | library summary in the footer (systems) | true/false |
list | the list | x y w h, row, selected (height of the selected row), gap, font, selected_font, radius, axis, mark, scroll, cursor |
panel | raised card (system panel, cover frame) | x y w h, radius |
art | console picture or game cover | x y w h, fit, radius |
backdrop | the cover behind the Games screen | opacity, solid (fraction of the width in plain background), edge (background opacity at the right edge) |
info | system name and details | x y w h, title, meta (sizes), align (left/center/right), valign (top/bottom), details (stacked: maker, name, count; inline: name and "213 games · Nintendo · 1990") |
meta | year, players and "played … ago" under the cover | x y w h, size |
dots | position dots between systems | x y w h |
decor | loose pictures (see Decorations) | list of { image, x, y, scale, frames, fps, layer } |
list values:
mark:"pill"(colored band),"bar"(small bar beside the text) or"tile"(systems as side-by-side tiles, the selected one large in the middle with the console picture; Systems screen only). In a tile carousel, ← and → (and the shoulders, which the launcher reads as the same buttons) move one system; ↑ and ↓ also move one, so no button jumps to the end. In a vertical list, ↑/↓ move one row and ←/→ one page, as many rows as the list shows (hdivided byrow + gap).scroll:"edge"(scrolls when reaching the edge) or"center"(keeps the selection in the middle).axis:"vertical"or"horizontal". Atilelist is always a horizontal row.cursor: short text (up to 4 characters, in the list font) drawn before the chosen item, blinking half a second on and half a second off, for examplecursor = "▶"(the font needs the character). With Animations Off it stays lit. Empty (the default) draws nothing. Does not apply to tiles (tile).
All unselected rows use one color: text_secondary (collections in muted).
An optional element disappears with false (panel = false) and appears with a table. Out-of-range values are clamped to the screen; unknown keys and wrong types are ignored with a warning in the log. None of this crashes the launcher.
The built-in vitrine view, for reference:
[view.vitrine]
name = { en = "Showcase", pt_BR = "Vitrine", es = "Escaparate", ru = "Витрина" }
[view.vitrine.systems]
divider = false
summary = false
panel = false
art = false
list = { x = 0.0, y = 0.2, w = 1.0, h = 0.383333, row = 0.2, selected = 0.383333, gap = 0.045833, radius = 0.058333, mark = "tile" }
info = { x = 0.0375, y = 0.633333, w = 0.925, h = 0.15, title = 0.0625, meta = 0.029167, align = "center", valign = "top", details = "inline" }
dots = { x = 0.0, y = 0.816667, w = 1.0, h = 0.0125 }
[view.vitrine.games]
header = "compact"
divider = false
backdrop = { opacity = 0.55, solid = 0.42, edge = 0.6 }
list = { x = 0.0375, y = 0.133333, w = 0.46875, h = 0.683333, row = 0.070833, selected = 0.1, gap = 0.004167, font = 0.03125, selected_font = 0.045833, mark = "bar", scroll = "center" }
panel = { x = 0.58125, y = 0.133333, w = 0.36875, h = 0.658333, radius = 0.029167 }
art = { x = 0.58125, y = 0.133333, w = 0.36875, h = 0.658333, fit = "cover", radius = 0.027083 }
meta = { x = 0.58125, y = 0.816667, w = 0.36875, h = 0.041667, size = 0.025 }Creating a new view
- Copy
themes/praia/to/storage/.config/archr-launcher/themes/mine/. - Change or remove the top-level
namein the copiedtheme.toml; otherwise the copy keeps Praia's display name. - Add
[view.myview]with anameand the elements that change. - In Appearance, pick the theme and the view by the display names they declare; when you leave the page the change applies at once, without restarting.
Game Details
The details screen (Y › Details in the game list) puts each block in a rectangle from theme.toml. The details_* keys sit at the top of theme.toml and apply to every view; each is optional and inherits from the default theme.
Each value is { x, y, w, h }: x and w are fractions of the screen width, y and h fractions of the height (640×480 reference). The rectangle must fit the screen (x + w <= 1, y + h <= 1); an invalid value falls back to the default.
| key | content |
|---|---|
details_shot | game screenshot (or the cover when there is none); with a video, the still frame |
details_info | table with genre, players, release, developer, publisher and rating |
details_title | game logo, left-aligned; without a logo, the name as text |
details_subtitle | system · last played |
details_text | description, scrolled with ↑ ↓ |
details_actions | Play, Favorite and Scrape again buttons |
Defaults:
details_shot = { x = 0.0375, y = 0.104166667, w = 0.390625, h = 0.3125 }
details_info = { x = 0.0375, y = 0.4375, w = 0.390625, h = 0.445833333 }
details_title = { x = 0.45625, y = 0.104166667, w = 0.50625, h = 0.095833333 }
details_subtitle = { x = 0.45625, y = 0.216666667, w = 0.50625, h = 0.033333333 }
details_text = { x = 0.45625, y = 0.266666667, w = 0.50625, h = 0.529166667 }
details_actions = { x = 0.45625, y = 0.8125, w = 0.50625, h = 0.070833333 }Module widgets on the Start Menu
start_widgets ({ x, y, w, h }, same rules, but as fractions of the Start Menu's 640×480 composition rather than of the screen; see start_area) is the strip for the 2×1 widgets of modules (up to 2, side by side, split equally). When there is a widget, the blocks move up and shrink to fit above the strip and the shortcuts go just below it; without a widget the Start Menu stays as usual. The strip is limited: it starts after 178 px (the blocks need 72 px), is at least 40 px tall and ends in time for the shortcuts and their names to fit above the footer; otherwise the launcher pushes it to the limit. Default:
start_widgets = { x = 0.153125, y = 0.454166667, w = 0.69375, h = 0.216666667 }Colors come from the general keys (surface, border, text_secondary, muted, accent, warning for the stars). Table rows that do not fit in details_info are left out; text is never cut with an ellipsis: it shrinks and wraps.
Module apps
A module with API 4 can have an app: a row of its own in the carousel (after the collections) that opens the module's screens (see Apps (API 4)). Nothing new in theme.toml; every theme already works with apps.
- Carousel art:
systems/app-<id>.png(or.jpg,.webp), where<id>is the module id, with the same rules as console pictures (the user's folder, then the theme, then the default). Without it the launcher uses the module's own art, then its large icon. - App screens follow your layouts. A list screen uses the
gameslayout of the active view, and a detail screen uses thedetails_*rectangles, with the module's picture indetails_shotand its buttons indetails_actions. Filter chips, tags, the search keyboard and the confirm question use the theme's colors. Text is never drawn under 10 px.
Game achievements
The achievements screen (Y › Achievements in the game list, with RetroAchievements on and an account signed in) also follows rectangles at the top of theme.toml, with the same rules as details_*.
| key | content |
|---|---|
achievements_progress | "Progress", count and points over the bar; the Hardcore badge at the right end |
achievements_list | the achievement list, 50 px rows (480 px reference) with 2 px between them; L/R turns the page |
Defaults:
achievements_progress = { x = 0.025, y = 0.108333333, w = 0.94375, h = 0.0625 }
achievements_list = { x = 0.025, y = 0.183333333, w = 0.94375, h = 0.7125 }A page holds as many rows as fit in the height of achievements_list. Colors: surface and accent on the selected row, accent on the bar and badge, border on the bar track, badge on the icon of a locked achievement without a picture, muted on descriptions and points. Locked achievements are dimmed. Titles and descriptions come from RetroAchievements: the title shrinks down to 12 px and the description wraps onto two smaller lines before being cut.
Game saves and screenshots
The Saves and Screenshots screens (Y › Saves or Y › Screenshots in the game list, when the game has saves or screenshots) also follow rectangles at the top of theme.toml, with the same rules as details_*.
| key | content |
|---|---|
saves_list | the saves (slots and the automatic one), 62 px rows (480 px reference) with 4 px between them, 64×48 thumbnail on the left; L/R turns the page |
saves_card | the selected save: the 4:3 picture at the rectangle's width, then Saved at, Emulator and Size, and the note about what happens when you play |
shots_grid | the screenshot grid: as many columns of about 138 px (12 px apart) as the width allows, stretched to fill; rows follow the height |
Defaults:
saves_list = { x = 0.01875, y = 0.1, w = 0.4875, h = 0.783333333 }
saves_card = { x = 0.53125, y = 0.1, w = 0.440625, h = 0.783333333 }
shots_grid = { x = 0.040625, y = 0.108333333, w = 0.91875, h = 0.791666667 }Colors come from the general keys: accent on the selected row and screenshot, surface and border on the picture frame, muted on dates and notes, dim on the relative time, danger on the Delete button of the confirmation. An opened screenshot fills the screen, with the game name at the top and the buttons in the footer; the confirmations (Play from here and Delete) are centered windows.
Switcher and resume at power on
The Switcher (a tap on FN during a game: the game is saved and closed and the launcher opens on the recent games) and the resume screen at power on (Settings › Games › Resume and switch) follow rectangles in theme.toml, with the same rules as details_*.
| key | content |
|---|---|
switcher_card | the selected game's picture, with the accent ring around it and the "Saved now" (or "No resume") badge in the corner |
switcher_side | the faded neighbors: w and h are their size, y the top and x the gap to the selected picture (left and right are mirrored; they may go off screen) |
switcher_info | the game name (19 px, centered), the line with system and when it was played, and one dot per game |
resume_shot | the game picture on the resume screen |
resume_info | "Going back to", the name (22 px), when it was saved, the countdown bar and the A and B buttons |
Defaults:
switcher_card = { x = 0.1875, y = 0.108333333, w = 0.625, h = 0.625 }
switcher_side = { x = 0.046875, y = 0.129166667, w = 0.375, h = 0.375 }
switcher_info = { x = 0.0, y = 0.758333333, w = 1.0, h = 0.125 }
resume_shot = { x = 0.25, y = 0.110416667, w = 0.5, h = 0.5 }
resume_info = { x = 0.0, y = 0.6375, w = 1.0, h = 0.258333333 }Colors come from the general keys: accent on the ring, on the selected game's dot and on the bar; badge on the other dots and the bar background; surface and border on the box of a game without a picture; muted on the support lines. The picture is the screen at the moment the game was saved; without it, the game screenshot or the cover.
Moving the ROMs
The screen shown in the maintenance boot that moves the ROMs to the exFAT partition (archr-launcher --roms-migrate-screen) always uses the installed default theme, never the chosen one: in that boot the STORAGE partition, where user themes live, is unmounted to shrink, and a font opened there would block it. Like Settings, its layout is fixed.
Animations
Settings › Appearance › Animations (launcher.animations): On (on), Reduced (reduced: only short fades, nothing moves or grows) and Off (off: everything instant). The timings are fixed and not set by themes: selection 180 ms, systems→games 220 ms, cover change 200 ms, menus 160 ms, notices 200 ms, switches 150 ms, opening 300 ms. The old values 1 and 0 count as On and Off. The theme chooses only the curve (easing, see Style). Any button ends the running animation; input never waits.
Sounds
A theme can have sounds: for movement, for actions and for each system. No sound is required: a missing one comes from the default theme.
The simplest way
Create a sounds/ folder in the theme and put .ogg or .wav files named after the event. Nothing needs to be declared in theme.toml.
| File | Plays when |
|---|---|
boot | the launcher opens |
move | the cursor changes item |
page | jumping a page (L/R) or letter |
select | opening a system, page or item (A) |
back | going back (B) |
launch | opening a game, tool or stream |
menu_open | opening the quick menu (Y) |
menu_close | closing the quick menu |
toggle | changing a value in Settings |
error | an operation fails |
notice | a notice appears |
favorite | marking or unmarking a favorite |
An event without a file plays the default theme's sound; to silence it, use "" in the [sounds] table.
Per-system sounds
sounds/systems/<system>.ogg (or .wav) plays when the cursor stops on that system, for example sounds/systems/snes.ogg. It only plays after about 350 ms standing still, so scrolling fast triggers nothing. The system shown when the launcher opens does not play.
[sounds] table (optional)
Only needed to rename a file or silence an event that has a file. Paths are relative to the theme and cannot leave it.
[sounds]
move = "sounds/tick.wav" # another file
select = "" # silence, even if sounds/select.ogg exists
[sounds.systems]
snes = "sounds/jingle.ogg"
nes = ""Limits and format
- WAV or OGG Vorbis, short (up to 1 MiB per file); mono or stereo.
- All sounds load at startup, off the drawing thread.
- Volume (0 to 100%) and the switch are in Settings › Sound and display (Theme sounds and Sound volume); the volume is relative to the system's.
- The
defaulttheme ships a synthesized set and no per-system sounds.
Testing your theme
Copy the folder to /storage/.config/archr-launcher/themes/ on the device over SSH or Samba and choose it in Settings › Appearance › Theme. Warnings about invalid keys go to the launcher's log. What works there works when installed from the store.
When developing the launcher itself, the repository can capture the Systems, Games, Game Details and Appearance screens for each folder:view:
ARCHR_SETTINGS_CAPTURE_DIR=/tmp/captures \
ARCHR_CAPTURE_VIEWS=default:vitrine,praia:lista,praia:vitrine:noart \
cargo test -p archr-ui --lib capture_views -- --ignored:noart captures as if there were no console pictures. The folder is relative to the repository's themes/, or an absolute path.
The captures are 640×480. For another screen add ARCHR_CAPTURE_SIZE=<width>x<height>. File names do not change with the size, so use one folder per size:
for size in 640x480 720x720 480x320; do
mkdir -p /tmp/captures/$size
ARCHR_CAPTURE_SIZE=$size \
ARCHR_SETTINGS_CAPTURE_DIR=/tmp/captures/$size \
ARCHR_CAPTURE_VIEWS=praia:lista,praia:vitrine \
cargo test -p archr-ui --lib capture_views -- --ignored
doneAny size works (854x480 for 16:9).
The resolutions test checks every theme with a theme.toml in the repository's themes/, and each of its views, at 640×480, 720×720 and 480×320: the list under the header, nothing off screen, panel, picture, text and dots clear of the list, and the Details, Saves, Switcher and resume rectangles not overlapping. To check your own theme, copy its folder into themes/ and run:
cargo test -p archr-ui --test resolutionsPublishing to the Theme store
Settings › Appearance › Theme store searches GitHub for themes, shows cover, details and pictures, and installs with one button. The store only sees repositories that follow the rules below.

What the store looks for
- A public GitHub repository, not archived.
- With the topic
archr-launcher-theme(on the repository page: gear next to "About" › Topics). - The repository root is the theme folder:
theme.tomlat the root, not in a subfolder. One repository = one theme. theme.tomlwithname. Withoutname, or with atheme.tomlthat is not valid TOML, the theme is listed but does not install.preview.pngat the root: 640×480, PNG, up to 2 MB. It is the cover in the grid and the first picture in the details. The grid crops it to a wide, short band in the middle, so keep the center representative: a capture of the Systems or Games screen with the theme works well.- Optional:
screenshots/1.png,2.pngand3.png, 640×480, up to 2 MB each. They show in the details, switched with L/R.
Owner, repository and tag names: letters, digits, ., _ and - (no /). The store shows the first 30 search results, by stars (Popular) or by last update (Recent).
Metadata in theme.toml
For store publication, name is required. The other fields are optional, and the details show whatever is there.
name = { en = "Neon Arcade", pt_BR = "Neon Arcade" }
description = { en = "Neon pink over deep purple.", pt_BR = "Rosa neon sobre roxo profundo." }
author = "kaio-dev" # without an author, the repository owner is shown
version = "1.2.0" # without a version, the release tag is shown
license = "CC BY 4.0" # short text; the LICENSE file is installed too
min_launcher = "0.2" # minimum launcher version; below it, no installdescription takes text or a per-language table, like name, up to 200 characters. "Includes" in the details is worked out from the files: colors always; fonts (.ttf/.otf), layout ([view.*]), art (systems/), icons (icons/) and sounds (sounds/) when present. "Size" counts only what gets installed.

Which version is installed
- With a GitHub release: the latest one ("Latest release"), by its tag.
- Without a release: the default branch as it is.
The store keeps the identifier of the installed content. When the release (or the branch) changes, the details button changes from Use now to Update. To ship a new version, publish a new release.
What is installed
The version's tarball is downloaded from codeload.github.com (there is no .zip and no asset to attach) and only these files are copied:
- extensions
toml,png,jpg,jpeg,webp,svg,ttf,otf,ogg,wav,txt,md; LICENSE,LICENCEandCOPYING.
Any other file is ignored, as are files and folders starting with . (.github/, .gitignore), symbolic links, hard links and special files. A path with .. or an absolute path rejects the whole theme. Nothing is executed.
Limits: download up to 25 MB, installed content up to 60 MB, up to 5000 entries in the archive and 8 folder levels. Theme sounds keep their own 1 MiB limit each.
The theme goes to /storage/.config/archr-launcher/themes/<owner>-<repository>/ with a .archr-store.toml file (where it came from, which version). The new version is downloaded and validated before it replaces the old one; if the final rename fails, the store tries to restore the previous version. The store never writes over a folder it did not create and only removes themes it installed; the theme in use cannot be removed. Installing does not activate: after installing, Use now selects the theme in Appearance, which saves when you leave.
Example repository
github.com/kaio-dev/archr-neon-arcade topic: archr-launcher-theme
├── theme.toml name, description, author, version, license
├── preview.png 640×480
├── screenshots/
│ ├── 1.png
│ └── 2.png
├── fonts/
│ └── Orbitron-Regular.ttf
├── systems/
│ └── snes.png
├── sounds/
│ ├── move.ogg
│ └── select.ogg
├── LICENSE
└── README.mdNetwork and GitHub limits
The store uses the GitHub API without signing in, which allows few requests per hour. Lists and details are kept for an hour (Y forces a new search); when GitHub rate-limits, the saved list keeps showing with a notice. Without a network, the Installed tab works and shows each theme's details from the installed folder.