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 HiresMode
s 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()
or reset(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.