High-resolution drawing canvas for Textual apps
Textual is an excellent Python framework for building applications in the terminal, or on the web. This library provides a canvas widget which your app can draw on using primitives like set_pixel(), draw_line() and draw_rectangle_box(). The canvas can also draw using high-resolution characters like unicode half blocks, quadrants and 8-dot Braille characters. It may still be apparent that these are drawn using characters that take up a full block in the terminal, especially when lines cross. However, the use of these characters can reduce the line thickness and improve the resolution tremendously.
Screenshots
Running the demo / installation
If you have uv installed, run
uvx textual-hires-canvas
If you use pipx, replace uvx with pipx. Alternatively, install the package with pip and run the demo:
pip install textual-hires-canvas
python -m textual_hires_canvas.demo
Tutorial
A simple example of using the canvas widget in your Textual app is given below.
from textual.app import App, ComposeResult
from textual_hires_canvas import Canvas, HiResMode, TextAlign
class MinimalApp(App[None]):
def compose(self) -> ComposeResult:
yield Canvas(40, 20)
def on_mount(self) -> None:
canvas = self.query_one(Canvas)
canvas.draw_rectangle_box(0, 0, 39, 19, thickness=2)
canvas.draw_line(1, 1, 38, 18, style="green")
canvas.draw_hires_line(1, 18.5, 38.5, 1, HiResMode.BRAILLE, style="blue")
canvas.write_text(
20,
1,
"A [italic]simple[/] demo of the [bold yellow]Canvas[/]",
TextAlign.CENTER,
)
if __name__ == "__main__":
MinimalApp().run()
Here, the Canvas widget is initialised with size 40 by 20 and a rectangular box, a line, a high-resolution line and some text is displayed. Coordinates are given in (x, y) fashion where (0, 0) is the top-left corner of the widget. The draw_line() method accepts a char argument which you can pass any unicode character you'd like to draw in the terminal. The style argument accepts Textual/Rich styles like green or yellow on blue. The HiresModes are HALFBLOCK, QUADRANT and BRAILLE.
Resizing the canvas
To automatically resize the Canvas to fit the available space in your app or the terminal, you can handle the Canvas.Resize event and call Canvas.reset(size=event.size) to resize the canvas. Be aware that the canvas is cleared and you have to redraw, like this:
from textual import on
from textual.app import App, ComposeResult
from textual_hires_canvas import Canvas, HiResMode, TextAlign
class MinimalApp(App[None]):
def compose(self) -> ComposeResult:
yield Canvas()
@on(Canvas.Resize)
def draw(self, event: Canvas.Resize):
canvas = event.canvas
size = event.size
canvas.reset(size=event.size)
canvas.draw_rectangle_box(0, 0, size.width - 1, size.height - 1, thickness=2)
canvas.draw_line(1, 1, size.width - 2, size.height - 2, style="green")
canvas.draw_hires_line(
1, size.height - 1.5, size.width - 1.5, 1, HiResMode.BRAILLE, style="blue"
)
canvas.write_text(
size.width // 2,
1,
"A [italic]simple[/] demo of the [bold yellow]Canvas[/]",
TextAlign.CENTER,
)
if __name__ == "__main__":
MinimalApp().run()
The full demo code
Finally, the code of the demo is given below, showing how you can handle simple animations:
from math import floor
from textual import on
from textual.app import App, ComposeResult
from textual_hires_canvas import Canvas, HiResMode
class DemoApp(App[None]):
_box_x_pos = 0
_box_y_pos = 0
_text_x_pos = 0.0
_box_x_step = 1
_box_y_step = 1
_text_x_step = 0.5
def compose(self) -> ComposeResult:
yield Canvas(1, 1)
def on_mount(self) -> None:
self.set_interval(1 / 10, self.redraw_canvas)
@on(Canvas.Resize)
def resize(self, event: Canvas.Resize) -> None:
event.canvas.reset(size=event.size)
def redraw_canvas(self) -> None:
canvas = self.query_one(Canvas)
canvas.reset()
canvas.draw_hires_line(2, 10, 78, 2, hires_mode=HiResMode.BRAILLE, style="blue")
canvas.draw_hires_line(2, 5, 78, 10, hires_mode=HiResMode.BRAILLE)
canvas.draw_line(0, 0, 8, 8)
canvas.draw_line(0, 19, 39, 0, char="X", style="red")
canvas.write_text(
floor(self._text_x_pos),
10,
"[green]This text is [bold]easy[/bold] to read",
)
canvas.draw_rectangle_box(
self._box_x_pos,
self._box_y_pos,
self._box_x_pos + 20,
self._box_y_pos + 10,
thickness=2,
)
self._box_x_pos += self._box_x_step
if (self._box_x_pos <= 0) or (self._box_x_pos + 20 >= canvas.size.width - 1):
self._box_x_step *= -1
self._box_y_pos += self._box_y_step
if (self._box_y_pos <= 0) or (self._box_y_pos + 10 >= canvas.size.height - 1):
self._box_y_step *= -1
self._text_x_pos += self._text_x_step
if self._text_x_pos >= canvas.size.width + 20:
self._text_x_pos = -20
def main():
DemoApp().run()
if __name__ == "__main__":
main()
List of canvas methods
reset()orreset(size): clear the canvas.get_pixel(x, y): get character at pixel coordinages.set_pixel(x, y, char, style): set a character at pixel coordinates.set_pixels(coordinates, char, style): set multiple pixels.set_hires_pixels(coordinates, hires_mode, style): set high-resolution pixels.draw_line(x0, y0, x1, y1, char, style): draw a line consisting of specific characters.draw_lines(coordinates, char, style): draw multiple lines.draw_hires_line(x0, y0, x1, y1, hires_mode, style): draw a high-resolution line using a particular mode.draw_hires_lines(coordinates, hires_mode, style): draw multiple high-resolution lines.draw_rectangle_box(x0, y0, x1, y1, thickness, style): draw a rectangle using box-drawing characters.
Alternatives
Textual-canvas by Dave Pearson is much better suited to display a large bitmap image with a scrollable viewport. It uses half-block characters to create square pixels.
Metadata
Release files for textual-hires-canvas 0.14.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| textual_hires_canvas-0.14.0.tar.gz | 1.3 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| textual_hires_canvas-0.14.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.3 MB
Release files / textual_hires_canvas-0.14.0.tar.gz
| Download URL | textual_hires_canvas-0.14.0.tar.gz |
|---|---|
| Size | 1.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0fa512492ddd2cd6c2a970a6beea58f5350f4cf25f40ad14ffd9fa86c6458769
|
|
BLAKE2b-256 checksum How to use checksums |
20f013f2a30ab7950cc3d5d5a2c893fbe3d40b4d129fd2cd30313260181578c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / textual_hires_canvas-0.14.0-py3-none-any.whl
| Download URL | textual_hires_canvas-0.14.0-py3-none-any.whl |
|---|---|
| Size | 19.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8cd67d656387220684449e837dd5809b6c00c5dfee8e4b494414b5837cb5a804
|
|
BLAKE2b-256 checksum How to use checksums |
85eae7e6d0f749395d61ca2ab8cda2f3c4e6613800b6b0d0b21f9d1d180c4d42
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for textual-hires-canvas 0.14.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.