sextile.forms

A field on a frame that a reader types into.

Everything else in this framework answers a keypress by going somewhere. A form answers one by changing what is on the screen without moving.

The shape is deliberately narrow. A form owns some rows of a frame, says which keys are typing rather than navigating, redraws its rows when the value changes, and says where its digits lead as the value now stands. The session does the remainder: it keeps the frame in step, sends the changed rows, and treats a digit that leads somewhere exactly as it treats a digit on a menu – so history, sequences and the back key all go on working with nothing added.

The package is the contract and the two field types built on it:

  • base: Form, the base a field type subclasses, draw_form, and Lookup.

  • type_ahead: TypeAhead, a field with the best few matches beneath it.

  • fields: Field and FieldSet, several fields with one live at a time.

All are re-exported here, so a service imports them from sextile.forms.

class sextile.forms.Field(name, label, row, accepts, width=12, hint='', hint_row=None, value='')[source]

Bases: object

One place on a frame that a reader types into.

Parameters:
name: str

What the form calls it when it hands the values over.

label: str

Shown before the value. It should not end in a space, since the attributes that follow occupy cells and show as spaces already.

accepts: Callable[[str], bool]

Whether a character belongs in this field. A form handles typing; what a particular field’s value is made of is the service’s concern.

width: int = 12

Cells the value may take, after the label and the attributes.

hint: str = ''

What this field takes, said beneath it and always.

Said beneath its own field rather than in one place that changes with the caret: a hint that changes is a row that repaints on every TAB, where a hint that stands still costs nothing to move about and lets a reader read both before deciding which field to start in.

class sextile.forms.FieldSet(*, fields, on_submit, footnote=None, footnote_row=None, submit_label='', footer_items=(), field_colour=Colour.BLUE, text_colour=Colour.WHITE)[source]

Bases: Form

Several fields, one of them live, and something said beneath them.

The interaction is settled by what a viewdata keypad can send, and it is narrower than it looks. Two of the four arrows are unusable on a form whose fields hold the letters W and S as data – up arrives as W and down as S – so the framework does not translate them, and this reads them as arrows. TAB shares a byte with cursor right, measured against Commstar, which is the key a reader will reach for first.

And 0 cannot be the way out where digits are data, so a page carrying one of these should say *1# in its footer rather than offer a key that would type a zero.

Nothing advances by itself. A field that jumped to the next when it judged itself full would put the caret where the reader did not, and no single rule for “full” fits every field.

Parameters:
footer_items()[source]

What the prompt should say about the keys this form answers.

Empty unless a form says otherwise. A form is the only part that answers letters, so it is the only one that has to explain them.

Return type:

Sequence[FooterItem]

property values: Mapping[str, str]

What has been keyed into each field, by name.

property live: Field

The field the caret is in.

property rows: range

Which rows of the frame this form owns, once it has been placed.

Only these are compared and redrawn, so a form cannot disturb the page around it however wrong it is about its own contents.

property caret: tuple[int, int]

Where the cursor goes after a repaint, as (row, column).

The reader is in the middle of a word. Every frame begins by hiding the cursor, and a field is the one place on a service where it tells them something.

accepts(key)[source]

Whether this key is typing rather than navigating.

Asked after the frame’s choices have been consulted, so a digit that leads somewhere is a selection and never a character.

Parameters:

key (str)

Return type:

bool

async typed(key)[source]

Take a key this form accepts, and change accordingly.

May go to a database or a network: a suggestion list is a query. It is awaited on the connection’s own task, so a slow one delays this caller and no other.

Parameters:

key (str)

Return type:

None

submit()[source]

Onward, and away from the last.

RETURN on a form is the same key as RETURN on a terminal has always been: it finishes the field you are in. On the last one there is nothing left to finish, so it finishes the form.

Which is why it does not cycle where TAB does. TAB moves about a form; RETURN gets to the end of one.

Return type:

PageAddress | None

draw(canvas)[source]

Draw this form’s rows as they now stand.

Parameters:

canvas (Canvas)

Return type:

None

class sextile.forms.Form[source]

Bases: ABC

Rows of a frame that answer keypresses by redrawing themselves.

A form is a layout.Drawable, and the one that is not a description: it holds what has been typed, so a layout carrying one is built for the request it answers rather than kept and built again.

A subclass numbers its rows from nought, and top_row is where the layout put it. Everything a form draws or reports is offset by that, so a form need not track where the content of a frame begins.

Example

A one-row field that collects text and offers nothing to choose:

class Text(Form):
    def __init__(self) -> None:
        self._value = ""

    @property
    def rows(self) -> range:
        return range(self.top_row, self.top_row + 1)

    @property
    def caret(self) -> tuple[int, int]:
        return self.top_row, len(self._value)

    def accepts(self, key: str) -> bool:
        return len(key) == 1 and key.isprintable()

    async def typed(self, key: str) -> None:
        self._value += key

    def draw(self, canvas: Canvas) -> None:
        canvas.row(self.top_row).text(self._value)
top_row: int = 0

The row this form was placed on. Nought until it has been.

place(canvas, space)[source]

Draw this form where the layout has put it, and claim its keys.

Parameters:
  • canvas (Canvas) – The frame being filled.

  • space (Space) – What the frame has left to give.

Return type:

Placed

Returns:

The rows it took, the digits its suggestions answer to, and itself as the frame’s form. A form is drawn whole or not at all, so a frame without room for it is asked to begin another.

abstract property rows: range

Which rows of the frame this form owns, once it has been placed.

Only these are compared and redrawn, so a form cannot disturb the page around it however wrong it is about its own contents.

abstract property caret: tuple[int, int]

Where the cursor goes after a repaint, as (row, column).

The reader is in the middle of a word. Every frame begins by hiding the cursor, and a field is the one place on a service where it tells them something.

abstractmethod accepts(key)[source]

Whether this key is typing rather than navigating.

Asked after the frame’s choices have been consulted, so a digit that leads somewhere is a selection and never a character.

Parameters:

key (str)

Return type:

bool

abstractmethod async typed(key)[source]

Take a key this form accepts, and change accordingly.

May go to a database or a network: a suggestion list is a query. It is awaited on the connection’s own task, so a slow one delays this caller and no other.

Parameters:

key (str)

Return type:

None

abstractmethod draw(canvas)[source]

Draw this form’s rows as they now stand.

Parameters:

canvas (Canvas)

Return type:

None

choices()[source]

Where this form’s digits lead, as the value now stands.

Empty unless a form says otherwise: a field that only collects text offers nothing to select.

Return type:

Mapping[str, PageAddress]

footer_items()[source]

What the prompt should say about the keys this form answers.

Empty unless a form says otherwise. A form is the only part that answers letters, so it is the only one that has to explain them.

Return type:

Sequence[FooterItem]

submit()[source]

Where RETURN leads, or None if there is nowhere to send the reader.

A reader who has typed something presses RETURN without being told to, so the default is the first thing on offer – the same as pressing 1 – which the reader can see. Refusing something visibly on offer because it is not character-for-character what was typed would surprise them.

None where nothing is on offer. The page already says so where the suggestions would be, and moving the reader is worse than leaving them to correct what they typed.

Return type:

PageAddress | None

class sextile.forms.TypeAhead(*, lookup, field_row=0, suggestions_row=2, label='', limit=3, no_match='', field_colour=Colour.BLUE, text_colour=Colour.WHITE)[source]

Bases: Form

A field, and the best few matches for what is in it.

The shape a viewdata reader already knows – a short numbered list, chosen with one keypress – with the list changing as they type instead of being given a page at a time.

Letters type. Digits choose. That means an entry whose text contains a digit cannot be keyed, which is a real limitation and the right trade: a service with such entries should fold the digits out of what it matches against, so the entry is still found by the letters around it.

It names nothing in the prompt: the suggestions are numbered where the reader is looking, and # is marked against the one it would take, so the footer holds only the way out.

Parameters:
property value: str

What has been typed so far.

property found: Sequence[Entry]

What is being offered, as the value now stands.

property rows: range

Which rows of the frame this form owns, once it has been placed.

Only these are compared and redrawn, so a form cannot disturb the page around it however wrong it is about its own contents.

property caret: tuple[int, int]

Where the cursor goes after a repaint, as (row, column).

The reader is in the middle of a word. Every frame begins by hiding the cursor, and a field is the one place on a service where it tells them something.

accepts(key)[source]

Whether this key is typing rather than navigating.

Asked after the frame’s choices have been consulted, so a digit that leads somewhere is a selection and never a character.

Parameters:

key (str)

Return type:

bool

async typed(key)[source]

Take a key this form accepts, and change accordingly.

May go to a database or a network: a suggestion list is a query. It is awaited on the connection’s own task, so a slow one delays this caller and no other.

Parameters:

key (str)

Return type:

None

choices()[source]

Where this form’s digits lead, as the value now stands.

Empty unless a form says otherwise: a field that only collects text offers nothing to select.

Return type:

Mapping[str, PageAddress]

draw(canvas)[source]

Draw this form’s rows as they now stand.

Parameters:

canvas (Canvas)

Return type:

None

sextile.forms.draw_form(frame, form)[source]

Redraw a form’s own rows onto a frame, leaving the rest of it alone.

Its rows are blanked first, so what a shorter suggestion vacates does not stay behind under a digit that now means something else.

Parameters:
Return type:

None