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