add now downloads http(s) URLs (with a Chrome user agent) through a private temporary file, and treats ".", ".." and "/" as "this folder's contents" so the destination folder is no longer dropped for "..". migrate is add followed by deleting each original and any directories left empty. It asks on the terminal before overwriting anything already in the vault, refusing when there is no terminal, unless --yes is given. Adding or migrating a directory skips the vault and key file if they live inside it, so migrate can never ingest or delete them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
package term
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// enableVirtualTerminalProcessing tries to enable virtual terminal processing
|
|
// on Windows. If it fails, avoid using escape sequences to prevent weird
|
|
// characters being printed to the console.
|
|
var enableVirtualTerminalProcessing func(out *os.File) error
|
|
|
|
// clearLine clears the current line on the terminal, or opens a new line if
|
|
// terminal escape codes don't work.
|
|
func clearLine(out *os.File) {
|
|
const (
|
|
CUI = "\033[" // Control Sequence Introducer
|
|
CPL = CUI + "F" // Cursor Previous Line
|
|
EL = CUI + "K" // Erase in Line
|
|
)
|
|
|
|
// First, open a new line, which is guaranteed to work everywhere. Then, try
|
|
// to erase the line above with escape codes, if possible.
|
|
//
|
|
// (We use CRLF instead of LF to work around an apparent bug in WSL2's
|
|
// handling of CONOUT$. Only when running a Windows binary from WSL2, the
|
|
// cursor would not go back to the start of the line with a simple LF.
|
|
// Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)
|
|
fmt.Fprintf(out, "\r\n")
|
|
if enableVirtualTerminalProcessing == nil || enableVirtualTerminalProcessing(out) == nil {
|
|
fmt.Fprintf(out, CPL+EL)
|
|
}
|
|
}
|
|
|
|
// WithTerminal runs f with the terminal input and output files, if available.
|
|
// WithTerminal does not open a non-terminal stdin, so the caller does not need
|
|
// to check if stdin is in use.
|
|
func WithTerminal(f func(in, out *os.File) error) error {
|
|
if runtime.GOOS == "windows" {
|
|
in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
return f(in, out)
|
|
} else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
|
|
defer tty.Close()
|
|
return f(tty, tty)
|
|
} else if term.IsTerminal(int(os.Stdin.Fd())) {
|
|
return f(os.Stdin, os.Stdin)
|
|
} else {
|
|
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
|
|
}
|
|
}
|
|
|
|
// ReadSecret reads a value from the terminal with no echo. The prompt is ephemeral.
|
|
func ReadSecret(prompt string) (s []byte, err error) {
|
|
err = WithTerminal(func(in, out *os.File) error {
|
|
fmt.Fprintf(out, "%s ", prompt)
|
|
defer clearLine(out)
|
|
s, err = term.ReadPassword(int(in.Fd()))
|
|
return err
|
|
})
|
|
return
|
|
}
|
|
|
|
// Confirm asks a yes/no question on the terminal. Only "y" or "yes" is yes.
|
|
func Confirm(prompt string) (yes bool, err error) {
|
|
err = WithTerminal(func(in, out *os.File) error {
|
|
fmt.Fprintf(out, "%s ", prompt)
|
|
line, err := bufio.NewReader(in).ReadString('\n')
|
|
if err != nil && line == "" {
|
|
return err
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(line)) {
|
|
case "y", "yes":
|
|
yes = true
|
|
}
|
|
return nil
|
|
})
|
|
return
|
|
}
|
|
|
|
// ReadPassphrase returns the passphrase from the environment variable env if
|
|
// it is set, otherwise prompts on the terminal.
|
|
func ReadPassphrase(prompt, env string) (string, error) {
|
|
if p, ok := os.LookupEnv(env); ok {
|
|
if p == "" {
|
|
return "", fmt.Errorf("%s is set but empty", env)
|
|
}
|
|
return p, nil
|
|
}
|
|
p, err := ReadSecret(prompt)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not read passphrase (set %s for non-interactive use): %v", env, err)
|
|
}
|
|
return string(p), nil
|
|
}
|