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