diff --git a/.coveragerc b/.coveragerc
index e7d592e..c712d25 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,2 +1,2 @@
[run]
-omit = */tests*
+omit = tests/*
diff --git a/.flake8 b/.flake8
index c057b0f..9a3ac51 100644
--- a/.flake8
+++ b/.flake8
@@ -1,2 +1,11 @@
[flake8]
-max-line-length=119
+exclude =
+ __pycache__/
+ .git/
+ .venv/
+ .pytest_cache/
+show-source = true
+statistics = true
+count = true
+max-complexity = 12
+max-line-length = 88
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..8a8274e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+## Describe the bug
+
+A clear and concise description of what the bug is.
+
+## To Reproduce
+
+Steps to reproduce the behaviour:
+
+1. Use this code '...'
+2. Do the following '....'
+3. See error
+
+## Expected behaviour
+
+A clear and concise description of what you expected to happen.
+
+## Screenshots
+
+If applicable, add screenshots to help explain your problem.
+
+## Environment (please complete the following information)
+
+- OS: [e.g. Linux / Windows / macOS / etc.]
+- python version: [get by running: `python --version`]
+- readchar version: [get by running: `pip show readchar`]
+
+## Additional context
+
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..6f7e174
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,24 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+## Is your feature request related to a problem? Please describe.
+
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+## Describe the solution you'd like
+
+A clear and concise description of what you want to happen.
+
+## Describe alternatives you've considered
+
+A clear and concise description of any alternative solutions or features you've considered.
+
+## Additional context
+
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
deleted file mode 100644
index 55e230b..0000000
--- a/.github/workflows/pre-commit.yml
+++ /dev/null
@@ -1,18 +0,0 @@
----
-name: pre-commit
-
-on:
- push:
- branches:
- - master
- pull_request:
- branches:
- - master
-
-jobs:
- pre-commit:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
- - uses: pre-commit/action@v2.0.3
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index d8e6a51..7bc558b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,34 +1,41 @@
---
-# This workflow will upload a Python Package using Twine when a release is
-# created
+# This workflow will upload a Python Package using Twine
# For more information see:
# https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Upload Python Package
on:
- release:
- types: [created]
+ push:
+ tags:
+ - 'v*.*.*'
+
jobs:
- deploy:
+ deploy:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - name: Checkout
+ uses: actions/checkout@v3
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
- python-version: 3.x
+ python-version: '3.x'
+ cache: pip
- name: Install dependencies
run: |
- python -m pip install --upgrade pip
pip install setuptools wheel twine
- - name: Build and publish
+ - name: Write Version
+ run: |
+ sed -i "s/__version__ = .*/__version__ = '${GITHUB_REF#refs/*/v}'/" readchar/__init__.py
+ - name: Build sdist and bdist_wheel
+ run: |
+ python setup.py sdist bdist_wheel
+ - name: publish to PyPi
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
- python setup.py sdist bdist_wheel
twine upload dist/*
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 9f6ea51..0bec7fc 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -4,40 +4,92 @@
# For more information see:
# https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
-name: Python package
+name: Tests
on:
- push:
- branches:
- - master
- pull_request:
- branches:
- - master
+ - push
+ - pull_request
+
jobs:
- build:
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.x'
+ - name: Run pre-commit
+ uses: pre-commit/action@v2.0.3
+
+ build:
runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.x'
+ cache: pip
+ - run: |
+ pip install setuptools wheel
+ - run: |
+ python setup.py sdist bdist_wheel
+
+ pytest:
+ runs-on: ${{ matrix.os }}
strategy:
matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
python-version:
- - "3.5"
- - "3.6"
- - "3.7"
- - "3.8"
- - "3.9"
- - "3.10"
-
+ - '3.6'
+ - '3.7'
+ - '3.8'
+ - '3.9'
+ - '3.10'
steps:
- - uses: actions/checkout@v2
+ - name: Checkout
+ uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
+ cache: pip
- name: Install dependencies
run: |
- python -m pip install --upgrade pip
- python -m pip install -r requirements-test.txt
+ pip install -r requirements.txt
+ pip install coveralls
+ pip install -e .
- name: Test with pytest
run: |
pytest
+ - name: Coverage upload
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COVERALLS_PARALLEL: true
+ COVERALLS_FLAG_NAME: ${{ join(matrix.*, ',') }}
+ run: |
+ coveralls --service=github
+
+ finish-coveralls:
+ needs: pytest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.x'
+ - name: Install dependencies
+ run: |
+ pip install coveralls
+ - name: Coverage finish
+ env:
+ GITHUB_TOKEN: ${{ secrets.github_token }}
+ run: |
+ coveralls --service=github --finish
diff --git a/.gitignore b/.gitignore
index 4301da0..047f31d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,20 @@
*~
\#*
\.\#*
+
+# Python Files
+/.venv/
+__pychache__/
*.pyc
+
+# Build files:
+/build/
+/dist/
*.egg-info/
-build/
-dist/
-venv/
-.coverage
-coverage.xml
.eggs
*.egg
+
+# Testing files:
+/.pytest_cache/
+.coverage
+coverage.xml
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 45f221f..2b45dc5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,30 +1,29 @@
---
repos:
- - repo: https://github.com/adrienverge/yamllint
- rev: v1.26.3
- hooks:
- - name: check YAML format
- id: yamllint
+
- repo: https://github.com/psf/black
- rev: 22.1.0
+ rev: 22.6.0
hooks:
- name: re-format with black
id: black
language_version: python3
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.1.0
+ rev: v4.3.0
hooks:
- name: remove whitespaces
id: trailing-whitespace
+ - name: add newline to end of files
+ id: end-of-file-fixer
- - repo: https://gitlab.com/pycqa/flake8
- rev: 21d3c70d676007470908d39b73f0521d39b3b997
+ - repo: https://github.com/pycqa/flake8
+ rev: 4.0.1
hooks:
- name: check-format with flake8
id: flake8
- args:
- - --show-source
- - --statistics
- - --count
- - --max-complexity=12
+
+ - repo: https://github.com/adrienverge/yamllint
+ rev: v1.27.1
+ hooks:
+ - name: check YAML format
+ id: yamllint
diff --git a/.yamllint.yaml b/.yamllint.yaml
deleted file mode 100644
index e9147c4..0000000
--- a/.yamllint.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-extends: default
-
-rules:
- truthy:
- ignore: |
- .github/workflows/*
- indentation:
- spaces: 2
diff --git a/.yamllint.yml b/.yamllint.yml
new file mode 100644
index 0000000..c2a3114
--- /dev/null
+++ b/.yamllint.yml
@@ -0,0 +1,12 @@
+extends: default
+
+ignore: |
+ .venv/*
+
+rules:
+ indentation:
+ spaces: 2
+ line-length:
+ max: 120
+ new-lines:
+ type: platform
diff --git a/LICENCE b/LICENCE
new file mode 100644
index 0000000..66937d1
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,21 @@
+MIT Licence
+
+Copyright (c) 2022 Miguel Angel Garcia
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicence, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
index f5ea67e..db56984 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,10 @@
-all: precommit test
+all: test precommit pack
test:
- python setup.py test
+ @pytest
-precommit::
- pre-commit run -a
+precommit:
+ @pre-commit run -a
-publish:
- @python setup.py bdist_wheel upload
+pack:
+ @python setup.py sdist bdist_wheel
diff --git a/README.rst b/README.rst
index 611cdac..c7a04a2 100644
--- a/README.rst
+++ b/README.rst
@@ -1,38 +1,28 @@
-See it at:
+| |GitHub badge| |PyPi badge| |python versions badge| |licence badge|
+| |test status| |coverage status| |pip downloads badge|
-- `pypi`_
-- `GitHub`_
+python-readchar
+***************
-============== =============== ========= ============
-VERSION DOWNLOADS TESTS COVERAGE
-============== =============== ========= ============
-|pip version| |pip downloads| |travis| |coveralls|
-============== =============== ========= ============
-
-Library to easily read single chars and key strokes.
-
-Goal and Philosophy
-===================
+Library to easily read single chars and keystrokes.
Born as a `python-inquirer`_ requirement.
-The idea is to have a portable way to read **single** characters and **key-strokes**.
-
-
-Documentation
-=============
Installation
-------------
+============
-::
+simply install it via :code:`pip`:
+
+.. code:: bash
pip install readchar
-The :code:`readchar` library works with python 2.7, 3.4, 3.5, 3.6 and Pypy.
+or download the source code from PyPi_.
+
Usage
------
+=====
Usage example:
@@ -44,44 +34,87 @@ Usage example:
key = readchar.readkey()
API
-----
+===
There are just two methods:
-:code:`readchar()`
-//////////////////
+:code:`readchar(blocking=True) -> str | None`
+---------------------------------------------
Reads the next char from :code:`stdin`, returning it as a string with length 1.
+By default, it will block until a character is available, but if you pass :code:`blocking=False` the function will not block and return :code:`None`
+if no character is available.
+
-:code:`readkey()`
-/////////////////
+:code:`readkey() -> str`
+------------------------
-Reads the next key-stroke from :code:`stdin`, returning it as a string.
+Reads the next keystroke from :code:`stdin`, returning it as a string. Waits until a keystroke is available.
-A key-stroke can have:
+A keystroke can be:
-- 1 character for normal keys: 'a', 'z', '9'...
-- 2 characters for combinations with ALT: ALT+A, ...
-- 3 characters for cursors: ->, <-, ...
-- 4 characters for combinations with CTRL and ALT: CTRL+ALT+SUPR, ...
+- single characters as returned by :code:`readchar()`. These include:
-There is a list of previously captured chars with their names in :code:`readchar.key`, in order to be used in comparisons and so on. This list is not enough tested and it can have mistakes, so use it carefully. Please, report them if found.
+ - character for normal keys: 'a', 'Z', '9'...
+ - special characters like 'ENTER', 'BACKSPACE', 'TAB'...
+ - combinations with 'CTRL': 'CTRL'+'A',...
+
+- keys that are made up of multiple characters:
+
+ - characters for cursors/arrows: 🡩, 🡪, 🡫, 🡨
+ - navigation keys: 'INSERT', 'HOME',...
+ - function keys: 'F1' to 'F12'
+ - combinations with 'ALT': 'ALT'+'A',...
+ - combinations with 'CTRL' and 'ALT': 'CTRL'+'ALT'+'SUPR',...
+
+.. attention::
+
+ 'CTRL'+'C' will not be returned by :code:`readkey()`, but instead raise a :code:`KeyboardInterupt`. If you what to handle it yourself,
+ use :code:`readchar()`. Also note that using the none-blocking version may result in unexpected behaviour for :code:`KeyboardInterupt`.
+
+
+:code:`key.py` module
+---------------------
+
+This submodule contains a list of available keys to compare against. You can use it like this:
+
+.. code:: python
+
+ from readchar import readkey, key
+
+ while True:
+ k = readkey()
+ if k == key.UP:
+ # do stuff
+ if k == key.DOWN:
+ # do stuff
+ if k == key.ENTER:
+ # do stuff
OS Support
-----------
+==========
+
+This library support both Linux and Windows, but on Windows the :code:`key` submodule has fewer keys available.
-Sadly, this library has only being probed on GNU/Linux. Please, if you can try it in another OS and find a bug, put an issue or send the pull-request.
+Currently unsupported, but enabled operating systems:
+
+- macOS
+- FreeBSD
+
+Theoretically every Unix based system should work, but they will not be actively tested. It is also required that somebody provides initial test
+results before the OS is enabled and added to the list. Feel free to open a PR for that.
Thank you!
+
How to contribute
=================
-You can download the code, make some changes with their tests, and make a pull-request.
+You can download the code, make some changes with their tests, and open a pull-request.
-In order to develop or running the tests, you can do:
+In order to develop and run the tests, follow these steps:
1. Clone the repository.
@@ -93,21 +126,27 @@ In order to develop or running the tests, you can do:
.. code:: bash
- virtualenv venv
+ python -m venv .venv
-3. Enter in the virtual environment
+3. Enter the virtual environment
.. code:: bash
- source venv/bin/activate
+ source .venv/bin/activate
4. Install dependencies
.. code:: bash
- pip install -r requirements-test.txt
+ pip install -r requirements.txt
-5. Run tests
+5. Install the local version of readchar (in edit mode, so it automatically reflects changes)
+
+.. code:: bash
+
+ pip install -e .
+
+6. Run tests
.. code:: bash
@@ -117,39 +156,42 @@ In order to develop or running the tests, you can do:
Please, **Execute the tests before any pull-request**. This will avoid invalid builds.
-License
+Licence
=======
-Copyright (c) 2014-2021 Miguel Angel Garcia (`@magmax_en`_).
-
-Based on previous work on gist `getch()-like unbuffered character reading from stdin on both Windows and Unix (Python recipe)`_, started by `Danny Yoo`_.
+Copyright (c) 2014-2022 Miguel Angel Garcia (`@magmax_en`_).
-Licensed under `the MIT license`_.
+Based on previous work on gist `getch()-like unbuffered character reading from stdin on both Windows and Unix (Python recipe)
+`_, started by Danny Yoo as well as gist
+`kbhit.py `_ by Michel Blancard.
+Licensed under `the MIT licence `_.
-.. |travis| image:: https://travis-ci.org/magmax/python-readchar.png
- :target: `Travis`_
- :alt: Travis results
-.. |coveralls| image:: https://coveralls.io/repos/magmax/python-readchar/badge.png
- :target: `Coveralls`_
- :alt: Coveralls results_
-
-.. |pip version| image:: https://img.shields.io/pypi/v/readchar.svg
- :target: https://pypi.python.org/pypi/readchar
+.. |GitHub badge| image:: https://badges.aleen42.com/src/github.svg
+ :target: GitHub_
+ :alt: GitHub Repository
+.. |PyPi badge| image:: https://img.shields.io/pypi/v/readchar.svg
+ :target: PyPi_
:alt: Latest PyPI version
-
-.. |pip downloads| image:: https://img.shields.io/pypi/dm/readchar.svg
- :target: https://pypi.python.org/pypi/readchar
+.. |Python versions badge| image:: https://img.shields.io/pypi/pyversions/readchar
+ :target: PyPi_
+ :alt: supported Python versions
+.. |licence badge| image:: https://img.shields.io/pypi/l/readchar?color=blue
+ :target: licence_
+ :alt: Project licence
+.. |test status| image:: https://github.com/magmax/python-readchar/actions/workflows/run-tests.yml/badge.svg
+ :target: github.com/magmax/python-readchar/actions/workflows/run-tests.yml?query=branch%3Amaster
+ :alt: Automated testing results
+.. |coverage status| image:: https://coveralls.io/repos/github/magmax/python-readchar/badge.svg?branch=master
+ :target: https://coveralls.io/github/magmax/python-readchar?branch=master
+ :alt: Coveralls results
+.. |pip downloads badge| image:: https://img.shields.io/pypi/dd/readchar.svg
+ :target: PyPi_
:alt: Number of PyPI downloads
-.. _pypi: https://pypi.python.org/pypi/readchar
.. _GitHub: https://github.com/magmax/python-readchar
+.. _PyPi: https://pypi.python.org/pypi/readchar
+.. _licence: LICENCE
.. _python-inquirer: https://github.com/magmax/python-inquirer
-.. _Travis: https://travis-ci.org/magmax/python-readchar
-.. _Coveralls: https://coveralls.io/r/magmax/python-readchar
.. _@magmax_en: https://twitter.com/magmax_en
-
-.. _the MIT license: http://opensource.org/licenses/MIT
-.. _getch()-like unbuffered character reading from stdin on both Windows and Unix (Python recipe): http://code.activestate.com/recipes/134892/
-.. _Danny Yoo: http://code.activestate.com/recipes/users/98032/
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..f43c620
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tests
+addopts = -r fEsxwX -s --cov=readchar
diff --git a/readchar/__init__.py b/readchar/__init__.py
index f4ffd5c..a713d05 100644
--- a/readchar/__init__.py
+++ b/readchar/__init__.py
@@ -1,4 +1,19 @@
-from . import key
-from .readchar import readchar, readkey
+"""Library to easily read single chars and key strokes"""
-__all__ = [readchar, readkey, key]
+__version__ = "0.0.0-dev"
+__all__ = ["readchar", "readkey", "key"]
+
+
+from sys import platform
+
+
+if (
+ platform.startswith("linux")
+ or platform == "darwin"
+ or platform.startswith("freebsd")
+):
+ from .posix_read import readchar, readkey
+elif platform in ("win32", "cygwin"):
+ from .win_read import readchar, readkey
+else:
+ raise NotImplementedError(f"The platform {platform} is not supported yet")
diff --git a/readchar/key.py b/readchar/key.py
index 865427f..a57b9b0 100644
--- a/readchar/key.py
+++ b/readchar/key.py
@@ -1,98 +1,14 @@
-# common
-LF = "\x0d"
-CR = "\x0a"
-ENTER = "\x0d"
-BACKSPACE = "\x08"
-SUPR = ""
-SPACE = "\x20"
-ESC = "\x1b"
-
-# CTRL
-CTRL_A = "\x01"
-CTRL_B = "\x02"
-CTRL_C = "\x03"
-CTRL_D = "\x04"
-CTRL_E = "\x05"
-CTRL_F = "\x06"
-CTRL_G = "\x07"
-CTRL_H = "\x08"
-CTRL_I = "\t"
-CTRL_J = "\n"
-CTRL_K = "\x0b"
-CTRL_L = "\x0c"
-CTRL_M = "\r"
-CTRL_N = "\x0e"
-CTRL_O = "\x0f"
-CTRL_P = "\x10"
-CTRL_Q = "\x11"
-CTRL_R = "\x12"
-CTRL_S = "\x13"
-CTRL_T = "\x14"
-CTRL_U = "\x15"
-CTRL_V = "\x16"
-CTRL_W = "\x17"
-CTRL_X = "\x18"
-CTRL_Y = "\x19"
-CTRL_Z = "\x1a"
-
-# ALT
-ALT_A = "\x1b\x61"
-
-# CTRL + ALT
-CTRL_ALT_A = "\x1b\x01"
-
-# cursors
-UP = "\x1b\x5b\x41"
-DOWN = "\x1b\x5b\x42"
-LEFT = "\x1b\x5b\x44"
-RIGHT = "\x1b\x5b\x43"
-
-CTRL_ALT_SUPR = "\x1b\x5b\x33\x5e"
-
-# other
-F1 = "\x1b\x4f\x50"
-F2 = "\x1b\x4f\x51"
-F3 = "\x1b\x4f\x52"
-F4 = "\x1b\x4f\x53"
-F5 = "\x1b\x4f\x31\x35\x7e"
-F6 = "\x1b\x4f\x31\x37\x7e"
-F7 = "\x1b\x4f\x31\x38\x7e"
-F8 = "\x1b\x4f\x31\x39\x7e"
-F9 = "\x1b\x4f\x32\x30\x7e"
-F10 = "\x1b\x4f\x32\x31\x7e"
-F11 = "\x1b\x4f\x32\x33\x7e"
-F12 = "\x1b\x4f\x32\x34\x7e"
-
-PAGE_UP = "\x1b\x5b\x35\x7e"
-PAGE_DOWN = "\x1b\x5b\x36\x7e"
-HOME = "\x1b\x5b\x48"
-END = "\x1b\x5b\x46"
-
-INSERT = "\x1b\x5b\x32\x7e"
-SUPR = "\x1b\x5b\x33\x7e"
-
-
-ESCAPE_SEQUENCES = (
- ESC,
- ESC + "\x5b",
- ESC + "\x5b" + "\x31",
- ESC + "\x5b" + "\x32",
- ESC + "\x5b" + "\x33",
- ESC + "\x5b" + "\x35",
- ESC + "\x5b" + "\x36",
- ESC + "\x5b" + "\x31" + "\x35",
- ESC + "\x5b" + "\x31" + "\x36",
- ESC + "\x5b" + "\x31" + "\x37",
- ESC + "\x5b" + "\x31" + "\x38",
- ESC + "\x5b" + "\x31" + "\x39",
- ESC + "\x5b" + "\x32" + "\x30",
- ESC + "\x5b" + "\x32" + "\x31",
- ESC + "\x5b" + "\x32" + "\x32",
- ESC + "\x5b" + "\x32" + "\x33",
- ESC + "\x5b" + "\x32" + "\x34",
- ESC + "\x4f",
- ESC + ESC,
- ESC + ESC + "\x5b",
- ESC + ESC + "\x5b" + "\x32",
- ESC + ESC + "\x5b" + "\x33",
-)
+# flake8: noqa E401,E403
+
+from . import platform
+
+if (
+ platform.startswith("linux")
+ or platform == "darwin"
+ or platform.startswith("freebsd")
+):
+ from .posix_key import *
+elif platform in ("win32", "cygwin"):
+ from .win_key import *
+else:
+ raise NotImplementedError(f"The platform {platform} is not supported yet")
diff --git a/readchar/posix_key.py b/readchar/posix_key.py
new file mode 100644
index 0000000..cb18392
--- /dev/null
+++ b/readchar/posix_key.py
@@ -0,0 +1,75 @@
+# common
+LF = "\x0a"
+CR = "\x0d"
+ENTER = LF
+BACKSPACE = "\x7f"
+SPACE = "\x20"
+ESC = "\x1b"
+TAB = "\x09"
+
+# CTRL
+CTRL_A = "\x01"
+CTRL_B = "\x02"
+CTRL_C = "\x03"
+CTRL_D = "\x04"
+CTRL_E = "\x05"
+CTRL_F = "\x06"
+CTRL_G = "\x07"
+CTRL_H = "\x08"
+CTRL_I = TAB
+CTRL_J = LF
+CTRL_K = "\x0b"
+CTRL_L = "\x0c"
+CTRL_M = CR
+CTRL_N = "\x0e"
+CTRL_O = "\x0f"
+CTRL_P = "\x10"
+CTRL_Q = "\x11"
+CTRL_R = "\x12"
+CTRL_S = "\x13"
+CTRL_T = "\x14"
+CTRL_U = "\x15"
+CTRL_V = "\x16"
+CTRL_W = "\x17"
+CTRL_X = "\x18"
+CTRL_Y = "\x19"
+CTRL_Z = "\x1a"
+
+# cursors
+UP = "\x1b\x5b\x41"
+DOWN = "\x1b\x5b\x42"
+LEFT = "\x1b\x5b\x44"
+RIGHT = "\x1b\x5b\x43"
+
+# navigation keys
+INSERT = "\x1b\x5b\x32\x7e"
+SUPR = "\x1b\x5b\x33\x7e"
+DELETE = SUPR
+HOME = "\x1b\x5b\x48"
+END = "\x1b\x5b\x46"
+PAGE_UP = "\x1b\x5b\x35\x7e"
+PAGE_DOWN = "\x1b\x5b\x36\x7e"
+
+# funcion keys
+F1 = "\x1b\x4f\x50"
+F2 = "\x1b\x4f\x51"
+F3 = "\x1b\x4f\x52"
+F4 = "\x1b\x4f\x53"
+F5 = "\x1b\x5b\x31\x35\x7e"
+F6 = "\x1b\x5b\x31\x37\x7e"
+F7 = "\x1b\x5b\x31\x38\x7e"
+F8 = "\x1b\x5b\x31\x39\x7e"
+F9 = "\x1b\x5b\x32\x30\x7e"
+F10 = "\x1b\x5b\x32\x31\x7e"
+F11 = "\x1b\x5b\x32\x33\x7e"
+F12 = "\x1b\x5b\x32\x34\x7e"
+
+# ALT
+ALT_A = "\x1b\x61"
+
+# SHIFT
+SHIFT_TAB = "\x1b\x5b\x5a"
+
+# CTRL + ALT
+CTRL_ALT_A = "\x1b\x01"
+CTRL_ALT_SUPR = "\x1b\x5b\x33\x5e"
diff --git a/readchar/posix_read.py b/readchar/posix_read.py
new file mode 100644
index 0000000..82315e6
--- /dev/null
+++ b/readchar/posix_read.py
@@ -0,0 +1,59 @@
+import sys
+import termios
+import tty
+import select
+
+
+# idea from:
+# https://repolinux.wordpress.com/2012/10/09/non-blocking-read-from-stdin-in-python/
+# Thanks to REPOLINUX
+def kbhit():
+ return sys.stdin in select.select([sys.stdin], [], [], 0)[0]
+
+
+# Initially taken from:
+# http://code.activestate.com/recipes/134892/
+# Thanks to Danny Yoo
+def readchar(blocking=True):
+ """Reads a single character from the input stream. Retruns None if none is avalable.
+ If blocking=True the function waits for the next character."""
+
+ if not (blocking or kbhit()):
+ return None
+
+ fd = sys.stdin.fileno()
+ old_settings = termios.tcgetattr(fd)
+ try:
+ tty.setraw(sys.stdin.fileno())
+ ch = sys.stdin.read(1)
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+ return ch
+
+
+def readkey():
+ """Get a single keypress. If an escaped key is pressed, the
+ following characters are read as well (see key_linux.py)."""
+
+ c1 = readchar()
+
+ if c1 == "\x03":
+ raise KeyboardInterrupt
+
+ if c1 != "\x1B":
+ return c1
+
+ c2 = readchar()
+ if c2 not in "\x4F\x5B":
+ return c1 + c2
+
+ c3 = readchar()
+ if c3 not in "\x31\x32\x33\x35\x36":
+ return c1 + c2 + c3
+
+ c4 = readchar()
+ if c4 not in "\x30\x31\x33\x34\x35\x37\x38\x39":
+ return c1 + c2 + c3 + c4
+
+ c5 = readchar()
+ return c1 + c2 + c3 + c4 + c5
diff --git a/readchar/readchar.py b/readchar/readchar.py
deleted file mode 100644
index 8aa352a..0000000
--- a/readchar/readchar.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# -*- coding: utf-8 -*-
-# This file is based on this gist:
-# http://code.activestate.com/recipes/134892/
-# So real authors are DannyYoo and company.
-import sys
-
-if sys.platform.startswith("linux"):
- from .readchar_linux import readchar
-elif sys.platform == "darwin":
- from .readchar_linux import readchar
-elif sys.platform in ("win32", "cygwin"):
- import msvcrt
-
- from . import key
- from .readchar_windows import readchar
-else:
- raise NotImplementedError("The platform %s is not supported yet" % sys.platform)
-
-
-if sys.platform in ("win32", "cygwin"):
- #
- # Windows uses scan codes for extended characters. The ordinal returned is
- # 256 * the scan code. This dictionary translates scan codes to the
- # unicode sequences expected by readkey.
- #
- # for windows scan codes see:
- # https://msdn.microsoft.com/en-us/library/aa299374
- # or
- # http://www.quadibloc.com/comp/scan.htm
- xlate_dict = {
- 13: key.ENTER,
- 27: key.ESC,
- 15104: key.F1,
- 15360: key.F2,
- 15616: key.F3,
- 15872: key.F4,
- 16128: key.F5,
- 16384: key.F6,
- 16640: key.F7,
- 16896: key.F8,
- 17152: key.F9,
- 17408: key.F10,
- 22272: key.F11,
- 34528: key.F12,
- 7680: key.ALT_A,
- # don't have table entries for...
- # CTRL_ALT_A, # Ctrl-Alt-A, etc.
- # CTRL_ALT_SUPR,
- # CTRL-F1
- 21216: key.INSERT,
- 21472: key.SUPR, # key.py uses SUPR, not DELETE
- 18912: key.PAGE_UP,
- 20960: key.PAGE_DOWN,
- 18400: key.HOME,
- 20448: key.END,
- 18432: key.UP, # 72 * 256
- 20480: key.DOWN, # 80 * 256
- 19200: key.LEFT, # 75 * 256
- 19712: key.RIGHT, # 77 * 256
- }
-
- def readkey(getchar_fn=None):
- # Get a single character on Windows. if an extended key is pressed, the
- # Windows scan code is translated into a the unicode sequences readchar
- # expects (see key.py).
- while True:
- if msvcrt.kbhit():
- ch = msvcrt.getch()
- a = ord(ch)
- if a == 0 or a == 224:
- b = ord(msvcrt.getch())
- x = a + (b * 256)
-
- try:
- return xlate_dict[x]
- except KeyError:
- return None
- return x
- else:
- return ch.decode()
-
-else:
-
- def readkey(getchar_fn=None):
- getchar = getchar_fn or readchar
- c1 = getchar()
- if ord(c1) != 0x1B:
- return c1
- c2 = getchar()
- if ord(c2) != 0x5B:
- return c1 + c2
- c3 = getchar()
- if ord(c3) != 0x33:
- return c1 + c2 + c3
- c4 = getchar()
- return c1 + c2 + c3 + c4
diff --git a/readchar/readchar_linux.py b/readchar/readchar_linux.py
deleted file mode 100644
index 6bcb4e2..0000000
--- a/readchar/readchar_linux.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-# Initially taken from:
-# http://code.activestate.com/recipes/134892/
-# Thanks to Danny Yoo
-import sys
-import termios
-import tty
-
-
-def readchar():
- fd = sys.stdin.fileno()
- old_settings = termios.tcgetattr(fd)
- try:
- tty.setraw(sys.stdin.fileno())
- ch = sys.stdin.read(1)
- finally:
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
- return ch
diff --git a/readchar/readchar_windows.py b/readchar/readchar_windows.py
deleted file mode 100644
index 3afb4f8..0000000
--- a/readchar/readchar_windows.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-# Initially taken from:
-# http://code.activestate.com/recipes/134892/#c9
-# Thanks to Stephen Chappell
-import msvcrt
-import sys
-
-win_encoding = "mbcs"
-
-
-XE0_OR_00 = "\x00\xe0"
-
-
-def readchar(blocking=False):
- "Get a single character on Windows."
-
- while msvcrt.kbhit():
- msvcrt.getch()
- ch = msvcrt.getch()
- # print('ch={}, type(ch)={}'.format(ch, type(ch)))
- # while ch.decode(win_encoding) in unicode('\x00\xe0', win_encoding):
- while ch.decode(win_encoding) in XE0_OR_00:
- # print('found x00 or xe0')
- msvcrt.getch()
- ch = msvcrt.getch()
-
- return ch if sys.version_info.major > 2 else ch.decode(encoding=win_encoding)
diff --git a/readchar/win_key.py b/readchar/win_key.py
new file mode 100644
index 0000000..eab9406
--- /dev/null
+++ b/readchar/win_key.py
@@ -0,0 +1,84 @@
+# common
+LF = "\x0a"
+CR = "\x0d"
+ENTER = CR
+BACKSPACE = "\x08"
+SPACE = "\x20"
+ESC = "\x1b"
+TAB = "\x09"
+
+# CTRL
+CTRL_A = "\x01"
+CTRL_B = "\x02"
+CTRL_C = "\x03"
+CTRL_D = "\x04"
+CTRL_E = "\x05"
+CTRL_F = "\x06"
+CTRL_G = "\x07"
+CTRL_H = BACKSPACE
+CTRL_I = TAB
+CTRL_J = LF
+CTRL_K = "\x0b"
+CTRL_L = "\x0c"
+CTRL_M = CR
+CTRL_N = "\x0e"
+CTRL_O = "\x0f"
+CTRL_P = "\x10"
+CTRL_Q = "\x11"
+CTRL_R = "\x12"
+CTRL_S = "\x13"
+CTRL_T = "\x14"
+CTRL_U = "\x15"
+CTRL_V = "\x16"
+CTRL_W = "\x17"
+CTRL_X = "\x18"
+CTRL_Y = "\x19"
+CTRL_Z = "\x1a"
+
+# Windows uses scan codes for extended characters. This dictionary
+# translates the second half of the scan codes of special Keys
+# into the corresponding variable used by readchar.
+#
+# for windows scan codes see:
+# https://msdn.microsoft.com/en-us/library/aa299374
+# or
+# https://www.freepascal.org/docs-html/rtl/keyboard/kbdscancode.html
+
+# cursors
+UP = "\x00\x48"
+DOWN = "\x00\x50"
+LEFT = "\x00\x4b"
+RIGHT = "\x00\x4d"
+
+# navigation keys
+INSERT = "\x00\x52"
+SUPR = "\x00\x53"
+DELETE = SUPR
+HOME = "\x00\x47"
+END = "\x00\x4f"
+PAGE_UP = "\x00\x49"
+PAGE_DOWN = "\x00\x51"
+
+# funcion keys
+F1 = "\x00\x3b"
+F2 = "\x00\x3c"
+F3 = "\x00\x3d"
+F4 = "\x00\x3e"
+F5 = "\x00\x3f"
+F6 = "\x00\x40"
+F7 = "\x00\x41"
+F8 = "\x00\x42"
+F9 = "\x00\x43"
+F10 = "\x00\x44"
+F11 = "\x00\x85" # only in second source
+F12 = "\x00\x86" # only in second source
+
+# other
+ESC_2 = "\x00\x01"
+ENTER_2 = "\x00\x1c"
+
+# don't have table entries for...
+# ALT_[A-Z]
+# CTRL_ALT_A, # Ctrl-Alt-A, etc.
+# CTRL_ALT_SUPR,
+# CTRL-F1
diff --git a/readchar/win_read.py b/readchar/win_read.py
new file mode 100644
index 0000000..64bde86
--- /dev/null
+++ b/readchar/win_read.py
@@ -0,0 +1,36 @@
+# This file is based on this gist:
+# http://code.activestate.com/recipes/134892/
+# So real authors are DannyYoo and company.
+
+import msvcrt
+
+
+def readchar(blocking=True):
+ """Reads a single character from the input stream. Retruns None if none is avalable.
+ If blocking=True the function waits for the next character."""
+
+ if not (blocking or msvcrt.kbhit()):
+ return None
+
+ # manual byte decoding because some bytes in windows are not utf-8 encodable.
+ return chr(int.from_bytes(msvcrt.getch(), "big"))
+
+
+def readkey():
+ """Get a single character on Windows. If an escaped key is pressed, the
+ Windows scan code is translated into a the unicode sequences readchar
+ expects (see key_windows.py)."""
+
+ ch = readchar()
+
+ if ch == "\x03":
+ raise KeyboardInterrupt
+
+ # if it is a normal character:
+ if ch not in "\x00\xe0":
+ return ch
+
+ # if it is a scpeal key, read second half:
+ ch2 = readchar()
+
+ return "\x00" + ch2
diff --git a/requirements-test.txt b/requirements.txt
similarity index 52%
rename from requirements-test.txt
rename to requirements.txt
index 462fa01..f23d1f3 100644
--- a/requirements-test.txt
+++ b/requirements.txt
@@ -1,4 +1,6 @@
+wheel
+black
flake8
+pre-commit
pytest
pytest-cov
-
diff --git a/setup.cfg b/setup.cfg
index c59270b..626b39e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,7 +1,57 @@
-[tool:pytest]
-norecursedirs = .git venv build dist *egg
-addopts = -rfEsxwX --cov readchar
+[metadata]
+name = readchar
+author_email = miguelangel.garcia@gmail.com
+license = MIT
+license_files = LICENSE
+version = attr: readchar.__version__
-[flake8]
-exclude = **/.*,venv/**,.eggs/**
+description = Library to easily read single chars and key strokes
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+keywords =
+ charaters
+ keystrokes
+ stdin
+ command line
+
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Environment :: Console
+ Intended Audience :: Developers
+ License :: OSI Approved :: MIT License
+ Programming Language :: Python :: 3.6
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Programming Language :: Python :: Implementation :: CPython
+ Topic :: Software Development
+ Topic :: Software Development :: User Interfaces
+
+url = https://github.com/magmax/python-readchar
+project_urls =
+ Download = https://pypi.org/project/readchar/#files
+ Bug Tracker = https://github.com/magmax/python-readchar/issues
+ Source Code = https://github.com/magmax/python-readchar
+
+
+[options]
+packages = find:
+
+python_requires = >=3.6
+
+install_requires =
+ setuptools
+ wheel
+
+test_suite = tests
+tests_require =
+ pytest
+
+[options.packages.find]
+exclude =
+ .venv/
+ .git/
+ .github/
+ tests/
diff --git a/setup.py b/setup.py
index c5b46b4..28b2495 100644
--- a/setup.py
+++ b/setup.py
@@ -1,86 +1,7 @@
-# -*- coding: utf-8 -*-
-
-import os
-import sys
-from io import open
-
-from setuptools import find_packages, setup
-from setuptools.command.test import test as TestCommand
-
-github_ref = os.getenv("GITHUB_REF")
-if github_ref and github_ref.startswith("refs/tags"):
- version = github_ref[10:]
-else:
- version = "0.0.0-local"
-
-
-def read_description():
- try:
- with open("README.rst", encoding="utf8") as fd:
- return fd.read()
- except: # noqa
- return "Error found retrieving description"
-
-
-class PyTest(TestCommand):
- user_options = [("pytest-args=", "a", "Arguments to pass to py.test")]
-
- def initialize_options(self):
- TestCommand.initialize_options(self)
- self.pytest_args = ["--cov-report=term-missing"]
-
- def finalize_options(self):
- TestCommand.finalize_options(self)
- self.test_args = []
- self.test_suite = True
-
- def run_tests(self):
- # import here, cause outside the eggs aren't loaded
- import pytest
-
- errno = pytest.main(self.pytest_args)
- sys.exit(errno)
-
+from setuptools import setup
setup(
- name="readchar",
- version=version,
- description="Utilities to read single characters and key-strokes",
- long_description=read_description(),
- classifiers=[
- "Development Status :: 5 - Production/Stable",
- "Environment :: Console",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
- "Operating System :: OS Independent",
- "Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3.4",
- "Programming Language :: Python :: 3.5",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: Implementation :: CPython",
- "Programming Language :: Python :: Implementation :: PyPy",
- "Topic :: Software Development",
- "Topic :: Software Development :: User Interfaces",
- ],
- keywords="stdin,command line",
- author="Miguel Ángel García",
- author_email="miguelangel.garcia@gmail.com",
- url="https://github.com/magmax/python-readchar",
- license="MIT",
- packages=find_packages(exclude=["tests", "venv"]),
- include_package_data=True,
- zip_safe=False,
- cmdclass={"test": PyTest},
- tests_require=[
- "pexpect",
- "coverage",
- "pytest",
- "pytest-cov",
- "wheel",
- ],
- install_requires=[],
- setup_requires=[],
+ # Author can not be read from setup.cfg as it causes encoding problems during
+ # windows installs
+ author="Miguel Ángel García"
)
diff --git a/test.py b/test.py
deleted file mode 100644
index d78b18f..0000000
--- a/test.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import readchar.key
-import readchar.readchar
-
-decode_dict = {
- readchar.key.ESC: "ESC",
- readchar.key.UP: "UP",
- readchar.key.DOWN: "DOWN",
- readchar.key.LEFT: "LEFT",
- readchar.key.RIGHT: "RIGHT",
- readchar.key.PAGE_UP: "PAGE_UP",
- readchar.key.PAGE_DOWN: "PAGE_DOWN",
- readchar.key.HOME: "HOME",
- readchar.key.END: "END",
- readchar.key.INSERT: "INSERT",
- readchar.key.SUPR: "DELETE",
- readchar.key.F1: "F1",
- readchar.key.F2: "F2",
- readchar.key.F3: "F3",
- readchar.key.F4: "F4",
- readchar.key.F5: "F5",
- readchar.key.F6: "F6",
- readchar.key.F7: "F7",
- readchar.key.F8: "F8",
- readchar.key.F9: "F9",
- readchar.key.F10: "F10",
- readchar.key.F12: "F12",
- readchar.key.ALT_A: "ALT_A",
-}
-
-while True:
- c = readchar.readkey()
-
- if c in decode_dict:
- print("got {}".format(decode_dict[c]))
- else:
- print(c)
-
- if c == "d":
- break
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/acceptance/__init__.py b/tests/acceptance/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/manual-test.py b/tests/manual-test.py
new file mode 100644
index 0000000..9c35649
--- /dev/null
+++ b/tests/manual-test.py
@@ -0,0 +1,17 @@
+from readchar import key, readkey
+
+
+# construct an inverted code -> key-name mapping
+# we need to revese the items so that aliases won't overrive the original name later on
+known_keys = {v: k for k, v in reversed(vars(key).items()) if not k.startswith("__")}
+
+
+while True:
+ data = readkey()
+
+ if data in known_keys:
+ print(f"got {known_keys[data]}", end="")
+ else:
+ print(data, end="")
+
+ print(" - " + "".join([f"\\x{ord(c):02x}" for c in data]))
diff --git a/tests/posix/conftest.py b/tests/posix/conftest.py
new file mode 100644
index 0000000..30e7ed8
--- /dev/null
+++ b/tests/posix/conftest.py
@@ -0,0 +1,50 @@
+import pytest
+import sys
+
+if sys.platform.startswith("linux"):
+ import termios
+ import tty
+ import readchar.posix_read as linux_read
+
+
+# ignore all tests in this folder if not on linux
+def pytest_ignore_collect(path, config):
+ if not sys.platform.startswith("linux"):
+ return True
+
+
+@pytest.fixture
+def patched_stdin():
+ class mocked_stdin:
+ buffer = []
+
+ def push(self, string):
+ for c in string:
+ self.buffer.append(c)
+
+ def read(self, n):
+ string = ""
+ for i in range(n):
+ string += self.buffer.pop(0)
+ return string
+
+ def mock_tcgetattr(fd):
+ return None
+
+ def mock_tcsetattr(fd, TCSADRAIN, old_settings):
+ return None
+
+ def mock_setraw(fd):
+ return None
+
+ def mock_kbhit():
+ return True
+
+ mock = mocked_stdin()
+ with pytest.MonkeyPatch.context() as mp:
+ mp.setattr(sys.stdin, "read", mock.read)
+ mp.setattr(termios, "tcgetattr", mock_tcgetattr)
+ mp.setattr(termios, "tcsetattr", mock_tcsetattr)
+ mp.setattr(tty, "setraw", mock_setraw)
+ mp.setattr(linux_read, "kbhit", mock_kbhit)
+ yield mock
diff --git a/tests/posix/test_import.py b/tests/posix/test_import.py
new file mode 100644
index 0000000..4b73256
--- /dev/null
+++ b/tests/posix/test_import.py
@@ -0,0 +1,19 @@
+import readchar
+
+
+def test_readcharImport():
+
+ assert readchar.readchar == readchar.posix_read.readchar
+
+
+def test_readkeyImport():
+
+ assert readchar.readkey == readchar.posix_read.readkey
+
+
+def test_keyImport():
+
+ a = {k: v for k, v in vars(readchar.key).items() if not k.startswith("__")}
+ del a["platform"]
+ b = {k: v for k, v in vars(readchar.posix_key).items() if not k.startswith("__")}
+ assert a == b
diff --git a/tests/posix/test_keys.py b/tests/posix/test_keys.py
new file mode 100644
index 0000000..dd3e212
--- /dev/null
+++ b/tests/posix/test_keys.py
@@ -0,0 +1,17 @@
+from readchar import key
+
+
+def test_character_length_1():
+ assert 1 == len(key.CTRL_A)
+
+
+def test_character_length_2():
+ assert 2 == len(key.ALT_A)
+
+
+def test_character_length_3():
+ assert 3 == len(key.UP)
+
+
+def test_character_length_4():
+ assert 4 == len(key.CTRL_ALT_SUPR)
diff --git a/tests/posix/test_readchar.py b/tests/posix/test_readchar.py
new file mode 100644
index 0000000..4c35d49
--- /dev/null
+++ b/tests/posix/test_readchar.py
@@ -0,0 +1,62 @@
+import pytest
+from string import printable
+from readchar import readchar, key
+
+
+@pytest.mark.parametrize("c", printable)
+def test_printableCharacters(patched_stdin, c):
+ patched_stdin.push(c)
+ assert c == readchar(blocking=True)
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ ("\x0a", key.LF),
+ ("\x0a", key.ENTER),
+ ("\x0d", key.CR),
+ ("\x7f", key.BACKSPACE),
+ ("\x20", key.SPACE),
+ ("\x1b", key.ESC),
+ ("\x09", key.TAB),
+ ],
+)
+def test_controlCharacters(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readchar()
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ ("\x01", key.CTRL_A),
+ ("\x02", key.CTRL_B),
+ ("\x03", key.CTRL_C),
+ ("\x04", key.CTRL_D),
+ ("\x05", key.CTRL_E),
+ ("\x06", key.CTRL_F),
+ ("\x07", key.CTRL_G),
+ ("\x08", key.CTRL_H),
+ ("\x09", key.CTRL_I),
+ ("\x0a", key.CTRL_J),
+ ("\x0b", key.CTRL_K),
+ ("\x0c", key.CTRL_L),
+ ("\x0d", key.CTRL_M),
+ ("\x0e", key.CTRL_N),
+ ("\x0f", key.CTRL_O),
+ ("\x10", key.CTRL_P),
+ ("\x11", key.CTRL_Q),
+ ("\x12", key.CTRL_R),
+ ("\x13", key.CTRL_S),
+ ("\x14", key.CTRL_T),
+ ("\x15", key.CTRL_U),
+ ("\x16", key.CTRL_V),
+ ("\x17", key.CTRL_W),
+ ("\x18", key.CTRL_X),
+ ("\x19", key.CTRL_Y),
+ ("\x1a", key.CTRL_Z),
+ ],
+)
+def test_CTRL_Characters(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readchar()
diff --git a/tests/posix/test_readkey.py b/tests/posix/test_readkey.py
new file mode 100644
index 0000000..d913bd3
--- /dev/null
+++ b/tests/posix/test_readkey.py
@@ -0,0 +1,65 @@
+import pytest
+from readchar import readkey, key
+
+
+def test_KeyboardInterrupt(patched_stdin):
+ patched_stdin.push("\x03")
+ with pytest.raises(KeyboardInterrupt):
+ readkey()
+
+
+def test_singleCharacter(patched_stdin):
+ patched_stdin.push("a")
+ assert "a" == readkey()
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ ("\x1b\x5b\x41", key.UP),
+ ("\x1b\x5b\x42", key.DOWN),
+ ("\x1b\x5b\x44", key.LEFT),
+ ("\x1b\x5b\x43", key.RIGHT),
+ ],
+)
+def test_arrowKeys(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readkey()
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ ("\x1b\x5b\x32\x7e", key.INSERT),
+ ("\x1b\x5b\x33\x7e", key.SUPR),
+ ("\x1b\x5b\x48", key.HOME),
+ ("\x1b\x5b\x46", key.END),
+ ("\x1b\x5b\x35\x7e", key.PAGE_UP),
+ ("\x1b\x5b\x36\x7e", key.PAGE_DOWN),
+ ],
+)
+def test_specialKeys(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readkey()
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ (key.F1, "\x1b\x4f\x50"),
+ (key.F2, "\x1b\x4f\x51"),
+ (key.F3, "\x1b\x4f\x52"),
+ (key.F4, "\x1b\x4f\x53"),
+ (key.F5, "\x1b\x5b\x31\x35\x7e"),
+ (key.F6, "\x1b\x5b\x31\x37\x7e"),
+ (key.F7, "\x1b\x5b\x31\x38\x7e"),
+ (key.F8, "\x1b\x5b\x31\x39\x7e"),
+ (key.F9, "\x1b\x5b\x32\x30\x7e"),
+ (key.F10, "\x1b\x5b\x32\x31\x7e"),
+ (key.F11, "\x1b\x5b\x32\x33\x7e"),
+ (key.F12, "\x1b\x5b\x32\x34\x7e"),
+ ],
+)
+def test_functionKeys(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readkey()
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/unit/test_key.py b/tests/unit/test_key.py
deleted file mode 100644
index ef605f3..0000000
--- a/tests/unit/test_key.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import unittest
-
-from readchar import key
-
-
-class KeyTest(unittest.TestCase):
- def test_character_length_1(self):
- self.assertEqual(1, len(key.CTRL_A))
-
- def test_character_length_2(self):
- self.assertEqual(2, len(key.ALT_A))
-
- def test_character_length_3(self):
- self.assertEqual(3, len(key.UP))
-
- def test_character_length_4(self):
- self.assertEqual(4, len(key.CTRL_ALT_SUPR))
diff --git a/tests/unit/test_readkey.py b/tests/unit/test_readkey.py
deleted file mode 100644
index 5f52727..0000000
--- a/tests/unit/test_readkey.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import unittest
-
-from readchar import readkey
-
-
-def readchar_fn_factory(stream):
-
- v = [x for x in stream]
-
- def inner():
- return v.pop(0)
-
- return inner
-
-
-class ReadKeyTest(unittest.TestCase):
- def test_basic_character(self):
- getchar_fn = readchar_fn_factory("a")
-
- result = readkey(getchar_fn)
-
- self.assertEqual("a", result)
-
- def test_string_instead_of_char(self):
- char = "a"
- getchar_fn = readchar_fn_factory(char + "bcde")
-
- result = readkey(getchar_fn)
-
- self.assertEqual(char, result)
-
- def test_special_combo_character(self):
- char = "\x1b\x01"
- getchar_fn = readchar_fn_factory(char + "foo")
-
- result = readkey(getchar_fn)
-
- self.assertEqual(char, result)
-
- def test_special_key(self):
- char = "\x1b\x5b\x41"
- getchar_fn = readchar_fn_factory(char + "foo")
-
- result = readkey(getchar_fn)
-
- self.assertEqual(char, result)
-
- def test_special_key_combo(self):
- char = "\x1b\x5b\x33\x5e"
- getchar_fn = readchar_fn_factory(char + "foo")
-
- result = readkey(getchar_fn)
-
- self.assertEqual(char, result)
diff --git a/tests/windows/conftest.py b/tests/windows/conftest.py
new file mode 100644
index 0000000..94299d4
--- /dev/null
+++ b/tests/windows/conftest.py
@@ -0,0 +1,22 @@
+import pytest
+import sys
+
+
+if sys.platform in ("win32", "cygwin"):
+ import msvcrt
+
+
+# ignore all tests in this folder if not on windows
+def pytest_ignore_collect(path, config):
+ if sys.platform not in ("win32", "cygwin"):
+ return True
+
+
+@pytest.fixture
+def patched_stdin():
+ class mocked_stdin:
+ def push(self, string):
+ for c in string:
+ msvcrt.ungetch(ord(c).to_bytes(1, "big"))
+
+ return mocked_stdin()
diff --git a/tests/windows/test_import.py b/tests/windows/test_import.py
new file mode 100644
index 0000000..0705825
--- /dev/null
+++ b/tests/windows/test_import.py
@@ -0,0 +1,19 @@
+import readchar
+
+
+def test_readcharImport():
+
+ assert readchar.readchar == readchar.win_read.readchar
+
+
+def test_readkeyImport():
+
+ assert readchar.readkey == readchar.win_read.readkey
+
+
+def test_keyImport():
+
+ a = {k: v for k, v in vars(readchar.key).items() if not k.startswith("__")}
+ del a["platform"]
+ b = {k: v for k, v in vars(readchar.win_key).items() if not k.startswith("__")}
+ assert a == b
diff --git a/tests/windows/test_keys.py b/tests/windows/test_keys.py
new file mode 100644
index 0000000..55a8d4a
--- /dev/null
+++ b/tests/windows/test_keys.py
@@ -0,0 +1,51 @@
+import pytest
+from readchar import key as keys
+
+
+defaultKeys = ["LF", "CR", "ENTER", "BACKSPACE", "SPACE", "ESC", "TAB"]
+
+
+@pytest.mark.parametrize("key", defaultKeys)
+def test_defaultKeysExists(key):
+ assert key in keys.__dict__
+
+
+@pytest.mark.parametrize("key", defaultKeys)
+def test_defaultKeysLength(key):
+ assert 1 == len(keys.__dict__[key])
+
+
+specialKeys = [
+ "INSERT",
+ "SUPR",
+ "PAGE_UP",
+ "PAGE_DOWN",
+ "HOME",
+ "END",
+ "UP",
+ "DOWN",
+ "LEFT",
+ "RIGHT",
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ "F5",
+ "F6",
+ "F7",
+ "F8",
+ "F9",
+ "F10",
+ "F11",
+ "F12",
+]
+
+
+@pytest.mark.parametrize("key", specialKeys)
+def test_specialKeysExists(key):
+ assert key in keys.__dict__
+
+
+@pytest.mark.parametrize("key", specialKeys)
+def test_specialKeysLength(key):
+ assert 2 == len(keys.__dict__[key])
diff --git a/tests/windows/test_readchar.py b/tests/windows/test_readchar.py
new file mode 100644
index 0000000..b1fd7bd
--- /dev/null
+++ b/tests/windows/test_readchar.py
@@ -0,0 +1,62 @@
+import pytest
+from string import printable
+from readchar import readchar, key
+
+
+@pytest.mark.parametrize("c", printable)
+def test_printableCharacters(patched_stdin, c):
+ patched_stdin.push(c)
+ assert c == readchar()
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ ("\n", key.LF),
+ ("\r", key.ENTER),
+ ("\r", key.CR),
+ ("\x08", key.BACKSPACE),
+ ("\x20", key.SPACE),
+ ("\x1b", key.ESC),
+ ("\t", key.TAB),
+ ],
+)
+def test_spectialCharacters(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readchar()
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ ("\x01", key.CTRL_A),
+ ("\x02", key.CTRL_B),
+ ("\x03", key.CTRL_C),
+ ("\x04", key.CTRL_D),
+ ("\x05", key.CTRL_E),
+ ("\x06", key.CTRL_F),
+ ("\x07", key.CTRL_G),
+ ("\x08", key.CTRL_H),
+ ("\x09", key.CTRL_I),
+ ("\x0a", key.CTRL_J),
+ ("\x0b", key.CTRL_K),
+ ("\x0c", key.CTRL_L),
+ ("\x0d", key.CTRL_M),
+ ("\x0e", key.CTRL_N),
+ ("\x0f", key.CTRL_O),
+ ("\x10", key.CTRL_P),
+ ("\x11", key.CTRL_Q),
+ ("\x12", key.CTRL_R),
+ ("\x13", key.CTRL_S),
+ ("\x14", key.CTRL_T),
+ ("\x15", key.CTRL_U),
+ ("\x16", key.CTRL_V),
+ ("\x17", key.CTRL_W),
+ ("\x18", key.CTRL_X),
+ ("\x19", key.CTRL_Y),
+ ("\x1a", key.CTRL_Z),
+ ],
+)
+def test_CTRL_Characters(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readchar()
diff --git a/tests/windows/test_readkey.py b/tests/windows/test_readkey.py
new file mode 100644
index 0000000..28fb48b
--- /dev/null
+++ b/tests/windows/test_readkey.py
@@ -0,0 +1,71 @@
+import pytest
+from readchar import readkey, key
+
+
+def test_KeyboardInterrupt(patched_stdin):
+ patched_stdin.push("\x03")
+ with pytest.raises(KeyboardInterrupt):
+ readkey()
+
+
+def test_singleCharacter(patched_stdin):
+ patched_stdin.push("a")
+ assert "a" == readkey()
+
+
+# for windows scan codes see:
+# https://msdn.microsoft.com/en-us/library/aa299374
+# or
+# https://www.freepascal.org/docs-html/rtl/keyboard/kbdscancode.html
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ ("\x00\x48", key.UP),
+ ("\x00\x50", key.DOWN),
+ ("\x00\x4b", key.LEFT),
+ ("\x00\x4d", key.RIGHT),
+ ],
+)
+def test_arrowKeys(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readkey()
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ ("\x00\x52", key.INSERT),
+ ("\x00\x53", key.SUPR),
+ ("\x00\x47", key.HOME),
+ ("\x00\x4f", key.END),
+ ("\x00\x49", key.PAGE_UP),
+ ("\x00\x51", key.PAGE_DOWN),
+ ],
+)
+def test_specialKeys(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readkey()
+
+
+@pytest.mark.parametrize(
+ ["seq", "key"],
+ [
+ ("\x00\x3b", key.F1),
+ ("\x00\x3c", key.F2),
+ ("\x00\x3d", key.F3),
+ ("\x00\x3e", key.F4),
+ ("\x00\x3f", key.F5),
+ ("\x00\x40", key.F6),
+ ("\x00\x41", key.F7),
+ ("\x00\x42", key.F8),
+ ("\x00\x43", key.F9),
+ ("\x00\x44", key.F10),
+ ("\x00\x85", key.F11),
+ ("\x00\x86", key.F12),
+ ],
+)
+def test_functionKeys(seq, key, patched_stdin):
+ patched_stdin.push(seq)
+ assert key == readkey()