outspin
outspin is a tiny, low-abstraction library bringing C's getch()
functionality to Python, with a sane API. An ideal choice for developers seeking
direct control over their TUI applications.
Installation
From PyPI:
pip install outspin
From source:
pip install git+https://github.com/trag1c/outspin.git
Examples
Select Menu
https://github.com/trag1c/outspin/assets/77130613/ea0be955-302d-4ff3-85c9-8f7c451e6026
Source
from outspin import wait_for
def _display_selected(*options: str, selected: int) -> None:
print("Select an option:")
for i, option in enumerate(options):
print(f"{'>' if i == selected else ' '} {option}")
print(f"\033[{len(options) + 1}F", end="")
def select(*options: str) -> str:
selected = 0
_display_selected(*options, selected=selected)
while (key := wait_for("up", "down", "enter")) != "enter":
selected += 1 if key == "down" else -1
selected %= len(options)
_display_selected(*options, selected=selected)
print("\n" * len(options))
return options[selected]
print("Selected", select("Python", "Rust", "Swift", "C++", "C", "Kotlin"))
Typing Test
https://github.com/trag1c/outspin/assets/77130613/c44a8d2f-8b1e-4948-8e78-15018e2e3667
Source
from __future__ import annotations
import sys
from collections.abc import Iterator
from datetime import datetime
from itertools import count, islice, zip_longest
from pathlib import Path
from random import choice
from string import ascii_lowercase
from dahlia import dprint
from outspin import pause, wait_for
NOUNS = [
w
for w in Path("nouns.txt").read_text().splitlines()
if len(w) < 12 and w.isalpha()
]
class WordQueue:
def __init__(self) -> None:
self._gen = (choice(NOUNS) for _ in count())
self._queue: list[str] = []
self.load(4)
def load(self, number: int = 1) -> None:
self._queue.extend(islice(self._gen, number))
@property
def loaded(self) -> tuple[str, ...]:
return tuple(self._queue)
def __iter__(self) -> Iterator[str]:
return self
def __next__(self) -> str:
self._queue.pop(0)
self.load()
return self._queue[0]
def render(wq: WordQueue, buffer: list[str]) -> None:
current, *up_next = wq.loaded
buf_str = "".join(buffer)
first_bad_idx = (
(
next(
i
for i, (a, b) in enumerate(zip_longest(buf_str, current, fillvalue="_"))
if a != b
)
if buf_str != current
else len(current)
)
if buf_str and current
else 0
)
dprint(f"\033[2F\033[0JUp next: &2{' '.join(up_next)}")
print(f"\n> {buf_str[:first_bad_idx]}", end="")
if bad_content := buf_str[first_bad_idx:]:
dprint(f"&4{bad_content}&8{current[first_bad_idx+len(bad_content):]}", end="")
else:
dprint(f"&8{current[first_bad_idx:]}", end="")
sys.stdout.flush()
def main(time: int) -> None:
pause()
start_time = datetime.now()
wq = WordQueue()
buffer: list[str] = []
word = list(next(wq))
typed_chars = 0
while (datetime.now() - start_time).seconds < time:
render(wq, buffer)
key = wait_for(*ascii_lowercase, "space", "backspace")
if key == "space":
if buffer == word:
buffer = []
typed_chars += len(word) + 1
word = list(next(wq))
elif key == "backspace":
if buffer:
buffer.pop()
else:
buffer.append(key)
print(f"\nWPM: {(typed_chars - 1) / 5 / (time / 60):.2f}")
if __name__ == "__main__":
main(int(sys.argv[1] if len(sys.argv) > 1 else 30))
Reference
get_key
Signature:
() -> str
Returns a keypress from standard input. It exclusively identifies keypresses
that result in tangible inputs, therefore modifier keys like shift or
caps lock are ignored. outspin also returns the actual input, meaning that
pressing, for instance, shift+a will make get_key() return A.
wait_for
Signature:
(*keys: str) -> str
Waits for one of the keys to be pressed and returns it.
>>> wait_for(*"wasd") # pressing g t 4 2 q a
'a'
wait_for requires at least one key to be provided.
>>> wait_for()
outspin.OutspinValueError: No keys to wait for
pause
Signature:
(prompt: str | None = None) -> None
Displays the prompt and pauses the program until a key is pressed.
The default prompt is Press any key to continue....
constants
A namespace containing a few useful characters groups:
ARROWS:updownleftrightF_KEYS:f1→f12DIGITSorNUMBERS:0→9(same asstring.digits)LOWERCASE:a→z(same asstring.ascii_lowercase)UPPERCASE:A→Z(same asstring.ascii_uppercase)LETTERS:LOWERCASE+UPPERCASE(same asstring.ascii_letters)PUNCTUATION:!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~(same asstring.punctuation)
Known Issues
- Some combinations (like
shift+uporalt+shift+right) may not work correctly on Windows.
Contributing
Contributions are welcome!
Please open an issue before submitting a pull request (unless it's a minor change like fixing a typo).
To get started:
just install
- After you're done, use the following
justrecipes to check your changes:
just check # pytest, mypy, ruff
just coverage # pytest (with coverage), interrogate (docstring coverage)
License
outspin is licensed under the MIT License.
© trag1c, 2023–2024
Metadata
Release files for outspin 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| outspin-0.3.2.tar.gz | 6.7 kB | Without provenance | Details for outspin-0.3.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| outspin-0.3.2-py3-none-any.whl | Python 3 | none | any | Details for outspin-0.3.2-py3-none-any.whl |
Total release size: 14.8 kB
Release files / outspin-0.3.2.tar.gz
| Download URL | outspin-0.3.2.tar.gz |
|---|---|
| Size | 6.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f7ba43123e7894c3004594d582f1542ffbda19a985d1acb9221245e2d7e19ed
|
|
BLAKE2b-256 checksum How to use checksums |
57faa9aa1ee42a83e0f48db7d5ee080592c58da3b0d853ae47325d36b2262737
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/1.8.2 CPython/3.8.18 Darwin/23.4.0
|
Release files / outspin-0.3.2-py3-none-any.whl
| Download URL | outspin-0.3.2-py3-none-any.whl |
|---|---|
| Size | 8.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
55fd8ef4ed47b3484a0571b0fc92b0f69fc6e56d9be2d90ad3f4c4a8c988ece7
|
|
BLAKE2b-256 checksum How to use checksums |
7e55086200073253e8b4eaa85f552e85ca875ef1c0f325f1e101c1cc3eced1c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/1.8.2 CPython/3.8.18 Darwin/23.4.0
|
Security for outspin 0.3.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.