]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
Initial bookmark creation
[rbdr/mobius] / hotline / ui.go
1 package hotline
2
3 import (
4 "errors"
5 "fmt"
6 "github.com/davecgh/go-spew/spew"
7 "github.com/gdamore/tcell/v2"
8 "github.com/rivo/tview"
9 "gopkg.in/yaml.v2"
10 "io"
11 "io/ioutil"
12 "os"
13 "strconv"
14 "strings"
15 )
16
17 type UI struct {
18 chatBox *tview.TextView
19 chatInput *tview.InputField
20 App *tview.Application
21 Pages *tview.Pages
22 userList *tview.TextView
23 agreeModal *tview.Modal
24 trackerList *tview.List
25 settingsPage *tview.Box
26 HLClient *Client
27 }
28
29 func NewUI(c *Client) *UI {
30 app := tview.NewApplication()
31 chatBox := tview.NewTextView().
32 SetScrollable(true).
33 SetDynamicColors(true).
34 SetWordWrap(true).
35 SetChangedFunc(func() {
36 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
37 })
38 chatBox.Box.SetBorder(true).SetTitle("Chat")
39
40 chatInput := tview.NewInputField()
41 chatInput.
42 SetLabel("> ").
43 SetFieldBackgroundColor(tcell.ColorDimGray).
44 SetDoneFunc(func(key tcell.Key) {
45 // skip send if user hit enter with no other text
46 if len(chatInput.GetText()) == 0 {
47 return
48 }
49
50 c.Send(
51 *NewTransaction(tranChatSend, nil,
52 NewField(fieldData, []byte(chatInput.GetText())),
53 ),
54 )
55 chatInput.SetText("") // clear the input field after chat send
56 })
57
58 chatInput.Box.SetBorder(true).SetTitle("Send")
59
60 userList := tview.
61 NewTextView().
62 SetDynamicColors(true).
63 SetChangedFunc(func() {
64 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
65 })
66 userList.Box.SetBorder(true).SetTitle("Users")
67
68 return &UI{
69 App: app,
70 chatBox: chatBox,
71 Pages: tview.NewPages(),
72 chatInput: chatInput,
73 userList: userList,
74 trackerList: tview.NewList(),
75 agreeModal: tview.NewModal(),
76 HLClient: c,
77 }
78 }
79
80 func (ui *UI) showBookmarks() *tview.List {
81 list := tview.NewList()
82 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
83 if event.Key() == tcell.KeyEsc {
84 ui.Pages.SwitchToPage("home")
85 }
86 return event
87 })
88 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
89
90 shortcut := 97 // rune for "a"
91 for i, srv := range ui.HLClient.pref.Bookmarks {
92 addr := srv.Addr
93 login := srv.Login
94 pass := srv.Password
95 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
96 ui.Pages.RemovePage("joinServer")
97
98 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
99
100 ui.Pages.AddPage("joinServer", newJS, true, true)
101 })
102 }
103
104 return list
105 }
106
107 func (ui *UI) getTrackerList() *tview.List {
108 listing, err := GetListing(ui.HLClient.pref.Tracker)
109 if err != nil {
110 spew.Dump(err)
111 }
112
113 list := tview.NewList()
114 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
115 if event.Key() == tcell.KeyEsc {
116 ui.Pages.SwitchToPage("home")
117 }
118 return event
119 })
120 list.Box.SetBorder(true).SetTitle("| Servers |")
121
122 shortcut := 97 // rune for "a"
123 for i, srv := range listing {
124 addr := srv.Addr()
125 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
126 ui.Pages.RemovePage("joinServer")
127
128 newJS := ui.renderJoinServerForm("", addr, GuestAccount, "", trackerListPage, false, true)
129
130 ui.Pages.AddPage("joinServer", newJS, true, true)
131 ui.Pages.ShowPage("joinServer")
132 })
133 }
134
135 return list
136 }
137
138 func (ui *UI) renderSettingsForm() *tview.Flex {
139 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
140 settingsForm := tview.NewForm()
141 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
142 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
143 _, err := strconv.Atoi(idStr)
144 return err == nil
145 }, nil)
146 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, 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
157 out, err := yaml.Marshal(&ui.HLClient.pref)
158 if err != nil {
159 // TODO: handle err
160 }
161 // TODO: handle err
162 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
163 if err != nil {
164 println(ui.HLClient.cfgPath)
165 panic(err)
166 }
167 ui.Pages.RemovePage("settings")
168 })
169 settingsForm.SetBorder(true)
170 settingsForm.SetCancelFunc(func() {
171 ui.Pages.RemovePage("settings")
172 })
173 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
174 settingsPage.Box.SetBorder(true).SetTitle("Settings")
175 settingsPage.AddItem(settingsForm, 0, 1, true)
176
177 centerFlex := tview.NewFlex().
178 AddItem(nil, 0, 1, false).
179 AddItem(tview.NewFlex().
180 SetDirection(tview.FlexRow).
181 AddItem(nil, 0, 1, false).
182 AddItem(settingsForm, 15, 1, true).
183 AddItem(nil, 0, 1, false), 40, 1, true).
184 AddItem(nil, 0, 1, false)
185
186 return centerFlex
187 }
188
189 func (ui *UI) joinServer(addr, login, password string) error {
190 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
191 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
192 }
193
194 go func() {
195 for {
196 err := ui.HLClient.ReadLoop()
197 if err != nil {
198 ui.HLClient.Logger.Errorw("read error", "err", err)
199
200 msg := err.Error()
201 if err == io.EOF {
202 msg = "The server connection has unexpectedly closed."
203 }
204
205 loginErrModal := tview.NewModal().
206 AddButtons([]string{"Ok"}).
207 SetText(msg).
208 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
209 ui.Pages.SwitchToPage("home")
210 })
211 loginErrModal.Box.SetTitle("Server Connection Error")
212
213 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
214 ui.App.Draw()
215 return
216 }
217 }
218 }()
219
220 return nil
221 }
222
223 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
224 joinServerForm := tview.NewForm()
225 joinServerForm.
226 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
227 // return false
228 //}, nil).
229 AddInputField("Server", server, 0, nil, nil).
230 AddInputField("Login", login, 0, nil, nil).
231 AddPasswordField("Password", password, 0, '*', nil).
232 AddCheckbox("Save", save, func(checked bool) {
233 ui.HLClient.Logger.Infow("saving bookmark")
234 // TODO: Implement bookmark saving
235
236 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())
237 out, err := yaml.Marshal(ui.HLClient.pref)
238 if err != nil {
239 panic(err)
240 }
241
242 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
243 if err != nil {
244 panic(err)
245 }
246 // pref := ui.HLClient.pref
247 }).
248 AddButton("Cancel", func() {
249 ui.Pages.SwitchToPage(backPage)
250 }).
251 AddButton("Connect", func() {
252 err := ui.joinServer(
253 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
254 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
255 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
256 )
257 if err != nil {
258 ui.HLClient.Logger.Errorw("login error", "err", err)
259 loginErrModal := tview.NewModal().
260 AddButtons([]string{"Oh no"}).
261 SetText(err.Error()).
262 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
263 ui.Pages.SwitchToPage(backPage)
264 })
265
266 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
267 }
268
269 // Save checkbox
270 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
271 // TODO: implement bookmark saving
272 }
273 })
274
275 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
276 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
277 if event.Key() == tcell.KeyEscape {
278 ui.Pages.SwitchToPage(backPage)
279 }
280 return event
281 })
282
283 if defaultConnect {
284 joinServerForm.SetFocus(5)
285 }
286
287 joinServerPage := tview.NewFlex().
288 AddItem(nil, 0, 1, false).
289 AddItem(tview.NewFlex().
290 SetDirection(tview.FlexRow).
291 AddItem(nil, 0, 1, false).
292 AddItem(joinServerForm, 14, 1, true).
293 AddItem(nil, 0, 1, false), 40, 1, true).
294 AddItem(nil, 0, 1, false)
295
296 return joinServerPage
297 }
298
299 func (ui *UI) renderServerUI() *tview.Flex {
300 commandList := tview.NewTextView().SetDynamicColors(true)
301 commandList.
302 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs\n").
303 SetBorder(true).
304 SetTitle("| Keyboard Shortcuts| ")
305
306 modal := tview.NewModal().
307 SetText("Disconnect from the server?").
308 AddButtons([]string{"Cancel", "Exit"}).
309 SetFocus(1)
310 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
311 if buttonIndex == 1 {
312 _ = ui.HLClient.Disconnect()
313 ui.Pages.SwitchToPage("home")
314 } else {
315 ui.Pages.HidePage("modal")
316 }
317 })
318
319 serverUI := tview.NewFlex().
320 AddItem(tview.NewFlex().
321 SetDirection(tview.FlexRow).
322 AddItem(commandList, 4, 0, false).
323 AddItem(ui.chatBox, 0, 8, false).
324 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
325 AddItem(ui.userList, 25, 1, false)
326 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
327 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
328 if event.Key() == tcell.KeyEscape {
329 ui.Pages.AddPage("modal", modal, false, true)
330 }
331
332 // List files
333 if event.Key() == tcell.KeyCtrlF {
334 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
335 ui.HLClient.Logger.Errorw("err", "err", err)
336 }
337 }
338
339 // Show News
340 if event.Key() == tcell.KeyCtrlN {
341 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
342 ui.HLClient.Logger.Errorw("err", "err", err)
343 }
344 }
345
346 // Post news
347 if event.Key() == tcell.KeyCtrlP {
348
349 newsFlex := tview.NewFlex()
350
351 newsPostTextArea := tview.NewTextView()
352 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
353 newsPostTextArea.SetChangedFunc(func() {
354 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
355 })
356 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
357
358 newsPostForm := tview.NewForm().
359 SetButtonsAlign(tview.AlignRight).
360 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
361 AddButton("Send", nil)
362 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
363 switch event.Key() {
364 case tcell.KeyTab:
365 ui.App.SetFocus(newsPostTextArea)
366 case tcell.KeyEnter:
367 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
368 err := ui.HLClient.Send(
369 *NewTransaction(tranOldPostNews, nil,
370 NewField(fieldData, []byte(newsText)),
371 ),
372 )
373 if err != nil {
374 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
375 // TODO: display errModal to user
376 }
377 ui.Pages.RemovePage("newsInput")
378 }
379
380 return event
381 })
382
383 newsFlex.
384 SetDirection(tview.FlexRow).
385 SetBorder(true).
386 SetTitle("| Post Message |")
387
388 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
389 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
390 switch event.Key() {
391 case tcell.KeyEscape:
392 ui.Pages.RemovePage("newsInput")
393 case tcell.KeyTab:
394 ui.App.SetFocus(newsPostForm)
395 case tcell.KeyEnter:
396 fmt.Fprintf(newsPostTextArea, "\n")
397 default:
398 const windowsBackspaceRune = 8
399 const macBackspaceRune = 127
400 switch event.Rune() {
401 case macBackspaceRune, windowsBackspaceRune:
402 curTxt := newsPostTextArea.GetText(true)
403 if len(curTxt) > 0 {
404 curTxt = curTxt[:len(curTxt)-1]
405 newsPostTextArea.SetText(curTxt)
406 }
407 default:
408 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
409 }
410 }
411
412 return event
413 })
414
415 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
416 newsFlex.AddItem(newsPostForm, 3, 0, false)
417
418 newsPostPage := tview.NewFlex().
419 AddItem(nil, 0, 1, false).
420 AddItem(tview.NewFlex().
421 SetDirection(tview.FlexRow).
422 AddItem(nil, 0, 1, false).
423 AddItem(newsFlex, 15, 1, true).
424 //AddItem(newsPostForm, 3, 0, false).
425 AddItem(nil, 0, 1, false), 40, 1, false).
426 AddItem(nil, 0, 1, false)
427
428 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
429 ui.App.SetFocus(newsPostTextArea)
430 }
431
432 return event
433 })
434 return serverUI
435 }
436
437 func (ui *UI) Start() {
438 home := tview.NewFlex().SetDirection(tview.FlexRow)
439 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
440 mainMenu := tview.NewList()
441
442 bannerItem := tview.NewTextView().
443 SetText(randomBanner()).
444 SetDynamicColors(true).
445 SetTextAlign(tview.AlignCenter)
446
447 home.AddItem(
448 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
449 14, 1, false)
450 home.AddItem(tview.NewFlex().
451 AddItem(nil, 0, 1, false).
452 AddItem(mainMenu, 0, 1, true).
453 AddItem(nil, 0, 1, false),
454 0, 1, true,
455 )
456
457 mainMenu.AddItem("Join Server", "", 'j', func() {
458 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
459 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
460 }).
461 AddItem("Bookmarks", "", 'b', func() {
462 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
463 }).
464 AddItem("Browse Tracker", "", 't', func() {
465 ui.trackerList = ui.getTrackerList()
466 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
467 }).
468 AddItem("Settings", "", 's', func() {
469 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
470 }).
471 AddItem("Quit", "", 'q', func() {
472 ui.App.Stop()
473 })
474
475 ui.Pages.AddPage("home", home, true, true)
476
477 // App level input capture
478 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
479 if event.Key() == tcell.KeyCtrlC {
480 ui.HLClient.Logger.Infow("Exiting")
481 ui.App.Stop()
482 os.Exit(0)
483 }
484 // Show Logs
485 if event.Key() == tcell.KeyCtrlL {
486 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
487 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
488 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
489 if key == tcell.KeyEscape {
490 ui.Pages.RemovePage("logs")
491 }
492 })
493
494 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
495 }
496 return event
497 })
498
499 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
500 ui.App.Stop()
501 os.Exit(1)
502 }
503 }