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