Skip to content

feat(registry): support multiple plugin and theme registries via env vars - #2972

Open
louzt wants to merge 3 commits into
AvengeMedia:masterfrom
louzt:feat/registry-list-config
Open

feat(registry): support multiple plugin and theme registries via env vars#2972
louzt wants to merge 3 commits into
AvengeMedia:masterfrom
louzt:feat/registry-list-config

Conversation

@louzt

@louzt louzt commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

feat(registry): support multiple plugin and theme registries via env vars

Closes #2763.

The plugin and theme registries currently hardcode a single git repo
(registryRepo const, cloned into a single cache dir). This blocks the
common case of users who maintain a personal or community registry
alongside the official one. This PR adds multi-registry support driven by
two env vars and keeps the default behavior identical for users who don't
opt in.

What this PR does

  • Both core/internal/plugins/registry.go and core/internal/themes/registry.go
    now hold []RegistryConfig{Name, URL} instead of a single hardcoded URL.
  • ParseRegistriesFromEnv() reads DMS_PLUGIN_REGISTRIES /
    DMS_THEME_REGISTRIES (comma-separated git URLs, trimmed, empty entries
    skipped). Defaults to the official AvengeMedia/dms-plugin-registry.git
    when unset, whitespace-only, or all-empty.
  • Update() iterates over registries, clones/pulls each into its own
    <cacheBase>/<name>/ subdir, and aggregates results in declaration order.
    First occurrence of each plugin/theme ID wins — duplicates from
    later registries are silently dropped, so a personal registry cannot
    shadow the official one by accident.
  • migrateLegacyCache() runs once per Update() to move the pre-refactor
    flat <base>/plugins/ (and <base>/themes/) into the new
    <base>/official/ layout. Idempotent. Without it, every existing
    user re-clones the official registry on first Update after upgrade.
  • loadPluginsFrom(dir) / loadThemesFrom(dir) extracted from the
    monolithic loader so each registry's cache is read independently.
  • GetThemeSourcePath / GetThemeDir now search all registries (first hit
    wins, fallback to first registry) so multi-registry themes resolve to the
    right path.
  • Per-registry errors are wrapped with the registry name
    (fmt.Errorf("registry %s: %w", cfg.Name, err)) so triage is one grep away.

Decisions taken

  • Env vars, not config file. Matches the existing DMS_* override
    pattern (DMS_SOCKET, DMS_DEBUG); avoids introducing a new on-disk
    format for a feature that may not need persistence yet. A future PR
    could add ~/.config/dms/registries.toml for users who want named
    registries (louzt, nix-community) with the same semantics.
  • Auto-naming r0/r1/... from index. Keeps the env var syntax to URLs
    only. If the maintainer prefers explicit names, a one-line parser change
    (name|url) is enough — happy to ship that as a follow-up.
  • Declaration order = priority. First hit wins in both Update()
    aggregation and GetThemeSourcePath resolution. No disabled: bool
    field yet — adding it later is additive (defaults to false).
  • official is the only stable name. When users set
    DMS_PLUGIN_REGISTRIES=https://github.com/foo/bar.git, the cache dir is
    <base>/r0. The official name only applies to the default fallback so
    users can layer a personal registry on top without conflicting with the
    default's stable name.

Scope boundary

This PR does NOT:

  • Add a per-user config file (registries.toml) — see Decisions, follow-up.
  • Add a disabled / per-registry priority flag — additive, follow-up.
  • Change how plugins/themes are installed (still per-plugin JSON /
    theme.json discovery) — only how the source list is composed.
  • Touch the IPC layer, the widget system, or the settings UI — those are
    separate concerns.

Validation

$ go build ./...
(no output)

$ go test ./internal/plugins/... ./internal/themes/...
ok      github.com/AvengeMedia/DankMaterialShell/core/internal/plugins   0.006s
ok      github.com/AvengeMedia/DankMaterialShell/core/internal/themes    0.004s

$ go vet ./internal/plugins/... ./internal/themes/...
(no output)

$ go test -run TestParseRegistriesFromEnv -v ./internal/plugins/...
=== RUN   TestParseRegistriesFromEnv
=== RUN   TestParseRegistriesFromEnv/defaults_to_official_when_env_unset
=== RUN   TestParseRegistriesFromEnv/defaults_to_official_when_env_is_whitespace
=== RUN   TestParseRegistriesFromEnv/parses_comma-separated_URLs
=== RUN   TestParseRegistriesFromEnv/trims_whitespace_around_URLs
=== RUN   TestParseRegistriesFromEnv/skips_empty_entries
=== RUN   TestParseRegistriesFromEnv/falls_back_to_official_when_all_entries_empty
--- PASS: TestParseRegistriesFromEnv (0.00s)
    --- PASS: TestParseRegistriesFromEnv/defaults_to_official_when_env_unset (0.00s)
    --- PASS: TestParseRegistriesFromEnv/defaults_to_official_when_env_is_whitespace (0.00s)
    --- PASS: TestParseRegistriesFromEnv/parses_comma-separated_URLs (0.00s)
    --- PASS: TestParseRegistriesFromEnv/trims_whitespace_around_URLs (0.00s)
    --- PASS: TestParseRegistriesFromEnv/skips_empty_entries (0.00s)
    --- PASS: TestParseRegistriesFromEnv/falls_back_to_official_when_all_entries_empty (0.00s)
PASS
ok      github.com/AvengeMedia/DankMaterialShell/core/internal/plugins   0.005s

Themes have parallel coverage (TestParseRegistriesFromEnv with three
subtests: unset, comma-separated, all-empty fallback).

Existing tests for TestUpdate, TestList, and TestLoadPlugins were
updated to the new method signatures; new subtests exercise the
multi-registry path end-to-end with a mock git client:

  • TestUpdate/aggregates_from_multiple_registries — official + louzt, both contribute
  • TestUpdate/dedupes_by_ID_with_declaration_order_priority — first registry wins
  • TestUpdate/migrates_legacy_cache_directory<base>/plugins/<base>/official/plugins/
  • TestUpdate/no-op_migration_when_legacy_cache_absent — fresh install skips migration
  • TestUpdateMigration/migrates_legacy_themes_cache — themes parallel coverage

Diffstat

 core/internal/plugins/manager.go        |  17 ++
 core/internal/plugins/registry.go       | 161 ++++++++++++++++++++-----
 core/internal/plugins/registry_test.go  | 212 ++++++++++++++++++++++++++++----
 core/internal/themes/registry.go        | 213 ++++++++++++++++++++++++++++-----
 core/internal/themes/registry_test.go   | 151 ++++++++++++++++++++++++
 5 files changed, 667 insertions(+), 87 deletions(-)

Three logical commits on feat/registry-list-config (master..HEAD):

  1. 86f7550b — feat(registry): support multiple plugin/theme registries via env
  2. de79ae8f — feat(registry): dedupe by ID + migrate legacy cache
  3. ed7511ec — test(themes): cover legacy cache migration (parallel coverage)

Two packages, one logical change. Plugins and themes share no code; this
PR keeps that property (no shared registries helper package introduced
yet — easy to extract later if a third caller appears).

Backwards compatibility

DMS_PLUGIN_REGISTRIES and DMS_THEME_REGISTRIES unset → single
official registry, identical to the previous behavior. Existing CLI
flags, config keys, and widget bindings are unchanged.

Cache dir moves from <base>/plugins/ to <base>/official/plugins/
on first Update() after this PR. The migration helper
(migrateLegacyCache) renames the existing flat layout in place; if no
legacy dir is present, it's a no-op. This is invisible for users on a
fresh install and recovers a fast-forward pull for users on an upgrade.

Why this is the right shape for #2763

Issue #2763 asks for "support custom plugin and theme registries". The
env-var approach:

  • Ships in one PR instead of three (config-file, naming, priority).
  • Doesn't require a settings-UI change to be useful.
  • Lets power users script it (DMS_PLUGIN_REGISTRIES=$WORK_REGISTRY dms).
  • Composable with future PRs: a config file can override the env var,
    a disabled flag is additive, named registries are a parser tweak.

louzt added 3 commits July 28, 2026 11:09
Resolves AvengeMedia#2763 — both plugin and theme
registries now read from a comma-separated list of git URLs
(DMS_PLUGIN_REGISTRIES, DMS_THEME_REGISTRIES) instead of a single
hardcoded repo. Each registry clones/pulls into its own subdir
under the cache base; results aggregate in declaration order.

Backwards compatible: when the env var is unset/empty/all-blank,
defaults to the existing single official registry. Existing
callers of NewRegistry() inherit the new behavior transparently.

GetThemeSourcePath/GetThemeDir now search all registered registries
and return the first match (falls back to first registry if none
have the theme), so multi-registry themes resolve correctly.

Tests:
- TestParseRegistriesFromEnv covers unset/whitespace, multi-URL,
  trim, empty entries, all-empty fallback (both packages)
- TestUpdate gains 'aggregates from multiple registries' subtest
- loadPlugins/loadThemes split into per-dir + Update-loop aggregate
  so each registry's cache is read independently

No push — local only, awaiting maintainer response on AvengeMedia#2763 before
opening upstream PR.
Two follow-ups to the multi-registry refactor, addressing issues raised in
self-review before opening the upstream PR:

1. Declaration-order ID dedupe. Update() now keeps the first occurrence of
   each plugin/theme ID and discards duplicates from later registries. List,
   Search, Get all see one plugin per ID. Tests: TestUpdate/dedupes_by_ID
   with declaration_order_priority (plugins).

2. Legacy cache migration. Update() now moves <base>/plugins/ to
   <base>/official/plugins/ (and <base>/themes/ to <base>/official/themes/)
   on first run, idempotent. Without this, every existing user re-clones
   the official registry on first Update after upgrade. Tests:
   TestUpdate/migrates_legacy_cache_directory + no-op_migration_when_legacy_cache_absent
   (plugins).

Also wraps per-registry errors with the registry name ("registry <name>: %w")
so triage is one grep away.

Plugins only — themes inherit the same shape but tests cover the
migration in a follow-up to keep this commit focused.

No build, no test regressions. No push (stage-local-first).
Parallel coverage to the plugins registry migration test. Seed
<base>/themes/example/theme.json as the pre-refactor layout, run
Update(), and assert the theme resolves at <base>/official/themes/example/
after migration. Idempotent: fresh installs skip the migration entirely.
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.

Support custom plugin and theme registries

1 participant