Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
a6bf243
feat: Continue integration and download of more artifacts
pawndev Feb 27, 2026
19814c2
fix(ES/artifacts): Add romm assets prefix if not present
pawndev Mar 2, 2026
da32c9c
fix: Correct error logs for artifacts and correct path check for mixi…
pawndev Mar 3, 2026
ea734a3
fix: update platform and roms structs for the new fields
BrandonKowalski Mar 14, 2026
ee17d85
fix: fix muOS dev env changes (broke when implementing the new paths)
BrandonKowalski Mar 15, 2026
ac311b9
fix: change how BIOS availability is determined using new bios count …
BrandonKowalski Mar 15, 2026
bd81a90
feat: add cfw artwork sync closes #130
BrandonKowalski Mar 15, 2026
2216dd0
feat: add API token authentication with pairing code flow
BrandonKowalski Mar 15, 2026
b8f3979
feat: add missing i18n keys for token auth, tools menu, and sync history
BrandonKowalski Mar 15, 2026
3f7885a
docs: add Batocera to site navigation
BrandonKowalski Mar 15, 2026
ac90a02
docs: rewrite login guide for token auth and server connection
BrandonKowalski Mar 15, 2026
37d5ca6
docs: update settings reference for new menu structure
BrandonKowalski Mar 15, 2026
a22ed06
docs: rewrite FAQ with collapsible sections and update save sync guide
BrandonKowalski Mar 15, 2026
6543848
fix: if backup happy path fails, cancel the sync
BrandonKowalski Mar 15, 2026
3fe4c28
docs: add missing Batocera reference to README
BrandonKowalski Mar 15, 2026
09850a8
fix: clean up SDL before exiting during login flow
BrandonKowalski Mar 18, 2026
c18a842
feat: add Onion OS as a supported CFW
BrandonKowalski Mar 18, 2026
7f6f541
fix: issue where Knulli will continually restart Grout when Quick Res…
BrandonKowalski Mar 18, 2026
f4fe81e
fix: set permissions to a+x before zipping launch script and binary u…
BrandonKowalski Mar 18, 2026
6add00e
feat(es/art): add more image art download for emulationstation
pawndev Mar 19, 2026
97b5615
chore: update dependencies
BrandonKowalski Mar 22, 2026
63fd02f
build: move packaging output to dist/, vendor Miyoo libs, add app icons
BrandonKowalski Mar 22, 2026
9e0ec70
fix: write splash image to cache directory instead of using embedded …
BrandonKowalski Mar 22, 2026
92bf1d6
fix: store config timeouts as seconds, clamp oversized API timeout
BrandonKowalski Mar 22, 2026
e55d796
feat: remap L2 as Menu button on Miyoo devices
BrandonKowalski Mar 22, 2026
0ee4840
docs: add Onion and Allium quick start guides
BrandonKowalski Mar 22, 2026
7cf3912
docs: alphabetize Quick Start nav
BrandonKowalski Mar 22, 2026
160a829
docs: add links and alphabetize Before You Begin CFW list
BrandonKowalski Mar 22, 2026
9bbac71
docs: alphabetize and add all CFW links to homepage and README
BrandonKowalski Mar 22, 2026
ba7a57c
docs: parameterize CFW links with single source of truth
BrandonKowalski Mar 22, 2026
4ec1cd4
ci: add Allium and Onion 32-bit builds to release workflow
BrandonKowalski Mar 22, 2026
b89a08f
merge: resolve conflict with v4.8.0.0, keep dist/ paths with a+x perm…
BrandonKowalski Mar 22, 2026
bce05ef
Merge pull request #167 from rommapp/onion
BrandonKowalski Mar 22, 2026
705c8f9
fix: add QEMU setup for ARM32 cross-build and --load flag to build-32…
BrandonKowalski Mar 22, 2026
5773ff3
Add instructions to set time and/or enable NTP for save sync compatib…
klinkertinlegs Mar 22, 2026
a6d1521
Merge pull request #169 from klinkertinlegs/patch-1
BrandonKowalski Mar 22, 2026
5099974
fix: add Allium and Onion config.json files for app metadata
BrandonKowalski Mar 23, 2026
bc4ed1b
feat: support ARM32 binary updates for Allium and Onion
BrandonKowalski Mar 23, 2026
ceb67d8
docs: add Allium and Onion platform mapping tables
BrandonKowalski Mar 23, 2026
a3734f2
feat(es/art): marquee tag can be marquee/logo image
pawndev Mar 23, 2026
e9582eb
Merge branch 'v4.8.0.0' into feat/139/finalize-ES-gamelist-integration
pawndev Mar 23, 2026
815c145
fix(es/art): thumbnail settings save
pawndev Mar 23, 2026
bc32bc7
feat(es/art): include batocera as a ES OS comptatible
pawndev Mar 23, 2026
9b47a95
fix(es/art): add romm resource prefix for manual download
pawndev Mar 24, 2026
c11b680
fix(es/art): fix some image path that can have '?' char in it
pawndev Mar 24, 2026
0f7b97c
Merge pull request #174 from rommapp/main
BrandonKowalski Mar 24, 2026
ef260ca
fix: Onion and Allium save directories to hopefully match what is act…
BrandonKowalski Mar 24, 2026
4c07ea5
fix: update Onion platform mappings
BrandonKowalski Mar 25, 2026
0d8a4fd
feat: add AMD64/x86_64 Batocera build support
BrandonKowalski Mar 25, 2026
0e5255b
refactor: use runtime.GOARCH for all update asset names
BrandonKowalski Mar 25, 2026
bd9edb9
feat: add x86 Batocera build and parallelize release workflow
BrandonKowalski Mar 25, 2026
6bcea38
fix: include legacy "grout" binary for backwards-compatible updates
BrandonKowalski Mar 25, 2026
abbb1bb
feat: add MinUI build
BrandonKowalski Mar 26, 2026
c92b305
Merge pull request #162 from pawndev/feat/139/finalize-ES-gamelist-in…
BrandonKowalski Mar 26, 2026
2d81086
fix: compare prerelease identifiers numerically
BrandonKowalski Mar 26, 2026
957c5e8
refactor: replace GitHub API with static versions file
BrandonKowalski Mar 26, 2026
f2f783d
feat: full distribution update with SHA256 verification
BrandonKowalski Mar 26, 2026
daa864c
feat: add update preamble to all launch scripts
BrandonKowalski Mar 26, 2026
f2a9737
fix: data race in auto-update and add versions.json CI job
BrandonKowalski Mar 26, 2026
362348e
Merge remote-tracking branch 'origin/v4.8.0.0' into v4.8.0.0
BrandonKowalski Mar 26, 2026
60f055e
fix: data race in auto-update and add versions.json CI job
BrandonKowalski Mar 26, 2026
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
371 changes: 326 additions & 45 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# Build
build64/
build32/
dist/
build-miyoo/
grout
app/app
Expand All @@ -17,6 +18,7 @@ es_restart_request
# Local config
.env
config.json
!scripts/**/config.json
config-*.json

# Cache & temp
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
<br />
<img src=".github/resources/logotipo.png" height="45px" width="auto" alt="romm grout logotype">
<h3 style="font-size: 25px;">
A RomM Client for <a href="https://muos.dev">muOS</a>, <a href="https://knulli.org">Knulli</a>, <a href="https://rocknix.org">ROCKNIX</a>, <a href="https://spruceui.github.io/">Spruce (v4)</a>, <a href="https://nextui.loveretro.games">NextUI</a> and <a href="https://trimui.com">TrimUI</a>.
A RomM Client for your retro doo-dad.
</h3>

[Allium][allium] · [Batocera][batocera] · [Knulli][knulli] · [muOS][muos] · [NextUI][nextui] · [Onion][onion] · [ROCKNIX][rocknix] · [Spruce][spruce] · [TrimUI][trimui]

<br>


Expand Down Expand Up @@ -92,6 +94,18 @@ If you enjoy using Grout and feel inclined to pay it forward, go do something ni

✌🏻

<!-- CFW Links -->

[allium]: https://github.com/goweiwen/Allium
[batocera]: https://batocera.org
[knulli]: https://knulli.org
[muos]: https://muos.dev
[nextui]: https://nextui.loveretro.games
[onion]: https://onionui.github.io
[rocknix]: https://rocknix.org
[spruce]: https://spruceui.github.io/
[trimui]: https://github.com/trimui

<!-- Badges -->

[docs-badge-img]: https://img.shields.io/badge/user%20guide-007C77?style=for-the-badge
Expand Down
2 changes: 2 additions & 0 deletions app/grout.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"grout/cfw"
"grout/update"
"os"

_ "github.com/BrandonKowalski/certifiable"
Expand Down Expand Up @@ -31,5 +32,6 @@ func cleanup() {
if err := os.RemoveAll(".tmp"); err != nil {
gaba.GetLogger().Error("Failed to clean .tmp directory", "error", err)
}
update.CleanupUpdateArtifacts()
gaba.Close()
}
8 changes: 4 additions & 4 deletions app/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func handlePlatformMappingUpdateUI(state *AppState, r ui.PlatformMappingOutput)
state.Config.PlatformOrder = internal.PrunePlatformOrder(state.Config.PlatformOrder, r.Mappings)
internal.SaveConfig(state.Config)

platforms, err := internal.GetMappedPlatforms(state.Host, r.Mappings, state.Config.ApiTimeout)
platforms, err := internal.GetMappedPlatforms(state.Host, r.Mappings, state.Config.ApiTimeout.Duration())
if err != nil {
gaba.GetLogger().Error("Failed to refresh platforms after mapping update", "error", err)
return
Expand Down Expand Up @@ -120,7 +120,7 @@ func handleLogout(state *AppState) {
screen := ui.NewPlatformMappingScreen()
result, err := screen.Draw(ui.PlatformMappingInput{
Host: state.Config.Hosts[0],
ApiTimeout: state.Config.ApiTimeout,
ApiTimeout: state.Config.ApiTimeout.Duration(),
CFW: state.CFW,
RomDirectory: cfw.GetRomDirectory(),
AutoSelect: false,
Expand All @@ -135,10 +135,10 @@ func handleLogout(state *AppState) {
}

if err := cache.InitCacheManager(state.Config.Hosts[0], state.Config); err != nil {
logger.Error("Failed to initialize cache manager after re-login", "error", err)
logger.Error("Failed to re-initialize cache manager", "error", err)
}

platforms, err := internal.GetMappedPlatforms(state.Config.Hosts[0], state.Config.DirectoryMappings, state.Config.ApiTimeout)
platforms, err := internal.GetMappedPlatforms(state.Config.Hosts[0], state.Config.DirectoryMappings, state.Config.ApiTimeout.Duration())
if err != nil {
logger.Error("Failed to load platforms after re-login", "error", err)
return
Expand Down
16 changes: 15 additions & 1 deletion app/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func registerScreens(r *router.Router, state *AppState) {
r.Register(ScreenArtworkSync, func(input any) (any, error) {
in := input.(ui.ArtworkSyncInput)
screen := ui.NewArtworkSyncScreen()
screen.Execute(in.Config, in.Host)
screen.Execute(in)
return ui.ArtworkSyncOutput{}, nil
})

Expand Down Expand Up @@ -228,6 +228,20 @@ func registerScreens(r *router.Router, state *AppState) {
return screen.Draw(input.(ui.SaveMappingInput))
})

r.Register(ScreenSwitchToToken, func(input any) (any, error) {
screen := ui.NewSwitchToTokenScreen()
result := screen.Execute(state.Config, state.Host)
if result.Success {
state.Host = result.Host
}
return result, nil
})

r.Register(ScreenToolsSettings, func(input any) (any, error) {
screen := ui.NewToolsSettingsScreen()
return screen.Draw(input.(ui.ToolsSettingsInput))
})

r.Register(ScreenServerAddress, func(input any) (any, error) {
screen := ui.NewServerAddressScreen()
return screen.Draw(input.(ui.ServerAddressInput))
Expand Down
2 changes: 2 additions & 0 deletions app/screens.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ const (
ScreenSyncHistory
ScreenSaveMapping
ScreenServerAddress
ScreenToolsSettings
ScreenSwitchToToken
)
101 changes: 92 additions & 9 deletions app/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"grout/cfw"
"grout/cfw/allium"
"grout/cfw/muos"
"grout/cfw/onion"
"grout/internal"
"grout/internal/environment"
"grout/internal/fileutil"
Expand Down Expand Up @@ -46,6 +47,8 @@ func setup() SetupResult {
mappingBytes, mappingErr = muos.GetInputMappingBytes()
case cfw.Allium:
mappingBytes, mappingErr = allium.GetInputMappingBytes()
case cfw.Onion:
mappingBytes, mappingErr = onion.GetInputMappingBytes()
}
if mappingBytes != nil && mappingErr == nil {
gaba.SetInputMappingBytes(mappingBytes)
Expand Down Expand Up @@ -117,8 +120,8 @@ func setup() SetupResult {
if config == nil {
config = &internal.Config{
ShowRegularCollections: true,
ApiTimeout: 30 * time.Minute,
DownloadTimeout: 60 * time.Minute,
ApiTimeout: internal.DurationSeconds(30 * time.Second),
DownloadTimeout: internal.DurationSeconds(60 * time.Minute),
}
}
config.Language = selectedLanguage
Expand All @@ -130,6 +133,7 @@ func setup() SetupResult {
loginConfig, loginErr := ui.LoginFlow(romm.Host{})
if loginErr != nil {
logger.Error("Login flow failed", "error", loginErr)
gaba.Close()
log.SetOutput(os.Stderr)
log.Fatalf("Login failed: %v", loginErr)
}
Expand Down Expand Up @@ -176,7 +180,7 @@ func setup() SetupResult {
screen := ui.NewPlatformMappingScreen()
result, err := screen.Draw(ui.PlatformMappingInput{
Host: config.Hosts[0],
ApiTimeout: config.ApiTimeout,
ApiTimeout: config.ApiTimeout.Duration(),
CFW: currentCFW,
RomDirectory: cfw.GetRomDirectory(),
AutoSelect: false,
Expand All @@ -198,32 +202,111 @@ func setup() SetupResult {
}

var platforms []romm.Platform
var loadErr error

splashBytes, _ := resources.GetSplashImageBytes()

for {
var connErr error
var authErr error
var loadErr error

gaba.ProcessMessage("", gaba.ProcessMessageOptions{
ImageBytes: splashBytes,
ImageWidth: 768,
ImageHeight: 540,
}, func() (interface{}, error) {
// Load platform bindings from RomM server (non-fatal if it fails)
if err := config.LoadPlatformsBinding(config.Hosts[0], config.ApiTimeout); err != nil {
host := config.Hosts[0]

// Step 1: Validate server connectivity
client := romm.NewClient(host.URL(), romm.WithInsecureSkipVerify(host.InsecureSkipVerify), romm.WithTimeout(internal.ValidationTimeout))
if err := client.ValidateConnection(); err != nil {
connErr = err
return nil, nil
}

// Step 2: Validate credentials/token
authClient := romm.NewClientFromHost(host, internal.LoginTimeout)
if host.HasTokenAuth() {
if err := authClient.ValidateToken(); err != nil {
authErr = err
return nil, nil
}
// Re-fetch username if missing
if host.Username == "" {
if user, err := authClient.GetCurrentUser(); err == nil {
host.Username = user.Username
config.Hosts[0] = host
internal.SaveConfig(config)
}
}
} else {
if err := authClient.Login(host.Username, host.Password); err != nil {
authErr = err
return nil, nil
}
}

// Step 3: Load platforms
if err := config.LoadPlatformsBinding(config.Hosts[0], config.ApiTimeout.Duration()); err != nil {
logger.Debug("Failed to load platform bindings", "error", err)
}

var err error
platforms, err = internal.GetMappedPlatforms(config.Hosts[0], config.DirectoryMappings, config.ApiTimeout)
platforms, err = internal.GetMappedPlatforms(config.Hosts[0], config.DirectoryMappings, config.ApiTimeout.Duration())
if err != nil {
loadErr = err
return nil, err
return nil, nil
}
loadErr = nil
platforms = internal.SortPlatformsByOrder(platforms, config.PlatformOrder)
return nil, nil
})

// Handle connectivity failure
if connErr != nil {
logger.Warn("Server connectivity failed", "error", connErr)
errorMessage := classifyStartupError(connErr)
errorMsg := i18n.Localize(errorMessage, nil)
retry := showStartupError(errorMsg)
if !retry {
gaba.Close()
os.Exit(1)
}
continue
}

// Handle auth failure — show message and offer re-login
if authErr != nil {
logger.Warn("Auth validation failed", "error", authErr)

var msg string
if config.Hosts[0].HasTokenAuth() {
msg = i18n.Localize(&goi18n.Message{ID: "startup_error_token_invalid", Other: "Your API token is invalid or expired.\nPlease set up a new one."}, nil)
} else {
msg = i18n.Localize(&goi18n.Message{ID: "startup_error_credentials_invalid", Other: "Your credentials are invalid.\nPlease log in again."}, nil)
}

gaba.ConfirmationMessage(msg, []gaba.FooterHelpItem{
{ButtonName: "A", HelpText: i18n.Localize(&goi18n.Message{ID: "button_continue", Other: "Continue"}, nil)},
}, gaba.MessageOptions{})

loginConfig, loginErr := ui.LoginFlow(config.Hosts[0])
if loginErr != nil {
logger.Error("Re-login failed", "error", loginErr)
gaba.Close()
log.SetOutput(os.Stderr)
log.Fatalf("Login failed: %v", loginErr)
}
config.Hosts = loginConfig.Hosts
config.PlatformsBinding = loginConfig.PlatformsBinding
internal.SaveConfig(config)

// Re-initialize cache manager with new credentials
if err := cache.InitCacheManager(config.Hosts[0], config); err != nil {
logger.Error("Failed to re-initialize cache manager", "error", err)
}
continue
}

if loadErr == nil {
break
}
Expand Down
39 changes: 38 additions & 1 deletion app/transitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ func buildTransitionFunc(state *AppState, quitOnBack bool, initialShowCollection
return transitionGeneralSettings(ctx, result)
case ScreenCollectionsSettings:
return transitionCollectionsSettings(ctx, result)
case ScreenToolsSettings:
return transitionToolsSettings(ctx, result)
case ScreenAdvancedSettings:
return transitionAdvancedSettings(ctx, result)
case ScreenPlatformMapping:
Expand All @@ -70,6 +72,14 @@ func buildTransitionFunc(state *AppState, quitOnBack bool, initialShowCollection
return popOrExit(stack)
case ScreenArtworkSync:
return popOrExit(stack)
case ScreenSwitchToToken:
// Pop the stacked settings entry so we don't go back to stale state
stack.Pop()
return ScreenSettings, ui.SettingsInput{
Config: ctx.state.Config,
CFW: ctx.state.CFW,
Host: ctx.state.Host,
}
case ScreenUpdateCheck:
return transitionUpdateCheck(ctx, result)
case ScreenGameFilters:
Expand Down Expand Up @@ -616,6 +626,14 @@ func transitionSettings(ctx *transitionContext, result any) (router.Screen, any)
ctx.stack.Push(ScreenSettings, pushInput, r)
return ScreenCollectionsSettings, ui.CollectionsSettingsInput{Config: ctx.state.Config}

case ui.SettingsActionSwitchToToken:
ctx.stack.Push(ScreenSettings, pushInput, r)
return ScreenSwitchToToken, nil

case ui.SettingsActionTools:
ctx.stack.Push(ScreenSettings, pushInput, r)
return ScreenToolsSettings, ui.ToolsSettingsInput{Config: ctx.state.Config, Host: ctx.state.Host}

case ui.SettingsActionAdvanced:
ctx.stack.Push(ScreenSettings, pushInput, r)
return ScreenAdvancedSettings, ui.AdvancedSettingsInput{Config: ctx.state.Config, Host: ctx.state.Host}
Expand All @@ -624,7 +642,7 @@ func transitionSettings(ctx *transitionContext, result any) (router.Screen, any)
ctx.stack.Push(ScreenSettings, pushInput, r)
return ScreenPlatformMapping, ui.PlatformMappingInput{
Host: ctx.state.Host,
ApiTimeout: ctx.state.Config.ApiTimeout,
ApiTimeout: ctx.state.Config.ApiTimeout.Duration(),
CFW: ctx.state.CFW,
RomDirectory: cfw.GetRomDirectory(),
ExistingMappings: ctx.state.Config.DirectoryMappings,
Expand Down Expand Up @@ -689,6 +707,25 @@ func transitionCollectionsSettings(ctx *transitionContext, result any) (router.S
return popOrExit(ctx.stack)
}

func transitionToolsSettings(ctx *transitionContext, result any) (router.Screen, any) {
r := result.(ui.ToolsSettingsOutput)

pushInput := ui.ToolsSettingsInput{Config: ctx.state.Config, Host: ctx.state.Host}

switch r.Action {
case ui.ToolsSettingsActionSyncLocalArtwork:
ctx.stack.Push(ScreenToolsSettings, pushInput, r)
return ScreenArtworkSync, ui.ArtworkSyncInput{
Config: *ctx.state.Config,
Host: ctx.state.Host,
DownloadedOnly: true,
}

default:
return popOrExit(ctx.stack)
}
}

func transitionAdvancedSettings(ctx *transitionContext, result any) (router.Screen, any) {
r := result.(ui.AdvancedSettingsOutput)

Expand Down
2 changes: 1 addition & 1 deletion cache/artwork.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func DownloadAndCacheArtwork(rom romm.Rom, kind artutil.ArtKind, host romm.Host)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", host.BasicAuthHeader())
req.Header.Set("Authorization", host.AuthHeader())

client := &http.Client{Timeout: romm.DefaultClientTimeout}
resp, err := client.Do(req)
Expand Down
Loading
Loading