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