From 439f09aa1c858265d90c9f3086cef4a71ffed848 Mon Sep 17 00:00:00 2001
From: Casper Weiss Bang <master@thecdk.net>
Date: Thu, 17 Oct 2019 07:56:47 +0200
Subject: [PATCH 1/8] 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 <team> <channel>", cmdPrefix))
-		printToView("Feed", fmt.Sprintf("To join a PM use %sjoin <user>", cmdPrefix))
+		printInfo(fmt.Sprintf("To join a team use %sjoin <team> <channel>", cmdPrefix))
+		printInfo(fmt.Sprintf("To join a PM use %sjoin <user>", 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("```", "<code>")
+	// TODO make background color cover whole line
+	output = output.colorRegex("<code>(.*\n)*<code>", 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<code>\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"

From 987eba51cf733e307e0383d91b58e8da9b9f5044 Mon Sep 17 00:00:00 2001
From: Gregory Rudolph <rudi@nightmare.haus>
Date: Thu, 24 Oct 2019 09:04:25 -0400
Subject: [PATCH 2/8] Check for env var config file

---
 cmdSet.go | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/cmdSet.go b/cmdSet.go
index b0e3121..565ddd2 100644
--- a/cmdSet.go
+++ b/cmdSet.go
@@ -4,6 +4,7 @@ package main
 
 import (
 	"fmt"
+	"os"
 	"strings"
 
 	"github.com/pelletier/go-toml"
@@ -67,7 +68,12 @@ func cmdSet(cmd []string) {
 
 }
 func loadFromToml() {
-	config, err := toml.LoadFile("kbtui.tml")
+	configFile, env := os.LookupEnv("KBTUI_CFG")
+	if !env {
+		configFile = "kbtui.toml"
+	}
+
+	config, err := toml.LoadFile(configFile)
 	if err != nil {
 		printError(fmt.Sprintf("Could not read config file: %+v", err))
 		return

From 87d1b19aebe478cbb4779f115e65e16f88cc30ab Mon Sep 17 00:00:00 2001
From: Gregory Rudolph <rudi@nightmare.haus>
Date: Thu, 24 Oct 2019 09:06:15 -0400
Subject: [PATCH 3/8] Rename kbtui.tml to kbtui.toml as per spec for toml

---
 kbtui.tml => kbtui.toml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
 rename kbtui.tml => kbtui.toml (91%)

diff --git a/kbtui.tml b/kbtui.toml
similarity index 91%
rename from kbtui.tml
rename to kbtui.toml
index c298287..37a5af4 100644
--- a/kbtui.tml
+++ b/kbtui.toml
@@ -18,7 +18,7 @@ timeFormat = "15:04"
 [Colors]
 	 [Colors.channels]
 		  [Colors.channels.basic]
-		  foreground = "normal" 
+		  foreground = "normal"
 		  [Colors.channels.header]
 		  foreground = "magenta"
 		  bold = true
@@ -28,7 +28,7 @@ timeFormat = "15:04"
 
 	 [Colors.message]
 		  [Colors.message.body]
-		  foreground = "normal" 
+		  foreground = "normal"
 		  [Colors.message.header]
 		  foreground = "grey"
 		  [Colors.message.mention]
@@ -45,11 +45,11 @@ timeFormat = "15:04"
 		  [Colors.message.sender_device]
 		  foreground = "cyan"
 		  [Colors.message.attachment]
-		  foreground = "red" 
+		  foreground = "red"
 		  [Colors.message.link_url]
-		  foreground = "yellow" 
+		  foreground = "yellow"
 		  [Colors.message.link_keybase]
-		  foreground = "yellow" 
+		  foreground = "yellow"
 		  [Colors.message.reaction]
 		  foreground = "magenta"
 		  bold = true

From a083eb3ca6bca12dc13c90a0acee206f6912197d Mon Sep 17 00:00:00 2001
From: Gregory Rudolph <rudi@nightmare.haus>
Date: Thu, 24 Oct 2019 09:48:24 -0400
Subject: [PATCH 4/8] Show what config file is being used in load

---
 cmdSet.go | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/cmdSet.go b/cmdSet.go
index 565ddd2..ab3ec9d 100644
--- a/cmdSet.go
+++ b/cmdSet.go
@@ -24,7 +24,6 @@ func printSetting(cmd []string) {
 	switch cmd[1] {
 	case "load":
 		loadFromToml()
-		printInfo("Loading config from toml")
 	case "downloadPath":
 		printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], downloadPath))
 	case "outputFormat":
@@ -72,7 +71,7 @@ func loadFromToml() {
 	if !env {
 		configFile = "kbtui.toml"
 	}
-
+	printInfoF("Loading config from toml: $TEXT", messageAttachmentColor.stylize(configFile))
 	config, err := toml.LoadFile(configFile)
 	if err != nil {
 		printError(fmt.Sprintf("Could not read config file: %+v", err))

From 55ac19beb3654f69d500436e70c0e274e68d3dde Mon Sep 17 00:00:00 2001
From: Gregory Rudolph <rudi@nightmare.haus>
Date: Thu, 24 Oct 2019 09:49:21 -0400
Subject: [PATCH 5/8] Auto load config TOML from #17

---
 main.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/main.go b/main.go
index 875aa13..d83ecbc 100644
--- a/main.go
+++ b/main.go
@@ -37,6 +37,7 @@ func main() {
 	}
 	defer g.Close()
 	g.SetManagerFunc(layout)
+	go RunCommand("config", "load")
 	go populateList()
 	go updateChatWindow()
 	if len(os.Args) > 1 {

From 1a75ac8a4993b1c0f165b34cb990b8bbcd83f934 Mon Sep 17 00:00:00 2001
From: Gregory Rudolph <rudi@nightmare.haus>
Date: Thu, 24 Oct 2019 09:51:22 -0400
Subject: [PATCH 6/8] Feature #17 - load ~/.config/kbtui.toml if it exists,
 otherwise use toml in dir

---
 cmdSet.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/cmdSet.go b/cmdSet.go
index ab3ec9d..812fb48 100644
--- a/cmdSet.go
+++ b/cmdSet.go
@@ -69,7 +69,11 @@ func cmdSet(cmd []string) {
 func loadFromToml() {
 	configFile, env := os.LookupEnv("KBTUI_CFG")
 	if !env {
-		configFile = "kbtui.toml"
+		configFile = "~/.config/kbtui.toml"
+		if _, err := os.Stat(configFile); os.IsNotExist(err) {
+			configFile = "kbtui.toml"
+		}
+
 	}
 	printInfoF("Loading config from toml: $TEXT", messageAttachmentColor.stylize(configFile))
 	config, err := toml.LoadFile(configFile)

From f09f2969d92efce61c6308e8ae1859387f252609 Mon Sep 17 00:00:00 2001
From: Sam <dxb@keybase.io>
Date: Thu, 24 Oct 2019 22:20:25 -0400
Subject: [PATCH 7/8] Fix bug in colorRegex() that was highighting usernames in
 the middle of strings when a user wasn't actually being mentioned

---
 colors.go | 23 ++++++++++++-----------
 main.go   |  2 +-
 2 files changed, 13 insertions(+), 12 deletions(-)

diff --git a/colors.go b/colors.go
index 1c1dc44..48b8a55 100644
--- a/colors.go
+++ b/colors.go
@@ -195,22 +195,23 @@ 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)
-	}
+	locations := re.FindAllStringIndex(t.message, -1)
+	var newMessage string
+	var prevIndex int
+	for _, loc := range locations {
+		cleanSubstring := style.stylize(removeFormatting(string(t.message[loc[0]:loc[1]])))
+		newMessage += t.message[prevIndex:loc[0]]
+		newMessage += cleanSubstring.stringFollowedByStyle(t.style)
+		prevIndex = loc[1]
+	}
+	// Append any string after the final match
+	newMessage += t.message[prevIndex:len(t.message)]
+	t.message = newMessage
 	return t
-	// Old versionreturn t.replaceRegex(match, style.stylize(`$1`))
 }
 
 // Appends the other stylize at the end, but retains same style
diff --git a/main.go b/main.go
index d83ecbc..27d9f95 100644
--- a/main.go
+++ b/main.go
@@ -407,7 +407,7 @@ func formatChannel(ch keybase.Channel) StyledString {
 }
 
 func colorReplaceMentionMe(msg StyledString) StyledString {
-	return msg.colorRegex("(@?"+k.Username+")", mentionColor)
+	return msg.colorRegex(`(@?\b`+k.Username+`\b)`, mentionColor)
 }
 func colorUsername(username string) StyledString {
 	var color = messageSenderDefaultColor

From 029799494be22b276840d67174d5de1251506d52 Mon Sep 17 00:00:00 2001
From: Gregory Rudolph <rudi@nightmare.haus>
Date: Fri, 25 Oct 2019 10:59:01 -0400
Subject: [PATCH 8/8] Add check that lastMessage.type = "text"

---
 cmdEdit.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/cmdEdit.go b/cmdEdit.go
index b8b0fe1..a37527b 100644
--- a/cmdEdit.go
+++ b/cmdEdit.go
@@ -26,6 +26,8 @@ func cmdEdit(cmd []string) {
 		if len(cmd) == 2 {
 			messageID, _ = strconv.Atoi(cmd[1])
 		} else if lastMessage.ID != 0 {
+			message, _ := chat.ReadMessage(lastMessage.ID)
+			lastMessage.Type = message.Result.Messages[0].Msg.Content.Type
 			if lastMessage.Type != "text" {
 				printError("Last message isn't editable (is it an edit?)")
 				return