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
+