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

Adding things

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

A setting

A setting is a value in ui.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/ui_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 UiSettings. The generated names derive from the first: property showClock, invokable applyShowClock.

  1. In QML, read uiSettings.showClock and write through uiSettings.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/ui_settings.rs; apply_ui_scale and apply_discord_rpc are the existing examples. .prop_readonly(...) declares a property with no setter (fill_fields, radius_scale). 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 ui.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, jadeite, …) 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,
    trigger: Trigger::OnDemand,
},
}
  1. Pick the trigger. Eager fetches at boot (only umu uses this). OnDemand fetches when something calls components::ensure(...). For OnDemand, add that ensure call at the point the tool becomes necessary (a store login, a game install). epic_tools and gacha_tools in components/mod.rs are the existing examples.

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.