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