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