]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
Fix error message on logout
[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 loginErrModal := tview.NewModal().
213 AddButtons([]string{"Ok"}).
214 SetText(msg).
215 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
216 ui.Pages.SwitchToPage("home")
217 })
218 loginErrModal.Box.SetTitle("Server Connection Error")
219
220 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
221 ui.App.Draw()
222 return
223 }
224 ui.Pages.SwitchToPage("home")
225
226 return
227 }
228 }
229 }()
230
231 return nil
232 }
233
234 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
235 joinServerForm := tview.NewForm()
236 joinServerForm.
237 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
238 // return false
239 //}, nil).
240 AddInputField("Server", server, 0, nil, nil).
241 AddInputField("Login", login, 0, nil, nil).
242 AddPasswordField("Password", password, 0, '*', nil).
243 AddCheckbox("Save", save, func(checked bool) {
244 ui.HLClient.Logger.Infow("saving bookmark")
245 // TODO: Implement bookmark saving
246
247 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())
248 out, err := yaml.Marshal(ui.HLClient.pref)
249 if err != nil {
250 panic(err)
251 }
252
253 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
254 if err != nil {
255 panic(err)
256 }
257 // pref := ui.HLClient.pref
258 }).
259 AddButton("Cancel", func() {
260 ui.Pages.SwitchToPage(backPage)
261 }).
262 AddButton("Connect", func() {
263 err := ui.joinServer(
264 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
265 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
266 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
267 )
268 if err != nil {
269 ui.HLClient.Logger.Errorw("login error", "err", err)
270 loginErrModal := tview.NewModal().
271 AddButtons([]string{"Oh no"}).
272 SetText(err.Error()).
273 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
274 ui.Pages.SwitchToPage(backPage)
275 })
276
277 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
278 }
279
280 // Save checkbox
281 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
282 // TODO: implement bookmark saving
283 }
284 })
285
286 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
287 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
288 if event.Key() == tcell.KeyEscape {
289 ui.Pages.SwitchToPage(backPage)
290 }
291 return event
292 })
293
294 if defaultConnect {
295 joinServerForm.SetFocus(5)
296 }
297
298 joinServerPage := tview.NewFlex().
299 AddItem(nil, 0, 1, false).
300 AddItem(tview.NewFlex().
301 SetDirection(tview.FlexRow).
302 AddItem(nil, 0, 1, false).
303 AddItem(joinServerForm, 14, 1, true).
304 AddItem(nil, 0, 1, false), 40, 1, true).
305 AddItem(nil, 0, 1, false)
306
307 return joinServerPage
308 }
309
310 func (ui *UI) renderServerUI() *tview.Flex {
311 ui.chatBox.SetText("") // clear any previously existing chatbox text
312 commandList := tview.NewTextView().SetDynamicColors(true)
313 commandList.
314 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
315 SetBorder(true).
316 SetTitle("| Keyboard Shortcuts| ")
317
318 modal := tview.NewModal().
319 SetText("Disconnect from the server?").
320 AddButtons([]string{"Cancel", "Exit"}).
321 SetFocus(1)
322 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
323 if buttonIndex == 1 {
324 _ = ui.HLClient.Disconnect()
325 ui.Pages.RemovePage(pageServerUI)
326 } else {
327 ui.Pages.HidePage("modal")
328 }
329 })
330
331 serverUI := tview.NewFlex().
332 AddItem(tview.NewFlex().
333 SetDirection(tview.FlexRow).
334 AddItem(commandList, 4, 0, false).
335 AddItem(ui.chatBox, 0, 8, false).
336 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
337 AddItem(ui.userList, 25, 1, false)
338 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
339 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
340 if event.Key() == tcell.KeyEscape {
341 ui.Pages.AddPage("modal", modal, false, true)
342 }
343
344 // List files
345 if event.Key() == tcell.KeyCtrlF {
346 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
347 ui.HLClient.Logger.Errorw("err", "err", err)
348 }
349 }
350
351 // Show News
352 if event.Key() == tcell.KeyCtrlN {
353 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
354 ui.HLClient.Logger.Errorw("err", "err", err)
355 }
356 }
357
358 // Post news
359 if event.Key() == tcell.KeyCtrlP {
360
361 newsFlex := tview.NewFlex()
362 newsFlex.SetBorderPadding(0, 0, 1, 1)
363 newsPostTextArea := tview.NewTextView()
364 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
365 newsPostTextArea.SetChangedFunc(func() {
366 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
367 })
368
369 newsPostForm := tview.NewForm().
370 SetButtonsAlign(tview.AlignRight).
371 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
372 AddButton("Send", nil)
373 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
374 switch event.Key() {
375 case tcell.KeyEscape:
376 ui.Pages.RemovePage("newsInput")
377 case tcell.KeyTab:
378 ui.App.SetFocus(newsPostTextArea)
379 case tcell.KeyEnter:
380 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
381 if len(newsText) == 0 {
382 return event
383 }
384 err := ui.HLClient.Send(
385 *NewTransaction(tranOldPostNews, nil,
386 NewField(fieldData, []byte(newsText)),
387 ),
388 )
389 if err != nil {
390 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
391 // TODO: display errModal to user
392 }
393 ui.Pages.RemovePage("newsInput")
394 }
395
396 return event
397 })
398
399 newsFlex.
400 SetDirection(tview.FlexRow).
401 SetBorder(true).
402 SetTitle("| Post Message |")
403
404 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
405 switch event.Key() {
406 case tcell.KeyEscape:
407 ui.Pages.RemovePage("newsInput")
408 case tcell.KeyTab:
409 ui.App.SetFocus(newsPostForm)
410 case tcell.KeyEnter:
411 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
412 default:
413 const windowsBackspaceRune = 8
414 const macBackspaceRune = 127
415 switch event.Rune() {
416 case macBackspaceRune, windowsBackspaceRune:
417 curTxt := newsPostTextArea.GetText(true)
418 if len(curTxt) > 0 {
419 curTxt = curTxt[:len(curTxt)-1]
420 newsPostTextArea.SetText(curTxt)
421 }
422 default:
423 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
424 }
425 }
426
427 return event
428 })
429
430 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
431 newsFlex.AddItem(newsPostForm, 3, 0, false)
432
433 newsPostPage := tview.NewFlex().
434 AddItem(nil, 0, 1, false).
435 AddItem(tview.NewFlex().
436 SetDirection(tview.FlexRow).
437 AddItem(nil, 0, 1, false).
438 AddItem(newsFlex, 15, 1, true).
439 //AddItem(newsPostForm, 3, 0, false).
440 AddItem(nil, 0, 1, false), 40, 1, false).
441 AddItem(nil, 0, 1, false)
442
443 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
444 ui.App.SetFocus(newsPostTextArea)
445 }
446
447 return event
448 })
449 return serverUI
450 }
451
452 func (ui *UI) Start() {
453 home := tview.NewFlex().SetDirection(tview.FlexRow)
454 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
455 mainMenu := tview.NewList()
456
457 bannerItem := tview.NewTextView().
458 SetText(randomBanner()).
459 SetDynamicColors(true).
460 SetTextAlign(tview.AlignCenter)
461
462 home.AddItem(
463 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
464 14, 1, false)
465 home.AddItem(tview.NewFlex().
466 AddItem(nil, 0, 1, false).
467 AddItem(mainMenu, 0, 1, true).
468 AddItem(nil, 0, 1, false),
469 0, 1, true,
470 )
471
472 mainMenu.AddItem("Join Server", "", 'j', func() {
473 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
474 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
475 }).
476 AddItem("Bookmarks", "", 'b', func() {
477 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
478 }).
479 AddItem("Browse Tracker", "", 't', func() {
480 ui.trackerList = ui.getTrackerList()
481 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
482 }).
483 AddItem("Settings", "", 's', func() {
484 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
485 }).
486 AddItem("Quit", "", 'q', func() {
487 ui.App.Stop()
488 })
489
490 ui.Pages.AddPage("home", home, true, true)
491
492 // App level input capture
493 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
494 if event.Key() == tcell.KeyCtrlC {
495 ui.HLClient.Logger.Infow("Exiting")
496 ui.App.Stop()
497 os.Exit(0)
498 }
499 // Show Logs
500 if event.Key() == tcell.KeyCtrlL {
501 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
502 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
503 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
504 if key == tcell.KeyEscape {
505 ui.Pages.RemovePage("logs")
506 }
507 })
508
509 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
510 }
511 return event
512 })
513
514 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
515 ui.App.Stop()
516 os.Exit(1)
517 }
518 }