├── .coveragerc
├── .editorconfig
├── .flake8
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── publish.yaml
│ └── run-tests.yaml
├── .gitignore
├── .isort.cfg
├── .pre-commit-config.yaml
├── .pytest.ini
├── .yamllint.yaml
├── CONTRIBUTING.md
├── LICENCE
├── Makefile
├── README.md
├── pyproject.toml
├── readchar
├── __init__.py
├── _base_key.py
├── _config.py
├── _posix_key.py
├── _posix_read.py
├── _win_key.py
├── _win_read.py
├── key.py
└── py.typed
├── requirements.txt
└── tests
├── linux
├── conftest.py
├── test_import.py
├── test_keys.py
├── test_readchar.py
└── test_readkey.py
├── manual-test.py
└── windows
├── conftest.py
├── test_import.py
├── test_keys.py
├── test_readchar.py
└── test_readkey.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit = tests/*
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | insert_final_newline = true
4 | trim_trailing_whitespace = true
5 |
6 | [*.py]
7 | indent_style = space
8 | indent_size = 4
9 | max_line_length = 88
10 |
11 | [*.md]
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.{yml,yaml}]
16 | indent_style = space
17 | indent_size = 2
18 | max_line_length = 120
19 |
20 | [Makefile]
21 | indent_style = tab
22 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-complexity = 12
3 | max-line-length = 88
4 | exclude =
5 | __pycache__/
6 | .git/
7 | .venv/
8 | .pytest_cache/
9 | show-source = true
10 | statistics = true
11 | count = true
12 | per-file-ignores =
13 | readchar/*_key.py:F403,F405
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ______________________________________________________________________
2 |
3 | name: Bug report about: Create a report to help us improve title: '' labels: bug
4 | assignees: ''
5 |
6 | ______________________________________________________________________
7 |
8 | ## Describe the bug
9 |
10 | A clear and concise description of what the bug is.
11 |
12 | ## To Reproduce
13 |
14 | Steps to reproduce the behaviour:
15 |
16 | 1. Use this code '...'
17 | 1. Do the following '....'
18 | 1. See error
19 |
20 | ## Expected behaviour
21 |
22 | A clear and concise description of what you expected to happen.
23 |
24 | ## Screenshots
25 |
26 | If applicable, add screenshots to help explain your problem.
27 |
28 | ## Environment (please complete the following information)
29 |
30 | - OS: \[e.g. Linux / Windows / macOS / etc.\]
31 | - python version: \[get by running: `python --version`\]
32 | - readchar version: \[get by running: `pip show readchar`\]
33 |
34 | ## Additional context
35 |
36 | Add any other context about the problem here.
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ______________________________________________________________________
2 |
3 | name: Feature request about: Suggest an idea for this project title: '' labels:
4 | enhancement assignees: ''
5 |
6 | ______________________________________________________________________
7 |
8 | ## Is your feature request related to a problem? Please describe.
9 |
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when
11 | \[...\]
12 |
13 | ## Describe the solution you'd like
14 |
15 | A clear and concise description of what you want to happen.
16 |
17 | ## Describe alternatives you've considered
18 |
19 | A clear and concise description of any alternative solutions or features you've
20 | considered.
21 |
22 | ## Additional context
23 |
24 | Add any other context or screenshots about the feature request here.
25 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # This workflow will upload a Python Package using Twine
3 | # For more information see:
4 | # https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
5 |
6 | name: Upload Python Package
7 |
8 | on:
9 | release:
10 | types: published
11 |
12 |
13 | jobs:
14 |
15 | check-version: # --------------------------------------------------------------------
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 |
21 | - name: get versions
22 | id: get
23 | run: |
24 | echo "NEW_VERSION=$(echo '${{ github.ref_name }}' | cut -c2-)" | tee -a $GITHUB_OUTPUT
25 | echo "OLD_VERSION=$(curl -s https://pypi.org/pypi/readchar/json |jq -r .info.version)" | tee -a $GITHUB_OUTPUT
26 |
27 | - name: validate version
28 | id: valid
29 | shell: python
30 | run: |
31 | from sys import exit
32 | from packaging import version
33 | new_version = version.parse("${{ steps.get.outputs.NEW_VERSION }}")
34 | old_version = version.parse("${{ steps.get.outputs.OLD_VERSION }}")
35 | if not new_version > old_version:
36 | print(f"::error::New version '{new_version}' not greatet than '{old_version}'")
37 | exit(1)
38 |
39 | outputs:
40 | version: ${{ steps.get.outputs.NEW_VERSION }}
41 |
42 |
43 | tag: # ------------------------------------------------------------------------------
44 | runs-on: ubuntu-latest
45 | needs: check-version
46 | permissions:
47 | contents: write
48 | env:
49 | VERSION: ${{ needs.check-version.outputs.version }}
50 | steps:
51 | - name: Checkout
52 | uses: actions/checkout@v4
53 |
54 | - name: update pyproject.toml
55 | run: sed -i -r "s/^(version = ).*$/\1\"$VERSION\"/" pyproject.toml
56 |
57 | - name: commit version
58 | env:
59 | USER: github-actions[bot]
60 | EMAIL: github-actions[bot]@users.noreply.github.com
61 | run: git -c user.name="$USER" -c user.email="$EMAIL" commit --all -m "release v$VERSION"
62 |
63 | - name: update tag
64 | run: git tag -f "v$VERSION"
65 |
66 | - name: push updates
67 | run: git push --tags -f
68 |
69 |
70 | deploy: # ---------------------------------------------------------------------------
71 | runs-on: ubuntu-latest
72 | needs: tag
73 | steps:
74 | - name: Checkout
75 | uses: actions/checkout@v4
76 | with:
77 | ref: ${{ github.ref }}
78 |
79 | - name: Set up Python
80 | uses: actions/setup-python@v5
81 | with:
82 | python-version: '3.x'
83 | cache: pip
84 |
85 | - name: Install dependencies
86 | run: pip install build twine
87 |
88 | - name: Build sdist and bdist_wheel
89 | run: python -m build
90 |
91 | - name: publish to PyPi
92 | env:
93 | TWINE_USERNAME: __token__
94 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
95 | run: twine upload dist/*
96 |
97 |
98 | increment: # ------------------------------------------------------------------------
99 | runs-on: ubuntu-latest
100 | needs: deploy
101 | steps:
102 | - name: Checkout
103 | uses: actions/checkout@v4
104 | with:
105 | ref: ${{ github.ref }}
106 | fetch-depth: 3
107 |
108 | - name: get versions
109 | id: get
110 | shell: python
111 | run: |
112 | from sys import exit
113 | from os import environ
114 | from packaging import version
115 | ver = version.parse("${{ github.ref_name }}")
116 | if ver.dev is not None:
117 | new_ver = f"{ver.base_version}-dev{ver.dev +1}"
118 | else:
119 | new_ver = f"{ver.major}.{ver.minor}.{ver.micro +1}-dev0"
120 | with open(environ.get("GITHUB_OUTPUT"), "a") as fp:
121 | towrite = f"NEW_VERSION={new_ver}"
122 | print(towrite)
123 | fp.write(towrite + "\n")
124 |
125 | - name: update pyproject.toml
126 | env:
127 | VERSION: ${{ steps.get.outputs.NEW_VERSION }}
128 | run: sed -i -r "s/^(version = ).*$/\1\"$VERSION\"/" pyproject.toml
129 |
130 | - name: commit version
131 | env:
132 | USER: github-actions[bot]
133 | EMAIL: github-actions[bot]@users.noreply.github.com
134 | run: git -c user.name="$USER" -c user.email="$EMAIL" commit --all -m "increment version after release"
135 |
136 | - name: push updates
137 | run: |
138 | git fetch origin 'refs/heads/*:refs/remotes/origin/*'
139 | git push origin "HEAD:$(git log --pretty='%D' | grep -oPm1 '(?<=origin/).*')"
140 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # This workflow will install Python dependencies, run tests and lint with a
3 | # variety of Python versions
4 | # For more information see:
5 | # https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
6 |
7 | name: Tests
8 |
9 | on:
10 | pull_request:
11 | branches:
12 | - 'master'
13 | push:
14 | branches:
15 | - '**'
16 | tags-ignore:
17 | - '**'
18 |
19 |
20 | jobs:
21 |
22 | pre-commit:
23 | runs-on: ubuntu-latest
24 | steps:
25 | - name: Checkout
26 | uses: actions/checkout@v3
27 | - name: Set up Python
28 | uses: actions/setup-python@v4
29 | with:
30 | python-version: '3.x'
31 | - name: Run pre-commit
32 | uses: pre-commit/action@v2.0.3
33 |
34 | build:
35 | runs-on: ubuntu-latest
36 | steps:
37 | - name: Checkout
38 | uses: actions/checkout@v3
39 | - name: Set up Python
40 | uses: actions/setup-python@v4
41 | with:
42 | python-version: '3.x'
43 | cache: pip
44 | - run: |
45 | pip install build
46 | - run: |
47 | python -m build
48 |
49 | pytest:
50 | runs-on: ${{ matrix.os }}
51 | strategy:
52 | matrix:
53 | os:
54 | - ubuntu-latest
55 | - windows-latest
56 | python-version:
57 | - '3.8'
58 | - '3.9'
59 | - '3.10'
60 | - '3.11'
61 | - '3.12'
62 | steps:
63 | - name: Checkout
64 | uses: actions/checkout@v3
65 | - name: Set up Python ${{ matrix.python-version }}
66 | uses: actions/setup-python@v4
67 | with:
68 | python-version: ${{ matrix.python-version }}
69 | cache: pip
70 | - name: Install dependencies
71 | run: |
72 | pip install -r requirements.txt
73 | pip install coveralls
74 | pip install -e .
75 | - name: Test with pytest
76 | run: |
77 | pytest
78 | - name: Coverage upload
79 | env:
80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
81 | COVERALLS_PARALLEL: true
82 | COVERALLS_FLAG_NAME: ${{ join(matrix.*, ',') }}
83 | run: |
84 | coveralls --service=github
85 |
86 | finish-coveralls:
87 | needs: pytest
88 | runs-on: ubuntu-latest
89 | steps:
90 | - name: Set up Python
91 | uses: actions/setup-python@v4
92 | with:
93 | python-version: '3.x'
94 | - name: Install dependencies
95 | run: |
96 | pip install coveralls
97 | - name: Coverage finish
98 | env:
99 | GITHUB_TOKEN: ${{ secrets.github_token }}
100 | run: |
101 | coveralls --service=github --finish
102 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | \#*
3 | \.\#*
4 |
5 | # Python Files
6 | /.venv/
7 | __pychache__/
8 | *.pyc
9 |
10 | # Build files:
11 | /build/
12 | /dist/
13 | *.egg-info/
14 | .eggs
15 | *.egg
16 |
17 | # Testing files:
18 | /.pytest_cache/
19 | .coverage
20 | coverage.xml
21 |
--------------------------------------------------------------------------------
/.isort.cfg:
--------------------------------------------------------------------------------
1 | [settings]
2 | profile = black
3 | src_paths = readchar,tests
4 | lines_after_imports = 2
5 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | repos:
3 |
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v4.4.0
6 | hooks:
7 | - name: remove trailing whitespace
8 | id: trailing-whitespace
9 | - name: add newline to end of files
10 | id: end-of-file-fixer
11 | - name: sort requirements.txt
12 | id: requirements-txt-fixer
13 |
14 | - repo: https://github.com/pycqa/isort
15 | rev: 5.12.0
16 | hooks:
17 | - name: sort python imports
18 | id: isort
19 |
20 | - repo: https://github.com/psf/black
21 | rev: 23.1.0
22 | hooks:
23 | - name: format python files
24 | id: black
25 | language_version: python3
26 |
27 | - repo: https://github.com/pycqa/flake8
28 | rev: 6.0.0
29 | hooks:
30 | - name: check python syntax
31 | id: flake8
32 |
33 | - repo: https://github.com/executablebooks/mdformat
34 | rev: 0.7.16
35 | hooks:
36 | - name: format Markdown files
37 | id: mdformat
38 | args:
39 | - --wrap=88
40 | - --end-of-line=keep
41 | additional_dependencies:
42 | - mdformat-gfm
43 |
44 | - repo: https://github.com/adrienverge/yamllint
45 | rev: v1.29.0
46 | hooks:
47 | - name: check YAML syntax + format
48 | id: yamllint
49 |
--------------------------------------------------------------------------------
/.pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | testpaths = tests
3 | addopts = -r fEsxwX -s --cov=readchar
4 |
--------------------------------------------------------------------------------
/.yamllint.yaml:
--------------------------------------------------------------------------------
1 | extends: default
2 |
3 | ignore: |
4 | .venv/*
5 |
6 | rules:
7 | indentation:
8 | spaces: 2
9 | line-length:
10 | max: 120
11 | new-lines:
12 | type: platform
13 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to readchar
2 |
3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4 |
5 | The following is a set of guidelines for contributing to this GitHub project. These are
6 | mostly guidelines, not rules. Use your best judgment, and feel free to propose changes
7 | to this document in a pull request.
8 |
9 | ## Opening an issue
10 |
11 | If you want to open an issue about a problem or bug you encountered, simply go to the
12 | GitHub page and click _New issue_ in the _issues_ section. You will be presented with
13 | templates to use, choose a relevant one. (If no template fits, you can open a blank
14 | issue. But make sure you input all the appropriate information!)
15 |
16 | Fill out the template. You should at least provide the following information:
17 |
18 | - a short but exact description of your problem (screenshots often help)
19 | - steps on how to reproduce the problem
20 | - Information about your system:
21 | - your OS
22 | - your Python version and implementation
23 | - the version of `readchar` you use
24 |
25 | ## Opening a pull request
26 |
27 | Follow these steps if you want to contribute code to the project:
28 |
29 | 1. [Fork](https://github.com/magmax/python-readchar/fork) this Git repository and create
30 | your branch from `master`.
31 |
32 | 1. Check out the code to your local machine by following the steps in
33 | [Getting the code](#getting-the-code) and make your changes.
34 |
35 | 1. **Make sure [the tests pass](#run-the-tests)!!**
36 |
37 | 1. If you added to the source code, add tests for your new code.
38 |
39 | 1. Update the documentation, if necessary.
40 |
41 | 1. Write a
42 | [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
43 | if you have multiple changes, make sure your commits are atomic (single irreducible
44 | change that makes sense on its own)
45 |
46 | 1. Done, you can open a pull request.
47 |
48 | ## Getting the code
49 |
50 | If you want to experiment with the code yourself, you can get started by following these
51 | steps.
52 |
53 | 1. Clone the repository. (or yours if you created a fork)
54 |
55 | ```bash
56 | git clone https://github.com/magmax/python-readchar.git
57 | cd python-readchar
58 | ```
59 |
60 | 1. Create a virtual environment:
61 |
62 | ```bash
63 | python -m venv .venv
64 | ```
65 |
66 | 1. Enter the virtual environment
67 |
68 | on Linux systems:
69 |
70 | ```bash
71 | source .venv/bin/activate
72 | ```
73 |
74 | or for Windows systems:
75 |
76 | ```bash
77 | .venv\Scripts\activate
78 | ```
79 |
80 | 1. Install dev-dependencies (this also automatically installs the library in editable
81 | mode)
82 |
83 | ```bash
84 | pip install -r requirements.txt
85 | ```
86 |
87 | ### Run the tests!
88 |
89 | Always make sure all tests pass before suggesting any changes! This will avoid invalid
90 | PR's.
91 |
92 | The simplest way is to just run `make`. The provided Makefile calls all tests for you.
93 |
94 | ```bash
95 | make
96 | ```
97 |
98 | If you don't have `make`, you could run all tests manually like this:
99 |
100 | - run `pytest` (source-code testing)
101 |
102 | ```bash
103 | pytest
104 | ```
105 |
106 | - run `pre-commit` (linting and styling)
107 |
108 | ```bash
109 | pre-commit run -a
110 | ```
111 |
112 | - run `setup.py` (to test build process)
113 |
114 | ```bash
115 | python setup.py sdist bdist_wheel
116 | ```
117 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT Licence
2 |
3 | Copyright (c) 2022 Miguel Angel Garcia
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, sublicence, 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 | .PHONY: tests build readchar
2 |
3 |
4 | # default target:
5 | all: tests pre-commit build
6 |
7 | test tests:
8 | @pytest
9 |
10 | pre-commit precommit:
11 | @pre-commit run -a
12 |
13 | build pack readchar:
14 | @python -m build
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/magmax/python-readchar)
2 | [](https://pypi.python.org/pypi/readchar)
3 | [](https://pypi.python.org/pypi/readchar)
4 | [](LICENCE)
5 | [](https://github.com/magmax/python-readchar/actions/workflows/run-tests.yaml?query=branch%3Amaster)
6 | [](https://coveralls.io/github/magmax/python-readchar?branch=master)
7 | [](https://pypi.python.org/pypi/readchar)
8 |
9 | # python-readchar
10 |
11 | Library to easily read single chars and keystrokes.
12 |
13 | Born as a [python-inquirer](https://github.com/magmax/python-inquirer) requirement.
14 |
15 | ## Installation
16 |
17 | simply install it via `pip`:
18 |
19 | ```bash
20 | pip install readchar
21 | ```
22 |
23 | Or download the source code from [PyPi](https://pypi.python.org/pypi/readchar).
24 |
25 | ## Usage
26 |
27 | Simply read a character or keystroke:
28 |
29 | ```python
30 | import readchar
31 |
32 | key = readchar.readkey()
33 | ```
34 |
35 | React to different kinds of key-presses:
36 |
37 | ```python
38 | from readchar import readkey, key
39 |
40 | while True:
41 | k = readkey()
42 | if k == "a":
43 | # do stuff
44 | if k == key.DOWN:
45 | # do stuff
46 | if k == key.ENTER:
47 | break
48 | ```
49 |
50 | ## Documentation
51 |
52 | There are just two methods:
53 |
54 | ### `readchar.readchar() -> str`
55 |
56 | Reads one character from `stdin`, returning it as a string with length 1. Waits until a
57 | character is available.
58 |
59 | As only ASCII characters are actually a single character, you usually want to use the
60 | next function, that also handles longer keys.
61 |
62 | ### `readchar.readkey() -> str`
63 |
64 | Reads the next keystroke from `stdin`, returning it as a string. Waits until a keystroke
65 | is available.
66 |
67 | A keystroke can be:
68 |
69 | - single characters as returned by `readchar()`. These include:
70 | - character for normal keys: a, Z, 9,...
71 | - special characters like ENTER, BACKSPACE, TAB,...
72 | - combinations with CTRL: CTRL+A,...
73 | - keys that are made up of multiple characters:
74 | - characters for cursors/arrows: 🡩, 🡪, 🡫,
75 | 🡨
76 | - navigation keys: INSERT, HOME,...
77 | - function keys: F1 to F12
78 | - combinations with ALT: ALT+A,...
79 | - combinations with CTRL and ALT:
80 | CTRL+ALT+SUPR,...
81 |
82 | > **Note** CTRL+C will not be returned by `readkey()`, but instead
83 | > raise a `KeyboardInterupt`. If you want to handle it yourself, use `readchar()`.
84 |
85 | ### `readchar.key` module
86 |
87 | This submodule contains a list of available keys to compare against. The constants are
88 | defined depending on your operating system, so it should be fully portable. If a key is
89 | listed here for your platform, `readkey()` can read it, and you can compare against it.
90 |
91 | ### `readchar.config` class
92 |
93 | This static class contains configurations for `readchar`. It holds constants that are
94 | used in other parts of the code as class attributes. You can override/change these to
95 | modify its behaviour. Here is a description of the existing attributes:
96 |
97 |
98 | INTERRUPT_KEYS
99 | -
100 |
101 | List of keys that will result in `readkey()` raising a `KeyboardInterrupt`.
102 | *Default:* `[key.CTRL_C]`
103 |
104 |
105 |
106 |
107 | ## OS Support
108 |
109 | This library actively supports these operating systems:
110 |
111 | - Linux
112 | - Windows
113 |
114 | Some operating systems are enabled, but not actively tested or supported:
115 |
116 | - macOS
117 | - FreeBSD / OpenBSD
118 |
119 | Theoretically every Unix based system should work, but they will not be actively tested.
120 | It is also required that somebody provides initial test results before the OS is enabled
121 | and added to the list. Feel free to open a PR for that.
122 |
123 | Thank you!
124 |
125 | ## How to contribute
126 |
127 | You have an issue problem or found a bug? You have a great new idea or just want to fix
128 | a typo? Great :+1:. We are happy to accept your issue or pull request, but first, please
129 | read our
130 | [contribution guidelines](https://github.com/magmax/python-readchar/blob/master/CONTRIBUTING.md).
131 | They will also tell you how to write code for this repo and how to properly prepare an
132 | issue or a pull request.
133 |
134 | ______________________________________________________________________
135 |
136 | *Copyright (c) 2014-2022 Miguel Ángel García*
137 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools >= 61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "readchar"
7 | version = "4.2.2-dev0"
8 | requires-python = ">= 3.8"
9 | dependencies = []
10 | authors = [
11 | { name = "Miguel Ángel García", email="miguelangel.garcia@gmail.com" },
12 | { name = "Jan Wille", email = "mail@janwille.de" },
13 | ]
14 | maintainers = [{ name = "Jan Wille", email = "mail@janwille.de" }]
15 | keywords = ["characters", "keystrokes", "stdin", "command line"]
16 | classifiers = [
17 | "Development Status :: 5 - Production/Stable",
18 | "License :: OSI Approved :: MIT License",
19 | "Operating System :: Microsoft :: Windows",
20 | "Operating System :: POSIX :: Linux",
21 | "Programming Language :: Python :: 3",
22 | "Programming Language :: Python :: 3 :: Only",
23 | "Programming Language :: Python :: 3.8",
24 | "Programming Language :: Python :: 3.9",
25 | "Programming Language :: Python :: 3.10",
26 | "Programming Language :: Python :: 3.11",
27 | "Programming Language :: Python :: 3.12",
28 | "Programming Language :: Python :: Implementation :: CPython",
29 | "Environment :: Console",
30 | "Intended Audience :: Developers",
31 | "Topic :: Software Development",
32 | "Topic :: Software Development :: User Interfaces",
33 | ]
34 | description = "Library to easily read single chars and key strokes"
35 | readme = { file = "README.md", content-type = "text/markdown" }
36 | license = { file = "LICENCE" }
37 |
38 | [project.urls]
39 | Homepage = "https://pypi.org/project/readchar"
40 | #Documentation = "https://readthedocs.org"
41 | Repository = "https://github.com/magmax/python-readchar"
42 | Issues = "https://github.com/magmax/python-readchar/issues"
43 | Changelog = "https://github.com/magmax/python-readchar/releases"
44 |
--------------------------------------------------------------------------------
/readchar/__init__.py:
--------------------------------------------------------------------------------
1 | import importlib.metadata
2 |
3 |
4 | __doc__ = """Library to easily read single chars and key strokes"""
5 |
6 | __version__ = importlib.metadata.version(__package__)
7 | __all__ = ["readchar", "readkey", "key", "config"]
8 |
9 | from sys import platform
10 |
11 | from ._config import config
12 |
13 |
14 | if platform.startswith(("linux", "darwin", "freebsd", "openbsd")):
15 | from . import _posix_key as key
16 | from ._posix_read import readchar, readkey
17 | elif platform in ("win32", "cygwin"):
18 | from . import _win_key as key
19 | from ._win_read import readchar, readkey
20 | else:
21 | raise NotImplementedError(f"The platform {platform} is not supported yet")
22 |
--------------------------------------------------------------------------------
/readchar/_base_key.py:
--------------------------------------------------------------------------------
1 | # common
2 | LF = "\x0a"
3 | CR = "\x0d"
4 | SPACE = "\x20"
5 | ESC = "\x1b"
6 | TAB = "\x09"
7 |
8 | # CTRL
9 | CTRL_A = "\x01"
10 | CTRL_B = "\x02"
11 | CTRL_C = "\x03"
12 | CTRL_D = "\x04"
13 | CTRL_E = "\x05"
14 | CTRL_F = "\x06"
15 | CTRL_G = "\x07"
16 | CTRL_H = "\x08"
17 | CTRL_I = TAB
18 | CTRL_J = LF
19 | CTRL_K = "\x0b"
20 | CTRL_L = "\x0c"
21 | CTRL_M = CR
22 | CTRL_N = "\x0e"
23 | CTRL_O = "\x0f"
24 | CTRL_P = "\x10"
25 | CTRL_Q = "\x11"
26 | CTRL_R = "\x12"
27 | CTRL_S = "\x13"
28 | CTRL_T = "\x14"
29 | CTRL_U = "\x15"
30 | CTRL_V = "\x16"
31 | CTRL_W = "\x17"
32 | CTRL_X = "\x18"
33 | CTRL_Y = "\x19"
34 | CTRL_Z = "\x1a"
35 |
--------------------------------------------------------------------------------
/readchar/_config.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from . import _base_key as key
4 |
5 |
6 | class config:
7 | """Static class that contains Constants used throughout the library.
8 | You can directly use the class-attributes, do not create instances of it!"""
9 |
10 | def __new__(cls):
11 | raise SyntaxError("you can't create instances of this class")
12 |
13 | INTERRUPT_KEYS: List[str] = [key.CTRL_C]
14 |
--------------------------------------------------------------------------------
/readchar/_posix_key.py:
--------------------------------------------------------------------------------
1 | from ._base_key import *
2 |
3 |
4 | # common
5 | BACKSPACE = "\x7f"
6 |
7 | # cursors
8 | UP = "\x1b\x5b\x41"
9 | DOWN = "\x1b\x5b\x42"
10 | LEFT = "\x1b\x5b\x44"
11 | RIGHT = "\x1b\x5b\x43"
12 |
13 | # navigation keys
14 | INSERT = "\x1b\x5b\x32\x7e"
15 | SUPR = "\x1b\x5b\x33\x7e"
16 | HOME = "\x1b\x5b\x48"
17 | END = "\x1b\x5b\x46"
18 | PAGE_UP = "\x1b\x5b\x35\x7e"
19 | PAGE_DOWN = "\x1b\x5b\x36\x7e"
20 |
21 | # function keys
22 | F1 = "\x1b\x4f\x50"
23 | F2 = "\x1b\x4f\x51"
24 | F3 = "\x1b\x4f\x52"
25 | F4 = "\x1b\x4f\x53"
26 | F5 = "\x1b\x5b\x31\x35\x7e"
27 | F6 = "\x1b\x5b\x31\x37\x7e"
28 | F7 = "\x1b\x5b\x31\x38\x7e"
29 | F8 = "\x1b\x5b\x31\x39\x7e"
30 | F9 = "\x1b\x5b\x32\x30\x7e"
31 | F10 = "\x1b\x5b\x32\x31\x7e"
32 | F11 = "\x1b\x5b\x32\x33\x7e"
33 | F12 = "\x1b\x5b\x32\x34\x7e"
34 |
35 | # SHIFT+_
36 | SHIFT_TAB = "\x1b\x5b\x5a"
37 |
38 | # other
39 | CTRL_ALT_SUPR = "\x1b\x5b\x33\x5e"
40 |
41 | # ALT+_
42 | ALT_A = "\x1b\x61"
43 |
44 | # CTRL+ALT+_
45 | CTRL_ALT_A = "\x1b\x01"
46 |
47 |
48 | # aliases
49 | ENTER = LF
50 | DELETE = SUPR
51 |
--------------------------------------------------------------------------------
/readchar/_posix_read.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import termios
3 |
4 | from ._config import config
5 |
6 |
7 | # Initially taken from:
8 | # http://code.activestate.com/recipes/134892/
9 | # Thanks to Danny Yoo
10 | # more infos from:
11 | # https://gist.github.com/michelbl/efda48b19d3e587685e3441a74457024
12 | # Thanks to Michel Blancard
13 | def readchar() -> str:
14 | """Reads a single character from the input stream.
15 | Blocks until a character is available."""
16 |
17 | fd = sys.stdin.fileno()
18 | old_settings = termios.tcgetattr(fd)
19 | term = termios.tcgetattr(fd)
20 | try:
21 | term[3] &= ~(termios.ICANON | termios.ECHO | termios.IGNBRK | termios.BRKINT)
22 | termios.tcsetattr(fd, termios.TCSAFLUSH, term)
23 |
24 | ch = sys.stdin.read(1)
25 | finally:
26 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
27 | return ch
28 |
29 |
30 | def readkey() -> str:
31 | """Get a keypress. If an escaped key is pressed, the full sequence is
32 | read and returned as noted in `_posix_key.py`."""
33 |
34 | c1 = readchar()
35 |
36 | if c1 in config.INTERRUPT_KEYS:
37 | raise KeyboardInterrupt
38 |
39 | if c1 != "\x1B":
40 | return c1
41 |
42 | c2 = readchar()
43 | if c2 not in "\x4F\x5B":
44 | return c1 + c2
45 |
46 | c3 = readchar()
47 | if c3 not in "\x31\x32\x33\x35\x36":
48 | return c1 + c2 + c3
49 |
50 | c4 = readchar()
51 | if c4 not in "\x30\x31\x33\x34\x35\x37\x38\x39":
52 | return c1 + c2 + c3 + c4
53 |
54 | c5 = readchar()
55 | return c1 + c2 + c3 + c4 + c5
56 |
--------------------------------------------------------------------------------
/readchar/_win_key.py:
--------------------------------------------------------------------------------
1 | from ._base_key import *
2 |
3 |
4 | # Windows uses scan codes for extended characters. This dictionary
5 | # translates the second half of the scan codes of special Keys
6 | # into the corresponding variable used by readchar.
7 | #
8 | # for windows scan codes see:
9 | # https://msdn.microsoft.com/en-us/library/aa299374
10 | # or
11 | # https://www.freepascal.org/docs-html/rtl/keyboard/kbdscancode.html
12 |
13 | # common
14 | BACKSPACE = "\x08"
15 |
16 | # cursors
17 | UP = "\x00\x48"
18 | DOWN = "\x00\x50"
19 | LEFT = "\x00\x4b"
20 | RIGHT = "\x00\x4d"
21 |
22 | # navigation keys
23 | INSERT = "\x00\x52"
24 | SUPR = "\x00\x53"
25 | HOME = "\x00\x47"
26 | END = "\x00\x4f"
27 | PAGE_UP = "\x00\x49"
28 | PAGE_DOWN = "\x00\x51"
29 |
30 | # function keys
31 | F1 = "\x00\x3b"
32 | F2 = "\x00\x3c"
33 | F3 = "\x00\x3d"
34 | F4 = "\x00\x3e"
35 | F5 = "\x00\x3f"
36 | F6 = "\x00\x40"
37 | F7 = "\x00\x41"
38 | F8 = "\x00\x42"
39 | F9 = "\x00\x43"
40 | F10 = "\x00\x44"
41 | F11 = "\x00\x85" # only in second source
42 | F12 = "\x00\x86" # only in second source
43 |
44 | # other
45 | ESC_2 = "\x00\x01"
46 | ENTER_2 = "\x00\x1c"
47 |
48 | # don't have table entries for...
49 | # ALT_[A-Z]
50 | # CTRL_ALT_[A-Z],
51 | # CTRL_ALT_SUPR,
52 | # CTRL-F1
53 |
54 |
55 | # aliases
56 | ENTER = CR
57 | DELETE = SUPR
58 |
--------------------------------------------------------------------------------
/readchar/_win_read.py:
--------------------------------------------------------------------------------
1 | import msvcrt
2 |
3 | from ._config import config
4 |
5 |
6 | def readchar() -> str:
7 | """Reads a single utf8-character from the input stream.
8 | Blocks until a character is available."""
9 |
10 | # read a single wide character from the input
11 | return msvcrt.getwch()
12 |
13 |
14 | def readkey() -> str:
15 | """Reads the next keypress. If an escaped key is pressed, the full
16 | sequence is read and returned as noted in `_win_key.py`."""
17 |
18 | # read first character
19 | ch = readchar()
20 |
21 | # keys like CTRL+C should cause a interrupt
22 | if ch in config.INTERRUPT_KEYS:
23 | raise KeyboardInterrupt
24 |
25 | # parse special multi character keys (see key module)
26 | # https://learn.microsoft.com/cpp/c-runtime-library/reference/getch-getwch#remarks
27 | if ch in "\x00\xe0":
28 | # read the second half
29 | # we always return the 0x00 prefix, this avoids duplications in the key module
30 | ch = "\x00" + readchar()
31 |
32 | # parse unicode surrogates
33 | # https://docs.python.org/3/c-api/unicode.html#c.Py_UNICODE_IS_SURROGATE
34 | if "\uD800" <= ch <= "\uDFFF":
35 | ch += readchar()
36 |
37 | # combine the characters into a single utf-16 encoded string.
38 | # this prevents the character from being treated as a surrogate pair again.
39 | ch = ch.encode("utf-16", errors="surrogatepass").decode("utf-16")
40 |
41 | return ch
42 |
--------------------------------------------------------------------------------
/readchar/key.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa E401,E403
2 | # this file exists only for backwards compatibility
3 | # it allows the use of `import readchar.key`
4 | import sys
5 |
6 | from . import key as __key
7 |
8 |
9 | for __k, __v in vars(__key).items():
10 | if not __k.startswith("__"):
11 | setattr(sys.modules[__name__], __k, __v)
12 |
--------------------------------------------------------------------------------
/readchar/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magmax/python-readchar/2204f05e47b7e5c6209a49dfde0ad64938e439a5/readchar/py.typed
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | -e .
2 | build
3 | pre-commit
4 | pytest>=6.0
5 | pytest-cov
6 | wheel
7 |
--------------------------------------------------------------------------------
/tests/linux/conftest.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 |
5 |
6 | if sys.platform.startswith("linux"):
7 | import termios
8 |
9 |
10 | # ignore all tests in this folder if not on linux
11 | def pytest_ignore_collect(path, config):
12 | if not sys.platform.startswith("linux"):
13 | return True
14 |
15 |
16 | @pytest.fixture
17 | def patched_stdin():
18 | class mocked_stdin:
19 | buffer = []
20 |
21 | def push(self, string):
22 | for c in string:
23 | self.buffer.append(c)
24 |
25 | def read(self, n):
26 | string = ""
27 | for i in range(n):
28 | string += self.buffer.pop(0)
29 | return string
30 |
31 | def mock_tcgetattr(fd):
32 | return [0, 0, 0, 0, None, None, None]
33 |
34 | def mock_tcsetattr(fd, TCSADRAIN, old_settings):
35 | return None
36 |
37 | mock = mocked_stdin()
38 | with pytest.MonkeyPatch.context() as mp:
39 | mp.setattr(sys.stdin, "read", mock.read)
40 | mp.setattr(termios, "tcgetattr", mock_tcgetattr)
41 | mp.setattr(termios, "tcsetattr", mock_tcsetattr)
42 | yield mock
43 |
--------------------------------------------------------------------------------
/tests/linux/test_import.py:
--------------------------------------------------------------------------------
1 | import readchar
2 |
3 |
4 | def test_readcharImport():
5 | assert readchar.readchar == readchar._posix_read.readchar
6 |
7 |
8 | def test_readkeyImport():
9 | assert readchar.readkey == readchar._posix_read.readkey
10 |
11 |
12 | def test_keyImport():
13 | a = {k: v for k, v in vars(readchar.key).items() if not k.startswith("__")}
14 | b = {k: v for k, v in vars(readchar._posix_key).items() if not k.startswith("__")}
15 | assert a == b
16 |
--------------------------------------------------------------------------------
/tests/linux/test_keys.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from readchar import key as keys
4 |
5 |
6 | defaultKeys = ["LF", "CR", "ENTER", "BACKSPACE", "SPACE", "ESC", "TAB"]
7 | CTRLkeys = [
8 | "CTRL_A",
9 | "CTRL_B",
10 | "CTRL_C",
11 | "CTRL_D",
12 | "CTRL_E",
13 | "CTRL_F",
14 | "CTRL_G",
15 | "CTRL_H",
16 | "CTRL_I",
17 | "CTRL_J",
18 | "CTRL_K",
19 | "CTRL_L",
20 | "CTRL_M",
21 | "CTRL_N",
22 | "CTRL_O",
23 | "CTRL_P",
24 | "CTRL_Q",
25 | "CTRL_R",
26 | "CTRL_S",
27 | "CTRL_T",
28 | "CTRL_U",
29 | "CTRL_V",
30 | "CTRL_W",
31 | "CTRL_X",
32 | "CTRL_Y",
33 | "CTRL_Z",
34 | ]
35 |
36 |
37 | @pytest.mark.parametrize("key", defaultKeys + CTRLkeys)
38 | def test_defaultKeysExists(key):
39 | assert key in keys.__dict__
40 |
41 |
42 | @pytest.mark.parametrize("key", defaultKeys + CTRLkeys)
43 | def test_defaultKeysLength(key):
44 | assert 1 == len(keys.__dict__[key])
45 |
46 |
47 | len3_keys = ["UP", "DOWN", "LEFT", "RIGHT", "HOME", "END", "F1", "F2", "F3", "F4"]
48 |
49 |
50 | @pytest.mark.parametrize("key", len3_keys)
51 | def test_character_length_3_exists(key):
52 | assert key in keys.__dict__
53 |
54 |
55 | @pytest.mark.parametrize("key", len3_keys)
56 | def test_character_length_3_lenght(key):
57 | assert 3 == len(keys.__dict__[key])
58 |
59 |
60 | len4_keys = ["INSERT", "SUPR", "DELETE", "PAGE_UP", "PAGE_DOWN"]
61 |
62 |
63 | @pytest.mark.parametrize("key", len4_keys)
64 | def test_character_length_4_exists(key):
65 | assert key in keys.__dict__
66 |
67 |
68 | @pytest.mark.parametrize("key", len4_keys)
69 | def test_character_length_4_lenght(key):
70 | assert 4 == len(keys.__dict__[key])
71 |
72 |
73 | len5_keys = ["F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
74 |
75 |
76 | @pytest.mark.parametrize("key", len5_keys)
77 | def test_character_length_5_exists(key):
78 | assert key in keys.__dict__
79 |
80 |
81 | @pytest.mark.parametrize("key", len5_keys)
82 | def test_character_length_5_lenght(key):
83 | assert 5 == len(keys.__dict__[key])
84 |
--------------------------------------------------------------------------------
/tests/linux/test_readchar.py:
--------------------------------------------------------------------------------
1 | from string import printable
2 |
3 | import pytest
4 |
5 | from readchar import key, readchar
6 |
7 |
8 | @pytest.mark.parametrize("c", printable)
9 | def test_printableCharacters(patched_stdin, c):
10 | patched_stdin.push(c)
11 | assert c == readchar()
12 |
13 |
14 | @pytest.mark.parametrize(
15 | ["seq", "key"],
16 | [
17 | ("\n", key.LF),
18 | ("\n", key.ENTER),
19 | ("\r", key.CR),
20 | ("\x7f", key.BACKSPACE),
21 | ("\x20", key.SPACE),
22 | ("\x1b", key.ESC),
23 | ("\t", key.TAB),
24 | ],
25 | )
26 | def test_controlCharacters(seq, key, patched_stdin):
27 | patched_stdin.push(seq)
28 | assert key == readchar()
29 |
30 |
31 | @pytest.mark.parametrize(
32 | ["seq", "key"],
33 | [
34 | ("\x01", key.CTRL_A),
35 | ("\x02", key.CTRL_B),
36 | ("\x03", key.CTRL_C),
37 | ("\x04", key.CTRL_D),
38 | ("\x05", key.CTRL_E),
39 | ("\x06", key.CTRL_F),
40 | ("\x07", key.CTRL_G),
41 | ("\x08", key.CTRL_H),
42 | ("\x09", key.CTRL_I),
43 | ("\x0a", key.CTRL_J),
44 | ("\x0b", key.CTRL_K),
45 | ("\x0c", key.CTRL_L),
46 | ("\x0d", key.CTRL_M),
47 | ("\x0e", key.CTRL_N),
48 | ("\x0f", key.CTRL_O),
49 | ("\x10", key.CTRL_P),
50 | ("\x11", key.CTRL_Q),
51 | ("\x12", key.CTRL_R),
52 | ("\x13", key.CTRL_S),
53 | ("\x14", key.CTRL_T),
54 | ("\x15", key.CTRL_U),
55 | ("\x16", key.CTRL_V),
56 | ("\x17", key.CTRL_W),
57 | ("\x18", key.CTRL_X),
58 | ("\x19", key.CTRL_Y),
59 | ("\x1a", key.CTRL_Z),
60 | ],
61 | )
62 | def test_CTRL_Characters(seq, key, patched_stdin):
63 | patched_stdin.push(seq)
64 | assert key == readchar()
65 |
--------------------------------------------------------------------------------
/tests/linux/test_readkey.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from readchar import key, readkey
4 |
5 |
6 | @pytest.mark.parametrize("key", ["\x03", key.CTRL_C])
7 | def test_KeyboardInterrupt(key, patched_stdin):
8 | patched_stdin.push(key)
9 | with pytest.raises(KeyboardInterrupt):
10 | readkey()
11 |
12 |
13 | def test_singleCharacter(patched_stdin):
14 | patched_stdin.push("a")
15 | assert "a" == readkey()
16 |
17 |
18 | @pytest.mark.parametrize(
19 | ["seq", "key"],
20 | [
21 | ("\x1b\x5b\x41", key.UP),
22 | ("\x1b\x5b\x42", key.DOWN),
23 | ("\x1b\x5b\x44", key.LEFT),
24 | ("\x1b\x5b\x43", key.RIGHT),
25 | ],
26 | )
27 | def test_cursorsKeys(seq, key, patched_stdin):
28 | patched_stdin.push(seq)
29 | assert key == readkey()
30 |
31 |
32 | @pytest.mark.parametrize(
33 | ["seq", "key"],
34 | [
35 | ("\x1b\x5b\x32\x7e", key.INSERT),
36 | ("\x1b\x5b\x33\x7e", key.SUPR),
37 | ("\x1b\x5b\x48", key.HOME),
38 | ("\x1b\x5b\x46", key.END),
39 | ("\x1b\x5b\x35\x7e", key.PAGE_UP),
40 | ("\x1b\x5b\x36\x7e", key.PAGE_DOWN),
41 | ],
42 | )
43 | def test_navigationKeys(seq, key, patched_stdin):
44 | patched_stdin.push(seq)
45 | assert key == readkey()
46 |
47 |
48 | @pytest.mark.parametrize(
49 | ["seq", "key"],
50 | [
51 | (key.F1, "\x1b\x4f\x50"),
52 | (key.F2, "\x1b\x4f\x51"),
53 | (key.F3, "\x1b\x4f\x52"),
54 | (key.F4, "\x1b\x4f\x53"),
55 | (key.F5, "\x1b\x5b\x31\x35\x7e"),
56 | (key.F6, "\x1b\x5b\x31\x37\x7e"),
57 | (key.F7, "\x1b\x5b\x31\x38\x7e"),
58 | (key.F8, "\x1b\x5b\x31\x39\x7e"),
59 | (key.F9, "\x1b\x5b\x32\x30\x7e"),
60 | (key.F10, "\x1b\x5b\x32\x31\x7e"),
61 | (key.F11, "\x1b\x5b\x32\x33\x7e"),
62 | (key.F12, "\x1b\x5b\x32\x34\x7e"),
63 | ],
64 | )
65 | def test_functionKeys(seq, key, patched_stdin):
66 | patched_stdin.push(seq)
67 | assert key == readkey()
68 |
--------------------------------------------------------------------------------
/tests/manual-test.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa E231
2 | from readchar import key, readkey
3 |
4 |
5 | # construct an inverted code -> key-name mapping
6 | # we need to reverse the items so that aliases won't override the original name later on
7 | known_keys = {v: k for k, v in reversed(vars(key).items()) if not k.startswith("__")}
8 |
9 |
10 | def main():
11 | while True:
12 | read_key = readkey()
13 | mykey = f"got {known_keys[read_key]}" if read_key in known_keys else read_key
14 |
15 | print(f"{mykey} - 0x{ read_key.encode().hex() }")
16 |
17 |
18 | if __name__ == "__main__":
19 | try:
20 | main()
21 | except KeyboardInterrupt:
22 | print("\nKeyboardInterrupt")
23 |
--------------------------------------------------------------------------------
/tests/windows/conftest.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 |
5 |
6 | # ignore all tests in this folder if not on windows
7 | def pytest_ignore_collect(path, config):
8 | if sys.platform not in ("win32", "cygwin"):
9 | return True
10 |
11 |
12 | @pytest.fixture
13 | def patched_stdin(monkeypatch):
14 | class mocked_stdin:
15 | def push(self, string):
16 | # Create an iterator from the string
17 | characters = iter(string)
18 |
19 | # Patch msvcrt.getwch to return the next character from the iterator.
20 | monkeypatch.setattr("msvcrt.getwch", lambda: next(characters))
21 |
22 | return mocked_stdin()
23 |
--------------------------------------------------------------------------------
/tests/windows/test_import.py:
--------------------------------------------------------------------------------
1 | import readchar
2 |
3 |
4 | def test_readcharImport():
5 | assert readchar.readchar == readchar._win_read.readchar
6 |
7 |
8 | def test_readkeyImport():
9 | assert readchar.readkey == readchar._win_read.readkey
10 |
11 |
12 | def test_keyImport():
13 | a = {k: v for k, v in vars(readchar.key).items() if not k.startswith("__")}
14 | b = {k: v for k, v in vars(readchar._win_key).items() if not k.startswith("__")}
15 | assert a == b
16 |
--------------------------------------------------------------------------------
/tests/windows/test_keys.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from readchar import key as keys
4 |
5 |
6 | defaultKeys = ["LF", "CR", "ENTER", "BACKSPACE", "SPACE", "ESC", "TAB"]
7 | CTRLkeys = [
8 | "CTRL_A",
9 | "CTRL_B",
10 | "CTRL_C",
11 | "CTRL_D",
12 | "CTRL_E",
13 | "CTRL_F",
14 | "CTRL_G",
15 | "CTRL_H",
16 | "CTRL_I",
17 | "CTRL_J",
18 | "CTRL_K",
19 | "CTRL_L",
20 | "CTRL_M",
21 | "CTRL_N",
22 | "CTRL_O",
23 | "CTRL_P",
24 | "CTRL_Q",
25 | "CTRL_R",
26 | "CTRL_S",
27 | "CTRL_T",
28 | "CTRL_U",
29 | "CTRL_V",
30 | "CTRL_W",
31 | "CTRL_X",
32 | "CTRL_Y",
33 | "CTRL_Z",
34 | ]
35 |
36 |
37 | @pytest.mark.parametrize("key", defaultKeys + CTRLkeys)
38 | def test_defaultKeysExists(key):
39 | assert key in keys.__dict__
40 |
41 |
42 | @pytest.mark.parametrize("key", defaultKeys + CTRLkeys)
43 | def test_defaultKeysLength(key):
44 | assert 1 == len(keys.__dict__[key])
45 |
46 |
47 | specialKeys = [
48 | "INSERT",
49 | "SUPR",
50 | "DELETE",
51 | "PAGE_UP",
52 | "PAGE_DOWN",
53 | "HOME",
54 | "END",
55 | "UP",
56 | "DOWN",
57 | "LEFT",
58 | "RIGHT",
59 | "F1",
60 | "F2",
61 | "F3",
62 | "F4",
63 | "F5",
64 | "F6",
65 | "F7",
66 | "F8",
67 | "F9",
68 | "F10",
69 | "F11",
70 | "F12",
71 | ]
72 |
73 |
74 | @pytest.mark.parametrize("key", specialKeys)
75 | def test_specialKeysExists(key):
76 | assert key in keys.__dict__
77 |
78 |
79 | @pytest.mark.parametrize("key", specialKeys)
80 | def test_specialKeysLength(key):
81 | assert 2 == len(keys.__dict__[key])
82 |
--------------------------------------------------------------------------------
/tests/windows/test_readchar.py:
--------------------------------------------------------------------------------
1 | from string import printable
2 |
3 | import pytest
4 |
5 | from readchar import key, readchar
6 |
7 |
8 | @pytest.mark.parametrize("c", printable)
9 | def test_printableCharacters(patched_stdin, c):
10 | patched_stdin.push(c)
11 | assert c == readchar()
12 |
13 |
14 | @pytest.mark.parametrize(
15 | ["seq", "key"],
16 | [
17 | ("\n", key.LF),
18 | ("\r", key.ENTER),
19 | ("\r", key.CR),
20 | ("\x08", key.BACKSPACE),
21 | ("\x20", key.SPACE),
22 | ("\x1b", key.ESC),
23 | ("\t", key.TAB),
24 | ],
25 | )
26 | def test_controlCharacters(seq, key, patched_stdin):
27 | patched_stdin.push(seq)
28 | assert key == readchar()
29 |
30 |
31 | @pytest.mark.parametrize(
32 | ["seq", "key"],
33 | [
34 | ("\x01", key.CTRL_A),
35 | ("\x02", key.CTRL_B),
36 | ("\x03", key.CTRL_C),
37 | ("\x04", key.CTRL_D),
38 | ("\x05", key.CTRL_E),
39 | ("\x06", key.CTRL_F),
40 | ("\x07", key.CTRL_G),
41 | ("\x08", key.CTRL_H),
42 | ("\x09", key.CTRL_I),
43 | ("\x0a", key.CTRL_J),
44 | ("\x0b", key.CTRL_K),
45 | ("\x0c", key.CTRL_L),
46 | ("\x0d", key.CTRL_M),
47 | ("\x0e", key.CTRL_N),
48 | ("\x0f", key.CTRL_O),
49 | ("\x10", key.CTRL_P),
50 | ("\x11", key.CTRL_Q),
51 | ("\x12", key.CTRL_R),
52 | ("\x13", key.CTRL_S),
53 | ("\x14", key.CTRL_T),
54 | ("\x15", key.CTRL_U),
55 | ("\x16", key.CTRL_V),
56 | ("\x17", key.CTRL_W),
57 | ("\x18", key.CTRL_X),
58 | ("\x19", key.CTRL_Y),
59 | ("\x1a", key.CTRL_Z),
60 | ],
61 | )
62 | def test_CTRL_Characters(seq, key, patched_stdin):
63 | patched_stdin.push(seq)
64 | assert key == readchar()
65 |
66 |
67 | @pytest.mark.parametrize(
68 | ["seq", "key"],
69 | [
70 | ("\xe4", "ä"),
71 | ("\xe1", "á"),
72 | ("\xe5", "å"),
73 | ("\xdf", "ß"),
74 | ("\u304c", "が"),
75 | ],
76 | )
77 | def test_Unicode_Characters(seq, key, patched_stdin):
78 | patched_stdin.push(seq)
79 | assert key == readchar()
80 |
--------------------------------------------------------------------------------
/tests/windows/test_readkey.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from readchar import key, readkey
4 |
5 |
6 | @pytest.mark.parametrize("key", ["\x03", key.CTRL_C])
7 | def test_KeyboardInterrupt(key, patched_stdin):
8 | patched_stdin.push(key)
9 | with pytest.raises(KeyboardInterrupt):
10 | readkey()
11 |
12 |
13 | def test_singleCharacter(patched_stdin):
14 | patched_stdin.push("a")
15 | assert "a" == readkey()
16 |
17 |
18 | @pytest.mark.parametrize(
19 | ["seq", "key"],
20 | [
21 | ("\x00\x48", key.UP),
22 | ("\x00\x50", key.DOWN),
23 | ("\x00\x4b", key.LEFT),
24 | ("\x00\x4d", key.RIGHT),
25 | ],
26 | )
27 | def test_cursorsKeys(seq, key, patched_stdin):
28 | patched_stdin.push(seq)
29 | assert key == readkey()
30 |
31 |
32 | @pytest.mark.parametrize(
33 | ["seq", "key"],
34 | [
35 | ("\x00\x52", key.INSERT),
36 | ("\x00\x53", key.SUPR),
37 | ("\x00\x47", key.HOME),
38 | ("\x00\x4f", key.END),
39 | ("\x00\x49", key.PAGE_UP),
40 | ("\x00\x51", key.PAGE_DOWN),
41 | ],
42 | )
43 | def test_navigationKeys(seq, key, patched_stdin):
44 | patched_stdin.push(seq)
45 | assert key == readkey()
46 |
47 |
48 | @pytest.mark.parametrize(
49 | ["seq", "key"],
50 | [
51 | ("\x00\x3b", key.F1),
52 | ("\x00\x3c", key.F2),
53 | ("\x00\x3d", key.F3),
54 | ("\x00\x3e", key.F4),
55 | ("\x00\x3f", key.F5),
56 | ("\x00\x40", key.F6),
57 | ("\x00\x41", key.F7),
58 | ("\x00\x42", key.F8),
59 | ("\x00\x43", key.F9),
60 | ("\x00\x44", key.F10),
61 | ("\x00\x85", key.F11),
62 | ("\x00\x86", key.F12),
63 | ],
64 | )
65 | def test_functionKeys(seq, key, patched_stdin):
66 | patched_stdin.push(seq)
67 | assert key == readkey()
68 |
69 |
70 | @pytest.mark.parametrize(
71 | ["seq", "key"],
72 | [
73 | ("\ud83d\ude00", "😀"),
74 | ("\ud83d\ude18", "😘"),
75 | ("\ud83d\ude09", "😉"),
76 | ("\ud83d\udc4d", "👍"),
77 | ("\ud83d\udc35", "🐵"),
78 | ("\ud83c\udf47", "🍇"),
79 | ("\ud83c\udf83", "🎃"),
80 | ("\ud83d\udc53", "👓"),
81 | ("\ud83c\udfc1", "🏁"),
82 | ],
83 | )
84 | def test_UnicodeSurrogates(seq, key, patched_stdin):
85 | patched_stdin.push(seq)
86 | assert key == readkey()
87 |
--------------------------------------------------------------------------------