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