ARCH R

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.

The example theme (themes/praia) in the Showcase view: list and art move, colours change


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 language

Without 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.

keyused for
backgroundbackground
surface, surface_elevatedpanels and windows
border, badgeborders and footer buttons
text, text_secondary, muted, dimtext, from strongest to faintest
accent, accent_text, accent_text_secondaryhighlight and the text on it
success, warning, dangerpositive, attention and failure states
scrimveil 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)valueseffect
Font weight (launcher.font_weight)from the theme (empty) / boldbold 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) / offoff hides the number in the status pill; the icon stays.
Box art (launcher.game_art)on (default) / offoff 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 widththe 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 pxreplaces 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) / #RRGGBBreplaces the theme's text; text_secondary is derived (25% toward background).
Selected text color (launcher.selected_text_color)from the theme (empty) / #RRGGBBreplaces the theme's highlight_text and the accent_text_secondary that goes with it.
Secondary text color (launcher.secondary_color)from the theme (empty) / #RRGGBBreplaces the theme's muted and the dim_text that goes with it.
Background (launcher.background)from the theme (empty) / black / midnight / #RRGGBBwith #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) / onfooter 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) / offon 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_text and muted, 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 the background.
  • 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 panel goes away only if it has the same x and w as the art; a panel with another rectangle (a larger card, a decorative frame) is still drawn.
  • Wider box art: the art rectangle 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 art and, when the panel coincides with it, to the panel (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 px

font_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:

screenscalescreen in reference pxshape
640×4801640×480classic
720×7201.125640×640square
480×3200.667720×480wide
1280×7201.5853×480wide

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_*, the x y w h of 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:

shaperatioscreens
squarebelow 1.2 (6:5)720×720
classic1.2 to 1.45 (29:20), inclusive640×480, 800×600
wideabove 1.45480×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 sets cover or list_width at 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, in vitrine, a wider cover.
  • [aspect.wide]: lists in larger text, 19 reference px (font_size = 0.039583333, 13 px on a 480×320 screen), and the vitrine game 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.

keyvaluesdefaulteffect
corners0 to 11.0multiplies every corner radius; 0 makes everything square
border_width1 to 81border 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 center

With 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 .png or .jpg.
  • Lookup order: the user folder /storage/.config/archr-launcher/backgrounds/ first, then the theme's backgrounds/, then the default theme's backgrounds/.
  • The picture covers the whole screen (cropped to fill) under a curtain of the theme's background color at about 59% opacity (alpha 150), so the text stays readable. It replaces the theme's background_image on 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 background that 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 stretching

Corners stay whole; edges and middle stretch (or repeat with tile). Boxes that accept a frame:

namewhere
header, footerheader and footer bands (height of header_height/footer_height)
listbehind the list; use pad so the frame sticks out
row, selectedeach list row and the chosen row (in place of the colored band)
panelsystem panel, menus and, in Details, the table and the title-to-buttons column
artframe of the cover and of the game screenshot in Details
button, button_selectedDetails buttons
badgefooter A/B/X/Y buttons (the letter uses the text color)
tile, tile_selectedblocks 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 left

The 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]):

keywhat it isfields
headerheader"full" (name and counter) or "compact" (one quiet line: "SNES · 4 of 213")
titletitle 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)
statusclock/battery pilltrue/false
dividerline above the footertrue/false
summarylibrary summary in the footer (systems)true/false
listthe listx y w h, row, selected (height of the selected row), gap, font, selected_font, radius, axis, mark, scroll, cursor
panelraised card (system panel, cover frame)x y w h, radius
artconsole picture or game coverx y w h, fit, radius
backdropthe cover behind the Games screenopacity, solid (fraction of the width in plain background), edge (background opacity at the right edge)
infosystem name and detailsx 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")
metayear, players and "played … ago" under the coverx y w h, size
dotsposition dots between systemsx y w h
decorloose 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 (h divided by row + gap).
  • scroll: "edge" (scrolls when reaching the edge) or "center" (keeps the selection in the middle).
  • axis: "vertical" or "horizontal". A tile list 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 example cursor = "▶" (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

  1. Copy themes/praia/ to /storage/.config/archr-launcher/themes/mine/.
  2. Change or remove the top-level name in the copied theme.toml; otherwise the copy keeps Praia's display name.
  3. Add [view.myview] with a name and the elements that change.
  4. 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.

keycontent
details_shotgame screenshot (or the cover when there is none); with a video, the still frame
details_infotable with genre, players, release, developer, publisher and rating
details_titlegame logo, left-aligned; without a logo, the name as text
details_subtitlesystem · last played
details_textdescription, scrolled with ↑ ↓
details_actionsPlay, 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 games layout of the active view, and a detail screen uses the details_* rectangles, with the module's picture in details_shot and its buttons in details_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_*.

keycontent
achievements_progress"Progress", count and points over the bar; the Hardcore badge at the right end
achievements_listthe 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_*.

keycontent
saves_listthe 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_cardthe 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_gridthe 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_*.

keycontent
switcher_cardthe selected game's picture, with the accent ring around it and the "Saved now" (or "No resume") badge in the corner
switcher_sidethe 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_infothe game name (19 px, centered), the line with system and when it was played, and one dot per game
resume_shotthe 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.

FilePlays when
bootthe launcher opens
movethe cursor changes item
pagejumping a page (L/R) or letter
selectopening a system, page or item (A)
backgoing back (B)
launchopening a game, tool or stream
menu_openopening the quick menu (Y)
menu_closeclosing the quick menu
togglechanging a value in Settings
erroran operation fails
noticea notice appears
favoritemarking 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 default theme 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
done

Any 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 resolutions

Publishing 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.

Theme store

What the store looks for

  1. A public GitHub repository, not archived.
  2. With the topic archr-launcher-theme (on the repository page: gear next to "About" › Topics).
  3. The repository root is the theme folder: theme.toml at the root, not in a subfolder. One repository = one theme.
  4. theme.toml with name. Without name, or with a theme.toml that is not valid TOML, the theme is listed but does not install.
  5. preview.png at 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.
  6. Optional: screenshots/1.png, 2.png and 3.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 install

description 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.

Theme store details

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, LICENCE and COPYING.

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.md

Network 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.

On this page