├── .coveragerc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── stale.yml └── workflows │ └── test.yml ├── .gitignore ├── .landscape.yml ├── .pylintrc ├── .readthedocs.yaml ├── .stickler.yml ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── PULL_REQUEST_TEMPLATE.md ├── README.rst ├── asciimatics ├── __init__.py ├── constants.py ├── effects.py ├── event.py ├── exceptions.py ├── parsers.py ├── particles.py ├── paths.py ├── renderers │ ├── __init__.py │ ├── base.py │ ├── box.py │ ├── charts.py │ ├── figlettext.py │ ├── fire.py │ ├── images.py │ ├── kaleidoscope.py │ ├── plasma.py │ ├── players.py │ ├── rainbow.py │ ├── rotatedduplicate.py │ ├── scales.py │ ├── speechbubble.py │ └── typewriter.py ├── scene.py ├── screen.py ├── sprites.py ├── strings.py ├── utilities.py └── widgets │ ├── __init__.py │ ├── baselistbox.py │ ├── basepicker.py │ ├── button.py │ ├── checkbox.py │ ├── datepicker.py │ ├── divider.py │ ├── dropdownlist.py │ ├── filebrowser.py │ ├── frame.py │ ├── label.py │ ├── layout.py │ ├── listbox.py │ ├── multicolumnlistbox.py │ ├── popupdialog.py │ ├── popupmenu.py │ ├── radiobuttons.py │ ├── scrollbar.py │ ├── temppopup.py │ ├── text.py │ ├── textbox.py │ ├── timepicker.py │ ├── utilities.py │ ├── verticaldivider.py │ └── widget.py ├── doc ├── build.sh └── source │ ├── _static │ └── custom.css │ ├── _templates │ └── page.html │ ├── animation.rst │ ├── asciimatics.renderers.rst │ ├── asciimatics.rst │ ├── asciimatics.widgets.rst │ ├── conf.py │ ├── contacts.png │ ├── contributing.rst │ ├── index.rst │ ├── intro.rst │ ├── io.rst │ ├── mac_settings.png │ ├── modules.rst │ ├── rendering.rst │ ├── troubleshooting.rst │ └── widgets.rst ├── mypy.ini ├── pyproject.toml ├── requirements.txt ├── requirements ├── base.txt └── dev.txt ├── samples ├── 256colour.py ├── bars.py ├── basics.py ├── bg_colours.py ├── cogs.py ├── colour_globe.gif ├── contact_list.py ├── credits.py ├── experimental.py ├── fire.py ├── fireworks.py ├── forms.py ├── frame_borders.py ├── fruit.ans ├── globe.gif ├── grumpy_cat.jpg ├── images.py ├── interactive.py ├── julia.py ├── kaleidoscope.py ├── maps.py ├── mapscache │ ├── 0.0.0.jpg │ ├── 0.0.0.json │ ├── 1.0.0.jpg │ └── 1.0.0.json ├── noise.py ├── pacman.png ├── pacman.py ├── particles.py ├── plasma.py ├── player.py ├── python.png ├── quick_model.py ├── ray_casting.py ├── rendering.py ├── simple.py ├── tab_demo.py ├── terminal.py ├── test.rec ├── top.py ├── treeview.py ├── wall.png └── xmas.py ├── setup.py └── tests ├── __init__.py ├── mock_objects.py ├── renderers ├── __init__.py ├── globe.gif ├── test.ans ├── test.rec ├── test2.ans ├── test_bad.rec ├── test_base.py ├── test_charts.py ├── test_images.py ├── test_other.py ├── test_players.py └── test_typewriter.py ├── test_effects.py ├── test_events.py ├── test_exceptions.py ├── test_parsers.py ├── test_particles.py ├── test_paths.py ├── test_scene.py ├── test_screen.py ├── test_sprites.py ├── test_strings.py ├── test_utilities.py └── test_widgets.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | relative_files = True 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior. This could be some simple steps in the provided samples, or a minimum, complete verifiable example of your own. See https://stackoverflow.com/help/mcve for details. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots or cut and paste the erroneous text to help explain your problem. 18 | 19 | **System details (please complete the following information):** 20 | - OS and version: [e.g. Windows 10] 21 | - Python version: [e.g. 3.9.6] 22 | - Python distribution: [e.g. CPython, pypy, anaconda, etc.] 23 | - Asciimatics version [e.g. 1.9.0] 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. For example: "I'm always frustrated when [...]" 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - enhancement 8 | - Documentation 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Backup files 2 | *.~ 3 | *.swp 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | bin/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | venv/ 29 | 30 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | 34 | # Unit test / coverage reports 35 | .tox/ 36 | .coverage 37 | .cache 38 | nosetests.xml 39 | coverage.xml 40 | 41 | # Translations 42 | *.mo 43 | 44 | # Sphinx documentation 45 | docs/_build/ 46 | 47 | # IDE 48 | .idea/ 49 | 50 | # setuptools_scm-generated file 51 | asciimatics/version.py 52 | 53 | # Log files 54 | *.log 55 | 56 | -------------------------------------------------------------------------------- /.landscape.yml: -------------------------------------------------------------------------------- 1 | doc-warnings: yes 2 | test-warnings: no 3 | strictness: high 4 | max-line-length: 110 5 | autodetect: yes 6 | ignore-paths: 7 | # Ignore code that's not in the delivered 8 | - doc 9 | - tests 10 | - samples 11 | # Ignore build files 12 | - coverage_fix.py 13 | - version.py 14 | python-targets: 15 | - 3 16 | pylint: 17 | run: false 18 | mccabe: 19 | run: false 20 | pep257: 21 | disable: 22 | - D102 23 | - D200 24 | - D203 25 | - D205 26 | - D212 27 | - D400 28 | - D401 29 | - D404 30 | - D415 31 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | jobs: 14 | pre_install: 15 | - git update-index --assume-unchanged doc/source/conf.py 16 | pre_build: 17 | - cd doc && bash ./build.sh 18 | 19 | # Build documentation in the docs/ directory with Sphinx 20 | sphinx: 21 | configuration: doc/source/conf.py 22 | 23 | # We recommend specifying your dependencies to enable reproducible builds: 24 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 25 | python: 26 | install: 27 | - requirements: requirements.txt 28 | - path: . 29 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | flake8: 3 | fixer: true 4 | fixers: 5 | enable: true 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | If you'd like to contribute to this project, see 2 | http://asciimatics.readthedocs.org/en/latest/contributing.html for details. 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Thanks for taking the time to contibute to this project. You are a star! 2 | 3 | Checks 4 | ------ 5 | _Before you go any further, please make sure that you have read the [contributing guidelines](https://asciimatics.readthedocs.io/en/latest/contributing.html), run the test suite and that your clone of this repository was taken after 18 October 2018. (I had to fix up the git history and so clones before that date will have all sorts of merge issues)._ 6 | 7 | _Now please delete this pre-amble section and fill in the rest of the template..._ 8 | 9 | Issues fixed by this PR 10 | ----------------------- 11 | _Please list any Issues here_ 12 | 13 | What does this implement/fix? 14 | ----------------------------- 15 | _Please add any further information about the fix if it is not obvious from the related Issue above_ 16 | 17 | Any other comments? 18 | ------------------- 19 | _If there is anything else you feel you need to add, please do so here!_ 20 | -------------------------------------------------------------------------------- /asciimatics/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Asciimatics is a package to help people create full-screen text UIs (from interactive forms to 3 | ASCII animations) on any platform. It is licensed under the Apache Software Foundation License 2.0. 4 | """ 5 | __author__ = 'Peter Brittain' 6 | 7 | try: 8 | from .version import version 9 | except ImportError: 10 | # Someone is running straight from the GIT repo - dummy out the version 11 | version = "0.0.0" 12 | 13 | __version__ = version 14 | -------------------------------------------------------------------------------- /asciimatics/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is just a collection of simple helper functions. 3 | """ 4 | 5 | #: Attribute conversion table for the ${c,a} form of attributes for 6 | #: :py:obj:`~.Screen.paint`. 7 | MAPPING_ATTRIBUTES = { 8 | "1": 1, 9 | "2": 2, 10 | "3": 3, 11 | "4": 4, 12 | } 13 | 14 | #: Regex for asciimatics ${c,a,b} embedded colour attributes. 15 | COLOUR_REGEX = r"^\$\{((\d+),(\d+),(\d+)|(\d+),(\d+)|(\d+))\}(.*)" 16 | 17 | # Text attributes for use when printing to the Screen. 18 | A_BOLD = 1 19 | A_NORMAL = 2 20 | A_REVERSE = 3 21 | A_UNDERLINE = 4 22 | 23 | # Text colours for use when printing to the Screen. 24 | COLOUR_DEFAULT = -1 25 | COLOUR_BLACK = 0 26 | COLOUR_RED = 1 27 | COLOUR_GREEN = 2 28 | COLOUR_YELLOW = 3 29 | COLOUR_BLUE = 4 30 | COLOUR_MAGENTA = 5 31 | COLOUR_CYAN = 6 32 | COLOUR_WHITE = 7 33 | 34 | # Line drawing style constants 35 | ASCII_LINE = 0 36 | SINGLE_LINE = 1 37 | DOUBLE_LINE = 2 38 | -------------------------------------------------------------------------------- /asciimatics/event.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines basic input events. For more details, see 3 | http://asciimatics.readthedocs.io/en/latest/.html 4 | """ 5 | 6 | 7 | class Event(): 8 | """ 9 | A class to hold information about an input event. 10 | 11 | The exact contents varies from event to event. See specific classes for more information. 12 | """ 13 | 14 | 15 | class KeyboardEvent(Event): 16 | """ 17 | An event that represents a key press. 18 | 19 | Its key field is the `key_code`. This is the ordinal representation of the key (taking into 20 | account keyboard state - e.g. caps lock) if possible, or an extended key code (the `KEY_xxx` 21 | constants in the :py:obj:`.Screen` class) where not. 22 | """ 23 | 24 | def __init__(self, key_code: int): 25 | """ 26 | :param key_code: the ordinal value of the key that was pressed. 27 | """ 28 | self.key_code = key_code 29 | 30 | def __repr__(self) -> str: 31 | """ 32 | :returns: a string representation of the keyboard event. 33 | """ 34 | return f"KeyboardEvent: {self.key_code}" 35 | 36 | 37 | class MouseEvent(Event): 38 | """ 39 | An event that represents a mouse move or click. 40 | 41 | Allowed values for the buttons are any bitwise combination of 42 | `LEFT_CLICK`, `RIGHT_CLICK` and `DOUBLE_CLICK`. 43 | """ 44 | 45 | # Mouse button states - bitwise flags 46 | LEFT_CLICK = 1 47 | RIGHT_CLICK = 2 48 | DOUBLE_CLICK = 4 49 | # Note: older versions of curses (e.g. in pypy) do not support up/down 50 | SCROLL_UP = 8 51 | SCROLL_DOWN = 16 52 | 53 | def __init__(self, x: int, y: int, buttons: int): 54 | """ 55 | :param x: The X coordinate of the mouse event. 56 | :param y: The Y coordinate of the mouse event. 57 | :param buttons: A bitwise flag for any mouse buttons that were pressed (if any). 58 | """ 59 | self.x = x 60 | self.y = y 61 | self.buttons = buttons 62 | 63 | def __repr__(self) -> str: 64 | """ 65 | :returns: a string representation of the mouse event. 66 | """ 67 | return f"MouseEvent ({self.x}, {self.y}) {self.buttons}" 68 | -------------------------------------------------------------------------------- /asciimatics/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines the exceptions used by asciimatics. 3 | """ 4 | from __future__ import annotations 5 | from typing import TYPE_CHECKING, List, Optional 6 | if TYPE_CHECKING: 7 | from asciimatics.scene import Scene 8 | 9 | 10 | class ResizeScreenError(Exception): 11 | """ 12 | Asciimatics raises this Exception if the terminal is resized while playing 13 | a Scene (and the Screen has been told not to ignore a resizing event). 14 | """ 15 | 16 | def __init__(self, message: str, scene: Optional[Scene] = None): 17 | """ 18 | :param message: Error message for this exception. 19 | :param scene: Scene that was active at time of resize. 20 | """ 21 | super().__init__() 22 | self._scene = scene 23 | self._message = message 24 | 25 | def __str__(self) -> str: 26 | """ 27 | Printable form of the exception. 28 | """ 29 | return self._message 30 | 31 | @property 32 | def scene(self) -> Optional[Scene]: 33 | """ 34 | The Scene that was running when the Screen resized. 35 | """ 36 | return self._scene 37 | 38 | 39 | class StopApplication(Exception): 40 | """ 41 | Any component can raise this exception to tell Asciimatics to stop running. 42 | If playing a Scene (i.e. inside `Screen.play()`) the Screen will return 43 | to the calling function. When used at any other time, the exception will 44 | need to be caught by the application using Asciimatics. 45 | """ 46 | 47 | def __init__(self, message: str): 48 | """ 49 | :param message: Error message for this exception. 50 | """ 51 | super().__init__() 52 | self._message = message 53 | 54 | def __str__(self) -> str: 55 | """ 56 | Printable form of the exception. 57 | """ 58 | return self._message 59 | 60 | 61 | class NextScene(Exception): 62 | """ 63 | Any component can raise this exception to tell Asciimatics to move to the 64 | next Scene being played. Only effective inside `Screen.play()`. 65 | """ 66 | 67 | def __init__(self, name: Optional[str] = None): 68 | """ 69 | :param name: Next Scene to invoke. Defaults to next in the list. 70 | """ 71 | super().__init__() 72 | self._name = name 73 | 74 | @property 75 | def name(self) -> Optional[str]: 76 | """ 77 | The name of the next Scene to invoke. 78 | """ 79 | return self._name 80 | 81 | 82 | class InvalidFields(Exception): 83 | """ 84 | When saving data from a Frame, you can ask the Frame to validate the data 85 | before saving. This is the exception that gets thrwn if any invalid data 86 | is found. 87 | """ 88 | 89 | def __init__(self, fields: List[Optional[str]]): 90 | """ 91 | :param fields: The list of the fields that are invalid. 92 | """ 93 | super().__init__() 94 | self._fields = fields 95 | 96 | @property 97 | def fields(self) -> List[Optional[str]]: 98 | """ 99 | The list of fields that are invalid. 100 | """ 101 | return self._fields 102 | 103 | 104 | class Highlander(Exception): 105 | """ 106 | There can be only one Layout or Widget with certain options set (designed 107 | to fill the rest of the screen). If you hit this exception you have 108 | a bug in your application. 109 | 110 | If you don't get the name, take a look at `this link 111 | `__. 112 | """ 113 | -------------------------------------------------------------------------------- /asciimatics/renderers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides `Renderers` to create complex animation effects. For more details see 3 | http://asciimatics.readthedocs.io/en/latest/rendering.html 4 | """ 5 | from asciimatics.renderers.base import Renderer, StaticRenderer, DynamicRenderer 6 | from asciimatics.renderers.box import Box 7 | from asciimatics.renderers.charts import BarChart, VBarChart 8 | from asciimatics.renderers.figlettext import FigletText 9 | from asciimatics.renderers.fire import Fire 10 | from asciimatics.renderers.images import ImageFile, ColourImageFile 11 | from asciimatics.renderers.players import AbstractScreenPlayer, AnsiArtPlayer, AsciinemaPlayer 12 | from asciimatics.renderers.kaleidoscope import Kaleidoscope 13 | from asciimatics.renderers.plasma import Plasma 14 | from asciimatics.renderers.rainbow import Rainbow 15 | from asciimatics.renderers.rotatedduplicate import RotatedDuplicate 16 | from asciimatics.renderers.scales import Scale, VScale 17 | from asciimatics.renderers.speechbubble import SpeechBubble 18 | from asciimatics.renderers.typewriter import Typewriter 19 | 20 | __all__ = ["Renderer", "StaticRenderer", "DynamicRenderer", "Box", "BarChart", "VBarChart", 21 | "FigletText", "Fire", "ImageFile", "ColourImageFile", "AbstractScreenPlayer", "AnsiArtPlayer", 22 | "AsciinemaPlayer", "Kaleidoscope", "Plasma", "Rainbow", "RotatedDuplicate", "Scale", 23 | "VScale", "SpeechBubble", "Typewriter"] 24 | -------------------------------------------------------------------------------- /asciimatics/renderers/box.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements an ASCII box renderer. 3 | """ 4 | 5 | from asciimatics.constants import SINGLE_LINE 6 | from asciimatics.renderers.base import StaticRenderer 7 | from asciimatics.utilities import BoxTool 8 | 9 | 10 | class Box(StaticRenderer): 11 | """ 12 | Renders a simple box using ASCII characters. This does not render in 13 | extended box drawing characters as that requires non-ASCII characters in 14 | Windows and direct access to curses in Linux. 15 | """ 16 | 17 | def __init__(self, width: int, height: int, uni: bool = False, style: int = SINGLE_LINE): 18 | """ 19 | :param width: width of box 20 | :param height: height of box 21 | :param uni: True to use UNICODE character set, defaults to False 22 | :param style: desired line style, based on line style definitions in 23 | :mod:`~asciimatics.constants`: `ASCII_LINE`, `SINGLE_LINE`, 24 | `DOUBLE_LINE`. `uni` parameter takes precedence and the style will be 25 | ignored if `uni==False` 26 | """ 27 | super().__init__() 28 | self._images = [BoxTool(uni, style).box(width, height)] 29 | -------------------------------------------------------------------------------- /asciimatics/renderers/figlettext.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements Figlet text renderer. 3 | """ 4 | 5 | from pyfiglet import Figlet, DEFAULT_FONT 6 | 7 | from asciimatics.renderers.base import StaticRenderer 8 | 9 | 10 | class FigletText(StaticRenderer): 11 | """ 12 | This class renders the supplied text using the specified Figlet font. 13 | See http://www.figlet.org/ for details of available fonts. 14 | """ 15 | 16 | def __init__(self, text: str, font: str = DEFAULT_FONT, width: int = 200): 17 | """ 18 | :param text: The text string to convert with Figlet. 19 | :param font: The Figlet font to use (optional). 20 | :param width: The maximum width for this text in characters. 21 | """ 22 | super().__init__() 23 | self._images = [Figlet(font=font, width=width).renderText(text)] 24 | -------------------------------------------------------------------------------- /asciimatics/renderers/fire.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements a fire effect renderer. 3 | """ 4 | 5 | import copy 6 | from random import randint, random 7 | from typing import List, Tuple, Iterable, Optional 8 | from asciimatics.renderers.base import DynamicRenderer 9 | from asciimatics.screen import Screen 10 | 11 | 12 | class Fire(DynamicRenderer): 13 | """ 14 | Renderer to create a fire effect based on a specified `emitter` that 15 | defines the heat source. 16 | 17 | The implementation here uses the same techniques described in 18 | http://freespace.virgin.net/hugo.elias/models/m_fire.htm, although a 19 | slightly different implementation. 20 | """ 21 | 22 | _COLOURS_16 = [ 23 | (Screen.COLOUR_RED, 0), 24 | (Screen.COLOUR_RED, 0), 25 | (Screen.COLOUR_RED, 0), 26 | (Screen.COLOUR_RED, 0), 27 | (Screen.COLOUR_RED, 0), 28 | (Screen.COLOUR_RED, 0), 29 | (Screen.COLOUR_RED, 0), 30 | (Screen.COLOUR_RED, Screen.A_BOLD), 31 | (Screen.COLOUR_RED, Screen.A_BOLD), 32 | (Screen.COLOUR_RED, Screen.A_BOLD), 33 | (Screen.COLOUR_RED, Screen.A_BOLD), 34 | (Screen.COLOUR_YELLOW, Screen.A_BOLD), 35 | (Screen.COLOUR_YELLOW, Screen.A_BOLD), 36 | (Screen.COLOUR_YELLOW, Screen.A_BOLD), 37 | (Screen.COLOUR_YELLOW, Screen.A_BOLD), 38 | (Screen.COLOUR_WHITE, Screen.A_BOLD), 39 | ] 40 | 41 | _COLOURS_256 = [ 42 | (0, 0), 43 | (52, 0), 44 | (88, 0), 45 | (124, 0), 46 | (160, 0), 47 | (196, 0), 48 | (202, 0), 49 | (208, 0), 50 | (214, 0), 51 | (220, 0), 52 | (226, 0), 53 | (227, 0), 54 | (228, 0), 55 | (229, 0), 56 | (230, 0), 57 | (231, 0), 58 | ] 59 | 60 | _CHARS = " ...::$$$&&&@@" 61 | 62 | def __init__(self, 63 | height: int, 64 | width: int, 65 | emitter: str, 66 | intensity: float, 67 | spot: int, 68 | colours: int, 69 | bg: bool = False): 70 | """ 71 | :param height: Height of the box to contain the flames. 72 | :param width: Width of the box to contain the flames. 73 | :param emitter: Heat source for the flames. Any non-whitespace 74 | character is treated as part of the heat source. 75 | :param intensity: The strength of the flames. The bigger the number, 76 | the hotter the fire. 0 <= intensity <= 1.0. 77 | :param spot: Heat of each spot source. Must be an integer > 0. 78 | :param colours: Number of colours the screen supports. 79 | :param bg: (Optional) Whether to render background colours only. 80 | """ 81 | super().__init__(height, width) 82 | self._emitter = emitter 83 | self._intensity = intensity 84 | self._spot_heat = spot 85 | self._colours = self._COLOURS_256 if colours >= 256 else \ 86 | self._COLOURS_16 87 | self._bg_too = bg 88 | 89 | # Figure out offset of emitter to centre at the bottom of the buffer 90 | e_width = 0 91 | e_height = 0 92 | for line in self._emitter.split("\n"): 93 | e_width = max(e_width, len(line)) 94 | e_height += 1 95 | self._x = (width - e_width) // 2 96 | self._y = height - e_height 97 | 98 | self.reset() 99 | 100 | def reset(self): 101 | line = [0 for _ in range(self._canvas.width)] 102 | self._buffer = [copy.deepcopy(line) for _ in range(self._canvas.width * 2)] 103 | 104 | def _render_all( 105 | self 106 | ) -> Iterable[Tuple[List[str], List[List[Tuple[Optional[int], Optional[int], Optional[int]]]]]]: 107 | return [self._render_now()] 108 | 109 | def _render_now(self) -> Tuple[List[str], List[List[Tuple[Optional[int], Optional[int], Optional[int]]]]]: 110 | # First make the fire rise with convection 111 | for y in range(len(self._buffer) - 1): 112 | self._buffer[y] = self._buffer[y + 1] 113 | self._buffer[len(self._buffer) - 1] = [0 for _ in range(self._canvas.width)] 114 | 115 | # Seed new hot spots 116 | x = self._x 117 | y = self._y 118 | for c in self._emitter: 119 | if c not in " \n" and random() < self._intensity: 120 | self._buffer[y][x] += randint(1, self._spot_heat) 121 | if c == "\n": 122 | x = self._x 123 | y += 1 124 | else: 125 | x += 1 126 | 127 | # Seed a few cooler spots 128 | for _ in range(self._canvas.width // 2): 129 | self._buffer[randint(0, self._canvas.height - 1)][randint(0, self._canvas.width - 1)] -= 10 130 | 131 | # Simulate cooling effect of the resulting environment. 132 | for y, row in enumerate(self._buffer): 133 | for x in range(self._canvas.width): 134 | new_val = row[x] 135 | if y < len(self._buffer) - 1: 136 | new_val += self._buffer[y + 1][x] 137 | if x > 0: 138 | new_val += self._buffer[y][x - 1] 139 | if x < self._canvas.width - 1: 140 | new_val += self._buffer[y][x + 1] 141 | self._buffer[y][x] = new_val // 4 142 | 143 | # Now build the rendered text from the simulated flames. 144 | self._clear() 145 | for x in range(self._canvas.width): 146 | for y, row in enumerate(self._buffer): 147 | if row[x] > 0: 148 | colour = self._colours[min(len(self._colours) - 1, row[x])] 149 | if self._bg_too: 150 | char = " " 151 | bg = colour[0] 152 | else: 153 | char = self._CHARS[min(len(self._CHARS) - 1, row[x])] 154 | bg = 0 155 | self._write(char, x, y, colour[0], colour[1], bg) 156 | 157 | return self._plain_image, self._colour_map 158 | -------------------------------------------------------------------------------- /asciimatics/renderers/kaleidoscope.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements a kaeldioscope effect renderer. 3 | """ 4 | 5 | from math import sin, cos, pi, atan2 6 | from typing import List, Tuple, Iterable, Optional 7 | from asciimatics.renderers.base import DynamicRenderer, Renderer 8 | 9 | 10 | class Kaleidoscope(DynamicRenderer): 11 | """ 12 | Renderer to create a 2-mirror kaleidoscope effect. 13 | 14 | This is a chained renderer (i.e. it acts upon the output of another Renderer which is 15 | passed to it on construction). The other Renderer is used as the cell that is rotated over 16 | time to create the animation. 17 | 18 | You can specify the desired rotational symmetry of the kaleidoscope (which determines the 19 | angle between the mirrors). If you chose values of less than 2, you are effectively removing 20 | one or both mirrors, thus either getting the original cell or a simple mirrored image of the 21 | cell. 22 | 23 | Since this renderer rotates the background cell, it needs operate on square pixels, which 24 | means each character in the cell is drawn as 2 next to each other on the screen. In other 25 | words the cell needs to be half the width of the desired output (when measured in text 26 | characters). 27 | """ 28 | 29 | def __init__(self, height: int, width: int, cell: Renderer, symmetry: int): 30 | """ 31 | :param height: Height of the box to contain the kaleidoscope. 32 | :param width: Width of the box to contain the kaleidoscope. 33 | :param cell: A Renderer to use as the backing cell for the kaleidoscope. 34 | :param symmetry: The desired rotational symmetry. Must be a non-negative integer. 35 | """ 36 | super().__init__(height, width) 37 | self._symmetry = symmetry 38 | self._rotation = 0.0 39 | self._cell = cell 40 | 41 | def reset(self): 42 | self._rotation = 0 43 | 44 | def _render_all( 45 | self 46 | ) -> Iterable[Tuple[List[str], List[List[Tuple[Optional[int], Optional[int], Optional[int]]]]]]: 47 | return [self._render_now()] 48 | 49 | def _render_now(self) -> Tuple[List[str], List[List[Tuple[Optional[int], Optional[int], Optional[int]]]]]: 50 | # Rotate a point (x, y) through an angle theta. 51 | def _rotate(x, y, theta): 52 | return x * cos(theta) - y * sin(theta), x * sin(theta) + y * cos(theta) 53 | 54 | # Reflect a point (x, y) in a line at angle theta 55 | def _reflect(x, y, theta): 56 | return x * cos(2 * theta) + y * sin(2 * theta), x * sin(2 * theta) - y * cos(2 * theta) 57 | 58 | # Get the base cell now - so we can pick out characters as needed. 59 | text, colour_map = self._cell.rendered_text 60 | 61 | # Integer maths will result in gaps between characters if you rotate from the starting 62 | # point to desired end-point. We therefore look for the reverse mapping from the final 63 | # character and trace-back instead. 64 | for dx in range(self._canvas.width // 2): 65 | for dy in range(self._canvas.height): 66 | # Figure out which segment of the circle we're in, so we know what affine 67 | # transformations to apply. 68 | ox = dx - self._canvas.width / 4 69 | oy = dy - self._canvas.height / 2 70 | segment = round(atan2(oy, ox) * self._symmetry / pi) 71 | if segment % 2 == 0: 72 | # Just a rotation required for even segments. 73 | x1, y1 = _rotate(ox, oy, 0 if self._symmetry == 0 else -segment * pi / self._symmetry) 74 | else: 75 | # Odd segments require a rotation and then a reflection. 76 | x1, y1 = _rotate(ox, oy, (1 - segment) * pi / self._symmetry) 77 | x1, y1 = _reflect(x1, y1, pi / self._symmetry / 2) 78 | 79 | # Now rotate once more to simulate the rotation of the background cell too. 80 | x1, y1 = _rotate(x1, y1, self._rotation) 81 | 82 | # Re-normalize back to the box coordinates and draw the character that we found 83 | # from the reverse mapping. 84 | x2 = int(x1 + self._cell.max_width / 2) 85 | y2 = int(y1 + self._cell.max_height / 2) 86 | if (0 <= y2 < len(text)) and (0 <= x2 < len(text[y2])): 87 | self._write(text[y2][x2] + text[y2][x2], 88 | dx * 2, 89 | dy, 90 | colour_map[y2][x2][0], 91 | colour_map[y2][x2][1], 92 | colour_map[y2][x2][2]) 93 | 94 | # Now rotate the background cell for the next frame. 95 | self._rotation += pi / 180 96 | 97 | return self._plain_image, self._colour_map 98 | -------------------------------------------------------------------------------- /asciimatics/renderers/plasma.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements a plasma effect renderer. 3 | """ 4 | 5 | from math import sin, pi, sqrt 6 | from typing import List, Tuple, Optional, Iterable 7 | from asciimatics.renderers.base import DynamicRenderer 8 | from asciimatics.screen import Screen 9 | 10 | 11 | class Plasma(DynamicRenderer): 12 | """ 13 | Renderer to create a "plasma" effect using sinusoidal functions. 14 | 15 | The implementation here uses the same techniques described in 16 | http://lodev.org/cgtutor/plasma.html 17 | """ 18 | 19 | # The ASCII grey scale from darkest to lightest. 20 | _greyscale = ' .:;rsA23hHG#9&@' 21 | 22 | # Colours for different environments 23 | _palette_8 = [ 24 | (Screen.COLOUR_BLUE, Screen.A_NORMAL), 25 | (Screen.COLOUR_BLUE, Screen.A_NORMAL), 26 | (Screen.COLOUR_MAGENTA, Screen.A_NORMAL), 27 | (Screen.COLOUR_MAGENTA, Screen.A_NORMAL), 28 | (Screen.COLOUR_RED, Screen.A_NORMAL), 29 | (Screen.COLOUR_RED, Screen.A_BOLD), 30 | ] 31 | _palette_256 = [ 32 | (18, 0), 33 | (19, 0), 34 | (20, 0), 35 | (21, 0), 36 | (57, 0), 37 | (93, 0), 38 | (129, 0), 39 | (201, 0), 40 | (200, 0), 41 | (199, 0), 42 | (198, 0), 43 | (197, 0), 44 | (196, 0), 45 | (196, 0), 46 | (196, 0), 47 | ] 48 | 49 | def __init__(self, height: int, width: int, colours: int): 50 | """ 51 | :param height: Height of the box to contain the plasma. 52 | :param width: Width of the box to contain the plasma. 53 | :param colours: Number of colours the screen supports. 54 | """ 55 | super().__init__(height, width) 56 | self._palette = self._palette_256 if colours >= 256 else self._palette_8 57 | self._t = 0 58 | 59 | def reset(self): 60 | self._t = 0 61 | 62 | def _render_all( 63 | self 64 | ) -> Iterable[Tuple[List[str], List[List[Tuple[Optional[int], Optional[int], Optional[int]]]]]]: 65 | return [self._render_now()] 66 | 67 | def _render_now(self) -> Tuple[List[str], List[List[Tuple[Optional[int], Optional[int], Optional[int]]]]]: 68 | # Internal function for creating a sine wave radiating out from a point 69 | def f(x1, y1, xp, yp, n): 70 | return sin( 71 | sqrt((x1 - self._canvas.width * xp)**2 + 4 * ((y1 - self._canvas.height * yp)**2)) * pi / n) 72 | 73 | self._t += 1 74 | for y in range(self._canvas.height - 1): 75 | for x in range(self._canvas.width - 1): 76 | value = abs( 77 | f(x + self._t / 3, y, 1 / 4, 1 / 3, 15) + f(x, y, 1 / 8, 1 / 5, 11) + 78 | f(x, y + self._t / 3, 1 / 2, 1 / 5, 13) + f(x, y, 3 / 4, 4 / 5, 13)) / 4.0 79 | fg, attr = self._palette[int(round(value * (len(self._palette) - 1)))] 80 | char = self._greyscale[int((len(self._greyscale) - 1) * value)] 81 | self._write(char, x, y, fg, attr, 0) 82 | 83 | return self._plain_image, self._colour_map 84 | -------------------------------------------------------------------------------- /asciimatics/renderers/rainbow.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements a rainbow effect renderer. 3 | """ 4 | from asciimatics.renderers.base import StaticRenderer, Renderer 5 | from asciimatics.screen import Screen 6 | 7 | 8 | class Rainbow(StaticRenderer): 9 | """ 10 | Chained renderer to add rainbow colours to output of another renderer. 11 | The embedded rendered must not use multi-colour mode (i.e. ${c,a} 12 | mark-ups) as these will be converted to explicit text by this renderer. 13 | """ 14 | 15 | # Colour palette when limited to 16 colours (8 dim and 8 bright). 16 | _16_palette = [1, 1, 3, 3, 2, 2, 6, 6, 4, 4, 5, 5] 17 | 18 | # Colour palette for 256 colour xterm mode. 19 | _256_palette = [196, 202, 208, 214, 220, 226, 154, 118, 82, 46, 47, 48, 49, 50, 51, 20 | 45, 39, 33, 27, 21, 57, 93, 129, 201, 200, 199, 198, 197] 21 | 22 | def __init__(self, screen: Screen, renderer: Renderer): 23 | """ 24 | :param screen: The screen object for this renderer. 25 | :param renderer: The renderer to wrap. 26 | """ 27 | super().__init__() 28 | palette = self._256_palette if screen.colours > 16 else self._16_palette 29 | for image in renderer.images: 30 | new_image = "" 31 | for y, line in enumerate(image): 32 | for x, c in enumerate(line): 33 | colour = (x + y) % len(palette) 34 | new_image += '${%d,1}%s' % (palette[colour], c) 35 | if y < len(image) - 1: 36 | new_image += "\n" 37 | self._images.append(new_image) 38 | -------------------------------------------------------------------------------- /asciimatics/renderers/rotatedduplicate.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements a renderer that renders another renderer but rotated. 3 | """ 4 | 5 | from asciimatics.renderers.base import StaticRenderer, Renderer 6 | 7 | 8 | class RotatedDuplicate(StaticRenderer): 9 | """ 10 | Chained renderer to add a rotated version of the original renderer underneath and centre the 11 | whole thing within within the specified dimensions. 12 | """ 13 | 14 | def __init__(self, width: int, height: int, renderer: Renderer): 15 | """ 16 | :param width: The maximum width of the rendered text. 17 | :param height: The maximum height of the rendered text. 18 | :param renderer: The renderer to wrap. 19 | """ 20 | super().__init__() 21 | for image in renderer.images: 22 | mx = (width - max(len(x) for x in image)) // 2 23 | my = height // 2 - len(image) 24 | tab = (" " * mx if mx > 0 else "") + "\n" + (" " * mx if mx > 0 else "") 25 | new_image = [] 26 | new_image.extend(["" for _ in range(max(0, my))]) 27 | new_image.extend(image) 28 | new_image.extend([x[::-1] for x in reversed(image)]) 29 | new_image.extend(["" for _ in range(max(0, my))]) 30 | if mx < 0: 31 | new_image = [x[-mx:mx] for x in new_image] 32 | if my < 0: 33 | new_image = new_image[-my:my] 34 | self._images.append(tab.join(new_image)) 35 | -------------------------------------------------------------------------------- /asciimatics/renderers/scales.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements renderers that show measuring scales to the screen. 3 | """ 4 | from asciimatics.renderers.base import StaticRenderer 5 | 6 | 7 | class Scale(StaticRenderer): 8 | """ 9 | This renders a linear scale, useful for debugging positions of your 10 | creations. Every 5 spaces gets a tick mark, every 10 a number. 11 | """ 12 | 13 | def __init__(self, width: int): 14 | """ 15 | :param width: The width of the scale 16 | """ 17 | super().__init__() 18 | 19 | contents = [] 20 | for x in range(1, width + 1): 21 | if x % 10 == 0 and x > 0: 22 | contents.append(str(x)[-2]) 23 | elif x % 5 == 0: 24 | contents.append('+') 25 | else: 26 | contents.append('-') 27 | 28 | text = ''.join(contents) 29 | 30 | self._images = [text] 31 | 32 | 33 | class VScale(StaticRenderer): 34 | """ 35 | This renders a vertical linear scale, useful for debugging positions of your 36 | creations. Writes lowest significant digit of a count running vertically. 37 | """ 38 | 39 | def __init__(self, height: int): 40 | """ 41 | :param width: The width of the scale 42 | """ 43 | super().__init__() 44 | 45 | contents = [str(i)[-1] for i in range(1, height + 1)] 46 | text = '\n'.join(contents) 47 | 48 | self._images = [text] 49 | -------------------------------------------------------------------------------- /asciimatics/renderers/speechbubble.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements a speech-bubble effect renderer. 3 | """ 4 | from typing import Optional, Union 5 | from wcwidth.wcwidth import wcswidth 6 | from asciimatics.renderers.base import StaticRenderer, Renderer 7 | 8 | 9 | class SpeechBubble(StaticRenderer): 10 | """ 11 | Renders supplied text into a speech bubble. 12 | """ 13 | 14 | def __init__(self, text: Union[str, Renderer], tail: Optional[str] = None, uni: bool = False): 15 | """ 16 | :param text: The text to be put into a speech bubble. 17 | :param tail: Where to put the bubble callout tail, specifying "L" or 18 | "R" for left or right tails. Can be None for no tail. 19 | :param uni: Whether to use unicode characters or not. 20 | """ 21 | super().__init__() 22 | source = text.images if isinstance(text, Renderer) else [text.split("\n")] 23 | for text_list in source: 24 | max_len = max(wcswidth(x) for x in text_list) 25 | if uni: 26 | bubble = "╭─" + "─" * max_len + "─╮\n" 27 | for line in text_list: 28 | filler = " " * (max_len - len(line)) 29 | bubble += "│ " + line + filler + " │\n" 30 | bubble += "╰─" + "─" * max_len + "─╯" 31 | else: 32 | bubble = ".-" + "-" * max_len + "-.\n" 33 | for line in text_list: 34 | filler = " " * (max_len - len(line)) 35 | bubble += "| " + line + filler + " |\n" 36 | bubble += "`-" + "-" * max_len + "-`" 37 | if tail == "L": 38 | bubble += "\n" 39 | bubble += " )/ \n" 40 | bubble += "-\"`\n" 41 | elif tail == "R": 42 | bubble += "\n" 43 | bubble += (" " * max_len) + "\\( \n" 44 | bubble += (" " * max_len) + " `\"-\n" 45 | self._images.append(bubble) 46 | -------------------------------------------------------------------------------- /asciimatics/renderers/typewriter.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements a typewriter renderer. 3 | """ 4 | from typing import Iterator, List, Tuple, Optional 5 | from asciimatics.renderers.base import Renderer, DynamicRenderer 6 | 7 | 8 | class Typewriter(DynamicRenderer): 9 | """ 10 | Renderer to create a typewriter effect based on a specified source. 11 | """ 12 | 13 | def __init__(self, source: Renderer): 14 | """ 15 | :param source: Source renderer to by typed. 16 | """ 17 | super().__init__(source.max_height, source.max_width) 18 | self._source = source 19 | self._count = 0 20 | 21 | def reset(self): 22 | self._count = 0 23 | 24 | def _render_all( 25 | self 26 | ) -> Iterator[Tuple[List[str], List[List[Tuple[Optional[int], Optional[int], Optional[int]]]]]]: 27 | self._clear() 28 | while self._count < sum(len(x) for x in self._source.rendered_text[0]): 29 | yield self._render_now() 30 | 31 | def _render_now(self) -> Tuple[List[str], List[List[Tuple[Optional[int], Optional[int], Optional[int]]]]]: 32 | # Now build the rendered text from the source and current limit. 33 | self._count += 1 34 | count = 0 35 | text, colour_map = self._source.rendered_text 36 | for y, row in enumerate(text): 37 | for x, char in enumerate(row): 38 | count += 1 39 | if count > self._count: 40 | break 41 | self._write(char, x, y, colour_map[y][x][0], colour_map[y][x][1], colour_map[y][x][2]) 42 | 43 | return self._plain_image, self._colour_map 44 | -------------------------------------------------------------------------------- /asciimatics/scene.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines Scene objects for animation purposes. For more details, see 3 | http://asciimatics.readthedocs.io/en/latest/animation.html 4 | """ 5 | from __future__ import annotations 6 | from typing import TYPE_CHECKING, Any, List, Optional 7 | if TYPE_CHECKING: 8 | from asciimatics.effects import Effect 9 | from asciimatics.event import Event 10 | from asciimatics.screen import Screen 11 | 12 | 13 | class Scene(): 14 | """ 15 | Class to store the details of a single scene to be displayed. This is 16 | made up of a set of :py:obj:`.Effect` objects. See the documentation for 17 | Effect to understand the interaction between the two classes and 18 | http://asciimatics.readthedocs.io/en/latest/animation.html for how to use them together. 19 | """ 20 | 21 | def __init__(self, 22 | effects: List[Effect], 23 | duration: int = 0, 24 | clear: bool = True, 25 | name: Optional[str] = None): 26 | """ 27 | :param effects: The list of effects to apply to this scene. 28 | :param duration: The number of frames in this Scene. A value of 0 means that the Scene 29 | should query the Effects to find the duration. A value of -1 means don't stop. 30 | :param clear: Whether to clear the Screen at the start of the Scene. 31 | :param name: Optional name to identify the scene. 32 | """ 33 | self._effects: list[Effect] = [] 34 | for effect in effects: 35 | self.add_effect(effect, reset=False) 36 | self._duration = duration 37 | if duration == 0: 38 | self._duration = max(x.stop_frame for x in effects) 39 | self._clear = clear 40 | self._name = name 41 | 42 | def reset(self, old_scene: Optional["Scene"] = None, screen: Optional[Screen] = None): 43 | """ 44 | Reset the scene ready for playing. 45 | 46 | :param old_scene: The previous version of this Scene that was running before the 47 | application reset - e.g. due to a screen resize. 48 | :param screen: New screen to use if old_scene is not None. 49 | """ 50 | # Always reset all the effects. 51 | for effect in self._effects: 52 | effect.reset() 53 | 54 | # If we have an old Scene to recreate, get the data out of that and 55 | # apply it where possible by cloning objects where appropriate. 56 | if old_scene: 57 | for old_effect in old_scene.effects: 58 | # catching AttributeErrors here has hidden bugs, so explicitly 59 | # check for the cloning interface before calling it. 60 | if hasattr(old_effect, "clone"): 61 | old_effect.clone(screen, self) 62 | 63 | def exit(self): 64 | """ 65 | Handle any tidy up required on the exit of the Scene. 66 | """ 67 | # Save off any persistent state for each effect. 68 | for effect in self._effects: 69 | if hasattr(effect, "save"): 70 | effect.save() 71 | 72 | def add_effect(self, effect: Effect, reset: bool = True): 73 | """ 74 | Add an effect to the Scene. 75 | 76 | This method can be called at any time - even when playing the Scene. The default logic 77 | assumes that the Effect needs to be reset before being displayed. This can be overridden 78 | using the `reset` parameter. 79 | 80 | :param effect: The Effect to be added. 81 | :param reset: Whether to reset the Effect that has just been added. 82 | """ 83 | # Reset the effect in case this is in the middle of a Scene. 84 | if reset: 85 | effect.reset() 86 | effect.register_scene(self) 87 | self._effects.append(effect) 88 | 89 | def remove_effect(self, effect: Effect): 90 | """ 91 | Remove an effect from the scene. 92 | 93 | :param effect: The effect to remove. 94 | """ 95 | self._effects.remove(effect) 96 | 97 | def process_event(self, event: Event) -> Optional[Event]: 98 | """ 99 | Process a new input event. 100 | 101 | This method will pass the event on to any Effects in reverse Z order so that the 102 | top-most Effect has priority. 103 | 104 | :param event: The Event that has been triggered. 105 | :returns: None if the Scene processed the event, else the original event. 106 | """ 107 | for effect in reversed(self._effects): 108 | new_event = effect.process_event(event) 109 | if new_event is None: 110 | break 111 | event = new_event 112 | return event 113 | 114 | @property 115 | def name(self) -> Optional[str]: 116 | """ 117 | :return: The name of this Scene. May be None. 118 | """ 119 | return self._name 120 | 121 | @property 122 | def effects(self) -> List[Any]: 123 | """ 124 | :return: The list of Effects in this Scene. 125 | """ 126 | return self._effects 127 | 128 | @property 129 | def duration(self) -> int: 130 | """ 131 | :return: The length of the scene in frames. 132 | """ 133 | return self._duration 134 | 135 | @property 136 | def clear(self) -> bool: 137 | """ 138 | :return: Whether the Scene should clear at the start. 139 | """ 140 | return self._clear 141 | -------------------------------------------------------------------------------- /asciimatics/sprites.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides `Sprites` to create animation effects with Paths. For more details see 3 | http://asciimatics.readthedocs.io/en/latest/animation.html 4 | """ 5 | import random 6 | from asciimatics.effects import Sprite 7 | from asciimatics.renderers import StaticRenderer 8 | from asciimatics.screen import Screen 9 | from asciimatics.paths import Path 10 | 11 | # Images for Sam-ple sprite. 12 | sam_default = [ 13 | """ 14 | ______ 15 | .` `. 16 | / - - \\ 17 | | __ | 18 | | | 19 | \\ / 20 | '.______.' 21 | """, 22 | """ 23 | ______ 24 | .` `. 25 | / o o \\ 26 | | __ | 27 | | | 28 | \\ / 29 | '.______.' 30 | """ 31 | ] 32 | sam_left = """ 33 | ______ 34 | .` `. 35 | / o \\ 36 | | | 37 | |-- | 38 | \\ / 39 | '.______.' 40 | """ 41 | sam_right = """ 42 | ______ 43 | .` `. 44 | / o \\ 45 | | | 46 | | --| 47 | \\ / 48 | '.______.' 49 | """ 50 | sam_down = """ 51 | ______ 52 | .` `. 53 | / \\ 54 | | | 55 | | ^ ^ | 56 | \\ __ / 57 | '.______.' 58 | """ 59 | sam_up = """ 60 | ______ 61 | .` __ `. 62 | / v v \\ 63 | | | 64 | | | 65 | \\ / 66 | '.______.' 67 | """ 68 | 69 | # Images for an arrow Sprite. 70 | left_arrow = """ 71 | /____ 72 | / 73 | \\ ____ 74 | \\ 75 | """ 76 | up_arrow = """ 77 | /\\ 78 | / \\ 79 | /| |\\ 80 | | | 81 | """ 82 | right_arrow = """ 83 | ____\\ 84 | \\ 85 | ____ / 86 | / 87 | """ 88 | down_arrow = """ 89 | | | 90 | \\| |/ 91 | \\ / 92 | \\/ 93 | """ 94 | default_arrow = [ 95 | """ 96 | /\\ 97 | / \\ 98 | /|><|\\ 99 | | | 100 | """, 101 | """ 102 | /\\ 103 | / \\ 104 | /|oo|\\ 105 | | | 106 | """, 107 | ] 108 | 109 | 110 | # Simple static function to swap between 2 images to make a sprite blink. 111 | def _blink(): 112 | if random.random() > 0.9: 113 | return 0 114 | else: 115 | return 1 116 | 117 | 118 | class Sam(Sprite): 119 | """ 120 | Sam Paul sprite - an simple sample animated character. 121 | """ 122 | 123 | def __init__(self, screen: Screen, path: Path, start_frame: int = 0, stop_frame: int = 0): 124 | """ 125 | See :py:obj:`.Sprite` for details. 126 | """ 127 | super().__init__(screen, 128 | renderer_dict={ 129 | "default": StaticRenderer(images=sam_default, animation=_blink), 130 | "left": StaticRenderer(images=[sam_left]), 131 | "right": StaticRenderer(images=[sam_right]), 132 | "down": StaticRenderer(images=[sam_down]), 133 | "up": StaticRenderer(images=[sam_up]), 134 | }, 135 | path=path, 136 | start_frame=start_frame, 137 | stop_frame=stop_frame) 138 | 139 | 140 | class Arrow(Sprite): 141 | """ 142 | Sample arrow sprite - points where it is going. 143 | """ 144 | 145 | def __init__(self, 146 | screen: Screen, 147 | path: Path, 148 | colour: int = Screen.COLOUR_WHITE, 149 | start_frame: int = 0, 150 | stop_frame: int = 0): 151 | """ 152 | See :py:obj:`.Sprite` for details. 153 | """ 154 | super().__init__(screen, 155 | renderer_dict={ 156 | "default": StaticRenderer(images=default_arrow, animation=_blink), 157 | "left": StaticRenderer(images=[left_arrow]), 158 | "right": StaticRenderer(images=[right_arrow]), 159 | "down": StaticRenderer(images=[down_arrow]), 160 | "up": StaticRenderer(images=[up_arrow]), 161 | }, 162 | path=path, 163 | colour=colour, 164 | start_frame=start_frame, 165 | stop_frame=stop_frame) 166 | 167 | 168 | class Plot(Sprite): 169 | """ 170 | Sample Sprite that simply plots an "X" for each step in the path. Useful 171 | for plotting a path to the screen. 172 | """ 173 | 174 | def __init__(self, 175 | screen: Screen, 176 | path: Path, 177 | colour: int = Screen.COLOUR_WHITE, 178 | start_frame: int = 0, 179 | stop_frame: int = 0): 180 | """ 181 | See :py:obj:`.Sprite` for details. 182 | """ 183 | super().__init__(screen, 184 | renderer_dict={"default": StaticRenderer(images=["X"])}, 185 | path=path, 186 | colour=colour, 187 | clear=False, 188 | start_frame=start_frame, 189 | stop_frame=stop_frame) 190 | -------------------------------------------------------------------------------- /asciimatics/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | """This is the module initialization for widgets""" 2 | # flake8: noqa 3 | from asciimatics.effects import Background 4 | from asciimatics.widgets.button import Button 5 | from asciimatics.widgets.checkbox import CheckBox 6 | from asciimatics.widgets.datepicker import DatePicker 7 | from asciimatics.widgets.divider import Divider 8 | from asciimatics.widgets.dropdownlist import DropdownList 9 | from asciimatics.widgets.filebrowser import FileBrowser 10 | from asciimatics.widgets.frame import Frame 11 | from asciimatics.widgets.label import Label 12 | from asciimatics.widgets.layout import Layout 13 | from asciimatics.widgets.listbox import ListBox 14 | from asciimatics.widgets.multicolumnlistbox import MultiColumnListBox 15 | from asciimatics.widgets.popupdialog import PopUpDialog 16 | from asciimatics.widgets.popupmenu import PopupMenu 17 | from asciimatics.widgets.radiobuttons import RadioButtons 18 | from asciimatics.widgets.textbox import TextBox 19 | from asciimatics.widgets.text import Text 20 | from asciimatics.widgets.timepicker import TimePicker 21 | from asciimatics.widgets.verticaldivider import VerticalDivider 22 | from asciimatics.widgets.widget import Widget 23 | from asciimatics.widgets.utilities import _enforce_width, _find_min_start,\ 24 | _get_offset, _split_text, _euclidian_distance 25 | -------------------------------------------------------------------------------- /asciimatics/widgets/basepicker.py: -------------------------------------------------------------------------------- 1 | """This module implements common func5ion for picker widgets""" 2 | from __future__ import annotations 3 | from abc import ABCMeta 4 | from typing import TYPE_CHECKING, Callable, Optional, Type, Any 5 | from asciimatics.event import KeyboardEvent, MouseEvent, Event 6 | from asciimatics.screen import Screen 7 | from asciimatics.widgets.widget import Widget 8 | if TYPE_CHECKING: 9 | from asciimatics.effects import Effect 10 | 11 | 12 | class _BasePicker(Widget, metaclass=ABCMeta): 13 | """ 14 | Common class for picker widgets. 15 | """ 16 | 17 | __slots__ = ["_on_change", "_child", "_popup"] 18 | 19 | def __init__(self, 20 | popup: Type[Any], 21 | label: Optional[str] = None, 22 | name: Optional[str] = None, 23 | on_change: Optional[Callable] = None, 24 | **kwargs): 25 | """ 26 | :param popup: Class to use to handle popup widget. 27 | :param label: An optional label for the widget. 28 | :param name: The name for the widget. 29 | :param on_change: Optional function to call when the selected time changes. 30 | 31 | Also see the common keyword arguments in :py:obj:`.Widget`. 32 | """ 33 | super().__init__(name, **kwargs) 34 | self._popup = popup 35 | self._label = label 36 | self._on_change = on_change 37 | self._child: Optional[Effect] = None 38 | 39 | def reset(self): 40 | pass 41 | 42 | def process_event(self, event: Optional[Event]) -> Optional[Event]: 43 | if event is not None: 44 | # Handle key or mouse selection events - e.g. click on widget or Enter. 45 | if isinstance(event, KeyboardEvent): 46 | if event.key_code in [Screen.ctrl("M"), Screen.ctrl("J"), ord(" ")]: 47 | event = None 48 | elif isinstance(event, MouseEvent): 49 | if event.buttons != 0: 50 | if self.is_mouse_over(event, include_label=False): 51 | event = None 52 | 53 | # Create the pop-up if needed 54 | if event is None: 55 | assert self.frame and self.frame.scene 56 | self._child = self._popup(self) 57 | assert self._child 58 | self.frame.scene.add_effect(self._child) 59 | 60 | return event 61 | 62 | def required_height(self, offset: int, width: int): 63 | return 1 64 | 65 | @property 66 | def value(self): 67 | """ 68 | The current selected time. 69 | """ 70 | return self._value 71 | 72 | @value.setter 73 | def value(self, new_value): 74 | # Only trigger the notification after we've changed the value. 75 | old_value = self._value 76 | self._value = new_value 77 | if old_value != self._value and self._on_change: 78 | self._on_change() 79 | -------------------------------------------------------------------------------- /asciimatics/widgets/button.py: -------------------------------------------------------------------------------- 1 | """This module defines a button widget""" 2 | from __future__ import annotations 3 | from typing import Callable, Optional 4 | from asciimatics.event import KeyboardEvent, MouseEvent, Event 5 | from asciimatics.widgets.widget import Widget 6 | 7 | 8 | class Button(Widget): 9 | """ 10 | A Button widget to be displayed in a Frame. 11 | 12 | It is typically used to represent a desired action for te user to invoke (e.g. a submit button 13 | on a form). 14 | """ 15 | 16 | __slots__ = ["_text", "_text_raw", "_add_box", "_on_click"] 17 | 18 | def __init__(self, 19 | text: str, 20 | on_click: Callable, 21 | label: Optional[str] = None, 22 | add_box: bool = True, 23 | name: Optional[str] = None, 24 | **kwargs): 25 | """ 26 | :param text: The text for the button. 27 | :param on_click: The function to invoke when the button is clicked. 28 | :param label: An optional label for the widget. 29 | :param add_box: Whether to wrap the text with chevrons. 30 | :param name: The name of this widget. 31 | 32 | Also see the common keyword arguments in :py:obj:`.Widget`. 33 | """ 34 | super().__init__(name, **kwargs) 35 | self._add_box = add_box 36 | self.text = text 37 | self._on_click = on_click 38 | self._label = label 39 | 40 | def set_layout(self, x: int, y: int, offset: int, w: int, h: int): 41 | # Do the usual layout work. then recalculate exact x/w values for the 42 | # rendered button. 43 | super().set_layout(x, y, offset, w, h) 44 | text_width = self.string_len(self._text) 45 | if self._add_box: 46 | # Minimize widget to make a nice little button. Only centre it if there are no label offsets. 47 | if offset == 0: 48 | self._x += max(0, (self.width - text_width) // 2) 49 | self._w = min(self._w, text_width) + offset 50 | else: 51 | # Maximize text to make for a consistent colouring when used in menus. 52 | self._text += " " * (self._w - text_width) 53 | 54 | def update(self, frame_no: int): 55 | self._draw_label() 56 | 57 | assert self._frame 58 | (colour, attr, bg) = self._pick_colours("button") 59 | self._frame.canvas.print_at(self._text, self._x + self._offset, self._y, colour, attr, bg) 60 | 61 | def reset(self): 62 | self._value = False 63 | 64 | def process_event(self, event: Optional[Event]) -> Optional[Event]: 65 | if isinstance(event, KeyboardEvent): 66 | if event.key_code in [ord(" "), 10, 13]: 67 | self._on_click() 68 | return None 69 | # Ignore any other key press. 70 | return event 71 | if isinstance(event, MouseEvent): 72 | if event.buttons != 0 and self.is_mouse_over(event, include_label=False): 73 | self._on_click() 74 | return None 75 | # Ignore other events 76 | return event 77 | 78 | def required_height(self, offset: int, width: int): 79 | return 1 80 | 81 | @property 82 | def text(self): 83 | """ 84 | The current text for this Button. 85 | """ 86 | return self._text_raw 87 | 88 | @text.setter 89 | def text(self, new_text): 90 | self._text_raw = new_text 91 | self._text = f"< {new_text} >" if self._add_box else new_text 92 | 93 | @property 94 | def value(self): 95 | """ 96 | The current value for this Button. 97 | """ 98 | return self._value 99 | 100 | @value.setter 101 | def value(self, new_value): 102 | self._value = new_value 103 | -------------------------------------------------------------------------------- /asciimatics/widgets/checkbox.py: -------------------------------------------------------------------------------- 1 | """This module defines a checkbox widget""" 2 | from __future__ import annotations 3 | from typing import Callable, Optional 4 | from asciimatics.event import KeyboardEvent, MouseEvent, Event 5 | from asciimatics.widgets.widget import Widget 6 | 7 | 8 | class CheckBox(Widget): 9 | """ 10 | A CheckBox widget is used to ask for Boolean (i.e. yes/no) input. 11 | 12 | It consists of an optional label (typically used for the first in a group of CheckBoxes), 13 | the box and a field name. 14 | """ 15 | 16 | __slots__ = ["_text", "_on_change"] 17 | 18 | def __init__(self, 19 | text: str, 20 | label: Optional[str] = None, 21 | name: Optional[str] = None, 22 | on_change: Optional[Callable] = None, 23 | **kwargs): 24 | """ 25 | :param text: The text to explain this specific field to the user. 26 | :param label: An optional label for the widget. 27 | :param name: The internal name for the widget. 28 | :param on_change: Optional function to call when text changes. 29 | 30 | Also see the common keyword arguments in :py:obj:`.Widget`. 31 | """ 32 | super().__init__(name, **kwargs) 33 | self._text = text 34 | self._label = label 35 | self._on_change = on_change 36 | 37 | def update(self, frame_no: int): 38 | self._draw_label() 39 | 40 | # Render this checkbox. 41 | assert self._frame 42 | check_char = "✓" if self._frame.canvas.unicode_aware else "X" 43 | (colour, attr, bg) = self._pick_colours("control", self._has_focus or self._value) 44 | self._frame.canvas.print_at(f"[{check_char if self._value else ' '}] ", 45 | self._x + self._offset, 46 | self._y, 47 | colour, 48 | attr, 49 | bg) 50 | (colour, attr, bg) = self._pick_colours("field", self._has_focus or self._value) 51 | self._frame.canvas.print_at(self._text, self._x + self._offset + 4, self._y, colour, attr, bg) 52 | 53 | def reset(self): 54 | pass 55 | 56 | def process_event(self, event: Optional[Event]) -> Optional[Event]: 57 | if isinstance(event, KeyboardEvent): 58 | if event.key_code in [ord(" "), 10, 13]: 59 | # Use property to trigger events. 60 | self.value = not self._value 61 | else: 62 | # Ignore any other key press. 63 | return event 64 | elif isinstance(event, MouseEvent): 65 | # Mouse event - rebase coordinates to Frame context. 66 | if event.buttons != 0: 67 | if self.is_mouse_over(event, include_label=False): 68 | # Use property to trigger events. 69 | self.value = not self._value 70 | return None 71 | # Ignore other mouse events. 72 | return event 73 | else: 74 | # Ignore other events 75 | return event 76 | 77 | # If we got here, we processed the event - swallow it. 78 | return None 79 | 80 | def required_height(self, offset: int, width: int): 81 | return 1 82 | 83 | @property 84 | def value(self): 85 | """ 86 | The current value for this Checkbox. 87 | """ 88 | return self._value 89 | 90 | @value.setter 91 | def value(self, new_value): 92 | # Only trigger the notification after we've changed the value. 93 | old_value = self._value 94 | self._value = new_value if new_value else False 95 | if old_value != self._value and self._on_change: 96 | self._on_change() 97 | -------------------------------------------------------------------------------- /asciimatics/widgets/datepicker.py: -------------------------------------------------------------------------------- 1 | """This module defines a datepicker widget""" 2 | from __future__ import annotations 3 | from datetime import date, datetime 4 | from typing import Callable, Optional 5 | from asciimatics.exceptions import InvalidFields 6 | from asciimatics.widgets.basepicker import _BasePicker 7 | from asciimatics.widgets.label import Label 8 | from asciimatics.widgets.layout import Layout 9 | from asciimatics.widgets.listbox import ListBox 10 | from asciimatics.widgets.temppopup import _TempPopup 11 | 12 | 13 | class _DatePickerPopup(_TempPopup): 14 | """ 15 | An internal Frame for editing the currently selected date. 16 | """ 17 | 18 | def __init__(self, parent: "DatePicker", year_range: Optional[range] = None): 19 | """ 20 | :param parent: The widget that spawned this pop-up. 21 | :param year_range: Optional range to limit the year selection to. 22 | """ 23 | # Create the lists for each entry. 24 | now = parent.value if parent.value else date.today() 25 | if year_range is None: 26 | year_range = range(now.year - 50, now.year + 50) 27 | self._days = ListBox(3, [(f"{x:02}", x) for x in range(1, 32)], 28 | centre=True, 29 | validator=self._check_date) 30 | self._months = ListBox(3, [(now.replace(day=1, month=x).strftime("%b"), x) for x in range(1, 13)], 31 | centre=True, 32 | on_change=self._refresh_day) 33 | self._years = ListBox(3, [(f"{x:04}", x) for x in year_range], 34 | centre=True, 35 | on_change=self._refresh_day) 36 | 37 | # Construct the Frame 38 | assert parent.frame 39 | location = parent.get_location() 40 | super().__init__(parent.frame.screen, parent, location[0] - 1, location[1] - 2, 13, 5) 41 | 42 | # Build the widget to display the time selection. 43 | layout = Layout([2, 1, 3, 1, 4], fill_frame=True) 44 | self.add_layout(layout) 45 | layout.add_widget(self._days, 0) 46 | layout.add_widget(Label("\n/", height=3), 1) 47 | layout.add_widget(self._months, 2) 48 | layout.add_widget(Label("\n/", height=3), 3) 49 | layout.add_widget(self._years, 4) 50 | self.fix() 51 | 52 | # Set up the correct time. 53 | self._years.value = parent.value.year 54 | self._months.value = parent.value.month 55 | self._days.value = parent.value.day 56 | 57 | def _check_date(self, value: int): 58 | try: 59 | date(self._years.value, self._months.value, value) 60 | return True 61 | except (TypeError, ValueError): 62 | return False 63 | 64 | def _refresh_day(self): 65 | self._days.value = self._days.value 66 | 67 | def _on_close(self, cancelled: bool): 68 | try: 69 | if not cancelled: 70 | self._parent.value = self._parent.value.replace(day=self._days.value, 71 | month=self._months.value, 72 | year=self._years.value) 73 | except ValueError as e: 74 | raise InvalidFields([self._days.value]) from e 75 | 76 | 77 | class DatePicker(_BasePicker): 78 | """ 79 | A DatePicker widget allows you to pick a date from a compact, temporary, pop-up Frame. 80 | """ 81 | 82 | __slots__ = ["_year_range"] 83 | 84 | def __init__(self, 85 | label: Optional[str] = None, 86 | name: Optional[str] = None, 87 | year_range: Optional[range] = None, 88 | on_change: Optional[Callable] = None, 89 | **kwargs): 90 | """ 91 | :param label: An optional label for the widget. 92 | :param name: The name for the widget. 93 | :param on_change: Optional function to call when the selected time changes. 94 | 95 | Also see the common keyword arguments in :py:obj:`.Widget`. 96 | """ 97 | super().__init__(_DatePickerPopup, label=label, name=name, on_change=on_change, **kwargs) 98 | self._value = datetime.now().date() 99 | self._year_range = year_range 100 | 101 | def update(self, frame_no: int): 102 | self._draw_label() 103 | 104 | # This widget only ever needs display the current selection - the separate Frame does all 105 | # the clever stuff when it has the focus. 106 | assert self._frame 107 | (colour, attr, background) = self._pick_colours("edit_text") 108 | self._frame.canvas.print_at(self._value.strftime("%d/%b/%Y"), 109 | self._x + self._offset, 110 | self._y, 111 | colour, 112 | attr, 113 | background) 114 | -------------------------------------------------------------------------------- /asciimatics/widgets/divider.py: -------------------------------------------------------------------------------- 1 | """This module defines a divider between widgets""" 2 | from __future__ import annotations 3 | from typing import TYPE_CHECKING, Optional 4 | from asciimatics.widgets.widget import Widget 5 | if TYPE_CHECKING: 6 | from asciimatics.event import Event 7 | from asciimatics.widgets.frame import Frame 8 | 9 | 10 | class Divider(Widget): 11 | """ 12 | A divider to break up a group of widgets. 13 | """ 14 | 15 | __slots__ = ["_draw_line", "_required_height", "_line_char"] 16 | 17 | def __init__(self, draw_line: bool = True, height: int = 1, line_char: Optional[str] = None): 18 | """ 19 | :param draw_line: Whether to draw a line in the centre of the gap. 20 | :param height: The required vertical gap. 21 | :param line_char: Optional character to use for drawing the line. 22 | """ 23 | # Dividers have no value and so should have no name for look-ups either. 24 | super().__init__(None, tab_stop=False) 25 | self._draw_line = draw_line 26 | self._required_height = height 27 | self._line_char = line_char 28 | 29 | def register_frame(self, frame: Frame): 30 | # Update line drawing character if needed once we have a canvas to query. 31 | super().register_frame(frame) 32 | assert self._frame 33 | if self._line_char is None: 34 | self._line_char = "─" if self._frame.canvas.unicode_aware else "-" 35 | 36 | def process_event(self, event: Optional[Event]) -> Optional[Event]: 37 | # Dividers have no user interactions 38 | return event 39 | 40 | def update(self, frame_no: int): 41 | assert self._frame 42 | (colour, attr, background) = self._frame.palette["borders"] 43 | if self._draw_line: 44 | assert self._line_char 45 | self._frame.canvas.print_at(self._line_char * self._w, 46 | self._x, 47 | self._y + (self._h // 2), 48 | colour, 49 | attr, 50 | background) 51 | 52 | def reset(self): 53 | pass 54 | 55 | def required_height(self, offset: int, width: int) -> int: 56 | return self._required_height 57 | 58 | @property 59 | def value(self): 60 | """ 61 | The current value for this Divider. 62 | """ 63 | return None 64 | 65 | @value.setter 66 | def value(self, new_value): 67 | self._value = new_value 68 | -------------------------------------------------------------------------------- /asciimatics/widgets/label.py: -------------------------------------------------------------------------------- 1 | """This mdoule implements a widget to give a text label""" 2 | from __future__ import annotations 3 | from typing import TYPE_CHECKING, Optional 4 | from asciimatics.widgets.widget import Widget 5 | from asciimatics.widgets.utilities import _split_text 6 | if TYPE_CHECKING: 7 | from asciimatics.event import Event 8 | 9 | 10 | class Label(Widget): 11 | """ 12 | A text label. 13 | """ 14 | 15 | __slots__ = ["_text", "_required_height", "_align"] 16 | 17 | def __init__(self, label: str, height: int = 1, align: str = "<", name: Optional[str] = None): 18 | """ 19 | :param label: The text to be displayed for the Label. 20 | :param height: Optional height for the label. Defaults to 1 line. 21 | :param align: Optional alignment for the Label. Defaults to left aligned. 22 | Options are "<" = left, ">" = right and "^" = centre 23 | :param name: The name of this widget. 24 | 25 | """ 26 | # Labels have no value and so should have no name for look-ups either. 27 | super().__init__(name, tab_stop=False) 28 | 29 | # Although this is a label, we don't want it to contribute to the layout 30 | # tab calculations, so leave internal `_label` value as None. 31 | # Also ensure that the label really is text. 32 | self._text = str(label) 33 | self._required_height = height 34 | self._align = align 35 | 36 | def process_event(self, event: Optional[Event]) -> Optional[Event]: 37 | # Labels have no user interactions 38 | return event 39 | 40 | def update(self, frame_no: int): 41 | assert self._frame 42 | (colour, attr, background) = self._frame.palette[self._pick_palette_key("label", 43 | selected=False, 44 | allow_input_state=False)] 45 | for i, text in enumerate(_split_text(self._text, self._w, self._h, self._frame.canvas.unicode_aware)): 46 | self._frame.canvas.paint(f"{text:{self._align}{self._w}}", 47 | self._x, 48 | self._y + i, 49 | colour, 50 | attr, 51 | background) 52 | 53 | def reset(self): 54 | pass 55 | 56 | def required_height(self, offset: int, width: int) -> int: 57 | # Allow one line for text and a blank spacer before it. 58 | return self._required_height 59 | 60 | @property 61 | def text(self): 62 | """ 63 | The current text for this Label. 64 | """ 65 | return self._text 66 | 67 | @text.setter 68 | def text(self, new_value): 69 | self._text = new_value 70 | 71 | @property 72 | def value(self) -> None: 73 | """ 74 | The current value for this Label. 75 | """ 76 | return None 77 | 78 | @value.setter 79 | def value(self, new_value): 80 | self._value = new_value 81 | -------------------------------------------------------------------------------- /asciimatics/widgets/popupdialog.py: -------------------------------------------------------------------------------- 1 | """This module implements a Pop up dialog message box""" 2 | from __future__ import annotations 3 | from inspect import isfunction 4 | from functools import partial 5 | from typing import TYPE_CHECKING, Callable, List, Optional 6 | from wcwidth import wcswidth 7 | from asciimatics.widgets.button import Button 8 | from asciimatics.widgets.frame import Frame 9 | from asciimatics.widgets.layout import Layout 10 | from asciimatics.widgets.textbox import TextBox 11 | from asciimatics.widgets.utilities import _split_text 12 | if TYPE_CHECKING: 13 | from asciimatics.scene import Scene 14 | from asciimatics.screen import Screen 15 | 16 | 17 | class PopUpDialog(Frame): 18 | """ 19 | A fixed implementation Frame that provides a standard message box dialog. 20 | """ 21 | 22 | def __init__(self, 23 | screen: Screen, 24 | text: str, 25 | buttons: List[str], 26 | on_close: Optional[Callable] = None, 27 | has_shadow: bool = False, 28 | theme: str = "warning"): 29 | """ 30 | :param screen: The Screen that owns this dialog. 31 | :param text: The message text to display. 32 | :param buttons: A list of button names to display. This may be an empty list. 33 | :param on_close: Optional function to invoke on exit. 34 | :param has_shadow: optional flag to specify if dialog should have a shadow when drawn. 35 | :param theme: optional colour theme for this pop-up. Defaults to the warning colours. 36 | 37 | The `on_close` method (if specified) will be called with one integer parameter that 38 | corresponds to the index of the button passed in the array of available `buttons`. 39 | 40 | Note that `on_close` must be a static method to work across screen resizing. Either it 41 | is static (and so the dialog will be cloned) or it is not (and the dialog will disappear 42 | when the screen is resized). 43 | """ 44 | # Remember parameters for cloning. 45 | self._text = text 46 | self._buttons = buttons 47 | self._on_close = on_close 48 | 49 | # Decide on optimum width of the dialog. Limit to 2/3 the screen width. 50 | string_len = wcswidth if screen.unicode_aware else len 51 | width = max(string_len(x) for x in text.split("\n")) 52 | width = max(width + 2, sum(string_len(x) + 4 for x in buttons) + len(buttons) + 5) 53 | width = min(width, screen.width * 2 // 3) 54 | 55 | # Figure out the necessary message and allow for buttons and borders 56 | # when deciding on height. 57 | delta_h = 4 if len(buttons) > 0 else 2 58 | self._message = _split_text(text, width - 2, screen.height - delta_h, screen.unicode_aware) 59 | height = len(self._message) + delta_h 60 | 61 | # Construct the Frame 62 | self._data = {"message": self._message} 63 | super().__init__(screen, height, width, self._data, has_shadow=has_shadow, is_modal=True) 64 | 65 | # Build up the message box 66 | layout = Layout([width - 2], fill_frame=True) 67 | self.add_layout(layout) 68 | text_box = TextBox(len(self._message), name="message") 69 | text_box.disabled = True 70 | layout.add_widget(text_box) 71 | layout2 = Layout([1 for _ in buttons]) 72 | self.add_layout(layout2) 73 | for i, button in enumerate(buttons): 74 | func = partial(self._destroy, i) 75 | layout2.add_widget(Button(button, func), i) 76 | self.fix() 77 | 78 | # Ensure that we have the right palette in place 79 | self.set_theme(theme) 80 | 81 | def _destroy(self, selected: int): 82 | if self._scene: 83 | self._scene.remove_effect(self) 84 | if self._on_close: 85 | self._on_close(selected) 86 | 87 | def clone(self, screen: Screen, scene: Scene): 88 | """ 89 | Create a clone of this Dialog into a new Screen. 90 | 91 | :param screen: The new Screen object to clone into. 92 | :param scene: The new Scene object to clone into. 93 | """ 94 | # Only clone the object if the function is safe to do so. 95 | if self._on_close is None or isfunction(self._on_close): 96 | scene.add_effect(PopUpDialog(screen, self._text, self._buttons, self._on_close)) 97 | -------------------------------------------------------------------------------- /asciimatics/widgets/popupmenu.py: -------------------------------------------------------------------------------- 1 | """This module implements a pop up menu widget""" 2 | from __future__ import annotations 3 | from collections import defaultdict 4 | from functools import partial 5 | from typing import Callable, List, Optional, Tuple 6 | from asciimatics.event import KeyboardEvent, MouseEvent, Event 7 | from asciimatics.screen import Screen 8 | from asciimatics.widgets.button import Button 9 | from asciimatics.widgets.frame import Frame 10 | from asciimatics.widgets.layout import Layout 11 | 12 | 13 | class PopupMenu(Frame): 14 | """ 15 | A widget for displaying a menu. 16 | """ 17 | 18 | palette = defaultdict(lambda: (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_CYAN)) 19 | palette["focus_button"] = (Screen.COLOUR_CYAN, Screen.A_NORMAL, Screen.COLOUR_WHITE) 20 | 21 | def __init__(self, 22 | screen: Screen, 23 | menu_items: List[Tuple[str, Callable]], 24 | x: int, 25 | y: int, 26 | has_border: bool = False): 27 | """ 28 | :param screen: The Screen being used for this pop-up. 29 | :param menu_items: a list of items to be displayed in the menu. 30 | :param x: The X coordinate for the desired pop-up. 31 | :param y: The Y coordinate for the desired pop-up. 32 | :param has_border: Whether the menu has a border box. Defaults to False. 33 | 34 | The menu_items parameter is a list of 2-tuples, which define the text to be displayed in 35 | the menu and the function to call when that menu item is clicked. For example: 36 | 37 | menu_items = [("Open", file_open), ("Save", file_save), ("Close", file_close)] 38 | """ 39 | border_adjustment = 0 40 | if has_border: 41 | border_adjustment = 2 # We add one character to each side for the border 42 | 43 | # Sort out location based on width of menu text. 44 | w = max(len(i[0]) for i in menu_items) + border_adjustment 45 | h = len(menu_items) + border_adjustment 46 | if x + w >= screen.width: 47 | x -= w - 1 48 | if y + h >= screen.height: 49 | y -= h - 1 50 | 51 | # Construct the Frame 52 | super().__init__(screen, 53 | h, 54 | w, 55 | x=x, 56 | y=y, 57 | has_border=has_border, 58 | can_scroll=False, 59 | is_modal=True, 60 | hover_focus=True) 61 | 62 | # Build the widget to display the time selection. 63 | layout = Layout([1], fill_frame=True) 64 | self.add_layout(layout) 65 | for item in menu_items: 66 | func = partial(self._destroy, item[1]) 67 | layout.add_widget(Button(item[0], func, add_box=False), 0) 68 | self.fix() 69 | 70 | def _destroy(self, callback: Optional[Callable] = None): 71 | if self._scene: 72 | self._scene.remove_effect(self) 73 | if callback is not None: 74 | callback() 75 | 76 | def process_event(self, event: Optional[Event]) -> Optional[Event]: 77 | # Look for events that will close the pop-up - e.g. clicking outside the Frame or ESC key. 78 | if event is not None: 79 | if isinstance(event, KeyboardEvent): 80 | if event.key_code == Screen.KEY_ESCAPE: 81 | event = None 82 | elif isinstance(event, MouseEvent) and event.buttons != 0: 83 | if self._outside_frame(event): 84 | event = None 85 | if event is None: 86 | self._destroy() 87 | return super().process_event(event) 88 | -------------------------------------------------------------------------------- /asciimatics/widgets/radiobuttons.py: -------------------------------------------------------------------------------- 1 | """This module implements the widget for radio buttons""" 2 | from __future__ import annotations 3 | from typing import Callable, List, Optional, Tuple 4 | from asciimatics.event import KeyboardEvent, MouseEvent, Event 5 | from asciimatics.screen import Screen 6 | from asciimatics.widgets.widget import Widget 7 | 8 | 9 | class RadioButtons(Widget): 10 | """ 11 | A RadioButtons widget is used to ask for one of a list of values to be selected by the user. 12 | 13 | It consists of an optional label and then a list of selection bullets with field names. 14 | """ 15 | 16 | __slots__ = ["_options", "_selection", "_start_column", "_on_change"] 17 | 18 | def __init__(self, 19 | options: List[Tuple[str, int]], 20 | label: Optional[str] = None, 21 | name: Optional[str] = None, 22 | on_change: Optional[Callable] = None, 23 | **kwargs): 24 | """ 25 | :param options: A list of (text, value) tuples for each radio button. 26 | :param label: An optional label for the widget. 27 | :param name: The internal name for the widget. 28 | :param on_change: Optional function to call when text changes. 29 | 30 | Also see the common keyword arguments in :py:obj:`.Widget`. 31 | """ 32 | super().__init__(name, **kwargs) 33 | self._options = options 34 | self._label = label 35 | self._selection = 0 36 | self._start_column = 0 37 | self._on_change = on_change 38 | 39 | def update(self, frame_no: int): 40 | self._draw_label() 41 | 42 | # Decide on check char 43 | assert self._frame 44 | check_char = "•" if self._frame.canvas.unicode_aware else "X" 45 | 46 | # Render the list of radio buttons. 47 | for i, (text, _) in enumerate(self._options): 48 | fg, attr, bg = self._pick_colours("control", i == self._selection) 49 | fg2, attr2, bg2 = self._pick_colours("field", i == self._selection) 50 | check = check_char if i == self._selection else " " 51 | self._frame.canvas.print_at(f"({check}) ", self._x + self._offset, self._y + i, fg, attr, bg) 52 | self._frame.canvas.print_at(text, self._x + self._offset + 4, self._y + i, fg2, attr2, bg2) 53 | 54 | def reset(self): 55 | pass 56 | 57 | def process_event(self, event: Optional[Event]) -> Optional[Event]: 58 | if isinstance(event, KeyboardEvent): 59 | if event.key_code == Screen.KEY_UP: 60 | # Use property to trigger events. 61 | self._selection = max(0, self._selection - 1) 62 | self.value = self._options[self._selection][1] 63 | elif event.key_code == Screen.KEY_DOWN: 64 | # Use property to trigger events. 65 | self._selection = min(self._selection + 1, len(self._options) - 1) 66 | self.value = self._options[self._selection][1] 67 | else: 68 | # Ignore any other key press. 69 | return event 70 | elif isinstance(event, MouseEvent): 71 | # Mouse event - rebase coordinates to Frame context. 72 | if event.buttons != 0: 73 | if self.is_mouse_over(event, include_label=False): 74 | # Use property to trigger events. 75 | self._selection = event.y - self._y 76 | self.value = self._options[self._selection][1] 77 | return None 78 | # Ignore other mouse events. 79 | return event 80 | else: 81 | # Ignore non-keyboard events 82 | return event 83 | 84 | # If we got here, we processed the event - swallow it. 85 | return None 86 | 87 | def required_height(self, offset: int, width: int) -> int: 88 | return len(self._options) 89 | 90 | @property 91 | def value(self): 92 | """ 93 | The current value for these RadioButtons. 94 | """ 95 | # The value is actually the value of the current selection. 96 | return self._options[self._selection][1] 97 | 98 | @value.setter 99 | def value(self, new_value): 100 | # Only trigger the notification after we've changed the value. 101 | old_value = self._value 102 | for i, (_, value) in enumerate(self._options): 103 | if new_value == value: 104 | self._selection = i 105 | break 106 | else: 107 | self._selection = 0 108 | self._value = self._options[self._selection][1] 109 | if old_value != self._value and self._on_change: 110 | self._on_change() 111 | -------------------------------------------------------------------------------- /asciimatics/widgets/scrollbar.py: -------------------------------------------------------------------------------- 1 | """This module implements a scroll bar capability for widgets""" 2 | from __future__ import annotations 3 | from typing import TYPE_CHECKING, Callable, Dict, Optional 4 | if TYPE_CHECKING: 5 | from asciimatics.event import MouseEvent 6 | from asciimatics.screen import Canvas 7 | 8 | 9 | class _ScrollBar(): 10 | """ 11 | Internal object to provide vertical scroll bars for widgets. 12 | """ 13 | 14 | def __init__(self, 15 | canvas: Canvas, 16 | palette: Dict[str, tuple[Optional[int], Optional[int], Optional[int]]], 17 | x: int, 18 | y: int, 19 | height: int, 20 | get_pos: Callable, 21 | set_pos: Callable, 22 | absolute: bool = False): 23 | """ 24 | :param canvas: The canvas on which to draw the scroll bar. 25 | :param palette: The palette of the parent Frame. 26 | :param x: The x location of the top of the scroll bar. 27 | :param y: The y location of the top of the scroll bar. 28 | :param height: The height of the scroll bar. 29 | :param get_pos: A function to return the current position of the scroll bar. 30 | :param set_pos: A function to set the current position of the scroll bar. 31 | :param absolute: Whether the scroll bar should use absolute co-ordinates when handling mouse 32 | events. 33 | 34 | The current position for the scroll bar is defined to be 0.0 at the top and 1.0 at the 35 | bottom. The scroll bar will call `get_pos` to find the current position when drawing and 36 | uses `set_pos` to update this position on a mouse click. 37 | 38 | The widget using the scroll bar is responsible for maintaining its own state of where the 39 | current view is scrolled (e.g. which is the top line in a text box) and for providing 40 | these two functions to translate that internal state into a form the scroll bar can use. 41 | """ 42 | self._canvas = canvas 43 | self.palette = palette 44 | self.max_height = 0 45 | self._x = x 46 | self._y = y 47 | self._height = height 48 | self._absolute = absolute 49 | self._get_pos = get_pos 50 | self._set_pos = set_pos 51 | 52 | def update(self): 53 | """ 54 | Draw the scroll bar. 55 | """ 56 | # Sort out chars 57 | cursor = "█" if self._canvas.unicode_aware else "O" 58 | back = "░" if self._canvas.unicode_aware else "|" 59 | 60 | # Now draw... 61 | try: 62 | sb_pos = self._get_pos() 63 | sb_pos = min(1, max(0, sb_pos)) 64 | sb_pos = max(int(self._height * sb_pos) - 1, 0) 65 | except ZeroDivisionError: 66 | sb_pos = 0 67 | (colour, attr, bg) = self.palette["scroll"] 68 | y = self._canvas.start_line if self._absolute else 0 69 | for dy in range(self._height): 70 | self._canvas.print_at(cursor if dy == sb_pos else back, 71 | self._x, 72 | y + self._y + dy, 73 | colour, 74 | attr, 75 | bg) 76 | 77 | def is_mouse_over(self, event: MouseEvent) -> bool: 78 | """ 79 | Check whether a MouseEvent is over thus scroll bar. 80 | 81 | :param event: The MouseEvent to check. 82 | 83 | :returns: True if the mouse event is over the scroll bar. 84 | """ 85 | return event.x == self._x and self._y <= event.y < self._y + self._height 86 | 87 | def process_event(self, event: MouseEvent) -> bool: 88 | """ 89 | Handle input on the scroll bar. 90 | 91 | :param event: the event to be processed. 92 | 93 | :returns: True if the scroll bar handled the event. 94 | """ 95 | # Convert into absolute coordinates if needed. 96 | new_event = event 97 | if self._absolute: 98 | new_event.y -= self._canvas.start_line 99 | 100 | # Process event if needed. 101 | if self.is_mouse_over(new_event) and event.buttons != 0: 102 | self._set_pos((new_event.y - self._y) / (self._height - 1)) 103 | return True 104 | return False 105 | -------------------------------------------------------------------------------- /asciimatics/widgets/temppopup.py: -------------------------------------------------------------------------------- 1 | """This module implements a base class for popups""" 2 | from __future__ import annotations 3 | from collections import defaultdict 4 | from abc import abstractmethod 5 | from typing import TYPE_CHECKING, Optional 6 | from asciimatics.event import KeyboardEvent, MouseEvent, Event 7 | from asciimatics.exceptions import InvalidFields 8 | from asciimatics.screen import Screen 9 | from asciimatics.widgets.frame import Frame 10 | if TYPE_CHECKING: 11 | from asciimatics.widgets.widget import Widget 12 | 13 | 14 | class _TempPopup(Frame): 15 | """ 16 | An internal Frame for creating a temporary pop-up for a Widget in another Frame. 17 | """ 18 | 19 | def __init__(self, screen: Screen, parent: Widget, x: int, y: int, w: int, h: int): 20 | """ 21 | :param screen: The Screen being used for this pop-up. 22 | :param parent: The widget that spawned this pop-up. 23 | :param x: The X coordinate for the desired pop-up. 24 | :param y: The Y coordinate for the desired pop-up. 25 | :param w: The width of the desired pop-up. 26 | :param h: The height of the desired pop-up. 27 | """ 28 | # Construct the Frame 29 | super().__init__(screen, h, w, x=x, y=y, has_border=True, can_scroll=False, is_modal=True) 30 | 31 | # Set up the new palette for this Frame 32 | assert parent.frame 33 | self.palette = defaultdict(lambda: parent.frame.palette["focus_field"]) # type: ignore 34 | self.palette["selected_field"] = parent.frame.palette["selected_field"] 35 | self.palette["selected_focus_field"] = parent.frame.palette["selected_focus_field"] 36 | self.palette["invalid"] = parent.frame.palette["invalid"] 37 | 38 | # Internal state for the pop-up 39 | self._parent = parent 40 | 41 | def process_event(self, event: Optional[Event]) -> Optional[Event]: 42 | # Look for events that will close the pop-up - e.g. clicking outside the Frame or Enter key. 43 | cancelled = False 44 | if event is not None: 45 | if isinstance(event, KeyboardEvent): 46 | if event.key_code in [Screen.ctrl("M"), Screen.ctrl("J"), ord(" ")]: 47 | event = None 48 | elif event.key_code == Screen.KEY_ESCAPE: 49 | event = None 50 | cancelled = True 51 | elif isinstance(event, MouseEvent) and event.buttons != 0: 52 | if self._outside_frame(event): 53 | event = None 54 | 55 | # Remove this pop-up if we're done; otherwise bubble up the event. 56 | if event is None: 57 | try: 58 | self.close(cancelled) 59 | except InvalidFields: 60 | # Nothing to do as we've already prevented the Effect from being removed. 61 | pass 62 | return super().process_event(event) 63 | 64 | def close(self, cancelled: bool = False): 65 | """ 66 | Close this temporary pop-up. 67 | 68 | :param cancelled: Whether the pop-up was cancelled (e.g. by pressing Esc). 69 | """ 70 | assert self._scene 71 | self._on_close(cancelled) 72 | self._scene.remove_effect(self) 73 | 74 | @abstractmethod 75 | def _on_close(self, cancelled): 76 | """ 77 | Method to handle any communication back to the parent widget on closure of this pop-up. 78 | 79 | :param cancelled: Whether the pop-up was cancelled (e.g. by pressing Esc). 80 | 81 | This method can raise an InvalidFields exception to indicate that the current selection is 82 | invalid and so the pop-up cannot be dismissed. 83 | """ 84 | -------------------------------------------------------------------------------- /asciimatics/widgets/timepicker.py: -------------------------------------------------------------------------------- 1 | """This module implements a time picker widget""" 2 | from __future__ import annotations 3 | from datetime import datetime 4 | from typing import Callable, Optional 5 | from asciimatics.widgets.basepicker import _BasePicker 6 | from asciimatics.widgets.label import Label 7 | from asciimatics.widgets.layout import Layout 8 | from asciimatics.widgets.listbox import ListBox 9 | from asciimatics.widgets.temppopup import _TempPopup 10 | 11 | 12 | class _TimePickerPopup(_TempPopup): 13 | """ 14 | An internal Frame for editing the currently selected time. 15 | """ 16 | 17 | def __init__(self, parent: "TimePicker"): 18 | """ 19 | :param parent: The widget that spawned this pop-up. 20 | """ 21 | # Construct the Frame 22 | assert parent.frame 23 | location = parent.get_location() 24 | super().__init__(parent.frame.screen, 25 | parent, 26 | location[0] - 1, 27 | location[1] - 2, 28 | 10 if parent.include_seconds else 7, 29 | 5) 30 | 31 | # Build the widget to display the time selection. 32 | assert isinstance(self._parent, TimePicker) 33 | self._hours = ListBox(3, [(f"{x:02}", x) for x in range(24)], centre=True) 34 | self._minutes = ListBox(3, [(f"{x:02}", x) for x in range(60)], centre=True) 35 | self._seconds = ListBox(3, [(f"{x:02}", x) for x in range(60)], centre=True) 36 | if self._parent.include_seconds: 37 | layout = Layout([2, 1, 2, 1, 2], fill_frame=True) 38 | else: 39 | layout = Layout([2, 1, 2], fill_frame=True) 40 | self.add_layout(layout) 41 | layout.add_widget(self._hours, 0) 42 | layout.add_widget(Label("\n:", height=3), 1) 43 | layout.add_widget(self._minutes, 2) 44 | if self._parent.include_seconds: 45 | layout.add_widget(Label("\n:", height=3), 3) 46 | layout.add_widget(self._seconds, 4) 47 | self.fix() 48 | 49 | # Set up the correct time. 50 | self._hours.value = parent.value.hour 51 | self._minutes.value = parent.value.minute 52 | self._seconds.value = parent.value.second 53 | 54 | def _on_close(self, cancelled: bool): 55 | if not cancelled: 56 | self._parent.value = self._parent.value.replace(hour=self._hours.value, 57 | minute=self._minutes.value, 58 | second=self._seconds.value) 59 | 60 | 61 | class TimePicker(_BasePicker): 62 | """ 63 | A TimePicker widget allows you to pick a time from a compact, temporary, pop-up Frame. 64 | """ 65 | 66 | __slots__ = ["include_seconds"] 67 | 68 | def __init__(self, 69 | label: Optional[str] = None, 70 | name: Optional[str] = None, 71 | seconds: bool = False, 72 | on_change: Optional[Callable] = None, 73 | **kwargs): 74 | """ 75 | :param label: An optional label for the widget. 76 | :param name: The name for the widget. 77 | :param seconds: Whether to include selection of seconds or not. 78 | :param on_change: Optional function to call when the selected time changes. 79 | 80 | Also see the common keyword arguments in :py:obj:`.Widget`. 81 | """ 82 | super().__init__(_TimePickerPopup, label=label, name=name, on_change=on_change, **kwargs) 83 | self._value = datetime.now().time() 84 | self.include_seconds = seconds 85 | 86 | def update(self, frame_no: int): 87 | self._draw_label() 88 | 89 | # This widget only ever needs display the current selection - the separate Frame does all 90 | # the clever stuff when it has the focus. 91 | assert self._frame 92 | (colour, attr, background) = self._pick_colours("edit_text") 93 | self._frame.canvas.print_at(self._value.strftime("%H:%M:%S" if self.include_seconds else "%H:%M"), 94 | self._x + self._offset, 95 | self._y, 96 | colour, 97 | attr, 98 | background) 99 | -------------------------------------------------------------------------------- /asciimatics/widgets/verticaldivider.py: -------------------------------------------------------------------------------- 1 | """This module implements a vertical division between widgets""" 2 | from __future__ import annotations 3 | from typing import Optional 4 | from asciimatics.widgets.widget import Widget 5 | from asciimatics.event import Event 6 | 7 | 8 | class VerticalDivider(Widget): 9 | """ 10 | A vertical divider for separating columns. 11 | 12 | This widget should be put into a column of its own in the Layout. 13 | """ 14 | 15 | __slots__ = ["_required_height"] 16 | 17 | def __init__(self, height: int = Widget.FILL_COLUMN): 18 | """ 19 | :param height: The required height for this divider. 20 | """ 21 | super().__init__(None, tab_stop=False) 22 | self._required_height = height 23 | 24 | def process_event(self, event: Optional[Event]) -> Optional[Event]: 25 | return event 26 | 27 | def update(self, frame_no: int): 28 | assert self._frame 29 | (color, attr, background) = self._frame.palette["borders"] 30 | vert = "│" if self._frame.canvas.unicode_aware else "|" 31 | for i in range(self._h): 32 | self._frame.canvas.print_at(vert, self._x, self._y + i, color, attr, background) 33 | 34 | def reset(self): 35 | pass 36 | 37 | def required_height(self, offset: int, width: int) -> int: 38 | return self._required_height 39 | 40 | @property 41 | def value(self) -> None: 42 | """ 43 | The current value for this VerticalDivider. 44 | """ 45 | return None 46 | 47 | @value.setter 48 | def value(self, new_value): 49 | self._value = new_value 50 | -------------------------------------------------------------------------------- /doc/build.sh: -------------------------------------------------------------------------------- 1 | PYTHONPATH=.. sphinx-apidoc ../asciimatics -o ./source -f 2 | cat source/asciimatics.rst | awk -- '/:undoc-members:/ {next} { print $0 } /:members:/ { print " :inherited-members:"}' > source/tmp.rst 3 | mv -f source/tmp.rst source/asciimatics.rst 4 | cat source/asciimatics.widgets.rst | awk -- '/:undoc-members:/ {next} { print $0 } /:members:/ { print " :inherited-members:"}' > source/tmp.rst 5 | mv -f source/tmp.rst source/asciimatics.widgets.rst 6 | cat source/asciimatics.renderers.rst | awk -- '/:undoc-members:/ {next} { print $0 } /:members:/ { print " :inherited-members:"}' > source/tmp.rst 7 | mv -f source/tmp.rst source/asciimatics.renderers.rst 8 | PYTHONPATH=.. sphinx-build -b html ./source ./build 9 | -------------------------------------------------------------------------------- /doc/source/_static/custom.css: -------------------------------------------------------------------------------- 1 | .toggle .header { 2 | display: block; 3 | clear: both; 4 | border-color: #c8c8c8 ; 5 | border-style: solid ; 6 | border-width: 1px ; 7 | font-size: x-small ; 8 | font-style: italic ; 9 | margin-left: auto ; 10 | margin-right: auto ; 11 | padding: 3px 2em ; 12 | } 13 | 14 | .toggle .header:after { 15 | content: " [+]"; 16 | } 17 | 18 | .toggle .header.open:after { 19 | content: " [-]"; 20 | } 21 | -------------------------------------------------------------------------------- /doc/source/_templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "!page.html" %} 2 | 3 | {% set css_files = css_files + ["_static/custom.css"] %} 4 | 5 | {% block footer %} 6 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /doc/source/asciimatics.renderers.rst: -------------------------------------------------------------------------------- 1 | asciimatics.renderers package 2 | ============================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | asciimatics.renderers.base module 8 | --------------------------------- 9 | 10 | .. automodule:: asciimatics.renderers.base 11 | :members: 12 | :inherited-members: 13 | :show-inheritance: 14 | 15 | asciimatics.renderers.box module 16 | -------------------------------- 17 | 18 | .. automodule:: asciimatics.renderers.box 19 | :members: 20 | :inherited-members: 21 | :show-inheritance: 22 | 23 | asciimatics.renderers.charts module 24 | ----------------------------------- 25 | 26 | .. automodule:: asciimatics.renderers.charts 27 | :members: 28 | :inherited-members: 29 | :show-inheritance: 30 | 31 | asciimatics.renderers.figlettext module 32 | --------------------------------------- 33 | 34 | .. automodule:: asciimatics.renderers.figlettext 35 | :members: 36 | :inherited-members: 37 | :show-inheritance: 38 | 39 | asciimatics.renderers.fire module 40 | --------------------------------- 41 | 42 | .. automodule:: asciimatics.renderers.fire 43 | :members: 44 | :inherited-members: 45 | :show-inheritance: 46 | 47 | asciimatics.renderers.images module 48 | ----------------------------------- 49 | 50 | .. automodule:: asciimatics.renderers.images 51 | :members: 52 | :inherited-members: 53 | :show-inheritance: 54 | 55 | asciimatics.renderers.kaleidoscope module 56 | ----------------------------------------- 57 | 58 | .. automodule:: asciimatics.renderers.kaleidoscope 59 | :members: 60 | :inherited-members: 61 | :show-inheritance: 62 | 63 | asciimatics.renderers.plasma module 64 | ----------------------------------- 65 | 66 | .. automodule:: asciimatics.renderers.plasma 67 | :members: 68 | :inherited-members: 69 | :show-inheritance: 70 | 71 | asciimatics.renderers.players module 72 | ------------------------------------ 73 | 74 | .. automodule:: asciimatics.renderers.players 75 | :members: 76 | :inherited-members: 77 | :show-inheritance: 78 | 79 | asciimatics.renderers.rainbow module 80 | ------------------------------------ 81 | 82 | .. automodule:: asciimatics.renderers.rainbow 83 | :members: 84 | :inherited-members: 85 | :show-inheritance: 86 | 87 | asciimatics.renderers.rotatedduplicate module 88 | --------------------------------------------- 89 | 90 | .. automodule:: asciimatics.renderers.rotatedduplicate 91 | :members: 92 | :inherited-members: 93 | :show-inheritance: 94 | 95 | asciimatics.renderers.scales module 96 | ----------------------------------- 97 | 98 | .. automodule:: asciimatics.renderers.scales 99 | :members: 100 | :inherited-members: 101 | :show-inheritance: 102 | 103 | asciimatics.renderers.speechbubble module 104 | ----------------------------------------- 105 | 106 | .. automodule:: asciimatics.renderers.speechbubble 107 | :members: 108 | :inherited-members: 109 | :show-inheritance: 110 | 111 | Module contents 112 | --------------- 113 | 114 | .. automodule:: asciimatics.renderers 115 | :members: 116 | :inherited-members: 117 | :show-inheritance: 118 | -------------------------------------------------------------------------------- /doc/source/asciimatics.rst: -------------------------------------------------------------------------------- 1 | asciimatics package 2 | =================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | asciimatics.renderers 11 | asciimatics.widgets 12 | 13 | Submodules 14 | ---------- 15 | 16 | asciimatics.constants module 17 | ---------------------------- 18 | 19 | .. automodule:: asciimatics.constants 20 | :members: 21 | :inherited-members: 22 | :show-inheritance: 23 | 24 | asciimatics.effects module 25 | -------------------------- 26 | 27 | .. automodule:: asciimatics.effects 28 | :members: 29 | :inherited-members: 30 | :show-inheritance: 31 | 32 | asciimatics.event module 33 | ------------------------ 34 | 35 | .. automodule:: asciimatics.event 36 | :members: 37 | :inherited-members: 38 | :show-inheritance: 39 | 40 | asciimatics.exceptions module 41 | ----------------------------- 42 | 43 | .. automodule:: asciimatics.exceptions 44 | :members: 45 | :inherited-members: 46 | :show-inheritance: 47 | 48 | asciimatics.parsers module 49 | -------------------------- 50 | 51 | .. automodule:: asciimatics.parsers 52 | :members: 53 | :inherited-members: 54 | :show-inheritance: 55 | 56 | asciimatics.particles module 57 | ---------------------------- 58 | 59 | .. automodule:: asciimatics.particles 60 | :members: 61 | :inherited-members: 62 | :show-inheritance: 63 | 64 | asciimatics.paths module 65 | ------------------------ 66 | 67 | .. automodule:: asciimatics.paths 68 | :members: 69 | :inherited-members: 70 | :show-inheritance: 71 | 72 | asciimatics.scene module 73 | ------------------------ 74 | 75 | .. automodule:: asciimatics.scene 76 | :members: 77 | :inherited-members: 78 | :show-inheritance: 79 | 80 | asciimatics.screen module 81 | ------------------------- 82 | 83 | .. automodule:: asciimatics.screen 84 | :members: 85 | :inherited-members: 86 | :show-inheritance: 87 | 88 | asciimatics.sprites module 89 | -------------------------- 90 | 91 | .. automodule:: asciimatics.sprites 92 | :members: 93 | :inherited-members: 94 | :show-inheritance: 95 | 96 | asciimatics.strings module 97 | -------------------------- 98 | 99 | .. automodule:: asciimatics.strings 100 | :members: 101 | :inherited-members: 102 | :show-inheritance: 103 | 104 | asciimatics.utilities module 105 | ---------------------------- 106 | 107 | .. automodule:: asciimatics.utilities 108 | :members: 109 | :inherited-members: 110 | :show-inheritance: 111 | 112 | asciimatics.version module 113 | -------------------------- 114 | 115 | .. automodule:: asciimatics.version 116 | :members: 117 | :inherited-members: 118 | :show-inheritance: 119 | 120 | Module contents 121 | --------------- 122 | 123 | .. automodule:: asciimatics 124 | :members: 125 | :inherited-members: 126 | :show-inheritance: 127 | -------------------------------------------------------------------------------- /doc/source/asciimatics.widgets.rst: -------------------------------------------------------------------------------- 1 | asciimatics.widgets package 2 | =========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | asciimatics.widgets.baselistbox module 8 | -------------------------------------- 9 | 10 | .. automodule:: asciimatics.widgets.baselistbox 11 | :members: 12 | :inherited-members: 13 | :show-inheritance: 14 | 15 | asciimatics.widgets.basepicker module 16 | ------------------------------------- 17 | 18 | .. automodule:: asciimatics.widgets.basepicker 19 | :members: 20 | :inherited-members: 21 | :show-inheritance: 22 | 23 | asciimatics.widgets.button module 24 | --------------------------------- 25 | 26 | .. automodule:: asciimatics.widgets.button 27 | :members: 28 | :inherited-members: 29 | :show-inheritance: 30 | 31 | asciimatics.widgets.checkbox module 32 | ----------------------------------- 33 | 34 | .. automodule:: asciimatics.widgets.checkbox 35 | :members: 36 | :inherited-members: 37 | :show-inheritance: 38 | 39 | asciimatics.widgets.datepicker module 40 | ------------------------------------- 41 | 42 | .. automodule:: asciimatics.widgets.datepicker 43 | :members: 44 | :inherited-members: 45 | :show-inheritance: 46 | 47 | asciimatics.widgets.divider module 48 | ---------------------------------- 49 | 50 | .. automodule:: asciimatics.widgets.divider 51 | :members: 52 | :inherited-members: 53 | :show-inheritance: 54 | 55 | asciimatics.widgets.dropdownlist module 56 | --------------------------------------- 57 | 58 | .. automodule:: asciimatics.widgets.dropdownlist 59 | :members: 60 | :inherited-members: 61 | :show-inheritance: 62 | 63 | asciimatics.widgets.filebrowser module 64 | -------------------------------------- 65 | 66 | .. automodule:: asciimatics.widgets.filebrowser 67 | :members: 68 | :inherited-members: 69 | :show-inheritance: 70 | 71 | asciimatics.widgets.frame module 72 | -------------------------------- 73 | 74 | .. automodule:: asciimatics.widgets.frame 75 | :members: 76 | :inherited-members: 77 | :show-inheritance: 78 | 79 | asciimatics.widgets.label module 80 | -------------------------------- 81 | 82 | .. automodule:: asciimatics.widgets.label 83 | :members: 84 | :inherited-members: 85 | :show-inheritance: 86 | 87 | asciimatics.widgets.layout module 88 | --------------------------------- 89 | 90 | .. automodule:: asciimatics.widgets.layout 91 | :members: 92 | :inherited-members: 93 | :show-inheritance: 94 | 95 | asciimatics.widgets.listbox module 96 | ---------------------------------- 97 | 98 | .. automodule:: asciimatics.widgets.listbox 99 | :members: 100 | :inherited-members: 101 | :show-inheritance: 102 | 103 | asciimatics.widgets.multicolumnlistbox module 104 | --------------------------------------------- 105 | 106 | .. automodule:: asciimatics.widgets.multicolumnlistbox 107 | :members: 108 | :inherited-members: 109 | :show-inheritance: 110 | 111 | asciimatics.widgets.popupdialog module 112 | -------------------------------------- 113 | 114 | .. automodule:: asciimatics.widgets.popupdialog 115 | :members: 116 | :inherited-members: 117 | :show-inheritance: 118 | 119 | asciimatics.widgets.popupmenu module 120 | ------------------------------------ 121 | 122 | .. automodule:: asciimatics.widgets.popupmenu 123 | :members: 124 | :inherited-members: 125 | :show-inheritance: 126 | 127 | asciimatics.widgets.radiobuttons module 128 | --------------------------------------- 129 | 130 | .. automodule:: asciimatics.widgets.radiobuttons 131 | :members: 132 | :inherited-members: 133 | :show-inheritance: 134 | 135 | asciimatics.widgets.scrollbar module 136 | ------------------------------------ 137 | 138 | .. automodule:: asciimatics.widgets.scrollbar 139 | :members: 140 | :inherited-members: 141 | :show-inheritance: 142 | 143 | asciimatics.widgets.temppopup module 144 | ------------------------------------ 145 | 146 | .. automodule:: asciimatics.widgets.temppopup 147 | :members: 148 | :inherited-members: 149 | :show-inheritance: 150 | 151 | asciimatics.widgets.text module 152 | ------------------------------- 153 | 154 | .. automodule:: asciimatics.widgets.text 155 | :members: 156 | :inherited-members: 157 | :show-inheritance: 158 | 159 | asciimatics.widgets.textbox module 160 | ---------------------------------- 161 | 162 | .. automodule:: asciimatics.widgets.textbox 163 | :members: 164 | :inherited-members: 165 | :show-inheritance: 166 | 167 | asciimatics.widgets.timepicker module 168 | ------------------------------------- 169 | 170 | .. automodule:: asciimatics.widgets.timepicker 171 | :members: 172 | :inherited-members: 173 | :show-inheritance: 174 | 175 | asciimatics.widgets.utilities module 176 | ------------------------------------ 177 | 178 | .. automodule:: asciimatics.widgets.utilities 179 | :members: 180 | :inherited-members: 181 | :show-inheritance: 182 | 183 | asciimatics.widgets.verticaldivider module 184 | ------------------------------------------ 185 | 186 | .. automodule:: asciimatics.widgets.verticaldivider 187 | :members: 188 | :inherited-members: 189 | :show-inheritance: 190 | 191 | asciimatics.widgets.widget module 192 | --------------------------------- 193 | 194 | .. automodule:: asciimatics.widgets.widget 195 | :members: 196 | :inherited-members: 197 | :show-inheritance: 198 | 199 | Module contents 200 | --------------- 201 | 202 | .. automodule:: asciimatics.widgets 203 | :members: 204 | :inherited-members: 205 | :show-inheritance: 206 | -------------------------------------------------------------------------------- /doc/source/contacts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/doc/source/contacts.png -------------------------------------------------------------------------------- /doc/source/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Getting started 5 | --------------- 6 | 7 | So you want to join in? Great! There's a few ground rules... 8 | 9 | #. Before you do anything else, read up on the design. 10 | 11 | * You should find all general background in these 4 classes: :py:obj:`.Screen`, 12 | :py:obj:`.Scene`, :py:obj:`.Effect` and :py:obj:`~renderers.Renderer`. 13 | * You will find more details on TUIs in these 3 classes: :py:obj:`.Frame`, :py:obj:`.Layout` 14 | and :py:obj:`.Widget`. 15 | 16 | #. If writing a new Effect, consider why it can't be handled by a combination of a new 17 | Renderer and the :py:obj:`.Print` Effect. For example, dynamic Effects such as 18 | :py:obj:`.Snow` depend on the current Screen state to render each new image. 19 | #. Go the extra yard. This project started on a whim to share the joy of someone starting out 20 | programming back in the 1980s. How do you sustain that joy? Not just by writing code that 21 | works, but by writing code that other programmers will admire. 22 | #. Make sure that your code is `PEP-8 `_ compliant. 23 | Tools such as flake8 and pylint or editors like pycharm really help here. 24 | #. Please run the existing unit tests against your new code to make sure that it still works 25 | as expected. I normally use nosetests to do this. In addition, if you are adding significant 26 | extra function, please write some new tests for your code. 27 | 28 | If you're not quite sure about something, feel free to join us at 29 | https://gitter.im/asciimatics/Lobby and share your ideas. 30 | 31 | When you've got something you're happy with, please feel free to submit a pull request at 32 | https://github.com/peterbrittain/asciimatics/issues. 33 | 34 | Building The Documentation 35 | -------------------------- 36 | 37 | Install the dependencies and build the documentation with: 38 | 39 | .. code-block:: bash 40 | 41 | $ pip install -r requirements/dev.txt 42 | $ cd doc 43 | $ ./build.sh 44 | 45 | You can then view your new shiny documentation in the ``build`` folder. 46 | 47 | Running The Tests 48 | ------------------ 49 | 50 | Install the dependencies and run the tests with the following: 51 | 52 | .. code-block:: bash 53 | 54 | $ pip install -r requirements/dev.txt 55 | $ python -m unittest 56 | 57 | On most systems this will avoid running tests that require a Linux TTY. If you are making changes to the 58 | Screen, you must also run the TTY tests. You can force that on a Linux box using the following: 59 | 60 | .. code-block:: bash 61 | 62 | $ FORCE_TTY=Y python -m unittest 63 | 64 | The reason for this split is that you only typically get a TTY on a live interactive connection to your 65 | terminal. This means you should always be able to run the full suite manually. However, many CI systems 66 | do not provide a valid TTY and so these tests regularly fail on various build servers. Fortunately, Travis 67 | provides a working TTY and so we enable the full suite of tests on any check-in to master. 68 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. asciimatics documentation master file, created by 2 | sphinx-quickstart on Fri Apr 3 17:57:45 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to asciimatics' documentation! 7 | ====================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | intro 15 | contributing 16 | io 17 | rendering 18 | animation 19 | widgets 20 | troubleshooting 21 | modules 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /doc/source/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Asciimatics is a package to help people create simple ASCII animations on any 5 | platform. It is licensed under the Apache Software Foundation License 2.0. 6 | 7 | 8 | Why? 9 | ---- 10 | 11 | Why not? It brings a little joy to anyone who was programming in the 80s... 12 | Oh and it provides a single cross-platform Python class to do all the low-level 13 | console function you could ask for, including: 14 | 15 | * Coloured/styled text - including 256 colour terminals 16 | * Cursor positioning 17 | * Keyboard input (without blocking or echoing) 18 | * Mouse input (terminal permitting) 19 | * Detecting and handling when the console resizes 20 | * Screen scraping 21 | 22 | In addition, it provides some simple, high-level APIs to provide more complex 23 | features including: 24 | 25 | * Anti-aliased ASCII line-drawing 26 | * Image to ASCII conversion - including JPEG and GIF formats 27 | * Many animation effects - e.g. sprites, particle systems, banners, etc. 28 | * Various widgets for text UIs - e.g. buttons, text boxes, radio buttons, etc. 29 | 30 | Currently this API has been proven to work on CentOS 6 & 7, Raspbian (i.e. 31 | Debian wheezy), Ubuntu 14.04, Windows 7, 8 & 10 and OSX 10.11, though it should 32 | also work for any other platform that provides a working curses implementation. 33 | 34 | 35 | Installation 36 | ------------ 37 | 38 | Asciimatics supports Python versions 2 & 3. For a list of the precise 39 | list of tested versions, see 40 | `here `__. 41 | 42 | To install asciimatics, simply install with `pip`. You can get it from 43 | `here `_ and then just run: 44 | 45 | .. code-block:: bash 46 | 47 | $ pip install asciimatics 48 | 49 | This should install all your dependencies for you. If you don't use pip 50 | or it fails to install them, you can install the dependencies directly 51 | using the packages listed in `requirements.txt 52 | `_. 53 | Additionally, Windows users will need to install `pywin32`. 54 | 55 | Quick start guide 56 | ----------------- 57 | 58 | Once you have installed asciimatics as per the instructions above, simply 59 | create a :py:obj:`.Screen`, put together a :py:obj:`.Scene` using some 60 | :py:obj:`.Effect` objects and then get the Screen to play it. An Effect 61 | will typically need to display some pre-formatted text. This is usually 62 | provided by a :py:obj:`~renderers.Renderer`. For example: 63 | 64 | .. code-block:: python 65 | 66 | from asciimatics.screen import Screen 67 | from asciimatics.scene import Scene 68 | from asciimatics.effects import Cycle, Stars 69 | from asciimatics.renderers import FigletText 70 | 71 | def demo(screen): 72 | effects = [ 73 | Cycle( 74 | screen, 75 | FigletText("ASCIIMATICS", font='big'), 76 | screen.height // 2 - 8), 77 | Cycle( 78 | screen, 79 | FigletText("ROCKS!", font='big'), 80 | screen.height // 2 + 3), 81 | Stars(screen, (screen.width + screen.height) // 2) 82 | ] 83 | screen.play([Scene(effects, 500)]) 84 | 85 | Screen.wrapper(demo) 86 | 87 | -------------------------------------------------------------------------------- /doc/source/mac_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/doc/source/mac_settings.png -------------------------------------------------------------------------------- /doc/source/modules.rst: -------------------------------------------------------------------------------- 1 | asciimatics 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | asciimatics 8 | -------------------------------------------------------------------------------- /doc/source/rendering.rst: -------------------------------------------------------------------------------- 1 | Advanced Output 2 | =============== 3 | 4 | Rendering 5 | --------- 6 | When you want to create an animation, you typically need a sequence of multi-coloured text images to create 7 | the desired effect. This is where a :py:obj:`~renderers.Renderer` object comes into play. 8 | 9 | A Renderer is simply an object that will return one or more text strings and associated colour maps in a 10 | format that is suitable for display using the :py:meth:`~Screen.paint` method. This collation of text string 11 | and colour map is referred to as the rendered text. It might vary in complexity from a single, monochrome 12 | string through to many frames from an ASCII rendition of a colour video or animated GIF. 13 | 14 | All renderers must implement the API of the abstract :py:obj:`~renderers.Renderer` class, however there are 2 basic 15 | variants. 16 | 17 | 1. The :py:obj:`~renderers.StaticRenderer` creates pre-rendered sequences of rendered text. They are usually 18 | initialized with some static content that can be calculated entirely in advance. For example: 19 | 20 | .. code-block:: python 21 | 22 | # Pre-render ASCIIMATICS using the big Figlet font 23 | renderer = FigletText("ASCIIMATICS", font='big') 24 | 25 | 2. The :py:obj:`~renderers.DynamicRenderer` creates the rendered text on demand. They are typically dependent on the 26 | state of the program or the Screen when rendered. For example: 27 | 28 | .. code-block:: python 29 | 30 | # Render a bar chart with random bars formed of equals signs. 31 | def fn(): 32 | return randint(0, 40) 33 | renderer = BarChart(10, 40, [fn, fn], char='=') 34 | 35 | Once you have a Renderer you can extract the next text to be displayed by calling 36 | :py:meth:`~asciimatics.renderers.Renderer.rendered_text`. This will cycle round the static rendered text 37 | sequentially or just create the new dynamic rendered text and return it (for use in the Screen paint method). 38 | Generally speaking, rather than doing this directly with the Screen, you will typically want to use an Effect 39 | to handle this. See :ref:`animation-ref` for more details. 40 | 41 | There are many built-in renderers provided by asciimatics. The following section gives you a quick run 42 | through of each one by area. For more examples of Renderers, see the asciimatics samples folder. 43 | 44 | Image to ASCII 45 | ~~~~~~~~~~~~~~ 46 | Asciimatics provides 2 ways to convert image files (e.g. JPEGs, GIFs, etc) into a text equivalent: 47 | 48 | * :py:obj:`~renderers.ImageFile` - converts the image to grey-scale text. 49 | * :py:obj:`~renderers.ColourImageFile` - converts the image to full colour text (using all the screen's palette). 50 | 51 | Both support animated GIFs and will cycle through each image when drawn. 52 | 53 | Animated objects 54 | ~~~~~~~~~~~~~~~~ 55 | Asciimatics provides the following renderers for more complex animation effects. 56 | 57 | * :py:obj:`~renderers.BarChart` - draws a horizontal bar chart for a set of data (that may be dynamic in nature). 58 | * :py:obj:`~renderers.Fire` - simulates a burning fire. 59 | * :py:obj:`~renderers.Plasma` - simulates an animated "plasma" (think lava lamp in 2-D). 60 | * :py:obj:`~renderers.Kaleidoscope` - simulates a 2 mirror kaleidoscope. 61 | 62 | Text/colour manipulation 63 | ~~~~~~~~~~~~~~~~~~~~~~~~ 64 | The following renderers provide some simple text and colour manipulation. 65 | 66 | * :py:obj:`~renderers.FigletText` - draws large FIGlet text 67 | * :py:obj:`~renderers.Rainbow` - recolours the specified Renderer in as a Rainbow 68 | * :py:obj:`~renderers.RotatedDuplicate` - creates a rotated duplicate of the specified Renderer. 69 | 70 | Boxes 71 | ~~~~~ 72 | The following renderers provide some simple boxes and boxed text. 73 | 74 | * :py:obj:`~renderers.Box` - draws a simple box. 75 | * :py:obj:`~renderers.SpeechBubble` - draws a speech bubble around some specified text. 76 | 77 | Static colour codes 78 | ------------------- 79 | When creating static rendered output, it can be helpful to define your colours inline with the rest of your 80 | text. The :py:obj:`~renderers.StaticRenderer` class supports this through the ${n1,n2,n3} escape sequence, where `n*` 81 | are digits. 82 | 83 | Formally this sequence is defined an escape sequence ${c,a,b} which changes the current colour tuple to be 84 | foreground colour 'c', attribute 'a' and background colour 'b' (using the values of the Screen COLOUR and ATTR 85 | constants). The attribute and background fields are optional. 86 | 87 | These tuples create a colour map (for input into :py:meth:`~Screen.paint`) and so the colours will reset to 88 | the defaults passed into `paint()` at the start of each line. For example, this code will produce a simple 89 | Xmas tree with coloured baubles when rendered (using green as the default colour). 90 | 91 | .. code-block:: python 92 | 93 | StaticRenderer(images=[r""" 94 | ${3,1}* 95 | / \ 96 | /${1}o${2} \ 97 | /_ _\ 98 | / \${4}b 99 | / \ 100 | / ${1}o${2} \ 101 | /__ __\ 102 | ${1}d${2} / ${4}o${2} \ 103 | / \ 104 | / ${4}o ${1}o${2}.\ 105 | /___________\ 106 | ${3}||| 107 | ${3}||| 108 | """]) 109 | 110 | Experimental 111 | ------------ 112 | A Renderer can also return a plain text string representation of the next rendered text image. This means 113 | they can be used outside of a Screen. For example: 114 | 115 | .. code-block:: python 116 | 117 | # Print a bar chart with random bars formed of equals signs. 118 | def fn(): 119 | return randint(0, 40) 120 | renderer = BarChart(10, 40, [fn, fn], char='=') 121 | print(renderer) 122 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | packages = asciimatics 3 | check_untyped_defs = True 4 | 5 | [mypy-wcwidth.*] 6 | ignore_missing_imports = True 7 | 8 | [mypy-pyfiglet.*] 9 | ignore_missing_imports = True 10 | 11 | [mypy-asciimatics.version] 12 | ignore_missing_imports = True 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", 'setuptools_scm'] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | version_file = "asciimatics/version.py" 7 | local_scheme = "no-local-version" 8 | 9 | [project] 10 | dynamic = ['version', 'readme'] 11 | name = 'asciimatics' 12 | description = 'A cross-platform package to create text UIs and ASCII animations' 13 | maintainers = [ 14 | {name = 'Peter Brittain', email = 'peter.brittain.os@gmail.com'}, 15 | ] 16 | license = 'Apache-2.0' 17 | license-files = ['LICENSE'] 18 | classifiers = [ 19 | 'Development Status :: 5 - Production/Stable', 20 | 'Environment :: Console', 21 | 'Environment :: Console :: Curses', 22 | 'Intended Audience :: Developers', 23 | 'Topic :: Text Processing :: General', 24 | 'Operating System :: OS Independent', 25 | 'Programming Language :: Python', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.9', 28 | 'Programming Language :: Python :: 3.10', 29 | 'Programming Language :: Python :: 3.11', 30 | 'Programming Language :: Python :: Implementation :: CPython', 31 | 'Programming Language :: Python :: Implementation :: PyPy', 32 | 'Topic :: Software Development :: User Interfaces', 33 | 'Topic :: Terminals', 34 | ] 35 | keywords = [ 36 | 'ascii', 37 | 'ansi', 38 | 'art', 39 | 'titles', 40 | 'animation', 41 | 'curses', 42 | 'ncurses', 43 | 'windows', 44 | 'xterm', 45 | 'mouse', 46 | 'keyboard', 47 | 'terminal', 48 | 'tty', 49 | 'color', 50 | 'colour', 51 | 'crossplatform', 52 | 'console', 53 | ] 54 | dependencies = [ 55 | 'pyfiglet >= 0.7.2', 56 | 'Pillow >= 2.7.0', 57 | 'wcwidth', 58 | "pywin32 >= 1.0; platform_system=='Windows'", 59 | ] 60 | requires-python = ">= 3.8" 61 | 62 | [project.urls] 63 | Repository = 'https://github.com/peterbrittain/asciimatics' 64 | 65 | [tool.yapf] 66 | COLUMN_LIMIT = 110 67 | SPLIT_ALL_TOP_LEVEL_COMMA_SEPARATED_VALUES = true 68 | INDENT_DICTIONARY_VALUE = true 69 | ALLOW_SPLIT_BEFORE_DICT_VALUE = false 70 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/base.txt 2 | Pillow 3 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | wcwidth 2 | pyfiglet >= 0.7.2 3 | setuptools_scm 4 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | Pillow 3 | coveralls 4 | sphinx 5 | urllib3 6 | -------------------------------------------------------------------------------- /samples/256colour.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import Print, Clock 4 | from asciimatics.renderers import FigletText, Rainbow 5 | from asciimatics.scene import Scene 6 | from asciimatics.screen import Screen 7 | from asciimatics.exceptions import ResizeScreenError 8 | import sys 9 | 10 | 11 | def demo(screen): 12 | effects = [ 13 | Print(screen, Rainbow(screen, FigletText("256 colours")), 14 | y=screen.height//2 - 8), 15 | Print(screen, Rainbow(screen, FigletText("for xterm users")), 16 | y=screen.height//2 + 3), 17 | Clock(screen, screen.width//2, screen.height//2, screen.height//2), 18 | ] 19 | screen.play([Scene(effects, -1)], stop_on_resize=True) 20 | 21 | 22 | while True: 23 | try: 24 | Screen.wrapper(demo) 25 | sys.exit(0) 26 | except ResizeScreenError: 27 | pass 28 | -------------------------------------------------------------------------------- /samples/bars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.constants import SINGLE_LINE, DOUBLE_LINE, ASCII_LINE 4 | from asciimatics.effects import Print 5 | from asciimatics.exceptions import ResizeScreenError 6 | from asciimatics.renderers import BarChart, VBarChart, FigletText 7 | from asciimatics.scene import Scene 8 | from asciimatics.screen import Screen 9 | from asciimatics.utilities import BoxTool 10 | import sys 11 | import math 12 | import time 13 | from random import randint 14 | 15 | 16 | def fn(): 17 | return randint(0, 10) 18 | 19 | def fn2(): 20 | return randint(0, 6) 21 | 22 | 23 | def wv(x): 24 | return lambda: 1 + math.sin(math.pi * (2*time.time()+x) / 5) 25 | 26 | 27 | def demo(screen): 28 | scenes = [] 29 | if screen.width != 132 or screen.height != 24: 30 | effects = [ 31 | Print(screen, FigletText("Resize to 132x24"), 32 | y=screen.height//2-3), 33 | ] 34 | else: 35 | # Horizontal Charts 36 | hchart1 = BarChart(9, 22, [fn, fn], char="═", 37 | gradient=[(7, Screen.COLOUR_GREEN), 38 | (9, Screen.COLOUR_YELLOW), 39 | (10, Screen.COLOUR_RED)], 40 | keys=["one", "two"], gap=1, 41 | ) 42 | hchart2 = BarChart(10, 25, [wv(1), wv(3), wv(5), wv(7), wv(9)], 43 | colour=Screen.COLOUR_GREEN, axes=BarChart.BOTH, scale=2.0) 44 | hchart2.border_style = ASCII_LINE 45 | hchart2.axes_style = ASCII_LINE 46 | hchart3 = BarChart(10, 40, [wv(1), wv(2), wv(3), wv(4), wv(5), wv(7), wv(8), wv(9)], 47 | colour=[c for c in range(1, 8)], bg=[c for c in range(1, 8)], 48 | scale=2.0, axes=BarChart.X_AXIS, intervals=0.5, labels=True, border=False) 49 | hchart4 = BarChart(7, 30, [lambda: time.time() * 10 % 101], 50 | gradient=[ 51 | (33, Screen.COLOUR_RED, Screen.COLOUR_RED), 52 | (66, Screen.COLOUR_YELLOW, Screen.COLOUR_YELLOW), 53 | (100, Screen.COLOUR_WHITE, Screen.COLOUR_WHITE), 54 | ] if screen.colours < 256 else [ 55 | (10, 234, 234), (20, 236, 236), (30, 238, 238), 56 | (40, 240, 240), (50, 242, 242), (60, 244, 244), 57 | (70, 246, 246), (80, 248, 248), (90, 250, 250), 58 | (100, 252, 252) 59 | ], 60 | char=">", scale=100.0, labels=True, axes=BarChart.X_AXIS) 61 | hchart4.border_style = SINGLE_LINE 62 | 63 | # Vertical Charts 64 | vchart1 = VBarChart(12, 21, [fn2, fn2], char="═", 65 | gradient=[(3, Screen.COLOUR_GREEN), 66 | (4, Screen.COLOUR_YELLOW), 67 | (5, Screen.COLOUR_RED)], 68 | keys=["one", "two"], 69 | ) 70 | vchart2 = VBarChart(12, 17, [wv(1), wv(3), wv(5), wv(7), wv(9)], 71 | colour=Screen.COLOUR_GREEN, axes=BarChart.BOTH, scale=2.0, gap=0) 72 | vchart2.border_style = ASCII_LINE 73 | vchart2.axes_style = ASCII_LINE 74 | vchart3 = VBarChart(12, 39, [wv(1), wv(2), wv(3), wv(4), wv(5), wv(7), wv(8), wv(9)], 75 | colour=[c for c in range(1, 8)], bg=[c for c in range(1, 8)], gap=0, 76 | scale=2.0, axes=BarChart.Y_AXIS, intervals=0.5, labels=True, border=False) 77 | vchart4 = VBarChart(12, 16, [lambda: time.time() * 10 % 101], 78 | gradient=[ 79 | (33, Screen.COLOUR_RED, Screen.COLOUR_RED), 80 | (66, Screen.COLOUR_YELLOW, Screen.COLOUR_YELLOW), 81 | (100, Screen.COLOUR_WHITE, Screen.COLOUR_WHITE), 82 | ] if screen.colours < 256 else [ 83 | (10, 234, 234), (20, 236, 236), (30, 238, 238), 84 | (40, 240, 240), (50, 242, 242), (60, 244, 244), 85 | (70, 246, 246), (80, 248, 248), (90, 250, 250), 86 | (100, 252, 252) 87 | ], 88 | char=">", scale=100.0, labels=True, axes=VBarChart.Y_AXIS) 89 | vchart4.border_style = SINGLE_LINE 90 | 91 | effects = [ 92 | Print(screen, hchart1, x=1, y=1, transparent=False, speed=2), 93 | Print(screen, hchart2, x=25, y=1, transparent=False, speed=2), 94 | Print(screen, hchart3, x=52, y=1, transparent=False, speed=2), 95 | Print(screen, hchart4, x=96, y=2, transparent=False, speed=2), 96 | 97 | Print(screen, vchart1, x=2, y=12, transparent=False, speed=2), 98 | Print(screen, vchart2, x=29, y=12, transparent=False, speed=2), 99 | Print(screen, vchart3, x=52, y=12, transparent=False, speed=2), 100 | Print(screen, vchart4, x=103, y=12, transparent=False, speed=2), 101 | ] 102 | 103 | scenes.append(Scene(effects, -1)) 104 | screen.play(scenes, stop_on_resize=True) 105 | 106 | 107 | while True: 108 | try: 109 | Screen.wrapper(demo) 110 | sys.exit(0) 111 | except ResizeScreenError: 112 | pass 113 | -------------------------------------------------------------------------------- /samples/bg_colours.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import Wipe, Print 4 | from asciimatics.renderers import FigletText, SpeechBubble 5 | from asciimatics.scene import Scene 6 | from asciimatics.screen import Screen 7 | from asciimatics.exceptions import ResizeScreenError 8 | import sys 9 | 10 | 11 | def demo(screen): 12 | scenes = [] 13 | 14 | for bg, name in [ 15 | (Screen.COLOUR_DEFAULT, "DEFAULT"), 16 | (Screen.COLOUR_RED, "RED"), 17 | (Screen.COLOUR_YELLOW, "YELLOW"), 18 | (Screen.COLOUR_GREEN, "GREEN"), 19 | (Screen.COLOUR_CYAN, "CYAN"), 20 | (Screen.COLOUR_BLUE, "BLUE"), 21 | (Screen.COLOUR_MAGENTA, "MAGENTA"), 22 | (Screen.COLOUR_WHITE, "WHITE")]: 23 | effects = [ 24 | Wipe(screen, bg=bg, stop_frame=screen.height * 2 + 30), 25 | Print(screen, FigletText(name, "epic"), screen.height // 2 - 4, 26 | colour=bg if bg == Screen.COLOUR_DEFAULT else 7 - bg, 27 | bg=bg, 28 | start_frame=screen.height * 2), 29 | Print(screen, 30 | SpeechBubble("Testing background colours - press X to exit"), 31 | screen.height-5, 32 | speed=1, transparent=False) 33 | ] 34 | scenes.append(Scene(effects, 0, clear=False)) 35 | 36 | screen.play(scenes, stop_on_resize=True) 37 | 38 | 39 | if __name__ == "__main__": 40 | while True: 41 | try: 42 | Screen.wrapper(demo) 43 | sys.exit(0) 44 | except ResizeScreenError: 45 | pass 46 | -------------------------------------------------------------------------------- /samples/cogs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import Cog, Print 4 | from asciimatics.renderers import FigletText 5 | from asciimatics.scene import Scene 6 | from asciimatics.screen import Screen 7 | from asciimatics.exceptions import ResizeScreenError 8 | import sys 9 | 10 | 11 | def demo(screen): 12 | # Typical terminals are 80x24 on UNIX and 80x25 on Windows 13 | if screen.width != 80 or screen.height not in (24, 25): 14 | effects = [ 15 | Print(screen, FigletText("Resize to 80x24"), 16 | y=screen.height//2-3), 17 | ] 18 | else: 19 | effects = [ 20 | Cog(screen, 20, 10, 10), 21 | Cog(screen, 60, 30, 15, direction=-1), 22 | Print(screen, FigletText("ascii", font="smkeyboard"), 23 | attr=Screen.A_BOLD, x=47, y=3, start_frame=50), 24 | Print(screen, FigletText("matics", font="smkeyboard"), 25 | attr=Screen.A_BOLD, x=45, y=7, start_frame=100), 26 | Print(screen, FigletText("by Peter Brittain", font="term"), 27 | x=8, y=22, start_frame=150) 28 | ] 29 | screen.play([Scene(effects, -1)], stop_on_resize=True) 30 | 31 | 32 | while True: 33 | try: 34 | Screen.wrapper(demo) 35 | sys.exit(0) 36 | except ResizeScreenError: 37 | pass 38 | -------------------------------------------------------------------------------- /samples/colour_globe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/samples/colour_globe.gif -------------------------------------------------------------------------------- /samples/credits.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from pyfiglet import Figlet 5 | 6 | from asciimatics.effects import Scroll, Mirage, Wipe, Cycle, Matrix, \ 7 | BannerText, Stars, Print 8 | from asciimatics.particles import DropScreen 9 | from asciimatics.renderers import FigletText, SpeechBubble, Rainbow, Fire 10 | from asciimatics.scene import Scene 11 | from asciimatics.screen import Screen 12 | from asciimatics.exceptions import ResizeScreenError 13 | 14 | 15 | def _credits(screen): 16 | scenes = [] 17 | 18 | text = Figlet(font="banner", width=200).renderText("ASCIIMATICS") 19 | width = max([len(x) for x in text.split("\n")]) 20 | 21 | effects = [ 22 | Print(screen, 23 | Fire(screen.height, 80, text, 0.4, 40, screen.colours), 24 | 0, 25 | speed=1, 26 | transparent=False), 27 | Print(screen, 28 | FigletText("ASCIIMATICS", "banner"), 29 | screen.height - 9, x=(screen.width - width) // 2 + 1, 30 | colour=Screen.COLOUR_BLACK, 31 | bg=Screen.COLOUR_BLACK, 32 | speed=1), 33 | Print(screen, 34 | FigletText("ASCIIMATICS", "banner"), 35 | screen.height - 9, 36 | colour=Screen.COLOUR_WHITE, 37 | bg=Screen.COLOUR_WHITE, 38 | speed=1), 39 | ] 40 | scenes.append(Scene(effects, 100)) 41 | 42 | effects = [ 43 | Matrix(screen, stop_frame=200), 44 | Mirage( 45 | screen, 46 | FigletText("Asciimatics"), 47 | screen.height // 2 - 3, 48 | Screen.COLOUR_GREEN, 49 | start_frame=100, 50 | stop_frame=200), 51 | Wipe(screen, start_frame=150), 52 | Cycle( 53 | screen, 54 | FigletText("Asciimatics"), 55 | screen.height // 2 - 3, 56 | start_frame=200) 57 | ] 58 | scenes.append(Scene(effects, 250, clear=False)) 59 | 60 | effects = [ 61 | BannerText( 62 | screen, 63 | Rainbow(screen, FigletText( 64 | "Reliving the 80s in glorious ASCII text...", font='slant')), 65 | screen.height // 2 - 3, 66 | Screen.COLOUR_GREEN) 67 | ] 68 | scenes.append(Scene(effects)) 69 | 70 | effects = [ 71 | Scroll(screen, 3), 72 | Mirage( 73 | screen, 74 | FigletText("Conceived and"), 75 | screen.height, 76 | Screen.COLOUR_GREEN), 77 | Mirage( 78 | screen, 79 | FigletText("written by:"), 80 | screen.height + 8, 81 | Screen.COLOUR_GREEN), 82 | Mirage( 83 | screen, 84 | FigletText("Peter Brittain"), 85 | screen.height + 16, 86 | Screen.COLOUR_GREEN) 87 | ] 88 | scenes.append(Scene(effects, (screen.height + 24) * 3)) 89 | 90 | colours = [Screen.COLOUR_RED, Screen.COLOUR_GREEN,] 91 | contributors = [ 92 | "Cory Benfield", 93 | "Bryce Guinta", 94 | "Aman Orazaev", 95 | "Daniel Kerr", 96 | "Dylan Janeke", 97 | "ianadeem", 98 | "Scott Mudge", 99 | "Luke Murphy", 100 | "mronkain", 101 | "Dougal Sutherland", 102 | "Kirtan Sakariya", 103 | "Jesse Lieberg", 104 | "Erik Doffagne", 105 | "Noah Ginsburg", 106 | "Davidy22", 107 | "Christopher Trudeau", 108 | "Beniamin Kalinowski" 109 | ] 110 | 111 | effects = [ 112 | Scroll(screen, 3), 113 | Mirage( 114 | screen, 115 | FigletText("With help from:"), 116 | screen.height, 117 | Screen.COLOUR_GREEN, 118 | ) 119 | ] 120 | 121 | pos = 8 122 | for i, name in enumerate(contributors): 123 | effects.append( 124 | Mirage( 125 | screen, 126 | FigletText(name), 127 | screen.height + pos, 128 | colours[i % len(colours)], 129 | ) 130 | ) 131 | 132 | pos += 8 133 | scenes.append(Scene(effects, (screen.height + pos) * 3)) 134 | 135 | effects = [ 136 | Cycle( 137 | screen, 138 | FigletText("ASCIIMATICS", font='big'), 139 | screen.height // 2 - 8, 140 | stop_frame=100), 141 | Cycle( 142 | screen, 143 | FigletText("ROCKS!", font='big'), 144 | screen.height // 2 + 3, 145 | stop_frame=100), 146 | Stars(screen, (screen.width + screen.height) // 2, stop_frame=100), 147 | DropScreen(screen, 200, start_frame=100) 148 | ] 149 | scenes.append(Scene(effects, 300)) 150 | 151 | effects = [ 152 | Print(screen, 153 | SpeechBubble("Press 'X' to exit."), screen.height // 2 - 1, attr=Screen.A_BOLD) 154 | ] 155 | scenes.append(Scene(effects, -1)) 156 | 157 | screen.play(scenes, stop_on_resize=True) 158 | 159 | 160 | if __name__ == "__main__": 161 | while True: 162 | try: 163 | Screen.wrapper(_credits) 164 | sys.exit(0) 165 | except ResizeScreenError: 166 | pass 167 | -------------------------------------------------------------------------------- /samples/fire.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.renderers import FigletText, Fire 4 | from asciimatics.scene import Scene 5 | from asciimatics.screen import Screen 6 | from asciimatics.effects import Print 7 | from asciimatics.exceptions import ResizeScreenError 8 | from pyfiglet import Figlet 9 | import sys 10 | 11 | 12 | def demo(screen): 13 | scenes = [] 14 | 15 | effects = [ 16 | Print(screen, 17 | Fire(screen.height, 80, "*" * 70, 0.8, 60, screen.colours, 18 | bg=screen.colours >= 256), 19 | 0, 20 | speed=1, 21 | transparent=False), 22 | Print(screen, 23 | FigletText("Help!", "banner3"), 24 | (screen.height - 4) // 2, 25 | colour=Screen.COLOUR_BLACK, 26 | speed=1, 27 | stop_frame=30), 28 | Print(screen, 29 | FigletText("I'm", "banner3"), 30 | (screen.height - 4) // 2, 31 | colour=Screen.COLOUR_BLACK, 32 | speed=1, 33 | start_frame=30, 34 | stop_frame=50), 35 | Print(screen, 36 | FigletText("on", "banner3"), 37 | (screen.height - 4) // 2, 38 | colour=Screen.COLOUR_BLACK, 39 | speed=1, 40 | start_frame=50, 41 | stop_frame=70), 42 | Print(screen, 43 | FigletText("Fire!", "banner3"), 44 | (screen.height - 4) // 2, 45 | colour=Screen.COLOUR_BLACK, 46 | speed=1, 47 | start_frame=70), 48 | ] 49 | scenes.append(Scene(effects, 100)) 50 | 51 | text = Figlet(font="banner", width=200).renderText("ASCIIMATICS") 52 | width = max([len(x) for x in text.split("\n")]) 53 | 54 | effects = [ 55 | Print(screen, 56 | Fire(screen.height, 80, text, 0.4, 40, screen.colours), 57 | 0, 58 | speed=1, 59 | transparent=False), 60 | Print(screen, 61 | FigletText("ASCIIMATICS", "banner"), 62 | screen.height - 9, x=(screen.width - width) // 2 + 1, 63 | colour=Screen.COLOUR_BLACK, 64 | bg=Screen.COLOUR_BLACK, 65 | speed=1), 66 | Print(screen, 67 | FigletText("ASCIIMATICS", "banner"), 68 | screen.height - 9, 69 | colour=Screen.COLOUR_WHITE, 70 | bg=Screen.COLOUR_WHITE, 71 | speed=1), 72 | ] 73 | scenes.append(Scene(effects, -1)) 74 | 75 | screen.play(scenes, stop_on_resize=True) 76 | 77 | 78 | if __name__ == "__main__": 79 | while True: 80 | try: 81 | Screen.wrapper(demo) 82 | sys.exit(0) 83 | except ResizeScreenError: 84 | pass 85 | -------------------------------------------------------------------------------- /samples/fireworks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import Stars, Print 4 | from asciimatics.particles import RingFirework, SerpentFirework, StarFirework, \ 5 | PalmFirework 6 | from asciimatics.renderers import SpeechBubble, FigletText, Rainbow 7 | from asciimatics.scene import Scene 8 | from asciimatics.screen import Screen 9 | from asciimatics.exceptions import ResizeScreenError 10 | from random import randint, choice 11 | import sys 12 | 13 | 14 | def demo(screen): 15 | scenes = [] 16 | effects = [ 17 | Stars(screen, screen.width), 18 | Print(screen, 19 | SpeechBubble("Press space to see it again"), 20 | y=screen.height - 3, 21 | start_frame=300) 22 | ] 23 | for _ in range(20): 24 | fireworks = [ 25 | (PalmFirework, 25, 30), 26 | (PalmFirework, 25, 30), 27 | (StarFirework, 25, 35), 28 | (StarFirework, 25, 35), 29 | (StarFirework, 25, 35), 30 | (RingFirework, 20, 30), 31 | (SerpentFirework, 30, 35), 32 | ] 33 | firework, start, stop = choice(fireworks) 34 | effects.insert( 35 | 1, 36 | firework(screen, 37 | randint(0, screen.width), 38 | randint(screen.height // 8, screen.height * 3 // 4), 39 | randint(start, stop), 40 | start_frame=randint(0, 250))) 41 | 42 | effects.append(Print(screen, 43 | Rainbow(screen, FigletText("HAPPY")), 44 | screen.height // 2 - 6, 45 | speed=1, 46 | start_frame=100)) 47 | effects.append(Print(screen, 48 | Rainbow(screen, FigletText("NEW YEAR!")), 49 | screen.height // 2 + 1, 50 | speed=1, 51 | start_frame=100)) 52 | scenes.append(Scene(effects, -1)) 53 | 54 | screen.play(scenes, stop_on_resize=True) 55 | 56 | 57 | while True: 58 | try: 59 | Screen.wrapper(demo) 60 | sys.exit(0) 61 | except ResizeScreenError: 62 | pass 63 | -------------------------------------------------------------------------------- /samples/frame_borders.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.constants import DOUBLE_LINE 4 | from asciimatics.widgets import Frame, Text, TextBox, Layout, Label, Button, PopUpDialog, Widget 5 | from asciimatics.effects import Background 6 | from asciimatics.scene import Scene 7 | from asciimatics.screen import Screen 8 | from asciimatics.exceptions import ResizeScreenError, StopApplication 9 | from asciimatics.utilities import BoxTool 10 | 11 | 12 | class TopFrame(Frame): 13 | def __init__(self, screen): 14 | super(TopFrame, self).__init__(screen, 15 | int(screen.height // 3) - 1, 16 | screen.width // 2, 17 | y=0, 18 | has_border=True, 19 | can_scroll=True, 20 | name="Top Form") 21 | self.border_box.style = DOUBLE_LINE 22 | layout = Layout([1, 18, 1]) 23 | self.add_layout(layout) 24 | layout.add_widget(Label("Scrolling, with border"), 1) 25 | for i in range(screen.height // 2): 26 | layout.add_widget(Text(label=f"Text {i}:"), 1) 27 | self.fix() 28 | 29 | 30 | class MidFrame(Frame): 31 | def __init__(self, screen): 32 | super(MidFrame, self).__init__(screen, 33 | int(screen.height // 3) - 1, 34 | screen.width // 2, 35 | y=int(screen.height // 3), 36 | has_border=False, 37 | can_scroll=True, 38 | name="Mid Form") 39 | layout = Layout([1, 18, 1]) 40 | self.add_layout(layout) 41 | layout.add_widget(Label("Scrolling, no border"), 1) 42 | for i in range(screen.height // 2): 43 | layout.add_widget(Text(label=f"Text {i}:"), 1) 44 | self.fix() 45 | 46 | 47 | class BottomFrame(Frame): 48 | def __init__(self, screen): 49 | super(BottomFrame, self).__init__(screen, 50 | int(screen.height // 3), 51 | screen.width // 2, 52 | y=int(screen.height * 2 // 3), 53 | has_border=False, 54 | can_scroll=False, 55 | name="Bottom Form") 56 | layout = Layout([1, 18, 1]) 57 | self.add_layout(layout) 58 | layout.add_widget(Label("No scrolling, no border"), 1) 59 | layout.add_widget(TextBox(Widget.FILL_FRAME, label="Box 3:", name="BOX3"), 1) 60 | layout.add_widget(Text(label="Text 3:", name="TEXT3"), 1) 61 | layout.add_widget(Button("Quit", self._quit, label="To exit:"), 1) 62 | self.fix() 63 | 64 | def _quit(self): 65 | popup = PopUpDialog(self._screen, "Are you sure?", ["Yes", "No"], 66 | has_shadow=True, on_close=self._quit_on_yes) 67 | self._scene.add_effect(popup) 68 | 69 | @staticmethod 70 | def _quit_on_yes(selected): 71 | # Yes is the first button 72 | if selected == 0: 73 | raise StopApplication("User requested exit") 74 | 75 | 76 | def demo(screen, scene): 77 | scenes = [Scene([ 78 | Background(screen), 79 | TopFrame(screen), 80 | MidFrame(screen), 81 | BottomFrame(screen), 82 | ], -1)] 83 | 84 | screen.play(scenes, stop_on_resize=True, start_scene=scene, allow_int=True) 85 | 86 | 87 | last_scene = None 88 | while True: 89 | try: 90 | Screen.wrapper(demo, catch_interrupt=False, arguments=[last_scene]) 91 | quit() 92 | except ResizeScreenError as e: 93 | last_scene = e.scene 94 | -------------------------------------------------------------------------------- /samples/fruit.ans: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/samples/fruit.ans -------------------------------------------------------------------------------- /samples/globe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/samples/globe.gif -------------------------------------------------------------------------------- /samples/grumpy_cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/samples/grumpy_cat.jpg -------------------------------------------------------------------------------- /samples/images.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import BannerText, Print, Scroll 4 | from asciimatics.renderers import ColourImageFile, FigletText, ImageFile 5 | from asciimatics.scene import Scene 6 | from asciimatics.screen import Screen 7 | from asciimatics.exceptions import ResizeScreenError 8 | import sys 9 | 10 | 11 | def demo(screen): 12 | scenes = [] 13 | effects = [ 14 | Print(screen, ImageFile("globe.gif", screen.height - 2, colours=screen.colours), 15 | 1, 16 | stop_frame=100), 17 | ] 18 | scenes.append(Scene(effects)) 19 | effects = [ 20 | Print(screen, 21 | ColourImageFile(screen, "colour_globe.gif", screen.height-2, 22 | uni=screen.unicode_aware, 23 | dither=screen.unicode_aware), 24 | 1, 25 | stop_frame=200), 26 | Print(screen, 27 | FigletText("ASCIIMATICS", 28 | font='banner3' if screen.width > 80 else 'banner'), 29 | screen.height//2-3, 30 | colour=7, bg=7 if screen.unicode_aware else 0), 31 | ] 32 | scenes.append(Scene(effects)) 33 | effects = [ 34 | Print(screen, 35 | ColourImageFile(screen, "grumpy_cat.jpg", screen.height, 36 | uni=screen.unicode_aware), 37 | screen.height, 38 | speed=1, 39 | stop_frame=(40+screen.height)*3), 40 | Scroll(screen, 3) 41 | ] 42 | scenes.append(Scene(effects)) 43 | effects = [ 44 | BannerText(screen, 45 | ColourImageFile(screen, "python.png", screen.height-2, 46 | uni=screen.unicode_aware, dither=screen.unicode_aware), 47 | 0, 0), 48 | ] 49 | scenes.append(Scene(effects)) 50 | 51 | screen.play(scenes, stop_on_resize=True) 52 | 53 | 54 | if __name__ == "__main__": 55 | while True: 56 | try: 57 | Screen.wrapper(demo) 58 | sys.exit(0) 59 | except ResizeScreenError: 60 | pass 61 | -------------------------------------------------------------------------------- /samples/interactive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import Sprite, Print 4 | from asciimatics.event import KeyboardEvent, MouseEvent 5 | from asciimatics.exceptions import ResizeScreenError 6 | from asciimatics.renderers import StaticRenderer, SpeechBubble, FigletText 7 | from asciimatics.screen import Screen 8 | from asciimatics.paths import DynamicPath 9 | from asciimatics.sprites import Arrow 10 | from asciimatics.scene import Scene 11 | import sys 12 | 13 | # Sprites used for the demo 14 | arrow = None 15 | cross_hairs = None 16 | 17 | 18 | class KeyboardController(DynamicPath): 19 | def process_event(self, event): 20 | if isinstance(event, KeyboardEvent): 21 | key = event.key_code 22 | if key == Screen.KEY_UP: 23 | self._y -= 1 24 | self._y = max(self._y, 2) 25 | elif key == Screen.KEY_DOWN: 26 | self._y += 1 27 | self._y = min(self._y, self._screen.height-2) 28 | elif key == Screen.KEY_LEFT: 29 | self._x -= 1 30 | self._x = max(self._x, 3) 31 | elif key == Screen.KEY_RIGHT: 32 | self._x += 1 33 | self._x = min(self._x, self._screen.width-3) 34 | else: 35 | return event 36 | else: 37 | return event 38 | 39 | 40 | class MouseController(DynamicPath): 41 | def __init__(self, sprite, scene, x, y): 42 | super(MouseController, self).__init__(scene, x, y) 43 | self._sprite = sprite 44 | 45 | def process_event(self, event): 46 | if isinstance(event, MouseEvent): 47 | self._x = event.x 48 | self._y = event.y 49 | if event.buttons & MouseEvent.DOUBLE_CLICK != 0: 50 | # Try to whack the other sprites when mouse is clicked 51 | self._sprite.whack("KERPOW!") 52 | elif event.buttons & MouseEvent.LEFT_CLICK != 0: 53 | # Try to whack the other sprites when mouse is clicked 54 | self._sprite.whack("BANG!") 55 | elif event.buttons & MouseEvent.RIGHT_CLICK != 0: 56 | # Try to whack the other sprites when mouse is clicked 57 | self._sprite.whack("CRASH!") 58 | elif event.buttons & MouseEvent.SCROLL_UP != 0: 59 | # Try to whack the other sprites when mouse is clicked 60 | self._sprite.whack("OOOOH!") 61 | elif event.buttons & MouseEvent.SCROLL_DOWN != 0: 62 | # Try to whack the other sprites when mouse is clicked 63 | self._sprite.whack("AAAAH!") 64 | else: 65 | return event 66 | 67 | 68 | class TrackingPath(DynamicPath): 69 | def __init__(self, scene, path): 70 | super(TrackingPath, self).__init__(scene, 0, 0) 71 | self._path = path 72 | 73 | def process_event(self, event): 74 | return event 75 | 76 | def next_pos(self): 77 | x, y = self._path.next_pos() 78 | return x + 8, y - 2 79 | 80 | 81 | class Speak(Sprite): 82 | def __init__(self, screen, scene, path, text, **kwargs): 83 | """ 84 | See :py:obj:`.Sprite` for details. 85 | """ 86 | super(Speak, self).__init__( 87 | screen, 88 | renderer_dict={ 89 | "default": SpeechBubble(text, "L") 90 | }, 91 | path=TrackingPath(scene, path), 92 | colour=Screen.COLOUR_CYAN, 93 | **kwargs) 94 | 95 | 96 | class InteractiveArrow(Arrow): 97 | def __init__(self, screen): 98 | """ 99 | See :py:obj:`.Sprite` for details. 100 | """ 101 | super(InteractiveArrow, self).__init__( 102 | screen, 103 | path=KeyboardController( 104 | screen, screen.width // 2, screen.height // 2), 105 | colour=Screen.COLOUR_GREEN) 106 | 107 | def say(self, text): 108 | self._scene.add_effect( 109 | Speak(self._screen, self._scene, self._path, text, delete_count=50)) 110 | 111 | 112 | class CrossHairs(Sprite): 113 | def __init__(self, screen): 114 | """ 115 | See :py:obj:`.Sprite` for details. 116 | """ 117 | super(CrossHairs, self).__init__( 118 | screen, 119 | renderer_dict={ 120 | "default": StaticRenderer(images=["X"]) 121 | }, 122 | path=MouseController( 123 | self, screen, screen.width // 2, screen.height // 2), 124 | colour=Screen.COLOUR_RED) 125 | 126 | def whack(self, sound): 127 | x, y = self._path.next_pos() 128 | if self.overlaps(arrow, use_new_pos=True): 129 | arrow.say("OUCH!") 130 | else: 131 | self._scene.add_effect(Print( 132 | self._screen, 133 | SpeechBubble(sound), y, x, clear=True, delete_count=50)) 134 | 135 | 136 | def demo(screen): 137 | global arrow, cross_hairs 138 | arrow = InteractiveArrow(screen) 139 | cross_hairs = CrossHairs(screen) 140 | 141 | scenes = [] 142 | effects = [ 143 | Print(screen, FigletText("Hit the arrow with the mouse!", "digital"), 144 | y=screen.height//3-3), 145 | Print(screen, FigletText("Press space when you're ready.", "digital"), 146 | y=2 * screen.height//3-3), 147 | ] 148 | scenes.append(Scene(effects, -1)) 149 | 150 | effects = [ 151 | arrow, 152 | cross_hairs 153 | ] 154 | scenes.append(Scene(effects, -1)) 155 | 156 | screen.play(scenes, stop_on_resize=True) 157 | 158 | 159 | if __name__ == "__main__": 160 | while True: 161 | try: 162 | Screen.wrapper(demo) 163 | sys.exit(0) 164 | except ResizeScreenError: 165 | pass 166 | -------------------------------------------------------------------------------- /samples/julia.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import Julia 4 | from asciimatics.scene import Scene 5 | from asciimatics.screen import Screen 6 | from asciimatics.exceptions import ResizeScreenError 7 | import sys 8 | 9 | 10 | def demo(screen): 11 | scenes = [] 12 | effects = [ 13 | Julia(screen), 14 | ] 15 | scenes.append(Scene(effects, -1)) 16 | screen.play(scenes, stop_on_resize=True) 17 | 18 | 19 | while True: 20 | try: 21 | Screen.wrapper(demo) 22 | sys.exit(0) 23 | except ResizeScreenError: 24 | pass 25 | -------------------------------------------------------------------------------- /samples/kaleidoscope.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from math import sqrt 4 | 5 | from asciimatics.renderers import Kaleidoscope, FigletText, Rainbow, RotatedDuplicate, \ 6 | StaticRenderer 7 | from asciimatics.scene import Scene 8 | from asciimatics.screen import Screen 9 | from asciimatics.effects import Print 10 | from asciimatics.exceptions import ResizeScreenError 11 | import sys 12 | 13 | 14 | def demo(screen): 15 | scenes = [] 16 | cell1 = Rainbow(screen, 17 | RotatedDuplicate(screen.width // 2, 18 | max(screen.width // 2, screen.height), 19 | FigletText("ASCII" if screen.width < 80 else "ASCII rules", 20 | font="banner", 21 | width=screen.width // 2))) 22 | cell2 = "" 23 | size = int(sqrt(screen.height ** 2 + screen.width ** 2 // 4)) 24 | for _ in range(size): 25 | for x in range(size): 26 | c = x * screen.colours // size 27 | cell2 += "${%d,2,%d}:" % (c, c) 28 | cell2 += "\n" 29 | for i in range(8): 30 | scenes.append( 31 | Scene([Print(screen, 32 | Kaleidoscope(screen.height, screen.width, cell1, i), 33 | 0, 34 | speed=1, 35 | transparent=False), 36 | Print(screen, 37 | FigletText(str(i)), screen.height - 6, x=screen.width - 8, speed=1)], 38 | duration=360)) 39 | scenes.append( 40 | Scene([Print(screen, 41 | Kaleidoscope(screen.height, screen.width, StaticRenderer([cell2]), i), 42 | 0, 43 | speed=1, 44 | transparent=False)], 45 | duration=360)) 46 | screen.play(scenes, stop_on_resize=True) 47 | 48 | 49 | if __name__ == "__main__": 50 | while True: 51 | try: 52 | Screen.wrapper(demo) 53 | sys.exit(0) 54 | except ResizeScreenError: 55 | pass 56 | -------------------------------------------------------------------------------- /samples/mapscache/0.0.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/samples/mapscache/0.0.0.jpg -------------------------------------------------------------------------------- /samples/mapscache/1.0.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/samples/mapscache/1.0.0.jpg -------------------------------------------------------------------------------- /samples/noise.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import RandomNoise 4 | from asciimatics.renderers import FigletText, Rainbow 5 | from asciimatics.scene import Scene 6 | from asciimatics.screen import Screen 7 | from asciimatics.exceptions import ResizeScreenError 8 | import sys 9 | 10 | 11 | def demo(screen): 12 | effects = [ 13 | RandomNoise(screen, 14 | signal=Rainbow(screen, 15 | FigletText("ASCIIMATICS"))) 16 | ] 17 | screen.play([Scene(effects, -1)], stop_on_resize=True) 18 | 19 | 20 | while True: 21 | try: 22 | Screen.wrapper(demo) 23 | sys.exit(0) 24 | except ResizeScreenError: 25 | pass 26 | -------------------------------------------------------------------------------- /samples/pacman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/samples/pacman.png -------------------------------------------------------------------------------- /samples/particles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from random import randint 4 | from asciimatics.effects import Print 5 | from asciimatics.particles import Explosion, StarFirework, DropScreen, Rain, ShootScreen 6 | from asciimatics.renderers import SpeechBubble, FigletText, Rainbow 7 | from asciimatics.scene import Scene 8 | from asciimatics.screen import Screen 9 | from asciimatics.exceptions import ResizeScreenError 10 | import sys 11 | 12 | 13 | def demo(screen): 14 | screen.set_title("ASCIIMATICS demo") 15 | 16 | scenes = [] 17 | 18 | # First scene: title page 19 | effects = [ 20 | Print(screen, 21 | Rainbow(screen, FigletText("ASCIIMATICS", font="big")), 22 | y=screen.height // 4 - 5), 23 | Print(screen, 24 | FigletText("Particle System"), 25 | screen.height // 2 - 3), 26 | Print(screen, 27 | FigletText("Effects Demo"), 28 | screen.height * 3 // 4 - 3), 29 | Print(screen, 30 | SpeechBubble("Press SPACE to continue..."), 31 | screen.height - 3, 32 | transparent=False, 33 | start_frame=70) 34 | ] 35 | scenes.append(Scene(effects, -1)) 36 | 37 | # Next scene: just dissolve the title. 38 | effects = [] 39 | for i in range(8): 40 | effects.append(ShootScreen( 41 | screen, 42 | randint(screen.width // 3, screen.width * 2 // 3), 43 | randint(screen.height // 4, screen.height * 3 // 4), 44 | 100, 45 | diameter=randint(8, 12), 46 | start_frame=i * 10)) 47 | effects.append(ShootScreen( 48 | screen, screen.width // 2, screen.height // 2, 100, start_frame=90)) 49 | scenes.append(Scene(effects, 120, clear=False)) 50 | 51 | # Next scene: sub-heading. 52 | effects = [ 53 | DropScreen(screen, 100), 54 | Print(screen, 55 | Rainbow(screen, FigletText("Explosions", font="doom")), 56 | y=screen.height // 2 - 5, 57 | stop_frame=30), 58 | DropScreen(screen, 100, start_frame=30) 59 | ] 60 | scenes.append(Scene(effects, 80)) 61 | 62 | # Next scene: explosions 63 | effects = [] 64 | for _ in range(20): 65 | effects.append( 66 | Explosion(screen, 67 | randint(3, screen.width - 4), 68 | randint(1, screen.height - 2), 69 | randint(20, 30), 70 | start_frame=randint(0, 250))) 71 | effects.append(Print(screen, 72 | SpeechBubble("Press SPACE to continue..."), 73 | screen.height - 6, 74 | speed=1, 75 | transparent=False, 76 | start_frame=100)) 77 | scenes.append(Scene(effects, -1)) 78 | 79 | # Next scene: sub-heading. 80 | effects = [ 81 | Print(screen, 82 | Rainbow(screen, FigletText("Rain", font="doom")), 83 | y=screen.height // 2 - 5, 84 | stop_frame=30), 85 | DropScreen(screen, 100, start_frame=30) 86 | ] 87 | scenes.append(Scene(effects, 80)) 88 | 89 | # Next scene: rain storm. 90 | effects = [ 91 | Rain(screen, 200), 92 | Print(screen, 93 | SpeechBubble("Press SPACE to continue..."), 94 | screen.height - 6, 95 | speed=1, 96 | transparent=False, 97 | start_frame=100) 98 | ] 99 | scenes.append(Scene(effects, -1)) 100 | 101 | # Next scene: sub-heading. 102 | effects = [ 103 | Print(screen, 104 | Rainbow(screen, FigletText("Fireworks", font="doom")), 105 | y=screen.height // 2 - 5, 106 | stop_frame=30), 107 | DropScreen(screen, 100, start_frame=30) 108 | ] 109 | scenes.append(Scene(effects, 80)) 110 | 111 | # Next scene: fireworks 112 | effects = [] 113 | for _ in range(20): 114 | effects.append( 115 | StarFirework(screen, 116 | randint(3, screen.width - 4), 117 | randint(1, screen.height - 2), 118 | randint(20, 30), 119 | start_frame=randint(0, 250))) 120 | effects.append(Print(screen, 121 | SpeechBubble("Press SPACE to continue..."), 122 | screen.height - 6, 123 | speed=1, 124 | transparent=False, 125 | start_frame=100)) 126 | scenes.append(Scene(effects, -1)) 127 | 128 | screen.play(scenes, stop_on_resize=True) 129 | 130 | 131 | while True: 132 | try: 133 | Screen.wrapper(demo) 134 | sys.exit(0) 135 | except ResizeScreenError: 136 | pass 137 | -------------------------------------------------------------------------------- /samples/plasma.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from random import choice 5 | from asciimatics.renderers import Plasma, Rainbow, FigletText 6 | from asciimatics.scene import Scene 7 | from asciimatics.screen import Screen 8 | from asciimatics.effects import Print 9 | from asciimatics.exceptions import ResizeScreenError 10 | 11 | 12 | class PlasmaScene(Scene): 13 | 14 | # Random cheesy comments 15 | _comments = [ 16 | "Far out!", 17 | "Groovy", 18 | "Heavy", 19 | "Right on!", 20 | "Cool", 21 | "Dude!" 22 | ] 23 | 24 | def __init__(self, screen): 25 | self._screen = screen 26 | effects = [ 27 | Print(screen, 28 | Plasma(screen.height, screen.width, screen.colours), 29 | 0, 30 | speed=1, 31 | transparent=False), 32 | ] 33 | super().__init__(effects, 200, clear=False) 34 | 35 | def _add_cheesy_comment(self): 36 | msg = FigletText(choice(self._comments), "banner3") 37 | self._effects.append( 38 | Print(self._screen, 39 | msg, 40 | (self._screen.height // 2) - 4, 41 | x=(self._screen.width - msg.max_width) // 2 + 1, 42 | colour=Screen.COLOUR_BLACK, 43 | stop_frame=80, 44 | speed=1)) 45 | self._effects.append( 46 | Print(self._screen, 47 | Rainbow(self._screen, msg), 48 | (self._screen.height // 2) - 4, 49 | x=(self._screen.width - msg.max_width) // 2, 50 | colour=Screen.COLOUR_BLACK, 51 | stop_frame=80, 52 | speed=1)) 53 | 54 | def reset(self, old_scene=None, screen=None): 55 | # Avoid reseting the Plasma effect so that the animation continues across scenes. 56 | plasma = self._effects[0] 57 | self._effects = [] 58 | super().reset(old_scene, screen) 59 | 60 | # Make sure that we only have the initial plasma Effect and a single cheesy comment. 61 | self._effects = [plasma] 62 | self._add_cheesy_comment() 63 | 64 | 65 | def demo(screen): 66 | screen.play([PlasmaScene(screen)], stop_on_resize=True) 67 | 68 | 69 | if __name__ == "__main__": 70 | while True: 71 | try: 72 | Screen.wrapper(demo) 73 | sys.exit(0) 74 | except ResizeScreenError: 75 | pass 76 | -------------------------------------------------------------------------------- /samples/player.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import logging 4 | from asciimatics.effects import Print 5 | from asciimatics.scene import Scene 6 | from asciimatics.screen import Screen 7 | from asciimatics.renderers import AnsiArtPlayer, AsciinemaPlayer, SpeechBubble 8 | from asciimatics.exceptions import ResizeScreenError 9 | 10 | logging.basicConfig(filename="debug.log", level=logging.DEBUG) 11 | 12 | 13 | def demo(screen, scene): 14 | with AsciinemaPlayer("test.rec", max_delay=0.1) as player, \ 15 | AnsiArtPlayer("fruit.ans") as player2: 16 | screen.play( 17 | [ 18 | Scene( 19 | [ 20 | Print(screen, player, 0, speed=1, transparent=False), 21 | Print(screen, 22 | SpeechBubble("Press space to see ansi art"), 23 | y=screen.height - 3, speed=0, transparent=False) 24 | ], -1), 25 | Scene( 26 | [ 27 | Print(screen, player2, 0, speed=1, transparent=False), 28 | Print(screen, 29 | SpeechBubble("Press space to see asciinema"), 30 | y=screen.height - 3, speed=0, transparent=False) 31 | ], -1) 32 | ], 33 | stop_on_resize=True, start_scene=scene, allow_int=True) 34 | 35 | 36 | last_scene = None 37 | while True: 38 | try: 39 | Screen.wrapper(demo, catch_interrupt=False, arguments=[last_scene]) 40 | sys.exit(0) 41 | except ResizeScreenError as e: 42 | last_scene = e.scene 43 | -------------------------------------------------------------------------------- /samples/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/samples/python.png -------------------------------------------------------------------------------- /samples/quick_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.widgets import Frame, ListBox, Layout, Divider, Text, \ 4 | Button, TextBox, Widget 5 | from asciimatics.scene import Scene 6 | from asciimatics.screen import Screen 7 | from asciimatics.exceptions import ResizeScreenError, NextScene, StopApplication 8 | import sys 9 | 10 | 11 | class ContactModel(): 12 | def __init__(self): 13 | # Current contact when editing. 14 | self.current_id = None 15 | 16 | # List of dicts, where each dict contains a single contact, containing 17 | # name, address, phone, email and notes fields. 18 | self.contacts = [] 19 | 20 | 21 | class ListView(Frame): 22 | def __init__(self, screen, model): 23 | super(ListView, self).__init__(screen, 24 | screen.height * 2 // 3, 25 | screen.width * 2 // 3, 26 | on_load=self._reload_list, 27 | hover_focus=True, 28 | can_scroll=False, 29 | title="Contact List") 30 | # Save off the model that accesses the contacts database. 31 | self._model = model 32 | 33 | # Create the form for displaying the list of contacts. 34 | self._list_view = ListBox( 35 | Widget.FILL_FRAME, 36 | [(x["name"], i) for i,x in enumerate(self._model.contacts)], 37 | name="contacts", 38 | add_scroll_bar=True, 39 | on_change=self._on_pick, 40 | on_select=self._edit) 41 | self._edit_button = Button("Edit", self._edit) 42 | self._delete_button = Button("Delete", self._delete) 43 | layout = Layout([100], fill_frame=True) 44 | self.add_layout(layout) 45 | layout.add_widget(self._list_view) 46 | layout.add_widget(Divider()) 47 | layout2 = Layout([1, 1, 1, 1]) 48 | self.add_layout(layout2) 49 | layout2.add_widget(Button("Add", self._add), 0) 50 | layout2.add_widget(self._edit_button, 1) 51 | layout2.add_widget(self._delete_button, 2) 52 | layout2.add_widget(Button("Quit", self._quit), 3) 53 | self.fix() 54 | self._on_pick() 55 | 56 | def _on_pick(self): 57 | self._edit_button.disabled = self._list_view.value is None 58 | self._delete_button.disabled = self._list_view.value is None 59 | 60 | def _reload_list(self, new_value=None): 61 | self._list_view.options = [(x["name"], i) for i,x in enumerate(self._model.contacts)] 62 | self._list_view.value = new_value 63 | 64 | def _add(self): 65 | self._model.current_id = None 66 | raise NextScene("Edit Contact") 67 | 68 | def _edit(self): 69 | self.save() 70 | self._model.current_id = self.data["contacts"] 71 | raise NextScene("Edit Contact") 72 | 73 | def _delete(self): 74 | self.save() 75 | del self._model.contacts[self.data["contacts"]] 76 | self._reload_list() 77 | 78 | @staticmethod 79 | def _quit(): 80 | raise StopApplication("User pressed quit") 81 | 82 | 83 | class ContactView(Frame): 84 | def __init__(self, screen, model): 85 | super(ContactView, self).__init__(screen, 86 | screen.height * 2 // 3, 87 | screen.width * 2 // 3, 88 | hover_focus=True, 89 | can_scroll=False, 90 | title="Contact Details", 91 | reduce_cpu=True) 92 | # Save off the model that accesses the contacts database. 93 | self._model = model 94 | 95 | # Create the form for displaying the list of contacts. 96 | layout = Layout([100], fill_frame=True) 97 | self.add_layout(layout) 98 | layout.add_widget(Text("Name:", "name")) 99 | layout.add_widget(Text("Address:", "address")) 100 | layout.add_widget(Text("Phone number:", "phone")) 101 | layout.add_widget(Text("Email address:", "email")) 102 | layout.add_widget(TextBox( 103 | Widget.FILL_FRAME, "Notes:", "notes", as_string=True, line_wrap=True)) 104 | layout2 = Layout([1, 1, 1, 1]) 105 | self.add_layout(layout2) 106 | layout2.add_widget(Button("OK", self._ok), 0) 107 | layout2.add_widget(Button("Cancel", self._cancel), 3) 108 | self.fix() 109 | 110 | def reset(self): 111 | # Do standard reset to clear out form, then populate with new data. 112 | super(ContactView, self).reset() 113 | if self._model.current_id is None: 114 | self.data = {"name": "", "address": "", "phone": "", "email": "", "notes": ""} 115 | else: 116 | self.data = self._model.contacts[self._model.current_id] 117 | 118 | def _ok(self): 119 | self.save() 120 | if self._model.current_id is None: 121 | self._model.contacts.append(self.data) 122 | else: 123 | self._model.contacts[self._model.current_id] = self.data 124 | raise NextScene("Main") 125 | 126 | @staticmethod 127 | def _cancel(): 128 | raise NextScene("Main") 129 | 130 | 131 | def demo(screen, scene): 132 | scenes = [ 133 | Scene([ListView(screen, contacts)], -1, name="Main"), 134 | Scene([ContactView(screen, contacts)], -1, name="Edit Contact") 135 | ] 136 | 137 | screen.play(scenes, stop_on_resize=True, start_scene=scene, allow_int=True) 138 | 139 | 140 | contacts = ContactModel() 141 | last_scene = None 142 | while True: 143 | try: 144 | Screen.wrapper(demo, catch_interrupt=True, arguments=[last_scene]) 145 | sys.exit(0) 146 | except ResizeScreenError as e: 147 | last_scene = e.scene 148 | -------------------------------------------------------------------------------- /samples/rendering.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.renderers import BarChart 4 | from asciimatics.screen import Screen 5 | import sys 6 | import math 7 | import time 8 | from random import randint 9 | 10 | 11 | def fn(): 12 | return randint(0, 40) 13 | 14 | 15 | def wv(x): 16 | return lambda: 1 + math.sin(math.pi * (2*time.time()+x) / 5) 17 | 18 | 19 | def demo(): 20 | chart = BarChart(10, 40, [fn, fn], 21 | char="=", 22 | gradient=[(20, Screen.COLOUR_GREEN), 23 | (30, Screen.COLOUR_YELLOW), 24 | (40, Screen.COLOUR_RED)]) 25 | print(chart) 26 | chart = BarChart(13, 60, 27 | [wv(1), wv(2), wv(3), wv(4), wv(5), wv(7), wv(8), wv(9)], 28 | colour=Screen.COLOUR_GREEN, 29 | axes=BarChart.BOTH, 30 | scale=2.0) 31 | print(chart) 32 | chart = BarChart(7, 60, [lambda: time.time() * 10 % 101], 33 | gradient=[(10, 234), (20, 236), (30, 238), (40, 240), 34 | (50, 242), (60, 244), (70, 246), (80, 248), 35 | (90, 250), (100, 252)], 36 | char=">", 37 | scale=100.0, 38 | labels=True, 39 | axes=BarChart.X_AXIS) 40 | print(chart) 41 | chart = BarChart(10, 60, 42 | [wv(1), wv(2), wv(3), wv(4), wv(5), wv(7), wv(8), wv(9)], 43 | colour=[c for c in range(1, 8)], 44 | scale=2.0, 45 | axes=BarChart.X_AXIS, 46 | intervals=0.5, 47 | labels=True, 48 | border=False) 49 | print(chart) 50 | 51 | 52 | demo() 53 | sys.exit(0) 54 | -------------------------------------------------------------------------------- /samples/simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import Cycle, Stars 4 | from asciimatics.renderers import FigletText 5 | from asciimatics.scene import Scene 6 | from asciimatics.screen import Screen 7 | 8 | 9 | def demo(screen): 10 | effects = [ 11 | Cycle( 12 | screen, 13 | FigletText("ASCIIMATICS", font='big'), 14 | screen.height // 2 - 8), 15 | Cycle( 16 | screen, 17 | FigletText("ROCKS!", font='big'), 18 | screen.height // 2 + 3), 19 | Stars(screen, (screen.width + screen.height) // 2) 20 | ] 21 | screen.play([Scene(effects, 500)]) 22 | 23 | 24 | Screen.wrapper(demo) 25 | -------------------------------------------------------------------------------- /samples/tab_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.widgets import Frame, Layout, Divider, Button 4 | from asciimatics.scene import Scene 5 | from asciimatics.screen import Screen 6 | from asciimatics.exceptions import ResizeScreenError, NextScene, StopApplication 7 | import sys 8 | 9 | 10 | class TabButtons(Layout): 11 | def __init__(self, frame, active_tab_idx): 12 | cols = [1, 1, 1, 1, 1] 13 | super().__init__(cols) 14 | self._frame = frame 15 | for i,_ in enumerate(cols): 16 | self.add_widget(Divider(), i) 17 | btns = [Button("Btn1", self._on_click_1), 18 | Button("Btn2", self._on_click_2), 19 | Button("Btn3", self._on_click_3), 20 | Button("Btn4", self._on_click_4), 21 | Button("Quit", self._on_click_Q)] 22 | for i, btn in enumerate(btns): 23 | self.add_widget(btn, i) 24 | btns[active_tab_idx].disabled = True 25 | 26 | def _on_click_1(self): 27 | raise NextScene("Tab1") 28 | 29 | def _on_click_2(self): 30 | raise NextScene("Tab2") 31 | 32 | def _on_click_3(self): 33 | raise NextScene("Tab3") 34 | 35 | def _on_click_4(self): 36 | raise NextScene("Tab4") 37 | 38 | def _on_click_Q(self): 39 | raise StopApplication("Quit") 40 | 41 | 42 | class RootPage(Frame): 43 | def __init__(self, screen): 44 | super().__init__(screen, 45 | screen.height, 46 | screen.width, 47 | can_scroll=False, 48 | title="Root Page") 49 | layout1 = Layout([1], fill_frame=True) 50 | self.add_layout(layout1) 51 | # add your widgets here 52 | 53 | layout2 = TabButtons(self, 0) 54 | self.add_layout(layout2) 55 | self.fix() 56 | 57 | 58 | class AlphaPage(Frame): 59 | def __init__(self, screen): 60 | super().__init__(screen, 61 | screen.height, 62 | screen.width, 63 | can_scroll=False, 64 | title="Alpha Page") 65 | layout1 = Layout([1], fill_frame=True) 66 | self.add_layout(layout1) 67 | # add your widgets here 68 | 69 | layout2 = TabButtons(self, 1) 70 | self.add_layout(layout2) 71 | self.fix() 72 | 73 | 74 | class BravoPage(Frame): 75 | def __init__(self, screen): 76 | super().__init__(screen, 77 | screen.height, 78 | screen.width, 79 | can_scroll=False, 80 | title="Bravo Page") 81 | layout1 = Layout([1], fill_frame=True) 82 | self.add_layout(layout1) 83 | # add your widgets here 84 | 85 | layout2 = TabButtons(self, 2) 86 | self.add_layout(layout2) 87 | self.fix() 88 | 89 | 90 | class CharliePage(Frame): 91 | def __init__(self, screen): 92 | super().__init__(screen, 93 | screen.height, 94 | screen.width, 95 | can_scroll=False, 96 | title="Charlie Page") 97 | layout1 = Layout([1], fill_frame=True) 98 | self.add_layout(layout1) 99 | # add your widgets here 100 | 101 | layout2 = TabButtons(self, 3) 102 | self.add_layout(layout2) 103 | self.fix() 104 | 105 | 106 | def demo(screen, scene): 107 | scenes = [ 108 | Scene([RootPage(screen)], -1, name="Tab1"), 109 | Scene([AlphaPage(screen)], -1, name="Tab2"), 110 | Scene([BravoPage(screen)], -1, name="Tab3"), 111 | Scene([CharliePage(screen)], -1, name="Tab4"), 112 | ] 113 | screen.play(scenes, stop_on_resize=True, start_scene=scene, allow_int=True) 114 | 115 | 116 | last_scene = None 117 | while True: 118 | try: 119 | Screen.wrapper(demo, catch_interrupt=True, arguments=[last_scene]) 120 | sys.exit(0) 121 | except ResizeScreenError as e: 122 | last_scene = e.scene 123 | -------------------------------------------------------------------------------- /samples/treeview.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.event import KeyboardEvent 4 | from asciimatics.widgets import Frame, Layout, FileBrowser, Widget, Label, PopUpDialog, Text, \ 5 | Divider 6 | from asciimatics.scene import Scene 7 | from asciimatics.screen import Screen 8 | from asciimatics.exceptions import ResizeScreenError, StopApplication 9 | import sys 10 | import os 11 | try: 12 | import magic 13 | except ImportError: 14 | pass 15 | 16 | 17 | class DemoFrame(Frame): 18 | def __init__(self, screen): 19 | super(DemoFrame, self).__init__( 20 | screen, screen.height, screen.width, has_border=False, name="My Form") 21 | 22 | # Create the (very simple) form layout... 23 | layout = Layout([1], fill_frame=True) 24 | self.add_layout(layout) 25 | 26 | # Now populate it with the widgets we want to use. 27 | self._details = Text() 28 | self._details.disabled = True 29 | self._details.custom_colour = "field" 30 | self._list = FileBrowser(Widget.FILL_FRAME, 31 | os.path.abspath("."), 32 | name="mc_list", 33 | on_select=self.popup, 34 | on_change=self.details) 35 | layout.add_widget(Label("Local disk browser sample")) 36 | layout.add_widget(Divider()) 37 | layout.add_widget(self._list) 38 | layout.add_widget(Divider()) 39 | layout.add_widget(self._details) 40 | layout.add_widget(Label("Press Enter to select or `q` to quit.")) 41 | 42 | # Prepare the Frame for use. 43 | self.fix() 44 | 45 | def popup(self): 46 | # Just confirm whenever the user actually selects something. 47 | self._scene.add_effect( 48 | PopUpDialog(self._screen, "You selected: {}".format(self._list.value), ["OK"])) 49 | 50 | def details(self): 51 | # If python magic is installed, provide a little more detail of the current file. 52 | if self._list.value: 53 | if os.path.isdir(self._list.value): 54 | self._details.value = "Directory" 55 | elif os.path.isfile(self._list.value): 56 | try: 57 | self._details.value = magic.from_file(self._list.value) 58 | except NameError: 59 | self._details.value = "File (run 'pip install python-magic' for more details)" 60 | else: 61 | self._details.value = "--" 62 | 63 | def process_event(self, event): 64 | # Do the key handling for this Frame. 65 | if isinstance(event, KeyboardEvent): 66 | if event.key_code in [ord('q'), ord('Q'), Screen.ctrl("c")]: 67 | raise StopApplication("User quit") 68 | 69 | # Now pass on to lower levels for normal handling of the event. 70 | return super(DemoFrame, self).process_event(event) 71 | 72 | 73 | def demo(screen, old_scene): 74 | screen.play([Scene([DemoFrame(screen)], -1)], stop_on_resize=True, start_scene=old_scene) 75 | 76 | 77 | last_scene = None 78 | while True: 79 | try: 80 | Screen.wrapper(demo, catch_interrupt=False, arguments=[last_scene]) 81 | sys.exit(0) 82 | except ResizeScreenError as e: 83 | last_scene = e.scene 84 | -------------------------------------------------------------------------------- /samples/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/samples/wall.png -------------------------------------------------------------------------------- /samples/xmas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asciimatics.effects import Cycle, Snow, Print 4 | from asciimatics.renderers import FigletText, StaticRenderer 5 | from asciimatics.scene import Scene 6 | from asciimatics.screen import Screen 7 | from asciimatics.exceptions import ResizeScreenError 8 | import sys 9 | 10 | # Tree definition 11 | tree = r""" 12 | ${3,1}* 13 | / \ 14 | /${1}o${2} \ 15 | /_ _\ 16 | / \${4}b 17 | / \ 18 | / ${1}o${2} \ 19 | /__ __\ 20 | ${1}d${2} / ${4}o${2} \ 21 | / \ 22 | / ${4}o ${1}o${2}.\ 23 | /___________\ 24 | ${3}||| 25 | ${3}||| 26 | """, r""" 27 | ${3}* 28 | / \ 29 | /${1}o${2} \ 30 | /_ _\ 31 | / \${4}b 32 | / \ 33 | / ${1}o${2} \ 34 | /__ __\ 35 | ${1}d${2} / ${4}o${2} \ 36 | / \ 37 | / ${4}o ${1}o${2} \ 38 | /___________\ 39 | ${3}||| 40 | ${3}||| 41 | """ 42 | 43 | 44 | def demo(screen): 45 | effects = [ 46 | Print(screen, StaticRenderer(images=tree), 47 | x=screen.width - 15, 48 | y=screen.height - 15, 49 | colour=Screen.COLOUR_GREEN), 50 | Snow(screen), 51 | Cycle( 52 | screen, 53 | FigletText("HAPPY"), 54 | screen.height // 2 - 6, 55 | start_frame=300), 56 | Cycle( 57 | screen, 58 | FigletText("XMAS!"), 59 | screen.height // 2 + 1, 60 | start_frame=300), 61 | ] 62 | screen.play([Scene(effects, -1)], stop_on_resize=True) 63 | 64 | 65 | while True: 66 | try: 67 | Screen.wrapper(demo) 68 | sys.exit(0) 69 | except ResizeScreenError: 70 | pass 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | A setuptools based setup module for asciimatics. 3 | 4 | Based on the sample Python packages at: 5 | https://packaging.python.org/en/latest/distributing.html 6 | https://github.com/pypa/sampleproject 7 | """ 8 | 9 | from codecs import open as file_open 10 | from os import path 11 | from setuptools import setup, find_packages 12 | 13 | 14 | # Get the long description from the relevant file and strip any pre-amble (i.e. badges) from it. 15 | here = path.abspath(path.dirname(__file__)) 16 | with file_open(path.join(here, 'README.rst'), encoding='utf-8') as f: 17 | long_description = f.read().split("\n") 18 | while long_description[0] not in ("ASCIIMATICS", "ASCIIMATICS\r"): 19 | long_description = long_description[1:] 20 | long_description = "\n".join(long_description) 21 | 22 | setup( 23 | long_description=long_description, 24 | packages=find_packages(exclude=['contrib', 'docs', 'tests*']), 25 | ) 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/tests/__init__.py -------------------------------------------------------------------------------- /tests/mock_objects.py: -------------------------------------------------------------------------------- 1 | from asciimatics.effects import Effect 2 | from asciimatics.exceptions import StopApplication, NextScene 3 | 4 | 5 | class MockEffect(Effect): 6 | """ 7 | Dummy Effect use for some UTs. 8 | """ 9 | def __init__(self, count=10, stop=True, swallow=False, next_scene=None, 10 | frame_rate=1, stop_frame=5, **kwargs): 11 | """ 12 | :param count: When to stop effect 13 | :param stop: Whether to stop the application or skip to next scene. 14 | :param swallow: Whether to swallow any events or not. 15 | :param next_scene: The next scene to move to (if stop=False) 16 | :param frame_rate: The frame rate for updates. 17 | """ 18 | super().__init__(None, **kwargs) 19 | self.stop_called = False 20 | self.reset_called = False 21 | self.event_called = False 22 | self.save_called = False 23 | self.update_called = False 24 | self._count = count 25 | self._stop = stop 26 | self._swallow = swallow 27 | self._next_scene = next_scene 28 | self._frame_rate = frame_rate 29 | 30 | # Ugly hack to stop clash with underlying Effect definition. Sorry. 31 | self._my_stop_frame = stop_frame 32 | 33 | @property 34 | def stop_frame(self): 35 | self.stop_called = True 36 | return self._my_stop_frame 37 | 38 | @property 39 | def frame_update_count(self): 40 | return self._frame_rate 41 | 42 | def _update(self, frame_no): 43 | self.update_called = True 44 | self._count -= 1 45 | if self._count <= 0: 46 | if self._stop: 47 | raise StopApplication("End of test") 48 | else: 49 | raise NextScene(self._next_scene) 50 | 51 | def reset(self): 52 | self.reset_called = True 53 | 54 | def process_event(self, event): 55 | self.event_called = True 56 | return None if self._swallow else event 57 | 58 | def save(self): 59 | self.save_called = True 60 | -------------------------------------------------------------------------------- /tests/renderers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/tests/renderers/__init__.py -------------------------------------------------------------------------------- /tests/renderers/globe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbrittain/asciimatics/0a400e6c6c52cb7f4ba15ffaaa55b535d0d1b2b1/tests/renderers/globe.gif -------------------------------------------------------------------------------- /tests/renderers/test.ans: -------------------------------------------------------------------------------- 1 | This is a test file 2 | with ansi codes... 3 | Check 4 | here 2nd 5 | abcdefghaab cdde[?25h[?25l 6 | abcdeabcdeabcdeabcdeaZbc d 7 | 123 8 | -------------------------------------------------------------------------------- /tests/renderers/test2.ans: -------------------------------------------------------------------------------- 1 | One 2 | Two 3 | ThreeFourFive 4 | Six 5 | -------------------------------------------------------------------------------- /tests/renderers/test_bad.rec: -------------------------------------------------------------------------------- 1 | {"version": 1, "width": 134, "height": 18, "timestamp": 1621173258, "env": {"SHELL": "/data/data/com.termux/files/usr/bin/bash", "TERM": "xterm-256color"}} 2 | -------------------------------------------------------------------------------- /tests/renderers/test_base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from asciimatics.renderers import StaticRenderer 3 | from asciimatics.screen import Screen 4 | 5 | 6 | class TestRenderers(unittest.TestCase): 7 | def test_height(self): 8 | """ 9 | Check that the max_height property works. 10 | """ 11 | # Max height should match largest height of any entry. 12 | renderer = StaticRenderer(images=["A\nB", "C "]) 13 | self.assertEqual(renderer.max_height, 2) 14 | 15 | def test_width(self): 16 | """ 17 | Check that the max_width property works. 18 | """ 19 | # Max width should match largest width of any entry. 20 | renderer = StaticRenderer(images=["A\nB", "C "]) 21 | self.assertEqual(renderer.max_width, 3) 22 | 23 | def test_images(self): 24 | """ 25 | Check that the images property works. 26 | """ 27 | # Images should be the parsed versions of the original strings. 28 | renderer = StaticRenderer(images=["A\nB", "C "]) 29 | images = renderer.images 30 | self.assertEqual(next(images), ["A", "B"]) 31 | self.assertEqual(next(images), ["C "]) 32 | 33 | def test_repr(self): 34 | """ 35 | Check that the string representation works. 36 | """ 37 | # String presentation should be the first image as a printable string. 38 | renderer = StaticRenderer(images=["A\nB", "C "]) 39 | self.assertEqual(str(renderer), "A\nB") 40 | 41 | def test_colour_maps(self): 42 | """ 43 | Check that the ${} syntax is parsed correctly. 44 | """ 45 | # Check the ${fg, attr, bg} variant 46 | renderer = StaticRenderer(images=["${3,1,2}*"]) 47 | output = renderer.rendered_text 48 | self.assertEqual(len(output[0]), len(output[1])) 49 | self.assertEqual(output[0], ["*"]) 50 | self.assertEqual( 51 | output[1][0][0], 52 | (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_GREEN)) 53 | 54 | # Check the ${fg, attr} variant 55 | renderer = StaticRenderer(images=["${3,1}*"]) 56 | output = renderer.rendered_text 57 | self.assertEqual(len(output[0]), len(output[1])) 58 | self.assertEqual(output[0], ["*"]) 59 | self.assertEqual(output[1][0][0], (Screen.COLOUR_YELLOW, Screen.A_BOLD, None)) 60 | 61 | # Check the ${fg} variant 62 | renderer = StaticRenderer(images=["${1}XY${2}Z"]) 63 | output = renderer.rendered_text 64 | self.assertEqual(len(output[0]), len(output[1])) 65 | self.assertEqual(output[0], ["XYZ"]) 66 | self.assertEqual(output[1][0][0], (Screen.COLOUR_RED, 0, None)) 67 | self.assertEqual(output[1][0][1], (Screen.COLOUR_RED, 0, None)) 68 | self.assertEqual(output[1][0][2], (Screen.COLOUR_GREEN, 0, None)) 69 | 70 | 71 | if __name__ == '__main__': 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /tests/renderers/test_players.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from asciimatics.renderers import AnsiArtPlayer, AsciinemaPlayer 4 | 5 | 6 | class TestRendererPlayers(unittest.TestCase): 7 | def test_ansi_art(self): 8 | """ 9 | Check that ansi art player works. 10 | """ 11 | with AnsiArtPlayer(os.path.join(os.path.dirname(__file__), "test.ans"), 12 | height=5, width=20) as renderer: 13 | self.assertEqual( 14 | str(renderer), 15 | "This is a test file \n" + 16 | "with ansi codes... \n" + 17 | " \n" + 18 | " \n" + 19 | " ") 20 | self.assertEqual( 21 | str(renderer), 22 | "This is a test file \n" + 23 | "with ansi codes... \n" + 24 | "Check \n" + 25 | "here 2nd \n" + 26 | " ") 27 | self.assertEqual( 28 | str(renderer), 29 | "This is a test file \n" + 30 | " abab c \n" + 31 | "dheck \n" + 32 | "here 2nd \n" + 33 | "cbdeefghab ") 34 | self.assertEqual( 35 | str(renderer), 36 | " \n" + 37 | " \n" + 38 | " \n" + 39 | "123 \n" + 40 | " ") 41 | 42 | # Check images just returns one frame. 43 | self.assertEqual(len(renderer.images), 1) 44 | 45 | # Test line stripping 46 | with AnsiArtPlayer(os.path.join(os.path.dirname(__file__), "test2.ans"), 47 | height=2, width=10, rate=1, strip=True) as renderer: 48 | self.assertEqual(str(renderer), "One \n ") 49 | self.assertEqual(str(renderer), "OneTwo \n ") 50 | self.assertEqual(str(renderer), "OneTwoThre\neFourFive ") 51 | self.assertEqual(str(renderer), "eFourFiveS\nix ") 52 | 53 | def test_asciinema(self): 54 | """ 55 | Check that asciinema player works. 56 | """ 57 | with AsciinemaPlayer(os.path.join(os.path.dirname(__file__), "test.rec"), max_delay=0.1) as renderer: 58 | self.assertEqual(renderer.max_height, 18) 59 | self.assertEqual(renderer.max_width, 134) 60 | 61 | # Check can play the file to the end. 62 | for _ in range(700): 63 | a = str(renderer) 64 | self.assertEqual(a, 65 | "~/asciimatics/samples $ ls \n" + 66 | "256colour.py colour_globe.gif fireworks.py images.py mapscache plasma.py rendering.py test2.rec \n" + 67 | "bars.py contact_list.py forms.log interactive.py noise.py player.py simple.py tests.py \n" + 68 | "basics.py credits.py forms.py julia.py pacman.png python.png tab_demo.py top.py \n" + 69 | "bg_colours.py experimental.py globe.gif kaleidoscope.py pacman.py quick_model.py terminal.py treeview.py \n" + 70 | "cogs.py fire.py grumpy_cat.jpg maps.py particles.py ray_casting.py test.rec xmas.py \n" + 71 | "~/asciimatics/samples $ \n" + 72 | "exit \n" + 73 | " \n" + 74 | " \n" + 75 | " \n" + 76 | " \n" + 77 | " \n" + 78 | " \n" + 79 | " \n" + 80 | " \n" + 81 | " \n" + 82 | " ") 83 | 84 | # Check images just returns one frame. 85 | self.assertEqual(len(renderer.images), 1) 86 | 87 | # Check for unsupported format 88 | with self.assertRaises(RuntimeError): 89 | with AsciinemaPlayer(os.path.join(os.path.dirname(__file__), "test_bad.rec")) as renderer: 90 | pass 91 | 92 | 93 | if __name__ == '__main__': 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /tests/renderers/test_typewriter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from asciimatics.renderers import Typewriter, StaticRenderer 3 | 4 | 5 | class TestRendererTypewriter(unittest.TestCase): 6 | 7 | def test_typewriter(self): 8 | """ 9 | Check that the Typewriter renderer works. 10 | """ 11 | # Check basic operation 12 | renderer = Typewriter(StaticRenderer(["Hello\nWorld"])) 13 | output = "\n".join(renderer.rendered_text[0]) 14 | self.assertEqual(output, "H \n ") 15 | output = "\n".join(renderer.rendered_text[0]) 16 | self.assertEqual(output, "He \n ") 17 | output = "\n".join(renderer.rendered_text[0]) 18 | self.assertEqual(output, "Hel \n ") 19 | output = "\n".join(renderer.rendered_text[0]) 20 | self.assertEqual(output, "Hell \n ") 21 | output = "\n".join(renderer.rendered_text[0]) 22 | self.assertEqual(output, "Hello\n ") 23 | output = "\n".join(renderer.rendered_text[0]) 24 | self.assertEqual(output, "Hello\nW ") 25 | output = "\n".join(renderer.rendered_text[0]) 26 | self.assertEqual(output, "Hello\nWo ") 27 | output = "\n".join(renderer.rendered_text[0]) 28 | self.assertEqual(output, "Hello\nWor ") 29 | output = "\n".join(renderer.rendered_text[0]) 30 | self.assertEqual(output, "Hello\nWorl ") 31 | output = "\n".join(renderer.rendered_text[0]) 32 | self.assertEqual(output, "Hello\nWorld") 33 | 34 | # Check dimensions 35 | self.assertEqual(renderer.max_height, 2) 36 | self.assertEqual(renderer.max_width, 5) 37 | 38 | def test_generator(self): 39 | """ 40 | Check that the Typewriter generator works. 41 | """ 42 | # Check images operation works for embedding in static renderers 43 | renderer = Typewriter(StaticRenderer(["Hello"])) 44 | output = renderer.images 45 | self.assertEqual(output, [["H "], ["He "], ["Hel "], ["Hell "], ["Hello"]]) 46 | 47 | # Check reset works 48 | renderer.reset() 49 | output = renderer.images 50 | self.assertEqual(output, [["H "], ["He "], ["Hel "], ["Hell "], ["Hello"]]) 51 | 52 | 53 | if __name__ == '__main__': 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /tests/test_events.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from asciimatics.event import KeyboardEvent, MouseEvent 3 | 4 | 5 | class TestEvents(unittest.TestCase): 6 | def test_keyboard_event(self): 7 | """ 8 | Check Keyboard event is consistent. 9 | """ 10 | code = 123 11 | event = KeyboardEvent(code) 12 | self.assertEqual(event.key_code, code) 13 | self.assertIn(str(code), str(event)) 14 | 15 | def test_mouse_event(self): 16 | """ 17 | Check Mouse event is consistent. 18 | """ 19 | x = 1 20 | y = 2 21 | buttons = MouseEvent.DOUBLE_CLICK 22 | event = MouseEvent(x, y, buttons) 23 | self.assertEqual(event.x, x) 24 | self.assertEqual(event.y, y) 25 | self.assertEqual(event.buttons, buttons) 26 | self.assertIn(f"({x}, {y})", str(event)) 27 | self.assertIn(str(buttons), str(event)) 28 | 29 | 30 | if __name__ == '__main__': 31 | unittest.main() 32 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from asciimatics.exceptions import ResizeScreenError, StopApplication 3 | from asciimatics.scene import Scene 4 | from tests.mock_objects import MockEffect 5 | 6 | 7 | class TestExceptions(unittest.TestCase): 8 | def test_resize(self): 9 | """ 10 | Check that we can create a ResizeScreenError 11 | """ 12 | scene = Scene([MockEffect()]) 13 | message = "Test message" 14 | error = ResizeScreenError(message, scene) 15 | self.assertEqual(error.scene, scene) 16 | self.assertEqual(str(error), message) 17 | 18 | def test_stop_app(self): 19 | """ 20 | Check that we can create a StopApplication. 21 | """ 22 | message = "Test message" 23 | error = StopApplication(message) 24 | self.assertEqual(str(error), message) 25 | 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /tests/test_paths.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from asciimatics.event import MouseEvent 3 | from asciimatics.paths import Path, DynamicPath 4 | 5 | 6 | class TestPaths(unittest.TestCase): 7 | def assert_path_equals(self, path, oracle): 8 | path.reset() 9 | positions = [] 10 | while not path.is_finished(): 11 | positions.append(path.next_pos()) 12 | self.assertEqual(positions, oracle) 13 | 14 | def test_jump_and_wait(self): 15 | """ 16 | Check basic movement of cursor works. 17 | """ 18 | path = Path() 19 | path.jump_to(10, 10) 20 | path.wait(3) 21 | self.assert_path_equals(path, [(10, 10), (10, 10), (10, 10), (10, 10)]) 22 | 23 | def test_straight_lines(self): 24 | """ 25 | Check a path works in straight lines. 26 | """ 27 | # Horizontal 28 | path = Path() 29 | path.jump_to(10, 10) 30 | path.move_straight_to(15, 10, 5) 31 | self.assert_path_equals( 32 | path, 33 | [(10, 10), (11, 10), (12, 10), (13, 10), (14, 10), (15, 10)]) 34 | 35 | # Vertical 36 | path = Path() 37 | path.jump_to(5, 5) 38 | path.move_straight_to(5, 10, 5) 39 | self.assert_path_equals( 40 | path, 41 | [(5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (5, 10)]) 42 | 43 | # Diagonal spaced 44 | path = Path() 45 | path.jump_to(5, 5) 46 | path.move_straight_to(15, 15, 5) 47 | self.assert_path_equals( 48 | path, 49 | [(5, 5), (7, 7), (9, 9), (11, 11), (13, 13), (15, 15)]) 50 | 51 | def test_spline(self): 52 | """ 53 | Check a path works with a spline curve. 54 | """ 55 | path = Path() 56 | path.jump_to(0, 10) 57 | path.move_round_to([(0, 10), (20, 0), (40, 10), (20, 20), (0, 10)], 20) 58 | self.assert_path_equals( 59 | path, 60 | [(0, 10), (0, 10), (0, 10), (0, 10), (0, 10), (5, 7), 61 | (10, 4), (15, 1), (20, 0), (25, 1), (30, 3), (35, 7), 62 | (40, 10), (35, 12), (30, 16), (25, 18), (20, 20), (15, 18), 63 | (10, 15), (5, 12), (0, 10)]) 64 | 65 | def test_dynamic_path(self): 66 | """ 67 | Check a dynamic path works as expected. 68 | """ 69 | class TestPath(DynamicPath): 70 | def process_event(self, event): 71 | # Assume that we're always passing in a MouseEvent. 72 | self._x = event.x 73 | self._y = event.y 74 | 75 | # Initial path should start at specified location. 76 | path = TestPath(None, 0, 0) 77 | self.assertEqual(path.next_pos(), (0, 0)) 78 | self.assertFalse(path.is_finished()) 79 | 80 | # Process event should move location. 81 | path.process_event(MouseEvent(10, 5, 0)) 82 | self.assertEqual(path.next_pos(), (10, 5)) 83 | 84 | # Reset should return to original location. 85 | path.reset() 86 | self.assertEqual(path.next_pos(), (0, 0)) 87 | 88 | 89 | if __name__ == '__main__': 90 | unittest.main() 91 | -------------------------------------------------------------------------------- /tests/test_scene.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from asciimatics.event import MouseEvent 3 | from asciimatics.scene import Scene 4 | from tests.mock_objects import MockEffect 5 | 6 | 7 | class TestScene(unittest.TestCase): 8 | def test_properties(self): 9 | """ 10 | Check properties work as expected. 11 | """ 12 | effect = MockEffect() 13 | scene = Scene([effect], duration=10, clear=False, name="blah") 14 | self.assertEqual(scene.name, "blah") 15 | self.assertEqual(scene.duration, 10) 16 | self.assertFalse(scene.clear) 17 | 18 | def test_dynamic_effects(self): 19 | """ 20 | Check adding and removing effects works. 21 | """ 22 | # Start with no effects 23 | effect = MockEffect() 24 | scene = Scene([], duration=10) 25 | self.assertEqual(scene.effects, []) 26 | 27 | # Add one - check internals for presence 28 | scene.add_effect(effect) 29 | self.assertEqual(scene.effects, [effect]) 30 | 31 | # Remove it - check it's gone 32 | scene.remove_effect(effect) 33 | self.assertEqual(scene.effects, []) 34 | 35 | def test_events(self): 36 | """ 37 | Check event processing is queued correctly. 38 | """ 39 | # Check that the scene passes events through to the effects 40 | effect1 = MockEffect() 41 | effect2 = MockEffect() 42 | scene = Scene([effect1, effect2], duration=10) 43 | scene.process_event(MouseEvent(10, 5, 0)) 44 | self.assertTrue(effect1.event_called) 45 | self.assertTrue(effect2.event_called) 46 | 47 | # Check that the scene passes stops event processing when required 48 | effect1 = MockEffect() 49 | effect2 = MockEffect(swallow=True) 50 | scene = Scene([effect1, effect2], duration=10) 51 | scene.process_event(MouseEvent(10, 5, 0)) 52 | self.assertFalse(effect1.event_called) 53 | self.assertTrue(effect2.event_called) 54 | 55 | def test_save(self): 56 | """ 57 | Check scene will save data on exit if needed. 58 | """ 59 | effect = MockEffect() 60 | scene = Scene([effect], duration=10) 61 | self.assertFalse(effect.save_called) 62 | scene.exit() 63 | self.assertTrue(effect.save_called) 64 | 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /tests/test_sprites.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from asciimatics.paths import Path 3 | from asciimatics.sprites import Sam, Arrow, Plot 4 | 5 | 6 | class TestSprites(unittest.TestCase): 7 | def test_init(self): 8 | # Most of the function in these classes is actually in the Sprite 9 | # base Effect - so just check we can build these classes 10 | self.assertIsNotNone(Sam(None, Path())) 11 | self.assertIsNotNone(Arrow(None, Path())) 12 | self.assertIsNotNone(Plot(None, Path())) 13 | 14 | 15 | if __name__ == '__main__': 16 | unittest.main() 17 | -------------------------------------------------------------------------------- /tests/test_strings.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import time 3 | import unittest 4 | from asciimatics.strings import ColouredText 5 | from asciimatics.parsers import AsciimaticsParser 6 | 7 | 8 | class TestUtilities(unittest.TestCase): 9 | 10 | def test_coloured_text(self): 11 | """ 12 | Check ColouredText works as expected. 13 | """ 14 | # No specified start colour 15 | ct = ColouredText("Some ${1}text", AsciimaticsParser()) 16 | self.assertEqual(str(ct), "Some text") 17 | self.assertEqual(ct.raw_text, "Some ${1}text") 18 | self.assertEqual(len(ct), 9) 19 | self.assertEqual(ct.first_colour, None) 20 | self.assertEqual(ct.last_colour, (1, 0, None)) 21 | self.assertEqual(ct.colour_map[0], (None, None, None)) 22 | 23 | # Specified start colour 24 | ct = ColouredText("Some ${1}text", AsciimaticsParser(), colour=(2, 1, 0)) 25 | self.assertEqual(str(ct), "Some text") 26 | self.assertEqual(ct.raw_text, "Some ${1}text") 27 | self.assertEqual(len(ct), 9) 28 | self.assertEqual(ct.first_colour, (2, 1, 0)) 29 | self.assertEqual(ct.last_colour, (1, 0, None)) 30 | self.assertEqual(ct.colour_map[0], (2, 1, 0)) 31 | 32 | # Slicing 33 | self.assertEqual(ct[0], ColouredText("S", AsciimaticsParser())) 34 | self.assertEqual(ct[1:-1], ColouredText("ome ${1}tex", AsciimaticsParser())) 35 | self.assertNotEqual(ct[1:-1], ColouredText("ome tex", AsciimaticsParser())) 36 | self.assertEqual(ct[100:101], ColouredText("", AsciimaticsParser())) 37 | 38 | # Adding 39 | self.assertEqual( 40 | ColouredText("Some ", AsciimaticsParser()) + 41 | ColouredText("${3}Text", AsciimaticsParser()), 42 | ColouredText("Some ${3}Text", AsciimaticsParser())) 43 | 44 | # Joining 45 | self.assertEqual(ColouredText(" ", AsciimaticsParser()).join([ 46 | ColouredText("Hello", AsciimaticsParser()), 47 | ColouredText("${3}World", AsciimaticsParser())]), 48 | ColouredText("Hello ${3}World", AsciimaticsParser())) 49 | 50 | # Bad data comparisons 51 | self.assertNotEqual(ct, 1) 52 | self.assertFalse(ct == "Some text") 53 | 54 | # Startswith 55 | self.assertTrue(ct.startswith("Some")) 56 | -------------------------------------------------------------------------------- /tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import time 3 | import unittest 4 | from asciimatics.constants import ASCII_LINE, SINGLE_LINE, DOUBLE_LINE 5 | from asciimatics.utilities import readable_mem, readable_timestamp, BoxTool 6 | 7 | 8 | class TestUtilities(unittest.TestCase): 9 | 10 | def test_readable_mem(self): 11 | """ 12 | Check readable_mem works as expected. 13 | """ 14 | # Check formatting works as expected. 15 | self.assertEqual("9999", readable_mem(9999)) 16 | self.assertEqual("10K", readable_mem(10240)) 17 | self.assertEqual("1024K", readable_mem(1024*1024)) 18 | self.assertEqual("10M", readable_mem(1024*1024*10)) 19 | self.assertEqual("10G", readable_mem(1024*1024*1024*10)) 20 | self.assertEqual("10T", readable_mem(1024*1024*1024*1024*10)) 21 | self.assertEqual("10P", readable_mem(1024*1024*1024*1024*1024*10)) 22 | 23 | def test_readable_timestamp(self): 24 | """ 25 | Check readable_timestamp works as expected. 26 | """ 27 | # Check formatting works as expected. 28 | self.assertEqual("12:01:02AM", readable_timestamp( 29 | time.mktime(datetime.now().replace(hour=0, minute=1, second=2).timetuple()))) 30 | self.assertEqual("1999-01-02", readable_timestamp( 31 | time.mktime(datetime.now().replace(year=1999, month=1, day=2).timetuple()))) 32 | 33 | 34 | def test_boxtool(self): 35 | # SINGLE_LINE 36 | tool = BoxTool(True) 37 | self.assertEqual("┌───┐", tool.box_top(5)) 38 | self.assertEqual("└───┘", tool.box_bottom(5)) 39 | self.assertEqual("│ │", tool.box_line(5)) 40 | 41 | self.assertEqual( 42 | tool.box(5, 3), 43 | "┌───┐\n" + 44 | "│ │\n" + 45 | "└───┘\n") 46 | 47 | # DOUBLE_LINE 48 | self.assertEqual(tool.style, SINGLE_LINE) 49 | tool.style = DOUBLE_LINE 50 | self.assertEqual("╔═══╗", tool.box_top(5)) 51 | self.assertEqual("╚═══╝", tool.box_bottom(5)) 52 | self.assertEqual("║ ║", tool.box_line(5)) 53 | 54 | self.assertEqual( 55 | tool.box(5, 3), 56 | "╔═══╗\n" + 57 | "║ ║\n" + 58 | "╚═══╝\n") 59 | 60 | # ASCII_LINE 61 | tool = BoxTool(False) 62 | self.assertEqual("+---+", tool.box_top(5)) 63 | self.assertEqual("+---+", tool.box_bottom(5)) 64 | self.assertEqual("| |", tool.box_line(5)) 65 | 66 | self.assertEqual( 67 | tool.box(5, 3), 68 | "+---+\n" + 69 | "| |\n" + 70 | "+---+\n") 71 | --------------------------------------------------------------------------------