Skip to content

UI description format

A plugin never draws widgets itself. It sends a page (a plain map, CBOR on the wire, a dict in Python) and the app draws it with native widgets. The host validates the page strictly before the app sees it: every unknown field, wrong type or out-of-range value rejects the whole call with the error code invalid (freshgram.Invalid in Python) and a message from the list below. The app receives a normalised copy in which all defaults are filled in.

This page is the reference. For an introduction see UI: menus, commands, windows; for error codes see Errors and limits; for the call names see Call protocol (ops); for event names see Events.

Python Operation Permission Notes
ui.settings_page(title, items, buttons=None) ui.settings.set ui.settings_page The plugin’s settings page. Needs "settingsPage": true in contributes. Send it in response to settings.open.
ui.open_settings() ui.open_settings ui.settings_page Opens that page. Only within 10 s after a user action (otherwise denied).
ui.dialog(title, items, buttons=None) ui.dialog.show ui.dialog One dialog (a box over the chat) per plugin and account.
ui.close_dialog() ui.dialog.close ui.dialog Closes it; no dialog.close event comes back.
ui.window(window_id, title, items, buttons=None, mode="layer") ui.window.show ui.dialog Opens a window, or updates it if id is already open.
ui.update_window(window_id, title, items, buttons=None) ui.window.update ui.dialog Updates an open window; not_found if it is not open.
ui.close_window(window_id) ui.window.close ui.dialog Closes it; no window.close event comes back.

Rust and C send the same operations with the arguments page (and id, mode for windows); see Call protocol (ops). The Python helpers pass only title, items and buttons; to send closable (see below) call the operation ui.dialog.show directly.

{
"title": "My plugin", # required
"items": [ ... ], # required, 0..64 items
"buttons": [ ... ], # optional, 0..3
"closable": True, # optional
}
Field Type Required Limit / default Meaning
title text yes up to 120 bytes, one line The title of the settings box, dialog, layer or window. An empty title is shown as the plugin name.
items list of items yes at most 64 on every level, 256 items in the whole page, nesting depth 4 The content. May be empty.
buttons list no at most 3 Page buttons, see below.
closable bool no default true false stops the user from closing a dialog or layer with Esc, a click outside or the Close button. It is allowed only together with at least one entry in buttons (otherwise a page that cannot be closed needs a button). An uncloseable dialog is closed by the app after 2 minutes. Windows with mode="window" ignore it.

Unknown keys on the page are rejected (unknown field …).

“Text” in this reference is a UTF-8 string, limited in bytes, without control characters (below U+0020 except \n where newlines are allowed, and U+007F) and without bidirectional controls (U+200E, U+200F, U+202A to U+202E, U+2066 to U+2069, U+061C). A field that breaks this rule fails with <field> is not valid text.

id: 1 to 64 characters from a-z 0-9 _ - .. Every id must be unique across the whole page: items, nested items, tab ids, list entry ids, sub-page ids and page button ids share one namespace. A missing, malformed or repeated id gives an item has a bad or repeated id. Window ids (ui.window.*) are limited to 32 characters from the same set.

alias (see common fields): up to 32 characters from the same set, unique per page.

"buttons": [
{"id": "ok", "label": "Save", "style": "primary"},
{"id": "cancel", "label": "Cancel"},
]
Field Type Required Limit / default
id id yes shares the page’s id namespace
label text yes up to 40 bytes, one line
style text no up to 16 bytes: default (default), primary or danger

In a dialog and in a mode="layer" window the buttons are drawn at the bottom of the box (a standard Close button is added after them unless closable is false). On the settings page and in a mode="window" window they are drawn as rows at the end of the page. A press sends <scope>.click with the button id.

Every item is a map with type and the fields of that type. Any item type can be used on the settings page, in a dialog and in a window; containers can hold any items up to the depth limit.

type Interactive Event it sends
text no none
header no none
code no none
divider no none
link opens a URL none
progress no none
toggle yes change, value is a bool
button yes click, optionally long_click
input yes change, value is text
slider yes change, value is an int
select yes change, value is an option value
chat yes change, value is a chat id
page opens a sub-page long_click optionally
row, column, card containers none
tabs yes change, value is a tab id
image no none
list yes click / long_click of an entry
table no none

The event names are described in Events sent back.

Limit Value Error
items in any one list (items, a container’s items, a tab’s items) 64 items must be a list of at most 64 entries
items in the whole page (containers count, tab/list entries and buttons do not) 256 the page is too large
nesting depth (top-level items are level 1; row, column, card, page and a tab add a level) 4 the page is too large
peer: and emoji: icons per page 32 a page may use at most 32 peer and emoji icons

Several item types share these optional fields.

Field Type Applies to Meaning
hint text, up to 300 bytes, newlines allowed toggle, button, input, slider, chat, select, page A small grey line under the item; empty or absent hides it.
disabled bool, default false same as hint Greys the item out.
icon text link, button, toggle, chat, select, page, list entries An icon name, see Icons.
alias id, up to 32 chars, unique toggle, button, chat, select, page A stable name of the item on the settings page. The context menu of the item gets “Copy link”, the link tg://ayuplugin?id=<plugin id>&alias=<alias> opens the plugin’s settings scrolled to the item.
long_click bool, default false button, page, list entries A right click (long press) on the item sends <scope>.long_click. With the value false nothing is sent.

A paragraph. Selectable.

Field Type Required Limit / default
text text yes up to 1000 bytes, newlines allowed
style text no up to 16 bytes: title, subtitle, muted, accent, danger; absent means plain. Another value gives style must be title, subtitle, muted, accent or danger
{"type": "text", "text": "Connected", "style": "accent"}

A section title.

Field Type Required Limit
text text yes up to 120 bytes, one line

A monospace block, selectable (for logs, JSON, commands).

Field Type Required Limit
text text yes up to 1000 bytes, newlines allowed

A separator. Takes no fields besides type.

A row that opens a URL in the browser. The host name is shown on the right.

Field Type Required Limit / default
label text yes up to 120 bytes
url text yes up to 512 bytes, must start with https:// and be longer than 8 bytes; spaces and the characters " < > \ are not allowed. Nothing else is checked: the host is not parsed by the host
icon text no default link

Error: a link needs an https:// url.

A label with a progress bar. Not interactive; update it by sending the page again (see Updating a page).

Field Type Required Limit / default
label text no up to 120 bytes, default empty
value int no -1 to 100, default 0. -1 means unknown (no percent is shown). A float is rejected.

A switch row.

Field Type Required Limit / default
id id yes
label text yes up to 120 bytes
value bool no default false
hint, disabled, icon, alias no common fields
{"type": "toggle", "id": "loud", "label": "Loud", "value": False, "hint": "Plays a sound"}

A row that acts as a button.

Field Type Required Limit / default
id id yes
label text yes up to 120 bytes
style text no up to 16 bytes: default (default), primary, danger
hint, disabled, icon, alias, long_click no common fields

Error for a bad style: style must be default, primary or danger.

A text field. The value is sent 600 ms after the last edit, when the field loses focus and when the page closes. While the field has focus or an edit is pending, a new page does not overwrite what the user typed.

Field Type Required Limit / default
id id yes
label text yes up to 120 bytes. Used as the placeholder if placeholder is empty; otherwise shown above the field.
value text no at most max bytes; newlines only with multiline: true. Default empty.
placeholder text no up to 120 bytes
max int no 1 to 4096, default 1000 (the mask length if there is a mask)
multiline bool no multi-line field, default false
password bool no hides the characters, default false
mask text no up to 64 bytes; see below
hint, disabled no common fields

A mask describes the format: # is a digit, A is a Latin letter, * is any character, any other character stands for itself and is inserted automatically. A mask must contain at least one of # A * and cannot be combined with multiline or password (the keys must be absent, even set to false). The initial value must match the mask exactly (or be empty). Error: mask needs # (a digit), A (a letter) or *, and no multiline or password and the value does not fit the mask.

{"type": "input", "id": "phone", "label": "Phone", "mask": "+# (###) ###-##-##"}

An integer slider with a visible value.

Field Type Required Limit / default
id id yes
label text yes up to 120 bytes
min int no -10^9 to 10^9, default 0
max int no -10^9 to 10^9, default 100
step int no 1 to 10^9, default 1
value int no between min and max, default min; rounded down to a multiple of step from min
hint, disabled no common fields

At least two positions are required (max - min >= step) and at most 1000 steps ((max - min) / step <= 1000): a slider needs at least two positions and at most 1000 steps.

A row that opens a list of options to choose from.

Field Type Required Limit / default
id id yes
label text yes up to 120 bytes
options list yes 1 to 16 options
value text no up to 64 bytes, one of the option values; default is the first option
hint, disabled, icon, alias no common fields

An option is a map with exactly value (text, required, up to 64 bytes, unique in the list) and label (text, required, up to 120 bytes). Errors: a select needs 1 to 16 options, repeated option, the value is not one of the options.

{"type": "select", "id": "lang", "label": "Language", "value": "en",
"options": [{"value": "en", "label": "English"}, {"value": "ru", "label": "Russian"}]}

A row that opens the chat picker; shows the chosen chat’s name on the right.

Field Type Required Limit / default
id id yes
label text yes up to 120 bytes
value int no chat id, 0 or more; 0 (default) means nothing chosen
icon text no default chats
hint, disabled, alias no common fields

The name of the chosen chat is shown only for chats the app has loaded, and never for hidden chats. A hidden chat cannot be picked.

A row that opens a box with its own items. Events from items inside carry the same window field as the parent.

Field Type Required Limit / default
id id yes
label text yes up to 120 bytes; also the box title
items list of items yes 1 to 64, counts as one nesting level
hint, disabled, icon, alias, long_click no common fields

Error: a page needs items. Sub-pages can be nested up to the depth limit.

Field Type Required Limit
items list of items yes column: 1 to 64. row: 1 to 4; each item gets an equal-width cell.

Error: a row holds 1 to 4 items, a column at least one. Containers take no id.

A framed group with an optional title.

Field Type Required Limit / default
title text no up to 120 bytes, default empty
items list of items yes 1 to 64

Error: a card needs items.

Field Type Required Limit / default
id id yes
tabs list yes 1 to 8 tabs
value text no up to 64 bytes, id of the selected tab; default is the first tab

A tab is a map with exactly id (id, required, unique on the page), label (text, required, up to 40 bytes) and items (list, required, up to 64 items, may be empty; one nesting level). Errors: tabs needs 1 to 8 tabs, the value is not one of the tabs.

{"type": "tabs", "id": "view", "tabs": [
{"id": "a", "label": "Main", "items": [{"type": "text", "text": "Hello"}]},
{"id": "b", "label": "More", "items": []},
]}

A picture. Not interactive.

Field Type Required Limit / default
data bytes yes 1 byte to 256 KiB
width int no 0 to 512, default 0
height int no 0 to 512, default 0
alt text no up to 120 bytes; shown as a tooltip

The format is detected from the contents:

  • PNG up to 2048 x 2048, or JPEG (the app also refuses images over 4 million pixels);
  • a plain SVG: it must start with <svg (an <?xml ?> prolog is allowed) and must not contain <script, <foreignobject, <image, <iframe, <embed, <audio, <video, <animate, <set, <!entity, <!doctype, javascript:, @import, <?xml-stylesheet, data:, file:, event attributes (onload= and so on), or any href / url(...) that does not point to a local #id;
  • a Lottie animation (JSON): layers not empty, w and h 1 to 1024, fr (frame rate) 1 to 120, and ip / op (first and last frame) all required, with op greater than ip and at most 600 frames (op - ip); no assets that reference files (p, u, e).

width and height of 0 mean the natural size for PNG and JPEG (the image is scaled down to fit the width). They are required (non-zero) for SVG and Lottie. Errors: an image needs data of up to 256 KiB (missing, empty, not bytes or too large), an image must be a PNG up to 2048x2048, a JPEG, a plain SVG or a bounded Lottie animation, an SVG or Lottie image needs a width and a height.

A list of clickable rows.

Field Type Required Limit
items list of entries yes 1 to 64 entries (a list needs 1 to 64 entries)

An entry is a map with exactly these fields:

Field Type Required Limit / default
id id yes
title text yes up to 120 bytes
subtitle text no up to 120 bytes
icon text no icon
long_click bool no default false

A click on an entry sends <scope>.click with the entry’s id.

A read-only table.

Field Type Required Limit
columns list of text yes 1 to 6 titles, each up to 40 bytes, one line
rows list of lists of text yes 0 to 32 rows; each row has exactly as many cells as columns; each cell up to 120 bytes, one line

Errors: a table needs 1 to 6 columns and at most 32 rows, every row needs a cell for each column, a column title is not valid text, a table cell is not valid text. Cells are text only; convert numbers with str().

When the user touches an item, the plugin receives an event whose type depends on where the page is shown:

Page Change Click Long click Other
settings page settings.change settings.click settings.long_click settings.open (the page is being opened)
dialog dialog.change dialog.click dialog.long_click dialog.close (the user closed it)
window window.change window.click window.long_click window.close (the user closed it)

Payload:

Field Present in Meaning
type all the name from the table
id *.change, *.click, *.long_click the id of the item, list entry or page button
value *.change the new value: toggle bool, input text, slider int, select option value text, chat chat id (int), tabs tab id text
window all window.* events (and window.close) the window id given to ui.window

*.click has no value. The events settings.open, dialog.close and window.close have no id. dialog.close and window.close are sent when the user closes the dialog/window, when the app refuses to show it (no account, passcode lock) and never after your own close_* call. If the plugin is disabled or stops, its dialogs close silently.

In the Python SDK:

@freshgram.on_settings_open
def open_page(event): ... # settings.open
@freshgram.on_setting("enabled") # settings.change / settings.click for this id
def changed(event):
print(event["type"], event.get("value"))
@freshgram.on_dialog("ok") # dialog.change / click / close for this id
def dialog_event(event): ...
@freshgram.on_window("board", "go") # window.* of window "board", item "go"
def board(event): ...
@freshgram.on_long_click("row1", window="board") # *.long_click, any scope
def held(event): ...

on_setting, on_dialog and on_window skip *.long_click events (use on_long_click); on_setting also skips settings.open; on_dialog and on_window without an item id also receive the *.close event. Subscribe to everything with @freshgram.on("settings.*"), "dialog.*", "window.*".

Events from the page count as a user action: for the next 10 seconds the plugin may open a dialog or window and call ui.open_settings (see limits).

Send the page again with the same call. If the “structure” is unchanged, the app updates the existing widgets in place (texts, labels, values, disabled, hints, progress, table cells, list titles). The structure is: the types and ids of all items, icon, style, multiline, password, whether placeholder is set, slider min/max/step, the number of children of row/column/card and the card title, tab ids/labels/child counts, list entry ids/icons, table dimensions and the page buttons. If the structure changes, the page is rebuilt from scratch (pending input is sent first). For a settings page, send a new page from the settings.open handler and after any change of state. Windows use ui.window.update.

@freshgram.on_window("job", "stop")
def stop(event):
freshgram.ui.update_window("job", "Job", [
{"type": "progress", "label": "Stopped", "value": 100},
{"type": "button", "id": "stop", "label": "Stop", "disabled": True},
])
  • Dialog (ui.dialog.show): one per plugin and account. Showing a dialog while it is open replaces its page.
  • Window (ui.window.show / ui.window.update): identified by id (1 to 32 characters a-z 0-9 _ - .), up to 3 open at once per plugin.
  • mode (windows only): "layer" (default), a box over the chat, with the page buttons at the bottom; or "window", a separate panel with the buttons as rows at the end. An invalid value gives invalid (mode). The mode is chosen when the window opens; later calls for an open window keep its mode.
Rule Value Error
open windows per plugin 3 limit: too many windows are open
updates of one window not more often than every 100 ms limit: windows are updated too often
opening a dialog or window The first one is free. A further one within 60 s of the previous opening is allowed only if the user acted (any click/change event, a menu click, a command) in the last 10 s; for a dialog, a dialog that is still open also counts, for a window it does not. limit: dialogs open after a user action, windows open after a user action
ui.window.update of a window that is not open not_found: the window is not open
ui.open_settings within 10 s after a user action; one action allows one call denied: only right after a user action

Without the permission the call fails with denied (the message is the permission name). When the app has no UI to show the page the call fails with unavailable.

An icon is one of three things:

  1. A built-in icon name (list below).
  2. A name of the plugin’s own icon from contributes.icons in the manifest.
  3. peer:<chat id> or emoji:<custom emoji id>: digits only, up to 20; the app draws a chat avatar or a custom emoji from its own data. At most 32 of these per page. peer: needs the permission chat.info or chats.list, works only for chats in the plugin’s scope, and is not drawn for hidden chats, the service chat or in streamer mode. emoji: is drawn only if the app can load that emoji.

A name is validated only by its shape: 1 to 32 characters of a-z 0-9 _ -. An unknown name is not an error: the app draws a generic bot icon instead. The shape error is icon must be a lowercase name, peer:<id> or emoji:<id>.

Built-in icons: add, archive, block, bot, cancel, chat, chats, clear, collapse, commands, copy, customize, delete, devices, download, edit, emoji, expand, export, file, folder, font, forward, gif, hide, hourglass, info, like, link, lock, mark_read, mute, network, night, notifications, palette, permissions, phone, photo, pin, premium, profile, qr, reactions, remove, reply, report, restore, schedule, search, select, send, settings, share, show, silent, sound_off, sound_on, spoiler, star, stats, stickers, storage, timer, translate, unmute, unpin, users, zoom_in, zoom_out. A plugin’s own icon with the same name as a built-in one is never used: the built-in wins.

Own icons are declared in the manifest and listed in files:

"contributes": {
"icons": { "rocket": "icons/rocket.svg", "logo": "icons/logo.png" },
"menus": [ { "surface": "chat", "id": "launch", "title": "Launch", "icon": "rocket" } ]
}

At most 32 icons; names are 1 to 32 characters a-z 0-9 _ - (keys starting with x- are ignored); the path must be a safe relative path to a .png or .svg file of at most 64 KiB that is listed in files. Icons are drawn as a one-colour mask in the colour of the surrounding interface, so use a single-colour image with a transparent background. The same name is then valid as icon in menu items, page items and badges.

"permissions": { "required": [ { "id": "ui.menu", "reason": "Adds a message action." } ] },
"contributes": { "menus": [
{ "surface": "message", "id": "copy-link", "title": "Copy link", "icon": "link",
"when": { "chat_type": ["group", "supergroup"], "has_text": true } }
] }

Up to 32 entries. Needs the permission ui.menu (and ui.composer_button for the composer surface).

Field Type Required Limit
surface text yes one of the surfaces below
id text yes 1 to 64 characters of a-z 0-9 - (no underscore, no dot)
title text or map of language to text yes 1 to 80 bytes
icon text no a built-in icon or an own icon from contributes.icons; unknown names fail the manifest
when map no conditions, see below

Surfaces:

surface Where it appears Event fields besides type, surface, id
message context menu of a message chat, topic, message, from, text
chat context menu of a chat chat, topic
profile menu of a profile chat, topic
composer buttons next to the input field (at most 3 are shown, the rest go into an overflow menu) chat, topic, reply_to; button: "right" on a right click
selection menu of a selection of messages (up to 100) chat, ids (message ids)
shared_media menu of a selection in shared media (up to 100) same as selection
main_menu the main (side) menu none

The app also passes secret and self, but the host always removes them from these events. chat, topic and reply_to are removed when the chat is outside the scope the user granted, and message, from, text and ids are removed unless the plugin has messages.read; read such keys with event.get(...). See Event payloads. | tray | the tray icon menu | none |

The event is menu.click. Handle it with @freshgram.on_menu("copy-link", surface="message"). The user can hide and reorder menu items in the settings; hidden items are not shown. The service chat, hidden chats and chats outside the plugin’s chat scope never show items. A message event is only sent for server messages. The menu.<surface> features can be required in the manifest for compatibility checks.

All given conditions must hold (for selection and shared_media, for every selected message). Unknown keys fail the manifest. A message condition on a surface without messages fails with <key> does not apply to the <surface> surface; main_menu and tray take no conditions at all.

Key Type Surfaces Meaning
chat_type list of up to 16 of user, bot, self, group, supergroup, forum, channel all except main_menu, tray the chat is of one of these types
out bool message, selection, shared_media the message is outgoing (true) or incoming (false)
has_text bool same the message has text
has_media bool same the message has media (a web page preview does not count)
media list of up to 16 of photo, video, gif, round, voice, audio, file, sticker, poll, contact, location, webpage, other same the media kind is one of these
forwarded bool same the message is forwarded
freshgram.ui.toast("Saved")
freshgram.ui.toast("Deleted", action="undo-1", undo=True, duration=6, title="Messages")
freshgram.ui.toast("Open the report", action="open", label="Open")
@freshgram.on_toast("undo-1")
def undo(event): ... # toast.click, event["id"] == "undo-1"
@freshgram.on_toast_close("undo-1")
def gone(event): ... # toast.close, event["reason"] in ("action", "timeout")

Permission ui.toast. Arguments of the operation ui.toast:

Argument Type Limit / default
text text required, up to 200 bytes
title text up to 64 bytes, no newline; shown as the first line
duration int seconds, 1 to 8, default 5
style text default or undo (Python: undo=True)
action map {"id": ..., "label": ...} (Python: action="id", label="..."): id 1 to 32 bytes; label up to 40 bytes, required unless style is undo (then “Undo” is shown)

style: "undo" requires action. The plugin id is added in front of the text (or of the title). Not more than one toast per 2 seconds (limit: toasts are shown too often). Errors are invalid with the argument name (text, title, style, duration, action). Only toasts with an action send events: toast.click (id) when the button is pressed and toast.close (id, reason: action or timeout) when the toast goes away.

Permission ui.badges.

freshgram.badges.set(user_id, [
{"id": "vip", "icon": "star", "tooltip": "VIP"},
{"id": "logo", "image": open("logo.png", "rb").read()},
])
freshgram.badges.set(user_id, []) # remove this user's badges
freshgram.badges.clear() # remove all of the plugin's badges

badges.set takes user (a positive id other than the service chat) and items, a list of up to 4 maps with exactly these keys:

Field Type Required Limit
id id yes up to 32 characters, unique in the list
icon text one of icon / image a built-in or own icon name (peer: and emoji: are not allowed): a badge has a bad icon name
image bytes one of icon / image up to 64 KiB; PNG up to 128 x 128, or JPEG (a badge image is a PNG up to 128x128 or a JPEG)
tooltip text no up to 80 bytes, one line

Exactly one of icon and image (a badge has an icon or an image). Other badge errors: badges are a list of up to 4 entries, badges must be objects, a badge needs a unique id of up to 32 characters, a badge image is up to 64 KiB (empty, not bytes or too large). A plugin may have badges on up to 256 users (quota). A click arrives as badge.click with id, user, surface (message, chat, profile or chats) and, for a message, chat and message; handle it with @freshgram.on_badge_click("vip").

All of these come back as invalid (freshgram.Invalid), with the text as the message.

Message Cause
the page must be an object the page is not a map
unknown field <name> an unknown key on the page or on an item, option, tab, entry or button
<field> is required a required text field is missing
<field> is not valid text wrong type, too long, control or bidirectional characters, or a newline where none is allowed
<field> must be true or false a bool field has another type
<field> is out of range an int field has another type, is a float, or is out of its range
items must be a list of at most 64 entries items is missing, not a list or too long
the page is too large more than 256 items or nesting deeper than 4
every item must be an object / an item has no type an item is not a map or has no type
unknown item type <type> the type is not in the table; the shown type is cut to its first 20 bytes
an item has a bad or repeated id see Identifiers
alias must be a unique lowercase token of up to 32 characters bad or repeated alias
icon must be a lowercase name, peer:<id> or emoji:<id> bad icon
a page may use at most 32 peer and emoji icons too many rich icons
style must be ... a style outside the allowed set
buttons must be a list of at most 3 entries / bad button bad buttons
closable must be true or false / a page that cannot be closed needs a button bad closable

The remaining messages are given next to the item type. Wrong operation arguments give invalid with the argument name: page, id, mode.

Taken from the style of the ui_demo test plugin:

import freshgram
@freshgram.on_init
def start(info):
freshgram.ui.settings_page("Demo", [
{"type": "toggle", "id": "loud", "label": "Loud", "value": False},
{"type": "button", "id": "go", "label": "Open board", "icon": "settings"},
])
@freshgram.on_setting("go")
def go(event):
freshgram.ui.window("board", "Board", [
{"type": "card", "title": "Status", "items": [
{"type": "row", "items": [
{"type": "text", "text": "Ready", "style": "accent"},
{"type": "button", "id": "run", "label": "Run", "style": "primary", "long_click": True},
]},
{"type": "progress", "label": "Progress", "value": 40},
]},
{"type": "tabs", "id": "view", "tabs": [
{"id": "log", "label": "Log", "items": [{"type": "code", "text": "line 1\nline 2"}]},
{"id": "tbl", "label": "Data", "items": [
{"type": "table", "columns": ["Name", "Count"], "rows": [["a", "1"], ["b", "2"]]},
]},
]},
], buttons=[{"id": "close", "label": "Done"}], mode="window")
@freshgram.on_window("board", "run")
def run(event):
freshgram.ui.update_window("board", "Board", [
{"type": "progress", "label": "Done", "value": 100},
])
@freshgram.on_long_click("run", window="board")
def held(event):
freshgram.ui.toast("Long press")

Note that the update_window call above changes the structure, so the window is rebuilt; it also must come more than 100 ms after the previous update of that window.