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..02f0e9c --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,241 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "sort" + "strings" + + "github.com/BurntSushi/toml" + "github.com/awesome-gocui/gocui" + "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/pkg/msg" + + "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") + + var config cfg.Config + if _, err := toml.DecodeFile(configPath, &config); err != nil { + log.Fatal("Error reading config:", err) + } + + sessionID := flag.String("sessionID", "", "resh generated session id") + flag.Parse() + + if *sessionID == "" { + fmt.Println("Error: you need to specify sessionId") + } + + 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 = false + + mess := msg.InspectMsg{SessionID: *sessionID, Count: 40} + resp := SendInspectMsg(mess, strconv.Itoa(config.Port)) + + st := state{ + // lock sync.Mutex + dataOriginal: resp.CmdLines, + data: resp.CmdLines, + } + layout := manager{ + sessionID: *sessionID, + 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) + } + + err = g.MainLoop() + if err != nil && gocui.IsQuit(err) == false { + log.Panicln(err) + } + layout.Output() +} + +// returns the number of hits for query +func queryHits(cmdline string, queryTerms []string) int { + hits := 0 + for _, term := range queryTerms { + if strings.Contains(cmdline, term) { + hits++ + } + } + return hits +} + +type state struct { + dataOriginal []string + data []string + highlightedItem int + + outputBuffer string +} + +type manager struct { + sessionID string + config cfg.Config + + s *state +} + +func (m manager) Output() { + if len(m.s.outputBuffer) > 0 { + fmt.Print(m.s.outputBuffer) + } +} + +func (m manager) SelectExecute(g *gocui.Gui, v *gocui.View) error { + if m.s.highlightedItem < len(m.s.data) { + m.s.outputBuffer = m.s.data[m.s.highlightedItem] + return gocui.ErrQuit + } + return nil +} + +func (m manager) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { + gocui.DefaultEditor.Edit(v, key, ch, mod) + query := v.Buffer() + terms := strings.Split(query, " ") + var dataHits []int + m.s.data = nil + for _, entry := range m.s.dataOriginal { + hits := queryHits(entry, terms) + if hits > 0 { + m.s.data = append(m.s.data, entry) + dataHits = append(dataHits, hits) + } + } + sort.SliceStable(m.s.data, func(p, q int) bool { + return dataHits[p] > dataHits[q] + }) + m.s.highlightedItem = 0 +} + +func (m manager) Next(g *gocui.Gui, v *gocui.View) error { + _, y := g.Size() + if m.s.highlightedItem < y { + m.s.highlightedItem++ + } + return nil +} + +func (m manager) Prev(g *gocui.Gui, v *gocui.View) error { + 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 = true + v.Clear() + for _, cmdLine := range m.s.data { + entry := strings.Trim(cmdLine, "\n") + "\n" + v.WriteString(entry) + } + if m.s.highlightedItem < len(m.s.data) { + v.SetHighlight(m.s.highlightedItem, true) + } + return nil +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +// SendInspectMsg to daemon +func SendInspectMsg(m msg.InspectMsg, port string) msg.MultiResponse { + recJSON, err := json.Marshal(m) + if err != nil { + log.Fatal("send err 1", err) + } + + req, err := http.NewRequest("POST", "http://localhost:"+port+"/inspect", + 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.MultiResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + log.Fatal("unmarshal resp error: ", err) + } + return response +} 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/scripts/reshctl.sh b/scripts/reshctl.sh index f836163..0c93377 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" > ~/.resh/cli_last_run_out.txt 2>&1; then + # insert on cmdline + echo "$(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