From cbb8c42e1ff6c54592dc1ff2035af3b72cb3194d Mon Sep 17 00:00:00 2001 From: Laurence Withers Date: Mon, 10 Feb 2020 21:07:43 +0000 Subject: [PATCH] Initial import from github --- LICENSE | 19 +++++++++++++++ README.md | 7 ++++++ example_test.go | 22 +++++++++++++++++ go.mod | 3 +++ stdinprompt.go | 57 +++++++++++++++++++++++++++++++++++++++++++ unit_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 example_test.go create mode 100644 go.mod create mode 100644 stdinprompt.go create mode 100644 unit_test.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c33580 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Laurence Withers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac3cd37 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# src.lwithers.me.uk/go/stdinprompt + +[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/src.lwithers.me.uk/go/stdinprompt) + +This package provides an `io.Reader` that will prompt the user if no data is +available within a short period of time. This can be used to prompt a user that +data is awaited on stdin, for example. diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..9ec95fc --- /dev/null +++ b/example_test.go @@ -0,0 +1,22 @@ +package stdinprompt_test + +import ( + "bytes" + "fmt" + "os" + + "src.lwithers.me.uk/go/stdinprompt" +) + +func ExampleNew() { + q := make([]byte, 4096) + in := stdinprompt.New() + for { + n, err := in.Read(q) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Stdout.Write(bytes.ToUpper(q[:n])) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..39d8ef7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module src.lwithers.me.uk/go/stdinprompt + +go 1.12 diff --git a/stdinprompt.go b/stdinprompt.go new file mode 100644 index 0000000..2eaa44a --- /dev/null +++ b/stdinprompt.go @@ -0,0 +1,57 @@ +/* +Package stdinprompt provides an io.Reader that will prompt the user if nothing +is read within a short period of time. It is useful for programs which default +to reading from stdin if no commandline arguments are provided, as it can +indicate to the user that a program is not actually performing any action until +data arrives. The prompt is not displayed if data becomes available before the +timeout. +*/ +package stdinprompt + +import ( + "fmt" + "io" + "os" + "time" +) + +const ( + // DefaultPromptTime is the timeout period after which, if no data has + // been read, we will display a prompt. + DefaultPromptTime = 250 * time.Millisecond + + // StdinPromptMsg is the default message displayed. + StdinPromptMsg = "Waiting for data on stdin." +) + +// New returns a new prompting reader for stdin. The prompt will be written to +// stderr. +func New() io.Reader { + return NewEx(os.Stdin, DefaultPromptTime, os.Stderr, StdinPromptMsg) +} + +// NewEx returns a new prompting reader. The source may be specified along with +// the prompt message, timeout and destination. +func NewEx(raw io.Reader, when time.Duration, term io.Writer, msg string, +) io.Reader { + pr := &prompter{ + raw: raw, + tmr: time.AfterFunc(when, func() { + fmt.Fprintln(term, msg) + }), + } + return pr +} + +type prompter struct { + raw io.Reader + tmr *time.Timer +} + +func (pr *prompter) Read(buf []byte) (int, error) { + n, err := pr.raw.Read(buf) + if n > 0 { + pr.tmr.Stop() + } + return n, err +} diff --git a/unit_test.go b/unit_test.go new file mode 100644 index 0000000..6fb3607 --- /dev/null +++ b/unit_test.go @@ -0,0 +1,64 @@ +package stdinprompt + +import ( + "bytes" + "strings" + "testing" + "time" +) + +func TestNoPrompt(t *testing.T) { + // set up a buffer (which supports io.Reader) with some test data + exp := []byte{0xa, 0xb, 0xc, 0xd} + in := bytes.NewBuffer(nil) + in.Write(exp) + + // set up a message capture buffer + msgcap := bytes.NewBuffer(nil) + + // set up the prompter + inpr := NewEx(in, DefaultPromptTime, msgcap, StdinPromptMsg) + + // read test data + out := make([]byte, len(exp)) + n, err := inpr.Read(out) + if n != len(exp) { + t.Errorf("read %d bytes, expected %d", n, len(exp)) + } + if err != nil { + t.Errorf("unexpected read error: %v", err) + } + if !bytes.Equal(out, exp) { + t.Errorf("read mismatch: got %X expected %X", out, exp) + } + + // wait for at least the prompt timer + time.Sleep(2 * DefaultPromptTime) + + // verify we didn't get anything in our capture buffer + if msgcap.Len() > 0 { + t.Errorf("got unexpected error %q", msgcap.String()) + } +} + +func TestPrompt(t *testing.T) { + // set up an (unused) input buffer + in := bytes.NewBuffer(nil) + + // set up a message capture buffer + msgcap := bytes.NewBuffer(nil) + + // set up the prompter + _ = NewEx(in, DefaultPromptTime, msgcap, StdinPromptMsg) + + // wait for at least the prompt timer + time.Sleep(2 * DefaultPromptTime) + + // verify that we received the expected message in the capture buffer + switch { + case msgcap.Len() == 0: + t.Errorf("no prompt was received") + case strings.TrimSpace(msgcap.String()) != StdinPromptMsg: + t.Errorf("got unexpected message %q", msgcap.String()) + } +}