5 "github.com/gdamore/tcell/v2"
6 "github.com/rivo/tview"
14 chatBox *tview.TextView
15 chatInput *tview.InputField
16 App *tview.Application
18 userList *tview.TextView
19 trackerList *tview.List
25 pageServerUI = "serverUI"
28 func NewUI(c *Client) *UI {
29 app := tview.NewApplication()
30 chatBox := tview.NewTextView().
32 SetDynamicColors(true).
34 SetChangedFunc(func() {
35 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
37 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
39 chatInput := tview.NewInputField()
42 SetFieldBackgroundColor(tcell.ColorDimGray).
43 SetDoneFunc(func(key tcell.Key) {
44 // skip send if user hit enter with no other text
45 if len(chatInput.GetText()) == 0 {
50 *NewTransaction(TranChatSend, nil,
51 NewField(FieldData, []byte(chatInput.GetText())),
54 chatInput.SetText("") // clear the input field after chat send
57 chatInput.Box.SetBorder(true).SetTitle("Send")
61 SetDynamicColors(true).
62 SetChangedFunc(func() {
63 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
65 userList.Box.SetBorder(true).SetTitle("Users")
70 Pages: tview.NewPages(),
73 trackerList: tview.NewList(),
78 func (ui *UI) showBookmarks() *tview.List {
79 list := tview.NewList()
80 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
81 if event.Key() == tcell.KeyEsc {
82 ui.Pages.SwitchToPage("home")
86 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
88 shortcut := 97 // rune for "a"
89 for i, srv := range ui.HLClient.Pref.Bookmarks {
93 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
94 ui.Pages.RemovePage("joinServer")
96 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
98 ui.Pages.AddPage("joinServer", newJS, true, true)
105 func (ui *UI) getTrackerList() *tview.List {
106 listing, err := GetListing(ui.HLClient.Pref.Tracker)
111 list := tview.NewList()
112 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
113 if event.Key() == tcell.KeyEsc {
114 ui.Pages.SwitchToPage("home")
118 list.Box.SetBorder(true).SetTitle("| Servers |")
120 shortcut := 97 // rune for "a"
121 for i, srv := range listing {
124 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
125 ui.Pages.RemovePage("joinServer")
127 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
129 ui.Pages.AddPage("joinServer", newJS, true, true)
130 ui.Pages.ShowPage("joinServer")
137 func (ui *UI) renderSettingsForm() *tview.Flex {
138 iconStr := strconv.Itoa(ui.HLClient.Pref.IconID)
139 settingsForm := tview.NewForm()
140 settingsForm.AddInputField("Your Name", ui.HLClient.Pref.Username, 0, nil, nil)
141 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
142 _, err := strconv.Atoi(idStr)
145 settingsForm.AddInputField("Tracker", ui.HLClient.Pref.Tracker, 0, nil, nil)
146 settingsForm.AddCheckbox("Enable Terminal Bell", ui.HLClient.Pref.EnableBell, 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()
156 ui.HLClient.Pref.EnableBell = settingsForm.GetFormItem(3).(*tview.Checkbox).IsChecked()
158 out, err := yaml.Marshal(&ui.HLClient.Pref)
163 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
165 println(ui.HLClient.cfgPath)
168 ui.Pages.RemovePage("settings")
170 settingsForm.SetBorder(true)
171 settingsForm.SetCancelFunc(func() {
172 ui.Pages.RemovePage("settings")
174 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
175 settingsPage.Box.SetBorder(true).SetTitle("Settings")
176 settingsPage.AddItem(settingsForm, 0, 1, true)
178 centerFlex := tview.NewFlex().
179 AddItem(nil, 0, 1, false).
180 AddItem(tview.NewFlex().
181 SetDirection(tview.FlexRow).
182 AddItem(nil, 0, 1, false).
183 AddItem(settingsForm, 15, 1, true).
184 AddItem(nil, 0, 1, false), 40, 1, true).
185 AddItem(nil, 0, 1, false)
190 func (ui *UI) joinServer(addr, login, password string) error {
191 // append default port to address if no port supplied
192 if len(strings.Split(addr, ":")) == 1 {
195 if err := ui.HLClient.Connect(addr, login, password); err != nil {
196 return fmt.Errorf("Error joining server: %v\n", err)
200 if err := ui.HLClient.HandleTransactions(); err != nil {
201 ui.Pages.SwitchToPage("home")
204 loginErrModal := tview.NewModal().
205 AddButtons([]string{"Ok"}).
206 SetText("The server connection has closed.").
207 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
208 ui.Pages.SwitchToPage("home")
210 loginErrModal.Box.SetTitle("Server Connection Error")
212 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
219 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
220 joinServerForm := tview.NewForm()
222 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
225 AddInputField("Server", server, 0, nil, nil).
226 AddInputField("Login", login, 0, nil, nil).
227 AddPasswordField("Password", password, 0, '*', nil).
228 AddCheckbox("Save", save, func(checked bool) {
229 ui.HLClient.Logger.Infow("saving bookmark")
230 // TODO: Implement bookmark saving
232 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())
233 out, err := yaml.Marshal(ui.HLClient.Pref)
238 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
242 // Pref := ui.HLClient.Pref
244 AddButton("Cancel", func() {
245 ui.Pages.SwitchToPage(backPage)
247 AddButton("Connect", func() {
248 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
249 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
250 err := ui.joinServer(
253 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
256 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
258 ui.HLClient.serverName = name
261 ui.HLClient.Logger.Errorw("login error", "err", err)
262 loginErrModal := tview.NewModal().
263 AddButtons([]string{"Oh no"}).
264 SetText(err.Error()).
265 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
266 ui.Pages.SwitchToPage(backPage)
269 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
273 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
274 // TODO: implement bookmark saving
278 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
279 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
280 if event.Key() == tcell.KeyEscape {
281 ui.Pages.SwitchToPage(backPage)
287 joinServerForm.SetFocus(5)
290 joinServerPage := tview.NewFlex().
291 AddItem(nil, 0, 1, false).
292 AddItem(tview.NewFlex().
293 SetDirection(tview.FlexRow).
294 AddItem(nil, 0, 1, false).
295 AddItem(joinServerForm, 14, 1, true).
296 AddItem(nil, 0, 1, false), 40, 1, true).
297 AddItem(nil, 0, 1, false)
299 return joinServerPage
302 func (ui *UI) renderServerUI() *tview.Flex {
303 ui.chatBox.SetText("") // clear any previously existing chatbox text
304 commandList := tview.NewTextView().SetDynamicColors(true)
306 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
308 SetTitle("| Keyboard Shortcuts| ")
310 modal := tview.NewModal().
311 SetText("Disconnect from the server?").
312 AddButtons([]string{"Cancel", "Exit"}).
314 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
315 if buttonIndex == 1 {
316 _ = ui.HLClient.Disconnect()
317 ui.Pages.RemovePage(pageServerUI)
318 ui.Pages.SwitchToPage("home")
320 ui.Pages.HidePage("modal")
324 serverUI := tview.NewFlex().
325 AddItem(tview.NewFlex().
326 SetDirection(tview.FlexRow).
327 AddItem(commandList, 4, 0, false).
328 AddItem(ui.chatBox, 0, 8, false).
329 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
330 AddItem(ui.userList, 25, 1, false)
331 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
332 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
333 if event.Key() == tcell.KeyEscape {
334 ui.Pages.AddPage("modal", modal, false, true)
338 if event.Key() == tcell.KeyCtrlF {
339 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
340 ui.HLClient.Logger.Errorw("err", "err", err)
345 if event.Key() == tcell.KeyCtrlN {
346 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
347 ui.HLClient.Logger.Errorw("err", "err", err)
352 if event.Key() == tcell.KeyCtrlP {
353 newsFlex := tview.NewFlex()
354 newsFlex.SetBorderPadding(0, 0, 1, 1)
355 newsPostTextArea := tview.NewTextView()
356 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
357 newsPostTextArea.SetChangedFunc(func() {
358 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
361 newsPostForm := tview.NewForm().
362 SetButtonsAlign(tview.AlignRight).
363 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
364 AddButton("Send", nil)
365 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
367 case tcell.KeyEscape:
368 ui.Pages.RemovePage("newsInput")
370 ui.App.SetFocus(newsPostTextArea)
372 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
373 if len(newsText) == 0 {
376 err := ui.HLClient.Send(
377 *NewTransaction(TranOldPostNews, nil,
378 NewField(FieldData, []byte(newsText)),
382 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
383 // TODO: display errModal to user
385 ui.Pages.RemovePage("newsInput")
392 SetDirection(tview.FlexRow).
394 SetTitle("| Post Message |")
396 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
398 case tcell.KeyEscape:
399 ui.Pages.RemovePage("newsInput")
401 ui.App.SetFocus(newsPostForm)
403 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
405 const windowsBackspaceRune = 8
406 const macBackspaceRune = 127
407 switch event.Rune() {
408 case macBackspaceRune, windowsBackspaceRune:
409 curTxt := newsPostTextArea.GetText(true)
411 curTxt = curTxt[:len(curTxt)-1]
412 newsPostTextArea.SetText(curTxt)
415 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
422 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
423 newsFlex.AddItem(newsPostForm, 3, 0, false)
425 newsPostPage := tview.NewFlex().
426 AddItem(nil, 0, 1, false).
427 AddItem(tview.NewFlex().
428 SetDirection(tview.FlexRow).
429 AddItem(nil, 0, 1, false).
430 AddItem(newsFlex, 15, 1, true).
431 // AddItem(newsPostForm, 3, 0, false).
432 AddItem(nil, 0, 1, false), 40, 1, false).
433 AddItem(nil, 0, 1, false)
435 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
436 ui.App.SetFocus(newsPostTextArea)
444 func (ui *UI) Start() {
445 home := tview.NewFlex().SetDirection(tview.FlexRow)
446 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
447 mainMenu := tview.NewList()
449 bannerItem := tview.NewTextView().
450 SetText(randomBanner()).
451 SetDynamicColors(true).
452 SetTextAlign(tview.AlignCenter)
455 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
457 home.AddItem(tview.NewFlex().
458 AddItem(nil, 0, 1, false).
459 AddItem(mainMenu, 0, 1, true).
460 AddItem(nil, 0, 1, false),
464 mainMenu.AddItem("Join Server", "", 'j', func() {
465 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
466 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
468 AddItem("Bookmarks", "", 'b', func() {
469 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
471 AddItem("Browse Tracker", "", 't', func() {
472 ui.trackerList = ui.getTrackerList()
473 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
475 AddItem("Settings", "", 's', func() {
476 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
478 AddItem("Quit", "", 'q', func() {
482 ui.Pages.AddPage("home", home, true, true)
484 // App level input capture
485 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
486 if event.Key() == tcell.KeyCtrlC {
487 ui.HLClient.Logger.Infow("Exiting")
492 if event.Key() == tcell.KeyCtrlL {
493 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
494 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
495 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
496 if key == tcell.KeyEscape {
497 ui.Pages.RemovePage("logs")
501 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
506 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {