]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
Minor cleanup
[rbdr/mobius] / hotline / ui.go
1 package hotline
2
3 import (
4 "errors"
5 "fmt"
6 "github.com/davecgh/go-spew/spew"
7 "github.com/gdamore/tcell/v2"
8 "github.com/rivo/tview"
9 "gopkg.in/yaml.v2"
10 "io"
11 "io/ioutil"
12 "os"
13 "strconv"
14 "strings"
15 )
16
17 type UI struct {
18 chatBox *tview.TextView
19 chatInput *tview.InputField
20 App *tview.Application
21 Pages *tview.Pages
22 userList *tview.TextView
23 agreeModal *tview.Modal
24 trackerList *tview.List
25 settingsPage *tview.Box
26 HLClient *Client
27 }
28
29 // pages
30 const (
31 pageServerUI = "serverUI"
32 )
33
34 func NewUI(c *Client) *UI {
35 app := tview.NewApplication()
36 chatBox := tview.NewTextView().
37 SetScrollable(true).
38 SetDynamicColors(true).
39 SetWordWrap(true).
40 SetChangedFunc(func() {
41 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
42 })
43 chatBox.Box.SetBorder(true).SetTitle("Chat")
44
45 chatInput := tview.NewInputField()
46 chatInput.
47 SetLabel("> ").
48 SetFieldBackgroundColor(tcell.ColorDimGray).
49 SetDoneFunc(func(key tcell.Key) {
50 // skip send if user hit enter with no other text
51 if len(chatInput.GetText()) == 0 {
52 return
53 }
54
55 _ = c.Send(
56 *NewTransaction(tranChatSend, nil,
57 NewField(fieldData, []byte(chatInput.GetText())),
58 ),
59 )
60 chatInput.SetText("") // clear the input field after chat send
61 })
62
63 chatInput.Box.SetBorder(true).SetTitle("Send")
64
65 userList := tview.
66 NewTextView().
67 SetDynamicColors(true).
68 SetChangedFunc(func() {
69 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
70 })
71 userList.Box.SetBorder(true).SetTitle("Users")
72
73 return &UI{
74 App: app,
75 chatBox: chatBox,
76 Pages: tview.NewPages(),
77 chatInput: chatInput,
78 userList: userList,
79 trackerList: tview.NewList(),
80 agreeModal: tview.NewModal(),
81 HLClient: c,
82 }
83 }
84
85 func (ui *UI) showBookmarks() *tview.List {
86 list := tview.NewList()
87 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
88 if event.Key() == tcell.KeyEsc {
89 ui.Pages.SwitchToPage("home")
90 }
91 return event
92 })
93 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
94
95 shortcut := 97 // rune for "a"
96 for i, srv := range ui.HLClient.pref.Bookmarks {
97 addr := srv.Addr
98 login := srv.Login
99 pass := srv.Password
100 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
101 ui.Pages.RemovePage("joinServer")
102
103 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
104
105 ui.Pages.AddPage("joinServer", newJS, true, true)
106 })
107 }
108
109 return list
110 }
111
112 func (ui *UI) getTrackerList() *tview.List {
113 listing, err := GetListing(ui.HLClient.pref.Tracker)
114 if err != nil {
115 spew.Dump(err)
116 }
117
118 list := tview.NewList()
119 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
120 if event.Key() == tcell.KeyEsc {
121 ui.Pages.SwitchToPage("home")
122 }
123 return event
124 })
125 list.Box.SetBorder(true).SetTitle("| Servers |")
126
127 shortcut := 97 // rune for "a"
128 for i, srv := range listing {
129 addr := srv.Addr()
130 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
131 ui.Pages.RemovePage("joinServer")
132
133 newJS := ui.renderJoinServerForm("", addr, GuestAccount, "", trackerListPage, false, true)
134
135 ui.Pages.AddPage("joinServer", newJS, true, true)
136 ui.Pages.ShowPage("joinServer")
137 })
138 }
139
140 return list
141 }
142
143 func (ui *UI) renderSettingsForm() *tview.Flex {
144 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
145 settingsForm := tview.NewForm()
146 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
147 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
148 _, err := strconv.Atoi(idStr)
149 return err == nil
150 }, nil)
151 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
152 settingsForm.AddButton("Save", func() {
153 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
154 if len(usernameInput) == 0 {
155 usernameInput = "unnamed"
156 }
157 ui.HLClient.pref.Username = usernameInput
158 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
159 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
160 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
161
162 out, err := yaml.Marshal(&ui.HLClient.pref)
163 if err != nil {
164 // TODO: handle err
165 }
166 // TODO: handle err
167 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
168 if err != nil {
169 println(ui.HLClient.cfgPath)
170 panic(err)
171 }
172 ui.Pages.RemovePage("settings")
173 })
174 settingsForm.SetBorder(true)
175 settingsForm.SetCancelFunc(func() {
176 ui.Pages.RemovePage("settings")
177 })
178 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
179 settingsPage.Box.SetBorder(true).SetTitle("Settings")
180 settingsPage.AddItem(settingsForm, 0, 1, true)
181
182 centerFlex := tview.NewFlex().
183 AddItem(nil, 0, 1, false).
184 AddItem(tview.NewFlex().
185 SetDirection(tview.FlexRow).
186 AddItem(nil, 0, 1, false).
187 AddItem(settingsForm, 15, 1, true).
188 AddItem(nil, 0, 1, false), 40, 1, true).
189 AddItem(nil, 0, 1, false)
190
191 return centerFlex
192 }
193
194 func (ui *UI) joinServer(addr, login, password string) error {
195 // append default port to address if no port supplied
196 if len(strings.Split(addr, ":")) == 1 {
197 addr += ":5500"
198 }
199 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
200 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
201 }
202
203 go func() {
204 for {
205 err := ui.HLClient.ReadLoop()
206 if err != nil {
207 ui.HLClient.Logger.Errorw("read error", "err", err)
208
209 msg := err.Error()
210 if err == io.EOF {
211 msg = "The server connection has unexpectedly closed."
212 }
213
214 loginErrModal := tview.NewModal().
215 AddButtons([]string{"Ok"}).
216 SetText(msg).
217 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
218 ui.Pages.SwitchToPage("home")
219 })
220 loginErrModal.Box.SetTitle("Server Connection Error")
221
222 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
223 ui.App.Draw()
224 return
225 }
226 }
227 }()
228
229 return nil
230 }
231
232 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
233 joinServerForm := tview.NewForm()
234 joinServerForm.
235 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
236 // return false
237 //}, nil).
238 AddInputField("Server", server, 0, nil, nil).
239 AddInputField("Login", login, 0, nil, nil).
240 AddPasswordField("Password", password, 0, '*', nil).
241 AddCheckbox("Save", save, func(checked bool) {
242 ui.HLClient.Logger.Infow("saving bookmark")
243 // TODO: Implement bookmark saving
244
245 ui.HLClient.pref.AddBookmark(joinServerForm.GetFormItem(0).(*tview.InputField).GetText(), joinServerForm.GetFormItem(0).(*tview.InputField).GetText(), joinServerForm.GetFormItem(1).(*tview.InputField).GetText(), joinServerForm.GetFormItem(2).(*tview.InputField).GetText())
246 out, err := yaml.Marshal(ui.HLClient.pref)
247 if err != nil {
248 panic(err)
249 }
250
251 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
252 if err != nil {
253 panic(err)
254 }
255 // pref := ui.HLClient.pref
256 }).
257 AddButton("Cancel", func() {
258 ui.Pages.SwitchToPage(backPage)
259 }).
260 AddButton("Connect", func() {
261 err := ui.joinServer(
262 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
263 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
264 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
265 )
266 if err != nil {
267 ui.HLClient.Logger.Errorw("login error", "err", err)
268 loginErrModal := tview.NewModal().
269 AddButtons([]string{"Oh no"}).
270 SetText(err.Error()).
271 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
272 ui.Pages.SwitchToPage(backPage)
273 })
274
275 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
276 }
277
278 // Save checkbox
279 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
280 // TODO: implement bookmark saving
281 }
282 })
283
284 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
285 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
286 if event.Key() == tcell.KeyEscape {
287 ui.Pages.SwitchToPage(backPage)
288 }
289 return event
290 })
291
292 if defaultConnect {
293 joinServerForm.SetFocus(5)
294 }
295
296 joinServerPage := tview.NewFlex().
297 AddItem(nil, 0, 1, false).
298 AddItem(tview.NewFlex().
299 SetDirection(tview.FlexRow).
300 AddItem(nil, 0, 1, false).
301 AddItem(joinServerForm, 14, 1, true).
302 AddItem(nil, 0, 1, false), 40, 1, true).
303 AddItem(nil, 0, 1, false)
304
305 return joinServerPage
306 }
307
308 func (ui *UI) renderServerUI() *tview.Flex {
309 ui.chatBox.SetText("") // clear any previously existing chatbox text
310 commandList := tview.NewTextView().SetDynamicColors(true)
311 commandList.
312 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
313 SetBorder(true).
314 SetTitle("| Keyboard Shortcuts| ")
315
316 modal := tview.NewModal().
317 SetText("Disconnect from the server?").
318 AddButtons([]string{"Cancel", "Exit"}).
319 SetFocus(1)
320 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
321 if buttonIndex == 1 {
322 _ = ui.HLClient.Disconnect()
323 ui.Pages.RemovePage(pageServerUI)
324 } else {
325 ui.Pages.HidePage("modal")
326 }
327 })
328
329 serverUI := tview.NewFlex().
330 AddItem(tview.NewFlex().
331 SetDirection(tview.FlexRow).
332 AddItem(commandList, 4, 0, false).
333 AddItem(ui.chatBox, 0, 8, false).
334 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
335 AddItem(ui.userList, 25, 1, false)
336 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
337 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
338 if event.Key() == tcell.KeyEscape {
339 ui.Pages.AddPage("modal", modal, false, true)
340 }
341
342 // List files
343 if event.Key() == tcell.KeyCtrlF {
344 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
345 ui.HLClient.Logger.Errorw("err", "err", err)
346 }
347 }
348
349 // Show News
350 if event.Key() == tcell.KeyCtrlN {
351 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
352 ui.HLClient.Logger.Errorw("err", "err", err)
353 }
354 }
355
356 // Post news
357 if event.Key() == tcell.KeyCtrlP {
358
359 newsFlex := tview.NewFlex()
360 newsFlex.SetBorderPadding(0, 0, 1, 1)
361 newsPostTextArea := tview.NewTextView()
362 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
363 newsPostTextArea.SetChangedFunc(func() {
364 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
365 })
366
367 newsPostForm := tview.NewForm().
368 SetButtonsAlign(tview.AlignRight).
369 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
370 AddButton("Send", nil)
371 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
372 switch event.Key() {
373 case tcell.KeyEscape:
374 ui.Pages.RemovePage("newsInput")
375 case tcell.KeyTab:
376 ui.App.SetFocus(newsPostTextArea)
377 case tcell.KeyEnter:
378 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
379 if len(newsText) == 0 {
380 return event
381 }
382 err := ui.HLClient.Send(
383 *NewTransaction(tranOldPostNews, nil,
384 NewField(fieldData, []byte(newsText)),
385 ),
386 )
387 if err != nil {
388 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
389 // TODO: display errModal to user
390 }
391 ui.Pages.RemovePage("newsInput")
392 }
393
394 return event
395 })
396
397 newsFlex.
398 SetDirection(tview.FlexRow).
399 SetBorder(true).
400 SetTitle("| Post Message |")
401
402 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
403 switch event.Key() {
404 case tcell.KeyEscape:
405 ui.Pages.RemovePage("newsInput")
406 case tcell.KeyTab:
407 ui.App.SetFocus(newsPostForm)
408 case tcell.KeyEnter:
409 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
410 default:
411 const windowsBackspaceRune = 8
412 const macBackspaceRune = 127
413 switch event.Rune() {
414 case macBackspaceRune, windowsBackspaceRune:
415 curTxt := newsPostTextArea.GetText(true)
416 if len(curTxt) > 0 {
417 curTxt = curTxt[:len(curTxt)-1]
418 newsPostTextArea.SetText(curTxt)
419 }
420 default:
421 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
422 }
423 }
424
425 return event
426 })
427
428 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
429 newsFlex.AddItem(newsPostForm, 3, 0, false)
430
431 newsPostPage := tview.NewFlex().
432 AddItem(nil, 0, 1, false).
433 AddItem(tview.NewFlex().
434 SetDirection(tview.FlexRow).
435 AddItem(nil, 0, 1, false).
436 AddItem(newsFlex, 15, 1, true).
437 //AddItem(newsPostForm, 3, 0, false).
438 AddItem(nil, 0, 1, false), 40, 1, false).
439 AddItem(nil, 0, 1, false)
440
441 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
442 ui.App.SetFocus(newsPostTextArea)
443 }
444
445 return event
446 })
447 return serverUI
448 }
449
450 func (ui *UI) Start() {
451 home := tview.NewFlex().SetDirection(tview.FlexRow)
452 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
453 mainMenu := tview.NewList()
454
455 bannerItem := tview.NewTextView().
456 SetText(randomBanner()).
457 SetDynamicColors(true).
458 SetTextAlign(tview.AlignCenter)
459
460 home.AddItem(
461 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
462 14, 1, false)
463 home.AddItem(tview.NewFlex().
464 AddItem(nil, 0, 1, false).
465 AddItem(mainMenu, 0, 1, true).
466 AddItem(nil, 0, 1, false),
467 0, 1, true,
468 )
469
470 mainMenu.AddItem("Join Server", "", 'j', func() {
471 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
472 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
473 }).
474 AddItem("Bookmarks", "", 'b', func() {
475 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
476 }).
477 AddItem("Browse Tracker", "", 't', func() {
478 ui.trackerList = ui.getTrackerList()
479 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
480 }).
481 AddItem("Settings", "", 's', func() {
482 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
483 }).
484 AddItem("Quit", "", 'q', func() {
485 ui.App.Stop()
486 })
487
488 ui.Pages.AddPage("home", home, true, true)
489
490 // App level input capture
491 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
492 if event.Key() == tcell.KeyCtrlC {
493 ui.HLClient.Logger.Infow("Exiting")
494 ui.App.Stop()
495 os.Exit(0)
496 }
497 // Show Logs
498 if event.Key() == tcell.KeyCtrlL {
499 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
500 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
501 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
502 if key == tcell.KeyEscape {
503 ui.Pages.RemovePage("logs")
504 }
505 })
506
507 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
508 }
509 return event
510 })
511
512 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
513 ui.App.Stop()
514 os.Exit(1)
515 }
516 }