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
.idea/* .idea/*
.idea .idea
*.log *.log
.travis.yml

3
cmdClean.go

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

88
cmdConfig.go

@ -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 @@
package main package main
import ( import (
"fmt"
"strconv" "strconv"
) )
@ -28,7 +27,7 @@ func cmdDelete(cmd []string) {
chat := k.NewChat(channel) chat := k.NewChat(channel)
_, err := chat.Delete(messageID) _, err := chat.Delete(messageID)
if err != nil { 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() {
func cmdDownloadFile(cmd []string) { func cmdDownloadFile(cmd []string) {
if len(cmd) < 2 { 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 return
} }
messageID, err := strconv.Atoi(cmd[1]) messageID, err := strconv.Atoi(cmd[1])
if err != nil { 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 return
} }
chat := k.NewChat(channel) chat := k.NewChat(channel)
api, err := chat.ReadMessage(messageID) api, err := chat.ReadMessage(messageID)
if err != nil { 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 return
} }
if api.Result.Messages[0].Msg.Content.Type != "attachment" { if api.Result.Messages[0].Msg.Content.Type != "attachment" {
printToView("Feed", "No attachment detected") printError("No attachment detected")
return return
} }
var fileName string var fileName string
@ -46,10 +46,11 @@ func cmdDownloadFile(cmd []string) {
fileName = api.Result.Messages[0].Msg.Content.Attachment.Object.Filename 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 { 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 { } 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) {
if len(cmd) == 2 { if len(cmd) == 2 {
messageID, _ = strconv.Atoi(cmd[1]) messageID, _ = strconv.Atoi(cmd[1])
} else if lastMessage.ID != 0 { } else if lastMessage.ID != 0 {
message, _ := chat.ReadMessage(lastMessage.ID)
lastMessage.Type = message.Result.Messages[0].Msg.Content.Type
if lastMessage.Type != "text" { 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 return
} }
messageID = lastMessage.ID messageID = lastMessage.ID
} else { } else {
printToView("Feed", "No message to edit") printError("No message to edit")
return return
} }
origMessage, _ := chat.ReadMessage(messageID) origMessage, _ := chat.ReadMessage(messageID)
if origMessage.Result.Messages[0].Msg.Content.Type != "text" { if origMessage.Result.Messages[0].Msg.Content.Type != "text" {
printToView("Feed", fmt.Sprintf("%+v", origMessage)) printInfo(fmt.Sprintf("%+v", origMessage))
return return
} }
if origMessage.Result.Messages[0].Msg.Sender.Username != k.Username { 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 return
} }
editString := origMessage.Result.Messages[0].Msg.Content.Text.Body editString := origMessage.Result.Messages[0].Msg.Content.Text.Body
@ -53,14 +55,14 @@ func cmdEdit(cmd []string) {
return return
} }
if len(cmd) < 3 { if len(cmd) < 3 {
printToView("Feed", "Not enough options for Edit") printError("Not enough options for Edit")
return return
} }
messageID, _ = strconv.Atoi(cmd[1]) messageID, _ = strconv.Atoi(cmd[1])
newMessage := strings.Join(cmd[2:], " ") newMessage := strings.Join(cmd[2:], " ")
_, err := chat.Edit(messageID, newMessage) _, err := chat.Edit(messageID, newMessage)
if err != nil { 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 @@
// +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) {
if len(cmd) == 1 { if len(cmd) == 1 {
sort.Strings(baseCommands) sort.Strings(baseCommands)
for _, c := range 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 { if len(typeCommands) > 0 {
for c := range typeCommands { for c := range typeCommands {

8
cmdJoin.go

@ -41,12 +41,14 @@ func cmdJoin(cmd []string) {
channel.TopicName = "" channel.TopicName = ""
channel.MembersType = keybase.USER 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") clearView("Chat")
setViewTitle("Input", fmt.Sprintf(" %s ", joinedName)) setViewTitle("Input", fmt.Sprintf(" %s ", joinedName))
lastChat = joinedName
autoScrollView("Chat")
go populateChat() go populateChat()
default: default:
printToView("Feed", fmt.Sprintf("To join a team use %sjoin <team> <channel>", cmdPrefix)) printInfo(fmt.Sprintf("To join a team use %sjoin <team> <channel>", config.Basics.CmdPrefix))
printToView("Feed", fmt.Sprintf("To join a PM use %sjoin <user>", 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) {
chat := k.NewChat(pubChan) chat := k.NewChat(pubChan)
_, err := chat.Send(post) _, err := chat.Send(post)
if err != nil { 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 { } 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) {
chat := k.NewChat(channel) chat := k.NewChat(channel)
_, err := chat.React(messageID, reaction) _, err := chat.React(messageID, reaction)
if err != nil { 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() {
func cmdReply(cmd []string) { func cmdReply(cmd []string) {
chat := k.NewChat(channel) chat := k.NewChat(channel)
if len(cmd) < 2 { 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 return
} }
messageID, err := strconv.Atoi(cmd[1]) messageID, err := strconv.Atoi(cmd[1])
if err != nil { 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 return
} }
_, err = chat.Reply(messageID, strings.Join(cmd[2:], " ")) _, err = chat.Reply(messageID, strings.Join(cmd[2:], " "))
if err != nil { if err != nil {
printToView("Feed", "There was an error with your reply.") printError("There was an error with your reply.")
return return
} }
return
} }

94
cmdSet.go

@ -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 @@
package main package main
import (
"fmt"
)
func init() { func init() {
command := Command{ command := Command{
Cmd: []string{"stream", "s"}, Cmd: []string{"stream", "s"},
@ -17,7 +21,7 @@ func cmdStream(cmd []string) {
stream = true stream = true
channel.Name = "" channel.Name = ""
printToView("Feed", "You are now viewing the formatted stream") printInfo("You are now viewing the formatted stream")
setViewTitle("Input", " Stream - Not in a chat /j to join ") setViewTitle("Input", fmt.Sprintf(" Stream - Not in a chat. %sj to join ", config.Basics.CmdPrefix))
clearView("Chat") clearView("Chat")
} }

9
cmdUploadFile.go

@ -21,14 +21,14 @@ func init() {
func cmdUploadFile(cmd []string) { func cmdUploadFile(cmd []string) {
if len(cmd) < 2 { 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 return
} }
filePath := cmd[1] filePath := cmd[1]
if !strings.HasPrefix(filePath, "/") { if !strings.HasPrefix(filePath, "/") {
dir, err := os.Getwd() dir, err := os.Getwd()
if err != nil { 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) filePath = fmt.Sprintf("%s/%s", dir, filePath)
} }
@ -40,9 +40,10 @@ func cmdUploadFile(cmd []string) {
} }
chat := k.NewChat(channel) chat := k.NewChat(channel)
_, err := chat.Upload(fileName, filePath) _, err := chat.Upload(fileName, filePath)
channelName := config.Colors.Message.LinkKeybase.stylize(channel.Name).string()
if err != nil { 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 { } 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) {
if len(users) < 1 { if len(users) < 1 {
return 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 { for _, chann := range users {
chat := k.NewChat(chann) chat := k.NewChat(chann)
api, err := chat.Read() api, err := chat.Read()
if err != nil { if err != nil {
if len(users) < 6 { 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 return
} }
} else { } else {

8
cmdWallet.go

@ -42,20 +42,20 @@ func cmdWallet(cmd []string) {
walletConfirmationCode = b.String() walletConfirmationCode = b.String()
walletConfirmationUser = cmd[1] walletConfirmationUser = cmd[1]
walletTransactionAmnt = cmd[2] 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" { } else if cmd[0] == "confirm" {
if cmd[1] == walletConfirmationUser && cmd[2] == walletConfirmationCode { if cmd[1] == walletConfirmationUser && cmd[2] == walletConfirmationCode {
txWallet := k.NewWallet() txWallet := k.NewWallet()
wAPI, err := txWallet.SendXLM(walletConfirmationUser, walletTransactionAmnt, "") wAPI, err := txWallet.SendXLM(walletConfirmationUser, walletTransactionAmnt, "")
if err != nil { 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 { } 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 { } 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
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strings"
) )
// TODO maybe datastructure const (
// BASH-like PS1 variable equivalent (without colours) black int = iota
// TODO bold? cursive etc? red
func color(c int) string { green
if colorless { 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 "" return ""
} }
if c < 0 { output := "\x1b[0m\x1b[0"
return "\033[0m" if colorFromString(s.Foreground) != normal {
} else { output += fmt.Sprintf(";%d", 30+colorFromString(s.Foreground))
return fmt.Sprintf("\033[0;%dm", 29+c)
} }
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 (s Style) stylize(msg string) StyledString {
func colorText(text string, color string, offColor string) string { return StyledString{msg, s}
return fmt.Sprintf("%s%s%s", color, text, offColor) }
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 { func (t StyledString) replace(match string, value StyledString) StyledString {
var color = messageSenderDefaultColor return t.replaceN(match, value, -1)
if username == k.Username { }
color = mentionColor 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) // Appends the other stylize at the end, but retains same style
return re.ReplaceAllString(msg, colorText(`$1`, color, offColor)) 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 { func removeFormatting(s string) string {
//var coloredOwnName = colorText(k.Username, mentionColor, offColor) reFormatting := regexp.MustCompile(`(?m)\x1b\[(\d*;?)*m`)
//return strings.Replace(msg, k.Username, coloredOwnName, -1) return reFormatting.ReplaceAllString(s, "")
return colorRegex(msg, "(@?"+k.Username+")", mentionColor, offColor)
} }

72
defaultConfig.go

@ -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 @@
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 @@
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 @@
[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 @@
[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 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"os" "os"
"strings"
"github.com/magefile/mage/mg" "github.com/magefile/mage/mg"
"github.com/magefile/mage/sh" "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 { func getRemotePackages() error {
var packages = []string{ var packages = []string{
"samhofi.us/x/keybase", "samhofi.us/x/keybase",
@ -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. // Build kbtui with just the basic commands.
func Build() { func Build() {
mg.Deps(getRemotePackages) mg.Deps(getRemotePackages)
@ -157,7 +83,7 @@ func BuildAllCommands() {
// Build kbtui with all Commands and TypeCommands enabled. // Build kbtui with all Commands and TypeCommands enabled.
func BuildAllCommandsT() { func BuildAllCommandsT() {
mg.Deps(getRemotePackages) 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() { defer func() {
exit(err) exit(err)
}() }()
@ -167,8 +93,7 @@ func BuildAllCommandsT() {
// Build kbtui with beta functionality // Build kbtui with beta functionality
func BuildBeta() { func BuildBeta() {
mg.Deps(getRemotePackages) mg.Deps(getRemotePackages)
mg.Deps(BuildEmoji) if err := sh.Run("go", "build", "-tags", "allcommands showreactionscmd tabcompletion execcmd"); err != nil {
if err := sh.Run("go", "build", "-tags", "allcommands,showreactionscmd,emojiList,tabcompletion"); err != nil {
defer func() { defer func() {
exit(err) exit(err)
}() }()

248
main.go

@ -22,9 +22,12 @@ var (
channels []keybase.Channel channels []keybase.Channel
stream = false stream = false
lastMessage keybase.ChatAPI lastMessage keybase.ChatAPI
lastChat = ""
g *gocui.Gui g *gocui.Gui
) )
var config *Config
func main() { func main() {
if !k.LoggedIn { if !k.LoggedIn {
fmt.Println("You are not logged in.") fmt.Println("You are not logged in.")
@ -37,6 +40,7 @@ func main() {
} }
defer g.Close() defer g.Close()
g.SetManagerFunc(layout) g.SetManagerFunc(layout)
RunCommand("config", "load")
go populateList() go populateList()
go updateChatWindow() go updateChatWindow()
if len(os.Args) > 1 { if len(os.Args) > 1 {
@ -72,7 +76,7 @@ func layout(g *gocui.Gui) error {
feedView.Autoscroll = true feedView.Autoscroll = true
feedView.Wrap = true feedView.Wrap = true
feedView.Title = "Feed Window" 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 chatView, err2 := g.SetView("Chat", maxX/2-maxX/3, maxY/5+1, maxX-1, maxY-5, 0); err2 != nil {
if !gocui.IsUnknownView(err2) { if !gocui.IsUnknownView(err2) {
@ -80,7 +84,9 @@ func layout(g *gocui.Gui) error {
} }
chatView.Autoscroll = true chatView.Autoscroll = true
chatView.Wrap = 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") RunCommand("help")
} }
if inputView, err3 := g.SetView("Input", maxX/2-maxX/3, maxY-4, maxX-1, maxY-1, 0); err3 != nil { 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 {
} }
inputView.Editable = true inputView.Editable = true
inputView.Wrap = 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 g.Cursor = true
} }
if listView, err4 := g.SetView("List", 0, 0, maxX/2-maxX/3-1, maxY-1, 0); err4 != nil { 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 {
} }
return nil 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 { 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, if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error { func(g *gocui.Gui, v *gocui.View) error {
input, err := getInputString("Input") input, err := getInputString("Input")
@ -119,6 +186,13 @@ func initKeybindings() error {
}); err != nil { }); err != nil {
return err 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, if err := g.SetKeybinding("Edit", gocui.KeyCtrlC, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error { func(g *gocui.Gui, v *gocui.View) error {
popupView("Chat") popupView("Chat")
@ -176,20 +250,19 @@ func getViewTitle(viewName string) string {
view, err := g.View(viewName) view, err := g.View(viewName)
if err != nil { if err != nil {
// in case there is active tab completion, filter that to just the view title and not the completion options. // 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 ""
} }
return strings.Split(view.Title, "||")[0] return strings.Split(view.Title, "||")[0]
} }
func popupView(viewName string) { func popupView(viewName string) {
_, err := g.SetCurrentView(viewName) _, err := g.SetCurrentView(viewName)
if err != nil { if err != nil {
printToView("Feed", fmt.Sprintf("%+v", err)) printError(fmt.Sprintf("%+v", err))
} }
_, err = g.SetViewOnTop(viewName) _, err = g.SetViewOnTop(viewName)
if err != nil { if err != nil {
printToView("Feed", fmt.Sprintf("%+v", err)) printError(fmt.Sprintf("%+v", err))
} }
g.Update(func(g *gocui.Gui) error { g.Update(func(g *gocui.Gui) error {
updatingView, err := g.View(viewName) updatingView, err := g.View(viewName)
@ -247,13 +320,35 @@ func writeToView(viewName string, message string) {
return nil 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) { func printToView(viewName string, message string) {
g.Update(func(g *gocui.Gui) error { g.Update(func(g *gocui.Gui) error {
updatingView, err := g.View(viewName) updatingView, err := g.View(viewName)
if err != nil { if err != nil {
return err return err
} else {
if config.Basics.UnicodeEmojis {
message = emojiUnicodeConvert(message)
}
fmt.Fprintf(updatingView, "%s\n", message)
} }
fmt.Fprintf(updatingView, "%s\n", message)
return nil return nil
}) })
} }
@ -287,13 +382,12 @@ func populateChat() {
chat = k.NewChat(channel) chat = k.NewChat(channel)
_, err2 := chat.Read(2) _, err2 := chat.Read(2)
if err2 != nil { if err2 != nil {
printToView("Feed", fmt.Sprintf("%+v", err)) printError(fmt.Sprintf("%+v", err))
return return
} }
go populateChat() go populateChat()
go generateChannelTabCompletionSlice() go generateChannelTabCompletionSlice()
return return
} }
var printMe []string var printMe []string
var actuallyPrintMe string var actuallyPrintMe string
@ -318,82 +412,129 @@ func populateChat() {
} }
} }
printToView("Chat", actuallyPrintMe) printToView("Chat", actuallyPrintMe)
go populateList()
} }
func populateList() { func populateList() {
_, maxY := g.Size() _, maxY := g.Size()
if testVar, err := k.ChatList(); err != nil { if testVar, err := k.ChatList(); err != nil {
log.Printf("%+v", err) log.Printf("%+v", err)
} else { } else {
clearView("List") 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 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 var recentChannelsCount = 0
for _, s := range testVar.Result.Conversations { for _, s := range testVar.Result.Conversations {
channels = append(channels, s.Channel) channels = append(channels, s.Channel)
if s.Channel.MembersType == keybase.TEAM { if s.Channel.MembersType == keybase.TEAM {
recentChannelsCount++ recentChannelsCount++
if recentChannelsCount <= ((maxY - 2) / 3) { if recentChannelsCount <= ((maxY - 2) / 3) {
channel := fmt.Sprintf("%s\n\t#%s\n", s.Channel.Name, s.Channel.TopicName)
if s.Unread { 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 { } else {
recentPMsCount++ recentPMsCount++
if recentPMsCount <= ((maxY - 2) / 3) { if recentPMsCount <= ((maxY - 2) / 3) {
pmName := fmt.Sprintf("%s\n", cleanChannelName(s.Channel.Name))
if s.Unread { 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) time.Sleep(1 * time.Millisecond)
printToView("List", fmt.Sprintf("%s%s%s%s", channelsColor, recentPMs, recentChannels, noColor)) printToView("List", fmt.Sprintf("%s%s", recentPMs.string(), recentChannels.string()))
go generateRecentTabCompletionSlice() generateRecentTabCompletionSlice()
} }
} }
// End update/populate views automatically // End update/populate views automatically
// Formatting // 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 { func cleanChannelName(c string) string {
newChannelName := strings.Replace(c, fmt.Sprintf("%s,", k.Username), "", 1) newChannelName := strings.Replace(c, fmt.Sprintf("%s,", k.Username), "", 1)
return strings.Replace(newChannelName, 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 msgType := api.Msg.Content.Type
switch msgType { switch msgType {
case "text", "attachment": case "text", "attachment":
var c = messageHeaderColor ret = config.Colors.Message.Header.stylize(formatString)
ret = colorText(outputFormat, c, noColor)
tm := time.Unix(int64(api.Msg.SentAt), 0) tm := time.Unix(int64(api.Msg.SentAt), 0)
var msg = api.Msg.Content.Text.Body var msg = formatMessageBody(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)
if msgType == "attachment" { 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) user := colorUsername(api.Msg.Sender.Username)
msgID := colorText(fmt.Sprintf("%d", api.Msg.ID), messageIdColor, c) device := config.Colors.Message.SenderDevice.stylize(api.Msg.Sender.DeviceName)
ts := colorText(tm.Format(timeFormat), messageTimeColor, c) msgID := config.Colors.Message.ID.stylize(fmt.Sprintf("%d", api.Msg.ID))
ret = strings.Replace(ret, "$MSG", msg, 1) date := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.DateFormat))
ret = strings.Replace(ret, "$USER", user, 1) msgTime := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.TimeFormat))
ret = strings.Replace(ret, "$DEVICE", device, 1)
ret = strings.Replace(ret, "$ID", msgID, 1) channelName := config.Colors.Message.ID.stylize(fmt.Sprintf("@%s#%s", api.Msg.Channel.Name, api.Msg.Channel.TopicName))
ret = strings.Replace(ret, "$TIME", ts, 1) ret = ret.replace("$MSG", msg)
ret = strings.Replace(ret, "$DATE", colorText(tm.Format(dateFormat), messageTimeColor, c), 1) ret = ret.replace("$USER", user)
ret = strings.Replace(ret, "```", fmt.Sprintf("\n<code>\n"), -1) 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 // End formatting
@ -410,9 +551,7 @@ func handleMessage(api keybase.ChatAPI) {
} }
if api.Msg.Content.Type == "text" || api.Msg.Content.Type == "attachment" { if api.Msg.Content.Type == "text" || api.Msg.Content.Type == "attachment" {
go populateList() go populateList()
msgBody := api.Msg.Content.Text.Body
msgSender := api.Msg.Sender.Username msgSender := api.Msg.Sender.Username
channelName := api.Msg.Channel.Name
if !stream { if !stream {
if msgSender != k.Username { if msgSender != k.Username {
if api.Msg.Channel.MembersType == keybase.TEAM { if api.Msg.Channel.MembersType == keybase.TEAM {
@ -421,7 +560,7 @@ func handleMessage(api keybase.ChatAPI) {
if m.Text == k.Username { if m.Text == k.Username {
// We are in a team // We are in a team
if topicName != channel.TopicName { 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") fmt.Print("\a")
} }
@ -430,7 +569,7 @@ func handleMessage(api keybase.ChatAPI) {
} }
} else { } else {
if msgSender != channel.Name { if msgSender != channel.Name {
printToView("Feed", fmt.Sprintf("PM from @%s: %s", cleanChannelName(channelName), msgBody)) printInfo(formatMessage(api, config.Formatting.PMFormat))
fmt.Print("\a") fmt.Print("\a")
} }
@ -446,10 +585,9 @@ func handleMessage(api keybase.ChatAPI) {
} }
} else { } else {
if api.Msg.Channel.MembersType == keybase.TEAM { if api.Msg.Channel.MembersType == keybase.TEAM {
topicName := api.Msg.Channel.TopicName printToView("Chat", formatOutput(api))
printToView("Chat", fmt.Sprintf("@%s#%s [%s]: %s", channelName, topicName, msgSender, msgBody))
} else { } else {
printToView("Chat", fmt.Sprintf("PM @%s [%s]: %s", cleanChannelName(channelName), msgSender, msgBody)) printToView("Chat", formatMessage(api, config.Formatting.PMFormat))
} }
} }
} else { } else {
@ -483,8 +621,8 @@ func handleInput(viewName string) error {
if inputString == "" { if inputString == "" {
return nil return nil
} }
if strings.HasPrefix(inputString, cmdPrefix) { if strings.HasPrefix(inputString, config.Basics.CmdPrefix) {
cmd := deleteEmpty(strings.Split(inputString[len(cmdPrefix):], " ")) cmd := deleteEmpty(strings.Split(inputString[len(config.Basics.CmdPrefix):], " "))
if len(cmd) < 1 { if len(cmd) < 1 {
return nil return nil
} }
@ -494,7 +632,7 @@ func handleInput(viewName string) error {
} else if cmd[0] == "q" || cmd[0] == "quit" { } else if cmd[0] == "q" || cmd[0] == "quit" {
return gocui.ErrQuit return gocui.ErrQuit
} else { } else {
printToView("Feed", fmt.Sprintf("Command '%s' not recognized", cmd[0])) printError(fmt.Sprintf("Command '%s' not recognized", cmd[0]))
return nil return nil
} }
} }
@ -503,6 +641,7 @@ func handleInput(viewName string) error {
cmd[0] = inputString[:1] cmd[0] = inputString[:1]
RunCommand(cmd...) RunCommand(cmd...)
} else { } else {
inputString = resolveRootEmojis(inputString)
go sendChat(inputString) go sendChat(inputString)
} }
// restore any tab completion view titles on input commit // restore any tab completion view titles on input commit
@ -514,10 +653,11 @@ func handleInput(viewName string) error {
return nil return nil
} }
func sendChat(message string) { func sendChat(message string) {
autoScrollView("Chat")
chat := k.NewChat(channel) chat := k.NewChat(channel)
_, err := chat.Send(message) _, err := chat.Send(message)
if err != nil { 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 {
// Main tab completion functions // Main tab completion functions
func getEmojiTabCompletionSlice(inputWord string) []string { func getEmojiTabCompletionSlice(inputWord string) []string {
// use the emojiSlice from emojiList.go and filter it for the input word // 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 return resultSlice
} }
func getChannelTabCompletionSlice(inputWord string) []string { func getChannelTabCompletionSlice(inputWord string) []string {
@ -152,6 +153,15 @@ func filterStringSlice(ss []string, fv string) []string {
} }
return rs 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 { func longestCommonPrefix(ss []string) string {
// cover the case where the slice has no or one members // cover the case where the slice has no or one members
switch len(ss) { switch len(ss) {

10
tcmdShowReactions.go

@ -20,15 +20,17 @@ func init() {
} }
func tcmdShowReactions(m keybase.ChatAPI) { func tcmdShowReactions(m keybase.ChatAPI) {
where := ""
team := false 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 { if m.Msg.Channel.MembersType == keybase.TEAM {
team = true team = true
where = fmt.Sprintf("in @%s#%s", m.Msg.Channel.Name, m.Msg.Channel.TopicName) where = formatChannel(m.Msg.Channel)
} else { } 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 channel.Name == m.Msg.Channel.Name {
if team { if team {
if channel.TopicName == m.Msg.Channel.TopicName { if channel.TopicName == m.Msg.Channel.TopicName {

73
types.go

@ -17,3 +17,76 @@ type TypeCommand struct {
Description string // A short description of the command Description string // A short description of the command
Exec func(keybase.ChatAPI) // A function that takes a raw chat message as input 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 @@
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