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