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