commit b28b04c60378adb3e075ed7c9be575acf7171f29 Author: onyx-and-iris Date: Sat Feb 21 21:06:21 2026 +0000 initial commit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..bbec031 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +name: Publish to PyPI + +on: + release: + types: [published] + push: + tags: + - 'v*.*.*' + +jobs: + pypi-publish: + name: upload release to PyPI + runs-on: ubuntu-latest + environment: pypi + permissions: + # This permission is needed for private repositories. + contents: read + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: pdm-project/setup-pdm@v4 + + - name: Publish package distributions to PyPI + run: pdm publish \ No newline at end of file diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..4d71024 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,19 @@ +name: Ruff + +on: + push: + branches: [main] + + pull_request: + branches: [main] + + workflow_dispatch: + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 + with: + args: 'format --check --diff' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06d0662 --- /dev/null +++ b/.gitignore @@ -0,0 +1,212 @@ +# Generated by ignr: github.com/onyx-and-iris/ignr + +## Python ## +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# End of ignr diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77c9e18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Onyx and Iris + +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, sublicense, 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..988192a --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Lottery TUI + +[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![PyPI - Version](https://img.shields.io/pypi/v/lottery-tui.svg)](https://pypi.org/project/lottery-tui) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lottery-tui.svg)](https://pypi.org/project/q3rcon-tui) + +----- + +![img](./img/tui.png) + +## Table of Contents + +- [Installation](#installation) +- [License](#license) + +## Installation + +*with uv* + +```console +uv tool install lottery-tui +``` + +*with pipx* + +```console +pipx install lotter-tui +``` + +The TUI should now be discoverable as lottery-tui. + +## Use + +Launch the TUI, select a lottery and press the Draw button! + +To exit from the application press *q* or *Ctrl+q* + +## License + +`lottery-tui` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/img/tui.png b/img/tui.png new file mode 100755 index 0000000..2b34523 Binary files /dev/null and b/img/tui.png differ diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..0f3fcc2 --- /dev/null +++ b/pdm.lock @@ -0,0 +1,169 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:842afa14523f463c1a73e53c7aeb6d697673d95a2db9adbf935807b1fe5d021a" + +[[metadata.targets]] +requires_python = "==3.13.*" + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +requires_python = ">=3.7" +summary = "Links recognition library with FULL unicode support." +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "uc-micro-py", +] +files = [ + {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, + {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +requires_python = ">=3.10" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +extras = ["linkify"] +requires_python = ">=3.10" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "linkify-it-py<3,>=1", + "markdown-it-py==4.0.0", +] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +requires_python = ">=3.10" +summary = "Collection of plugins for markdown-it-py" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "markdown-it-py<5.0.0,>=2.0.0", +] +files = [ + {file = "mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f"}, + {file = "mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +requires_python = ">=3.10" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd"}, + {file = "platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[[package]] +name = "rich" +version = "14.3.3" +requires_python = ">=3.8.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", +] +files = [ + {file = "rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}, + {file = "rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}, +] + +[[package]] +name = "textual" +version = "8.0.0" +requires_python = "<4.0,>=3.9" +summary = "Modern Text User Interface framework" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "markdown-it-py[linkify]>=2.1.0", + "mdit-py-plugins", + "platformdirs<5,>=3.6.0", + "pygments<3.0.0,>=2.19.2", + "rich>=14.2.0", + "typing-extensions<5.0.0,>=4.4.0", +] +files = [ + {file = "textual-8.0.0-py3-none-any.whl", hash = "sha256:8908f4ebe93a6b4f77ca7262197784a52162bc88b05f4ecf50ac93a92d49bb8f"}, + {file = "textual-8.0.0.tar.gz", hash = "sha256:ce48f83a3d686c0fac0e80bf9136e1f8851c653aa6a4502e43293a151df18809"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +requires_python = ">=3.7" +summary = "Micro subset of unicode data files for linkify-it-py projects." +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, + {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fafa6d5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "lottery-tui" +version = "0.1.0" +description = "A terminal user interface for lottery games." +authors = [{ name = "onyx-and-iris", email = "code@onyxandiris.online" }] +dependencies = ["textual>=8.0.0"] +requires-python = ">=3.10" +readme = "README.md" +license = { text = "MIT" } + +[project.scripts] +lottery-tui = "lottery_tui.tui:main" + +[tool.pdm] +distribution = true diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..1d12d30 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,77 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.10 +target-version = "py310" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Unlike Black, use single quotes for strings. +quote-style = "single" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" diff --git a/src/lottery_tui/lottery.py b/src/lottery_tui/lottery.py new file mode 100644 index 0000000..1486167 --- /dev/null +++ b/src/lottery_tui/lottery.py @@ -0,0 +1,120 @@ +import random +from abc import ABC, abstractmethod +from typing import NamedTuple + + +class Result(NamedTuple): + """A named tuple to hold the results of a lottery draw.""" + + kind: str + numbers: list[int] + bonus: list[int] | None + + def __str__(self) -> str: + """Return a string representation of the lottery result.""" + out = f'Numbers: {", ".join(str(n) for n in self.numbers)}' + if self.bonus: + match self.kind: + case 'EuroMillions': + bonus_name = 'Lucky Stars' + case 'Set For Life': + bonus_name = 'Life Ball' + case 'Thunderball': + bonus_name = 'Thunderball' + case _: + bonus_name = 'Bonus Numbers' + out += f'\n{bonus_name}: {", ".join(str(n) for n in self.bonus)}' + return out + + +registry = {} + + +def register_lottery(cls): + """A decorator to register lottery classes in the registry.""" + registry[cls.__name__.lower()] = cls + return cls + + +class Lottery(ABC): + """An abstract base class for different types of lotteries.""" + + @abstractmethod + def draw(self): + """Perform a lottery draw.""" + + +@register_lottery +class UKlotto(Lottery): + """A class representing the UK Lotto lottery. + + Uk Lotto draws 6 numbers from a pool of 1 to 59, without replacement. + There is no bonus number in UK Lotto. + """ + + POSSIBLE_NUMBERS = range(1, 60) + + def draw(self): + """Perform a UK Lotto draw.""" + result = random.sample(UKlotto.POSSIBLE_NUMBERS, 6) + return Result(kind='UK Lotto', numbers=result, bonus=None) + + +@register_lottery +class EuroMillions(Lottery): + """A class representing the EuroMillions lottery. + + EuroMillions draws 5 numbers from a pool of 1 to 50, without replacement, + and 2 "Lucky Star" numbers from a separate pool of 1 to 12, also without replacement. + """ + + POSSIBLE_NUMBERS = range(1, 51) + POSSIBLE_BONUS_NUMBERS = range(1, 13) + + def draw(self): + """Perform a EuroMillions draw.""" + numbers = random.sample(EuroMillions.POSSIBLE_NUMBERS, 5) + bonus = random.sample(EuroMillions.POSSIBLE_BONUS_NUMBERS, 2) + return Result(kind='EuroMillions', numbers=numbers, bonus=bonus) + + +@register_lottery +class SetForLife(Lottery): + """A class representing the Set For Life lottery. + + Set For Life draws 5 numbers from a pool of 1 to 39, without replacement, + and 1 "Life Ball" number from a separate pool of 1 to 10, also without replacement. + """ + + POSSIBLE_NUMBERS = range(1, 40) + + def draw(self): + """Perform a Set For Life draw.""" + numbers = random.sample(SetForLife.POSSIBLE_NUMBERS, 5) + life_ball = [random.randint(1, 10)] + return Result(kind='Set For Life', numbers=numbers, bonus=life_ball) + + +@register_lottery +class Thunderball(Lottery): + """A class representing the Thunderball lottery. + + Thunderball draws 5 numbers from a pool of 1 to 39, without replacement, + and 1 "Thunderball" number from a separate pool of 1 to 14, also without replacement. + """ + + POSSIBLE_NUMBERS = range(1, 40) # Thunderball numbers range from 1 to 39 + + def draw(self): + """Perform a Thunderball draw.""" + numbers = random.sample(Thunderball.POSSIBLE_NUMBERS, 5) + thunderball = [random.randint(1, 14)] + return Result(kind='Thunderball', numbers=numbers, bonus=thunderball) + + +def request_lottery_obj(lottery_name: str) -> Lottery: + """Return a lottery object based on the provided lottery name.""" + lottery_cls = registry.get(lottery_name.lower()) + if lottery_cls is None: + raise ValueError(f"Lottery '{lottery_name}' not found.") + return lottery_cls() diff --git a/src/lottery_tui/tui.py b/src/lottery_tui/tui.py new file mode 100644 index 0000000..8b50b8f --- /dev/null +++ b/src/lottery_tui/tui.py @@ -0,0 +1,54 @@ +from textual.app import App, ComposeResult +from textual.containers import Container +from textual.widgets import Button, Label, Select, Static + +from .lottery import request_lottery_obj + + +class LotteryTUI(App): + """A Textual TUI for the Lottery application.""" + + CSS_PATH = 'tui.tcss' + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Container( + Static('Welcome to the Lottery TUI!', id='welcome'), + Static('Pick a lottery to play:', id='instructions'), + Select( + options=[ + ('UK Lotto', 'uklotto'), + ('EuroMillions', 'euromillions'), + ('Set For Life', 'setforlife'), + ('Thunderball', 'thunderball'), + ], + id='lottery-select', + ), + Button('Draw', id='draw-button'), + Label('', id='result-label'), + id='main-container', + ) + + def on_key(self, event): + """Handle key events.""" + if event.key == 'q': + self.exit() + + def on_button_pressed(self, event): + """Handle button press events.""" + if event.button.id == 'draw-button': + selected_lottery = self.query_one('#lottery-select').value + try: + lottery_obj = request_lottery_obj(selected_lottery) + result = lottery_obj.draw() + self.query_one('#result-label').update(f'Result: {result}') + except ValueError as e: + self.query_one('#result-label').update(str(e)) + + self.query_one('#result-label').update(str(result)) + + +def main(): + """Entry point for the Lottery TUI.""" + app = LotteryTUI() + app.run() diff --git a/src/lottery_tui/tui.tcss b/src/lottery_tui/tui.tcss new file mode 100644 index 0000000..5d2cc8b --- /dev/null +++ b/src/lottery_tui/tui.tcss @@ -0,0 +1,276 @@ +/* Lottery TUI CSS Styling */ + +/* Global App Styling */ +LotteryTUI { + background: $surface; +} + +/* Main Container */ +#main-container { + align: center middle; + background: #1a1a2e; + border: round #ffd700; + border-title-align: center; + border-title-color: #ffd700; + border-title-style: bold; + height: auto; + layout: vertical; + margin: 2; + min-height: 20; + min-width: 60; + padding: 3 5; + width: auto; +} + +/* Welcome Message */ +#welcome { + color: #ffd700; + content-align: center middle; + height: auto; + margin: 0 0 2 0; + padding: 1; + text-style: bold; + width: 100%; +} + +/* Instructions */ +#instructions { + color: #a0aec0; + content-align: center middle; + height: auto; + margin: 1 0 0 0; + padding: 1; + text-style: italic; + width: 100%; +} + +/* Lottery Select Styling */ +#lottery-select { + margin: 1 0 2 0; + width: 100%; +} + +/* Hover Effects */ +#welcome:hover { + color: #ffed4a; + text-style: bold italic; +} + +#instructions:hover { + color: #cbd5e0; + text-style: bold italic; +} + +/* Additional styling for potential future widgets */ + +/* Button styling for lottery buttons */ +Button { + background: #ffd700; + border: round #e6c200; + color: #1a1a2e; + height: 3; + margin: 1; + min-width: 12; + text-style: bold; +} + +Button:hover { + background: #ffed4a; + border: round #ffd700; + color: #16213e; +} + +Button:focus { + background: #e6c200; + border: round #b8860b; + color: #1a1a2e; +} + +/* Input styling for lottery number inputs */ +Input { + background: #2d3748; + border: round #4a5568; + color: #ffd700; + height: 3; + margin: 1; + padding: 0 1; +} + +Input:focus { + background: #374151; + border: round #ffd700; + color: #ffed4a; +} + +/* Label styling */ +Label { + color: #e2e8f0; + margin: 0 0 1 0; + text-style: bold; +} + +/* Draw Button Specific Styling */ +#draw-button { + background: #ffd700; + border: round #e6c200; + color: #1a1a2e; + height: 3; + margin: 1 0 0 0; + text-style: bold; + width: 100%; +} + +#draw-button:hover { + background: #ffed4a; + border: round #ffd700; + color: #16213e; +} + +#draw-button:focus { + background: #e6c200; + border: round #b8860b; + color: #1a1a2e; +} + +/* Results Label Styling - Enhanced Appearance */ +#result-label { + background: #1a365d; + border: thick #ffd700; + color: #ffffff; + height: auto; + margin: 1 0 0 0; + min-height: 4; + padding: 1 2; + text-style: bold; + content-align: left middle; + width: 100%; +} + +/* Container for lottery number display */ +.lottery-numbers { + align: center middle; + background: #2d3748; + border: round #ffd700; + height: auto; + margin: 2; + padding: 2; +} + +/* Individual lottery number balls */ +.lottery-ball { + background: #ffd700; + border: round #e6c200; + color: #1a1a2e; + height: 3; + margin: 0 1; + text-align: center; + text-style: bold; + width: 6; +} + +.lottery-ball:hover { + background: #ffed4a; + color: #16213e; +} + +/* Results display */ +.results { + background: #1a202c; + border: round #4a5568; + color: #e2e8f0; + height: auto; + margin: 2; + padding: 2; +} + +.winning-number { + color: #48bb78; + text-style: bold; +} + +.losing-number { + color: #f56565; + text-style: italic; +} + +/* Status bar */ +.status-bar { + background: #2d3748; + color: #a0aec0; + dock: bottom; + height: 1; + padding: 0 1; +} + +/* Header */ +.header { + background: #ffd700; + color: #1a1a2e; + dock: top; + height: 3; + text-align: center; + text-style: bold; +} + +/* Footer */ +.footer { + background: #1a1a2e; + color: #a0aec0; + dock: bottom; + height: 1; + text-align: center; + text-style: italic; +} + +/* Sidebar styling */ +.sidebar { + background: #2d3748; + border-right: solid #4a5568; + dock: left; + width: 20; +} + +/* Content area */ +.content { + background: $surface; + margin: 1; + padding: 1; +} + +/* Error messages */ +.error { + background: #fed7d7; + border: round #f56565; + color: #c53030; + margin: 1; + padding: 1; + text-style: bold; +} + +/* Success messages */ +.success { + background: #c6f6d5; + border: round #48bb78; + color: #22543d; + margin: 1; + padding: 1; + text-style: bold; +} + +/* Loading spinner */ +.loading { + color: #ffd700; + text-align: center; + text-style: bold; +} + +/* Prize display */ +.prize { + background: #ffd700; + border: round #e6c200; + color: #1a1a2e; + margin: 1; + padding: 2; + text-align: center; + text-style: bold; +} \ No newline at end of file