skyrama_tui/cmd/skyrama/main.go
2026-06-04 20:51:20 +03:00

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)
}
}