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.KeyCtrlN {
318 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
319 ui.HLClient.Logger.Errorw("err", "err", err)
324 if event.Key() == tcell.KeyCtrlP {
326 newsFlex := tview.NewFlex()
328 newsPostTextArea := tview.NewTextView()
329 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
330 newsPostTextArea.SetChangedFunc(func() {
331 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
333 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
335 newsPostForm := tview.NewForm().
336 SetButtonsAlign(tview.AlignRight).
337 AddButton("Post", nil)
338 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
341 ui.App.SetFocus(newsPostTextArea)
343 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
344 err := ui.HLClient.Send(
345 *NewTransaction(tranOldPostNews, nil,
346 NewField(fieldData, []byte(newsText)),
350 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
351 // TODO: display errModal to user
353 //newsInput.SetText("") // clear the input field after chat send
354 ui.Pages.RemovePage("newsInput")
361 SetDirection(tview.FlexRow).
363 SetTitle("News Post")
365 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
366 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
368 case tcell.KeyEscape:
369 ui.Pages.RemovePage("newsInput")
371 ui.App.SetFocus(newsPostForm)
373 fmt.Fprintf(newsPostTextArea, "\n")
375 const windowsBackspaceRune = 8
376 const macBackspaceRune = 127
377 switch event.Rune() {
378 case macBackspaceRune, windowsBackspaceRune:
379 curTxt := newsPostTextArea.GetText(true)
381 curTxt = curTxt[:len(curTxt)-1]
382 newsPostTextArea.SetText(curTxt)
385 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
392 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
393 newsFlex.AddItem(newsPostForm, 3, 0, false)
395 newsPostPage := tview.NewFlex().
396 AddItem(nil, 0, 1, false).
397 AddItem(tview.NewFlex().
398 SetDirection(tview.FlexRow).
399 AddItem(nil, 0, 1, false).
400 AddItem(newsFlex, 15, 1, true).
401 //AddItem(newsPostForm, 3, 0, false).
402 AddItem(nil, 0, 1, false), 40, 1, false).
403 AddItem(nil, 0, 1, false)
405 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
406 ui.App.SetFocus(newsPostTextArea)
414 func (ui *UI) Start() {
415 home := tview.NewFlex().SetDirection(tview.FlexRow)
416 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
417 mainMenu := tview.NewList()
419 bannerItem := tview.NewTextView().
420 SetText(randomBanner()).
421 SetDynamicColors(true).
422 SetTextAlign(tview.AlignCenter)
425 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
427 home.AddItem(tview.NewFlex().
428 AddItem(nil, 0, 1, false).
429 AddItem(mainMenu, 0, 1, true).
430 AddItem(nil, 0, 1, false),
434 mainMenu.AddItem("Join Server", "", 'j', func() {
435 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
436 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
438 AddItem("Bookmarks", "", 'b', func() {
439 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
441 AddItem("Browse Tracker", "", 't', func() {
442 ui.trackerList = ui.getTrackerList()
443 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
445 AddItem("Settings", "", 's', func() {
446 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
448 AddItem("Quit", "", 'q', func() {
452 ui.Pages.AddPage("home", home, true, true)
454 // App level input capture
455 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
456 if event.Key() == tcell.KeyCtrlC {
457 ui.HLClient.Logger.Infow("Exiting")
462 if event.Key() == tcell.KeyCtrlL {
463 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
464 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
465 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
466 if key == tcell.KeyEscape {
467 ui.Pages.RemovePage("logs")
471 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
476 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {