~mna/sendkeys

29516cc0d07b63dec684a3ecdb98ef68f55a2bbb — Martin Angers 2 years ago
initial commit
7 files changed, 178 insertions(+), 0 deletions(-)

A .gitignore
A LICENSE
A README.md
A cmd/sendkeys/main.go
A go.mod
A go.sum
A sendkeys.go
A  => .gitignore +12 -0
@@ 1,12 @@
# environment files
.env*

# compiled binary
/out
/sendkeys

# output of various helper commands
*.out

# binaries
/bin/

A  => LICENSE +11 -0
@@ 1,11 @@
Copyright (c) 2020, Martin Angers

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

A  => README.md +20 -0
@@ 1,20 @@
# sendkeys [![GoDoc](https://godoc.org/git.sr.ht/~mna/sendkeys?status.svg)](http://godoc.org/git.sr.ht/~mna/sendkeys) [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/git.sr.ht/~mna/sendkeys)

Package sendkeys provides functions to simulate user input in the terminal by
sending keys to a TTY. Useful especially to test terminal programs that run in
raw mode. See the [package documentation][godoc] for details, API reference and
usage example (alternatively, on [pkg.go.dev][pgd]).

* Canonical repository: https://git.sr.ht/~mna/sendkeys
* Issues: https://todo.sr.ht/~mna/sendkeys

It only works on Unix-like systems. Note that programs using this will require
sudo-like privileges.

## License

The [BSD 3-Clause license][bsd].

[bsd]: http://opensource.org/licenses/BSD-3-Clause
[godoc]: http://godoc.org/git.sr.ht/~mna/sendkeys
[pgd]: https://pkg.go.dev/git.sr.ht/~mna/sendkeys

A  => cmd/sendkeys/main.go +61 -0
@@ 1,61 @@
package main

import (
	"flag"
	"fmt"
	"os"
	"strconv"
	"time"

	"git.sr.ht/~mna/sendkeys"
)

func main() {
	var (
		flagBytes = flag.Bool("-bytes", false, "Send bytes instead of runes.")
		flagTTY   = flag.String("-tty", "", "Path of the target tty.")
		flagDelay = flag.Duration("-delay", 100*time.Millisecond, "Delay between each string to send.")
	)
	flag.Usage = usage
	flag.Parse()

	if flag.NArg() == 0 || *flagTTY == "" {
		flag.Usage()
		return
	}

	args := flag.Args()
	for i, s := range args {
		v, err := strconv.Unquote(`"` + s + `"`)
		if err != nil {
			fmt.Fprintf(os.Stderr, "invalid argument when treated as a Go double-quoted string: %s", s)
			os.Exit(1)
		}
		args[i] = v
	}

	t, err := sendkeys.Open(*flagTTY, *flagDelay)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	defer t.Close()

	if *flagBytes {
		_, err = t.SendBytes(args...)
	} else {
		_, err = t.SendRunes(args...)
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func usage() {
	const msg = `usage: sendkeys -tty PATH [-delay DUR] [-bytes] STRING...
Each STRING is treated as if it was a Go double-quoted string, so that
e.g. "\x1b" is treated as an escape sequence.
`
	fmt.Println(msg)
}

A  => go.mod +5 -0
@@ 1,5 @@
module git.sr.ht/~mna/sendkeys

go 1.14

require golang.org/x/sys v0.0.0-20200413165638-669c56c373c4

A  => go.sum +2 -0
@@ 1,2 @@
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

A  => sendkeys.go +67 -0
@@ 1,67 @@
// Package sendkeys simulates user input in a terminal.
package sendkeys

import (
	"os"
	"time"

	"golang.org/x/sys/unix"
)

// Target represents the tty to which the keys are sent. Create one by calling
// Open and send keys using Target.SendRunes or Target.SendBytes. The Target
// must be closed after use.
type Target struct {
	f *os.File
	d time.Duration
}

// Open returns a Target that sends keys to the specified TTY. The delay is the
// time to wait before sending each separate string when SendRunes or SendBytes
// is called.
func Open(ttyPath string, delay time.Duration) (*Target, error) {
	f, err := os.Open(ttyPath)
	if err != nil {
		return nil, err
	}
	return &Target{f: f, d: delay}, nil
}

// Close releases the resources used by the Target.
func (t *Target) Close() error {
	return t.f.Close()
}

// SendRunes sends each rune of each string the the Target. All runes of a
// given string are sent without delay, and the target's delay is applied
// between each string.
func (t *Target) SendRunes(strs ...string) (int, error) {
	var n int
	for _, str := range strs {
		for _, r := range str {
			if err := unix.IoctlSetPointerInt(int(t.f.Fd()), unix.TIOCSTI, int(r)); err != nil {
				return n, err
			}
			n++
		}
		time.Sleep(t.d)
	}
	return n, nil
}

// SendBytes is like SendRunes except it sends each byte separately, instead if
// each rune.
func (t *Target) SendBytes(strs ...string) (int, error) {
	var n int
	for _, str := range strs {
		for i := 0; i < len(str); i++ {
			v := int(str[i])
			if err := unix.IoctlSetPointerInt(int(t.f.Fd()), unix.TIOCSTI, v); err != nil {
				return n, err
			}
			n++
		}
		time.Sleep(t.d)
	}
	return n, nil
}