]> git.r.bdr.sh - rbdr/mobius/blame - hotline/ui.go
Replace deprecated iotuil usage
[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.
da1e0d79
JH
222 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
223 // return false
aebc4d36 224 // }, nil).
b198b22b
JH
225 AddInputField("Server", server, 0, nil, nil).
226 AddInputField("Login", login, 0, nil, nil).
227 AddPasswordField("Password", password, 0, '*', nil).
228 AddCheckbox("Save", save, func(checked bool) {
da1e0d79 229 ui.HLClient.Logger.Infow("saving bookmark")
b198b22b 230 // TODO: Implement bookmark saving
da1e0d79 231
d005ef04
JH
232 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())
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 }
d005ef04 242 // Pref := ui.HLClient.Pref
b198b22b
JH
243 }).
244 AddButton("Cancel", func() {
245 ui.Pages.SwitchToPage(backPage)
246 }).
247 AddButton("Connect", func() {
e005c191
JH
248 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
249 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
b198b22b 250 err := ui.joinServer(
e005c191
JH
251 srvAddr,
252 loginInput,
b198b22b
JH
253 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
254 )
e005c191
JH
255 if name == "" {
256 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
257 }
258 ui.HLClient.serverName = name
259
b198b22b
JH
260 if err != nil {
261 ui.HLClient.Logger.Errorw("login error", "err", err)
262 loginErrModal := tview.NewModal().
263 AddButtons([]string{"Oh no"}).
264 SetText(err.Error()).
265 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
266 ui.Pages.SwitchToPage(backPage)
267 })
268
269 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
270 }
271
272 // Save checkbox
273 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
274 // TODO: implement bookmark saving
275 }
276 })
277
278 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
279 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
280 if event.Key() == tcell.KeyEscape {
281 ui.Pages.SwitchToPage(backPage)
282 }
283 return event
284 })
285
286 if defaultConnect {
287 joinServerForm.SetFocus(5)
288 }
289
290 joinServerPage := tview.NewFlex().
291 AddItem(nil, 0, 1, false).
292 AddItem(tview.NewFlex().
293 SetDirection(tview.FlexRow).
294 AddItem(nil, 0, 1, false).
295 AddItem(joinServerForm, 14, 1, true).
296 AddItem(nil, 0, 1, false), 40, 1, true).
297 AddItem(nil, 0, 1, false)
298
299 return joinServerPage
300}
301
302func (ui *UI) renderServerUI() *tview.Flex {
e75ba43a 303 ui.chatBox.SetText("") // clear any previously existing chatbox text
b198b22b
JH
304 commandList := tview.NewTextView().SetDynamicColors(true)
305 commandList.
e75ba43a 306 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
b198b22b 307 SetBorder(true).
43ecc0f4 308 SetTitle("| Keyboard Shortcuts| ")
b198b22b
JH
309
310 modal := tview.NewModal().
311 SetText("Disconnect from the server?").
312 AddButtons([]string{"Cancel", "Exit"}).
313 SetFocus(1)
314 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
315 if buttonIndex == 1 {
316 _ = ui.HLClient.Disconnect()
e75ba43a 317 ui.Pages.RemovePage(pageServerUI)
3178ae58 318 ui.Pages.SwitchToPage("home")
b198b22b
JH
319 } else {
320 ui.Pages.HidePage("modal")
321 }
322 })
323
324 serverUI := tview.NewFlex().
325 AddItem(tview.NewFlex().
326 SetDirection(tview.FlexRow).
327 AddItem(commandList, 4, 0, false).
328 AddItem(ui.chatBox, 0, 8, false).
329 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
330 AddItem(ui.userList, 25, 1, false)
e005c191 331 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
b198b22b
JH
332 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
333 if event.Key() == tcell.KeyEscape {
334 ui.Pages.AddPage("modal", modal, false, true)
335 }
336
43ecc0f4
JH
337 // List files
338 if event.Key() == tcell.KeyCtrlF {
d005ef04 339 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
43ecc0f4
JH
340 ui.HLClient.Logger.Errorw("err", "err", err)
341 }
342 }
343
b198b22b
JH
344 // Show News
345 if event.Key() == tcell.KeyCtrlN {
d005ef04 346 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
b198b22b
JH
347 ui.HLClient.Logger.Errorw("err", "err", err)
348 }
349 }
350
351 // Post news
352 if event.Key() == tcell.KeyCtrlP {
353
354 newsFlex := tview.NewFlex()
e75ba43a 355 newsFlex.SetBorderPadding(0, 0, 1, 1)
b198b22b 356 newsPostTextArea := tview.NewTextView()
e75ba43a 357 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
b198b22b
JH
358 newsPostTextArea.SetChangedFunc(func() {
359 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
360 })
b198b22b
JH
361
362 newsPostForm := tview.NewForm().
363 SetButtonsAlign(tview.AlignRight).
aebc4d36 364 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
c1ab228e 365 AddButton("Send", nil)
b198b22b
JH
366 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
367 switch event.Key() {
e75ba43a
JH
368 case tcell.KeyEscape:
369 ui.Pages.RemovePage("newsInput")
b198b22b
JH
370 case tcell.KeyTab:
371 ui.App.SetFocus(newsPostTextArea)
372 case tcell.KeyEnter:
373 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
e75ba43a
JH
374 if len(newsText) == 0 {
375 return event
376 }
b198b22b 377 err := ui.HLClient.Send(
d005ef04
JH
378 *NewTransaction(TranOldPostNews, nil,
379 NewField(FieldData, []byte(newsText)),
b198b22b
JH
380 ),
381 )
382 if err != nil {
383 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
384 // TODO: display errModal to user
385 }
b198b22b
JH
386 ui.Pages.RemovePage("newsInput")
387 }
388
389 return event
390 })
391
392 newsFlex.
393 SetDirection(tview.FlexRow).
394 SetBorder(true).
c1ab228e 395 SetTitle("| Post Message |")
b198b22b
JH
396
397 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
b198b22b
JH
398 switch event.Key() {
399 case tcell.KeyEscape:
400 ui.Pages.RemovePage("newsInput")
401 case tcell.KeyTab:
402 ui.App.SetFocus(newsPostForm)
403 case tcell.KeyEnter:
e75ba43a 404 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
b198b22b
JH
405 default:
406 const windowsBackspaceRune = 8
407 const macBackspaceRune = 127
408 switch event.Rune() {
409 case macBackspaceRune, windowsBackspaceRune:
410 curTxt := newsPostTextArea.GetText(true)
411 if len(curTxt) > 0 {
412 curTxt = curTxt[:len(curTxt)-1]
413 newsPostTextArea.SetText(curTxt)
414 }
415 default:
e75ba43a 416 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
b198b22b
JH
417 }
418 }
419
420 return event
421 })
422
423 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
424 newsFlex.AddItem(newsPostForm, 3, 0, false)
425
426 newsPostPage := tview.NewFlex().
427 AddItem(nil, 0, 1, false).
428 AddItem(tview.NewFlex().
429 SetDirection(tview.FlexRow).
430 AddItem(nil, 0, 1, false).
431 AddItem(newsFlex, 15, 1, true).
aebc4d36 432 // AddItem(newsPostForm, 3, 0, false).
b198b22b
JH
433 AddItem(nil, 0, 1, false), 40, 1, false).
434 AddItem(nil, 0, 1, false)
435
436 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
437 ui.App.SetFocus(newsPostTextArea)
438 }
439
440 return event
441 })
442 return serverUI
443}
444
445func (ui *UI) Start() {
446 home := tview.NewFlex().SetDirection(tview.FlexRow)
447 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
448 mainMenu := tview.NewList()
449
450 bannerItem := tview.NewTextView().
451 SetText(randomBanner()).
452 SetDynamicColors(true).
453 SetTextAlign(tview.AlignCenter)
454
455 home.AddItem(
456 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
457 14, 1, false)
458 home.AddItem(tview.NewFlex().
459 AddItem(nil, 0, 1, false).
460 AddItem(mainMenu, 0, 1, true).
461 AddItem(nil, 0, 1, false),
462 0, 1, true,
463 )
464
465 mainMenu.AddItem("Join Server", "", 'j', func() {
da1e0d79 466 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
b198b22b
JH
467 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
468 }).
469 AddItem("Bookmarks", "", 'b', func() {
470 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
471 }).
472 AddItem("Browse Tracker", "", 't', func() {
473 ui.trackerList = ui.getTrackerList()
474 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
475 }).
476 AddItem("Settings", "", 's', func() {
477 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
478 }).
479 AddItem("Quit", "", 'q', func() {
480 ui.App.Stop()
481 })
482
483 ui.Pages.AddPage("home", home, true, true)
484
485 // App level input capture
486 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
487 if event.Key() == tcell.KeyCtrlC {
488 ui.HLClient.Logger.Infow("Exiting")
489 ui.App.Stop()
490 os.Exit(0)
491 }
492 // Show Logs
493 if event.Key() == tcell.KeyCtrlL {
494 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
495 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
496 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
497 if key == tcell.KeyEscape {
498 ui.Pages.RemovePage("logs")
499 }
500 })
501
502 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
503 }
504 return event
505 })
506
507 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
508 ui.App.Stop()
509 os.Exit(1)
510 }
511}