# Keyboards

> Static and dynamic keyboard layouts for bot UI.

## Static Keyboard

The `Keyboard` model defines a fixed set of buttons arranged in rows.

| Field | Type | Default | Description |
|---|---|---|---|
| `buttons` | `list[list[Button]]` | required | Row-major grid. Outer list = rows, inner list = buttons within a row. |
| `is_inline` | `bool` | `True` | `True` for inline keyboard (attached under the message). `False` for reply keyboard (persistent panel at the bottom of the screen). |
| `resize_keyboard` | `bool` | `False` | Shrink reply keyboard to fit its buttons. Only applies when `is_inline=False`. |
| `one_time_keyboard` | `bool` | `False` | Hide reply keyboard after a button is pressed. Only applies when `is_inline=False`. |

## Button

Every button in a `Keyboard` is a `Button` instance.

| Field | Type | Default | Description |
|---|---|---|---|
| `text` | `str \| DynamicContent \| LocalizedContent` | required | The button label displayed to the user. |
| `callback_action` | `str \| None` | `None` | Links to a `Handler.name` in the SAME scene. Pressing the button triggers that handler's actions. |
| `callback_global` | `str \| None` | `None` | Links to a `GlobalHandler.name`. Used for cross-scene actions that are not tied to the current scene. |
| `callback_params` | `dict[str, str]` | `{}` | Key-value pairs encoded into callback data. These are automatically saved as user input when the button is pressed. Read them in the handler via `context.get_user_input("key")`. |
| `url` | `str \| DynamicContent \| None` | `None` | Opens a URL when pressed. No callback is fired. |
| `web_app_url` | `str \| None` | `None` | Opens a Telegram Web App at this URL. |
| `style` | `"danger" \| "success" \| "primary" \| None` | `None` | Visual hint. The framework adds an emoji prefix to the button text based on the style value. |
| `switch_inline_query` | `str \| None` | `None` | Switches to inline mode in another chat with the given query prefilled. |
| `switch_inline_query_current_chat` | `str \| None` | `None` | Switches to inline mode in the current chat. |
| `switch_inline_query_chosen_chat` | `SwitchInlineQueryChosenChat \| None` | `None` | Lets the user pick a chat for inline mode. |
| `icon_custom_emoji_id` | `str \| None` | `None` | Custom emoji ID displayed as a button icon. |

## How Callback Routing Works

When a user presses a button with `callback_action="buy"`, the framework looks up the `Handler` whose `name` equals `"buy"` in the current scene's `handlers` list and executes that handler's actions. For `callback_global="cancel"`, it looks up the `GlobalHandler` with `name="cancel"` instead, which works across all scenes.

`callback_params` are automatically injected into the execution context as user input. For example, a button with `callback_params={"product_id": "42"}` makes `context.get_user_input("product_id")` return `"42"` inside the triggered handler.

## Dynamic Keyboard

The `DynamicKeyboard` model generates buttons from a data source at render time.

| Field | Type | Default | Description |
|---|---|---|---|
| `type` | `Literal["dynamic"]` | `"dynamic"` | Discriminator literal. |
| `data_source` | `str` | required | Registry name of a getter function that returns a list of items. |
| `layout` | `"grid" \| "list" \| "inline"` | `"grid"` | How buttons are arranged. |
| `columns` | `int` | `2` | Number of columns per row (for grid layout). |
| `button_template` | `ButtonTemplate` | required | Template applied to each item from the data source. |
| `pagination` | `PaginationConfig` | see below | Pagination settings. |
| `header_buttons` | `list[list[Button]]` | `[]` | Static button rows rendered above the dynamic content. |
| `footer_buttons` | `list[list[Button]]` | `[]` | Static button rows rendered below the dynamic content. |
| `is_inline` | `bool` | `True` | Same as `Keyboard.is_inline`. |

### ButtonTemplate

| Field | Type | Default | Description |
|---|---|---|---|
| `text` | `str` | required | Template string. Item fields are interpolated with `{field_name}` syntax, e.g. `"{name} - ${price}"`. |
| `callback_action` | `str` | required | Handler name to invoke when the generated button is pressed. |
| `callback_params` | `dict[str, str]` | `{}` | Template for params. Values support `{field_name}` interpolation. |
| `condition` | `str \| None` | `None` | Registry function name. The item is shown only if this function returns `True`. |
| `style` | same as `Button.style` | `None` | Visual hint for generated buttons. |
| `icon_custom_emoji_id` | `str \| None` | `None` | Custom emoji icon for generated buttons. |

### PaginationConfig

| Field | Default | Description |
|---|---|---|
| `enabled` | `False` | Whether pagination is active. |
| `page_size` | `10` | Items per page. |
| `cache_ttl` | `300` | Cache TTL in seconds; `None` disables caching. |
| `prev_button_text` | `"◀️"` | Label for the previous-page button. |
| `next_button_text` | `"▶️"` | Label for the next-page button. |

## Examples

Static inline keyboard with two rows:

```python
Keyboard(buttons=[
    [Button(text="📦 Products", callback_action="show_products"),
     Button(text="👤 Profile", callback_action="show_profile")],
    [Button(text="❓ Help", url="https://example.com/help")],
])
```

Button with `callback_params` for product selection:

```python
Button(
    text="iPhone 15",
    callback_action="select_product",
    callback_params={"product_id": "iphone15"},
)
# In the handler: context.get_user_input("product_id") -> "iphone15"
```

Dynamic keyboard generated from a data source:

```python
DynamicKeyboard(
    type="dynamic",
    data_source="get_products_list",
    layout="grid",
    columns=2,
    button_template=ButtonTemplate(
        text="{name} - ${price}",
        callback_action="select_product",
        callback_params={"product_id": "{id}"},
    ),
    pagination=PaginationConfig(enabled=True, page_size=6),
    footer_buttons=[[Button(text="◀️ Back", callback_action="go_back")]],
)
```
