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