]> git.r.bdr.sh - rbdr/mobius/blame - hotline/ui.go
Minor cleanup
[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 {
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
b198b22b
JH
25 HLClient *Client
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()
129 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
130 ui.Pages.RemovePage("joinServer")
131
da1e0d79 132 newJS := ui.renderJoinServerForm("", 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
JH
198 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
199 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
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() {
260 err := ui.joinServer(
261 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
262 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
263 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
264 )
265 if err != nil {
266 ui.HLClient.Logger.Errorw("login error", "err", err)
267 loginErrModal := tview.NewModal().
268 AddButtons([]string{"Oh no"}).
269 SetText(err.Error()).
270 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
271 ui.Pages.SwitchToPage(backPage)
272 })
273
274 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
275 }
276
277 // Save checkbox
278 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
279 // TODO: implement bookmark saving
280 }
281 })
282
283 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
284 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
285 if event.Key() == tcell.KeyEscape {
286 ui.Pages.SwitchToPage(backPage)
287 }
288 return event
289 })
290
291 if defaultConnect {
292 joinServerForm.SetFocus(5)
293 }
294
295 joinServerPage := tview.NewFlex().
296 AddItem(nil, 0, 1, false).
297 AddItem(tview.NewFlex().
298 SetDirection(tview.FlexRow).
299 AddItem(nil, 0, 1, false).
300 AddItem(joinServerForm, 14, 1, true).
301 AddItem(nil, 0, 1, false), 40, 1, true).
302 AddItem(nil, 0, 1, false)
303
304 return joinServerPage
305}
306
307func (ui *UI) renderServerUI() *tview.Flex {
e75ba43a 308 ui.chatBox.SetText("") // clear any previously existing chatbox text
b198b22b
JH
309 commandList := tview.NewTextView().SetDynamicColors(true)
310 commandList.
e75ba43a 311 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
b198b22b 312 SetBorder(true).
43ecc0f4 313 SetTitle("| Keyboard Shortcuts| ")
b198b22b
JH
314
315 modal := tview.NewModal().
316 SetText("Disconnect from the server?").
317 AddButtons([]string{"Cancel", "Exit"}).
318 SetFocus(1)
319 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
320 if buttonIndex == 1 {
321 _ = ui.HLClient.Disconnect()
e75ba43a 322 ui.Pages.RemovePage(pageServerUI)
b198b22b
JH
323 } else {
324 ui.Pages.HidePage("modal")
325 }
326 })
327
328 serverUI := tview.NewFlex().
329 AddItem(tview.NewFlex().
330 SetDirection(tview.FlexRow).
331 AddItem(commandList, 4, 0, false).
332 AddItem(ui.chatBox, 0, 8, false).
333 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
334 AddItem(ui.userList, 25, 1, false)
335 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
336 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
337 if event.Key() == tcell.KeyEscape {
338 ui.Pages.AddPage("modal", modal, false, true)
339 }
340
43ecc0f4
JH
341 // List files
342 if event.Key() == tcell.KeyCtrlF {
343 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
344 ui.HLClient.Logger.Errorw("err", "err", err)
345 }
346 }
347
b198b22b
JH
348 // Show News
349 if event.Key() == tcell.KeyCtrlN {
350 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
351 ui.HLClient.Logger.Errorw("err", "err", err)
352 }
353 }
354
355 // Post news
356 if event.Key() == tcell.KeyCtrlP {
357
358 newsFlex := tview.NewFlex()
e75ba43a 359 newsFlex.SetBorderPadding(0, 0, 1, 1)
b198b22b 360 newsPostTextArea := tview.NewTextView()
e75ba43a 361 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
b198b22b
JH
362 newsPostTextArea.SetChangedFunc(func() {
363 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
364 })
b198b22b
JH
365
366 newsPostForm := tview.NewForm().
367 SetButtonsAlign(tview.AlignRight).
c1ab228e
JH
368 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
369 AddButton("Send", nil)
b198b22b
JH
370 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
371 switch event.Key() {
e75ba43a
JH
372 case tcell.KeyEscape:
373 ui.Pages.RemovePage("newsInput")
b198b22b
JH
374 case tcell.KeyTab:
375 ui.App.SetFocus(newsPostTextArea)
376 case tcell.KeyEnter:
377 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
e75ba43a
JH
378 if len(newsText) == 0 {
379 return event
380 }
b198b22b
JH
381 err := ui.HLClient.Send(
382 *NewTransaction(tranOldPostNews, nil,
383 NewField(fieldData, []byte(newsText)),
384 ),
385 )
386 if err != nil {
387 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
388 // TODO: display errModal to user
389 }
b198b22b
JH
390 ui.Pages.RemovePage("newsInput")
391 }
392
393 return event
394 })
395
396 newsFlex.
397 SetDirection(tview.FlexRow).
398 SetBorder(true).
c1ab228e 399 SetTitle("| Post Message |")
b198b22b
JH
400
401 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
b198b22b
JH
402 switch event.Key() {
403 case tcell.KeyEscape:
404 ui.Pages.RemovePage("newsInput")
405 case tcell.KeyTab:
406 ui.App.SetFocus(newsPostForm)
407 case tcell.KeyEnter:
e75ba43a 408 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
b198b22b
JH
409 default:
410 const windowsBackspaceRune = 8
411 const macBackspaceRune = 127
412 switch event.Rune() {
413 case macBackspaceRune, windowsBackspaceRune:
414 curTxt := newsPostTextArea.GetText(true)
415 if len(curTxt) > 0 {
416 curTxt = curTxt[:len(curTxt)-1]
417 newsPostTextArea.SetText(curTxt)
418 }
419 default:
e75ba43a 420 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
b198b22b
JH
421 }
422 }
423
424 return event
425 })
426
427 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
428 newsFlex.AddItem(newsPostForm, 3, 0, false)
429
430 newsPostPage := tview.NewFlex().
431 AddItem(nil, 0, 1, false).
432 AddItem(tview.NewFlex().
433 SetDirection(tview.FlexRow).
434 AddItem(nil, 0, 1, false).
435 AddItem(newsFlex, 15, 1, true).
436 //AddItem(newsPostForm, 3, 0, false).
437 AddItem(nil, 0, 1, false), 40, 1, false).
438 AddItem(nil, 0, 1, false)
439
440 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
441 ui.App.SetFocus(newsPostTextArea)
442 }
443
444 return event
445 })
446 return serverUI
447}
448
449func (ui *UI) Start() {
450 home := tview.NewFlex().SetDirection(tview.FlexRow)
451 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
452 mainMenu := tview.NewList()
453
454 bannerItem := tview.NewTextView().
455 SetText(randomBanner()).
456 SetDynamicColors(true).
457 SetTextAlign(tview.AlignCenter)
458
459 home.AddItem(
460 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
461 14, 1, false)
462 home.AddItem(tview.NewFlex().
463 AddItem(nil, 0, 1, false).
464 AddItem(mainMenu, 0, 1, true).
465 AddItem(nil, 0, 1, false),
466 0, 1, true,
467 )
468
469 mainMenu.AddItem("Join Server", "", 'j', func() {
da1e0d79 470 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
b198b22b
JH
471 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
472 }).
473 AddItem("Bookmarks", "", 'b', func() {
474 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
475 }).
476 AddItem("Browse Tracker", "", 't', func() {
477 ui.trackerList = ui.getTrackerList()
478 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
479 }).
480 AddItem("Settings", "", 's', func() {
481 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
482 }).
483 AddItem("Quit", "", 'q', func() {
484 ui.App.Stop()
485 })
486
487 ui.Pages.AddPage("home", home, true, true)
488
489 // App level input capture
490 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
491 if event.Key() == tcell.KeyCtrlC {
492 ui.HLClient.Logger.Infow("Exiting")
493 ui.App.Stop()
494 os.Exit(0)
495 }
496 // Show Logs
497 if event.Key() == tcell.KeyCtrlL {
498 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
499 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
500 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
501 if key == tcell.KeyEscape {
502 ui.Pages.RemovePage("logs")
503 }
504 })
505
506 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
507 }
508 return event
509 })
510
511 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
512 ui.App.Stop()
513 os.Exit(1)
514 }
515}