746 lines
16 KiB
Go
746 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math/rand/v2"
|
|
"net"
|
|
"os"
|
|
"time"
|
|
|
|
"charm.land/bubbles/v2/key"
|
|
"charm.land/bubbles/v2/spinner"
|
|
"charm.land/bubbles/v2/textinput"
|
|
tea "charm.land/bubbletea/v2"
|
|
"charm.land/lipgloss/v2"
|
|
"charm.land/ssh"
|
|
"charm.land/wish/v2"
|
|
"charm.land/wish/v2/activeterm"
|
|
"charm.land/wish/v2/bubbletea"
|
|
"charm.land/wish/v2/logging"
|
|
|
|
gossh "golang.org/x/crypto/ssh"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/joho/godotenv"
|
|
"golang.org/x/sync/errgroup"
|
|
"jp.wtf/go-fishing/internal/db"
|
|
"jp.wtf/go-fishing/internal/handlers"
|
|
"jp.wtf/go-fishing/internal/migrations"
|
|
"jp.wtf/go-fishing/internal/models"
|
|
|
|
// "charm.land/log/v2"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/pressly/goose/v3"
|
|
)
|
|
|
|
const (
|
|
ssh_host = "0.0.0.0"
|
|
ssh_port = "22"
|
|
api_port = "9900"
|
|
)
|
|
|
|
func VersionMiddleware(version string) gin.HandlerFunc {
|
|
return func(c *gin.Context) { c.Set("APIVersion", version) }
|
|
}
|
|
|
|
func main() {
|
|
err := godotenv.Load()
|
|
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
log.Fatal("Failed to load .env: ", err.Error())
|
|
}
|
|
|
|
g, ctx := errgroup.WithContext(context.Background())
|
|
|
|
ENVIRONMENT := os.Getenv("ENV")
|
|
|
|
DB_CONNECTION_STRING, db_conn_string_exists := os.LookupEnv("DB_CONNECTION_STRING")
|
|
COOKIE_SECRET, secret_env_exists := os.LookupEnv("COOKIE_STORE_SECRET")
|
|
|
|
if !db_conn_string_exists {
|
|
log.Fatal("DB_CONNECTION_STRING env var is required")
|
|
}
|
|
|
|
if !secret_env_exists {
|
|
log.Fatal("COOKIE_STORE_SECRET env var is required")
|
|
}
|
|
|
|
pool, err := pgxpool.New(ctx, DB_CONNECTION_STRING)
|
|
|
|
if err != nil {
|
|
log.Fatal("Failed to connect to DB: ", err.Error())
|
|
}
|
|
|
|
defer pool.Close()
|
|
|
|
database := sqlx.NewDb(stdlib.OpenDBFromPool(pool), "pgx")
|
|
|
|
if err := database.Ping(); err != nil {
|
|
log.Fatal("Database is not reachable: ", err)
|
|
}
|
|
|
|
db := db.Database{Conn: database}
|
|
|
|
goose.SetBaseFS(migrations.Files)
|
|
|
|
if err := goose.SetDialect("postgres"); err != nil {
|
|
log.Fatal("Failed to set goos dialect: ", err)
|
|
}
|
|
if err := goose.Up(database.DB, "."); err != nil {
|
|
log.Fatal("Failed to execute migrations: ", err)
|
|
}
|
|
|
|
g.Go(func() error {
|
|
if ENVIRONMENT == "PRODUCTION" {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
|
|
r := gin.Default()
|
|
|
|
if ENVIRONMENT == "PRODUCTION" {
|
|
r.TrustedPlatform = gin.PlatformFlyIO
|
|
} else {
|
|
r.SetTrustedProxies(nil)
|
|
}
|
|
|
|
store := cookie.NewStore([]byte(COOKIE_SECRET))
|
|
r.Use(sessions.Sessions("auth", store))
|
|
|
|
v1 := r.Group("/api/v1")
|
|
|
|
{
|
|
v1.Use(VersionMiddleware("v1"))
|
|
handlers := handlers.Env{DB: &db}
|
|
handlers.RegisterRoutes(v1)
|
|
}
|
|
|
|
return r.Run(fmt.Sprintf(":%s", api_port))
|
|
})
|
|
|
|
srv, err := wish.NewServer(
|
|
// The address the server will listen to.
|
|
wish.WithAddress(net.JoinHostPort(ssh_host, ssh_port)),
|
|
|
|
// The SSH server need its own keys, this will create a keypair in the
|
|
// given path if it doesn't exist yet.
|
|
// By default, it will create an ED25519 key.
|
|
wish.WithHostKeyPath("/data/.ssh/ssh_host_ed25519_key"),
|
|
|
|
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
|
angler, err := db.GetAnglerByKey(key)
|
|
|
|
if err != nil {
|
|
if !errors.Is(sql.ErrNoRows, err) {
|
|
return false
|
|
}
|
|
} else {
|
|
ctx.SetValue("angler", angler)
|
|
}
|
|
|
|
return key.Type() == "ssh-ed25519"
|
|
}),
|
|
|
|
// Middlewares do something on a ssh.Session, and then call the next
|
|
// middleware in the stack.
|
|
wish.WithMiddleware(
|
|
bubbletea.Middleware(teaHandler(&db)),
|
|
activeterm.Middleware(), // Bubble Tea apps usually require a PTY.
|
|
func(next ssh.Handler) ssh.Handler {
|
|
return func(sess ssh.Session) {
|
|
raw := sess.Context().Value("angler")
|
|
|
|
if raw != nil {
|
|
next(sess)
|
|
} else {
|
|
angler, err := db.CreateNewAnglerFromKey(sess.PublicKey())
|
|
|
|
if err != nil {
|
|
// TODO: hangle error
|
|
println("err", err.Error())
|
|
}
|
|
|
|
sess.Context().SetValue("angler", angler)
|
|
|
|
next(sess)
|
|
}
|
|
}
|
|
},
|
|
// The last item in the chain is the first to be called.
|
|
logging.Middleware(),
|
|
),
|
|
)
|
|
|
|
if err != nil {
|
|
log.Fatal("Could not start server", "error", err)
|
|
}
|
|
|
|
log.Print("Starting SSH server @", ssh_host, ":", ssh_port)
|
|
|
|
g.Go(func() error {
|
|
return srv.ListenAndServe()
|
|
})
|
|
|
|
g.Go(func() error {
|
|
<-ctx.Done()
|
|
|
|
// one of the servers failed,
|
|
// or SIGINT/SIGTERM happened
|
|
|
|
return nil
|
|
})
|
|
|
|
g.Wait()
|
|
}
|
|
|
|
type page int
|
|
|
|
const (
|
|
PageChooseName page = iota
|
|
PageGameplay
|
|
)
|
|
|
|
type playerState int
|
|
|
|
const (
|
|
PlayerIdle playerState = iota
|
|
PlayerCasting
|
|
PlayerCaught
|
|
PlayerFailed
|
|
)
|
|
|
|
type tab int
|
|
|
|
const (
|
|
TabFishin tab = iota
|
|
TabWares
|
|
TabDock
|
|
TabSettings
|
|
)
|
|
|
|
type model struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
profile string
|
|
term string
|
|
width int
|
|
height int
|
|
bg string
|
|
DB *db.Database
|
|
page page
|
|
angler models.Angler
|
|
tabs []string
|
|
tab tab
|
|
error string
|
|
ti textinput.Model
|
|
spinner spinner.Model
|
|
active tea.Model
|
|
cast models.Cast
|
|
playerState playerState
|
|
message string
|
|
}
|
|
|
|
func (m model) Init() tea.Cmd {
|
|
var cmd tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
|
// Pages that need initialization should be called here if we can start on them
|
|
switch m.page {
|
|
case PageGameplay:
|
|
m, cmd = m.gotoGameplayView()
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
|
|
// default values
|
|
return tea.Batch(
|
|
append(cmds, tea.RequestBackgroundColor)...,
|
|
)
|
|
}
|
|
|
|
const chooseNamePrompt = "aye fresh meat, got a name laddie?"
|
|
|
|
func (m model) ChooseNameView() tea.View {
|
|
var error string
|
|
|
|
if len(m.error) > 0 {
|
|
error = lipgloss.NewStyle().Foreground(lipgloss.Red).Render(m.error)
|
|
}
|
|
|
|
// m.ti.SetWidth(lipgloss.Width(chooseNamePrompt))
|
|
|
|
input := lipgloss.JoinVertical(lipgloss.Top, chooseNamePrompt, m.ti.View(), error)
|
|
|
|
center := lipgloss.Place(
|
|
m.width,
|
|
m.height-3,
|
|
lipgloss.Center,
|
|
lipgloss.Center,
|
|
input,
|
|
)
|
|
|
|
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("(esc to quit)")
|
|
|
|
bottom := lipgloss.Place(
|
|
m.width,
|
|
3,
|
|
lipgloss.Center,
|
|
lipgloss.Center,
|
|
footer,
|
|
)
|
|
|
|
str := lipgloss.JoinVertical(
|
|
lipgloss.Left,
|
|
center,
|
|
bottom,
|
|
)
|
|
|
|
// var c *tea.Cursor
|
|
|
|
// c = m.ti.Cursor()
|
|
|
|
// c.Y += lipgloss.Height(prompt) + (m.height-3-lipgloss.Height(input))/2
|
|
// c.X += (m.width - lipgloss.Width(input)) / 2
|
|
|
|
v := tea.NewView(str)
|
|
// v.Cursor = c
|
|
v.AltScreen = true
|
|
return v
|
|
}
|
|
|
|
func (m model) ChooseNameUpdate(msg tea.Msg) (model, tea.Cmd) {
|
|
var cmd tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
|
switch msg := msg.(type) {
|
|
case tea.KeyPressMsg:
|
|
m.error = ""
|
|
switch msg.String() {
|
|
case "enter":
|
|
existing := 0
|
|
desired_username := m.ti.Value()
|
|
err := m.DB.Conn.Get(&existing, "SELECT 1 FROM anglers WHERE angler_username = $1", desired_username)
|
|
|
|
if err != nil {
|
|
// TODO: handle error
|
|
}
|
|
|
|
if existing > 0 {
|
|
m.error = "that one is taken"
|
|
} else {
|
|
err = m.DB.Conn.Get(
|
|
&m.angler,
|
|
"UPDATE anglers SET angler_username = $1 WHERE angler_id = $2 RETURNING angler_id, angler_username, angler_email",
|
|
desired_username, m.angler.ID,
|
|
)
|
|
|
|
if err != nil {
|
|
// TODO: handle error
|
|
}
|
|
|
|
m, cmd = m.gotoGameplayView()
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
}
|
|
}
|
|
|
|
m.ti.Placeholder = m.angler.Username
|
|
m.ti, cmd = m.ti.Update(msg)
|
|
cmds = append(cmds, cmd)
|
|
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
|
|
var buttonStyle = lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center).Background(lipgloss.Color("8"))
|
|
var buttonActiveStyle = buttonStyle.Background(lipgloss.Color("30"))
|
|
|
|
var keybindKeyStyle = lipgloss.NewStyle()
|
|
var keybindActionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
|
|
|
var sidebarBtnStyle = lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center).Foreground(lipgloss.Color("30"))
|
|
var sidebarBtnActiveStyle = sidebarBtnStyle.UnsetForeground().Background(lipgloss.Color("30"))
|
|
|
|
func (m model) GameplayFishingView(width int, height int) string {
|
|
help := keybindKeyStyle.Render("c") + keybindActionStyle.Render(" cast") + "\t" + keybindKeyStyle.Render("x") + keybindActionStyle.Render(" cancel")
|
|
|
|
switch m.playerState {
|
|
case PlayerIdle:
|
|
return lipgloss.JoinVertical(
|
|
lipgloss.Left,
|
|
lipgloss.NewStyle().MarginBottom(1).Render(fmt.Sprintf("Aye matie %s...time to get fishin'", m.angler.Username)),
|
|
help,
|
|
)
|
|
case PlayerCasting:
|
|
return lipgloss.JoinVertical(
|
|
lipgloss.Left,
|
|
lipgloss.NewStyle().MarginBottom(1).Render(m.spinner.View()+" waitin' for a bite"),
|
|
help,
|
|
)
|
|
case PlayerCaught:
|
|
return lipgloss.JoinVertical(
|
|
lipgloss.Left,
|
|
lipgloss.NewStyle().MarginBottom(1).Render(m.message),
|
|
help,
|
|
)
|
|
case PlayerFailed:
|
|
return lipgloss.JoinVertical(
|
|
lipgloss.Left,
|
|
lipgloss.NewStyle().MarginBottom(1).Foreground(lipgloss.Red).Render(m.message),
|
|
help,
|
|
)
|
|
}
|
|
|
|
return "bad player state"
|
|
}
|
|
|
|
func (m model) GameplayView() tea.View {
|
|
|
|
var renderedTabs []string
|
|
|
|
for i, tab := range m.tabs {
|
|
if i == int(m.tab) {
|
|
renderedTabs = append(renderedTabs, sidebarBtnActiveStyle.Render(tab))
|
|
} else {
|
|
renderedTabs = append(renderedTabs, sidebarBtnStyle.Render(tab))
|
|
}
|
|
}
|
|
|
|
tabs := lipgloss.NewStyle().Margin(0, 1).Render(lipgloss.JoinHorizontal(
|
|
lipgloss.Top,
|
|
renderedTabs...,
|
|
))
|
|
|
|
paddingY, paddingX := 1, 2
|
|
|
|
maxWidth := min(m.width, 80)
|
|
maxHeight := min(m.height, 80)
|
|
|
|
var content string
|
|
|
|
if m.tab == TabFishin {
|
|
content = m.GameplayFishingView(maxWidth, maxHeight)
|
|
}
|
|
|
|
wrapped := lipgloss.NewStyle().
|
|
Width(maxWidth-paddingX*2).
|
|
Height(maxHeight-2-paddingY*2).
|
|
BorderStyle(lipgloss.NormalBorder()).
|
|
BorderForeground(lipgloss.Color("30")).
|
|
Padding(paddingY, paddingX).
|
|
Render(content)
|
|
|
|
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("(esc to quit)")
|
|
|
|
box := lipgloss.NewStyle().
|
|
Width(maxWidth).
|
|
Height(maxHeight).
|
|
Padding(paddingY, paddingX).
|
|
Render(lipgloss.JoinVertical(
|
|
lipgloss.Left,
|
|
tabs,
|
|
wrapped,
|
|
footer,
|
|
))
|
|
|
|
page := lipgloss.Place(
|
|
m.width,
|
|
m.height-1,
|
|
lipgloss.Center,
|
|
lipgloss.Center,
|
|
box,
|
|
)
|
|
|
|
// bottom := lipgloss.Place(
|
|
// m.width,
|
|
// 3,
|
|
// lipgloss.Center,
|
|
// lipgloss.Center,
|
|
// footer,
|
|
// )
|
|
|
|
v := tea.NewView(page)
|
|
v.AltScreen = true
|
|
return v
|
|
}
|
|
|
|
type GameplayKeyMap struct {
|
|
CycleTab key.Binding
|
|
AltCycleTab key.Binding
|
|
Cast key.Binding
|
|
Cancel key.Binding
|
|
Interact key.Binding
|
|
}
|
|
|
|
var DefaultGameplayKeyMap = GameplayKeyMap{
|
|
CycleTab: key.NewBinding(
|
|
key.WithKeys("tab"),
|
|
key.WithHelp("tab", "Cycle Tab Right"),
|
|
),
|
|
AltCycleTab: key.NewBinding(
|
|
key.WithKeys("shift+tab"),
|
|
key.WithHelp("shift+tab", "Cycle Tab Left"),
|
|
),
|
|
Cast: key.NewBinding(
|
|
key.WithKeys("c"),
|
|
key.WithHelp("c", "Cast"),
|
|
),
|
|
Cancel: key.NewBinding(
|
|
key.WithKeys("x"),
|
|
key.WithHelp("x", "Cancel action."),
|
|
),
|
|
Interact: key.NewBinding(
|
|
key.WithKeys("enter"),
|
|
key.WithHelp("enter", "Interact with selected option"),
|
|
),
|
|
}
|
|
|
|
var sayings = []string{
|
|
// Near Misses
|
|
"Close but no cigar",
|
|
"Almost only counts in horseshoes and hand grenades",
|
|
"A day late and a dollar short",
|
|
"A miss is as good as a mile",
|
|
"Always a bridesmaid, never a bride",
|
|
"Snatched defeat from the jaws of victory",
|
|
"Fell at the last hurdle",
|
|
"Missed the boat",
|
|
"Missed it by a whisker",
|
|
"So near, and yet so far",
|
|
"A swing and a miss",
|
|
|
|
// Futile Efforts
|
|
"Beating a dead horse",
|
|
"Tilting at windmills",
|
|
"Squeezing blood from a stone",
|
|
"Nailing jelly to a wall",
|
|
"Herding cats",
|
|
"Rearranging deck chairs on the Titanic",
|
|
"Pushing a rope",
|
|
"Spitting in the wind",
|
|
"Putting toothpaste back in the tube",
|
|
"Shoveling snow in a blizzard",
|
|
"Spinning your wheels",
|
|
"Emptying the ocean with a teaspoon",
|
|
"Barking up the wrong tree",
|
|
"A wild goose chase",
|
|
"Catching the wind in a net",
|
|
}
|
|
|
|
type castFinishedMsg struct {
|
|
caught bool
|
|
err error
|
|
}
|
|
|
|
func waitForCast(
|
|
ctx context.Context,
|
|
wait time.Duration,
|
|
) tea.Cmd {
|
|
return func() tea.Msg {
|
|
timer := time.NewTimer(wait)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case <-timer.C:
|
|
caught := rand.IntN(100) < 75
|
|
|
|
return castFinishedMsg{
|
|
caught: caught,
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
return castFinishedMsg{
|
|
err: ctx.Err(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m model) GameplayUpdate(msg tea.Msg) (model, tea.Cmd) {
|
|
var cmd tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
|
switch msg := msg.(type) {
|
|
case spinner.TickMsg:
|
|
m.spinner, cmd = m.spinner.Update(msg)
|
|
cmds = append(cmds, cmd)
|
|
case tea.KeyPressMsg:
|
|
switch {
|
|
case key.Matches(msg, DefaultGameplayKeyMap.CycleTab):
|
|
if int(m.tab) >= len(m.tabs)-1 {
|
|
m.tab = 0
|
|
} else {
|
|
m.tab += 1
|
|
}
|
|
case key.Matches(msg, DefaultGameplayKeyMap.AltCycleTab):
|
|
if int(m.tab) <= 0 {
|
|
m.tab = tab(len(m.tabs) - 1)
|
|
} else {
|
|
m.tab -= 1
|
|
}
|
|
case key.Matches(msg, DefaultGameplayKeyMap.Cast):
|
|
m.playerState = PlayerCasting
|
|
|
|
// if m.active = {
|
|
is_casting, err := m.DB.IsCasting(m.angler.ID)
|
|
|
|
if is_casting {
|
|
m.playerState = PlayerIdle
|
|
m.error = "looks like you are already casting somewhere"
|
|
// TODO: clear after a timeout
|
|
} else {
|
|
m.cast, err = m.DB.CreateNewCast(m.angler.ID)
|
|
|
|
if err != nil {
|
|
m.playerState = PlayerIdle
|
|
m.error = "unable to start your cast"
|
|
log.Print(err)
|
|
} else {
|
|
// Fishing can take anywhere from 2-5 seconds
|
|
minTime, maxTime := 2, 5
|
|
timeToWait := time.Duration(rand.IntN(maxTime-minTime+1)+minTime) * time.Second
|
|
|
|
cmds = append(cmds, waitForCast(m.ctx, timeToWait), m.spinner.Tick)
|
|
}
|
|
}
|
|
}
|
|
case castFinishedMsg:
|
|
_, err := m.DB.EndCastByID(m.cast.ID)
|
|
|
|
if err != nil {
|
|
m.error = "unable to end your cast"
|
|
}
|
|
|
|
if msg.err != nil {
|
|
m.error = msg.err.Error()
|
|
return m, nil
|
|
}
|
|
|
|
if msg.caught {
|
|
m.playerState = PlayerCaught
|
|
m.message = "Got one! Nice!"
|
|
} else {
|
|
m.playerState = PlayerFailed
|
|
m.message = sayings[rand.IntN(len(sayings))]
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
cmds = append(cmds, cmd)
|
|
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (m model) gotoGameplayView() (model, tea.Cmd) {
|
|
m.page = PageGameplay
|
|
return m, nil
|
|
}
|
|
|
|
type GlobalKeyMap struct {
|
|
Close key.Binding
|
|
}
|
|
|
|
var DefaultGlobalKeyMap = GlobalKeyMap{
|
|
Close: key.NewBinding(
|
|
key.WithKeys("esc", "ctrl+c"),
|
|
key.WithHelp("esc", "Quit"),
|
|
),
|
|
}
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
var cmd tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
|
switch msg := msg.(type) {
|
|
case tea.ColorProfileMsg:
|
|
m.profile = msg.String()
|
|
case tea.BackgroundColorMsg:
|
|
if msg.IsDark() {
|
|
m.bg = "dark"
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
m.height = msg.Height
|
|
m.width = msg.Width
|
|
case tea.KeyPressMsg:
|
|
switch {
|
|
case key.Matches(msg, DefaultGlobalKeyMap.Close):
|
|
return m, tea.Quit
|
|
}
|
|
}
|
|
|
|
switch m.page {
|
|
case PageChooseName:
|
|
m, cmd = m.ChooseNameUpdate(msg)
|
|
cmds = append(cmds, cmd)
|
|
case PageGameplay:
|
|
m, cmd = m.GameplayUpdate(msg)
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
|
|
cmds = append(cmds, cmd)
|
|
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (m model) View() tea.View {
|
|
switch m.page {
|
|
case PageChooseName:
|
|
return m.ChooseNameView()
|
|
case PageGameplay:
|
|
return m.GameplayView()
|
|
}
|
|
|
|
return tea.NewView("")
|
|
}
|
|
|
|
func teaHandler(DB *db.Database) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
|
|
return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
|
pty, _, _ := s.Pty()
|
|
angler, _ := s.Context().Value("angler").(models.Angler)
|
|
ctx, cancel := context.WithCancel(s.Context())
|
|
|
|
m := model{
|
|
// using ssh context for now
|
|
// TODO: may make sens to use a custom one
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
term: pty.Term,
|
|
width: pty.Window.Width,
|
|
height: pty.Window.Height,
|
|
bg: "light",
|
|
DB: DB,
|
|
page: PageChooseName,
|
|
angler: angler,
|
|
tabs: []string{"fishin'", "wares", "dock", "settings"},
|
|
tab: TabFishin,
|
|
ti: textinput.New(),
|
|
spinner: spinner.New(),
|
|
playerState: PlayerIdle,
|
|
}
|
|
|
|
if angler.Username == gossh.FingerprintSHA256(s.PublicKey()) {
|
|
m.page = PageChooseName
|
|
} else {
|
|
m.page = PageGameplay
|
|
}
|
|
|
|
m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("30"))
|
|
m.spinner.Spinner = spinner.Jump
|
|
|
|
// m.ti.SetVirtualCursor(false)
|
|
m.ti.SetWidth(lipgloss.Width(chooseNamePrompt) - 3)
|
|
m.ti.Focus()
|
|
m.ti.CharLimit = 156
|
|
|
|
return m, []tea.ProgramOption{}
|
|
}
|
|
}
|