Skip to content

feat(mods): add native modification manager - #230

Open
flathead wants to merge 18 commits into
Axenide:devfrom
flathead:feature/mod-manager
Open

feat(mods): add native modification manager#230
flathead wants to merge 18 commits into
Axenide:devfrom
flathead:feature/mod-manager

Conversation

@flathead

@flathead flathead commented Aug 31, 2026

Copy link
Copy Markdown

I noticed a lot of useful improvements, including a few of mine (hehe), sitting in the review queue. To take some pressure off the maintainer, I built a native mod manager for Ambxst.

The manager gives community work a practical route to users while it is under review. People can install, configure, update, reorder, disable, and remove mods from Settings instead of maintaining a separate fork. That should make it easier for contributors to test focused changes and share them with other Ambxst users.

If a mod belongs in the main project, it remains ordinary Ambxst source code. Its patch or overlay can be reviewed and merged as a normal feature. The package format records compatibility, tested base revisions, affected files, dependencies, and conflicts, so packages do not create a parallel architecture.

What this PR adds:

  • a native, responsive Settings → Mods panel with search, sorting, exact drag ordering, package details, settings, and recovery actions
  • installation from local directories, archives, Git repositories, and GitHub package-directory links
  • required-mod status and an explicit Install required mods action
  • strict manifest and settings schemas
  • immutable shell generations built from the clean Ambxst source
  • compatibility checks, dependency ordering, patch composition, atomic activation, rollback, and startup recovery
  • the same operations through the ambxst mods CLI
  • documentation and a compact-player example suitable for normal upstream review

The manager has no polling loop, repository watcher, or background update worker. Network access and generation builds happen only after an explicit action. A newly built generation gets an eight-second startup check; if Quickshell exits during that window, Ambxst restores the last known-good generation.

Installed QML runs with the user's permissions. The UI states this clearly, and packages never run installer scripts.

Trying this branch

This PR changes both QML and the Go backend. The mods service lives in the binary, and install.sh downloads a prebuilt one, so testing the branch needs a local build:

make build
sudo install -m 755 ./ambxst /usr/local/bin/ambxst
ambxst reload

That step exists only while the manager is a pull request. After a release carries it, ambxst update brings the binary, and installing, updating, or reordering a mod never compiles Ambxst.

Installing community mods

The first converted packages are in flathead/ambxst-mods. Paste a package-directory link directly into Package source:

https://github.com/flathead/ambxst-mods/tree/main/packages/volume-scroll

The manager recognizes GitHub /tree/<branch>/<path> links and uses a shallow sparse checkout. A local clone and the CLI work too:

git clone https://github.com/flathead/ambxst-mods.git
ambxst mods install ./ambxst-mods/packages/volume-scroll
ambxst mods enable community.volume-scroll
ambxst reload

Packages install disabled, leaving time to review the manifest and patch. Updates, removal, rollback, and the same install flow are available from Settings.

Required mods and i18n

A manifest lists required IDs in dependencies and may publish their package URLs in dependencySources. The details pane marks each requirement as missing, disabled, or ready. Install required mods validates exact IDs, installs missing packages, enables the dependency chain in topological order, and rebuilds once. A failed download, validation, or composition leaves the previous state and active generation intact. The selected package stays disabled until the user enables it.

This supports shared packages such as community.i18n, which lives in its own repository at flathead/ambxst-mod-i18n and is pulled in by the packages that declare it. The Mods panel uses that translator only while the i18n mod is active and keeps complete English fallbacks, so Settings still opens when i18n is absent, disabled, or broken.

Load order and runtime cost

Switch the list to Sort: Load order to drag packages into an exact position. Dependencies still load before dependents.

Composition does not depend on exact patch context. A patch is tried verbatim first, then merged three-way against the pre-image blob recorded in its diff, so an earlier mod or an Ambxst update can move the lines it was written against. The merge runs in a temporary Git repository that borrows the base object store and is deleted before activation, so a generation stays plain source. When two mods only insert new lines at the same anchor — two bar widgets registering next to each other — both blocks are kept and load order decides which comes first. Two mods rewriting the same existing lines still stop the build and leave the active generation untouched.

compatibility.ambxst is enforced. compatibility.testedBaseCommits is advisory: the base moves with every Ambxst update, so an unlisted revision only marks the package as untested in Settings.

The manager is not a runtime plugin host. It composes ordinary Ambxst source into an immutable generation, then Quickshell loads that tree normally. Moving disabled packages only updates state and does not rebuild. Ongoing resource use comes from the enabled feature itself, as it would after merging the same code into core.

After an Ambxst update

A generation records the base version and revision it was built from. On launch, Ambxst compares them with the current base and skips a stale generation, starting the clean base instead. Settings then shows Rebuild required with the reason and a Rebuild button. Rebuilding re-composes the same packages onto the new base; a patch that can no longer be merged stops the build and leaves the running shell alone. An update therefore costs the mods until a rebuild, never a broken desktop.

Writing a package or moving it into core

A package needs an ambxst.mod.json plus patch or overlay files. docs/mods/README.md documents the format, dependency sources, GitHub directory installs, settings, and the QML settings API.

A package patch is generated against a tree that already carries this manager, since that is the only tree a mod can be installed on. To promote a mod into Ambxst, review the feature's own branch as normal project code, or reproduce the packaged source and diff it against dev:

git switch -c mods/example feature/mod-manager
git apply --3way /path/to/feature.patch
go test ./...
go vet ./...

There is no second implementation to port: the generated tree and the upstream change use the same QML, JavaScript, Go, and configuration files.

Checks

  • go test ./...
  • go vet ./...
  • dependency installation, rollback, and ID-validation tests
  • real GitHub package-directory installation through a shallow sparse checkout
  • offscreen Quickshell loads with and without the optional i18n singleton
  • three-way composition of six community packages on this branch, then an offscreen load of the generated tree
  • shared-anchor and same-line conflict cases covered by unit tests

Screenshots

image image image image

Add declarative mod packages that can be installed from local directories, archives, or Git repositories. Enabled packages are composed onto a clean shell source tree and activated through immutable generations, with compatibility checks, conflict detection, rollback, and startup recovery.

Expose the same operations through the ambxst mods CLI and a lazy-loaded Settings panel. The manager performs no polling or background update checks; repository access and generation builds happen only after explicit user actions.
Several focused mods need to register against the same Ambxst integration files. Rejecting shared filenames prevented otherwise independent patches from being enabled together.

Apply operations in load order and let the existing patch preflight reject overlapping hunks. Failed composition still leaves the active generation unchanged.
Expose an exact-position move operation and add a native drag handle when the list is sorted by load order. Reordering enabled packages rebuilds the immutable generation in the chosen order.

Disabled-only moves update state without rebuilding, and failed composition leaves the active generation untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant