diff --git a/.gitignore b/.gitignore index aafda3a..e1e182a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,132 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +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/ + +# 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 +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.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/ + .vscode .idea diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf63df9..89b9309 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,59 +3,89 @@ ## Setup your development environment -You need: -* `Python3.6`: you can install it by following [Python3's documentation](https://www.python.org/downloads/). -* `curses`: available in standard library of `Python` but it doesn't work out-of-the-box on Windows. See [this](https://www.devdungeon.com/content/curses-windows-python) explanations to install `curses` on Windows. +You need [all requirements](README#requirements). ## Branches -* main : stable releases. +* `main` : stable releases. +* `dev` : beta releases. To fix a minor problem or add new features create a new branch in this form: `username-dev`. -Please test your code with `Python3.6` version before **pull request** to be sure not to break compatability. +Please push on the `dev` branch, any pull request on the `main` branch will be refused. -## Download +## Conventions -Download projet: -```bash -git clone https://github.com/Tim-ats-d/Visual-dialog -``` +When you make a pull request make sure to: -Install Visual-dialog using `pip`: -```bash -python3 -m pip install git+git://github.com/Tim-ats-d/Visual-dialog -``` -or update lib to the latest version: -```bash -python3 -m pip install git+git://github.com/Tim-ats-d/Visual-dialog --upgrade +* Test your code with `Python3.7` to be sure not to break compatability. +* Format your code according [PEP 8](https://www.python.org/dev/peps/pep-0008/). [PEP8 Check](https://github.com/quentinguidee/actions-pep8) will ensure that your code is correctly formatted . +* Document your code with [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) if you add new functionality. + +If you add a feature that changes the API, notify it explicitly. + +## Download + +Download the `dev` branch of the project and install dev release: +```sh +git clone -b dev https://github.com/Tim-ats-d/Visual-dialog.git +cd Visual-dialog +pip install . ``` The list of versions and their changelogs can be found [here](https://github.com/Tim-ats-d/Visual-dialog/releases/). ## Repository Structure The following snippet describes Visual-dialog's repository structure. +This tree does not contain a description of all the files in the repository, only the most relevant ones ```text . ├── .github/ -| Contains Github specific files such as actions definitions and issue templates. +│ Contains Github specific files such as actions definitions and issue templates. │ ├── doc/ -| Contains documentation. +│ Contains the files related to the documentation. +│ │ +│ ├── source/ +│ │ Contains images used in documentation. +│ │ │ +│ │ ├── images/ +│ │ │ Contains images used in documentation. +│ │ │ +│ │ ├── conf.py +│ │ │ Sphinx's configuration file. +│ │ │ +│ │ ├── index.rst +│ │ │ Documentation home page. +│ │ │ +│ │ └── visualdialog.rst +│ │ Documentation of all the public classes and methods in Visual-dialog. │ │ -│ ├── examples/ -│ │ Contains several examples of how to use Visual-dialog. +│ ├── make.bat +│ │ To generate documentation on Windows. │ │ -│ └── documentation.md -│ Documentation of public API (coming soon). +│ └── Makefile +│ To generate documentation on GNU/Linux or MacOS. +│ +├── examples/ +│ Contains several examples of use cases of Visual-dialog. +│ +├── tests/ +│ Contains tests for debugging libraries. │ ├── visualdialog/ -| Source for Visual-dialog's library. +│ Source for Visual-dialog's library. +│ │ +│ ├── __init__.py +│ │ +│ ├── box.py +│ │ Contains the parent class TextBox which serves as a basis for the implementation of the other classes. │ │ -| ├── __init__.py +│ ├── dialog.py +│ │ Contains the DialogBox class, which is the main class of the library. │ │ -| └── core.py -| Contains Visual-dialog's core functionnalities. +│ └── utils.py +│ Contains the classes and functions used but not related to the libriarie. │ ├── LICENSE │ @@ -68,5 +98,5 @@ The following snippet describes Visual-dialog's repository structure. ├── README.md │ └── setup.py - Installation of the library. + Installation of the library. ``` diff --git a/README.md b/README.md index 31def27..bc3e7cc 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,22 @@ -

- Visual-dialog -

- Library to make easier dialog box in terminal. -

FeaturesInstallation • + RequirementsDocumentationQuick startContributingLicense

-
- Demo -
+

+ Visual-dialog +
+ Library to make easier dialog box in terminal. +
+ This library is still under development. + API can change. +

## Features @@ -23,40 +24,107 @@ 🔖 Text coloring and formatting. -⚙️ Hackable and configurable . +⚙️ Hackable and configurable. ## Installation ### Using pip -```bash +Install Visual-dialog using `pip` (The lib is not yet available on **pypi**): + +```sh python3 -m pip install git+git://github.com/Tim-ats-d/Visual-dialog ``` -(The lib is not yet available on **pypy**). +or update lib to the latest version: + +```sh +python3 -m pip install git+git://github.com/Tim-ats-d/Visual-dialog --upgrade +``` -### Requirements -* **Python 3.6** or more. -* [`curses`](https://docs.python.org/3/library/curses.html) module (available on **UNIX** system by default). -* Knowledge of [`curses`](https://docs.python.org/3/library/curses.html) librairie. +### From source + +```sh +git clone https://github.com/Tim-ats-d/Visual-dialog.git +cd Visual-dialog +pip install . +``` + +## Requirements + +### Curses + +**Visual-dialog** works with `curses` Python module. It is available in the standard **Python** library on **UNIX** but it doesn’t work out-of-the-box on **Windows**. + +See [this explanations](https://www.devdungeon.com/content/curses-windows-python) to install `curses` on **Windows** (untested). + +### Other requirements +* [**Python 3.7**](https://www.python.org/downloads/) or more. +* [**Sphinx**](https://www.sphinx-doc.org/en/master/usage/installation.html) to generate the documentation of library. +* [**sphinx-rtd-theme**](https://pypi.org/project/sphinx-rtd-theme/) used as documentation theme. ## Quick-start -Read these [examples](doc/examples/). +### Hello world with **Visual-dialog** + +```python3 +import curses + +from visualdialog import DialogBox + + +x, y = (0, 0) +height, width = (35, 6) + +def main(stdscr): + curses.curs_set(False) + + textbox = DialogBox(x, y, + height, width, + title="Demo") + textbox.char_by_char(stdscr, + "Hello world") + + +curses.wrapper(main) +``` + +### Examples + +Other various examples showing the capabilities of **Visual-dialog** can be found in [examples](examples/). ## Documentation -Coming soon ! +Visualdialog's documentation is automatically generated from the source code by **Sphinx**. +To build it on **GNU/Linux** or **MacOS**: +```sh +git clone https://github.com/Tim-ats-d/Visual-dialog.git +cd Visual-dialog/doc +make html +``` +Or on **Windows** with **Git Bash**: +```sh +git clone https://github.com/Tim-ats-d/Visual-dialog.git +cd Visual-dialog/doc +./make.bat html +``` +Once generated, the result will be in the `doc/build/html/` folder. + +You can also generate the documentation in **Latex**, **Texinfo** or **man-pages**. ## Contributing We would love for you to contribute to improve **Visual-dialog**. -Take a look at our [Contributing guide](CONTRIBUTING.md) to get started. +For major changes, please open an issue first to discuss what you would like to change. + +Take a look at our [contributing guide](CONTRIBUTING.md) to get started. +You can also help by reporting **bugs**. ## License -Distributed under the **GPL-2.0 License** . See [LICENSE](LICENSE) for more information. +Distributed under the **GPL-2.0 License** . See [license](LICENSE) for more information. + ## Acknowledgements diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..ba501f6 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/examples/confrontation.py b/doc/examples/confrontation.py deleted file mode 100644 index 738a78b..0000000 --- a/doc/examples/confrontation.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -# confrontation_example.py - -import curses - -from visualdialog import DialogBox - - -def main(stdscr): - # Makes the cursor invisible. - curses.curs_set(0) - - # Definition of several colors pairs. - curses.start_color() - curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK) - curses.init_pair(2, curses.COLOR_MAGENTA, curses.COLOR_BLACK) - curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) - - textbox_position = (10, 10) # Position 10;10 in terminal. - textbox_dimension = (40, 6) # Length and width (in character). - - phoenix_wright = DialogBox( - *textbox_position, - *textbox_dimension, - title="Phoenix", title_colors_pair_nb=1 # Title and color_pair used to colored title. - ) - - april_may = DialogBox( - *textbox_position, - *textbox_dimension, - title="April", title_colors_pair_nb=2 # Title and color_pair used to colored title. - ) - - miles_edgeworth = DialogBox( - *textbox_position, - *textbox_dimension, - title="Edgeworth", title_colors_pair_nb=3 # Title and color_pair used to colored title. - ) - - # Definition of accepted key codes to pass a dialog. - # See documentation of curses constants for more informations. - phoenix_wright.confirm_dialog_key = (10, 32) # Key Enter and Space. - april_may.confirm_dialog_key = (10, 32) # Key Enter and Space. - miles_edgeworth.confirm_dialog_key = (10, 32) # Key Enter and Space. - - phoenix_wright.char_by_char(stdscr, - "This testimony is a pure invention !", - colors_pair_nb=0, - delay=0.03) - - phoenix_wright.getkey(stdscr) # Wait until a key contained in phoenix_wright.confirm_dialog_key is pressed. - stdscr.clear() # Clear the screen. - - phoenix_wright.char_by_char(stdscr, - "You're lying April May !", - colors_pair_nb=0, - flash_screen=True, - delay=0.03, - text_attributes=(curses.A_BOLD,)) - - phoenix_wright.getkey(stdscr) # Wait until a key contained in phoenix_wright.confirm_dialog_key is pressed. - stdscr.clear() - - april_may.char_by_char(stdscr, - "Arghh !", - colors_pair_nb=0, - delay=0.02, - text_attributes=(curses.A_ITALIC,)) - - april_may.getkey(stdscr) # Wait until a key contained in april_may.confirm_dialog_key is pressed. - stdscr.clear() - - miles_edgeworth.char_by_char(stdscr, - "OBJECTION !", - colors_pair_nb=0, - flash_screen=True, - delay=0.03, - text_attributes=(curses.A_BOLD,) - ) - - miles_edgeworth.getkey(stdscr) # Wait until a key contained in miles_edgeworth.confirm_dialog_key is pressed. - stdscr.clear() - - miles_edgeworth.char_by_char(stdscr, - "These accusations are irrelevant !", - colors_pair_nb=0, - delay=0.03) - - miles_edgeworth.getkey(stdscr) - stdscr.clear() - - -curses.wrapper(main) # Execution of the function. diff --git a/doc/examples/context.py b/doc/examples/context.py deleted file mode 100644 index 9978699..0000000 --- a/doc/examples/context.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -# monologue.py - -import curses - -from visualdialog import DialogBox - - -def main(stdscr): - # Makes the cursor invisible. - curses.curs_set(0) - - # Definition of several colors pairs. - curses.start_color() - curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) - curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK) - - textbox = DialogBox( - 0, 0, # Position 0;0 in terminal. - 40, 6, # Length and width (in character). - title="Tim-ats-d", - title_colors_pair_nb=1, # Title and color_pair used to colored title. - title_text_attributes=(curses.A_UNDERLINE, )) # Attribute to underline the title. - # It is necessary to think of passing a tuple even if it contains only one element - # (see doc for more informations). - - # Definition of accepted key codes to pass a dialog. - # See documentation of the curses constants for more informations. - textbox.confirm_dialog_key = (10, 32) # Key Enter and Space. - - sentences = { - "An important text.": (curses.A_BOLD, ), - "An action performed by a character.": (curses.A_ITALIC, ), - "An underlined text.": (curses.A_UNDERLINE, ), - "A very important text.": (curses.A_BOLD, curses.A_ITALIC), - "Incomprehensible gibberish ": (curses.A_ALTCHARSET, ), - "The colors of the front and the background reversed.": - (curses.A_REVERSE, ), - } - - for text, attributs in sentences.items(): - textbox.char_by_char( - stdscr, - text, - 2, # Display of the reply variable colored with color pair 2. - text_attributes=attributs) # Pass the attributes to the text. - - # Wait until a key contained in textbox.confirm_dialog_key is pressed. - textbox.getkey(stdscr) - stdscr.clear() # Clear the screen. - - -curses.wrapper(main) # Execution of the function. diff --git a/doc/examples/monologue.py b/doc/examples/monologue.py deleted file mode 100644 index dc17460..0000000 --- a/doc/examples/monologue.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# monologue.py - -import curses - -from visualdialog import DialogBox - - -def main(stdscr): - replys = ( - "Hello world, how are you today ?", - "Press a key to skip this dialog.", - "That is a basic example.", - "See doc for more informations." - ) - - # Makes the cursor invisible. - curses.curs_set(0) - - # Definition of several colors pairs. - curses.start_color() - curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) - curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK) - curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) - - textbox = DialogBox( - 20, 15, # Position 20;15 in terminal. - 40, 6, # Length and width (in character). - title="Dogm", title_colors_pair_nb=3) # Title and color_pair used to colored title. - - # Definition of accepted key codes to pass a dialog. - # See documentation of the curses constants for more informations. - textbox.confirm_dialog_key = (10, 32) # Key Enter and Space. - - # We iterate on each sentence contained in replys. - for reply in replys: - textbox.char_by_char(stdscr, - reply, - 2, # Display of the reply variable colored with color pair 2. - delay=0.04) # Set delay between each characters to 0.04 seconde. - - textbox.getkey(stdscr) # Waiting for a key press. - stdscr.clear() # Clear the screen. - - -# Execution of the function. -curses.wrapper(main) diff --git a/doc/examples/text_attributes.py b/doc/examples/text_attributes.py deleted file mode 100644 index 9978699..0000000 --- a/doc/examples/text_attributes.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -# monologue.py - -import curses - -from visualdialog import DialogBox - - -def main(stdscr): - # Makes the cursor invisible. - curses.curs_set(0) - - # Definition of several colors pairs. - curses.start_color() - curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) - curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK) - - textbox = DialogBox( - 0, 0, # Position 0;0 in terminal. - 40, 6, # Length and width (in character). - title="Tim-ats-d", - title_colors_pair_nb=1, # Title and color_pair used to colored title. - title_text_attributes=(curses.A_UNDERLINE, )) # Attribute to underline the title. - # It is necessary to think of passing a tuple even if it contains only one element - # (see doc for more informations). - - # Definition of accepted key codes to pass a dialog. - # See documentation of the curses constants for more informations. - textbox.confirm_dialog_key = (10, 32) # Key Enter and Space. - - sentences = { - "An important text.": (curses.A_BOLD, ), - "An action performed by a character.": (curses.A_ITALIC, ), - "An underlined text.": (curses.A_UNDERLINE, ), - "A very important text.": (curses.A_BOLD, curses.A_ITALIC), - "Incomprehensible gibberish ": (curses.A_ALTCHARSET, ), - "The colors of the front and the background reversed.": - (curses.A_REVERSE, ), - } - - for text, attributs in sentences.items(): - textbox.char_by_char( - stdscr, - text, - 2, # Display of the reply variable colored with color pair 2. - text_attributes=attributs) # Pass the attributes to the text. - - # Wait until a key contained in textbox.confirm_dialog_key is pressed. - textbox.getkey(stdscr) - stdscr.clear() # Clear the screen. - - -curses.wrapper(main) # Execution of the function. diff --git a/doc/make.bat b/doc/make.bat new file mode 100755 index 0000000..fa24171 --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/doc/source/_static/visual-dialog.png b/doc/source/_static/visual-dialog.png new file mode 100644 index 0000000..6b13647 Binary files /dev/null and b/doc/source/_static/visual-dialog.png differ diff --git a/doc/source/api.rst b/doc/source/api.rst new file mode 100644 index 0000000..403fabe --- /dev/null +++ b/doc/source/api.rst @@ -0,0 +1,8 @@ +API documentation +================= + +.. toctree:: + :maxdepth: 2 + + visualdialog.rst + utils.rst diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..ae0ab39 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,80 @@ +# +# 2020 Timéo Arnouts +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# + +import os +import sys +from datetime import datetime + +from visualdialog import __author__, __version__ + + +sys.path.insert(0, os.path.abspath("../../")) + +project = "Visual-dialog" +copyright = f"2021-{datetime.now().year}, {__author__}" +author = __author__ + +extensions = [ + "sphinx.ext.autodoc" +] + +master_doc = "index" +source_suffix = ".rst" +autodoc_member_order = "bysource" + +version = str(__version__) +release = version + +templates_path = ["_templates"] + +exclude_patterns = [] + +pygments_style = "friendly" + +html_title = "Visual-dialog Documentation" +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] +html_show_sourcelink = True +html_theme_options = { + "display_version": True +} + +html_logo = "_static/visual-dialog.png" + +latex_logo = "_static/visual-dialog.png" + +latex_elements = { + "pointsize": "12pt" +} + +latex_documents = [ + (master_doc, "Visual-dialog.tex", "Visual-dialog Documentation", + "Arnouts Timéo", "manual"), +] + +man_pages = [ + (master_doc, "visual-dialog", "Visual-dialog Documentation", + [author], 1) +] + +texinfo_documents = [ + (master_doc, "Visual-dialog", "Visual-dialog Documentation", + author, "Visual-dialog", "A library to make easier dialog box in terminal.", + "Miscellaneous"), +] diff --git a/doc/source/faq.rst b/doc/source/faq.rst new file mode 100644 index 0000000..27ba176 --- /dev/null +++ b/doc/source/faq.rst @@ -0,0 +1,24 @@ +Frequently asked questions +========================== + +Why is DialogBox a context manager? +----------------------------------- + +You can use this behavior to avoid having to instantiate a new dialog box. + +See the `dedicated example `_. + +How can I continue to manage screen display while a DialogBox is writing text on the screen? +-------------------------------------------------------------------------------------------- + +``char_by_char`` and ``word_by_word`` methods of ``DialogBox`` class accept a callback in parameter. +You can also pass arguments to this callback via ``cargs`` parameter. + +The past callback is executed after downtime delay between character or word display. +You can use this behavior to perform multiple tasks while ``DialogBox`` scrolling. + +I am not satisfied with the behavior of DialogBox, how can I change it? +----------------------------------------------------------------------- + +You can create your own derived class by inheriting from ``BaseTextBox``. +Additionally, you can override the methods of ``DialogBox``. diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..ab430fd --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,57 @@ +.. Visual-dialog documentation master file, created by + sphinx-quickstart on Sat Mar 6 17:30:24 2021. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Visual-dialog's documentation +======================================== + +**Visual-dialog** is a **Python library** that allows you to make dialog box in a terminal easily. +**Visual-dialog** uses ``curses`` to display text in terminals. + +.. image:: _static/visual-dialog.png + :align: center + +**Features:** + + - Automatic text scrolling. + - Text coloring and formatting. + - Hackable and configurable. + +.. IMPORTANT:: + I recommend that you have some knowledge of Python ``curses`` module in order to use the library to its full potential. + + Here several links to learn ``curses``: + + - https://docs.python.org/3/howto/curses.html#curses-howto + - https://docs.python.org/3/library/curses.html#module-curses + +Getting started +--------------- + +- **First steps:** +- **Examples:** Many examples are available in the `repository `_. + +Getting help +------------ + +- If you're looking for something specific, try the :ref:`index ` or :ref:`searching `. +- Report bugs in the `issue tracker `_. + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + + requirements.rst + installation.rst + api.rst + faq.rst + +Changelog +--------- + +The list of versions and their changelogs can be found on repository: + +- https://github.com/Tim-ats-d/Visual-dialog/releases/ + + diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 0000000..84818fb --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,13 @@ +Installation +============ + +Using PIP +--------- + +Install **Visual-dialog** using ``pip`` (The lib is not yet available on **pypi**):: + + python3 -m pip install git+git://github.com/Tim-ats-d/Visual-dialog + +or update library to the latest version:: + + python3 -m pip install git+git://github.com/Tim-ats-d/Visual-dialog --upgrade diff --git a/doc/source/requirements.rst b/doc/source/requirements.rst new file mode 100644 index 0000000..ef84bc4 --- /dev/null +++ b/doc/source/requirements.rst @@ -0,0 +1,19 @@ +Requirements +============ + +Curses +------ + +**Visual-dialog** works with ``curses`` Python module. +It is available in the standard **Python** library on **UNIX** but it doesn't work out-of-the-box on **Windows**. + +See this explanations to install ``curses`` on **Windows** (untested). + +- https://www.devdungeon.com/content/curses-windows-python + +Other requirements +------------------ + +- **Python 3.7** or more. +- `Sphinx `_ to generate the documentation of library. +- `sphinx-rtd-theme `_ used as documentation theme. diff --git a/doc/source/utils.rst b/doc/source/utils.rst new file mode 100644 index 0000000..2eacc2e --- /dev/null +++ b/doc/source/utils.rst @@ -0,0 +1,20 @@ +Utils +===== + +.. NOTE:: + A sub-module of **Visual-dialog** (``visualdialog.utils``) contains + functions and classes used by the private API. The context manager + ``TextAttributes`` is used by the library to manage in a more + pythonic way the textual ``curses`` attributes curses but you can + also use it in your programs. + + ``visualdialog.utils`` is automatically imported when importing + visualdialog so you can use its module functions and classes just like this:: + + import visualdialog + + visualdialog.function(args) + + +.. automodule:: visualdialog.utils + :members: diff --git a/doc/source/visualdialog.rst b/doc/source/visualdialog.rst new file mode 100644 index 0000000..f35da96 --- /dev/null +++ b/doc/source/visualdialog.rst @@ -0,0 +1,22 @@ +Text boxes +========== + +.. IMPORTANT:: + **Visual-dialog** contains two **modules**: ``visualdialog.box`` and ``visualdialog.dialog``. + These two **modules** are both imported when you import ``visualdialog``. + + Two **classes** are defined in these modules but only ``DialogBox`` is destined to be instantiated. + +TextBox +------- + +.. autoclass:: visualdialog.box.BaseTextBox + :members: + :undoc-members: + +DialogBox +--------- + +.. autoclass:: visualdialog.dialog.DialogBox + :undoc-members: + :members: diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..d3f3b1e --- /dev/null +++ b/examples/README.md @@ -0,0 +1,24 @@ +# Examples + +This directory contains several examples of use of **Visual-dialog**. +They are classified below by order of difficulty (approximately). + +## [Monologue](monologue.py) + +A complete concrete example of how to use **Visual-dialog**. + +## [Word](word.py) + +An example of using ``word_by_word`` method from text boxes. + +## [Text attributes](text_attributes.py) + +An example showing the possibilities of text formatting in a text box. + +## [Context](contex.py) + +An example of how to use a text box as a **context manager**. + +## [Confrontation](confrontation.py) + +A concrete example exploiting the possibilities of library. diff --git a/examples/confrontation.py b/examples/confrontation.py new file mode 100644 index 0000000..1a05cfa --- /dev/null +++ b/examples/confrontation.py @@ -0,0 +1,79 @@ +# confrontation.py +# +#  A concrete example exploiting the possibilities of Visual-dialog. + +import curses + +from visualdialog import DialogBox + + +# Definition of curses key constants. +# 10 and 32 correspond to enter and space keys. +PASS_DIALOG_KEY = (10, 32) + +def main(stdscr): + # Makes the cursor invisible. + curses.curs_set(False) + + # Definition of several colors pairs. + curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK) + curses.init_pair(2, curses.COLOR_MAGENTA, curses.COLOR_BLACK) + curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) + + width, height = 6, 35 # Width and height (in character). + + max_y, max_x = stdscr.getmaxyx() + + left_x = 2 # Left alignment. + right_x = max_x - height - 4 # Calculation of right alignment. + center_x = max_x // 2 - height // 2 # Calculation of center alignment. + bottom_y = max_y - width - 4 # Calculation of bottom alignment. + + phoenix_wright = DialogBox(left_x, bottom_y, + height, width, + title="Phoenix", + title_colors_pair_nb=1) # Title and color_pair used to colored title. + + april_may = DialogBox(center_x, bottom_y, + height, width, + title="April", + title_colors_pair_nb=2) + + miles_edgeworth = DialogBox(right_x, bottom_y, + height, width, + title="Edgeworth", + title_colors_pair_nb=3) + + # Definition of accepted key codes to pass a dialog. + phoenix_wright.confirm_dialog_key = PASS_DIALOG_KEY + april_may.confirm_dialog_key = PASS_DIALOG_KEY + miles_edgeworth.confirm_dialog_key = PASS_DIALOG_KEY + + phoenix_wright.char_by_char(stdscr, + "This testimony is a pure invention !", + delay=0.03) # Set delay between writting each characters to 0.03 seconde. + + phoenix_wright.char_by_char(stdscr, + "You're lying April May !", + flash_screen=True, # A short luminous glow will be displayed before writing the text. + delay=0.03, + text_attr=curses.A_BOLD) + + april_may.char_by_char(stdscr, + "Arghh !", + delay=0.02, + text_attr=curses.A_ITALIC) + + miles_edgeworth.char_by_char(stdscr, + "OBJECTION !", + flash_screen=True, + delay=0.03, + text_attr=curses.A_BOLD) + + miles_edgeworth.char_by_char(stdscr, + "These accusations are irrelevant !", + delay=0.03) + + +# Execution of main function. +curses.wrapper(main) diff --git a/examples/context.py b/examples/context.py new file mode 100644 index 0000000..c4c14e1 --- /dev/null +++ b/examples/context.py @@ -0,0 +1,34 @@ +# context.py +# +# An example of how to use a text box as a context manager. + +import curses + +from visualdialog import DialogBox + + +# Definition of curses key constants. +# 10 and 32 correspond to enter and space keys. +ENTER_KEY = 10 +SPACE_KEY = 32 + + +def main(stdscr): + # Makes the cursor invisible. + curses.curs_set(False) + + replys = ( + "This text is displayed by an anonymous text box.", + "Its behavior is the same as that of a normal dialog box.", + "The advantage is that the syntax is lighter." + ) + + for reply in replys: + # The keyword "as" allows to capture the returned DialogBox object. + with DialogBox(1, 1, 30, 6) as db: + db.confirm_dialog_key = (ENTER_KEY, SPACE_KEY) + db.char_by_char(stdscr, reply) + + +# Execution of main function. +curses.wrapper(main) diff --git a/examples/monologue.py b/examples/monologue.py new file mode 100644 index 0000000..44cceae --- /dev/null +++ b/examples/monologue.py @@ -0,0 +1,47 @@ +# monologue.py +# +#  A simple example of how to use Visual-dialog. + +import curses + +from visualdialog import DialogBox + + +# Definition of curses key constants. +# 10 and 32 correspond to enter and space keys. +ENTER_KEY = 10 +SPACE_KEY = 32 + +def main(stdscr): + replys = ( + "Hello world", + "Press a key to skip this dialog.", + "That is a basic example.", + "See doc for more informations." + ) + + # Makes the cursor invisible. + curses.curs_set(False) + + # Definition of several colors pairs. + curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) + curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK) + + textbox = DialogBox(1, 1, # Position 1;1 in stdscr. + 40, 6, # Length and width of textbox (in character). + title="Tim-ats-d", # Title of textbox. + title_colors_pair_nb=1) # Curses color_pair used to colored title. + + # Definition of accepted key codes to pass a dialog. + textbox.confirm_dialog_key = (ENTER_KEY, SPACE_KEY) + + # Iterate on each sentence contained in replys. + for reply in replys: + textbox.char_by_char(stdscr, + reply, + 2, # Display text colored with color pair 2. + delay=0.04) # Set delay between writting each characters to 0.04 seconde. + + +# Execution of main function. +curses.wrapper(main) diff --git a/examples/text_attributes.py b/examples/text_attributes.py new file mode 100644 index 0000000..2e25c83 --- /dev/null +++ b/examples/text_attributes.py @@ -0,0 +1,51 @@ +# text_attributes.py +# +# An example showing the possibilities of text formatting. + +import curses + +from visualdialog import DialogBox + + +# Definition of curses key constants. +# 10 and 32 correspond to enter and space keys. +ENTER_KEY = 10 +SPACE_KEY = 32 + +def main(stdscr): + # Makes the cursor invisible. + curses.curs_set(False) + + # Definition of several colors pairs. + curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_YELLOW) + curses.init_pair(2, curses.COLOR_MAGENTA, curses.COLOR_BLACK) + + demo_textbox = DialogBox(1, 1, + 40, 6, + title="Demo", + title_colors_pair_nb=1, # Display title colored with color pair 1. + title_text_attr=curses.A_UNDERLINE) # curse text attributes that will be applied to the title. + demo_textbox.confirm_dialog_key = (ENTER_KEY, SPACE_KEY) + + # A key/value dictionary containing the text and the attributes + # with which it will be displayed. + # You can pass one or more curses text attributes arguments as a tuple. + sentences = { + "An important text.": curses.A_BOLD, + "An action performed by a character.": curses.A_ITALIC, + "An underlined text.": curses.A_UNDERLINE, + "A very important text.": (curses.A_BOLD, curses.A_ITALIC), + "Incomprehensible gibberish.": curses.A_ALTCHARSET, + "A blinking text.": curses.A_BLINK, + "The colors of the front and the background reversed.": curses.A_REVERSE, + } + + for text, attributes in sentences.items(): + demo_textbox.char_by_char(stdscr, + text, + 2, # Display text colored with color pair 2. + text_attr=attributes) # Pass attributes to text. + + +# Execution of main function. +curses.wrapper(main) diff --git a/examples/word.py b/examples/word.py new file mode 100644 index 0000000..dd907c2 --- /dev/null +++ b/examples/word.py @@ -0,0 +1,40 @@ +# words.py +# +#  An example of using the word_by_word method from text boxes. + +import curses + +from visualdialog import DialogBox + + +# Definition of curses key constants. +# 10 and 32 correspond to enter and space keys. +ENTER_KEY = 10 +SPACE_KEY = 32 + +def main(stdscr): + instructions = ( + "Instead of the char_by_char method, word_by_word displays the " + "given text word by word.", + "It can be useful to make robots talk for example." + ) + + # Makes the cursor invisible. + curses.curs_set(False) + + textbox = DialogBox(1, 1, # Position 1;1 in stdscr. + 40, 6, # Length and width of textbox (in character). + title="Robot") # Title of textbox. + + # Definition of accepted key codes to pass a dialog. + textbox.confirm_dialog_key = (ENTER_KEY, SPACE_KEY) + + # Iterate on each sentence contained in replys. + for instruction in instructions: + textbox.word_by_word(stdscr, + instruction, + delay=0.2) # Set delay between writting each words to 0.1 seconde. + + +# Execution of main function. +curses.wrapper(main) diff --git a/setup.py b/setup.py index 06e57a4..f95b201 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,30 @@ -#!/usr/bin/env python3 +from setuptools import find_packages, setup + +from visualdialog import __author__, __version__ -from setuptools import setup, find_packages setup( name="visualdialog", - version=0.6, + version=__version__, packages=find_packages(), - author="Arnouts Timéo", + author=__author__, author_email="tim.arnouts@protonmail.com", description="A library to make easier dialog box in terminal.", long_description=open("README.md").read(), include_package_data=True, url="https://github.com/Tim-ats-d/Visual-dialog", classifiers=[ - "Development Status :: 3 - Alpha", "Environment :: Console :: Curses", + "Development Status :: 3 - Alpha", + "Environment :: Console :: Curses", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", "Natural Language :: English", "Natural Language :: French", "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3.0", - "Programming Language :: Python :: 3.1", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", - "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", "Topic :: Games/Entertainment :: Role-Playing", "Topic :: Software Development :: Libraries :: Python Modules" - ]) + ], + keywords="curses, ncurses, ui, dialogbox") diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 0000000..c0cbbcd --- /dev/null +++ b/tests/test.py @@ -0,0 +1,55 @@ +# test.py +# +#  This file contains the tests used to debug the library. + +import curses + +from visualdialog import DialogBox +import visualdialog + + +def main(stdscr): + text = ( + "Hello world, how are you today ? test", + "Press a key to skip this dialog. ", + "This is a basic example. See doc for more informations." + " If you have a problem don't hesitate to open an issue.", + ) + + curses.curs_set(0) + + curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) + curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK) + curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) + + textbox = DialogBox(0, 0, + 40, 6, + # title="Tim-ats-d", + # title_colors_pair_nb=3, + end_indicator="o") + + textbox.confirm_dialog_key = (32, ) + textbox.panic_key = (10, ) + + special_words = { + "test": (curses.A_BOLD, curses.A_ITALIC), + "this": (curses.A_BLINK, curses.color_pair(1)) + } + + def func(text: str): + stdscr.addstr(0, 0, str(visualdialog.__version__)) + + for reply in text: + textbox.char_by_char(stdscr, + reply, + cargs=(reply, ), + callback=func, + text_attr=(curses.A_ITALIC, curses.A_BOLD), + words_attr=special_words) + + with visualdialog.TextAttributes(stdscr, curses.A_BOLD, curses.A_ITALIC): + ... + + +if __name__ == "__main__": + curses.wrapper(main) diff --git a/visualdialog/__init__.py b/visualdialog/__init__.py index 1ee755f..2a3130e 100644 --- a/visualdialog/__init__.py +++ b/visualdialog/__init__.py @@ -1,9 +1,9 @@ -#!/usr/bin/env python3 - """ -A librairie which provides class and functions to make easier dialog box in terminal. +A library to make easier dialog box in terminal. """ -__version__ = 0.6 +__version__ = 0.7 +__author__ = "Timéo Arnouts" -from .core import DialogBox +from .dialog import DialogBox +from .utils import TextAttributes diff --git a/visualdialog/box.py b/visualdialog/box.py new file mode 100644 index 0000000..4d5df8d --- /dev/null +++ b/visualdialog/box.py @@ -0,0 +1,217 @@ +# box.py +# +# 2020 Timéo Arnouts +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +__all__ = ["BaseTextBox"] + +import curses +import curses.textpad +from numbers import Number +from typing import List, Tuple, Union + +from .utils import (CursesKeyConstant, + CursesKeyConstants, + CursesTextAttributesConstant, + CursesTextAttributesConstants, + CursesWindow, + TextAttributes) + + +class PanicError(Exception): + """Exception thrown when a key contained in ``TextBox.panic_key`` is + pressed. + + :param key: Key pressed that caused the exception to be thrown. + """ + def __init__(self, + key: CursesKeyConstant): + self.key = key + + def __str__(self): + return f"text box was aborted by pressing the {self.key} key" + + +class BaseTextBox: + """This class provides attributs and methods to manage a text box. + + .. NOTE:: + This class provides a general API for text boxes, it is not + intended to be instantiated. + + :param pos_x: x position of the dialog box in ``curses`` window + object on which methods will have effects. + + :param pos_y: y position of the dialog box in ``curses`` window + object on which methods will have effects. + + :param height: Height of the dialog box in ``curses`` window object + on which methods will have effects. + + :param width: Width of the dialog box in ``curses`` window object on + which methods will have effects. + + :param title: String that will be displayed in the upper left corner + of dialog box. + If title is an empty string, the title will not be displayed. + This defaults an empty string. + + :param title_colors_pair_nb: + Number of the curses color pair that will be used to color the + title. Zero corresponding to the pair of white color on black + background initialized by ``curses``). This defaults to ``0``. + + :param title_text_attr: + Dialog box title text attributes. It should be a single curses + text attribute or a tuple of curses text attribute. This + defaults to ``curses.A_BOLD``. + + :param downtime_chars: + List of characters that will trigger a ``downtime_chars_delay`` + time second between the writing of each character. + This defaults to ``(",", ".", ":", ";", "!", "?")``. + + :param downtime_chars_delay: + Waiting time in seconds after writing a character contained in + ``downtime_chars``. + This defaults to ``0.6``. + """ + + def __init__( + self, + pos_x: int, + pos_y: int, + height: int, + width: int, + title: str = "", + title_colors_pair_nb: int = 0, + title_text_attr: Union[CursesTextAttributesConstant, + CursesTextAttributesConstants] = curses.A_BOLD, + downtime_chars: Union[Tuple[str], + List[str]] = (",", ".", ":", ";", "!", "?"), + downtime_chars_delay: Number = .6): + self.pos_x, self.pos_y = pos_x, pos_y + self.height, self.width = height, width + + self.title_offsetting_y = 2 if title else 0 + + # Compensation for the left border of the dialog box. + self.text_pos_x = pos_x + 2 + # Compensation for the upper border of the dialog box. + self.text_pos_y = pos_y + self.title_offsetting_y + 1 + + self.nb_char_max_line = height - 4 + self.nb_lines_max = width - 2 + + self.title = title + if title: + self.title_colors = curses.color_pair(title_colors_pair_nb) + + # Test if only one argument is passed instead of a tuple + if isinstance(title_text_attr, int): + self.title_text_attr = (title_text_attr, ) + else: + self.title_text_attr = title_text_attr + + self.downtime_chars = downtime_chars + self.downtime_chars_delay = downtime_chars_delay + + #: List of accepted key codes to skip dialog. ``curses`` constants are supported. This defaults to an empty tuple. + self.confirm_dialog_key: List[CursesKeyConstant] = [] + #: List of accepted key codes to raise PanicError. ``curses`` constants are supported. This defaults to an empty tuple. + self.panic_key: List[CursesKeyConstant] = [] + + @property + def position(self) -> Tuple[int]: + """Returns a tuple contains x;y position of ``TextBox``. + + :returns: x;y position of ``TextBox``. + """ + return self.text_pos_x - 2, self.text_pos_y - 3 + + @property + def dimensions(self) -> Tuple[int]: + """Returns a tuple contains dimensions of ``TextBox``. + + :returns: Height and width of ``TextBox``. + """ + return self.height, self.width + + def framing_box(self, win: CursesWindow): + """Displays dialog box borders and his title. + + If attribute ``self.title`` is empty doesn't display the title. + + :param win: ``curses`` window object on which the method will + have effect. + """ + title_length = len(self.title) + 4 + title_width = 2 + + # Displays the title and the title box. + if self.title: + attr = (self.title_colors, *self.title_text_attr) + + curses.textpad.rectangle(win, + self.pos_y, + self.pos_x + 1, + self.pos_y + title_width, + self.pos_x + title_length) + + with TextAttributes(win, *attr): + win.addstr(self.pos_y + 1, + self.pos_x + 3, + self.title) + + # Displays the borders of the dialog box. + curses.textpad.rectangle(win, + self.pos_y + self.title_offsetting_y, + self.pos_x, + self.pos_y + self.title_offsetting_y + self.width, + self.pos_x + self.height) + + def getkey(self, win: CursesWindow): + """Blocks execution as long as a key contained in + ``self.confirm_dialog_key`` is not detected. + + + :param win: ``curses`` window object on which the method will + have effect. + :raises PanicError: If a key contained in ``self.panic_key`` is + pressed. + + .. NOTE:: + - To see the list of key constants please refer to + `this curses documentation + `_. + - This method uses ``window.getch`` method from ``curses`` + module. Please refer to `curses documentation + `_ + for more informations. + """ + while 1: + key = win.getch() + + if key in self.confirm_dialog_key: + break + elif key in self.panic_key: + raise PanicError(key) + else: + # Ignore incorrect keys. + ... diff --git a/visualdialog/choices.py b/visualdialog/choices.py new file mode 100644 index 0000000..cb000cc --- /dev/null +++ b/visualdialog/choices.py @@ -0,0 +1,62 @@ +# choices.py +# +# 2020 Timéo Arnouts +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +import curses +from typing import Any, Dict, Tuple, Union + +from .dialog import DialogBox +from .utils import (CursesTextAttributesConstants, + TextAttributes, + _make_chunk) + + +class ChoiceBox(DialogBox): + + def __init__(self, + **kwargs): + super().__init__(**kwargs) + + def chain( + self, + stdscr, + *propositions: Dict[str, Any]) -> Any: + """""" + super().framing_box(stdscr) + + for y, proposition in enumerate(propositions): + stdscr.addstr(self.pos_y + y*2, + self.pos_x, + proposition) + stdscr.refresh() + + +def main(stdscr): + choices_box = ChoiceBox(10, 10, 40, 4) + + choices_box.chain(stdscr, + "Quel âge as-tu ?" + "14", + "16", + "18") + stdscr.getch() + + +curses.wrapper(main) diff --git a/visualdialog/core.py b/visualdialog/core.py deleted file mode 100755 index b128c91..0000000 --- a/visualdialog/core.py +++ /dev/null @@ -1,484 +0,0 @@ -#!/usr/bin/env python3 -# -# visualdialog.py -# -# Copyright 2020 Timéo Arnouts -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -# -# - -__all__ = ["DialogBox"] -__version__ = 0.6 -__author__ = "Arnouts Timéo" - -import curses -import curses.textpad -import random -import textwrap -import time - -from typing import Callable, Generator, Iterable, Tuple - - -def _make_chunk(iterable: Iterable, chunk_length: int) -> Generator: - """Returns a generator that contains the given iterator separated into - chunk_length bundles.""" - return (iterable[chunk:chunk + chunk_length] - for chunk in range(0, len(iterable), chunk_length)) - - -class TextAttributes: - """A context manager to manage curses text attributs. - - Attributes - ---------- - window - `curses` window object for which the attributes will be managed. - attributes - List of attributes to activate and deactivate. - """ - - def __init__(self, stdscr, *attributes): - self.window = stdscr - self.attributes = attributes - - def __enter__(self): - """Activates one by the attributes contained in self.attributes.""" - for attribute in self.attributes: - self.window.attron(attribute) - - def __exit__(self, type, value, traceback): - """Disable one by the attributes contained in self.attributes.""" - for attribute in self.attributes: - self.window.attroff(attribute) - - -class DialogBox: - """This class provides methods and attributs to manage a dialog text - box. - - Attributes - ---------- - pos_x : int - x position of the dialog box in the terminal. - pos_y : int - y position of the dialog box in the terminal - box_length : int, - Length of the dialog box in the terminal. - box_width : int, - Width of the dialog box in the terminal. - title : str, - String that will be displayed in the upper left corner of dialog box. - title_colors_pair_nb : int, - Number of the curses color pair that will be used to color the title. - title_text_attributes : list of str or tuple of str, optional - Dialog box title text attributes (by default (curses.A_BOLD, )). - downtime_chars : Tuple[str], optional - List of characters that will trigger a `downtime_chars_delay` time second between the - writing of each character (by default (",", ".", ":", ";", "!", "?")). - `word_by_word` method was not effected by this parameter. - downtime_chars_delay : float, optional - Waiting time in seconds after writing a character contained in `downtime_chars` (by - default 0.6). - `word_by_word` method was not effected by this parameter. - end_dialog_indicator : str - Character that will be displayed in the lower right corner the character once all the - characters have been completed (by default "►"). - String with a length of more than 1 character can lead to an overflow of the dialog - box frame. - - Class attributes - ---------------- - confirm_dialog_key : tuple - List of accepted key codes to skip dialog curses constants are supported. - - To see the list of key constants please refer to `curse` module documentation - (https://docs.python.org/3/library/curses.html?#constants). - """ - confirm_dialog_key: Tuple = () - - def __init__(self, - pos_x: int, - pos_y: int, - box_length: int, - box_width: int, - title: str, - title_colors_pair_nb: int, - title_text_attributes: Tuple = (curses.A_BOLD, ), - downtime_chars: Tuple[str] = (",", ".", ":", ";", "!", "?"), - downtime_chars_delay: float = 0.6, - end_dialog_indicator: str = "►"): - self.pos_x, self.pos_y = pos_x, pos_y - self.box_length, self.box_width = box_length, box_width - - self.text_pos_x = pos_x + 2 # Compensation for the left border of the dialog box. - self.text_pos_y = pos_y + 3 # Compensation for the upper border of the dialog box. - - self.nb_char_max_line = box_length - 7 - self.nb_lines_max = box_width - 2 - - self.title = title - self.title_colors = curses.color_pair(title_colors_pair_nb) - self.title_text_attributes = title_text_attributes - - self.end_dialog_indicator_pos_x = pos_x + box_length - 2 - self.end_dialog_indicator_pos_y = pos_y + box_width + 1 - - self.downtime_chars = downtime_chars - self.downtime_chars_delay = downtime_chars_delay - - self.end_dialog_indicator_char = end_dialog_indicator - - def __enter__(self): - """Returns self.""" - return self - - def __exit__(self, type, value, traceback): - """Returns None.""" - ... - - def framing_box(self, stdscr): - """Displays dialog box and his title. - - Displayed dialog box have for position self.pos_x;self.pos_y and for size - `self.box_length` × `self.box_width`. - - Parameters - --------- - stdscr - `curses` window object on which the method will have effect. - - Returns - ------- - None. - - Notes - ----- - Method flow: - - Display title frame box. - - Display title . - - Display text frame box. - """ - title_box_length = len(self.title) + 4 - title_box_width = 2 - - attr = (self.title_colors, *self.title_text_attributes) - - curses.textpad.rectangle(stdscr, self.pos_y, self.pos_x + 1, - self.pos_y + title_box_width, - self.pos_x + title_box_length) - - with TextAttributes(stdscr, *attr): - stdscr.addstr(self.pos_y + 1, self.pos_x + 3, self.title) - - curses.textpad.rectangle(stdscr, self.pos_y + 2, self.pos_x, - self.pos_y + 2 + self.box_width, - self.pos_x + self.box_length) - - def getkey(self, stdscr): - """Blocks execution as long as a key contained in `self.confirm_dialog_key` is - not detected. - - Parameters - --------- - stdscr - `curses` window object on which the method will have effect. - - Returns - ------- - None. - - See also - -------- - - To see the list of key constants please refer to `curse` module documentation - (https://docs.python.org/3/library/curses.html?#constants). - - Documentation of `window.getch` method from `curses` module - (https://docs.python.org/3/library/curses.html?#curses.window.getch). - """ - while 1: - if stdscr.getch() in self.confirm_dialog_key: - break - - def _display_end_dialog_indicator(self, - stdscr, - text_attributes: Tuple = (curses.A_BOLD, curses.A_BLINK)): - """Displays an end of dialog indicator in the lower right corner of textbox. - - Parameters - ---------- - stdscr - `curses` window object on which the method will have effect. - text_attributes : tuple of curses text attribute constants, optional - Text attributes of `end_dialog_indicator` method - (by default (curses.A_BOLD, curses.A_BLINK)). - - Returns - ------- - None. - - See also - -------- - """ - if self.end_dialog_indicator_char: - with TextAttributes(stdscr, *text_attributes): - stdscr.addch(self.end_dialog_indicator_pos_y, - self.end_dialog_indicator_pos_x, - self.end_dialog_indicator_char) - - def char_by_char(self, - stdscr, - text: str, - colors_pair_nb: int, - text_attributes: Tuple = (), - flash_screen: bool = False, - delay: float = .05, - random_delay: Tuple[int, int] = (0, 0), - callback: Callable = None, - cargs=()): - """Writes the given text character by character at position in the current dialog box. - - Parameters - ---------- - stdscr - `curses` window object on which the method will have effect. - text : str - Text that will be displayed character by character in the dialog box. This text can - be wrapped to fit the proportions of the dialog box. See Notes section for more - informations. - colors_pair_nb : int, - Number of the curses color pair that will be used to color the text. - text_attributes : tuple of curses text attribute constants, optional - Dialog box text attributes (by default an empty tuple). - flash_screen : bool, optional - Allows or not to flash screen with a short light effect done before writing the first - character via `flash` function from `curses` module (by default False). - delay : int or float, optional - Waiting time between the writing of each character of text in second (by default 0.05). - random_delay : list of two number or tuple of two number, optional - Waiting time between the writing of each character in seconds where time waited is a - random number generated in `random_delay` interval (by default (0, 0)). - callback : callable, optional - Callable called after writing a character and the delay time has elapsed - (by default None). - cargs : list or tuple, optional - All the arguments that will be passed to callback (by default an empty tuple). - - Returns - ------- - None. - - Notes - ----- - Method flow: - - Calling `framing_box` method. - - Flash screen depending `flash_screen` parameter. - - Cutting text into line via `wrap` function from `textwrap` module (to stay within the - dialog box frame). - - Writing paragraph by paragraph. - - Writing each line of the current paragraph, character by character. - - Calling `_display_end_dialog_indicator` method. - - Notes - ----- - If the volume of text displayed is too large to be contained in a dialog box, text - will be automatically cut into paragraphs. The screen will be completely cleaned when - writing each paragraph via `window.clear()` method of `curses` module. - - See Also - -------- - - Documentation of `wrap` function from `textwrap` module for more information - of the behavior of text wrap - (https://docs.python.org/fr/3.8/library/textwrap.html#textwrap.wrap). - - Documentation of `flash` function from `curses` module - (https://docs.python.org/3/library/curses.html?#curses.flash). - - Documentation of `window.clear()` method from `curses` module - (https://docs.python.org/3/library/curses.html?#curses.window.clear). - """ - self.framing_box(stdscr) - - if flash_screen: - curses.flash() - - wrapped_text = textwrap.wrap(text, self.nb_char_max_line - 1) - wrapped_text = _make_chunk(wrapped_text, self.nb_lines_max) - - for paragraph in wrapped_text: - stdscr.clear() - self.framing_box(stdscr) - for y, line in enumerate(paragraph): - for x, char in enumerate(line): - attr = (curses.color_pair(colors_pair_nb), - *text_attributes) - - with TextAttributes(stdscr, *attr): - stdscr.addstr(self.text_pos_y + y, self.text_pos_x + x, - char) - stdscr.refresh() - - if char in self.downtime_chars: - time.sleep(self.downtime_chars_delay + - random.uniform(*random_delay)) - else: - time.sleep(delay + random.uniform(*random_delay)) - - if callback: - callback(*cargs) - - self._display_end_dialog_indicator(stdscr) - - def word_by_word(self, - stdscr, - text: str, - colors_pair_nb: int, - cut_char: str = " ", - text_attributes: Tuple = (), - flash_screen: bool = False, - delay: float = .15, - random_delay: Tuple[int, int] = (0, 0), - callback: Callable = None, - cargs=()): - """Writes the given text word by word at position at position in the current dialog box. - - Parameters - ---------- - stdscr - `curses` window object on which the method will have effect. - text : str - Text that will be displayed word by word in the dialog box. This text can be wrapped - to fit the proportions of the dialog box. See Notes section for more informations. - colors_pair_nb : int - Number of the curses color pair that will be used to color the text. - cut_char : str, optional - The delimiter according which to split the text in word (by default a space). - flash_screen : bool, optional - Allows or not to flash screen with a short light effect done before writing the first - word via `flash` function from `curses` module (by default False). - delay : int or float, optional - Waiting time between the writing of each character of text in second (by default 0.15). - random_delay : list of two number or tuple of two number, optional - Waiting time between the writing of each character in seconds where time waited is a - random number generated in `random_delay` interval (by default (0, 0)). - callback : callable, optional - Callable called after writing a character and the `delay` time has elapsed - (by default None). - cargs : list or tuple, optional - All the arguments that will be passed to callback (by default an empty tuple). - - Returns - ------- - None. - - Notes - ----- - Method flow: - - Calling `framing_box` method. - - Flash screen depending `flash_screen` parameter. - - Cutting text into line via `textwrap.wrap` function from `textwrap` module (to - stay within the dialog box frame). - - Writing each line of the current paragraph, word by word. - - Calling `_display_end_dialog_indicator` method. - - Notes - ----- - If the volume of text displayed is too large to be contained in a dialog box, text - will be automatically cut into paragraphs. The screen will be completely cleaned when - writing each paragraph via `window.clear` method from `curses` module. - - See Also - -------- - - Documentation of `wrap` function from `textwrap` module for more information - on the behavior of text wrap - (https://docs.python.org/fr/3.8/library/textwrap.html#textwrap.wrap). - - Documentation of `flash` function from `curses` module - (https://docs.python.org/3/library/curses.html?#curses.flash). - - Documentation of `window.clear()` method from `curses` module - (https://docs.python.org/3/library/curses.html?#curses.window.clear). - """ - self.framing_box(stdscr) - - if flash_screen: - curses.flash() - - attr = (curses.color_pair(colors_pair_nb), - *text_attributes) - - wrapped_text = textwrap.wrap(text, self.nb_char_max_line - 1) - wrapped_text = _make_chunk(wrapped_text, self.nb_lines_max) - - for paragraph in wrapped_text: - stdscr.clear() - self.framing_box(stdscr) - for y, line in enumerate(paragraph): - offsetting_x = 0 - for word in line.split(cut_char): - attr = (curses.color_pair(colors_pair_nb), - *text_attributes) - - with TextAttributes(stdscr, *attr): - stdscr.addstr(self.text_pos_y + y, self.text_pos_x + offsetting_x, - word) - stdscr.refresh() - - offsetting_x += len(word) + 1 # Compensates for the space between words. - time.sleep(delay + random.uniform(*random_delay)) - - if callback: - callback(*cargs) - - self._display_end_dialog_indicator(stdscr) - - -def main(stdscr): - text = ("Hello world, how are you today ? ", - "Press a key to skip this dialog. " - "This is a basic example. See doc for more informations. " - "If you have a problem don't hesitate to open an issue.",) - - curses.curs_set(0) - - curses.start_color() - curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) - curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK) - curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) - - - textbox = DialogBox(20, 15, - 40, 6, - title="Tim-ats-d", - title_colors_pair_nb=3, - end_dialog_indicator="►") - - textbox.confirm_dialog_key = (10, 32) - - def func(reply: str): - stdscr.addstr(0, 0, reply) - - for reply in text: - textbox.char_by_char(stdscr, - reply, - 2, - cargs=(reply, ), - callback=func, - text_attributes=(curses.A_ITALIC, curses.A_BOLD)) - - textbox.getkey(stdscr) - stdscr.clear() - - -if __name__ == "__main__": - curses.wrapper(main) - diff --git a/visualdialog/dialog.py b/visualdialog/dialog.py new file mode 100644 index 0000000..211dada --- /dev/null +++ b/visualdialog/dialog.py @@ -0,0 +1,389 @@ +# dialog.py +# +# 2020 Timéo Arnouts +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +__all__ = ["DialogBox"] + +import curses +from numbers import Number +import random +import textwrap +import time +from typing import Callable, Dict, List, Optional, Tuple, Union + +from .box import BaseTextBox +from .utils import (CursesTextAttributesConstant, + CursesTextAttributesConstants, + CursesWindow, + TextAttributes, + _make_chunk) + + +class DialogBox(BaseTextBox): + """This class provides methods and attributs to manage a dialog box. + + :param end_dialog_indicator: Character that will be displayed in the + lower right corner the character once all the characters have + been completed. String with a length of more than one character + can lead to an overflow of the dialog box frame. This defaults + to ``"►"``. + + :key kwargs: Keyword arguments correspond to the instance attributes + of ``TextBox``. + + .. NOTE:: + This class inherits from ``BaseTextBox``. + + .. NOTE:: + This class is a context manager. + + .. WARNING:: + Parameters ``downtime_chars`` and ``downtime_chars_delay`` do + not affect ``word_by_word`` method. + """ + + def __init__( + self, + pos_x: int, + pos_y: int, + height: int, + width: int, + title: str = "", + title_colors_pair_nb: int = 0, + title_text_attr: Union[CursesTextAttributesConstant, + CursesTextAttributesConstants] = curses.A_BOLD, + downtime_chars: Union[Tuple[str], + List[str]] = (",", ".", ":", ";", "!", "?"), + downtime_chars_delay: Number = .6, + end_indicator: str = "►"): + BaseTextBox.__init__(self, + pos_x, pos_y, + height, width, + title, + title_colors_pair_nb, title_text_attr, + downtime_chars, downtime_chars_delay) + + self.end_indicator_char = end_indicator + self.end_indicator_pos_x = self.pos_x + self.height - 2 + + if self.title: + self.end_indicator_pos_y = self.pos_y + self.width + 1 + else: + self.end_indicator_pos_y = self.pos_y + self.width - 1 + + self.text_wrapper = textwrap.TextWrapper(width=self.nb_char_max_line) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + ... + + def _display_end_indicator( + self, + win: CursesWindow, + text_attr: CursesTextAttributesConstants = (curses.A_BOLD, + curses.A_BLINK)): + """Displays an end indicator in the lower right corner of + textbox. + + :param win: ``curses`` window object on which the method + will have effect. + + :param text_attr: Text attributes of + ``end_indicator`` method. This defaults to + ``(curses.A_BOLD, curses.A_BLINK)``. + """ + if self.end_indicator_char: + with TextAttributes(win, *text_attr): + win.addch(self.end_indicator_pos_y, + self.end_indicator_pos_x, + self.end_indicator_char) + + def char_by_char( + self, + win: CursesWindow, + text: str, + colors_pair_nb: int = 0, + text_attr: Union[CursesTextAttributesConstant, + CursesTextAttributesConstants] = (), + words_attr: Union[Dict[Tuple[str], CursesTextAttributesConstant], + Dict[Tuple[str], CursesTextAttributesConstants]] = {}, + flash_screen: bool = False, + delay: Number = .04, + random_delay: Union[Tuple[Number], List[Number]] = (0, 0), + callback: Callable = lambda: None, + cargs: Union[Tuple, List] = ()): + """Writes the given text character by character in the current + dialog box. + + :param win: ``curses`` window object on which the method will + have effect. + + :param text: Text that will be displayed character by character + in the dialog box. This text can be wrapped to fit the + proportions of the dialog box. + + :param colors_pair_nb: Number of the curses color pair that + will be used to color the text. The number zero + corresponding to the pair of white color on black + background initialized by ``curses``). This defaults to + ``0``. + + :param text_attr: Dialog box curses text attributes. It should + be a single curses text attribute or a tuple of curses text + attribute. This defaults an empty tuple. + + :param words_attr: Dictionary composed of string as a key and a + single curses text attribute or tuple as a value. Each key + is colored with its associated values This defaults to an + empty dictionary. + + :param flash_screen: Allows or not to flash screen with a short + light effect done before writing the first character by + ``flash`` function from ``curses`` module. This defaults to + ``False``. + + :param delay: Waiting time between the writing of each character + of text in second. This defaults to ``0.04``. + + :param random_delay: Waiting time between the writing of each + character in seconds where time waited is a random number + generated in ``random_delay`` interval. This defaults to + ``(0, 0)``. + + :param callback: Callable called after writing a character and + the delay time has elapsed. This defaults to a lambda which + do nothing. + + :param cargs: All the arguments that will be passed to callback. + This defaults to an empty tuple. + + .. NOTE:: + Method flow: + - Calling ``framing_box`` method. + - Flash screen depending ``flash_screen`` parameter. + - Cutting text into line to stay within the dialog box + frame. + - Writing paragraph by paragraph. + - Writing each line of the current paragraph, character + by character. + - Waits until a key contained in the class attribute + ``confirm_dialog_key`` was pressed before writing the + following paragraph. + - Complete cleaning ``win``. + + .. WARNING:: + If the volume of text displayed is too large to be contained + in a dialog box, text will be automatically cut into + paragraphs using ``textwrap.wrap`` function. See + `textwrap module documentation + `_. + for more information of the behavior of text wrap. + + .. WARNING:: + ``win`` will be completely cleaned when writing each + paragraph by ``window.clear()`` method of ``curses`` + module. + """ + self.framing_box(win) + + if flash_screen: + curses.flash() + + wrapped_text = self.text_wrapper.wrap(text) + wrapped_text = _make_chunk(wrapped_text, self.nb_lines_max) + + for paragraph in wrapped_text: + win.clear() + self.framing_box(win) + + for y, line in enumerate(paragraph): + offsetting_x = 0 + for word in line.split(): + if word in words_attr.keys(): + attr = words_attr[word] + + # Test if only one argument is passed instead of a tuple. + if isinstance(attr, int): + attr = (attr, ) + else: + if isinstance(text_attr, int): + text_attr = (text_attr, ) + + attr = (curses.color_pair(colors_pair_nb), *text_attr) + + with TextAttributes(win, *attr): + for x, char in enumerate(word): + win.addstr(self.text_pos_y + y, + self.text_pos_x + x + offsetting_x, + char) + win.refresh() + + if char in self.downtime_chars: + time.sleep(self.downtime_chars_delay + + random.uniform(*random_delay)) + else: + time.sleep(delay + + random.uniform(*random_delay)) + + callback(*cargs) + + # Waiting for space character. + time.sleep(delay) + + # Compensates for the space between words. + offsetting_x += len(word) + 1 + + self._display_end_indicator(win) + self.getkey(win) + + def word_by_word( + self, + win: CursesWindow, + text: str, + colors_pair_nb: int = 0, + cut_char: str = " ", + text_attr: Union[CursesTextAttributesConstant, + CursesTextAttributesConstants] = (), + words_attr: Union[Dict[Tuple[str], CursesTextAttributesConstant], + Dict[Tuple[str], CursesTextAttributesConstants]] = {}, + flash_screen: bool = False, + delay: Number = .15, + random_delay: Union[Tuple[Number], List[Number]] = (0, 0), + callback: Callable = lambda: None, + cargs: Union[Tuple, List] = ()): + """Writes the given text word by word at position in the current + dialog box. + + :param win: ``curses`` window object on which the method will + have effect. + + :param text: Text that will be displayed word by word in the + dialog box. This text can be wrapped to fit the proportions + of the dialog box. + + :param colors_pair_nb: + Number of the curses color pair that will be used to color + the text. The number zero corresponding to the pair of + white color on black background initialized by ``curses``). + This defaults to ``0``. + + :param text_attr: Dialog box curses text attributes. It should + be a single curses text attribute or a tuple of curses text + attribute. This defaults an empty tuple. + + :param words_attr: Dictionary composed of string as a key and a + single curses text attribute or tuple as a value. Each key + is colored with its associated values This defaults to an + empty dictionary. + + :param cut_char: The delimiter according which to split the text + in word. This defaults to ``" "``. + + :param flash_screen: Allows or not to flash screen with a short + light effect done before writing the first character by + ``flash`` function from ``curses`` module. This defaults to + ``False``. + + :param delay: Waiting time between the writing of each word of + ``text`` in second. This defaults to ``0.15``. + + :param random_delay: Waiting time between the writing of each + word in seconds where time waited is a random number + generated in ``random_delay`` interval. This defaults to + ``(0, 0)``. + + :param callback: Callable called after writing a word and the + delay time has elapsed. This defaults to a lambda which do + nothing. + + :param cargs: All the arguments that will be passed to callback. + This defaults to an empty tuple. + + .. NOTE:: + Method flow: + - Calling ``framing_box`` method. + - Flash screen depending ``flash_screen`` parameter. + - Cutting text into line to stay within the dialog box + frame. + - Writing paragraph by paragraph. + - Writing each line of the current paragraph, word by + word. + - Waits until a key contained in the class attribute + ``confirm_dialog_key`` was pressed before writing the + following paragraph. + - Complete cleaning ``win``. + + .. WARNING:: + If the volume of text displayed is too large to be contained + in a dialog box, text will be automatically cut into + paragraphs using ``textwrap.wrap`` function. See + `textwrap module documentation + `_ + for more information of the behavior of text wrap. + + .. WARNING:: + ``win`` will be completely cleaned when writing each + paragraph by ``window.clear()`` method of ``curses`` + module. + """ + self.framing_box(win) + + if flash_screen: + curses.flash() + + attr = (curses.color_pair(colors_pair_nb), *text_attr) + + wrapped_text = self.text_wrapper.wrap(text) + wrapped_text = _make_chunk(wrapped_text, self.nb_lines_max) + + for paragraph in wrapped_text: + win.clear() + self.framing_box(win) + for y, line in enumerate(paragraph): + offsetting_x = 0 + for word in line.split(cut_char): + if word in words_attr.keys(): + attr = words_attr[word] + + # Test if only one argument is passed instead of a tuple. + if isinstance(text_attr, int): + text_attr = (text_attr, ) + else: + if isinstance(text_attr, int): + text_attr = (text_attr, ) + + attr = (curses.color_pair(colors_pair_nb), *text_attr) + + with TextAttributes(win, *attr): + win.addstr(self.text_pos_y + y, + self.text_pos_x + offsetting_x, word) + win.refresh() + + # Compensates for the space between words. + offsetting_x += len(word) + 1 + + time.sleep(delay + random.uniform(*random_delay)) + + callback(*cargs) + + self._display_end_indicator(win) + self.getkey(win) diff --git a/visualdialog/utils.py b/visualdialog/utils.py new file mode 100644 index 0000000..9396e4c --- /dev/null +++ b/visualdialog/utils.py @@ -0,0 +1,79 @@ +# utils.py +# +# 2020 Timéo Arnouts +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +from contextlib import ContextDecorator +import _curses +from typing import (Generator, Iterable, List, NoReturn, Tuple, TypeVar, + Union) + + +CursesWindow = _curses.window + +#: curses text attribute constants are integers. +#: See https://docs.python.org/3/library/curses.html?#constants +CursesTextAttributesConstant = int +CursesTextAttributesConstants = Union[Tuple[int], List[int]] + +#: curses key constants are integers. +#: See https://docs.python.org/3/library/curses.html?#constants +CursesKeyConstant = int +CursesKeyConstants = Union[Tuple[int], List[int]] + + +def _make_chunk(iterable: Union[Tuple, List], + chunk_length: int) -> Generator: + """Returns a tuple that contains given iterable separated into + ``chunk_length`` bundles. + + :returns: Generator separated into ``chunk_length`` bundles. + """ + return (iterable[chunk:chunk + chunk_length] + for chunk in range(0, len(iterable), chunk_length)) + + +class TextAttributes(ContextDecorator): + """A context manager to manage ``curses`` text attributes. + + :param win: ``curses`` window object for which the attributes will + be managed. + + :param attributes: Iterable of ``curses`` text attributes to activate + and desactivate. + """ + def __init__(self, + win: CursesWindow, + *attributes: Iterable[CursesTextAttributesConstant]): + self.win = win + self.attributes = attributes + + def __enter__(self) -> NoReturn: + """Activate one by one attributes contained in self.attributes + on ``self.win``. + """ + for attr in self.attributes: + self.win.attron(attr) + + def __exit__(self, type, value, traceback) -> NoReturn: + """Disable one by one attributes contained in + ``self.attributes`` on ``self.win``. + """ + for attr in self.attributes: + self.win.attroff(attr)