6 "github.com/gdamore/tcell/v2"
7 "github.com/rivo/tview"
16 chatBox *tview.TextView
17 chatInput *tview.InputField
18 App *tview.Application
20 userList *tview.TextView
21 trackerList *tview.List
27 pageServerUI = "serverUI"
30 func NewUI(c *Client) *UI {
31 app := tview.NewApplication()
32 chatBox := tview.NewTextView().
34 SetDynamicColors(true).
36 SetChangedFunc(func() {
37 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
39 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
41 chatInput := tview.NewInputField()
44 SetFieldBackgroundColor(tcell.ColorDimGray).
45 SetDoneFunc(func(key tcell.Key) {
46 // skip send if user hit enter with no other text
47 if len(chatInput.GetText()) == 0 {
52 *NewTransaction(tranChatSend, nil,
53 NewField(fieldData, []byte(chatInput.GetText())),
56 chatInput.SetText("") // clear the input field after chat send
59 chatInput.Box.SetBorder(true).SetTitle("Send")
63 SetDynamicColors(true).
64 SetChangedFunc(func() {
65 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
67 userList.Box.SetBorder(true).SetTitle("Users")
72 Pages: tview.NewPages(),
75 trackerList: tview.NewList(),
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")
88 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
90 shortcut := 97 // rune for "a"
91 for i, srv := range ui.HLClient.pref.Bookmarks {
95 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
96 ui.Pages.RemovePage("joinServer")
98 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
100 ui.Pages.AddPage("joinServer", newJS, true, true)
107 func (ui *UI) getTrackerList() *tview.List {
108 listing, err := GetListing(ui.HLClient.pref.Tracker)
113 list := tview.NewList()
114 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
115 if event.Key() == tcell.KeyEsc {
116 ui.Pages.SwitchToPage("home")
120 list.Box.SetBorder(true).SetTitle("| Servers |")
122 shortcut := 97 // rune for "a"
123 for i, srv := range listing {
126 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
127 ui.Pages.RemovePage("joinServer")
129 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
131 ui.Pages.AddPage("joinServer", newJS, true, true)
132 ui.Pages.ShowPage("joinServer")
139 func (ui *UI) renderSettingsForm() *tview.Flex {
140 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
141 settingsForm := tview.NewForm()
142 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
143 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
144 _, err := strconv.Atoi(idStr)
147 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
148 settingsForm.AddButton("Save", func() {
149 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
150 if len(usernameInput) == 0 {
151 usernameInput = "unnamed"
153 ui.HLClient.pref.Username = usernameInput
154 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
155 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
156 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
158 out, err := yaml.Marshal(&ui.HLClient.pref)
163 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
165 println(ui.HLClient.cfgPath)
168 ui.Pages.RemovePage("settings")
170 settingsForm.SetBorder(true)
171 settingsForm.SetCancelFunc(func() {
172 ui.Pages.RemovePage("settings")
174 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
175 settingsPage.Box.SetBorder(true).SetTitle("Settings")
176 settingsPage.AddItem(settingsForm, 0, 1, true)
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)
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 {
195 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
196 return fmt.Errorf("Error joining server: %v\n", err)
200 // Create a new scanner for parsing incoming bytes into transaction tokens
201 scanner := bufio.NewScanner(ui.HLClient.Connection)
202 scanner.Split(transactionScanner)
204 // Scan for new transactions and handle them as they come in.
206 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
207 // scanner re-uses the buffer for subsequent scans.
208 buf := make([]byte, len(scanner.Bytes()))
209 copy(buf, scanner.Bytes())
212 _, err := t.Write(buf)
216 if err := ui.HLClient.HandleTransaction(&t); err != nil {
217 ui.HLClient.Logger.Errorw("Error handling transaction", "err", err)
221 if scanner.Err() == nil {
222 loginErrModal := tview.NewModal().
223 AddButtons([]string{"Ok"}).
224 SetText("The server connection has closed.").
225 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
226 ui.Pages.SwitchToPage("home")
228 loginErrModal.Box.SetTitle("Server Connection Error")
230 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
234 ui.Pages.SwitchToPage("home")
241 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
242 joinServerForm := tview.NewForm()
244 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
247 AddInputField("Server", server, 0, nil, nil).
248 AddInputField("Login", login, 0, nil, nil).
249 AddPasswordField("Password", password, 0, '*', nil).
250 AddCheckbox("Save", save, func(checked bool) {
251 ui.HLClient.Logger.Infow("saving bookmark")
252 // TODO: Implement bookmark saving
254 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())
255 out, err := yaml.Marshal(ui.HLClient.pref)
260 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
264 // pref := ui.HLClient.pref
266 AddButton("Cancel", func() {
267 ui.Pages.SwitchToPage(backPage)
269 AddButton("Connect", func() {
270 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
271 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
272 err := ui.joinServer(
275 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
278 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
280 ui.HLClient.serverName = name
283 ui.HLClient.Logger.Errorw("login error", "err", err)
284 loginErrModal := tview.NewModal().
285 AddButtons([]string{"Oh no"}).
286 SetText(err.Error()).
287 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
288 ui.Pages.SwitchToPage(backPage)
291 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
295 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
296 // TODO: implement bookmark saving
300 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
301 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
302 if event.Key() == tcell.KeyEscape {
303 ui.Pages.SwitchToPage(backPage)
309 joinServerForm.SetFocus(5)
312 joinServerPage := tview.NewFlex().
313 AddItem(nil, 0, 1, false).
314 AddItem(tview.NewFlex().
315 SetDirection(tview.FlexRow).
316 AddItem(nil, 0, 1, false).
317 AddItem(joinServerForm, 14, 1, true).
318 AddItem(nil, 0, 1, false), 40, 1, true).
319 AddItem(nil, 0, 1, false)
321 return joinServerPage
324 func (ui *UI) renderServerUI() *tview.Flex {
325 ui.chatBox.SetText("") // clear any previously existing chatbox text
326 commandList := tview.NewTextView().SetDynamicColors(true)
328 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
330 SetTitle("| Keyboard Shortcuts| ")
332 modal := tview.NewModal().
333 SetText("Disconnect from the server?").
334 AddButtons([]string{"Cancel", "Exit"}).
336 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
337 if buttonIndex == 1 {
338 _ = ui.HLClient.Disconnect()
339 ui.Pages.RemovePage(pageServerUI)
340 ui.Pages.SwitchToPage("home")
342 ui.Pages.HidePage("modal")
346 serverUI := tview.NewFlex().
347 AddItem(tview.NewFlex().
348 SetDirection(tview.FlexRow).
349 AddItem(commandList, 4, 0, false).
350 AddItem(ui.chatBox, 0, 8, false).
351 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
352 AddItem(ui.userList, 25, 1, false)
353 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
354 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
355 if event.Key() == tcell.KeyEscape {
356 ui.Pages.AddPage("modal", modal, false, true)
360 if event.Key() == tcell.KeyCtrlF {
361 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
362 ui.HLClient.Logger.Errorw("err", "err", err)
367 if event.Key() == tcell.KeyCtrlN {
368 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
369 ui.HLClient.Logger.Errorw("err", "err", err)
374 if event.Key() == tcell.KeyCtrlP {
376 newsFlex := tview.NewFlex()
377 newsFlex.SetBorderPadding(0, 0, 1, 1)
378 newsPostTextArea := tview.NewTextView()
379 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
380 newsPostTextArea.SetChangedFunc(func() {
381 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
384 newsPostForm := tview.NewForm().
385 SetButtonsAlign(tview.AlignRight).
386 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
387 AddButton("Send", nil)
388 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
390 case tcell.KeyEscape:
391 ui.Pages.RemovePage("newsInput")
393 ui.App.SetFocus(newsPostTextArea)
395 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
396 if len(newsText) == 0 {
399 err := ui.HLClient.Send(
400 *NewTransaction(tranOldPostNews, nil,
401 NewField(fieldData, []byte(newsText)),
405 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
406 // TODO: display errModal to user
408 ui.Pages.RemovePage("newsInput")
415 SetDirection(tview.FlexRow).
417 SetTitle("| Post Message |")
419 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
421 case tcell.KeyEscape:
422 ui.Pages.RemovePage("newsInput")
424 ui.App.SetFocus(newsPostForm)
426 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
428 const windowsBackspaceRune = 8
429 const macBackspaceRune = 127
430 switch event.Rune() {
431 case macBackspaceRune, windowsBackspaceRune:
432 curTxt := newsPostTextArea.GetText(true)
434 curTxt = curTxt[:len(curTxt)-1]
435 newsPostTextArea.SetText(curTxt)
438 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
445 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
446 newsFlex.AddItem(newsPostForm, 3, 0, false)
448 newsPostPage := tview.NewFlex().
449 AddItem(nil, 0, 1, false).
450 AddItem(tview.NewFlex().
451 SetDirection(tview.FlexRow).
452 AddItem(nil, 0, 1, false).
453 AddItem(newsFlex, 15, 1, true).
454 // AddItem(newsPostForm, 3, 0, false).
455 AddItem(nil, 0, 1, false), 40, 1, false).
456 AddItem(nil, 0, 1, false)
458 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
459 ui.App.SetFocus(newsPostTextArea)
467 func (ui *UI) Start() {
468 home := tview.NewFlex().SetDirection(tview.FlexRow)
469 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
470 mainMenu := tview.NewList()
472 bannerItem := tview.NewTextView().
473 SetText(randomBanner()).
474 SetDynamicColors(true).
475 SetTextAlign(tview.AlignCenter)
478 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
480 home.AddItem(tview.NewFlex().
481 AddItem(nil, 0, 1, false).
482 AddItem(mainMenu, 0, 1, true).
483 AddItem(nil, 0, 1, false),
487 mainMenu.AddItem("Join Server", "", 'j', func() {
488 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
489 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
491 AddItem("Bookmarks", "", 'b', func() {
492 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
494 AddItem("Browse Tracker", "", 't', func() {
495 ui.trackerList = ui.getTrackerList()
496 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
498 AddItem("Settings", "", 's', func() {
499 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
501 AddItem("Quit", "", 'q', func() {
505 ui.Pages.AddPage("home", home, true, true)
507 // App level input capture
508 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
509 if event.Key() == tcell.KeyCtrlC {
510 ui.HLClient.Logger.Infow("Exiting")
515 if event.Key() == tcell.KeyCtrlL {
516 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
517 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
518 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
519 if key == tcell.KeyEscape {
520 ui.Pages.RemovePage("logs")
524 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
529 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {