6 "github.com/davecgh/go-spew/spew"
7 "github.com/gdamore/tcell/v2"
8 "github.com/rivo/tview"
18 chatBox *tview.TextView
19 chatInput *tview.InputField
20 App *tview.Application
22 userList *tview.TextView
23 agreeModal *tview.Modal
24 trackerList *tview.List
30 pageServerUI = "serverUI"
33 func NewUI(c *Client) *UI {
34 app := tview.NewApplication()
35 chatBox := tview.NewTextView().
37 SetDynamicColors(true).
39 SetChangedFunc(func() {
40 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
42 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
44 chatInput := tview.NewInputField()
47 SetFieldBackgroundColor(tcell.ColorDimGray).
48 SetDoneFunc(func(key tcell.Key) {
49 // skip send if user hit enter with no other text
50 if len(chatInput.GetText()) == 0 {
55 *NewTransaction(tranChatSend, nil,
56 NewField(fieldData, []byte(chatInput.GetText())),
59 chatInput.SetText("") // clear the input field after chat send
62 chatInput.Box.SetBorder(true).SetTitle("Send")
66 SetDynamicColors(true).
67 SetChangedFunc(func() {
68 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
70 userList.Box.SetBorder(true).SetTitle("Users")
75 Pages: tview.NewPages(),
78 trackerList: tview.NewList(),
79 agreeModal: tview.NewModal(),
84 func (ui *UI) showBookmarks() *tview.List {
85 list := tview.NewList()
86 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
87 if event.Key() == tcell.KeyEsc {
88 ui.Pages.SwitchToPage("home")
92 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
94 shortcut := 97 // rune for "a"
95 for i, srv := range ui.HLClient.pref.Bookmarks {
99 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
100 ui.Pages.RemovePage("joinServer")
102 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
104 ui.Pages.AddPage("joinServer", newJS, true, true)
111 func (ui *UI) getTrackerList() *tview.List {
112 listing, err := GetListing(ui.HLClient.pref.Tracker)
117 list := tview.NewList()
118 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
119 if event.Key() == tcell.KeyEsc {
120 ui.Pages.SwitchToPage("home")
124 list.Box.SetBorder(true).SetTitle("| Servers |")
126 shortcut := 97 // rune for "a"
127 for i, srv := range listing {
130 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
131 ui.Pages.RemovePage("joinServer")
133 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
135 ui.Pages.AddPage("joinServer", newJS, true, true)
136 ui.Pages.ShowPage("joinServer")
143 func (ui *UI) renderSettingsForm() *tview.Flex {
144 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
145 settingsForm := tview.NewForm()
146 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
147 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
148 _, err := strconv.Atoi(idStr)
151 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
152 settingsForm.AddButton("Save", func() {
153 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
154 if len(usernameInput) == 0 {
155 usernameInput = "unnamed"
157 ui.HLClient.pref.Username = usernameInput
158 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
159 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
160 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
162 out, err := yaml.Marshal(&ui.HLClient.pref)
167 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
169 println(ui.HLClient.cfgPath)
172 ui.Pages.RemovePage("settings")
174 settingsForm.SetBorder(true)
175 settingsForm.SetCancelFunc(func() {
176 ui.Pages.RemovePage("settings")
178 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
179 settingsPage.Box.SetBorder(true).SetTitle("Settings")
180 settingsPage.AddItem(settingsForm, 0, 1, true)
182 centerFlex := tview.NewFlex().
183 AddItem(nil, 0, 1, false).
184 AddItem(tview.NewFlex().
185 SetDirection(tview.FlexRow).
186 AddItem(nil, 0, 1, false).
187 AddItem(settingsForm, 15, 1, true).
188 AddItem(nil, 0, 1, false), 40, 1, true).
189 AddItem(nil, 0, 1, false)
194 func (ui *UI) joinServer(addr, login, password string) error {
195 // append default port to address if no port supplied
196 if len(strings.Split(addr, ":")) == 1 {
199 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
200 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
205 err := ui.HLClient.ReadLoop()
207 ui.HLClient.Logger.Errorw("read error", "err", err)
210 loginErrModal := tview.NewModal().
211 AddButtons([]string{"Ok"}).
212 SetText("The server connection has closed.").
213 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
214 ui.Pages.SwitchToPage("home")
216 loginErrModal.Box.SetTitle("Server Connection Error")
218 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
222 ui.Pages.SwitchToPage("home")
232 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
233 joinServerForm := tview.NewForm()
235 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
238 AddInputField("Server", server, 0, nil, nil).
239 AddInputField("Login", login, 0, nil, nil).
240 AddPasswordField("Password", password, 0, '*', nil).
241 AddCheckbox("Save", save, func(checked bool) {
242 ui.HLClient.Logger.Infow("saving bookmark")
243 // TODO: Implement bookmark saving
245 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())
246 out, err := yaml.Marshal(ui.HLClient.pref)
251 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
255 // pref := ui.HLClient.pref
257 AddButton("Cancel", func() {
258 ui.Pages.SwitchToPage(backPage)
260 AddButton("Connect", func() {
261 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
262 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
263 err := ui.joinServer(
266 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
269 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
271 ui.HLClient.serverName = name
274 ui.HLClient.Logger.Errorw("login error", "err", err)
275 loginErrModal := tview.NewModal().
276 AddButtons([]string{"Oh no"}).
277 SetText(err.Error()).
278 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
279 ui.Pages.SwitchToPage(backPage)
282 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
286 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
287 // TODO: implement bookmark saving
291 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
292 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
293 if event.Key() == tcell.KeyEscape {
294 ui.Pages.SwitchToPage(backPage)
300 joinServerForm.SetFocus(5)
303 joinServerPage := tview.NewFlex().
304 AddItem(nil, 0, 1, false).
305 AddItem(tview.NewFlex().
306 SetDirection(tview.FlexRow).
307 AddItem(nil, 0, 1, false).
308 AddItem(joinServerForm, 14, 1, true).
309 AddItem(nil, 0, 1, false), 40, 1, true).
310 AddItem(nil, 0, 1, false)
312 return joinServerPage
315 func (ui *UI) renderServerUI() *tview.Flex {
316 ui.chatBox.SetText("") // clear any previously existing chatbox text
317 commandList := tview.NewTextView().SetDynamicColors(true)
319 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
321 SetTitle("| Keyboard Shortcuts| ")
323 modal := tview.NewModal().
324 SetText("Disconnect from the server?").
325 AddButtons([]string{"Cancel", "Exit"}).
327 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
328 if buttonIndex == 1 {
329 _ = ui.HLClient.Disconnect()
330 ui.Pages.RemovePage(pageServerUI)
332 ui.Pages.HidePage("modal")
336 serverUI := tview.NewFlex().
337 AddItem(tview.NewFlex().
338 SetDirection(tview.FlexRow).
339 AddItem(commandList, 4, 0, false).
340 AddItem(ui.chatBox, 0, 8, false).
341 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
342 AddItem(ui.userList, 25, 1, false)
343 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
344 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
345 if event.Key() == tcell.KeyEscape {
346 ui.Pages.AddPage("modal", modal, false, true)
350 if event.Key() == tcell.KeyCtrlF {
351 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
352 ui.HLClient.Logger.Errorw("err", "err", err)
357 if event.Key() == tcell.KeyCtrlN {
358 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
359 ui.HLClient.Logger.Errorw("err", "err", err)
364 if event.Key() == tcell.KeyCtrlP {
366 newsFlex := tview.NewFlex()
367 newsFlex.SetBorderPadding(0, 0, 1, 1)
368 newsPostTextArea := tview.NewTextView()
369 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
370 newsPostTextArea.SetChangedFunc(func() {
371 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
374 newsPostForm := tview.NewForm().
375 SetButtonsAlign(tview.AlignRight).
376 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
377 AddButton("Send", nil)
378 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
380 case tcell.KeyEscape:
381 ui.Pages.RemovePage("newsInput")
383 ui.App.SetFocus(newsPostTextArea)
385 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
386 if len(newsText) == 0 {
389 err := ui.HLClient.Send(
390 *NewTransaction(tranOldPostNews, nil,
391 NewField(fieldData, []byte(newsText)),
395 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
396 // TODO: display errModal to user
398 ui.Pages.RemovePage("newsInput")
405 SetDirection(tview.FlexRow).
407 SetTitle("| Post Message |")
409 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
411 case tcell.KeyEscape:
412 ui.Pages.RemovePage("newsInput")
414 ui.App.SetFocus(newsPostForm)
416 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
418 const windowsBackspaceRune = 8
419 const macBackspaceRune = 127
420 switch event.Rune() {
421 case macBackspaceRune, windowsBackspaceRune:
422 curTxt := newsPostTextArea.GetText(true)
424 curTxt = curTxt[:len(curTxt)-1]
425 newsPostTextArea.SetText(curTxt)
428 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
435 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
436 newsFlex.AddItem(newsPostForm, 3, 0, false)
438 newsPostPage := tview.NewFlex().
439 AddItem(nil, 0, 1, false).
440 AddItem(tview.NewFlex().
441 SetDirection(tview.FlexRow).
442 AddItem(nil, 0, 1, false).
443 AddItem(newsFlex, 15, 1, true).
444 //AddItem(newsPostForm, 3, 0, false).
445 AddItem(nil, 0, 1, false), 40, 1, false).
446 AddItem(nil, 0, 1, false)
448 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
449 ui.App.SetFocus(newsPostTextArea)
457 func (ui *UI) Start() {
458 home := tview.NewFlex().SetDirection(tview.FlexRow)
459 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
460 mainMenu := tview.NewList()
462 bannerItem := tview.NewTextView().
463 SetText(randomBanner()).
464 SetDynamicColors(true).
465 SetTextAlign(tview.AlignCenter)
468 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
470 home.AddItem(tview.NewFlex().
471 AddItem(nil, 0, 1, false).
472 AddItem(mainMenu, 0, 1, true).
473 AddItem(nil, 0, 1, false),
477 mainMenu.AddItem("Join Server", "", 'j', func() {
478 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
479 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
481 AddItem("Bookmarks", "", 'b', func() {
482 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
484 AddItem("Browse Tracker", "", 't', func() {
485 ui.trackerList = ui.getTrackerList()
486 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
488 AddItem("Settings", "", 's', func() {
489 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
491 AddItem("Quit", "", 'q', func() {
495 ui.Pages.AddPage("home", home, true, true)
497 // App level input capture
498 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
499 if event.Key() == tcell.KeyCtrlC {
500 ui.HLClient.Logger.Infow("Exiting")
505 if event.Key() == tcell.KeyCtrlL {
506 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
507 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
508 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
509 if key == tcell.KeyEscape {
510 ui.Pages.RemovePage("logs")
514 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
519 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {