├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── cli ├── cli.go ├── exec.go └── screen.go ├── cmd ├── add.go ├── config.go ├── delete.go ├── edit.go ├── list.go ├── root.go ├── search.go └── sync.go ├── config ├── config.go └── config_test.go ├── go.mod ├── go.sum ├── history ├── history.go ├── history_test.go ├── record.go └── sync.go ├── main.go └── misc ├── fish ├── README.md └── init.fish └── zsh ├── completions └── _history ├── history.zsh ├── init.zsh ├── keybind.zsh └── substring.zsh /.gitignore: -------------------------------------------------------------------------------- 1 | bin/history 2 | vendor 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - "1.11" 5 | - "1.12" 6 | - "1.13" 7 | - tip 8 | os: 9 | - linux 10 | - osx 11 | script: 12 | - GO111MODULE=on make test 13 | branches: 14 | only: 15 | - master 16 | after_success: 17 | - make cross 18 | - ghr --username b4b4r07 --token $GITHUB_TOKEN --replace $(make version) dist/ 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST?= $(shell go list ./... | grep -v vendor) 2 | DEPS = $(shell go list -f '{{range .TestImports}}{{.}} {{end}}' ./...) 3 | BIN = history 4 | DIST = dist 5 | VERSION = $(shell grep 'Version =' cmd/root.go | sed -E 's/.*"(.+)"$$/\1/') 6 | GOVERSION = $(shell go version) 7 | GOOS = $(word 1,$(subst /, ,$(lastword $(GOVERSION)))) 8 | GOARCH = $(word 2,$(subst /, ,$(lastword $(GOVERSION)))) 9 | ARCNAME = $(BIN)-$(VERSION)-$(GOOS)-$(GOARCH) 10 | RELDIR = $(BIN)-$(GOOS)-$(GOARCH) 11 | 12 | all: build 13 | 14 | build: 15 | mkdir -p bin 16 | go build -o bin/$(BIN) 17 | 18 | install: build 19 | go install 20 | if echo $$SHELL | grep "zsh" &>/dev/null; then \ 21 | install -m 644 ./misc/zsh/completions/_history $(shell zsh -c 'echo $$fpath[1]'); \ 22 | fi 23 | 24 | test: 25 | go test $(TEST) $(TESTARGS) -timeout=3s -parallel=4 26 | go vet $(TEST) 27 | go test $(TEST) -race 28 | 29 | release: 30 | rm -rf $(DIST)/$(RELDIR) 31 | mkdir -p $(DIST)/$(RELDIR) 32 | go clean 33 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags='-X main.version=$(VERSION)' -o bin/$(BIN)$(SUFFIX_EXE) 34 | mv bin/$(BIN)$(SUFFIX_EXE) $(DIST)/$(RELDIR)/ 35 | cp README.md $(DIST)/$(RELDIR)/ 36 | tar czf $(DIST)/$(ARCNAME).tar.gz -C $(DIST) $(RELDIR) 37 | rm -rf $(DIST)/$(RELDIR)/ 38 | go clean 39 | 40 | cross: 41 | @$(MAKE) release GOOS=windows GOARCH=amd64 SUFFIX_EXE=.exe 42 | @$(MAKE) release GOOS=windows GOARCH=386 SUFFIX_EXE=.exe 43 | @$(MAKE) release GOOS=linux GOARCH=amd64 44 | @$(MAKE) release GOOS=linux GOARCH=386 45 | @$(MAKE) release GOOS=darwin GOARCH=amd64 46 | @$(MAKE) release GOOS=darwin GOARCH=386 47 | 48 | version: 49 | @echo $(VERSION) 50 | 51 | .PHONY: all build test release cross version 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | history [![Build Status](https://travis-ci.org/b4b4r07/history.svg?branch=master)](https://travis-ci.org/b4b4r07/history) 2 | ======= 3 | 4 | A CLI to provide enhanced history for your shell 5 | 6 | - Submatch & Fuzzy search 7 | - Share the history among multiple machines 8 | - Easy to customize with the config file in TOML 9 | - Run immediately when just selecting command on the screen via interactive filters 10 | - Customizable what information as a column is displayed on the screen line 11 | - Filter the directory and/or branch that the command was executed 12 | - Text database based on [LTSV](http://ltsv.org) 13 | - Automatically backup 14 | - and more... 15 | 16 | ## Installation 17 | 18 | - Get binaries 19 | - via GitHub Releases: 20 | - for Gophers: `go get -u github.com/b4b4r07/history` 21 | - (Additional) if you're zsh user, it's better to intergrate your shell 22 | 23 | ```console 24 | $ git clone https://github.com/b4b4r07/history && cd history 25 | $ source misc/zsh/init.zsh 26 | ``` 27 | 28 | ## Usage 29 | 30 | You should specify some enviroment variables for using this tool. 31 | 32 |
33 | ZSH_HISTORY_KEYBIND_GET 34 |
35 | 36 | You can set keybind for getting history. 37 | 38 | Example: 39 | 40 | ```zsh 41 | export ZSH_HISTORY_KEYBIND_GET="^r" 42 | export ZSH_HISTORY_FILTER_OPTIONS="--filter-branch --filter-dir" 43 | ``` 44 | 45 | In fact, when you invoke that keybind (in this example, `^r`), the following command will be executed and supplemented to your ZLE (on shell). 46 | 47 | ```zsh 48 | command history search $ZSH_HISTORY_FILTER_OPTIONS --query "$LBUFFER" 49 | ``` 50 | 51 | If you set `ZSH_HISTORY_FILTER_OPTIONS` like above, it's equals to `$ZSH_HISTORY_KEYBIND_GET`'s function behavior. 52 | 53 |
54 | 55 |
56 | ZSH_HISTORY_FILTER_OPTIONS 57 |
58 | 59 | It should be set `history search` option. See also `command history help search`. 60 | 61 |
62 | 63 |
64 | ZSH_HISTORY_KEYBIND_GET_BY_DIR 65 |
66 | 67 | It's equals to `$ZSH_HISTORY_KEYBIND_GET` with `ZSH_HISTORY_FILTER_OPTIONS="--filter-branch --filter-dir"`. 68 | 69 |
70 | 71 |
72 | ZSH_HISTORY_KEYBIND_GET_ALL 73 |
74 | 75 | Ignore `ZSH_HISTORY_FILTER_OPTIONS` and search all history. 76 | 77 | Example: 78 | 79 | ```zsh 80 | export ZSH_HISTORY_KEYBIND_GET_ALL="^r^a" 81 | ``` 82 | 83 |
84 | 85 |
86 | ZSH_HISTORY_COLUMNS_GET_ALL 87 |
88 | 89 | Specify the screen column when displaying with `ZSH_HISTORY_KEYBIND_GET_ALL` 90 | 91 | Defaults to `"{{.Time}},{{.Status}},{{.Command}},({{.Base}}:{{.Branch}})"` 92 | 93 |
94 | 95 |
96 | ZSH_HISTORY_KEYBIND_ARROW_UP 97 |
98 | 99 | Example: 100 | 101 | ```zsh 102 | export ZSH_HISTORY_KEYBIND_ARROW_UP="^p" 103 | ``` 104 | 105 |
106 | 107 |
108 | ZSH_HISTORY_KEYBIND_ARROW_DOWN 109 |
110 | 111 | Example: 112 | 113 | ```zsh 114 | export ZSH_HISTORY_KEYBIND_ARROW_DOWN="^n" 115 | ``` 116 | 117 |
118 | 119 |
120 | ZSH_HISTORY_AUTO_SYNC 121 |
122 | 123 | Example: 124 | 125 | ```zsh 126 | export ZSH_HISTORY_AUTO_SYNC=true 127 | ``` 128 | 129 | If you set sync option (for more datail, see and run `history config`) 130 | 131 |
132 | 133 |
134 | ZSH_HISTORY_AUTO_SYNC_INTERVAL 135 |
136 | 137 | Example: 138 | 139 | ```zsh 140 | export ZSH_HISTORY_AUTO_SYNC_INTERVAL="1h" 141 | ``` 142 | 143 |
144 | 145 | --- 146 | 147 | Anyway, if you want to use it immediately please copy and paste the following into your `zshrc`: 148 | 149 | ```zsh 150 | alias hs="command history" # as you like 151 | 152 | ZSH_HISTORY_KEYBIND_GET="^r" 153 | ZSH_HISTORY_FILTER_OPTIONS="--filter-branch --filter-dir" 154 | ZSH_HISTORY_KEYBIND_ARROW_UP="^p" 155 | ZSH_HISTORY_KEYBIND_ARROW_DOWN="^n" 156 | ``` 157 | 158 | and invoke that zle function with typing Ctrl-R after reloading your zsh, of cource ;) 159 | 160 | For more info and customization, see also wiki pages in this project: 161 | 162 | 163 | 164 | ## License 165 | 166 | MIT 167 | 168 | ## Author 169 | 170 | b4b4r07 171 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/b4b4r07/history/config" 7 | ) 8 | 9 | var ( 10 | ErrConfigEditor = errors.New("config core.editor not set") 11 | ErrConfigHistoryPath = errors.New("config history.path not set") 12 | ) 13 | 14 | func Edit(fname string) error { 15 | editor := config.Conf.Core.Editor 16 | if editor == "" { 17 | return ErrConfigEditor 18 | } 19 | 20 | return Run(editor, fname) 21 | } 22 | -------------------------------------------------------------------------------- /cli/exec.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | colon "github.com/b4b4r07/go-colon" 13 | "github.com/kballard/go-shellquote" 14 | ) 15 | 16 | func expandPath(s string) string { 17 | if len(s) >= 2 && s[0] == '~' && os.IsPathSeparator(s[1]) { 18 | if runtime.GOOS == "windows" { 19 | s = filepath.Join(os.Getenv("USERPROFILE"), s[2:]) 20 | } else { 21 | s = filepath.Join(os.Getenv("HOME"), s[2:]) 22 | } 23 | } 24 | return os.Expand(s, os.Getenv) 25 | } 26 | 27 | func runFilter(command string, r io.Reader, w io.Writer) error { 28 | command = expandPath(command) 29 | result, err := colon.Parse(command) 30 | if err != nil { 31 | return err 32 | } 33 | first, err := result.Executable().First() 34 | if err != nil { 35 | return err 36 | } 37 | command = first.Item 38 | var cmd *exec.Cmd 39 | if runtime.GOOS == "windows" { 40 | cmd = exec.Command("cmd", "/c", command) 41 | } else { 42 | cmd = exec.Command("sh", "-c", command) 43 | } 44 | cmd.Stderr = os.Stderr 45 | cmd.Stdout = w 46 | cmd.Stdin = r 47 | return cmd.Run() 48 | } 49 | 50 | func escape(command string, args []string) string { 51 | for _, arg := range args { 52 | command = shellquote.Join(command, arg) 53 | } 54 | return command 55 | } 56 | 57 | func Run(command string, args ...string) error { 58 | if command == "" { 59 | return errors.New("command not found") 60 | } 61 | command = escape(command, args) 62 | var cmd *exec.Cmd 63 | if runtime.GOOS == "windows" { 64 | cmd = exec.Command("cmd", "/c", command) 65 | } else { 66 | cmd = exec.Command("sh", "-c", command) 67 | } 68 | cmd.Stderr = os.Stderr 69 | cmd.Stdout = os.Stdout 70 | cmd.Stdin = os.Stdin 71 | return cmd.Run() 72 | } 73 | 74 | func runGetOutput(dir, command string, args ...string) (string, error) { 75 | cmd := exec.Command(command, args...) 76 | if dir != "." { 77 | cmd.Dir = dir 78 | } 79 | b, err := cmd.Output() 80 | return string(b), err 81 | } 82 | 83 | func GetBranchName() string { 84 | s, _ := runGetOutput(".", "git", "rev-parse", "--abbrev-ref", "HEAD") 85 | return strings.TrimPrefix(strings.TrimSpace(s), "heads/") 86 | } 87 | func GetDirName() string { 88 | s, _ := os.Getwd() 89 | return s 90 | } 91 | 92 | func GetHostName() string { 93 | s, _ := os.Hostname() 94 | return s 95 | } 96 | -------------------------------------------------------------------------------- /cli/screen.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/b4b4r07/history/config" 9 | "github.com/b4b4r07/history/history" 10 | ) 11 | 12 | type Screen struct { 13 | Lines []string 14 | Records []history.Record 15 | } 16 | 17 | func NewScreen() (s *Screen, err error) { 18 | var ( 19 | lines []string 20 | records history.Records 21 | ) 22 | 23 | h, err := history.Load() 24 | if err != nil { 25 | return 26 | } 27 | 28 | h.Records.Sort() 29 | h.Records.Reverse() 30 | h.Records.Unique() 31 | 32 | cc := config.Conf.Screen 33 | cv := config.Conf.Var 34 | 35 | if cv.Query != "" { 36 | h.Records.Contains(cv.Query) 37 | } 38 | 39 | columns := []string{} 40 | if cv.Columns != "" { 41 | columns = strings.Split(cv.Columns, ",") 42 | if len(columns) > 0 { 43 | config.Conf.Screen.Columns = columns 44 | } 45 | } 46 | 47 | if idx := history.IndexCommandColumns(); idx == -1 { 48 | if len(config.Conf.Screen.Columns) > 0 { 49 | // Other elements are specified although {{.Command}} is not specified in column 50 | err = errors.New("Error: {{.Command}} tepmplete should be contained in columns") 51 | return 52 | } 53 | } 54 | 55 | for _, record := range h.Records { 56 | if cc.FilterDir && cv.Dir != record.Dir { 57 | continue 58 | } 59 | if cc.FilterBranch && cv.Branch != record.Branch { 60 | continue 61 | } 62 | if cc.FilterHostname && cv.Hostname != record.Hostname { 63 | continue 64 | } 65 | if cc.FilterStatus && cv.Status && record.Status != 0 { 66 | continue 67 | } 68 | lines = append(lines, record.Render()) 69 | records = append(records, record) 70 | } 71 | 72 | return &Screen{ 73 | Lines: lines, 74 | Records: records, 75 | }, nil 76 | } 77 | 78 | type Line struct { 79 | history.Record 80 | } 81 | 82 | type Lines []Line 83 | 84 | func (s *Screen) parseLine(line string) (*Line, error) { 85 | l := strings.Split(line, "\t") 86 | var record history.Record 87 | idx := history.IndexCommandColumns() 88 | if idx == -1 { 89 | // default 90 | idx = 0 91 | } 92 | for _, record = range s.Records { 93 | if record.Command == l[idx] { 94 | break 95 | } 96 | } 97 | return &Line{record}, nil 98 | } 99 | 100 | func (l *Lines) Filter(fn func(Line) bool) *Lines { 101 | lines := make(Lines, 0) 102 | for _, line := range *l { 103 | if fn(line) { 104 | lines = append(lines, line) 105 | } 106 | } 107 | return &lines 108 | } 109 | 110 | func (s *Screen) Select() (lines Lines, err error) { 111 | if len(s.Lines) == 0 { 112 | err = errors.New("no text to display") 113 | return 114 | } 115 | selectcmd := config.Conf.Core.SelectCmd 116 | if selectcmd == "" { 117 | err = errors.New("no selectcmd specified") 118 | return 119 | } 120 | 121 | text := strings.NewReader(strings.Join(s.Lines, "\n")) 122 | var buf bytes.Buffer 123 | err = runFilter(selectcmd, text, &buf) 124 | if err != nil { 125 | return 126 | } 127 | 128 | if buf.Len() == 0 { 129 | err = errors.New("no lines selected") 130 | return 131 | } 132 | 133 | selectedLines := strings.Split(buf.String(), "\n") 134 | for _, line := range selectedLines { 135 | if line == "" { 136 | continue 137 | } 138 | parsedLine, err := s.parseLine(line) 139 | if err != nil { 140 | return lines, err 141 | } 142 | lines = append(lines, *parsedLine) 143 | } 144 | 145 | if len(lines) == 0 { 146 | err = errors.New("no lines selected") 147 | return 148 | } 149 | 150 | return 151 | } 152 | -------------------------------------------------------------------------------- /cmd/add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/b4b4r07/history/history" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var addCmd = &cobra.Command{ 12 | Use: "add", 13 | Short: "Add new history", 14 | Long: "Add new history", 15 | RunE: add, 16 | } 17 | 18 | func add(cmd *cobra.Command, args []string) error { 19 | h, err := history.Load() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | r := history.NewRecord() 25 | if addCommand == "" { 26 | return errors.New("--command option is required") 27 | } 28 | if addDir == "" { 29 | return errors.New("--dir option is required") 30 | } 31 | 32 | // Skip adding if the command is registed as ignoring word 33 | if history.CheckIgnores(addCommand) { 34 | return nil 35 | } 36 | 37 | r.SetCommand(addCommand) 38 | r.SetDir(addDir) 39 | r.SetBranch(addBranch) 40 | r.SetStatus(addStatus) 41 | 42 | // Backup before adding new record 43 | // However don't backup many times on the same day 44 | if h.Records.Latest().Date.Day() != time.Now().Day() { 45 | if err := h.Backup(); err != nil { 46 | return err 47 | } 48 | } 49 | h.Records.Add(*r) 50 | 51 | return h.Save() 52 | } 53 | 54 | var ( 55 | addCommand string 56 | addDir string 57 | addBranch string 58 | addStatus int 59 | ) 60 | 61 | func init() { 62 | RootCmd.AddCommand(addCmd) 63 | addCmd.Flags().StringVarP(&addCommand, "command", "", "", "Set command") 64 | addCmd.Flags().StringVarP(&addDir, "dir", "", "", "Set dir") 65 | addCmd.Flags().StringVarP(&addBranch, "branch", "", "", "Set branch") 66 | addCmd.Flags().IntVarP(&addStatus, "status", "", 0, "Set status") 67 | } 68 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/b4b4r07/history/cli" 9 | "github.com/b4b4r07/history/config" 10 | toml "github.com/pelletier/go-toml" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var confCmd = &cobra.Command{ 15 | Use: "config", 16 | Short: "Config the setting file", 17 | Long: "Config the setting file with your editor (default: vim)", 18 | RunE: conf, 19 | } 20 | 21 | var ( 22 | confGetKey string 23 | confAllKeys bool 24 | ) 25 | 26 | func conf(cmd *cobra.Command, args []string) error { 27 | tomlfile := config.Conf.Core.TomlFile.Abs() 28 | if tomlfile == "" { 29 | dir, _ := config.GetDefaultDir() 30 | tomlfile = filepath.Join(dir, "config.toml") 31 | } 32 | 33 | toml, err := toml.LoadFile(tomlfile) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | if confAllKeys { 39 | allMap := toml.ToMap() 40 | for _, key := range toml.Keys() { 41 | fmt.Println(strings.Join(findKey(allMap, key), "\n")) 42 | } 43 | return nil 44 | } 45 | if confGetKey != "" { 46 | value := toml.Get(confGetKey) 47 | if value != nil { 48 | fmt.Printf("%v\n", value) 49 | return nil 50 | } 51 | return fmt.Errorf("%s: no such key found", confGetKey) 52 | } 53 | 54 | editor := config.Conf.Core.Editor 55 | if editor == "" { 56 | return cli.ErrConfigEditor 57 | } 58 | return cli.Run(editor, tomlfile) 59 | } 60 | 61 | func findKey(m map[string]interface{}, k string) []string { 62 | var ret []string 63 | originKey := k 64 | if v, ok := m[k]; ok { 65 | switch v.(type) { 66 | case map[string]interface{}: 67 | m = v.(map[string]interface{}) 68 | default: 69 | } 70 | } else { 71 | return []string{} 72 | } 73 | for k, _ := range m { 74 | ret = append(ret, originKey+"."+k) 75 | } 76 | return ret 77 | } 78 | 79 | func init() { 80 | RootCmd.AddCommand(confCmd) 81 | confCmd.Flags().StringVarP(&confGetKey, "get", "", "", "Get the config value") 82 | confCmd.Flags().BoolVarP(&confAllKeys, "keys", "", false, "Get the config keys") 83 | } 84 | -------------------------------------------------------------------------------- /cmd/delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/b4b4r07/history/cli" 5 | "github.com/b4b4r07/history/config" 6 | "github.com/b4b4r07/history/history" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var deleteCmd = &cobra.Command{ 11 | Use: "delete", 12 | Short: "Delete the record from history file", 13 | Long: "Delete the selected record from history file", 14 | RunE: delete, 15 | } 16 | 17 | func delete(cmd *cobra.Command, args []string) error { 18 | if config.Conf.Screen.FilterDir { 19 | config.Conf.Var.Dir = cli.GetDirName() 20 | } 21 | if config.Conf.Screen.FilterBranch { 22 | config.Conf.Var.Branch = cli.GetBranchName() 23 | } 24 | if config.Conf.Screen.FilterHostname { 25 | config.Conf.Var.Hostname = cli.GetHostName() 26 | } 27 | 28 | screen, err := cli.NewScreen() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | lines, err := screen.Select() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | h, err := history.Load() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | for _, line := range lines { 44 | h.Records.Delete(history.Record{ 45 | Command: line.Command, 46 | Dir: line.Dir, 47 | Branch: line.Branch, 48 | }) 49 | } 50 | 51 | return h.Save() 52 | } 53 | 54 | var ( 55 | deleteDir, deleteBranch bool 56 | ) 57 | 58 | func init() { 59 | RootCmd.AddCommand(deleteCmd) 60 | deleteCmd.Flags().BoolVarP(&config.Conf.Screen.FilterDir, "filter-dir", "d", config.Conf.Screen.FilterDir, "Delete with dir") 61 | deleteCmd.Flags().BoolVarP(&config.Conf.Screen.FilterBranch, "filter-branch", "b", config.Conf.Screen.FilterBranch, "Delete with branch") 62 | deleteCmd.Flags().BoolVarP(&config.Conf.Screen.FilterHostname, "filter-hostname", "p", config.Conf.Screen.FilterHostname, "Delete with hostname") 63 | deleteCmd.Flags().StringVarP(&config.Conf.Var.Query, "query", "q", config.Conf.Var.Query, "Delete with query") 64 | deleteCmd.Flags().StringVarP(&config.Conf.Var.Columns, "columns", "c", config.Conf.Var.Columns, "Specify columns with options") 65 | } 66 | -------------------------------------------------------------------------------- /cmd/edit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/b4b4r07/history/cli" 5 | "github.com/b4b4r07/history/config" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var editCmd = &cobra.Command{ 10 | Use: "edit", 11 | Short: "Edit your history file directly", 12 | Long: "Edit your history file directly", 13 | RunE: edit, 14 | } 15 | 16 | func edit(cmd *cobra.Command, args []string) error { 17 | path := config.Conf.History.Path.Abs() 18 | if path == "" { 19 | return cli.ErrConfigHistoryPath 20 | } 21 | return cli.Edit(path) 22 | } 23 | 24 | func init() { 25 | RootCmd.AddCommand(editCmd) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/b4b4r07/history/cli" 9 | "github.com/b4b4r07/history/config" 10 | "github.com/b4b4r07/history/history" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var listCmd = &cobra.Command{ 15 | Use: "list", 16 | Short: "List the history", 17 | Long: "List the history", 18 | Run: list, 19 | } 20 | 21 | func list(cmd *cobra.Command, args []string) { 22 | h, err := history.Load() 23 | if err != nil { 24 | os.Exit(1) 25 | } 26 | 27 | h.Records.Sort() 28 | h.Records.Reverse() 29 | h.Records.Unique() 30 | h.Records.Reverse() 31 | 32 | if config.Conf.Screen.FilterDir { 33 | h.Records.Dir(cli.GetDirName()) 34 | } 35 | if config.Conf.Screen.FilterBranch { 36 | h.Records.Branch(cli.GetBranchName()) 37 | } 38 | if config.Conf.Screen.FilterHostname { 39 | config.Conf.Var.Hostname = cli.GetHostName() 40 | } 41 | if config.Conf.Var.Query != "" { 42 | h.Records.Contains(config.Conf.Var.Query) 43 | } 44 | 45 | for _, record := range h.Records { 46 | if config.Conf.Var.Columns == "" { 47 | fmt.Println(record.Raw()) 48 | } else { 49 | // TODO 50 | config.Conf.Screen.Columns = strings.Split(config.Conf.Var.Columns, ",") 51 | fmt.Println(record.Render()) 52 | } 53 | } 54 | } 55 | 56 | func init() { 57 | RootCmd.AddCommand(listCmd) 58 | listCmd.Flags().BoolVarP(&config.Conf.Screen.FilterDir, "filter-dir", "d", config.Conf.Screen.FilterDir, "List with dir") 59 | listCmd.Flags().BoolVarP(&config.Conf.Screen.FilterBranch, "filter-branch", "b", config.Conf.Screen.FilterBranch, "List with branch") 60 | listCmd.Flags().BoolVarP(&config.Conf.Screen.FilterHostname, "filter-hostname", "p", config.Conf.Screen.FilterHostname, "List with hostname") 61 | listCmd.Flags().StringVarP(&config.Conf.Var.Query, "query", "q", config.Conf.Var.Query, "List with query") 62 | listCmd.Flags().StringVarP(&config.Conf.Var.Columns, "columns", "c", config.Conf.Var.Columns, "Specify columns with options") 63 | } 64 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | 9 | "github.com/b4b4r07/history/config" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | const Version = "0.0.1" 14 | 15 | var showVersion bool 16 | 17 | var RootCmd = &cobra.Command{ 18 | Use: "history", 19 | Short: "Enhanced shell history with LTSV", 20 | Long: "Enhanced shell history with LTSV", 21 | SilenceUsage: true, 22 | SilenceErrors: true, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | if showVersion { 25 | fmt.Printf("version %s/%s\n", Version, runtime.Version()) 26 | return 27 | } 28 | cmd.Usage() 29 | }, 30 | } 31 | 32 | func Execute() { 33 | err := RootCmd.Execute() 34 | if err != nil { 35 | fmt.Fprintln(os.Stderr, err) 36 | os.Exit(1) 37 | } 38 | } 39 | 40 | func init() { 41 | initConf() 42 | RootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit") 43 | } 44 | 45 | func initConf() { 46 | dir, _ := config.GetDefaultDir() 47 | toml := filepath.Join(dir, "config.toml") 48 | 49 | err := config.Conf.LoadFile(toml) 50 | if err != nil { 51 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 52 | os.Exit(1) 53 | } 54 | 55 | if histPath := filepath.Dir(config.Conf.History.Path.Abs()); histPath != "" { 56 | if _, err := os.Stat(histPath); err != nil { 57 | fmt.Printf("Creating directory '%s' for history storage", histPath) 58 | os.MkdirAll(histPath, 0700) 59 | } 60 | } 61 | 62 | if backupPath := config.Conf.History.BackupPath.Abs(); backupPath != "" { 63 | if _, err := os.Stat(backupPath); err != nil { 64 | fmt.Printf("Creating directory '%s' for backup storage", backupPath) 65 | os.MkdirAll(backupPath, 0700) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cmd/search.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/b4b4r07/history/cli" 7 | "github.com/b4b4r07/history/config" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var searchCmd = &cobra.Command{ 12 | Use: "search", 13 | Short: "Search the command from the history file", 14 | Long: "Search the command from the history file", 15 | RunE: search, 16 | } 17 | 18 | func search(cmd *cobra.Command, args []string) error { 19 | if config.Conf.Screen.FilterDir { 20 | config.Conf.Var.Dir = cli.GetDirName() 21 | } 22 | if config.Conf.Screen.FilterBranch { 23 | config.Conf.Var.Branch = cli.GetBranchName() 24 | } 25 | if config.Conf.Screen.FilterHostname { 26 | config.Conf.Var.Hostname = cli.GetHostName() 27 | } 28 | if config.Conf.Screen.FilterStatus { 29 | config.Conf.Var.Status = true 30 | } 31 | 32 | screen, err := cli.NewScreen() 33 | if err != nil { 34 | return err 35 | } 36 | 37 | lines, err := screen.Select() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | command := lines[0].Command 43 | for _, line := range lines[1:] { 44 | command += "; " + line.Command 45 | } 46 | fmt.Println(command) 47 | 48 | return nil 49 | } 50 | 51 | func init() { 52 | RootCmd.AddCommand(searchCmd) 53 | searchCmd.Flags().BoolVarP(&config.Conf.Screen.FilterDir, "filter-dir", "d", config.Conf.Screen.FilterDir, "Search with dir") 54 | searchCmd.Flags().BoolVarP(&config.Conf.Screen.FilterBranch, "filter-branch", "b", config.Conf.Screen.FilterBranch, "Search with branch") 55 | searchCmd.Flags().BoolVarP(&config.Conf.Screen.FilterHostname, "filter-hostname", "p", config.Conf.Screen.FilterHostname, "Search with hostname") 56 | searchCmd.Flags().BoolVarP(&config.Conf.Screen.FilterStatus, "filter-status", "s", config.Conf.Screen.FilterStatus, "Search with status OK") 57 | searchCmd.Flags().StringVarP(&config.Conf.Var.Query, "query", "q", config.Conf.Var.Query, "Search with query") 58 | searchCmd.Flags().StringVarP(&config.Conf.Var.Columns, "columns", "c", config.Conf.Var.Columns, "Specify columns with options") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/sync.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/b4b4r07/go-ask" 11 | "github.com/b4b4r07/history/config" 12 | "github.com/b4b4r07/history/history" 13 | "github.com/briandowns/spinner" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var syncCmd = &cobra.Command{ 18 | Use: "sync", 19 | Short: "Sync the history file with gist", 20 | Long: "Sync the history file with gist", 21 | RunE: sync, 22 | } 23 | 24 | func sync(cmd *cobra.Command, args []string) error { 25 | s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 26 | s.Prefix = "\r" 27 | s.Writer = os.Stdout 28 | s.Start() 29 | defer s.Stop() 30 | 31 | h, err := history.Load() 32 | if err != nil { 33 | return err 34 | } 35 | if syncInterval > 0 { 36 | if skipSyncFor(syncInterval) { 37 | return fmt.Errorf("interval %v has not passed yet", syncInterval) 38 | } 39 | } 40 | diff, err := h.GetDiff() 41 | if err != nil { 42 | return err 43 | } 44 | if config.Conf.History.Sync.Size != 0 { 45 | if diff.Size < config.Conf.History.Sync.Size { 46 | return fmt.Errorf("The history difference %d is less than the specified size %d", 47 | diff.Size, config.Conf.History.Sync.Size) 48 | } 49 | } 50 | if syncAsk { 51 | s.Stop() 52 | if !ask.NewQ().Confirmf("%s: sync immediately?", config.Conf.History.Path) { 53 | return errors.New("canceled") 54 | } 55 | } 56 | s.Start() 57 | return h.Sync(diff) 58 | } 59 | 60 | func skipSyncFor(interval time.Duration) bool { 61 | file := filepath.Join(filepath.Dir(config.Conf.History.Path.Abs()), ".sync") 62 | f, err := os.OpenFile(file, os.O_RDONLY|os.O_CREATE, 0600) 63 | if err != nil { 64 | // Doesn't skip if some errors occur 65 | return false 66 | } 67 | defer f.Close() 68 | fi, err := f.Stat() 69 | if err != nil { 70 | // Doesn't skip if some errors occur 71 | return false 72 | } 73 | if time.Now().Sub(fi.ModTime()).Hours() < interval.Hours() { 74 | // Skip if the fixed time has not elapsed 75 | return true 76 | } 77 | // Update the timestamp if sync 78 | os.Chtimes(file, time.Now(), time.Now()) 79 | return false 80 | } 81 | 82 | var ( 83 | syncInterval time.Duration 84 | syncAsk bool 85 | ) 86 | 87 | func init() { 88 | RootCmd.AddCommand(syncCmd) 89 | syncCmd.Flags().DurationVarP(&syncInterval, "interval", "", 0, "Sync with the interval") 90 | syncCmd.Flags().BoolVarP(&syncAsk, "ask", "", false, "Sync after the confirmation") 91 | syncCmd.Flags().IntVarP(&config.Conf.History.Sync.Size, "diff", "", config.Conf.History.Sync.Size, "Sync if the diff exceeds a certain number") 92 | } 93 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/user" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/BurntSushi/toml" 13 | ) 14 | 15 | type Path struct { 16 | path string 17 | } 18 | 19 | func NewPath(path string) Path { 20 | p := Path{path: path} 21 | return p 22 | } 23 | 24 | func (p *Path) UnmarshalText(text []byte) error { 25 | p.path = string(text) 26 | return nil 27 | } 28 | 29 | func (p Path) MarshalText() (text []byte, err error) { 30 | return []byte(p.path), nil 31 | } 32 | 33 | func (p *Path) Abs() string { 34 | path := p.path 35 | 36 | if strings.HasPrefix(path, "~/") || strings.HasPrefix(path, "$HOME/") { 37 | home := "" 38 | if home = os.Getenv("HOME"); home == "" { 39 | user, err := user.Current() 40 | if err != nil { 41 | log.Fatalf("Failed to get user home and $HOME not set.") 42 | } 43 | home = user.HomeDir 44 | } 45 | 46 | if home == "" { 47 | log.Fatalf("Failed to get user home and $HOME not set.") 48 | } 49 | 50 | if strings.HasPrefix(path, "~/") { 51 | path = strings.Replace(path, "~/", home+"/", 1) 52 | } 53 | if strings.HasPrefix(path, "$HOME/") { 54 | path = strings.Replace(path, "$HOME/", home+"/", 1) 55 | } 56 | } 57 | return path 58 | } 59 | 60 | type Config struct { 61 | Core CoreConfig `toml:"core"` 62 | History HistoryConfig `toml:"history"` 63 | Screen ScreenConfig `toml:"screen"` 64 | 65 | // Var cooperates with other packages 66 | Var VarConfig `toml:"-"` 67 | } 68 | 69 | type CoreConfig struct { 70 | Editor string `toml:"editor"` 71 | SelectCmd string `toml:"selectcmd"` 72 | TomlFile Path `toml:"tomlfile"` 73 | } 74 | 75 | type HistoryConfig struct { 76 | Path Path `toml:"path"` 77 | BackupPath Path `toml:"backup_path"` 78 | Ignores []string `toml:"ignore_words"` 79 | Sync SyncConfig `toml:"sync"` 80 | UseColor bool `toml:"use_color"` 81 | } 82 | 83 | type SyncConfig struct { 84 | ID string `toml:"id"` 85 | Token string `toml:"token"` 86 | Size int `toml:"size"` 87 | } 88 | 89 | type ScreenConfig struct { 90 | FilterDir bool `toml:"filter_dir"` 91 | FilterBranch bool `toml:"filter_branch"` 92 | FilterHostname bool `toml:"filter_hostname"` 93 | FilterStatus bool `toml:"filter_status"` 94 | Columns []string `toml:"columns"` 95 | StatusOK string `toml:"status_ok"` 96 | StatusNG string `toml:"status_ng"` 97 | } 98 | 99 | type VarConfig struct { 100 | Dir string 101 | Branch string 102 | Hostname string 103 | Status bool 104 | Query string 105 | Columns string 106 | } 107 | 108 | var Conf Config 109 | 110 | func GetDefaultDir() (string, error) { 111 | var dir string 112 | 113 | switch runtime.GOOS { 114 | default: 115 | dir = filepath.Join(os.Getenv("HOME"), ".config") 116 | case "windows": 117 | dir = os.Getenv("APPDATA") 118 | if dir == "" { 119 | dir = filepath.Join(os.Getenv("USERPROFILE"), "Application Data") 120 | } 121 | } 122 | dir = filepath.Join(dir, "history") 123 | 124 | err := os.MkdirAll(dir, 0700) 125 | if err != nil { 126 | return dir, fmt.Errorf("cannot create directory: %v", err) 127 | } 128 | 129 | return dir, nil 130 | } 131 | 132 | func (cfg *Config) Save() error { 133 | f, err := os.OpenFile(cfg.Core.TomlFile.Abs(), os.O_RDWR|os.O_CREATE, 0644) 134 | if err != nil { 135 | return err 136 | } 137 | return toml.NewEncoder(f).Encode(cfg) 138 | } 139 | 140 | func (cfg *Config) LoadFile(file string) error { 141 | _, err := os.Stat(file) 142 | if err == nil { 143 | _, err := toml.DecodeFile(file, cfg) 144 | if err != nil { 145 | return err 146 | } 147 | return nil 148 | } 149 | 150 | if !os.IsNotExist(err) { 151 | return err 152 | } 153 | f, err := os.Create(file) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | // base dir 159 | dir := filepath.Dir(file) 160 | 161 | cfg.Core.Editor = os.Getenv("EDITOR") 162 | if cfg.Core.Editor == "" { 163 | cfg.Core.Editor = "vim" 164 | } 165 | cfg.Core.SelectCmd = "fzf-tmux --multi:fzf --multi:peco" 166 | cfg.Core.TomlFile = NewPath(file) 167 | 168 | cfg.History.Path = NewPath(filepath.Join(dir, "history.ltsv")) 169 | cfg.History.BackupPath = NewPath(filepath.Join(dir, ".backup")) 170 | 171 | cfg.History.Ignores = []string{} 172 | cfg.History.UseColor = false 173 | cfg.History.Sync.ID = "" 174 | cfg.History.Sync.Token = "$GITHUB_TOKEN" 175 | if token := os.Getenv("GITHUB_TOKEN"); token != "" { 176 | cfg.History.Sync.Token = token 177 | } 178 | cfg.History.Sync.Size = 100 179 | 180 | cfg.Screen.FilterDir = false 181 | cfg.Screen.FilterBranch = false 182 | cfg.Screen.FilterHostname = false 183 | cfg.Screen.FilterStatus = false 184 | cfg.Screen.Columns = []string{"{{.Time}}", "{{.Status}}", "{{.Command}}"} 185 | cfg.Screen.StatusOK = " " 186 | cfg.Screen.StatusNG = "x" 187 | 188 | return toml.NewEncoder(f).Encode(cfg) 189 | } 190 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "path/filepath" 5 | "io/ioutil" 6 | "testing" 7 | "os" 8 | ) 9 | 10 | func TestMarshalDefaultConfig(t *testing.T) { 11 | dir, err := ioutil.TempDir("", "history-test") 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | defer os.RemoveAll(dir) 16 | 17 | file := filepath.Join(dir, "config.toml") 18 | 19 | // Loading the config for the first time 20 | // creates the default and marshals it 21 | err = Conf.LoadFile(file) 22 | if err != nil { 23 | t.Errorf("Failed to create default config file: %v", err) 24 | } 25 | 26 | // Loading the config for the second time 27 | // reads it from disk and unmarshals it 28 | err = Conf.LoadFile(file) 29 | if err != nil { 30 | t.Errorf("Failed to load default config file: %v", err) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/b4b4r07/history 2 | 3 | require ( 4 | github.com/BurntSushi/toml v0.3.0 5 | github.com/Songmu/go-ltsv v0.0.0-20160713011217-15982a68f758 6 | github.com/b4b4r07/go-ask v0.0.0-20170613063323-51df57094104 7 | github.com/b4b4r07/go-colon v0.0.0-20170422170013-bc5ae99f1c2b 8 | github.com/b4b4r07/go-pathshorten v0.0.0-20170605152316-b07af83744ea 9 | github.com/briandowns/spinner v0.0.0-20170404174923-fb621c2fd72f 10 | github.com/davecgh/go-spew v1.1.1 // indirect 11 | github.com/dustin/go-humanize v0.0.0-20151125214831-8929fe90cee4 12 | github.com/fatih/color v1.4.1 13 | github.com/golang/protobuf v0.0.0-20170601230230-5a0f697c9ed9 // indirect 14 | github.com/google/go-github v0.0.0-20170512175250-4a42a1def4af 15 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect 16 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 17 | github.com/kballard/go-shellquote v0.0.0-20150810074751-d8ec1a69a250 18 | github.com/kr/pretty v0.1.0 // indirect 19 | github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561 // indirect 20 | github.com/mattn/go-isatty v0.0.2 // indirect 21 | github.com/mattn/go-pipeline v0.0.0-20160420152404-4861b26ff5ba 22 | github.com/mattn/go-shellwords v1.0.3 // indirect 23 | github.com/pelletier/go-buffruneio v0.2.0 // indirect 24 | github.com/pelletier/go-toml v0.0.0-20170321141048-25e50242f65c 25 | github.com/spf13/cobra v0.0.0-20170514125104-51b7cf57e102 26 | github.com/spf13/pflag v1.0.0 // indirect 27 | golang.org/x/crypto v0.0.0-20170512130425-ab89591268e0 28 | golang.org/x/net v0.0.0-20170513003010-84f0e6f92b10 // indirect 29 | golang.org/x/oauth2 v0.0.0-20170510215623-ad516a297a9f 30 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 31 | golang.org/x/sys v0.0.0-20160717071931-a646d33e2ee3 // indirect 32 | google.golang.org/appengine v1.0.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= 2 | github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/Songmu/go-ltsv v0.0.0-20160713011217-15982a68f758 h1:aDTK7JRZlkb0H0d1P5CQ2C2LG/RX7Ottz7UYYZaf0fI= 4 | github.com/Songmu/go-ltsv v0.0.0-20160713011217-15982a68f758/go.mod h1:LBP+tS9C2iiUoR7AGPaZYY+kjXgB5eZxZKbSEBL9UFw= 5 | github.com/b4b4r07/go-ask v0.0.0-20170613063323-51df57094104 h1:ZG/bSddWhaCm8S4oR0w8WCo5nQzKhv0JXk/jzOU1JU0= 6 | github.com/b4b4r07/go-ask v0.0.0-20170613063323-51df57094104/go.mod h1:wLg91grRA3oBxS+jxg1gEvUE7J79ebFzUL5iJZXsjcs= 7 | github.com/b4b4r07/go-colon v0.0.0-20170422170013-bc5ae99f1c2b h1:HsUCBMd35ftBSjQXlg0BR+wdbjMlJyV6j8tOC7VwV34= 8 | github.com/b4b4r07/go-colon v0.0.0-20170422170013-bc5ae99f1c2b/go.mod h1:Zgql1XCCHFhr7N1n4rWNS1f7TTLNkVxGyD6vFr18DdE= 9 | github.com/b4b4r07/go-pathshorten v0.0.0-20170605152316-b07af83744ea h1:O3+d9y9YSEwY+ZsqBYvzYOIEtJUgTOk6qtcbqxg2gu4= 10 | github.com/b4b4r07/go-pathshorten v0.0.0-20170605152316-b07af83744ea/go.mod h1:HdhsBEviOgiPNlurQx1Fbr/17SkwL6vUYMRZLNgXy0Y= 11 | github.com/briandowns/spinner v0.0.0-20170404174923-fb621c2fd72f h1:vv09SPvWkvmEzbRlEb/0wSz9lab6+FiUAZTkkpx0WUU= 12 | github.com/briandowns/spinner v0.0.0-20170404174923-fb621c2fd72f/go.mod h1:hw/JEQBIE+c/BLI4aKM8UU8v+ZqrD3h7HC27kKt8JQU= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/dustin/go-humanize v0.0.0-20151125214831-8929fe90cee4 h1:WX/DKY159S5AHCpmUWGsVKoCXqLSpKd0R1150CWscw8= 16 | github.com/dustin/go-humanize v0.0.0-20151125214831-8929fe90cee4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 17 | github.com/fatih/color v1.4.1 h1:YJhD/SoQqn7ev9zwhIm7lHTAqsOAF2AN4xlAVZzNZnU= 18 | github.com/fatih/color v1.4.1/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 19 | github.com/golang/protobuf v0.0.0-20170601230230-5a0f697c9ed9 h1:6w6GCsh1LARYT2JCCS9B+cHIzp/zNoKCrEQrReZZ2p8= 20 | github.com/golang/protobuf v0.0.0-20170601230230-5a0f697c9ed9/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/google/go-github v0.0.0-20170512175250-4a42a1def4af h1:+xW5mRARtj6byThrH4y4PPUlBEysNPWLu8WBT6NvZyE= 22 | github.com/google/go-github v0.0.0-20170512175250-4a42a1def4af/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 23 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= 24 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 25 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 26 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 27 | github.com/kballard/go-shellquote v0.0.0-20150810074751-d8ec1a69a250 h1:QyPDU73WRl/8CnuK3JltZLLuNhL3E4o3BROt4g8nFf0= 28 | github.com/kballard/go-shellquote v0.0.0-20150810074751-d8ec1a69a250/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 29 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 30 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 31 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 32 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 33 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 34 | github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561 h1:isR/L+BIZ+rqODWYR/f526ygrBMGKZYFhaaFRDGvuZ8= 35 | github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 36 | github.com/mattn/go-isatty v0.0.2 h1:F+DnWktyadxnOrohKLNUC9/GjFii5RJgY4GFG6ilggw= 37 | github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 38 | github.com/mattn/go-pipeline v0.0.0-20160420152404-4861b26ff5ba h1:OfT26h4km8cOog+h3vU3Si0sLP0OJ74B8Sn4PbJXVKU= 39 | github.com/mattn/go-pipeline v0.0.0-20160420152404-4861b26ff5ba/go.mod h1:THCMZVX5asLpinN+6hFlR1xKFcFsaDpAtUltGqZauBM= 40 | github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk= 41 | github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= 42 | github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= 43 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 44 | github.com/pelletier/go-toml v0.0.0-20170321141048-25e50242f65c h1:jNDPqXknGk4ipBeSN+QFWUHAdx222yrVA5lU+NSgAMU= 45 | github.com/pelletier/go-toml v0.0.0-20170321141048-25e50242f65c/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 46 | github.com/spf13/cobra v0.0.0-20170514125104-51b7cf57e102 h1:zVWWwqwYXKcDPq7G63KcHiks7RFR6D9LHmpIh1rxc3o= 47 | github.com/spf13/cobra v0.0.0-20170514125104-51b7cf57e102/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 48 | github.com/spf13/pflag v1.0.0 h1:oaPbdDe/x0UncahuwiPxW1GYJyilRAdsPnq3e1yaPcI= 49 | github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 50 | golang.org/x/crypto v0.0.0-20170512130425-ab89591268e0 h1:Kv0JVjoWyBVkLETNHnV/PxoZcMP3J7+WTc6+QQnzZmY= 51 | golang.org/x/crypto v0.0.0-20170512130425-ab89591268e0/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 52 | golang.org/x/net v0.0.0-20170513003010-84f0e6f92b10 h1:ZKvORbFCQlZvHCWzpNCaToNXamq9BJj6rIodfQM1zfY= 53 | golang.org/x/net v0.0.0-20170513003010-84f0e6f92b10/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 54 | golang.org/x/oauth2 v0.0.0-20170510215623-ad516a297a9f h1:CFH9uw6aZZxLWsJbWLYF+gzwJwaIBn0xeo9xdJuhTWA= 55 | golang.org/x/oauth2 v0.0.0-20170510215623-ad516a297a9f/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 56 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 57 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sys v0.0.0-20160717071931-a646d33e2ee3 h1:ZLExsLvnoqWSw6JB6k6RjWobIHGR3NG9dzVANJ7SVKc= 59 | golang.org/x/sys v0.0.0-20160717071931-a646d33e2ee3/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 60 | google.golang.org/appengine v1.0.0 h1:dN4LljjBKVChsv0XCSI+zbyzdqrkEwX5LQFUMRSGqOc= 61 | google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 62 | -------------------------------------------------------------------------------- /history/history.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "time" 10 | 11 | "github.com/b4b4r07/history/config" 12 | "github.com/google/go-github/github" 13 | ) 14 | 15 | type History struct { 16 | Records Records 17 | Path string 18 | 19 | client *github.Client 20 | } 21 | 22 | func Load() (h *History, err error) { 23 | var records []Record 24 | path := config.Conf.History.Path.Abs() 25 | h = &History{Records: Records{}, Path: path} 26 | 27 | file, err := os.Open(path) 28 | if err != nil { 29 | // Return nil to regard it as no history (new) 30 | // if an open error occurs 31 | err = nil 32 | return 33 | } 34 | 35 | scanner := bufio.NewScanner(file) 36 | for scanner.Scan() { 37 | record := &Record{} 38 | record.Unmarshal(scanner.Text()) 39 | records = append(records, *record) 40 | } 41 | 42 | err = scanner.Err() 43 | if err != nil { 44 | return 45 | } 46 | 47 | return &History{ 48 | Records: records, 49 | Path: path, 50 | }, nil 51 | } 52 | 53 | func (h *History) Save() error { 54 | file, err := os.OpenFile(h.Path, os.O_WRONLY|os.O_CREATE, 0600) 55 | if err != nil { 56 | return err 57 | } 58 | defer file.Close() 59 | 60 | w := bufio.NewWriter(file) 61 | for _, record := range h.Records { 62 | b, _ := record.Marshal() 63 | w.Write(b) 64 | w.Write([]byte("\n")) 65 | } 66 | 67 | return w.Flush() 68 | } 69 | 70 | func (h *History) Backup() (err error) { 71 | if _, err := os.Stat(h.Path); err != nil { 72 | // cannot backup if no history 73 | return nil 74 | } 75 | 76 | dir := "" 77 | p := config.Conf.History.BackupPath 78 | if p.Abs() != "" { 79 | dir = p.Abs() 80 | } else { 81 | dir, err = config.GetDefaultDir() 82 | if err != nil { 83 | return err 84 | } 85 | } 86 | 87 | dir = filepath.Join(dir, ".backup", time.Now().Format("2006/01/02")) 88 | err = os.MkdirAll(dir, 0700) 89 | if err != nil { 90 | return 91 | } 92 | 93 | src, err := os.Open(h.Path) 94 | if err != nil { 95 | return 96 | } 97 | defer src.Close() 98 | 99 | dst, err := os.Create(filepath.Join(dir, filepath.Base(h.Path))) 100 | if err != nil { 101 | return 102 | } 103 | defer dst.Close() 104 | 105 | _, err = io.Copy(dst, src) 106 | return err 107 | } 108 | 109 | func CheckIgnores(command string) bool { 110 | for _, ignore := range config.Conf.History.Ignores { 111 | re := regexp.MustCompile(ignore) 112 | if re.MatchString(command) { 113 | return true 114 | } 115 | } 116 | return false 117 | } 118 | 119 | func IndexCommandColumns() int { 120 | for i, v := range config.Conf.Screen.Columns { 121 | if v == "{{.Command}}" { 122 | return i 123 | } 124 | } 125 | return -1 126 | } 127 | -------------------------------------------------------------------------------- /history/history_test.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/b4b4r07/history/config" 7 | ) 8 | 9 | func TestCheckIgnores(t *testing.T) { 10 | config.Conf.History.Ignores = []string{ 11 | `^cd(\s+-+\w?)?$`, 12 | } 13 | tests := []struct { 14 | command string 15 | want bool 16 | }{ 17 | {command: "cd", want: true}, 18 | {command: "cd ", want: false}, 19 | {command: " cd", want: false}, 20 | {command: "cd -", want: true}, 21 | {command: "cd -G", want: true}, 22 | } 23 | for _, test := range tests { 24 | got := CheckIgnores(test.command) 25 | if got != test.want { 26 | t.Fatalf("want %v, but %v:", test.want, got) 27 | } 28 | } 29 | } 30 | 31 | func TestIndexCommandColumns(t *testing.T) { 32 | tests := []struct { 33 | columns []string 34 | want int 35 | }{ 36 | {columns: []string{"{{.Command}}"}, want: 0}, 37 | {columns: []string{"{{.Time}}", "{{.Status}}"}, want: -1}, 38 | {columns: []string{"{{.Time}}", "{{.Status}}", "{{.Command}}"}, want: 2}, 39 | } 40 | for _, test := range tests { 41 | config.Conf.Screen.Columns = test.columns 42 | got := IndexCommandColumns() 43 | if got != test.want { 44 | t.Fatalf("want %v, but %v:", test.want, got) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /history/record.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "sort" 10 | "strings" 11 | tt "text/template" 12 | "time" 13 | 14 | "golang.org/x/crypto/ssh/terminal" 15 | 16 | ltsv "github.com/Songmu/go-ltsv" 17 | pathshorten "github.com/b4b4r07/go-pathshorten" 18 | "github.com/b4b4r07/history/config" 19 | "github.com/dustin/go-humanize" 20 | "github.com/fatih/color" 21 | pipeline "github.com/mattn/go-pipeline" 22 | ) 23 | 24 | type Record struct { 25 | Date time.Time 26 | Command string 27 | Dir string 28 | Branch string 29 | Status int 30 | Hostname string 31 | } 32 | 33 | type Records []Record 34 | 35 | func NewRecord() *Record { 36 | hostname, _ := os.Hostname() 37 | return &Record{ 38 | Date: time.Now(), 39 | Hostname: hostname, 40 | } 41 | } 42 | 43 | func (r *Record) SetCommand(arg string) { r.Command = arg } 44 | func (r *Record) SetDir(arg string) { r.Dir = arg } 45 | func (r *Record) SetBranch(arg string) { r.Branch = arg } 46 | func (r *Record) SetStatus(arg int) { r.Status = arg } 47 | 48 | func (r *Record) Raw() string { 49 | out, _ := r.Marshal() 50 | return string(out) 51 | } 52 | 53 | func (r *Record) Render() (line string) { 54 | var tmpl *tt.Template 55 | columns := config.Conf.Screen.Columns 56 | if len(columns) == 0 { 57 | // default 58 | columns = []string{"{{.Command}}"} 59 | } 60 | format := columns[0] 61 | for _, v := range columns[1:] { 62 | format += "\t" + v 63 | } 64 | t, err := tt.New("format").Parse(format) 65 | if err != nil { 66 | return 67 | } 68 | tmpl = t 69 | if tmpl != nil { 70 | var b bytes.Buffer 71 | err := tmpl.Execute(&b, map[string]interface{}{ 72 | "Date": r.Date.Format("2006-01-02"), 73 | "Time": fmt.Sprintf("%-15s", humanize.Time(r.Date)), 74 | "Command": r.renderCommand(), 75 | "Dir": r.renderDir(), 76 | "Path": r.Dir, 77 | "Base": color.BlueString(filepath.Base(r.Dir)), 78 | "Branch": r.Branch, 79 | "Status": func(status int) string { 80 | switch status { 81 | case 0: 82 | ok := config.Conf.Screen.StatusOK 83 | if ok == "" { 84 | ok = "o" 85 | } 86 | return color.GreenString(ok) 87 | default: 88 | ng := config.Conf.Screen.StatusNG 89 | if ng == "" { 90 | ng = "x" 91 | } 92 | return color.RedString(ng) 93 | } 94 | }(r.Status), 95 | "Hostname": r.Hostname, 96 | }) 97 | if err != nil { 98 | return 99 | } 100 | line = b.String() 101 | } 102 | return 103 | } 104 | 105 | func (r *Record) renderCommand() string { 106 | if !config.Conf.History.UseColor { 107 | return r.Command 108 | } 109 | highlight, err := exec.LookPath("highlight") 110 | if err != nil { 111 | return r.Command 112 | } 113 | // TODO: more faster 114 | out, err := pipeline.Output( 115 | []string{"echo", r.Command}, 116 | []string{highlight, "-S", "sh", "-O", "ansi"}, 117 | ) 118 | if err != nil { 119 | return r.Command 120 | } 121 | return strings.TrimSuffix(string(out), "\n") 122 | } 123 | 124 | func (r *Record) renderDir() string { 125 | w, _, err := terminal.GetSize(int(os.Stdout.Fd())) 126 | if err != nil { 127 | w = 20 128 | } 129 | dir := r.Dir 130 | if len(dir) > w/3 { 131 | dir = pathshorten.Run(dir) 132 | } 133 | return color.BlueString(dir) 134 | } 135 | 136 | func (r *Record) Unmarshal(line string) { 137 | ltsv.Unmarshal([]byte(line), r) 138 | } 139 | 140 | func (r *Record) Marshal() (line []byte, err error) { 141 | b, err := ltsv.Marshal(r) 142 | if err != nil { 143 | return 144 | } 145 | return b, nil 146 | } 147 | 148 | func (rs *Records) Add(r Record) { 149 | *rs = append(*rs, r) 150 | } 151 | 152 | func (rs *Records) Delete(r Record) { 153 | *rs = *rs.Reduce(func(rr Record) bool { 154 | return rr.Command == r.Command && rr.Dir == r.Dir && rr.Branch == r.Branch 155 | }) 156 | } 157 | 158 | func (r *Records) Reduce(fn func(Record) bool) *Records { 159 | records := make(Records, 0) 160 | for _, record := range *r { 161 | if !fn(record) { 162 | records = append(records, record) 163 | } 164 | } 165 | return &records 166 | } 167 | 168 | func (r *Records) Filter(fn func(Record) bool) *Records { 169 | records := make(Records, 0) 170 | for _, record := range *r { 171 | if fn(record) { 172 | records = append(records, record) 173 | } 174 | } 175 | return &records 176 | } 177 | 178 | func (r *Records) Unique() { 179 | rs := make(Records, 0) 180 | encountered := map[string]bool{} 181 | for _, record := range *r { 182 | if !encountered[record.Command] { 183 | encountered[record.Command] = true 184 | rs = append(rs, record) 185 | } 186 | } 187 | *r = rs 188 | } 189 | 190 | func (r *Records) Reverse() { 191 | var rs Records 192 | for i := len(*r) - 1; i >= 0; i-- { 193 | rs = append(rs, (*r)[i]) 194 | } 195 | *r = rs 196 | } 197 | 198 | func (r *Records) Contains(word string) { 199 | *r = *r.Filter(func(r Record) bool { 200 | return strings.Contains(r.Command, word) 201 | }) 202 | } 203 | 204 | func (r *Records) Branch(branch string) { 205 | *r = *r.Filter(func(r Record) bool { 206 | return r.Branch == branch 207 | }) 208 | } 209 | 210 | func (r *Records) Dir(dir string) { 211 | *r = *r.Filter(func(r Record) bool { 212 | return r.Dir == dir 213 | }) 214 | } 215 | 216 | func (r *Records) Latest() Record { 217 | if len(*r) < 1 { 218 | return Record{} 219 | } 220 | return (*r)[len(*r)-1] 221 | } 222 | 223 | func (r Records) Len() int { return len(r) } 224 | func (r Records) Less(i, j int) bool { return r[i].Date.Before(r[j].Date) } 225 | func (r Records) Swap(i, j int) { r[i], r[j] = r[j], r[i] } 226 | 227 | func (r *Records) Sort() { 228 | sort.Sort(*r) 229 | } 230 | 231 | func init() { 232 | color.NoColor = false 233 | } 234 | -------------------------------------------------------------------------------- /history/sync.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "math" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | 15 | "github.com/b4b4r07/history/config" 16 | "github.com/google/go-github/github" 17 | "golang.org/x/oauth2" 18 | ) 19 | 20 | func getClient() (gc *github.Client, err error) { 21 | cfg := config.Conf.History.Sync 22 | if cfg.Token == "" { 23 | err = errors.New("config history.sync.token is missing") 24 | return 25 | } 26 | ts := oauth2.StaticTokenSource( 27 | &oauth2.Token{AccessToken: expandPath(cfg.Token)}, 28 | ) 29 | tc := oauth2.NewClient(oauth2.NoContext, ts) 30 | return github.NewClient(tc), nil 31 | } 32 | 33 | func expandPath(s string) string { 34 | if len(s) >= 2 && s[0] == '~' && os.IsPathSeparator(s[1]) { 35 | if runtime.GOOS == "windows" { 36 | s = filepath.Join(os.Getenv("USERPROFILE"), s[2:]) 37 | } else { 38 | s = filepath.Join(os.Getenv("HOME"), s[2:]) 39 | } 40 | } 41 | return os.Expand(s, os.Getenv) 42 | } 43 | 44 | func (h *History) Merge(a, b string) { 45 | lines := strings.Split(a+b, "\n") 46 | var records Records 47 | for _, line := range lines { 48 | if line == "" { 49 | continue 50 | } 51 | var r Record 52 | r.Unmarshal(line) 53 | records = append(records, r) 54 | } 55 | records.Sort() 56 | rs := make(Records, 0) 57 | encountered := map[Record]bool{} 58 | for _, record := range records { 59 | if !encountered[record] { 60 | encountered[record] = true 61 | rs = append(rs, record) 62 | } 63 | } 64 | h.Records = rs 65 | } 66 | 67 | func (h *History) updateLocal() error { 68 | var b bytes.Buffer 69 | for _, record := range h.Records { 70 | line, _ := record.Marshal() 71 | b.Write(line) 72 | b.WriteString("\n") 73 | } 74 | return ioutil.WriteFile(h.Path, b.Bytes(), os.ModePerm) 75 | } 76 | 77 | func (h *History) updateRemote() error { 78 | var b bytes.Buffer 79 | for _, record := range h.Records { 80 | line, _ := record.Marshal() 81 | b.Write(line) 82 | b.WriteString("\n") 83 | } 84 | gist := github.Gist{ 85 | Files: map[github.GistFilename]github.GistFile{ 86 | github.GistFilename(filepath.Base(h.Path)): { 87 | Content: github.String(b.String()), 88 | }, 89 | }, 90 | } 91 | _, _, err := h.client.Gists.Edit(context.Background(), config.Conf.History.Sync.ID, &gist) 92 | return err 93 | } 94 | 95 | func (h *History) getGistID() (id string, err error) { 96 | var items []*github.Gist 97 | ctx := context.Background() 98 | 99 | // List items from gist.github.com 100 | gists, resp, err := h.client.Gists.List(ctx, "", &github.GistListOptions{}) 101 | if err != nil { 102 | return 103 | } 104 | items = append(items, gists...) 105 | 106 | // pagenation 107 | for i := 2; i <= resp.LastPage; i++ { 108 | gists, _, err := h.client.Gists.List(ctx, "", &github.GistListOptions{ 109 | ListOptions: github.ListOptions{Page: i}, 110 | }) 111 | if err != nil { 112 | continue 113 | } 114 | items = append(items, gists...) 115 | } 116 | 117 | for _, item := range items { 118 | for _, file := range item.Files { 119 | if *file.Filename == filepath.Base(config.Conf.History.Path.Abs()) { 120 | id = *item.ID 121 | break 122 | } 123 | } 124 | } 125 | 126 | // Case that couldn't be found 127 | if id == "" { 128 | id, err = h.create() 129 | } 130 | 131 | return 132 | } 133 | 134 | func (h *History) create() (id string, err error) { 135 | out, err := ioutil.ReadFile(h.Path) 136 | if err != nil { 137 | return 138 | } 139 | localContent := string(out) 140 | var ( 141 | files = map[github.GistFilename]github.GistFile{ 142 | github.GistFilename(filepath.Base(h.Path)): { 143 | Content: github.String(localContent), 144 | }, 145 | } 146 | public = false 147 | desc = "" 148 | ) 149 | item, resp, err := h.client.Gists.Create(context.Background(), &github.Gist{ 150 | Files: files, 151 | Public: &public, 152 | Description: &desc, 153 | }) 154 | if item == nil { 155 | err = errors.New("unexpected fatal error: item is nil") 156 | return 157 | } 158 | if resp == nil { 159 | err = errors.New("Try again when you have a better network connection") 160 | return 161 | } 162 | id = *item.ID 163 | return 164 | } 165 | 166 | type Diff struct { 167 | Local struct { 168 | Size int 169 | Content string 170 | } 171 | Remote struct { 172 | Size int 173 | Content string 174 | } 175 | Size int 176 | } 177 | 178 | func (h *History) GetDiff() (d Diff, err error) { 179 | h.client, err = getClient() 180 | if err != nil { 181 | return 182 | } 183 | 184 | if config.Conf.History.Sync.ID == "" { 185 | id, err := h.getGistID() 186 | if err != nil { 187 | return d, err 188 | } 189 | if id != "" { 190 | config.Conf.History.Sync.ID = id 191 | } 192 | if err := config.Conf.Save(); err != nil { 193 | return d, err 194 | } 195 | } 196 | 197 | gist, _, err := h.client.Gists.Get(context.Background(), config.Conf.History.Sync.ID) 198 | if err != nil { 199 | return 200 | } 201 | var ( 202 | remoteContent, localContent string 203 | ) 204 | out, err := ioutil.ReadFile(h.Path) 205 | if err != nil { 206 | return 207 | } 208 | localContent = string(out) 209 | for _, file := range gist.Files { 210 | if *file.Filename != filepath.Base(h.Path) { 211 | err = fmt.Errorf("%s: not found on cloud", filepath.Base(h.Path)) 212 | return 213 | } 214 | remoteContent = *file.Content 215 | } 216 | 217 | return Diff{ 218 | Local: struct { 219 | Size int 220 | Content string 221 | }{ 222 | Size: strings.Count(localContent, "\n"), 223 | Content: localContent, 224 | }, 225 | Remote: struct { 226 | Size int 227 | Content string 228 | }{ 229 | Size: strings.Count(remoteContent, "\n"), 230 | Content: remoteContent, 231 | }, 232 | Size: int(math.Abs(float64( 233 | strings.Count(localContent, "\n") - strings.Count(remoteContent, "\n"), 234 | ))), 235 | }, nil 236 | } 237 | 238 | func (h *History) Sync(diff Diff) (err error) { 239 | if err := h.Backup(); err != nil { 240 | return err 241 | } 242 | 243 | h.Merge(diff.Remote.Content, diff.Local.Content) 244 | if err := h.updateLocal(); err != nil { 245 | return err 246 | } 247 | if err := h.updateRemote(); err != nil { 248 | return err 249 | } 250 | 251 | return 252 | } 253 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/b4b4r07/history/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /misc/fish/README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | You should get binaries in advance (see [README.md](https://github.com/b4b4r07/history/blob/master/README.md#installation)). Then you can get fish-shell support with your plugin manager. 4 | 5 | - Install with [fundle](https://github.com/tuvistavie/fundle) 6 | 7 | Add the following in your config.fish. 8 | 9 | ```fish 10 | fundle plugin 'b4b4r07/history' --path 'misc/fish' 11 | fundle init 12 | ``` 13 | 14 | - Install with [fresco](https://github.com/masa0x80/fresco) 15 | 16 | Run the following command. 17 | ```fish 18 | fresco b4b4r07/history 19 | ``` 20 | 21 | - Install with [fisherman](https://github.com/fisherman/fisherman) 22 | 23 | You cannot choose a specific directory in a repository to load as a fish plugin. 24 | But you can load the local directory misc/fish as a plugin. 25 | If you get the binaries with `go get`, run the following command. 26 | 27 | ``` 28 | fisher $GOPATH/src/github.com/b4b4r07/history/misc/fish 29 | ``` 30 | 31 | ## Usage 32 | 33 | You should specify some enviroment variables for using this tool. 34 | 35 |
36 | fish_history_cmd_name 37 | 38 | 39 | 40 | It should be used as an alias of `command history`. Completions are genereted for this alias. 41 | 42 |
43 | 44 |
45 | fish_history_filter_options 46 | 47 | 48 | 49 | It should be set `history search` option. See also `command history help search`. 50 | 51 |
52 | 53 |
54 | fish_history_auto_sync 55 | 56 | 57 | 58 | Example: 59 | 60 | ```fish 61 | set -U fish_history_auto_sync true 62 | ``` 63 | 64 | If you set sync option (for more datail, see and run `history config`) 65 | 66 |
67 | 68 |
69 | fish_history_auto_sync_interval 70 | 71 | 72 | 73 | Example: 74 | 75 | ```zsh 76 | set -U fish_history_auto_sync_intareval "1h" 77 | ``` 78 | 79 |
80 | 81 | ## Keybindings 82 | 83 | These are functions to use for user specified keybindings. 84 | To save custom keybindings, put the bind statements into your `fish_user_key_bindings` function. 85 | 86 |
87 | __history_keybind_get 88 | 89 | 90 | 91 | You can set keybind for getting history. 92 | 93 | Example: 94 | 95 | ```fish 96 | bind \cr __history_keybind_get 97 | ``` 98 | 99 |
100 | 101 |
102 | __history_keybind_get_all 103 | 104 | Ignore `fish_history_filter_options` and search all history. 105 | 106 | Example: 107 | 108 | ```fish 109 | bind \cr\ca __fish_history_keybind_get_all 110 | ``` 111 | 112 | 113 |
114 | 115 |
116 | __history_keybind_get_by_dir 117 | 118 | It's equals to `__fish_history_keybind_get` with `fish_history_filter_options="--filter-branch --filter-dir"`. 119 | 120 |
121 | 122 |
123 | __history_keybind_arrow_up 124 | 125 | Example: 126 | 127 | ```fish 128 | bind \cp __history_keybind_arrow_up 129 | ``` 130 | 131 |
132 | 133 |
134 | __history_keybind_arrow_down 135 | 136 | Example: 137 | 138 | ```fish 139 | bind \cn __history_keybind_arrow_down 140 | ``` 141 | 142 |
143 | 144 | --- 145 | 146 | Anyway, if you want to use it immediately please run the following commands: 147 | 148 | ```fish 149 | set -U fish_history_cmd_name hs # as you like 150 | set -U fish_history_auto_sync true 151 | set -U fish_history_filter_options "--filter-branch --filter-dir" 152 | ``` 153 | 154 | Then, add the following statements into the definition of `fish_user_key_bindings` function. 155 | 156 | (You can edit and save `fish_user_key_bindings` by `funced fish_user_key_bindings; and funcsave fish_user_key_bindings`.) 157 | 158 | ```fish 159 | function fish_user_key_bindings 160 | 161 | bind \cr __fish_history_keybind_get 162 | bind \cp __fish_history_keybind_arrow_up 163 | bind \cn __fish_history_keybind_arrow_down 164 | 165 | end 166 | ``` 167 | 168 | 169 | -------------------------------------------------------------------------------- /misc/fish/init.fish: -------------------------------------------------------------------------------- 1 | # 2 | # Configurations 3 | # 4 | 5 | if test -z "$fish_history_cmd_name" 6 | set -g fish_history_cmd_name history 7 | end 8 | 9 | if test -z "$fish_history_auto_sync" 10 | set -g fish_history_auto_sync false 11 | end 12 | 13 | if test -z "$fish_history_auto_sync_interval" 14 | set -g fish_history_auto_sync_interval 1h 15 | end 16 | 17 | if test -z "$fish_history_columns_get_all" 18 | set -g fish_history_columns_get_all "{{.Time}}, {{.Status}},({{.Base}}:{{.Branch}})" 19 | end 20 | 21 | if test -z "$fish_history_filter_options" 22 | set -g fish_history_filter_options "--filter-dir --filter-branch" 23 | end 24 | 25 | # 26 | # Alias 27 | # 28 | 29 | function $fish_history_cmd_name -d "enhanced history for your shell" 30 | command history $argv 31 | end 32 | 33 | # 34 | # Completions 35 | # 36 | 37 | ## erase old completions 38 | complete -ec $fish_history_cmd_name 39 | 40 | ## subcommands 41 | complete -xc $fish_history_cmd_name -n '__fish_use_subcommand' -a 'add' -d 'Add new history' 42 | complete -xc $fish_history_cmd_name -n '__fish_use_subcommand' -a 'config' -d 'Config the setting file' 43 | complete -xc $fish_history_cmd_name -n '__fish_use_subcommand' -a 'edit' -d 'Edit your history file directly' 44 | complete -xc $fish_history_cmd_name -n '__fish_use_subcommand' -a 'list' -d 'List the history' 45 | complete -xc $fish_history_cmd_name -n '__fish_use_subcommand' -a 'search' -d 'Search the command from the history file' 46 | complete -xc $fish_history_cmd_name -n '__fish_use_subcommand' -a 'delete' -d 'Delete the record from history file' 47 | complete -xc $fish_history_cmd_name -n '__fish_use_subcommand' -a 'sync' -d 'Sync the history file with gist' 48 | complete -xc $fish_history_cmd_name -n '__fish_use_subcommand' -a 'help' -d 'Show help for any command' 49 | 50 | ## global options 51 | complete -xc $fish_history_cmd_name -n '__fish_no_arguments' -s h -l help -d 'Show the help message' 52 | complete -xc $fish_history_cmd_name -n '__fish_no_arguments' -s v -l version -d 'Show the version and exit' 53 | 54 | ## options for add 55 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from add' -s h -l help -d 'Show the help message' 56 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from add' -l branch -d 'Set branch' 57 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from add' -l command -d 'Set command' 58 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from add' -l dir -d 'Set dir' 59 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from add' -l status -d 'Set status' 60 | 61 | ## options for search/list/delete 62 | for cmd in search list delete 63 | set -l Cmd (string sub -l 1 $cmd | tr '[:lower:]' '[:upper:]')(string sub -s 2 $cmd) 64 | 65 | complete -xc $fish_history_cmd_name -n "__fish_seen_subcommand_from $cmd" -s h -l help -d "Show the help and exit" 66 | complete -xc $fish_history_cmd_name -n "__fish_seen_subcommand_from $cmd" -s b -l filter-branch -d "$Cmd with branch" 67 | complete -xc $fish_history_cmd_name -n "__fish_seen_subcommand_from $cmd" -s d -l filter-dir -d "$Cmd with dir" 68 | complete -xc $fish_history_cmd_name -n "__fish_seen_subcommand_from $cmd" -s p -l filter-hostname -d "$Cmd with hostname" 69 | complete -xc $fish_history_cmd_name -n "__fish_seen_subcommand_from $cmd" -s q -l query -d "$Cmd with query" 70 | complete -xc $fish_history_cmd_name -n "__fish_seen_subcommand_from $cmd" -s c -l filter-branch -d "$Cmd columns with options" 71 | end 72 | 73 | ## options for sync 74 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from sync' -s h -l help -d 'Show the help message' 75 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from sync' -l interval -d 'Sync with the interval' 76 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from sync' -l diff -d 'Sync if the diff exceeds a certain number' 77 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from sync' -l ask -d 'Sync after the confirmation' 78 | 79 | ## options for edit 80 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from edit' -s h -l help -d 'Show the help message' 81 | 82 | ## options for config 83 | and set -l keys (command history config --keys) 84 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from config' -s h -l help -d 'Show the help message' 85 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from config' -l get -a "$keys" -d 'Get the config value' 86 | complete -xc $fish_history_cmd_name -n '__fish_seen_subcommand_from config' -l keys -d 'Get the config keys' 87 | 88 | # 89 | # Hooks 90 | # 91 | 92 | function __history_add --on-event fish_postexec 93 | if test -n $argv 94 | 95 | set -l status_code $status 96 | set -l last_command $argv 97 | set -l git_branch (git rev-parse --abbrev-ref HEAD ^/dev/null) 98 | 99 | command history add --command "$last_command" --dir "$PWD" --status "$status_code" --branch "$git_branch" 100 | 101 | end 102 | end 103 | 104 | if test "$fish_history_auto_sync" = true 105 | 106 | function __history_sync --on-event fish_postexec 107 | set -l before (date +%s) 108 | set -l sync_interval (set -q fish_history_auto_sync_interval 109 | and echo $fish_history_auto_sync_interval 110 | or echo -1) 111 | 112 | command history sync --diff=100 --interval="$sync_interval" ^/dev/null 113 | 114 | set -l status_code $status 115 | set -l after (date +%s) 116 | 117 | if test $status_code = 0 -a (math $after - $before) -gt 1 118 | echo "["(date)"] Synced successfully" 119 | end 120 | end 121 | 122 | end 123 | 124 | # 125 | # Substring search 126 | # 127 | 128 | function __history_substring_search_begin 129 | 130 | set -l buffer (commandline) 131 | if test -z "$buffer" -o "$buffer" != "$__history_substring_search_result" 132 | set -g __history_substring_search_query $buffer 133 | set -g __history_substring_search_matches (command history list \ 134 | --filter-branch \ 135 | --filter-dir \ 136 | --columns '{{.Command}}' \ 137 | --query (string escape -n $buffer)) 138 | 139 | set -g __history_substring_search_matches_count (count $__history_substring_search_matches) 140 | set -g __history_substring_search_match_index (math $__history_substring_search_matches_count + 1) 141 | end 142 | end 143 | 144 | function __history_substring_search_end 145 | set -g __history_substring_search_result (commandline) 146 | 147 | function __history_substring_reset --on-event fish_preexec 148 | set -g __history_substring_search_result 149 | end 150 | end 151 | 152 | function __history_substring_history_up 153 | if test "$__history_substring_search_match_index" -gt 0 154 | set -g __history_substring_search_match_index (math $__history_substring_search_match_index - 1) 155 | commandline $__history_substring_search_matches[$__history_substring_search_match_index] 156 | else 157 | __history_substring_not_found 158 | end 159 | end 160 | 161 | function __history_substring_history_down 162 | if test "$__history_substring_search_match_index" -lt (count $__history_substring_search_matches) 163 | set -g __history_substring_search_match_index (math $__history_substring_search_match_index + 1) 164 | commandline $__history_substring_search_matches[$__history_substring_search_match_index] 165 | else 166 | set -g __history_substring_search_old_buffer (commandline) 167 | commandline $__history_substring_search_query 168 | end 169 | end 170 | 171 | # 172 | # Keybindings 173 | # 174 | 175 | function __history_keybind_get 176 | set -l buf (command history search $fish_history_filter_options \ 177 | --query (commandline -c)) 178 | 179 | test -n "$buf" 180 | and commandline $buf 181 | 182 | commandline -f repaint 183 | end 184 | 185 | function __history_keybind_get_by_dir 186 | set -l buf (command history search \ 187 | --filter-dir \ 188 | --filter-branch \ 189 | --query (commandline -c)) 190 | 191 | test -n "$buf" 192 | and commandline $buf 193 | 194 | commandline -f repaint 195 | end 196 | 197 | function __history_keybind_get_all 198 | set -l opt 199 | test -n "$fish_history_columns_get_all" 200 | and set opt "--columns $fish_history_columns_get_all" 201 | 202 | set -l buf (command history search $opt --query (commandline -c)) 203 | 204 | test -n "$buf" 205 | and commandline $buf 206 | 207 | commandline -f repaint 208 | end 209 | 210 | function __history_keybind_arrow_up 211 | __history_substring_search_begin 212 | __history_substring_history_up 213 | __history_substring_search_end 214 | end 215 | 216 | function __history_keybind_arrow_down 217 | __history_substring_search_begin 218 | __history_substring_history_down 219 | __history_substring_search_end 220 | end 221 | -------------------------------------------------------------------------------- /misc/zsh/completions/_history: -------------------------------------------------------------------------------- 1 | #compdef history 2 | # vim: ft=zsh 3 | 4 | _history () { 5 | local -a _1st_arguments 6 | _1st_arguments=( 7 | 'add:Add new history' 8 | 'config:Config the setting file' 9 | 'edit:Edit your history file directly' 10 | 'list:List the history' 11 | 'search:Search the command from the history file' 12 | 'delete:Delete the record from history file' 13 | 'sync:Sync the history file with gist' 14 | 'help:Show help for any command' 15 | ) 16 | 17 | _arguments \ 18 | '(--help)--help[Show the help message]' \ 19 | '(-v --version)'{-v,--version}'[Show the version and exit]' \ 20 | '*:: :->subcmds' \ 21 | && return 0 22 | 23 | if (( CURRENT == 1 )); then 24 | _describe -t commands "history subcommand" _1st_arguments 25 | return 26 | fi 27 | 28 | case "$words[1]" in 29 | (add) 30 | _arguments \ 31 | '(- :)--help[Show this help and exit]' \ 32 | '(--branch)--branch=[Set branch]' \ 33 | '(--dir)--dir=[Set dir]' \ 34 | '(--command)--command=[Set command]' \ 35 | '(--status)--status=[Set status]' \ 36 | && return 0 37 | ;; 38 | (search|list|delete) 39 | _arguments \ 40 | '(- :)--help[Show this help and exit]' \ 41 | '(--filter-branch -b)'{--filter-branch,-b}'['${(C)words[1]}' with branch]' \ 42 | '(--filter-dir -d)'{--filter-dir,-d}'['${(C)words[1]}' with dir]' \ 43 | '(--filter-hostname -p)'{--filter-hostname,-p}'['${(C)words[1]}' with hostname]' \ 44 | '(--query -q)'{--query,-q}'=['${(C)words[1]}' with query]' \ 45 | '(--columns -c)'{--columns,-c}'=['${(C)words[1]}' columns with options]' \ 46 | && return 0 47 | ;; 48 | (sync) 49 | _arguments \ 50 | '(- :)--help[Show this help and exit]' \ 51 | '(--interval)--interval=[Sync with the interval]' \ 52 | '(--diff)--diff=[Sync if the diff exceeds a certain number]' \ 53 | '(--ask)--ask[Sync after the confirmation]' \ 54 | && return 0 55 | ;; 56 | (edit) 57 | _arguments \ 58 | '(- :)--help[Show this help and exit]' \ 59 | && return 0 60 | ;; 61 | (config) 62 | keys=( $(command history config --keys) ) 63 | _arguments \ 64 | '(- :)--help[Show this help and exit]' \ 65 | '(--get)--get[Get the config value]: :->keys' \ 66 | '(--keys)--keys[Get the config keys]' 67 | case "$state" in 68 | (keys) 69 | _describe -t keys 'toml keys' keys && return 0 70 | ;; 71 | esac 72 | ;; 73 | (help) 74 | _values 'help message' ${_1st_arguments[@]%:*} && return 0 75 | ;; 76 | esac 77 | } 78 | 79 | _history "$@" 80 | -------------------------------------------------------------------------------- /misc/zsh/history.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | __history::history::add() 4 | { 5 | local status_code="$status" 6 | local last_command="$(fc -ln -1)" 7 | 8 | command history add \ 9 | --command "$last_command" \ 10 | --dir "$PWD" \ 11 | --status "$status_code" \ 12 | --branch "$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" 13 | } 14 | 15 | __history::history::sync() 16 | { 17 | local status_code 18 | local before after 19 | before=$SECONDS 20 | command history sync \ 21 | --ask \ 22 | --diff=100 \ 23 | --interval=${ZSH_HISTORY_AUTO_SYNC_INTERVAL:-"1h"} \ 24 | 2>/dev/null 25 | status_code=$status 26 | after=$SECONDS 27 | if (( $status_code == 0 && (after - before) > 1 )); then 28 | printf "[$(date)] Synced successfully\n" 29 | fi 30 | } 31 | -------------------------------------------------------------------------------- /misc/zsh/init.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | ZSH_HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND="bg=magenta,fg=white,bold" 4 | ZSH_HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND="bg=red,fg=white,bold" 5 | ZSH_HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS="i" 6 | 7 | # 8 | # Keybindings 9 | # 10 | 11 | if [[ -n $ZSH_HISTORY_KEYBIND_GET ]]; then 12 | zle -N "__history::keybind::get" 13 | bindkey "$ZSH_HISTORY_KEYBIND_GET" "__history::keybind::get" 14 | fi 15 | 16 | if [[ -n $ZSH_HISTORY_KEYBIND_GET_BY_DIR ]]; then 17 | zle -N "__history::keybind::get_by_dir" 18 | bindkey "$ZSH_HISTORY_KEYBIND_GET_BY_DIR" "__history::keybind::get_by_dir" 19 | fi 20 | 21 | if [[ -n $ZSH_HISTORY_KEYBIND_GET_ALL ]]; then 22 | zle -N "__history::keybind::get_all" 23 | bindkey "$ZSH_HISTORY_KEYBIND_GET_ALL" "__history::keybind::get_all" 24 | fi 25 | 26 | if [[ -n $ZSH_HISTORY_KEYBIND_ARROW_UP ]]; then 27 | zle -N "__history::keybind::arrow_up" 28 | bindkey "$ZSH_HISTORY_KEYBIND_ARROW_UP" "__history::keybind::arrow_up" 29 | fi 30 | 31 | if [[ -n $ZSH_HISTORY_KEYBIND_ARROW_DOWN ]]; then 32 | zle -N "__history::keybind::arrow_down" 33 | bindkey "$ZSH_HISTORY_KEYBIND_ARROW_DOWN" "__history::keybind::arrow_down" 34 | fi 35 | 36 | # 37 | # Configurations 38 | # 39 | 40 | if [[ $ZSH_HISTORY_CASE_SENSITIVE == true ]]; then 41 | unset ZSH_HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS 42 | fi 43 | 44 | if [[ $ZSH_HISTORY_DISABLE_COLOR == true ]]; then 45 | unset ZSH_HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND 46 | unset ZSH_HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND 47 | fi 48 | 49 | if [[ -z $ZSH_HISTORY_AUTO_SYNC ]]; then 50 | export ZSH_HISTORY_AUTO_SYNC=true 51 | fi 52 | 53 | if [[ -z $ZSH_HISTORY_AUTO_SYNC_INTERVAL ]]; then 54 | export ZSH_HISTORY_AUTO_SYNC_INTERVAL="1h" 55 | fi 56 | 57 | if [[ -z $ZSH_HISTORY_COLUMNS_GET_ALL ]]; then 58 | export ZSH_HISTORY_COLUMNS_GET_ALL="{{.Time}},{{.Status}},{{.Command}},({{.Base}}:{{.Branch}})" 59 | fi 60 | 61 | # TODO: ZSH_HISTORY_COLUMNS_GET_ALL 62 | 63 | if [[ -z $ZSH_HISTORY_FILTER_OPTIONS ]]; then 64 | # by default, equals to __history::keybind::get_by_dir behavior 65 | export ZSH_HISTORY_FILTER_OPTIONS="${ZSH_HISTORY_OPTIONS_BY_DIR}" 66 | fi 67 | 68 | if [[ -z $ZSH_HISTORY_FILTER_OPTIONS_BY_DIR ]]; then 69 | export ZSH_HISTORY_FILTER_OPTIONS="--filter-dir --filter-branch" 70 | fi 71 | 72 | # 73 | # Loading 74 | # 75 | 76 | for f in "${0:A:h}"/*.zsh(N-.) 77 | do 78 | source "$f" 2>/dev/null 79 | done 80 | unset f 81 | 82 | autoload -Uz add-zsh-hook 83 | 84 | add-zsh-hook precmd "__history::history::add" 85 | add-zsh-hook preexec "__history::substring::reset" 86 | 87 | if [[ $ZSH_HISTORY_AUTO_SYNC == true ]]; then 88 | add-zsh-hook precmd "__history::history::sync" 89 | fi 90 | -------------------------------------------------------------------------------- /misc/zsh/keybind.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | __history::keybind::get() 4 | { 5 | local buf opt 6 | # by default, equals to __history::keybind::get_by_dir behavior 7 | cmd="command history search $ZSH_HISTORY_FILTER_OPTIONS" 8 | if [[ -n "$LBUFFER" ]]; then 9 | cmd="$cmd --query "$LBUFFER"" 10 | fi 11 | buf="$(eval $cmd)" 12 | if [[ -n $buf ]]; then 13 | BUFFER="$buf" 14 | CURSOR=$#BUFFER 15 | fi 16 | zle reset-prompt 17 | } 18 | 19 | __history::keybind::get_by_dir() 20 | { 21 | local buf 22 | cmd="command history search $ZSH_HISTORY_FILTER_OPTIONS_BY_DIR" 23 | if [[ -n "$LBUFFER" ]]; then 24 | cmd="$cmd --query "$LBUFFER"" 25 | fi 26 | buf="$(eval $cmd)" 27 | if [[ -n $buf ]]; then 28 | BUFFER="$buf" 29 | CURSOR=$#BUFFER 30 | fi 31 | zle reset-prompt 32 | } 33 | 34 | __history::keybind::get_all() 35 | { 36 | local buf opt 37 | if [[ -n $ZSH_HISTORY_COLUMNS_GET_ALL ]]; then 38 | opt="--columns $ZSH_HISTORY_COLUMNS_GET_ALL" 39 | fi 40 | buf="$(command history search $opt --query "$LBUFFER")" 41 | if [[ -n $buf ]]; then 42 | BUFFER="$buf" 43 | CURSOR=$#BUFFER 44 | fi 45 | zle reset-prompt 46 | } 47 | 48 | __history::keybind::arrow_up() 49 | { 50 | __history::substring::search_begin 51 | __history::substring::history_up 52 | __history::substring::search_end 53 | } 54 | 55 | __history::keybind::arrow_down() 56 | { 57 | __history::substring::search_begin 58 | __history::substring::history_down 59 | __history::substring::search_end 60 | } 61 | -------------------------------------------------------------------------------- /misc/zsh/substring.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | __history::substring::search_begin() 4 | { 5 | setopt localoptions extendedglob 6 | 7 | _history_substring_search_refresh_display=0 8 | _history_substring_search_query_highlight= 9 | 10 | if [[ -z $BUFFER || $BUFFER != $_history_substring_search_result ]]; then 11 | _history_substring_search_query=$BUFFER 12 | _history_substring_search_query_escaped=${BUFFER//(#m)[\][()|\\*?#<>~^]/\\$MATCH} 13 | 14 | _history_substring_search_matches=( ${(@f)"$(command history list \ 15 | --filter-branch \ 16 | --filter-dir \ 17 | --columns "{{.Command}}" \ 18 | --query "$_history_substring_search_query_escaped")"} 19 | ) 20 | if [[ $#_history_substring_search_matches -eq 0 ]]; then 21 | _history_substring_search_matches=() 22 | fi 23 | 24 | _history_substring_search_matches_count=$#_history_substring_search_matches 25 | 26 | if [[ $WIDGET == history-substring-search-up ]]; then 27 | _history_substring_search_match_index=$(( _history_substring_search_matches_count + 1 )) 28 | else 29 | _history_substring_search_match_index=$_history_substring_search_matches_count 30 | fi 31 | fi 32 | } 33 | 34 | __history::substring::search_end() 35 | { 36 | setopt localoptions extendedglob 37 | 38 | _history_substring_search_result=$BUFFER 39 | 40 | if (( $_history_substring_search_refresh_display == 1 )); then 41 | region_highlight=() 42 | CURSOR=$#BUFFER 43 | fi 44 | 45 | # highlight command line using zsh-syntax-highlighting 46 | if (( $+functions[_zsh_highlight] )); then 47 | _zsh_highlight 48 | fi 49 | 50 | # highlight the search query inside the command line 51 | if [[ -n $_history_substring_search_query_highlight && -n $_history_substring_search_query ]]; then 52 | : ${(S)BUFFER##(#m$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)($_history_substring_search_query##)} 53 | local begin=$(( MBEGIN - 1 )) 54 | local end=$(( begin + $#_history_substring_search_query )) 55 | region_highlight+=("$begin $end $_history_substring_search_query_highlight") 56 | fi 57 | } 58 | 59 | __history::substring::history_up() 60 | { 61 | _history_substring_search_refresh_display=1 62 | 63 | if (( $_history_substring_search_match_index > 0 )); then 64 | BUFFER=$_history_substring_search_matches[$_history_substring_search_match_index] 65 | _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND 66 | (( _history_substring_search_match_index-- )) 67 | else 68 | __history::substring::not_found 69 | fi 70 | } 71 | 72 | __history::substring::history_down() 73 | { 74 | _history_substring_search_refresh_display=1 75 | 76 | if (( _history_substring_search_match_index < $#_history_substring_search_matches )); then 77 | (( _history_substring_search_match_index++ )) 78 | BUFFER=$_history_substring_search_matches[$_history_substring_search_match_index] 79 | _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND 80 | else 81 | BUFFER=$_history_substring_search_old_buffer 82 | _history_substring_search_query_highlight= 83 | fi 84 | } 85 | 86 | __history::substring::not_found() 87 | { 88 | _history_substring_search_old_buffer=$BUFFER 89 | BUFFER=$_history_substring_search_query 90 | _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND 91 | } 92 | 93 | __history::substring::reset() 94 | { 95 | _history_substring_search_result= 96 | } 97 | --------------------------------------------------------------------------------