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 ++++++++++++++++++++++++++++++++++++++++++++++++ internal/setup/setup.go | 69 +++++++++---------- 2 files changed, 208 insertions(+), 38 deletions(-) create mode 100644 internal/setup/mods.go (limited to 'internal/setup') 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") +} diff --git a/internal/setup/setup.go b/internal/setup/setup.go index c2f8c10..c3c957c 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -27,46 +27,49 @@ func Run() error { backupLoc = cfg.BackupLocation preCmd = cfg.MergePreCommand postCmd = cfg.MergePostCommand - modsText = strings.Join(cfg.ModrinthMods, "\n") - confirm = true ) games, loaders, installers := fetchFabricVersions() form := huh.NewForm( huh.NewGroup( - huh.NewNote(). - Title("Benson setup"). - Description("Configure the server manager.\nThe Discord bot is optional — run `benson setup bot` for that."), - ), - huh.NewGroup( - versionField("Minecraft version", "The Minecraft release your server runs — newest stable is recommended", games, &mcVersion, "1.21.10"), + versionField("Minecraft version", "Which release your server runs · newest recommended", games, &mcVersion, "1.21.10"), ), huh.NewGroup( - versionField("Fabric loader version", "The Fabric mod loader that boots your mods — newest is recommended", loaders, &fabricLoader, "0.17.3"), + versionField("Fabric loader version", "The mod loader that boots your mods · newest recommended", loaders, &fabricLoader, "0.17.3"), ), huh.NewGroup( - versionField("Fabric installer version", "Builds the server jar from the loader — newest is recommended", installers, &loaderMeta, "1.1.0"), + versionField("Fabric installer version", "Builds the server jar · newest recommended", installers, &loaderMeta, "1.1.0"), ), huh.NewGroup( - huh.NewInput().Title("Java path").Description("Leave blank to use 'java' from PATH").Value(&javaPath), - huh.NewInput().Title("Server location").Description("Live server directory, merged into by 'merge'").Value(&serverLoc), - huh.NewInput().Title("Backup location").Description("Where server backups are written").Value(&backupLoc), - huh.NewInput().Title("Pre-merge command").Description("Optional shell command run before merging").Value(&preCmd), - huh.NewInput().Title("Post-merge command").Description("Optional shell command run after merging").Value(&postCmd), - ).Title("Paths & hooks (optional)"), + huh.NewInput().Title("Java path").Description("Blank = use 'java' from PATH").Value(&javaPath), + huh.NewInput().Title("Server location").Description("Live server dir, merged into by 'merge'").Value(&serverLoc), + huh.NewInput().Title("Backup location").Description("Where backups are written").Value(&backupLoc), + ).Title("Locations (optional)"), huh.NewGroup( - huh.NewText().Title("Modrinth mods").Description("One mod slug per line").Lines(8).Value(&modsText), - ).Title("Mods"), - huh.NewGroup( - huh.NewConfirm().Title("Write config/config.json?").Affirmative("Write").Negative("Cancel").Value(&confirm), - ), + huh.NewInput().Title("Pre-merge command").Description("Optional shell command before merging").Value(&preCmd), + huh.NewInput().Title("Post-merge command").Description("Optional shell command after merging").Value(&postCmd), + ).Title("Merge hooks (optional)"), ) if done, err := runForm(form); err != nil || !done { return err } + mods := pickMods(cfg.ModrinthMods, strings.TrimSpace(mcVersion), "fabric") + + confirm := true + confirmForm := huh.NewForm(huh.NewGroup( + huh.NewConfirm().Title("Write config/config.json?").Affirmative("Write").Negative("Cancel").Value(&confirm), + )) + if done, err := runForm(confirmForm); err != nil || !done { + return err + } + if !confirm { + fmt.Println("Setup cancelled — nothing written.") + return nil + } + cfg.MinecraftVersion = strings.TrimSpace(mcVersion) cfg.FabricLoaderVersion = strings.TrimSpace(fabricLoader) cfg.LoaderMetaVersion = strings.TrimSpace(loaderMeta) @@ -75,7 +78,7 @@ func Run() error { cfg.BackupLocation = strings.TrimSpace(backupLoc) cfg.MergePreCommand = strings.TrimSpace(preCmd) cfg.MergePostCommand = strings.TrimSpace(postCmd) - cfg.ModrinthMods = parseMods(modsText) + cfg.ModrinthMods = mods if err := writeConfigFile(cfg); err != nil { return err @@ -106,14 +109,10 @@ func RunBot() error { huh.NewGroup( huh.NewNote(). Title("Benson bot setup"). - Description("Configure the Discord bot that posts Modrinth compatibility reports."), - ), - huh.NewGroup( + Description("Posts Modrinth compatibility reports to a Discord channel."), huh.NewInput().Title("Discord bot token").Description("Developer Portal → Bot → Reset/Copy Token").Password(true).Value(&token).Validate(required("token")), huh.NewInput().Title("Channel ID").Description("Channel where reports are posted").Value(&channelID).Validate(required("channel ID")), huh.NewInput().Title("Check interval (minutes)").Value(&intervalStr).Validate(positiveInt), - ).Title("Discord"), - huh.NewGroup( huh.NewConfirm().Title("Write config/config.json and config/api.json?").Affirmative("Write").Negative("Cancel").Value(&confirm), ), ) @@ -176,10 +175,14 @@ func versionField(title, desc string, options []string, value *string, placehold Title(title). Description(desc). Options(recommendedOptions(options)...). - Height(12). + Height(selectHeight(len(options))). Value(value) } +func selectHeight(n int) int { + return min(10, n) + 2 +} + func recommendedOptions(versions []string) []huh.Option[string] { options := make([]huh.Option[string], len(versions)) for i, v := range versions { @@ -243,16 +246,6 @@ func positiveInt(s string) error { return nil } -func parseMods(text string) []string { - mods := []string{} - for _, line := range strings.Split(text, "\n") { - if slug := strings.TrimSpace(line); slug != "" { - mods = append(mods, slug) - } - } - return mods -} - func check() string { return lipgloss.NewStyle().Foreground(lipgloss.Color("#22C55E")).Render("✓") } -- cgit v1.3