diff --git a/src/history.go b/src/history.go new file mode 100644 index 0000000..66159eb --- /dev/null +++ b/src/history.go @@ -0,0 +1,94 @@ +package fzf + +import ( + "errors" + "io/ioutil" + "os" + "strings" +) + +type History struct { + path string + lines []string + modified map[int]string + maxSize int + cursor int +} + +func NewHistory(path string, maxSize int) (*History, error) { + fmtError := func(e error) error { + if os.IsPermission(e) { + return errors.New("permission denied: " + path) + } + return errors.New("invalid history file: " + e.Error()) + } + + // Read history file + data, err := ioutil.ReadFile(path) + if err != nil { + // If it doesn't exist, check if we can create a file with the name + if os.IsNotExist(err) { + data = []byte{} + if err := ioutil.WriteFile(path, data, 0600); err != nil { + return nil, fmtError(err) + } + } else { + return nil, fmtError(err) + } + } + // Split lines and limit the maximum number of lines + lines := strings.Split(strings.Trim(string(data), "\n"), "\n") + if len(lines[len(lines)-1]) > 0 { + lines = append(lines, "") + } + return &History{ + path: path, + maxSize: maxSize, + lines: lines, + modified: make(map[int]string), + cursor: len(lines) - 1}, nil +} + +func (h *History) append(line string) error { + // We don't append empty lines + if len(line) == 0 { + return nil + } + + lines := append(h.lines[:len(h.lines)-1], line) + if len(lines) > h.maxSize { + lines = lines[len(lines)-h.maxSize : len(lines)] + } + h.lines = append(lines, "") + return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600) +} + +func (h *History) override(str string) { + // You can update the history but they're not written to the file + if h.cursor == len(h.lines)-1 { + h.lines[h.cursor] = str + } else if h.cursor < len(h.lines)-1 { + h.modified[h.cursor] = str + } +} + +func (h *History) current() string { + if str, prs := h.modified[h.cursor]; prs { + return str + } + return h.lines[h.cursor] +} + +func (h *History) previous() string { + if h.cursor > 0 { + h.cursor-- + } + return h.current() +} + +func (h *History) next() string { + if h.cursor < len(h.lines)-1 { + h.cursor++ + } + return h.current() +} diff --git a/src/history_test.go b/src/history_test.go new file mode 100644 index 0000000..83e4029 --- /dev/null +++ b/src/history_test.go @@ -0,0 +1,53 @@ +package fzf + +import ( + "testing" +) + +func TestHistory(t *testing.T) { + maxHistory := 50 + + // Invalid arguments + for _, path := range []string{"/etc", "/proc", "/etc/sudoers"} { + if _, e := NewHistory(path, maxHistory); e == nil { + t.Error("Error expected for: " + path) + } + } + { // Append lines + h, _ := NewHistory("/tmp/fzf-history", maxHistory) + for i := 0; i < maxHistory+10; i++ { + h.append("foobar") + } + } + { // Read lines + h, _ := NewHistory("/tmp/fzf-history", maxHistory) + if len(h.lines) != maxHistory+1 { + t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) + } + for i := 0; i < maxHistory; i++ { + if h.lines[i] != "foobar" { + t.Error("Expected: foobar, actual: " + h.lines[i]) + } + } + } + { // Append lines + h, _ := NewHistory("/tmp/fzf-history", maxHistory) + h.append("barfoo") + h.append("") + h.append("foobarbaz") + } + { // Read lines again + h, _ := NewHistory("/tmp/fzf-history", maxHistory) + if len(h.lines) != maxHistory+1 { + t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) + } + compare := func(idx int, exp string) { + if h.lines[idx] != exp { + t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx]) + } + } + compare(maxHistory-3, "foobar") + compare(maxHistory-2, "barfoo") + compare(maxHistory-1, "foobarbaz") + } +}