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
31 pageServerUI = "serverUI"
34 func NewUI(c *Client) *UI {
35 app := tview.NewApplication()
36 chatBox := tview.NewTextView().
38 SetDynamicColors(true).
40 SetChangedFunc(func() {
41 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
43 chatBox.Box.SetBorder(true).SetTitle("Chat")
45 chatInput := tview.NewInputField()
48 SetFieldBackgroundColor(tcell.ColorDimGray).
49 SetDoneFunc(func(key tcell.Key) {
50 // skip send if user hit enter with no other text
51 if len(chatInput.GetText()) == 0 {
56 *NewTransaction(tranChatSend, nil,
57 NewField(fieldData, []byte(chatInput.GetText())),
60 chatInput.SetText("") // clear the input field after chat send
63 chatInput.Box.SetBorder(true).SetTitle("Send")
67 SetDynamicColors(true).
68 SetChangedFunc(func() {
69 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
71 userList.Box.SetBorder(true).SetTitle("Users")
76 Pages: tview.NewPages(),
79 trackerList: tview.NewList(),
80 agreeModal: tview.NewModal(),
85 func (ui *UI) showBookmarks() *tview.List {
86 list := tview.NewList()
87 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
88 if event.Key() == tcell.KeyEsc {
89 ui.Pages.SwitchToPage("home")
93 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
95 shortcut := 97 // rune for "a"
96 for i, srv := range ui.HLClient.pref.Bookmarks {
100 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
101 ui.Pages.RemovePage("joinServer")
103 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
105 ui.Pages.AddPage("joinServer", newJS, true, true)
112 func (ui *UI) getTrackerList() *tview.List {
113 listing, err := GetListing(ui.HLClient.pref.Tracker)
118 list := tview.NewList()
119 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
120 if event.Key() == tcell.KeyEsc {
121 ui.Pages.SwitchToPage("home")
125 list.Box.SetBorder(true).SetTitle("| Servers |")
127 shortcut := 97 // rune for "a"
128 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("", 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)
211 msg = "The server connection has unexpectedly closed."
214 loginErrModal := tview.NewModal().
215 AddButtons([]string{"Ok"}).
217 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
218 ui.Pages.SwitchToPage("home")
220 loginErrModal.Box.SetTitle("Server Connection Error")
222 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
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 err := ui.joinServer(
262 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
263 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
264 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
267 ui.HLClient.Logger.Errorw("login error", "err", err)
268 loginErrModal := tview.NewModal().
269 AddButtons([]string{"Oh no"}).
270 SetText(err.Error()).
271 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
272 ui.Pages.SwitchToPage(backPage)
275 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
279 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
280 // TODO: implement bookmark saving
284 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
285 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
286 if event.Key() == tcell.KeyEscape {
287 ui.Pages.SwitchToPage(backPage)
293 joinServerForm.SetFocus(5)
296 joinServerPage := tview.NewFlex().
297 AddItem(nil, 0, 1, false).
298 AddItem(tview.NewFlex().
299 SetDirection(tview.FlexRow).
300 AddItem(nil, 0, 1, false).
301 AddItem(joinServerForm, 14, 1, true).
302 AddItem(nil, 0, 1, false), 40, 1, true).
303 AddItem(nil, 0, 1, false)
305 return joinServerPage
308 func (ui *UI) renderServerUI() *tview.Flex {
309 ui.chatBox.SetText("") // clear any previously existing chatbox text
310 commandList := tview.NewTextView().SetDynamicColors(true)
312 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
314 SetTitle("| Keyboard Shortcuts| ")
316 modal := tview.NewModal().
317 SetText("Disconnect from the server?").
318 AddButtons([]string{"Cancel", "Exit"}).
320 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
321 if buttonIndex == 1 {
322 _ = ui.HLClient.Disconnect()
323 ui.Pages.RemovePage(pageServerUI)
325 ui.Pages.HidePage("modal")
329 serverUI := tview.NewFlex().
330 AddItem(tview.NewFlex().
331 SetDirection(tview.FlexRow).
332 AddItem(commandList, 4, 0, false).
333 AddItem(ui.chatBox, 0, 8, false).
334 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
335 AddItem(ui.userList, 25, 1, false)
336 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
337 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
338 if event.Key() == tcell.KeyEscape {
339 ui.Pages.AddPage("modal", modal, false, true)
343 if event.Key() == tcell.KeyCtrlF {
344 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
345 ui.HLClient.Logger.Errorw("err", "err", err)
350 if event.Key() == tcell.KeyCtrlN {
351 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
352 ui.HLClient.Logger.Errorw("err", "err", err)
357 if event.Key() == tcell.KeyCtrlP {
359 newsFlex := tview.NewFlex()
360 newsFlex.SetBorderPadding(0, 0, 1, 1)
361 newsPostTextArea := tview.NewTextView()
362 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
363 newsPostTextArea.SetChangedFunc(func() {
364 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
367 newsPostForm := tview.NewForm().
368 SetButtonsAlign(tview.AlignRight).
369 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
370 AddButton("Send", nil)
371 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
373 case tcell.KeyEscape:
374 ui.Pages.RemovePage("newsInput")
376 ui.App.SetFocus(newsPostTextArea)
378 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
379 if len(newsText) == 0 {
382 err := ui.HLClient.Send(
383 *NewTransaction(tranOldPostNews, nil,
384 NewField(fieldData, []byte(newsText)),
388 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
389 // TODO: display errModal to user
391 ui.Pages.RemovePage("newsInput")
398 SetDirection(tview.FlexRow).
400 SetTitle("| Post Message |")
402 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
404 case tcell.KeyEscape:
405 ui.Pages.RemovePage("newsInput")
407 ui.App.SetFocus(newsPostForm)
409 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
411 const windowsBackspaceRune = 8
412 const macBackspaceRune = 127
413 switch event.Rune() {
414 case macBackspaceRune, windowsBackspaceRune:
415 curTxt := newsPostTextArea.GetText(true)
417 curTxt = curTxt[:len(curTxt)-1]
418 newsPostTextArea.SetText(curTxt)
421 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
428 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
429 newsFlex.AddItem(newsPostForm, 3, 0, false)
431 newsPostPage := tview.NewFlex().
432 AddItem(nil, 0, 1, false).
433 AddItem(tview.NewFlex().
434 SetDirection(tview.FlexRow).
435 AddItem(nil, 0, 1, false).
436 AddItem(newsFlex, 15, 1, true).
437 //AddItem(newsPostForm, 3, 0, false).
438 AddItem(nil, 0, 1, false), 40, 1, false).
439 AddItem(nil, 0, 1, false)
441 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
442 ui.App.SetFocus(newsPostTextArea)
450 func (ui *UI) Start() {
451 home := tview.NewFlex().SetDirection(tview.FlexRow)
452 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
453 mainMenu := tview.NewList()
455 bannerItem := tview.NewTextView().
456 SetText(randomBanner()).
457 SetDynamicColors(true).
458 SetTextAlign(tview.AlignCenter)
461 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
463 home.AddItem(tview.NewFlex().
464 AddItem(nil, 0, 1, false).
465 AddItem(mainMenu, 0, 1, true).
466 AddItem(nil, 0, 1, false),
470 mainMenu.AddItem("Join Server", "", 'j', func() {
471 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
472 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
474 AddItem("Bookmarks", "", 'b', func() {
475 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
477 AddItem("Browse Tracker", "", 't', func() {
478 ui.trackerList = ui.getTrackerList()
479 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
481 AddItem("Settings", "", 's', func() {
482 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
484 AddItem("Quit", "", 'q', func() {
488 ui.Pages.AddPage("home", home, true, true)
490 // App level input capture
491 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
492 if event.Key() == tcell.KeyCtrlC {
493 ui.HLClient.Logger.Infow("Exiting")
498 if event.Key() == tcell.KeyCtrlL {
499 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
500 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
501 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
502 if key == tcell.KeyEscape {
503 ui.Pages.RemovePage("logs")
507 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
512 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {