From 5aeb62ef187e4be37d4b72d95ffa42fcd5138ef2 Mon Sep 17 00:00:00 2001 From: David Haukeness Date: Thu, 17 Oct 2019 10:03:04 -0600 Subject: [PATCH 1/5] moved tab completion to separate file, updated handleTab() to new structure. Created new global to hold a copy of the tab completion slice thats always up to date instead of generating each time, hooked completion options updates into view updates and channel join activity. --- main.go | 131 +++------------------------------------------ tabComplete.go | 142 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 125 deletions(-) create mode 100644 tabComplete.go diff --git a/main.go b/main.go index 82cc72b..f090b7e 100644 --- a/main.go +++ b/main.go @@ -52,6 +52,7 @@ func main() { if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { fmt.Printf("%+v", err) } + go generateChannelTabCompletionSlice() } // Gocui basic setup @@ -277,6 +278,7 @@ func populateChat() { return } else { go populateChat() + go generateChannelTabCompletionSlice() return } } @@ -338,6 +340,7 @@ func populateList() { } time.Sleep(1 * time.Millisecond) printToView("List", fmt.Sprintf("%s%s%s%s", channelsColor, recentPMs, recentChannels, noColor)) + go generateRecentTabCompletionSlice() } } @@ -383,126 +386,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 { @@ -515,14 +398,14 @@ func handleTab() error { var resultSlice []string // if the word starts with a : its an emoji lookup if strings.HasPrefix(s, ":") { - resultSlice = generateEmojiTabCompletionSlice(s) + resultSlice = getEmojiTabCompletionSlice(s) } else { - // now in case the word (s) is a mention @something, lets remove it to normalize 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 = generateChannelTabCompletionSlice(s) + resultSlice = getChannelTabCompletionSlice(s) } rLen := len(resultSlice) lcp := longestCommonPrefix(resultSlice) @@ -546,8 +429,6 @@ func handleTab() error { 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..1271064 --- /dev/null +++ b/tabComplete.go @@ -0,0 +1,142 @@ +package main + +import ( + "fmt" + "strings" + + "samhofi.us/x/keybase" +) + +var ( + tabSlice []string +) + +// 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 +} + +//Generator Functions +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 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) +} From 5ed7cb6972a905ec261b1d1f4bafc1dfcb7ca361 Mon Sep 17 00:00:00 2001 From: David Haukeness Date: Thu, 17 Oct 2019 11:00:55 -0600 Subject: [PATCH 2/5] moved all tabcomplete to external file --- main.go | 44 -------------------------------------------- tabComplete.go | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/main.go b/main.go index f090b7e..9074c9f 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "os" - "regexp" "strings" "time" @@ -386,49 +385,6 @@ func formatOutput(api keybase.ChatAPI) string { // End formatting -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 = getEmojiTabCompletionSlice(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("Input", newViewTitle) - remainder := stringRemainder(s, lcp) - writeToView("Input", remainder) - } - } - } - return nil -} - // Input handling func handleMessage(api keybase.ChatAPI) { if _, ok := typeCommands[api.Msg.Content.Type]; ok { diff --git a/tabComplete.go b/tabComplete.go index 1271064..7f784b9 100644 --- a/tabComplete.go +++ b/tabComplete.go @@ -1,7 +1,9 @@ +// +build !rm_basic_commands allcommands tabcompletion package main import ( "fmt" + "regexp" "strings" "samhofi.us/x/keybase" @@ -11,6 +13,50 @@ var ( tabSlice []string ) +// This defines the handleTab function thats called by key bindind tab for the input control. +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 = getEmojiTabCompletionSlice(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("Input", newViewTitle) + remainder := stringRemainder(s, lcp) + writeToView("Input", 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 @@ -23,7 +69,7 @@ func getChannelTabCompletionSlice(inputWord string) []string { return resultSlice } -//Generator Functions +//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() From f7144f3ad5a0150f6399e8702c2152c06430a974 Mon Sep 17 00:00:00 2001 From: David Haukeness Date: Thu, 17 Oct 2019 11:01:16 -0600 Subject: [PATCH 3/5] updated mage for tabComplete option --- mage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mage.go b/mage.go index b2737d8..e6d0824 100644 --- a/mage.go +++ b/mage.go @@ -116,5 +116,5 @@ func BuildAllCommandsT() { // Build kbtui with beta functionality func BuildBeta() { mg.Deps(BuildEmoji) - sh.Run("go", "build", "-tags", "allcommands,showreactionscmd,emojiList") + sh.Run("go", "build", "-tags", "allcommands,showreactionscmd,emojiList,tabcompletion") } From dffff400ff8ada034e6819b04fbf3b5ce807a873 Mon Sep 17 00:00:00 2001 From: David Haukeness Date: Thu, 17 Oct 2019 11:04:51 -0600 Subject: [PATCH 4/5] changed handleTab() to accomodate viewName --- main.go | 2 +- tabComplete.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 9074c9f..1a7fc10 100644 --- a/main.go +++ b/main.go @@ -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 } diff --git a/tabComplete.go b/tabComplete.go index 7f784b9..7bbbb39 100644 --- a/tabComplete.go +++ b/tabComplete.go @@ -14,8 +14,8 @@ var ( ) // This defines the handleTab function thats called by key bindind tab for the input control. -func handleTab() error { - inputString, err := getInputString("Input") +func handleTab(viewName string) error { + inputString, err := getInputString(viewName) if err != nil { return err } else { @@ -48,9 +48,9 @@ func handleTab() error { } else if rLen > 5 { newViewTitle = fmt.Sprintf("%s|| %s +%d more", originalViewTitle, strings.Join(resultSlice[:6], " "), rLen-5) } - setViewTitle("Input", newViewTitle) + setViewTitle(viewName, newViewTitle) remainder := stringRemainder(s, lcp) - writeToView("Input", remainder) + writeToView(viewName, remainder) } } } From 73d9bfdd5dc46b2ec663a8acea59966e7e52833d Mon Sep 17 00:00:00 2001 From: David Haukeness Date: Thu, 17 Oct 2019 13:19:07 -0600 Subject: [PATCH 5/5] added command tab completion --- tabComplete.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tabComplete.go b/tabComplete.go index 7bbbb39..0391c1a 100644 --- a/tabComplete.go +++ b/tabComplete.go @@ -10,7 +10,8 @@ import ( ) var ( - tabSlice []string + tabSlice []string + commandSlice []string ) // This defines the handleTab function thats called by key bindind tab for the input control. @@ -27,6 +28,10 @@ func handleTab(viewName string) error { // 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 @@ -68,6 +73,11 @@ func getChannelTabCompletionSlice(inputWord string) []string { 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() { @@ -77,6 +87,19 @@ func generateChannelTabCompletionSlice() { 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 {