ui.go
1 package ui 2 3 import ( 4 // "fmt" 5 6 "strings" 7 8 "github.com/mrusme/neonmodem/aggregator" 9 "github.com/mrusme/neonmodem/models/forum" 10 "github.com/mrusme/neonmodem/system" 11 "github.com/mrusme/neonmodem/ui/cmd" 12 "github.com/mrusme/neonmodem/ui/ctx" 13 "github.com/mrusme/neonmodem/ui/header" 14 "github.com/mrusme/neonmodem/ui/views/posts" 15 "github.com/mrusme/neonmodem/ui/views/splash" 16 "github.com/mrusme/neonmodem/ui/windowmanager" 17 "github.com/mrusme/neonmodem/ui/windows/msgerror" 18 "github.com/mrusme/neonmodem/ui/windows/popuplist" 19 "github.com/mrusme/neonmodem/ui/windows/postcreate" 20 "github.com/mrusme/neonmodem/ui/windows/postshow" 21 22 "github.com/mrusme/neonmodem/ui/views" 23 24 "github.com/charmbracelet/bubbles/key" 25 "github.com/charmbracelet/bubbles/list" 26 "github.com/charmbracelet/bubbles/spinner" 27 tea "github.com/charmbracelet/bubbletea" 28 ) 29 30 type KeyMap struct { 31 SystemSelect key.Binding 32 ForumSelect key.Binding 33 Close key.Binding 34 } 35 36 var DefaultKeyMap = KeyMap{ 37 SystemSelect: key.NewBinding( 38 key.WithKeys("ctrl+e"), 39 key.WithHelp("C-e", "System selector"), 40 ), 41 ForumSelect: key.NewBinding( 42 key.WithKeys("ctrl+t"), 43 key.WithHelp("C-t", "Forum selector"), 44 ), 45 Close: key.NewBinding( 46 key.WithKeys("esc"), 47 key.WithHelp("esc", "close"), 48 ), 49 } 50 51 type Model struct { 52 keymap KeyMap 53 header header.Model 54 views []views.View 55 currentView int 56 wm *windowmanager.WM 57 ctx *ctx.Ctx 58 59 a *aggregator.Aggregator 60 61 viewcache string 62 viewcacheID string 63 renderOnlyFocused bool 64 } 65 66 func NewModel(c *ctx.Ctx) Model { 67 m := Model{ 68 keymap: DefaultKeyMap, 69 currentView: 0, 70 wm: windowmanager.New(c), 71 ctx: c, 72 73 viewcache: "", 74 renderOnlyFocused: false, 75 } 76 77 m.header = header.NewModel(m.ctx) 78 m.views = append(m.views, splash.NewModel(m.ctx)) 79 m.views = append(m.views, posts.NewModel(m.ctx)) 80 81 m.a, _ = aggregator.New(m.ctx) 82 83 return m 84 } 85 86 func (m Model) Init() tea.Cmd { 87 return tea.Batch( 88 tea.EnterAltScreen, 89 ) 90 } 91 92 func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 93 cmds := make([]tea.Cmd, 0) 94 95 m.viewcacheID = m.wm.Focused() 96 97 switch msg := msg.(type) { 98 99 case tea.KeyMsg: 100 switch { 101 case key.Matches(msg, m.keymap.Close): 102 closed, ccmds := m.wm.CloseFocused() 103 if !closed { 104 break 105 // return m, tea.Quit 106 } 107 return m, tea.Batch(ccmds...) 108 case key.Matches(msg, m.keymap.SystemSelect): 109 var listItems []list.Item 110 111 all, _ := system.New("all", nil, m.ctx.Logger) 112 all.SetID(-1) 113 listItems = append(listItems, all) 114 115 for _, sys := range m.ctx.Systems { 116 listItems = append(listItems, *sys) 117 } 118 119 ccmds := m.wm.Open( 120 popuplist.WIN_ID, 121 popuplist.NewModel(m.ctx), 122 [4]int{ 123 int(m.ctx.Content[1] / 2), 124 int(m.ctx.Content[1] / 4), 125 int(m.ctx.Content[1] / 2), 126 int(m.ctx.Content[1] / 4), 127 }, 128 cmd.New( 129 cmd.WinOpen, 130 popuplist.WIN_ID, 131 cmd.Arg{Name: "selectionID", Value: "system"}, 132 cmd.Arg{Name: "items", Value: listItems}, 133 ), 134 ) 135 136 return m, tea.Batch(ccmds...) 137 138 case key.Matches(msg, m.keymap.ForumSelect): 139 var listItems []list.Item 140 ccmds := make([]tea.Cmd, 0) 141 142 all := forum.Forum{ID: "", Name: "All", SysIDX: m.ctx.GetCurrentSystem()} 143 listItems = append(listItems, all) 144 145 forums, errs := m.a.ListForums() 146 for _, err := range errs { 147 if err != nil { 148 m.ctx.Logger.Error(err) 149 ccmds = append(ccmds, cmd.New( 150 cmd.MsgError, 151 "*", 152 cmd.Arg{Name: "errors", Value: errs}, 153 ).Tea()) 154 } 155 } 156 157 for _, f := range forums { 158 listItems = append(listItems, f) 159 } 160 161 ccmds = m.wm.Open( 162 popuplist.WIN_ID, 163 popuplist.NewModel(m.ctx), 164 [4]int{ 165 int(m.ctx.Content[1] / 2), 166 int(m.ctx.Content[1] / 4), 167 int(m.ctx.Content[1] / 2), 168 int(m.ctx.Content[1] / 4), 169 }, 170 cmd.New( 171 cmd.WinOpen, 172 popuplist.WIN_ID, 173 cmd.Arg{Name: "selectionID", Value: "forum"}, 174 cmd.Arg{Name: "items", Value: listItems}, 175 ), 176 ) 177 178 return m, tea.Batch(ccmds...) 179 180 default: 181 if m.wm.GetNumberOpen() > 0 { 182 cmd := m.wm.Update(m.wm.Focused(), msg) 183 return m, cmd 184 } 185 } 186 187 case tea.WindowSizeMsg: 188 m.setSizes(msg.Width, msg.Height) 189 for i := range m.views { 190 v, cmd := m.views[i].Update(msg) 191 m.views[i] = v 192 cmds = append(cmds, cmd) 193 } 194 m.ctx.Logger.Debugf("resizing all: %v\n", m.ctx.Content) 195 ccmds := m.wm.ResizeAll(m.ctx.Content[0], m.ctx.Content[1]) 196 cmds = append(cmds, ccmds...) 197 198 case cmd.Command: 199 var ccmds []tea.Cmd 200 201 switch msg.Call { 202 203 case cmd.ViewOpen: 204 m.ctx.Logger.Debug("got cmd.ViewOpen") 205 switch msg.Target { 206 case posts.VIEW_ID: 207 m.currentView = 1 208 m.viewcache = m.buildView(false) 209 ccmds = append(ccmds, 210 cmd.New(cmd.ViewFocus, "*").Tea(), 211 cmd.New(cmd.ViewRefreshData, "*").Tea(), 212 ) 213 return m, tea.Batch(ccmds...) 214 } 215 216 case cmd.WinOpen: 217 switch msg.Target { 218 case postshow.WIN_ID: 219 ccmds = m.wm.Open( 220 msg.Target, 221 postshow.NewModel(m.ctx), 222 [4]int{ 223 3, 224 1, 225 6, 226 4, 227 }, 228 &msg, 229 ) 230 case postcreate.WIN_ID: 231 ccmds = m.wm.Open( 232 msg.Target, 233 postcreate.NewModel(m.ctx), 234 [4]int{ 235 6, 236 m.ctx.Content[1] - 16, 237 10, 238 4, 239 }, 240 &msg, 241 ) 242 m.viewcache = m.buildView(false) 243 } 244 245 case cmd.WinClose: 246 m.ctx.Logger.Debugf("got cmd.WinClose, target: %s", msg.Target) 247 248 switch msg.Target { 249 250 case postcreate.WIN_ID: 251 // TODO: Anything? 252 253 case popuplist.WIN_ID: 254 selectionIDIf := msg.GetArg("selectionID") 255 if selectionIDIf == nil { 256 return m, nil 257 } 258 switch selectionIDIf.(string) { 259 case "system": 260 selected := msg.GetArg("selected").(system.System) 261 m.ctx.SetCurrentSystem(selected.GetID()) 262 m.ctx.SetCurrentForum(forum.Forum{}) 263 case "forum": 264 selected := msg.GetArg("selected").(forum.Forum) 265 m.ctx.SetCurrentSystem(selected.SysIDX) 266 m.ctx.SetCurrentForum(selected) 267 } 268 return m, cmd.New(cmd.ViewRefreshData, "*").Tea() 269 270 } 271 272 case cmd.WMCloseWin: 273 if ok, clcmds := m.wm.Close(msg.Target, msg.GetArgs()...); ok { 274 cmds = append(cmds, clcmds...) 275 } 276 277 case cmd.MsgError: 278 ccmds = m.wm.Open( 279 msgerror.WIN_ID, 280 msgerror.NewModel(m.ctx), 281 [4]int{ 282 int(m.ctx.Content[1] / 2), 283 int(m.ctx.Content[1] / 4), 284 int(m.ctx.Content[1] / 2), 285 int(m.ctx.Content[1] / 4), 286 }, 287 &msg, 288 ) 289 290 default: 291 m.ctx.Logger.Debugf("updating all with cmd: %v\n", msg) 292 ccmds = m.wm.UpdateAll(msg) 293 } 294 295 cmds = append(cmds, ccmds...) 296 297 case spinner.TickMsg: 298 // Do nothing 299 300 default: 301 m.ctx.Logger.Debugf("updating focused with default: %v\n", msg) 302 cmds = append(cmds, m.wm.UpdateFocused(msg)...) 303 } 304 305 v, vcmd := m.views[m.currentView].Update(msg) 306 m.views[m.currentView] = v 307 cmds = append(cmds, vcmd) 308 309 header, hcmd := m.header.Update(msg) 310 m.header = header 311 cmds = append(cmds, hcmd) 312 313 return m, tea.Batch(cmds...) 314 } 315 316 func (m Model) View() string { 317 return m.buildView(true) 318 } 319 320 func (m Model) buildView(cached bool) string { 321 s := strings.Builder{} 322 var tmp string = "" 323 324 m.ctx.Logger.Debugf("viewcacheID: %s\n", m.viewcacheID) 325 if cached && m.viewcache != "" && m.viewcacheID == m.wm.Focused() && 326 m.viewcacheID == postcreate.WIN_ID { 327 m.ctx.Logger.Debug("hitting UI viewcache") 328 tmp = m.viewcache 329 m.renderOnlyFocused = true 330 } else { 331 m.ctx.Logger.Debug("generating UI viewcache") 332 m.renderOnlyFocused = false 333 if m.currentView > 0 { 334 s.WriteString(m.header.View() + "\n") 335 } 336 s.WriteString(m.views[m.currentView].View()) 337 tmp = s.String() 338 } 339 340 return m.wm.View(tmp, m.renderOnlyFocused) 341 } 342 343 func (m Model) setSizes(winWidth int, winHeight int) { 344 (*m.ctx).Screen[0] = winWidth 345 (*m.ctx).Screen[1] = winHeight 346 m.ctx.Content[0] = m.ctx.Screen[0] 347 m.ctx.Content[1] = m.ctx.Screen[1] - 8 348 }