├── .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 | [![GitHub Repository](https://img.shields.io/badge/-GitHub-%230D0D0D?logo=github&labelColor=gray)](https://github.com/magmax/python-readchar) 2 | [![Latest PyPi version](https://img.shields.io/pypi/v/readchar.svg)](https://pypi.python.org/pypi/readchar) 3 | [![supported Python versions](https://img.shields.io/pypi/pyversions/readchar)](https://pypi.python.org/pypi/readchar) 4 | [![Project licence](https://img.shields.io/pypi/l/readchar?color=blue)](LICENCE)
5 | [![Automated testing results](https://img.shields.io/github/actions/workflow/status/magmax/python-readchar/run-tests.yaml?branch=master)](https://github.com/magmax/python-readchar/actions/workflows/run-tests.yaml?query=branch%3Amaster) 6 | [![Coveralls results](https://coveralls.io/repos/github/magmax/python-readchar/badge.svg?branch=master)](https://coveralls.io/github/magmax/python-readchar?branch=master) 7 | [![Number of PyPi downloads](https://img.shields.io/pypi/dd/readchar.svg)](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 | --------------------------------------------------------------------------------