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(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
224 joinServerForm := tview.NewForm()
226 AddInputField("Server", server, 0, nil, nil).
227 AddInputField("Login", login, 0, nil, nil).
228 AddPasswordField("Password", password, 0, '*', nil).
229 AddCheckbox("Save", save, func(checked bool) {
230 // TODO: Implement bookmark saving
232 AddButton("Cancel", func() {
233 ui.Pages.SwitchToPage(backPage)
235 AddButton("Connect", func() {
236 err := ui.joinServer(
237 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
238 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
239 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
242 ui.HLClient.Logger.Errorw("login error", "err", err)
243 loginErrModal := tview.NewModal().
244 AddButtons([]string{"Oh no"}).
245 SetText(err.Error()).
246 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
247 ui.Pages.SwitchToPage(backPage)
250 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
254 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
255 // TODO: implement bookmark saving
259 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
260 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
261 if event.Key() == tcell.KeyEscape {
262 ui.Pages.SwitchToPage(backPage)
268 joinServerForm.SetFocus(5)
271 joinServerPage := tview.NewFlex().
272 AddItem(nil, 0, 1, false).
273 AddItem(tview.NewFlex().
274 SetDirection(tview.FlexRow).
275 AddItem(nil, 0, 1, false).
276 AddItem(joinServerForm, 14, 1, true).
277 AddItem(nil, 0, 1, false), 40, 1, true).
278 AddItem(nil, 0, 1, false)
280 return joinServerPage
283 func (ui *UI) renderServerUI() *tview.Flex {
284 commandList := tview.NewTextView().SetDynamicColors(true)
286 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs\n").
288 SetTitle("| Keyboard Shortcuts| ")
290 modal := tview.NewModal().
291 SetText("Disconnect from the server?").
292 AddButtons([]string{"Cancel", "Exit"}).
294 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
295 if buttonIndex == 1 {
296 _ = ui.HLClient.Disconnect()
297 ui.Pages.SwitchToPage("home")
299 ui.Pages.HidePage("modal")
303 serverUI := tview.NewFlex().
304 AddItem(tview.NewFlex().
305 SetDirection(tview.FlexRow).
306 AddItem(commandList, 4, 0, false).
307 AddItem(ui.chatBox, 0, 8, false).
308 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
309 AddItem(ui.userList, 25, 1, false)
310 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
311 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
312 if event.Key() == tcell.KeyEscape {
313 ui.Pages.AddPage("modal", modal, false, true)
317 if event.Key() == tcell.KeyCtrlF {
318 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
319 ui.HLClient.Logger.Errorw("err", "err", err)
324 if event.Key() == tcell.KeyCtrlN {
325 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
326 ui.HLClient.Logger.Errorw("err", "err", err)
331 if event.Key() == tcell.KeyCtrlP {
333 newsFlex := tview.NewFlex()
335 newsPostTextArea := tview.NewTextView()
336 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
337 newsPostTextArea.SetChangedFunc(func() {
338 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
340 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
342 newsPostForm := tview.NewForm().
343 SetButtonsAlign(tview.AlignRight).
344 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
345 AddButton("Send", nil)
346 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
349 ui.App.SetFocus(newsPostTextArea)
351 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
352 err := ui.HLClient.Send(
353 *NewTransaction(tranOldPostNews, nil,
354 NewField(fieldData, []byte(newsText)),
358 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
359 // TODO: display errModal to user
361 ui.Pages.RemovePage("newsInput")
368 SetDirection(tview.FlexRow).
370 SetTitle("| Post Message |")
372 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
373 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
375 case tcell.KeyEscape:
376 ui.Pages.RemovePage("newsInput")
378 ui.App.SetFocus(newsPostForm)
380 fmt.Fprintf(newsPostTextArea, "\n")
382 const windowsBackspaceRune = 8
383 const macBackspaceRune = 127
384 switch event.Rune() {
385 case macBackspaceRune, windowsBackspaceRune:
386 curTxt := newsPostTextArea.GetText(true)
388 curTxt = curTxt[:len(curTxt)-1]
389 newsPostTextArea.SetText(curTxt)
392 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
399 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
400 newsFlex.AddItem(newsPostForm, 3, 0, false)
402 newsPostPage := tview.NewFlex().
403 AddItem(nil, 0, 1, false).
404 AddItem(tview.NewFlex().
405 SetDirection(tview.FlexRow).
406 AddItem(nil, 0, 1, false).
407 AddItem(newsFlex, 15, 1, true).
408 //AddItem(newsPostForm, 3, 0, false).
409 AddItem(nil, 0, 1, false), 40, 1, false).
410 AddItem(nil, 0, 1, false)
412 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
413 ui.App.SetFocus(newsPostTextArea)
421 func (ui *UI) Start() {
422 home := tview.NewFlex().SetDirection(tview.FlexRow)
423 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
424 mainMenu := tview.NewList()
426 bannerItem := tview.NewTextView().
427 SetText(randomBanner()).
428 SetDynamicColors(true).
429 SetTextAlign(tview.AlignCenter)
432 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
434 home.AddItem(tview.NewFlex().
435 AddItem(nil, 0, 1, false).
436 AddItem(mainMenu, 0, 1, true).
437 AddItem(nil, 0, 1, false),
441 mainMenu.AddItem("Join Server", "", 'j', func() {
442 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
443 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
445 AddItem("Bookmarks", "", 'b', func() {
446 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
448 AddItem("Browse Tracker", "", 't', func() {
449 ui.trackerList = ui.getTrackerList()
450 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
452 AddItem("Settings", "", 's', func() {
453 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
455 AddItem("Quit", "", 'q', func() {
459 ui.Pages.AddPage("home", home, true, true)
461 // App level input capture
462 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
463 if event.Key() == tcell.KeyCtrlC {
464 ui.HLClient.Logger.Infow("Exiting")
469 if event.Key() == tcell.KeyCtrlL {
470 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
471 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
472 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
473 if key == tcell.KeyEscape {
474 ui.Pages.RemovePage("logs")
478 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
483 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {