hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/internal/setup/mods.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/setup/mods.go')
-rw-r--r--internal/setup/mods.go177
1 files changed, 177 insertions, 0 deletions
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")
+}