{"id":25404989,"url":"https://github.com/trag1c/outspin","last_synced_at":"2025-07-14T17:06:54.978Z","repository":{"id":206738190,"uuid":"717583663","full_name":"trag1c/outspin","owner":"trag1c","description":"Conveniently read single char inputs in the console.","archived":false,"fork":false,"pushed_at":"2024-12-01T01:13:39.000Z","size":81,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-28T17:09:44.421Z","etag":null,"topics":["cli","getch","input","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/trag1c.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-11-11T22:57:20.000Z","updated_at":"2024-12-01T01:13:42.000Z","dependencies_parsed_at":"2023-11-26T23:23:10.249Z","dependency_job_id":"a4dc12b2-429b-427c-ac4c-d771331a6832","html_url":"https://github.com/trag1c/outspin","commit_stats":null,"previous_names":["trag1c/outspin"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/trag1c/outspin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trag1c%2Foutspin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trag1c%2Foutspin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trag1c%2Foutspin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trag1c%2Foutspin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/trag1c","download_url":"https://codeload.github.com/trag1c/outspin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trag1c%2Foutspin/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265322202,"owners_count":23746572,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","getch","input","python"],"created_at":"2025-02-16T04:29:48.859Z","updated_at":"2025-07-14T17:06:54.934Z","avatar_url":"https://github.com/trag1c.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# outspin\n\n`outspin` is a tiny, low-abstraction library bringing C's `getch()`\nfunctionality to Python, with a sane API. An ideal choice for developers seeking\ndirect control over their TUI applications.\n\n- [Installation](#installation)\n- [Examples](#examples)\n  - [Select Menu](#select-menu)\n  - [Typing Test](#typing-test)\n- [Reference](#reference)\n  - [`get_key`](#get_key)\n  - [`wait_for`](#wait_for)\n  - [`pause`](#pause)\n  - [`constants`](#constants)\n- [Known Issues](#known-issues)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Installation\n\nFrom PyPI:\n```bash\npip install outspin\n```\nFrom source:\n```bash\npip install git+https://github.com/trag1c/outspin.git\n```\n\n## Examples\n\n### Select Menu\n\nhttps://github.com/trag1c/outspin/assets/77130613/ea0be955-302d-4ff3-85c9-8f7c451e6026\n\n\u003cdetails\u003e\n    \u003csummary\u003eSource\u003c/summary\u003e\n\n```py\nfrom outspin import wait_for\n\n\ndef _display_selected(*options: str, selected: int) -\u003e None:\n    print(\"Select an option:\")\n    for i, option in enumerate(options):\n        print(f\"{'\u003e' if i == selected else ' '} {option}\")\n    print(f\"\\033[{len(options) + 1}F\", end=\"\")\n\n\ndef select(*options: str) -\u003e str:\n    selected = 0\n    _display_selected(*options, selected=selected)\n    while (key := wait_for(\"up\", \"down\", \"enter\")) != \"enter\":\n        selected += 1 if key == \"down\" else -1\n        selected %= len(options)\n        _display_selected(*options, selected=selected)\n    print(\"\\n\" * len(options))\n    return options[selected]\n\n\nprint(\"Selected\", select(\"Python\", \"Rust\", \"Swift\", \"C++\", \"C\", \"Kotlin\"))\n```\n\u003c/details\u003e\n\n### Typing Test\n\nhttps://github.com/trag1c/outspin/assets/77130613/c44a8d2f-8b1e-4948-8e78-15018e2e3667\n\n\u003cdetails\u003e\n  \u003csummary\u003eSource\u003c/summary\u003e\n\n\u003e Requires [dahlia] and [nouns.txt]\n```py\nfrom __future__ import annotations\n\nimport sys\nfrom collections.abc import Iterator\nfrom datetime import datetime\nfrom itertools import count, islice, zip_longest\nfrom pathlib import Path\nfrom random import choice\nfrom string import ascii_lowercase\n\nfrom dahlia import dprint\nfrom outspin import pause, wait_for\n\nNOUNS = [\n    w\n    for w in Path(\"nouns.txt\").read_text().splitlines()\n    if len(w) \u003c 12 and w.isalpha()\n]\n\n\nclass WordQueue:\n    def __init__(self) -\u003e None:\n        self._gen = (choice(NOUNS) for _ in count())\n        self._queue: list[str] = []\n        self.load(4)\n\n    def load(self, number: int = 1) -\u003e None:\n        self._queue.extend(islice(self._gen, number))\n\n    @property\n    def loaded(self) -\u003e tuple[str, ...]:\n        return tuple(self._queue)\n\n    def __iter__(self) -\u003e Iterator[str]:\n        return self\n\n    def __next__(self) -\u003e str:\n        self._queue.pop(0)\n        self.load()\n        return self._queue[0]\n\n\ndef render(wq: WordQueue, buffer: list[str]) -\u003e None:\n    current, *up_next = wq.loaded\n    buf_str = \"\".join(buffer)\n    first_bad_idx = (\n        (\n            next(\n                i\n                for i, (a, b) in enumerate(zip_longest(buf_str, current, fillvalue=\"_\"))\n                if a != b\n            )\n            if buf_str != current\n            else len(current)\n        )\n        if buf_str and current\n        else 0\n    )\n    dprint(f\"\\033[2F\\033[0JUp next: \u00262{' '.join(up_next)}\")\n    print(f\"\\n\u003e {buf_str[:first_bad_idx]}\", end=\"\")\n    if bad_content := buf_str[first_bad_idx:]:\n        dprint(f\"\u00264{bad_content}\u00268{current[first_bad_idx+len(bad_content):]}\", end=\"\")\n    else:\n        dprint(f\"\u00268{current[first_bad_idx:]}\", end=\"\")\n    sys.stdout.flush()\n\n\ndef main(time: int) -\u003e None:\n    pause()\n    start_time = datetime.now()\n\n    wq = WordQueue()\n    buffer: list[str] = []\n    word = list(next(wq))\n    typed_chars = 0\n\n    while (datetime.now() - start_time).seconds \u003c time:\n        render(wq, buffer)\n        key = wait_for(*ascii_lowercase, \"space\", \"backspace\")\n        if key == \"space\":\n            if buffer == word:\n                buffer = []\n                typed_chars += len(word) + 1\n                word = list(next(wq))\n        elif key == \"backspace\":\n            if buffer:\n                buffer.pop()\n        else:\n            buffer.append(key)\n\n    print(f\"\\nWPM: {(typed_chars - 1) / 5 / (time / 60):.2f}\")\n\n\nif __name__ == \"__main__\":\n    main(int(sys.argv[1] if len(sys.argv) \u003e 1 else 30))\n```\n\u003c/details\u003e\n\n## Reference\n\n### `get_key`\n\u003e Signature: `() -\u003e str`\n\nReturns a keypress from standard input. It exclusively identifies keypresses\nthat result in tangible inputs, therefore modifier keys like `shift` or\n`caps lock` are ignored. `outspin` also returns the actual input, meaning that\npressing, for instance, `shift+a` will make `get_key()` return `A`.\n\n\u003e [!Note]\n\u003e `outspin` translates dozens of ANSI codes to human-readable names under the\n\u003e hood. If you spot a case where an ANSI code (e.g. `\\x1b[15;2~`) doesn't get\n\u003e converted, please open an issue and/or submit a PR adding the code.\n\n### `wait_for`\n\u003e Signature: `(*keys: str) -\u003e str`\n\nWaits for one of the keys to be pressed and returns it.\n```pycon\n\u003e\u003e\u003e wait_for(*\"wasd\")  # pressing g t 4 2 q a\n'a'\n```\n`wait_for` requires at least one key to be provided.\n```pycon\n\u003e\u003e\u003e wait_for()\noutspin.OutspinValueError: No keys to wait for\n```\n\n### `pause`\n\u003e Signature: `(prompt: str | None = None) -\u003e None`\n\nDisplays the prompt and pauses the program until a key is pressed.  \nThe default prompt is `Press any key to continue...`.\n\n### `constants`\n\nA namespace containing a few useful characters groups:\n- `ARROWS`: `up` `down` `left` `right`\n- `F_KEYS`: `f1` → `f12`\n- `DIGITS` or `NUMBERS`: `0` → `9` (same as `string.digits`)\n- `LOWERCASE`: `a` → `z` (same as `string.ascii_lowercase`)\n- `UPPERCASE`: `A` → `Z` (same as `string.ascii_uppercase`)\n- `LETTERS`: `LOWERCASE` + `UPPERCASE` (same as `string.ascii_letters`)\n- `PUNCTUATION`: ``!\"#$%\u0026'()*+,-./:;\u003c=\u003e?@[\\]^_`{|}~`` (same as\n  `string.punctuation`)\n\n## Known Issues\n* Some combinations (like `shift+up` or `alt+shift+right`) may not work\n  correctly on Windows.\n\n## Contributing\n\nContributions are welcome!\n\nPlease open an issue before submitting a pull request\n(doesn't apply to minor changes like typos).\n\nTo get started:\n1. Clone your fork of the project.\n2. Install the project with [uv]:\n```sh\nuv sync\n```\n3. After you're done, use the following [`just`][just] recipes to check your\n   changes (or run the commands manually):\n```sh\njust check     # pytest, mypy, ruff\njust coverage  # pytest (with coverage), interrogate (docstring coverage)\n```\n\n## License\n`outspin` is licensed under the [MIT License].  \n© [trag1c], 2023–2024\n\n[MIT License]: https://opensource.org/license/mit/\n[just]: https://github.com/casey/just/\n[trag1c]: https://github.com/trag1c/\n[dahlia]: https://github.com/dahlia-lib/dahlia/\n[nouns.txt]: https://gist.github.com/trag1c/f74b2ab3589bc4ce5706f934616f6195/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrag1c%2Foutspin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftrag1c%2Foutspin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrag1c%2Foutspin/lists"}