Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Omikuji

User guide built on 0.14.0, dev guide built on 0.14.0.

- If you need an installation guide, start here: User Guide.

- If you need an usage guide or more infos on about how the app works or check the Q&A, start here: User Guide.

- If you want to contribute: Developer.

- If you’re sane: github.com/lutris/lutris.

Installation

This is how you install omikuji. Pick whatever fits your setup, cutie pie~

Arch (AUR)

yay -S omikuji-bin   # prebuilt
yay -S omikuji-git   # builds from latest source

Fedora (COPR)

sudo dnf copr enable reakjra/omikuji
sudo dnf install omikuji

Fedora 43 and 44. Or grab the .rpm from the releases page.

Flatpak

Not on Flathub yet. There is a signed repository instead:

flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists omikuji https://omikuji-launcher.github.io/omikuji/flatpak/index.flatpakrepo
flatpak install omikuji io.github.reakjra.omikuji

Flathub is required as well: omikuji builds on the KDE and Wine runtimes, which are hosted there.

A single .flatpak bundle is also attached to every release.

flatpak install omikuji.flatpak

Nix

For flake issues, mention @claymorwan when opening a issue.

On NixOS with flakes, add the input:

# flake.nix
inputs.omikuji = {
    url = "github:omikuji-launcher/omikuji";
    inputs.nixpkgs.follows = "nixpkgs";
};

Then install via the Home Manager module (recommended) or as a package:

# home-manager module
programs.omikuji.enable = true;

# or as a package (NixOS or home-manager)
environment.systemPackages = [
    inputs.omikuji.packages.${pkgs.stdenv.hostPlatform.system}.default
];

To skip compiling, add the Cachix cache:

nix.settings = {
    substituters = [ "https://omikuji.cachix.org" ];
    trusted-substituters = [ "https://omikuji.cachix.org" ];
    trusted-public-keys = [ "omikuji.cachix.org-1:dS6sbpMxarHWIIk3y0R7KXz3eVHUg1lo/y3gMbv4JhM=" ];
};

Or run it without installing:

nix run github:omikuji-launcher/omikuji

From source

Needs Rust (2024 edition), Qt 6.7+, pkgconf, and cmake.

git clone https://github.com/omikuji-launcher/omikuji.git
cd omikuji
cargo build --release
./target/release/omikuji

Guide

First Steps

Welcome Page

This is the welcome page where you can get started with omikuji. Yes it’s very tiny. From here you can click the ‘install runner’ button to install a runner from the default list.

If your system lacks umu, as you can see, you can choose to let it install by keeping the toggle on. It’ll install a built-in umu-run. If it happens that you have umu-run already installed system-wide, it’ll hide the option.

Welcome’s Component Installer

This is the welcome component installer where you can install a runner from the default list by clicking install (optionally you can choose which version of each release with the filter, if more than one option is available (_v3, x86_64, etc.)).

The ‘Latest’ option will install the latest release and it’ll update automatically the next time you start omikuji, if a new release is available.

Logging into Epic Games / GOG

First, select one of the two stores in the left navbar.

Then, click the ‘Open Login Page’ text. It’ll redirect you to the Epic Games or GOG login page. From there, login with your account, it’ll open one of the two pages (Epic Games and GOG respectively).

For Epic Games, you need to copy the authorizationCode key.

For GOG, you need to copy the authorization code from the link itself after the code= text (mind it’s very long. make sure to copy it all).

Paste the code into the input field and click ‘Login’.

Adding/Installing a Game

Omikuji has a bunch of ways to install games.

Stores

Steam

Steam doesn’t install anything from the launcher, you need to install games from Steam itself. However, you can import them in your library by just clicking the + button on the bottom left of the card. Importing Steam games gives them the steam runner type, that means it delegates the game launching to the Steam client.

Epic Games / GOG

Omikuji uses Legendary to manage your Epic Games library and gogdl to manage your GOG library. You can scroll the page to see all the games you can install. By clicking the +, it opens an install dialog.

The Installation path field is where the game will be installed.

The Prefix path field is the prefix you want the game to use when installed and added to the library.

Runner dropdown is where you can select the runner to use for the game. (Along with the prefix path, these two will just seed the game settings after the download is finished and the entry in library gets created, you can of course change them later in the game settings)

DLC checkboxes are where you can select the DLCs to install with the game. You can select multiple DLCs at once or select none at all. You can also install DLCs separately after the game is installed, just re-open the game’s store card and checks the DLCs you want and press the Install DLC button.

All this applies to GOG installations too, they’re the same.

Gacha

Gacha games are similar to Epic Games / GOG installations. However, some of them may come with the ability to toggle various voice packs installation. Also, if the game supports, you’re able to change the version (Global, CN, JP, etc.)

Some gacha may come with additional things like, for example, Genshin Impact has a toggle to install the Fps Unlocker. If checked, it will install the latter alongside the game and will apply, in the game’s settings, the Run Alongside field filled with the path to the unlocker executable.

Or additionally, some gacha may have a ‘suggested’ runner, which, if selected, will be installed on the spot and applied to that game (for example, hsr, that needs a specific Dawn Winery Proton to run). You still are able to select any other runner from the list and ignore the suggested one.

Also some gacha games may have a ‘temp path’ separated from the game’s install path. The temp path is used for storing temporary files during the game’s installation or update process. You can point the two in two different locations entirely.

Manually

Games that don’t come from a store get added by hand with the + button in the top right (and then Add Game in the small popup), which opens the New Game dialog.

In the Game Info tab, a Name is the only thing actually required, everything else can be filled later. The Runner dropdown is the important one:

- Wine for Windows games (.exe)

- Native for Linux ones (binaries or .AppImage)

- Steam to delegate the launch to the Steam client (requires the game to be installed in Steam)

- Flatpak for flatpak apps.

In the Runner tab you’ll want the Path to the executable (for Steam and Flatpak you get an Application ID field instead), a Version (the wine or proton build it runs on, install one from Settings > Components if the list is empty or just drop one manually in the runners dir) and optionally a Prefix. Leave the prefix empty and omikuji creates a fresh one for that game.

Create adds it to the library, Create & Play adds it and launches it right away.

This is the short version. A good amount of fields in both tabs, plus the translation layers, environment variables and everything else, is covered in Game Settings below.

Import

If you want to add an Epic Games, GOG or Gacha game, you should NOT add them manually! Instead, go to the store the game is from and click the game card’s + to open the dialog of the game you want to import.

Epic Games & GOG

For both Epic Games and GOG, there are two situations:

- The game is installed AND there’s an entry in Legendary/gogdl .json files. This will lock the path since it already knows where the game to import is. Pressing import will add the game to the library with that path.

- The game is installed but there’s no entry in Legendary/gogdl .json files. This will let you choose the path manually, since we want to import, you have to specify the path where the game is currently installed. Pressing import will add the game to the library with that path.

For Repairs, the game has to be installed and in the library. You’ll find the game in the store, open the install dialog, it should resolve the installed path automatically (since it needs to be registered in the Legendary/gogdl .json files). Hit repair and it’ll repair the game. Careful! Legendary will repair as the term intends! GOG will reinstall the game.

Gacha

For Gacha games the pattern is the same, with the difference that you’ll always have to specify the path manually. Also, as the note chip says when it successfully finds the game in the path specified, make sure to select the proper version of the game (e.g. Global, CN, etc.) so the game will be registered properly. This is important for gacha updates.

Also, for most games it’ll detect the installed game version automatically.

Regarding repairs, for now only HoYo games support them.

Downloads

You can check your own downloads in the Downloads tab in the left navbar.

When starting a download from any store, if nothing is already in progress, the download will start automatically. If there is already a download in progress, the download will be queued and will start once the current download is finished or paused.

From the page you can see the net speed, the disk usage, and the progress of the download.

Game Settings

Per-game settings open from the library: right-click a game and click Configure, or use the gear button on the bottom floating bar. Changes are staged until you hit Save or Save & Play.

Runner type

On the Game Info tab, the runner type decides how the game launches:

- Wine: run a Windows game through wine or proton. The default for most things.

- Steam: hand off to Steam, for imported Steam games.

- Native: run a Linux binary or .AppImage directly.

- Flatpak: launch an installed flatpak app.

The rest of Game Info (name, artwork, color) is optional and self-explanatory.

Runner (Wine)

Wine/proton config. Only shown for Wine games. (we’ll see the settings per header)

Executable

- Path: the game’s .exe.

- Working Directory: where the process starts. Empty uses the exe’s folder.

- Arguments: passed to the game, e.g. --skip-intro, -use-d3d12.

- Command Prefix: prepended to the whole launch command, for a custom wrapper.

Wine

- Version: which wine/proton build to use.

- Prefix: the wine prefix. Empty auto-creates a fresh one per game (named <slug>-<id>), the usual setup.

- Architecture: 64- or 32-bit prefix.

Translation Layers

- DXVK: Direct3D 9/10/11 to Vulkan.

- VKD3D: Direct3D 12 to Vulkan.

- DXVK-NVAPI: exposes NVIDIA features (DLSS, Reflex).

For this there are two things happening. First of all, toggling these layers (except D3D Extras) will show dropdowns. These dropdowns allow you to select the version of the layer to use. Default (Global) takes the version selected in Settings => Components.

Now, for Proton, since it overrides the wine prefix dlls for these layers, we simply cannot just swap the prefix ones only. So, when you enable this toggle, on launch, omikuji will swap the dlls in the wine prefix and the runner’s built-ins.

For Wine, since this doesn’t occur, it will simply swap the prefix dlls.

Omikuji tracks both the swapped ones and the old ones. So, let’s say you have two games with Proton. Resident Evil Revelations and Cyberpunk 2077. If on the first game you enable a layer toggle, on launch, it will swap the dlls as said. Note that nothing will revert these dlls when you close the game. However, if you then run the second game, Cyberpunk 2077, that has these toggles OFF, it will revert them to the runner’s default. So be careful, because if they’re overridden, and you launch a game from another launcher, you will still be using the overridden dlls.

Also of course for wine it adds the env vars to work.

Misc

- Display: DPI Scaling with a DPI slider.

- Drivers: the wine Audio Driver (Default, PulseAudio, or ALSA).

- Graphics Driver: X11/Wayland picker.

- DLL Overrides: Key-value pairs of DLLs to override with their path.

Runner (Steam)

- Application ID: The Steam application ID of the game.

- Arguments: arguments to pass to the game.

Runner (Native)

- Executable: binary or .AppImage of the game path.

- Working Directory: the parent directory of the game.

- Arguments: arguments to pass to the game.

- Command Prefix: prepended to the whole launch command, for a custom wrapper.

Runner (Flatpak)

- Application ID: the flatpak application ID of the app.

- Arguments: arguments to pass to the game.

System

Performance: GameMode (Feral’s gamemoderun) and a CPU core limit.

Display: MangoHUD overlay and the GPU to run on. The GPU list comes from your installed Vulkan drivers, handy on multi-GPU setups to pin a game to the dGPU or iGPU.

Audio: Reduce Pulse Latency.

Power: Prevent Sleep keeps the screen awake while the game runs.

Discord Rich Presence: enable Discord Rich Presence for the game.

Gamescope: runs the game inside Valve’s gamescope compositor. Enabling it exposes output and game resolution, an FPS and Refresh Rate cap, fullscreen or borderless, integer scaling, and an upscaling filter (FSR, NIS, nearest, linear) and HDR.

Environment: env vars to use when launching a game.

Sets: Stored env vars that you can:

- Sync (they will be used along the current game’s ones. Synced ones are not added in the game settings, so, changing a synced set will apply to all games)

- Add: (adding an env set will apply the contained vars to the game settings).

Scripts:

- Pre-Launch: Script to run before launching the game (waits for the script to finish before launching the game).

- Post-Exit: Script to run once the game exits (waits for the game to close before running).

- Run Alongside: Script/Wine .exe to run alongside the game.

When you fill the path field, it’ll show you three more options below it:

Arguments: arguments to pass to the script/binary.

Start it First: starts the script/binary before launching the game.

Delay: delay in seconds for the wait between the two (game and script/binary). For example, if start it first is disabled and you set delay to 10, the game will launch first and then wait 10 seconds before running the script/binary.

Epic

Only appears for Epic games.

- EOS Overlay: installs and enables the Epic Online Services overlay for the game (if it’s already installed you still need to enable it here, but it won’t download again).

- Auto Sync: Auto-sync pulls saves before launch and pushes them after exit. Save Path Override covers the case where the path isn’t detected automatically. Note that it won’t find the path if the game was never run first.

- DLC: List of the installed DLCs for the game. Legendary allows you to uninstall them, just hover the dlc you want to remove and click the x on the right.

GOG

Only appears for GOG games.

- DLC: List of the installed DLCs for the game. However, unlike Legendary, gogdl DLCs are not uninstallable.

But! If the page is empty, there’s a cute dino!

App Settings

You can configure the app settings in the Settings tab in the left navbar.

Here’s a little explanation of the settings tabs.

- App Settings: General settings for the app. Mainly behavior. Such as tray toggle, Legendary / GOG threads, Bandwidth limits, update checks, and more.

- Interface: This one for whatever is mainly the visual interface of the app. Language, zoom, card settings, icons, highlight logs (and rules), stores and library tabs toggle/drag.

- Defaults: Defaults for games. These will seed the games you’ll create in the library. It applies automatically for installed games, while, if you add them manually, they’ll seed the fields automatically but you can still change them before creating the entry of the game. You can also use the top right button, apply to existing, to apply the defaults to all the games in the library (it has various checkboxes to decide which categories of options to apply to the games in the library).

- Presets: Environment sets and DLL overrides sets for games. Sets are groups of key value pairs that you can copy in a game setting OR sync with the game’s settings (in the game’s settings). Syncing means that it does not copy the pairs in the game settings, but will apply them on top of the game’s ones at launch. So, if you add lets say, a ‘Raytracing’ set and sync it with a game, if you in future change the pairs set, the game will have the new pairs applied without manually editing the game’s settings. It also has Template Literals that allow you to use variables in some fields. For example, ${prefixes_path} will resolve the settings.toml paths.prefixes_dir value. Useful if you want to back up your settings, change user, and all games will resolve with the new prefixes path without any manual intervention per-game. You can also make custom ones.

Template Literals allow you do stuff like this. So that you can use variables in some fields, and as shown in the example I can just use this as a global variable that seeds any game I make, and it will resolve the game’s slug for the file name. I’ll just create that file with the parameters and it’s resolved without me having to do anything.

- Components: Here you can manage your layers (dxvk, vkd3d), runners (Wine-Spritz, Proton-GE, Proton-Cachyos, etc.) and runtime (umu-run, legendary, gogdl, etc.) components. You can add sources for layers and runners from the Add Source button. Manage the already existing ones from the Manage button on each item (changing values, installing from Git releases, etc.). Runtime has a ‘Check for Updates’ button that checks if there’s an update available for any of the runtime components. Also you can reinstall them or delete them with the x button on the right. The Add Source dialog is explained in Adding a component source below.

- Ofuda: Essentially a prefix manager. Here it shows all your prefixes (in omikuji’s prefixes_path and any prefix used by a game in the library). You can check their sizes, how many games use each prefix (orphan if no game uses it anymore). You can also manage them with the Manage button. It will open a dialog with some information about the prefix and some actions, such as delete prefix and basic winetools commands (winecfg, winetricks, run wine command, kill wineserver, etc.). It will also tell you precisely which games use the prefix. There’s also a new prefix button on the top right of the page. It’ll open a dialog to create a new prefix with a Name field, Runner and Set dropdowns. The Set is essentially Game or Application (they just install a bunch of stuff in the prefix)

- Theme: Here you can change the theme of the application. There’s a follow system toggle for colors, if enabled it will use KDEPlasmaPlatformTheme6.so to follow the system theme. If off, you can change the Window background, content surface, accent, accent text, text, error, success, warning colors. Also a Font section, same follow system toggle, a font family picker (needs restart) and font sizes dialog that has display, headline, title, subtitle, body, label, caption, and micro spinboxes (also a reset all button). At the bottom there’s a Corner radius section that lets you manage various rounding spinboxes.

- About: Information about the application, including version, license and various links. Also your system information that you can copy paste for opening an issue.

Adding a component source

The Add Source button in Components lets you point omikuji at any repo that publishes runners or layers as release archives.

Name and Releases URL are the only required fields. Description is optional and Kind tells omikuji what the thing actually is (Proton or Wine for runners, DXVK, VKD3D, DXVK-NVAPI or Other for layers).

Releases URL takes a plain repo link, like https://github.com/owner/repo. You don’t need to hunt down the API endpoint yourself, links get converted to their releases API automatically. When the link you typed differs from what omikuji will actually call, the converted URL shows up in small monospace right under the field, so you can check it resolved to what you expected. You can also directly enter the releases API URL if you know it.

Latest build priority only shows up for runners, and it’s what makes the -Latest slot pick the right file. Some projects ship several builds per release (_v3, _x86_64_v3, aarch64, and so on) and you probably want a specific one every time. Write the tokens space separated, and the first one that matches wins, so anything further right is a lower priority fallback. If none of them match, omikuji falls back to its normal pick instead of giving up.

Skip releases with no match toggle appears once you’ve written a priority, and it changes what happens when the newest release has nothing matching your tokens. Off, -Latest takes that latest release anyway with the normal pick and follows the priority list. On, it skips an entire release if no match is found. Useful when a project occasionally publishes a release that doesn’t include your build flavour and you’d rather stay on an older one than get the wrong file and/or undesired version.

If you want, you can move an installed runner that is in the runners dir to the compatibilitytools.d directory to make it available to Steam (and omikuji), you can just click the Steam button in the runner manager, it’ll open a dialog with the found Steam paths. If there’s only one path it’ll just move it there, if there are multiple paths (and you select more than one, since you could just select one), it’ll copy the runner in each selected path.

It changes a bit for the -Latest type of runners. In that case, it won’t copy nor move, it’ll symlink the folder to the compatibilitytools.d directory of each selected path. Why? Primarily because this way -Latest runners won’t be duplicated in the runner selection dropdowns, and then because we have to manage the update of the runner on new releases. Also, only when we symlink, omikuji will overwrite the compatibilitytool.vdf file in the runner’s folder to a generic <runnername>-Latest, with this when updating the runner, Steam won’t deselect it for games that use it. (yes, Steam reads that file, if the display or internal name of the runner changes, Steam will deselect it from the games you’re using it on, this prevents that.). Also, if you re-open the dialog, and there’s a symlink already, you will be able to uncheck the path and remove the symlink (once you apply the edits ofc).

Library Overview

Just some things about the main library. On the left there’s the navbar with the various categories for games and below them the stores, and at the bottom of the navbar we have the Downloads and Settings tabs (settings opens a dialog, downloads is a page). At center there’s the main library view. When you click a game card, it’ll outline it with an accent color and will show the bottom floating bar. In order, the floating bar has: Game title, Playtime, Last Played, Runner of the game on the left, then on the far right, still in order: Wine button (opens a context menu with Configure (winecfg), winetricks, Registry (regedit), Command Prompt (cmd), File Explorer (explorer), Run EXE in prefix..., Run wine command..., Kill wineserver). Running one of these will execute the corresponding action inside the selected game’s prefix (also, they’re 1:1 to the Ofuda buttons. Run wine command simply opens a dialog with a field where you can execute any wine command (winetricks is slow as balls to scroll through)). Then a gear button that opens the selected game settings, and a play button (has various states. Play, Stop, Starting.)

At the top of the library view there’s a search bar. On the far right on the same row (top right edge of the window) there are three buttons. In order: Console Mode (opens console mode), Quick settings (opens quick settings popup for the library), + (opens a popup with Add Game and Install Script entries).

Also, if in the settings (or quick settings) you enable the custom order for the cards sorting, you can hold with your left click a card for ~2 seconds and then drag it to a new position.

Right clicking on a game card will open a context menu. Its entries are:

- Play: starts the game.

- Show Logs: opens the game logs in a separate window.

- Configure: opens the game settings dialog.

- Categories: opens the game categories dialog.

- Browse Files: opens the game files in the file manager.

- Add to favorites / Remove from favorites: adds/removes the game from the favorites list.

- Hide/Unhide: hides/unhides the game from the library.

- Shortcuts: opens the game shortcuts subdialog. Create desktop shortcut, Create application menu shortcut, Create Steam shortcut. If they already exist, it’ll be Remove.

- Duplicate: duplicates the game entry.

- Remove: removes the game entry. Hold shift and it becomes Remove + prefix, it will open a dialog to confirm. It will remove the game entry from the library and delete the game prefix.

HoYo Games

- Repair: repairs the game installation.

Epic Games / GOG

- Check for updates: checks for updates for the game and queues them in the downloads page if present.

- Uninstall: removes the game installation (both from the library and the files on disk, also clears the Legendary/gogdl .json files).

Quick Settings

- Card size: slider for the card size.

- Card spacing: slider for the card spacing.

- Sort by: dropdown for sorting the cards. Custom allows to hold a card with the left-click and drag it to reorder. Date added, Name A-Z, Name Z-A

- Card style: dropdown for the card style. Normal, Fit, Frameless.

- Show hidden games: toggle for showing hidden games.

Community Scripts

Community scripts are essentially a DSL, made with .toml files and hosted in the scripts repository. They allow a user to run them for installing launchers, editing prefixes and such.

To install one, click the + button and select Install Script in the tiny popup. The dialog will normally just show the locally installed scripts in the scripts directory. To query online ones, just type something in the search bar. They’ll get listed. A script item has: an icon, a name, a small download icon beside it if it’s not installed, below them a short description and on the far right the author name on top of the last updated date.

Clicking a script pulls it locally and opens a new dialog. The fields in it are all declared in the script’s .toml file. The only thing that is always present is the show source button that expands a code block with the script’s source code. For more information, see the scripts documentation.


Q&A

Where does omikuji install its own stuff, creates prefixes and store stuff? and can I change it?

See Configuration > [paths].

My store page is empty, or it says I’m not logged in

See Logging into Epic Games / GOG.

Why can’t I install Steam games from omikuji?

See Steam.

How do I add a game that doesn’t come from a store?

See Manually.

Can I install a DLC (Epic Games / GOG) for a game already installed?

See Installing DLC (Epic Games / GOG).

How do I run a random .exe inside a game’s prefix?

See Library Overview.

What’s the difference between copying and syncing an env set?

See App Settings.

How do I make a runner available to Steam?

See Move / Symlink to Steam compatibilitytools.d.

What are community scripts and how do I install one?

See Community Scripts.

How do I point omikuji at my own scripts registry?

See Configuration > [scripts].

How do I change the card size, sorting, or hide games?

See Quick Settings.

How do I uninstall a Epic Games / GOG game?

see Uninstalling a Game

Can omikuji use wine and proton builds already installed on my system?

Yes, they’re picked up automatically, no configuration needed. The runner dropdown lists omikuji’s own runners, every Proton in Steam’s various compatibilitytools.d directories and your system wine.

How do I update a game?

Several ways:

- Epic Games / GOG: Right click on the game in the library and select “Check for updates”. Or you can enable the toggles that check for updates on game launch.

- Gacha: Gachas are always checked on launch.

All three share the Settings -> App -> Check for updates on app launch toggle. If on, it’ll check for updates for all three and queues them in the downloads page if there are any.

Do I need umu?

Only for Proton. The app doesn’t implicitly install umu for you. You have a toggle in the welcome page or you can install it Settings -> Components -> Runtime -> Install on umu’s button. It will ask you if you want to install umu in other occasions such as when you launch a game with proton but umu is not installed. Also if you have umu installed system-wide, omikuji will use that.

A game ran once and now my DLL overrides (layers) are still in place

Turn off the toggles of the layers you activated and run the game again to make omikuji swap them back to the default ones.

Where are the logs?

For games, logs can be seen by right clicking the game card and selecting Show logs. It will open a new window with the logs when you run the game. Also comes with a search (Ctrl+F), clear, Copy all and Save buttons. Or you can enable the automatic save on disk in Settings -> App -> Save game logs to disk.

Can I install the same game twice?

For Epic Games and GOG, no. But you can install as many copies as you want of a gacha game.

Can I use omikuji for regular apps instead of games?

Yes. Add the app the same way as a game and assign it a custom category. There is no separate “Apps” section, so it will also appear under All Games.

Configuration files

Main config lives at ~/.local/share/omikuji/settings.toml. Edit it and restart to apply (app.toml and the others are live-watched).

Most sections rarely need touching. However older installs may be missing some keys added in later versions. The app fallbacks internally, but if you need to override one that misses in your settings.toml just check down here.

settings.toml

[paths]

Where omikuji keeps its data.

# !! data_dir is not changeable! Even if editing the line, it won't actually change it. It's to avoid handling ugly behaviours.

[paths]
data_dir = "/home/reakjra/.local/share/omikuji"
library_dir = "/home/reakjra/.local/share/omikuji/library"
gachas_dir = "/home/reakjra/.local/share/omikuji/gachas"
components_dir = "/home/reakjra/.local/share/omikuji/components"
runners_dir = "/home/reakjra/.local/share/omikuji/components/runners"
layers_dir = "/home/reakjra/.local/share/omikuji/components/layers"
tools_dir = "/home/reakjra/.local/share/omikuji/components/tools"
prefixes_dir = "/home/reakjra/.local/share/omikuji/prefixes"
cache_dir = "/home/reakjra/.local/share/omikuji/cache"
logs_dir = "/home/reakjra/.local/share/omikuji/logs"
runtime_dir = "/home/reakjra/.local/share/omikuji/runtime"
scripts_dir = "/home/reakjra/.local/share/omikuji/scripts"

A leading ~ is expanded to $HOME on read (crazy right?).

[assets]

Where gacha manifests and artwork are fetched from.

[assets]
fetch_url = "https://raw.githubusercontent.com/reakjra/omikuji-assets/main"

[scripts]

Where the community scripts registry is fetched from. Point it elsewhere to use your own registry instead.

[scripts]
fetch_url = "https://raw.githubusercontent.com/reakjra/omikuji-scripts/master"

[components]

Download URLs for the runtime tools (umu, hpatchz, legendary, gogdl, EGL dummy). They’re fetched when first needed, like a store login. umu is never fetched on its own, it’ll always prompt.

[components]
umu_run = "https://api.github.com/repos/Open-Wine-Components/umu-launcher/releases/latest"
hpatchz = "https://api.github.com/repos/sisong/HDiffPatch/releases/latest"
legendary = "https://api.github.com/repos/derrod/legendary/releases/latest"
gogdl = "https://api.github.com/repos/Heroic-Games-Launcher/heroic-gogdl/releases/latest"
egl_dummy = "https://raw.githubusercontent.com/reakjra/omikuji-assets/main/runtime/epic/EpicGamesLauncher.exe"

[steam]

[steam]
api_key = ""
install_dirs = [""]

Optional Steam Web API key (get one here). Without it, Steam library listing still works (read locally from ACF files), only remote playtime sync is off.

install_dirs points omikuji at Steam installations outside the checked locations (~/.steam, ~/.local/share/Steam, the Flatpak and Snap paths, /usr/share/steam), for example install_dirs = ["/mnt/games/Steam"]. Entries are read before the built-in locations and in the order given, so they also decide which install is used when more than one exists, and they cover library listing, Proton detection and compatibility tool links. Library folders on other drives are read from libraryfolders.vdf and do not need listing here.

app.toml

UI preferences and app behaviour live in ~/.local/share/omikuji/app.toml: categories, nav rail, tab visibility, zoom, theme, bandwidth limit, threads, etc. Almost all of it is set through the app, and the file is live-watched, so edits apply without a restart.

The only option that isn’t in the GUI is this one, makes the fields like the older version. (equals making them ugly)

[theme]
fill_fields = true

components.toml

file that holds all the runners and layers sources. Plus, layers’ state (active)

example one:

[[runners]]
name = "Proton-Spritz"
kind = "proton"
api_url = "https://api.github.com/repos/NelloKudo/proton-cachyos/releases"
desc = ""
asset_priority = []
require_asset_match = false

[[runners]]
name = "Proton-GE"
kind = "proton"
api_url = "https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases"
desc = ""
asset_priority = []
require_asset_match = false

[[runners]]
name = "Dawn Winery Proton"
kind = "proton"
api_url = "https://dawn.wine/api/v1/repos/dawn-winery/dwproton/releases"
desc = ""
asset_priority = []
require_asset_match = false

[[runners]]
name = "Proton-Cachyos"
kind = "proton"
api_url = "https://api.github.com/repos/CachyOS/proton-cachyos/releases"
desc = ""
asset_priority = ["_v3"]
require_asset_match = false

[[runners]]
name = "Wine-Spritz"
kind = "wine"
api_url = "https://api.github.com/repos/NelloKudo/spritz-wine/releases"
desc = ""
asset_priority = []
require_asset_match = false

[[layers]]
name = "DXVK"
kind = "dxvk"
api_url = "https://api.github.com/repos/doitsujin/dxvk/releases"
desc = ""
asset_priority = []
require_asset_match = false

[[layers]]
name = "VKD3D-Proton"
kind = "vkd3d"
api_url = "https://api.github.com/repos/HansKristian-Work/vkd3d-proton/releases"
desc = ""
asset_priority = []
require_asset_match = false

[[layers]]
name = "DXVK-NVAPI"
kind = "dxvk_nvapi"
api_url = "https://api.github.com/repos/jp7677/dxvk-nvapi/releases"
desc = ""
asset_priority = []
require_asset_match = false

[active]
DXVK-NVAPI = "v0.9.2"
DXVK = "dxvk-3.0.2"
VKD3D-Proton = "vkd3d-proton-3.0.1"

defaults.toml

file that holds the global defaults for games (Settings -> Defaults).

example:

[wine]
version = "Proton-Cachyos-Latest"
prefix = "${prefixes_path}/PrefixGE"
esync = true
fsync = true
ntsync = true
vkd3d = false
dxvk_nvapi = false
battleye = false

[launch.env]
PROTON_USE_NTSYNC = "1"
ENABLE_VKSUMI = "1"
VKMIRU = "1"
OBS_VKCAPTURE = "1"

[graphics]
gpu = "00000000-0300-0000-0000-000000000000"

[graphics.gamescope]

[system]
gamemode = false
cpu_limit = 0

This is just for fun. i was bored when i wrote it ok? also its cute.

Little Big Launcher

Oh, hello there. Are you lost? Don’t you fret.

Come take a little tour through this page, silly and unimportant to the cold, indifferent universe as it may be, and I shall guide you through your very first steps. Together we’ll coax one humble game into running. Today’s brave volunteer is a noble title named WizardGoose.exe, though whatever game you happen to have will do just as nicely.

A home for your game

Up in the top right of your little, adorable application window, there’s a very calm + button. Give it a gentle press, and a window titled New Game drifts open.

This is where your game introduces itself:

- A Name, so it has something to be called. Ours shall be “Wizard Goose”.

- A Runner, which is how it should run. Our goose is a Windows creature, so we leave it on Wine. A native Linux creature would prefer Native, a Steam import would prefer Steam, but those are tales for another day.

A name is all it truly needs to exist. The rest is just kindness.

Dressing it up

Wander over to the Runner tab. Here live the knobs that teach a Windows game how to behave on Linux.

You needn’t touch most of them. Three deserve a glance:

- The Path to its .exe, the little file that actually starts the thing. Browse to wherever it lives on your disk.

- Version: the wine or proton build it runs on. Choose one from the list. If the list is bare, slip into Settings and install a runner first, then come back.

- Prefix: the little fake C: drive your game will call home. Leave it empty and omikuji quietly conjures a fresh one just for this game, which is exactly what you want.

Should your goose demand finer graphics, the Translation Layers section holds toggles like DXVK and VKD3D. Flick them on if it asks. Every last knob in here is catalogued, calmly and exhaustively, over in Game Settings. For now, we keep things simple.

The first flight

At the top wait two buttons. Create tucks the game safely into your library. Create & Play does the same, then sets it flying at once.

Press Create & Play.

And off it goes. Your goose takes wing. You did it.

Now go, back into the cold and indifferent universe, a little less lost than you were. Your library is right over there whenever you want it, and every game you bring home gets its own quiet little corner.

But… but what if a third-world eldritch being offers me games…?

Oh… little summer dear. Don’t you just worry. Look at your left, what do you see? Exactly. Four, cosmological creatures waiting for your interest. Let’s try the first one. Click.

- Steam: A religion, someone would say. It just shows you, doesn’t it? A tiny, colored + awaits on each contemporary art. Click it. And it’ll come home.

- Epic Games: Loved by its generosity. Same as before, however, now you’ll have to make your first choice. The installation path. Where do you want to keep the bloody membrane of this one? Choose carefully. Then, a prefix, just leave it empty. A runner. Of course. Once you made your life-bearing decisions, don’t worry yet, it’s gonna take just a moment. Click gently on the button at the bottom right, ‘Install’. You won’t believe what will happen.

- GOG: I’m not gonna explain this one, you’re grown enough. You already know how to do it.

- Gachas: Oh dear. Now some more paths belie in front of us. But stay calm. Let your inner self tell you where do you feel the most and what voices you’re hearing.

What is this…?

My, my. This is, in plain terms, a context menu. See? It’s very simple to understand. But let’s talk abotu some distinctive choices.

- Check for updates: This works for GOG and Epic Games. It’s self explanatory, isn’t it?

- Uninstall (Epic Games): Oh, this is unique. This, will help you delete a silly, naughty creature from your library and your disk. It’s important to do this for this type of creatures, because they need extra implicit steps that sometimes, our life, won’t ever tell us.

- Repair: This… this one is healing a wound that sometimes life scars us with. This one, is unique too. It only applies on creatures from a very specific company.

Is… is it infecting my digital life?

Of course not, adorable creature. This one, helps you out when sometimes reaching for something important feels to heavy so you need something lighter to assist you.

See?

Very minimal.

Overview

Omikuji is two crates and a QML frontend.

  • crates/omikuji-core is the backend. Pure Rust, no Qt. Game library, runners, prefixes, downloaders, per-publisher install logic. If it doesn’t touch the UI, it lives here.
  • crates/omikuji is the app: main.rs, the cxx-qt bridges in src/bridge/, and the QML in qml/.

The split is enforced. omikuji-core never imports Qt, and QML never calls core directly. Everything crosses through a bridge object.

Building

cargo build -p omikuji

cxx-qt regenerates C++ glue on every build, so it isn’t fast. The QML is compiled into the binary as a Qt resource, so editing a .qml (or adding one) needs a rebuild to show up.

Syntax-check QML without a full build:

qmllint qml/components/SomeFile.qml

qmllint only sees syntax. It doesn’t know the types a bridge exposes, so it flags every bridge call as unknown.

Shape of a change

A typical feature touches three layers: something in omikuji-core, a bridge in src/bridge/ to expose it, and the QML that calls it. Adding things covers the common cases.

The cxx-qt bridge

A bridge is a QObject whose state and methods live in Rust. QML instantiates it, reads its properties, calls its methods, and reacts to its signals. Each bridge is one file in crates/omikuji/src/bridge/.

Anatomy

A bridge has three parts in the same file: the bridge module, the Rust state struct, and the impl.

#![allow(unused)]
fn main() {
#[cxx_qt::bridge]
pub mod qobject {
    extern "RustQt" {
        #[qobject]
        #[qml_element]
        #[qproperty(i32, count)]
        type Counter = super::CounterRust;
    }

    unsafe extern "RustQt" {
        #[qinvokable]
        fn increment(self: Pin<&mut Counter>);
    }
}

pub struct CounterRust {
    pub count: i32,
}

impl Default for CounterRust {
    fn default() -> Self {
        Self { count: 0 }
    }
}

impl qobject::Counter {
    fn increment(mut self: Pin<&mut Self>) {
        let next = self.count + 1;
        self.as_mut().set_count(next);
    }
}
}
  • #[qobject] + #[qml_element] declare the QObject and make it instantiable from QML.
  • type Counter = super::CounterRust; ties the QObject to the plain Rust struct that holds its state.
  • The first extern "RustQt" block declares properties. The unsafe extern "RustQt" block declares signals and invokables.
  • impl qobject::Counter holds the method bodies.

Wiring it up

build.rs has two lists. A new handwritten bridge .rs goes in the kushi::stage_files([...]) list, a new .qml goes in qml_files([...]).

#![allow(unused)]
fn main() {
let staged = kushi::stage_files(
    [
        "src/bridge/counter.rs",
        // ...
    ],
    &out_dir,
);
}

The staging copies each file into OUT_DIR before handing it to cxx-qt: cxx-qt-build accepts only one directory per QML module (QTBUG-93443), and the generated bridges already live there. rustc compiles the originals in src/bridge/ as normal modules; the copies only feed cxx-qt’s parser.

Forgetting to register a new .qml is the usual trip: use MyWidget {} somewhere and you get “MyWidget is not a type” at runtime, with no compile error and nothing from qmllint. Add the file to qml_files and rebuild.

Once registered, QML imports the module and instantiates the type:

import omikuji 1.0

Counter { id: counter }

The module name is set in build.rs with QmlModule::new("omikuji").

Properties, signals, invokables

Properties. #[qproperty(T, name)] generates a getter, a setter (set_name), and a name_changed signal. QML binds to the property and re-evaluates when the signal fires. cxx_name sets the name QML sees:

#![allow(unused)]
fn main() {
#[qproperty(bool, is_logged_in, cxx_name = "isLoggedIn")]
}

Rust calls it is_logged_in, QML sees isLoggedIn. Without cxx_name, the Rust name is used as-is.

Signals. Declared with #[qsignal] in an unsafe extern "RustQt" block. You emit one by calling it on self. QML listens with onNameChanged handlers or a Connections block.

Invokables. #[qinvokable] exposes a method to QML. Invokables are not auto-camelCased the way properties are: without cxx_name, QML calls the snake_case Rust name.

#![allow(unused)]
fn main() {
#[qinvokable]
fn get_login_url(self: &Counter) -> QString;   // QML: counter.get_login_url()

#[qinvokable]
#[cxx_name = "installAll"]
fn install_all(self: Pin<&mut Counter>);        // QML: counter.installAll()
}

A property is isLoggedIn but a plain invokable next to it stays get_login_url. If you want camelCase on an invokable, give it a cxx_name.

Reading and writing state

Methods that read take &self, methods that mutate take Pin<&mut Self>. The struct fields are reachable directly, and there are helpers for the rest:

#![allow(unused)]
fn main() {
fn example(mut self: Pin<&mut Self>) {
    let current = self.count;                       // read a field directly
    self.as_mut().set_count(current + 1);           // property setter, fires count_changed
    self.as_mut().rust_mut().get_mut().count = 0;   // write the field directly, no signal
}
}

Reach for set_* when QML needs to react to the change. Use rust_mut().get_mut() for fields that aren’t properties, or for batch edits that fire one signal at the end. self.rust() returns the whole struct. self.as_mut() reborrows the pin for chaining further calls.

The paperwork

Adding one user-visible value is more steps than it looks. A property that persists needs a field on the Rust struct, the #[qproperty] line, an entry wherever the struct is built (Default, or a from_settings), and usually an invokable that sets the value and writes it to disk. Settings also mirror into a core struct, so the same field name shows up in the core type, the bridge struct, and two or three conversion functions.

That repetition is handled two ways. For settings and the download model, the whole mirror is generated from a declaration; see Generated bridges. In the handwritten bridges, macro_rules! does the same job in-file: defaults_fields! and game_fields! build whole families of getters and setters from a single field table. The rule of thumb: when the only thing that changes between copies is a name (a field, a type, a signal), that is generation’s job, not copy-paste. Adding things has the full add-a-setting walkthrough.

Threading

A bridge object lives on the Qt thread and can only be touched there. To update it from other work, grab a handle first, move it into the work, then queue a closure back onto the Qt thread:

#![allow(unused)]
fn main() {
fn refresh(mut self: Pin<&mut Self>) {
    let handle = self.as_mut().qt_thread();
    tokio::spawn(async move {
        let value = fetch_something().await;
        let _ = handle.queue(move |mut obj: Pin<&mut qobject::Counter>| {
            obj.as_mut().set_count(value);
        });
    });
}
}

qt_thread() needs impl cxx_qt::Threading for Counter {} in the bridge module. The closure given to queue runs back on the Qt thread with a fresh Pin<&mut> to the object, which is the only place set_* is safe to call.

Blocking on async

main is #[tokio::main], so invokable bodies run inside the runtime, and calling block_on there panics with “cannot start a runtime from within a runtime”. When you need a result synchronously, run the work on a separate OS thread with its own runtime:

#![allow(unused)]
fn main() {
std::thread::spawn(move || {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();
    rt.block_on(async {
        // ...
    });
});
}

Binding bridges into delegates

Inside a QML Component (a Repeater/ListView delegate, a Loader source), a binding like model: gameModel resolves the right-hand gameModel to a property on the delegate itself, which is null, not the outer id. The fix used throughout Main.qml is a *Ref alias on the root:

readonly property var gameModelRef: gameModel

// inside a delegate:
SomeItem {
    model: root.gameModelRef
}

Bind bridges into delegates through these aliases, never by the bare same name.

List models

A bridge that backs a QML list (the library grid, the stores, downloads) is a QAbstractListModel. It adds #[base = QAbstractListModel] and overrides three methods:

#![allow(unused)]
fn main() {
extern "RustQt" {
    #[qobject]
    #[qml_element]
    #[base = QAbstractListModel]
    type GameList = super::GameListRust;
}

unsafe extern "RustQt" {
    #[cxx_name = "rowCount"]
    #[cxx_override]
    fn row_count(self: &GameList, parent: &QModelIndex) -> i32;

    #[cxx_override]
    fn data(self: &GameList, index: &QModelIndex, role: i32) -> QVariant;

    #[cxx_name = "roleNames"]
    #[cxx_override]
    fn role_names(self: &GameList) -> QHash_i32_QByteArray;
}
}

Roles are an enum, and role_names maps each to the name QML reads (model.title, model.banner, …):

#![allow(unused)]
fn main() {
enum Role {
    Title = 0,
    Banner = 1,
}

// role_names:
roles.insert_clone(&(Role::Title as i32), &QByteArray::from("title"));

// data:
match role {
    r if r == Role::Title as i32 => QVariant::from(&QString::from(&item.title)),
    _ => QVariant::default(),
}
}

Mutating the backing data has to be wrapped in the inherited model signals or QML won’t update. A full swap uses reset:

#![allow(unused)]
fn main() {
self.as_mut().begin_reset_model();
self.as_mut().rust_mut().get_mut().items = new_items;
self.as_mut().end_reset_model();
}

For finer changes there’s begin_insert_rows(&QModelIndex::default(), row, row) / end_insert_rows, and data_changed(&index, &index, &roles) to repaint one row. These inherited methods (begin_reset_model, begin_insert_rows, data_changed, model_index, …) are declared with #[inherit] in their own unsafe extern "RustQt" block before use. When the data comes from async work, run the fetch off-thread and do the reset inside qt_thread().queue(...).

Generated bridges

Two bridges are not written by hand: AppSettingsBridge and DownloadModel. Their bridge modules are generated by kushi from declarations in crates/omikuji/build.rs and written into OUT_DIR at build time. The declarations list every property, role, signal, and invokable; changing the bridge means changing the declaration.

The two halves

Each generated bridge has a handwritten half in src/bridge/ that pulls the generated module in and implements everything with logic in it:

#![allow(unused)]
fn main() {
include!(concat!(env!("OUT_DIR"), "/app_settings_bridge.rs"));
}
  • src/bridge/app_settings.rs: the persist implementation (the watcher suppress window), the custom apply bodies (apply_ui_scale, apply_card_flow, …), the reload hook, the file watcher, and the misc invokables (fonts, icons, color overrides).
  • src/bridge/download_model.rs: the custom Default (counts via recompute), the enqueue/pause/resume invokable bodies, drain_events, and the role_* helpers behind computed roles (status label, byte casts).

The generated half calls into the handwritten one where the declaration says so: .prop_custom_apply declares an invokable without generating a body, .role_fn points a role at a function, .custom_invokable declares any signature. A missing implementation is a compile error.

Rules

  • Generated files live in OUT_DIR and are rewritten every build. Never edit them; edit the declaration or the handwritten half. The current output is inspectable at target/debug/build/omikuji-*/out/.
  • The strings passed to the builders are QML API. Property and role names come out camelCased (show_hidden becomes showHidden). Signatures passed to qsignal_raw and custom_invokable_raw are exposed under their literal snake_case names, which is what the existing QML calls.

Adding things

Three common changes, start to finish. Each builds on The cxx-qt bridge.

A setting

A setting is a value in app.toml that QML reads and writes live. The bridge for it is generated by kushi from a declaration in crates/omikuji/build.rs. For a new bool show_clock:

  1. Add the field to the core struct in omikuji-core/src/app_settings.rs (the right section, e.g. DisplaySettings) and to that section’s Default.

  2. Declare it in the ObjectBridge builder in crates/omikuji/build.rs, next to the other props:

#![allow(unused)]
fn main() {
.prop_at("show_clock", kushi::Kind::Bool, "display.show_clock")
}

The last argument is the field’s path inside AppSettings. The generated names derive from the first: property showClock, invokable applyShowClock.

  1. In QML, read appSettings.showClock and write through appSettings.applyShowClock(value).

The property, the apply invokable, persistence, and hot reload are all generated. When the setter needs logic (clamping, a side effect, a signal), declare it with .prop_custom_apply(...) instead and write apply_show_clock by hand in src/bridge/app_settings.rs; apply_ui_scale and apply_discord_rpc are the existing examples. .prop_readonly(...) declares a property with no setter (fill_fields). List-shaped settings that cross as JSON use .json_accessor(...); categories is the model.

A per-game setting

Per-game config (the fields in GameSettingsPage: wine version, env, launch options) lives on the Game struct in library.toml, not app.toml, and it doesn’t use properties: the whole game config crosses to QML as one string-keyed map, so a new field is one row in a table.

  1. Add the field to the right struct on Game in omikuji-core/src/library/mod.rs (WineConfig, LaunchConfig, …), with a serde default.

  2. Register it in the game_fields! table in crates/omikuji/src/bridge/game_model.rs:

#![allow(unused)]
fn main() {
"wine.my_flag" => bool, wine.my_flag,
}

That row wires both directions: reading the field into the config map and writing it back from QML. The kind tag picks the conversion (str, path, bool, int, json for maps and vecs, args for launch args). Add readonly after the kind for a read-only field.

  1. In the settings tab QML (TabRunnerOptions, TabSystem, …), add the control. Read the value from config["wine.my_flag"] and write changes through the page’s updateField("wine.my_flag", value), which updates the draft and refreshes config.

A runtime tool

Runtime tools (umu, legendary, gogdl, …) are fetched by the component system. To add one:

  1. Add a variant to SettingsKey in omikuji-core/src/components/spec.rs.

  2. Add the download URL to ComponentsSettings in omikuji-core/src/settings.rs and its Default. These are release-latest API URLs (GitHub, Codeberg) or a direct URL.

  3. Map the key to the URL in url_for in omikuji-core/src/components/mod.rs.

  4. Add the ComponentSpec in omikuji-core/src/components/specs.rs:

#![allow(unused)]
fn main() {
ComponentSpec {
    name: "mytool",
    source: Source::GithubRelease { asset_matcher: |n| n.ends_with(".tar.gz") },
    extract: ExtractStrategy::TarGz { inner_path: "mytool" },
    dest: "mytool",
    settings_key: SettingsKey::MyTool,
    system_probe: None,
},
}
  1. Add a components::ensure(...) call at the point the tool becomes necessary (a store login, a game install). Nothing is fetched at boot. epic_tools, gog_tools and gacha_tools in components/mod.rs are the existing examples.

  2. Set system_probe if the tool is also packaged by distros. It takes a resolver returning the path of a system copy, and a hit reports the component as present so nothing is downloaded. umu-run uses launch::umu_system_path for this.

A dialog

Dialogs are built on DialogCard, which owns the backdrop, dim, click-outside, Esc, shadow, and scrolling. A dialog is a config of its slots.

  1. Create qml/components/dialogs/MyDialog.qml:
import QtQuick
import "../widgets"

DialogCard {
    id: root
    title: "Do the thing?"
    maxWidth: 480

    body: Column {
        spacing: theme.space.md
        Text { text: "..."; color: theme.text }
    }

    actions: Row {
        spacing: theme.space.sm
        M3Button { text: "Cancel"; variant: "text"; onClicked: root.close() }
        M3Button { text: "Confirm"; variant: "tonal"; onClicked: { root.close() } }
    }

    onCloseRequested: close()
}
  1. Register it in qml_files in build.rs.

  2. Instantiate it once where it’s used (a page, or Main.qml), give it an id, and call .open() to show it.

The slots take a Component, but a plain element works directly. Don’t add your own backdrop or scrollbar, DialogCard owns those. For a fixed-height list dialog, set fillHeight: true and scrollable: false.

Translations

omikuji’s UI strings are wrapped in Qt’s qsTr(). A QTranslator loads a compiled .qm at startup and resolves them; an unwrapped or untranslated string falls back to its English source. Translations live in crates/omikuji/i18n/, one .ts per language. build.rs compiles each .ts to a .qm and embeds it, so only the .ts is committed. The scope is the QML UI: the CLI and the Rust backend stay in English (aint doing allat).

omikuji_en.ts is the English source catalog. It is the template new languages are created from, and it is not compiled or shipped.

Weblate

Translations are hosted on Weblate. Pick a language or start a new one, translate in the browser, and Weblate opens a pull request against master. No local setup, no Qt tools.

Prerequisites

For working on the catalogs locally: the Qt Linguist tools, lupdate and lrelease. On Arch they ship in qt6-tools (as lupdate6 / lrelease6), idk other distros. scripts/update-translations.sh accepts either the suffixed or the plain name. Without them the build still works and ships English only.

Adding a language locally

As an example, for Italian (it):

  1. ./scripts/update-translations.sh it harvests every qsTr/tr string into crates/omikuji/i18n/omikuji_it.ts. The file name carries the locale code Qt expects (omikuji_it.ts, omikuji_pt_BR.ts, omikuji_ja.ts).

  2. Translate omikuji_it.ts, in either any text editor, by filling the <translation> elements, or QtLinguistic if you’re sane.

  3. Build. The language appears in the picker at Settings > Interface under its own native name.

  4. Commit omikuji_it.ts.

Updating a language

After UI strings change, refresh the catalogs:

./scripts/update-translations.sh with no arguments re-harvests every language already in i18n/. New strings land in the .ts marked unfinished. Commit the updated .ts.

Weblate picks the new strings up on its next pull and marks them for translation. Changed source strings mark the existing translation as needing update.

Wrapping a new UI string

Any user-visible literal in a .qml file goes through qsTr:

text: qsTr("Play")

Text combined with data uses placeholders, not concatenation, because word order differs between languages:

text: qsTr("%1 left").arg(formatEta(secs))
text: qsTr("%n game(s)", "", count)

%n picks the plural form for the count.

Left unwrapped: icon names (name:, icon:), color tokens, runner and kind values ("wine", "native", "steam"), any literal used in a comparison, config keys, paths, and bare brand names (Steam, Epic Games, GOG, Proton, DXVK). A brand inside a sentence stays literal while the sentence is wrapped, as in qsTr("Run with Omikuji").