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 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
191 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
196 err := ui.HLClient.ReadLoop()
198 ui.HLClient.Logger.Errorw("read error", "err", err)
202 msg = "The server connection has unexpectedly closed."
205 loginErrModal := tview.NewModal().
206 AddButtons([]string{"Ok"}).
208 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
209 ui.Pages.SwitchToPage("home")
211 loginErrModal.Box.SetTitle("Server Connection Error")
213 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
223 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
224 joinServerForm := tview.NewForm()
226 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
229 AddInputField("Server", server, 0, nil, nil).
230 AddInputField("Login", login, 0, nil, nil).
231 AddPasswordField("Password", password, 0, '*', nil).
232 AddCheckbox("Save", save, func(checked bool) {
233 ui.HLClient.Logger.Infow("saving bookmark")
234 // TODO: Implement bookmark saving
236 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())
237 out, err := yaml.Marshal(ui.HLClient.pref)
242 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
246 // pref := ui.HLClient.pref
248 AddButton("Cancel", func() {
249 ui.Pages.SwitchToPage(backPage)
251 AddButton("Connect", func() {
252 err := ui.joinServer(
253 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
254 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
255 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
258 ui.HLClient.Logger.Errorw("login error", "err", err)
259 loginErrModal := tview.NewModal().
260 AddButtons([]string{"Oh no"}).
261 SetText(err.Error()).
262 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
263 ui.Pages.SwitchToPage(backPage)
266 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
270 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
271 // TODO: implement bookmark saving
275 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
276 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
277 if event.Key() == tcell.KeyEscape {
278 ui.Pages.SwitchToPage(backPage)
284 joinServerForm.SetFocus(5)
287 joinServerPage := tview.NewFlex().
288 AddItem(nil, 0, 1, false).
289 AddItem(tview.NewFlex().
290 SetDirection(tview.FlexRow).
291 AddItem(nil, 0, 1, false).
292 AddItem(joinServerForm, 14, 1, true).
293 AddItem(nil, 0, 1, false), 40, 1, true).
294 AddItem(nil, 0, 1, false)
296 return joinServerPage
299 func (ui *UI) renderServerUI() *tview.Flex {
300 commandList := tview.NewTextView().SetDynamicColors(true)
302 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs\n").
304 SetTitle("| Keyboard Shortcuts| ")
306 modal := tview.NewModal().
307 SetText("Disconnect from the server?").
308 AddButtons([]string{"Cancel", "Exit"}).
310 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
311 if buttonIndex == 1 {
312 _ = ui.HLClient.Disconnect()
313 ui.Pages.SwitchToPage("home")
315 ui.Pages.HidePage("modal")
319 serverUI := tview.NewFlex().
320 AddItem(tview.NewFlex().
321 SetDirection(tview.FlexRow).
322 AddItem(commandList, 4, 0, false).
323 AddItem(ui.chatBox, 0, 8, false).
324 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
325 AddItem(ui.userList, 25, 1, false)
326 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
327 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
328 if event.Key() == tcell.KeyEscape {
329 ui.Pages.AddPage("modal", modal, false, true)
333 if event.Key() == tcell.KeyCtrlF {
334 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
335 ui.HLClient.Logger.Errorw("err", "err", err)
340 if event.Key() == tcell.KeyCtrlN {
341 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
342 ui.HLClient.Logger.Errorw("err", "err", err)
347 if event.Key() == tcell.KeyCtrlP {
349 newsFlex := tview.NewFlex()
351 newsPostTextArea := tview.NewTextView()
352 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
353 newsPostTextArea.SetChangedFunc(func() {
354 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
356 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
358 newsPostForm := tview.NewForm().
359 SetButtonsAlign(tview.AlignRight).
360 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
361 AddButton("Send", nil)
362 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
365 ui.App.SetFocus(newsPostTextArea)
367 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
368 err := ui.HLClient.Send(
369 *NewTransaction(tranOldPostNews, nil,
370 NewField(fieldData, []byte(newsText)),
374 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
375 // TODO: display errModal to user
377 ui.Pages.RemovePage("newsInput")
384 SetDirection(tview.FlexRow).
386 SetTitle("| Post Message |")
388 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
389 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
391 case tcell.KeyEscape:
392 ui.Pages.RemovePage("newsInput")
394 ui.App.SetFocus(newsPostForm)
396 fmt.Fprintf(newsPostTextArea, "\n")
398 const windowsBackspaceRune = 8
399 const macBackspaceRune = 127
400 switch event.Rune() {
401 case macBackspaceRune, windowsBackspaceRune:
402 curTxt := newsPostTextArea.GetText(true)
404 curTxt = curTxt[:len(curTxt)-1]
405 newsPostTextArea.SetText(curTxt)
408 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
415 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
416 newsFlex.AddItem(newsPostForm, 3, 0, false)
418 newsPostPage := tview.NewFlex().
419 AddItem(nil, 0, 1, false).
420 AddItem(tview.NewFlex().
421 SetDirection(tview.FlexRow).
422 AddItem(nil, 0, 1, false).
423 AddItem(newsFlex, 15, 1, true).
424 //AddItem(newsPostForm, 3, 0, false).
425 AddItem(nil, 0, 1, false), 40, 1, false).
426 AddItem(nil, 0, 1, false)
428 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
429 ui.App.SetFocus(newsPostTextArea)
437 func (ui *UI) Start() {
438 home := tview.NewFlex().SetDirection(tview.FlexRow)
439 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
440 mainMenu := tview.NewList()
442 bannerItem := tview.NewTextView().
443 SetText(randomBanner()).
444 SetDynamicColors(true).
445 SetTextAlign(tview.AlignCenter)
448 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
450 home.AddItem(tview.NewFlex().
451 AddItem(nil, 0, 1, false).
452 AddItem(mainMenu, 0, 1, true).
453 AddItem(nil, 0, 1, false),
457 mainMenu.AddItem("Join Server", "", 'j', func() {
458 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
459 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
461 AddItem("Bookmarks", "", 'b', func() {
462 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
464 AddItem("Browse Tracker", "", 't', func() {
465 ui.trackerList = ui.getTrackerList()
466 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
468 AddItem("Settings", "", 's', func() {
469 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
471 AddItem("Quit", "", 'q', func() {
475 ui.Pages.AddPage("home", home, true, true)
477 // App level input capture
478 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
479 if event.Key() == tcell.KeyCtrlC {
480 ui.HLClient.Logger.Infow("Exiting")
485 if event.Key() == tcell.KeyCtrlL {
486 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
487 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
488 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
489 if key == tcell.KeyEscape {
490 ui.Pages.RemovePage("logs")
494 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
499 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {