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 }