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