diff --git a/changelog/unreleased/issue-5354 b/changelog/unreleased/issue-5354 index 1fbda9077..d69d9fd2b 100644 --- a/changelog/unreleased/issue-5354 +++ b/changelog/unreleased/issue-5354 @@ -10,3 +10,5 @@ This has been fixed. https://github.com/restic/restic/issues/5354 https://github.com/restic/restic/pull/5358 +https://github.com/restic/restic/pull/5493 +https://github.com/restic/restic/pull/5494 diff --git a/cmd/restic/cleanup.go b/cmd/restic/cleanup.go index 90ea93b92..45dc43b5c 100644 --- a/cmd/restic/cleanup.go +++ b/cmd/restic/cleanup.go @@ -23,7 +23,7 @@ func createGlobalContext() context.Context { func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc) { s := <-c debug.Log("signal %v received, cleaning up", s) - Warnf("%ssignal %v received, cleaning up\n", clearLine(0), s) + Warnf("\rsignal %v received, cleaning up \n", s) if val, _ := os.LookupEnv("RESTIC_DEBUG_STACKTRACE_SIGINT"); val != "" { _, _ = os.Stderr.WriteString("\n--- STACKTRACE START ---\n\n") diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 978b64616..921309101 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -11,6 +11,7 @@ import ( "github.com/restic/restic/internal/dump" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/terminal" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -199,7 +200,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] } func checkStdoutArchive() error { - if stdoutIsTerminal() { + if terminal.StdoutIsTerminal() { return fmt.Errorf("stdout is the terminal, please redirect output") } return nil diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index 5ced5fe01..3c5ddffd5 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -6,6 +6,7 @@ import ( "time" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/terminal" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" "github.com/spf13/pflag" @@ -71,7 +72,7 @@ func writeManpages(root *cobra.Command, dir string) error { } func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) { - if stdoutIsTerminal() { + if terminal.StdoutIsTerminal() { Verbosef("writing %s completion file to %v\n", shell, filename) } var outWriter io.Writer diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index bb32333c9..f0c84c628 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -10,6 +10,7 @@ import ( "github.com/restic/restic/internal/filter" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restorer" + "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" restoreui "github.com/restic/restic/internal/ui/restore" "github.com/restic/restic/internal/ui/termstatus" @@ -260,7 +261,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, } var count int t0 := time.Now() - bar := newTerminalProgressMax(!gopts.Quiet && !gopts.JSON && stdoutIsTerminal(), 0, "files verified", term) + bar := newTerminalProgressMax(!gopts.Quiet && !gopts.JSON && terminal.StdoutIsTerminal(), 0, "files verified", term) count, err = res.VerifyFiles(ctx, opts.Target, countRestoredFiles, bar) if err != nil { return err diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 6e58a0d73..df9461272 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -8,7 +8,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strconv" "strings" "time" @@ -33,13 +32,11 @@ import ( "github.com/restic/restic/internal/options" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/textfile" - "github.com/restic/restic/internal/ui/termstatus" "github.com/spf13/pflag" "github.com/restic/restic/internal/errors" - - "golang.org/x/term" ) // ErrNoRepository is used to report if opening a repository failed due @@ -199,46 +196,6 @@ func collectBackends() *location.Registry { return backends } -func stdinIsTerminal() bool { - return term.IsTerminal(int(os.Stdin.Fd())) -} - -func stdoutIsTerminal() bool { - // mintty on windows can use pipes which behave like a posix terminal, - // but which are not a terminal handle - return term.IsTerminal(int(os.Stdout.Fd())) || stdoutCanUpdateStatus() -} - -func stdoutCanUpdateStatus() bool { - return termstatus.CanUpdateStatus(os.Stdout.Fd()) -} - -func stdoutTerminalWidth() int { - w, _, err := term.GetSize(int(os.Stdout.Fd())) - if err != nil { - return 0 - } - return w -} - -// ClearLine creates a platform dependent string to clear the current -// line, so it can be overwritten. -// -// w should be the terminal width, or 0 to let clearLine figure it out. -func clearLine(w int) string { - if runtime.GOOS != "windows" { - return "\x1b[2K" - } - - // ANSI sequences are not supported on Windows cmd shell. - if w <= 0 { - if w = stdoutTerminalWidth(); w <= 0 { - return "" - } - } - return strings.Repeat(" ", w-1) + "\r" -} - // Printf writes the message to the configured stdout stream. func Printf(format string, args ...interface{}) { _, err := fmt.Fprintf(globalOptions.stdout, format, args...) @@ -333,52 +290,6 @@ func readPassword(in io.Reader) (password string, err error) { return sc.Text(), errors.WithStack(sc.Err()) } -// readPasswordTerminal reads the password from the given reader which must be a -// tty. Prompt is printed on the writer out before attempting to read the -// password. If the context is canceled, the function leaks the password reading -// goroutine. -func readPasswordTerminal(ctx context.Context, in *os.File, out *os.File, prompt string) (password string, err error) { - fd := int(out.Fd()) - state, err := term.GetState(fd) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unable to get terminal state: %v\n", err) - return "", err - } - - done := make(chan struct{}) - var buf []byte - - go func() { - defer close(done) - _, err = fmt.Fprint(out, prompt) - if err != nil { - return - } - buf, err = term.ReadPassword(int(in.Fd())) - if err != nil { - return - } - _, err = fmt.Fprintln(out) - }() - - select { - case <-ctx.Done(): - err := term.Restore(fd, state) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unable to restore terminal state: %v\n", err) - } - return "", ctx.Err() - case <-done: - // clean shutdown, nothing to do - } - - if err != nil { - return "", errors.Wrap(err, "ReadPassword") - } - - return string(buf), nil -} - // ReadPassword reads the password from a password file, the environment // variable RESTIC_PASSWORD or prompts the user. If the context is canceled, // the function leaks the password reading goroutine. @@ -399,11 +310,11 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin err error ) - if stdinIsTerminal() { - password, err = readPasswordTerminal(ctx, os.Stdin, os.Stderr, prompt) + if terminal.StdinIsTerminal() { + password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt) } else { password, err = readPassword(os.Stdin) - if stdoutIsTerminal() { + if terminal.StdoutIsTerminal() { Verbosef("reading repository password from stdin\n") } } @@ -427,7 +338,7 @@ func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt if err != nil { return "", err } - if stdinIsTerminal() { + if terminal.StdinIsTerminal() { pw2, err := ReadPassword(ctx, gopts, prompt2) if err != nil { return "", err @@ -490,7 +401,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi } passwordTriesLeft := 1 - if stdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword { + if terminal.StdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword { passwordTriesLeft = 3 } @@ -520,7 +431,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi return nil, errors.Fatalf("%s", err) } - if stdoutIsTerminal() && !opts.JSON { + if terminal.StdoutIsTerminal() && !opts.JSON { id := s.Config().ID if len(id) > 8 { id = id[:8] @@ -544,7 +455,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi return s, nil } - if c.Created && !opts.JSON && stdoutIsTerminal() { + if c.Created && !opts.JSON && terminal.StdoutIsTerminal() { Verbosef("created new cache in %v\n", c.Base) } @@ -563,7 +474,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi // cleanup old cache dirs if instructed to do so if opts.CleanupCache { - if stdoutIsTerminal() && !opts.JSON { + if terminal.StdoutIsTerminal() && !opts.JSON { Verbosef("removing %d old cache dirs from %v\n", len(oldCacheDirs), c.Base) } for _, item := range oldCacheDirs { @@ -574,7 +485,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi } } } else { - if stdoutIsTerminal() { + if terminal.StdoutIsTerminal() { Verbosef("found %d old cache directories in %v, run `restic cache --cleanup` to remove them\n", len(oldCacheDirs), c.Base) } diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go index 6f1626772..b2abd61d2 100644 --- a/cmd/restic/progress.go +++ b/cmd/restic/progress.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/ui/termstatus" @@ -23,7 +24,7 @@ func calculateProgressInterval(show bool, json bool) time.Duration { fps = 60 } interval = time.Duration(float64(time.Second) / fps) - } else if !json && !stdoutCanUpdateStatus() || !show { + } else if !json && !terminal.StdoutCanUpdateStatus() || !show { interval = 0 } return interval @@ -67,10 +68,9 @@ func newProgressMax(show bool, max uint64, description string) *progress.Counter } func printProgress(status string, final bool) { + canUpdateStatus := terminal.StdoutCanUpdateStatus() - canUpdateStatus := stdoutCanUpdateStatus() - - w := stdoutTerminalWidth() + w := terminal.StdoutWidth() if w > 0 { if w < 3 { status = termstatus.Truncate(status, w) @@ -82,12 +82,7 @@ func printProgress(status string, final bool) { } } - var carriageControl, cl string - - if canUpdateStatus { - cl = clearLine(w) - } - + var carriageControl string if !(strings.HasSuffix(status, "\r") || strings.HasSuffix(status, "\n")) { if canUpdateStatus { carriageControl = "\r" @@ -96,18 +91,23 @@ func printProgress(status string, final bool) { } } - _, _ = os.Stdout.Write([]byte(cl + status + carriageControl)) + if canUpdateStatus { + clearCurrentLine := terminal.ClearCurrentLine(os.Stdout.Fd()) + clearCurrentLine(os.Stdout, os.Stdout.Fd()) + } + + _, _ = os.Stdout.Write([]byte(status + carriageControl)) if final { _, _ = os.Stdout.Write([]byte("\n")) } } func newIndexProgress(quiet bool, json bool) *progress.Counter { - return newProgressMax(!quiet && !json && stdoutIsTerminal(), 0, "index files loaded") + return newProgressMax(!quiet && !json && terminal.StdoutIsTerminal(), 0, "index files loaded") } func newIndexTerminalProgress(quiet bool, json bool, term *termstatus.Terminal) *progress.Counter { - return newTerminalProgressMax(!quiet && !json && stdoutIsTerminal(), 0, "index files loaded", term) + return newTerminalProgressMax(!quiet && !json && terminal.StdoutIsTerminal(), 0, "index files loaded", term) } type terminalProgressPrinter struct { diff --git a/internal/backend/rclone/backend.go b/internal/backend/rclone/backend.go index 74b2148ce..5ee330a69 100644 --- a/internal/backend/rclone/backend.go +++ b/internal/backend/rclone/backend.go @@ -21,9 +21,9 @@ import ( "github.com/restic/restic/internal/backend/limiter" "github.com/restic/restic/internal/backend/location" "github.com/restic/restic/internal/backend/rest" - "github.com/restic/restic/internal/backend/util" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/terminal" "golang.org/x/net/http2" ) @@ -82,7 +82,7 @@ func run(command string, args ...string) (*StdioConn, *sync.WaitGroup, chan stru cmd.Stdin = r cmd.Stdout = w - bg, err := util.StartForeground(cmd) + bg, err := terminal.StartForeground(cmd) // close rclone side of pipes errR := r.Close() errW := w.Close() diff --git a/internal/backend/sftp/sftp.go b/internal/backend/sftp/sftp.go index c735b9b3a..78757a249 100644 --- a/internal/backend/sftp/sftp.go +++ b/internal/backend/sftp/sftp.go @@ -21,6 +21,7 @@ import ( "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/feature" + "github.com/restic/restic/internal/terminal" "github.com/cenkalti/backoff/v4" "github.com/pkg/sftp" @@ -84,7 +85,7 @@ func startClient(cfg Config) (*SFTP, error) { return nil, errors.Wrap(err, "cmd.StdoutPipe") } - bg, err := util.StartForeground(cmd) + bg, err := terminal.StartForeground(cmd) if err != nil { if errors.Is(err, exec.ErrDot) { return nil, errors.Errorf("cannot implicitly run relative executable %v found in current directory, use -o sftp.command=./ to override", cmd.Path) diff --git a/internal/ui/termstatus/background_unix.go b/internal/terminal/background_unix.go similarity index 87% rename from internal/ui/termstatus/background_unix.go rename to internal/terminal/background_unix.go index 666e42dd1..9564c7cde 100644 --- a/internal/ui/termstatus/background_unix.go +++ b/internal/terminal/background_unix.go @@ -1,6 +1,6 @@ //go:build unix -package termstatus +package terminal import "github.com/restic/restic/internal/debug" @@ -16,9 +16,9 @@ func IsProcessBackground(fd uintptr) bool { } func isProcessBackground(fd int) (bg bool, err error) { - pgid, err := Tcgetpgrp(fd) + pgid, err := tcgetpgrp(fd) if err != nil { return false, err } - return pgid != Getpgrp(), nil + return pgid != getpgrp(), nil } diff --git a/internal/ui/termstatus/background_unix_test.go b/internal/terminal/background_unix_test.go similarity index 94% rename from internal/ui/termstatus/background_unix_test.go rename to internal/terminal/background_unix_test.go index 301e780de..189e8c7d4 100644 --- a/internal/ui/termstatus/background_unix_test.go +++ b/internal/terminal/background_unix_test.go @@ -1,6 +1,6 @@ //go:build unix -package termstatus +package terminal import ( "os" diff --git a/internal/ui/termstatus/background_windows.go b/internal/terminal/background_windows.go similarity index 90% rename from internal/ui/termstatus/background_windows.go rename to internal/terminal/background_windows.go index b210509e7..9b370d074 100644 --- a/internal/ui/termstatus/background_windows.go +++ b/internal/terminal/background_windows.go @@ -1,4 +1,4 @@ -package termstatus +package terminal // IsProcessBackground reports whether the current process is running in the // background. Not implemented for this platform. diff --git a/internal/backend/util/foreground.go b/internal/terminal/foreground.go similarity index 97% rename from internal/backend/util/foreground.go rename to internal/terminal/foreground.go index 477fc8900..e3c1ff0b5 100644 --- a/internal/backend/util/foreground.go +++ b/internal/terminal/foreground.go @@ -1,4 +1,4 @@ -package util +package terminal import ( "os" diff --git a/internal/backend/util/foreground_test.go b/internal/terminal/foreground_test.go similarity index 84% rename from internal/backend/util/foreground_test.go rename to internal/terminal/foreground_test.go index c26861a6c..c8392ab88 100644 --- a/internal/backend/util/foreground_test.go +++ b/internal/terminal/foreground_test.go @@ -1,7 +1,7 @@ //go:build !windows // +build !windows -package util_test +package terminal_test import ( "bufio" @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/restic/restic/internal/backend/util" + "github.com/restic/restic/internal/terminal" rtest "github.com/restic/restic/internal/test" ) @@ -22,7 +22,7 @@ func TestForeground(t *testing.T) { stdout, err := cmd.StdoutPipe() rtest.OK(t, err) - bg, err := util.StartForeground(cmd) + bg, err := terminal.StartForeground(cmd) rtest.OK(t, err) defer func() { rtest.OK(t, cmd.Wait()) diff --git a/internal/backend/util/foreground_unix.go b/internal/terminal/foreground_unix.go similarity index 86% rename from internal/backend/util/foreground_unix.go rename to internal/terminal/foreground_unix.go index 3952885f8..a4f15eccf 100644 --- a/internal/backend/util/foreground_unix.go +++ b/internal/terminal/foreground_unix.go @@ -1,6 +1,6 @@ //go:build unix -package util +package terminal import ( "os" @@ -9,7 +9,6 @@ import ( "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" - "github.com/restic/restic/internal/ui/termstatus" "golang.org/x/sys/unix" ) @@ -29,13 +28,13 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) { } // only move child process to foreground if restic is in the foreground - prev, err := termstatus.Tcgetpgrp(int(tty.Fd())) + prev, err := tcgetpgrp(int(tty.Fd())) if err != nil { _ = tty.Close() return nil, err } - self := termstatus.Getpgrp() + self := getpgrp() if prev != self { debug.Log("restic is not controlling the tty; err = %v", err) if err := tty.Close(); err != nil { @@ -56,7 +55,7 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) { } // move the command's process group into the foreground - err = termstatus.Tcsetpgrp(int(tty.Fd()), cmd.Process.Pid) + err = tcsetpgrp(int(tty.Fd()), cmd.Process.Pid) if err != nil { _ = tty.Close() return nil, err @@ -67,7 +66,7 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) { signal.Reset(unix.SIGTTOU) // reset the foreground process group - err = termstatus.Tcsetpgrp(int(tty.Fd()), prev) + err = tcsetpgrp(int(tty.Fd()), prev) if err != nil { _ = tty.Close() return err diff --git a/internal/backend/util/foreground_windows.go b/internal/terminal/foreground_windows.go similarity index 96% rename from internal/backend/util/foreground_windows.go rename to internal/terminal/foreground_windows.go index f9b753c35..8c541fe47 100644 --- a/internal/backend/util/foreground_windows.go +++ b/internal/terminal/foreground_windows.go @@ -1,4 +1,4 @@ -package util +package terminal import ( "os/exec" diff --git a/internal/ui/termstatus/getpgrp_solaris.go b/internal/terminal/getpgrp_solaris.go similarity index 64% rename from internal/ui/termstatus/getpgrp_solaris.go rename to internal/terminal/getpgrp_solaris.go index 1df46d93c..71e02e7bf 100644 --- a/internal/ui/termstatus/getpgrp_solaris.go +++ b/internal/terminal/getpgrp_solaris.go @@ -1,8 +1,8 @@ -package termstatus +package terminal import "golang.org/x/sys/unix" -func Getpgrp() int { +func getpgrp() int { pid, _ := unix.Getpgrp() return pid } diff --git a/internal/terminal/getpgrp_unix.go b/internal/terminal/getpgrp_unix.go new file mode 100644 index 000000000..c9fe1470f --- /dev/null +++ b/internal/terminal/getpgrp_unix.go @@ -0,0 +1,7 @@ +//go:build unix && !solaris + +package terminal + +import "golang.org/x/sys/unix" + +func getpgrp() int { return unix.Getpgrp() } diff --git a/internal/terminal/password.go b/internal/terminal/password.go new file mode 100644 index 000000000..6d1b6c912 --- /dev/null +++ b/internal/terminal/password.go @@ -0,0 +1,55 @@ +package terminal + +import ( + "context" + "fmt" + "os" + + "golang.org/x/term" +) + +// ReadPassword reads the password from the given reader which must be a +// tty. Prompt is printed on the writer out before attempting to read the +// password. If the context is canceled, the function leaks the password reading +// goroutine. +func ReadPassword(ctx context.Context, in *os.File, out *os.File, prompt string) (password string, err error) { + fd := int(out.Fd()) + state, err := term.GetState(fd) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "unable to get terminal state: %v\n", err) + return "", err + } + + done := make(chan struct{}) + var buf []byte + + go func() { + defer close(done) + _, err = fmt.Fprint(out, prompt) + if err != nil { + return + } + buf, err = term.ReadPassword(int(in.Fd())) + if err != nil { + return + } + _, err = fmt.Fprintln(out) + }() + + select { + case <-ctx.Done(): + err := term.Restore(fd, state) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "unable to restore terminal state: %v\n", err) + } + return "", ctx.Err() + case <-done: + // clean shutdown, nothing to do + } + + if err != nil { + return "", fmt.Errorf("ReadPassword: %w", err) + } + + return string(buf), nil +} diff --git a/internal/terminal/stdio.go b/internal/terminal/stdio.go new file mode 100644 index 000000000..70e465ccb --- /dev/null +++ b/internal/terminal/stdio.go @@ -0,0 +1,33 @@ +package terminal + +import ( + "os" + + "golang.org/x/term" +) + +func StdinIsTerminal() bool { + return term.IsTerminal(int(os.Stdin.Fd())) +} + +func StdoutIsTerminal() bool { + // mintty on windows can use pipes which behave like a posix terminal, + // but which are not a terminal handle + return term.IsTerminal(int(os.Stdout.Fd())) || StdoutCanUpdateStatus() +} + +func StdoutCanUpdateStatus() bool { + return CanUpdateStatus(os.Stdout.Fd()) +} + +func StdoutWidth() int { + return Width(os.Stdout.Fd()) +} + +func Width(fd uintptr) int { + w, _, err := term.GetSize(int(fd)) + if err != nil { + return 0 + } + return w +} diff --git a/internal/ui/termstatus/tcgetpgrp_linux.go b/internal/terminal/tcgetpgrp_linux.go similarity index 85% rename from internal/ui/termstatus/tcgetpgrp_linux.go rename to internal/terminal/tcgetpgrp_linux.go index 51a9fd2da..b51796ace 100644 --- a/internal/ui/termstatus/tcgetpgrp_linux.go +++ b/internal/terminal/tcgetpgrp_linux.go @@ -1,8 +1,8 @@ -package termstatus +package terminal import "golang.org/x/sys/unix" -func Tcgetpgrp(ttyfd int) (int, error) { +func tcgetpgrp(ttyfd int) (int, error) { // We need to use IoctlGetUint32 here, because pid_t is 32-bit even on // 64-bit Linux. IoctlGetInt doesn't work on big-endian platforms: // https://github.com/golang/go/issues/45585 diff --git a/internal/ui/termstatus/tcgetpgrp_unix.go b/internal/terminal/tcgetpgrp_unix.go similarity index 64% rename from internal/ui/termstatus/tcgetpgrp_unix.go rename to internal/terminal/tcgetpgrp_unix.go index 1359b5e46..c24bc513f 100644 --- a/internal/ui/termstatus/tcgetpgrp_unix.go +++ b/internal/terminal/tcgetpgrp_unix.go @@ -1,9 +1,9 @@ //go:build unix && !linux -package termstatus +package terminal import "golang.org/x/sys/unix" -func Tcgetpgrp(ttyfd int) (int, error) { +func tcgetpgrp(ttyfd int) (int, error) { return unix.IoctlGetInt(ttyfd, unix.TIOCGPGRP) } diff --git a/internal/ui/termstatus/tcsetpgrp_aix.go b/internal/terminal/tcsetpgrp_aix.go similarity index 80% rename from internal/ui/termstatus/tcsetpgrp_aix.go rename to internal/terminal/tcsetpgrp_aix.go index 8f2a5cab1..f8e8b2972 100644 --- a/internal/ui/termstatus/tcsetpgrp_aix.go +++ b/internal/terminal/tcsetpgrp_aix.go @@ -1,8 +1,8 @@ -package termstatus +package terminal import "golang.org/x/sys/unix" -func Tcsetpgrp(fd int, pid int) error { +func tcsetpgrp(fd int, pid int) error { // The second argument to IoctlSetPointerInt has type int on AIX, // but the constant overflows 64-bit int, hence the two-step cast. req := uint(unix.TIOCSPGRP) diff --git a/internal/ui/termstatus/tcsetpgrp_unix.go b/internal/terminal/tcsetpgrp_unix.go similarity index 66% rename from internal/ui/termstatus/tcsetpgrp_unix.go rename to internal/terminal/tcsetpgrp_unix.go index 7762ee448..4a2c1a12e 100644 --- a/internal/ui/termstatus/tcsetpgrp_unix.go +++ b/internal/terminal/tcsetpgrp_unix.go @@ -1,9 +1,9 @@ //go:build unix && !aix -package termstatus +package terminal import "golang.org/x/sys/unix" -func Tcsetpgrp(fd int, pid int) error { +func tcsetpgrp(fd int, pid int) error { return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pid) } diff --git a/internal/terminal/terminal_posix.go b/internal/terminal/terminal_posix.go new file mode 100644 index 000000000..e8a5abc59 --- /dev/null +++ b/internal/terminal/terminal_posix.go @@ -0,0 +1,39 @@ +package terminal + +import ( + "bytes" + "fmt" + "io" + "os" +) + +const ( + // PosixControlMoveCursorHome moves cursor to the first column + PosixControlMoveCursorHome = "\r" + // PosixControlMoveCursorUp moves cursor up one line + PosixControlMoveCursorUp = "\x1b[1A" + // PosixControlClearLine clears the current line + PosixControlClearLine = "\x1b[2K" +) + +// PosixClearCurrentLine removes all characters from the current line and resets the +// cursor position to the first column. +func PosixClearCurrentLine(wr io.Writer, _ uintptr) { + // clear current line + _, err := wr.Write([]byte(PosixControlMoveCursorHome + PosixControlClearLine)) + if err != nil { + fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + return + } +} + +// PosixMoveCursorUp moves the cursor to the line n lines above the current one. +func PosixMoveCursorUp(wr io.Writer, _ uintptr, n int) { + data := []byte(PosixControlMoveCursorHome) + data = append(data, bytes.Repeat([]byte(PosixControlMoveCursorUp), n)...) + _, err := wr.Write(data) + if err != nil { + fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + return + } +} diff --git a/internal/ui/termstatus/terminal_unix.go b/internal/terminal/terminal_unix.go similarity index 62% rename from internal/ui/termstatus/terminal_unix.go rename to internal/terminal/terminal_unix.go index e112be233..65e353d9f 100644 --- a/internal/ui/termstatus/terminal_unix.go +++ b/internal/terminal/terminal_unix.go @@ -1,7 +1,7 @@ //go:build !windows // +build !windows -package termstatus +package terminal import ( "io" @@ -10,15 +10,15 @@ import ( "golang.org/x/term" ) -// clearCurrentLine removes all characters from the current line and resets the +// ClearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. -func clearCurrentLine(_ uintptr) func(io.Writer, uintptr) { - return posixClearCurrentLine +func ClearCurrentLine(_ uintptr) func(io.Writer, uintptr) { + return PosixClearCurrentLine } -// moveCursorUp moves the cursor to the line n lines above the current one. -func moveCursorUp(_ uintptr) func(io.Writer, uintptr, int) { - return posixMoveCursorUp +// MoveCursorUp moves the cursor to the line n lines above the current one. +func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) { + return PosixMoveCursorUp } // CanUpdateStatus returns true if status lines can be printed, the process diff --git a/internal/ui/termstatus/terminal_windows.go b/internal/terminal/terminal_windows.go similarity index 94% rename from internal/ui/termstatus/terminal_windows.go rename to internal/terminal/terminal_windows.go index 3603f16a3..d68d0197e 100644 --- a/internal/ui/termstatus/terminal_windows.go +++ b/internal/terminal/terminal_windows.go @@ -1,7 +1,7 @@ //go:build windows // +build windows -package termstatus +package terminal import ( "io" @@ -15,25 +15,25 @@ import ( // clearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. -func clearCurrentLine(fd uintptr) func(io.Writer, uintptr) { +func ClearCurrentLine(fd uintptr) func(io.Writer, uintptr) { // easy case, the terminal is cmd or psh, without redirection if isWindowsTerminal(fd) { return windowsClearCurrentLine } // assume we're running in mintty/cygwin - return posixClearCurrentLine + return PosixClearCurrentLine } // moveCursorUp moves the cursor to the line n lines above the current one. -func moveCursorUp(fd uintptr) func(io.Writer, uintptr, int) { +func MoveCursorUp(fd uintptr) func(io.Writer, uintptr, int) { // easy case, the terminal is cmd or psh, without redirection if isWindowsTerminal(fd) { return windowsMoveCursorUp } // assume we're running in mintty/cygwin - return posixMoveCursorUp + return PosixMoveCursorUp } var kernel32 = syscall.NewLazyDLL("kernel32.dll") diff --git a/internal/ui/termstatus/terminal_windows_test.go b/internal/terminal/terminal_windows_test.go similarity index 97% rename from internal/ui/termstatus/terminal_windows_test.go rename to internal/terminal/terminal_windows_test.go index e6eb39dd5..a6b853bae 100644 --- a/internal/ui/termstatus/terminal_windows_test.go +++ b/internal/terminal/terminal_windows_test.go @@ -1,4 +1,4 @@ -package termstatus +package terminal import ( "syscall" diff --git a/internal/ui/termstatus/getpgrp_unix.go b/internal/ui/termstatus/getpgrp_unix.go deleted file mode 100644 index f5a7b213f..000000000 --- a/internal/ui/termstatus/getpgrp_unix.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build unix && !solaris - -package termstatus - -import "golang.org/x/sys/unix" - -func Getpgrp() int { return unix.Getpgrp() } diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index d3674ac09..71be8ec4e 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -10,8 +10,9 @@ import ( "strings" "unicode" - "golang.org/x/term" "golang.org/x/text/width" + + "github.com/restic/restic/internal/terminal" ) // Terminal is used to write messages and display status lines which can be @@ -67,12 +68,12 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal { return t } - if d, ok := wr.(fder); ok && CanUpdateStatus(d.Fd()) { + if d, ok := wr.(fder); ok && terminal.CanUpdateStatus(d.Fd()) { // only use the fancy status code when we're running on a real terminal. t.canUpdateStatus = true t.fd = d.Fd() - t.clearCurrentLine = clearCurrentLine(t.fd) - t.moveCursorUp = moveCursorUp(t.fd) + t.clearCurrentLine = terminal.ClearCurrentLine(t.fd) + t.moveCursorUp = terminal.MoveCursorUp(t.fd) } return t @@ -101,14 +102,14 @@ func (t *Terminal) run(ctx context.Context) { for { select { case <-ctx.Done(): - if !IsProcessBackground(t.fd) { + if !terminal.IsProcessBackground(t.fd) { t.writeStatus([]string{}) } return case msg := <-t.msg: - if IsProcessBackground(t.fd) { + if terminal.IsProcessBackground(t.fd) { // ignore all messages, do nothing, we are in the background process group continue } @@ -140,13 +141,13 @@ func (t *Terminal) run(ctx context.Context) { } case stat := <-t.status: - if IsProcessBackground(t.fd) { + status = append(status[:0], stat.lines...) + + if terminal.IsProcessBackground(t.fd) { // ignore all messages, do nothing, we are in the background process group continue } - status = status[:0] - status = append(status, stat.lines...) t.writeStatus(status) } } @@ -319,9 +320,8 @@ func (t *Terminal) SetStatus(lines []string) { // only truncate interactive status output var width int if t.canUpdateStatus { - var err error - width, _, err = term.GetSize(int(t.fd)) - if err != nil || width <= 0 { + width = terminal.Width(t.fd) + if width <= 0 { // use 80 columns by default width = 80 } diff --git a/internal/ui/termstatus/status_test.go b/internal/ui/termstatus/status_test.go index b4e9be5b0..8e5414686 100644 --- a/internal/ui/termstatus/status_test.go +++ b/internal/ui/termstatus/status_test.go @@ -8,6 +8,7 @@ import ( "strconv" "testing" + "github.com/restic/restic/internal/terminal" rtest "github.com/restic/restic/internal/test" ) @@ -17,16 +18,16 @@ func TestSetStatus(t *testing.T) { term.canUpdateStatus = true term.fd = ^uintptr(0) - term.clearCurrentLine = posixClearCurrentLine - term.moveCursorUp = posixMoveCursorUp + term.clearCurrentLine = terminal.PosixClearCurrentLine + term.moveCursorUp = terminal.PosixMoveCursorUp ctx, cancel := context.WithCancel(context.Background()) go term.Run(ctx) const ( - cl = posixControlClearLine - home = posixControlMoveCursorHome - up = posixControlMoveCursorUp + cl = terminal.PosixControlClearLine + home = terminal.PosixControlMoveCursorHome + up = terminal.PosixControlMoveCursorUp ) term.SetStatus([]string{"first"}) diff --git a/internal/ui/termstatus/terminal_posix.go b/internal/ui/termstatus/terminal_posix.go deleted file mode 100644 index ca5468f45..000000000 --- a/internal/ui/termstatus/terminal_posix.go +++ /dev/null @@ -1,36 +0,0 @@ -package termstatus - -import ( - "bytes" - "fmt" - "io" - "os" -) - -const ( - posixControlMoveCursorHome = "\r" - posixControlMoveCursorUp = "\x1b[1A" - posixControlClearLine = "\x1b[2K" -) - -// posixClearCurrentLine removes all characters from the current line and resets the -// cursor position to the first column. -func posixClearCurrentLine(wr io.Writer, _ uintptr) { - // clear current line - _, err := wr.Write([]byte(posixControlMoveCursorHome + posixControlClearLine)) - if err != nil { - fmt.Fprintf(os.Stderr, "write failed: %v\n", err) - return - } -} - -// posixMoveCursorUp moves the cursor to the line n lines above the current one. -func posixMoveCursorUp(wr io.Writer, _ uintptr, n int) { - data := []byte(posixControlMoveCursorHome) - data = append(data, bytes.Repeat([]byte(posixControlMoveCursorUp), n)...) - _, err := wr.Write(data) - if err != nil { - fmt.Fprintf(os.Stderr, "write failed: %v\n", err) - return - } -}