Browse Source

Merge pull request #41 from Rudi9719/dev

Dev
pull/43/head 1.0.0
Gregory Rudolph 5 years ago committed by GitHub
parent
commit
90d336988e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 3
      cmdClean.go
  3. 88
      cmdConfig.go
  4. 3
      cmdDelete.go
  5. 15
      cmdDownload.go
  6. 14
      cmdEdit.go
  7. 55
      cmdExec.go
  8. 2
      cmdHelp.go
  9. 8
      cmdJoin.go
  10. 4
      cmdPost.go
  11. 2
      cmdReact.go
  12. 7
      cmdReply.go
  13. 94
      cmdSet.go
  14. 8
      cmdStream.go
  15. 9
      cmdUploadFile.go
  16. 5
      cmdWall.go
  17. 8
      cmdWallet.go
  18. 218
      colors.go
  19. 72
      defaultConfig.go
  20. 47
      emojiMap.go
  21. 12
      go.mod
  22. 23
      go.sum
  23. 30
      kbtui.tml
  24. 68
      kbtui.toml
  25. 79
      mage.go
  26. 248
      main.go
  27. 12
      tabComplete.go
  28. 10
      tcmdShowReactions.go
  29. 73
      types.go
  30. 30
      userConfigs.go

1
.gitignore vendored

@ -6,3 +6,4 @@ emojiList.go @@ -6,3 +6,4 @@ emojiList.go
.idea/*
.idea
*.log
.travis.yml

3
cmdClean.go

@ -15,6 +15,7 @@ func init() { @@ -15,6 +15,7 @@ func init() {
func cmdClean(cmd []string) {
clearView("Chat")
clearView("List")
go populateChat()
go populateList()
}

88
cmdConfig.go

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
// +build !rm_basic_commands allcommands setcmd
package main
import (
"fmt"
"io/ioutil"
"os"
"github.com/pelletier/go-toml"
)
func init() {
command := Command{
Cmd: []string{"config"},
Description: "Change various settings",
Help: "",
Exec: cmdConfig,
}
RegisterCommand(command)
}
func cmdConfig(cmd []string) {
var err error
switch {
case len(cmd) == 2:
if cmd[1] == "load" {
config, err = readConfig()
if err != nil {
printError(err.Error())
return
}
printInfoF("Config file loaded: $TEXT", config.Colors.Message.Attachment.stylize(config.filepath))
return
}
case len(cmd) > 2:
if cmd[1] == "load" {
config, err = readConfig(cmd[3])
if err != nil {
printError(err.Error())
return
}
printInfoF("Config file loaded: $TEXT", config.Colors.Message.Attachment.stylize(config.filepath))
return
}
}
printError("Must pass a valid command")
}
func readConfig(filepath ...string) (*Config, error) {
var result = new(Config)
var configFile string
var env bool
// Load default config first, this way any values missing from the provided config file will remain the default value
d := []byte(defaultConfig)
toml.Unmarshal(d, result)
switch len(filepath) {
case 0:
configFile, env = os.LookupEnv("KBTUI_CFG")
if !env {
configFile = "~/.config/kbtui.toml"
if _, err := os.Stat(configFile); os.IsNotExist(err) {
configFile = "kbtui.toml"
}
}
default:
configFile = filepath[0]
if _, err := os.Stat(configFile); os.IsNotExist(err) {
return result, fmt.Errorf("Unable to load config: %s not found", configFile)
}
}
f, err := ioutil.ReadFile(configFile)
if err != nil {
f = []byte(defaultConfig)
}
err = toml.Unmarshal(f, result)
if err != nil {
return result, err
}
result.filepath = configFile
return result, nil
}

3
cmdDelete.go

@ -3,7 +3,6 @@ @@ -3,7 +3,6 @@
package main
import (
"fmt"
"strconv"
)
@ -28,7 +27,7 @@ func cmdDelete(cmd []string) { @@ -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.")
}
}

15
cmdDownload.go

@ -21,22 +21,22 @@ func init() { @@ -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", config.Basics.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
@ -46,10 +46,11 @@ func cmdDownloadFile(cmd []string) { @@ -46,10 +46,11 @@ func cmdDownloadFile(cmd []string) {
fileName = api.Result.Messages[0].Msg.Content.Attachment.Object.Filename
}
_, err = chat.Download(messageID, fmt.Sprintf("%s/%s", downloadPath, fileName))
_, err = chat.Download(messageID, fmt.Sprintf("%s/%s", config.Basics.DownloadPath, fileName))
channelName := config.Colors.Message.LinkKeybase.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)
}
}

14
cmdEdit.go

@ -26,22 +26,24 @@ func cmdEdit(cmd []string) { @@ -26,22 +26,24 @@ 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" {
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 +55,14 @@ func cmdEdit(cmd []string) { @@ -53,14 +55,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))
}
}

55
cmdExec.go

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
// +build !rm_basic_commands allcommands execcmd
package main
import (
"fmt"
"strings"
)
func init() {
command := Command{
Cmd: []string{"exec", "ex"},
Description: "$keybase args - executes keybase $args and returns the output",
Help: "",
Exec: cmdExec,
}
RegisterCommand(command)
}
func cmdExec(cmd []string) {
l := len(cmd)
switch {
case l >= 2:
if cmd[1] == "keybase" {
// if the user types /exec keybase wallet list
// only send ["wallet", "list"]
runKeybaseExec(cmd[2:])
} else {
// send everything except the command
runKeybaseExec(cmd[1:])
}
case l == 1:
fallthrough
default:
printExecHelp()
}
}
func runKeybaseExec(args []string) {
outputBytes, err := k.Exec(args...)
if err != nil {
printToView("Feed", fmt.Sprintf("Exec error: %+v", err))
} else {
channel.Name = ""
// unjoin the chat
clearView("Chat")
setViewTitle("Input", fmt.Sprintf(" /exec %s ", strings.Join(args, " ")))
output := string(outputBytes)
printToView("Chat", fmt.Sprintf("%s", output))
}
}
func printExecHelp() {
printInfo(fmt.Sprintf("To execute a keybase command use %sexec <keybase args>", config.Basics.CmdPrefix))
}

2
cmdHelp.go

@ -25,7 +25,7 @@ func cmdHelp(cmd []string) { @@ -25,7 +25,7 @@ func cmdHelp(cmd []string) {
if len(cmd) == 1 {
sort.Strings(baseCommands)
for _, c := range baseCommands {
helpText = fmt.Sprintf("%s%s%s\t\t%s\n", helpText, cmdPrefix, c, commands[c].Description)
helpText = fmt.Sprintf("%s%s%s\t\t%s\n", helpText, config.Basics.CmdPrefix, c, commands[c].Description)
}
if len(typeCommands) > 0 {
for c := range typeCommands {

8
cmdJoin.go

@ -41,12 +41,14 @@ func cmdJoin(cmd []string) { @@ -41,12 +41,14 @@ func cmdJoin(cmd []string) {
channel.TopicName = ""
channel.MembersType = keybase.USER
}
printToView("Feed", fmt.Sprintf("You are joining: %s", joinedName))
printInfoF("You are joining: $TEXT", config.Colors.Message.LinkKeybase.stylize(joinedName))
clearView("Chat")
setViewTitle("Input", fmt.Sprintf(" %s ", joinedName))
lastChat = joinedName
autoScrollView("Chat")
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>", config.Basics.CmdPrefix))
printInfo(fmt.Sprintf("To join a PM use %sjoin <user>", config.Basics.CmdPrefix))
}
}

4
cmdPost.go

@ -29,8 +29,8 @@ func cmdPost(cmd []string) { @@ -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.")
}
}

2
cmdReact.go

@ -38,6 +38,6 @@ func doReact(messageID int, reaction string) { @@ -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.")
}
}

7
cmdReply.go

@ -22,18 +22,17 @@ func init() { @@ -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", config.Basics.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
}

94
cmdSet.go

@ -1,94 +0,0 @@ @@ -1,94 +0,0 @@
// +build !rm_basic_commands allcommands setcmd
package main
import (
"fmt"
"strings"
"github.com/pelletier/go-toml"
)
func init() {
command := Command{
Cmd: []string{"set", "config"},
Description: "Change various settings",
Help: "",
Exec: cmdSet,
}
RegisterCommand(command)
}
func printSetting(cmd []string) {
switch cmd[1] {
case "load":
loadFromToml()
printToView("Feed", fmt.Sprintf("Loading config from toml"))
case "downloadPath":
printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], downloadPath))
case "outputFormat":
printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], outputFormat))
case "dateFormat":
printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], dateFormat))
case "timeFormat":
printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], timeFormat))
case "cmdPrefix":
printToView("Feed", fmt.Sprintf("Setting for %s -> %s", cmd[1], cmdPrefix))
default:
printToView("Feed", fmt.Sprintf("Unknown config value %s", cmd[1]))
}
return
}
func cmdSet(cmd []string) {
if len(cmd) < 2 {
printToView("Feed", "No config value specified")
return
}
if len(cmd) < 3 {
printSetting(cmd)
}
switch cmd[1] {
case "downloadPath":
if len(cmd) != 3 {
printToView("Feed", "Invalid download path.")
}
downloadPath = cmd[2]
case "outputFormat":
outputFormat = strings.Join(cmd[1:], " ")
case "dateFormat":
dateFormat = strings.Join(cmd[1:], " ")
case "timeFormat":
timeFormat = strings.Join(cmd[1:], " ")
case "cmdPrefix":
cmdPrefix = cmd[2]
default:
printToView("Feed", fmt.Sprintf("Unknown config value %s", cmd[1]))
}
}
func loadFromToml() {
config, err := toml.LoadFile("kbtui.tml")
if err != nil {
printToView("Feed", fmt.Sprintf("Could not read config file: %+v", err))
return
}
if config.Has("Basics.colorless") {
colorless = config.Get("Basics.colorless").(bool)
}
if config.Has("Basics.downloadPath") {
downloadPath = config.Get("Basics.downloadPath").(string)
}
if config.Has("Basics.cmdPrefix") {
cmdPrefix = config.Get("Basics.cmdPrefix").(string)
}
if config.Has("Formatting.outputFormat") {
outputFormat = config.Get("Formatting.outputFormat").(string)
}
if config.Has("Formatting.dateFormat") {
dateFormat = config.Get("Formatting.dateFormat").(string)
}
if config.Has("Formatting.timeFormat") {
timeFormat = config.Get("Formatting.timeFormat").(string)
}
}

8
cmdStream.go

@ -2,6 +2,10 @@ @@ -2,6 +2,10 @@
package main
import (
"fmt"
)
func init() {
command := Command{
Cmd: []string{"stream", "s"},
@ -17,7 +21,7 @@ func cmdStream(cmd []string) { @@ -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 ", config.Basics.CmdPrefix))
clearView("Chat")
}

9
cmdUploadFile.go

@ -21,14 +21,14 @@ func init() { @@ -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", config.Basics.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) { @@ -40,9 +40,10 @@ func cmdUploadFile(cmd []string) {
}
chat := k.NewChat(channel)
_, err := chat.Upload(fileName, filePath)
channelName := config.Colors.Message.LinkKeybase.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))
}
}

5
cmdWall.go

@ -64,13 +64,14 @@ func cmdPopulateWall(cmd []string) { @@ -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", config.Colors.Message.LinkKeybase.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 {

8
cmdWallet.go

@ -42,20 +42,20 @@ func cmdWallet(cmd []string) { @@ -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.")
}
}

218
colors.go

@ -3,41 +3,211 @@ package main @@ -3,41 +3,211 @@ package main
import (
"fmt"
"regexp"
"strings"
)
// TODO maybe datastructure
// BASH-like PS1 variable equivalent (without colours)
// TODO bold? cursive etc?
func color(c int) string {
if colorless {
const (
black int = iota
red
green
yellow
purple
magenta
cyan
grey
normal int = -1
)
var colorMapString = map[string]int{
"black": black,
"red": red,
"green": green,
"yellow": yellow,
"purple": purple,
"magenta": magenta,
"cyan": cyan,
"grey": grey,
"normal": normal,
}
var colorMapInt = map[int]string{
black: "black",
red: "red",
green: "green",
yellow: "yellow",
purple: "purple",
magenta: "magenta",
cyan: "cyan",
grey: "grey",
normal: "normal",
}
func colorFromString(color string) int {
var result int
color = strings.ToLower(color)
result, ok := colorMapString[color]
if !ok {
return normal
}
return result
}
func colorFromInt(color int) string {
var result string
result, ok := colorMapInt[color]
if !ok {
return "normal"
}
return result
}
var basicStyle = Style{
Foreground: colorMapInt[normal],
Background: colorMapInt[normal],
Italic: false,
Bold: false,
Underline: false,
Strikethrough: false,
Inverse: false,
}
func (s Style) withForeground(color int) Style {
s.Foreground = colorFromInt(color)
return s
}
func (s Style) withBackground(color int) Style {
s.Background = colorFromInt(color)
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 config.Basics.Colorless {
return ""
}
if c < 0 {
return "\033[0m"
} else {
return fmt.Sprintf("\033[0;%dm", 29+c)
output := "\x1b[0m\x1b[0"
if colorFromString(s.Foreground) != normal {
output += fmt.Sprintf(";%d", 30+colorFromString(s.Foreground))
}
if colorFromString(s.Background) != normal {
output += fmt.Sprintf(";%d", 40+colorFromString(s.Background))
}
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"
}
// 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
}
// 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 text
}
// 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)
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 (t StyledString) string() string {
return t.stringFollowedByStyle(basicStyle)
}
func colorUsername(username string, offColor string) string {
var color = messageSenderDefaultColor
if username == k.Username {
color = mentionColor
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
}
// Overrides current formatting
func (t StyledString) colorRegex(match string, style Style) StyledString {
re := regexp.MustCompile("(" + match + ")")
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]
}
return colorText(username, color, offColor)
// Append any string after the final match
newMessage += t.message[prevIndex:len(t.message)]
t.message = newMessage
return t
}
func colorRegex(msg string, match string, color string, offColor string) string {
var re = regexp.MustCompile(match)
return re.ReplaceAllString(msg, colorText(`$1`, color, offColor))
// 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 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 removeFormatting(s string) string {
reFormatting := regexp.MustCompile(`(?m)\x1b\[(\d*;?)*m`)
return reFormatting.ReplaceAllString(s, "")
}

72
defaultConfig.go

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
package main
var defaultConfig = `
[basics]
download_path = "/tmp/"
colorless = false
unicode_emojis = true
# The prefix before evaluating a command
cmd_prefix = "/"
[formatting]
# BASH-like PS1 variable equivalent
output_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
output_stream_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
output_mention_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
pm_format = "PM from $USER@$DEVICE: $MSG"
# 02 = Day, Jan = Month, 06 = Year
date_format = "02Jan06"
# 15 = hours, 04 = minutes, 05 = seconds
time_format = "15:04"
[colors]
[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"
`

47
emojiMap.go

File diff suppressed because one or more lines are too long

12
go.mod

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
module github.com/Rudi9719/kbtui
go 1.12
require (
github.com/awesome-gocui/gocui v0.6.0
github.com/magefile/mage v1.9.0
github.com/mattn/go-runewidth v0.0.5 // indirect
github.com/pelletier/go-toml v1.6.0
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
samhofi.us/x/keybase v0.0.0-20191023034410-b00e56e8dd3c
)

23
go.sum

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/awesome-gocui/gocui v0.6.0 h1:hhDJiQC12tEsJNJ+iZBBVaSSLFYo9llFuYpQlL5JZVI=
github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0=
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.5 h1:jrGtp51JOKTWgvLFzfG6OtZOJcK2sEnzc/U+zw7TtbA=
github.com/mattn/go-runewidth v0.0.5/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/pelletier/go-toml v1.5.0 h1:5BakdOZdtKJ1FFk6QdL8iSGrMWsXgchNJcrnarjbmJQ=
github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
samhofi.us/x/keybase v0.0.0-20191023034410-b00e56e8dd3c h1:qIKOKqYnRCx+O2IOz3a/lplrD0p1e3n/VoGOdrTGrVo=
samhofi.us/x/keybase v0.0.0-20191023034410-b00e56e8dd3c/go.mod h1:fcva80IUFyWcHtV4bBSzgKg07K6Rvuvi3GtGCLNGkyE=

30
kbtui.tml

@ -1,30 +0,0 @@ @@ -1,30 +0,0 @@
[Basics]
downloadPath = "/tmp/"
colorless = false
# The prefix before evaluating a command
cmdPrefix = "/"
[Formatting]
# BASH-like PS1 variable equivalent
outputFormat = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
# 02 = Day, Jan = Month, 06 = Year
dateFormat = "02Jan06"
# 15 = hours, 04 = minutes, 05 = seconds
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

68
kbtui.toml

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
[basics]
download_path = "/tmp/"
colorless = false
unicode_emojis = true
# The prefix before evaluating a command
cmd_prefix = "/"
[formatting]
# BASH-like PS1 variable equivalent
output_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
output_stream_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
output_mention_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
pm_format = "PM from $USER@$DEVICE: $MSG"
# 02 = Day, Jan = Month, 06 = Year
date_format = "02Jan06"
# 15 = hours, 04 = minutes, 05 = seconds
time_format = "15:04"
[colors]
[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"

79
mage.go

@ -3,65 +3,13 @@ @@ -3,65 +3,13 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
)
// emoji related constants
const emojiList = "https://raw.githubusercontent.com/CodeFreezr/emojo/master/db/v5/emoji-v5.json"
const emojiFileName = "emojiList.go"
// json parsing structure
type emoji struct {
Num int `json:"No"`
Emoji string `json:"Emoji"`
Category string `json:"Category"`
SubCategory string `json:"SubCategory"`
Unicode string `json:"Unicode"`
Name string `json:"Name"`
Tags string `json:"Tags"`
Shortcode string `json:"Shortcode"`
}
// This func downloaded and parses the emojis from online into a slice of all shortnames
// to be used as a lookup for tab completion for emojis
// this way the pull from GitHub only has to be done at build time.
func createEmojiSlice() ([]string, error) {
result, err := http.Get(emojiList)
if err != nil {
return nil, err
}
defer result.Body.Close()
emojiList, err := ioutil.ReadAll(result.Body)
if err != nil {
return nil, err
}
var emojis []emoji
if err := json.Unmarshal(emojiList, &emojis); err != nil {
return nil, err
}
var emojiSlice []string
for _, emj := range emojis {
if len(emj.Shortcode) == 0 || strings.Contains(emj.Shortcode, "_tone") {
// dont add them
continue
}
emojiSlice = append(emojiSlice, emj.Shortcode)
}
return emojiSlice, nil
}
func getRemotePackages() error {
var packages = []string{
"samhofi.us/x/keybase",
@ -87,28 +35,6 @@ func exit(err error) { @@ -87,28 +35,6 @@ func exit(err error) {
}
}
// Build kbtui with emoji lookup support
func BuildEmoji() error {
mg.Deps(getRemotePackages)
emojis, err := createEmojiSlice()
if err != nil {
return err
}
f, err := os.Create(emojiFileName)
if err != nil {
return err
}
defer f.Close()
fileContent := fmt.Sprintf("package main\n\nvar emojiSlice = %#v", emojis)
_, err = f.WriteString(fileContent)
if err != nil {
return err
}
f.Sync()
return nil
}
// Build kbtui with just the basic commands.
func Build() {
mg.Deps(getRemotePackages)
@ -157,7 +83,7 @@ func BuildAllCommands() { @@ -157,7 +83,7 @@ func BuildAllCommands() {
// Build kbtui with all Commands and TypeCommands enabled.
func BuildAllCommandsT() {
mg.Deps(getRemotePackages)
if err := sh.Run("go", "build", "-tags", "type_commands,allcommands"); err != nil {
if err := sh.Run("go", "build", "-tags", "type_commands allcommands"); err != nil {
defer func() {
exit(err)
}()
@ -167,8 +93,7 @@ func BuildAllCommandsT() { @@ -167,8 +93,7 @@ func BuildAllCommandsT() {
// Build kbtui with beta functionality
func BuildBeta() {
mg.Deps(getRemotePackages)
mg.Deps(BuildEmoji)
if err := sh.Run("go", "build", "-tags", "allcommands,showreactionscmd,emojiList,tabcompletion"); err != nil {
if err := sh.Run("go", "build", "-tags", "allcommands showreactionscmd tabcompletion execcmd"); err != nil {
defer func() {
exit(err)
}()

248
main.go

@ -22,9 +22,12 @@ var ( @@ -22,9 +22,12 @@ var (
channels []keybase.Channel
stream = false
lastMessage keybase.ChatAPI
lastChat = ""
g *gocui.Gui
)
var config *Config
func main() {
if !k.LoggedIn {
fmt.Println("You are not logged in.")
@ -37,6 +40,7 @@ func main() { @@ -37,6 +40,7 @@ func main() {
}
defer g.Close()
g.SetManagerFunc(layout)
RunCommand("config", "load")
go populateList()
go updateChatWindow()
if len(os.Args) > 1 {
@ -72,7 +76,7 @@ func layout(g *gocui.Gui) error { @@ -72,7 +76,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 +84,9 @@ func layout(g *gocui.Gui) error { @@ -80,7 +84,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", config.Colors.Message.Mention.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 {
@ -92,7 +98,7 @@ func layout(g *gocui.Gui) error { @@ -92,7 +98,7 @@ func layout(g *gocui.Gui) error {
}
inputView.Editable = true
inputView.Wrap = true
inputView.Title = fmt.Sprintf(" Not in a chat - write `%sj` to join", cmdPrefix)
inputView.Title = fmt.Sprintf(" Not in a chat - write `%sj` to join", config.Basics.CmdPrefix)
g.Cursor = true
}
if listView, err4 := g.SetView("List", 0, 0, maxX/2-maxX/3-1, maxY-1, 0); err4 != nil {
@ -104,7 +110,68 @@ func layout(g *gocui.Gui) error { @@ -104,7 +110,68 @@ func layout(g *gocui.Gui) error {
}
return nil
}
func scrollViewUp(v *gocui.View) error {
scrollView(v, -1)
return nil
}
func scrollViewDown(v *gocui.View) error {
scrollView(v, 1)
return nil
}
func scrollView(v *gocui.View, delta int) error {
if v != nil {
_, y := v.Size()
ox, oy := v.Origin()
if oy+delta > strings.Count(v.ViewBuffer(), "\n")-y {
v.Autoscroll = true
} else {
v.Autoscroll = false
if err := v.SetOrigin(ox, oy+delta); err != nil {
return err
}
}
}
return nil
}
func autoScrollView(vn string) error {
v, err := g.View(vn)
if err != nil {
return err
} else if v != nil {
v.Autoscroll = true
}
return nil
}
func initKeybindings() error {
if err := g.SetKeybinding("", gocui.KeyPgup, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
cv, _ := g.View("Chat")
err := scrollViewUp(cv)
if err != nil {
return err
}
return nil
}); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
cv, _ := g.View("Chat")
err := scrollViewDown(cv)
if err != nil {
return err
}
return nil
}); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyEsc, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
autoScrollView("Chat")
return nil
}); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
input, err := getInputString("Input")
@ -119,6 +186,13 @@ func initKeybindings() error { @@ -119,6 +186,13 @@ func initKeybindings() error {
}); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyCtrlZ, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
cmdJoin([]string{"/join", lastChat})
return nil
}); err != nil {
return err
}
if err := g.SetKeybinding("Edit", gocui.KeyCtrlC, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
popupView("Chat")
@ -176,20 +250,19 @@ func getViewTitle(viewName string) string { @@ -176,20 +250,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,13 +320,35 @@ func writeToView(viewName string, message string) { @@ -247,13 +320,35 @@ 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", config.Colors.Feed.Error.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", config.Colors.Feed.Basic.sprintf(removeFormatting(message), parts...).string())
}
func printToView(viewName string, message string) {
g.Update(func(g *gocui.Gui) error {
updatingView, err := g.View(viewName)
if err != nil {
return err
} else {
if config.Basics.UnicodeEmojis {
message = emojiUnicodeConvert(message)
}
fmt.Fprintf(updatingView, "%s\n", message)
}
fmt.Fprintf(updatingView, "%s\n", message)
return nil
})
}
@ -287,13 +382,12 @@ func populateChat() { @@ -287,13 +382,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
@ -318,82 +412,129 @@ func populateChat() { @@ -318,82 +412,129 @@ func populateChat() {
}
}
printToView("Chat", actuallyPrintMe)
go populateList()
}
func populateList() {
_, maxY := g.Size()
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 = config.Colors.Channels.Basic.stylize("")
var recentPMs = textBase.append(config.Colors.Channels.Header.stylize("---[PMs]---\n"))
var recentPMsCount = 0
var recentChannels = fmt.Sprintf("%s---[Teams]---%s\n", channelsHeaderColor, channelsColor)
var recentChannels = textBase.append(config.Colors.Channels.Header.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(config.Colors.Channels.Unread.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(config.Colors.Channels.Unread.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 := config.Colors.Message.Body.stylize(body)
output = colorReplaceMentionMe(output)
output = output.colorRegex(`_[^_]*_`, config.Colors.Message.Body.withItalic())
output = output.colorRegex(`~[^~]*~`, config.Colors.Message.Body.withStrikethrough())
output = output.colorRegex(`@[\w_]*(\.[\w_]+)*`, config.Colors.Message.LinkKeybase)
// TODO change how bold, italic etc works, so it uses boldOn boldOff ([1m and [22m)
output = output.colorRegex(`\*[^\*]*\*`, config.Colors.Message.Body.withBold())
output = output.replaceString("```", "\n<code>\n")
// TODO make background color cover whole line
output = output.colorRegex("<code>(.*\n)*<code>", config.Colors.Message.Code)
output = output.colorRegex("`[^`]*`", config.Colors.Message.Code)
// mention URL
output = output.colorRegex(`(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))`, config.Colors.Message.LinkURL)
return output
}
// TODO use this more
func formatChannel(ch keybase.Channel) StyledString {
return config.Colors.Message.LinkKeybase.stylize(fmt.Sprintf("@%s#%s", ch.Name, ch.TopicName))
}
func colorReplaceMentionMe(msg StyledString) StyledString {
return msg.colorRegex(`(@?\b`+k.Username+`\b)`, config.Colors.Message.Mention)
}
func colorUsername(username string) StyledString {
var color = config.Colors.Message.SenderDefault
if username == k.Username {
color = config.Colors.Message.Mention
}
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 := config.Colors.Message.Header.stylize("")
msgType := api.Msg.Content.Type
switch msgType {
case "text", "attachment":
var c = messageHeaderColor
ret = colorText(outputFormat, c, noColor)
ret = config.Colors.Message.Header.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 = config.Colors.Message.Body.stylize("$TITLE\n$FILE")
attachment := api.Msg.Content.Attachment
msg = msg.replaceString("$TITLE", attachment.Object.Title)
msg = msg.replace("$FILE", config.Colors.Message.Attachment.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 := config.Colors.Message.SenderDevice.stylize(api.Msg.Sender.DeviceName)
msgID := config.Colors.Message.ID.stylize(fmt.Sprintf("%d", api.Msg.ID))
date := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.DateFormat))
msgTime := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.TimeFormat))
channelName := config.Colors.Message.ID.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 := config.Formatting.OutputFormat
if stream {
format = config.Formatting.OutputStreamFormat
}
return ret
return formatMessage(api, format)
}
// End formatting
@ -410,9 +551,7 @@ func handleMessage(api keybase.ChatAPI) { @@ -410,9 +551,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 +560,7 @@ func handleMessage(api keybase.ChatAPI) { @@ -421,7 +560,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, config.Formatting.OutputMentionFormat))
fmt.Print("\a")
}
@ -430,7 +569,7 @@ func handleMessage(api keybase.ChatAPI) { @@ -430,7 +569,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, config.Formatting.PMFormat))
fmt.Print("\a")
}
@ -446,10 +585,9 @@ func handleMessage(api keybase.ChatAPI) { @@ -446,10 +585,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, config.Formatting.PMFormat))
}
}
} else {
@ -483,8 +621,8 @@ func handleInput(viewName string) error { @@ -483,8 +621,8 @@ func handleInput(viewName string) error {
if inputString == "" {
return nil
}
if strings.HasPrefix(inputString, cmdPrefix) {
cmd := deleteEmpty(strings.Split(inputString[len(cmdPrefix):], " "))
if strings.HasPrefix(inputString, config.Basics.CmdPrefix) {
cmd := deleteEmpty(strings.Split(inputString[len(config.Basics.CmdPrefix):], " "))
if len(cmd) < 1 {
return nil
}
@ -494,7 +632,7 @@ func handleInput(viewName string) error { @@ -494,7 +632,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
}
}
@ -503,6 +641,7 @@ func handleInput(viewName string) error { @@ -503,6 +641,7 @@ func handleInput(viewName string) error {
cmd[0] = inputString[:1]
RunCommand(cmd...)
} else {
inputString = resolveRootEmojis(inputString)
go sendChat(inputString)
}
// restore any tab completion view titles on input commit
@ -514,10 +653,11 @@ func handleInput(viewName string) error { @@ -514,10 +653,11 @@ func handleInput(viewName string) error {
return nil
}
func sendChat(message string) {
autoScrollView("Chat")
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))
}
}

12
tabComplete.go

@ -66,7 +66,8 @@ func handleTab(viewName string) error { @@ -66,7 +66,8 @@ func handleTab(viewName string) error {
// Main tab completion functions
func getEmojiTabCompletionSlice(inputWord string) []string {
// use the emojiSlice from emojiList.go and filter it for the input word
resultSlice := filterStringSlice(emojiSlice, inputWord)
//resultSlice := filterStringSlice(emojiSlice, inputWord)
resultSlice := filterEmojiMap(emojiMap, inputWord)
return resultSlice
}
func getChannelTabCompletionSlice(inputWord string) []string {
@ -152,6 +153,15 @@ func filterStringSlice(ss []string, fv string) []string { @@ -152,6 +153,15 @@ func filterStringSlice(ss []string, fv string) []string {
}
return rs
}
func filterEmojiMap(eMap map[string]emojiData, fv string) []string {
var rs []string
for k, _ := range eMap {
if strings.HasPrefix(k, fv) {
rs = append(rs, k)
}
}
return rs
}
func longestCommonPrefix(ss []string) string {
// cover the case where the slice has no or one members
switch len(ss) {

10
tcmdShowReactions.go

@ -20,15 +20,17 @@ func init() { @@ -20,15 +20,17 @@ func init() {
}
func tcmdShowReactions(m keybase.ChatAPI) {
where := ""
team := false
user := colorUsername(m.Msg.Sender.Username)
id := config.Colors.Message.ID.stylize(fmt.Sprintf("%d", m.Msg.Content.Reaction.M))
reaction := config.Colors.Message.Reaction.stylize(m.Msg.Content.Reaction.B)
where := config.Colors.Message.LinkKeybase.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 {

73
types.go

@ -17,3 +17,76 @@ type TypeCommand struct { @@ -17,3 +17,76 @@ type TypeCommand struct {
Description string // A short description of the command
Exec func(keybase.ChatAPI) // A function that takes a raw chat message as input
}
// Config holds user-configurable values
type Config struct {
filepath string `toml:"-"` // filepath is not stored in the config file, but is written to the Config struct so it's known where the config was loaded from
Basics Basics `toml:"basics"`
Formatting Formatting `toml:"formatting"`
Colors Colors `toml:"colors"`
}
// Basics holds the 'basics' section of the config file
type Basics struct {
DownloadPath string `toml:"download_path"`
Colorless bool `toml:"colorless"`
CmdPrefix string `toml:"cmd_prefix"`
UnicodeEmojis bool `toml:"unicode_emojis"`
}
// Formatting holds the 'formatting' section of the config file
type Formatting struct {
OutputFormat string `toml:"output_format"`
OutputStreamFormat string `toml:"output_stream_format"`
OutputMentionFormat string `toml:"output_mention_format"`
PMFormat string `toml:"pm_format"`
DateFormat string `toml:"date_format"`
TimeFormat string `toml:"time_format"`
}
// Colors holds the 'colors' section of the config file
type Colors struct {
Channels Channels `toml:"channels"`
Message Message `toml:"message"`
Feed Feed `toml:"feed"`
}
// Style holds basic style information
type Style struct {
Foreground string `toml:"foreground"`
Background string `toml:"background"`
Italic bool `toml:"italic"`
Bold bool `toml:"bold"`
Underline bool `toml:"underline"`
Strikethrough bool `toml:"strikethrough"`
Inverse bool `toml:"inverse"`
}
// Channels holds the style information for various elements of a channel
type Channels struct {
Basic Style `toml:"basic"`
Header Style `toml:"header"`
Unread Style `toml:"unread"`
}
// Message holds the style information for various elements of a message
type Message struct {
Body Style `toml:"body"`
Header Style `toml:"header"`
Mention Style `toml:"mention"`
ID Style `toml:"id"`
Time Style `toml:"time"`
SenderDefault Style `toml:"sender_default"`
SenderDevice Style `toml:"sender_device"`
Attachment Style `toml:"attachment"`
LinkURL Style `toml:"link_url"`
LinkKeybase Style `toml:"link_keybase"`
Reaction Style `toml:"reaction"`
Code Style `toml:"code"`
}
// Feed holds the style information for various elements of the feed window
type Feed struct {
Basic Style `toml:"basic"`
Error Style `toml:"error"`
}

30
userConfigs.go

@ -1,30 +0,0 @@ @@ -1,30 +0,0 @@
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)
// BASH-like PS1 variable equivalent
var outputFormat = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
// 02 = Day, Jan = Month, 06 = Year
var dateFormat = "02Jan06"
// 15 = hours, 04 = minutes, 05 = seconds
var timeFormat = "15:04"
// The prefix before evaluating a command
var cmdPrefix = "/"
Loading…
Cancel
Save