5 "github.com/gdamore/tcell/v2"
6 "github.com/rivo/tview"
15 chatBox *tview.TextView
16 chatInput *tview.InputField
17 App *tview.Application
19 userList *tview.TextView
20 trackerList *tview.List
26 pageServerUI = "serverUI"
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(),
79 func (ui *UI) showBookmarks() *tview.List {
80 list := tview.NewList()
81 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
82 if event.Key() == tcell.KeyEsc {
83 ui.Pages.SwitchToPage("home")
87 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
89 shortcut := 97 // rune for "a"
90 for i, srv := range ui.HLClient.Pref.Bookmarks {
94 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
95 ui.Pages.RemovePage("joinServer")
97 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
99 ui.Pages.AddPage("joinServer", newJS, true, true)
106 func (ui *UI) getTrackerList() *tview.List {
107 listing, err := GetListing(ui.HLClient.Pref.Tracker)
112 list := tview.NewList()
113 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
114 if event.Key() == tcell.KeyEsc {
115 ui.Pages.SwitchToPage("home")
119 list.Box.SetBorder(true).SetTitle("| Servers |")
121 shortcut := 97 // rune for "a"
122 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(string(srvName), 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.AddCheckbox("Enable Terminal Bell", ui.HLClient.Pref.EnableBell, nil)
148 settingsForm.AddButton("Save", func() {
149 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
150 if len(usernameInput) == 0 {
151 usernameInput = "unnamed"
153 ui.HLClient.Pref.Username = usernameInput
154 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
155 ui.HLClient.Pref.IconID, _ = strconv.Atoi(iconStr)
156 ui.HLClient.Pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
157 ui.HLClient.Pref.EnableBell = settingsForm.GetFormItem(3).(*tview.Checkbox).IsChecked()
159 out, err := yaml.Marshal(&ui.HLClient.Pref)
164 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
166 println(ui.HLClient.cfgPath)
169 ui.Pages.RemovePage("settings")
171 settingsForm.SetBorder(true)
172 settingsForm.SetCancelFunc(func() {
173 ui.Pages.RemovePage("settings")
175 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
176 settingsPage.Box.SetBorder(true).SetTitle("Settings")
177 settingsPage.AddItem(settingsForm, 0, 1, true)
179 centerFlex := tview.NewFlex().
180 AddItem(nil, 0, 1, false).
181 AddItem(tview.NewFlex().
182 SetDirection(tview.FlexRow).
183 AddItem(nil, 0, 1, false).
184 AddItem(settingsForm, 15, 1, true).
185 AddItem(nil, 0, 1, false), 40, 1, true).
186 AddItem(nil, 0, 1, false)
191 func (ui *UI) joinServer(addr, login, password string) error {
192 // append default port to address if no port supplied
193 if len(strings.Split(addr, ":")) == 1 {
196 if err := ui.HLClient.Connect(addr, login, password); err != nil {
197 return fmt.Errorf("Error joining server: %v\n", err)
201 if err := ui.HLClient.HandleTransactions(); err != nil {
202 ui.Pages.SwitchToPage("home")
205 loginErrModal := tview.NewModal().
206 AddButtons([]string{"Ok"}).
207 SetText("The server connection has closed.").
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)
220 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
221 joinServerForm := tview.NewForm()
223 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
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 ui.HLClient.Logger.Infow("saving bookmark")
231 // TODO: Implement bookmark saving
233 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())
234 out, err := yaml.Marshal(ui.HLClient.Pref)
239 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
243 // Pref := ui.HLClient.Pref
245 AddButton("Cancel", func() {
246 ui.Pages.SwitchToPage(backPage)
248 AddButton("Connect", func() {
249 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
250 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
251 err := ui.joinServer(
254 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
257 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
259 ui.HLClient.serverName = name
262 ui.HLClient.Logger.Errorw("login error", "err", err)
263 loginErrModal := tview.NewModal().
264 AddButtons([]string{"Oh no"}).
265 SetText(err.Error()).
266 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
267 ui.Pages.SwitchToPage(backPage)
270 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
274 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
275 // TODO: implement bookmark saving
279 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
280 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
281 if event.Key() == tcell.KeyEscape {
282 ui.Pages.SwitchToPage(backPage)
288 joinServerForm.SetFocus(5)
291 joinServerPage := tview.NewFlex().
292 AddItem(nil, 0, 1, false).
293 AddItem(tview.NewFlex().
294 SetDirection(tview.FlexRow).
295 AddItem(nil, 0, 1, false).
296 AddItem(joinServerForm, 14, 1, true).
297 AddItem(nil, 0, 1, false), 40, 1, true).
298 AddItem(nil, 0, 1, false)
300 return joinServerPage
303 func (ui *UI) renderServerUI() *tview.Flex {
304 ui.chatBox.SetText("") // clear any previously existing chatbox text
305 commandList := tview.NewTextView().SetDynamicColors(true)
307 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
309 SetTitle("| Keyboard Shortcuts| ")
311 modal := tview.NewModal().
312 SetText("Disconnect from the server?").
313 AddButtons([]string{"Cancel", "Exit"}).
315 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
316 if buttonIndex == 1 {
317 _ = ui.HLClient.Disconnect()
318 ui.Pages.RemovePage(pageServerUI)
319 ui.Pages.SwitchToPage("home")
321 ui.Pages.HidePage("modal")
325 serverUI := tview.NewFlex().
326 AddItem(tview.NewFlex().
327 SetDirection(tview.FlexRow).
328 AddItem(commandList, 4, 0, false).
329 AddItem(ui.chatBox, 0, 8, false).
330 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
331 AddItem(ui.userList, 25, 1, false)
332 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
333 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
334 if event.Key() == tcell.KeyEscape {
335 ui.Pages.AddPage("modal", modal, false, true)
339 if event.Key() == tcell.KeyCtrlF {
340 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
341 ui.HLClient.Logger.Errorw("err", "err", err)
346 if event.Key() == tcell.KeyCtrlN {
347 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
348 ui.HLClient.Logger.Errorw("err", "err", err)
353 if event.Key() == tcell.KeyCtrlP {
355 newsFlex := tview.NewFlex()
356 newsFlex.SetBorderPadding(0, 0, 1, 1)
357 newsPostTextArea := tview.NewTextView()
358 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
359 newsPostTextArea.SetChangedFunc(func() {
360 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
363 newsPostForm := tview.NewForm().
364 SetButtonsAlign(tview.AlignRight).
365 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
366 AddButton("Send", nil)
367 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
369 case tcell.KeyEscape:
370 ui.Pages.RemovePage("newsInput")
372 ui.App.SetFocus(newsPostTextArea)
374 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
375 if len(newsText) == 0 {
378 err := ui.HLClient.Send(
379 *NewTransaction(TranOldPostNews, nil,
380 NewField(FieldData, []byte(newsText)),
384 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
385 // TODO: display errModal to user
387 ui.Pages.RemovePage("newsInput")
394 SetDirection(tview.FlexRow).
396 SetTitle("| Post Message |")
398 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
400 case tcell.KeyEscape:
401 ui.Pages.RemovePage("newsInput")
403 ui.App.SetFocus(newsPostForm)
405 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
407 const windowsBackspaceRune = 8
408 const macBackspaceRune = 127
409 switch event.Rune() {
410 case macBackspaceRune, windowsBackspaceRune:
411 curTxt := newsPostTextArea.GetText(true)
413 curTxt = curTxt[:len(curTxt)-1]
414 newsPostTextArea.SetText(curTxt)
417 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
424 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
425 newsFlex.AddItem(newsPostForm, 3, 0, false)
427 newsPostPage := tview.NewFlex().
428 AddItem(nil, 0, 1, false).
429 AddItem(tview.NewFlex().
430 SetDirection(tview.FlexRow).
431 AddItem(nil, 0, 1, false).
432 AddItem(newsFlex, 15, 1, true).
433 // AddItem(newsPostForm, 3, 0, false).
434 AddItem(nil, 0, 1, false), 40, 1, false).
435 AddItem(nil, 0, 1, false)
437 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
438 ui.App.SetFocus(newsPostTextArea)
446 func (ui *UI) Start() {
447 home := tview.NewFlex().SetDirection(tview.FlexRow)
448 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
449 mainMenu := tview.NewList()
451 bannerItem := tview.NewTextView().
452 SetText(randomBanner()).
453 SetDynamicColors(true).
454 SetTextAlign(tview.AlignCenter)
457 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
459 home.AddItem(tview.NewFlex().
460 AddItem(nil, 0, 1, false).
461 AddItem(mainMenu, 0, 1, true).
462 AddItem(nil, 0, 1, false),
466 mainMenu.AddItem("Join Server", "", 'j', func() {
467 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
468 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
470 AddItem("Bookmarks", "", 'b', func() {
471 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
473 AddItem("Browse Tracker", "", 't', func() {
474 ui.trackerList = ui.getTrackerList()
475 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
477 AddItem("Settings", "", 's', func() {
478 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
480 AddItem("Quit", "", 'q', func() {
484 ui.Pages.AddPage("home", home, true, true)
486 // App level input capture
487 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
488 if event.Key() == tcell.KeyCtrlC {
489 ui.HLClient.Logger.Infow("Exiting")
494 if event.Key() == tcell.KeyCtrlL {
495 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
496 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
497 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
498 if key == tcell.KeyEscape {
499 ui.Pages.RemovePage("logs")
503 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
508 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {