]> git.r.bdr.sh - rbdr/mobius/blame - hotline/ui.go
Default to port 5500 if undefined
[rbdr/mobius] / hotline / ui.go
CommitLineData
d80b37ca 1package hotline
b198b22b
JH
2
3import (
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
17type 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
29func 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
80func (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
da1e0d79 98 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
b198b22b
JH
99
100 ui.Pages.AddPage("joinServer", newJS, true, true)
101 })
102 }
103
104 return list
105}
106
107func (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
da1e0d79 128 newJS := ui.renderJoinServerForm("", addr, GuestAccount, "", trackerListPage, false, true)
b198b22b
JH
129
130 ui.Pages.AddPage("joinServer", newJS, true, true)
131 ui.Pages.ShowPage("joinServer")
132 })
133 }
134
135 return list
136}
137
138func (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
189func (ui *UI) joinServer(addr, login, password string) error {
58efb95f
JH
190 // append default port to address if no port supplied
191 if len(strings.Split(addr, ":")) == 1 {
192 addr += ":5500"
193 }
b198b22b
JH
194 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
195 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
196 }
197
198 go func() {
199 for {
200 err := ui.HLClient.ReadLoop()
201 if err != nil {
202 ui.HLClient.Logger.Errorw("read error", "err", err)
203
204 msg := err.Error()
205 if err == io.EOF {
206 msg = "The server connection has unexpectedly closed."
207 }
208
209 loginErrModal := tview.NewModal().
210 AddButtons([]string{"Ok"}).
211 SetText(msg).
212 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
213 ui.Pages.SwitchToPage("home")
214 })
215 loginErrModal.Box.SetTitle("Server Connection Error")
216
217 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
218 ui.App.Draw()
219 return
220 }
221 }
222 }()
223
224 return nil
225}
226
da1e0d79 227func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
b198b22b
JH
228 joinServerForm := tview.NewForm()
229 joinServerForm.
da1e0d79
JH
230 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
231 // return false
232 //}, nil).
b198b22b
JH
233 AddInputField("Server", server, 0, nil, nil).
234 AddInputField("Login", login, 0, nil, nil).
235 AddPasswordField("Password", password, 0, '*', nil).
236 AddCheckbox("Save", save, func(checked bool) {
da1e0d79 237 ui.HLClient.Logger.Infow("saving bookmark")
b198b22b 238 // TODO: Implement bookmark saving
da1e0d79
JH
239
240 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())
241 out, err := yaml.Marshal(ui.HLClient.pref)
242 if err != nil {
243 panic(err)
244 }
245
246 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
247 if err != nil {
248 panic(err)
249 }
250 // pref := ui.HLClient.pref
b198b22b
JH
251 }).
252 AddButton("Cancel", func() {
253 ui.Pages.SwitchToPage(backPage)
254 }).
255 AddButton("Connect", func() {
256 err := ui.joinServer(
257 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
258 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
259 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
260 )
261 if err != nil {
262 ui.HLClient.Logger.Errorw("login error", "err", err)
263 loginErrModal := tview.NewModal().
264 AddButtons([]string{"Oh no"}).
265 SetText(err.Error()).
266 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
267 ui.Pages.SwitchToPage(backPage)
268 })
269
270 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
271 }
272
273 // Save checkbox
274 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
275 // TODO: implement bookmark saving
276 }
277 })
278
279 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
280 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
281 if event.Key() == tcell.KeyEscape {
282 ui.Pages.SwitchToPage(backPage)
283 }
284 return event
285 })
286
287 if defaultConnect {
288 joinServerForm.SetFocus(5)
289 }
290
291 joinServerPage := tview.NewFlex().
292 AddItem(nil, 0, 1, false).
293 AddItem(tview.NewFlex().
294 SetDirection(tview.FlexRow).
295 AddItem(nil, 0, 1, false).
296 AddItem(joinServerForm, 14, 1, true).
297 AddItem(nil, 0, 1, false), 40, 1, true).
298 AddItem(nil, 0, 1, false)
299
300 return joinServerPage
301}
302
303func (ui *UI) renderServerUI() *tview.Flex {
304 commandList := tview.NewTextView().SetDynamicColors(true)
305 commandList.
306 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs\n").
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()
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 " + "TODO" + " |").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
43ecc0f4
JH
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
b198b22b
JH
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
353 newsFlex := tview.NewFlex()
354
355 newsPostTextArea := tview.NewTextView()
356 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
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 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
361
362 newsPostForm := tview.NewForm().
363 SetButtonsAlign(tview.AlignRight).
c1ab228e
JH
364 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
365 AddButton("Send", nil)
b198b22b
JH
366 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
367 switch event.Key() {
368 case tcell.KeyTab:
369 ui.App.SetFocus(newsPostTextArea)
370 case tcell.KeyEnter:
371 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
372 err := ui.HLClient.Send(
373 *NewTransaction(tranOldPostNews, nil,
374 NewField(fieldData, []byte(newsText)),
375 ),
376 )
377 if err != nil {
378 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
379 // TODO: display errModal to user
380 }
b198b22b
JH
381 ui.Pages.RemovePage("newsInput")
382 }
383
384 return event
385 })
386
387 newsFlex.
388 SetDirection(tview.FlexRow).
389 SetBorder(true).
c1ab228e 390 SetTitle("| Post Message |")
b198b22b
JH
391
392 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
393 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
394 switch event.Key() {
395 case tcell.KeyEscape:
396 ui.Pages.RemovePage("newsInput")
397 case tcell.KeyTab:
398 ui.App.SetFocus(newsPostForm)
399 case tcell.KeyEnter:
400 fmt.Fprintf(newsPostTextArea, "\n")
401 default:
402 const windowsBackspaceRune = 8
403 const macBackspaceRune = 127
404 switch event.Rune() {
405 case macBackspaceRune, windowsBackspaceRune:
406 curTxt := newsPostTextArea.GetText(true)
407 if len(curTxt) > 0 {
408 curTxt = curTxt[:len(curTxt)-1]
409 newsPostTextArea.SetText(curTxt)
410 }
411 default:
412 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
413 }
414 }
415
416 return event
417 })
418
419 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
420 newsFlex.AddItem(newsPostForm, 3, 0, false)
421
422 newsPostPage := tview.NewFlex().
423 AddItem(nil, 0, 1, false).
424 AddItem(tview.NewFlex().
425 SetDirection(tview.FlexRow).
426 AddItem(nil, 0, 1, false).
427 AddItem(newsFlex, 15, 1, true).
428 //AddItem(newsPostForm, 3, 0, false).
429 AddItem(nil, 0, 1, false), 40, 1, false).
430 AddItem(nil, 0, 1, false)
431
432 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
433 ui.App.SetFocus(newsPostTextArea)
434 }
435
436 return event
437 })
438 return serverUI
439}
440
441func (ui *UI) Start() {
442 home := tview.NewFlex().SetDirection(tview.FlexRow)
443 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
444 mainMenu := tview.NewList()
445
446 bannerItem := tview.NewTextView().
447 SetText(randomBanner()).
448 SetDynamicColors(true).
449 SetTextAlign(tview.AlignCenter)
450
451 home.AddItem(
452 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
453 14, 1, false)
454 home.AddItem(tview.NewFlex().
455 AddItem(nil, 0, 1, false).
456 AddItem(mainMenu, 0, 1, true).
457 AddItem(nil, 0, 1, false),
458 0, 1, true,
459 )
460
461 mainMenu.AddItem("Join Server", "", 'j', func() {
da1e0d79 462 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
b198b22b
JH
463 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
464 }).
465 AddItem("Bookmarks", "", 'b', func() {
466 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
467 }).
468 AddItem("Browse Tracker", "", 't', func() {
469 ui.trackerList = ui.getTrackerList()
470 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
471 }).
472 AddItem("Settings", "", 's', func() {
473 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
474 }).
475 AddItem("Quit", "", 'q', func() {
476 ui.App.Stop()
477 })
478
479 ui.Pages.AddPage("home", home, true, true)
480
481 // App level input capture
482 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
483 if event.Key() == tcell.KeyCtrlC {
484 ui.HLClient.Logger.Infow("Exiting")
485 ui.App.Stop()
486 os.Exit(0)
487 }
488 // Show Logs
489 if event.Key() == tcell.KeyCtrlL {
490 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
491 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
492 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
493 if key == tcell.KeyEscape {
494 ui.Pages.RemovePage("logs")
495 }
496 })
497
498 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
499 }
500 return event
501 })
502
503 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
504 ui.App.Stop()
505 os.Exit(1)
506 }
507}