The framework’s pages, and drawing your own

A tutorial step: the pages the framework brings for free, a page you draw cell by cell, and how a caller rings off.

Draw a month as a grid

A menu and a notice are laid out for you. A month is not either: it is a grid, placed by cell. For that a page hands the layout a Custom part — a block of a stated height it draws itself, given a Canvas — and reaches, once, into sextile.viewdata for the Canvas and the Colour. Add the drawing and the two month pages:

_WEEKDAYS = ("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN")


def _months_either_side(day: date) -> tuple[date, date]:
    first = day.replace(day=1)
    previous = (first - timedelta(days=1)).replace(day=1)
    following = (first + timedelta(days=31)).replace(day=1)
    return previous, following


def _draw_month(canvas: Canvas, row: int, day: date, weeks: list[list[int]]) -> None:
    canvas.row(row).text("  ".join(weekday[:2] for weekday in _WEEKDAYS), Colour.CYAN)
    for offset, week in enumerate(weeks):
        cells = " ".join(f"{number:>3}" if number else "   " for number in week)
        colour = Colour.YELLOW if day.day in week else Colour.WHITE
        canvas.row(row + 1 + offset).text(cells.rstrip(), colour)


def _month_page(request: PageRequest, day: date) -> Page:
    app = request.app
    weeks = calendar.Calendar().monthdayscalendar(day.year, day.month)
    previous, following = _months_either_side(day)
    return PageLayout(
        title=_month_name(day).upper(),
        parts=[OnOneFrame(Custom(rows=1 + len(weeks), draw=lambda canvas, row: _draw_month(canvas, row, day, weeks)))],
        shortcuts=[
            Shortcut(key=keys.PREVIOUS_ITEM, destination=app.address_for("month", day=previous), with_arrow=True),
            Shortcut(key=keys.NEXT_ITEM, destination=app.address_for("month", day=following), with_arrow=True),
        ],
        item_noun="month",
    ).build(request)


@router.page("3", name="this_month", title="This month", detail="as a grid", keywords=("MONTH",))
async def this_month(request: PageRequest) -> Page:
    return _month_page(request, _today(request))


@router.page("32{day:date}", name="month", title="One month")
async def month(request: PageRequest, day: date) -> Page:
    return _month_page(request, day)
 AUGUST 2026                          3a
  
 MO  TU  WE  TH  FR  SA  SU             
                       1   2            
  3   4   5   6   7   8   9             
 10  11  12  13  14  15  16             
 17  18  19  20  21  22  23             
 24  25  26  27  28  29  30             
 31                                     
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
  
 A previous month, D next month, 0 index

Custom(rows=..., draw=...) is given the canvas and the row it starts on, and draws the weekday heads and the weeks under them. The week the day falls in is coloured, not the day: a colour attribute takes a cell, and a row of seven figures has none to spare. The two Shortcuts wire A and D to the months either side. Give the one-day page a shortcut to its month too, and add “this month” to the index menu.

Take the framework’s own pages

standard_pages returns routes for the pages every service can offer — where a caller has been, every page and its number, the words they can key — at whatever numbers you give them. Spread them into the service beside your own, and rename the factory’s assembly:

return Sextile(
    name=SERVICE_NAME.title(),
    pages=[*router, *standard_pages(history="92", contents="93", keywords="94")],
    lifespan=lifespan,
)

Each carries the framework’s own title, detail and keywords, drawn from what it already knows through app.title_for, so a contents page cannot drift from the service it lists. This is *93#:

 EVERY PAGE                          93a
  
 *1#   The index                        
 *2#   The time now                     
 *92#  Where you have been              
 *93#  Every page                       
 *94#  Words you can key                
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
                                        
  
 0 index                                

Ring off

*90# says goodbye and drops the line. farewell_page is the shape for it: no way home, since there is none, and a hang-up once it has been shown.

 GOODBYE                                
                                        
Thank you for calling.                  
                                        
Ring off.