From ab49aab4a30a25e0f7f210a247b41426fa08cb0a Mon Sep 17 00:00:00 2001
From: reugn
Date: Mon, 4 Mar 2024 19:24:37 +0200
Subject: [PATCH] feat: add support for multi-line input
---
README.md | 2 ++
cli/chat.go | 57 ++++++++++++++++++++++++++++++++++++++--------
cli/command.go | 5 +++-
cli/prompt.go | 14 +++++++-----
cmd/gemini/main.go | 5 ++--
5 files changed, 65 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 16ff3c0..039c6f4 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,9 @@ Usage:
Flags:
-f, --format render markdown-formatted response (default true)
-h, --help help for this command
+ -m, --multiline read input as a multi-line string
-s, --style string markdown format style (ascii, dark, light, pink, notty, dracula) (default "auto")
+ -t, --term string multi-line input terminator (default "$")
-v, --version version for this command
```
diff --git a/cli/chat.go b/cli/chat.go
index 15c49aa..1a0e0c1 100644
--- a/cli/chat.go
+++ b/cli/chat.go
@@ -1,6 +1,7 @@
package cli
import (
+ "errors"
"fmt"
"strings"
@@ -10,8 +11,10 @@ import (
// ChatOpts represents Chat configuration options.
type ChatOpts struct {
- Format bool
- Style string
+ Format bool
+ Style string
+ Multiline bool
+ Terminator string
}
// Chat controls the chat flow.
@@ -41,7 +44,7 @@ func NewChat(user string, model *gemini.ChatSession, opts *ChatOpts) (*Chat, err
// StartChat starts the chat loop.
func (c *Chat) StartChat() {
for {
- message, ok := c.readLine()
+ message, ok := c.read()
if !ok {
continue
}
@@ -52,17 +55,40 @@ func (c *Chat) StartChat() {
}
}
+func (c *Chat) read() (string, bool) {
+ if c.opts.Multiline {
+ return c.readMultiLine()
+ }
+ return c.readLine()
+}
+
func (c *Chat) readLine() (string, bool) {
input, err := c.reader.Readline()
if err != nil {
- fmt.Printf("%s%s\n", c.prompt.cli, err)
- return "", false
+ return c.handleReadError(err)
}
- input = strings.ReplaceAll(input, "\n", "")
- if strings.TrimSpace(input) == "" {
- return "", false
+ return validateInput(input)
+}
+
+func (c *Chat) readMultiLine() (string, bool) {
+ var builder strings.Builder
+ term := c.opts.Terminator
+ for {
+ input, err := c.reader.Readline()
+ if err != nil {
+ return c.handleReadError(err)
+ }
+ if strings.HasSuffix(input, term) {
+ builder.WriteString(strings.TrimSuffix(input, term))
+ break
+ }
+ if builder.Len() == 0 {
+ c.reader.SetPrompt(c.prompt.userNext)
+ }
+ builder.WriteString(input + "\n")
}
- return input, true
+ c.reader.SetPrompt(c.prompt.user)
+ return validateInput(builder.String())
}
func (c *Chat) parseCommand(message string) command {
@@ -71,3 +97,16 @@ func (c *Chat) parseCommand(message string) command {
}
return newGeminiCommand(c.model, c.prompt, c.opts)
}
+
+func (c *Chat) handleReadError(err error) (string, bool) {
+ if errors.Is(err, readline.ErrInterrupt) {
+ return systemCmdQuit, true
+ }
+ fmt.Printf("%s%s\n", c.prompt.cli, err)
+ return "", false
+}
+
+func validateInput(input string) (string, bool) {
+ input = strings.TrimSpace(input)
+ return input, input != ""
+}
diff --git a/cli/command.go b/cli/command.go
index c8c821e..50d5e9b 100644
--- a/cli/command.go
+++ b/cli/command.go
@@ -14,7 +14,10 @@ import (
"google.golang.org/api/iterator"
)
-const systemCmdPrefix = "!"
+const (
+ systemCmdPrefix = "!"
+ systemCmdQuit = "!q"
+)
type command interface {
run(message string) bool
diff --git a/cli/prompt.go b/cli/prompt.go
index a5c5d06..4cbfd13 100644
--- a/cli/prompt.go
+++ b/cli/prompt.go
@@ -13,17 +13,19 @@ const (
)
type prompt struct {
- user string
- gemini string
- cli string
+ user string
+ userNext string
+ gemini string
+ cli string
}
func newPrompt(currentUser string) *prompt {
maxLength := maxLength(currentUser, geminiUser, cliUser)
return &prompt{
- user: color.Blue(buildPrompt(currentUser, maxLength)),
- gemini: color.Green(buildPrompt(geminiUser, maxLength)),
- cli: color.Yellow(buildPrompt(cliUser, maxLength)),
+ user: color.Blue(buildPrompt(currentUser, maxLength)),
+ userNext: color.Blue(buildPrompt(strings.Repeat(" ", len(currentUser)), maxLength)),
+ gemini: color.Green(buildPrompt(geminiUser, maxLength)),
+ cli: color.Yellow(buildPrompt(cliUser, maxLength)),
}
}
diff --git a/cmd/gemini/main.go b/cmd/gemini/main.go
index d4cb1d9..6cc125b 100644
--- a/cmd/gemini/main.go
+++ b/cmd/gemini/main.go
@@ -15,17 +15,18 @@ const (
apiKeyEnv = "GEMINI_API_KEY"
)
-var opts = cli.ChatOpts{}
-
func run() int {
rootCmd := &cobra.Command{
Short: "Gemini CLI Tool",
Version: version,
}
+ var opts cli.ChatOpts
rootCmd.Flags().BoolVarP(&opts.Format, "format", "f", true, "render markdown-formatted response")
rootCmd.Flags().StringVarP(&opts.Style, "style", "s", "auto",
"markdown format style (ascii, dark, light, pink, notty, dracula)")
+ rootCmd.Flags().BoolVarP(&opts.Multiline, "multiline", "m", false, "read input as a multi-line string")
+ rootCmd.Flags().StringVarP(&opts.Terminator, "term", "t", "$", "multi-line input terminator")
rootCmd.RunE = func(_ *cobra.Command, _ []string) error {
apiKey := os.Getenv(apiKeyEnv)