Merge pull request #58 from curusarn/feature_resh_hstr

Feature resh cli
pull/77/head v2.5.0
Šimon Let 6 years ago committed by GitHub
commit 6c57306964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Makefile
  2. 559
      cmd/cli/main.go
  3. 54
      cmd/daemon/dump.go
  4. 1
      cmd/daemon/run-server.go
  5. 2
      go.mod
  6. 9
      go.sum
  7. 23
      pkg/histcli/histcli.go
  8. 20
      pkg/histfile/histfile.go
  9. 13
      pkg/msg/msg.go
  10. 15
      scripts/reshctl.sh

@ -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\ 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 install: build conf/config-dev.toml
scripts/install.sh scripts/install.sh

@ -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
}

@ -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")
}

@ -62,6 +62,7 @@ func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPa
mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers})
mux.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) mux.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch})
mux.Handle("/inspect", &inspectHandler{sesshistDispatch: sesshistDispatch}) mux.Handle("/inspect", &inspectHandler{sesshistDispatch: sesshistDispatch})
mux.Handle("/dump", &dumpHandler{histfileBox: histfileBox})
server := &http.Server{Addr: ":" + strconv.Itoa(config.Port), Handler: mux} server := &http.Server{Addr: ":" + strconv.Itoa(config.Port), Handler: mux}
go server.ListenAndServe() go server.ListenAndServe()

@ -4,7 +4,9 @@ go 1.12
require ( require (
github.com/BurntSushi/toml v0.3.1 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/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/mattn/go-shellwords v1.0.6
github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7 github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7
github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b

@ -1,18 +1,27 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/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/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-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/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/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/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/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/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 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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 h1:1dSBUfGlorLAua2CRx0zFN7kQsTpE2DQSmr7rrTNgY8=
github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629/go.mod h1:mb5nS4uRANwOJSZj8rlCWAfAcGi72GGMIXx+xGOjA7M= 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/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 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= 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= github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7 h1:VsJjhYhufMGXICLwLYr8mFVMp8/A+YqmagMHnG/BA/4=

@ -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)
}

@ -8,6 +8,7 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/curusarn/resh/pkg/histcli"
"github.com/curusarn/resh/pkg/histlist" "github.com/curusarn/resh/pkg/histlist"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/pkg/records"
) )
@ -25,6 +26,8 @@ type Histfile struct {
// resh_history itself is common for both bash and zsh // resh_history itself is common for both bash and zsh
bashCmdLines histlist.Histlist bashCmdLines histlist.Histlist
zshCmdLines histlist.Histlist zshCmdLines histlist.Histlist
fullRecords histcli.Histcli
} }
// New creates new histfile and runs its gorutines // New creates new histfile and runs its gorutines
@ -38,13 +41,24 @@ func New(input chan records.Record, sessionsToDrop chan string,
historyPath: reshHistoryPath, historyPath: reshHistoryPath,
bashCmdLines: histlist.New(), bashCmdLines: histlist.New(),
zshCmdLines: histlist.New(), zshCmdLines: histlist.New(),
fullRecords: histcli.New(),
} }
go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB) go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB)
go hf.writer(input, signals, shutdownDone) go hf.writer(input, signals, shutdownDone)
go hf.sessionGC(sessionsToDrop) go hf.sessionGC(sessionsToDrop)
go hf.loadFullRecords()
return &hf 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 // 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) { func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) {
h.recentMutex.Lock() 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)) log.Println("histfile: history copied (zsh) - cmdLine count:", len(hl.List))
return hl return hl
} }
// DumpRecords returns enriched records
func (h *Histfile) DumpRecords() histcli.Histcli {
// don't forget locks in the future
return h.fullRecords
}

@ -1,5 +1,18 @@
package msg 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 // InspectMsg struct
type InspectMsg struct { type InspectMsg struct {
SessionID string `json:"sessionId"` SessionID string `json:"sessionId"`

@ -75,6 +75,21 @@ __resh_unbind_all() {
__resh_unbind_control_R __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() { reshctl() {
# local log=~/.resh/reshctl.log # local log=~/.resh/reshctl.log
# export current shell because resh-control needs to know # export current shell because resh-control needs to know

Loading…
Cancel
Save