├── .github
└── workflows
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .goreleaser.yml
├── Gopkg.lock
├── Gopkg.toml
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── add.go
├── cd.go
├── cleanup.go
├── remove.go
├── reset.go
├── root.go
└── shell.go
├── go.mod
├── go.sum
├── logo.svg
├── main.go
└── pkg
├── database
└── config.go
├── entry
├── entry.go
├── entry_sort.go
└── frecency.go
├── file
└── flock.go
└── path
├── lcp.go
└── lcp_test.go
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - '*'
6 |
7 | jobs:
8 | release:
9 | name: Release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Check out code
13 | uses: actions/checkout@master
14 | - name: goreleaser
15 | uses: docker://goreleaser/goreleaser
16 | env:
17 | GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }}
18 | with:
19 | args: release
20 | if: success()
21 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | on: [push]
3 | jobs:
4 |
5 | test:
6 | name: Test on go ${{ matrix.go_version }} and ${{ matrix.os }}
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | go_version: [ '1.12.x', '1.13.x' ]
11 | os: [ubuntu-latest, macOS-latest]
12 |
13 | steps:
14 |
15 | - name: Set up Go
16 | uses: actions/setup-go@v1
17 | with:
18 | go-version: ${{ matrix.go_version }}
19 |
20 | - name: Check out code into the Go module directory
21 | uses: actions/checkout@v1
22 |
23 | - name: test
24 | run: go test -v ./... --race
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Go ###
2 | # Binaries for programs and plugins
3 | *.exe
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
15 | .glide/
16 |
17 | # Dep
18 | vendor/
19 |
20 | .idea/
21 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | builds:
2 | - binary: compass
3 | goos:
4 | - darwin
5 | - linux
6 | goarch:
7 | - amd64
8 |
9 | brew:
10 | name: compass
11 |
12 | github:
13 | owner: khoi
14 | name: homebrew-tap
15 |
16 | folder: Formula
17 |
18 | test: |
19 | system "#{bin}/program -h"
20 |
--------------------------------------------------------------------------------
/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
6 | name = "github.com/inconshreveable/mousetrap"
7 | packages = ["."]
8 | pruneopts = ""
9 | revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
10 | version = "v1.0"
11 |
12 | [[projects]]
13 | digest = "1:2208a80fc3259291e43b30f42f844d18f4218036dff510f42c653ec9890d460a"
14 | name = "github.com/spf13/cobra"
15 | packages = ["."]
16 | pruneopts = ""
17 | revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b"
18 | version = "v0.0.1"
19 |
20 | [[projects]]
21 | digest = "1:261bc565833ef4f02121450d74eb88d5ae4bd74bfe5d0e862cddb8550ec35000"
22 | name = "github.com/spf13/pflag"
23 | packages = ["."]
24 | pruneopts = ""
25 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
26 | version = "v1.0.0"
27 |
28 | [solve-meta]
29 | analyzer-name = "dep"
30 | analyzer-version = 1
31 | input-imports = ["github.com/spf13/cobra"]
32 | solver-name = "gps-cdcl"
33 | solver-version = 1
34 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | [[constraint]]
2 | name = "github.com/spf13/cobra"
3 | version = "0.0.1"
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 khoiracle
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PKGS := $(shell go list ./... | grep -v /vendor)
2 |
3 | .PHONY: test install
4 |
5 | all: test install
6 |
7 | test:
8 | go test -v $(PKGS)
9 |
10 | install:
11 | go install
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/khoi/compass/actions)
2 | [](https://github.com/khoi/compass/actions)
3 | [](https://twitter.com/khoiracle)
4 |
5 | # Compass
6 | Compass learns your habit, and help navigate to your "frecently used" directory.
7 |
8 | ## Usage
9 | By default, `s` is the key-binding wrapper around `compass`.
10 |
11 | - Fuzzily navigate to directory contains `go` and `compass` :
12 |
13 | ```bash
14 | s compass
15 | # ~/Workspace/go/src/github.com/khoi/compass
16 | ```
17 |
18 | - For more option refer to:
19 |
20 | ```bash
21 | compass --help
22 | ```
23 |
24 | ## Install
25 |
26 | Use Homebrew:
27 |
28 | ```bash
29 | brew install khoi/tap/compass
30 | ```
31 |
32 | For development build:
33 |
34 | ```bash
35 | go get github.com/khoi/compass
36 | ```
37 |
38 | Add this to the end of your `.zshrc` or `.bash_profile`
39 |
40 | ```bash
41 | eval "$(compass shell)"
42 | ```
43 |
44 | For fish shell add the line below to your `~/.config/fish/config.fish`
45 |
46 | ```bash
47 | if type -q compass
48 | status --is-interactive; and source (compass shell --type fish -|psub)
49 | end
50 | ```
51 |
52 | If you want to use different key binding pass `--bind-to` to the `compass shell` command:
53 |
54 | For instance, if you want to use `z` instead of `s`
55 |
56 | ```bash
57 | eval "$(compass shell --bind-to z)"
58 | ```
59 |
60 | ## References
61 |
62 | - [rupa/z](https://github.com/rupa/z)
63 | - [wting/autojump](https://github.com/wting/autojump)
64 | - [gsamokovarov/jump](https://github.com/gsamokovarov/jump)
65 |
--------------------------------------------------------------------------------
/cmd/add.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "path/filepath"
6 | "sort"
7 | "time"
8 |
9 | "github.com/khoi/compass/pkg/entry"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | // addCmd represents the add command
14 | var addCmd = &cobra.Command{
15 | Use: "add",
16 | Short: "Add a folder to the database",
17 | Run: addRun,
18 | }
19 |
20 | func addRun(cmd *cobra.Command, args []string) {
21 | if len(args) == 0 {
22 | exit(errors.New("Missing folder."))
23 | }
24 |
25 | path, err := filepath.Abs(args[0])
26 |
27 | if err != nil {
28 | exit(err)
29 | }
30 |
31 | entries, err := fileDb.Read()
32 |
33 | if err != nil {
34 | exit(err)
35 | }
36 |
37 | sort.Sort(entry.ByPath(entries))
38 | idx := sort.Search(len(entries), func(i int) bool {
39 | return entries[i].Path >= path
40 | })
41 |
42 | if idx < len(entries) && entries[idx].Path == path { // Entry exists, update the score
43 | entries[idx].VisitedCount += 1
44 | entries[idx].LastVisited = int(time.Now().Unix())
45 | } else { // Create a new entry
46 | entries = append(entries, nil)
47 | copy(entries[idx+1:], entries[idx:])
48 | entries[idx] = &entry.Entry{
49 | Path: path,
50 | VisitedCount: 1,
51 | LastVisited: int(time.Now().Unix()),
52 | }
53 | }
54 |
55 | if err := fileDb.Write(entries); err != nil {
56 | exit(err)
57 | }
58 | }
59 |
60 | func init() {
61 | rootCmd.AddCommand(addCmd)
62 | }
63 |
--------------------------------------------------------------------------------
/cmd/cd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 |
8 | "errors"
9 |
10 | "github.com/khoi/compass/pkg/entry"
11 | "github.com/khoi/compass/pkg/path"
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | var cdCmd = &cobra.Command{
16 | Use: "cd",
17 | Short: "Print the top match for search terms",
18 | Run: cdRun,
19 | }
20 |
21 | func cdRun(cmd *cobra.Command, args []string) {
22 | var queries []string
23 |
24 | if len(args) > 0 {
25 | queries = args[0:]
26 | }
27 |
28 | entries, err := fileDb.Read()
29 |
30 | if err != nil {
31 | exit(err)
32 | }
33 |
34 | var matchedEntries = entry.Entries(entries)
35 |
36 | for _, query := range queries {
37 | matchedEntries = matchedEntries.Filter(func(e *entry.Entry) bool {
38 | return strings.Contains(e.Path, query) || strings.Contains(strings.ToLower(e.Path), strings.ToLower(query))
39 | })
40 | }
41 |
42 | var filtered []*entry.Entry
43 | var filteredPaths []string
44 |
45 | for _, e := range matchedEntries {
46 | filtered = append(filtered, e)
47 | filteredPaths = append(filteredPaths, e.Path)
48 | }
49 |
50 | sort.Sort(entry.ByFrecency(filtered))
51 |
52 | for _, e := range filtered {
53 | filteredPaths = append(filteredPaths, e.Path)
54 | }
55 |
56 | if len(filteredPaths) == 0 {
57 | exit(errors.New(""))
58 | }
59 |
60 | output := filteredPaths[len(filteredPaths)-1]
61 | if common := path.LCP(filteredPaths); common != "" {
62 | for _, p := range filteredPaths {
63 | if p == common {
64 | output = common
65 | break
66 | }
67 | }
68 | }
69 |
70 | fmt.Printf("%s\n", output)
71 | }
72 |
73 | func init() {
74 | rootCmd.AddCommand(cdCmd)
75 | }
76 |
--------------------------------------------------------------------------------
/cmd/cleanup.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/khoi/compass/pkg/entry"
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | // cleanupCmd represents the cleanup command
12 | var cleanupCmd = &cobra.Command{
13 | Use: "cleanup",
14 | Short: "Remove non-existing folder from the database",
15 | Run: func(cmd *cobra.Command, args []string) {
16 | if verbose {
17 | fmt.Println("compass cleaning up.")
18 | }
19 |
20 | entries, err := fileDb.Read()
21 |
22 | if err != nil {
23 | exit(err)
24 | }
25 |
26 | var valid []*entry.Entry
27 |
28 | for _, e := range entries {
29 | if _, err := os.Stat(e.Path); err == nil {
30 | valid = append(valid, e)
31 | continue
32 | }
33 | if verbose {
34 | fmt.Fprintf(os.Stdout, "Removed %s\n", e.Path)
35 | }
36 | }
37 |
38 | if err := fileDb.Write(valid); err != nil {
39 | exit(err)
40 | }
41 | },
42 | }
43 |
44 | func init() {
45 | rootCmd.AddCommand(cleanupCmd)
46 | }
47 |
--------------------------------------------------------------------------------
/cmd/remove.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "path/filepath"
7 |
8 | "github.com/khoi/compass/pkg/entry"
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | var removeCmd = &cobra.Command{
13 | Use: "remove",
14 | Short: "Remove a folder from the database",
15 | Run: removeRun,
16 | }
17 |
18 | func removeRun(cmd *cobra.Command, args []string) {
19 | if len(args) == 0 {
20 | exit(errors.New("Missing folder."))
21 | }
22 |
23 | path, err := filepath.Abs(args[0])
24 |
25 | if err != nil {
26 | exit(err)
27 | }
28 |
29 | entries, err := fileDb.Read()
30 |
31 | if err != nil {
32 | exit(err)
33 | }
34 |
35 | newEntries := entry.Entries(entries).Filter(func(e *entry.Entry) bool {
36 | return e.Path != path
37 | })
38 |
39 | if err := fileDb.Write(newEntries); err != nil {
40 | exit(err)
41 | }
42 |
43 | fmt.Printf("%s removed.\n", path)
44 | }
45 |
46 | func init() {
47 | rootCmd.AddCommand(removeCmd)
48 | }
49 |
--------------------------------------------------------------------------------
/cmd/reset.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "github.com/spf13/cobra"
6 | )
7 |
8 | var purgeCmd = &cobra.Command{
9 | Use: "purge",
10 | Short: "Purge the database.",
11 | Run: func(cmd *cobra.Command, args []string) {
12 | fmt.Printf("%s purged.\n", cfgFile)
13 | fileDb.Truncate()
14 | },
15 | }
16 |
17 | func init() {
18 | rootCmd.AddCommand(purgeCmd)
19 | }
20 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "os/user"
8 | "path/filepath"
9 |
10 | "github.com/khoi/compass/pkg/database"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var defaultConfigFileName = ".compass"
15 | var cfgFile string
16 | var verbose bool
17 | var fileDb database.DB
18 |
19 | // rootCmd represents the base command when called without any subcommands
20 | var rootCmd = &cobra.Command{
21 | Use: "compass",
22 | Short: "Compass, navigate around your pirate 🛳",
23 | }
24 |
25 | // Execute adds all child commands to the root command and sets flags appropriately.
26 | // This is called by main.main(). It only needs to happen once to the rootCmd.
27 | func Execute() {
28 | if err := rootCmd.Execute(); err != nil {
29 | exit(err)
30 | }
31 | }
32 |
33 | func init() {
34 | cobra.OnInitialize(initDB)
35 | rootCmd.PersistentFlags().StringVarP(&cfgFile, "file", "f", "", "path to the db file (default is $HOME/.compass)")
36 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
37 | }
38 |
39 | func initDB() {
40 | var err error
41 |
42 | if cfgFile == "" {
43 | usr, err := user.Current()
44 | if err != nil {
45 | exit(err)
46 | }
47 | cfgFile = filepath.Join(usr.HomeDir, defaultConfigFileName)
48 | }
49 |
50 | if fileDb, err = database.New(cfgFile); err != nil {
51 | exit(err)
52 | }
53 | }
54 |
55 | func exit(err error) {
56 | fmt.Println(err)
57 | os.Exit(1)
58 | }
59 |
--------------------------------------------------------------------------------
/cmd/shell.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "html/template"
7 |
8 | "bytes"
9 |
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | const sh = `#!bin/sh
14 | if [ -n "${BASH}" ]; then
15 | shell="bash"
16 | elif [ -n "${ZSH_NAME}" ]; then
17 | shell="zsh"
18 | elif [ -n "${__fish_datadir}" ]; then
19 | shell="fish"
20 | else
21 | shell=$(echo "${SHELL}" | awk -F/ '{ print $NF }')
22 | fi
23 | if [ "${shell}" = "sh" ]; then
24 | return 0
25 | fi
26 | eval "$(compass shell --type "$shell" --bind-to {{.Binding}})"
27 | `
28 |
29 | const zsh = `__compass_chpwd() {
30 | [[ "$(pwd)" == "$HOME" ]] && return
31 | (compass add "$(pwd)" &)
32 | }
33 | [[ -n "${precmd_functions[(r)__compass_chpwd]}" ]] || {
34 | precmd_functions[$(($#precmd_functions+1))]=__compass_chpwd
35 | }
36 | {{.Binding}}() {
37 | local output="$(compass cd $@)"
38 | if [ -d "$output" ]; then
39 | builtin cd "$output"
40 | else
41 | compass cleanup && false
42 | fi
43 | }
44 | __compass_completion() {
45 | reply=(${(f)"$(compass ls --path-only "$1")"})
46 | }
47 | compctl -U -K __compass_completion {{.Binding}}
48 | `
49 |
50 | const bash = `__compass_chpwd() {
51 | [[ "$(pwd)" == "$HOME" ]] && return
52 | (compass add "$(pwd)" &)
53 | }
54 | grep "compass add" <<< "$PROMPT_COMMAND" >/dev/null || {
55 | PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''(__compass_chpwd 2>/dev/null &);'
56 | }
57 | {{.Binding}}() {
58 | local output="$(compass cd $@)"
59 | if [ -d "$output" ]; then
60 | builtin cd "$output"
61 | else
62 | compass cleanup && false
63 | fi
64 | }
65 | complete -o dirnames -C 'compass ls --path-only "${COMP_LINE/#{{.Binding}} /}"' {{.Binding}}
66 | `
67 |
68 | const fish = `function {{.Binding}}
69 | set -l output (compass cd $argv)
70 | if test -d "$output"
71 | cd $output
72 | else
73 | compass cleanup; false
74 | end
75 | end
76 |
77 | function __compass_add --on-variable PWD
78 | status --is-command-substitution; and return
79 | if contains -- (pwd) $HOME
80 | return
81 | end
82 | compass add (pwd)
83 | end
84 |
85 | complete -c {{.Binding}} -x -a '(compass ls --path-only (commandline -t))'
86 | `
87 |
88 | func scriptForShell(shell string, keyBinding string) string {
89 | var b struct {
90 | Binding string
91 | }
92 | b.Binding = keyBinding
93 | shellType := func() string {
94 | switch shell {
95 | case "bash":
96 | return bash
97 | case "zsh":
98 | return zsh
99 | case "fish":
100 | return fish
101 | default:
102 | return sh
103 | }
104 | }()
105 |
106 | tmpl, err := template.New("compass").Parse(shellType)
107 |
108 | if err != nil {
109 | panic(err)
110 | }
111 |
112 | var buf bytes.Buffer
113 | err = tmpl.Execute(&buf, b)
114 |
115 | if err != nil {
116 | panic(err)
117 | }
118 |
119 | return buf.String()
120 | }
121 |
122 | var shellType string
123 | var keyBinding string
124 |
125 | var shellCmd = &cobra.Command{
126 | Use: "shell",
127 | Short: "Prints out the shell integration scripts.",
128 | Run: func(cmd *cobra.Command, args []string) {
129 | fmt.Printf(scriptForShell(shellType, keyBinding))
130 | },
131 | }
132 |
133 | func init() {
134 | rootCmd.AddCommand(shellCmd)
135 | shellCmd.Flags().StringVarP(&shellType, "type", "t", "sh", "Type of the shell (bash|zsh|fish)")
136 | shellCmd.Flags().StringVarP(&keyBinding, "bind-to", "b", "s", "Key binding (default is `s`)")
137 | }
138 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/khoi/compass
2 |
3 | require (
4 | github.com/inconshreveable/mousetrap v1.0.0
5 | github.com/spf13/cobra v0.0.1
6 | github.com/spf13/pflag v1.0.0
7 | )
8 |
9 | go 1.13
10 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
2 | github.com/spf13/cobra v0.0.1 h1:zZh3X5aZbdnoj+4XkaBxKfhO4ot82icYdhhREIAXIj8=
3 | github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
4 | github.com/spf13/pflag v1.0.0 h1:oaPbdDe/x0UncahuwiPxW1GYJyilRAdsPnq3e1yaPcI=
5 | github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
6 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 khoiracle
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import "github.com/khoi/compass/cmd"
18 |
19 | func main() {
20 | cmd.Execute()
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/database/config.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "encoding/csv"
5 | "strconv"
6 |
7 | "github.com/khoi/compass/pkg/entry"
8 | "github.com/khoi/compass/pkg/file"
9 | )
10 |
11 | type DB interface {
12 | Read() ([]*entry.Entry, error)
13 | Write([]*entry.Entry) error
14 | Truncate() error
15 | }
16 |
17 | type fileDb struct {
18 | dbPath string
19 | }
20 |
21 | func (f *fileDb) Write(entries []*entry.Entry) error {
22 | flock := file.NewFlock(f.dbPath)
23 | if err := flock.Lock(); err != nil {
24 | return err
25 | }
26 | defer flock.Unlock()
27 |
28 | err := flock.File().Truncate(0)
29 | _, err = flock.File().Seek(0, 0)
30 | if err != nil {
31 | return err
32 | }
33 |
34 | w := csv.NewWriter(flock.File())
35 | defer w.Flush()
36 |
37 | for _, e := range entries {
38 | data := []string{e.Path, strconv.Itoa(e.VisitedCount), strconv.Itoa(e.LastVisited)}
39 | if err := w.Write(data); err != nil {
40 | return err
41 | }
42 | }
43 |
44 | return nil
45 | }
46 |
47 | func (f *fileDb) Read() ([]*entry.Entry, error) {
48 | flock := file.NewFlock(f.dbPath)
49 | if err := flock.Lock(); err != nil {
50 | return nil, err
51 | }
52 | defer flock.Unlock()
53 |
54 | r := csv.NewReader(flock.File())
55 | records, err := r.ReadAll()
56 |
57 | if err != nil {
58 | return nil, err
59 | }
60 |
61 | entries := make([]*entry.Entry, 0, len(records))
62 | for _, r := range records {
63 | visitedCount, err := strconv.Atoi(r[1])
64 | lastVisited, err := strconv.Atoi(r[2])
65 |
66 | if err != nil {
67 | continue
68 | }
69 |
70 | entries = append(entries, &entry.Entry{
71 | Path: r[0],
72 | VisitedCount: visitedCount,
73 | LastVisited: lastVisited,
74 | })
75 | }
76 |
77 | return entries, nil
78 | }
79 |
80 | func (f *fileDb) Truncate() error {
81 | flock := file.NewFlock(f.dbPath)
82 | if err := flock.Lock(); err != nil {
83 | return err
84 | }
85 | defer flock.Unlock()
86 | flock.File().Truncate(0)
87 | return flock.File().Sync()
88 | }
89 |
90 | func New(filePath string) (DB, error) {
91 | return &fileDb{
92 | dbPath: filePath,
93 | }, nil
94 | }
95 |
--------------------------------------------------------------------------------
/pkg/entry/entry.go:
--------------------------------------------------------------------------------
1 | package entry
2 |
3 | type Entry struct {
4 | Path string
5 | VisitedCount int
6 | LastVisited int
7 | }
8 |
9 | type Entries []*Entry
10 |
11 | func (e Entries) Map(f func(*Entry) interface{}) Entries {
12 | result := make(Entries, len(e))
13 | for _, v := range e {
14 | result = append(result, v)
15 | }
16 | return result
17 | }
18 |
19 | func (e Entries) Filter(f func(*Entry) bool) Entries {
20 | result := make(Entries, 0)
21 | for _, v := range e {
22 | if f(v) {
23 | result = append(result, v)
24 | }
25 | }
26 | return result
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/entry/entry_sort.go:
--------------------------------------------------------------------------------
1 | package entry
2 |
3 | type ByPath []*Entry
4 |
5 | func (e ByPath) Len() int {
6 | return len(e)
7 | }
8 |
9 | func (e ByPath) Less(i, j int) bool {
10 | return e[i].Path < e[j].Path
11 | }
12 |
13 | func (e ByPath) Swap(i, j int) {
14 | e[i], e[j] = e[j], e[i]
15 | }
16 |
17 | type ByFrecency []*Entry
18 |
19 | func (e ByFrecency) Len() int {
20 | return len(e)
21 | }
22 |
23 | func (e ByFrecency) Less(i, j int) bool {
24 | return Frecency(e[i]) < Frecency(e[j])
25 | }
26 |
27 | func (e ByFrecency) Swap(i, j int) {
28 | e[i], e[j] = e[j], e[i]
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/entry/frecency.go:
--------------------------------------------------------------------------------
1 | package entry
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // Based off https://github.com/rupa/z/wiki/frecency
8 | func Frecency(e *Entry) float64 {
9 | dx := int(time.Now().Unix()) - e.LastVisited
10 | if dx < 3600 {
11 | return float64(e.VisitedCount) * 4
12 | }
13 | if dx < 86400 {
14 | return float64(e.VisitedCount) * 2
15 | }
16 | if dx < 604800 {
17 | return float64(e.VisitedCount) / 2
18 | }
19 | return float64(e.VisitedCount) / 4
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/file/flock.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "os"
5 | "syscall"
6 | )
7 |
8 | type Flock struct {
9 | path string
10 | file *os.File
11 | }
12 |
13 | func NewFlock(path string) *Flock {
14 | return &Flock{path: path}
15 | }
16 |
17 | func (f *Flock) File() *os.File {
18 | return f.file
19 | }
20 |
21 | func (f *Flock) Lock() error {
22 | if f.file == nil {
23 | if err := f.createOrOpenFile(); err != nil {
24 | return err
25 | }
26 | }
27 |
28 | if err := syscall.Flock(int(f.file.Fd()), syscall.LOCK_EX); err != nil {
29 | return err
30 | }
31 |
32 | return nil
33 | }
34 |
35 | func (f *Flock) Unlock() error {
36 | if err := syscall.Flock(int(f.file.Fd()), syscall.LOCK_UN); err != nil {
37 | return err
38 | }
39 | return f.file.Close()
40 | }
41 |
42 | func (f *Flock) createOrOpenFile() error {
43 | file, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDWR, os.FileMode(0644))
44 | if err != nil {
45 | return err
46 | }
47 | f.file = file
48 | return nil
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/path/lcp.go:
--------------------------------------------------------------------------------
1 | package path
2 |
3 | import (
4 | "os"
5 | "path"
6 | )
7 |
8 | // LCP returns the longest common path for the list of path,
9 | // or an empty string if no prefix is found.
10 | func LCP(l []string) string {
11 | if len(l) == 0 {
12 | return ""
13 | }
14 |
15 | if len(l) == 1 {
16 | return path.Clean(l[0])
17 | }
18 |
19 | min := path.Clean(l[0])
20 | max := min
21 |
22 | for _, p := range l[1:] {
23 | p = path.Clean(p)
24 |
25 | switch {
26 | case p < min:
27 | min = p
28 | case p > max:
29 | max = p
30 | }
31 | }
32 |
33 | result := append([]byte(min), os.PathSeparator)
34 | for i := 0; i < len(result) && i < len(max); i++ {
35 | if result[i] != max[i] {
36 | result = result[:i]
37 | break
38 | }
39 | }
40 |
41 | for i := len(result) - 1; i >= 1; i-- {
42 | if result[i] == os.PathSeparator {
43 | result = result[:i]
44 | break
45 | }
46 | }
47 |
48 | return string(result)
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/path/lcp_test.go:
--------------------------------------------------------------------------------
1 | package path
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | var testTable = []struct {
8 | in []string
9 | out string
10 | }{
11 | {[]string{}, ""},
12 | {[]string{"foo"}, "foo"},
13 | {[]string{"/foo/bar/.."}, "/foo"},
14 | {[]string{"foo", "bar"}, ""},
15 | {[]string{"home/khoiracle", "home/khoiracle/foo", "home/khoiracle/bar"}, "home/khoiracle"},
16 | {[]string{"home/khoiracle/bar/..", "home/khoiracle/foo"}, "home/khoiracle"},
17 | {[]string{"/abc/bcd/cdf", "/abc/bcd/cdf/foo", "/abc/bcd/chi/hij", "/abc/bcd/cdd"}, "/abc/bcd"},
18 | {[]string{"./abc/bcd/cdf", "./abc/bcd/cdf/foo", "./abc/bcd/chi/hij", "./abc/bcd/cdd"}, "abc/bcd"},
19 | {[]string{"/abc/bcd/cdf", "/"}, "/"},
20 | {[]string{"/abc/def/ghj", "/abc/def"}, "/abc/def"},
21 | {[]string{"Github/khoi/ios", "Github/khoi/webcontent-ios", "Github/khoi/ios/iosNetworking"}, "Github/khoi"},
22 | }
23 |
24 | func TestLCP(t *testing.T) {
25 | for _, c := range testTable {
26 | if out := LCP(c.in); out != c.out {
27 | t.Errorf("Expected %s - Got %s", c.out, out)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------