├── .github ├── atomicgo │ └── custom_readme ├── release.yml ├── settings.yml └── workflows │ ├── atomicgo.yml │ ├── go.yml │ ├── lint.yml │ └── tweet-release.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── Taskfile.yml ├── codecov.yml ├── doc.go ├── doc_test.go ├── go.mod ├── go.sum ├── input.go ├── internal └── keys.go ├── keyboard.go ├── keyboard_test.go ├── keys └── keys.go ├── tty_unix.go ├── tty_windows.go ├── utils.go └── utils_windows.go /.github/atomicgo/custom_readme: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | changelog: 12 | exclude: 13 | labels: 14 | - ignore-for-release 15 | authors: 16 | - octocat 17 | categories: 18 | - title: Breaking Changes 🛠 19 | labels: 20 | - breaking 21 | - title: Exciting New Features 🎉 22 | labels: 23 | - feature 24 | - title: Fixes 🔧 25 | labels: 26 | - fix 27 | - title: Other Changes 28 | labels: 29 | - "*" 30 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | 3 | repository: 4 | # See https://developer.github.com/v3/repos/#edit for all available settings. 5 | 6 | # A short description of the repository that will show up on GitHub 7 | description: ⌨️ Read keyboard events in your terminal applications! (Arrow keys, Home, End, etc.) 8 | 9 | # A comma-separated list of topics to set on the repository 10 | topics: atomicgo, go, golang, keyboard, terminal, cli 11 | 12 | # Either `true` to make the repository private, or `false` to make it public. 13 | private: false 14 | 15 | # Either `true` to enable issues for this repository, `false` to disable them. 16 | has_issues: true 17 | 18 | # Either `true` to enable projects for this repository, or `false` to disable them. 19 | # If projects are disabled for the organization, passing `true` will cause an API error. 20 | has_projects: false 21 | 22 | # Either `true` to enable the wiki for this repository, `false` to disable it. 23 | has_wiki: false 24 | -------------------------------------------------------------------------------- /.github/workflows/atomicgo.yml: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | name: AtomicGo 12 | 13 | on: 14 | push: 15 | branches: 16 | - main 17 | 18 | permissions: 19 | contents: write 20 | packages: write 21 | 22 | jobs: 23 | test: 24 | name: Test Go Code 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | os: [ubuntu-latest, windows-latest, macos-latest] 29 | steps: 30 | - name: Set up Go 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: stable 34 | 35 | - name: Check out code into the Go module directory 36 | uses: actions/checkout@v3 37 | 38 | - name: Get dependencies 39 | run: go get -v -t -d ./... 40 | 41 | - name: Build 42 | run: go build -v . 43 | 44 | - name: Test 45 | run: go test -coverprofile="coverage.txt" -covermode=atomic -v -p 1 . 46 | 47 | - name: Upload coverage to Codecov 48 | uses: codecov/codecov-action@v3 49 | 50 | build: 51 | name: Build AtomicGo Package 52 | runs-on: ubuntu-latest 53 | needs: test 54 | 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | 58 | steps: 59 | - name: Checkout repository 60 | uses: actions/checkout@v3 61 | with: 62 | fetch-depth: 0 63 | 64 | - name: Download assets 65 | run: | 66 | mkdir -p .templates 67 | wget https://raw.githubusercontent.com/atomicgo/atomicgo/main/templates/example.gotxt -O .templates/example.gotxt 68 | wget https://raw.githubusercontent.com/atomicgo/atomicgo/main/templates/readme.md -O .templates/readme.md 69 | 70 | - name: Set up Go 71 | uses: actions/setup-go@v4 72 | with: 73 | go-version: stable 74 | 75 | - name: Install Go tools 76 | run: | 77 | go install github.com/robertkrimen/godocdown/godocdown@latest 78 | go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest 79 | go install github.com/caarlos0/svu@latest 80 | 81 | - name: Set up Git configuration 82 | run: | 83 | REPO_FULLNAME="${{ github.repository }}" 84 | echo "::group::Setup git" 85 | git config --global --add safe.directory /github/workspace 86 | 87 | echo "::notice::Login into git" 88 | git config --global user.email "git@marvinjwendt.com" 89 | git config --global user.name "MarvinJWendt" 90 | 91 | echo "::notice::Ignore workflow files (we may not touch them)" 92 | git update-index --assume-unchanged .github/workflows/* 93 | 94 | - name: Generate README.md 95 | run: | 96 | echo "::group::Generate README.md" 97 | FILE=./.github/atomicgo/custom_readme 98 | INCLUDE_UNEXPORTED=./.github/atomicgo/include_unexported 99 | if test -f "$FILE"; then 100 | echo "::notice::.github/custom_readme is present. Not generating a new readme." 101 | else 102 | echo "::notice::Running Godocdown" 103 | $(go env GOPATH)/bin/godocdown -template ./.templates/readme.md >README.md 104 | echo "::notice::Running gomarkdoc" 105 | GOMARKDOC_FLAGS="--template-file example=./.templates/example.gotxt" 106 | if test -f "$INCLUDE_UNEXPORTED"; then 107 | GOMARKDOC_FLAGS+=" -u" 108 | fi 109 | 110 | $(go env GOPATH)/bin/gomarkdoc $GOMARKDOC_FLAGS --repository.url "https://github.com/${{ github.repository }}" --repository.default-branch main --repository.path / -e -o README.md . 111 | fi 112 | echo "::endgroup::" 113 | 114 | - name: Run custom CI system 115 | run: | 116 | echo "::group::Run custom CI system" 117 | echo "::notice::Counting unit tests" 118 | unittest_count=$(go test -v -p 1 ./... | tee /dev/tty | grep -c "RUN") 119 | 120 | echo "::notice::Replacing badge in README.md" 121 | sed -i 's|> $GITHUB_ENV 148 | echo "::notice::Current version is $(svu current)" 149 | 150 | - name: Calculate next version 151 | id: next_version 152 | run: | 153 | echo "next_version=$(svu next)" >> $GITHUB_ENV 154 | echo "::notice::Next version is $(svu next)" 155 | 156 | - name: Check if release is needed 157 | id: check_release 158 | run: | 159 | echo "release_needed=$( [ '${{ env.current_version }}' != '${{ env.next_version }}' ] && echo true || echo false )" >> $GITHUB_ENV 160 | 161 | - name: Create tag 162 | if: env.release_needed == 'true' 163 | run: | 164 | git tag -a ${{ env.next_version }} -m "Release v${{ env.next_version }}" 165 | git push origin ${{ env.next_version }} 166 | sleep 5 # sleep for 5 seconds to allow GitHub to process the tag 167 | 168 | - name: Release 169 | if: env.release_needed == 'true' 170 | uses: softprops/action-gh-release@v2 171 | with: 172 | token: ${{ secrets.GITHUB_TOKEN }} 173 | generate_release_notes: true 174 | tag_name: ${{ env.next_version }} 175 | 176 | - name: Tweet release 177 | if: env.release_needed == 'true' && !github.event.repository.private 178 | uses: Eomm/why-don-t-you-tweet@v1 179 | with: 180 | tweet-message: 181 | "New ${{ github.event.repository.name }} release: ${{ env.next_version }} 🚀 182 | 183 | Try it out: atomicgo.dev/${{ github.event.repository.name }} 184 | 185 | #go #golang #opensource #library #release #atomicgo" 186 | env: 187 | TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} 188 | TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} 189 | TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} 190 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 191 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | name: Go 12 | 13 | on: 14 | pull_request: 15 | 16 | jobs: 17 | test: 18 | name: Test Go code 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest, windows-latest, macos-latest] 23 | steps: 24 | - name: Set up Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: stable 28 | 29 | - name: Check out code into the Go module directory 30 | uses: actions/checkout@v3 31 | 32 | - name: Get dependencies 33 | run: go get -v -t -d ./... 34 | 35 | - name: Build 36 | run: go build -v . 37 | 38 | - name: Test 39 | run: go test -coverprofile="coverage.txt" -covermode=atomic -v -p 1 . 40 | 41 | - name: Upload coverage to Codecov 42 | uses: codecov/codecov-action@v1 43 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | name: Code Analysis 12 | 13 | on: [push, pull_request] 14 | 15 | jobs: 16 | lint: 17 | if: "!contains(github.event.head_commit.message, 'autoupdate')" 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: "stable" 26 | 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v3 29 | with: 30 | version: latest 31 | -------------------------------------------------------------------------------- /.github/workflows/tweet-release.yml: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | name: Tweet release 12 | 13 | # Listen to the `release` event 14 | on: 15 | release: 16 | types: [published] 17 | 18 | jobs: 19 | tweet: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: Eomm/why-don-t-you-tweet@v1 23 | # We don't want to tweet if the repository is not a public one 24 | if: ${{ !github.event.repository.private }} 25 | with: 26 | tweet-message: 27 | "New ${{ github.event.repository.name }} release: ${{ github.event.release.tag_name }}! 🎉 28 | 29 | Try it out: atomicgo.dev/${{ github.event.repository.name }} 30 | 31 | #go #golang #opensource #library #release #atomicgo" 32 | env: 33 | TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} 34 | TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} 35 | TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} 36 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | # Binaries 12 | *.exe 13 | *.exe~ 14 | *.so 15 | 16 | # Go specifics 17 | 18 | ## Test binary, built with `go test -c` 19 | *.test 20 | 21 | ## Output of the go coverage tool 22 | *.out 23 | 24 | ## Vendored dependencies 25 | vendor/ 26 | 27 | # IDEs 28 | 29 | ## IntelliJ 30 | .idea 31 | *.iml 32 | out 33 | gen 34 | 35 | ## Visual Studio Code 36 | .vscode 37 | *.code-workspace 38 | 39 | # Operating System Files 40 | 41 | ## macOS 42 | .DS_Store 43 | 44 | # Other 45 | 46 | ## Experimenting folder 47 | experimenting 48 | 49 | ## CI assets 50 | .templates 51 | 52 | ## Taskfile 53 | .task 54 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | run: 12 | timeout: 3m 13 | 14 | linters: 15 | enable: 16 | - errcheck # check for unchecked errors 17 | - gosimple # specializes in simplifying code 18 | - govet # roughly the same as go vet 19 | - ineffassign # detects when assignments to existing variables are not used 20 | - staticcheck # staticcheck is a go vet on steroids, applying static analysis to your code 21 | - unused # finds unused variables and constants 22 | - asasalint # check `any` variadic funcs 23 | - asciicheck # check for non-ASCII characters 24 | - bidichk # check for dangerous unicode character sequences 25 | - bodyclose # check that HTTP response body is closed 26 | - canonicalheader # check that canonical headers are used 27 | - containedctx # detects struct contained context.Context field 28 | - contextcheck # check whether the function uses a non-inherited context 29 | - decorder # check declaration order and count of types, constants, variables and functions 30 | - dupl # finds duplicated code 31 | - durationcheck # check for two durations multiplied together 32 | - err113 # check the errors handling expressions 33 | - errchkjson # checks types passed to the json encoding functions 34 | - errname # check error names 35 | - errorlint # check error wrapping 36 | - exhaustive # check that all enum cases are handled 37 | - exportloopref # checks for pointers to enclosing loop variables 38 | - fatcontext # detects nested contexts in loops 39 | - forcetypeassert # finds unchecked type assertions 40 | - funlen # check for long functions 41 | - gci # controls Go package import order and makes it always deterministic 42 | - gocheckcompilerdirectives # checks that go compiler directive comments (//go:) are valid 43 | - gochecksumtype # exhaustiveness checks on Go "sum types" 44 | - gocognit # check for high cognitive complexity 45 | - gocritic # Go source code linter that provides a ton of rules 46 | - gocyclo # checks cyclomatic complexity 47 | - gofmt # checks whether code was gofmt-ed 48 | - gofumpt # checks whether code was gofumpt-ed 49 | - goimports # check import statements are formatted according to the 'goimport' command 50 | - goprintffuncname # checks that printf-like functions are named with f at the end 51 | - gosec # inspects source code for security problems 52 | - gosmopolitan # report certain i18n/l10n anti-patterns in your Go codebase 53 | - inamedparam # reports interfaces with unnamed method parameters 54 | - interfacebloat # check for large interfaces 55 | - intrange # find places where for loops could make use of an integer range 56 | - lll # check for long lines 57 | - maintidx # measures the maintainability index of each function 58 | - mirror # reports wrong mirror patterns of bytes/strings usage 59 | - misspell # finds commonly misspelled English words 60 | - musttag # enforce field tags in (un)marshaled structs 61 | - nakedret # checks that functions with naked returns are not longer than a maximum size 62 | - nestif # reports deeply nested if statements 63 | - nilerr # finds code that returns nil even if it checks that the error is not nil 64 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value 65 | - nlreturn # checks for a new line before return and branch statements to increase code clarity 66 | - nolintlint # reports ill-formed or insufficient nolint directives 67 | - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL 68 | - paralleltest # detects missing usage of t.Parallel() method in your Go test 69 | - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative 70 | - prealloc # finds slice declarations that could potentially be pre-allocated 71 | - predeclared # finds code that shadows one of Go's predeclared identifiers 72 | - promlinter # checks Prometheus metrics naming via promlint 73 | - protogetter # reports direct reads from proto message fields when getters should be used 74 | - reassign # checks that package variables are not reassigned 75 | - revive # drop-in replacement of golint. 76 | - rowserrcheck # checks whether Rows.Err of rows is checked successfully 77 | - sloglint # ensures consistent code style when using log/slog 78 | - spancheck # checks for mistakes with OpenTelemetry/Census spans 79 | - sqlclosecheck # checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed 80 | - stylecheck # replacement for golint 81 | - tagalign # checks that struct tags are well aligned 82 | - tagliatelle # checks the struct tags 83 | - tenv # analyzer that detects using os.Setenv instead of t.Setenv 84 | - thelper # detects tests helpers which is not start with t.Helper() method 85 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes 86 | - unconvert # unnecessary type conversions 87 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library 88 | - varnamelen # checks that the length of a variable's name matches its scope 89 | - wastedassign # finds wasted assignment statements 90 | - whitespace # checks for unnecessary newlines at the start and end of functions, if, for, etc 91 | - wrapcheck # checks that errors returned from external packages are wrapped 92 | - wsl # add or remove empty lines 93 | 94 | disable: 95 | - copyloopvar # fixed in go 1.22+ 96 | - depguard # no forbidden imports 97 | - dogsled # blank identifiers are allowed 98 | - dupword # duplicate words are allowed 99 | - exhaustruct # many structs don't need to be exhaustive 100 | - forbidigo # no forbidden identifiers 101 | - ginkgolinter # not used 102 | - gochecknoinits # init functions are fine, if used carefully 103 | - goconst # many false positives 104 | - godot # comments don't need to be complete sentences 105 | - godox # todo comments are allowed 106 | - goheader # no need for a header 107 | - gomoddirectives # allow all directives 108 | - gomodguard # no forbidden imports 109 | - grouper # unused 110 | - importas # some aliases are fine 111 | - loggercheck # no slog support 112 | - makezero # make with non-zero initial length is fine 113 | - noctx # http request may be sent without context 114 | - nonamedreturns # named returns are fine 115 | - testableexamples # examples do not need to be testable (have declared output) 116 | - testifylint # testify is not recommended 117 | - testpackage # not a go best practice 118 | - unparam # interfaces can enforce parameters 119 | - zerologlint # slog should be used instead of zerlog 120 | - execinquery # deprecated 121 | - gomnd # deprecated 122 | - mnd # too many detections 123 | - cyclop # covered by gocyclo 124 | - gochecknoglobals # there are many valid reasons for global variables, depending on the project 125 | - ireturn # there are too many exceptions 126 | 127 | linters-settings: 128 | wsl: 129 | allow-cuddle-declarations: true 130 | force-err-cuddling: true 131 | force-case-trailing-whitespace: 3 132 | 133 | funlen: 134 | lines: 100 135 | statements: 50 136 | ignore-comments: true 137 | 138 | lll: 139 | line-length: 140 140 | tab-width: 1 141 | 142 | nlreturn: 143 | block-size: 2 144 | 145 | exhaustive: 146 | check-generated: false 147 | default-signifies-exhaustive: true 148 | 149 | varnamelen: 150 | ignore-type-assert-ok: true # ignore "ok" variables 151 | ignore-map-index-ok: true 152 | ignore-chan-recv-ok: true 153 | ignore-decls: 154 | - n int # generic number 155 | - x int # generic number (e.g. coordinate) 156 | - y int # generic number (e.g. coordinate) 157 | - z int # generic number (e.g. coordinate) 158 | - i int # generic number 159 | - a int # generic number 160 | - r int # generic number (e.g. red or radius) 161 | - g int # generic number (e.g. green) 162 | - b int # generic number (e.g. blue) 163 | - c int # generic number (e.g. count) 164 | - j int # generic number (e.g. index) 165 | - T any # generic type 166 | - a any # generic any (e.g. data) 167 | - b any # generic any (e.g. body) 168 | - c any # generic any 169 | - d any # generic any (e.g. data) 170 | - data any # generic data 171 | - n any # generic any 172 | - t time.Time # often used as a variable name 173 | - f func() # often used as a callback variable name 174 | - cb func() # often used as a callback variable name 175 | - t testing.T # default testing.T variable name 176 | - b testing.B # default testing.B variable name 177 | - sb strings.Builder # often used as a variable name 178 | 179 | issues: 180 | exclude-rules: 181 | - path: "_test(_[^/]+)?\\.go" 182 | linters: 183 | - gochecknoglobals 184 | - noctx 185 | - funlen 186 | - dupl 187 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Marvin Wendt (aka. MarvinJWendt) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | test: 12 | @echo "# Running tests..." 13 | @go test -v ./... 14 | 15 | lint: 16 | @echo "# Linting..." 17 | @echo "## Go mod tidy..." 18 | @go mod tidy 19 | @echo "## Fixing whitespaces..." 20 | @wsl --allow-cuddle-declarations --force-err-cuddling --force-case-trailing-whitespace 3 --fix ./... 21 | @echo "## Running golangci-lint..." 22 | @golangci-lint run 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

AtomicGo | keyboard

2 | 3 |

4 | 5 | 6 | Latest Release 7 | 8 | 9 | 10 | Tests 11 | 12 | 13 | 14 | Coverage 15 | 16 | 17 | 18 | Unit test count 19 | 20 | 21 | 22 | Issues 23 | 24 | 25 | 26 | License: MIT 27 | 28 | 29 |

30 | 31 | --- 32 | 33 |

34 | Get The Module 35 | | 36 | Documentation 37 | | 38 | Contributing 39 | | 40 | Code of Conduct 41 |

42 | 43 | --- 44 | 45 |

46 | AtomicGo 47 |

48 | 49 |

50 | 51 | 52 | 57 | 58 |
53 |
54 | ----------------------------------------------------------------------------------------------------- 55 | 56 |
59 |

60 |

go get atomicgo.dev/keyboard

61 |

62 | 63 | 64 | 69 | 70 |
65 |
66 | ----------------------------------------------------------------------------------------------------- 67 | 68 |
71 |

72 | 73 | ## Description 74 | 75 | Package keyboard can be used to read key presses from the keyboard, while in a 76 | terminal application. It's crossplatform and keypresses can be combined to check 77 | for ctrl+c, alt+4, ctrl-shift, alt+ctrl+right, etc. It can also be used to 78 | simulate (mock) keypresses for CI testing. 79 | 80 | Works nicely with https://atomicgo.dev/cursor 81 | 82 | ## Simple Usage 83 | 84 | ```go 85 | keyboard.Listen(func(key keys.Key) (stop bool, err error) { 86 | if key.Code == keys.CtrlC { 87 | return true, nil // Stop listener by returning true on Ctrl+C 88 | } 89 | 90 | fmt.Println("\r" + key.String()) // Print every key press 91 | return false, nil // Return false to continue listening 92 | }) 93 | ``` 94 | 95 | ## Advanced Usage 96 | 97 | ```go 98 | // Stop keyboard listener on Escape key press or CTRL+C. 99 | // Exit application on "q" key press. 100 | // Print every rune key press. 101 | // Print every other key press. 102 | keyboard.Listen(func(key keys.Key) (stop bool, err error) { 103 | switch key.Code { 104 | case keys.CtrlC, keys.Escape: 105 | return true, nil // Return true to stop listener 106 | case keys.RuneKey: // Check if key is a rune key (a, b, c, 1, 2, 3, ...) 107 | if key.String() == "q" { // Check if key is "q" 108 | fmt.Println("\rQuitting application") 109 | os.Exit(0) // Exit application 110 | } 111 | fmt.Printf("\rYou pressed the rune key: %s\n", key) 112 | default: 113 | fmt.Printf("\rYou pressed: %s\n", key) 114 | } 115 | 116 | return false, nil // Return false to continue listening 117 | }) 118 | ``` 119 | 120 | ## Simulate Key Presses (for mocking in tests) 121 | 122 | ```go 123 | go func() { 124 | keyboard.SimulateKeyPress("Hello") // Simulate key press for every letter in string 125 | keyboard.SimulateKeyPress(keys.Enter) // Simulate key press for Enter 126 | keyboard.SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right 127 | keyboard.SimulateKeyPress('x') // Simulate key press for a single rune 128 | keyboard.SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs 129 | 130 | keyboard.SimulateKeyPress(keys.Escape) // Simulate key press for Escape, which quits the program 131 | }() 132 | 133 | keyboard.Listen(func(key keys.Key) (stop bool, err error) { 134 | if key.Code == keys.Escape || key.Code == keys.CtrlC { 135 | os.Exit(0) // Exit program on Escape 136 | } 137 | 138 | fmt.Println("\r" + key.String()) // Print every key press 139 | return false, nil // Return false to continue listening 140 | }) 141 | ``` 142 | 143 | ## Usage 144 | 145 | #### func Listen 146 | 147 | ```go 148 | func Listen(onKeyPress func(key keys.Key) (stop bool, err error)) error 149 | ``` 150 | Listen calls a callback function when a key is pressed. 151 | 152 | Simple example: 153 | 154 | keyboard.Listen(func(key keys.Key) (stop bool, err error) { 155 | if key.Code == keys.CtrlC { 156 | return true, nil // Stop listener by returning true on Ctrl+C 157 | } 158 | 159 | fmt.Println("\r" + key.String()) // Print every key press 160 | return false, nil // Return false to continue listening 161 | }) 162 | 163 | #### func SimulateKeyPress 164 | 165 | ```go 166 | func SimulateKeyPress(input ...interface{}) error 167 | ``` 168 | SimulateKeyPress simulate a key press. It can be used to mock user input and 169 | test your application. 170 | 171 | Example: 172 | 173 | go func() { 174 | keyboard.SimulateKeyPress("Hello") // Simulate key press for every letter in string 175 | keyboard.SimulateKeyPress(keys.Enter) // Simulate key press for Enter 176 | keyboard.SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right 177 | keyboard.SimulateKeyPress('x') // Simulate key press for a single rune 178 | keyboard.SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs 179 | }() 180 | 181 | --- 182 | 183 | > [AtomicGo.dev](https://atomicgo.dev)  ·  184 | > with ❤️ by [@MarvinJWendt](https://github.com/MarvinJWendt) | 185 | > [MarvinJWendt.com](https://marvinjwendt.com) 186 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | version: "3" 12 | 13 | tasks: 14 | test: 15 | desc: Run all tests 16 | cmds: 17 | - go test ./... 18 | tdd: 19 | desc: Test Driven Development - Watch tests 20 | watch: true 21 | sources: 22 | - "**/*.go" 23 | cmds: 24 | - go test ./... 25 | 26 | lint: 27 | desc: Run all linters 28 | cmds: 29 | - go mod tidy 30 | - wsl --allow-cuddle-declarations --force-err-cuddling --force-case-trailing-whitespace 3 --fix ./... 31 | - golangci-lint run 32 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # ┌───────────────────────────────────────────────────────────────────┐ 2 | # │ │ 3 | # │ IMPORTANT NOTE │ 4 | # │ │ 5 | # │ This file is synced with https://github.com/atomicgo/template │ 6 | # │ │ 7 | # │ Please apply all changes to the template repository │ 8 | # │ │ 9 | # └───────────────────────────────────────────────────────────────────┘ 10 | 11 | coverage: 12 | status: 13 | project: 14 | default: 15 | informational: true 16 | patch: 17 | default: 18 | informational: true 19 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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. 3 | It can also be used to simulate (mock) keypresses for CI testing. 4 | 5 | Works nicely with https://atomicgo.dev/cursor 6 | 7 | ## Simple Usage 8 | 9 | keyboard.Listen(func(key keys.Key) (stop bool, err error) { 10 | if key.Code == keys.CtrlC { 11 | return true, nil // Stop listener by returning true on Ctrl+C 12 | } 13 | 14 | fmt.Println("\r" + key.String()) // Print every key press 15 | return false, nil // Return false to continue listening 16 | }) 17 | 18 | ## Advanced Usage 19 | 20 | // Stop keyboard listener on Escape key press or CTRL+C. 21 | // Exit application on "q" key press. 22 | // Print every rune key press. 23 | // Print every other key press. 24 | keyboard.Listen(func(key keys.Key) (stop bool, err error) { 25 | switch key.Code { 26 | case keys.CtrlC, keys.Escape: 27 | return true, nil // Return true to stop listener 28 | case keys.RuneKey: // Check if key is a rune key (a, b, c, 1, 2, 3, ...) 29 | if key.String() == "q" { // Check if key is "q" 30 | fmt.Println("\rQuitting application") 31 | os.Exit(0) // Exit application 32 | } 33 | fmt.Printf("\rYou pressed the rune key: %s\n", key) 34 | default: 35 | fmt.Printf("\rYou pressed: %s\n", key) 36 | } 37 | 38 | return false, nil // Return false to continue listening 39 | }) 40 | 41 | ## Simulate Key Presses (for mocking in tests) 42 | 43 | go func() { 44 | keyboard.SimulateKeyPress("Hello") // Simulate key press for every letter in string 45 | keyboard.SimulateKeyPress(keys.Enter) // Simulate key press for Enter 46 | keyboard.SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right 47 | keyboard.SimulateKeyPress('x') // Simulate key press for a single rune 48 | keyboard.SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs 49 | 50 | keyboard.SimulateKeyPress(keys.Escape) // Simulate key press for Escape, which quits the program 51 | }() 52 | 53 | keyboard.Listen(func(key keys.Key) (stop bool, err error) { 54 | if key.Code == keys.Escape || key.Code == keys.CtrlC { 55 | os.Exit(0) // Exit program on Escape 56 | } 57 | 58 | fmt.Println("\r" + key.String()) // Print every key press 59 | return false, nil // Return false to continue listening 60 | }) 61 | */ 62 | package keyboard 63 | -------------------------------------------------------------------------------- /doc_test.go: -------------------------------------------------------------------------------- 1 | package keyboard 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "atomicgo.dev/keyboard/keys" 8 | ) 9 | 10 | func ExampleListen_simple() { 11 | Listen(func(key keys.Key) (stop bool, err error) { 12 | if key.Code == keys.CtrlC { 13 | return true, nil // Stop listener by returning true on Ctrl+C 14 | } 15 | 16 | fmt.Println("\r" + key.String()) // Print every key press 17 | return false, nil // Return false to continue listening 18 | }) 19 | } 20 | 21 | func ExampleListen_advanced() { 22 | // Stop keyboard listener on Escape key press or CTRL+C. 23 | // Exit application on "q" key press. 24 | // Print every rune key press. 25 | // Print every other key press. 26 | Listen(func(key keys.Key) (stop bool, err error) { 27 | switch key.Code { 28 | case keys.CtrlC, keys.Escape: 29 | return true, nil // Return true to stop listener 30 | case keys.RuneKey: // Check if key is a rune key (a, b, c, 1, 2, 3, ...) 31 | if key.String() == "q" { // Check if key is "q" 32 | fmt.Println("\rQuitting application") 33 | os.Exit(0) // Exit application 34 | } 35 | fmt.Printf("\rYou pressed the rune key: %s\n", key) 36 | default: 37 | fmt.Printf("\rYou pressed: %s\n", key) 38 | } 39 | 40 | return false, nil // Return false to continue listening 41 | }) 42 | } 43 | 44 | func ExampleSimulateKeyPress() { 45 | go func() { 46 | SimulateKeyPress("Hello") // Simulate key press for every letter in string 47 | SimulateKeyPress(keys.Enter) // Simulate key press for Enter 48 | SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right 49 | SimulateKeyPress('x') // Simulate key press for a single rune 50 | SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs 51 | 52 | SimulateKeyPress(keys.Escape) // Simulate key press for Escape, which quits the program 53 | }() 54 | 55 | Listen(func(key keys.Key) (stop bool, err error) { 56 | if key.Code == keys.Escape || key.Code == keys.CtrlC { 57 | os.Exit(0) // Exit program on Escape 58 | } 59 | 60 | fmt.Println("\r" + key.String()) // Print every key press 61 | return false, nil // Return false to continue listening 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module atomicgo.dev/keyboard 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/MarvinJWendt/testza v0.4.2 7 | github.com/containerd/console v1.0.3 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= 2 | github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= 3 | github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= 4 | github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= 5 | github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= 6 | github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= 7 | github.com/MarvinJWendt/testza v0.4.2 h1:Vbw9GkSB5erJI2BPnBL9SVGV9myE+XmUSFahBGUhW2Q= 8 | github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= 9 | github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU= 10 | github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= 11 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= 12 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= 17 | github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw= 18 | github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= 19 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 20 | github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= 21 | github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= 22 | github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= 23 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 24 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 25 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 26 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 27 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 28 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 29 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= 33 | github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= 34 | github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= 35 | github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= 36 | github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= 37 | github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= 38 | github.com/pterm/pterm v0.12.40 h1:LvQE43RYegVH+y5sCDcqjlbsRu0DlAecEn9FDfs9ePs= 39 | github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= 40 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 41 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 42 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 43 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 46 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 47 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 48 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= 50 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= 51 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= 57 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 59 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 60 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 61 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 64 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 66 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 67 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 69 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 70 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package keyboard 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "unicode/utf8" 8 | 9 | "atomicgo.dev/keyboard/internal" 10 | "atomicgo.dev/keyboard/keys" 11 | ) 12 | 13 | // Sequence mappings. 14 | var sequences = map[string]keys.Key{ 15 | // Arrow keys 16 | "\x1b[A": {Code: keys.Up}, 17 | "\x1b[B": {Code: keys.Down}, 18 | "\x1b[C": {Code: keys.Right}, 19 | "\x1b[D": {Code: keys.Left}, 20 | "\x1b[1;2A": {Code: keys.ShiftUp}, 21 | "\x1b[1;2B": {Code: keys.ShiftDown}, 22 | "\x1b[1;2C": {Code: keys.ShiftRight}, 23 | "\x1b[1;2D": {Code: keys.ShiftLeft}, 24 | "\x1b[OA": {Code: keys.ShiftUp}, 25 | "\x1b[OB": {Code: keys.ShiftDown}, 26 | "\x1b[OC": {Code: keys.ShiftRight}, 27 | "\x1b[OD": {Code: keys.ShiftLeft}, 28 | "\x1b[a": {Code: keys.ShiftUp}, 29 | "\x1b[b": {Code: keys.ShiftDown}, 30 | "\x1b[c": {Code: keys.ShiftRight}, 31 | "\x1b[d": {Code: keys.ShiftLeft}, 32 | "\x1b[1;3A": {Code: keys.Up, AltPressed: true}, 33 | "\x1b[1;3B": {Code: keys.Down, AltPressed: true}, 34 | "\x1b[1;3C": {Code: keys.Right, AltPressed: true}, 35 | "\x1b[1;3D": {Code: keys.Left, AltPressed: true}, 36 | "\x1b\x1b[A": {Code: keys.Up, AltPressed: true}, 37 | "\x1b\x1b[B": {Code: keys.Down, AltPressed: true}, 38 | "\x1b\x1b[C": {Code: keys.Right, AltPressed: true}, 39 | "\x1b\x1b[D": {Code: keys.Left, AltPressed: true}, 40 | "\x1b[1;4A": {Code: keys.ShiftUp, AltPressed: true}, 41 | "\x1b[1;4B": {Code: keys.ShiftDown, AltPressed: true}, 42 | "\x1b[1;4C": {Code: keys.ShiftRight, AltPressed: true}, 43 | "\x1b[1;4D": {Code: keys.ShiftLeft, AltPressed: true}, 44 | "\x1b\x1b[a": {Code: keys.ShiftUp, AltPressed: true}, 45 | "\x1b\x1b[b": {Code: keys.ShiftDown, AltPressed: true}, 46 | "\x1b\x1b[c": {Code: keys.ShiftRight, AltPressed: true}, 47 | "\x1b\x1b[d": {Code: keys.ShiftLeft, AltPressed: true}, 48 | "\x1b[1;5A": {Code: keys.CtrlUp}, 49 | "\x1b[1;5B": {Code: keys.CtrlDown}, 50 | "\x1b[1;5C": {Code: keys.CtrlRight}, 51 | "\x1b[1;5D": {Code: keys.CtrlLeft}, 52 | "\x1b[Oa": {Code: keys.CtrlUp, AltPressed: true}, 53 | "\x1b[Ob": {Code: keys.CtrlDown, AltPressed: true}, 54 | "\x1b[Oc": {Code: keys.CtrlRight, AltPressed: true}, 55 | "\x1b[Od": {Code: keys.CtrlLeft, AltPressed: true}, 56 | "\x1b[1;6A": {Code: keys.CtrlShiftUp}, 57 | "\x1b[1;6B": {Code: keys.CtrlShiftDown}, 58 | "\x1b[1;6C": {Code: keys.CtrlShiftRight}, 59 | "\x1b[1;6D": {Code: keys.CtrlShiftLeft}, 60 | "\x1b[1;7A": {Code: keys.CtrlUp, AltPressed: true}, 61 | "\x1b[1;7B": {Code: keys.CtrlDown, AltPressed: true}, 62 | "\x1b[1;7C": {Code: keys.CtrlRight, AltPressed: true}, 63 | "\x1b[1;7D": {Code: keys.CtrlLeft, AltPressed: true}, 64 | "\x1b[1;8A": {Code: keys.CtrlShiftUp, AltPressed: true}, 65 | "\x1b[1;8B": {Code: keys.CtrlShiftDown, AltPressed: true}, 66 | "\x1b[1;8C": {Code: keys.CtrlShiftRight, AltPressed: true}, 67 | "\x1b[1;8D": {Code: keys.CtrlShiftLeft, AltPressed: true}, 68 | 69 | // Miscellaneous keys 70 | "\x1b[Z": {Code: keys.ShiftTab}, 71 | "\x1b[3~": {Code: keys.Delete}, 72 | "\x1b[3;3~": {Code: keys.Delete, AltPressed: true}, 73 | "\x1b[1~": {Code: keys.Home}, 74 | "\x1b[1;3H~": {Code: keys.Home, AltPressed: true}, 75 | "\x1b[4~": {Code: keys.End}, 76 | "\x1b[1;3F~": {Code: keys.End, AltPressed: true}, 77 | "\x1b[5~": {Code: keys.PgUp}, 78 | "\x1b[5;3~": {Code: keys.PgUp, AltPressed: true}, 79 | "\x1b[6~": {Code: keys.PgDown}, 80 | "\x1b[6;3~": {Code: keys.PgDown, AltPressed: true}, 81 | "\x1b[7~": {Code: keys.Home}, 82 | "\x1b[8~": {Code: keys.End}, 83 | "\x1b\x1b[3~": {Code: keys.Delete, AltPressed: true}, 84 | "\x1b\x1b[5~": {Code: keys.PgUp, AltPressed: true}, 85 | "\x1b\x1b[6~": {Code: keys.PgDown, AltPressed: true}, 86 | "\x1b\x1b[7~": {Code: keys.Home, AltPressed: true}, 87 | "\x1b\x1b[8~": {Code: keys.End, AltPressed: true}, 88 | 89 | // Function keys 90 | "\x1bOP": {Code: keys.F1}, 91 | "\x1bOQ": {Code: keys.F2}, 92 | "\x1bOR": {Code: keys.F3}, 93 | "\x1bOS": {Code: keys.F4}, 94 | "\x1b[15~": {Code: keys.F5}, 95 | "\x1b[17~": {Code: keys.F6}, 96 | "\x1b[18~": {Code: keys.F7}, 97 | "\x1b[19~": {Code: keys.F8}, 98 | "\x1b[20~": {Code: keys.F9}, 99 | "\x1b[21~": {Code: keys.F10}, 100 | "\x1b[23~": {Code: keys.F11}, 101 | "\x1b[24~": {Code: keys.F12}, 102 | "\x1b[1;2P": {Code: keys.F13}, 103 | "\x1b[1;2Q": {Code: keys.F14}, 104 | "\x1b[1;2R": {Code: keys.F15}, 105 | "\x1b[1;2S": {Code: keys.F16}, 106 | "\x1b[15;2~": {Code: keys.F17}, 107 | "\x1b[17;2~": {Code: keys.F18}, 108 | "\x1b[18;2~": {Code: keys.F19}, 109 | "\x1b[19;2~": {Code: keys.F20}, 110 | 111 | // Function keys with the alt modifier 112 | "\x1b[1;3P": {Code: keys.F1, AltPressed: true}, 113 | "\x1b[1;3Q": {Code: keys.F2, AltPressed: true}, 114 | "\x1b[1;3R": {Code: keys.F3, AltPressed: true}, 115 | "\x1b[1;3S": {Code: keys.F4, AltPressed: true}, 116 | "\x1b[15;3~": {Code: keys.F5, AltPressed: true}, 117 | "\x1b[17;3~": {Code: keys.F6, AltPressed: true}, 118 | "\x1b[18;3~": {Code: keys.F7, AltPressed: true}, 119 | "\x1b[19;3~": {Code: keys.F8, AltPressed: true}, 120 | "\x1b[20;3~": {Code: keys.F9, AltPressed: true}, 121 | "\x1b[21;3~": {Code: keys.F10, AltPressed: true}, 122 | "\x1b[23;3~": {Code: keys.F11, AltPressed: true}, 123 | "\x1b[24;3~": {Code: keys.F12, AltPressed: true}, 124 | 125 | // Function keys, urxvt 126 | "\x1b[11~": {Code: keys.F1}, 127 | "\x1b[12~": {Code: keys.F2}, 128 | "\x1b[13~": {Code: keys.F3}, 129 | "\x1b[14~": {Code: keys.F4}, 130 | "\x1b[25~": {Code: keys.F13}, 131 | "\x1b[26~": {Code: keys.F14}, 132 | "\x1b[28~": {Code: keys.F15}, 133 | "\x1b[29~": {Code: keys.F16}, 134 | "\x1b[31~": {Code: keys.F17}, 135 | "\x1b[32~": {Code: keys.F18}, 136 | "\x1b[33~": {Code: keys.F19}, 137 | "\x1b[34~": {Code: keys.F20}, 138 | 139 | // Function keys with the alt modifier, urxvt 140 | "\x1b\x1b[11~": {Code: keys.F1, AltPressed: true}, 141 | "\x1b\x1b[12~": {Code: keys.F2, AltPressed: true}, 142 | "\x1b\x1b[13~": {Code: keys.F3, AltPressed: true}, 143 | "\x1b\x1b[14~": {Code: keys.F4, AltPressed: true}, 144 | "\x1b\x1b[25~": {Code: keys.F13, AltPressed: true}, 145 | "\x1b\x1b[26~": {Code: keys.F14, AltPressed: true}, 146 | "\x1b\x1b[28~": {Code: keys.F15, AltPressed: true}, 147 | "\x1b\x1b[29~": {Code: keys.F16, AltPressed: true}, 148 | "\x1b\x1b[31~": {Code: keys.F17, AltPressed: true}, 149 | "\x1b\x1b[32~": {Code: keys.F18, AltPressed: true}, 150 | "\x1b\x1b[33~": {Code: keys.F19, AltPressed: true}, 151 | "\x1b\x1b[34~": {Code: keys.F20, AltPressed: true}, 152 | } 153 | 154 | var hexCodes = map[string]keys.Key{ 155 | "1b0d": {Code: keys.Enter, AltPressed: true}, 156 | "1b7f": {Code: keys.Backspace, AltPressed: true}, 157 | // support other backspace variants 158 | "1b08": {Code: keys.Backspace, AltPressed: true}, 159 | "08": {Code: keys.Backspace}, 160 | 161 | // Powershell 162 | "1b4f41": {Code: keys.Up, AltPressed: false}, 163 | "1b4f42": {Code: keys.Down, AltPressed: false}, 164 | "1b4f43": {Code: keys.Right, AltPressed: false}, 165 | "1b4f44": {Code: keys.Left, AltPressed: false}, 166 | } 167 | 168 | func getKeyPress() (keys.Key, error) { 169 | var buf [256]byte 170 | 171 | // Read 172 | numBytes, err := inputTTY.Read(buf[:]) 173 | if err != nil { 174 | if errors.Is(err, os.ErrClosed) { 175 | return keys.Key{}, nil 176 | } 177 | 178 | if err.Error() == "EOF" { 179 | return keys.Key{}, nil 180 | } else if err.Error() == "invalid argument" { 181 | return keys.Key{}, nil 182 | } 183 | 184 | return keys.Key{}, nil 185 | } 186 | 187 | // Check if it's a sequence 188 | if k, ok := sequences[string(buf[:numBytes])]; ok { 189 | return k, nil 190 | } 191 | 192 | hex := fmt.Sprintf("%x", buf[:numBytes]) 193 | if k, ok := hexCodes[hex]; ok { 194 | return k, nil 195 | } 196 | 197 | // Check if the alt key is pressed. 198 | if numBytes > 1 && buf[0] == 0x1b { 199 | // Remove the initial escape sequence 200 | c, _ := utf8.DecodeRune(buf[1:]) 201 | if c == utf8.RuneError { 202 | return keys.Key{}, fmt.Errorf("could not decode rune after removing initial escape sequence") 203 | } 204 | 205 | return keys.Key{AltPressed: true, Code: keys.RuneKey, Runes: []rune{c}}, nil 206 | } 207 | 208 | var runes []rune 209 | b := buf[:numBytes] 210 | 211 | // Translate stdin into runes. 212 | for i, w := 0, 0; i < len(b); i += w { //nolint:wastedassign 213 | r, width := utf8.DecodeRune(b[i:]) 214 | if r == utf8.RuneError { 215 | return keys.Key{}, fmt.Errorf("could not decode rune: %w", err) 216 | } 217 | runes = append(runes, r) 218 | w = width 219 | } 220 | 221 | if len(runes) == 0 { 222 | return keys.Key{}, fmt.Errorf("received 0 runes from stdin") 223 | } else if len(runes) > 1 { 224 | return keys.Key{Code: keys.RuneKey, Runes: runes}, nil 225 | } 226 | 227 | r := keys.KeyCode(runes[0]) 228 | if numBytes == 1 && r <= internal.KeyUnitSeparator || r == internal.KeyDelete { 229 | return keys.Key{Code: r}, nil 230 | } 231 | 232 | if runes[0] == ' ' { 233 | return keys.Key{Code: keys.Space, Runes: runes}, nil 234 | } 235 | 236 | return keys.Key{Code: keys.RuneKey, Runes: runes}, nil 237 | } 238 | -------------------------------------------------------------------------------- /internal/keys.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes 4 | const ( 5 | KeyNull = 0 6 | KeyStartOfHeading = 1 7 | KeyStartOfText = 2 8 | KeyExit = 3 // ctrl-c 9 | KeyEndOfTransimission = 4 10 | KeyEnquiry = 5 11 | KeyAcknowledge = 6 12 | KeyBELL = 7 13 | KeyBackspace = 8 14 | KeyHorizontalTabulation = 9 15 | KeyLineFeed = 10 16 | KeyVerticalTabulation = 11 17 | KeyFormFeed = 12 18 | KeyCarriageReturn = 13 19 | KeyShiftOut = 14 20 | KeyShiftIn = 15 21 | KeyDataLinkEscape = 16 22 | KeyDeviceControl1 = 17 23 | KeyDeviceControl2 = 18 24 | KeyDeviceControl3 = 19 25 | KeyDeviceControl4 = 20 26 | KeyNegativeAcknowledge = 21 27 | KeySynchronousIdle = 22 28 | KeyEndOfTransmissionBlock = 23 29 | KeyCancel = 24 30 | KeyEndOfMedium = 25 31 | KeySubstitution = 26 32 | KeyEscape = 27 33 | KeyFileSeparator = 28 34 | KeyGroupSeparator = 29 35 | KeyRecordSeparator = 30 36 | KeyUnitSeparator = 31 37 | KeyDelete = 127 38 | ) 39 | -------------------------------------------------------------------------------- /keyboard.go: -------------------------------------------------------------------------------- 1 | package keyboard 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/containerd/console" 8 | 9 | "atomicgo.dev/keyboard/keys" 10 | ) 11 | 12 | var windowsStdin *os.File 13 | var con console.Console 14 | var stdin = os.Stdin 15 | var inputTTY *os.File 16 | var mockChannel = make(chan keys.Key) 17 | 18 | var mocking = false 19 | 20 | func startListener() error { 21 | err := initInput() 22 | if err != nil { 23 | return err 24 | } 25 | 26 | if mocking { 27 | return nil 28 | } 29 | 30 | if con != nil { 31 | err := con.SetRaw() 32 | if err != nil { 33 | return fmt.Errorf("failed to set raw mode: %w", err) 34 | } 35 | } 36 | 37 | inputTTY, err = openInputTTY() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func stopListener() error { 46 | if con != nil { 47 | err := con.Reset() 48 | if err != nil { 49 | 50 | return fmt.Errorf("failed to reset console: %w", err) 51 | } 52 | } 53 | 54 | return restoreInput() 55 | } 56 | 57 | // Listen calls a callback function when a key is pressed. 58 | // 59 | // Simple example: 60 | // 61 | // keyboard.Listen(func(key keys.Key) (stop bool, err error) { 62 | // if key.Code == keys.CtrlC { 63 | // return true, nil // Stop listener by returning true on Ctrl+C 64 | // } 65 | // 66 | // fmt.Println("\r" + key.String()) // Print every key press 67 | // return false, nil // Return false to continue listening 68 | // }) 69 | func Listen(onKeyPress func(key keys.Key) (stop bool, err error)) error { 70 | cancel := make(chan bool) 71 | stopRoutine := false 72 | 73 | go func() { 74 | for { 75 | select { 76 | case c := <-cancel: 77 | if c { 78 | return 79 | } 80 | case keyInfo := <-mockChannel: 81 | stopRoutine, _ = onKeyPress(keyInfo) 82 | if stopRoutine { 83 | closeInput() 84 | inputTTY.Close() 85 | } 86 | } 87 | } 88 | }() 89 | 90 | err := startListener() 91 | if err != nil { 92 | if err.Error() != "provided file is not a console" { 93 | return err 94 | } 95 | } 96 | 97 | for !stopRoutine { 98 | key, err := getKeyPress() 99 | if err != nil { 100 | return err 101 | } 102 | 103 | // check if returned key is empty 104 | // if reflect.DeepEqual(key, keys.Key{}) { 105 | // return nil 106 | // } 107 | 108 | stop, err := onKeyPress(key) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | if stop { 114 | closeInput() 115 | inputTTY.Close() 116 | break 117 | } 118 | } 119 | 120 | err = stopListener() 121 | if err != nil { 122 | return err 123 | } 124 | 125 | cancel <- true 126 | 127 | return nil 128 | } 129 | 130 | // SimulateKeyPress simulate a key press. It can be used to mock user stdin and test your application. 131 | // 132 | // Example: 133 | // 134 | // go func() { 135 | // keyboard.SimulateKeyPress("Hello") // Simulate key press for every letter in string 136 | // keyboard.SimulateKeyPress(keys.Enter) // Simulate key press for Enter 137 | // keyboard.SimulateKeyPress(keys.CtrlShiftRight) // Simulate key press for Ctrl+Shift+Right 138 | // keyboard.SimulateKeyPress('x') // Simulate key press for a single rune 139 | // keyboard.SimulateKeyPress('x', keys.Down, 'a') // Simulate key presses for multiple inputs 140 | // }() 141 | func SimulateKeyPress(input ...interface{}) error { 142 | for _, key := range input { 143 | // Check if key is a keys.Key 144 | if key, ok := key.(keys.Key); ok { 145 | mockChannel <- key 146 | return nil 147 | } 148 | 149 | // Check if key is a rune 150 | if key, ok := key.(rune); ok { 151 | mockChannel <- keys.Key{ 152 | Code: keys.RuneKey, 153 | Runes: []rune{key}, 154 | } 155 | return nil 156 | } 157 | 158 | // Check if key is a string 159 | if key, ok := key.(string); ok { 160 | for _, r := range key { 161 | mockChannel <- keys.Key{ 162 | Code: keys.RuneKey, 163 | Runes: []rune{r}, 164 | } 165 | } 166 | return nil 167 | } 168 | 169 | // Check if key is a KeyCode 170 | if key, ok := key.(keys.KeyCode); ok { 171 | mockChannel <- keys.Key{ 172 | Code: key, 173 | } 174 | return nil 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /keyboard_test.go: -------------------------------------------------------------------------------- 1 | package keyboard_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "atomicgo.dev/keyboard" 7 | "atomicgo.dev/keyboard/keys" 8 | "github.com/MarvinJWendt/testza" 9 | ) 10 | 11 | func TestMocking(t *testing.T) { 12 | go func() { 13 | keyboard.SimulateKeyPress('a') 14 | keyboard.SimulateKeyPress("b") 15 | keyboard.SimulateKeyPress("c") 16 | keyboard.SimulateKeyPress(keys.Enter) 17 | }() 18 | 19 | var aPressed, bPressed, cPressed, enterPressed bool 20 | var keyList []keys.Key 21 | 22 | err := keyboard.Listen(func(key keys.Key) (stop bool, err error) { 23 | keyList = append(keyList, key) 24 | switch key.Code { 25 | case keys.RuneKey: 26 | switch key.String() { 27 | case "a": 28 | println("a pressed") 29 | aPressed = true 30 | return false, nil 31 | case "b": 32 | println("b pressed") 33 | bPressed = true 34 | return false, nil 35 | case "c": 36 | println("c pressed") 37 | cPressed = true 38 | return false, nil 39 | } 40 | case keys.Enter: 41 | println("enter pressed") 42 | enterPressed = true 43 | return true, nil 44 | } 45 | 46 | return false, nil 47 | }) 48 | 49 | testza.AssertNoError(t, err) 50 | 51 | testza.AssertTrue(t, aPressed, "A | %s", keyList) 52 | testza.AssertTrue(t, bPressed, "B | %s", keyList) 53 | testza.AssertTrue(t, cPressed, "C | %s", keyList) 54 | testza.AssertTrue(t, enterPressed, "Enter | %s", keyList) 55 | } 56 | -------------------------------------------------------------------------------- /keys/keys.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import "atomicgo.dev/keyboard/internal" 4 | 5 | // Key contains information about a keypress. 6 | type Key struct { 7 | Code KeyCode 8 | Runes []rune // Runes that the key produced. Most key pressed produce one single rune. 9 | AltPressed bool // True when alt is pressed while the key is typed. 10 | } 11 | 12 | // String returns a string representation of the key. 13 | // (e.g. "a", "B", "alt+a", "enter", "ctrl+c", "shift-down", etc.) 14 | // 15 | // Example: 16 | // 17 | // k := keys.Key{Code: keys.Enter} 18 | // fmt.Println(k) 19 | // // Output: enter 20 | func (k Key) String() (str string) { 21 | if k.AltPressed { 22 | str += "alt+" 23 | } 24 | if k.Code == RuneKey { 25 | str += string(k.Runes) 26 | 27 | return str 28 | } else if s, ok := keyNames[k.Code]; ok { 29 | str += s 30 | 31 | return str 32 | } 33 | 34 | return "" 35 | } 36 | 37 | // KeyCode is an integer representation of a non-rune key, such as Escape, Enter, etc. 38 | // All other keys are represented by a rune and have the KeyCode: RuneKey. 39 | // 40 | // Example: 41 | // 42 | // k := Key{Code: RuneKey, Runes: []rune{'x'}, AltPressed: true} 43 | // if k.Code == RuneKey { 44 | // fmt.Println(k.Runes) 45 | // // Output: x 46 | // 47 | // fmt.Println(k.String()) 48 | // // Output: alt+x 49 | // } 50 | type KeyCode int 51 | 52 | func (k KeyCode) String() (str string) { 53 | if s, ok := keyNames[k]; ok { 54 | return s 55 | } 56 | 57 | return "" 58 | } 59 | 60 | // All control keys. 61 | const ( 62 | Null KeyCode = internal.KeyNull 63 | Break KeyCode = internal.KeyExit 64 | Enter KeyCode = internal.KeyCarriageReturn 65 | Backspace KeyCode = internal.KeyDelete 66 | Tab KeyCode = internal.KeyHorizontalTabulation 67 | Esc KeyCode = internal.KeyEscape 68 | Escape KeyCode = internal.KeyEscape 69 | 70 | CtrlAt KeyCode = internal.KeyNull 71 | CtrlA KeyCode = internal.KeyStartOfHeading 72 | CtrlB KeyCode = internal.KeyStartOfText 73 | CtrlC KeyCode = internal.KeyExit 74 | CtrlD KeyCode = internal.KeyEndOfTransimission 75 | CtrlE KeyCode = internal.KeyEnquiry 76 | CtrlF KeyCode = internal.KeyAcknowledge 77 | CtrlG KeyCode = internal.KeyBELL 78 | CtrlH KeyCode = internal.KeyBackspace 79 | CtrlI KeyCode = internal.KeyHorizontalTabulation 80 | CtrlJ KeyCode = internal.KeyLineFeed 81 | CtrlK KeyCode = internal.KeyVerticalTabulation 82 | CtrlL KeyCode = internal.KeyFormFeed 83 | CtrlM KeyCode = internal.KeyCarriageReturn 84 | CtrlN KeyCode = internal.KeyShiftOut 85 | CtrlO KeyCode = internal.KeyShiftIn 86 | CtrlP KeyCode = internal.KeyDataLinkEscape 87 | CtrlQ KeyCode = internal.KeyDeviceControl1 88 | CtrlR KeyCode = internal.KeyDeviceControl2 89 | CtrlS KeyCode = internal.KeyDeviceControl3 90 | CtrlT KeyCode = internal.KeyDeviceControl4 91 | CtrlU KeyCode = internal.KeyNegativeAcknowledge 92 | CtrlV KeyCode = internal.KeySynchronousIdle 93 | CtrlW KeyCode = internal.KeyEndOfTransmissionBlock 94 | CtrlX KeyCode = internal.KeyCancel 95 | CtrlY KeyCode = internal.KeyEndOfMedium 96 | CtrlZ KeyCode = internal.KeySubstitution 97 | 98 | CtrlOpenBracket KeyCode = internal.KeyEscape 99 | CtrlBackslash KeyCode = internal.KeyFileSeparator 100 | CtrlCloseBracket KeyCode = internal.KeyGroupSeparator 101 | CtrlCaret KeyCode = internal.KeyRecordSeparator 102 | CtrlUnderscore KeyCode = internal.KeyUnitSeparator 103 | CtrlQuestionMark KeyCode = internal.KeyDelete 104 | ) 105 | 106 | // Other keys. 107 | const ( 108 | RuneKey KeyCode = -(iota + 1) 109 | Up 110 | Down 111 | Right 112 | Left 113 | ShiftTab 114 | Home 115 | End 116 | PgUp 117 | PgDown 118 | Delete 119 | Space 120 | CtrlUp 121 | CtrlDown 122 | CtrlRight 123 | CtrlLeft 124 | ShiftUp 125 | ShiftDown 126 | ShiftRight 127 | ShiftLeft 128 | CtrlShiftUp 129 | CtrlShiftDown 130 | CtrlShiftLeft 131 | CtrlShiftRight 132 | F1 133 | F2 134 | F3 135 | F4 136 | F5 137 | F6 138 | F7 139 | F8 140 | F9 141 | F10 142 | F11 143 | F12 144 | F13 145 | F14 146 | F15 147 | F16 148 | F17 149 | F18 150 | F19 151 | F20 152 | ) 153 | 154 | var keyNames = map[KeyCode]string{ 155 | // Control keys. 156 | internal.KeyNull: "ctrl+@", // also ctrl+backtick 157 | internal.KeyStartOfHeading: "ctrl+a", 158 | internal.KeyStartOfText: "ctrl+b", 159 | internal.KeyExit: "ctrl+c", 160 | internal.KeyEndOfTransimission: "ctrl+d", 161 | internal.KeyEnquiry: "ctrl+e", 162 | internal.KeyAcknowledge: "ctrl+f", 163 | internal.KeyBELL: "ctrl+g", 164 | internal.KeyBackspace: "ctrl+h", 165 | internal.KeyHorizontalTabulation: "tab", // also ctrl+i 166 | internal.KeyLineFeed: "ctrl+j", 167 | internal.KeyVerticalTabulation: "ctrl+k", 168 | internal.KeyFormFeed: "ctrl+l", 169 | internal.KeyCarriageReturn: "enter", 170 | internal.KeyShiftOut: "ctrl+n", 171 | internal.KeyShiftIn: "ctrl+o", 172 | internal.KeyDataLinkEscape: "ctrl+p", 173 | internal.KeyDeviceControl1: "ctrl+q", 174 | internal.KeyDeviceControl2: "ctrl+r", 175 | internal.KeyDeviceControl3: "ctrl+s", 176 | internal.KeyDeviceControl4: "ctrl+t", 177 | internal.KeyNegativeAcknowledge: "ctrl+u", 178 | internal.KeySynchronousIdle: "ctrl+v", 179 | internal.KeyEndOfTransmissionBlock: "ctrl+w", 180 | internal.KeyCancel: "ctrl+x", 181 | internal.KeyEndOfMedium: "ctrl+y", 182 | internal.KeySubstitution: "ctrl+z", 183 | internal.KeyEscape: "esc", 184 | internal.KeyFileSeparator: "ctrl+\\", 185 | internal.KeyGroupSeparator: "ctrl+]", 186 | internal.KeyRecordSeparator: "ctrl+^", 187 | internal.KeyUnitSeparator: "ctrl+_", 188 | internal.KeyDelete: "backspace", 189 | 190 | // Other keys. 191 | RuneKey: "runes", 192 | Up: "up", 193 | Down: "down", 194 | Right: "right", 195 | Space: "space", 196 | Left: "left", 197 | ShiftTab: "shift+tab", 198 | Home: "home", 199 | End: "end", 200 | PgUp: "pgup", 201 | PgDown: "pgdown", 202 | Delete: "delete", 203 | CtrlUp: "ctrl+up", 204 | CtrlDown: "ctrl+down", 205 | CtrlRight: "ctrl+right", 206 | CtrlLeft: "ctrl+left", 207 | ShiftUp: "shift+up", 208 | ShiftDown: "shift+down", 209 | ShiftRight: "shift+right", 210 | ShiftLeft: "shift+left", 211 | CtrlShiftUp: "ctrl+shift+up", 212 | CtrlShiftDown: "ctrl+shift+down", 213 | CtrlShiftLeft: "ctrl+shift+left", 214 | CtrlShiftRight: "ctrl+shift+right", 215 | F1: "f1", 216 | F2: "f2", 217 | F3: "f3", 218 | F4: "f4", 219 | F5: "f5", 220 | F6: "f6", 221 | F7: "f7", 222 | F8: "f8", 223 | F9: "f9", 224 | F10: "f10", 225 | F11: "f11", 226 | F12: "f12", 227 | F13: "f13", 228 | F14: "f14", 229 | F15: "f15", 230 | F16: "f16", 231 | F17: "f17", 232 | F18: "f18", 233 | F19: "f19", 234 | F20: "f20", 235 | } 236 | -------------------------------------------------------------------------------- /tty_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 3 | 4 | package keyboard 5 | 6 | import ( 7 | "os" 8 | 9 | "github.com/containerd/console" 10 | ) 11 | 12 | func restoreInput() error { 13 | if con != nil { 14 | return con.Reset() 15 | } 16 | return nil 17 | } 18 | 19 | func initInput() error { 20 | c, err := console.ConsoleFromFile(stdin) 21 | if err != nil { 22 | return err 23 | } 24 | con = c 25 | 26 | return nil 27 | } 28 | 29 | func openInputTTY() (*os.File, error) { 30 | f, err := os.Open("/dev/tty") 31 | if err != nil { 32 | return nil, err 33 | } 34 | return f, nil 35 | } 36 | -------------------------------------------------------------------------------- /tty_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package keyboard 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "syscall" 10 | 11 | "github.com/containerd/console" 12 | ) 13 | 14 | func restoreInput() error { 15 | if windowsStdin != nil { 16 | os.Stdin = windowsStdin 17 | } 18 | 19 | return nil 20 | } 21 | 22 | func initInput() error { 23 | windowsStdin = os.Stdin 24 | 25 | os.Stdin = stdin 26 | 27 | var mode uint32 28 | err := syscall.GetConsoleMode(syscall.Stdin, &mode) 29 | 30 | if err != nil { 31 | mocking = true 32 | return nil 33 | } 34 | 35 | con = console.Current() 36 | 37 | return nil 38 | } 39 | 40 | func openInputTTY() (*os.File, error) { 41 | f, err := os.OpenFile("CONIN$", os.O_RDWR, 0644) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to open stdin TTY: %w", err) 44 | } 45 | 46 | return f, nil 47 | } 48 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 3 | 4 | package keyboard 5 | 6 | func closeInput() { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /utils_windows.go: -------------------------------------------------------------------------------- 1 | package keyboard 2 | 3 | import "syscall" 4 | 5 | func closeInput() { 6 | syscall.CancelIoEx(syscall.Handle(inputTTY.Fd()), nil) 7 | } 8 | --------------------------------------------------------------------------------