diff --git a/.travis.yml b/.travis.yml index 085d22d..b17a781 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,6 @@ go: install: true script: - - go get -u samhofi.us/x/keybase - - go get -u github.com/awesome-gocui/gocui - go get -u github.com/magefile/mage/mage - go run build.go buildBeta - go vet ./... diff --git a/README.md b/README.md index d814c18..1b0dd55 100644 --- a/README.md +++ b/README.md @@ -31,16 +31,9 @@ go get -u github.com/rudi9719/kbtui ``` Or you can do the following: ``` -go get ./... -go run build.go +go get github.com/magefile/mage/mage go run build.go {build, buildBeta... etc} ./kbtui ``` - -You may see an error with `go get ./...` about PATHs, that may be safely ignored. - -If you see an error about a missing dependancy during a build, you'll want to resolve that. - - -Occasionally when [@dxb](https://keybase.io/dxb) updates his API it will be necessary to run -`go get -u ./...` or `go get -u samhofi.us/x/keybase` +Mage is a requirement for building `kbtui` as it will automatically handle/manage imports as well as mage is used to generate the +file for emoji completion. diff --git a/cmdClean.go b/cmdClean.go index aeb2eed..21e2853 100644 --- a/cmdClean.go +++ b/cmdClean.go @@ -15,6 +15,7 @@ func init() { func cmdClean(cmd []string) { clearView("Chat") + clearView("List") go populateChat() - + go populateList() } diff --git a/cmdDownload.go b/cmdDownload.go index ff6cbbf..796517c 100644 --- a/cmdDownload.go +++ b/cmdDownload.go @@ -24,16 +24,29 @@ func cmdDownloadFile(cmd []string) { printToView("Feed", fmt.Sprintf("%s%s $messageId $fileName - Download a file to user's downloadpath", cmdPrefix, cmd[0])) return } - messageID, _ := strconv.Atoi(cmd[1]) + messageID, err := strconv.Atoi(cmd[1]) + if err != nil { + printToView("Feed", "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)) + return + } + if api.Result.Messages[0].Msg.Content.Type != "attachment" { + printToView("Feed", "No attachment detected") + return + } var fileName string if len(cmd) == 3 { fileName = cmd[2] } else { - fileName = "" + fileName = api.Result.Messages[0].Msg.Content.Attachment.Object.Filename } - chat := k.NewChat(channel) - _, err := chat.Download(messageID, fmt.Sprintf("%s/%s", downloadPath, fileName)) + _, err = chat.Download(messageID, fmt.Sprintf("%s/%s", downloadPath, fileName)) if err != nil { printToView("Feed", fmt.Sprintf("There was an error downloading %s from %s", fileName, channel.Name)) } else { diff --git a/cmdEdit.go b/cmdEdit.go index af40142..ed950eb 100644 --- a/cmdEdit.go +++ b/cmdEdit.go @@ -11,7 +11,7 @@ import ( func init() { command := Command{ Cmd: []string{"edit", "e"}, - Description: "$messageId - Edit a message (messageID is optional)", + Description: "$messageID - Edit a message (messageID is optional)", Help: "", Exec: cmdEdit, } @@ -20,16 +20,22 @@ func init() { } func cmdEdit(cmd []string) { - var messageId int + var messageID int chat := k.NewChat(channel) if len(cmd) == 2 || len(cmd) == 1 { if len(cmd) == 2 { - messageId, _ = strconv.Atoi(cmd[1]) + messageID, _ = strconv.Atoi(cmd[1]) + } else if lastMessage.ID != 0 { + if lastMessage.Type != "text" { + printToView("Feed", "Last message isn't editable (is it an edit?)") + return + } + messageID = lastMessage.ID } else { - messageId = lastMessage.ID + printToView("Feed", "No message to edit") + return } - - origMessage, _ := chat.ReadMessage(messageId) + origMessage, _ := chat.ReadMessage(messageID) if origMessage.Result.Messages[0].Msg.Content.Type != "text" { printToView("Feed", fmt.Sprintf("%+v", origMessage)) return @@ -41,19 +47,20 @@ func cmdEdit(cmd []string) { editString := origMessage.Result.Messages[0].Msg.Content.Text.Body clearView("Edit") popupView("Edit") - printToView("Edit", fmt.Sprintf("/e %d %s", messageId, editString)) - setViewTitle("Edit", fmt.Sprintf(" Editing message %d ", messageId)) + printToView("Edit", fmt.Sprintf("/e %d %s", messageID, editString)) + setViewTitle("Edit", fmt.Sprintf(" Editing message %d ", messageID)) + moveCursorToEnd("Edit") return } if len(cmd) < 3 { printToView("Feed", "Not enough options for Edit") return } - messageId, _ = strconv.Atoi(cmd[1]) + messageID, _ = strconv.Atoi(cmd[1]) newMessage := strings.Join(cmd[2:], " ") - _, err := chat.Edit(messageId, newMessage) + _, err := chat.Edit(messageID, newMessage) if err != nil { - printToView("Feed", fmt.Sprintf("Error editing message %d, %+v", messageId, err)) + printToView("Feed", fmt.Sprintf("Error editing message %d, %+v", messageID, err)) } } diff --git a/cmdHelp.go b/cmdHelp.go index 0c9b1fa..bbf71de 100644 --- a/cmdHelp.go +++ b/cmdHelp.go @@ -28,7 +28,7 @@ func cmdHelp(cmd []string) { helpText = fmt.Sprintf("%s%s%s\t\t%s\n", helpText, cmdPrefix, c, commands[c].Description) } if len(typeCommands) > 0 { - for c, _ := range typeCommands { + for c := range typeCommands { tCommands = append(tCommands, typeCommands[c].Name) } sort.Strings(tCommands) diff --git a/cmdReact.go b/cmdReact.go index 1175216..ddd92f6 100644 --- a/cmdReact.go +++ b/cmdReact.go @@ -10,7 +10,7 @@ import ( func init() { command := Command{ Cmd: []string{"react", "r", "+"}, - Description: "$messageId $reaction - React to a message (messageID is optional)", + Description: "$messageID $reaction - React to a message (messageID is optional)", Help: "", Exec: cmdReact, } @@ -20,7 +20,7 @@ func init() { func cmdReact(cmd []string) { if len(cmd) > 2 { - reactToMessageId(cmd[1], strings.Join(cmd[2:], " ")) + reactToMessageID(cmd[1], strings.Join(cmd[2:], " ")) } else if len(cmd) == 2 { reactToMessage(cmd[1]) } @@ -30,13 +30,13 @@ func cmdReact(cmd []string) { func reactToMessage(reaction string) { doReact(lastMessage.ID, reaction) } -func reactToMessageId(messageId string, reaction string) { - ID, _ := strconv.Atoi(messageId) +func reactToMessageID(messageID string, reaction string) { + ID, _ := strconv.Atoi(messageID) doReact(ID, reaction) } -func doReact(messageId int, reaction string) { +func doReact(messageID int, reaction string) { chat := k.NewChat(channel) - _, err := chat.React(messageId, reaction) + _, err := chat.React(messageID, reaction) if err != nil { printToView("Feed", "There was an error reacting to the message.") } diff --git a/cmdSet.go b/cmdSet.go index fabdc06..bd5e17a 100644 --- a/cmdSet.go +++ b/cmdSet.go @@ -5,6 +5,8 @@ package main import ( "fmt" "strings" + + "github.com/pelletier/go-toml" ) func init() { @@ -17,30 +19,33 @@ func init() { RegisterCommand(command) } +func printSetting(cmd []string) { + switch cmd[1] { + case "load": + loadFromToml() + 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 { - switch cmd[1] { - case "load": - printToView("Feed", "Load values from file?") - 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])) - } - + printSetting(cmd) return } switch cmd[1] { @@ -62,3 +67,30 @@ func cmdSet(cmd []string) { } } +func loadFromToml() { + printToView("Feed", fmt.Sprintf("Loading config from toml")) + config, err := toml.LoadFile("kbtui.tml") + if err != nil { + printToView("Feed", fmt.Sprintf("Could not read config file: %+v", err)) + 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) + } + RunCommand("clean") +} diff --git a/kbtui.tml b/kbtui.tml new file mode 100644 index 0000000..4ce8ecb --- /dev/null +++ b/kbtui.tml @@ -0,0 +1,30 @@ +[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 diff --git a/mage.go b/mage.go index 4d8611d..a7f02d1 100644 --- a/mage.go +++ b/mage.go @@ -4,10 +4,29 @@ package main import ( "fmt" - "github.com/magefile/mage/sh" "os" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" ) +func getRemotePackages() error { + var packages = []string{ + "samhofi.us/x/keybase", + "github.com/awesome-gocui/gocui", + "github.com/magefile/mage/mage", + "github.com/magefile/mage/mg", + "github.com/magefile/mage/sh", + "github.com/pelletier/go-toml", + } + for _, p := range packages { + if err := sh.Run("go", "get", "-u", p); err != nil { + return err + } + } + return nil +} + // proper error reporting and exit code func exit(err error) { if err != nil { @@ -18,6 +37,7 @@ func exit(err error) { // Build kbtui with just the basic commands. func Build() { + mg.Deps(getRemotePackages) if err := sh.Run("go", "build"); err != nil { defer func() { exit(err) @@ -29,6 +49,7 @@ func Build() { // The ShowReactions TypeCommand will print a message in the feed window when // a reaction is received in the current conversation. func BuildShowReactions() { + mg.Deps(getRemotePackages) if err := sh.Run("go", "build", "-tags", "showreactionscmd"); err != nil { defer func() { exit(err) @@ -41,6 +62,7 @@ func BuildShowReactions() { // received in the current conversation. This gets pretty annoying, and // is not recommended. func BuildAutoReact() { + mg.Deps(getRemotePackages) if err := sh.Run("go", "build", "-tags", "autoreactcmd"); err != nil { defer func() { exit(err) @@ -50,6 +72,7 @@ func BuildAutoReact() { // Build kbtui with all commands and TypeCommands disabled. func BuildAllCommands() { + mg.Deps(getRemotePackages) if err := sh.Run("go", "build", "-tags", "allcommands"); err != nil { defer func() { exit(err) @@ -59,6 +82,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 { defer func() { exit(err) @@ -68,7 +92,8 @@ func BuildAllCommandsT() { // Build kbtui with beta functionality func BuildBeta() { - if err := sh.Run("go", "build", "-tags", "allcommands,showreactionscmd,tabcompletion"); err != nil { + mg.Deps(getRemotePackages) + if err := sh.Run("go", "build", "-tags", "allcommands,showreactionscmd,emojiList,tabcompletion"); err != nil { defer func() { exit(err) }() diff --git a/main.go b/main.go index 98de32b..68d0fc2 100644 --- a/main.go +++ b/main.go @@ -112,9 +112,8 @@ func initKeybindings() error { if input != "" { clearView("Input") return nil - } else { - return gocui.ErrQuit } + return gocui.ErrQuit }); err != nil { return err } @@ -166,9 +165,8 @@ func setViewTitle(viewName string, title string) { updatingView, err := g.View(viewName) if err != nil { return err - } else { - updatingView.Title = title } + updatingView.Title = title return nil }) } @@ -178,9 +176,9 @@ func getViewTitle(viewName string) string { // 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)) return "" - } else { - return strings.Split(view.Title, "||")[0] } + return strings.Split(view.Title, "||")[0] + } func popupView(viewName string) { _, err := g.SetCurrentView(viewName) @@ -195,10 +193,27 @@ func popupView(viewName string) { updatingView, err := g.View(viewName) if err != nil { return err - } else { - viewX, viewY := updatingView.Size() - updatingView.MoveCursor(viewX, viewY, true) } + updatingView.MoveCursor(0, 0, true) + + return nil + + }) +} +func moveCursorToEnd(viewName string) { + g.Update(func(g *gocui.Gui) error { + inputView, err := g.View(viewName) + if err != nil { + return err + } + inputString, _ := getInputString(viewName) + stringLen := len(inputString) + maxX, _ := inputView.Size() + x := stringLen % maxX + y := stringLen / maxX + inputView.SetCursor(0, 0) + inputView.SetOrigin(0, 0) + inputView.MoveCursor(x, y, true) return nil }) @@ -208,11 +223,11 @@ func clearView(viewName string) { inputView, err := g.View(viewName) if err != nil { return err - } else { - inputView.Clear() - inputView.SetCursor(0, 0) - inputView.SetOrigin(0, 0) } + inputView.Clear() + inputView.SetCursor(0, 0) + inputView.SetOrigin(0, 0) + return nil }) @@ -222,11 +237,11 @@ func writeToView(viewName string, message string) { updatingView, err := g.View(viewName) if err != nil { return err - } else { - for _, c := range message { - updatingView.EditWrite(c) - } } + for _, c := range message { + updatingView.EditWrite(c) + } + return nil }) } @@ -276,11 +291,11 @@ func populateChat() { if err2 != nil { printToView("Feed", fmt.Sprintf("%+v", err)) return - } else { - go populateChat() - go generateChannelTabCompletionSlice() - return } + go populateChat() + go generateChannelTabCompletionSlice() + return + } var printMe []string var actuallyPrintMe string @@ -366,7 +381,7 @@ func formatOutput(api keybase.ChatAPI) string { 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" { - msg = fmt.Sprintf("%s", colorText("[Attachment]", messageAttachmentColor, c)) + 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)) } user := colorUsername(api.Msg.Sender.Username, c) device := colorText(api.Msg.Sender.DeviceName, messageSenderDeviceColor, c) diff --git a/tabComplete.go b/tabComplete.go index 45451e8..2a4fc4b 100644 --- a/tabComplete.go +++ b/tabComplete.go @@ -20,46 +20,46 @@ func handleTab(viewName string) error { inputString, err := getInputString(viewName) if err != nil { return err + } + // if you successfully get an input string, grab the last word from the string + ss := regexp.MustCompile(`[ #]`).Split(inputString, -1) + s := ss[len(ss)-1] + // create a variable in which to store the result + var resultSlice []string + // if the word starts with a : its an emoji lookup + if strings.HasPrefix(s, ":") { + resultSlice = getEmojiTabCompletionSlice(s) + } else if strings.HasPrefix(s, "/") { + generateCommandTabCompletionSlice() + s = strings.Replace(s, "/", "", 1) + resultSlice = getCommandTabCompletionSlice(s) } else { - // if you successfully get an input string, grab the last word from the string - ss := regexp.MustCompile(`[ #]`).Split(inputString, -1) - s := ss[len(ss)-1] - // create a variable in which to store the result - var resultSlice []string - // if the word starts with a : its an emoji lookup - if strings.HasPrefix(s, ":") { - resultSlice = getEmojiTabCompletionSlice(s) - } else if strings.HasPrefix(s, "/") { - generateCommandTabCompletionSlice() - s = strings.Replace(s, "/", "", 1) - resultSlice = getCommandTabCompletionSlice(s) - } else { - if strings.HasPrefix(s, "@") { - // now in case the word (s) is a mention @something, lets remove it to normalize - s = strings.Replace(s, "@", "", 1) - } - // now call get the list of all possible cantidates that have that as a prefix - resultSlice = getChannelTabCompletionSlice(s) + if strings.HasPrefix(s, "@") { + // now in case the word (s) is a mention @something, lets remove it to normalize + s = strings.Replace(s, "@", "", 1) } - rLen := len(resultSlice) - lcp := longestCommonPrefix(resultSlice) - if lcp != "" { - originalViewTitle := getViewTitle("Input") - newViewTitle := "" - if rLen >= 1 && originalViewTitle != "" { - if rLen == 1 { - newViewTitle = originalViewTitle - } else if rLen <= 5 { - newViewTitle = fmt.Sprintf("%s|| %s", originalViewTitle, strings.Join(resultSlice, " ")) - } else if rLen > 5 { - newViewTitle = fmt.Sprintf("%s|| %s +%d more", originalViewTitle, strings.Join(resultSlice[:6], " "), rLen-5) - } - setViewTitle(viewName, newViewTitle) - remainder := stringRemainder(s, lcp) - writeToView(viewName, remainder) + // now call get the list of all possible cantidates that have that as a prefix + resultSlice = getChannelTabCompletionSlice(s) + } + rLen := len(resultSlice) + lcp := longestCommonPrefix(resultSlice) + if lcp != "" { + originalViewTitle := getViewTitle("Input") + newViewTitle := "" + if rLen >= 1 && originalViewTitle != "" { + if rLen == 1 { + newViewTitle = originalViewTitle + } else if rLen <= 5 { + newViewTitle = fmt.Sprintf("%s|| %s", originalViewTitle, strings.Join(resultSlice, " ")) + } else if rLen > 5 { + newViewTitle = fmt.Sprintf("%s|| %s +%d more", originalViewTitle, strings.Join(resultSlice[:6], " "), rLen-5) } + setViewTitle(viewName, newViewTitle) + remainder := stringRemainder(s, lcp) + writeToView(viewName, remainder) } } + return nil } @@ -124,22 +124,23 @@ func getCurrentChannelMembership() []string { var rs []string if channel.Name != "" { t := k.NewTeam(channel.Name) - if testVar, err := t.MemberList(); err != nil { + testVar, err := t.MemberList() + if err != nil { return rs // then this isn't a team, its a PM or there was an error in the API call - } else { - for _, m := range testVar.Result.Members.Owners { - rs = append(rs, fmt.Sprintf("%+v", m.Username)) - } - for _, m := range testVar.Result.Members.Admins { - rs = append(rs, fmt.Sprintf("%+v", m.Username)) - } - for _, m := range testVar.Result.Members.Writers { - rs = append(rs, fmt.Sprintf("%+v", m.Username)) - } - for _, m := range testVar.Result.Members.Readers { - rs = append(rs, fmt.Sprintf("%+v", m.Username)) - } } + for _, m := range testVar.Result.Members.Owners { + rs = append(rs, fmt.Sprintf("%+v", m.Username)) + } + for _, m := range testVar.Result.Members.Admins { + rs = append(rs, fmt.Sprintf("%+v", m.Username)) + } + for _, m := range testVar.Result.Members.Writers { + rs = append(rs, fmt.Sprintf("%+v", m.Username)) + } + for _, m := range testVar.Result.Members.Readers { + rs = append(rs, fmt.Sprintf("%+v", m.Username)) + } + } return rs }