Skip to main content

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

Requires dahlia and nouns.txt

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: up down left right
  • F_KEYS: f1 → f12
  • DIGITS or NUMBERS: 0 → 9 (same as string.digits)
  • LOWERCASE: a → z (same as string.ascii_lowercase)
  • UPPERCASE: A → Z (same as string.ascii_uppercase)
  • LETTERS: LOWERCASE + UPPERCASE (same as string.ascii_letters)
  • PUNCTUATION: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ (same as string.punctuation)

Known Issues

  • Some combinations (like shift+up or alt+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:

  1. Clone your fork of the project.
  2. Set up the project with just (make sure you have poetry installed):
just install
  1. After you're done, use the following just recipes 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)

Source distribution for outspin 0.3.2
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)

Table of built distributions (wheels) for outspin 0.3.2
File Interpreter ABI Platform Actions
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page