From 8360c7318c31e32950043f8b80e06cc51bc5dde1 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Mon, 27 Apr 2020 18:24:42 +0200 Subject: [PATCH 1/8] show exit status if non-zero --- cmd/cli/main.go | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 9513e75..4d7aecf 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -202,6 +202,14 @@ func highlightMatch(str string) string { return redBold + cleanHighlight(str) + end } +func highlightExitStatus(str string) string { + // template "\033[3%d;%dm" + // orangeBold := "\033[33;1m" + magentaBold := "\033[35;1m" + end := "\033[0m" + return magentaBold + cleanHighlight(str) + end +} + func toString(record records.EnrichedRecord, lineLength int) string { dirColWidth := 24 // make this dynamic somehow return leftCutPadString(strings.Replace(record.Pwd, record.Home, "~", 1), dirColWidth) + " " + @@ -266,6 +274,7 @@ type item struct { pwd string pwdTilde string hits float64 + exitStatus int } func (i item) less(i2 item) bool { @@ -279,6 +288,26 @@ func (i item) key() string { return i.cmdLine + unlikelySeparator + i.pwd } +func (i item) toString(length int) string { + // dots := "…" + if i.exitStatus == 0 { + return i.display + } + exitStStr := "EXIT-STATUS: " + strconv.Itoa(i.exitStatus) + // x := length - len(exitStStr) + exitStStr = highlightExitStatus(exitStStr) + str := i.display + return str + " " + exitStStr + //if len(i.display) < x { + // str := i.display + // // for len(str) < x { + // // str += " " + // // } + // return str + " " + exitStStr + //} + //return i.display[:x] + dots + " " + exitStStr +} + // func (i item) equals(i2 item) bool { // return i.cmdLine == i2.cmdLine && i.pwd == i2.pwd // } @@ -295,7 +324,6 @@ func properMatch(str, term, padChar string) bool { // newItemFromRecordForQuery creates new item from record based on given query // returns error if the query doesn't match the record func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug bool) (item, error) { - // TODO: use color to highlight matches const hitScore = 1.0 const hitScoreConsecutive = 0.1 const properMatchScore = 0.3 @@ -408,6 +436,7 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug pwd: record.Pwd, pwdTilde: pwdTilde, hits: hits, + exitStatus: record.ExitCode, } return it, nil } @@ -571,7 +600,7 @@ func (m manager) Layout(g *gocui.Gui) error { } break } - displayStr := itm.display + displayStr := itm.toString(maxX - 2) if m.s.highlightedItem == i { // use actual min requried length instead of 420 constant displayStr = doHighlightString(displayStr, 420) From 694958b2ebb7da595bd729f57a79dba442045500 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Mon, 27 Apr 2020 21:11:11 +0200 Subject: [PATCH 2/8] align exit status --- cmd/cli/main.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 4d7aecf..5e08dcd 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -296,8 +296,15 @@ func (i item) toString(length int) string { exitStStr := "EXIT-STATUS: " + strconv.Itoa(i.exitStatus) // x := length - len(exitStStr) exitStStr = highlightExitStatus(exitStStr) - str := i.display - return str + " " + exitStStr + + // visually align + spaces := " " // 20 spaces + block := len(spaces) + str := i.display + spaces + x := len(str) / block * block + str = str[:x] + return str + exitStStr + //if len(i.display) < x { // str := i.display // // for len(str) < x { From e981f743bf3f10ccf28c11f25c918f7df87de695 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Wed, 29 Apr 2020 20:10:18 +0200 Subject: [PATCH 3/8] rewrite CLI line rendering to column based, show host --- cmd/cli/main.go | 335 ++++++++++++++++++++++++++------------------- scripts/reshctl.sh | 3 +- scripts/widgets.sh | 3 +- 3 files changed, 201 insertions(+), 140 deletions(-) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 5e08dcd..707464f 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -67,17 +67,26 @@ func runReshCli() (string, int) { } sessionID := flag.String("sessionID", "", "resh generated session id") + host := flag.String("host", "", "host") pwd := flag.String("pwd", "", "present working directory") + gitOriginRemote := flag.String("gitOriginRemote", "DEFAULT", "git origin remote") query := flag.String("query", "", "search query") flag.Parse() if *sessionID == "" { - fmt.Println("Error: you need to specify sessionId") + log.Println("Error: you need to specify sessionId") + } + if *host == "" { + log.Println("Error: you need to specify HOST") } if *pwd == "" { - fmt.Println("Error: you need to specify PWD") + log.Println("Error: you need to specify PWD") + } + if *gitOriginRemote == "DEFAULT" { + log.Println("Error: you need to specify gitOriginRemote") } + log.Printf("gitRemoteOrigin: %s\n", *gitOriginRemote) g, err := gocui.NewGui(gocui.OutputNormal, false) if err != nil { log.Panicln(err) @@ -102,10 +111,12 @@ func runReshCli() (string, int) { } layout := manager{ - sessionID: *sessionID, - pwd: *pwd, - config: config, - s: &st, + sessionID: *sessionID, + host: *host, + pwd: *pwd, + gitOriginRemote: *gitOriginRemote, + config: config, + s: &st, } g.SetManager(layout) @@ -169,13 +180,17 @@ func cleanHighlight(str string) string { invert := "\033[32;7;1m" end := "\033[0m" - blueBold := "\033[34;1m" - redBold := "\033[31;1m" - repace := []string{invert, end, blueBold, redBold} + replace := []string{invert, end} + for i := 30; i < 48; i++ { + base := prefix + strconv.Itoa(i) + normal := base + "m" + bold := base + ";1m" + replace = append(replace, normal, bold) + } if strings.Contains(str, prefix) == false { return str } - for _, escSeq := range repace { + for _, escSeq := range replace { str = strings.ReplaceAll(str, escSeq, "") } return str @@ -188,7 +203,14 @@ func highlightSelected(str string) string { return invert + cleanHighlight(str) + end } -func highlightMatchAlternative(str string) string { +func highlightHost(str string) string { + // template "\033[3%d;%dm" + redNormal := "\033[31m" + end := "\033[0m" + return redNormal + cleanHighlight(str) + end +} + +func highlightPwd(str string) string { // template "\033[3%d;%dm" blueBold := "\033[34;1m" end := "\033[0m" @@ -197,17 +219,24 @@ func highlightMatchAlternative(str string) string { func highlightMatch(str string) string { // template "\033[3%d;%dm" + magentaBold := "\033[35;1m" + end := "\033[0m" + return magentaBold + cleanHighlight(str) + end +} + +func highlightWarn(str string) string { + // template "\033[3%d;%dm" + // orangeBold := "\033[33;1m" redBold := "\033[31;1m" end := "\033[0m" return redBold + cleanHighlight(str) + end } -func highlightExitStatus(str string) string { +func highlightGit(str string) string { // template "\033[3%d;%dm" - // orangeBold := "\033[33;1m" - magentaBold := "\033[35;1m" + greenBold := "\033[32;1m" end := "\033[0m" - return magentaBold + cleanHighlight(str) + end + return greenBold + cleanHighlight(str) + end } func toString(record records.EnrichedRecord, lineLength int) string { @@ -217,8 +246,10 @@ func toString(record records.EnrichedRecord, lineLength int) string { } type query struct { - terms []string - pwd string + terms []string + host string + pwd string + gitOriginRemote string // pwdTilde string } @@ -242,7 +273,7 @@ func filterTerms(terms []string) []string { return newTerms } -func newQueryFromString(queryInput string, pwd string) query { +func newQueryFromString(queryInput string, host string, pwd string, gitOriginRemote string) query { if debug { log.Println("QUERY input = <" + queryInput + ">") } @@ -263,18 +294,33 @@ func newQueryFromString(queryInput string, pwd string) query { log.Println("QUERY filtered terms =" + logStr) log.Println("QUERY pwd =" + pwd) } - return query{terms: terms, pwd: pwd} + return query{ + terms: terms, + host: host, + pwd: pwd, + gitOriginRemote: gitOriginRemote, + } } type item struct { - // record records.EnrichedRecord - display string - displayNoColor string - cmdLine string - pwd string - pwdTilde string - hits float64 - exitStatus int + // dateWithColor string + // date string + + // [host:]pwd + locationWithColor string + location string + + // [G] [E#] + flagsWithColor string + flags string + + cmdLineWithColor string + cmdLine string + + hits float64 + + key string + // cmdLineRaw string } func (i item) less(i2 item) bool { @@ -282,37 +328,28 @@ func (i item) less(i2 item) bool { return i.hits > i2.hits } -// used for deduplication -func (i item) key() string { - unlikelySeparator := "|||||" - return i.cmdLine + unlikelySeparator + i.pwd -} - -func (i item) toString(length int) string { - // dots := "…" - if i.exitStatus == 0 { - return i.display +func (i item) produceLine(flagLength int) (string, int) { + line := "" + line += i.locationWithColor + line += i.flagsWithColor + flags := i.flags + if flagLength < len(i.flags) { + log.Printf("produceLine can't specify line w/ flags shorter than the actual size. - len(flags) %v, requested %v\n", len(i.flags), flagLength) } - exitStStr := "EXIT-STATUS: " + strconv.Itoa(i.exitStatus) - // x := length - len(exitStStr) - exitStStr = highlightExitStatus(exitStStr) - - // visually align - spaces := " " // 20 spaces - block := len(spaces) - str := i.display + spaces - x := len(str) / block * block - str = str[:x] - return str + exitStStr + for len(flags) < flagLength { + line += " " + flags += " " + } + spacer := " " + if flagLength > 5 { + // use shorter spacer + // because there is likely a long flag like E130 in the view + spacer = " " + } + line += spacer + i.cmdLineWithColor - //if len(i.display) < x { - // str := i.display - // // for len(str) < x { - // // str += " " - // // } - // return str + " " + exitStStr - //} - //return i.display[:x] + dots + " " + exitStStr + length := len(i.location) + flagLength + len(spacer) + len(i.cmdLine) + return line, length } // func (i item) equals(i2 item) bool { @@ -335,22 +372,20 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug const hitScoreConsecutive = 0.1 const properMatchScore = 0.3 const actualPwdScore = 0.9 - const actualPwdScoreExtra = 0.2 // this + hitScore > actualPwdScore - const nonZeroExitCodeScorePenalty = 0.8 // this < min(hitScore, actualPwdScore) + const nonZeroExitCodeScorePenalty = 0.5 + const sameGitRepoScore = 0.7 + // const sameGitRepoScoreExtra = 0.0 + const differentHostScorePenalty = 0.2 + + // nonZeroExitCodeScorePenalty + differentHostScorePenalty hits := 0.0 - if record.ExitCode != 0 { - hits -= nonZeroExitCodeScorePenalty - } + anyHit := false cmd := record.CmdLine - pwdTilde := strings.Replace(record.Pwd, record.Home, "~", 1) - pwdDisp := leftCutPadString(pwdTilde, 25) - pwdRawDisp := leftCutPadString(record.Pwd, 25) - var useRawPwd bool - var dirHit bool for _, term := range query.terms { termHit := false if strings.Contains(record.CmdLine, term) { + anyHit = true if termHit == false { hits += hitScore } else { @@ -363,87 +398,99 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug cmd = strings.ReplaceAll(cmd, term, highlightMatch(term)) // NO continue } - if strings.Contains(pwdTilde, term) { - if termHit == false { - hits += hitScore - } else { - hits += hitScoreConsecutive - } - termHit = true - if properMatch(pwdTilde, term, "/") { - hits += properMatchScore - } - pwdDisp = strings.ReplaceAll(pwdDisp, term, highlightMatch(term)) - pwdRawDisp = strings.ReplaceAll(pwdRawDisp, term, highlightMatch(term)) - dirHit = true - } else if strings.Contains(record.Pwd, term) { - if termHit == false { - hits += hitScore - } else { - hits += hitScoreConsecutive - } - termHit = true - if properMatch(pwdTilde, term, "/") { - hits += properMatchScore - } - pwdRawDisp = strings.ReplaceAll(pwdRawDisp, term, highlightMatch(term)) - dirHit = true - useRawPwd = true - } - // if strings.Contains(record.GitOriginRemote, term) { - // hits++ - // } } // actual pwd matches - // only use if there was no directory match on any of the terms // N terms can only produce: // -> N matches against the command - // -> N matches against the directory // -> 1 extra match for the actual directory match + sameGitRepo := false + if query.gitOriginRemote != "" && query.gitOriginRemote == record.GitOriginRemote { + sameGitRepo = true + } + + samePwd := false if record.Pwd == query.pwd { - if dirHit { - hits += actualPwdScoreExtra - } else { - hits += actualPwdScore - } - pwdDisp = highlightMatchAlternative(pwdDisp) - // pwdRawDisp = highlightMatchAlternative(pwdRawDisp) - useRawPwd = false + anyHit = true + samePwd = true + hits += actualPwdScore + } else if sameGitRepo { + anyHit = true + hits += sameGitRepoScore } - if hits <= 0 { + + differentHost := false + if record.Host != query.host { + differentHost = true + hits -= differentHostScorePenalty + } + errorExitStatus := false + if record.ExitCode != 0 { + errorExitStatus = true + hits -= nonZeroExitCodeScorePenalty + } + if hits <= 0 && !anyHit { return item{}, errors.New("no match for given record and query") } - display := "" - // pwd := leftCutPadString("<"+pwdTilde+">", 20) - if useRawPwd { - display += pwdRawDisp + + // KEY for deduplication + + unlikelySeparator := "|||||" + key := record.CmdLine + unlikelySeparator + record.Pwd + + unlikelySeparator + strconv.Itoa(record.ExitCode) + unlikelySeparator + + record.GitOriginRemote + unlikelySeparator + record.Host + + // DISPLAY + // DISPLAY > date + // TODO + + // DISPLAY > location + location := "" + locationWithColor := "" + if differentHost { + location += record.Host + ":" + locationWithColor += highlightHost(record.Host) + ":" + } + const locationLenght = 30 + pwdLength := locationLenght - len(location) + pwdTilde := strings.Replace(record.Pwd, record.Home, "~", 1) + location += leftCutPadString(pwdTilde, pwdLength) + if samePwd { + locationWithColor += highlightPwd(leftCutPadString(pwdTilde, pwdLength)) } else { - display += pwdDisp + locationWithColor += leftCutPadString(pwdTilde, pwdLength) } + + // DISPLAY > flags + flags := "" + flagsWithColor := "" if debug { hitsStr := fmt.Sprintf("%.1f", hits) - hitsDisp := " " + hitsStr + " " - display += hitsDisp - } else { - display += " " + flags += " S" + hitsStr + } + if sameGitRepo { + flags += " G" + flagsWithColor += " " + highlightGit("G") + } + if errorExitStatus { + flags += " E" + strconv.Itoa(record.ExitCode) + flagsWithColor += " " + highlightWarn("E"+strconv.Itoa(record.ExitCode)) } + + // DISPLAY > cmdline + // cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">" - cmd = strings.ReplaceAll(cmd, "\n", ";") - display += cmd - // itDummy := item{ - // cmdLine: record.CmdLine, - // pwd: record.Pwd, - // } - // + " #K:<" + itDummy.key() + ">" + cmdLine := strings.ReplaceAll(record.CmdLine, "\n", ";") + cmdLineWithColor := strings.ReplaceAll(cmd, "\n", ";") it := item{ - display: display, - displayNoColor: display, - cmdLine: record.CmdLine, - pwd: record.Pwd, - pwdTilde: pwdTilde, - hits: hits, - exitStatus: record.ExitCode, + location: location, + locationWithColor: locationWithColor, + flags: flags, + flagsWithColor: flagsWithColor, + cmdLine: cmdLine, + cmdLineWithColor: cmdLineWithColor, + hits: hits, + key: key, } return it, nil } @@ -468,9 +515,11 @@ type state struct { } type manager struct { - sessionID string - pwd string - config cfg.Config + sessionID string + host string + pwd string + gitOriginRemote string + config cfg.Config s *state } @@ -503,7 +552,7 @@ func (m manager) UpdateData(input string) { log.Println("len(fullRecords) =", len(m.s.fullRecords)) log.Println("len(data) =", len(m.s.data)) } - query := newQueryFromString(input, m.pwd) + query := newQueryFromString(input, m.host, m.pwd, m.gitOriginRemote) var data []item itemSet := make(map[string]bool) m.s.lock.Lock() @@ -515,11 +564,11 @@ func (m manager) UpdateData(input string) { // log.Println(" * continue (no match)", rec.Pwd) continue } - if itemSet[itm.key()] { + if itemSet[itm.key] { // log.Println(" * continue (already present)", itm.key(), itm.pwd) continue } - itemSet[itm.key()] = true + itemSet[itm.key] = true data = append(data, itm) // log.Println("DATA =", itm.display) } @@ -600,6 +649,16 @@ func (m manager) Layout(g *gocui.Gui) error { v.Clear() v.Rewind() + longestFlagsLen := 2 // at least 2 + for i, itm := range m.s.data { + if i == maxY { + break + } + if len(itm.flags) > longestFlagsLen { + longestFlagsLen = len(itm.flags) + } + } + for i, itm := range m.s.data { if i == maxY { if debug { @@ -607,10 +666,10 @@ func (m manager) Layout(g *gocui.Gui) error { } break } - displayStr := itm.toString(maxX - 2) + displayStr, _ := itm.produceLine(longestFlagsLen) if m.s.highlightedItem == i { // use actual min requried length instead of 420 constant - displayStr = doHighlightString(displayStr, 420) + displayStr = doHighlightString(displayStr, maxX*2) if debug { log.Println("### HightlightedItem string :", displayStr) } diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index ef4809e..750fc6d 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -108,7 +108,8 @@ __resh_unbind_all() { # wrapper for resh-cli for calling resh directly resh() { local buffer - buffer=$(resh-cli --sessionID "$__RESH_SESSION_ID" --pwd "$PWD") + local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)" + buffer=$(resh-cli --sessionID "$__RESH_SESSION_ID" --host "$HOST" --pwd "$PWD" --gitOriginRemote "$git_remote") status_code=$? if [ $status_code = 111 ]; then # execute diff --git a/scripts/widgets.sh b/scripts/widgets.sh index 79e2cd8..b66cd31 100644 --- a/scripts/widgets.sh +++ b/scripts/widgets.sh @@ -100,7 +100,8 @@ __resh_widget_control_R() { __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS|||control_R:$BUFFER" local status_code - BUFFER=$(resh-cli --sessionID "$__RESH_SESSION_ID" --pwd "$PWD" --query "$BUFFER") + local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)" + BUFFER=$(resh-cli --sessionID "$__RESH_SESSION_ID" --host "$HOST" --pwd "$PWD" --gitOriginRemote "$git_remote" --query "$BUFFER") status_code=$? if [ $status_code = 111 ]; then # execute From f18b3afa5c64f017b55dba837d23d8b9ba5aba1c Mon Sep 17 00:00:00 2001 From: Simon Let Date: Wed, 29 Apr 2020 22:03:09 +0200 Subject: [PATCH 4/8] normalize git remotes --- pkg/records/records.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/records/records.go b/pkg/records/records.go index 15d5ee5..b9914ad 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -186,6 +186,7 @@ func Enriched(r Record) EnrichedRecord { // log.Println("Invalid command:", rec) record.Invalid = true } + record.GitOriginRemote = NormalizeGitRemote(record.GitOriginRemote) return record // TODO: Detect and mark simple commands r.Simple } @@ -327,6 +328,14 @@ func GetCommandAndFirstWord(cmdLine string) (string, string, error) { return "ERROR", "ERROR", errors.New("this should not happen - contact developer ;)") } +// NormalizeGitRomote func +func NormalizeGitRemote(gitRemote string) string { + if strings.HasSuffix(gitRemote, ".git") { + return gitRemote[:len(gitRemote)-4] + } + return gitRemote +} + // DistParams is used to supply params to Enrichedrecords.DistanceTo() type DistParams struct { ExitCode float64 From 282f3bd759d87e297779842810ccfba5447d06b5 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Wed, 29 Apr 2020 23:05:18 +0200 Subject: [PATCH 5/8] normalize git remote in cli, fix typo --- cmd/cli/main.go | 3 +-- pkg/records/records.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 707464f..b50e431 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -86,7 +86,6 @@ func runReshCli() (string, int) { log.Println("Error: you need to specify gitOriginRemote") } - log.Printf("gitRemoteOrigin: %s\n", *gitOriginRemote) g, err := gocui.NewGui(gocui.OutputNormal, false) if err != nil { log.Panicln(err) @@ -114,7 +113,7 @@ func runReshCli() (string, int) { sessionID: *sessionID, host: *host, pwd: *pwd, - gitOriginRemote: *gitOriginRemote, + gitOriginRemote: records.NormalizeGitRemote(*gitOriginRemote), config: config, s: &st, } diff --git a/pkg/records/records.go b/pkg/records/records.go index b9914ad..b2ef7e6 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -328,7 +328,7 @@ func GetCommandAndFirstWord(cmdLine string) (string, string, error) { return "ERROR", "ERROR", errors.New("this should not happen - contact developer ;)") } -// NormalizeGitRomote func +// NormalizeGitRemote func func NormalizeGitRemote(gitRemote string) string { if strings.HasSuffix(gitRemote, ".git") { return gitRemote[:len(gitRemote)-4] From 78d9d5148413942e0fa3eb827e5781d068873e34 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 30 Apr 2020 01:29:16 +0200 Subject: [PATCH 6/8] add raw mode --- cmd/cli/highlight.go | 79 +++++++ cmd/cli/item.go | 278 ++++++++++++++++++++++ cmd/cli/main.go | 540 +++++++++++++------------------------------ cmd/cli/query.go | 83 +++++++ 4 files changed, 596 insertions(+), 384 deletions(-) create mode 100644 cmd/cli/highlight.go create mode 100644 cmd/cli/item.go create mode 100644 cmd/cli/query.go diff --git a/cmd/cli/highlight.go b/cmd/cli/highlight.go new file mode 100644 index 0000000..775d782 --- /dev/null +++ b/cmd/cli/highlight.go @@ -0,0 +1,79 @@ +package main + +import ( + "strconv" + "strings" +) + +func cleanHighlight(str string) string { + prefix := "\033[" + + invert := "\033[7;1m" + invertGreen := "\033[32;7;1m" + end := "\033[0m" + replace := []string{invert, invertGreen, end} + for i := 30; i < 48; i++ { + base := prefix + strconv.Itoa(i) + normal := base + "m" + bold := base + ";1m" + replace = append(replace, normal, bold) + } + if strings.Contains(str, prefix) == false { + return str + } + for _, escSeq := range replace { + str = strings.ReplaceAll(str, escSeq, "") + } + return str +} + +func highlightSelected(str string) string { + // template "\033[3%d;%dm" + // invertGreen := "\033[32;7;1m" + invert := "\033[7;1m" + end := "\033[0m" + return invert + cleanHighlight(str) + end +} + +func highlightHost(str string) string { + // template "\033[3%d;%dm" + redNormal := "\033[31m" + end := "\033[0m" + return redNormal + cleanHighlight(str) + end +} + +func highlightPwd(str string) string { + // template "\033[3%d;%dm" + blueBold := "\033[34;1m" + end := "\033[0m" + return blueBold + cleanHighlight(str) + end +} + +func highlightMatch(str string) string { + // template "\033[3%d;%dm" + magentaBold := "\033[35;1m" + end := "\033[0m" + return magentaBold + cleanHighlight(str) + end +} + +func highlightWarn(str string) string { + // template "\033[3%d;%dm" + // orangeBold := "\033[33;1m" + redBold := "\033[31;1m" + end := "\033[0m" + return redBold + cleanHighlight(str) + end +} + +func highlightGit(str string) string { + // template "\033[3%d;%dm" + greenBold := "\033[32;1m" + end := "\033[0m" + return greenBold + cleanHighlight(str) + end +} + +func doHighlightString(str string, minLength int) string { + if len(str) < minLength { + str = str + strings.Repeat(" ", minLength-len(str)) + } + return highlightSelected(str) +} diff --git a/cmd/cli/item.go b/cmd/cli/item.go new file mode 100644 index 0000000..c3e11bb --- /dev/null +++ b/cmd/cli/item.go @@ -0,0 +1,278 @@ +package main + +import ( + "errors" + "fmt" + "log" + "strconv" + "strings" + + "github.com/curusarn/resh/pkg/records" +) + +type item struct { + // dateWithColor string + // date string + + // [host:]pwd + locationWithColor string + location string + + // [G] [E#] + flagsWithColor string + flags string + + cmdLineWithColor string + cmdLine string + + hits float64 + + key string + // cmdLineRaw string +} + +func (i item) less(i2 item) bool { + // reversed order + return i.hits > i2.hits +} + +func (i item) produceLine(flagLength int) (string, int) { + line := "" + line += i.locationWithColor + line += i.flagsWithColor + flags := i.flags + if flagLength < len(i.flags) { + log.Printf("produceLine can't specify line w/ flags shorter than the actual size. - len(flags) %v, requested %v\n", len(i.flags), flagLength) + } + for len(flags) < flagLength { + line += " " + flags += " " + } + spacer := " " + if flagLength > 5 { + // use shorter spacer + // because there is likely a long flag like E130 in the view + spacer = " " + } + line += spacer + i.cmdLineWithColor + + length := len(i.location) + flagLength + len(spacer) + len(i.cmdLine) + return line, length +} + +func leftCutPadString(str string, newLen int) string { + dots := "…" + strLen := len(str) + if newLen > strLen { + return strings.Repeat(" ", newLen-strLen) + str + } else if newLen < strLen { + return dots + str[strLen-newLen+1:] + } + return str +} + +func rightCutPadString(str string, newLen int) string { + dots := "…" + strLen := len(str) + if newLen > strLen { + return str + strings.Repeat(" ", newLen-strLen) + } else if newLen < strLen { + return str[:newLen-1] + dots + } + return str +} + +// proper match for path is when whole directory is matched +// proper match for command is when term matches word delimeted by whitespace +func properMatch(str, term, padChar string) bool { + if strings.Contains(padChar+str+padChar, padChar+term+padChar) { + return true + } + return false +} + +// newItemFromRecordForQuery creates new item from record based on given query +// returns error if the query doesn't match the record +func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug bool) (item, error) { + const hitScore = 1.0 + const hitScoreConsecutive = 0.1 + const properMatchScore = 0.3 + const actualPwdScore = 0.9 + const nonZeroExitCodeScorePenalty = 0.5 + const sameGitRepoScore = 0.7 + // const sameGitRepoScoreExtra = 0.0 + const differentHostScorePenalty = 0.2 + + // nonZeroExitCodeScorePenalty + differentHostScorePenalty + + hits := 0.0 + anyHit := false + cmd := record.CmdLine + for _, term := range query.terms { + termHit := false + if strings.Contains(record.CmdLine, term) { + anyHit = true + if termHit == false { + hits += hitScore + } else { + hits += hitScoreConsecutive + } + termHit = true + if properMatch(cmd, term, " ") { + hits += properMatchScore + } + cmd = strings.ReplaceAll(cmd, term, highlightMatch(term)) + // NO continue + } + } + // actual pwd matches + // N terms can only produce: + // -> N matches against the command + // -> 1 extra match for the actual directory match + sameGitRepo := false + if query.gitOriginRemote != "" && query.gitOriginRemote == record.GitOriginRemote { + sameGitRepo = true + } + + samePwd := false + if record.Pwd == query.pwd { + anyHit = true + samePwd = true + hits += actualPwdScore + } else if sameGitRepo { + anyHit = true + hits += sameGitRepoScore + } + + differentHost := false + if record.Host != query.host { + differentHost = true + hits -= differentHostScorePenalty + } + errorExitStatus := false + if record.ExitCode != 0 { + errorExitStatus = true + hits -= nonZeroExitCodeScorePenalty + } + if hits <= 0 && !anyHit { + return item{}, errors.New("no match for given record and query") + } + + // KEY for deduplication + + unlikelySeparator := "|||||" + key := record.CmdLine + unlikelySeparator + record.Pwd + + unlikelySeparator + strconv.Itoa(record.ExitCode) + unlikelySeparator + + record.GitOriginRemote + unlikelySeparator + record.Host + + // DISPLAY + // DISPLAY > date + // TODO + + // DISPLAY > location + location := "" + locationWithColor := "" + if differentHost { + location += record.Host + ":" + locationWithColor += highlightHost(record.Host) + ":" + } + const locationLenght = 30 + pwdLength := locationLenght - len(location) + pwdTilde := strings.Replace(record.Pwd, record.Home, "~", 1) + location += leftCutPadString(pwdTilde, pwdLength) + if samePwd { + locationWithColor += highlightPwd(leftCutPadString(pwdTilde, pwdLength)) + } else { + locationWithColor += leftCutPadString(pwdTilde, pwdLength) + } + + // DISPLAY > flags + flags := "" + flagsWithColor := "" + if debug { + hitsStr := fmt.Sprintf("%.1f", hits) + flags += " S" + hitsStr + } + if sameGitRepo { + flags += " G" + flagsWithColor += " " + highlightGit("G") + } + if errorExitStatus { + flags += " E" + strconv.Itoa(record.ExitCode) + flagsWithColor += " " + highlightWarn("E"+strconv.Itoa(record.ExitCode)) + } + + // DISPLAY > cmdline + + // cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">" + cmdLine := strings.ReplaceAll(record.CmdLine, "\n", ";") + cmdLineWithColor := strings.ReplaceAll(cmd, "\n", ";") + + it := item{ + location: location, + locationWithColor: locationWithColor, + flags: flags, + flagsWithColor: flagsWithColor, + cmdLine: cmdLine, + cmdLineWithColor: cmdLineWithColor, + hits: hits, + key: key, + } + return it, nil +} + +type rawItem struct { + cmdLineWithColor string + cmdLine string + + hits float64 + + key string + // cmdLineRaw string +} + +// newRawItemFromRecordForQuery creates new item from record based on given query +// returns error if the query doesn't match the record +func newRawItemFromRecordForQuery(record records.EnrichedRecord, terms []string, debug bool) (rawItem, error) { + const hitScore = 1.0 + const hitScoreConsecutive = 0.1 + const properMatchScore = 0.3 + + hits := 0.0 + anyHit := false + cmd := record.CmdLine + for _, term := range terms { + termHit := false + if strings.Contains(record.CmdLine, term) { + anyHit = true + if termHit == false { + hits += hitScore + } else { + hits += hitScoreConsecutive + } + termHit = true + if properMatch(cmd, term, " ") { + hits += properMatchScore + } + cmd = strings.ReplaceAll(cmd, term, highlightMatch(term)) + // NO continue + } + } + _ = anyHit + // KEY for deduplication + key := record.CmdLine + + // DISPLAY > cmdline + + // cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">" + cmdLine := strings.ReplaceAll(record.CmdLine, "\n", ";") + cmdLineWithColor := strings.ReplaceAll(cmd, "\n", ";") + + it := rawItem{ + cmdLine: cmdLine, + cmdLineWithColor: cmdLineWithColor, + hits: hits, + key: key, + } + return it, nil +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index b50e431..8d789ce 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -3,7 +3,6 @@ package main import ( "bytes" "encoding/json" - "errors" "flag" "fmt" "io/ioutil" @@ -143,8 +142,12 @@ func runReshCli() (string, int) { if err := g.SetKeybinding("", gocui.KeyArrowRight, gocui.ModNone, layout.SelectPaste); err != nil { log.Panicln(err) } + if err := g.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, layout.SwitchModes); err != nil { + log.Panicln(err) + } layout.UpdateData(*query) + layout.UpdateRawData(*query) err = g.MainLoop() if err != nil && gocui.IsQuit(err) == false { log.Panicln(err) @@ -152,361 +155,15 @@ func runReshCli() (string, int) { return layout.s.output, layout.s.exitCode } -func leftCutPadString(str string, newLen int) string { - dots := "…" - strLen := len(str) - if newLen > strLen { - return strings.Repeat(" ", newLen-strLen) + str - } else if newLen < strLen { - return dots + str[strLen-newLen+1:] - } - return str -} - -func rightCutPadString(str string, newLen int) string { - dots := "…" - strLen := len(str) - if newLen > strLen { - return str + strings.Repeat(" ", newLen-strLen) - } else if newLen < strLen { - return str[:newLen-1] + dots - } - return str -} - -func cleanHighlight(str string) string { - prefix := "\033[" - - invert := "\033[32;7;1m" - end := "\033[0m" - replace := []string{invert, end} - for i := 30; i < 48; i++ { - base := prefix + strconv.Itoa(i) - normal := base + "m" - bold := base + ";1m" - replace = append(replace, normal, bold) - } - if strings.Contains(str, prefix) == false { - return str - } - for _, escSeq := range replace { - str = strings.ReplaceAll(str, escSeq, "") - } - return str -} - -func highlightSelected(str string) string { - // template "\033[3%d;%dm" - invert := "\033[32;7;1m" - end := "\033[0m" - return invert + cleanHighlight(str) + end -} - -func highlightHost(str string) string { - // template "\033[3%d;%dm" - redNormal := "\033[31m" - end := "\033[0m" - return redNormal + cleanHighlight(str) + end -} - -func highlightPwd(str string) string { - // template "\033[3%d;%dm" - blueBold := "\033[34;1m" - end := "\033[0m" - return blueBold + cleanHighlight(str) + end -} - -func highlightMatch(str string) string { - // template "\033[3%d;%dm" - magentaBold := "\033[35;1m" - end := "\033[0m" - return magentaBold + cleanHighlight(str) + end -} - -func highlightWarn(str string) string { - // template "\033[3%d;%dm" - // orangeBold := "\033[33;1m" - redBold := "\033[31;1m" - end := "\033[0m" - return redBold + cleanHighlight(str) + end -} - -func highlightGit(str string) string { - // template "\033[3%d;%dm" - greenBold := "\033[32;1m" - end := "\033[0m" - return greenBold + cleanHighlight(str) + end -} - -func toString(record records.EnrichedRecord, lineLength int) string { - dirColWidth := 24 // make this dynamic somehow - return leftCutPadString(strings.Replace(record.Pwd, record.Home, "~", 1), dirColWidth) + " " + - rightCutPadString(strings.ReplaceAll(record.CmdLine, "\n", "; "), lineLength-dirColWidth-3) + "\n" -} - -type query struct { - terms []string - host string - pwd string - gitOriginRemote string - // pwdTilde string -} - -func isValidTerm(term string) bool { - if len(term) == 0 { - return false - } - if strings.Contains(term, " ") { - return false - } - return true -} - -func filterTerms(terms []string) []string { - var newTerms []string - for _, term := range terms { - if isValidTerm(term) { - newTerms = append(newTerms, term) - } - } - return newTerms -} - -func newQueryFromString(queryInput string, host string, pwd string, gitOriginRemote string) query { - if debug { - log.Println("QUERY input = <" + queryInput + ">") - } - terms := strings.Fields(queryInput) - var logStr string - for _, term := range terms { - logStr += " <" + term + ">" - } - if debug { - log.Println("QUERY raw terms =" + logStr) - } - terms = filterTerms(terms) - logStr = "" - for _, term := range terms { - logStr += " <" + term + ">" - } - if debug { - log.Println("QUERY filtered terms =" + logStr) - log.Println("QUERY pwd =" + pwd) - } - return query{ - terms: terms, - host: host, - pwd: pwd, - gitOriginRemote: gitOriginRemote, - } -} - -type item struct { - // dateWithColor string - // date string - - // [host:]pwd - locationWithColor string - location string - - // [G] [E#] - flagsWithColor string - flags string - - cmdLineWithColor string - cmdLine string - - hits float64 - - key string - // cmdLineRaw string -} - -func (i item) less(i2 item) bool { - // reversed order - return i.hits > i2.hits -} - -func (i item) produceLine(flagLength int) (string, int) { - line := "" - line += i.locationWithColor - line += i.flagsWithColor - flags := i.flags - if flagLength < len(i.flags) { - log.Printf("produceLine can't specify line w/ flags shorter than the actual size. - len(flags) %v, requested %v\n", len(i.flags), flagLength) - } - for len(flags) < flagLength { - line += " " - flags += " " - } - spacer := " " - if flagLength > 5 { - // use shorter spacer - // because there is likely a long flag like E130 in the view - spacer = " " - } - line += spacer + i.cmdLineWithColor - - length := len(i.location) + flagLength + len(spacer) + len(i.cmdLine) - return line, length -} - -// func (i item) equals(i2 item) bool { -// return i.cmdLine == i2.cmdLine && i.pwd == i2.pwd -// } - -// proper match for path is when whole directory is matched -// proper match for command is when term matches word delimeted by whitespace -func properMatch(str, term, padChar string) bool { - if strings.Contains(padChar+str+padChar, padChar+term+padChar) { - return true - } - return false -} - -// newItemFromRecordForQuery creates new item from record based on given query -// returns error if the query doesn't match the record -func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug bool) (item, error) { - const hitScore = 1.0 - const hitScoreConsecutive = 0.1 - const properMatchScore = 0.3 - const actualPwdScore = 0.9 - const nonZeroExitCodeScorePenalty = 0.5 - const sameGitRepoScore = 0.7 - // const sameGitRepoScoreExtra = 0.0 - const differentHostScorePenalty = 0.2 - - // nonZeroExitCodeScorePenalty + differentHostScorePenalty - - hits := 0.0 - anyHit := false - cmd := record.CmdLine - for _, term := range query.terms { - termHit := false - if strings.Contains(record.CmdLine, term) { - anyHit = true - if termHit == false { - hits += hitScore - } else { - hits += hitScoreConsecutive - } - termHit = true - if properMatch(cmd, term, " ") { - hits += properMatchScore - } - cmd = strings.ReplaceAll(cmd, term, highlightMatch(term)) - // NO continue - } - } - // actual pwd matches - // N terms can only produce: - // -> N matches against the command - // -> 1 extra match for the actual directory match - sameGitRepo := false - if query.gitOriginRemote != "" && query.gitOriginRemote == record.GitOriginRemote { - sameGitRepo = true - } - - samePwd := false - if record.Pwd == query.pwd { - anyHit = true - samePwd = true - hits += actualPwdScore - } else if sameGitRepo { - anyHit = true - hits += sameGitRepoScore - } - - differentHost := false - if record.Host != query.host { - differentHost = true - hits -= differentHostScorePenalty - } - errorExitStatus := false - if record.ExitCode != 0 { - errorExitStatus = true - hits -= nonZeroExitCodeScorePenalty - } - if hits <= 0 && !anyHit { - return item{}, errors.New("no match for given record and query") - } - - // KEY for deduplication - - unlikelySeparator := "|||||" - key := record.CmdLine + unlikelySeparator + record.Pwd + - unlikelySeparator + strconv.Itoa(record.ExitCode) + unlikelySeparator + - record.GitOriginRemote + unlikelySeparator + record.Host - - // DISPLAY - // DISPLAY > date - // TODO - - // DISPLAY > location - location := "" - locationWithColor := "" - if differentHost { - location += record.Host + ":" - locationWithColor += highlightHost(record.Host) + ":" - } - const locationLenght = 30 - pwdLength := locationLenght - len(location) - pwdTilde := strings.Replace(record.Pwd, record.Home, "~", 1) - location += leftCutPadString(pwdTilde, pwdLength) - if samePwd { - locationWithColor += highlightPwd(leftCutPadString(pwdTilde, pwdLength)) - } else { - locationWithColor += leftCutPadString(pwdTilde, pwdLength) - } - - // DISPLAY > flags - flags := "" - flagsWithColor := "" - if debug { - hitsStr := fmt.Sprintf("%.1f", hits) - flags += " S" + hitsStr - } - if sameGitRepo { - flags += " G" - flagsWithColor += " " + highlightGit("G") - } - if errorExitStatus { - flags += " E" + strconv.Itoa(record.ExitCode) - flagsWithColor += " " + highlightWarn("E"+strconv.Itoa(record.ExitCode)) - } - - // DISPLAY > cmdline - - // cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">" - cmdLine := strings.ReplaceAll(record.CmdLine, "\n", ";") - cmdLineWithColor := strings.ReplaceAll(cmd, "\n", ";") - - it := item{ - location: location, - locationWithColor: locationWithColor, - flags: flags, - flagsWithColor: flagsWithColor, - cmdLine: cmdLine, - cmdLineWithColor: cmdLineWithColor, - hits: hits, - key: key, - } - return it, nil -} - -func doHighlightString(str string, minLength int) string { - if len(str) < minLength { - str = str + strings.Repeat(" ", minLength-len(str)) - } - return highlightSelected(str) -} - type state struct { lock sync.Mutex fullRecords []records.EnrichedRecord data []item + rawData []rawItem highlightedItem int + rawMode bool + initialQuery string output string @@ -592,8 +249,58 @@ func (m manager) UpdateData(input string) { } } +func (m manager) UpdateRawData(input string) { + if debug { + log.Println("EDIT start") + log.Println("len(fullRecords) =", len(m.s.fullRecords)) + log.Println("len(data) =", len(m.s.data)) + } + query := getRawTermsFromString(input) + var data []rawItem + itemSet := make(map[string]bool) + m.s.lock.Lock() + defer m.s.lock.Unlock() + for _, rec := range m.s.fullRecords { + itm, err := newRawItemFromRecordForQuery(rec, query, m.config.Debug) + if err != nil { + // records didn't match the query + // log.Println(" * continue (no match)", rec.Pwd) + continue + } + if itemSet[itm.key] { + // log.Println(" * continue (already present)", itm.key(), itm.pwd) + continue + } + itemSet[itm.key] = true + data = append(data, itm) + // log.Println("DATA =", itm.display) + } + if debug { + log.Println("len(tmpdata) =", len(data)) + } + sort.SliceStable(data, func(p, q int) bool { + return data[p].hits > data[q].hits + }) + m.s.rawData = nil + for _, itm := range data { + if len(m.s.rawData) > 420 { + break + } + m.s.rawData = append(m.s.rawData, itm) + } + m.s.highlightedItem = 0 + if debug { + log.Println("len(fullRecords) =", len(m.s.fullRecords)) + log.Println("len(data) =", len(m.s.data)) + log.Println("EDIT end") + } +} func (m manager) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { gocui.DefaultEditor.Edit(v, key, ch, mod) + if m.s.rawMode { + m.UpdateRawData(v.Buffer()) + return + } m.UpdateData(v.Buffer()) } @@ -616,6 +323,19 @@ func (m manager) Prev(g *gocui.Gui, v *gocui.View) error { return nil } +func (m manager) SwitchModes(g *gocui.Gui, v *gocui.View) error { + m.s.lock.Lock() + m.s.rawMode = !m.s.rawMode + m.s.lock.Unlock() + + if m.s.rawMode { + m.UpdateRawData(v.Buffer()) + return nil + } + m.UpdateData(v.Buffer()) + return nil +} + func (m manager) Layout(g *gocui.Gui) error { var b byte maxX, maxY := g.Size() @@ -627,7 +347,11 @@ func (m manager) Layout(g *gocui.Gui) error { v.Editable = true v.Editor = m - v.Title = "resh cli" + if m.s.rawMode { + v.Title = " RESH CLI - NON-CONTEXTUAL \"RAW\" MODE " + } else { + v.Title = " RESH CLI - CONTEXTUAL MODE " + } g.SetCurrentView("input") @@ -648,6 +372,53 @@ func (m manager) Layout(g *gocui.Gui) error { v.Clear() v.Rewind() + if m.s.rawMode { + return m.rawMode(g, v) + } + return m.normalMode(g, v) +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +// SendDumpMsg to daemon +func SendDumpMsg(m msg.DumpMsg, port string) msg.DumpResponse { + recJSON, err := json.Marshal(m) + if err != nil { + log.Fatal("send err 1", err) + } + + req, err := http.NewRequest("POST", "http://localhost:"+port+"/dump", + bytes.NewBuffer(recJSON)) + if err != nil { + log.Fatal("send err 2", err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatal("resh-daemon is not running :(") + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal("read response error") + } + // log.Println(string(body)) + response := msg.DumpResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + log.Fatal("unmarshal resp error: ", err) + } + return response +} + +func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { + maxX, maxY := g.Size() + longestFlagsLen := 2 // at least 2 for i, itm := range m.s.data { if i == maxY { @@ -694,40 +465,41 @@ func (m manager) Layout(g *gocui.Gui) error { return nil } -func quit(g *gocui.Gui, v *gocui.View) error { - return gocui.ErrQuit -} - -// SendDumpMsg to daemon -func SendDumpMsg(m msg.DumpMsg, port string) msg.DumpResponse { - recJSON, err := json.Marshal(m) - if err != nil { - log.Fatal("send err 1", err) - } - - req, err := http.NewRequest("POST", "http://localhost:"+port+"/dump", - bytes.NewBuffer(recJSON)) - if err != nil { - log.Fatal("send err 2", err) - } - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - log.Fatal("resh-daemon is not running :(") - } +func (m manager) rawMode(g *gocui.Gui, v *gocui.View) error { + maxX, maxY := g.Size() - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Fatal("read response error") + for i, itm := range m.s.rawData { + if i == maxY { + if debug { + log.Println(maxY) + } + break + } + displayStr := itm.cmdLineWithColor + if m.s.highlightedItem == i { + // use actual min requried length instead of 420 constant + displayStr = doHighlightString(displayStr, maxX*2) + if debug { + log.Println("### HightlightedItem string :", displayStr) + } + } else if debug { + log.Println(displayStr) + } + if strings.Contains(displayStr, "\n") { + log.Println("display string contained \\n") + displayStr = strings.ReplaceAll(displayStr, "\n", "#") + if debug { + log.Println("display string contained \\n") + } + } + v.WriteString(displayStr + "\n") + // if m.s.highlightedItem == i { + // v.SetHighlight(m.s.highlightedItem, true) + // } } - // log.Println(string(body)) - response := msg.DumpResponse{} - err = json.Unmarshal(body, &response) - if err != nil { - log.Fatal("unmarshal resp error: ", err) + if debug { + log.Println("len(data) =", len(m.s.data)) + log.Println("highlightedItem =", m.s.highlightedItem) } - return response + return nil } diff --git a/cmd/cli/query.go b/cmd/cli/query.go new file mode 100644 index 0000000..df72873 --- /dev/null +++ b/cmd/cli/query.go @@ -0,0 +1,83 @@ +package main + +import ( + "log" + "strings" +) + +type query struct { + terms []string + host string + pwd string + gitOriginRemote string + // pwdTilde string +} + +func isValidTerm(term string) bool { + if len(term) == 0 { + return false + } + if strings.Contains(term, " ") { + return false + } + return true +} + +func filterTerms(terms []string) []string { + var newTerms []string + for _, term := range terms { + if isValidTerm(term) { + newTerms = append(newTerms, term) + } + } + return newTerms +} + +func newQueryFromString(queryInput string, host string, pwd string, gitOriginRemote string) query { + if debug { + log.Println("QUERY input = <" + queryInput + ">") + } + terms := strings.Fields(queryInput) + var logStr string + for _, term := range terms { + logStr += " <" + term + ">" + } + if debug { + log.Println("QUERY raw terms =" + logStr) + } + terms = filterTerms(terms) + logStr = "" + for _, term := range terms { + logStr += " <" + term + ">" + } + if debug { + log.Println("QUERY filtered terms =" + logStr) + log.Println("QUERY pwd =" + pwd) + } + return query{ + terms: terms, + host: host, + pwd: pwd, + gitOriginRemote: gitOriginRemote, + } +} + +func getRawTermsFromString(queryInput string) []string { + if debug { + log.Println("QUERY input = <" + queryInput + ">") + } + terms := strings.Fields(queryInput) + var logStr string + for _, term := range terms { + logStr += " <" + term + ">" + } + if debug { + log.Println("QUERY raw terms =" + logStr) + } + terms = filterTerms(terms) + logStr = "" + for _, term := range terms { + logStr += " <" + term + ">" + } + return terms +} From 14611c87c915e155e5c42d2a573564e5c55028a6 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 1 May 2020 12:23:40 +0200 Subject: [PATCH 7/8] fix deduplication shadowing, minor visual improvement --- cmd/cli/highlight.go | 9 +++++++++ cmd/cli/item.go | 33 +++++++++++++++++---------------- cmd/cli/main.go | 29 ++++++++++++++++++++--------- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/cmd/cli/highlight.go b/cmd/cli/highlight.go index 775d782..eabe539 100644 --- a/cmd/cli/highlight.go +++ b/cmd/cli/highlight.go @@ -77,3 +77,12 @@ func doHighlightString(str string, minLength int) string { } return highlightSelected(str) } + +// EXTRAS + +func highlightModeTitle(str string) string { + // template "\033[3%d;%dm" + greenNormal := "\033[32;1m" + end := "\033[0m" + return greenNormal + cleanHighlight(str) + end +} diff --git a/cmd/cli/item.go b/cmd/cli/item.go index c3e11bb..98a919f 100644 --- a/cmd/cli/item.go +++ b/cmd/cli/item.go @@ -25,7 +25,7 @@ type item struct { cmdLineWithColor string cmdLine string - hits float64 + score float64 key string // cmdLineRaw string @@ -33,7 +33,7 @@ type item struct { func (i item) less(i2 item) bool { // reversed order - return i.hits > i2.hits + return i.score > i2.score } func (i item) produceLine(flagLength int) (string, int) { @@ -105,7 +105,7 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug // nonZeroExitCodeScorePenalty + differentHostScorePenalty - hits := 0.0 + score := 0.0 anyHit := false cmd := record.CmdLine for _, term := range query.terms { @@ -113,13 +113,13 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug if strings.Contains(record.CmdLine, term) { anyHit = true if termHit == false { - hits += hitScore + score += hitScore } else { - hits += hitScoreConsecutive + score += hitScoreConsecutive } termHit = true if properMatch(cmd, term, " ") { - hits += properMatchScore + score += properMatchScore } cmd = strings.ReplaceAll(cmd, term, highlightMatch(term)) // NO continue @@ -138,32 +138,32 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug if record.Pwd == query.pwd { anyHit = true samePwd = true - hits += actualPwdScore + score += actualPwdScore } else if sameGitRepo { anyHit = true - hits += sameGitRepoScore + score += sameGitRepoScore } differentHost := false if record.Host != query.host { differentHost = true - hits -= differentHostScorePenalty + score -= differentHostScorePenalty } errorExitStatus := false if record.ExitCode != 0 { errorExitStatus = true - hits -= nonZeroExitCodeScorePenalty + score -= nonZeroExitCodeScorePenalty } - if hits <= 0 && !anyHit { + if score <= 0 && !anyHit { return item{}, errors.New("no match for given record and query") } // KEY for deduplication unlikelySeparator := "|||||" - key := record.CmdLine + unlikelySeparator + record.Pwd + - unlikelySeparator + strconv.Itoa(record.ExitCode) + unlikelySeparator + + key := record.CmdLine + unlikelySeparator + record.Pwd + unlikelySeparator + record.GitOriginRemote + unlikelySeparator + record.Host + // + strconv.Itoa(record.ExitCode) + unlikelySeparator // DISPLAY // DISPLAY > date @@ -176,7 +176,8 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug location += record.Host + ":" locationWithColor += highlightHost(record.Host) + ":" } - const locationLenght = 30 + const locationLenght = 35 + // const locationLenght = 20 // small screenshots pwdLength := locationLenght - len(location) pwdTilde := strings.Replace(record.Pwd, record.Home, "~", 1) location += leftCutPadString(pwdTilde, pwdLength) @@ -190,7 +191,7 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug flags := "" flagsWithColor := "" if debug { - hitsStr := fmt.Sprintf("%.1f", hits) + hitsStr := fmt.Sprintf("%.1f", score) flags += " S" + hitsStr } if sameGitRepo { @@ -215,7 +216,7 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug flagsWithColor: flagsWithColor, cmdLine: cmdLine, cmdLineWithColor: cmdLineWithColor, - hits: hits, + score: score, key: key, } return it, nil diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 8d789ce..f61b5ad 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -92,7 +92,7 @@ func runReshCli() (string, int) { defer g.Close() g.Cursor = true - g.SelFgColor = gocui.ColorGreen + // g.SelFgColor = gocui.ColorGreen // g.SelBgColor = gocui.ColorGreen g.Highlight = true @@ -202,6 +202,11 @@ func (m manager) SelectPaste(g *gocui.Gui, v *gocui.View) error { return nil } +type dedupRecord struct { + dataIndex int + score float32 +} + func (m manager) UpdateData(input string) { if debug { log.Println("EDIT start") @@ -210,7 +215,7 @@ func (m manager) UpdateData(input string) { } query := newQueryFromString(input, m.host, m.pwd, m.gitOriginRemote) var data []item - itemSet := make(map[string]bool) + itemSet := make(map[string]int) m.s.lock.Lock() defer m.s.lock.Unlock() for _, rec := range m.s.fullRecords { @@ -220,19 +225,25 @@ func (m manager) UpdateData(input string) { // log.Println(" * continue (no match)", rec.Pwd) continue } - if itemSet[itm.key] { - // log.Println(" * continue (already present)", itm.key(), itm.pwd) + if idx, ok := itemSet[itm.key]; ok { + // duplicate found + if data[idx].score >= itm.score { + // skip duplicate item + continue + } + // update duplicate item + data[idx] = itm continue } - itemSet[itm.key] = true + // add new item + itemSet[itm.key] = len(data) data = append(data, itm) - // log.Println("DATA =", itm.display) } if debug { log.Println("len(tmpdata) =", len(data)) } sort.SliceStable(data, func(p, q int) bool { - return data[p].hits > data[q].hits + return data[p].score > data[q].score }) m.s.data = nil for _, itm := range data { @@ -348,9 +359,9 @@ func (m manager) Layout(g *gocui.Gui) error { v.Editable = true v.Editor = m if m.s.rawMode { - v.Title = " RESH CLI - NON-CONTEXTUAL \"RAW\" MODE " + v.Title = " RESH CLI - NON-CONTEXTUAL \"RAW\" MODE - (CTRL+R to switch BACK) " } else { - v.Title = " RESH CLI - CONTEXTUAL MODE " + v.Title = " RESH CLI - CONTEXTUAL MODE - (CTRL+R to switch to RAW MODE) " } g.SetCurrentView("input") From 4d3a6fd1c47b553c65218a481b11e12bfd2e8067 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 1 May 2020 12:49:47 +0200 Subject: [PATCH 8/8] improve enrichment, fix git normalization --- cmd/cli/item.go | 3 +++ pkg/records/records.go | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cmd/cli/item.go b/cmd/cli/item.go index 98a919f..54e9fa3 100644 --- a/cmd/cli/item.go +++ b/cmd/cli/item.go @@ -202,6 +202,9 @@ func newItemFromRecordForQuery(record records.EnrichedRecord, query query, debug flags += " E" + strconv.Itoa(record.ExitCode) flagsWithColor += " " + highlightWarn("E"+strconv.Itoa(record.ExitCode)) } + // NOTE: you can debug arbitrary metadata like this + // flags += " <" + record.GitOriginRemote + ">" + // flagsWithColor += " <" + record.GitOriginRemote + ">" // DISPLAY > cmdline diff --git a/pkg/records/records.go b/pkg/records/records.go index b2ef7e6..0d0be91 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -169,26 +169,26 @@ func (r EnrichedRecord) ToString() (string, error) { // Enriched - returnd enriched record func Enriched(r Record) EnrichedRecord { record := EnrichedRecord{Record: r} + // normlize git remote + record.GitOriginRemote = NormalizeGitRemote(record.GitOriginRemote) + record.GitOriginRemoteAfter = NormalizeGitRemote(record.GitOriginRemoteAfter) // Get command/first word from commandline var err error - record.Command, record.FirstWord, err = GetCommandAndFirstWord(r.CmdLine) + err = r.Validate() if err != nil { - record.Errors = append(record.Errors, "GetCommandAndFirstWord error:"+err.Error()) + record.Errors = append(record.Errors, "Validate error:"+err.Error()) // rec, _ := record.ToString() // log.Println("Invalid command:", rec) record.Invalid = true - return record } - err = r.Validate() + record.Command, record.FirstWord, err = GetCommandAndFirstWord(r.CmdLine) if err != nil { - record.Errors = append(record.Errors, "Validate error:"+err.Error()) + record.Errors = append(record.Errors, "GetCommandAndFirstWord error:"+err.Error()) // rec, _ := record.ToString() // log.Println("Invalid command:", rec) - record.Invalid = true + record.Invalid = true // should this be really invalid ? } - record.GitOriginRemote = NormalizeGitRemote(record.GitOriginRemote) return record - // TODO: Detect and mark simple commands r.Simple } // Merge two records (part1 - collect + part2 - postcollect)