diff --git a/.travis.yml b/.travis.yml index 5d5f8d2..776f709 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.9 + - 1.11 - master script: @@ -12,3 +12,7 @@ matrix: fast_finish: true allow_failures: - go: master + +env: + global: + - GO111MODULE=on diff --git a/README.md b/README.md index 8f409f3..8fc1f23 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,26 @@ To trigger a case-sensitive search, use a term that has a capital letter. The jump will resolve to `/Users/genadi/Development` even if there is `/Users/genadi/Development/dev-tools` that scores better. +## Is it like autojump or z? + +Yes, it is! You can import your datafile from `autojump` or `z` with: + +```bash +$ jump import +``` + +This will try `z` first then `autojump`, so you can even combine all the +entries from both tools. + +The command is safe to run on pre-existing jump database, because if an entry +exist in jump already, it won't be imported and it's score will remain +unchanged. You can be explicit and choose to import `autojump` or `z` with: + +```bash +$ jump import autojump +$ jump import z +``` + ## Installation Jump comes in packages for macOS through homebrew and linux. diff --git a/cmd/cd_test.go b/cmd/cd_test.go index 3442247..eee1139 100644 --- a/cmd/cd_test.go +++ b/cmd/cd_test.go @@ -8,11 +8,12 @@ import ( "github.com/gsamokovarov/assert" "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" s "github.com/gsamokovarov/jump/scoring" ) func Test_cdCmd(t *testing.T) { - conf := &testConfig{ + conf := &config.Testing{ Entries: s.Entries{ &s.Entry{p.Join(td, "web-console"), &s.Score{Weight: 100, Age: s.Now}}, &s.Entry{p.Join(td, "/client/website"), &s.Score{Weight: 90, Age: s.Now}}, @@ -27,7 +28,7 @@ func Test_cdCmd(t *testing.T) { } func Test_cdCmd_noEntries(t *testing.T) { - conf := &testConfig{} + conf := &config.Testing{} output := capture(&os.Stderr, func() { assert.Nil(t, cdCmd(cli.Args{"wc"}, conf)) @@ -37,7 +38,7 @@ func Test_cdCmd_noEntries(t *testing.T) { } func Test_cdCmd_multipleArgumentsAsSeparators(t *testing.T) { - conf := &testConfig{ + conf := &config.Testing{ Entries: s.Entries{ &s.Entry{p.Join(td, "web-console"), &s.Score{Weight: 100, Age: s.Now}}, &s.Entry{p.Join(td, "/client/website"), &s.Score{Weight: 90, Age: s.Now}}, @@ -52,7 +53,7 @@ func Test_cdCmd_multipleArgumentsAsSeparators(t *testing.T) { } func Test_cdCmd_absolutePath(t *testing.T) { - conf := &testConfig{ + conf := &config.Testing{ Entries: s.Entries{ &s.Entry{p.Join(td, "web-console"), &s.Score{Weight: 100, Age: s.Now}}, &s.Entry{p.Join(td, "/client/website"), &s.Score{Weight: 90, Age: s.Now}}, @@ -71,7 +72,7 @@ func Test_cdCmd_exactMatch(t *testing.T) { p2 := p.Join(td, "/client/website") p3 := p.Join(td, "web") - conf := &testConfig{ + conf := &config.Testing{ Entries: s.Entries{ &s.Entry{p1, &s.Score{Weight: 100, Age: s.Now}}, &s.Entry{p2, &s.Score{Weight: 90, Age: s.Now}}, diff --git a/cmd/chdir_test.go b/cmd/chdir_test.go index aba2194..ecf0195 100644 --- a/cmd/chdir_test.go +++ b/cmd/chdir_test.go @@ -7,13 +7,14 @@ import ( "github.com/gsamokovarov/assert" "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" ) func Test_chdirCmd(t *testing.T) { p1 := p.Join(td, "web-console") p2 := p.Join(td, "/client/website") - conf := &testConfig{} + conf := &config.Testing{} entries, err := conf.ReadEntries() assert.Nil(t, err) @@ -47,7 +48,7 @@ func Test_chdirCmd(t *testing.T) { } func Test_chdirCmd_cwd(t *testing.T) { - conf := &testConfig{} + conf := &config.Testing{} entries, err := conf.ReadEntries() assert.Nil(t, err) diff --git a/cmd/clean_test.go b/cmd/clean_test.go index 8aea87e..f80013f 100644 --- a/cmd/clean_test.go +++ b/cmd/clean_test.go @@ -7,10 +7,11 @@ import ( "github.com/gsamokovarov/assert" "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" ) func Test_cleanCmd(t *testing.T) { - conf := &testConfig{} + conf := &config.Testing{} assert.Nil(t, chdirCmd(cli.Args{"/inexistent/dir/dh891n2kisdha"}, conf)) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index ae87d1f..2130d2d 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -4,57 +4,10 @@ import ( "io/ioutil" "os" "path" - - "github.com/gsamokovarov/jump/config" - "github.com/gsamokovarov/jump/scoring" ) var td string -type testConfig struct { - Entries scoring.Entries - Search config.Search - Pins map[string]string - Pin string -} - -func (c *testConfig) ReadEntries() (scoring.Entries, error) { - return c.Entries, nil -} - -func (c *testConfig) WriteEntries(entries scoring.Entries) error { - c.Entries = entries - return nil -} - -func (c *testConfig) ReadSearch() config.Search { - return c.Search -} - -func (c *testConfig) WriteSearch(term string, index int) error { - c.Search.Term = term - c.Search.Index = index - return nil -} - -func (c *testConfig) ReadPins() (map[string]string, error) { - return c.Pins, nil -} - -func (c *testConfig) FindPin(term string) (string, bool) { - return c.Pin, c.Pin != "" -} - -func (c *testConfig) WritePin(_, value string) error { - c.Pin = value - return nil -} - -func (c *testConfig) RemovePin(term string) error { - c.Pin = "" - return nil -} - func capture(stream **os.File, fn func()) string { rescue := *stream r, w, _ := os.Pipe() diff --git a/cmd/forget_test.go b/cmd/forget_test.go index a669e9f..99d3ce9 100644 --- a/cmd/forget_test.go +++ b/cmd/forget_test.go @@ -8,13 +8,14 @@ import ( "github.com/gsamokovarov/assert" "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" "github.com/gsamokovarov/jump/scoring" ) func Test_forgetCmd(t *testing.T) { p := p.Join(td, "web-console") - conf := &testConfig{ + conf := &config.Testing{ Entries: scoring.Entries{scoring.NewEntry(p)}, } diff --git a/cmd/help_test.go b/cmd/help_test.go index 0027c60..60fd702 100644 --- a/cmd/help_test.go +++ b/cmd/help_test.go @@ -18,6 +18,7 @@ func Example_helpCmd() { // clean Cleans the database of inexisting entries. // forget Removes the current directory from the database. // hint Hints relevant paths for jumping. + // import Import autojump or z scores. // pin Pin a directory to a search term. // pins Lists all the pinned search terms. // shell Display a shell integration script. diff --git a/cmd/hint_test.go b/cmd/hint_test.go index 19578cb..04e8b25 100644 --- a/cmd/hint_test.go +++ b/cmd/hint_test.go @@ -8,6 +8,7 @@ import ( "github.com/gsamokovarov/assert" "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" s "github.com/gsamokovarov/jump/scoring" ) @@ -15,7 +16,7 @@ func Test_hintCmd_short(t *testing.T) { p1 := p.Join(td, "web-console") p2 := p.Join(td, "/client/website") - conf := &testConfig{ + conf := &config.Testing{ Entries: s.Entries{ &s.Entry{p2, &s.Score{Weight: 90, Age: s.Now}}, &s.Entry{p1, &s.Score{Weight: 100, Age: s.Now}}, @@ -33,7 +34,7 @@ func Test_hintCmd_short(t *testing.T) { } func Test_hintCmd_noEntries(t *testing.T) { - conf := &testConfig{} + conf := &config.Testing{} output := capture(&os.Stdout, func() { assert.Nil(t, hintCmd(cli.Args{"webcons"}, conf)) diff --git a/cmd/import.go b/cmd/import.go new file mode 100644 index 0000000..b1bcb39 --- /dev/null +++ b/cmd/import.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" + "github.com/gsamokovarov/jump/importer" + "github.com/gsamokovarov/jump/scoring" +) + +func importCmd(args cli.Args, conf config.Config) error { + imp := importer.Guess(args.CommandName(), conf) + + return imp.Import(func(entry *scoring.Entry) { + cli.Outf("Importing %s\n", entry.Path) + }) +} + +func init() { + cli.RegisterCommand("import", "Import autojump or z scores.", importCmd) +} diff --git a/cmd/import_test.go b/cmd/import_test.go new file mode 100644 index 0000000..d7964a6 --- /dev/null +++ b/cmd/import_test.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "os" + "testing" + + "github.com/gsamokovarov/assert" + "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" +) + +func Test_importCmd_autojump(t *testing.T) { + oldHOME := os.Getenv("HOME") + defer os.Setenv("HOME", oldHOME) + + os.Setenv("HOME", td) + + conf := &config.Testing{} + + output := capture(&os.Stdout, func() { + assert.Nil(t, importCmd(cli.Args{"autojump"}, conf)) + }) + + assert.Equal(t, `Importing /Users/genadi/Development/jump +Importing /Users/genadi/Development/mock_last_status +Importing /Users/genadi/Development +Importing /Users/genadi/.go/src/github.com/gsamokovarov/jump +Importing /usr/local/Cellar/autojump +Importing /Users/genadi/Development/gloat +`, output) + + assert.Len(t, 6, conf.Entries) +} + +func Test_importCmd_z(t *testing.T) { + oldHOME := os.Getenv("HOME") + defer os.Setenv("HOME", oldHOME) + + os.Setenv("HOME", td) + + conf := &config.Testing{} + + output := capture(&os.Stdout, func() { + assert.Nil(t, importCmd(cli.Args{"z"}, conf)) + }) + + assert.Equal(t, `Importing /Users/genadi/Development/hack +Importing /Users/genadi/Development/masse +Importing /Users/genadi/Development +Importing /Users/genadi/.go/src/github.com/gsamokovarov/jump +`, output) + + assert.Len(t, 4, conf.Entries) +} + +func Test_importCmd_itALL(t *testing.T) { + oldHOME := os.Getenv("HOME") + defer os.Setenv("HOME", oldHOME) + + os.Setenv("HOME", td) + + conf := &config.Testing{} + + output := capture(&os.Stdout, func() { + assert.Nil(t, importCmd(cli.Args{""}, conf)) + }) + + assert.Equal(t, `Importing /Users/genadi/Development/hack +Importing /Users/genadi/Development/masse +Importing /Users/genadi/Development +Importing /Users/genadi/.go/src/github.com/gsamokovarov/jump +Importing /Users/genadi/Development/jump +Importing /Users/genadi/Development/mock_last_status +Importing /usr/local/Cellar/autojump +Importing /Users/genadi/Development/gloat +`, output) + + assert.Len(t, 8, conf.Entries) +} diff --git a/cmd/pin_test.go b/cmd/pin_test.go index a44b68a..f664f70 100644 --- a/cmd/pin_test.go +++ b/cmd/pin_test.go @@ -7,6 +7,7 @@ import ( "github.com/gsamokovarov/assert" "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" s "github.com/gsamokovarov/jump/scoring" ) @@ -14,7 +15,7 @@ func Test_pinCmd(t *testing.T) { p1 := p.Join(td, "web") p2 := p.Join(td, "web-console") - conf := &testConfig{ + conf := &config.Testing{ Entries: s.Entries{ &s.Entry{p2, &s.Score{Weight: 1, Age: s.Now}}, &s.Entry{p1, &s.Score{Weight: 100, Age: s.Now}}, @@ -34,7 +35,7 @@ func Test_pinCmd_normalizedTerms(t *testing.T) { p1 := p.Join(td, "web") p2 := p.Join(td, "web-console") - conf := &testConfig{ + conf := &config.Testing{ Entries: s.Entries{ &s.Entry{p2, &s.Score{Weight: 1, Age: s.Now}}, &s.Entry{p1, &s.Score{Weight: 100, Age: s.Now}}, diff --git a/cmd/pins_test.go b/cmd/pins_test.go index 489b128..abdce9f 100644 --- a/cmd/pins_test.go +++ b/cmd/pins_test.go @@ -5,10 +5,11 @@ import ( "testing" "github.com/gsamokovarov/assert" + "github.com/gsamokovarov/jump/config" ) func Test_pinsCmd(t *testing.T) { - conf := &testConfig{ + conf := &config.Testing{ Pins: map[string]string{ "r": "/home/user/projects/rails", }, diff --git a/cmd/testdata/.local/share/autojump/autojump.txt b/cmd/testdata/.local/share/autojump/autojump.txt new file mode 100644 index 0000000..1d4e20c --- /dev/null +++ b/cmd/testdata/.local/share/autojump/autojump.txt @@ -0,0 +1,6 @@ +38.729833462 /Users/genadi/Development/jump +14.1421356237 /Users/genadi/Development/mock_last_status +33.1662479035 /Users/genadi/Development +14.1421356237 /Users/genadi/.go/src/github.com/gsamokovarov/jump +43.5889894353 /usr/local/Cellar/autojump +20.0 /Users/genadi/Development/gloat diff --git a/cmd/testdata/.z b/cmd/testdata/.z new file mode 100644 index 0000000..88ffd8f --- /dev/null +++ b/cmd/testdata/.z @@ -0,0 +1,4 @@ +/Users/genadi/Development/hack|11|1536272816 +/Users/genadi/Development/masse|1|1536272502 +/Users/genadi/Development|3|1536272506 +/Users/genadi/.go/src/github.com/gsamokovarov/jump|1|1536272492 diff --git a/cmd/top_test.go b/cmd/top_test.go index a232306..8418565 100644 --- a/cmd/top_test.go +++ b/cmd/top_test.go @@ -8,6 +8,7 @@ import ( "github.com/gsamokovarov/assert" "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" s "github.com/gsamokovarov/jump/scoring" ) @@ -15,7 +16,7 @@ func Test_topCmd(t *testing.T) { wc := p.Join(td, "web-console") web := p.Join(td, "/client/website") - conf := &testConfig{ + conf := &config.Testing{ Entries: s.Entries{ &s.Entry{wc, &s.Score{Weight: 100, Age: s.Now}}, &s.Entry{web, &s.Score{Weight: 90, Age: s.Now}}, diff --git a/cmd/unpin_test.go b/cmd/unpin_test.go index 9923f1b..78937b5 100644 --- a/cmd/unpin_test.go +++ b/cmd/unpin_test.go @@ -7,6 +7,7 @@ import ( "github.com/gsamokovarov/assert" "github.com/gsamokovarov/jump/cli" + "github.com/gsamokovarov/jump/config" s "github.com/gsamokovarov/jump/scoring" ) @@ -14,7 +15,7 @@ func Test_unpinCmd(t *testing.T) { p1 := p.Join(td, "web") p2 := p.Join(td, "web-console") - conf := &testConfig{ + conf := &config.Testing{ Entries: s.Entries{ &s.Entry{p2, &s.Score{Weight: 1, Age: s.Now}}, &s.Entry{p1, &s.Score{Weight: 100, Age: s.Now}}, diff --git a/config/atom/atom.go b/config/atom/atom.go index 021613d..fd76ee8 100644 --- a/config/atom/atom.go +++ b/config/atom/atom.go @@ -52,8 +52,10 @@ func Open(name string) (File, error) { } type file struct { - to string - tmp *os.File + to string + tmp *os.File + // dirty indicates whether this tmpfile has experienced errors which mean it + // should not be used to update the original. dirty bool } diff --git a/config/config.go b/config/config.go index cc7c5d5..7a4b4df 100644 --- a/config/config.go +++ b/config/config.go @@ -71,10 +71,24 @@ func normalizeDir(dir string) (string, error) { return dir, nil } - usr, err := user.Current() + home, err := homeDir() if err != nil { return dir, err } - return filepath.Join(usr.HomeDir, defaultDirName), nil + return filepath.Join(home, defaultDirName), nil +} + +// See https://github.com/golang/go/issues/26463 +func homeDir() (string, error) { + home := os.Getenv("HOME") + if home != "" { + return home, nil + } + usr, err := user.Current() + if err != nil { + return "", err + } + + return usr.HomeDir, nil } diff --git a/config/entries.go b/config/entries.go index 5ee2c6f..b1b6406 100644 --- a/config/entries.go +++ b/config/entries.go @@ -16,10 +16,7 @@ func (c *fileConfig) ReadEntries() (entries scoring.Entries, err error) { } defer file.Close() - if err = jsonio.Decode(file, &entries); err != nil { - return - } - + err = jsonio.Decode(file, &entries) return } diff --git a/config/testing.go b/config/testing.go new file mode 100644 index 0000000..1fe5afe --- /dev/null +++ b/config/testing.go @@ -0,0 +1,58 @@ +package config + +import "github.com/gsamokovarov/jump/scoring" + +// Testing is an in-memory testing config which loosely follows the default +// file-based configuration behavior. +type Testing struct { + Entries scoring.Entries + Search Search + Pins map[string]string + Pin string +} + +// ReadEntries implements the Config interface. +func (c *Testing) ReadEntries() (scoring.Entries, error) { + return c.Entries, nil +} + +// WriteEntries implements the Config interface. +func (c *Testing) WriteEntries(entries scoring.Entries) error { + c.Entries = entries + c.Entries.Sort() + return nil +} + +// ReadSearch implements the Config interface. +func (c *Testing) ReadSearch() Search { + return c.Search +} + +// WriteSearch implements the Config interface. +func (c *Testing) WriteSearch(term string, index int) error { + c.Search.Term = term + c.Search.Index = index + return nil +} + +// ReadPins implements the Config interface. +func (c *Testing) ReadPins() (map[string]string, error) { + return c.Pins, nil +} + +// FindPin implements the Config interface. +func (c *Testing) FindPin(term string) (string, bool) { + return c.Pin, c.Pin != "" +} + +// WritePin implements the Config interface. +func (c *Testing) WritePin(_, value string) error { + c.Pin = value + return nil +} + +// RemovePin implements the Config interface. +func (c *Testing) RemovePin(term string) error { + c.Pin = "" + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..29233b6 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/gsamokovarov/jump + +require github.com/gsamokovarov/assert v0.0.0-20180414063448-8cd8ab63a335 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3908c05 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gsamokovarov/assert v0.0.0-20180414063448-8cd8ab63a335 h1:MFE3iUApg9Sl5MmZnosCEhYXRQCKz5coShpoAF86IiE= +github.com/gsamokovarov/assert v0.0.0-20180414063448-8cd8ab63a335/go.mod h1:ejyiK4+/RLW9C/QgBK+nlwDmNB9pIW9i2WVqMmAa7no= diff --git a/importer/autojump.go b/importer/autojump.go new file mode 100644 index 0000000..911b433 --- /dev/null +++ b/importer/autojump.go @@ -0,0 +1,110 @@ +package importer + +import ( + "fmt" + "io" + "math" + "strconv" + "strings" + + "github.com/gsamokovarov/jump/config" + "github.com/gsamokovarov/jump/scoring" +) + +var autojumpDefaultConfigPaths = []string{ + "$HOME/.local/share/autojump/autojump.txt", + "$HOME/Library/autojump/autojump.txt", +} + +// Autojump is an importer for the autojump tool. +func Autojump(conf config.Config, configPaths ...string) Importer { + if len(configPaths) == 0 { + configPaths = autojumpDefaultConfigPaths + } + + return &autojump{ + config: conf, + configPaths: configPaths, + } +} + +type autojump struct { + config config.Config + configPaths []string +} + +func (i *autojump) Import(fn Callback) error { + autojumpEntries, err := i.parseConfig() + if err != nil { + return err + } + + jumpEntries, err := i.config.ReadEntries() + if err != nil { + return err + } + + for _, entry := range autojumpEntries { + if _, found := jumpEntries.Find(entry.Path); found { + continue + } + + fn.Call(entry) + + jumpEntries = append(jumpEntries, entry) + } + + return i.config.WriteEntries(jumpEntries) +} + +func (i *autojump) parseConfig() (scoring.Entries, error) { + content, err := readConfig(i.configPaths) + if err != nil { + return nil, err + } + + var entries scoring.Entries + + for _, line := range strings.Split(content, "\n") { + entry, err := i.newEntryFromLine(line) + if err == io.EOF { + continue + } + if err != nil { + return nil, err + } + + if _, found := entries.Find(entry.Path); found { + continue + } + + entries = append(entries, entry) + } + + return entries, nil +} + +func (i *autojump) newEntryFromLine(line string) (*scoring.Entry, error) { + if line == "" { + return nil, io.EOF + } + + parts := strings.Split(line, "\t") + if len(parts) != 2 { + return nil, fmt.Errorf("importer: cannot parse entry: %s", line) + } + + path := parts[1] + weight, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return nil, err + } + + return &scoring.Entry{ + Path: path, + Score: &scoring.Score{ + Weight: int64(math.Round(weight)), + Age: scoring.Now, + }, + }, nil +} diff --git a/importer/autojump_test.go b/importer/autojump_test.go new file mode 100644 index 0000000..17c5113 --- /dev/null +++ b/importer/autojump_test.go @@ -0,0 +1,44 @@ +package importer + +import ( + p "path" + "testing" + + "github.com/gsamokovarov/assert" + "github.com/gsamokovarov/jump/config" +) + +func TestAutojump(t *testing.T) { + conf := &config.Testing{} + configPath := p.Join(td, "autojump.txt") + + imp := Autojump(conf, configPath) + + err := imp.Import(nil) + assert.Nil(t, err) + + assert. + Len(t, 6, conf.Entries). + // 0 + Equal(t, "/Users/genadi/Development/mock_last_status", conf.Entries[0].Path). + Equal(t, 14, conf.Entries[0].Score.Weight). + // 1 + Equal(t, "/Users/genadi/.go/src/github.com/gsamokovarov/jump", conf.Entries[1].Path). + Equal(t, 14, conf.Entries[1].Score.Weight). + // 2 + Equal(t, "/Users/genadi/Development/gloat", conf.Entries[2].Path). + Equal(t, 20, conf.Entries[2].Score.Weight). + // 3 + Equal(t, "/Users/genadi/Development", conf.Entries[3].Path). + Equal(t, 33, conf.Entries[3].Score.Weight). + // 4 + Equal(t, "/Users/genadi/Development/jump", conf.Entries[4].Path). + Equal(t, 39, conf.Entries[4].Score.Weight). + // 5 + Equal(t, "/usr/local/Cellar/autojump", conf.Entries[5].Path). + Equal(t, 44, conf.Entries[5].Score.Weight) + + for i, j := 0, 1; i < len(conf.Entries)-1; i, j = i+1, j+1 { + assert.True(t, conf.Entries[i].CalculateScore() <= conf.Entries[j].CalculateScore()) + } +} diff --git a/importer/importer.go b/importer/importer.go new file mode 100644 index 0000000..96ffbc5 --- /dev/null +++ b/importer/importer.go @@ -0,0 +1,71 @@ +package importer + +import ( + "errors" + "io/ioutil" + "os" + + "github.com/gsamokovarov/jump/config" + "github.com/gsamokovarov/jump/scoring" +) + +// NotFoundErr is an error returned when the importing tool is not found. +var NotFoundErr = errors.New("importer: cannot find autojump or z data files") + +// Callback is called on every import. +type Callback func(*scoring.Entry) + +// Call calls the callback without the need of nil checks. +func (fn Callback) Call(entry *scoring.Entry) { + if fn != nil { + fn(entry) + } +} + +// Importer imports a configuration from an external tool into jump. +type Importer interface { + Import(fn Callback) error +} + +// Guess tries to guess the importer to use based on a hint. +func Guess(hint string, conf config.Config) Importer { + var imp Importer + + switch hint { + case "autojump": + imp = Autojump(conf) + case "z": + imp = Z(conf) + default: + // First try z, then try autojump. + imp = multiImporter{Z(conf), Autojump(conf)} + } + + return imp +} + +func readConfig(paths []string) (string, error) { + path, err := findPath(paths) + if err != nil { + return "", err + } + + bytes, err := ioutil.ReadFile(path) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +func findPath(paths []string) (string, error) { + for _, path := range paths { + path = os.ExpandEnv(path) + + if _, err := os.Stat(path); !os.IsNotExist(err) { + return path, nil + } + } + + return "", NotFoundErr +} diff --git a/importer/importer_test.go b/importer/importer_test.go new file mode 100644 index 0000000..8b38351 --- /dev/null +++ b/importer/importer_test.go @@ -0,0 +1,45 @@ +package importer + +import ( + "os" + "path" + "testing" + + "github.com/gsamokovarov/assert" + "github.com/gsamokovarov/jump/config" +) + +var td string + +func TestGuess_Autojump(t *testing.T) { + imp := Guess("autojump", &config.Testing{}) + + _, ok := imp.(*autojump) + assert.True(t, ok) +} + +func TestGuess_Z(t *testing.T) { + imp := Guess("z", &config.Testing{}) + + _, ok := imp.(*z) + assert.True(t, ok) +} + +func TestGuess_Both(t *testing.T) { + imp := Guess("", &config.Testing{}) + + _, ok := imp.(multiImporter) + assert.True(t, ok) +} + +func TestCallback(t *testing.T) { + var fn Callback + + // Does not crash when fn is nil. + fn.Call(nil) +} + +func init() { + cwd, _ := os.Getwd() + td = path.Join(cwd, "testdata") +} diff --git a/importer/multi.go b/importer/multi.go new file mode 100644 index 0000000..a6d1fc7 --- /dev/null +++ b/importer/multi.go @@ -0,0 +1,28 @@ +package importer + +// multiImporter tries to import configurations from multiple importers. If at +// least on of the importers succeed, no errors will be returned. +type multiImporter []Importer + +func (mi multiImporter) Import(fn Callback) error { + var lastErr error + atLeastOneSucceeded := false + + for _, i := range mi { + err := i.Import(fn) + if err == NotFoundErr { + continue + } + if err != nil { + lastErr = err + } + + atLeastOneSucceeded = true + } + + if !atLeastOneSucceeded && lastErr != nil { + return lastErr + } + + return nil +} diff --git a/importer/multi_test.go b/importer/multi_test.go new file mode 100644 index 0000000..f93b6dd --- /dev/null +++ b/importer/multi_test.go @@ -0,0 +1,45 @@ +package importer + +import ( + "errors" + p "path" + "testing" + + "github.com/gsamokovarov/assert" + "github.com/gsamokovarov/jump/config" +) + +type failingImporter struct{} + +func (failingImporter) Import(Callback) error { return errors.New("importer: failing") } + +func Test_multiImporter(t *testing.T) { + conf := &config.Testing{} + autojumpPath := p.Join(td, "autojump.txt") + zPath := p.Join(td, "z") + + imp := multiImporter{ + Autojump(conf, autojumpPath), + Z(conf, zPath), + } + + err := imp.Import(nil) + assert.Nil(t, err) + + assert.Len(t, 8, conf.Entries) +} + +func Test_multiImporter_oneErrored(t *testing.T) { + conf := &config.Testing{} + autojumpPath := p.Join(td, "autojump.txt") + + imp := multiImporter{ + failingImporter{}, + Autojump(conf, autojumpPath), + } + + err := imp.Import(nil) + assert.Nil(t, err) + + assert.Len(t, 6, conf.Entries) +} diff --git a/importer/testdata/autojump.txt b/importer/testdata/autojump.txt new file mode 100644 index 0000000..1d4e20c --- /dev/null +++ b/importer/testdata/autojump.txt @@ -0,0 +1,6 @@ +38.729833462 /Users/genadi/Development/jump +14.1421356237 /Users/genadi/Development/mock_last_status +33.1662479035 /Users/genadi/Development +14.1421356237 /Users/genadi/.go/src/github.com/gsamokovarov/jump +43.5889894353 /usr/local/Cellar/autojump +20.0 /Users/genadi/Development/gloat diff --git a/importer/testdata/z b/importer/testdata/z new file mode 100644 index 0000000..88ffd8f --- /dev/null +++ b/importer/testdata/z @@ -0,0 +1,4 @@ +/Users/genadi/Development/hack|11|1536272816 +/Users/genadi/Development/masse|1|1536272502 +/Users/genadi/Development|3|1536272506 +/Users/genadi/.go/src/github.com/gsamokovarov/jump|1|1536272492 diff --git a/importer/z.go b/importer/z.go new file mode 100644 index 0000000..609e367 --- /dev/null +++ b/importer/z.go @@ -0,0 +1,113 @@ +package importer + +import ( + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/gsamokovarov/jump/config" + "github.com/gsamokovarov/jump/scoring" +) + +var zDefaultConfigPaths = []string{ + "$HOME/.z", +} + +// Z is an importer for the z tool. +func Z(conf config.Config, configPaths ...string) Importer { + if len(configPaths) == 0 { + configPaths = zDefaultConfigPaths + } + + return &z{ + config: conf, + configPaths: configPaths, + } +} + +type z struct { + config config.Config + configPaths []string +} + +func (i *z) Import(fn Callback) error { + zEntries, err := i.parseConfig() + if err != nil { + return err + } + + jumpEntries, err := i.config.ReadEntries() + if err != nil { + return err + } + + for _, entry := range zEntries { + if _, found := jumpEntries.Find(entry.Path); found { + continue + } + + fn.Call(entry) + + jumpEntries = append(jumpEntries, entry) + } + + return i.config.WriteEntries(jumpEntries) +} + +func (i *z) parseConfig() (scoring.Entries, error) { + content, err := readConfig(i.configPaths) + if err != nil { + return nil, err + } + + var entries scoring.Entries + + for _, line := range strings.Split(content, "\n") { + entry, err := i.newEntryFromLine(line) + if err == io.EOF { + continue + } + if err != nil { + return nil, err + } + + if _, found := entries.Find(entry.Path); found { + continue + } + + entries = append(entries, entry) + } + + return entries, nil +} + +func (i *z) newEntryFromLine(line string) (*scoring.Entry, error) { + if line == "" { + return nil, io.EOF + } + + parts := strings.Split(line, "|") + if len(parts) != 3 { + return nil, fmt.Errorf("importer: cannot parse entry: %s", line) + } + + path := parts[0] + weight, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return nil, err + } + epoch, err := strconv.ParseInt(parts[2], 10, 64) + if err != nil { + return nil, err + } + + return &scoring.Entry{ + Path: path, + Score: &scoring.Score{ + Weight: weight, + Age: time.Unix(epoch, 0), + }, + }, nil +} diff --git a/importer/z_test.go b/importer/z_test.go new file mode 100644 index 0000000..11152a3 --- /dev/null +++ b/importer/z_test.go @@ -0,0 +1,43 @@ +package importer + +import ( + p "path" + "testing" + "time" + + "github.com/gsamokovarov/assert" + "github.com/gsamokovarov/jump/config" +) + +func TestZ(t *testing.T) { + conf := &config.Testing{} + configPath := p.Join(td, "z") + + imp := Z(conf, configPath) + + err := imp.Import(nil) + assert.Nil(t, err) + + assert. + Len(t, 4, conf.Entries). + // 0 + Equal(t, "/Users/genadi/.go/src/github.com/gsamokovarov/jump", conf.Entries[0].Path). + Equal(t, 1, conf.Entries[0].Score.Weight). + Equal(t, time.Unix(1536272492, 0), conf.Entries[0].Score.Age). + // 1 + Equal(t, "/Users/genadi/Development/masse", conf.Entries[1].Path). + Equal(t, 1, conf.Entries[1].Score.Weight). + Equal(t, time.Unix(1536272502, 0), conf.Entries[1].Score.Age). + // 2 + Equal(t, "/Users/genadi/Development", conf.Entries[2].Path). + Equal(t, 3, conf.Entries[2].Score.Weight). + Equal(t, time.Unix(1536272506, 0), conf.Entries[2].Score.Age). + // 3 + Equal(t, "/Users/genadi/Development/hack", conf.Entries[3].Path). + Equal(t, 11, conf.Entries[3].Score.Weight). + Equal(t, time.Unix(1536272816, 0), conf.Entries[3].Score.Age) + + for i, j := 0, 1; i < len(conf.Entries)-1; i, j = i+1, j+1 { + assert.True(t, conf.Entries[i].CalculateScore() <= conf.Entries[j].CalculateScore()) + } +}