]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
Initial refactor to split client from protocol package
[rbdr/mobius] / hotline / ui.go
1 package hotline
2
3 import (
4 "fmt"
5 "github.com/gdamore/tcell/v2"
6 "github.com/rivo/tview"
7 "gopkg.in/yaml.v3"
8 "io/ioutil"
9 "os"
10 "strconv"
11 "strings"
12 )
13
14 type UI struct {
15 chatBox *tview.TextView
16 chatInput *tview.InputField
17 App *tview.Application
18 Pages *tview.Pages
19 userList *tview.TextView
20 trackerList *tview.List
21 HLClient *Client
22 }
23
24 // pages
25 const (
26 pageServerUI = "serverUI"
27 )
28
29 func 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 })
38 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
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
50 _ = c.Send(
51 *NewTransaction(TranChatSend, nil,
52 NewField(FieldData, []byte(chatInput.GetText())),
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(),
75 HLClient: c,
76 }
77 }
78
79 func (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"
90 for i, srv := range ui.HLClient.Pref.Bookmarks {
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
97 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
98
99 ui.Pages.AddPage("joinServer", newJS, true, true)
100 })
101 }
102
103 return list
104 }
105
106 func (ui *UI) getTrackerList() *tview.List {
107 listing, err := GetListing(ui.HLClient.Pref.Tracker)
108 if err != nil {
109 // TODO
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()
124 srvName := srv.Name
125 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
126 ui.Pages.RemovePage("joinServer")
127
128 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
129
130 ui.Pages.AddPage("joinServer", newJS, true, true)
131 ui.Pages.ShowPage("joinServer")
132 })
133 }
134
135 return list
136 }
137
138 func (ui *UI) renderSettingsForm() *tview.Flex {
139 iconStr := strconv.Itoa(ui.HLClient.Pref.IconID)
140 settingsForm := tview.NewForm()
141 settingsForm.AddInputField("Your Name", ui.HLClient.Pref.Username, 0, nil, nil)
142 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
143 _, err := strconv.Atoi(idStr)
144 return err == nil
145 }, nil)
146 settingsForm.AddInputField("Tracker", ui.HLClient.Pref.Tracker, 0, nil, nil)
147 settingsForm.AddCheckbox("Enable Terminal Bell", ui.HLClient.Pref.EnableBell, nil)
148 settingsForm.AddButton("Save", func() {
149 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
150 if len(usernameInput) == 0 {
151 usernameInput = "unnamed"
152 }
153 ui.HLClient.Pref.Username = usernameInput
154 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
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()
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
191 func (ui *UI) joinServer(addr, login, password string) error {
192 // append default port to address if no port supplied
193 if len(strings.Split(addr, ":")) == 1 {
194 addr += ":5500"
195 }
196 if err := ui.HLClient.Connect(addr, login, password); err != nil {
197 return fmt.Errorf("Error joining server: %v\n", err)
198 }
199
200 go func() {
201 if err := ui.HLClient.HandleTransactions(); err != nil {
202 ui.Pages.SwitchToPage("home")
203 }
204
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")
212
213 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
214 ui.App.Draw()
215 }()
216
217 return nil
218 }
219
220 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
221 joinServerForm := tview.NewForm()
222 joinServerForm.
223 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
224 // return false
225 // }, nil).
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) {
230 ui.HLClient.Logger.Infow("saving bookmark")
231 // TODO: Implement bookmark saving
232
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)
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 }
243 // Pref := ui.HLClient.Pref
244 }).
245 AddButton("Cancel", func() {
246 ui.Pages.SwitchToPage(backPage)
247 }).
248 AddButton("Connect", func() {
249 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
250 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
251 err := ui.joinServer(
252 srvAddr,
253 loginInput,
254 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
255 )
256 if name == "" {
257 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
258 }
259 ui.HLClient.serverName = name
260
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
303 func (ui *UI) renderServerUI() *tview.Flex {
304 ui.chatBox.SetText("") // clear any previously existing chatbox text
305 commandList := tview.NewTextView().SetDynamicColors(true)
306 commandList.
307 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
308 SetBorder(true).
309 SetTitle("| Keyboard Shortcuts| ")
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()
318 ui.Pages.RemovePage(pageServerUI)
319 ui.Pages.SwitchToPage("home")
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)
332 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
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
338 // List files
339 if event.Key() == tcell.KeyCtrlF {
340 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
341 ui.HLClient.Logger.Errorw("err", "err", err)
342 }
343 }
344
345 // Show News
346 if event.Key() == tcell.KeyCtrlN {
347 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
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()
356 newsFlex.SetBorderPadding(0, 0, 1, 1)
357 newsPostTextArea := tview.NewTextView()
358 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
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 })
362
363 newsPostForm := tview.NewForm().
364 SetButtonsAlign(tview.AlignRight).
365 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
366 AddButton("Send", nil)
367 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
368 switch event.Key() {
369 case tcell.KeyEscape:
370 ui.Pages.RemovePage("newsInput")
371 case tcell.KeyTab:
372 ui.App.SetFocus(newsPostTextArea)
373 case tcell.KeyEnter:
374 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
375 if len(newsText) == 0 {
376 return event
377 }
378 err := ui.HLClient.Send(
379 *NewTransaction(TranOldPostNews, nil,
380 NewField(FieldData, []byte(newsText)),
381 ),
382 )
383 if err != nil {
384 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
385 // TODO: display errModal to user
386 }
387 ui.Pages.RemovePage("newsInput")
388 }
389
390 return event
391 })
392
393 newsFlex.
394 SetDirection(tview.FlexRow).
395 SetBorder(true).
396 SetTitle("| Post Message |")
397
398 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
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:
405 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
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:
417 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
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).
433 // AddItem(newsPostForm, 3, 0, false).
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
446 func (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() {
467 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
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 }