Skip to content

Commit

Permalink
fix: prompt text should handle piped input
Browse files Browse the repository at this point in the history
  • Loading branch information
sweatybridge committed Apr 18, 2024
1 parent 5619752 commit 82b3892
Show file tree
Hide file tree
Showing 14 changed files with 90 additions and 67 deletions.
2 changes: 1 addition & 1 deletion cmd/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ var (
RunE: func(cmd *cobra.Command, args []string) error {
if !viper.IsSet("WORKDIR") {
title := fmt.Sprintf("Enter a directory to bootstrap your project (or leave blank to use %s): ", utils.Bold(utils.CurrentDirAbs))
workdir, err := utils.PromptText(title, os.Stdin)
workdir, err := utils.NewConsole().PromptText(title)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options ..
return errors.Errorf("failed to read workdir: %w", err)
} else if !empty {
title := fmt.Sprintf("Do you want to overwrite existing files in %s directory?", utils.Bold(workdir))
if !utils.PromptYesNo(title, true, os.Stdin) {
if !utils.NewConsole().PromptYesNo(title, true) {
return context.Canceled
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/db/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func Run(ctx context.Context, schema []string, config pgconn.Config, name string
}
// 4. Insert a row to `schema_migrations`
fmt.Fprintln(os.Stderr, "Schema written to "+utils.Bold(path))
if shouldUpdate := utils.PromptYesNo("Update remote migration history table?", true, os.Stdin); shouldUpdate {
if shouldUpdate := utils.NewConsole().PromptYesNo("Update remote migration history table?", true); shouldUpdate {
return repair.UpdateMigrationTable(ctx, conn, []string{timestamp}, repair.Applied, false, fsys)
}
return nil
Expand Down
2 changes: 1 addition & 1 deletion internal/db/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func Run(ctx context.Context, dryRun, ignoreVersionMismatch bool, includeRoles,
}
} else {
msg := fmt.Sprintf("Do you want to push these migrations to the remote database?\n • %s\n\n", strings.Join(pending, "\n • "))
if shouldPush := utils.PromptYesNo(msg, true, os.Stdin); !shouldPush {
if shouldPush := utils.NewConsole().PromptYesNo(msg, true); !shouldPush {
utils.CmdSuggestion = ""
return errors.New(context.Canceled)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/db/reset/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func Run(ctx context.Context, version string, config pgconn.Config, fsys afero.F
}
if !utils.IsLocalDatabase(config) {
msg := "Do you want to reset the remote database?"
if shouldReset := utils.PromptYesNo(msg, true, os.Stdin); !shouldReset {
if shouldReset := utils.NewConsole().PromptYesNo(msg, true); !shouldReset {
utils.CmdSuggestion = ""
return errors.New(context.Canceled)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ func Run(fsys afero.Fs, createVscodeSettings, createIntellijSettings *bool, para
return writeIntelliJConfig(fsys)
}
} else {
if isVscode := utils.PromptYesNo("Generate VS Code settings for Deno?", false, os.Stdin); isVscode {
console := utils.NewConsole()
if isVscode := console.PromptYesNo("Generate VS Code settings for Deno?", false); isVscode {
return writeVscodeConfig(fsys)
}
if isIntelliJ := utils.PromptYesNo("Generate IntelliJ Settings for Deno?", false, os.Stdin); isIntelliJ {
if isIntelliJ := console.PromptYesNo("Generate IntelliJ Settings for Deno?", false); isIntelliJ {
return writeIntelliJConfig(fsys)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/logout/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

func Run(ctx context.Context, stdout *os.File, fsys afero.Fs) error {
if !utils.PromptYesNo("Do you want to log out? This will remove the access token from your system.", false, os.Stdin) {
if !utils.NewConsole().PromptYesNo("Do you want to log out? This will remove the access token from your system.", false) {
fmt.Fprintln(os.Stderr, "Not deleting access token.")
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/migration/repair/repair.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func Run(ctx context.Context, config pgconn.Config, version []string, status str
repairAll := len(version) == 0
if repairAll {
msg := "Do you want to repair the entire migration history table to match local migration files?"
if shouldRepair := utils.PromptYesNo(msg, false, os.Stdin); !shouldRepair {
if shouldRepair := utils.NewConsole().PromptYesNo(msg, false); !shouldRepair {
utils.CmdSuggestion = ""
return errors.New(context.Canceled)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/migration/squash/squash.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func Run(ctx context.Context, version string, config pgconn.Config, fsys afero.F
return err
}
// 2. Update migration history
if utils.IsLocalDatabase(config) || !utils.PromptYesNo("Update remote migration history table?", true, os.Stdin) {
if utils.IsLocalDatabase(config) || !utils.NewConsole().PromptYesNo("Update remote migration history table?", true) {
return nil
}
return baselineMigrations(ctx, config, version, fsys, options...)
Expand Down
2 changes: 1 addition & 1 deletion internal/projects/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func promptMissingParams(ctx context.Context, body *api.CreateProjectBody) error
}

func promptProjectName() (string, error) {
name, err := utils.PromptText("Enter your project name: ", os.Stdin)
name, err := utils.NewConsole().PromptText("Enter your project name: ")
if err != nil {
return "", err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/projects/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func PreRun(ref string) error {
if err := utils.AssertProjectRefIsValid(ref); err != nil {
return err
}
if !utils.PromptYesNo("Do you want to delete project "+utils.Aqua(ref)+"? This action is irreversible.", true, os.Stdin) {
if !utils.NewConsole().PromptYesNo("Do you want to delete project "+utils.Aqua(ref)+"? This action is irreversible.", true) {
return errors.New("Not deleting project: " + utils.Aqua(ref))
}
return nil
Expand Down
2 changes: 1 addition & 1 deletion internal/storage/rm/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func Run(ctx context.Context, paths []string, recursive bool, fsys afero.Fs) err
}
for bucket, prefixes := range groups {
confirm := fmt.Sprintf("Confirm deleting files in bucket %v?", utils.Bold(bucket))
if shouldDelete := utils.PromptYesNo(confirm, true, os.Stdin); !shouldDelete {
if shouldDelete := utils.NewConsole().PromptYesNo(confirm, true); !shouldDelete {
continue
}
// Always try deleting first in case the paths resolve to extensionless files
Expand Down
76 changes: 76 additions & 0 deletions internal/utils/console.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package utils

import (
"bufio"
"fmt"
"io"
"os"
"strings"

"github.com/go-errors/errors"
"golang.org/x/term"
)

type Console struct {
isTTY bool
stdin *bufio.Scanner
stdout io.Writer
stderr io.Writer
}

func NewConsole() Console {
return Console{
isTTY: term.IsTerminal(int(os.Stdin.Fd())),
stdin: bufio.NewScanner(os.Stdin),
stdout: os.Stdout,
stderr: GetDebugLogger(),
}
}

// PromptYesNo asks yes/no questions using the label.
func (c Console) PromptYesNo(label string, def bool) bool {
choices := "Y/n"
if !def {
choices = "y/N"
}
labelWithChoice := fmt.Sprintf("%s [%s] ", label, choices)
// Any error will be handled as default value
if s, err := c.PromptText(labelWithChoice); err != nil {
fmt.Fprintln(c.stdout)
if !errors.Is(err, io.EOF) {
fmt.Fprintln(c.stderr, err)
}
} else if answer := parseYesNo(s); answer != nil {
return *answer
}
return def
}

func parseYesNo(s string) *bool {
s = strings.ToLower(s)
if s == "y" || s == "yes" {
return Ptr(true)
}
if s == "n" || s == "no" {
return Ptr(false)
}
return nil
}

// PromptText asks for input using the label.
func (c Console) PromptText(label string) (string, error) {
fmt.Fprint(os.Stderr, label)
// Scan a single line from input or file
if !c.stdin.Scan() {
return "", errors.New(io.EOF)
}
if err := c.stdin.Err(); err != nil {
return "", errors.Errorf("failed to scan stdin: %w", err)
}
token := strings.TrimSpace(c.stdin.Text())
// Echo input from non-interactive terminal
if !c.isTTY {
fmt.Fprintln(c.stdout, token)
}
return token, nil
}
54 changes: 0 additions & 54 deletions internal/utils/prompt.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package utils

import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"strings"

"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/go-errors/errors"
"golang.org/x/term"
)

var (
Expand Down Expand Up @@ -146,53 +142,3 @@ func PromptChoice(ctx context.Context, title string, items []PromptItem) (Prompt
}
return initial.choice, err
}

// PromptYesNo asks yes/no questions using the label.
func PromptYesNo(label string, def bool, stdin *os.File) bool {
logger := GetDebugLogger()
if !term.IsTerminal(int(stdin.Fd())) {
var buf bytes.Buffer
if _, err := io.Copy(&buf, stdin); err != nil {
fmt.Fprintln(logger, err)
} else if answer := parseYesNo(buf.String()); answer != nil {
return *answer
}
return def
}

choices := "Y/n"
if !def {
choices = "y/N"
}
labelWithChoice := fmt.Sprintf("%s [%s] ", label, choices)

// Any error will be handled as empty string
if s, err := PromptText(labelWithChoice, stdin); err != nil {
fmt.Fprintln(logger, err)
} else if answer := parseYesNo(s); answer != nil {
return *answer
}
return def
}

func parseYesNo(s string) *bool {
s = strings.ToLower(strings.TrimSpace(s))
if s == "y" || s == "yes" {
return Ptr(true)
}
if s == "n" || s == "no" {
return Ptr(false)
}
return nil
}

func PromptText(label string, stdin io.Reader) (string, error) {
fmt.Fprint(os.Stderr, label)
scanner := bufio.NewScanner(stdin)
// Scan a single line for input
scanner.Scan()
if err := scanner.Err(); err != nil {
return "", errors.Errorf("failed to read stdin: %w", err)
}
return strings.TrimSpace(scanner.Text()), nil
}

0 comments on commit 82b3892

Please sign in to comment.