package main import ( "bufio" "fmt" "io" "os/exec" "strings" "time" "code.rocketnine.space/tslocum/cbind" "github.com/gdamore/tcell/v2" "github.com/normen/whatscli/config" "github.com/normen/whatscli/messages" "github.com/rivo/tview" "github.com/skratchdot/open-golang/open" "github.com/zyedidia/clipboard" ) var VERSION string = "v1.0.11" var sndTxt string = "" var currentReceiver messages.Chat = messages.Chat{} var curRegions []messages.Message var textView *tview.TextView var treeView *tview.TreeView var textInput *tview.InputField var topBar *tview.TextView var infoBar *tview.TextView var chatRoot *tview.TreeNode var app *tview.Application var sessionManager *messages.SessionManager var keyBindings *cbind.Configuration var uiHandler messages.UiMessageHandler func main() { config.InitConfig() uiHandler = UiHandler{} sessionManager = &messages.SessionManager{} sessionManager.Init(uiHandler) app = tview.NewApplication() sideBarWidth := config.Config.Ui.ChatSidebarWidth gridLayout := tview.NewGrid() gridLayout.SetRows(1, 0, 1) gridLayout.SetColumns(sideBarWidth, 0, sideBarWidth) gridLayout.SetBorders(true) gridLayout.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background]) gridLayout.SetBordersColor(tcell.ColorNames[config.Config.Colors.Borders]) cmdPrefix := config.Config.General.CmdPrefix topBar = tview.NewTextView() topBar.SetDynamicColors(true) topBar.SetScrollable(false) topBar.SetText("[::b] WhatsCLI " + VERSION + " [-::d]Type " + cmdPrefix + "help or press " + config.Config.Keymap.CommandHelp + " for help") topBar.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background]) infoBar = tview.NewTextView() infoBar.SetDynamicColors(true) UpdateStatusBar(messages.SessionStatus{}) textView = tview.NewTextView(). SetDynamicColors(true). SetRegions(true). SetWordWrap(true). SetChangedFunc(func() { app.Draw() }) textView.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background]) textView.SetTextColor(tcell.ColorNames[config.Config.Colors.Text]) PrintHelp() textInput = tview.NewInputField() textInput.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background]) textInput.SetFieldBackgroundColor(tcell.ColorNames[config.Config.Colors.InputBackground]) textInput.SetFieldTextColor(tcell.ColorNames[config.Config.Colors.InputText]) textInput.SetChangedFunc(func(change string) { sndTxt = change }) textInput.SetDoneFunc(EnterCommand) textInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyDown { offset, _ := textView.GetScrollOffset() offset += 1 textView.ScrollTo(offset, 0) return nil } if event.Key() == tcell.KeyUp { offset, _ := textView.GetScrollOffset() offset -= 1 textView.ScrollTo(offset, 0) return nil } if event.Key() == tcell.KeyPgDn { offset, _ := textView.GetScrollOffset() offset += 10 textView.ScrollTo(offset, 0) return nil } if event.Key() == tcell.KeyPgUp { offset, _ := textView.GetScrollOffset() offset -= 10 textView.ScrollTo(offset, 0) return nil } return event }) gridLayout.AddItem(topBar, 0, 0, 1, 4, 0, 0, false) gridLayout.AddItem(infoBar, 2, 0, 1, 1, 0, 0, false) gridLayout.AddItem(MakeTree(), 1, 0, 1, 1, 0, 0, false) gridLayout.AddItem(textView, 1, 1, 1, 3, 0, 0, false) gridLayout.AddItem(textInput, 2, 1, 1, 3, 0, 0, false) app.SetRoot(gridLayout, true) app.EnableMouse(true) app.SetFocus(textInput) if err := sessionManager.StartManager(); err != nil { PrintError(err) } LoadShortcuts() app.Run() } // creates the TreeView for chats func MakeTree() *tview.TreeView { rootDir := "Chats" chatRoot = tview.NewTreeNode(rootDir). SetColor(tcell.ColorNames[config.Config.Colors.ListHeader]) treeView = tview.NewTreeView(). SetRoot(chatRoot). SetCurrentNode(chatRoot) treeView.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background]) // If a chat was selected, open it. treeView.SetChangedFunc(func(node *tview.TreeNode) { reference := node.GetReference() if reference == nil { SetDisplayedChat(messages.Chat{"", false, "", 0, 0}) return // Selecting the root node does nothing. } children := node.GetChildren() if len(children) == 0 { // Load and show files in this directory. recv := reference.(messages.Chat) SetDisplayedChat(recv) } else { // Collapse if visible, expand if collapsed. node.SetExpanded(!node.IsExpanded()) } }) return treeView } func handleFocusMessage(ev *tcell.EventKey) *tcell.EventKey { if !textView.HasFocus() { app.SetFocus(textView) if curRegions != nil && len(curRegions) > 0 { textView.Highlight(curRegions[len(curRegions)-1].Id) } } return nil } func handleFocusInput(ev *tcell.EventKey) *tcell.EventKey { ResetMsgSelection() if !textInput.HasFocus() { app.SetFocus(textInput) } return nil } func handleFocusContacts(ev *tcell.EventKey) *tcell.EventKey { ResetMsgSelection() if !treeView.HasFocus() { app.SetFocus(treeView) } return nil } func handleSwitchPanels(ev *tcell.EventKey) *tcell.EventKey { ResetMsgSelection() if !textInput.HasFocus() { app.SetFocus(textInput) } else { app.SetFocus(treeView) } return nil } func handleCommand(command string) func(ev *tcell.EventKey) *tcell.EventKey { return func(ev *tcell.EventKey) *tcell.EventKey { sessionManager.CommandChannel <- messages.Command{command, nil} return nil } } func handleCopyUser(ev *tcell.EventKey) *tcell.EventKey { if hls := textView.GetHighlights(); len(hls) > 0 { for _, val := range curRegions { if val.Id == hls[0] { clipboard.WriteAll(val.ContactId, "clipboard") PrintText("copied id of " + val.ContactName + " to clipboard") } } ResetMsgSelection() } else if currentReceiver.Id != "" { clipboard.WriteAll(currentReceiver.Id, "clipboard") PrintText("copied id of " + currentReceiver.Name + " to clipboard") } return nil } func handlePasteUser(ev *tcell.EventKey) *tcell.EventKey { if clip, err := clipboard.ReadAll("clipboard"); err == nil { textInput.SetText(textInput.GetText() + " " + clip) } return nil } func handleQuit(ev *tcell.EventKey) *tcell.EventKey { sessionManager.CommandChannel <- messages.Command{"disconnect", nil} app.Stop() return nil } func handleHelp(ev *tcell.EventKey) *tcell.EventKey { PrintHelp() return nil } func handleMessageCommand(command string) func(ev *tcell.EventKey) *tcell.EventKey { return func(ev *tcell.EventKey) *tcell.EventKey { hls := textView.GetHighlights() if len(hls) > 0 { sessionManager.CommandChannel <- messages.Command{command, []string{hls[0]}} ResetMsgSelection() app.SetFocus(textInput) } return nil } } func handleMessagesMove(amount int) func(ev *tcell.EventKey) *tcell.EventKey { return func(ev *tcell.EventKey) *tcell.EventKey { if curRegions == nil || len(curRegions) == 0 { return nil } hls := textView.GetHighlights() if len(hls) > 0 { newId := GetOffsetMsgId(hls[0], amount) if newId != "" { textView.Highlight(newId) } } else { if amount < 0 { textView.Highlight(curRegions[0].Id) } else { textView.Highlight(curRegions[len(curRegions)-1].Id) } } textView.ScrollToHighlight() return nil } } func handleChatPanelUp(ev *tcell.EventKey) *tcell.EventKey { //TODO: scroll selection in treeView? or chatRoot? How? return ev } func handleChatPanelDown(ev *tcell.EventKey) *tcell.EventKey { return ev } func handleMessagesLast(ev *tcell.EventKey) *tcell.EventKey { if curRegions == nil || len(curRegions) == 0 { return nil } textView.Highlight(curRegions[len(curRegions)-1].Id) textView.ScrollToHighlight() return nil } func handleMessagesFirst(ev *tcell.EventKey) *tcell.EventKey { if curRegions == nil || len(curRegions) == 0 { return nil } textView.Highlight(curRegions[0].Id) textView.ScrollToHighlight() return nil } func handleExitMessages(ev *tcell.EventKey) *tcell.EventKey { if curRegions == nil || len(curRegions) == 0 { return nil } ResetMsgSelection() app.SetFocus(textInput) return nil } // load the key map func LoadShortcuts() { // global bindings for app keyBindings = cbind.NewConfiguration() if err := keyBindings.Set(config.Config.Keymap.FocusMessages, handleFocusMessage); err != nil { PrintErrorMsg("focus_messages:", err) } if err := keyBindings.Set(config.Config.Keymap.FocusInput, handleFocusInput); err != nil { PrintErrorMsg("focus_input:", err) } if err := keyBindings.Set(config.Config.Keymap.FocusChats, handleFocusContacts); err != nil { PrintErrorMsg("focus_contacts:", err) } if err := keyBindings.Set(config.Config.Keymap.SwitchPanels, handleSwitchPanels); err != nil { PrintErrorMsg("switch_panels:", err) } if err := keyBindings.Set(config.Config.Keymap.CommandRead, handleCommand("read")); err != nil { PrintErrorMsg("command_read:", err) } if err := keyBindings.Set(config.Config.Keymap.Copyuser, handleCopyUser); err != nil { PrintErrorMsg("copyuser:", err) } if err := keyBindings.Set(config.Config.Keymap.Pasteuser, handlePasteUser); err != nil { PrintErrorMsg("pasteuser:", err) } if err := keyBindings.Set(config.Config.Keymap.CommandBacklog, handleCommand("backlog")); err != nil { PrintErrorMsg("command_backlog:", err) } if err := keyBindings.Set(config.Config.Keymap.CommandConnect, handleCommand("login")); err != nil { PrintErrorMsg("command_connect:", err) } if err := keyBindings.Set(config.Config.Keymap.CommandQuit, handleQuit); err != nil { PrintErrorMsg("command_quit:", err) } if err := keyBindings.Set(config.Config.Keymap.CommandHelp, handleHelp); err != nil { PrintErrorMsg("command_help:", err) } app.SetInputCapture(keyBindings.Capture) // bindings for chat message text view keysMessages := cbind.NewConfiguration() if err := keysMessages.Set(config.Config.Keymap.MessageDownload, handleMessageCommand("download")); err != nil { PrintErrorMsg("message_download:", err) } if err := keysMessages.Set(config.Config.Keymap.MessageOpen, handleMessageCommand("open")); err != nil { PrintErrorMsg("message_open:", err) } if err := keysMessages.Set(config.Config.Keymap.Copyuser, handleCopyUser); err != nil { PrintErrorMsg("copyuser:", err) } if err := keysMessages.Set(config.Config.Keymap.Pasteuser, handlePasteUser); err != nil { PrintErrorMsg("pasteuser:", err) } if err := keysMessages.Set(config.Config.Keymap.MessageShow, handleMessageCommand("show")); err != nil { PrintErrorMsg("message_show:", err) } if err := keysMessages.Set(config.Config.Keymap.MessageUrl, handleMessageCommand("url")); err != nil { PrintErrorMsg("message_url:", err) } if err := keysMessages.Set(config.Config.Keymap.MessageInfo, handleMessageCommand("info")); err != nil { PrintErrorMsg("message_info:", err) } if err := keysMessages.Set(config.Config.Keymap.MessageRevoke, handleMessageCommand("revoke")); err != nil { PrintErrorMsg("message_revoke:", err) } keysMessages.SetKey(tcell.ModNone, tcell.KeyEscape, handleExitMessages) keysMessages.SetKey(tcell.ModNone, tcell.KeyUp, handleMessagesMove(-1)) keysMessages.SetKey(tcell.ModNone, tcell.KeyDown, handleMessagesMove(1)) keysMessages.SetKey(tcell.ModNone, tcell.KeyPgUp, handleMessagesMove(-10)) keysMessages.SetKey(tcell.ModNone, tcell.KeyPgDn, handleMessagesMove(10)) keysMessages.SetRune(tcell.ModNone, 'k', handleMessagesMove(-1)) keysMessages.SetRune(tcell.ModNone, 'j', handleMessagesMove(1)) keysMessages.SetRune(tcell.ModNone, 'g', handleMessagesFirst) keysMessages.SetRune(tcell.ModNone, 'G', handleMessagesLast) keysMessages.SetRune(tcell.ModCtrl, 'u', handleMessagesMove(-10)) keysMessages.SetRune(tcell.ModCtrl, 'd', handleMessagesMove(10)) textView.SetInputCapture(keysMessages.Capture) keysChatPanel := cbind.NewConfiguration() keysChatPanel.SetRune(tcell.ModCtrl, 'u', handleChatPanelUp) keysChatPanel.SetRune(tcell.ModCtrl, 'd', handleChatPanelDown) treeView.SetInputCapture(keysChatPanel.Capture) } // prints help to chat view func PrintHelp() { cmdPrefix := config.Config.General.CmdPrefix fmt.Fprintln(textView, "[-::u]Keys:[-::-]") fmt.Fprintln(textView, "") fmt.Fprintln(textView, "Global") fmt.Fprintln(textView, "[::b] Up/Down[::-] = Scroll history/chats") fmt.Fprintln(textView, "[::b]", config.Config.Keymap.SwitchPanels, "[::-] = Switch input/chats") fmt.Fprintln(textView, "[::b]", config.Config.Keymap.FocusMessages, "[::-] = Focus message panel") fmt.Fprintln(textView, "[::b]", config.Config.Keymap.CommandQuit, "[::-] = Exit app") fmt.Fprintln(textView, "") fmt.Fprintln(textView, "[-::-]Message panel[-::-]") fmt.Fprintln(textView, "[::b] Up/Down[::-] = select message") fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageDownload, "[::-] = Download attachment") fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageOpen, "[::-] = Download & open attachment") fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageShow, "[::-] = Download & show image using", config.Config.General.ShowCommand) fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageUrl, "[::-] = Find URL in message and open it") fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageRevoke, "[::-] = Revoke message") fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageInfo, "[::-] = Info about message") fmt.Fprintln(textView, "") fmt.Fprintln(textView, "Config file in ->", config.GetConfigFilePath()) fmt.Fprintln(textView, "") fmt.Fprintln(textView, "Type [::b]"+cmdPrefix+"commands[::-] to see all commands") fmt.Fprintln(textView, "") } func PrintCommands() { cmdPrefix := config.Config.General.CmdPrefix fmt.Fprintln(textView, "") fmt.Fprintln(textView, "[-::u]Commands:[-::-]") fmt.Fprintln(textView, "") fmt.Fprintln(textView, "[-::-]Global[-::-]") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"connect [::-]or[::b]", config.Config.Keymap.CommandConnect, "[::-] = (Re)Connect to server") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"disconnect[::-] = Close the connection") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"logout[::-] = Remove login data from computer") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"quit [::-]or[::b]", config.Config.Keymap.CommandQuit, "[::-] = Exit app") fmt.Fprintln(textView, "") fmt.Fprintln(textView, "[-::-]Chat[-::-]") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"backlog [::-]or[::b]", config.Config.Keymap.CommandBacklog, "[::-] = load next", config.Config.General.BacklogMsgQuantity, "previous messages") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"read [::-]or[::b]", config.Config.Keymap.CommandRead, "[::-] = mark new messages in chat as read") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"upload[::-] /path/to/file = Upload any file as document") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendimage[::-] /path/to/file = Send image message") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendvideo[::-] /path/to/file = Send video message") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendaudio[::-] /path/to/file = Send audio message") fmt.Fprintln(textView, "") fmt.Fprintln(textView, "[-::-]Groups[-::-]") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"leave[::-] = Leave group") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"create[::-] [user-id[] [user-id[] Group Subject = Create group with users") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"subject[::-] New Subject = Change subject of group") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"add[::-] [user-id[] = Add user to group") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"remove[::-] [user-id[] = Remove user from group") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"admin[::-] [user-id[] = Set admin role for user in group") fmt.Fprintln(textView, "[::b] "+cmdPrefix+"removeadmin[::-] [user-id[] = Remove admin role for user in group") fmt.Fprintln(textView, "") fmt.Fprintln(textView, "Use[::b]", config.Config.Keymap.Copyuser, "[::-]to copy a selected user id to clipboard") fmt.Fprintln(textView, "Use[::b]", config.Config.Keymap.Pasteuser, "[::-]to paste clipboard to text input") fmt.Fprintln(textView, "") } // called when text is entered by the user func EnterCommand(key tcell.Key) { if sndTxt == "" { return } if key == tcell.KeyEsc { textInput.SetText("") return } cmdPrefix := config.Config.General.CmdPrefix if sndTxt == cmdPrefix+"help" { PrintHelp() textInput.SetText("") return } if sndTxt == cmdPrefix+"commands" { PrintCommands() textInput.SetText("") return } if sndTxt == cmdPrefix+"quit" { sessionManager.CommandChannel <- messages.Command{"disconnect", nil} app.Stop() return } if strings.HasPrefix(sndTxt, cmdPrefix) { cmd := strings.TrimPrefix(sndTxt, cmdPrefix) var params []string if strings.Index(cmd, " ") >= 0 { cmdParts := strings.Split(cmd, " ") cmd = cmdParts[0] params = cmdParts[1:] } sessionManager.CommandChannel <- messages.Command{cmd, params} textInput.SetText("") return } if currentReceiver.Id == "" { PrintText("no receiver") textInput.SetText("") return } // no command, send as message msg := messages.Command{ Name: "send", Params: []string{currentReceiver.Id, sndTxt}, } sessionManager.CommandChannel <- msg textInput.SetText("") } // get the next message id to select (highlighted + offset) func GetOffsetMsgId(curId string, offset int) string { if curRegions == nil || len(curRegions) == 0 { return "" } for idx, val := range curRegions { if val.Id == curId { arrPos := idx + offset if len(curRegions) > arrPos && arrPos >= 0 { return curRegions[arrPos].Id } } } if offset > 0 { return curRegions[0].Id } else { return curRegions[len(curRegions)-1].Id } } // resets the selection in the textView and scrolls it down func ResetMsgSelection() { if len(textView.GetHighlights()) > 0 { textView.Highlight("") } textView.ScrollToEnd() } // prints text to the TextView func PrintText(txt string) { fmt.Fprintln(textView, txt) } // prints an error to the TextView func PrintError(err error) { if err == nil { return } fmt.Fprintln(textView, "["+config.Config.Colors.Negative+"]", err.Error(), "[-]") } // prints an error to the TextView func PrintErrorMsg(text string, err error) { if err == nil { return } fmt.Fprintln(textView, "["+config.Config.Colors.Negative+"]", text, err.Error(), "[-]") } // prints an image attachment to the TextView (by message id) func PrintImage(path string) { var err error cmdParts := strings.Split(config.Config.General.ShowCommand, " ") cmdParts = append(cmdParts, path) var cmd *exec.Cmd size := len(cmdParts) if size > 1 { cmd = exec.Command(cmdParts[0], cmdParts[1:]...) } else if size > 0 { cmd = exec.Command(cmdParts[0]) } var stdout io.ReadCloser if stdout, err = cmd.StdoutPipe(); err == nil { if err = cmd.Start(); err == nil { reader := bufio.NewReader(stdout) io.Copy(tview.ANSIWriter(textView), reader) return } } PrintError(err) } // updates the status bar func UpdateStatusBar(statusInfo messages.SessionStatus) { out := " " if statusInfo.Connected { out += "[" + config.Config.Colors.Positive + "]online[-]" } else { out += "[" + config.Config.Colors.Negative + "]offline[-]" } out += " " out += "[::d] (" out += fmt.Sprint(statusInfo.BatteryCharge) out += "%" if statusInfo.BatteryLoading { out += " [" + config.Config.Colors.Positive + "]L[-]" } else { out += " [" + config.Config.Colors.Negative + "]l[-]" } if statusInfo.BatteryPowersave { out += " [" + config.Config.Colors.Negative + "]S[-]" } else { out += " [" + config.Config.Colors.Positive + "]s[-]" } out += ")[::-] " out += statusInfo.LastSeen infoBar.SetText(out) //infoBar.SetText("🔋: ??%") } // sets the current chat, loads text from storage to TextView func SetDisplayedChat(wid messages.Chat) { //TODO: how to get chat to set currentReceiver = wid textView.Clear() textView.SetTitle(wid.Name) sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver.Id}} } // get a string representation of all messages for chat func getMessagesString(msgs []messages.Message) string { out := "" for _, msg := range msgs { out += getTextMessageString(&msg) out += "\n" } return out } // create a formatted string with regions based on message ID from a text message //TODO: optimize, use Sprintf etc func getTextMessageString(msg *messages.Message) string { colorMe := config.Config.Colors.ChatMe colorContact := config.Config.Colors.ChatContact out := "" text := tview.Escape(msg.Text) if msg.Forwarded { text = "[" + config.Config.Colors.ForwardedText + "]" + text + "[-]" } tim := time.Unix(int64(msg.Timestamp), 0) time := tim.Format("02-01-06 15:04:05") out += "[\"" out += msg.Id out += "\"]" if msg.FromMe { //msg from me out += "[-::d](" + time + ") [" + colorMe + "::b]Me: [-::-]" + text } else { // message from others out += "[-::d](" + time + ") [" + colorContact + "::b]" + msg.ContactShort + ": [-::-]" + text } out += "[\"\"]" return out } type UiHandler struct{} func (u UiHandler) NewMessage(msg messages.Message) { //TODO: its stupid to "go" this as its supposed to run //on the ui thread anyway. But QueueUpdate blocks...? go app.QueueUpdateDraw(func() { curRegions = append(curRegions, msg) PrintText(getTextMessageString(&msg)) }) } func (u UiHandler) NewScreen(msgs []messages.Message) { go app.QueueUpdateDraw(func() { textView.Clear() screen := getMessagesString(msgs) textView.SetText(screen) curRegions = msgs if screen == "" { if currentReceiver.Id == "" { PrintHelp() } else { PrintText("[::d] ~~~ no messages, press " + config.Config.Keymap.CommandBacklog + " to load backlog if available ~~~[::-]") } } }) } // loads the chat data from storage to the TreeView func (u UiHandler) SetChats(ids []messages.Chat) { go app.QueueUpdateDraw(func() { chatRoot.ClearChildren() oldId := currentReceiver.Id for _, element := range ids { name := element.Name if name == "" { name = strings.TrimSuffix(strings.TrimSuffix(element.Id, messages.GROUPSUFFIX), messages.CONTACTSUFFIX) } if element.Unread > 0 { name += " ([" + config.Config.Colors.UnreadCount + "]" + fmt.Sprint(element.Unread) + "[-])" //tim := time.Unix(element.LastMessage, 0) //sin := time.Since(tim) //since := fmt.Sprintf("%s", sin) //time := tim.Format("02-01-06 15:04:05") //name += since } node := tview.NewTreeNode(name). SetReference(element). SetSelectable(true) if element.IsGroup { node.SetColor(tcell.ColorNames[config.Config.Colors.ListGroup]) } else { node.SetColor(tcell.ColorNames[config.Config.Colors.ListContact]) } // store new currentReceiver, else the selection on the left goes off if element.Id == oldId { currentReceiver = element } chatRoot.AddChild(node) if element.Id == currentReceiver.Id { treeView.SetCurrentNode(node) } } }) } func (u UiHandler) PrintError(err error) { PrintError(err) } func (u UiHandler) PrintText(msg string) { PrintText(msg) } func (u UiHandler) PrintFile(path string) { go app.QueueUpdateDraw(func() { PrintImage(path) }) } func (u UiHandler) OpenFile(path string) { open.Run(path) } func (u UiHandler) SetStatus(status messages.SessionStatus) { go app.QueueUpdateDraw(func() { UpdateStatusBar(status) }) } func (u UiHandler) GetWriter() io.Writer { return textView }