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."
212 loginErrModal := tview.NewModal().
213 AddButtons([]string{"Ok"}).
215 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
216 ui.Pages.SwitchToPage("home")
218 loginErrModal.Box.SetTitle("Server Connection Error")
220 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
224 ui.Pages.SwitchToPage("home")
234 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
235 joinServerForm := tview.NewForm()
237 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
240 AddInputField("Server", server, 0, nil, nil).
241 AddInputField("Login", login, 0, nil, nil).
242 AddPasswordField("Password", password, 0, '*', nil).
243 AddCheckbox("Save", save, func(checked bool) {
244 ui.HLClient.Logger.Infow("saving bookmark")
245 // TODO: Implement bookmark saving
247 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())
248 out, err := yaml.Marshal(ui.HLClient.pref)
253 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
257 // pref := ui.HLClient.pref
259 AddButton("Cancel", func() {
260 ui.Pages.SwitchToPage(backPage)
262 AddButton("Connect", func() {
263 err := ui.joinServer(
264 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
265 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
266 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
269 ui.HLClient.Logger.Errorw("login error", "err", err)
270 loginErrModal := tview.NewModal().
271 AddButtons([]string{"Oh no"}).
272 SetText(err.Error()).
273 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
274 ui.Pages.SwitchToPage(backPage)
277 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
281 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
282 // TODO: implement bookmark saving
286 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
287 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
288 if event.Key() == tcell.KeyEscape {
289 ui.Pages.SwitchToPage(backPage)
295 joinServerForm.SetFocus(5)
298 joinServerPage := tview.NewFlex().
299 AddItem(nil, 0, 1, false).
300 AddItem(tview.NewFlex().
301 SetDirection(tview.FlexRow).
302 AddItem(nil, 0, 1, false).
303 AddItem(joinServerForm, 14, 1, true).
304 AddItem(nil, 0, 1, false), 40, 1, true).
305 AddItem(nil, 0, 1, false)
307 return joinServerPage
310 func (ui *UI) renderServerUI() *tview.Flex {
311 ui.chatBox.SetText("") // clear any previously existing chatbox text
312 commandList := tview.NewTextView().SetDynamicColors(true)
314 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
316 SetTitle("| Keyboard Shortcuts| ")
318 modal := tview.NewModal().
319 SetText("Disconnect from the server?").
320 AddButtons([]string{"Cancel", "Exit"}).
322 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
323 if buttonIndex == 1 {
324 _ = ui.HLClient.Disconnect()
325 ui.Pages.RemovePage(pageServerUI)
327 ui.Pages.HidePage("modal")
331 serverUI := tview.NewFlex().
332 AddItem(tview.NewFlex().
333 SetDirection(tview.FlexRow).
334 AddItem(commandList, 4, 0, false).
335 AddItem(ui.chatBox, 0, 8, false).
336 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
337 AddItem(ui.userList, 25, 1, false)
338 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
339 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
340 if event.Key() == tcell.KeyEscape {
341 ui.Pages.AddPage("modal", modal, false, true)
345 if event.Key() == tcell.KeyCtrlF {
346 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
347 ui.HLClient.Logger.Errorw("err", "err", err)
352 if event.Key() == tcell.KeyCtrlN {
353 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
354 ui.HLClient.Logger.Errorw("err", "err", err)
359 if event.Key() == tcell.KeyCtrlP {
361 newsFlex := tview.NewFlex()
362 newsFlex.SetBorderPadding(0, 0, 1, 1)
363 newsPostTextArea := tview.NewTextView()
364 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
365 newsPostTextArea.SetChangedFunc(func() {
366 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
369 newsPostForm := tview.NewForm().
370 SetButtonsAlign(tview.AlignRight).
371 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
372 AddButton("Send", nil)
373 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
375 case tcell.KeyEscape:
376 ui.Pages.RemovePage("newsInput")
378 ui.App.SetFocus(newsPostTextArea)
380 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
381 if len(newsText) == 0 {
384 err := ui.HLClient.Send(
385 *NewTransaction(tranOldPostNews, nil,
386 NewField(fieldData, []byte(newsText)),
390 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
391 // TODO: display errModal to user
393 ui.Pages.RemovePage("newsInput")
400 SetDirection(tview.FlexRow).
402 SetTitle("| Post Message |")
404 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
406 case tcell.KeyEscape:
407 ui.Pages.RemovePage("newsInput")
409 ui.App.SetFocus(newsPostForm)
411 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
413 const windowsBackspaceRune = 8
414 const macBackspaceRune = 127
415 switch event.Rune() {
416 case macBackspaceRune, windowsBackspaceRune:
417 curTxt := newsPostTextArea.GetText(true)
419 curTxt = curTxt[:len(curTxt)-1]
420 newsPostTextArea.SetText(curTxt)
423 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
430 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
431 newsFlex.AddItem(newsPostForm, 3, 0, false)
433 newsPostPage := tview.NewFlex().
434 AddItem(nil, 0, 1, false).
435 AddItem(tview.NewFlex().
436 SetDirection(tview.FlexRow).
437 AddItem(nil, 0, 1, false).
438 AddItem(newsFlex, 15, 1, true).
439 //AddItem(newsPostForm, 3, 0, false).
440 AddItem(nil, 0, 1, false), 40, 1, false).
441 AddItem(nil, 0, 1, false)
443 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
444 ui.App.SetFocus(newsPostTextArea)
452 func (ui *UI) Start() {
453 home := tview.NewFlex().SetDirection(tview.FlexRow)
454 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
455 mainMenu := tview.NewList()
457 bannerItem := tview.NewTextView().
458 SetText(randomBanner()).
459 SetDynamicColors(true).
460 SetTextAlign(tview.AlignCenter)
463 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
465 home.AddItem(tview.NewFlex().
466 AddItem(nil, 0, 1, false).
467 AddItem(mainMenu, 0, 1, true).
468 AddItem(nil, 0, 1, false),
472 mainMenu.AddItem("Join Server", "", 'j', func() {
473 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
474 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
476 AddItem("Bookmarks", "", 'b', func() {
477 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
479 AddItem("Browse Tracker", "", 't', func() {
480 ui.trackerList = ui.getTrackerList()
481 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
483 AddItem("Settings", "", 's', func() {
484 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
486 AddItem("Quit", "", 'q', func() {
490 ui.Pages.AddPage("home", home, true, true)
492 // App level input capture
493 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
494 if event.Key() == tcell.KeyCtrlC {
495 ui.HLClient.Logger.Infow("Exiting")
500 if event.Key() == tcell.KeyCtrlL {
501 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
502 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
503 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
504 if key == tcell.KeyEscape {
505 ui.Pages.RemovePage("logs")
509 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
514 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {