├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
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 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ![]()
54 | -----------------------------------------------------------------------------------------------------
55 |
56 | |
57 |
58 |
59 |
60 | go get atomicgo.dev/keyboard
61 |
62 |
63 |
64 |
65 | ![]()
66 | -----------------------------------------------------------------------------------------------------
67 |
68 | |
69 |
70 |
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 |
--------------------------------------------------------------------------------