clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

feat/app-decomposition
puzzletea / app / screen_core.go
screen_core.go 8.1K
1package app23import (4 "time"56 "github.com/FelineStateMachine/puzzletea/registry"7 sessionflow "github.com/FelineStateMachine/puzzletea/session"8 "github.com/FelineStateMachine/puzzletea/store"9 "github.com/FelineStateMachine/puzzletea/theme"10 "github.com/FelineStateMachine/puzzletea/ui"1112 tea "charm.land/bubbletea/v2"13)1415type screenAction interface {16 applyToModel(m model) (model, tea.Cmd)17}1819type openPlayMenuAction struct{}2021func (a openPlayMenuAction) applyToModel(m model) (model, tea.Cmd) {22 m.nav.playMenu = ui.NewMainMenu(buildPlayMenuItems(time.Now(), m.currentWeeklyMenuIndex()))23 m.state = playMenuView24 m = m.clearNotice()25 return m.initScreen(playMenuView), nil26}2728type openStatsAction struct{}2930func (a openStatsAction) applyToModel(m model) (model, tea.Cmd) {31 return m.handleStatsEnter()32}3334type openOptionsMenuAction struct{}3536func (a openOptionsMenuAction) applyToModel(m model) (model, tea.Cmd) {37 m.nav.optionsMenu = ui.NewMainMenu(optionsMenuItems)38 m.state = optionsMenuView39 m = m.clearNotice()40 return m.initScreen(optionsMenuView), nil41}4243type quitAction struct{}4445func (a quitAction) applyToModel(m model) (model, tea.Cmd) {46 return m, tea.Quit47}4849type openGameSelectAction struct{}5051func (a openGameSelectAction) applyToModel(m model) (model, tea.Cmd) {52 m.state = gameSelectView53 m = m.updateCategoryDetailViewport()54 m = m.clearNotice()55 return m.initScreen(gameSelectView), nil56}5758type openContinueAction struct{}5960func (a openContinueAction) applyToModel(m model) (model, tea.Cmd) {61 m.cont.table, m.cont.games = ui.InitContinueTable(m.store, m.height)62 m.state = continueView63 m = m.clearNotice()64 return m.initScreen(continueView), nil65}6667type openDailyAction struct{}6869func (a openDailyAction) applyToModel(m model) (model, tea.Cmd) {70 return m.handleDailyPuzzle()71}7273type openExportAction struct{}7475func (a openExportAction) applyToModel(m model) (model, tea.Cmd) {76 return m.handleExportEnter()77}7879type openWeeklyAction struct{}8081func (a openWeeklyAction) applyToModel(m model) (model, tea.Cmd) {82 return m.enterWeeklyView()83}8485type openSeedInputAction struct{}8687func (a openSeedInputAction) applyToModel(m model) (model, tea.Cmd) {88 return m.enterSeedInputView()89}9091type backAction struct {92 target viewState93}9495func (a backAction) applyToModel(m model) (model, tea.Cmd) {96 if m.state == themeSelectView {97 previous := m.theme.previous98 if ts, ok := m.screens[themeSelectView].(themeSelectScreen); ok {99 previous = ts.theme.previous100 }101 _ = theme.Apply(previous)102 }103 m.state = a.target104 return m.resizeActiveScreen(), nil105}106107type gameSelectEnterAction struct {108 entry registry.Entry109}110111func (a gameSelectEnterAction) applyToModel(m model) (model, tea.Cmd) {112 m.nav.selectedCategory = a.entry113 m.nav.modeSelectList = ui.InitList(buildModeDisplayItems(a.entry), a.entry.Definition.Name+" - Select Mode")114 m.nav.modeSelectList.SetSize(min(m.width, 64), min(m.height, ui.ListHeight(m.nav.modeSelectList)))115 m.state = modeSelectView116 m = m.initScreen(modeSelectView)117 return m, nil118}119120type modeSelectEnterAction struct {121 entry registry.Entry122 mode registry.ModeEntry123}124125func (a modeSelectEnterAction) applyToModel(m model) (model, tea.Cmd) {126 m.nav.selectedCategory = a.entry127 m.nav.selectedModeTitle = a.mode.Definition.Title128 cmd := newSessionController(&m).startSpawn(a.mode.Spawner, spawnRequest{129 source: spawnSourceNormal,130 name: sessionflow.GenerateUniqueName(m.store),131 gameType: a.entry.Definition.Name,132 modeTitle: a.mode.Definition.Title,133 run: store.NormalRunMetadata(),134 returnState: modeSelectView,135 exitState: mainMenuView,136 })137 return m, cmd138}139140type continueEnterAction struct {141 record store.GameRecord142}143144func (a continueEnterAction) applyToModel(m model) (model, tea.Cmd) {145 m, _ = m.importAndActivateRecord(a.record)146 return m, nil147}148149type weeklyShiftAction struct {150 delta int151 weekly weeklyState152}153154func (a weeklyShiftAction) applyToModel(m model) (model, tea.Cmd) {155 m.weekly = a.weekly156 m = m.moveWeeklyWeek(a.delta)157 m = m.initScreen(weeklyView)158 return m, nil159}160161type weeklyEnterAction struct {162 weekly weeklyState163}164165func (a weeklyEnterAction) applyToModel(m model) (model, tea.Cmd) {166 m.weekly = a.weekly167 return m.handleWeeklyEnter()168}169170type helpSelectEnterAction struct {171 entry registry.Entry172}173174func (a helpSelectEnterAction) applyToModel(m model) (model, tea.Cmd) {175 m.help.category = a.entry176 return m.handleHelpSelectEnter()177}178179type openThemeSelectAction struct{}180181func (a openThemeSelectAction) applyToModel(m model) (model, tea.Cmd) {182 return m.handleThemeEnter()183}184185type openHelpSelectAction struct{}186187func (a openHelpSelectAction) applyToModel(m model) (model, tea.Cmd) {188 m.help.selectList = ui.InitList(gameCategoryItems, "How to Play")189 m.state = helpSelectView190 m = m.clearNotice()191 return m.initScreen(helpSelectView), nil192}193194type previewThemeAction struct {195 name string196}197198func (a previewThemeAction) applyToModel(m model) (model, tea.Cmd) {199 _ = theme.Apply(a.name)200 if ts, ok := m.screens[themeSelectView].(themeSelectScreen); ok {201 ui.UpdateThemeListStyles(&ts.theme.list)202 m.screens[themeSelectView] = ts203 }204 return m, nil205}206207type confirmThemeAction struct {208 theme themeState209}210211func (a confirmThemeAction) applyToModel(m model) (model, tea.Cmd) {212 m.theme = a.theme213 return m.handleThemeConfirm()214}215216type seedConfirmAction struct {217 seed seedState218}219220func (a seedConfirmAction) applyToModel(m model) (model, tea.Cmd) {221 m.seed = a.seed222 return m.handleSeedConfirm()223}224225// exportSubmitAction is a pure tea.Msg emitted by exportSubmitCmd.226// It is handled in the root Update() and does not implement screenAction.227type exportSubmitAction struct{}228229type screenModel interface {230 Resize(width, height int) screenModel231 Update(msg tea.Msg) (screenModel, tea.Cmd, screenAction)232 View(notice noticeState) string233}234235type screenFactory func(m model) screenModel236237var screenRegistry = map[viewState]screenFactory{238 mainMenuView: func(m model) screenModel {239 return mainMenuScreen{width: m.width, height: m.height, menu: m.nav.mainMenu}240 },241 playMenuView: func(m model) screenModel {242 return playMenuScreen{width: m.width, height: m.height, menu: m.nav.playMenu}243 },244 optionsMenuView: func(m model) screenModel {245 return optionsMenuScreen{width: m.width, height: m.height, menu: m.nav.optionsMenu}246 },247 seedInputView: func(m model) screenModel {248 return seedInputScreen{width: m.width, height: m.height, seed: m.seed}249 },250 gameSelectView: func(m model) screenModel {251 return gameSelectScreen{width: m.width, height: m.height, list: m.nav.gameSelectList, detail: m.nav.categoryDetail}252 },253 modeSelectView: func(m model) screenModel {254 return modeSelectScreen{width: m.width, height: m.height, entry: m.nav.selectedCategory, list: m.nav.modeSelectList}255 },256 continueView: func(m model) screenModel {257 return continueScreen{width: m.width, height: m.height, cont: m.cont}258 },259 weeklyView: func(m model) screenModel {260 return weeklyScreen{width: m.width, height: m.height, weekly: m.weekly}261 },262 helpSelectView: func(m model) screenModel {263 return helpSelectScreen{width: m.width, height: m.height, help: m.help}264 },265 helpDetailView: func(m model) screenModel {266 return helpDetailScreen{width: m.width, height: m.height, help: m.help}267 },268 statsView: func(m model) screenModel {269 return statsScreen{width: m.width, height: m.height, stats: m.stats}270 },271 themeSelectView: func(m model) screenModel {272 return themeSelectScreen{width: m.width, height: m.height, theme: m.theme}273 },274 generatingView: func(m model) screenModel {275 return generatingScreen{width: m.width, height: m.height, spinner: m.spinner}276 },277 exportRunningView: func(m model) screenModel {278 return exportRunningScreen{width: m.width, height: m.height, spinner: m.spinner}279 },280}281282func (m model) activeScreen() screenModel {283 return m.screens[m.state]284}285286// initScreen creates a fresh screen for the given state (using the registry287// factory) and stores it in m.screens. It also calls Resize so dimensions288// are set correctly.289func (m model) initScreen(state viewState) model {290 factory, ok := screenRegistry[state]291 if !ok {292 return m293 }294 if m.screens == nil {295 m.screens = make(map[viewState]screenModel)296 }297 m.screens[state] = factory(m).Resize(m.width, m.height)298 return m299}