package main import ( "flag" "fmt" "log" "os" tea "github.com/charmbracelet/bubbletea" "skyrama-tui/internal/db" "skyrama-tui/internal/service" "skyrama-tui/internal/ui" ) // ─── Root model ─────────────────────────────────────────────────────────────── type screen int const ( screenLogin screen = iota screenApp ) type rootModel struct { client *service.GameService screen screen login ui.LoginModel app ui.AppModel winWidth int winHeight int } func newRootModel(client *service.GameService) rootModel { return rootModel{ client: client, screen: screenLogin, login: ui.NewLoginModel(client), } } func (r rootModel) Init() tea.Cmd { return r.login.Init() } func (r rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Always track window size so we can replay it into AppModel on login. if ws, ok := msg.(tea.WindowSizeMsg); ok { r.winWidth, r.winHeight = ws.Width, ws.Height } switch r.screen { // ── Login screen ────────────────────────────────────────────────────── case screenLogin: // Handle login result messages at this level so we can transition screens switch m := msg.(type) { case ui.LoginSuccessMsg: // Build the full AppModel and inject sub-models app := ui.NewAppModel(r.client, m.UserID, m.Username) fleet := ui.NewFleetModel(r.client, 0) app.Fleet(&fleet) buildings := ui.NewBuildingsModel(r.client, 0) app.SetBuildings(&buildings) contracts := ui.NewContractsModel(r.client, 0) app.SetContracts(&contracts) shop := ui.NewShopModel(r.client, 0, 1) app.SetShop(&shop) r.screen = screenApp r.app = app // Replay the window size into AppModel so list viewports are // sized correctly — the initial WindowSizeMsg arrived while // the login screen was active and was never forwarded to AppModel. var cmds []tea.Cmd cmds = append(cmds, r.app.Init()) if r.winWidth > 0 { updated, cmd := r.app.Update(tea.WindowSizeMsg{ Width: r.winWidth, Height: r.winHeight, }) r.app = updated.(ui.AppModel) if cmd != nil { cmds = append(cmds, cmd) } } return r, tea.Batch(cmds...) case ui.LoginErrMsg: // Inject the error then re-run update for cursor blink etc. r.login.SetErr(m.Err.Error()) updated, cmd := r.login.Update(msg) r.login = updated return r, cmd default: updated, cmd := r.login.Update(msg) r.login = updated return r, cmd } // ── Main app ────────────────────────────────────────────────────────── case screenApp: updated, cmd := r.app.Update(msg) r.app = updated.(ui.AppModel) return r, cmd } return r, nil } func (r rootModel) View() string { switch r.screen { case screenLogin: return r.login.View() case screenApp: return r.app.View() } return "" } // ─── Entry point ────────────────────────────────────────────────────────────── func main() { dsn := flag.String("dsn", "", "MySQL DSN string. Overrides SKYRAMA_DSN env var.\n"+ " Format: user:pass@tcp(host:port)/dbname?parseTime=true") flag.Parse() if *dsn == "" { *dsn = os.Getenv("SKYRAMA_DSN") } // Fall back to the same DSN used in db_manager.go if *dsn == "" { *dsn = "skyramaUser:Skyrama1234.@tcp(127.0.0.1:3306)/skyrama_clone?parseTime=true" } client, err := db.NewClient(*dsn) if err != nil { fmt.Fprintf(os.Stderr, "cannot connect to database: %v\n\n", err) fmt.Fprintln(os.Stderr, "Set the connection string via --dsn flag or the SKYRAMA_DSN environment variable.") fmt.Fprintln(os.Stderr, "Example:") fmt.Fprintln(os.Stderr, " SKYRAMA_DSN='skyramaUser:Skyrama1234.@tcp(127.0.0.1:3306)/skyrama_clone?parseTime=true' ./skyrama-tui") os.Exit(1) } // Wrap the data-access client in the business-logic service the UI depends on. // Central ports / factions are seeded out-of-band via sql/seed_factions.sql. svc := service.NewGameService(client) defer svc.Close() p := tea.NewProgram( newRootModel(svc), tea.WithAltScreen(), // use alternate screen buffer (hides shell history) tea.WithMouseCellMotion(), // mouse support for scroll ) if _, err := p.Run(); err != nil { log.Fatalf("fatal: %v", err) } }