Skip to content

Per-cave settings: schema & IPC designΒ #299

@leafo

Description

@leafo

Introduce per-cave settings to allow users to configure launch behavior on a per-game basis, overriding global preferences. Settings are stored in butler's database alongside the cave they belong to (this is tied to their profile?). This could include things like game specific launch arguments, sandbox settings overrides, wine config.

References:

Storage

Approach: A Settings column (type TEXT, stores JSON) on the existing caves table.

This follows the same pattern as the existing Verdict column on caves, which stores a JSON-serialized Go struct as a text blob. Hades (the ORM) auto-migrates new columns β€” adding a text column with an empty string default requires no explicit migration. An empty string deserializes to a zero-value CaveSettings{} (all fields nil/empty), meaning existing caves behave as if they have no overrides.

Settings are deleted automatically when a cave is uninstalled since they live on the cave row itself.

Types

CaveSettings

Flat per-cave configuration. All fields are optional β€” omitted fields mean "use global default." Sandbox-related fields are prefixed with sandbox since they only apply when the sandbox is active.

type CaveSettings struct {
    // Overrides the global sandbox enabled/disabled preference.
    // nil = inherit global, true = force on, false = force off
    Sandbox *bool `json:"sandbox,omitempty"`

    // Override sandbox runner type (bubblewrap, firejail, flatpak, fuji, auto).
    SandboxType *SandboxType `json:"sandboxType,omitempty"`

    // Override network restriction within the sandbox.
    SandboxNoNetwork *bool `json:"sandboxNoNetwork,omitempty"`

    // Override allowed environment variables within the sandbox.
    SandboxAllowEnv *[]string `json:"sandboxAllowEnv,omitempty"`

    // Additional command-line arguments appended after manifest args.
    ExtraArgs []string `json:"extraArgs,omitempty"`
}

Override Semantics

Single-level override: per-cave value wins if set, otherwise global preference applies. Each field is independent.

Per-cave field Global preference Effective value
undefined any global value
set any per-cave value

ExtraArgs: Always appended after manifest action args. Not an override β€” additive only.

IPC Methods

Caves.GetSettings

Retrieve per-cave settings for a given cave.

β†’ Request: Caves.GetSettings
  { caveId: string }

← Response:
  { settings: CaveSettings }

Returns a CaveSettings with all nil/empty fields if no settings have been configured.

Caves.SetSettings

Store per-cave settings. Full replacement β€” the entire CaveSettings object is written, not merged.

β†’ Request: Caves.SetSettings
  { caveId: string, settings: CaveSettings }

← Response:
  {}

To clear all overrides, pass an empty CaveSettings{}. To update a single field, the client should GetSettings, modify, then SetSettings.

Launch (modified)

One new optional field on the existing LaunchParams:

β†’ Request: Launch
  {
    caveId: string,
    prereqsDir: string,
    sandbox?: boolean,
    sandboxOptions?: SandboxOptions,
    extraArgs?: string[],           // NEW: appended after manifest args
    ...existing fields...
  }

This keeps the client in control of the merge β€” the itch app reads cave settings, merges with global preferences, and sends the resolved values to Launch. Butler doesn't need to know about global preferences.

Schema Migration

None required. The ORM (hades) auto-migrates by adding the new settings TEXT column with an empty string default. Empty string deserializes to zero-value CaveSettings{}. This is the same mechanism used when the verdict column was added to caves.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions