]> git.r.bdr.sh - rbdr/mobius/blame - hotline/ui.go
Cleanup README
[rbdr/mobius] / hotline / ui.go
CommitLineData
d80b37ca 1package hotline
b198b22b
JH
2
3import (
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
17type UI struct {
e005c191
JH
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
b198b22b
JH
26}
27
e75ba43a
JH
28// pages
29const (
30 pageServerUI = "serverUI"
31)
32
b198b22b
JH
33func 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 })
40afb444 42 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
b198b22b
JH
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
e75ba43a 54 _ = c.Send(
b198b22b
JH
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
84func (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
da1e0d79 102 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
b198b22b
JH
103
104 ui.Pages.AddPage("joinServer", newJS, true, true)
105 })
106 }
107
108 return list
109}
110
111func (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()
e005c191 129 srvName := srv.Name
b198b22b
JH
130 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
131 ui.Pages.RemovePage("joinServer")
132
e005c191 133 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
b198b22b
JH
134
135 ui.Pages.AddPage("joinServer", newJS, true, true)
136 ui.Pages.ShowPage("joinServer")
137 })
138 }
139
140 return list
141}
142
143func (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
194func (ui *UI) joinServer(addr, login, password string) error {
58efb95f
JH
195 // append default port to address if no port supplied
196 if len(strings.Split(addr, ":")) == 1 {
197 addr += ":5500"
198 }
b198b22b
JH
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
b198b22b 209 if err == io.EOF {
06e94003
JH
210 loginErrModal := tview.NewModal().
211 AddButtons([]string{"Ok"}).
40afb444 212 SetText("The server connection has closed.").
06e94003
JH
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
b198b22b 221 }
06e94003 222 ui.Pages.SwitchToPage("home")
b198b22b 223
b198b22b
JH
224 return
225 }
226 }
227 }()
228
229 return nil
230}
231
da1e0d79 232func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
b198b22b
JH
233 joinServerForm := tview.NewForm()
234 joinServerForm.
da1e0d79
JH
235 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
236 // return false
237 //}, nil).
b198b22b
JH
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) {
da1e0d79 242 ui.HLClient.Logger.Infow("saving bookmark")
b198b22b 243 // TODO: Implement bookmark saving
da1e0d79
JH
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
b198b22b
JH
256 }).
257 AddButton("Cancel", func() {
258 ui.Pages.SwitchToPage(backPage)
259 }).
260 AddButton("Connect", func() {
e005c191
JH
261 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
262 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
b198b22b 263 err := ui.joinServer(
e005c191
JH
264 srvAddr,
265 loginInput,
b198b22b
JH
266 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
267 )
e005c191
JH
268 if name == "" {
269 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
270 }
271 ui.HLClient.serverName = name
272
b198b22b
JH
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
315func (ui *UI) renderServerUI() *tview.Flex {
e75ba43a 316 ui.chatBox.SetText("") // clear any previously existing chatbox text
b198b22b
JH
317 commandList := tview.NewTextView().SetDynamicColors(true)
318 commandList.
e75ba43a 319 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
b198b22b 320 SetBorder(true).
43ecc0f4 321 SetTitle("| Keyboard Shortcuts| ")
b198b22b
JH
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()
e75ba43a 330 ui.Pages.RemovePage(pageServerUI)
b198b22b
JH
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)
e005c191 343 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
b198b22b
JH
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
43ecc0f4
JH
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
b198b22b
JH
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()
e75ba43a 367 newsFlex.SetBorderPadding(0, 0, 1, 1)
b198b22b 368 newsPostTextArea := tview.NewTextView()
e75ba43a 369 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
b198b22b
JH
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 })
b198b22b
JH
373
374 newsPostForm := tview.NewForm().
375 SetButtonsAlign(tview.AlignRight).
c1ab228e
JH
376 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
377 AddButton("Send", nil)
b198b22b
JH
378 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
379 switch event.Key() {
e75ba43a
JH
380 case tcell.KeyEscape:
381 ui.Pages.RemovePage("newsInput")
b198b22b
JH
382 case tcell.KeyTab:
383 ui.App.SetFocus(newsPostTextArea)
384 case tcell.KeyEnter:
385 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
e75ba43a
JH
386 if len(newsText) == 0 {
387 return event
388 }
b198b22b
JH
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 }
b198b22b
JH
398 ui.Pages.RemovePage("newsInput")
399 }
400
401 return event
402 })
403
404 newsFlex.
405 SetDirection(tview.FlexRow).
406 SetBorder(true).
c1ab228e 407 SetTitle("| Post Message |")
b198b22b
JH
408
409 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
b198b22b
JH
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:
e75ba43a 416 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
b198b22b
JH
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:
e75ba43a 428 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
b198b22b
JH
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
457func (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() {
da1e0d79 478 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
b198b22b
JH
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}