Extensions
An extension is a module, a source or a theme that does not ship with Universe. The UI calls them
add-ons. A module or a source is the same folder a shipped one is (docs/api.md, Module protocol
and Source protocol): a module.toml or a source.toml at its root, and the programs it names. A
theme is a look for Universe Big Screen: a theme.toml and its own QML tree, whose root is
theme.qml (docs/frontends.md, Themes). An extension runs as you, with your files and your
accounts: nothing sandboxes it, and a theme’s QML can reach them as well as a program can.
Layout
Section titled “Layout”my-module/ module.toml # or source.toml: one kind's manifest, never two bin/… # the hooks or the source's exe the manifest names, executable
my-theme/ theme.toml theme.qml # the look's root, loaded in place of a built-in one screenshot.png # what theme.toml's `screenshot` names: shown beside its name … # the rest of its QML tree, its pictures and soundsEvery manifest starts with the same fields: api, id, name, version, description, and
optionally author, license and screenshot (a picture inside the folder, by its path there).
examples/theme/ is a theme of a few files to start from:
api = 2id = "sample"name = "Sample"version = "1.0.0"description = "The smallest theme: the library as a row of covers, and every look a Start away."author = "Ilyas Turki"license = "MIT"screenshot = "screenshot.png"Universe looks extensions up in three places. On the same id, a later one wins:
$XDG_DATA_HOME/universe/extensions/<module|source|theme>/<id>/: whatuniverse extension installput there, with<id>.jsonbeside each one (where it came from, whichupdatefetches again);- the shipped ones (
$UNIVERSE_MODULES_PATH/$UNIVERSE_SOURCES_PATH, then$XDG_DATA_DIRS/universe/{modules,sources}); - your own,
$XDG_CONFIG_HOME/universe/{modules,sources}/<id>/, the folder to work on one in.
A theme has only the first: the built-in looks (reprise, the Universe look, which universe also
names; switch2; ps5) are not folders, and
there is no folder of your own for themes.
Installing an id that ships (a built-in look’s, for a theme), or that your own folder holds, is refused, and so is an id another kind holds: two kinds never share one.
API number
Section titled “API number”The manifest’s api names the extension API it was written for. This Universe reads api 2.
An extension whose api it does not read, or that names none, is refused at install. Found later,
after an update of Universe, it stays listed but unavailable: its hooks never run, its source
verbs are refused, a theme is listed in Settings › Appearance but cannot be picked, and
universe doctor shows an extension-api line for it, whether it is on or off.
The API grows without a bump while every change is an addition: a new hook, field, setting key,
event or api member that an older extension simply does not use. For a theme, the API is the part
of the QML api object docs/frontends.md lists under What a theme can rely on. A change that
would break an extension written for api 2 bumps the number, and Universe then reads the range it
still supports.
[requires] core (comparators on Universe’s version) still applies on top, for an extension that
needs a feature added later in the same api: an install refuses a module or a source it rules out, with
the version it needs.
A source’s [[tools]] pins only stand in for its own programs: it pins nothing while it is off, a
shipped source’s pin wins over an extension’s of the same id, and a pin is only fetched for the
source that declares it. Once fetched, its program sits in $XDG_DATA_HOME/universe/bin like every
fetched tool.
Installing
Section titled “Installing”universe extension ls # the index's extensions and the installed onesuniverse extension install now-playing # an id from the indexuniverse extension install https://example.org/now-playing-1.2.0.tar.gzuniverse extension install ./now-playing.tar.gzuniverse extension install ~/src/now-playing # a folder, copieduniverse extension update [id] # every installed one when no id is givenuniverse extension remove now-playingAn install asks first. It names the extension’s kind, says it runs programs as you, and says where
it comes from: the index, or Unlisted for a URL, an archive or a folder. --yes accepts without
asking. An archive is a tar (plain, gzip or xz) or a zip, holding the folder at its root or in its
one folder, as a release archive does. URLs are https:// only. A path holds a / (./now-playing):
a bare word is always an id of the index. Replacing an installed extension waits until no game
runs.
Installing does not switch an extension on: universe module enable <id> or
universe source enable <id> does, and so do the Features and Stores pages. A theme is picked in
Settings › Appearance. An update replaces the folder in place. An index install takes the index’s
version when it is newer: X.Y.Z compared as numbers, so an index that goes back offers nothing (a
version that is not X.Y.Z offers any other). Any other install is fetched again from its URL, archive or folder. Removing
switches a module or a source off, then
deletes its folder; removing the theme in use puts the default look back. What it saved under
$XDG_DATA_HOME/universe/{modules,sources}/<id>/ stays.
A theme that fails to load puts the default look back, with a notice. One that loads but leaves no way out, because it breaks once running or offers no other look, is left by holding HOME on the pad for 5 s: Universe reads that hold itself, whatever the theme’s QML does, puts the default look back with the same notice, and no longer remembers the theme as the look to start on.
In the UI, the Features and Stores pages of every look end on a “Get more…” row, and Settings › Appearance has one after the looks. It lists the index’s add-ons of that kind and installs one after the same confirmation. The UI installs from the index only. An unlisted extension installed from the CLI shows an Unlisted tag in the lists and can be removed there.
By hand
Section titled “By hand”The CLI is not needed:
- Put the folder under
$XDG_DATA_HOME/universe/extensions/module/<id>/(orsource/<id>/,theme/<id>/), named after its manifest’sid. Make its programs executable. - Switch it on:
universe module enable <id>, orenabledinconfig.toml’s[modules]or[sources]; a theme shows in Settings › Appearance the next time it opens.
Without a <id>.json beside it, it reads as unlisted and update leaves it alone. To work on an
extension, use $XDG_CONFIG_HOME/universe/modules/<id>/ instead: that folder wins over an
installed copy. A theme has no such folder: install it from the folder you work in, then
universe extension update <id> copies it again, which the UI loads at its next start.
The index
Section titled “The index”extensions.index in config.toml, or UNIVERSE_EXTENSIONS_INDEX, is an https:// URL or a file.
When empty it is the one the registry (below) publishes,
https://raw.githubusercontent.com/ilyasturki/universe-extensions/index/index.json:
{ "schema": 1, "extensions": [ { "id": "now-playing", "kind": "module", "name": "Now Playing", "version": "1.2.0", "description": "Shares the game you play as your chat status.", "api": 2, "url": "https://example.org/now-playing-1.2.0.tar.gz", "sha256": "…", "size": 48000, "homepage": "https://example.org/now-playing" } ]}kind is module, source or theme. url is the archive and sha256 its digest: an archive
whose digest differs is refused, and so is one whose manifest holds another id, kind or version than
its entry. A url that names a file is only read from an index that is a file itself. An update that
the index lists for another api is not offered. The index is HTTPS plus these pins, with no
signature. Universe reads schema 1 and refuses a newer one.
The registry is ilyasturki/universe-extensions.
To list an extension there, open a pull request adding extensions/<id>.toml to its main branch.
CI writes index.json to the index branch from those entries, so nobody edits it by hand. The
registry’s README says what an entry holds and what a pull request’s check covers.