From 439f09aa1c858265d90c9f3086cef4a71ffed848 Mon Sep 17 00:00:00 2001 From: Casper Weiss Bang Date: Thu, 17 Oct 2019 07:56:47 +0200 Subject: [PATCH] Mo' colors Changes: - Stream is colored now - Stream is formatted - Stream has it's own formatting option - Colors are now a style, and is a struct - Color struct has a pretty cool functional interface - colored mentions and PMs - Every message uses the same function (it's dry!!) - Colorize errors! - Create function for visualizing errors - colorized some of the command output! - Color is stored in a Style - Create a Text struct that can use to stylize strings "easily" - Text can be used to build strings - color highlighting on code - added tml config support - added different color for mention url - Added sprintf to use formatting with PrintFeed and PrintError Known Bugs: (added as todos whereever) - Cannot use multiple formatting at the same time (*bold _italic_* doesn't work - sprintf is pretty shit - background doesn't cover as a `block` in codeblocks - not possible to escape sprintf thing --- cmdDelete.go | 3 +- cmdDownload.go | 13 +-- cmdEdit.go | 12 +-- cmdJoin.go | 6 +- cmdPost.go | 4 +- cmdReact.go | 2 +- cmdReply.go | 7 +- cmdSet.go | 46 ++++++--- cmdStream.go | 8 +- cmdUploadFile.go | 9 +- cmdWall.go | 5 +- cmdWallet.go | 8 +- colors.go | 234 ++++++++++++++++++++++++++++++++++++++----- kbtui.tml | 57 ++++++++--- main.go | 160 ++++++++++++++++++++--------- tcmdShowReactions.go | 10 +- userConfigs.go | 36 ++++--- 17 files changed, 470 insertions(+), 150 deletions(-) diff --git a/cmdDelete.go b/cmdDelete.go index 7fa1c72..5c74b1d 100644 --- a/cmdDelete.go +++ b/cmdDelete.go @@ -3,7 +3,6 @@ package main import ( - "fmt" "strconv" ) @@ -28,7 +27,7 @@ func cmdDelete(cmd []string) { chat := k.NewChat(channel) _, err := chat.Delete(messageID) if err != nil { - printToView("Feed", fmt.Sprintf("There was an error deleting your message.")) + printError("There was an error deleting your message.") } } diff --git a/cmdDownload.go b/cmdDownload.go index 796517c..bda6233 100644 --- a/cmdDownload.go +++ b/cmdDownload.go @@ -21,22 +21,22 @@ func init() { func cmdDownloadFile(cmd []string) { if len(cmd) < 2 { - printToView("Feed", fmt.Sprintf("%s%s $messageId $fileName - Download a file to user's downloadpath", cmdPrefix, cmd[0])) + printInfo(fmt.Sprintf("%s%s $messageId $fileName - Download a file to user's downloadpath", cmdPrefix, cmd[0])) return } messageID, err := strconv.Atoi(cmd[1]) if err != nil { - printToView("Feed", "There was an error converting your messageID to an int") + printError("There was an error converting your messageID to an int") return } chat := k.NewChat(channel) api, err := chat.ReadMessage(messageID) if err != nil { - printToView("Feed", fmt.Sprintf("There was an error pulling message %d", messageID)) + printError(fmt.Sprintf("There was an error pulling message %d", messageID)) return } if api.Result.Messages[0].Msg.Content.Type != "attachment" { - printToView("Feed", "No attachment detected") + printError("No attachment detected") return } var fileName string @@ -47,9 +47,10 @@ func cmdDownloadFile(cmd []string) { } _, err = chat.Download(messageID, fmt.Sprintf("%s/%s", downloadPath, fileName)) + channelName := messageLinkKeybaseColor.stylize(channel.Name) if err != nil { - printToView("Feed", fmt.Sprintf("There was an error downloading %s from %s", fileName, channel.Name)) + printErrorF(fmt.Sprintf("There was an error downloading %s from $TEXT", fileName), channelName) } else { - printToView("Feed", fmt.Sprintf("Downloaded %s from %s", fileName, channel.Name)) + printInfoF(fmt.Sprintf("Downloaded %s from $TEXT", fileName), channelName) } } diff --git a/cmdEdit.go b/cmdEdit.go index ed950eb..b8b0fe1 100644 --- a/cmdEdit.go +++ b/cmdEdit.go @@ -27,21 +27,21 @@ func cmdEdit(cmd []string) { messageID, _ = strconv.Atoi(cmd[1]) } else if lastMessage.ID != 0 { if lastMessage.Type != "text" { - printToView("Feed", "Last message isn't editable (is it an edit?)") + printError("Last message isn't editable (is it an edit?)") return } messageID = lastMessage.ID } else { - printToView("Feed", "No message to edit") + printError("No message to edit") return } origMessage, _ := chat.ReadMessage(messageID) if origMessage.Result.Messages[0].Msg.Content.Type != "text" { - printToView("Feed", fmt.Sprintf("%+v", origMessage)) + printInfo(fmt.Sprintf("%+v", origMessage)) return } if origMessage.Result.Messages[0].Msg.Sender.Username != k.Username { - printToView("Feed", "You cannot edit another user's messages.") + printError("You cannot edit another user's messages.") return } editString := origMessage.Result.Messages[0].Msg.Content.Text.Body @@ -53,14 +53,14 @@ func cmdEdit(cmd []string) { return } if len(cmd) < 3 { - printToView("Feed", "Not enough options for Edit") + printError("Not enough options for Edit") return } messageID, _ = strconv.Atoi(cmd[1]) newMessage := strings.Join(cmd[2:], " ") _, err := chat.Edit(messageID, newMessage) if err != nil { - printToView("Feed", fmt.Sprintf("Error editing message %d, %+v", messageID, err)) + printError(fmt.Sprintf("Error editing message %d, %+v", messageID, err)) } } diff --git a/cmdJoin.go b/cmdJoin.go index fa5aee0..05858c7 100644 --- a/cmdJoin.go +++ b/cmdJoin.go @@ -41,12 +41,12 @@ func cmdJoin(cmd []string) { channel.TopicName = "" channel.MembersType = keybase.USER } - printToView("Feed", fmt.Sprintf("You are joining: %s", joinedName)) + printInfoF("You are joining: $TEXT", messageLinkKeybaseColor.stylize(joinedName)) clearView("Chat") setViewTitle("Input", fmt.Sprintf(" %s ", joinedName)) go populateChat() default: - printToView("Feed", fmt.Sprintf("To join a team use %sjoin ", cmdPrefix)) - printToView("Feed", fmt.Sprintf("To join a PM use %sjoin ", cmdPrefix)) + printInfo(fmt.Sprintf("To join a team use %sjoin ", cmdPrefix)) + printInfo(fmt.Sprintf("To join a PM use %sjoin ", cmdPrefix)) } } diff --git a/cmdPost.go b/cmdPost.go index 27008b5..35e44d3 100644 --- a/cmdPost.go +++ b/cmdPost.go @@ -29,8 +29,8 @@ func cmdPost(cmd []string) { chat := k.NewChat(pubChan) _, err := chat.Send(post) if err != nil { - printToView("Feed", fmt.Sprintf("There was an error with your post: %+v", err)) + printError(fmt.Sprintf("There was an error with your post: %+v", err)) } else { - printToView("Feed", "You have publically posted to your wall, signed by your current device.") + printInfo("You have publically posted to your wall, signed by your current device.") } } diff --git a/cmdReact.go b/cmdReact.go index ddd92f6..cf11e3e 100644 --- a/cmdReact.go +++ b/cmdReact.go @@ -38,6 +38,6 @@ func doReact(messageID int, reaction string) { chat := k.NewChat(channel) _, err := chat.React(messageID, reaction) if err != nil { - printToView("Feed", "There was an error reacting to the message.") + printError("There was an error reacting to the message.") } } diff --git a/cmdReply.go b/cmdReply.go index 898d5e3..9a21aa3 100644 --- a/cmdReply.go +++ b/cmdReply.go @@ -22,18 +22,17 @@ func init() { func cmdReply(cmd []string) { chat := k.NewChat(channel) if len(cmd) < 2 { - printToView("Feed", fmt.Sprintf("%s%s $ID - Reply to message $ID", cmdPrefix, cmd[0])) + printInfo(fmt.Sprintf("%s%s $ID - Reply to message $ID", cmdPrefix, cmd[0])) return } messageID, err := strconv.Atoi(cmd[1]) if err != nil { - printToView("Feed", fmt.Sprintf("There was an error determining message ID %s", cmd[1])) + printError(fmt.Sprintf("There was an error determining message ID %s", cmd[1])) return } _, err = chat.Reply(messageID, strings.Join(cmd[2:], " ")) if err != nil { - printToView("Feed", "There was an error with your reply.") + printError("There was an error with your reply.") return } - return } diff --git a/cmdSet.go b/cmdSet.go index bd5e17a..b0e3121 100644 --- a/cmdSet.go +++ b/cmdSet.go @@ -23,25 +23,24 @@ func printSetting(cmd []string) { switch cmd[1] { case "load": loadFromToml() + printInfo("Loading config from toml") case "downloadPath": - printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], downloadPath)) + printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], downloadPath)) case "outputFormat": - printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], outputFormat)) + printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], outputFormat)) case "dateFormat": - printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], dateFormat)) + printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], dateFormat)) case "timeFormat": - printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], timeFormat)) + printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], timeFormat)) case "cmdPrefix": - printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], cmdPrefix)) + printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], cmdPrefix)) default: - printToView("Feed", fmt.Sprintf("Unknown config value %s", cmd[1])) + printError(fmt.Sprintf("Unknown config value %s", cmd[1])) } - - return } func cmdSet(cmd []string) { if len(cmd) < 2 { - printToView("Feed", "No config value specified") + printError("No config value specified") return } if len(cmd) < 3 { @@ -51,7 +50,7 @@ func cmdSet(cmd []string) { switch cmd[1] { case "downloadPath": if len(cmd) != 3 { - printToView("Feed", "Invalid download path.") + printError("Invalid download path.") } downloadPath = cmd[2] case "outputFormat": @@ -63,17 +62,17 @@ func cmdSet(cmd []string) { case "cmdPrefix": cmdPrefix = cmd[2] default: - printToView("Feed", fmt.Sprintf("Unknown config value %s", cmd[1])) + printError(fmt.Sprintf("Unknown config value %s", cmd[1])) } } func loadFromToml() { - printToView("Feed", fmt.Sprintf("Loading config from toml")) config, err := toml.LoadFile("kbtui.tml") if err != nil { - printToView("Feed", fmt.Sprintf("Could not read config file: %+v", err)) + printError(fmt.Sprintf("Could not read config file: %+v", err)) return } + colorless = config.GetDefault("Basics.colorless", false).(bool) if config.Has("Basics.colorless") { colorless = config.Get("Basics.colorless").(bool) } @@ -92,5 +91,26 @@ func loadFromToml() { if config.Has("Formatting.timeFormat") { timeFormat = config.Get("Formatting.timeFormat").(string) } + channelsColor = styleFromConfig(config, "channels.basic") + + channelsHeaderColor = styleFromConfig(config, "channels.header") + channelUnreadColor = styleFromConfig(config, "channels.unread") + + mentionColor = styleFromConfig(config, "message.mention") + messageHeaderColor = styleFromConfig(config, "message.header") + messageIDColor = styleFromConfig(config, "message.id") + messageTimeColor = styleFromConfig(config, "message.time") + messageSenderDefaultColor = styleFromConfig(config, "message.sender_default") + messageSenderDeviceColor = styleFromConfig(config, "message.sender_device") + messageBodyColor = styleFromConfig(config, "message.body") + messageAttachmentColor = styleFromConfig(config, "message.attachment") + messageLinkURLColor = styleFromConfig(config, "message.link_url") + messageLinkKeybaseColor = styleFromConfig(config, "message.link_keybase") + messageReactionColor = styleFromConfig(config, "message.reaction") + messageCodeColor = styleFromConfig(config, "message.code") + + feedColor = styleFromConfig(config, "feed.basic") + errorColor = styleFromConfig(config, "feed.error") + RunCommand("clean") } diff --git a/cmdStream.go b/cmdStream.go index ee68867..4e8b848 100644 --- a/cmdStream.go +++ b/cmdStream.go @@ -2,6 +2,10 @@ package main +import ( + "fmt" +) + func init() { command := Command{ Cmd: []string{"stream", "s"}, @@ -17,7 +21,7 @@ func cmdStream(cmd []string) { stream = true channel.Name = "" - printToView("Feed", "You are now viewing the formatted stream") - setViewTitle("Input", " Stream - Not in a chat /j to join ") + printInfo("You are now viewing the formatted stream") + setViewTitle("Input", fmt.Sprintf(" Stream - Not in a chat. %sj to join ", cmdPrefix)) clearView("Chat") } diff --git a/cmdUploadFile.go b/cmdUploadFile.go index 3d76d4f..e6f61ec 100644 --- a/cmdUploadFile.go +++ b/cmdUploadFile.go @@ -21,14 +21,14 @@ func init() { func cmdUploadFile(cmd []string) { if len(cmd) < 2 { - printToView("Feed", fmt.Sprintf("%s%s $filePath $fileName - Upload file from absolute path with optional name", cmdPrefix, cmd[0])) + printInfo(fmt.Sprintf("%s%s $filePath $fileName - Upload file from absolute path with optional name", cmdPrefix, cmd[0])) return } filePath := cmd[1] if !strings.HasPrefix(filePath, "/") { dir, err := os.Getwd() if err != nil { - printToView("Feed", fmt.Sprintf("There was an error determining path %+v", err)) + printError(fmt.Sprintf("There was an error determining path %+v", err)) } filePath = fmt.Sprintf("%s/%s", dir, filePath) } @@ -40,9 +40,10 @@ func cmdUploadFile(cmd []string) { } chat := k.NewChat(channel) _, err := chat.Upload(fileName, filePath) + channelName := messageLinkKeybaseColor.stylize(channel.Name).string() if err != nil { - printToView("Feed", fmt.Sprintf("There was an error uploading %s to %s\n%+v", filePath, channel.Name, err)) + printError(fmt.Sprintf("There was an error uploading %s to %s\n%+v", filePath, channelName, err)) } else { - printToView("Feed", fmt.Sprintf("Uploaded %s to %s", filePath, channel.Name)) + printInfo(fmt.Sprintf("Uploaded %s to %s", filePath, channelName)) } } diff --git a/cmdWall.go b/cmdWall.go index b34db98..7fe0904 100644 --- a/cmdWall.go +++ b/cmdWall.go @@ -64,13 +64,14 @@ func cmdPopulateWall(cmd []string) { if len(users) < 1 { return } - printToView("Feed", fmt.Sprintf("Displaying public messages for user %s", requestedUsers)) + + printInfoF("Displaying public messages for user $TEXT", messageLinkKeybaseColor.stylize(requestedUsers)) for _, chann := range users { chat := k.NewChat(chann) api, err := chat.Read() if err != nil { if len(users) < 6 { - printToView("Feed", fmt.Sprintf("There was an error for user %s: %+v", cleanChannelName(chann.Name), err)) + printError(fmt.Sprintf("There was an error for user %s: %+v", cleanChannelName(chann.Name), err)) return } } else { diff --git a/cmdWallet.go b/cmdWallet.go index b26eca2..4336e7e 100644 --- a/cmdWallet.go +++ b/cmdWallet.go @@ -42,20 +42,20 @@ func cmdWallet(cmd []string) { walletConfirmationCode = b.String() walletConfirmationUser = cmd[1] walletTransactionAmnt = cmd[2] - printToView("Feed", fmt.Sprintf("To confirm sending %s to %s, type /confirm %s %s", cmd[2], cmd[1], cmd[1], walletConfirmationCode)) + printInfo(fmt.Sprintf("To confirm sending %s to %s, type /confirm %s %s", cmd[2], cmd[1], cmd[1], walletConfirmationCode)) } else if cmd[0] == "confirm" { if cmd[1] == walletConfirmationUser && cmd[2] == walletConfirmationCode { txWallet := k.NewWallet() wAPI, err := txWallet.SendXLM(walletConfirmationUser, walletTransactionAmnt, "") if err != nil { - printToView("Feed", fmt.Sprintf("There was an error with your wallet tx:\n\t%+v", err)) + printError(fmt.Sprintf("There was an error with your wallet tx:\n\t%+v", err)) } else { - printToView("Feed", fmt.Sprintf("You have sent %sXLM to %s with tx ID: %s", wAPI.Result.Amount, wAPI.Result.ToUsername, wAPI.Result.TxID)) + printInfo(fmt.Sprintf("You have sent %sXLM to %s with tx ID: %s", wAPI.Result.Amount, wAPI.Result.ToUsername, wAPI.Result.TxID)) } } else { - printToView("Feed", "There was an error validating your confirmation. Your wallet has been untouched.") + printError("There was an error validating your confirmation. Your wallet has been untouched.") } } diff --git a/colors.go b/colors.go index 2625b13..1c1dc44 100644 --- a/colors.go +++ b/colors.go @@ -2,42 +2,230 @@ package main import ( "fmt" + "github.com/pelletier/go-toml" "regexp" + "strings" ) -// TODO maybe datastructure -// BASH-like PS1 variable equivalent (without colours) -// TODO bold? cursive etc? -func color(c int) string { +// Begin Colors +type color int + +const ( + black color = iota + red + green + yellow + purple + magenta + cyan + grey + normal color = -1 +) + +func colorFromString(s string) color { + s = strings.ToLower(s) + switch s { + case "black": + return black + case "red": + return red + case "green": + return green + case "yellow": + return yellow + case "purple": + return purple + case "magenta": + return magenta + case "cyan": + return cyan + case "grey": + return grey + case "normal": + return normal + default: + printError(fmt.Sprintf("color `%s` cannot be parsed.", s)) + } + return normal +} + +// Style struct for specializing the style/color of a stylize +type Style struct { + foregroundColor color + backgroundColor color + bold bool + italic bool // Currently not supported by the UI library + underline bool + strikethrough bool // Currently not supported by the UI library + inverse bool +} + +var basicStyle = Style{normal, normal, false, false, false, false, false} + +func styleFromConfig(config *toml.Tree, key string) Style { + key = "Colors." + key + "." + style := basicStyle + if config.Has(key + "foreground") { + style = style.withForeground(colorFromString(config.Get(key + "foreground").(string))) + } + if config.Has(key + "background") { + style = style.withForeground(colorFromString(config.Get(key + "background").(string))) + } + if config.GetDefault(key+"bold", false).(bool) { + style = style.withBold() + } + if config.GetDefault(key+"italic", false).(bool) { + style = style.withItalic() + } + if config.GetDefault(key+"underline", false).(bool) { + style = style.withUnderline() + } + if config.GetDefault(key+"strikethrough", false).(bool) { + style = style.withStrikethrough() + } + if config.GetDefault(key+"inverse", false).(bool) { + style = style.withInverse() + } + + return style +} + +func (s Style) withForeground(f color) Style { + s.foregroundColor = f + return s +} +func (s Style) withBackground(f color) Style { + s.backgroundColor = f + return s +} +func (s Style) withBold() Style { + s.bold = true + return s +} +func (s Style) withInverse() Style { + s.inverse = true + return s +} +func (s Style) withItalic() Style { + s.italic = true + return s +} +func (s Style) withStrikethrough() Style { + s.strikethrough = true + return s +} +func (s Style) withUnderline() Style { + s.underline = true + return s +} + +// TODO create both as `reset` (which it is now) as well as `append` +// which essentially just adds on top. that is relevant in the case of +// bold/italic etc - it should add style - not clear. +func (s Style) toANSI() string { if colorless { return "" } - if c < 0 { - return "\033[0m" - } else { - return fmt.Sprintf("\033[0;%dm", 29+c) + output := "\x1b[0m\x1b[0" + if s.foregroundColor != normal { + output += fmt.Sprintf(";%d", 30+s.foregroundColor) + } + if s.backgroundColor != normal { + output += fmt.Sprintf(";%d", 40+s.backgroundColor) + } + if s.bold { + output += ";1" + } + if s.italic { + output += ";3" + } + if s.underline { + output += ";4" + } + if s.inverse { + output += ";7" } + if s.strikethrough { + output += ";9" + } + + return output + "m" } -// TODO maybe make the text into some datastructure which remembers the color -func colorText(text string, color string, offColor string) string { - return fmt.Sprintf("%s%s%s", color, text, offColor) +// End Colors +// Begin StyledString + +// StyledString is used to save a message with a style, which can then later be rendered to a string +type StyledString struct { + message string + style Style } -func colorUsername(username string, offColor string) string { - var color = messageSenderDefaultColor - if username == k.Username { - color = mentionColor +// TODO handle all formatting types +func (s Style) sprintf(base string, parts ...StyledString) StyledString { + text := s.stylize(removeFormatting(base)) + //TODO handle posibility to escape + re := regexp.MustCompile(`\$TEXT`) + for len(re.FindAllString(text.message, 1)) > 0 { + part := parts[0] + parts = parts[1:] + text = text.replaceN("$TEXT", part, 1) } - return colorText(username, color, offColor) + return text +} + +func (s Style) stylize(msg string) StyledString { + return StyledString{msg, s} +} +func (t StyledString) stringFollowedByStyle(style Style) string { + return t.style.toANSI() + t.message + style.toANSI() } -func colorRegex(msg string, match string, color string, offColor string) string { - var re = regexp.MustCompile(match) - return re.ReplaceAllString(msg, colorText(`$1`, color, offColor)) +func (t StyledString) string() string { + return t.stringFollowedByStyle(basicStyle) } -func colorReplaceMentionMe(msg string, offColor string) string { - //var coloredOwnName = colorText(k.Username, mentionColor, offColor) - //return strings.Replace(msg, k.Username, coloredOwnName, -1) - return colorRegex(msg, "(@?"+k.Username+")", mentionColor, offColor) +func (t StyledString) replace(match string, value StyledString) StyledString { + return t.replaceN(match, value, -1) +} +func (t StyledString) replaceN(match string, value StyledString, n int) StyledString { + t.message = strings.Replace(t.message, match, value.stringFollowedByStyle(t.style), n) + return t +} +func (t StyledString) replaceString(match string, value string) StyledString { + t.message = strings.Replace(t.message, match, value, -1) + return t +} +func (t StyledString) replaceRegex(match string, value StyledString) StyledString { + var re = regexp.MustCompile("(" + match + ")") + t.message = re.ReplaceAllString(t.message, value.stringFollowedByStyle(t.style)) + return t +} + +// Overrides current formatting +func (t StyledString) colorRegex(match string, style Style) StyledString { + re := regexp.MustCompile("(" + match + ")") + subStrings := re.FindAllString(t.message, -1) + for _, element := range subStrings { + cleanSubstring := style.stylize(removeFormatting(element)) + t.message = strings.Replace(t.message, element, cleanSubstring.stringFollowedByStyle(t.style), -1) + } + return t + // Old versionreturn t.replaceRegex(match, style.stylize(`$1`)) +} + +// Appends the other stylize at the end, but retains same style +func (t StyledString) append(other StyledString) StyledString { + t.message = t.message + other.stringFollowedByStyle(t.style) + return t +} +func (t StyledString) appendString(other string) StyledString { + t.message += other + return t +} + +// Begin Formatting + +func removeFormatting(s string) string { + reFormatting := regexp.MustCompile(`(?m)\x1b\[(\d*;?)*m`) + return reFormatting.ReplaceAllString(s, "") } diff --git a/kbtui.tml b/kbtui.tml index 4ce8ecb..c298287 100644 --- a/kbtui.tml +++ b/kbtui.tml @@ -16,15 +16,48 @@ timeFormat = "15:04" [Colors] -channelsColor = 8 -channelsHeaderColor = 6 -noColor = -1 -mentionColor = 3 -messageHeaderColor = 8 -messageIdColor = 7 -messageTimeColor = 6 -messageSenderDefaultColor = 8 -messageSenderDeviceColor = 8 -messageBodyColor = -1 -messageAttachmentColor = 2 -messageLinkColor = 4 + [Colors.channels] + [Colors.channels.basic] + foreground = "normal" + [Colors.channels.header] + foreground = "magenta" + bold = true + [Colors.channels.unread] + foreground = "green" + italic = true + + [Colors.message] + [Colors.message.body] + foreground = "normal" + [Colors.message.header] + foreground = "grey" + [Colors.message.mention] + foreground = "green" + italic = true + bold = true + [Colors.message.id] + foreground = "yellow" + [Colors.message.time] + foreground = "magenta" + [Colors.message.sender_default] + foreground = "cyan" + bold = true + [Colors.message.sender_device] + foreground = "cyan" + [Colors.message.attachment] + foreground = "red" + [Colors.message.link_url] + foreground = "yellow" + [Colors.message.link_keybase] + foreground = "yellow" + [Colors.message.reaction] + foreground = "magenta" + bold = true + [Colors.message.code] + foreground = "cyan" + background = "grey" + [Colors.feed] + [Colors.feed.basic] + foreground = "grey" + [Colors.feed.error] + foreground = "red" \ No newline at end of file diff --git a/main.go b/main.go index 0e2f84d..875aa13 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ func layout(g *gocui.Gui) error { feedView.Autoscroll = true feedView.Wrap = true feedView.Title = "Feed Window" - fmt.Fprintln(feedView, "Feed Window - If you are mentioned or receive a PM it will show here") + printInfo("Feed Window - If you are mentioned or receive a PM it will show here") } if chatView, err2 := g.SetView("Chat", maxX/2-maxX/3, maxY/5+1, maxX-1, maxY-5, 0); err2 != nil { if !gocui.IsUnknownView(err2) { @@ -80,7 +80,9 @@ func layout(g *gocui.Gui) error { } chatView.Autoscroll = true chatView.Wrap = true - fmt.Fprintf(chatView, "Welcome %s!\n\nYour chats will appear here.\nSupported commands are as follows:\n\n", k.Username) + welcomeText := basicStyle.stylize("Welcome $USER!\n\nYour chats will appear here.\nSupported commands are as follows:\n") + welcomeText = welcomeText.replace("$USER", mentionColor.stylize(k.Username)) + fmt.Fprintln(chatView, welcomeText.string()) RunCommand("help") } if inputView, err3 := g.SetView("Input", maxX/2-maxX/3, maxY-4, maxX-1, maxY-1, 0); err3 != nil { @@ -176,20 +178,19 @@ func getViewTitle(viewName string) string { view, err := g.View(viewName) if err != nil { // in case there is active tab completion, filter that to just the view title and not the completion options. - printToView("Feed", fmt.Sprintf("Error getting view title: %s", err)) + printError(fmt.Sprintf("Error getting view title: %s", err)) return "" } return strings.Split(view.Title, "||")[0] - } func popupView(viewName string) { _, err := g.SetCurrentView(viewName) if err != nil { - printToView("Feed", fmt.Sprintf("%+v", err)) + printError(fmt.Sprintf("%+v", err)) } _, err = g.SetViewOnTop(viewName) if err != nil { - printToView("Feed", fmt.Sprintf("%+v", err)) + printError(fmt.Sprintf("%+v", err)) } g.Update(func(g *gocui.Gui) error { updatingView, err := g.View(viewName) @@ -247,6 +248,24 @@ func writeToView(viewName string, message string) { return nil }) } + +// this removes formatting +func printError(message string) { + printErrorF(message) +} +func printErrorF(message string, parts ...StyledString) { + printToView("Feed", errorColor.sprintf(removeFormatting(message), parts...).string()) +} + +// this removes formatting +func printInfo(message string) { + printInfoF(message) +} + +// this removes formatting +func printInfoF(message string, parts ...StyledString) { + printToView("Feed", feedColor.sprintf(removeFormatting(message), parts...).string()) +} func printToView(viewName string, message string) { g.Update(func(g *gocui.Gui) error { updatingView, err := g.View(viewName) @@ -287,13 +306,12 @@ func populateChat() { chat = k.NewChat(channel) _, err2 := chat.Read(2) if err2 != nil { - printToView("Feed", fmt.Sprintf("%+v", err)) + printError(fmt.Sprintf("%+v", err)) return } go populateChat() go generateChannelTabCompletionSlice() return - } var printMe []string var actuallyPrintMe string @@ -325,75 +343,122 @@ func populateList() { if testVar, err := k.ChatList(); err != nil { log.Printf("%+v", err) } else { - clearView("List") - var recentPMs = fmt.Sprintf("%s---[PMs]---%s\n", channelsHeaderColor, channelsColor) + var textBase = channelsColor.stylize("") + var recentPMs = textBase.append(channelsHeaderColor.stylize("---[PMs]---\n")) var recentPMsCount = 0 - var recentChannels = fmt.Sprintf("%s---[Teams]---%s\n", channelsHeaderColor, channelsColor) + var recentChannels = textBase.append(channelsHeaderColor.stylize("---[Teams]---\n")) var recentChannelsCount = 0 for _, s := range testVar.Result.Conversations { channels = append(channels, s.Channel) if s.Channel.MembersType == keybase.TEAM { recentChannelsCount++ if recentChannelsCount <= ((maxY - 2) / 3) { + channel := fmt.Sprintf("%s\n\t#%s\n", s.Channel.Name, s.Channel.TopicName) if s.Unread { - recentChannels += fmt.Sprintf("%s*", color(0)) + recentChannels = recentChannels.append(channelUnreadColor.stylize("*" + channel)) + } else { + recentChannels = recentChannels.appendString(channel) } - recentChannels += fmt.Sprintf("%s\n\t#%s\n%s", s.Channel.Name, s.Channel.TopicName, channelsColor) } } else { recentPMsCount++ if recentPMsCount <= ((maxY - 2) / 3) { + pmName := fmt.Sprintf("%s\n", cleanChannelName(s.Channel.Name)) if s.Unread { - recentChannels += fmt.Sprintf("%s*", color(0)) + recentPMs = recentPMs.append(channelUnreadColor.stylize("*" + pmName)) + } else { + recentPMs = recentPMs.appendString(pmName) } - recentPMs += fmt.Sprintf("%s\n%s", cleanChannelName(s.Channel.Name), channelsColor) } } } time.Sleep(1 * time.Millisecond) - printToView("List", fmt.Sprintf("%s%s%s%s", channelsColor, recentPMs, recentChannels, noColor)) - go generateRecentTabCompletionSlice() + printToView("List", fmt.Sprintf("%s%s", recentPMs.string(), recentChannels.string())) + generateRecentTabCompletionSlice() } } // End update/populate views automatically // Formatting +func formatMessageBody(body string) StyledString { + output := messageBodyColor.stylize(body) + + output = colorReplaceMentionMe(output) + output = output.colorRegex(`_[^_]*_`, messageBodyColor.withItalic()) + output = output.colorRegex(`~[^~]*~`, messageBodyColor.withStrikethrough()) + output = output.colorRegex(`@[\w_]*(\.[\w_]+)*`, messageLinkKeybaseColor) + // TODO change how bold, italic etc works, so it uses boldOn boldOff ([1m and [22m) + output = output.colorRegex(`\*[^\*]*\*`, messageBodyColor.withBold()) + output = output.replaceString("```", "") + // TODO make background color cover whole line + output = output.colorRegex("(.*\n)*", messageCodeColor) + output = output.colorRegex("`[^`]*`", messageCodeColor) + // mention URL + output = output.colorRegex(`(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))`, messageLinkURLColor) + return output +} + +// TODO use this more +func formatChannel(ch keybase.Channel) StyledString { + return messageLinkKeybaseColor.stylize(fmt.Sprintf("@%s#%s", ch.Name, ch.TopicName)) +} + +func colorReplaceMentionMe(msg StyledString) StyledString { + return msg.colorRegex("(@?"+k.Username+")", mentionColor) +} +func colorUsername(username string) StyledString { + var color = messageSenderDefaultColor + if username == k.Username { + color = mentionColor + } + return color.stylize(username) +} + func cleanChannelName(c string) string { newChannelName := strings.Replace(c, fmt.Sprintf("%s,", k.Username), "", 1) return strings.Replace(newChannelName, fmt.Sprintf(",%s", k.Username), "", 1) } -func formatOutput(api keybase.ChatAPI) string { - ret := "" + +func formatMessage(api keybase.ChatAPI, formatString string) string { + ret := messageHeaderColor.stylize("") msgType := api.Msg.Content.Type switch msgType { case "text", "attachment": - var c = messageHeaderColor - ret = colorText(outputFormat, c, noColor) + ret = messageHeaderColor.stylize(formatString) tm := time.Unix(int64(api.Msg.SentAt), 0) - var msg = api.Msg.Content.Text.Body - // mention teams or users - msg = colorRegex(msg, `(@\w*(\.\w+)*)`, messageLinkColor, messageBodyColor) - // mention URL - msg = colorRegex(msg, `(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))`, messageLinkColor, messageBodyColor) - msg = colorText(colorReplaceMentionMe(msg, messageBodyColor), messageBodyColor, c) + var msg = formatMessageBody(api.Msg.Content.Text.Body) if msgType == "attachment" { - msg = fmt.Sprintf("%s\n%s", api.Msg.Content.Attachment.Object.Title, colorText(fmt.Sprintf("[Attachment: %s]", api.Msg.Content.Attachment.Object.Filename), messageAttachmentColor, c)) + msg = messageBodyColor.stylize("$TITLE\n$FILE") + attachment := api.Msg.Content.Attachment + msg = msg.replaceString("$TITLE", attachment.Object.Title) + msg = msg.replace("$FILE", messageAttachmentColor.stylize(fmt.Sprintf("[Attachment: %s]", attachment.Object.Filename))) } - user := colorUsername(api.Msg.Sender.Username, c) - device := colorText(api.Msg.Sender.DeviceName, messageSenderDeviceColor, c) - msgID := colorText(fmt.Sprintf("%d", api.Msg.ID), messageIdColor, c) - ts := colorText(tm.Format(timeFormat), messageTimeColor, c) - ret = strings.Replace(ret, "$MSG", msg, 1) - ret = strings.Replace(ret, "$USER", user, 1) - ret = strings.Replace(ret, "$DEVICE", device, 1) - ret = strings.Replace(ret, "$ID", msgID, 1) - ret = strings.Replace(ret, "$TIME", ts, 1) - ret = strings.Replace(ret, "$DATE", colorText(tm.Format(dateFormat), messageTimeColor, c), 1) - ret = strings.Replace(ret, "```", fmt.Sprintf("\n\n"), -1) + + user := colorUsername(api.Msg.Sender.Username) + device := messageSenderDeviceColor.stylize(api.Msg.Sender.DeviceName) + msgID := messageIDColor.stylize(fmt.Sprintf("%d", api.Msg.ID)) + date := messageTimeColor.stylize(tm.Format(dateFormat)) + msgTime := messageTimeColor.stylize(tm.Format(timeFormat)) + + channelName := messageIDColor.stylize(fmt.Sprintf("@%s#%s", api.Msg.Channel.Name, api.Msg.Channel.TopicName)) + ret = ret.replace("$MSG", msg) + ret = ret.replace("$USER", user) + ret = ret.replace("$DEVICE", device) + ret = ret.replace("$ID", msgID) + ret = ret.replace("$TIME", msgTime) + ret = ret.replace("$DATE", date) + ret = ret.replace("$TEAM", channelName) + } + return ret.string() +} +func formatOutput(api keybase.ChatAPI) string { + format := outputFormat + if stream { + format = outputStreamFormat } - return ret + return formatMessage(api, format) } // End formatting @@ -410,9 +475,7 @@ func handleMessage(api keybase.ChatAPI) { } if api.Msg.Content.Type == "text" || api.Msg.Content.Type == "attachment" { go populateList() - msgBody := api.Msg.Content.Text.Body msgSender := api.Msg.Sender.Username - channelName := api.Msg.Channel.Name if !stream { if msgSender != k.Username { if api.Msg.Channel.MembersType == keybase.TEAM { @@ -421,7 +484,7 @@ func handleMessage(api keybase.ChatAPI) { if m.Text == k.Username { // We are in a team if topicName != channel.TopicName { - printToView("Feed", fmt.Sprintf("[ %s#%s ] %s: %s", channelName, topicName, msgSender, msgBody)) + printInfo(formatMessage(api, mentionFormat)) fmt.Print("\a") } @@ -430,7 +493,7 @@ func handleMessage(api keybase.ChatAPI) { } } else { if msgSender != channel.Name { - printToView("Feed", fmt.Sprintf("PM from @%s: %s", cleanChannelName(channelName), msgBody)) + printInfo(formatMessage(api, pmFormat)) fmt.Print("\a") } @@ -446,10 +509,9 @@ func handleMessage(api keybase.ChatAPI) { } } else { if api.Msg.Channel.MembersType == keybase.TEAM { - topicName := api.Msg.Channel.TopicName - printToView("Chat", fmt.Sprintf("@%s#%s [%s]: %s", channelName, topicName, msgSender, msgBody)) + printToView("Chat", formatOutput(api)) } else { - printToView("Chat", fmt.Sprintf("PM @%s [%s]: %s", cleanChannelName(channelName), msgSender, msgBody)) + printToView("Chat", formatMessage(api, pmFormat)) } } } else { @@ -494,7 +556,7 @@ func handleInput(viewName string) error { } else if cmd[0] == "q" || cmd[0] == "quit" { return gocui.ErrQuit } else { - printToView("Feed", fmt.Sprintf("Command '%s' not recognized", cmd[0])) + printError(fmt.Sprintf("Command '%s' not recognized", cmd[0])) return nil } } @@ -517,7 +579,7 @@ func sendChat(message string) { chat := k.NewChat(channel) _, err := chat.Send(message) if err != nil { - printToView("Feed", fmt.Sprintf("There was an error %+v", err)) + printError(fmt.Sprintf("There was an error %+v", err)) } } diff --git a/tcmdShowReactions.go b/tcmdShowReactions.go index 328bcea..5fa278f 100644 --- a/tcmdShowReactions.go +++ b/tcmdShowReactions.go @@ -20,15 +20,17 @@ func init() { } func tcmdShowReactions(m keybase.ChatAPI) { - where := "" team := false + user := colorUsername(m.Msg.Sender.Username) + id := messageIDColor.stylize(fmt.Sprintf("%d", m.Msg.Content.Reaction.M)) + reaction := messageReactionColor.stylize(m.Msg.Content.Reaction.B) + where := messageLinkKeybaseColor.stylize("a PM") if m.Msg.Channel.MembersType == keybase.TEAM { team = true - where = fmt.Sprintf("in @%s#%s", m.Msg.Channel.Name, m.Msg.Channel.TopicName) + where = formatChannel(m.Msg.Channel) } else { - where = fmt.Sprintf("in a PM") } - printToView("Feed", fmt.Sprintf("%s reacted to %d with %s %s", m.Msg.Sender.Username, m.Msg.Content.Reaction.M, m.Msg.Content.Reaction.B, where)) + printInfoF("$TEXT reacted to [$TEXT] with $TEXT in $TEXT", user, id, reaction, where) if channel.Name == m.Msg.Channel.Name { if team { if channel.TopicName == m.Msg.Channel.TopicName { diff --git a/userConfigs.go b/userConfigs.go index f01bdde..d0c7fd7 100644 --- a/userConfigs.go +++ b/userConfigs.go @@ -3,22 +3,32 @@ package main // Path where Downloaded files will default to var downloadPath = "/tmp/" -var colorless = false -var channelsColor = color(8) -var channelsHeaderColor = color(6) -var noColor = color(-1) -var mentionColor = color(3) -var messageHeaderColor = color(8) -var messageIdColor = color(7) -var messageTimeColor = color(6) -var messageSenderDefaultColor = color(8) -var messageSenderDeviceColor = color(8) -var messageBodyColor = noColor -var messageAttachmentColor = color(2) -var messageLinkColor = color(4) +var colorless bool = false +var channelsColor = basicStyle +var channelUnreadColor = channelsColor.withForeground(green).withItalic() +var channelsHeaderColor = channelsColor.withForeground(magenta).withBold() + +var mentionColor = basicStyle.withForeground(green) +var messageHeaderColor = basicStyle.withForeground(grey) +var messageIDColor = basicStyle.withForeground(yellow) +var messageTimeColor = basicStyle.withForeground(magenta) +var messageSenderDefaultColor = basicStyle.withForeground(cyan) +var messageSenderDeviceColor = messageSenderDefaultColor +var messageBodyColor = basicStyle +var messageAttachmentColor = basicStyle.withForeground(red) +var messageLinkURLColor = basicStyle.withForeground(yellow) +var messageLinkKeybaseColor = basicStyle.withForeground(yellow) +var messageReactionColor = basicStyle.withForeground(magenta) +var messageCodeColor = basicStyle.withBackground(grey).withForeground(cyan) + +var feedColor = basicStyle.withForeground(grey) +var errorColor = basicStyle.withForeground(red) // BASH-like PS1 variable equivalent var outputFormat = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG" +var outputStreamFormat = "┌──[$TEAM] [$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG" +var mentionFormat = outputStreamFormat +var pmFormat = "PM from $USER@$DEVICE: $MSG" // 02 = Day, Jan = Month, 06 = Year var dateFormat = "02Jan06"