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
25 settingsPage *tview.Box
29 func NewUI(c *Client) *UI {
30 app := tview.NewApplication()
31 chatBox := tview.NewTextView().
33 SetDynamicColors(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??
38 chatBox.Box.SetBorder(true).SetTitle("Chat")
40 chatInput := tview.NewInputField()
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 {
51 *NewTransaction(tranChatSend, nil,
52 NewField(fieldData, []byte(chatInput.GetText())),
55 chatInput.SetText("") // clear the input field after chat send
58 chatInput.Box.SetBorder(true).SetTitle("Send")
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??
66 userList.Box.SetBorder(true).SetTitle("Users")
71 Pages: tview.NewPages(),
74 trackerList: tview.NewList(),
75 agreeModal: tview.NewModal(),
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 {
125 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
126 ui.Pages.RemovePage("joinServer")
128 newJS := ui.renderJoinServerForm("", addr, GuestAccount, "", trackerListPage, false, true)
130 ui.Pages.AddPage("joinServer", newJS, true, true)
131 ui.Pages.ShowPage("joinServer")
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)
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"
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()
157 out, err := yaml.Marshal(&ui.HLClient.pref)
162 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
164 println(ui.HLClient.cfgPath)
167 ui.Pages.RemovePage("settings")
169 settingsForm.SetBorder(true)
170 settingsForm.SetCancelFunc(func() {
171 ui.Pages.RemovePage("settings")
173 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
174 settingsPage.Box.SetBorder(true).SetTitle("Settings")
175 settingsPage.AddItem(settingsForm, 0, 1, true)
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)
189 func (ui *UI) joinServer(addr, login, password string) error {
190 // append default port to address if no port supplied
191 if len(strings.Split(addr, ":")) == 1 {
194 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
195 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
200 err := ui.HLClient.ReadLoop()
202 ui.HLClient.Logger.Errorw("read error", "err", err)
206 msg = "The server connection has unexpectedly closed."
209 loginErrModal := tview.NewModal().
210 AddButtons([]string{"Ok"}).
212 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
213 ui.Pages.SwitchToPage("home")
215 loginErrModal.Box.SetTitle("Server Connection Error")
217 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
227 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
228 joinServerForm := tview.NewForm()
230 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
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) {
237 ui.HLClient.Logger.Infow("saving bookmark")
238 // TODO: Implement bookmark saving
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)
246 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
250 // pref := ui.HLClient.pref
252 AddButton("Cancel", func() {
253 ui.Pages.SwitchToPage(backPage)
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(),
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)
270 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
274 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
275 // TODO: implement bookmark saving
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)
288 joinServerForm.SetFocus(5)
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)
300 return joinServerPage
303 func (ui *UI) renderServerUI() *tview.Flex {
304 commandList := tview.NewTextView().SetDynamicColors(true)
306 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs\n").
308 SetTitle("| Keyboard Shortcuts| ")
310 modal := tview.NewModal().
311 SetText("Disconnect from the server?").
312 AddButtons([]string{"Cancel", "Exit"}).
314 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
315 if buttonIndex == 1 {
316 _ = ui.HLClient.Disconnect()
317 ui.Pages.SwitchToPage("home")
319 ui.Pages.HidePage("modal")
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)
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)
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)
351 if event.Key() == tcell.KeyCtrlP {
353 newsFlex := tview.NewFlex()
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??
360 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
362 newsPostForm := tview.NewForm().
363 SetButtonsAlign(tview.AlignRight).
364 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
365 AddButton("Send", nil)
366 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
369 ui.App.SetFocus(newsPostTextArea)
371 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
372 err := ui.HLClient.Send(
373 *NewTransaction(tranOldPostNews, nil,
374 NewField(fieldData, []byte(newsText)),
378 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
379 // TODO: display errModal to user
381 ui.Pages.RemovePage("newsInput")
388 SetDirection(tview.FlexRow).
390 SetTitle("| Post Message |")
392 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
393 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
395 case tcell.KeyEscape:
396 ui.Pages.RemovePage("newsInput")
398 ui.App.SetFocus(newsPostForm)
400 fmt.Fprintf(newsPostTextArea, "\n")
402 const windowsBackspaceRune = 8
403 const macBackspaceRune = 127
404 switch event.Rune() {
405 case macBackspaceRune, windowsBackspaceRune:
406 curTxt := newsPostTextArea.GetText(true)
408 curTxt = curTxt[:len(curTxt)-1]
409 newsPostTextArea.SetText(curTxt)
412 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
419 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
420 newsFlex.AddItem(newsPostForm, 3, 0, false)
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)
432 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
433 ui.App.SetFocus(newsPostTextArea)
441 func (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()
446 bannerItem := tview.NewTextView().
447 SetText(randomBanner()).
448 SetDynamicColors(true).
449 SetTextAlign(tview.AlignCenter)
452 tview.NewFlex().AddItem(bannerItem, 0, 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),
461 mainMenu.AddItem("Join Server", "", 'j', func() {
462 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
463 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
465 AddItem("Bookmarks", "", 'b', func() {
466 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
468 AddItem("Browse Tracker", "", 't', func() {
469 ui.trackerList = ui.getTrackerList()
470 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
472 AddItem("Settings", "", 's', func() {
473 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
475 AddItem("Quit", "", 'q', func() {
479 ui.Pages.AddPage("home", home, true, true)
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")
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")
498 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
503 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {