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") }