diff --git a/.chglog/CHANGELOG.tpl.md b/.chglog/CHANGELOG.tpl.md new file mode 100644 index 0000000..8225760 --- /dev/null +++ b/.chglog/CHANGELOG.tpl.md @@ -0,0 +1,57 @@ +# CHANGELOG + +{{ if .Versions -}} + +## [Unreleased] +{{ if .Unreleased.CommitGroups -}} +{{ range .Unreleased.CommitGroups }} +### {{ .Title }} +{{ range .Commits -}} +{{ if not (contains .Subject "") -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ if .Subject }}{{ .Subject }}{{ else }}{{ .Header }}{{ end }} +{{ end -}} +{{ end -}} +{{ end }} +{{ else }} +{{ range .Unreleased.Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ if .Subject }}{{ .Subject }}{{ else }}{{ .Header }}{{ end }} +{{ end }} +{{ end -}} + +{{- if .Unreleased.NoteGroups -}} +{{ range .Unreleased.NoteGroups -}} +### {{ .Title }} +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} + +{{ range .Versions }} + +## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} +{{ if .CommitGroups -}} +{{ range .CommitGroups }} +### {{ .Title }} +{{ range .Commits -}} +{{ if not (contains .Subject "") -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ if .Subject }}{{ .Subject }}{{ else }}{{ .Header }}{{ end }} +{{ end -}} +{{ end -}} +{{ end }} +{{ else }} +{{ range .Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ if .Subject }}{{ .Subject }}{{ else }}{{ .Header }}{{ end }} +{{ end }} +{{ end -}} + +{{- if .NoteGroups -}} +{{ range .NoteGroups -}} +### {{ .Title }} +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} diff --git a/.chglog/config.yml b/.chglog/config.yml new file mode 100644 index 0000000..d6575ee --- /dev/null +++ b/.chglog/config.yml @@ -0,0 +1,76 @@ +style: github +template: CHANGELOG.tpl.md +info: + title: CHANGELOG + repository_url: https://github.com/lzambarda/hbt + +options: + commits: + # filters: + # Type: + # - feat + # - add + # - return + # - feature + # - fix + # - perf + # - refactor + + commit_groups: + # group_by: Type + + title_maps: + feat: Added + Feat: Added + add: Added + Add: Added + feature: New Feature + Feature: New Feature + fix: Fixed + Fix: Fixed + merge: Merged + Merge: Merged + perf: Performance Improvements + Perf: Performance Improvements + Performance Improvements: Performance Improvements + refactor: Code Refactoring + Refactor: Code Refactoring + Update: Code Refactoring + + header: + # { header } + # {type}{scope} { subject } + # add(core): new feature to the thing (closes #666) + # We are ignoring paranteses around the scope. + pattern: "^((\\w+)(\\((\\w+)\\))?:?\\s(.+))$" + pattern_maps: + - Subject + - Type + - + - Scope + + issues: + prefix: + - # + + refs: + actions: + - Closes + - Fixes + - Refs + + merges: + pattern: "^Merge branch '(\\w+)'$" + pattern_maps: + - Source + + reverts: + pattern: "^Revert \"([\\s\\S]*)\"$" + pattern_maps: + - Header + + notes: + keywords: + - BREAKING CHANGE + - IMPORTANT + - important diff --git a/Makefile b/Makefile index 181e756..03c4230 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,20 @@ +.PHONY: help +help: ## Show this + @grep -E '^[0-9a-zA-Z_-]+:(.*?## .*|[a-z _0-9]+)?$$' Makefile | sort | awk 'BEGIN {FS = ":(.*## |[\t ]*)"}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + NAME:=hbtsrv BUILD_TAG:=$(shell git describe --tags) -BUILDFLAGS:="-s -w -X internal.Version=$(BUILD_TAG)" +BUILDFLAGS:="-s -w -X github.com/lzambarda/hbt/internal.Version=$(BUILD_TAG)" + +.PHONY: dependencies +dependencies: ## Install dependencies requried for development operations. + @go get -u github.com/git-chglog/git-chglog/cmd/git-chglog + @go mod tidy + .PHONY: build build: - go build -ldflags $(BUILDFLAGS) -o ./bin/$(NAME) ./main.go + @go build -ldflags $(BUILDFLAGS) -o ./bin/$(NAME) ./main.go run="." dir="./..." @@ -13,3 +23,13 @@ short="-short" test: @go test --timeout=40s $(short) $(dir) -run $(run); +.PHONY: changelog +changelog: ## Update the changelog. + @git-chglog > CHANGELOG.md + @echo "Changelog has been updated." + + +.PHONY: changelog_release +changelog_release: ## Update the changelog with a release tag. + @git-chglog --next-tag $(tag) > CHANGELOG.md + @echo "Changelog has been updated." diff --git a/README.md b/README.md index f881838..8de7fe0 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,30 @@ A heuristic command suggestion system for zsh. 2. Amend the environment variables in `zsh/hbt.zsh` to match where you store hbt (fancy the PATH?) 3. Make sure that `zsh/hbt.zsh` is loaded by your shell. +## Development + +There is a `--debug` flag (or `HBT_DEBUG` env var) which can be used to print extra information. +By default the TCP server runs in the foreground. If you want to work on the same terminal you can use something like `nohup`. +Check out [`zsh/hbt.zsh`](./zsh/hbt.zsh) for an implementation of it. + +You can change `HBT_BIN_PATH` to point to the binary in this repo. +It is usually better to run the built binary otherwise `hbt_stop` won't be able to find the running process and terminate it. + ## Usage The zsh bit of hbt talks to a locally spawned TCP server handled by a go binary. Hbt will track every command that you type and store it into a graph. Upon pressing TAB with an empty prompty buffer, it will try to hint at a good command, according to your typing history. Shrugs otherwise (seriously). +### Manual interaction with hbt + +[`zsh/hbt.zsh`](./zsh/hbt.zsh) provides some functions you can use to interact with a running hbt server. + +## Why the mix of go and shell functions? + +At first I wanted to developed the whole thing in go, but for tracking and hinting I couldn't find an implementation faster than pure shell commands. +Given that the functions use zsh hooks which are executed at every command, I didn't want this to have a too significant impact on the terminal experience. + ## Todo - [x] Naive graph implementation @@ -22,9 +40,10 @@ Upon pressing TAB with an empty prompty buffer, it will try to hint at a good co This hasn't really been done the proper way, but it works. - [x] Tests -- [ ] Migrate what can be migrated from zsh to go +- [x] Migrate what can be migrated from zsh to go - [ ] Benchmarking -- [ ] R/B tree implementation??? +- [ ] R/B tree / ngram tree implementation??? - [x] Partial path search - [ ] Better error catching - [ ] More dynamic graph parameters (env variables or flags) +- [ ] Do not store sensistive information (is it even possible to detect it?) diff --git a/client/client.go b/client/client.go deleted file mode 100644 index a44c721..0000000 --- a/client/client.go +++ /dev/null @@ -1,59 +0,0 @@ -package client - -import ( - "fmt" - "net" - "os/exec" - "strings" - - "github.com/lzambarda/hbt/internal" -) - -func send(msg string, read bool) (string, error) { - addr, err := net.ResolveTCPAddr("tcp", "localhost:"+internal.Port) - if err != nil { - return "", err - } - conn, err := net.DialTCP("tcp", nil, addr) - if err != nil { - return "", err - } - defer conn.Close() - _, err = conn.Write([]byte(msg)) - if err != nil { - return "", err - } - err = conn.CloseWrite() - if err != nil { - return "", err - } - if !read { - return "", nil - } - buffer := make([]byte, 1024) - _, err = conn.Read(buffer) - return string(buffer), err -} - -func SendStop() error { - return exec.Command("sh", "-c", "kill -TERM $(pgrep hbtsrv)").Run() -} - -func SendTrack(args []string) error { - _, err := send(strings.Join(append([]string{"track"}, args...), "\n"), false) - return err -} - -func SendHint(args []string) error { - msg, err := send(strings.Join(append([]string{"hint"}, args...), "\n"), true) - if err != nil { - return err - } - fmt.Print(msg) - return nil -} - -func SendEnd(args []string) error { - _, err := send(strings.Join(append([]string{"end"}, args...), "\n"), false) - return err -} diff --git a/cmd/cmd.go b/cmd/cmd.go index 37205d0..5890559 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,11 +1,8 @@ package cmd import ( - "os" "path" - "sort" - "github.com/lzambarda/hbt/client" "github.com/lzambarda/hbt/graph/naive" "github.com/lzambarda/hbt/internal" "github.com/lzambarda/hbt/server" @@ -15,85 +12,46 @@ import ( var ( root = &cli.App{ Name: "hbt", - Description: "a zsh suggestion system", + Usage: "a zsh suggestion system", + Description: `Running hbt will spawn a TCP server listening on the local port 43111 (can be changed with HBT_PORT).`, Version: internal.Version, - Flags: []cli.Flag{&cli.BoolFlag{ - Name: "debug", - Aliases: []string{"d"}, - DefaultText: "true", - Value: true, - Destination: &internal.Debug, - }, - &cli.StringFlag{ - Name: "port", - Aliases: []string{"p"}, - DefaultText: internal.DefaultPort, - Value: internal.DefaultPort, - Destination: &internal.Port, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Aliases: []string{"d"}, + DefaultText: "false", + Destination: &internal.Debug, + EnvVars: []string{internal.DebugName}, }, &cli.StringFlag{ Name: "cache", Aliases: []string{"c"}, DefaultText: "binary location", - Value: ".", + Value: internal.DefaultCachePath, Destination: &internal.CachePath, - }}, - Commands: []*cli.Command{ - { - Name: "start", - Description: "start the TCP server", - Usage: "start", - Action: func(_ *cli.Context) error { - cachePath := path.Join(internal.CachePath, internal.CacheName) - g := naive.NewGraph(10, 3) - err := g.Load(cachePath) - if err != nil { - return err - } - return server.Start(g, cachePath) - }, - }, - { - Name: "stop", - Description: "stop the TCP server", - Usage: "stop", - Action: func(c *cli.Context) error { - return client.SendStop() - }, - }, - { - Name: "track", - Description: "track the executed command and directory", - Usage: "track ", - Action: func(c *cli.Context) error { - return client.SendTrack(c.Args().Slice()) - }, - }, - { - Name: "hint", - Description: "try to suggest a command to execute given the tracked history", - Usage: "hint ", - Action: func(c *cli.Context) error { - return client.SendHint(c.Args().Slice()) - }, + EnvVars: []string{internal.CachePathName}, }, - { - Name: "end", - Description: "end a tracking session", - Usage: "end ", - Action: func(c *cli.Context) error { - return client.SendEnd(c.Args().Slice()) - }, + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + DefaultText: internal.DefaultPort, + Value: internal.DefaultPort, + Destination: &internal.Port, + EnvVars: []string{internal.PortName}, }, }, + Action: func(_ *cli.Context) error { + cachePath := path.Join(internal.CachePath, internal.CacheName) + g := naive.NewGraph(10, 3) + err := g.Load(cachePath) + if err != nil { + return err + } + return server.Start(g, cachePath) + }, } ) -func init() { - sort.Sort(cli.FlagsByName(root.Flags)) - sort.Sort(cli.CommandsByName(root.Commands)) -} - func Run(arguments []string) error { - return root.Run(os.Args) + return root.Run(arguments) } diff --git a/cmd/errors.go b/cmd/errors.go index 9ad3557..f00a495 100644 --- a/cmd/errors.go +++ b/cmd/errors.go @@ -16,5 +16,5 @@ func NewErrUnrecognisedCommand(cmd string) error { } func NewErrWrongUsage(correct string) error { - return fmt.Errorf("%w: correct %s", correct) + return fmt.Errorf("%w: correct %s", ErrWrongUsage, correct) } diff --git a/go.mod b/go.mod index b976863..bc2c405 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/lzambarda/hbt go 1.16 require ( + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/go.sum b/go.sum index 9680a61..d0d8670 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,15 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= diff --git a/graph/naive/naive.go b/graph/naive/naive.go index 50ffc0e..8943c3f 100644 --- a/graph/naive/naive.go +++ b/graph/naive/naive.go @@ -133,6 +133,9 @@ const shrug = "¯\\_(ツ)_/¯" func (g *Graph) Hint(id, wd string) string { if _, ok := g.Nodes[wd]; !ok { // Try to see if we have a node with a similar structure + if strings.HasPrefix(wd, "/") { + wd = wd[1:] + } pathComponents := strings.Split(wd, "/") if len(pathComponents) > g.MinCommonPath { // Reduce the path to the common path and check again diff --git a/graph/naive/naive_test.go b/graph/naive/naive_test.go index 1fe4467..a2f950f 100644 --- a/graph/naive/naive_test.go +++ b/graph/naive/naive_test.go @@ -80,6 +80,9 @@ func TestNaiveHintBasic(t *testing.T) { got := g.Hint(id, wd1) assert.Equal(t, shrug, got, "no matching node, no tracking info") + got = g.Hint(id, "d1/d2/d3/d4") // shold be longer than minCommonPath + assert.Equal(t, shrug, got, "no matching node, no tracking info, long path") + cmd1 := "c1" g.Track(id, wd1, cmd1) got = g.Hint(id, wd1) @@ -96,7 +99,7 @@ func TestNaiveHintBasic(t *testing.T) { cmd2 := "c2" g.Track(id, wd1, cmd2) got = g.Hint(id, wd1) - assert.Equal(t, cmd1, got, "two commands, same hits") + assert.Contains(t, []string{cmd1, cmd2}, got, "two commands, same hits, random result") g.Track(id, wd1, cmd2) got = g.Hint(id, wd1) diff --git a/internal/internal.go b/internal/internal.go index 71fccfe..db36ea9 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -1,17 +1,26 @@ package internal const ( + DebugName = "HBT_DEBUG" + CachePathName = "HBT_CACHE" + PortName = "HBT_PORT" +) + +const ( + // Found by looking at unused ports at: + // https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers DefaultPort = "43111" DefaultCachePath = "." ) var ( - Port string Debug bool CachePath string + Port string + // Must be var, otherwise -X flag can't modify it + Version = "unknown" ) const ( - Version string = "unknown" - CacheName string = ".hbtcache" + CacheName = ".hbtcache" ) diff --git a/server/server.go b/server/server.go index 7143b20..4737d0c 100644 --- a/server/server.go +++ b/server/server.go @@ -73,10 +73,11 @@ func handleConnection(c net.Conn, g graph.Graph) { } g.Track(args[1], args[2], args[3]) case "hint": - if len(args) != 3 { - fmt.Printf("wrong number of arguments, expected 3, got %d\n", len(args)) + if len(args) != 4 { + fmt.Printf("wrong number of arguments, expected 4, got %d\n", len(args)) return } + // args[3] is unused for now c.Write([]byte(g.Hint(args[1], args[2]))) case "end": if len(args) != 2 { diff --git a/zsh/hbt.zsh b/zsh/hbt.zsh index 4c33ea8..5a6055e 100644 --- a/zsh/hbt.zsh +++ b/zsh/hbt.zsh @@ -1,18 +1,22 @@ +# To be able to use zsh hooks autoload -Uz add-zsh-hook -HBT_BIN_PATH=~/Repositories/hbt/bin/hbtsrv +export PATH="$HOME/Repositories/hbt/bin:$PATH" +HBT_CACHE_PATH=~/dotfiles/hbt/ +HBT_PORT=43111 function hbt_start() { pid=$(pgrep hbtsrv) if [ -z $pid ]; then echo "starting hbt, you can stop it with: hbt_stop" if [ "$1" = "--debug" ]; then - $HBT_BIN_PATH --cache $HBT_CACHE_PATH --debug true + hbtsrv --debug else - nohup $HBT_BIN_PATH --cache $HBT_CACHE_PATH --debug false >/dev/null 2>&1 & + nohup hbtsrv >/dev/null 2>&1 & fi fi } + function hbt_stop() { pid=$(pgrep hbtsrv) if [ ! -z $pid ]; then @@ -25,30 +29,25 @@ if [ -z $(pgrep hbtsrv) ]; then hbt_start fi -function _hbt_exit() { - $HBT_BIN_PATH exit $$ - #echo -n "$$\nexit\n\n" | nc localhost $HBT_PORT -} -add-zsh-hook zshexit _hbt_exit +function _hbt_end_session() { echo -n "end\n$$" | nc localhost $HBT_PORT ; } +add-zsh-hook zshexit _hbt_end_session -function _hbt_track () { - $HBT_BIN_PATH track $$ $(pwd) $1 - #echo -n "$$\ntrack\n$(pwd)\n$1" | nc localhost $HBT_PORT -} +function _hbt_track () { echo -n "track\n$$\n$(pwd)\n$1" | nc localhost $HBT_PORT ; } add-zsh-hook preexec _hbt_track # list dir with TAB, when there are only spaces/no text before cursor, # or complete words, that are before cursor only (like in tcsh) function _hbt_search () { if [[ -z ${LBUFFER// } ]]; then - suggestion=$($HBT_BIN_PATH hint $$ $(pwd) $1) - #suggestion=$(echo -n "$$\nhint\n$(pwd)\n$1" | nc localhost $HBT_PORT) + suggestion=$(echo -n "hint\n$$\n$(pwd)\n$1" | nc localhost $HBT_PORT) POSTDISPLAY="${suggestion#$BUFFER}" _zsh_autosuggest_highlight_apply else zle expand-or-complete-prefix; fi } +zle -N _hbt_search +bindkey '^I' _hbt_search function _hbt_clear() { if [[ -z ${LBUFFER// } ]]; then @@ -57,10 +56,6 @@ function _hbt_clear() { zle backward-delete-char fi } - -# https://unix.stackexchange.com/questions/289883/binding-key-shortcuts-to-shell-functions-in-zsh -zle -N _hbt_search zle -N _hbt_clear - -bindkey '^I' _hbt_search bindkey '^?' _hbt_clear +