kopia lustrzana https://github.com/abourget/shuttle-go
Removed dependency on uinput, we're using `xdotool` right now.
rodzic
4adc1c931d
commit
059cdb49ec
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash -xe
|
||||
|
||||
#CGO_ENABLED=0
|
||||
GOOS=linux GOARCH=amd64 go build -v -o shuttle-go
|
10
main.go
10
main.go
|
@ -3,11 +3,9 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bendahl/uinput"
|
||||
"github.com/gvalkov/golang-evdev"
|
||||
)
|
||||
|
||||
|
@ -49,12 +47,6 @@ func main() {
|
|||
|
||||
go watcher.Run()
|
||||
|
||||
// Virtual keyboard
|
||||
vk, err := uinput.CreateKeyboard("/dev/uinput", []byte("Go Virtual Shuttle Pro V2"))
|
||||
if err != nil {
|
||||
log.Println("Can't open dev:", err)
|
||||
}
|
||||
|
||||
// Shuttle device event receiver
|
||||
dev, err := evdev.Open(devicePath)
|
||||
if err != nil {
|
||||
|
@ -63,7 +55,7 @@ func main() {
|
|||
}
|
||||
|
||||
fmt.Println("ready")
|
||||
mapper := NewMapper(vk, dev)
|
||||
mapper := NewMapper(dev)
|
||||
mapper.watcher = watcher
|
||||
for {
|
||||
if err := mapper.Process(); err != nil {
|
||||
|
|
13
mapper.go
13
mapper.go
|
@ -7,17 +7,15 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bendahl/uinput"
|
||||
evdev "github.com/gvalkov/golang-evdev"
|
||||
)
|
||||
|
||||
// Mapper receives events from the Shuttle devices, and maps (through
|
||||
// configuration) to the Virtual Keyboard events.
|
||||
type Mapper struct {
|
||||
virtualKeyboard uinput.Keyboard
|
||||
inputDevice *evdev.InputDevice
|
||||
state buttonsState
|
||||
watcher *watcher
|
||||
inputDevice *evdev.InputDevice
|
||||
state buttonsState
|
||||
watcher *watcher
|
||||
}
|
||||
|
||||
type buttonsState struct {
|
||||
|
@ -27,10 +25,9 @@ type buttonsState struct {
|
|||
lastJog time.Time
|
||||
}
|
||||
|
||||
func NewMapper(virtualKeyboard uinput.Keyboard, inputDevice *evdev.InputDevice) *Mapper {
|
||||
func NewMapper(inputDevice *evdev.InputDevice) *Mapper {
|
||||
m := &Mapper{
|
||||
virtualKeyboard: virtualKeyboard,
|
||||
inputDevice: inputDevice,
|
||||
inputDevice: inputDevice,
|
||||
}
|
||||
m.state.buttonsHeld = make(map[int]bool)
|
||||
m.state.jog = -1
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Benjamin Dahlmanns
|
||||
|
||||
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.
|
|
@ -1,41 +0,0 @@
|
|||
Uinput [![Build Status](https://travis-ci.org/bendahl/uinput.svg?branch=master)](https://travis-ci.org/bendahl/uinput) [![GoDoc](https://godoc.org/github.com/bendahl/uinput?status.png)](https://godoc.org/github.com/bendahl/uinput) [![Go Report Card](https://goreportcard.com/badge/github.com/bendahl/uinput)](https://goreportcard.com/report/github.com/bendahl/uinput)
|
||||
====
|
||||
|
||||
This package provides pure go wrapper functions for the LINUX uinput device, which allows to create virtual input devices
|
||||
in userspace. At the moment this package offers a virtual keyboard implementation as well as a virtual mouse device and
|
||||
a touch pad device.
|
||||
The keyboard can be used to either send single key presses or hold down a specified key and release it later
|
||||
(useful for building game controllers). The mouse device issues relative positional change events to the x and y axis
|
||||
of the mouse pointer and may also fire click events (left and right click). More functionality will be added in future
|
||||
version.
|
||||
The touch pad, on the other hand can be used to move the mouse cursor to the specified position on the screen and to
|
||||
issue left and right clicks. Note that you'll need to specify the region size of your screen first though (happens during
|
||||
device creation).
|
||||
|
||||
Please note that you will need to make sure to have the necessary rights to write to uinput. You can either chmod your
|
||||
uinput device, or add a rule in /etc/udev/rules.d to allow your user's group or a dedicated group to write to the device.
|
||||
You may use the following two commands to add the necessary rights for you current user to a file called 99-$USER.rules
|
||||
(where $USER is your current user's name):
|
||||
<pre><code>
|
||||
echo KERNEL==\"uinput\", GROUP=\"$USER\", MODE:=\"0660\" | sudo tee /etc/udev/rules.d/99-$USER.rules
|
||||
sudo udevadm trigger
|
||||
</code></pre>
|
||||
|
||||
Installation
|
||||
-------------
|
||||
Simply check out the repository and use the commands <pre><code>go build && go install</code></pre>
|
||||
The package will then be installed to your local respository, along with the package documentation.
|
||||
The documentation contains more details on the usage of this package.
|
||||
|
||||
License
|
||||
--------
|
||||
The package falls under the MIT license. Please see the "LICENSE" file for details.
|
||||
|
||||
ToDos
|
||||
------------------
|
||||
All testing has been done on Ubunu 14.04 and 16.04 x86\_64.
|
||||
Testing for other platforms will need to be done.
|
||||
To get an idea of the things that are on the current todo list, check out the file "TODO.md".
|
||||
As always, helpful comments and ideas are always welcome.
|
||||
Feel free to do some testing on your own if you're up to it.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
TODO
|
||||
====
|
||||
|
||||
1. ~~Create Tests for the uinput package~~
|
||||
2. ~~Migrate code from C to GO~~
|
||||
3. ~~Implement relative input~~
|
||||
4. ~~Implement absolute input~~
|
||||
5. Test on different platforms (besides x86_64)
|
||||
6. Implement functions to allow mouse button up and down events (for region selects)
|
||||
7. Extend test cases
|
|
@ -1,375 +0,0 @@
|
|||
/*
|
||||
Package uinput is a pure go package that provides access to the userland input device driver uinput on linux systems.
|
||||
Virtual keyboard devices as well as virtual mouse input devices may be created using this package.
|
||||
The keycodes and other event definitions, that are available and can be used to trigger input events,
|
||||
are part of this package ("Key1" for number 1, for example).
|
||||
|
||||
In order to use the virtual keyboard, you will need to follow these three steps:
|
||||
|
||||
1. Initialize the device
|
||||
Example: vk, err := CreateKeyboard("/dev/uinput", "Virtual Keyboard")
|
||||
|
||||
2. Send Button events to the device
|
||||
Example (print a single D):
|
||||
err = vk.KeyPress(uinput.KeyD)
|
||||
|
||||
Example (keep moving right by holding down right arrow key):
|
||||
err = vk.KeyDown(uinput.KeyRight)
|
||||
|
||||
Example (stop moving right by releasing the right arrow key):
|
||||
err = vk.KeyUp(uinput.KeyRight)
|
||||
|
||||
3. Close the device
|
||||
Example: err = vk.Close()
|
||||
|
||||
A virtual mouse input device is just as easy to create and use:
|
||||
|
||||
1. Initialize the device:
|
||||
Example: vm, err := CreateMouse("/dev/uinput", "DangerMouse")
|
||||
|
||||
2. Move the cursor around and issue click events
|
||||
Example (move mouse right):
|
||||
err = vm.MoveRight(42)
|
||||
|
||||
Example (move mouse left):
|
||||
err = vm.MoveLeft(42)
|
||||
|
||||
Example (move mouse up):
|
||||
err = vm.MoveUp(42)
|
||||
|
||||
Example (move mouse down):
|
||||
err = vm.MoveDown(42)
|
||||
|
||||
Example (trigger a left click):
|
||||
err = vm.LeftClick()
|
||||
|
||||
Example (trigger a right click):
|
||||
err = vm.RightClick()
|
||||
|
||||
3. Close the device
|
||||
Example: err = vm.Close()
|
||||
|
||||
|
||||
If you'd like to use absolute input events (move the cursor to specific positions on screen), use the touch pad.
|
||||
Note that you'll need to specify the size of the screen area you want to use when you initialize the
|
||||
device. Here are a few examples of how to use the virtual touch pad:
|
||||
|
||||
1. Initialize the device:
|
||||
Example: vt, err := CreateTouchPad("/dev/uinput", "DontTouchThis", 0, 1024, 0, 768)
|
||||
|
||||
2. Move the cursor around and issue click events
|
||||
Example (move cursor to the top left corner of the screen):
|
||||
err = vt.MoveTo(0, 0)
|
||||
|
||||
Example (move cursor to the position x: 100, y: 250):
|
||||
err = vt.MoveTo(100, 250)
|
||||
|
||||
Example (trigger a left click):
|
||||
err = vt.LeftClick()
|
||||
|
||||
Example (trigger a right click):
|
||||
err = vt.RightClick()
|
||||
|
||||
3. Close the device
|
||||
Example: err = vt.Close()
|
||||
|
||||
*/
|
||||
package uinput
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// A Keyboard is an key event output device. It is used to
|
||||
// enable a program to simulate HID keyboard input events.
|
||||
type Keyboard interface {
|
||||
// KeyPress will cause the key to be pressed and immediately released.
|
||||
KeyPress(key int) error
|
||||
|
||||
// KeyDown will send a keypress event to an existing keyboard device.
|
||||
// The key can be any of the predefined keycodes from uinputdefs.
|
||||
// Note that the key will be "held down" until "KeyUp" is called.
|
||||
KeyDown(key int) error
|
||||
|
||||
// KeyUp will send a keyrelease event to an existing keyboard device.
|
||||
// The key can be any of the predefined keycodes from uinputdefs.
|
||||
KeyUp(key int) error
|
||||
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type vKeyboard struct {
|
||||
name []byte
|
||||
deviceFile *os.File
|
||||
}
|
||||
|
||||
// A Mouse is a device that will trigger an absolute change event.
|
||||
// For details see: https://www.kernel.org/doc/Documentation/input/event-codes.txt
|
||||
type Mouse interface {
|
||||
// MoveLeft will move the mouse cursor left by the given number of pixel.
|
||||
MoveLeft(pixel int32) error
|
||||
|
||||
// MoveRight will move the mouse cursor right by the given number of pixel.
|
||||
MoveRight(pixel int32) error
|
||||
|
||||
// MoveUp will move the mouse cursor up by the given number of pixel.
|
||||
MoveUp(pixel int32) error
|
||||
|
||||
// MoveDown will move the mouse cursor down by the given number of pixel.
|
||||
MoveDown(pixel int32) error
|
||||
|
||||
// LeftClick will issue a single left click.
|
||||
LeftClick() error
|
||||
|
||||
// RightClick will issue a right click.
|
||||
RightClick() error
|
||||
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type vMouse struct {
|
||||
name []byte
|
||||
deviceFile *os.File
|
||||
}
|
||||
|
||||
// A TouchPad is an input device that uses absolute axis events, meaning that you can specify
|
||||
// the exact position the cursor should move to. Therefore, it is necessary to define the size
|
||||
// of the rectangle in which the cursor may move upon creation of the device.
|
||||
type TouchPad interface {
|
||||
// MoveTo will move the cursor to the specified position on the screen
|
||||
MoveTo(x int32, y int32) error
|
||||
|
||||
// LeftClick will issue a single left click.
|
||||
LeftClick() error
|
||||
|
||||
// RightClick will issue a right click.
|
||||
RightClick() error
|
||||
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type vTouchPad struct {
|
||||
name []byte
|
||||
deviceFile *os.File
|
||||
}
|
||||
|
||||
// CreateTouchPad will create a new touch pad device. note that you will need to define the x and y axis boundaries
|
||||
// (min and max) within which the cursor maybe moved around.
|
||||
func CreateTouchPad(path string, name []byte, minX int32, maxX int32, minY int32, maxY int32) (TouchPad, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("device path must not be empty")
|
||||
}
|
||||
if len(name) > uinputMaxNameSize {
|
||||
return nil, fmt.Errorf("device name %s is too long (maximum of %d characters allowed)", name, uinputMaxNameSize)
|
||||
}
|
||||
|
||||
fd, err := createTouchPad(path, name, minX, maxX, minY, maxY)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vTouchPad{name: name, deviceFile: fd}, nil
|
||||
}
|
||||
|
||||
func (vTouch vTouchPad) MoveTo(x int32, y int32) error {
|
||||
return sendAbsEvent(vTouch.deviceFile, x, y)
|
||||
}
|
||||
|
||||
func (vTouch vTouchPad) LeftClick() error {
|
||||
err := sendBtnEvent(vTouch.deviceFile, evBtnLeft, btnStatePressed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the LeftClick event: %v", err)
|
||||
}
|
||||
|
||||
err = sendBtnEvent(vTouch.deviceFile, evBtnLeft, btnStateReleased)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the KeyUp event: %v", err)
|
||||
}
|
||||
|
||||
err = syncEvents(vTouch.deviceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync to device file failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vTouch vTouchPad) RightClick() error {
|
||||
err := sendBtnEvent(vTouch.deviceFile, evBtnRight, btnStatePressed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the RightClick event: %v", err)
|
||||
}
|
||||
|
||||
err = sendBtnEvent(vTouch.deviceFile, evBtnRight, btnStateReleased)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the KeyUp event: %v", err)
|
||||
}
|
||||
|
||||
err = syncEvents(vTouch.deviceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync to device file failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vTouch vTouchPad) Close() error {
|
||||
return closeDevice(vTouch.deviceFile)
|
||||
}
|
||||
|
||||
// CreateMouse will create a new mouse input device. A mouse is a device that allows relative input.
|
||||
// Relative input means that all changes to the x and y coordinates of the mouse pointer will be
|
||||
func CreateMouse(path string, name []byte) (Mouse, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("device path must not be empty")
|
||||
}
|
||||
if len(name) > uinputMaxNameSize {
|
||||
return nil, fmt.Errorf("device name %s is too long (maximum of %d characters allowed)", name, uinputMaxNameSize)
|
||||
}
|
||||
|
||||
fd, err := createMouse(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vMouse{name: name, deviceFile: fd}, nil
|
||||
}
|
||||
|
||||
// MoveLeft will move the cursor left by the number of pixel specified.
|
||||
func (vRel vMouse) MoveLeft(pixel int32) error {
|
||||
return sendRelEvent(vRel.deviceFile, relX, -pixel)
|
||||
}
|
||||
|
||||
// MoveRight will move the cursor right by the number of pixel specified.
|
||||
func (vRel vMouse) MoveRight(pixel int32) error {
|
||||
return sendRelEvent(vRel.deviceFile, relX, pixel)
|
||||
}
|
||||
|
||||
// MoveUp will move the cursor up by the number of pixel specified.
|
||||
func (vRel vMouse) MoveUp(pixel int32) error {
|
||||
return sendRelEvent(vRel.deviceFile, relY, -pixel)
|
||||
}
|
||||
|
||||
// MoveDown will move the cursor down by the number of pixel specified.
|
||||
func (vRel vMouse) MoveDown(pixel int32) error {
|
||||
return sendRelEvent(vRel.deviceFile, relY, pixel)
|
||||
}
|
||||
|
||||
// LeftClick will issue a LeftClick.
|
||||
func (vRel vMouse) LeftClick() error {
|
||||
err := sendBtnEvent(vRel.deviceFile, evBtnLeft, btnStatePressed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the LeftClick event: %v", err)
|
||||
}
|
||||
|
||||
err = sendBtnEvent(vRel.deviceFile, evBtnLeft, btnStateReleased)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the KeyUp event: %v", err)
|
||||
}
|
||||
|
||||
err = syncEvents(vRel.deviceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync to device file failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RightClick will issue a RightClick
|
||||
func (vRel vMouse) RightClick() error {
|
||||
err := sendBtnEvent(vRel.deviceFile, evBtnRight, btnStatePressed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the RightClick event: %v", err)
|
||||
}
|
||||
|
||||
err = sendBtnEvent(vRel.deviceFile, evBtnRight, btnStateReleased)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the KeyUp event: %v", err)
|
||||
}
|
||||
|
||||
err = syncEvents(vRel.deviceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync to device file failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the device and releases the device.
|
||||
func (vRel vMouse) Close() error {
|
||||
return closeDevice(vRel.deviceFile)
|
||||
}
|
||||
|
||||
// CreateKeyboard will create a new keyboard using the given uinput
|
||||
// device path of the uinput device.
|
||||
func CreateKeyboard(path string, name []byte) (Keyboard, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("device path must not be empty")
|
||||
}
|
||||
if len(name) > uinputMaxNameSize {
|
||||
return nil, fmt.Errorf("device name %s is too long (maximum of %d characters allowed)", name, uinputMaxNameSize)
|
||||
}
|
||||
|
||||
fd, err := createVKeyboardDevice(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vKeyboard{name: name, deviceFile: fd}, nil
|
||||
}
|
||||
|
||||
// KeyPress will issue a single key press (push down a key and then immediately release it).
|
||||
func (vk vKeyboard) KeyPress(key int) error {
|
||||
err := sendBtnEvent(vk.deviceFile, key, btnStatePressed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the KeyDown event: %v", err)
|
||||
}
|
||||
|
||||
err = sendBtnEvent(vk.deviceFile, key, btnStateReleased)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the KeyUp event: %v", err)
|
||||
}
|
||||
|
||||
err = syncEvents(vk.deviceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync to device file failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyDown will send the key code passed (see uinputdefs.go for available keycodes). Note that unless a key release
|
||||
// event is sent to the device, the key will remain pressed and therefore input will continuously be generated. Therefore,
|
||||
// do not forget to call "KeyUp" afterwards.
|
||||
func (vk vKeyboard) KeyDown(key int) error {
|
||||
err := sendBtnEvent(vk.deviceFile, key, btnStatePressed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the KeyDown event: %v", err)
|
||||
}
|
||||
|
||||
err = syncEvents(vk.deviceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync to device file failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyUp will release the given key passed as a parameter (see uinputdefs.go for available keycodes). In most
|
||||
// cases it is recommended to call this function immediately after the "KeyDown" function in order to only issue a
|
||||
// single key press.
|
||||
func (vk vKeyboard) KeyUp(key int) error {
|
||||
err := sendBtnEvent(vk.deviceFile, key, btnStatePressed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to issue the KeyUp event: %v", err)
|
||||
}
|
||||
|
||||
err = syncEvents(vk.deviceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync to device file failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close will close the device and free resources.
|
||||
// It's usually a good idea to use defer to call this function.
|
||||
func (vk vKeyboard) Close() error {
|
||||
return closeDevice(vk.deviceFile)
|
||||
}
|
|
@ -1,249 +0,0 @@
|
|||
package uinput
|
||||
|
||||
// the constants were copied from input.h for convenience reasons
|
||||
const (
|
||||
keyReserved = 0
|
||||
KeyEsc = 1
|
||||
Key1 = 2
|
||||
Key2 = 3
|
||||
Key3 = 4
|
||||
Key4 = 5
|
||||
Key5 = 6
|
||||
Key6 = 7
|
||||
Key7 = 8
|
||||
Key8 = 9
|
||||
Key9 = 10
|
||||
Key0 = 11
|
||||
KeyMinus = 12
|
||||
KeyEqual = 13
|
||||
KeyBackspace = 14
|
||||
KeyTab = 15
|
||||
KeyQ = 16
|
||||
KeyW = 17
|
||||
KeyE = 18
|
||||
KeyR = 19
|
||||
KeyT = 20
|
||||
KeyY = 21
|
||||
KeyU = 22
|
||||
KeyI = 23
|
||||
KeyO = 24
|
||||
KeyP = 25
|
||||
KeyLeftbrace = 26
|
||||
KeyRightbrace = 27
|
||||
KeyEnter = 28
|
||||
KeyLeftctrl = 29
|
||||
KeyA = 30
|
||||
KeyS = 31
|
||||
KeyD = 32
|
||||
KeyF = 33
|
||||
KeyG = 34
|
||||
KeyH = 35
|
||||
KeyJ = 36
|
||||
KeyK = 37
|
||||
KeyL = 38
|
||||
KeySemicolon = 39
|
||||
KeyApostrophe = 40
|
||||
KeyGrave = 41
|
||||
KeyLeftshift = 42
|
||||
KeyBackslash = 43
|
||||
KeyZ = 44
|
||||
KeyX = 45
|
||||
KeyC = 46
|
||||
KeyV = 47
|
||||
KeyB = 48
|
||||
KeyN = 49
|
||||
KeyM = 50
|
||||
KeyComma = 51
|
||||
KeyDot = 52
|
||||
KeySlash = 53
|
||||
KeyRightshift = 54
|
||||
KeyKpasterisk = 55
|
||||
KeyLeftalt = 56
|
||||
KeySpace = 57
|
||||
KeyCapslock = 58
|
||||
KeyF1 = 59
|
||||
KeyF2 = 60
|
||||
KeyF3 = 61
|
||||
KeyF4 = 62
|
||||
KeyF5 = 63
|
||||
KeyF6 = 64
|
||||
KeyF7 = 65
|
||||
KeyF8 = 66
|
||||
KeyF9 = 67
|
||||
KeyF10 = 68
|
||||
KeyNumlock = 69
|
||||
KeyScrolllock = 70
|
||||
KeyKp7 = 71
|
||||
KeyKp8 = 72
|
||||
KeyKp9 = 73
|
||||
KeyKpminus = 74
|
||||
KeyKp4 = 75
|
||||
KeyKp5 = 76
|
||||
KeyKp6 = 77
|
||||
KeyKpplus = 78
|
||||
KeyKp1 = 79
|
||||
KeyKp2 = 80
|
||||
KeyKp3 = 81
|
||||
KeyKp0 = 82
|
||||
KeyKpdot = 83
|
||||
KeyZenkakuhankaku = 85
|
||||
Key102Nd = 86
|
||||
KeyF11 = 87
|
||||
KeyF12 = 88
|
||||
KeyRo = 89
|
||||
KeyKatakana = 90
|
||||
KeyHiragana = 91
|
||||
KeyHenkan = 92
|
||||
KeyKatakanahiragana = 93
|
||||
KeyMuhenkan = 94
|
||||
KeyKpjpcomma = 95
|
||||
KeyKpenter = 96
|
||||
KeyRightctrl = 97
|
||||
KeyKpslash = 98
|
||||
KeySysrq = 99
|
||||
KeyRightalt = 100
|
||||
KeyLinefeed = 101
|
||||
KeyHome = 102
|
||||
KeyUp = 103
|
||||
KeyPageup = 104
|
||||
KeyLeft = 105
|
||||
KeyRight = 106
|
||||
KeyEnd = 107
|
||||
KeyDown = 108
|
||||
KeyPagedown = 109
|
||||
KeyInsert = 110
|
||||
KeyDelete = 111
|
||||
KeyMacro = 112
|
||||
KeyMute = 113
|
||||
KeyVolumedown = 114
|
||||
KeyVolumeup = 115
|
||||
KeyPower = 116 /*ScSystemPowerDown*/
|
||||
KeyKpequal = 117
|
||||
KeyKpplusminus = 118
|
||||
KeyPause = 119
|
||||
KeyScale = 120 /*AlCompizScale(Expose)*/
|
||||
KeyKpcomma = 121
|
||||
KeyHangeul = 122
|
||||
KeyHanja = 123
|
||||
KeyYen = 124
|
||||
KeyLeftmeta = 125
|
||||
KeyRightmeta = 126
|
||||
KeyCompose = 127
|
||||
KeyStop = 128 /*AcStop*/
|
||||
KeyAgain = 129
|
||||
KeyProps = 130 /*AcProperties*/
|
||||
KeyUndo = 131 /*AcUndo*/
|
||||
KeyFront = 132
|
||||
KeyCopy = 133 /*AcCopy*/
|
||||
KeyOpen = 134 /*AcOpen*/
|
||||
KeyPaste = 135 /*AcPaste*/
|
||||
KeyFind = 136 /*AcSearch*/
|
||||
KeyCut = 137 /*AcCut*/
|
||||
KeyHelp = 138 /*AlIntegratedHelpCenter*/
|
||||
KeyMenu = 139 /*Menu(ShowMenu)*/
|
||||
KeyCalc = 140 /*AlCalculator*/
|
||||
KeySetup = 141
|
||||
KeySleep = 142 /*ScSystemSleep*/
|
||||
KeyWakeup = 143 /*SystemWakeUp*/
|
||||
KeyFile = 144 /*AlLocalMachineBrowser*/
|
||||
KeySendfile = 145
|
||||
KeyDeletefile = 146
|
||||
KeyXfer = 147
|
||||
KeyProg1 = 148
|
||||
KeyProg2 = 149
|
||||
KeyWww = 150 /*AlInternetBrowser*/
|
||||
KeyMsdos = 151
|
||||
KeyCoffee = 152 /*AlTerminalLock/Screensaver*/
|
||||
KeyDirection = 153
|
||||
KeyCyclewindows = 154
|
||||
KeyMail = 155
|
||||
KeyBookmarks = 156 /*AcBookmarks*/
|
||||
KeyComputer = 157
|
||||
KeyBack = 158 /*AcBack*/
|
||||
KeyForward = 159 /*AcForward*/
|
||||
KeyClosecd = 160
|
||||
KeyEjectcd = 161
|
||||
KeyEjectclosecd = 162
|
||||
KeyNextsong = 163
|
||||
KeyPlaypause = 164
|
||||
KeyPrevioussong = 165
|
||||
KeyStopcd = 166
|
||||
KeyRecord = 167
|
||||
KeyRewind = 168
|
||||
KeyPhone = 169 /*MediaSelectTelephone*/
|
||||
KeyIso = 170
|
||||
KeyConfig = 171 /*AlConsumerControlConfiguration*/
|
||||
KeyHomepage = 172 /*AcHome*/
|
||||
KeyRefresh = 173 /*AcRefresh*/
|
||||
KeyExit = 174 /*AcExit*/
|
||||
KeyMove = 175
|
||||
KeyEdit = 176
|
||||
KeyScrollup = 177
|
||||
KeyScrolldown = 178
|
||||
KeyKpleftparen = 179
|
||||
KeyKprightparen = 180
|
||||
KeyNew = 181 /*AcNew*/
|
||||
KeyRedo = 182 /*AcRedo/Repeat*/
|
||||
KeyF13 = 183
|
||||
KeyF14 = 184
|
||||
KeyF15 = 185
|
||||
KeyF16 = 186
|
||||
KeyF17 = 187
|
||||
KeyF18 = 188
|
||||
KeyF19 = 189
|
||||
KeyF20 = 190
|
||||
KeyF21 = 191
|
||||
KeyF22 = 192
|
||||
KeyF23 = 193
|
||||
KeyF24 = 194
|
||||
KeyPlaycd = 200
|
||||
KeyPausecd = 201
|
||||
KeyProg3 = 202
|
||||
KeyProg4 = 203
|
||||
KeyDashboard = 204 /*AlDashboard*/
|
||||
KeySuspend = 205
|
||||
KeyClose = 206 /*AcClose*/
|
||||
KeyPlay = 207
|
||||
KeyFastforward = 208
|
||||
KeyBassboost = 209
|
||||
KeyPrint = 210 /*AcPrint*/
|
||||
KeyHp = 211
|
||||
KeyCamera = 212
|
||||
KeySound = 213
|
||||
KeyQuestion = 214
|
||||
KeyEmail = 215
|
||||
KeyChat = 216
|
||||
KeySearch = 217
|
||||
KeyConnect = 218
|
||||
KeyFinance = 219 /*AlCheckbook/Finance*/
|
||||
KeySport = 220
|
||||
KeyShop = 221
|
||||
KeyAlterase = 222
|
||||
KeyCancel = 223 /*AcCancel*/
|
||||
KeyBrightnessdown = 224
|
||||
KeyBrightnessup = 225
|
||||
KeyMedia = 226
|
||||
KeySwitchvideomode = 227 /*CycleBetweenAvailableVideo */
|
||||
KeyKbdillumtoggle = 228
|
||||
KeyKbdillumdown = 229
|
||||
KeyKbdillumup = 230
|
||||
KeySend = 231 /*AcSend*/
|
||||
KeyReply = 232 /*AcReply*/
|
||||
KeyForwardmail = 233 /*AcForwardMsg*/
|
||||
KeySave = 234 /*AcSave*/
|
||||
KeyDocuments = 235
|
||||
KeyBattery = 236
|
||||
KeyBluetooth = 237
|
||||
KeyWlan = 238
|
||||
KeyUwb = 239
|
||||
KeyUnknown = 240
|
||||
KeyVideoNext = 241 /*DriveNextVideoSource*/
|
||||
KeyVideoPrev = 242 /*DrivePreviousVideoSource*/
|
||||
KeyBrightnessCycle = 243 /*BrightnessUp,AfterMaxIsMin*/
|
||||
KeyBrightnessZero = 244 /*BrightnessOff,UseAmbient*/
|
||||
KeyDisplayOff = 245 /*DisplayDeviceToOffState*/
|
||||
KeyWimax = 246
|
||||
KeyRfkill = 247 /*KeyThatControlsAllRadios*/
|
||||
KeyMicmute = 248 /*Mute/UnmuteTheMicrophone*/
|
||||
keyMax = 248 // highest key currently defined
|
||||
)
|
|
@ -1,369 +0,0 @@
|
|||
package uinput
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// types needed from uinput.h
|
||||
const (
|
||||
uinputMaxNameSize = 80
|
||||
uiDevCreate = 0x5501
|
||||
uiDevDestroy = 0x5502
|
||||
uiSetEvBit = 0x40045564
|
||||
uiSetKeyBit = 0x40045565
|
||||
uiSetRelBit = 0x40045566
|
||||
uiSetAbsBit = 0x40045567
|
||||
busUsb = 0x03
|
||||
)
|
||||
|
||||
// input event codes as specified in input-event-codes.h
|
||||
const (
|
||||
evSyn = 0x00
|
||||
evKey = 0x01
|
||||
evRel = 0x02
|
||||
evAbs = 0x03
|
||||
relX = 0x0
|
||||
relY = 0x1
|
||||
absX = 0x0
|
||||
absY = 0x1
|
||||
synReport = 0
|
||||
evBtnLeft = 0x110
|
||||
evBtnRight = 0x111
|
||||
)
|
||||
|
||||
const (
|
||||
btnStateReleased = 0
|
||||
btnStatePressed = 1
|
||||
absSize = 64
|
||||
)
|
||||
|
||||
type inputID struct {
|
||||
Bustype uint16
|
||||
Vendor uint16
|
||||
Product uint16
|
||||
Version uint16
|
||||
}
|
||||
|
||||
// translated to go from uinput.h
|
||||
type uinputUserDev struct {
|
||||
Name [uinputMaxNameSize]byte
|
||||
ID inputID
|
||||
EffectsMax uint32
|
||||
Absmax [absSize]int32
|
||||
Absmin [absSize]int32
|
||||
Absfuzz [absSize]int32
|
||||
Absflat [absSize]int32
|
||||
}
|
||||
|
||||
// translated to go from input.h
|
||||
type inputEvent struct {
|
||||
Time syscall.Timeval
|
||||
Type uint16
|
||||
Code uint16
|
||||
Value int32
|
||||
}
|
||||
|
||||
func closeDevice(deviceFile *os.File) (err error) {
|
||||
err = releaseDevice(deviceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to close device: %v", err)
|
||||
}
|
||||
return deviceFile.Close()
|
||||
}
|
||||
|
||||
func releaseDevice(deviceFile *os.File) (err error) {
|
||||
return ioctl(deviceFile, uiDevDestroy, uintptr(0))
|
||||
}
|
||||
|
||||
func createDeviceFile(path string) (fd *os.File, err error) {
|
||||
deviceFile, err := os.OpenFile(path, syscall.O_WRONLY|syscall.O_NONBLOCK, 0660)
|
||||
if err != nil {
|
||||
return nil, errors.New("could not open device file")
|
||||
}
|
||||
return deviceFile, err
|
||||
}
|
||||
|
||||
func registerDevice(deviceFile *os.File, evType uintptr) error {
|
||||
err := ioctl(deviceFile, uiSetEvBit, evType)
|
||||
if err != nil {
|
||||
err = releaseDevice(deviceFile)
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return fmt.Errorf("failed to close device: %v", err)
|
||||
}
|
||||
deviceFile.Close()
|
||||
return fmt.Errorf("invalid file handle returned from ioctl: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createVKeyboardDevice(path string, name []byte) (fd *os.File, err error) {
|
||||
deviceFile, err := createDeviceFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create virtual keyboard device: %v", err)
|
||||
}
|
||||
|
||||
err = registerDevice(deviceFile, uintptr(evKey))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register virtual keyboard device: %v", err)
|
||||
}
|
||||
|
||||
// register key events
|
||||
for i := 0; i < keyMax; i++ {
|
||||
err = ioctl(deviceFile, uiSetKeyBit, uintptr(i))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register key number %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return createUsbDevice(deviceFile,
|
||||
uinputUserDev{
|
||||
Name: toUinputName(name),
|
||||
ID: inputID{
|
||||
Bustype: busUsb,
|
||||
Vendor: 0x4711,
|
||||
Product: 0x0815,
|
||||
Version: 1}})
|
||||
}
|
||||
|
||||
func toUinputName(name []byte) (uinputName [uinputMaxNameSize]byte) {
|
||||
var fixedSizeName [uinputMaxNameSize]byte
|
||||
copy(fixedSizeName[:], name)
|
||||
return fixedSizeName
|
||||
}
|
||||
|
||||
func createTouchPad(path string, name []byte, minX int32, maxX int32, minY int32, maxY int32) (fd *os.File, err error) {
|
||||
deviceFile, err := createDeviceFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create absolute axis input device: %v", err)
|
||||
}
|
||||
|
||||
err = registerDevice(deviceFile, uintptr(evKey))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register key device: %v", err)
|
||||
}
|
||||
// register button events (in order to enable left and right click)
|
||||
err = ioctl(deviceFile, uiSetKeyBit, uintptr(evBtnLeft))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register left click event: %v", err)
|
||||
}
|
||||
err = ioctl(deviceFile, uiSetKeyBit, uintptr(evBtnRight))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register right click event: %v", err)
|
||||
}
|
||||
|
||||
err = registerDevice(deviceFile, uintptr(evAbs))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register absolute axis input device: %v", err)
|
||||
}
|
||||
|
||||
// register x and y axis events
|
||||
err = ioctl(deviceFile, uiSetAbsBit, uintptr(absX))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register absolute x axis events: %v", err)
|
||||
}
|
||||
err = ioctl(deviceFile, uiSetAbsBit, uintptr(absY))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register absolute y axis events: %v", err)
|
||||
}
|
||||
|
||||
var absMin [absSize]int32
|
||||
absMin[absX] = minX
|
||||
absMin[absY] = minY
|
||||
|
||||
var absMax [absSize]int32
|
||||
absMax[absX] = maxX
|
||||
absMax[absY] = maxY
|
||||
|
||||
return createUsbDevice(deviceFile,
|
||||
uinputUserDev{
|
||||
Name: toUinputName(name),
|
||||
ID: inputID{
|
||||
Bustype: busUsb,
|
||||
Vendor: 0x4711,
|
||||
Product: 0x0817,
|
||||
Version: 1},
|
||||
Absmin: absMin,
|
||||
Absmax: absMax})
|
||||
}
|
||||
|
||||
func createMouse(path string, name []byte) (fd *os.File, err error) {
|
||||
deviceFile, err := createDeviceFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create relative axis input device: %v", err)
|
||||
}
|
||||
|
||||
err = registerDevice(deviceFile, uintptr(evKey))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register key device: %v", err)
|
||||
}
|
||||
// register button events (in order to enable left and right click)
|
||||
err = ioctl(deviceFile, uiSetKeyBit, uintptr(evBtnLeft))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register left click event: %v", err)
|
||||
}
|
||||
err = ioctl(deviceFile, uiSetKeyBit, uintptr(evBtnRight))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register right click event: %v", err)
|
||||
}
|
||||
|
||||
err = registerDevice(deviceFile, uintptr(evRel))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register relative axis input device: %v", err)
|
||||
}
|
||||
|
||||
// register x and y axis events
|
||||
err = ioctl(deviceFile, uiSetRelBit, uintptr(relX))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register relative x axis events: %v", err)
|
||||
}
|
||||
err = ioctl(deviceFile, uiSetRelBit, uintptr(relY))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to register relative y axis events: %v", err)
|
||||
}
|
||||
|
||||
return createUsbDevice(deviceFile,
|
||||
uinputUserDev{
|
||||
Name: toUinputName(name),
|
||||
ID: inputID{
|
||||
Bustype: busUsb,
|
||||
Vendor: 0x4711,
|
||||
Product: 0x0816,
|
||||
Version: 1}})
|
||||
}
|
||||
|
||||
func createUsbDevice(deviceFile *os.File, dev uinputUserDev) (fd *os.File, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err = binary.Write(buf, binary.LittleEndian, dev)
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to write user device buffer: %v", err)
|
||||
}
|
||||
_, err = deviceFile.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to write uidev struct to device file: %v", err)
|
||||
}
|
||||
|
||||
err = ioctl(deviceFile, uiDevCreate, uintptr(0))
|
||||
if err != nil {
|
||||
deviceFile.Close()
|
||||
return nil, fmt.Errorf("failed to create device: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
|
||||
return deviceFile, err
|
||||
}
|
||||
|
||||
func sendBtnEvent(deviceFile *os.File, key int, btnState int) (err error) {
|
||||
buf, err := inputEventToBuffer(inputEvent{
|
||||
Time: syscall.Timeval{Sec: 0, Usec: 0},
|
||||
Type: evKey,
|
||||
Code: uint16(key),
|
||||
Value: int32(btnState)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("key event could not be set: %v", err)
|
||||
}
|
||||
_, err = deviceFile.Write(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing btnEvent structure to the device file failed: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func sendAbsEvent(deviceFile *os.File, xPos int32, yPos int32) error {
|
||||
var ev [2]inputEvent
|
||||
ev[0].Type = evAbs
|
||||
ev[0].Code = absX
|
||||
ev[0].Value = xPos
|
||||
|
||||
ev[1].Type = evAbs
|
||||
ev[1].Code = absY
|
||||
ev[1].Value = yPos
|
||||
|
||||
for _, iev := range ev {
|
||||
buf, err := inputEventToBuffer(iev)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing abs event failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = deviceFile.Write(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write abs event to device file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return syncEvents(deviceFile)
|
||||
}
|
||||
|
||||
func sendRelEvent(deviceFile *os.File, eventCode uint16, pixel int32) error {
|
||||
iev := inputEvent{
|
||||
Time: syscall.Timeval{Sec: 0, Usec: 0},
|
||||
Type: evRel,
|
||||
Code: eventCode,
|
||||
Value: pixel}
|
||||
|
||||
buf, err := inputEventToBuffer(iev)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing abs event failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = deviceFile.Write(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write rel event to device file: %v", err)
|
||||
}
|
||||
|
||||
return syncEvents(deviceFile)
|
||||
}
|
||||
|
||||
func syncEvents(deviceFile *os.File) (err error) {
|
||||
buf, err := inputEventToBuffer(inputEvent{
|
||||
Time: syscall.Timeval{Sec: 0, Usec: 0},
|
||||
Type: evSyn,
|
||||
Code: 0,
|
||||
Value: int32(synReport)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing sync event failed: %v", err)
|
||||
}
|
||||
_, err = deviceFile.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func inputEventToBuffer(iev inputEvent) (buffer []byte, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err = binary.Write(buf, binary.LittleEndian, iev)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write input event to buffer: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// original function taken from: https://github.com/tianon/debian-golang-pty/blob/master/ioctl.go
|
||||
func ioctl(deviceFile *os.File, cmd, ptr uintptr) error {
|
||||
_, _, errorCode := syscall.Syscall(syscall.SYS_IOCTL, deviceFile.Fd(), cmd, ptr)
|
||||
if errorCode != 0 {
|
||||
return errorCode
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -20,12 +20,6 @@
|
|||
"revision": "27f122750802c950b2c869a5b63dafcf590ced95",
|
||||
"revisionTime": "2016-05-22T18:18:43Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "DuHM9UBDn6AHdGqYDMZZqzFMOkI=",
|
||||
"path": "github.com/bendahl/uinput",
|
||||
"revision": "050ec524df761f5a128a5bb9bb5b7609f1886f21",
|
||||
"revisionTime": "2017-06-20T19:50:18Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "OFu4xJEIjiI8Suu+j/gabfp+y6Q=",
|
||||
"origin": "github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew",
|
||||
|
|
Ładowanie…
Reference in New Issue