/ ui / ui.go
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  }