prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

update.go (7258B)


      1 package tui
      2 
      3 import (
      4 	"time"
      5 
      6 	"github.com/charmbracelet/bubbles/key"
      7 	"github.com/charmbracelet/bubbles/viewport"
      8 	tea "github.com/charmbracelet/bubbletea"
      9 )
     10 
     11 func tickCmd() tea.Cmd {
     12 	return tea.Tick(time.Second, func(t time.Time) tea.Msg {
     13 		return tickMsg(t)
     14 	})
     15 }
     16 
     17 func (m portfolioModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
     18 	switch msg := msg.(type) {
     19 	case tea.WindowSizeMsg:
     20 		m.width = msg.Width
     21 		m.height = msg.Height
     22 		m.syncViewport()
     23 		return m, m.refreshDataCmd()
     24 	case tickMsg:
     25 		m.now = time.Time(msg)
     26 		return m, tea.Batch(tickCmd(), m.refreshDataCmd())
     27 	case bootTickMsg:
     28 		if !m.bootActive {
     29 			return m, nil
     30 		}
     31 		m.syncBootFrame(time.Time(msg))
     32 		if !m.bootActive {
     33 			return m, nil
     34 		}
     35 		return m, bootTickCmd()
     36 	case wakaFooterInitMsg:
     37 		return m, m.maybeLoadWakaFooterCmd()
     38 	case projectReadmeMsg:
     39 		m.projectReadmeLoads[msg.projectID] = false
     40 		cmd := tea.Cmd(nil)
     41 		if msg.err != nil {
     42 			m.projectReadmeErrors[msg.projectID] = msg.err.Error()
     43 		} else {
     44 			m.projectReadmes[msg.projectID] = msg.readme
     45 			delete(m.projectReadmeErrors, msg.projectID)
     46 			delete(m.projectReadmeRender, msg.projectID)
     47 			cmd = m.maybeRenderSelectedProjectMarkdownCmd()
     48 		}
     49 		if m.activeTab == tabProjects {
     50 			if selected := m.selectedProject(); selected != nil && selected.ID == msg.projectID {
     51 				m.syncViewport()
     52 			}
     53 		}
     54 		return m, cmd
     55 	case projectMarkdownRenderedMsg:
     56 		delete(m.projectMarkdownLoads, projectMarkdownLoadKey(msg.projectID, msg.width))
     57 		m.setCachedProjectMarkdown(msg.projectID, msg.width, msg.rendered)
     58 		if m.activeTab == tabProjects {
     59 			if selected := m.selectedProject(); selected != nil && selected.ID == msg.projectID {
     60 				m.syncViewport()
     61 			}
     62 		}
     63 		return m, nil
     64 	case blogMarkdownRenderedMsg:
     65 		delete(m.blogMarkdownLoads, blogMarkdownLoadKey(msg.blogID, msg.width))
     66 		m.setCachedBlogMarkdown(msg.blogID, msg.width, msg.rendered)
     67 		if m.activeTab == tabBlog && m.blogOpen {
     68 			if m.blogIndex >= 0 && m.blogIndex < len(m.blogs) && m.blogs[m.blogIndex].ID == msg.blogID {
     69 				m.syncViewport()
     70 			}
     71 		}
     72 		return m, nil
     73 	case aboutASCIIRenderedMsg:
     74 		m.applyAboutASCIIRendered(msg)
     75 		if m.activeTab == tabAbout {
     76 			m.syncViewport()
     77 		}
     78 		return m, nil
     79 	case aboutNameRenderedMsg:
     80 		m.applyAboutNameRendered(msg)
     81 		if m.activeTab == tabAbout {
     82 			m.syncViewport()
     83 		}
     84 		return m, nil
     85 	case wakaFooterMsg:
     86 		m.wakaLoading = false
     87 		m.wakaLoaded = true
     88 		m.wakaWeekly = msg.weekly
     89 		if msg.ok {
     90 			m.wakaHours = msg.hours
     91 		} else {
     92 			m.wakaHours = ""
     93 		}
     94 		if m.activeTab == tabAbout {
     95 			m.syncViewport()
     96 		}
     97 		return m, nil
     98 	case tea.KeyMsg:
     99 		return m, m.handleKeyMsg(msg)
    100 	}
    101 
    102 	return m, nil
    103 }
    104 
    105 func (m *portfolioModel) refreshDataCmd() tea.Cmd {
    106 	return tea.Batch(
    107 		m.maybeLoadProjectReadmeCmd(),
    108 		m.maybePrefetchProjectReadmesCmd(),
    109 		m.maybeRenderSelectedProjectMarkdownCmd(),
    110 		m.maybeRenderSelectedBlogCmd(),
    111 		m.maybeLoadWakaFooterCmd(),
    112 		m.maybeRenderAboutASCIICmd(),
    113 		m.maybeRenderAboutNameCmd(),
    114 	)
    115 }
    116 
    117 func (m *portfolioModel) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
    118 	if m.bootActive {
    119 		if key.Matches(msg, m.keys.Quit) {
    120 			return tea.Quit
    121 		}
    122 		return nil
    123 	}
    124 
    125 	if m.help.ShowAll {
    126 		switch {
    127 		case key.Matches(msg, m.keys.Quit):
    128 			return tea.Quit
    129 		case key.Matches(msg, m.keys.Help), key.Matches(msg, m.keys.Back):
    130 			m.help.ShowAll = false
    131 			return nil
    132 		default:
    133 			return nil
    134 		}
    135 	}
    136 
    137 	if m.consumeProjectReadmeBack(msg) {
    138 		return nil
    139 	}
    140 
    141 	if m.consumeBlogBack(msg) {
    142 		return nil
    143 	}
    144 
    145 	if tab, ok := tabIndexFromKey(msg.String()); ok {
    146 		return m.activateTab(tab)
    147 	}
    148 
    149 	switch {
    150 	case key.Matches(msg, m.keys.Quit):
    151 		return tea.Quit
    152 	case key.Matches(msg, m.keys.Help):
    153 		m.help.ShowAll = !m.help.ShowAll
    154 		return nil
    155 	case key.Matches(msg, m.keys.TabNext):
    156 		return m.activateTab(wrapIndex(m.activeTab+1, len(tabLabels)))
    157 	case key.Matches(msg, m.keys.TabPrev):
    158 		return m.activateTab(wrapIndex(m.activeTab-1, len(tabLabels)))
    159 	case key.Matches(msg, m.keys.Open):
    160 		return m.handleOpenKey()
    161 	case key.Matches(msg, m.keys.Back):
    162 		return m.handleBackKey()
    163 	case key.Matches(msg, m.keys.Up):
    164 		return tea.Batch(m.moveSelection(-1), m.refreshDataCmd())
    165 	case key.Matches(msg, m.keys.Down):
    166 		return tea.Batch(m.moveSelection(1), m.refreshDataCmd())
    167 	case key.Matches(msg, m.keys.PageUp):
    168 		if m.canPageScroll() {
    169 			m.viewport.HalfViewUp()
    170 		}
    171 		return nil
    172 	case key.Matches(msg, m.keys.PageDown):
    173 		if m.canPageScroll() {
    174 			m.viewport.HalfViewDown()
    175 		}
    176 		return nil
    177 	default:
    178 		return nil
    179 	}
    180 }
    181 
    182 func (m *portfolioModel) activateTab(tab int) tea.Cmd {
    183 	if tab < 0 || tab >= len(tabLabels) {
    184 		return nil
    185 	}
    186 
    187 	m.activeTab = tab
    188 	m.projectReadmeScroll = false
    189 	m.blogOpen = false
    190 	m.syncViewport()
    191 
    192 	return m.refreshDataCmd()
    193 }
    194 
    195 func (m *portfolioModel) consumeProjectReadmeBack(msg tea.KeyMsg) bool {
    196 	if m.activeTab != tabProjects || !m.projectReadmeScroll {
    197 		return false
    198 	}
    199 
    200 	if msg.String() != "left" && !key.Matches(msg, m.keys.Back) {
    201 		return false
    202 	}
    203 
    204 	m.projectReadmeScroll = false
    205 	m.syncViewport()
    206 	return true
    207 }
    208 
    209 func (m *portfolioModel) consumeBlogBack(msg tea.KeyMsg) bool {
    210 	if m.activeTab != tabBlog || !m.blogOpen {
    211 		return false
    212 	}
    213 
    214 	if msg.String() != "left" && !key.Matches(msg, m.keys.Back) {
    215 		return false
    216 	}
    217 
    218 	m.blogOpen = false
    219 	m.syncViewport()
    220 	return true
    221 }
    222 
    223 func (m *portfolioModel) handleOpenKey() tea.Cmd {
    224 	switch m.activeTab {
    225 	case tabProjects:
    226 		if len(m.projects) == 0 {
    227 			return nil
    228 		}
    229 		m.projectReadmeScroll = !m.projectReadmeScroll
    230 		return nil
    231 	case tabBlog:
    232 		m.blogOpen = !m.blogOpen
    233 		m.syncViewport()
    234 		return m.maybeRenderOpenBlogCmd()
    235 	default:
    236 		return nil
    237 	}
    238 }
    239 
    240 func (m *portfolioModel) handleBackKey() tea.Cmd {
    241 	if m.activeTab == tabProjects && m.projectReadmeScroll {
    242 		m.projectReadmeScroll = false
    243 		m.syncViewport()
    244 		return nil
    245 	}
    246 
    247 	if m.activeTab == tabBlog && m.blogOpen {
    248 		m.blogOpen = false
    249 		m.syncViewport()
    250 		return nil
    251 	}
    252 
    253 	return tea.Quit
    254 }
    255 
    256 func (m portfolioModel) canPageScroll() bool {
    257 	return m.activeTab != tabProjects || m.projectReadmeScroll
    258 }
    259 
    260 func tabIndexFromKey(keyString string) (int, bool) {
    261 	if len(keyString) != 1 {
    262 		return 0, false
    263 	}
    264 
    265 	if keyString[0] < '1' || keyString[0] > '9' {
    266 		return 0, false
    267 	}
    268 
    269 	idx := int(keyString[0] - '1')
    270 	if idx < 0 || idx >= len(tabLabels) {
    271 		return 0, false
    272 	}
    273 
    274 	return idx, true
    275 }
    276 
    277 func (m *portfolioModel) moveSelection(delta int) tea.Cmd {
    278 	if m.activeTab == tabAbout || (m.activeTab == tabBlog && m.blogOpen) || (m.activeTab == tabProjects && m.projectReadmeScroll) {
    279 		scrollViewport(&m.viewport, delta)
    280 		return nil
    281 	}
    282 
    283 	switch m.activeTab {
    284 	case tabProjects:
    285 		if len(m.projects) == 0 {
    286 			scrollViewport(&m.viewport, delta)
    287 			return nil
    288 		}
    289 		m.projectIndex = wrapIndex(m.projectIndex+delta, len(m.projects))
    290 		m.syncViewport()
    291 	case tabExperience:
    292 		if len(m.experience) == 0 {
    293 			scrollViewport(&m.viewport, delta)
    294 			return nil
    295 		}
    296 		m.experienceIndex = wrapIndex(m.experienceIndex+delta, len(m.experience))
    297 		m.syncViewport()
    298 	case tabBlog:
    299 		if len(m.blogs) == 0 {
    300 			scrollViewport(&m.viewport, delta)
    301 			return nil
    302 		}
    303 		m.blogIndex = wrapIndex(m.blogIndex+delta, len(m.blogs))
    304 		m.syncViewport()
    305 	}
    306 
    307 	return nil
    308 }
    309 
    310 func scrollViewport(vp *viewport.Model, delta int) {
    311 	if delta > 0 {
    312 		_ = vp.LineDown(delta)
    313 		return
    314 	}
    315 	_ = vp.LineUp(-delta)
    316 }