159 lines
4.5 KiB
Go
159 lines
4.5 KiB
Go
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)
|
|
}
|
|
}
|