diff --git a/cmd/cli/highlight.go b/cmd/cli/highlight.go index eabe539..a121dc9 100644 --- a/cmd/cli/highlight.go +++ b/cmd/cli/highlight.go @@ -27,6 +27,19 @@ func cleanHighlight(str string) string { return str } +func highlightHeader(str string) string { + underline := "\033[4m" + end := "\033[0m" + // no clean highlight + return underline + str + end +} + +func highlightStatus(str string) string { + invert := "\033[7;1m" + end := "\033[0m" + return invert + cleanHighlight(str) + end +} + func highlightSelected(str string) string { // template "\033[3%d;%dm" // invertGreen := "\033[32;7;1m" @@ -35,6 +48,13 @@ func highlightSelected(str string) string { return invert + cleanHighlight(str) + end } +func highlightDate(str string) string { + // template "\033[3%d;%dm" + yellowNormal := "\033[33m" + end := "\033[0m" + return yellowNormal + cleanHighlight(str) + end +} + func highlightHost(str string) string { // template "\033[3%d;%dm" redNormal := "\033[31m" @@ -71,6 +91,13 @@ func highlightGit(str string) string { return greenBold + cleanHighlight(str) + end } +func doHighlightHeader(str string, minLength int) string { + if len(str) < minLength { + str = str + strings.Repeat(" ", minLength-len(str)) + } + return highlightHeader(str) +} + func doHighlightString(str string, minLength int) string { if len(str) < minLength { str = str + strings.Repeat(" ", minLength-len(str)) diff --git a/cmd/cli/item.go b/cmd/cli/item.go index 349da75..b72a919 100644 --- a/cmd/cli/item.go +++ b/cmd/cli/item.go @@ -6,17 +6,49 @@ import ( "log" "strconv" "strings" + "time" "github.com/curusarn/resh/pkg/records" ) +const itemLocationLenght = 30 + type item struct { - // dateWithColor string - // date string + isRaw bool + + realtimeBefore float64 // [host:]pwd - locationWithColor string - location string + differentHost bool + host string + home string + samePwd bool + pwd string + + // [G] [E#] + sameGitRepo bool + exitCode int + + cmdLineWithColor string + cmdLine string + + score float64 + + key string + // cmdLineRaw string +} + +type itemColumns struct { + dateWithColor string + date string + + // [host:]pwd + hostWithColor string + host string + pwdTilde string + samePwd bool + //locationWithColor string + //location string // [G] [E#] flagsWithColor string @@ -25,7 +57,7 @@ type item struct { cmdLineWithColor string cmdLine string - score float64 + // score float64 key string // cmdLineRaw string @@ -36,27 +68,166 @@ func (i item) less(i2 item) bool { return i.score > i2.score } -func (i item) produceLine(flagLength int) (string, int) { +func splitStatusLineToLines(statusLine string, printedLineLength, realLineLength int) []string { + var statusLineSlice []string + // status line + var idxSt, idxEnd int + var nextLine bool + tab := " " + tabSize := len(tab) + for idxSt < len(statusLine) { + idxEnd = idxSt + printedLineLength + if nextLine { + idxEnd -= tabSize + } + + if idxEnd > len(statusLine) { + idxEnd = len(statusLine) + } + str := statusLine[idxSt:idxEnd] + + indent := " " + if nextLine { + indent += tab + } + statusLineSlice = append(statusLineSlice, highlightStatus(rightCutPadString(indent+str, realLineLength))+"\n") + idxSt += printedLineLength + nextLine = true + } + return statusLineSlice +} + +func (i item) drawStatusLine(compactRendering bool, printedLineLength, realLineLength int) []string { + if i.isRaw { + return splitStatusLineToLines(i.cmdLine, printedLineLength, realLineLength) + } + secs := int64(i.realtimeBefore) + nsecs := int64((i.realtimeBefore - float64(secs)) * 1e9) + tm := time.Unix(secs, nsecs) + const timeFormat = "2006-01-02 15:04:05" + timeString := tm.Format(timeFormat) + + pwdTilde := strings.Replace(i.pwd, i.home, "~", 1) + + separator := " " + stLine := timeString + separator + i.host + ":" + pwdTilde + separator + i.cmdLine + return splitStatusLineToLines(stLine, printedLineLength, realLineLength) +} + +func (i item) drawItemColumns(compactRendering bool) itemColumns { + if i.isRaw { + notAvailable := "n/a" + return itemColumns{ + date: notAvailable + " ", + dateWithColor: notAvailable + " ", + // dateWithColor: highlightDate(notAvailable) + " ", + host: "", + hostWithColor: "", + pwdTilde: notAvailable, + cmdLine: i.cmdLine, + cmdLineWithColor: i.cmdLineWithColor, + // score: i.score, + key: i.key, + } + } + + // DISPLAY + // DISPLAY > date + secs := int64(i.realtimeBefore) + nsecs := int64((i.realtimeBefore - float64(secs)) * 1e9) + tm := time.Unix(secs, nsecs) + + var date string + if compactRendering { + date = formatTimeRelativeShort(tm) + " " + } else { + date = formatTimeRelativeLong(tm) + " " + } + dateWithColor := highlightDate(date) + // DISPLAY > location + // DISPLAY > location > host + host := "" + hostWithColor := "" + if i.differentHost { + host += i.host + ":" + hostWithColor += highlightHost(i.host) + ":" + } + // DISPLAY > location > directory + pwdTilde := strings.Replace(i.pwd, i.home, "~", 1) + + // DISPLAY > flags + flags := "" + flagsWithColor := "" + if debug { + hitsStr := fmt.Sprintf("%.1f", i.score) + flags += " S" + hitsStr + flagsWithColor += " S" + hitsStr + } + if i.sameGitRepo { + flags += " G" + flagsWithColor += " " + highlightGit("G") + } + if i.exitCode != 0 { + flags += " E" + strconv.Itoa(i.exitCode) + flagsWithColor += " " + highlightWarn("E"+strconv.Itoa(i.exitCode)) + } + // NOTE: you can debug arbitrary metadata like this + // flags += " <" + record.GitOriginRemote + ">" + // flagsWithColor += " <" + record.GitOriginRemote + ">" + return itemColumns{ + date: date, + dateWithColor: dateWithColor, + host: host, + hostWithColor: hostWithColor, + pwdTilde: pwdTilde, + samePwd: i.samePwd, + flags: flags, + flagsWithColor: flagsWithColor, + cmdLine: i.cmdLine, + cmdLineWithColor: i.cmdLineWithColor, + // score: i.score, + key: i.key, + } +} + +func (ic itemColumns) produceLine(dateLength int, locationLength int, flagLength int, header bool, showDate bool) (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) + if showDate { + date := ic.date + for len(date) < dateLength { + line += " " + date += " " + } + // TODO: use strings.Repeat + line += ic.dateWithColor + } + // LOCATION + locationWithColor := ic.hostWithColor + pwdLength := locationLength - len(ic.host) + if ic.samePwd { + locationWithColor += highlightPwd(leftCutPadString(ic.pwdTilde, pwdLength)) + } else { + locationWithColor += leftCutPadString(ic.pwdTilde, pwdLength) + } + line += locationWithColor + line += ic.flagsWithColor + flags := ic.flags + if flagLength < len(ic.flags) { + log.Printf("produceLine can't specify line w/ flags shorter than the actual size. - len(flags) %v, requested %v\n", len(ic.flags), flagLength) } for len(flags) < flagLength { line += " " flags += " " } spacer := " " - if flagLength > 5 { + if flagLength > 5 || header { // use shorter spacer // because there is likely a long flag like E130 in the view spacer = " " } - line += spacer + i.cmdLineWithColor + line += spacer + ic.cmdLineWithColor - length := len(i.location) + flagLength + len(spacer) + len(i.cmdLine) + length := dateLength + locationLength + flagLength + len(spacer) + len(ic.cmdLine) return line, length } @@ -94,15 +265,26 @@ 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.CliRecord, 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 + // Use numbers that won't add up to same score for any number of query words + const hitScore = 1.307 + const properMatchScore = 0.603 + const hitScoreConsecutive = 0.002 + + // Host penalty + var actualPwdScore = 0.9 + var sameGitRepoScore = 0.8 + var nonZeroExitCodeScorePenalty = 0.4 + var differentHostScorePenalty = 0.2 + + reduceHostPenalty := false + if reduceHostPenalty { + actualPwdScore = 0.9 + sameGitRepoScore = 0.7 + nonZeroExitCodeScorePenalty = 0.4 + differentHostScorePenalty = 0.1 + } + const timeScoreCoef = 1e-13 // nonZeroExitCodeScorePenalty + differentHostScorePenalty score := 0.0 @@ -114,7 +296,8 @@ func newItemFromRecordForQuery(record records.CliRecord, query query, debug bool anyHit = true if termHit == false { score += hitScore - } else { + } else if len(term) > 1 { + // only count consecutive matches for queries longer than 1 score += hitScoreConsecutive } termHit = true @@ -122,9 +305,33 @@ func newItemFromRecordForQuery(record records.CliRecord, query query, debug bool score += properMatchScore } cmd = strings.ReplaceAll(cmd, term, highlightMatch(term)) - // NO continue } } + // DISPLAY > cmdline + + // cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">" + cmdLine := strings.ReplaceAll(record.CmdLine, "\n", ";") + cmdLineWithColor := strings.ReplaceAll(cmd, "\n", ";") + + // KEY for deduplication + + key := record.CmdLine + // NOTE: since we import standard history we need a compatible key without metadata + /* + unlikelySeparator := "|||||" + key := record.CmdLine + unlikelySeparator + record.Pwd + unlikelySeparator + + record.GitOriginRemote + unlikelySeparator + record.Host + */ + if record.IsRaw { + return item{ + isRaw: true, + + cmdLine: cmdLine, + cmdLineWithColor: cmdLineWithColor, + score: score, + key: key, + }, nil + } // actual pwd matches // N terms can only produce: // -> N matches against the command @@ -149,78 +356,31 @@ func newItemFromRecordForQuery(record records.CliRecord, query query, debug bool differentHost = true score -= differentHostScorePenalty } - errorExitStatus := false + // errorExitStatus := false if record.ExitCode != 0 { - errorExitStatus = true + // errorExitStatus = true score -= nonZeroExitCodeScorePenalty } if score <= 0 && !anyHit { return item{}, errors.New("no match for given record and query") } + score += record.RealtimeBefore * timeScoreCoef - // KEY for deduplication - - unlikelySeparator := "|||||" - key := record.CmdLine + unlikelySeparator + record.Pwd + unlikelySeparator + - record.GitOriginRemote + unlikelySeparator + record.Host - // + strconv.Itoa(record.ExitCode) + unlikelySeparator - - // DISPLAY - // DISPLAY > date - // TODO - - // DISPLAY > location - location := "" - locationWithColor := "" - if differentHost { - location += record.Host + ":" - locationWithColor += highlightHost(record.Host) + ":" - } - const locationLenght = 30 - // const locationLenght = 20 // small screenshots - 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", score) - 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)) - } - // NOTE: you can debug arbitrary metadata like this - // flags += " <" + record.GitOriginRemote + ">" - // flagsWithColor += " <" + record.GitOriginRemote + ">" - - // DISPLAY > cmdline + it := item{ + realtimeBefore: record.RealtimeBefore, - // cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">" - cmdLine := strings.ReplaceAll(record.CmdLine, "\n", ";") - cmdLineWithColor := strings.ReplaceAll(cmd, "\n", ";") + differentHost: differentHost, + host: record.Host, + home: record.Home, + samePwd: samePwd, + pwd: record.Pwd, - it := item{ - location: location, - locationWithColor: locationWithColor, - flags: flags, - flagsWithColor: flagsWithColor, - cmdLine: cmdLine, - cmdLineWithColor: cmdLineWithColor, - score: score, - key: key, + sameGitRepo: sameGitRepo, + exitCode: record.ExitCode, + cmdLine: cmdLine, + cmdLineWithColor: cmdLineWithColor, + score: score, + key: key, } return it, nil } diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 7fde661..c5c337e 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -124,24 +124,32 @@ func runReshCli() (string, int) { if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, layout.Next); err != nil { log.Panicln(err) } - if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, layout.Prev); err != nil { + if err := g.SetKeybinding("", gocui.KeyCtrlN, gocui.ModNone, layout.Next); err != nil { log.Panicln(err) } - if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, layout.Prev); err != nil { log.Panicln(err) } - if err := g.SetKeybinding("", gocui.KeyCtrlG, gocui.ModNone, quit); err != nil { + if err := g.SetKeybinding("", gocui.KeyCtrlP, gocui.ModNone, layout.Prev); err != nil { log.Panicln(err) } - if err := g.SetKeybinding("", gocui.KeyCtrlD, gocui.ModNone, quit); err != nil { + + if err := g.SetKeybinding("", gocui.KeyArrowRight, gocui.ModNone, layout.SelectPaste); err != nil { log.Panicln(err) } if err := g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, layout.SelectExecute); err != nil { log.Panicln(err) } - if err := g.SetKeybinding("", gocui.KeyArrowRight, gocui.ModNone, layout.SelectPaste); err != nil { + if err := g.SetKeybinding("", gocui.KeyCtrlG, gocui.ModNone, layout.AbortPaste); err != nil { log.Panicln(err) } + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding("", gocui.KeyCtrlD, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, layout.SwitchModes); err != nil { log.Panicln(err) } @@ -156,11 +164,12 @@ func runReshCli() (string, int) { } type state struct { - lock sync.Mutex - cliRecords []records.CliRecord - data []item - rawData []rawItem - highlightedItem int + lock sync.Mutex + cliRecords []records.CliRecord + data []item + rawData []rawItem + highlightedItem int + displayedItemsCount int rawMode bool @@ -202,6 +211,17 @@ func (m manager) SelectPaste(g *gocui.Gui, v *gocui.View) error { return nil } +func (m manager) AbortPaste(g *gocui.Gui, v *gocui.View) error { + m.s.lock.Lock() + defer m.s.lock.Unlock() + if m.s.highlightedItem < len(m.s.data) { + m.s.output = v.Buffer() + m.s.exitCode = 0 // success + return gocui.ErrQuit + } + return nil +} + type dedupRecord struct { dataIndex int score float32 @@ -316,10 +336,9 @@ func (m manager) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) } func (m manager) Next(g *gocui.Gui, v *gocui.View) error { - _, y := g.Size() m.s.lock.Lock() defer m.s.lock.Unlock() - if m.s.highlightedItem < y { + if m.s.highlightedItem < m.s.displayedItemsCount-1 { m.s.highlightedItem++ } return nil @@ -393,64 +412,109 @@ func quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } -// SendCliMsg to daemon -func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse { - recJSON, err := json.Marshal(m) - if err != nil { - log.Fatal("send err 1", err) +func getHeader(compactRendering bool) itemColumns { + date := "TIME " + host := "HOST:" + dir := "DIRECTORY" + if compactRendering { + dir = "DIR" + } + flags := " FLAGS" + cmdLine := "COMMAND-LINE" + return itemColumns{ + date: date, + dateWithColor: date, + host: host, + hostWithColor: host, + pwdTilde: dir, + samePwd: false, + flags: flags, + flagsWithColor: flags, + cmdLine: cmdLine, + cmdLineWithColor: cmdLine, + // score: i.score, + key: "_HEADERS_", } +} - 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") +const smallTerminalTresholdWidth = 110 - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - log.Fatal("resh-daemon is not running :(") - } +func (m manager) normalMode(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") - } - // log.Println(string(body)) - response := msg.CliResponse{} - err = json.Unmarshal(body, &response) - if err != nil { - log.Fatal("unmarshal resp error: ", err) + compactRenderingMode := false + if maxX < smallTerminalTresholdWidth { + compactRenderingMode = true } - return response -} -func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { - maxX, maxY := g.Size() + data := []itemColumns{} - longestFlagsLen := 2 // at least 2 + header := getHeader(compactRenderingMode) + longestDateLen := len(header.date) + longestLocationLen := len(header.host) + len(header.pwdTilde) + longestFlagsLen := 2 + maxPossibleMainViewHeight := maxY - 3 - 1 - 1 - 1 // - top box - header - status - help for i, itm := range m.s.data { if i == maxY { break } - if len(itm.flags) > longestFlagsLen { - longestFlagsLen = len(itm.flags) + ic := itm.drawItemColumns(compactRenderingMode) + data = append(data, ic) + if i > maxPossibleMainViewHeight { + // do not stretch columns because of results that will end up outside of the page + continue + } + if len(ic.date) > longestDateLen { + longestDateLen = len(ic.date) + } + if len(ic.host)+len(ic.pwdTilde) > longestLocationLen { + longestLocationLen = len(ic.host) + len(ic.pwdTilde) } + if len(ic.flags) > longestFlagsLen { + longestFlagsLen = len(ic.flags) + } + } + maxLocationLen := maxX/7 + 8 + if longestLocationLen > maxLocationLen { + longestLocationLen = maxLocationLen } - for i, itm := range m.s.data { - if i == maxY { - if debug { - log.Println(maxY) - } + if m.s.highlightedItem >= len(m.s.data) { + m.s.highlightedItem = len(m.s.data) - 1 + } + // status line + topBoxHeight := 3 // size of the query box up top + topBoxHeight++ // headers + realLineLength := maxX - 2 + printedLineLength := maxX - 4 + statusLine := m.s.data[m.s.highlightedItem].drawStatusLine(compactRenderingMode, printedLineLength, realLineLength) + var statusLineHeight int = len(statusLine) + 1 // help line + + helpLineHeight := 1 + const helpLine = "HELP: type to search, UP/DOWN to select, RIGHT to edit, ENTER to execute, CTRL+G to abort, CTRL+C/D to quit; " + + "TIP: when resh-cli is launched command line is used as initial search query" + + mainViewHeight := maxY - topBoxHeight - statusLineHeight - helpLineHeight + m.s.displayedItemsCount = mainViewHeight + + // header + // header := getHeader() + dispStr, _ := header.produceLine(longestDateLen, longestLocationLen, longestFlagsLen, true, true) + dispStr = doHighlightHeader(dispStr, maxX*2) + v.WriteString(dispStr + "\n") + + var index int + for index < len(data) { + itm := data[index] + if index == mainViewHeight { + // page is full break } - displayStr, _ := itm.produceLine(longestFlagsLen) - if m.s.highlightedItem == i { - // use actual min requried length instead of 420 constant - displayStr = doHighlightString(displayStr, maxX*2) + + displayStr, _ := itm.produceLine(longestDateLen, longestLocationLen, longestFlagsLen, false, true) + if m.s.highlightedItem == index { + // maxX * 2 because there are escape sequences that make it hard to tell the real string lenght + displayStr = doHighlightString(displayStr, maxX*3) if debug { log.Println("### HightlightedItem string :", displayStr) } @@ -465,10 +529,17 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { } } v.WriteString(displayStr + "\n") - // if m.s.highlightedItem == i { - // v.SetHighlight(m.s.highlightedItem, true) - // } + index++ + } + // push the status line to the bottom of the page + for index < mainViewHeight { + v.WriteString("\n") + index++ } + for _, line := range statusLine { + v.WriteString(line) + } + v.WriteString(helpLine) if debug { log.Println("len(data) =", len(m.s.data)) log.Println("highlightedItem =", m.s.highlightedItem) @@ -478,6 +549,8 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { func (m manager) rawMode(g *gocui.Gui, v *gocui.View) error { maxX, maxY := g.Size() + topBoxSize := 3 + m.s.displayedItemsCount = maxY - topBoxSize for i, itm := range m.s.rawData { if i == maxY { @@ -514,3 +587,37 @@ func (m manager) rawMode(g *gocui.Gui, v *gocui.View) error { } return nil } + +// SendCliMsg to daemon +func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse { + 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.CliResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + log.Fatal("unmarshal resp error: ", err) + } + return response +} diff --git a/cmd/cli/query.go b/cmd/cli/query.go index df72873..7fd8d4f 100644 --- a/cmd/cli/query.go +++ b/cmd/cli/query.go @@ -2,6 +2,7 @@ package main import ( "log" + "sort" "strings" ) @@ -54,6 +55,7 @@ func newQueryFromString(queryInput string, host string, pwd string, gitOriginRem log.Println("QUERY filtered terms =" + logStr) log.Println("QUERY pwd =" + pwd) } + sort.SliceStable(terms, func(i, j int) bool { return len(terms[i]) < len(terms[j]) }) return query{ terms: terms, host: host, diff --git a/cmd/cli/time.go b/cmd/cli/time.go new file mode 100644 index 0000000..77ca367 --- /dev/null +++ b/cmd/cli/time.go @@ -0,0 +1,181 @@ +package main + +import ( + "strconv" + "time" +) + +func formatTimeRelativeLongest(tm time.Time) string { + tmSince := time.Since(tm) + hrs := tmSince.Hours() + yrs := int(hrs / (365 * 24)) + if yrs > 0 { + if yrs == 1 { + return "1 year ago" + } + return strconv.Itoa(yrs) + " years ago" + } + months := int(hrs / (30 * 24)) + if months > 0 { + if months == 1 { + return "1 month ago" + } + return strconv.Itoa(months) + " months ago" + } + days := int(hrs / 24) + if days > 0 { + if days == 1 { + return "1 day ago" + } + return strconv.Itoa(days) + " days ago" + } + hrsInt := int(hrs) + if hrsInt > 0 { + if hrsInt == 1 { + return "1 hour ago" + } + return strconv.Itoa(hrsInt) + " hours ago" + } + mins := int(hrs*60) % 60 + if mins > 0 { + if mins == 1 { + return "1 min ago" + } + return strconv.Itoa(mins) + " mins ago" + } + secs := int(hrs*60*60) % 60 + if secs > 0 { + if secs == 1 { + return "1 sec ago" + } + return strconv.Itoa(secs) + " secs ago" + } + return "now" +} + +func formatTimeRelativeLong(tm time.Time) string { + tmSince := time.Since(tm) + hrs := tmSince.Hours() + yrs := int(hrs / (365 * 24)) + if yrs > 0 { + if yrs == 1 { + return "1 year" + } + return strconv.Itoa(yrs) + " years" + } + months := int(hrs / (30 * 24)) + if months > 0 { + if months == 1 { + return "1 month" + } + return strconv.Itoa(months) + " months" + } + days := int(hrs / 24) + if days > 0 { + if days == 1 { + return "1 day" + } + return strconv.Itoa(days) + " days" + } + hrsInt := int(hrs) + if hrsInt > 0 { + if hrsInt == 1 { + return "1 hour" + } + return strconv.Itoa(hrsInt) + " hours" + } + mins := int(hrs*60) % 60 + if mins > 0 { + if mins == 1 { + return "1 min" + } + return strconv.Itoa(mins) + " mins" + } + secs := int(hrs*60*60) % 60 + if secs > 0 { + if secs == 1 { + return "1 sec" + } + return strconv.Itoa(secs) + " secs" + } + return "now" +} + +func formatTimeMixedLongest(tm time.Time) string { + tmSince := time.Since(tm) + hrs := tmSince.Hours() + yrs := int(hrs / (365 * 24)) + if yrs > 0 { + if yrs == 1 { + return "1 year ago" + } + return strconv.Itoa(yrs) + " years ago" + } + months := int(hrs / (30 * 24)) + if months > 0 { + if months == 1 { + return "1 month ago" + } + return strconv.Itoa(months) + " months ago" + } + days := int(hrs / 24) + if days > 0 { + if days == 1 { + return "1 day ago" + } + return strconv.Itoa(days) + " days ago" + } + hrsInt := int(hrs) + mins := int(hrs*60) % 60 + return strconv.Itoa(hrsInt) + ":" + strconv.Itoa(mins) +} + +func formatTimeRelativeShort(tm time.Time) string { + tmSince := time.Since(tm) + hrs := tmSince.Hours() + yrs := int(hrs / (365 * 24)) + if yrs > 0 { + return strconv.Itoa(yrs) + " Y" + } + months := int(hrs / (30 * 24)) + if months > 0 { + return strconv.Itoa(months) + " M" + } + days := int(hrs / 24) + if days > 0 { + return strconv.Itoa(days) + " D" + } + hrsInt := int(hrs) + if hrsInt > 0 { + return strconv.Itoa(hrsInt) + " h" + } + mins := int(hrs*60) % 60 + if mins > 0 { + return strconv.Itoa(mins) + " m" + } + secs := int(hrs*60*60) % 60 + if secs > 0 { + return strconv.Itoa(secs) + " s" + } + return "now" +} + +func formatTimeMixedShort(tm time.Time) string { + tmSince := time.Since(tm) + hrs := tmSince.Hours() + yrs := int(hrs / (365 * 24)) + if yrs > 0 { + return strconv.Itoa(yrs) + " Y" + } + months := int(hrs / (30 * 24)) + if months > 0 { + return strconv.Itoa(months) + " M" + } + days := int(hrs / 24) + if days > 0 { + return strconv.Itoa(days) + " D" + } + hrsInt := int(hrs) + mins := int(hrs*60) % 60 + return strconv.Itoa(hrsInt) + ":" + strconv.Itoa(mins) +} diff --git a/cmd/collect/main.go b/cmd/collect/main.go index 8eba780..d2d2815 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -53,6 +53,7 @@ func main() { shell := flag.String("shell", "", "actual shell") uname := flag.String("uname", "", "uname") sessionID := flag.String("sessionId", "", "resh generated session id") + recordID := flag.String("recordId", "", "resh generated record id") // recall metadata recallActions := flag.String("recall-actions", "", "recall actions that took place before executing the command") @@ -195,6 +196,7 @@ func main() { Shell: *shell, Uname: *uname, SessionID: *sessionID, + RecordID: *recordID, // posix Home: *home, diff --git a/cmd/postcollect/main.go b/cmd/postcollect/main.go index d987bc5..b5cb6b0 100644 --- a/cmd/postcollect/main.go +++ b/cmd/postcollect/main.go @@ -44,6 +44,8 @@ func main() { cmdLine := flag.String("cmdLine", "", "command line") exitCode := flag.Int("exitCode", -1, "exit code") sessionID := flag.String("sessionId", "", "resh generated session id") + recordID := flag.String("recordId", "", "resh generated record id") + shlvl := flag.Int("shlvl", -1, "$SHLVL") shell := flag.String("shell", "", "actual shell") @@ -118,6 +120,7 @@ func main() { CmdLine: *cmdLine, ExitCode: *exitCode, SessionID: *sessionID, + RecordID: *recordID, Shlvl: *shlvl, Shell: *shell, diff --git a/conf/config.toml b/conf/config.toml index 2e0adc2..3cd028b 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -4,4 +4,4 @@ sesshistInitHistorySize = 1000 debug = false bindArrowKeysBash = false bindArrowKeysZsh = true -bindControlR = false +bindControlR = true diff --git a/pkg/histcli/histcli.go b/pkg/histcli/histcli.go index 2abb799..f9b6611 100644 --- a/pkg/histcli/histcli.go +++ b/pkg/histcli/histcli.go @@ -22,3 +22,10 @@ func (h *Histcli) AddRecord(record records.Record) { h.List = append(h.List, cli) } + +// AddCmdLine to the histcli +func (h *Histcli) AddCmdLine(cmdline string) { + cli := records.NewCliRecordFromCmdLine(cmdline) + + h.List = append(h.List, cli) +} diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go index a72da7d..b1d8019 100644 --- a/pkg/histfile/histfile.go +++ b/pkg/histfile/histfile.go @@ -50,7 +50,13 @@ func New(input chan records.Record, sessionsToDrop chan string, } // load records from resh history, reverse, enrich and save -func (h *Histfile) loadFullRecords(recs []records.Record) { +func (h *Histfile) loadCliRecords(recs []records.Record) { + for _, cmdline := range h.bashCmdLines.List { + h.cliRecords.AddCmdLine(cmdline) + } + for _, cmdline := range h.zshCmdLines.List { + h.cliRecords.AddCmdLine(cmdline) + } for i := len(recs) - 1; i >= 0; i-- { rec := recs[i] h.cliRecords.AddRecord(rec) @@ -82,7 +88,7 @@ func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHi } log.Println("histfile: Loading resh history from file ...") history := records.LoadFromFile(h.historyPath, math.MaxInt32) - go h.loadFullRecords(history) + go h.loadCliRecords(history) // NOTE: keeping this weird interface for now because we might use it in the future // when we only load bash or zsh history reshCmdLines := loadCmdLines(history) diff --git a/pkg/records/records.go b/pkg/records/records.go index b8d9d67..c4e91db 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -23,6 +23,7 @@ type BaseRecord struct { Shell string `json:"shell"` Uname string `json:"uname"` SessionID string `json:"sessionId"` + RecordID string `json:"recordId"` // posix Home string `json:"home"` @@ -149,6 +150,7 @@ type SlimRecord struct { // CliRecord used for sending records to RESH-CLI type CliRecord struct { + IsRaw bool `json:"isRaw"` SessionID string `json:"sessionId"` CmdLine string `json:"cmdLine"` @@ -158,14 +160,23 @@ type CliRecord struct { GitOriginRemote string `json:"gitOriginRemote"` ExitCode int `json:"exitCode"` - // RealtimeBefore float64 `json:"realtimeBefore"` + RealtimeBefore float64 `json:"realtimeBefore"` // RealtimeAfter float64 `json:"realtimeAfter"` // RealtimeDuration float64 `json:"realtimeDuration"` } +// NewCliRecordFromCmdLine from EnrichedRecord +func NewCliRecordFromCmdLine(cmdLine string) CliRecord { + return CliRecord{ + IsRaw: true, + CmdLine: cmdLine, + } +} + // NewCliRecord from EnrichedRecord func NewCliRecord(r EnrichedRecord) CliRecord { return CliRecord{ + IsRaw: false, SessionID: r.SessionID, CmdLine: r.CmdLine, Host: r.Host, @@ -173,6 +184,7 @@ func NewCliRecord(r EnrichedRecord) CliRecord { Home: r.Home, GitOriginRemote: r.GitOriginRemote, ExitCode: r.ExitCode, + RealtimeBefore: r.RealtimeBefore, } } @@ -231,6 +243,9 @@ func (r *Record) Merge(r2 Record) error { if r.CmdLine != r2.CmdLine { return errors.New("Records to merge are not parts of the same records - r1:" + r.CmdLine + " r2:" + r2.CmdLine) } + if r.RecordID != r2.RecordID { + return errors.New("Records to merge do not have the same ID - r1:" + r.RecordID + " r2:" + r2.RecordID) + } // r.RealtimeBefore != r2.RealtimeBefore - can't be used because of bash-preexec runs when it's not supposed to r.ExitCode = r2.ExitCode r.PwdAfter = r2.PwdAfter diff --git a/scripts/hooks.sh b/scripts/hooks.sh index b0fd1ce..6a1d6bf 100644 --- a/scripts/hooks.sh +++ b/scripts/hooks.sh @@ -9,6 +9,7 @@ __resh_reset_variables() { __RESH_HIST_RECALL_ACTIONS="" __RESH_HIST_NO_PREFIX_MODE=0 __RESH_HIST_RECALL_STRATEGY="" + __RESH_RECORD_ID=$(__resh_get_uuid) } __resh_preexec() { @@ -81,6 +82,7 @@ __resh_collect() { -shell "$__RESH_SHELL" \ -uname "$__RESH_UNAME" \ -sessionId "$__RESH_SESSION_ID" \ + -recordId "$__RESH_RECORD_ID" \ -cols "$__RESH_COLS" \ -home "$__RESH_HOME" \ -lang "$__RESH_LANG" \ @@ -157,6 +159,7 @@ __resh_precmd() { -realtimeBefore "$__RESH_RT_BEFORE" \ -exitCode "$__RESH_EXIT_CODE" \ -sessionId "$__RESH_SESSION_ID" \ + -recordId "$__RESH_RECORD_ID" \ -shell "$__RESH_SHELL" \ -shlvl "$__RESH_SHLVL" \ -pwdAfter "$__RESH_PWD_AFTER" \