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