Skip to content

Add terminal dashboard #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Configuration validation on load
- Verbose logging and automatic confirmation via `--yes`
- Command aliases for faster workflows
- Optional curses-based `dashboard` for interactive status viewing

## Prerequisites

Expand Down Expand Up @@ -71,6 +72,10 @@ Or download a prebuilt binary for your platform if available.
# or use alias:
# .\modpilot.exe clean MyPack
```
9. Launch the interactive dashboard:
```pwsh
.\modpilot.exe dashboard
```

## Commands

Expand All @@ -86,6 +91,7 @@ Or download a prebuilt binary for your platform if available.
| `check-updates [pack]` | | Check Modrinth for newer versions and check for missing local files |
| `update [pack]` | `update-pack`, `upd` | Check & download new/missing versions for a modpack, updating state |
| `sync [pack]` | `sync-pack`, `clean` | Remove JARs from the modpack's directory that aren't listed in `state.json` |
| `dashboard` | | Interactive curses UI for viewing pack status |

Global flags: `-c, --config`, `-s, --state`, `-m, --mods-dir`, `-y, --yes`, `-g, --mc-version` (override), `-l, --loader` (override), `-v, --verbose`.

Expand Down Expand Up @@ -162,6 +168,7 @@ Example: `mods/MyPack/fabric-api-0.100.0+1.21.5.jar`
- `list-mods` (`lm`)
- `update` (`update-pack`, `upd`)
- `sync` (`sync-pack`, `clean`)
- `dashboard`
// Note: check-updates does not have an alias currently

## Contributing
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module deadfrost.dev/modpack-manager
go 1.23.4

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/rivo/tview v0.0.0-20230720074347-8d10e88583f3
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
)
122 changes: 122 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"path/filepath"
"strings"

"github.com/rivo/tview"

Check failure on line 11 in main.go

View workflow job for this annotation

GitHub Actions / build

missing go.sum entry for module providing package github.com/rivo/tview (imported by deadfrost.dev/modpack-manager); to add:
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -670,6 +671,7 @@
update,
checkUpdatesCmd,
syncCmd,
dashboardCmd,
)

if err := root.Execute(); err != nil {
Expand All @@ -683,5 +685,125 @@
if condition {
return trueVal
}

// dashboard
dashboardCmd := &cobra.Command{
Use: "dashboard",
Short: "Launch interactive curses dashboard",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := LoadConfig(cfgFile)
if err != nil {
return err
}
state, err := LoadState(stateFile)
if err != nil {
return err
}

app := tview.NewApplication()
list := tview.NewList().ShowSecondaryText(false)
details := tview.NewTextView().SetDynamicColors(true)
details.SetBorder(true).SetTitle("Status")

for name, pack := range cfg.Modpacks {
n := name
p := pack
list.AddItem(name, "", 0, func() {
lines, err := checkUpdatesSummary(n, p, state[n])
if err != nil {
details.SetText(fmt.Sprintf("Error: %v", err))
} else {
details.SetText(strings.Join(lines, "\n"))
}
app.Draw()
})
}
list.SetBorder(true).SetTitle("Modpacks")

flex := tview.NewFlex().AddItem(list, 0, 1, true).AddItem(details, 0, 2, false)
return app.SetRoot(flex, true).Run()
},
}
return falseVal
}

// checkUpdatesSummary returns lines describing update status for a pack
func checkUpdatesSummary(packName string, packCfg ModpackConfig, packState map[string]ModState) ([]string, error) {
gameVersion := packCfg.MCVersion
loader := packCfg.Loader
destDir := filepath.Join(modsDir, packName)
if packState == nil {
packState = make(map[string]ModState)
}
lines := []string{}
for _, slug := range packCfg.Mods {
modState, modInState := packState[slug]
fileExists := false
if modInState && modState.Filename != "" {
if _, err := os.Stat(filepath.Join(destDir, modState.Filename)); err == nil {
fileExists = true
}
}
ver, err := FetchLatestVersion(slug, gameVersion, loader)
if err != nil {
lines = append(lines, fmt.Sprintf("[red]✗ %s: %v[-]", slug, err))
continue
}
if !modInState {
lines = append(lines, fmt.Sprintf("[green]+ %s: latest %s[-]", slug, ver.ID))
} else if ver.ID != modState.VersionID {
missing := ""
if !fileExists {
missing = " (file missing!)"
}
lines = append(lines, fmt.Sprintf("[yellow]⚠ %s: %s → %s%s[-]", slug, modState.VersionID, ver.ID, missing))
} else if !fileExists {
lines = append(lines, fmt.Sprintf("[yellow]! %s: file missing for %s[-]", slug, ver.ID))
} else {
lines = append(lines, fmt.Sprintf("[blue]✓ %s: up to date (%s)[-]", slug, ver.ID))
}
}
return lines, nil
}

// updatePackNoPrompt downloads needed mods for a pack without prompting
func updatePackNoPrompt(packName string, packCfg ModpackConfig, state State) error {
gameVersion := packCfg.MCVersion
loader := packCfg.Loader
if state[packName] == nil {
state[packName] = make(map[string]ModState)
}
packState := state[packName]
destDir := filepath.Join(modsDir, packName)
for _, slug := range packCfg.Mods {
modState, modInState := packState[slug]
fileExists := false
expectedPath := ""
if modInState && modState.Filename != "" {
expectedPath = filepath.Join(destDir, modState.Filename)
if _, err := os.Stat(expectedPath); err == nil {
fileExists = true
}
}
ver, err := FetchLatestVersion(slug, gameVersion, loader)
if err != nil {
return err
}
needsDownload := !modInState || ver.ID != modState.VersionID || !fileExists
if !needsDownload {
continue
}
if fileExists && expectedPath != "" && modState.Filename != ver.Files[0].Filename {
_ = os.Remove(expectedPath)
}
if len(ver.Files) == 0 {
continue
}
outPath, err := DownloadFile(ver.Files[0].URL, destDir)
if err != nil {
return err
}
packState[slug] = ModState{VersionID: ver.ID, Filename: filepath.Base(outPath)}
}
return SaveState(stateFile, state)
}
Loading