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