diff --git a/main.go b/main.go index 050047b..efa5640 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "os" - "regexp" "strings" "time" @@ -52,6 +51,7 @@ func main() { if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { fmt.Printf("%+v", err) } + go generateChannelTabCompletionSlice() } // Gocui basic setup @@ -137,7 +137,7 @@ func initKeybindings() error { } if err := g.SetKeybinding("Input", gocui.KeyTab, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error { - return handleTab() + return handleTab("Input") }); err != nil { return err } @@ -277,6 +277,7 @@ func populateChat() { return } else { go populateChat() + go generateChannelTabCompletionSlice() return } } @@ -338,6 +339,7 @@ func populateList() { } time.Sleep(1 * time.Millisecond) printToView("List", fmt.Sprintf("%s%s%s%s", channelsColor, recentPMs, recentChannels, noColor)) + go generateRecentTabCompletionSlice() } } @@ -382,171 +384,6 @@ func formatOutput(api keybase.ChatAPI) string { // End formatting -// Tab completion -func getCurrentChannelMembership() []string { - var rs []string - if channel.Name != "" { - t := k.NewTeam(channel.Name) - if testVar, err := t.MemberList(); 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)) - } - } - } - return rs -} -func filterStringSlice(ss []string, fv string) []string { - var rs []string - for _, s := range ss { - if strings.HasPrefix(s, fv) { - rs = append(rs, s) - } - } - return rs -} -func longestCommonPrefix(ss []string) string { - // cover the case where the slice has no or one members - switch len(ss) { - case 0: - return "" - case 1: - return ss[0] - } - // all strings are compared by bytes here forward (TBD unicode normalization?) - // establish min, max lenth members of the slice by iterating over the members - min, max := ss[0], ss[0] - for _, s := range ss[1:] { - switch { - case s < min: - min = s - case s > max: - max = s - } - } - // then iterate over the characters from min to max, as soon as chars don't match return - for i := 0; i < len(min) && i < len(max); i++ { - if min[i] != max[i] { - return min[:i] - } - } - // to cover the case where all members are equal, just return one - return min -} -func stringRemainder(aStr, bStr string) string { - var long, short string - //figure out which string is longer - switch { - case len(aStr) < len(bStr): - short = aStr - long = bStr - default: - short = bStr - long = aStr - } - // iterate over the strings using an external iterator so we don't lose the value - i := 0 - for i < len(short) && i < len(long) { - if short[i] != long[i] { - // the strings aren't equal so don't return anything - return "" - } - i++ - } - // return whatever's left of the longer string - return long[i:] -} -func appendIfNotInSlice(ss []string, s string) []string { - for _, element := range ss { - if element == s { - return ss - } - } - return append(ss, s) -} -func generateChannelTabCompletionSlice(inputWord string) []string { - // create a slice to hold the values - var firstSlice []string - // iterate over all the conversation results - for _, s := range channels { - if s.MembersType == keybase.TEAM { - // its a team so add the topic name as a possible tab completion - firstSlice = appendIfNotInSlice(firstSlice, s.TopicName) - firstSlice = appendIfNotInSlice(firstSlice, s.Name) - } else { - // its a user, so clean the name and append the users name as a possible tab completion - firstSlice = appendIfNotInSlice(firstSlice, cleanChannelName(s.Name)) - } - } - // next fetch all members of the current channel and add them to the slice - secondSlice := getCurrentChannelMembership() - for _, m := range secondSlice { - firstSlice = appendIfNotInSlice(firstSlice, m) - } - // now return the resultSlice which contains all that are prefixed with inputWord - resultSlice := filterStringSlice(firstSlice, inputWord) - return resultSlice -} -func generateEmojiTabCompletionSlice(inputWord string) []string { - // use the emojiSlice from emojiList.go and filter it for the input word - resultSlice := filterStringSlice(emojiSlice, inputWord) - return resultSlice -} -func handleTab() error { - inputString, err := getInputString("Input") - if err != nil { - return err - } 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 = generateEmojiTabCompletionSlice(s) - } else { - // now in case the word (s) is a mention @something, lets remove it to normalize - if strings.HasPrefix(s, "@") { - s = strings.Replace(s, "@", "", 1) - } - // now call get the list of all possible cantidates that have that as a prefix - resultSlice = generateChannelTabCompletionSlice(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("Input", newViewTitle) - remainder := stringRemainder(s, lcp) - writeToView("Input", remainder) - } - } - } - return nil -} - -// End tab completion - // Input handling func handleMessage(api keybase.ChatAPI) { if _, ok := typeCommands[api.Msg.Content.Type]; ok { diff --git a/tabComplete.go b/tabComplete.go new file mode 100644 index 0000000..0391c1a --- /dev/null +++ b/tabComplete.go @@ -0,0 +1,211 @@ +// +build !rm_basic_commands allcommands tabcompletion +package main + +import ( + "fmt" + "regexp" + "strings" + + "samhofi.us/x/keybase" +) + +var ( + tabSlice []string + commandSlice []string +) + +// This defines the handleTab function thats called by key bindind tab for the input control. +func handleTab(viewName string) error { + inputString, err := getInputString(viewName) + if err != nil { + return err + } 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) + } + 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 +} + +// 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) + return resultSlice +} +func getChannelTabCompletionSlice(inputWord string) []string { + // use the tabSlice from above and filter it for the input word + resultSlice := filterStringSlice(tabSlice, inputWord) + return resultSlice +} +func getCommandTabCompletionSlice(inputWord string) []string { + // use the commandSlice from above and filter it for the input word + resultSlice := filterStringSlice(commandSlice, inputWord) + return resultSlice +} + +//Generator Functions (should be called externally when chat/list/join changes +func generateChannelTabCompletionSlice() { + // fetch all members of the current channel and add them to the slice + channelSlice := getCurrentChannelMembership() + for _, m := range channelSlice { + tabSlice = appendIfNotInSlice(tabSlice, m) + } +} +func generateCommandTabCompletionSlice() { + // get the maps of all built commands - this should only need to be done on startup + // removing typeCommands for now, since they aren't actually commands you can type - contrary to the naming + /*for commandString1 := range typeCommands { + commandSlice = appendIfNotInSlice(commandSlice, commandString1) + }*/ + for commandString2 := range commands { + commandSlice = appendIfNotInSlice(commandSlice, commandString2) + } + for _, commandString3 := range baseCommands { + commandSlice = appendIfNotInSlice(commandSlice, commandString3) + } +} +func generateRecentTabCompletionSlice() { + var recentSlice []string + for _, s := range channels { + if s.MembersType == keybase.TEAM { + // its a team so add the topic name and channel name + recentSlice = appendIfNotInSlice(recentSlice, s.TopicName) + recentSlice = appendIfNotInSlice(recentSlice, s.Name) + } else { + //its a user, so clean the name and append + recentSlice = appendIfNotInSlice(recentSlice, cleanChannelName(s.Name)) + } + } + for _, s := range recentSlice { + tabSlice = appendIfNotInSlice(tabSlice, s) + } +} + +// Helper functions +func getCurrentChannelMembership() []string { + var rs []string + if channel.Name != "" { + t := k.NewTeam(channel.Name) + if testVar, err := t.MemberList(); 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)) + } + } + } + return rs +} +func filterStringSlice(ss []string, fv string) []string { + var rs []string + for _, s := range ss { + if strings.HasPrefix(s, fv) { + rs = append(rs, s) + } + } + return rs +} +func longestCommonPrefix(ss []string) string { + // cover the case where the slice has no or one members + switch len(ss) { + case 0: + return "" + case 1: + return ss[0] + } + // all strings are compared by bytes here forward (TBD unicode normalization?) + // establish min, max lenth members of the slice by iterating over the members + min, max := ss[0], ss[0] + for _, s := range ss[1:] { + switch { + case s < min: + min = s + case s > max: + max = s + } + } + // then iterate over the characters from min to max, as soon as chars don't match return + for i := 0; i < len(min) && i < len(max); i++ { + if min[i] != max[i] { + return min[:i] + } + } + // to cover the case where all members are equal, just return one + return min +} +func stringRemainder(aStr, bStr string) string { + var long, short string + //figure out which string is longer + switch { + case len(aStr) < len(bStr): + short = aStr + long = bStr + default: + short = bStr + long = aStr + } + // iterate over the strings using an external iterator so we don't lose the value + i := 0 + for i < len(short) && i < len(long) { + if short[i] != long[i] { + // the strings aren't equal so don't return anything + return "" + } + i++ + } + // return whatever's left of the longer string + return long[i:] +} +func appendIfNotInSlice(ss []string, s string) []string { + for _, element := range ss { + if element == s { + return ss + } + } + return append(ss, s) +}