]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
Replace deprecated ioutil
[rbdr/mobius] / hotline / ui.go
1 package hotline
2
3 import (
4 "fmt"
5 "github.com/gdamore/tcell/v2"
6 "github.com/rivo/tview"
7 "gopkg.in/yaml.v3"
8 "os"
9 "strconv"
10 "strings"
11 )
12
13 type UI struct {
14 chatBox *tview.TextView
15 chatInput *tview.InputField
16 App *tview.Application
17 Pages *tview.Pages
18 userList *tview.TextView
19 trackerList *tview.List
20 HLClient *Client
21 }
22
23 // pages
24 const (
25 pageServerUI = "serverUI"
26 )
27
28 func 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 })
37 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
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
49 _ = c.Send(
50 *NewTransaction(TranChatSend, nil,
51 NewField(FieldData, []byte(chatInput.GetText())),
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(),
74 HLClient: c,
75 }
76 }
77
78 func (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"
89 for i, srv := range ui.HLClient.Pref.Bookmarks {
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
96 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
97
98 ui.Pages.AddPage("joinServer", newJS, true, true)
99 })
100 }
101
102 return list
103 }
104
105 func (ui *UI) getTrackerList() *tview.List {
106 listing, err := GetListing(ui.HLClient.Pref.Tracker)
107 if err != nil {
108 // TODO
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()
123 srvName := srv.Name
124 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
125 ui.Pages.RemovePage("joinServer")
126
127 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
128
129 ui.Pages.AddPage("joinServer", newJS, true, true)
130 ui.Pages.ShowPage("joinServer")
131 })
132 }
133
134 return list
135 }
136
137 func (ui *UI) renderSettingsForm() *tview.Flex {
138 iconStr := strconv.Itoa(ui.HLClient.Pref.IconID)
139 settingsForm := tview.NewForm()
140 settingsForm.AddInputField("Your Name", ui.HLClient.Pref.Username, 0, nil, nil)
141 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
142 _, err := strconv.Atoi(idStr)
143 return err == nil
144 }, nil)
145 settingsForm.AddInputField("Tracker", ui.HLClient.Pref.Tracker, 0, nil, nil)
146 settingsForm.AddCheckbox("Enable Terminal Bell", ui.HLClient.Pref.EnableBell, nil)
147 settingsForm.AddButton("Save", func() {
148 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
149 if len(usernameInput) == 0 {
150 usernameInput = "unnamed"
151 }
152 ui.HLClient.Pref.Username = usernameInput
153 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
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()
157
158 out, err := yaml.Marshal(&ui.HLClient.Pref)
159 if err != nil {
160 // TODO: handle err
161 }
162 // TODO: handle err
163 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
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
190 func (ui *UI) joinServer(addr, login, password string) error {
191 // append default port to address if no port supplied
192 if len(strings.Split(addr, ":")) == 1 {
193 addr += ":5500"
194 }
195 if err := ui.HLClient.Connect(addr, login, password); err != nil {
196 return fmt.Errorf("Error joining server: %v\n", err)
197 }
198
199 go func() {
200 if err := ui.HLClient.HandleTransactions(); err != nil {
201 ui.Pages.SwitchToPage("home")
202 }
203
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")
211
212 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
213 ui.App.Draw()
214 }()
215
216 return nil
217 }
218
219 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
220 joinServerForm := tview.NewForm()
221 joinServerForm.
222 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
223 // return false
224 // }, nil).
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) {
229 ui.HLClient.Logger.Infow("saving bookmark")
230 // TODO: Implement bookmark saving
231
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)
234 if err != nil {
235 panic(err)
236 }
237
238 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
239 if err != nil {
240 panic(err)
241 }
242 // Pref := ui.HLClient.Pref
243 }).
244 AddButton("Cancel", func() {
245 ui.Pages.SwitchToPage(backPage)
246 }).
247 AddButton("Connect", func() {
248 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
249 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
250 err := ui.joinServer(
251 srvAddr,
252 loginInput,
253 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
254 )
255 if name == "" {
256 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
257 }
258 ui.HLClient.serverName = name
259
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
302 func (ui *UI) renderServerUI() *tview.Flex {
303 ui.chatBox.SetText("") // clear any previously existing chatbox text
304 commandList := tview.NewTextView().SetDynamicColors(true)
305 commandList.
306 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
307 SetBorder(true).
308 SetTitle("| Keyboard Shortcuts| ")
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()
317 ui.Pages.RemovePage(pageServerUI)
318 ui.Pages.SwitchToPage("home")
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)
331 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
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
337 // List files
338 if event.Key() == tcell.KeyCtrlF {
339 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
340 ui.HLClient.Logger.Errorw("err", "err", err)
341 }
342 }
343
344 // Show News
345 if event.Key() == tcell.KeyCtrlN {
346 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
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()
355 newsFlex.SetBorderPadding(0, 0, 1, 1)
356 newsPostTextArea := tview.NewTextView()
357 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
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 })
361
362 newsPostForm := tview.NewForm().
363 SetButtonsAlign(tview.AlignRight).
364 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
365 AddButton("Send", nil)
366 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
367 switch event.Key() {
368 case tcell.KeyEscape:
369 ui.Pages.RemovePage("newsInput")
370 case tcell.KeyTab:
371 ui.App.SetFocus(newsPostTextArea)
372 case tcell.KeyEnter:
373 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
374 if len(newsText) == 0 {
375 return event
376 }
377 err := ui.HLClient.Send(
378 *NewTransaction(TranOldPostNews, nil,
379 NewField(FieldData, []byte(newsText)),
380 ),
381 )
382 if err != nil {
383 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
384 // TODO: display errModal to user
385 }
386 ui.Pages.RemovePage("newsInput")
387 }
388
389 return event
390 })
391
392 newsFlex.
393 SetDirection(tview.FlexRow).
394 SetBorder(true).
395 SetTitle("| Post Message |")
396
397 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
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:
404 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
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:
416 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
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).
432 // AddItem(newsPostForm, 3, 0, false).
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
445 func (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() {
466 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
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 }