A Go-based UI library for building graphical interfaces on retro gaming handhelds that support SDL2.
๐ฎ๐น (Chase, Grey, & HBO Home Entertainment, 1999โ2007) ๐ฎ๐น
- Router-based navigation - Type-safe screen transitions with explicit data flow
- Rich UI components - Lists, keyboards, dialogs, detail screens, and more
- Advanced input handling - Chord/sequence detection, configurable button mapping
- Thread-safe updates - Atomic operations for progress bars, status text, visibility
- Responsive design - Automatic scaling based on screen resolution
- Image support - PNG, JPEG, and SVG rendering with scaling
- Platform support - NextUI and Cannoli CFW theming integration
brew install sdl2 sdl2_image sdl2_ttf sdl2_gfxsudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-gfx-devgo get github.com/BrandonKowalski/gabagool/v2package main
import (
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
)
func main() {
gaba.Init(gaba.Options{
WindowTitle: "My App",
ShowBackground: true,
IsNextUI: true, // or IsCannoli: true
})
defer gaba.Close()
// Display a simple list
result, err := gaba.List(gaba.DefaultListOptions("Main Menu", []gaba.MenuItem{
{Text: "Option 1"},
{Text: "Option 2"},
{Text: "Option 3"},
}))
if err == gaba.ErrCancelled {
// User pressed back
return
}
// result.SelectedIndex contains the selected item
_ = result
}Scrollable list with multi-select, reordering, and customizable appearance.
Rich content display with slideshows, metadata sections, descriptions, and images.
Text input with multiple layouts (QWERTY, URL-optimized, numeric) and symbol modes.
Settings menu with toggles, text input, color pickers, and clickable items.
Dialog with customizable confirm/cancel buttons and optional imagery.
Multiple choice dialog with descriptions for each option.
Loading screen for async operations with optional progress bar.
Hexagonal grid color selector with 25 distinguishable colors.
Multi-threaded downloads with progress tracking and speed calculation.
Top-right display for clock, battery, WiFi, and custom icons.
Gabagool abstracts physical controller inputs into virtual buttons, allowing the same code to work across different devices.
Load a custom mapping from JSON:
// From embedded bytes
gaba.SetInputMappingBytes(mappingJSON)
// Or via environment variable
// INPUT_MAPPING_PATH=/path/to/mapping.jsonRegister chord (simultaneous) or sequence (ordered) button combinations:
import (
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
"github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/constants"
)
// Chord: buttons pressed simultaneously
gaba.RegisterChord("quick_menu", []constants.VirtualButton{
constants.VirtualButtonL1,
constants.VirtualButtonR1,
}, gaba.ChordOptions{
OnTrigger: func() {
// triggered when L1+R1 pressed together
},
})
// Sequence: buttons pressed in order (like Konami code)
gaba.RegisterSequence("secret", []constants.VirtualButton{
constants.VirtualButtonUp,
constants.VirtualButtonUp,
constants.VirtualButtonDown,
constants.VirtualButtonDown,
}, gaba.SequenceOptions{
OnTrigger: func() {
// triggered after sequence completes
},
})For apps with multiple screens, use the router package for type-safe navigation with explicit data flow:
import (
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
"github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/router"
)
// Define screen identifiers
const (
ScreenList router.Screen = iota
ScreenDetail
)
func main() {
gaba.Init(gaba.Options{WindowTitle: "My App", IsNextUI: true})
defer gaba.Close()
r := router.New()
// Register screen handlers
r.Register(ScreenList, func(input any) (any, error) {
return showList(input.(ListInput)), nil
})
r.Register(ScreenDetail, func(input any) (any, error) {
return showDetail(input.(DetailInput)), nil
})
// Define navigation transitions
r.OnTransition(func(from router.Screen, result any, stack *router.Stack) (router.Screen, any) {
switch from {
case ScreenList:
r := result.(ListResult)
if r.Action == ActionSelected {
stack.Push(from, input, r.Resume) // Save for back navigation
return ScreenDetail, DetailInput{Item: r.Selected}
}
case ScreenDetail:
// Pop returns to previous screen with resume state
entry := stack.Pop()
return entry.Screen, entry.Input
}
return router.ScreenExit, nil
})
r.Run(ScreenList, ListInput{})
}See the router package documentation for complete examples.
