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)