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