1package store
2
3import (
4	"bytes"
5	"encoding/binary"
6
7	bolt "go.etcd.io/bbolt"
8	. "src.elv.sh/pkg/store/storedefs"
9)
10
11func init() {
12	initDB["initialize command history table"] = func(tx *bolt.Tx) error {
13		_, err := tx.CreateBucketIfNotExists([]byte(bucketCmd))
14		return err
15	}
16}
17
18// NextCmdSeq returns the next sequence number of the command history.
19func (s *dbStore) NextCmdSeq() (int, error) {
20	var seq uint64
21	err := s.db.View(func(tx *bolt.Tx) error {
22		b := tx.Bucket([]byte(bucketCmd))
23		seq = b.Sequence() + 1
24		return nil
25	})
26	return int(seq), err
27}
28
29// AddCmd adds a new command to the command history.
30func (s *dbStore) AddCmd(cmd string) (int, error) {
31	var (
32		seq uint64
33		err error
34	)
35	err = s.db.Update(func(tx *bolt.Tx) error {
36		b := tx.Bucket([]byte(bucketCmd))
37		seq, err = b.NextSequence()
38		if err != nil {
39			return err
40		}
41		return b.Put(marshalSeq(seq), []byte(cmd))
42	})
43	return int(seq), err
44}
45
46// DelCmd deletes a command history item with the given sequence number.
47func (s *dbStore) DelCmd(seq int) error {
48	return s.db.Update(func(tx *bolt.Tx) error {
49		b := tx.Bucket([]byte(bucketCmd))
50		return b.Delete(marshalSeq(uint64(seq)))
51	})
52}
53
54// Cmd queries the command history item with the specified sequence number.
55func (s *dbStore) Cmd(seq int) (string, error) {
56	var cmd string
57	err := s.db.View(func(tx *bolt.Tx) error {
58		b := tx.Bucket([]byte(bucketCmd))
59		v := b.Get(marshalSeq(uint64(seq)))
60		if v == nil {
61			return ErrNoMatchingCmd
62		}
63		cmd = string(v)
64		return nil
65	})
66	return cmd, err
67}
68
69// IterateCmds iterates all the commands in the specified range, and calls the
70// callback with the content of each command sequentially.
71func (s *dbStore) IterateCmds(from, upto int, f func(Cmd)) error {
72	return s.db.View(func(tx *bolt.Tx) error {
73		b := tx.Bucket([]byte(bucketCmd))
74		c := b.Cursor()
75		for k, v := c.Seek(marshalSeq(uint64(from))); k != nil && unmarshalSeq(k) < uint64(upto); k, v = c.Next() {
76			f(Cmd{Text: string(v), Seq: int(unmarshalSeq(k))})
77		}
78		return nil
79	})
80}
81
82// CmdsWithSeq returns all commands within the specified range.
83func (s *dbStore) CmdsWithSeq(from, upto int) ([]Cmd, error) {
84	var cmds []Cmd
85	err := s.IterateCmds(from, upto, func(cmd Cmd) {
86		cmds = append(cmds, cmd)
87	})
88	return cmds, err
89}
90
91// NextCmd finds the first command after the given sequence number (inclusive)
92// with the given prefix.
93func (s *dbStore) NextCmd(from int, prefix string) (Cmd, error) {
94	var cmd Cmd
95	err := s.db.View(func(tx *bolt.Tx) error {
96		b := tx.Bucket([]byte(bucketCmd))
97		c := b.Cursor()
98		p := []byte(prefix)
99		for k, v := c.Seek(marshalSeq(uint64(from))); k != nil; k, v = c.Next() {
100			if bytes.HasPrefix(v, p) {
101				cmd = Cmd{Text: string(v), Seq: int(unmarshalSeq(k))}
102				return nil
103			}
104		}
105		return ErrNoMatchingCmd
106	})
107	return cmd, err
108}
109
110// PrevCmd finds the last command before the given sequence number (exclusive)
111// with the given prefix.
112func (s *dbStore) PrevCmd(upto int, prefix string) (Cmd, error) {
113	var cmd Cmd
114	err := s.db.View(func(tx *bolt.Tx) error {
115		b := tx.Bucket([]byte(bucketCmd))
116		c := b.Cursor()
117		p := []byte(prefix)
118
119		var v []byte
120		k, _ := c.Seek(marshalSeq(uint64(upto)))
121		if k == nil { // upto > LAST
122			k, v = c.Last()
123			if k == nil {
124				return ErrNoMatchingCmd
125			}
126		} else {
127			k, v = c.Prev() // upto exists, find the previous one
128		}
129
130		for ; k != nil; k, v = c.Prev() {
131			if bytes.HasPrefix(v, p) {
132				cmd = Cmd{Text: string(v), Seq: int(unmarshalSeq(k))}
133				return nil
134			}
135		}
136		return ErrNoMatchingCmd
137	})
138	return cmd, err
139}
140
141func marshalSeq(seq uint64) []byte {
142	b := make([]byte, 8)
143	binary.BigEndian.PutUint64(b, seq)
144	return b
145}
146
147func unmarshalSeq(key []byte) uint64 {
148	return binary.BigEndian.Uint64(key)
149}
150