diff --git a/README.md b/README.md index e5af537..6d918e4 100644 --- a/README.md +++ b/README.md @@ -46,40 +46,12 @@ AtomicGo

-## Description - -Package keyboard can be used to read key presses from the keyboard, while in a -terminal application. It's crossplatform and keypresses can be combined to check -for ctrl+c, alt+4, ctrl-shift, alt+ctrl+right, etc. - -Works nicely with https://atomicgo.dev/cursor - -```go - - keyboard.StartListener() - defer keyboard.StopListener() - - for { - keyInfo, _ := keyboard.GetKey() - key := keyInfo.Code - - if key == keys.CtrlC { - break - } - - fmt.Println("\r", keyInfo.String()) - } - -``` - -## Install -

@@ -91,91 +63,115 @@ Works nicely with https://atomicgo.dev/cursor

- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ---------------------------------------------------------------------------------------------------------------------------

- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ---------------------------------------------------------------------------------------------------------------------------

-```go -// Add this to your imports -import "atomicgo.dev/keyboard" -``` +## Description -## Usage +Package keyboard can be used to read key presses from the keyboard, while in a +terminal application. It's crossplatform and keypresses can be combined to check +for ctrl+c, alt+4, ctrl-shift, alt+ctrl+right, etc. It can also be used to +simulate (mock) keypresses for CI testing. -#### func GetKey +Works nicely with https://atomicgo.dev/cursor -```go -func GetKey() (keys.Key, error) -``` -GetKey blocks until a key is pressed and returns the key info. +## Simple Usage -Example: + keyboard.Listen(func(key keys.Key) (stop bool, err error) { + if key.Code == keys.CtrlC { + return true, nil // Stop listener by returning true on Ctrl+C + } - keyboard.StartListener() + fmt.Println("\r" + key.String()) // Print every key press + return false, nil // Return false to continue listening + }) + +## Advanced Usage + + // Stop keyboard listener on Escape key press or CTRL+C. + // Exit application on "q" key press. + // Print every rune key press. + // Print every other key press. + keyboard.Listen(func(key keys.Key) (stop bool, err error) { + switch key.Code { + case keys.CtrlC, keys.Escape: + return true, nil // Return true to stop listener + case keys.RuneKey: // Check if key is a rune key (a, b, c, 1, 2, 3, ...) + if key.String() == "q" { // Check if key is "q" + fmt.Println("\rQuitting application") + os.Exit(0) // Exit application + } + fmt.Printf("\rYou pressed the rune key: %s\n", key) + default: + fmt.Printf("\rYou pressed: %s\n", key) + } - for { - keyInfo, _ := keyboard.GetKey() - key := keyInfo.Code + return false, nil // Return false to continue listening + }) - if key == keys.CtrlC { - break - } +## Simulate Key Presses (for mocking in tests) - fmt.Println("\r", keyInfo.String()) - } + go func() { + keyboard.SimulateKeyPress("Hello") // Simulate key press for every letter in string + keyboard.SimulateKeyPress(keys.Enter) // Simulate key press for Enter + keyboard.SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right + keyboard.SimulateKeyPress('x') // Simulate key press for a single rune + keyboard.SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs - keyboard.StopListener() + keyboard.SimulateKeyPress(keys.Escape) // Simulate key press for Escape, which quits the program + }() -#### func StartListener + keyboard.Listen(func(key keys.Key) (stop bool, err error) { + if key.Code == keys.Escape || key.Code == keys.CtrlC { + os.Exit(0) // Exit program on Escape + } -```go -func StartListener() error -``` -StartListener starts the keyboard listener + fmt.Println("\r" + key.String()) // Print every key press + return false, nil // Return false to continue listening + }) -Example: - keyboard.StartListener() +## Usage + +#### func Listen - for { - keyInfo, _ := keyboard.GetKey() - key := keyInfo.Code +```go +func Listen(onKeyPress func(key keys.Key) (stop bool, err error)) error +``` +Listen calls a callback function when a key is pressed. - if key == keys.CtrlC { - break - } +Simple example: - fmt.Println("\r", keyInfo.String()) - } + keyboard.Listen(func(key keys.Key) (stop bool, err error) { + if key.Code == keys.CtrlC { + return true, nil // Stop listener by returning true on Ctrl+C + } - keyboard.StopListener() + fmt.Println("\r" + key.String()) // Print every key press + return false, nil // Return false to continue listening + }) -#### func StopListener +#### func SimulateKeyPress ```go -func StopListener() error +func SimulateKeyPress(input ...interface{}) error ``` -StopListener stops the keyboard listener +SimulateKeyPress simulate a key press. It can be used to mock user input and +test your application. Example: - keyboard.StartListener() - - for { - keyInfo, _ := keyboard.GetKey() - key := keyInfo.Code - - if key == keys.CtrlC { - break - } - - fmt.Println("\r", keyInfo.String()) - } - - keyboard.StopListener() + go func() { + keyboard.SimulateKeyPress("Hello") // Simulate key press for every letter in string + keyboard.SimulateKeyPress(keys.Enter) // Simulate key press for Enter + keyboard.SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right + keyboard.SimulateKeyPress('x') // Simulate key press for a single rune + keyboard.SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs + }() --- diff --git a/doc.go b/doc.go index f9cb66a..b9bc706 100644 --- a/doc.go +++ b/doc.go @@ -1,24 +1,64 @@ /* Package keyboard can be used to read key presses from the keyboard, while in a terminal application. It's crossplatform and keypresses can be combined to check for ctrl+c, alt+4, ctrl-shift, alt+ctrl+right, etc. +It can also be used to simulate (mock) keypresses for CI testing. Works nicely with https://atomicgo.dev/cursor -```go +## Simple Usage + + keyboard.Listen(func(key keys.Key) (stop bool, err error) { + if key.Code == keys.CtrlC { + return true, nil // Stop listener by returning true on Ctrl+C + } + + fmt.Println("\r" + key.String()) // Print every key press + return false, nil // Return false to continue listening + }) - keyboard.StartListener() - defer keyboard.StopListener() - for { - keyInfo, _ := keyboard.GetKey() - key := keyInfo.Code +## Advanced Usage - if key == keys.CtrlC { - break + // Stop keyboard listener on Escape key press or CTRL+C. + // Exit application on "q" key press. + // Print every rune key press. + // Print every other key press. + keyboard.Listen(func(key keys.Key) (stop bool, err error) { + switch key.Code { + case keys.CtrlC, keys.Escape: + return true, nil // Return true to stop listener + case keys.RuneKey: // Check if key is a rune key (a, b, c, 1, 2, 3, ...) + if key.String() == "q" { // Check if key is "q" + fmt.Println("\rQuitting application") + os.Exit(0) // Exit application + } + fmt.Printf("\rYou pressed the rune key: %s\n", key) + default: + fmt.Printf("\rYou pressed: %s\n", key) } - fmt.Println("\r", keyInfo.String()) - } + return false, nil // Return false to continue listening + }) + + +## Simulate Key Presses (for mocking in tests) + + go func() { + keyboard.SimulateKeyPress("Hello") // Simulate key press for every letter in string + keyboard.SimulateKeyPress(keys.Enter) // Simulate key press for Enter + keyboard.SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right + keyboard.SimulateKeyPress('x') // Simulate key press for a single rune + keyboard.SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs + + keyboard.SimulateKeyPress(keys.Escape) // Simulate key press for Escape, which quits the program + }() + + keyboard.Listen(func(key keys.Key) (stop bool, err error) { + if key.Code == keys.Escape || key.Code == keys.CtrlC { + os.Exit(0) // Exit program on Escape + } -``` + fmt.Println("\r" + key.String()) // Print every key press + return false, nil // Return false to continue listening + }) */ package keyboard diff --git a/doc_test.go b/doc_test.go new file mode 100644 index 0000000..2db133e --- /dev/null +++ b/doc_test.go @@ -0,0 +1,63 @@ +package keyboard + +import ( + "fmt" + "os" + + "atomicgo.dev/keyboard/keys" +) + +func ExampleSimple() { + Listen(func(key keys.Key) (stop bool, err error) { + if key.Code == keys.CtrlC { + return true, nil // Stop listener by returning true on Ctrl+C + } + + fmt.Println("\r" + key.String()) // Print every key press + return false, nil // Return false to continue listening + }) +} + +func ExampleAdvanced() { + // Stop keyboard listener on Escape key press or CTRL+C. + // Exit application on "q" key press. + // Print every rune key press. + // Print every other key press. + Listen(func(key keys.Key) (stop bool, err error) { + switch key.Code { + case keys.CtrlC, keys.Escape: + return true, nil // Return true to stop listener + case keys.RuneKey: // Check if key is a rune key (a, b, c, 1, 2, 3, ...) + if key.String() == "q" { // Check if key is "q" + fmt.Println("\rQuitting application") + os.Exit(0) // Exit application + } + fmt.Printf("\rYou pressed the rune key: %s\n", key) + default: + fmt.Printf("\rYou pressed: %s\n", key) + } + + return false, nil // Return false to continue listening + }) +} + +func ExampleMocking() { + go func() { + SimulateKeyPress("Hello") // Simulate key press for every letter in string + SimulateKeyPress(keys.Enter) // Simulate key press for Enter + SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right + SimulateKeyPress('x') // Simulate key press for a single rune + SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs + + SimulateKeyPress(keys.Escape) // Simulate key press for Escape, which quits the program + }() + + Listen(func(key keys.Key) (stop bool, err error) { + if key.Code == keys.Escape || key.Code == keys.CtrlC { + os.Exit(0) // Exit program on Escape + } + + fmt.Println("\r" + key.String()) // Print every key press + return false, nil // Return false to continue listening + }) +} diff --git a/keyboard.go b/keyboard.go index 6582424..b64d4b4 100644 --- a/keyboard.go +++ b/keyboard.go @@ -4,33 +4,18 @@ import ( "fmt" "os" - "atomicgo.dev/keyboard/keys" "github.com/containerd/console" + + "atomicgo.dev/keyboard/keys" ) var windowsStdin *os.File var con console.Console var input = os.Stdin var inputTTY *os.File +var mockChannel = make(chan keys.Key) -// StartListener starts the keyboard listener -// -// Example: -// keyboard.StartListener() -// -// for { -// keyInfo, _ := keyboard.GetKey() -// key := keyInfo.Code -// -// if key == keys.CtrlC { -// break -// } -// -// fmt.Println("\r", keyInfo.String()) -// } -// -// keyboard.StopListener() -func StartListener() error { +func startListener() error { err := initInput() if err != nil { return err @@ -51,24 +36,7 @@ func StartListener() error { return nil } -// StopListener stops the keyboard listener -// -// Example: -// keyboard.StartListener() -// -// for { -// keyInfo, _ := keyboard.GetKey() -// key := keyInfo.Code -// -// if key == keys.CtrlC { -// break -// } -// -// fmt.Println("\r", keyInfo.String()) -// } -// -// keyboard.StopListener() -func StopListener() error { +func stopListener() error { if con != nil { err := con.Reset() if err != nil { @@ -80,23 +48,110 @@ func StopListener() error { return restoreInput() } -// GetKey blocks until a key is pressed and returns the key info. -// -// Example: -// keyboard.StartListener() -// -// for { -// keyInfo, _ := keyboard.GetKey() -// key := keyInfo.Code +// Listen calls a callback function when a key is pressed. // -// if key == keys.CtrlC { -// break -// } +// Simple example: +// keyboard.Listen(func(key keys.Key) (stop bool, err error) { +// if key.Code == keys.CtrlC { +// return true, nil // Stop listener by returning true on Ctrl+C +// } // -// fmt.Println("\r", keyInfo.String()) -// } +// fmt.Println("\r" + key.String()) // Print every key press +// return false, nil // Return false to continue listening +// }) +func Listen(onKeyPress func(key keys.Key) (stop bool, err error)) error { + cancel := make(chan bool) + + err := startListener() + if err != nil { + return err + } + + go func() { + for { + select { + case c := <-cancel: + if c { + return + } + case keyInfo := <-mockChannel: + onKeyPress(keyInfo) + } + } + }() + + for { + key, err := getKeyPress(inputTTY) + if err != nil { + return err + } + + stop, err := onKeyPress(key) + if err != nil { + return err + } + + if stop { + break + } + } + + err = stopListener() + if err != nil { + return err + } + + cancel <- true + + return nil +} + +// SimulateKeyPress simulate a key press. It can be used to mock user input and test your application. // -// keyboard.StopListener() -func GetKey() (keys.Key, error) { - return getKeyPress(inputTTY) +// Example: +// go func() { +// keyboard.SimulateKeyPress("Hello") // Simulate key press for every letter in string +// keyboard.SimulateKeyPress(keys.Enter) // Simulate key press for Enter +// keyboard.SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right +// keyboard.SimulateKeyPress('x') // Simulate key press for a single rune +// keyboard.SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs +// }() +func SimulateKeyPress(input ...interface{}) error { + for _, key := range input { + // Check if key is a keys.Key + if key, ok := key.(keys.Key); ok { + mockChannel <- key + return nil + } + + // Check if key is a rune + if key, ok := key.(rune); ok { + mockChannel <- keys.Key{ + Code: keys.RuneKey, + Runes: []rune{key}, + } + return nil + } + + // Check if key is a string + if key, ok := key.(string); ok { + for _, r := range key { + mockChannel <- keys.Key{ + Code: keys.RuneKey, + Runes: []rune{r}, + } + } + return nil + } + + // Check if key is a KeyCode + if key, ok := key.(keys.KeyCode); ok { + mockChannel <- keys.Key{ + Code: key, + } + return nil + } + } + + return nil }