]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
First pass at file browsing
[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(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
224 joinServerForm := tview.NewForm()
225 joinServerForm.
226 AddInputField("Server", server, 0, nil, nil).
227 AddInputField("Login", login, 0, nil, nil).
228 AddPasswordField("Password", password, 0, '*', nil).
229 AddCheckbox("Save", save, func(checked bool) {
230 // TODO: Implement bookmark saving
231 }).
232 AddButton("Cancel", func() {
233 ui.Pages.SwitchToPage(backPage)
234 }).
235 AddButton("Connect", func() {
236 err := ui.joinServer(
237 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
238 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
239 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
240 )
241 if err != nil {
242 ui.HLClient.Logger.Errorw("login error", "err", err)
243 loginErrModal := tview.NewModal().
244 AddButtons([]string{"Oh no"}).
245 SetText(err.Error()).
246 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
247 ui.Pages.SwitchToPage(backPage)
248 })
249
250 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
251 }
252
253 // Save checkbox
254 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
255 // TODO: implement bookmark saving
256 }
257 })
258
259 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
260 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
261 if event.Key() == tcell.KeyEscape {
262 ui.Pages.SwitchToPage(backPage)
263 }
264 return event
265 })
266
267 if defaultConnect {
268 joinServerForm.SetFocus(5)
269 }
270
271 joinServerPage := tview.NewFlex().
272 AddItem(nil, 0, 1, false).
273 AddItem(tview.NewFlex().
274 SetDirection(tview.FlexRow).
275 AddItem(nil, 0, 1, false).
276 AddItem(joinServerForm, 14, 1, true).
277 AddItem(nil, 0, 1, false), 40, 1, true).
278 AddItem(nil, 0, 1, false)
279
280 return joinServerPage
281 }
282
283 func (ui *UI) renderServerUI() *tview.Flex {
284 commandList := tview.NewTextView().SetDynamicColors(true)
285 commandList.
286 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs\n").
287 SetBorder(true).
288 SetTitle("| Keyboard Shortcuts| ")
289
290 modal := tview.NewModal().
291 SetText("Disconnect from the server?").
292 AddButtons([]string{"Cancel", "Exit"}).
293 SetFocus(1)
294 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
295 if buttonIndex == 1 {
296 _ = ui.HLClient.Disconnect()
297 ui.Pages.SwitchToPage("home")
298 } else {
299 ui.Pages.HidePage("modal")
300 }
301 })
302
303 serverUI := tview.NewFlex().
304 AddItem(tview.NewFlex().
305 SetDirection(tview.FlexRow).
306 AddItem(commandList, 4, 0, false).
307 AddItem(ui.chatBox, 0, 8, false).
308 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
309 AddItem(ui.userList, 25, 1, false)
310 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
311 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
312 if event.Key() == tcell.KeyEscape {
313 ui.Pages.AddPage("modal", modal, false, true)
314 }
315
316 // List files
317 if event.Key() == tcell.KeyCtrlF {
318 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
319 ui.HLClient.Logger.Errorw("err", "err", err)
320 }
321 }
322
323 // Show News
324 if event.Key() == tcell.KeyCtrlN {
325 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
326 ui.HLClient.Logger.Errorw("err", "err", err)
327 }
328 }
329
330 // Post news
331 if event.Key() == tcell.KeyCtrlP {
332
333 newsFlex := tview.NewFlex()
334
335 newsPostTextArea := tview.NewTextView()
336 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
337 newsPostTextArea.SetChangedFunc(func() {
338 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
339 })
340 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
341
342 newsPostForm := tview.NewForm().
343 SetButtonsAlign(tview.AlignRight).
344 AddButton("Post", nil)
345 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
346 switch event.Key() {
347 case tcell.KeyTab:
348 ui.App.SetFocus(newsPostTextArea)
349 case tcell.KeyEnter:
350 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
351 err := ui.HLClient.Send(
352 *NewTransaction(tranOldPostNews, nil,
353 NewField(fieldData, []byte(newsText)),
354 ),
355 )
356 if err != nil {
357 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
358 // TODO: display errModal to user
359 }
360 //newsInput.SetText("") // clear the input field after chat send
361 ui.Pages.RemovePage("newsInput")
362 }
363
364 return event
365 })
366
367 newsFlex.
368 SetDirection(tview.FlexRow).
369 SetBorder(true).
370 SetTitle("News Post")
371
372 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
373 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
374 switch event.Key() {
375 case tcell.KeyEscape:
376 ui.Pages.RemovePage("newsInput")
377 case tcell.KeyTab:
378 ui.App.SetFocus(newsPostForm)
379 case tcell.KeyEnter:
380 fmt.Fprintf(newsPostTextArea, "\n")
381 default:
382 const windowsBackspaceRune = 8
383 const macBackspaceRune = 127
384 switch event.Rune() {
385 case macBackspaceRune, windowsBackspaceRune:
386 curTxt := newsPostTextArea.GetText(true)
387 if len(curTxt) > 0 {
388 curTxt = curTxt[:len(curTxt)-1]
389 newsPostTextArea.SetText(curTxt)
390 }
391 default:
392 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
393 }
394 }
395
396 return event
397 })
398
399 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
400 newsFlex.AddItem(newsPostForm, 3, 0, false)
401
402 newsPostPage := tview.NewFlex().
403 AddItem(nil, 0, 1, false).
404 AddItem(tview.NewFlex().
405 SetDirection(tview.FlexRow).
406 AddItem(nil, 0, 1, false).
407 AddItem(newsFlex, 15, 1, true).
408 //AddItem(newsPostForm, 3, 0, false).
409 AddItem(nil, 0, 1, false), 40, 1, false).
410 AddItem(nil, 0, 1, false)
411
412 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
413 ui.App.SetFocus(newsPostTextArea)
414 }
415
416 return event
417 })
418 return serverUI
419 }
420
421 func (ui *UI) Start() {
422 home := tview.NewFlex().SetDirection(tview.FlexRow)
423 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
424 mainMenu := tview.NewList()
425
426 bannerItem := tview.NewTextView().
427 SetText(randomBanner()).
428 SetDynamicColors(true).
429 SetTextAlign(tview.AlignCenter)
430
431 home.AddItem(
432 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
433 14, 1, false)
434 home.AddItem(tview.NewFlex().
435 AddItem(nil, 0, 1, false).
436 AddItem(mainMenu, 0, 1, true).
437 AddItem(nil, 0, 1, false),
438 0, 1, true,
439 )
440
441 mainMenu.AddItem("Join Server", "", 'j', func() {
442 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
443 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
444 }).
445 AddItem("Bookmarks", "", 'b', func() {
446 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
447 }).
448 AddItem("Browse Tracker", "", 't', func() {
449 ui.trackerList = ui.getTrackerList()
450 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
451 }).
452 AddItem("Settings", "", 's', func() {
453 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
454 }).
455 AddItem("Quit", "", 'q', func() {
456 ui.App.Stop()
457 })
458
459 ui.Pages.AddPage("home", home, true, true)
460
461 // App level input capture
462 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
463 if event.Key() == tcell.KeyCtrlC {
464 ui.HLClient.Logger.Infow("Exiting")
465 ui.App.Stop()
466 os.Exit(0)
467 }
468 // Show Logs
469 if event.Key() == tcell.KeyCtrlL {
470 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
471 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
472 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
473 if key == tcell.KeyEscape {
474 ui.Pages.RemovePage("logs")
475 }
476 })
477
478 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
479 }
480 return event
481 })
482
483 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
484 ui.App.Stop()
485 os.Exit(1)
486 }
487 }