diff --git a/Makefile b/Makefile index 8881dca..616b057 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ GOFLAGS=-ldflags "-X main.version=${VERSION} -X main.commit=${REVISION}" build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect bin/resh-daemon\ - bin/resh-evaluate bin/resh-sanitize bin/resh-control bin/resh-config bin/resh-inspect + bin/resh-evaluate bin/resh-sanitize bin/resh-control bin/resh-config bin/resh-inspect bin/resh-cli install: build conf/config-dev.toml scripts/install.sh diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..1db540e --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,559 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io/ioutil" + "log" + "math" + "net/http" + "os" + "sort" + "strings" + "sync" + + "github.com/BurntSushi/toml" + "github.com/awesome-gocui/gocui" + "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/pkg/msg" + "github.com/curusarn/resh/pkg/records" + + "os/user" + "path/filepath" + "strconv" +) + +// version from git set during build +var version string + +// commit from git set during build +var commit string + +func main() { + usr, _ := user.Current() + dir := usr.HomeDir + configPath := filepath.Join(dir, "/.config/resh.toml") + logPath := filepath.Join(dir, ".resh/cli.log") + + f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + log.Fatal("Error opening file:", err) + } + defer f.Close() + + log.SetOutput(f) + + var config cfg.Config + if _, err := toml.DecodeFile(configPath, &config); err != nil { + log.Fatal("Error reading config:", err) + } + if config.Debug { + // Debug = true + log.SetFlags(log.LstdFlags | log.Lmicroseconds) + } + + sessionID := flag.String("sessionID", "", "resh generated session id") + pwd := flag.String("pwd", "", "present working directory") + flag.Parse() + + if *sessionID == "" { + fmt.Println("Error: you need to specify sessionId") + } + if *pwd == "" { + fmt.Println("Error: you need to specify PWD") + } + + g, err := gocui.NewGui(gocui.OutputNormal, false) + if err != nil { + log.Panicln(err) + } + defer g.Close() + + g.Cursor = true + g.SelFgColor = gocui.ColorGreen + // g.SelBgColor = gocui.ColorGreen + g.Highlight = true + + mess := msg.DumpMsg{ + SessionID: *sessionID, + PWD: *pwd, + } + resp := SendDumpMsg(mess, strconv.Itoa(config.Port)) + + st := state{ + // lock sync.Mutex + fullRecords: resp.FullRecords, + } + + layout := manager{ + sessionID: *sessionID, + pwd: *pwd, + config: config, + s: &st, + } + g.SetManager(layout) + + if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, layout.Next); err != nil { + log.Panicln(err) + } + 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 { + log.Panicln(err) + } + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, layout.SelectExecute); err != nil { + log.Panicln(err) + } + + layout.UpdateData("") + err = g.MainLoop() + if err != nil && gocui.IsQuit(err) == false { + log.Panicln(err) + } + layout.Output() +} + +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" + blueBold := "\033[34;1m" + redBold := "\033[31;1m" + repace := []string{invert, end, blueBold, redBold} + if strings.Contains(str, prefix) == false { + return str + } + for _, escSeq := range repace { + 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 highlightMatchAlternative(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" + redBold := "\033[31;1m" + end := "\033[0m" + return redBold + 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 + pwd 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, pwd string) query { + log.Println("QUERY input = <" + queryInput + ">") + terms := strings.Fields(queryInput) + var logStr string + for _, term := range terms { + logStr += " <" + term + ">" + } + log.Println("QUERY raw terms =" + logStr) + terms = filterTerms(terms) + logStr = "" + for _, term := range terms { + logStr += " <" + term + ">" + } + log.Println("QUERY filtered terms =" + logStr) + log.Println("QUERY pwd =" + pwd) + return query{terms: terms, pwd: pwd} +} + +type item struct { + // record records.EnrichedRecord + display string + displayNoColor string + cmdLine string + pwd string + pwdTilde string + hits float64 +} + +func (i item) less(i2 item) bool { + // reversed order + return i.hits > i2.hits +} + +// used for deduplication +func (i item) key() string { + unlikelySeparator := "|||||" + return i.cmdLine + unlikelySeparator + i.pwd +} + +// 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) (item, error) { + // TODO: use color to highlight matches + const properMatchScore = 0.3 + const actualPwdScore = 0.9 + + hits := 0.0 + cmd := record.CmdLine + pwdTilde := strings.Replace(record.Pwd, record.Home, "~", 1) + pwdDisp := leftCutPadString(pwdTilde, 25) + pwdRawDisp := leftCutPadString(record.Pwd, 25) + var useRawPwd bool + for _, term := range query.terms { + alreadyHit := false + if strings.Contains(record.CmdLine, term) { + if alreadyHit == false { + hits++ + } + alreadyHit = true + if properMatch(cmd, term, " ") { + hits += properMatchScore + } + cmd = strings.ReplaceAll(cmd, term, highlightMatch(term)) + // NO continue + } + if strings.Contains(pwdTilde, term) { + if alreadyHit == false { + hits++ + } + alreadyHit = true + if properMatch(pwdTilde, term, " ") { + hits += properMatchScore + } + pwdDisp = strings.ReplaceAll(pwdDisp, term, highlightMatch(term)) + useRawPwd = false + continue // IMPORTANT + } + if strings.Contains(record.Pwd, term) { + if alreadyHit == false { + hits++ + } + alreadyHit = true + if properMatch(pwdTilde, term, " ") { + hits += properMatchScore + } + pwdRawDisp = strings.ReplaceAll(pwdRawDisp, term, highlightMatch(term)) + useRawPwd = true + continue // IMPORTANT + } + // if strings.Contains(record.GitOriginRemote, term) { + // hits++ + // } + } + // actual pwd matches + if record.Pwd == query.pwd { + hits += actualPwdScore + pwdDisp = highlightMatchAlternative(pwdDisp) + // pwdRawDisp = highlightMatchAlternative(pwdRawDisp) + useRawPwd = false + } + if hits == 0 { + return item{}, errors.New("no match for given record and query") + } + display := "" + // pwd := leftCutPadString("<"+pwdTilde+">", 20) + if useRawPwd { + display += pwdRawDisp + } else { + display += pwdDisp + } + hitsDisp := " " + rightCutPadString(strconv.Itoa(int(math.Floor(hits))), 2) + display += hitsDisp + // cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">" + cmd = strings.ReplaceAll(cmd, "\n", ";") + display += cmd + // itDummy := item{ + // cmdLine: record.CmdLine, + // pwd: record.Pwd, + // } + // + " #K:<" + itDummy.key() + ">" + + it := item{ + display: display, + displayNoColor: display, + cmdLine: record.CmdLine, + pwd: record.Pwd, + pwdTilde: pwdTilde, + hits: hits, + } + 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 + highlightedItem int + + outputBuffer string +} + +type manager struct { + sessionID string + pwd string + config cfg.Config + + s *state +} + +func (m manager) Output() { + m.s.lock.Lock() + defer m.s.lock.Unlock() + if len(m.s.outputBuffer) > 0 { + fmt.Print(m.s.outputBuffer) + } +} + +func (m manager) SelectExecute(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.outputBuffer = m.s.data[m.s.highlightedItem].cmdLine + "\n" + return gocui.ErrQuit + } + return nil +} + +func (m manager) UpdateData(input string) { + log.Println("EDIT start") + log.Println("len(fullRecords) =", len(m.s.fullRecords)) + log.Println("len(data) =", len(m.s.data)) + query := newQueryFromString(input, m.pwd) + var data []item + itemSet := make(map[string]bool) + m.s.lock.Lock() + defer m.s.lock.Unlock() + for _, rec := range m.s.fullRecords { + itm, err := newItemFromRecordForQuery(rec, query) + 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) + } + log.Println("len(tmpdata) =", len(data)) + sort.SliceStable(data, func(p, q int) bool { + return data[p].hits > data[q].hits + }) + m.s.data = nil + for _, itm := range data { + if len(m.s.data) > 420 { + break + } + m.s.data = append(m.s.data, itm) + } + m.s.highlightedItem = 0 + 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) + m.UpdateData(v.Buffer()) +} + +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 { + m.s.highlightedItem++ + } + return nil +} + +func (m manager) Prev(g *gocui.Gui, v *gocui.View) error { + m.s.lock.Lock() + defer m.s.lock.Unlock() + if m.s.highlightedItem > 0 { + m.s.highlightedItem-- + } + return nil +} + +// you can have Layout with pointer reciever if you pass the layout function to the setmanger +// I dont think we need that tho +func (m manager) Layout(g *gocui.Gui) error { + var b byte + maxX, maxY := g.Size() + + v, err := g.SetView("input", 0, 0, maxX-1, 2, b) + if err != nil && gocui.IsUnknownView(err) == false { + log.Panicln(err.Error()) + } + + v.Editable = true + // v.Editor = gocui.EditorFunc(m.editor.Edit) + v.Editor = m + v.Title = "resh cli" + + g.SetCurrentView("input") + + v, err = g.SetView("body", 0, 2, maxX-1, maxY, b) + if err != nil && gocui.IsUnknownView(err) == false { + log.Panicln(err.Error()) + } + v.Frame = false + v.Autoscroll = false + v.Clear() + v.Rewind() + + m.s.lock.Lock() + defer m.s.lock.Unlock() + for i, itm := range m.s.data { + if i == maxY { + log.Println(maxY) + break + } + displayStr := itm.display + if m.s.highlightedItem == i { + // use actual min requried length instead of 420 constant + displayStr = doHighlightString(displayStr, 420) + log.Println("### HightlightedItem string :", displayStr) + } else { + log.Println(displayStr) + } + if strings.Contains(displayStr, "\n") { + log.Println("display string contained \\n") + displayStr = strings.ReplaceAll(displayStr, "\n", "#") + } + v.WriteString(displayStr + "\n") + // if m.s.highlightedItem == i { + // v.SetHighlight(m.s.highlightedItem, true) + // } + } + log.Println("len(data) =", len(m.s.data)) + log.Println("highlightedItem =", m.s.highlightedItem) + 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 :(") + } + + 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 +} diff --git a/cmd/daemon/dump.go b/cmd/daemon/dump.go new file mode 100644 index 0000000..d8cf5a6 --- /dev/null +++ b/cmd/daemon/dump.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + + "github.com/curusarn/resh/pkg/histfile" + "github.com/curusarn/resh/pkg/msg" +) + +type dumpHandler struct { + histfileBox *histfile.Histfile +} + +func (h *dumpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if Debug { + log.Println("/dump START") + log.Println("/dump reading body ...") + } + jsn, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println("Error reading the body", err) + return + } + + mess := msg.DumpMsg{} + if Debug { + log.Println("/dump unmarshaling record ...") + } + err = json.Unmarshal(jsn, &mess) + if err != nil { + log.Println("Decoding error:", err) + log.Println("Payload:", jsn) + return + } + if Debug { + log.Println("/dump dumping ...") + } + fullRecords := h.histfileBox.DumpRecords() + if err != nil { + log.Println("Dump error:", err) + } + + resp := msg.DumpResponse{FullRecords: fullRecords.List} + jsn, err = json.Marshal(&resp) + if err != nil { + log.Println("Encoding error:", err) + return + } + w.Write(jsn) + log.Println("/dump END") +} diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go index 4525470..370b662 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -62,6 +62,7 @@ func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPa mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) mux.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) mux.Handle("/inspect", &inspectHandler{sesshistDispatch: sesshistDispatch}) + mux.Handle("/dump", &dumpHandler{histfileBox: histfileBox}) server := &http.Server{Addr: ":" + strconv.Itoa(config.Port), Handler: mux} go server.ListenAndServe() diff --git a/go.mod b/go.mod index 75c5200..c3a34e1 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.12 require ( github.com/BurntSushi/toml v0.3.1 + github.com/awesome-gocui/gocui v0.6.0 github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 + github.com/mattn/go-runewidth v0.0.8 // indirect github.com/mattn/go-shellwords v1.0.6 github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7 github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b diff --git a/go.sum b/go.sum index 3252295..430efce 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,27 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/awesome-gocui/gocui v0.6.0 h1:hhDJiQC12tEsJNJ+iZBBVaSSLFYo9llFuYpQlL5JZVI= +github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= +github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0= +github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 h1:1dSBUfGlorLAua2CRx0zFN7kQsTpE2DQSmr7rrTNgY8= github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629/go.mod h1:mb5nS4uRANwOJSZj8rlCWAfAcGi72GGMIXx+xGOjA7M= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= +github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7 h1:VsJjhYhufMGXICLwLYr8mFVMp8/A+YqmagMHnG/BA/4= diff --git a/pkg/histcli/histcli.go b/pkg/histcli/histcli.go new file mode 100644 index 0000000..105cbeb --- /dev/null +++ b/pkg/histcli/histcli.go @@ -0,0 +1,23 @@ +package histcli + +import ( + "github.com/curusarn/resh/pkg/records" +) + +// Histcli is a dump of history preprocessed for resh cli purposes +type Histcli struct { + // list of records + List []records.EnrichedRecord +} + +// New Histcli +func New() Histcli { + return Histcli{} +} + +// AddRecord to the histcli +func (h *Histcli) AddRecord(record records.Record) { + enriched := records.Enriched(record) + + h.List = append(h.List, enriched) +} diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go index d2740d6..5436ee4 100644 --- a/pkg/histfile/histfile.go +++ b/pkg/histfile/histfile.go @@ -8,6 +8,7 @@ import ( "strconv" "sync" + "github.com/curusarn/resh/pkg/histcli" "github.com/curusarn/resh/pkg/histlist" "github.com/curusarn/resh/pkg/records" ) @@ -25,6 +26,8 @@ type Histfile struct { // resh_history itself is common for both bash and zsh bashCmdLines histlist.Histlist zshCmdLines histlist.Histlist + + fullRecords histcli.Histcli } // New creates new histfile and runs its gorutines @@ -38,13 +41,24 @@ func New(input chan records.Record, sessionsToDrop chan string, historyPath: reshHistoryPath, bashCmdLines: histlist.New(), zshCmdLines: histlist.New(), + fullRecords: histcli.New(), } go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB) go hf.writer(input, signals, shutdownDone) go hf.sessionGC(sessionsToDrop) + go hf.loadFullRecords() return &hf } +// load records from resh history, reverse, enrich and save +func (h *Histfile) loadFullRecords() { + recs := records.LoadFromFile(h.historyPath, math.MaxInt32) + for i := len(recs) - 1; i >= 0; i-- { + rec := recs[i] + h.fullRecords.AddRecord(rec) + } +} + // loadsHistory from resh_history and if there is not enough of it also load native shell histories func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) { h.recentMutex.Lock() @@ -209,3 +223,9 @@ func (h *Histfile) GetRecentCmdLines(shell string, limit int) histlist.Histlist log.Println("histfile: history copied (zsh) - cmdLine count:", len(hl.List)) return hl } + +// DumpRecords returns enriched records +func (h *Histfile) DumpRecords() histcli.Histcli { + // don't forget locks in the future + return h.fullRecords +} diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index c44fd59..b0a8d29 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -1,5 +1,18 @@ package msg +import "github.com/curusarn/resh/pkg/records" + +// DumpMsg struct +type DumpMsg struct { + SessionID string `json:"sessionID"` + PWD string `json:"pwd"` +} + +// DumpResponse struct +type DumpResponse struct { + FullRecords []records.EnrichedRecord `json:"fullRecords"` +} + // InspectMsg struct type InspectMsg struct { SessionID string `json:"sessionId"` diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index f836163..12ee683 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -75,6 +75,21 @@ __resh_unbind_all() { __resh_unbind_control_R } +# wrapper for resh-cli +# meant to be launched on ctrl+R +resh() { + if resh-cli --sessionID "$__RESH_SESSION_ID" --pwd "$PWD" > ~/.resh/cli_last_run_out.txt 2>&1; then + # insert on cmdline + cat ~/.resh/cli_last_run_out.txt + eval "$(cat ~/.resh/cli_last_run_out.txt)" + # TODO: get rid of eval + else + # print errors + echo "resh-cli ERROR:" + cat ~/.resh/cli_last_run_out.txt + fi +} + reshctl() { # local log=~/.resh/reshctl.log # export current shell because resh-control needs to know