From fcbbeccaa80c7e120933682b330272a1490c1cf5 Mon Sep 17 00:00:00 2001 From: hachem Date: Sat, 29 Aug 2026 18:24:42 +0200 Subject: refactor: unify mods command with the search --- internal/setup/mods.go | 177 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 internal/setup/mods.go (limited to 'internal/setup/mods.go') diff --git a/internal/setup/mods.go b/internal/setup/mods.go new file mode 100644 index 0000000..f8de840 --- /dev/null +++ b/internal/setup/mods.go @@ -0,0 +1,177 @@ +package setup + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/huh/spinner" + + "github.com/hachem-wtf/benson/internal/config" + "github.com/hachem-wtf/benson/internal/modrinth" +) + +func pickMods(existing []config.Mod, mc, loader string) []config.Mod { + mods := append([]config.Mod{}, existing...) + client := modrinth.NewClient() + + for { + options := []huh.Option[string]{huh.NewOption("Search & add a mod", "add")} + if len(mods) > 0 { + options = append(options, huh.NewOption("Remove a mod", "remove")) + } + options = append(options, huh.NewOption("Done", "done")) + + var action string + menu := huh.NewSelect[string](). + Title(fmt.Sprintf("Mods · %d configured", len(mods))). + Description(modsSummary(mods)). + Options(options...). + Value(&action) + + if err := runSub(menu); err != nil { + return mods + } + + switch action { + case "done": + return mods + case "add": + mods = addMod(mods, client, mc, loader) + case "remove": + mods = removeMod(mods) + } + } +} + +func addMod(mods []config.Mod, client *modrinth.Client, mc, loader string) []config.Mod { + var query string + if err := runSub(huh.NewInput().Title("Search Modrinth").Description("Mod name or keyword").Value(&query)); err != nil { + return mods + } + query = strings.TrimSpace(query) + if query == "" { + return mods + } + + var hits []modrinth.SearchHit + _ = spinner.New().Title("Searching Modrinth…").Action(func() { + hits, _ = client.Search(query, mc, loader) + }).Run() + + if len(hits) == 0 { + _ = runSub(huh.NewNote().Title("No results").Description("Nothing matched “" + query + "” for MC " + mc + ".")) + return mods + } + + options := make([]huh.Option[string], len(hits)) + for i, h := range hits { + options[i] = huh.NewOption(fmt.Sprintf("%s (%s)", h.Title, h.Slug), h.Slug) + } + + var slug string + if err := runSub(huh.NewSelect[string]().Title("Results for “" + query + "”").Options(options...).Height(selectHeight(len(options))).Value(&slug)); err != nil { + return mods + } + if slug == "" { + return mods + } + + return upsertMod(mods, config.Mod{Slug: slug, Version: pickVersion(client, slug, mc, loader)}) +} + +func pickVersion(client *modrinth.Client, slug, mc, loader string) string { + var versions []modrinth.Version + _ = spinner.New().Title("Loading versions…").Action(func() { + versions, _ = client.GetVersions(slug) + }).Run() + + options := []huh.Option[string]{huh.NewOption("Latest compatible · recommended", "")} + for _, v := range versions { + if compatibleWith(v, mc, loader) { + options = append(options, huh.NewOption(v.VersionNumber, v.VersionNumber)) + } + } + + var chosen string + _ = runSub(huh.NewSelect[string](). + Title("Version for " + slug). + Description("Keep it on latest, or pin a specific version"). + Options(options...). + Height(selectHeight(len(options))). + Value(&chosen)) + return chosen +} + +func removeMod(mods []config.Mod) []config.Mod { + options := make([]huh.Option[string], len(mods)) + for i, m := range mods { + options[i] = huh.NewOption(modLabel(m), m.Slug) + } + + var slug string + if err := runSub(huh.NewSelect[string]().Title("Remove which mod?").Options(options...).Height(selectHeight(len(options))).Value(&slug)); err != nil { + return mods + } + if slug == "" { + return mods + } + + kept := make([]config.Mod, 0, len(mods)) + for _, m := range mods { + if m.Slug != slug { + kept = append(kept, m) + } + } + return kept +} + +func runSub(field huh.Field) error { + return huh.NewForm(huh.NewGroup(field)).WithTheme(emberTheme()).Run() +} + +func upsertMod(mods []config.Mod, mod config.Mod) []config.Mod { + for i := range mods { + if mods[i].Slug == mod.Slug { + mods[i] = mod + return mods + } + } + return append(mods, mod) +} + +func compatibleWith(v modrinth.Version, mc, loader string) bool { + hasMC, hasLoader := false, false + for _, g := range v.GameVersions { + if g == mc { + hasMC = true + break + } + } + for _, l := range v.Loaders { + if l == loader { + hasLoader = true + break + } + } + return hasMC && hasLoader +} + +func modLabel(m config.Mod) string { + if m.Pinned() { + return m.Slug + " (" + m.Version + ")" + } + return m.Slug + " (latest)" +} + +func modsSummary(mods []config.Mod) string { + if len(mods) == 0 { + return "No mods yet — search Modrinth to add some." + } + + var b strings.Builder + for _, m := range mods { + fmt.Fprintf(&b, "• %s\n", modLabel(m)) + } + return strings.TrimRight(b.String(), "\n") +} -- cgit v1.3