diff --git a/cli/cli.go b/cli/cli.go index 724b3c6..73de7c7 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,13 +1,12 @@ package cli import ( - "bufio" - "bytes" "encoding/json" "fmt" "io" "log" "os" + "path/filepath" "strings" "syscall" @@ -16,12 +15,13 @@ import ( "github.com/HotPotatoC/kvstore/pkg/comm" "github.com/HotPotatoC/kvstore/pkg/utils" "github.com/HotPotatoC/kvstore/server/stats" + "github.com/peterh/liner" ) // CLI represents the cli client type CLI struct { - comm *comm.Comm - reader *bufio.Reader + comm *comm.Comm + terminal *liner.State } // New creates a new CLI client @@ -32,46 +32,38 @@ func New(addr string) *CLI { } return &CLI{ - comm: comm, - reader: bufio.NewReader(os.Stdin), + comm: comm, + terminal: newTerminal(), } } // Start runs the CLI client func (c *CLI) Start() { + defer c.terminal.Close() go func() { // Get server information on initial startup stats := c.getServerInformation() - logo := "" + - " _ _ _ _\n" + - "| | | | | (_)\n" + - "| | ____ _____| |_ ___ _ __ ___ ______ ___| |_\n" + - "| |/ /\\ \\ / / __| __/ _ \\| '__/ _ \\______/ __| | |\n" + - "| < \\ V /\\__ \\ || (_) | | | __/ | (__| | |\n" + - "|_|\\_\\ \\_/ |___/\\__\\___/|_| \\___| \\___|_|_|\n\n" - - fmt.Println(logo) - fmt.Printf("Connected to kvstore %s:%s server!\n\n", stats.Version, stats.Build) + c.printLogo() + fmt.Printf("🚀 Connected to kvstore %s:%s server!\n\n", stats.Version, stats.Build) start: for { - fmt.Printf("%s> ", c.comm.Connection().RemoteAddr().String()) - - input, err := c.reader.ReadBytes('\n') - if err != nil && err != io.EOF { + input, err := c.terminal.Prompt(fmt.Sprintf("%s> ", c.comm.Connection().RemoteAddr().String())) + if err != nil { + if err == io.EOF { + c.comm.Conn.Close() + os.Exit(1) + } log.Fatal(err) } - raw := bytes.Split(input, []byte(" "))[0] - cmd := bytes.ToLower( - bytes.TrimSpace(raw)) - args := bytes.TrimSpace( - bytes.TrimPrefix(input, raw)) + c.terminal.AppendHistory(input) + cmd, args := c.parseCommand(input) - switch string(cmd) { + switch cmd { // Displays all available commands with their args and description case "help": - var commands = []command.Op{ + commands := []command.Op{ command.SET, command.SETEX, command.GET, @@ -89,7 +81,6 @@ func (c *CLI) Start() { dimmed(cmd.Args()), cmd.Description()) } - continue start // Exit out of the CLI case "exit": c.comm.Conn.Close() @@ -107,13 +98,21 @@ func (c *CLI) Start() { log.Fatal(err) } - msg, _, err := c.comm.Read() + msg, n, err := c.comm.Read() if err != nil && err != io.EOF { log.Fatal(err) } - fmt.Print(string(msg)) + fmt.Print(string(msg[:n])) + } + + // Write history into tmp direcotry + f, err := os.Create(filepath.Join(os.TempDir(), ".kvstore-cli-history")) + if err != nil { + log.Printf("Failed creating history file %s\n", filepath.Join(os.TempDir(), ".kvstore-cli-history")) } + c.terminal.WriteHistory(f) + _ = f.Close() } }() @@ -122,6 +121,15 @@ func (c *CLI) Start() { os.Exit(0) } +func (c *CLI) printLogo() { + fmt.Println(" _ _ _ _\n" + + "| | | | | (_)\n" + + "| | ____ _____| |_ ___ _ __ ___ ______ ___| |_\n" + + "| |/ /\\ \\ / / __| __/ _ \\| '__/ _ \\______/ __| | |\n" + + "| < \\ V /\\__ \\ || (_) | | | __/ | (__| | |\n" + + "|_|\\_\\ \\_/ |___/\\__\\___/|_| \\___| \\___|_|_|\n\n") +} + func (c *CLI) getServerInformation() *stats.Stats { serverStats := new(stats.Stats) infoPacket := packet.NewPacket(command.INFO, []byte("")) @@ -147,3 +155,13 @@ func (c *CLI) getServerInformation() *stats.Stats { return serverStats } + +func (c *CLI) parseCommand(input string) (string, string) { + raw := strings.Fields(input)[0] + cmd := strings.ToLower( + strings.TrimSpace(raw)) + args := strings.TrimSpace( + strings.TrimPrefix(input, raw)) + + return cmd, args +} diff --git a/cli/preprocess_command.go b/cli/preprocess.go similarity index 88% rename from cli/preprocess_command.go rename to cli/preprocess.go index 56f5304..7b92409 100644 --- a/cli/preprocess_command.go +++ b/cli/preprocess.go @@ -8,25 +8,25 @@ import ( "github.com/HotPotatoC/kvstore/packet" ) -func preprocess(cmd, args []byte) (*bytes.Buffer, error) { +func preprocess(cmd string, args string) (*bytes.Buffer, error) { var packet *packet.Packet var err error switch string(cmd) { case command.SET.String(): - if packet, err = set(args); err != nil { + if packet, err = set([]byte(args)); err != nil { return nil, err } case command.SETEX.String(): - if packet, err = setex(args); err != nil { + if packet, err = setex([]byte(args)); err != nil { return nil, err } case command.GET.String(): - if packet, err = get(args); err != nil { + if packet, err = get([]byte(args)); err != nil { return nil, err } case command.DEL.String(): - if packet, err = del(args); err != nil { + if packet, err = del([]byte(args)); err != nil { return nil, err } case command.LIST.String(): diff --git a/cli/terminal.go b/cli/terminal.go new file mode 100644 index 0000000..3452260 --- /dev/null +++ b/cli/terminal.go @@ -0,0 +1,41 @@ +package cli + +import ( + "os" + "path/filepath" + "strings" + + "github.com/HotPotatoC/kvstore/command" + "github.com/peterh/liner" +) + +func newTerminal() *liner.State { + liner := liner.NewLiner() + + liner.SetCtrlCAborts(true) + liner.SetCompleter(func(s string) (c []string) { + commands := []command.Op{ + command.SET, + command.SETEX, + command.GET, + command.DEL, + command.LIST, + command.KEYS, + command.FLUSH, + command.INFO, + } + for _, n := range commands { + if strings.HasPrefix(n.String(), strings.ToLower(s)) { + c = append(c, n.String()) + } + } + return + }) + + if f, err := os.Open(filepath.Join(os.TempDir(), ".kvstore-cli-history")); err == nil { + liner.ReadHistory(f) + f.Close() + } + + return liner +} diff --git a/go.mod b/go.mod index 807b6ef..3249bdb 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,9 @@ go 1.15 require ( github.com/cespare/xxhash/v2 v2.1.1 github.com/mattn/go-colorable v0.1.8 + github.com/mattn/go-runewidth v0.0.12 // indirect + github.com/peterh/liner v1.2.1 + github.com/rivo/uniseg v0.2.0 // indirect go.uber.org/zap v1.16.0 golang.org/x/sys v0.0.0-20210414055047-fe65e336abe0 // indirect ) diff --git a/go.sum b/go.sum index 2876190..e57a28b 100644 --- a/go.sum +++ b/go.sum @@ -16,10 +16,18 @@ github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg= +github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=