]> git.r.bdr.sh - rbdr/mobius/blame - hotline/ui.go
Use strings.ReplaceAll method
[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 {
b198b22b 353 newsFlex := tview.NewFlex()
e75ba43a 354 newsFlex.SetBorderPadding(0, 0, 1, 1)
b198b22b 355 newsPostTextArea := tview.NewTextView()
e75ba43a 356 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
b198b22b
JH
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 })
b198b22b
JH
360
361 newsPostForm := tview.NewForm().
362 SetButtonsAlign(tview.AlignRight).
aebc4d36 363 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
c1ab228e 364 AddButton("Send", nil)
b198b22b
JH
365 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
366 switch event.Key() {
e75ba43a
JH
367 case tcell.KeyEscape:
368 ui.Pages.RemovePage("newsInput")
b198b22b
JH
369 case tcell.KeyTab:
370 ui.App.SetFocus(newsPostTextArea)
371 case tcell.KeyEnter:
372 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
e75ba43a
JH
373 if len(newsText) == 0 {
374 return event
375 }
b198b22b 376 err := ui.HLClient.Send(
d005ef04
JH
377 *NewTransaction(TranOldPostNews, nil,
378 NewField(FieldData, []byte(newsText)),
b198b22b
JH
379 ),
380 )
381 if err != nil {
382 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
383 // TODO: display errModal to user
384 }
b198b22b
JH
385 ui.Pages.RemovePage("newsInput")
386 }
387
388 return event
389 })
390
391 newsFlex.
392 SetDirection(tview.FlexRow).
393 SetBorder(true).
c1ab228e 394 SetTitle("| Post Message |")
b198b22b
JH
395
396 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
b198b22b
JH
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:
e75ba43a 403 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
b198b22b
JH
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:
e75ba43a 415 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
b198b22b
JH
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).
aebc4d36 431 // AddItem(newsPostForm, 3, 0, false).
b198b22b
JH
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
444func (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() {
da1e0d79 465 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
b198b22b
JH
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}