]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
Make UserFlags public
[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("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) {
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 )
232
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 }).
243 AddButton("Cancel", func() {
244 ui.Pages.SwitchToPage(backPage)
245 }).
246 AddButton("Connect", func() {
247 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
248 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
249 err := ui.joinServer(
250 srvAddr,
251 loginInput,
252 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
253 )
254 if name == "" {
255 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
256 }
257 ui.HLClient.serverName = name
258
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
301 func (ui *UI) renderServerUI() *tview.Flex {
302 ui.chatBox.SetText("") // clear any previously existing chatbox text
303 commandList := tview.NewTextView().SetDynamicColors(true)
304 commandList.
305 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
306 SetBorder(true).
307 SetTitle("| Keyboard Shortcuts| ")
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()
316 ui.Pages.RemovePage(pageServerUI)
317 ui.Pages.SwitchToPage("home")
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)
330 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
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
336 // List files
337 if event.Key() == tcell.KeyCtrlF {
338 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
339 ui.HLClient.Logger.Errorw("err", "err", err)
340 }
341 }
342
343 // Show News
344 if event.Key() == tcell.KeyCtrlN {
345 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
346 ui.HLClient.Logger.Errorw("err", "err", err)
347 }
348 }
349
350 // Post news
351 if event.Key() == tcell.KeyCtrlP {
352 newsFlex := tview.NewFlex()
353 newsFlex.SetBorderPadding(0, 0, 1, 1)
354 newsPostTextArea := tview.NewTextView()
355 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
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 })
359
360 newsPostForm := tview.NewForm().
361 SetButtonsAlign(tview.AlignRight).
362 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
363 AddButton("Send", nil)
364 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
365 switch event.Key() {
366 case tcell.KeyEscape:
367 ui.Pages.RemovePage("newsInput")
368 case tcell.KeyTab:
369 ui.App.SetFocus(newsPostTextArea)
370 case tcell.KeyEnter:
371 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
372 if len(newsText) == 0 {
373 return event
374 }
375 err := ui.HLClient.Send(
376 *NewTransaction(TranOldPostNews, nil,
377 NewField(FieldData, []byte(newsText)),
378 ),
379 )
380 if err != nil {
381 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
382 // TODO: display errModal to user
383 }
384 ui.Pages.RemovePage("newsInput")
385 }
386
387 return event
388 })
389
390 newsFlex.
391 SetDirection(tview.FlexRow).
392 SetBorder(true).
393 SetTitle("| Post Message |")
394
395 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
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:
402 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
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:
414 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
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).
430 // AddItem(newsPostForm, 3, 0, false).
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
443 func (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() {
464 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
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 }