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