# Launchers A **launcher** is the executable Kartend hands an item to when you press `Enter` (or double-click). It can be `xdg-open`, `mpv`, a libretro runtime, a wrapper script, or anything else that takes a file path on the command line. Each collection has at minimum one launcher (the **primary**) and can add any number of **additional launchers** for the same library — useful when one set of items can be opened with multiple tools (a video file that you sometimes run in `mpv` and sometimes in `vlc`, for example). Configurations can be saved as **presets** and reused across collections, and individual items can override the default choice via right-click. > **Where to find this** — Settings Dialog → **Launcher** tab. > Per-collection INI keys: `launcherPath`, `launchParameters`, > `corePath`, `launcherName`, `additionalLaunchers`, > `defaultLauncherIndex`. Reusable presets live in their own top-level > `[Launchers]` array group, and `[General] retroarchConfigPath` > points the core picker at a non-standard RetroArch install. ## How launching works When you trigger a launch, Kartend: 1. Resolves which launcher to use — primary, an additional, or a per-item override. See [Multi-launcher chooser](#multi-launcher-chooser). 2. Builds an **argument list** (no shell): ``, followed by `-L ` if the launcher path matches a libretro-style binary, followed by the absolute path to the item file. 3. Spawns the process with `QProcess`. By default Kartend detaches and forgets it; with **Runtime Detection** enabled (see [Splash & Now Playing](Splash-and-Now-Playing.md#what-runtime-detection-turns-on)) Kartend tracks the child's lifetime instead. 4. Records the launch in the database (per-item `play_count` increments, `last_played` timestamp, optional history row). See [History & Statistics](History-and-Statistics.md). ### Argument list, not shell command Arguments go through a `QProcess` argument list. There is **no shell interpolation**: `~`, `$VAR`, `&`, `>`, quote rules, glob patterns — none of them work the way they would in `bash`. If you need shell behavior, point the launcher at a small wrapper script: ```bash #!/bin/sh # /usr/local/bin/kartend-mpv-fullscreen exec /usr/bin/mpv --fs --really-quiet "$1" ``` ```ini launcherPath=/usr/local/bin/kartend-mpv-fullscreen ``` Spaces and ampersands in file paths are handled correctly without quoting because each argument is passed as a separate array element. You don't need to wrap launch parameters in quotes either. ### Where the file path goes The selected item's absolute path is appended **last** by default. To put it somewhere else, name it with a placeholder token — writing `%1` (or `%f`) anywhere in `launchParameters` places the path there instead, and suppresses the append-at-the-end fallback: ```ini launchParameters=--media %1 --extra-flag ``` ### Placeholder tokens Tokens are substituted **inside** each argument after the parameter string has been split, so a token can never introduce a new argument boundary — `"%1"` stays one argument even when the path contains spaces. Matching is case-insensitive. | Token | Expands to | Example | |-------|-----------|---------| | `%1`, `%f` | Full path of the selected item. Suppresses the append-at-end fallback. | `/media/talks/Keynote.mkv` | | `%name%` | Display title — the filename without its extension, the same text the grid shows. | `Keynote` | | `%filename%` | Filename with extension. | `Keynote.mkv` | | `%dir%` | Directory containing the item. | `/media/talks` | | `%ext%` | Extension, without the dot. | `mkv` | | `%core` | Configured libretro core path. Only meaningful for a libretro launcher. **No trailing `%`** — see the note below. | `/usr/lib/cores/engine.so` | | `%collection%` | Name of the collection being launched. Also expanded inside `launcherPath` and `corePath`, not just the parameters. | `Talks` | > **`%1`, `%f` and `%core` have no closing `%`.** They end at a word > boundary instead, so `%core` is the whole token — writing > `--core=%core%` leaves a stray `%` on the end of the expanded > argument. The `%word%`-delimited tokens (`%name%`, `%filename%`, > `%dir%`, `%ext%`, `%collection%`) do take the closing `%`. Getting > this wrong is the single most common way to end up with a launcher > that "almost" works. A worked example — passing the title to a player that shows it in the window bar, and pointing a subtitle flag at a sibling file: ```ini launchParameters=--title=%name% --sub-file=%dir%/%name%.srt %1 ``` Only `%1` and `%f` place the media argument. Naming a *part* of the path does not count, so a template that uses `%name%` but never `%1` still gets the full path appended at the end. Two things worth knowing: - **Launcher-import entries have no file on disk.** Items imported from Steam, Flatpak, Lutris and friends are shortcut stubs that resolve to a target like `steam://rungameid/220`. `%name%` still gives the title, but `%filename%`, `%dir%` and `%ext%` expand to empty. **Preview launch command…** warns when a template asks for them. - **Unknown tokens are left alone.** A `%something%` Kartend doesn't recognise is passed through verbatim rather than blanked, and the launch preview flags it as unresolved — so a typo is visible instead of silent. If you need genuine shell behavior — pipes, globs, conditionals — a wrapper script is still the answer: ```bash #!/bin/sh exec /usr/bin/some-tool --media "$1" --extra-flag ``` ## Primary launcher The required-by-default launcher. Set on the **Launcher** tab: | Field | INI key | Notes | |-------|---------|-------| | Launcher Path | `launcherPath` | Executable. Absolute path recommended. | | Launcher Name | `launcherName` | Display name in the multi-launcher chooser. Empty falls back to the basename of `launcherPath`. | | Core Path | `corePath` | RetroArch / libretro core (only used for libretro-style launchers). | | Launch Parameters | `launchParameters` | Arguments passed *before* the file path. | Examples: ```ini # xdg-open: opens with the user's default app for the file type [Documents] launcherPath=/usr/bin/xdg-open # mpv with fullscreen [Movies] launcherPath=/usr/bin/mpv launchParameters=--fs --really-quiet # RetroArch + libretro core [Retro Library] launcherPath=/usr/bin/retroarch corePath=/usr/lib/libretro/some_libretro.so launchParameters=--fullscreen # Standalone wrapper (no core) [Audiobooks] launcherPath=/usr/bin/mpv launchParameters=--no-video --save-position-on-quit ``` ## Additional launchers A collection can have a list of **additional launchers** in addition to the primary. They appear in the multi-launcher chooser dialog and let you pick at launch time which one to use. ```ini [Films] launcherPath=/usr/bin/mpv launcherName=mpv # additionalLaunchers is serialized; one row per additional entry. # In practice this is managed via the Settings → Launcher tab. ``` Settings dialog flow: 1. Open **Launcher** tab → **Additional Launchers** list. 2. Click **Add Launcher** to open the [Launcher Editor Dialog](#adding-a-launcher-from-a-preset). 3. Fill in name / path / core / parameters (or pick a preset). 4. Save. The new launcher appears in the list. The combined list (primary + additional) is what the multi-launcher chooser presents and what per-item overrides reference. See [Default launcher index](#default-launcher-index) for which one is pre-selected. ### Default launcher index `defaultLauncherIndex` (default `0` = primary) controls the pre-selection in the chooser dialog and the launcher used for items without a per-item override. ```ini [Films] launcherPath=/usr/bin/mpv launcherName=mpv defaultLauncherIndex=1 ; vlc (additional[0]) is the default instead ``` The Settings tab gives you a **Default Launcher** dropdown that lists the same options. ## Multi-launcher chooser When a collection has more than one launcher, you can launch with a non-default by: - **Right-click → Always launch with…** on a specific item — opens the [Launcher Chooser Dialog](#launcher-chooser-dialog) with the current default pre-selected. Picking a launcher creates a per-item override (stored in the database, *not* in the INI file). Picking once *without* saving is already the default: in a collection with more than one launcher, pressing `Enter` on an item that has no override opens the same chooser, and the pick applies to that launch only. Nothing is persisted unless you went in through **Always launch with…**. To remove a per-item override, right-click → **Clear launcher override**. ### Launcher Chooser Dialog Modal list of all launchers for the current collection, one row each. - Pre-selects the current default (or the existing override). - Rows show the launcher's resolved display name — preset references are resolved before the list is built. Path, core and parameters are not shown; use **Preview launch command…** if you need to see those. - **Launch** / **Cancel**. Double-clicking a row launches too. ## Launcher presets (global, reusable) Presets are reusable launcher configs that live in `[General]`, not per-collection. Useful if you have, say, the same `mpv --fs --really-quiet` config on twelve different movie collections. ```ini [General] launcherPresets=... ; serialized list of presets, managed via the UI ``` A preset has: - `id` — UUID. Stable across rename. References use the id, not the name. - `name` — display label. - `launcherPath`, `corePath`, `launchParameters` — same shape as a regular launcher. A launcher entry that **references** a preset stores the preset's `id` on its `presetId` field. At launch time, Kartend resolves the preset and uses its values; if the preset is deleted, Kartend falls back to any inline values on the launcher entry. ### Managing presets Settings → **Launcher** tab → **Global Launcher Presets** section. Add, rename (safe — references are by id), edit, or delete. ### Renaming preset is safe Renames update the display label everywhere instantly without breaking any references — entries stored the `id`, not the name. ### Adding a launcher from a preset In the Launcher Editor Dialog, set **Preset** to a preset id. The inline path / core / parameters fields gray out — the preset overrides them. Clear the preset reference to switch back to inline config. ## RetroArch / libretro RetroArch is supported as a special case because its CLI takes a core via `-L`: ``` retroarch -L ``` Note the order: your `launchParameters` go **first**, then the `-L ` triple. A flag that has to follow the core will not work from `launchParameters` alone. Kartend detects "RetroArch-style" launchers by checking whether the launcher path's filename contains `retroarch` (case-insensitive). If so, `corePath` is automatically passed as `-L `. Examples that match: `/usr/bin/retroarch`, `~/bin/retroarch.AppImage`, `retroarch.exe`. Examples that don't: `mednafen`, `pcsx2`, `dolphin`. For non-RetroArch launchers `corePath` is ignored — leave it blank. ## Archive extraction Some launchers can't open zipped content directly. Set the extraction options on the **Launcher** tab (the extension field stays hidden until you tick the checkbox): | Setting | INI key | Notes | |---------|---------|-------| | Extract Archives | `extractArchives` | Boolean toggle | | Extracted Extension | `extractedExtension` | Which extension inside the archive to launch (e.g. `pdf`, `cbz`) | `extractedExtension` accepts a **comma-separated preference list**, not just one value: `cue, bin, iso` launches the `.cue` if there is one and falls back to `.bin`, then `.iso`. Kartend makes a single recursive pass and picks the highest-priority match it finds, so ordering the list is how you express "prefer the index file over the raw image". Extraction is skipped entirely when `extractedExtension` is empty — there would be nothing to launch afterwards. Recognized archive types: `.zip`, `.7z`, `.rar`, `.gz`, `.tar`, `.bz2`, `.xz` (matched on suffix, so `.tar.gz` works). Extraction goes to `/tmp/kartend_extract//`, not the cache directory, and is **not** cleaned up on shutdown — it is a cross-run cache, and re-extracting a large archive on every launch would be slow. See [File Locations → Extracted archives](File-Locations.md#extracted-archives) for the collision and permission guards, and for how to reclaim the space. If the archive contains nothing matching `extractedExtension`, the launch fails with an error dialog. ## Per-item launcher override Items remember which launcher you "always launch with" via the database, not the INI: - Right-click → **Always launch with…** opens the chooser; pick one to set the override. - Right-click → **Clear launcher override** removes it; the collection default takes effect again. - Override is keyed by `(collection_uuid, source_path)` so it survives rescans and persists across rename of the source file (assuming you don't move it). The right-click options appear only when a collection has more than one launcher (otherwise there's nothing to choose). ## Validation and security Kartend performs basic validation before launching: - The launcher path must point to an existing, executable file. - The launcher path is re-validated immediately before `QProcess::start` to mitigate TOCTOU (time-of-check / time-of-use) attacks where the file is swapped between Kartend's first check and the actual launch. - The **launcher** path is refused if it resolves under `/proc`, `/sys` or `/dev`. The check runs both before and after symlink resolution, so `/proc/self/exe` is caught either way. There is **no** such directory blacklist on the *item* path — an item path is screened for shell metacharacters, NUL bytes, newlines, backslashes and `..` traversal segments, but not for where it lives. - On Linux and macOS all arguments go through `QProcess`'s argument list, never through a shell — there is no command-injection surface from item filenames or launch parameters. On **Windows**, a `.cmd` or `.bat` launcher is necessarily routed through `%COMSPEC% /c`, which *is* a command interpreter; arguments are quoted for `cmd.exe` on the way in, but it is not the same guarantee. If a launch fails, an error dialog reports which step failed (path validation, executable check, process start) and the reason. ## Recipes ### Run an item in a terminal If your launcher is CLI-only, wrap it in a terminal: ```bash #!/bin/sh # /usr/local/bin/kartend-cli-tool exec konsole -e /usr/bin/some-cli-tool "$1" ``` ### Pick a launcher based on filename If the same collection has files of different types (e.g. `.pdf` and `.cbz` mixed), use a dispatcher script as the launcher: ```bash #!/bin/sh # /usr/local/bin/kartend-multi-launcher case "$1" in *.pdf) exec /usr/bin/okular "$1" ;; *.cbz) exec /usr/bin/krita "$1" ;; *) exec xdg-open "$1" ;; esac ``` ### Pass shell-style flags through Kartend `launchParameters` is tokenized with shell-style **quoting rules**, so an argument containing spaces just needs quoting — no wrapper script: ```ini launchParameters=--title "My App" --fullscreen ``` passes three args: `--title`, `My App`, `--fullscreen`. Single quotes work too, and a backslash escapes a quote, a space, or another backslash. An explicitly quoted `""` survives as an empty argument. What is *not* done is shell **expansion** — `~`, `$VAR` and glob patterns stay literal, because nothing ever hands the string to a shell. ### Run a libretro core directly with a custom build of RetroArch ```ini launcherPath=/home/me/builds/retroarch corePath=/home/me/builds/cores/some_libretro.so launchParameters=--fullscreen --verbose ``` ## Launch command preview (dry-run) When a launch fails silently or behaves unexpectedly, **Launch Preview** shows you exactly what Kartend *would* run without actually running it. Open via the per-item right-click menu → **Preview launch command…**. ``` ┌────────────────────────────────────────────────────────────┐ │ Launch preview │ │ │ │ Command: │ │ /usr/bin/mpv --fs --really-quiet │ │ "/home/me/Videos/Films/Some Movie (2021).mkv" │ │ │ │ Resolved executable: /usr/bin/mpv │ │ │ │ Warnings │ │ ⚠ Core path will be ignored (launcher is not a │ │ libretro core launcher). │ │ │ │ [ Close ] │ └────────────────────────────────────────────────────────────┘ ``` What it shows: - **Command** — the full argv as a single joined line, quoting only the arguments that contain whitespace. No shell expansion is performed; what you see is what gets passed. - **Resolved executable** — what the launcher path resolves to after the `PATH` lookup and canonicalisation, or "(not found on PATH or disk)". - **Archive extraction** — when `extractArchives=true` and the item is an archive, the preview notes that Kartend would extract to a temp directory and launch the configured extension from inside. It does not extract, so it cannot show you the resolved inner path. - **Warnings** — the checks that run before a real launch. The full set is: launcher executable not found; file does not exist on disk; archive extraction enabled but the target extension is empty; core path will be ignored (launcher is not a libretro launcher); a `%filename%` / `%dir%` / `%ext%` token that is empty for a launcher-import stub; and an unresolved `%placeholder%`. The preview does **not** show a working directory, and the cwd is not what you might guess: `QProcess` is started in the **launcher's own** directory, so a launcher can find sibling libraries and config files next to itself. Useful when iterating on launcher arguments (you don't want to keep firing a real launch just to verify the command line), and as a self-service first stop when "the item won't launch" — the warnings list usually explains why. ## Common pitfalls | Symptom | Likely cause | Fix | |---------|--------------|-----| | Nothing happens on `Enter` | Empty `launcherPath` | Set the launcher in Settings → Launcher. | | "Launcher not executable" | Launcher path isn't `+x` | `chmod +x /path/to/launcher` or pick a different launcher. | | Launcher opens but the file doesn't load | Launcher expects the file at a non-final position | Use a wrapper script that re-arranges arguments. | | Item with `&` or spaces in name fails | (Unlikely — should work. Check the launcher's own escape rules.) | Confirm the launcher's CLI handles special characters; wrap if not. | | "No libretro core configured" error, nothing launches | Launcher filename contains "retroarch" but `corePath` is empty | Set `corePath`. Kartend refuses the launch rather than starting RetroArch with no core. | | Archive launches the `.zip` instead of the contained file | `extractArchives=false` or `extractedExtension` wrong | Enable extraction and set the right extension. | | "Always launch with…" missing from menu | Collection only has one launcher | Add at least one additional launcher. | ## For developers - Launch pipeline: [src/modules/input/launch/](../../src/modules/input/launch/) — `LaunchManager` (validation, process spawn) and `LaunchCommandBuilder` (the pure argv/token half, unit-testable without a process). The shared helpers are `LauncherUtils`, which lives outside this module in [src/utils/app/collection/launcherconfig.h](../../src/utils/app/collection/launcherconfig.h). - Preset resolution: `LauncherUtils::resolvePreset(config, presets)` in [src/utils/app/collection/launcherconfig.h](../../src/utils/app/collection/launcherconfig.h) — call this before reading path/core/params from a launcher entry. - libretro detection: `LauncherUtils::usesLibretroCore(path)`. - Per-item override storage: `item_metadata.launcher_index` in SQLite (see [Item Metadata](Item-Metadata.md#for-developers)). - TOCTOU re-validation: `LaunchManager::validateLauncherPath()` runs once up front; `finishLaunch()` then re-checks exists/isExecutable immediately before `QProcess::start`. The window is narrowed, not closed — see the comment at the second check. - Sensitive-directory list: an inline `static const QStringList sensitiveDirectories` inside `validateLauncherPath` (`/proc`, `/sys`, `/dev`), applied to the *launcher* path only. - Adding a new launcher field (e.g. environment variables): extend `LauncherConfig` in [src/utils/app/collection/launcherconfig.h](../../src/utils/app/collection/launcherconfig.h), add UI in `launchereditordialog`, propagate through `launcherprofile_persistence.cpp` (per-collection) and/or `launcher_settings_persistence.cpp` (presets), add it to `kartmanifest.cpp` so `.kart` import/export round-trips it, and update [Configuration Reference](Configuration-Reference.md).