End the mnemonic prompt at end of input (closes #78)
check / check (push) Waiting to run

term.ReadPassword reads the terminal with a raw read that returns 0
bytes at the end of input and takes that as nothing yet, so Ctrl-D at
the prompt did nothing. The prompt now sets the terminal as
ReadPassword does and reads the line from standard input, which
reports the end of input. Ctrl-D with nothing typed gives an empty
mnemonic, refused with status 1, and the terminal settings are put
back.

A test runs the tool on a pseudo-terminal (github.com/creack/pty, at a
commit past v1.1.24, whose Open on Linux can return another pty's
terminal) and types Ctrl-D at the prompt.

Model: opus-5-5
This commit is contained in:
2026-10-14 20:26:26 +00:00
parent 5582c02edf
commit 2d4e0bc335
7 changed files with 142 additions and 6 deletions
+29 -4
View File
@@ -2,15 +2,18 @@
package mnemonic
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"slices"
"strings"
"golang.org/x/sys/unix"
"golang.org/x/term"
"sneak.berlin/go/keyfunc/internal/bip39"
)
@@ -126,7 +129,10 @@ func ChildEnv() []string {
return kept
}
// ask prompts on the terminal with echo turned off.
// ask prompts on the terminal with echo turned off and reads one line,
// which ends at a newline or at the end of input (Ctrl-D). The terminal
// is set as term.ReadPassword sets it; ReadPassword itself is not used
// because it never sees the end of input and goes on waiting.
func ask() (string, error) {
fd := int(os.Stdin.Fd())
@@ -134,17 +140,36 @@ func ask() (string, error) {
return "", ErrMissing
}
settings, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
if err != nil {
return "", fmt.Errorf("reading the terminal settings: %w", err)
}
hidden := *settings
hidden.Lflag &^= unix.ECHO
hidden.Lflag |= unix.ICANON | unix.ISIG
hidden.Iflag |= unix.ICRNL
err = unix.IoctlSetTermios(fd, ioctlWriteTermios, &hidden)
if err != nil {
return "", fmt.Errorf("turning echo off: %w", err)
}
defer func() { _ = unix.IoctlSetTermios(fd, ioctlWriteTermios, settings) }()
fmt.Fprint(os.Stderr, "mnemonic: ")
typed, err := term.ReadPassword(fd)
// With ICANON set, a read from the terminal returns at most one
// line, so the buffer takes nothing past the newline.
typed, err := bufio.NewReader(os.Stdin).ReadString('\n')
fmt.Fprintln(os.Stderr)
if err != nil {
if err != nil && !errors.Is(err, io.EOF) {
return "", fmt.Errorf("reading the mnemonic: %w", err)
}
return checked(string(typed))
return checked(typed)
}
// checked joins the words with single spaces, whatever whitespace