92 lines
2.5 KiB
Go
92 lines
2.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
"github.com/spf13/afero"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// Global scanner for consistent stdin reading
|
|
var stdinScanner *bufio.Scanner
|
|
|
|
// CLIInstance encapsulates all CLI functionality and state
|
|
type CLIInstance struct {
|
|
fs afero.Fs
|
|
stateDir string
|
|
}
|
|
|
|
// NewCLIInstance creates a new CLI instance with the real filesystem
|
|
func NewCLIInstance() *CLIInstance {
|
|
fs := afero.NewOsFs()
|
|
stateDir := secret.DetermineStateDir("")
|
|
return &CLIInstance{
|
|
fs: fs,
|
|
stateDir: stateDir,
|
|
}
|
|
}
|
|
|
|
// NewCLIInstanceWithFs creates a new CLI instance with the given filesystem (for testing)
|
|
func NewCLIInstanceWithFs(fs afero.Fs) *CLIInstance {
|
|
stateDir := secret.DetermineStateDir("")
|
|
return &CLIInstance{
|
|
fs: fs,
|
|
stateDir: stateDir,
|
|
}
|
|
}
|
|
|
|
// NewCLIInstanceWithStateDir creates a new CLI instance with custom state directory (for testing)
|
|
func NewCLIInstanceWithStateDir(fs afero.Fs, stateDir string) *CLIInstance {
|
|
return &CLIInstance{
|
|
fs: fs,
|
|
stateDir: stateDir,
|
|
}
|
|
}
|
|
|
|
// SetFilesystem sets the filesystem for this CLI instance (for testing)
|
|
func (cli *CLIInstance) SetFilesystem(fs afero.Fs) {
|
|
cli.fs = fs
|
|
}
|
|
|
|
// SetStateDir sets the state directory for this CLI instance (for testing)
|
|
func (cli *CLIInstance) SetStateDir(stateDir string) {
|
|
cli.stateDir = stateDir
|
|
}
|
|
|
|
// GetStateDir returns the state directory for this CLI instance
|
|
func (cli *CLIInstance) GetStateDir() string {
|
|
return cli.stateDir
|
|
}
|
|
|
|
// getStdinScanner returns a shared scanner for stdin to avoid buffering issues
|
|
func getStdinScanner() *bufio.Scanner {
|
|
if stdinScanner == nil {
|
|
stdinScanner = bufio.NewScanner(os.Stdin)
|
|
}
|
|
return stdinScanner
|
|
}
|
|
|
|
// readLineFromStdin reads a single line from stdin with a prompt
|
|
// Uses a shared scanner to avoid buffering issues between multiple calls
|
|
func readLineFromStdin(prompt string) (string, error) {
|
|
// Check if stderr is a terminal - if not, we can't prompt interactively
|
|
if !term.IsTerminal(int(syscall.Stderr)) {
|
|
return "", fmt.Errorf("cannot prompt for input: stderr is not a terminal (running in non-interactive mode)")
|
|
}
|
|
|
|
fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
|
|
scanner := getStdinScanner()
|
|
if !scanner.Scan() {
|
|
if err := scanner.Err(); err != nil {
|
|
return "", fmt.Errorf("failed to read from stdin: %w", err)
|
|
}
|
|
return "", fmt.Errorf("failed to read from stdin: EOF")
|
|
}
|
|
return strings.TrimSpace(scanner.Text()), nil
|
|
}
|