mirror of
https://github.com/FiloSottile/age.git
synced 2026-03-11 08:55:41 +00:00
plugin: add NewTerminalUI
Closes #611 Closes #591 Co-authored-by: Nicolas Dumazet <nicdumz.commits@gmail.com>
This commit is contained in:
parent
a62324430d
commit
92ac13f51c
7 changed files with 267 additions and 238 deletions
|
|
@ -22,8 +22,8 @@ import (
|
|||
"filippo.io/age"
|
||||
"filippo.io/age/agessh"
|
||||
"filippo.io/age/armor"
|
||||
"filippo.io/age/internal/term"
|
||||
"filippo.io/age/plugin"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const usage = `Usage:
|
||||
|
|
@ -251,7 +251,7 @@ func main() {
|
|||
in = f
|
||||
} else {
|
||||
stdinInUse = true
|
||||
if decryptFlag && term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
if decryptFlag && term.IsTerminal(os.Stdin) {
|
||||
// If the input comes from a TTY, assume it's armored, and buffer up
|
||||
// to the END line (or EOF/EOT) so that a password prompt or the
|
||||
// output don't get in the way of typing the input. See Issue 364.
|
||||
|
|
@ -275,7 +275,7 @@ func main() {
|
|||
}
|
||||
}()
|
||||
out = f
|
||||
} else if term.IsTerminal(int(os.Stdout.Fd())) {
|
||||
} else if term.IsTerminal(os.Stdout) {
|
||||
if name != "-" {
|
||||
if decryptFlag {
|
||||
// TODO: buffer the output and check it's printable.
|
||||
|
|
@ -287,7 +287,7 @@ func main() {
|
|||
`force anyway with "-o -"`)
|
||||
}
|
||||
}
|
||||
if in == os.Stdin && term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
if in == os.Stdin && term.IsTerminal(os.Stdin) {
|
||||
// If the input comes from a TTY and output will go to a TTY,
|
||||
// buffer it up so it doesn't get in the way of typing the input.
|
||||
buf := &bytes.Buffer{}
|
||||
|
|
@ -309,7 +309,7 @@ func main() {
|
|||
}
|
||||
|
||||
func passphrasePromptForEncryption() (string, error) {
|
||||
pass, err := readSecret("Enter passphrase (leave empty to autogenerate a secure one):")
|
||||
pass, err := term.ReadSecret("Enter passphrase (leave empty to autogenerate a secure one):")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
|
|
@ -325,7 +325,7 @@ func passphrasePromptForEncryption() (string, error) {
|
|||
return "", fmt.Errorf("could not print passphrase: %v", err)
|
||||
}
|
||||
} else {
|
||||
confirm, err := readSecret("Confirm passphrase:")
|
||||
confirm, err := term.ReadSecret("Confirm passphrase:")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
|
|
@ -370,7 +370,7 @@ func encryptNotPass(recs, files []string, identities identityFlags, in io.Reader
|
|||
}
|
||||
recipients = append(recipients, r...)
|
||||
case "j":
|
||||
id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI)
|
||||
id, err := plugin.NewIdentityWithoutData(f.Value, plugin.NewTerminalUI(printf, warningf))
|
||||
if err != nil {
|
||||
errorf("initializing %q: %v", f.Value, err)
|
||||
}
|
||||
|
|
@ -450,7 +450,7 @@ func decryptNotPass(flags identityFlags, in io.Reader, out io.Writer) {
|
|||
}
|
||||
identities = append(identities, ids...)
|
||||
case "j":
|
||||
id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI)
|
||||
id, err := plugin.NewIdentityWithoutData(f.Value, plugin.NewTerminalUI(printf, warningf))
|
||||
if err != nil {
|
||||
errorf("initializing %q: %v", f.Value, err)
|
||||
}
|
||||
|
|
@ -509,7 +509,7 @@ func decrypt(identities []age.Identity, in io.Reader, out io.Writer) {
|
|||
var lazyScryptIdentity = &LazyScryptIdentity{passphrasePromptForDecryption}
|
||||
|
||||
func passphrasePromptForDecryption() (string, error) {
|
||||
pass, err := readSecret("Enter passphrase:")
|
||||
pass, err := term.ReadSecret("Enter passphrase:")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"filippo.io/age"
|
||||
"filippo.io/age/agessh"
|
||||
"filippo.io/age/armor"
|
||||
"filippo.io/age/internal/term"
|
||||
"filippo.io/age/plugin"
|
||||
"filippo.io/age/tag"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
|
|
@ -37,7 +38,7 @@ func parseRecipient(arg string) (age.Recipient, error) {
|
|||
case strings.HasPrefix(arg, "age1pq1"):
|
||||
return age.ParseHybridRecipient(arg)
|
||||
case strings.HasPrefix(arg, "age1") && strings.Count(arg, "1") > 1:
|
||||
return plugin.NewRecipient(arg, pluginTerminalUI)
|
||||
return plugin.NewRecipient(arg, plugin.NewTerminalUI(printf, warningf))
|
||||
case strings.HasPrefix(arg, "age1"):
|
||||
return age.ParseX25519Recipient(arg)
|
||||
case strings.HasPrefix(arg, "ssh-"):
|
||||
|
|
@ -175,7 +176,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
|||
return []age.Identity{&EncryptedIdentity{
|
||||
Contents: contents,
|
||||
Passphrase: func() (string, error) {
|
||||
pass, err := readSecret(fmt.Sprintf("Enter passphrase for identity file %q:", name))
|
||||
pass, err := term.ReadSecret(fmt.Sprintf("Enter passphrase for identity file %q:", name))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
|
|
@ -211,7 +212,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
|||
func parseIdentity(s string) (age.Identity, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(s, "AGE-PLUGIN-"):
|
||||
return plugin.NewIdentity(s, pluginTerminalUI)
|
||||
return plugin.NewIdentity(s, plugin.NewTerminalUI(printf, warningf))
|
||||
case strings.HasPrefix(s, "AGE-SECRET-KEY-1"):
|
||||
return age.ParseX25519Identity(s)
|
||||
case strings.HasPrefix(s, "AGE-SECRET-KEY-PQ-1"):
|
||||
|
|
@ -265,7 +266,7 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
|||
}
|
||||
}
|
||||
passphrasePrompt := func() ([]byte, error) {
|
||||
pass, err := readSecret(fmt.Sprintf("Enter passphrase for %q:", name))
|
||||
pass, err := term.ReadSecret(fmt.Sprintf("Enter passphrase for %q:", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
|
||||
}
|
||||
|
|
|
|||
180
cmd/age/tui.go
180
cmd/age/tui.go
|
|
@ -12,19 +12,19 @@ package main
|
|||
//
|
||||
// - Everything else goes to standard error with an "age:" prefix.
|
||||
// No capitalized initials and no periods at the end.
|
||||
//
|
||||
// The one exception is the autogenerated passphrase, which goes to
|
||||
// the terminal, since we really want it to reach the user only.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"filippo.io/age/armor"
|
||||
"filippo.io/age/plugin"
|
||||
"golang.org/x/term"
|
||||
"filippo.io/age/internal/term"
|
||||
)
|
||||
|
||||
// l is a logger with no prefixes.
|
||||
|
|
@ -53,183 +53,13 @@ func errorWithHint(error string, hints ...string) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
// avoidTerminalEscapeSequences is set if we need to avoid using escape
|
||||
// sequences to prevent weird characters being printed to the console. This will
|
||||
// happen on Windows when virtual terminal processing cannot be enabled.
|
||||
var avoidTerminalEscapeSequences bool
|
||||
|
||||
// clearLine clears the current line on the terminal, or opens a new line if
|
||||
// terminal escape codes don't work.
|
||||
func clearLine(out io.Writer) {
|
||||
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 !avoidTerminalEscapeSequences {
|
||||
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 stdinInUse.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func printfToTerminal(format string, v ...interface{}) error {
|
||||
return withTerminal(func(_, out *os.File) error {
|
||||
return term.WithTerminal(func(_, out *os.File) error {
|
||||
_, err := fmt.Fprintf(out, "age: "+format+"\n", v...)
|
||||
return 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
|
||||
}
|
||||
|
||||
// readPublic reads a value from the terminal. The prompt is ephemeral.
|
||||
func readPublic(prompt string) (s []byte, err error) {
|
||||
err = withTerminal(func(in, out *os.File) error {
|
||||
fmt.Fprintf(out, "%s ", prompt)
|
||||
defer clearLine(out)
|
||||
|
||||
oldState, err := term.MakeRaw(int(in.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer term.Restore(int(in.Fd()), oldState)
|
||||
|
||||
t := term.NewTerminal(in, "")
|
||||
line, err := t.ReadLine()
|
||||
s = []byte(line)
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// readCharacter reads a single character from the terminal with no echo. The
|
||||
// prompt is ephemeral.
|
||||
func readCharacter(prompt string) (c byte, err error) {
|
||||
err = withTerminal(func(in, out *os.File) error {
|
||||
fmt.Fprintf(out, "%s ", prompt)
|
||||
defer clearLine(out)
|
||||
|
||||
oldState, err := term.MakeRaw(int(in.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer term.Restore(int(in.Fd()), oldState)
|
||||
|
||||
b := make([]byte, 1)
|
||||
if _, err := in.Read(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c = b[0]
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var pluginTerminalUI = &plugin.ClientUI{
|
||||
DisplayMessage: func(name, message string) error {
|
||||
printf("%s plugin: %s", name, message)
|
||||
return nil
|
||||
},
|
||||
RequestValue: func(name, message string, isSecret bool) (s string, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
warningf("could not read value for age-plugin-%s: %v", name, err)
|
||||
}
|
||||
}()
|
||||
if isSecret {
|
||||
secret, err := readSecret(message)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(secret), nil
|
||||
} else {
|
||||
public, err := readPublic(message)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(public), nil
|
||||
}
|
||||
},
|
||||
Confirm: func(name, message, yes, no string) (choseYes bool, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
warningf("could not read value for age-plugin-%s: %v", name, err)
|
||||
}
|
||||
}()
|
||||
if no == "" {
|
||||
message += fmt.Sprintf(" (press enter for %q)", yes)
|
||||
_, err := readSecret(message)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
message += fmt.Sprintf(" (press [1] for %q or [2] for %q)", yes, no)
|
||||
for {
|
||||
selection, err := readCharacter(message)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch selection {
|
||||
case '1':
|
||||
return true, nil
|
||||
case '2':
|
||||
return false, nil
|
||||
case '\x03': // CTRL-C
|
||||
return false, errors.New("user cancelled prompt")
|
||||
default:
|
||||
warningf("reading value for age-plugin-%s: invalid selection %q", name, selection)
|
||||
}
|
||||
}
|
||||
},
|
||||
WaitTimer: func(name string) {
|
||||
printf("waiting on %s plugin...", name)
|
||||
},
|
||||
}
|
||||
|
||||
func bufferTerminalInput(in io.Reader) (io.Reader, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
if _, err := buf.ReadFrom(ReaderFunc(func(p []byte) (n int, err error) {
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
// Copyright 2022 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Some instances of the Windows Console (e.g., cmd.exe and Windows PowerShell)
|
||||
// do not have the virtual terminal processing enabled, which is necessary to
|
||||
// make terminal escape sequences work. For this reason the clearLine function
|
||||
// may not properly work. Here we enable the virtual terminal processing, if
|
||||
// possible.
|
||||
//
|
||||
// See https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences.
|
||||
func init() {
|
||||
const (
|
||||
ENABLE_PROCESSED_OUTPUT uint32 = 0x1
|
||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING uint32 = 0x4
|
||||
)
|
||||
|
||||
kernel32DLL := windows.NewLazySystemDLL("Kernel32.dll")
|
||||
setConsoleMode := kernel32DLL.NewProc("SetConsoleMode")
|
||||
|
||||
if err := withTerminal(func(in, out *os.File) error {
|
||||
var mode uint32
|
||||
if err := syscall.GetConsoleMode(syscall.Handle(out.Fd()), &mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mode |= ENABLE_PROCESSED_OUTPUT
|
||||
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
|
||||
// If the SetConsoleMode function fails, the return value is zero.
|
||||
// See https://learn.microsoft.com/en-us/windows/console/setconsolemode#return-value.
|
||||
if ret, _, _ := setConsoleMode.Call(out.Fd(), uintptr(mode)); ret == 0 {
|
||||
return errors.New("SetConsoleMode failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
avoidTerminalEscapeSequences = true
|
||||
}
|
||||
}
|
||||
122
internal/term/term.go
Normal file
122
internal/term/term.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// ReadPublic reads a value from the terminal. The prompt is ephemeral.
|
||||
func ReadPublic(prompt string) (s []byte, err error) {
|
||||
err = WithTerminal(func(in, out *os.File) error {
|
||||
fmt.Fprintf(out, "%s ", prompt)
|
||||
defer clearLine(out)
|
||||
|
||||
oldState, err := term.MakeRaw(int(in.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer term.Restore(int(in.Fd()), oldState)
|
||||
|
||||
t := term.NewTerminal(in, "")
|
||||
line, err := t.ReadLine()
|
||||
s = []byte(line)
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ReadCharacter reads a single character from the terminal with no echo. The
|
||||
// prompt is ephemeral.
|
||||
func ReadCharacter(prompt string) (c byte, err error) {
|
||||
err = WithTerminal(func(in, out *os.File) error {
|
||||
fmt.Fprintf(out, "%s ", prompt)
|
||||
defer clearLine(out)
|
||||
|
||||
oldState, err := term.MakeRaw(int(in.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer term.Restore(int(in.Fd()), oldState)
|
||||
|
||||
b := make([]byte, 1)
|
||||
if _, err := in.Read(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c = b[0]
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// IsTerminal returns whether the given file is a terminal.
|
||||
func IsTerminal(f *os.File) bool {
|
||||
return term.IsTerminal(int(f.Fd()))
|
||||
}
|
||||
48
internal/term/term_windows.go
Normal file
48
internal/term/term_windows.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2022 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package term
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func init() {
|
||||
enableVirtualTerminalProcessing = func(out *os.File) error {
|
||||
// Some instances of the Windows Console (e.g., cmd.exe and Windows PowerShell)
|
||||
// do not have the virtual terminal processing enabled, which is necessary to
|
||||
// make terminal escape sequences work. For this reason the clearLine function
|
||||
// may not properly work. Here we enable the virtual terminal processing, if
|
||||
// possible.
|
||||
//
|
||||
// See https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences.
|
||||
|
||||
const (
|
||||
ENABLE_PROCESSED_OUTPUT uint32 = 0x1
|
||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING uint32 = 0x4
|
||||
)
|
||||
|
||||
kernel32DLL := windows.NewLazySystemDLL("Kernel32.dll")
|
||||
setConsoleMode := kernel32DLL.NewProc("SetConsoleMode")
|
||||
|
||||
var mode uint32
|
||||
if err := syscall.GetConsoleMode(syscall.Handle(out.Fd()), &mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mode |= ENABLE_PROCESSED_OUTPUT
|
||||
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
|
||||
// If the SetConsoleMode function fails, the return value is zero.
|
||||
// See https://learn.microsoft.com/en-us/windows/console/setconsolemode#return-value.
|
||||
if ret, _, _ := setConsoleMode.Call(out.Fd(), uintptr(mode)); ret == 0 {
|
||||
return errors.New("SetConsoleMode failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
78
plugin/tui.go
Normal file
78
plugin/tui.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"filippo.io/age/internal/term"
|
||||
)
|
||||
|
||||
// NewTerminalUI returns a [ClientUI] that uses the terminal to request inputs,
|
||||
// and the provided functions to display messages and errors.
|
||||
//
|
||||
// The terminal is reached directly through /dev/tty or CONIN$/CONOUT$,
|
||||
// bypassing standard input and output, so this UI can be used even when
|
||||
// standard input or output are redirected.
|
||||
func NewTerminalUI(printf, warningf func(format string, v ...interface{})) *ClientUI {
|
||||
return &ClientUI{
|
||||
DisplayMessage: func(name, message string) error {
|
||||
printf("%s plugin: %s", name, message)
|
||||
return nil
|
||||
},
|
||||
RequestValue: func(name, message string, isSecret bool) (s string, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
warningf("could not read value for age-plugin-%s: %v", name, err)
|
||||
}
|
||||
}()
|
||||
if isSecret {
|
||||
secret, err := term.ReadSecret(message)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(secret), nil
|
||||
} else {
|
||||
public, err := term.ReadPublic(message)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(public), nil
|
||||
}
|
||||
},
|
||||
Confirm: func(name, message, yes, no string) (choseYes bool, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
warningf("could not read value for age-plugin-%s: %v", name, err)
|
||||
}
|
||||
}()
|
||||
if no == "" {
|
||||
message += fmt.Sprintf(" (press enter for %q)", yes)
|
||||
_, err := term.ReadSecret(message)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
message += fmt.Sprintf(" (press [1] for %q or [2] for %q)", yes, no)
|
||||
for {
|
||||
selection, err := term.ReadCharacter(message)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch selection {
|
||||
case '1':
|
||||
return true, nil
|
||||
case '2':
|
||||
return false, nil
|
||||
case '\x03': // CTRL-C
|
||||
return false, errors.New("user cancelled prompt")
|
||||
default:
|
||||
warningf("reading value for age-plugin-%s: invalid selection %q", name, selection)
|
||||
}
|
||||
}
|
||||
},
|
||||
WaitTimer: func(name string) {
|
||||
printf("waiting on %s plugin...", name)
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue