]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
Use strings.ReplaceAll method
[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 newsFlex := tview.NewFlex()
354 newsFlex.SetBorderPadding(0, 0, 1, 1)
355 newsPostTextArea := tview.NewTextView()
356 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
357 newsPostTextArea.SetChangedFunc(func() {
358 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
359 })
360
361 newsPostForm := tview.NewForm().
362 SetButtonsAlign(tview.AlignRight).
363 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
364 AddButton("Send", nil)
365 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
366 switch event.Key() {
367 case tcell.KeyEscape:
368 ui.Pages.RemovePage("newsInput")
369 case tcell.KeyTab:
370 ui.App.SetFocus(newsPostTextArea)
371 case tcell.KeyEnter:
372 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
373 if len(newsText) == 0 {
374 return event
375 }
376 err := ui.HLClient.Send(
377 *NewTransaction(TranOldPostNews, nil,
378 NewField(FieldData, []byte(newsText)),
379 ),
380 )
381 if err != nil {
382 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
383 // TODO: display errModal to user
384 }
385 ui.Pages.RemovePage("newsInput")
386 }
387
388 return event
389 })
390
391 newsFlex.
392 SetDirection(tview.FlexRow).
393 SetBorder(true).
394 SetTitle("| Post Message |")
395
396 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
397 switch event.Key() {
398 case tcell.KeyEscape:
399 ui.Pages.RemovePage("newsInput")
400 case tcell.KeyTab:
401 ui.App.SetFocus(newsPostForm)
402 case tcell.KeyEnter:
403 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
404 default:
405 const windowsBackspaceRune = 8
406 const macBackspaceRune = 127
407 switch event.Rune() {
408 case macBackspaceRune, windowsBackspaceRune:
409 curTxt := newsPostTextArea.GetText(true)
410 if len(curTxt) > 0 {
411 curTxt = curTxt[:len(curTxt)-1]
412 newsPostTextArea.SetText(curTxt)
413 }
414 default:
415 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
416 }
417 }
418
419 return event
420 })
421
422 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
423 newsFlex.AddItem(newsPostForm, 3, 0, false)
424
425 newsPostPage := tview.NewFlex().
426 AddItem(nil, 0, 1, false).
427 AddItem(tview.NewFlex().
428 SetDirection(tview.FlexRow).
429 AddItem(nil, 0, 1, false).
430 AddItem(newsFlex, 15, 1, true).
431 // AddItem(newsPostForm, 3, 0, false).
432 AddItem(nil, 0, 1, false), 40, 1, false).
433 AddItem(nil, 0, 1, false)
434
435 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
436 ui.App.SetFocus(newsPostTextArea)
437 }
438
439 return event
440 })
441 return serverUI
442 }
443
444 func (ui *UI) Start() {
445 home := tview.NewFlex().SetDirection(tview.FlexRow)
446 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
447 mainMenu := tview.NewList()
448
449 bannerItem := tview.NewTextView().
450 SetText(randomBanner()).
451 SetDynamicColors(true).
452 SetTextAlign(tview.AlignCenter)
453
454 home.AddItem(
455 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
456 14, 1, false)
457 home.AddItem(tview.NewFlex().
458 AddItem(nil, 0, 1, false).
459 AddItem(mainMenu, 0, 1, true).
460 AddItem(nil, 0, 1, false),
461 0, 1, true,
462 )
463
464 mainMenu.AddItem("Join Server", "", 'j', func() {
465 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
466 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
467 }).
468 AddItem("Bookmarks", "", 'b', func() {
469 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
470 }).
471 AddItem("Browse Tracker", "", 't', func() {
472 ui.trackerList = ui.getTrackerList()
473 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
474 }).
475 AddItem("Settings", "", 's', func() {
476 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
477 }).
478 AddItem("Quit", "", 'q', func() {
479 ui.App.Stop()
480 })
481
482 ui.Pages.AddPage("home", home, true, true)
483
484 // App level input capture
485 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
486 if event.Key() == tcell.KeyCtrlC {
487 ui.HLClient.Logger.Infow("Exiting")
488 ui.App.Stop()
489 os.Exit(0)
490 }
491 // Show Logs
492 if event.Key() == tcell.KeyCtrlL {
493 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
494 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
495 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
496 if key == tcell.KeyEscape {
497 ui.Pages.RemovePage("logs")
498 }
499 })
500
501 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
502 }
503 return event
504 })
505
506 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
507 ui.App.Stop()
508 os.Exit(1)
509 }
510 }