telega/keyboard
Keyboard Module
This module provides comprehensive keyboard functionality for Telegram bots, including:
- Regular reply keyboards with special buttons (contact, location, polls, etc.)
- Inline keyboards with callback data, URLs, and other interactive elements
- Helper functions for creating grid layouts and complex keyboard structures
- Type-safe callback data handling with validation
- Error handling and validation for Telegram API constraints
Quick Start
Basic Reply Keyboard
// Traditional array-based approach
let keyboard = keyboard.new([
[keyboard.button("Option 1"), keyboard.button("Option 2")],
[keyboard.contact_button("๐ Share Contact"), keyboard.location_button("๐ Location")],
])
|> keyboard.one_time()
|> keyboard.resized()
// New builder pattern approach (recommended)
let keyboard = keyboard.builder()
|> keyboard.text("Option 1")
|> keyboard.text("Option 2")
|> keyboard.next_row()
|> keyboard.contact("๐ Share Contact")
|> keyboard.location("๐ Location")
|> keyboard.build()
|> keyboard.one_time()
|> keyboard.resized()
Inline Keyboard with Callbacks
let callback_data = keyboard.string_callback_data("action")
// Traditional approach
let assert Ok(button) = keyboard.inline_button("Click Me",
keyboard.pack_callback(callback_data, "click"))
let keyboard = keyboard.new_inline([[
button,
keyboard.inline_url_button("Visit", "https://example.com"),
]])
// Builder pattern approach (recommended)
let callback = keyboard.pack_callback(callback_data, "click")
let assert Ok(keyboard) = {
use builder <- result.try(keyboard.inline_builder()
|> keyboard.inline_text("Click Me", callback))
let builder = keyboard.inline_url(builder, "Visit", "https://example.com")
Ok(keyboard.inline_build(builder))
}
// In your handler:
let assert Ok(filter) = keyboard.filter_inline_keyboard_query(keyboard)
Grid Layouts
let buttons = [
keyboard.button("1"), keyboard.button("2"),
keyboard.button("3"), keyboard.button("4"),
]
let grid_keyboard = keyboard.grid(buttons, 2) // 2x2 grid
Special Button Types
- Contact buttons: Request userโs phone number
- Location buttons: Request userโs location
- Poll buttons: Allow users to create polls
- Web App buttons: Launch mini-apps
- User/Chat request buttons: Request access to users or chats
Inline Button Types
- Callback buttons: Execute bot commands with data
- URL buttons: Open external links
- Switch inline buttons: Switch to inline mode
- Copy text buttons: Copy text to clipboard
- Web app buttons: Launch inline web applications
Error Handling
The module uses Result types for operations that can fail:
- Callback data validation (max 64 bytes)
- Inline button creation with validation
- Filter creation for callback queries
Builder Pattern API
The keyboard module provides a fluent builder API similar to Grammy/GramIO:
Reply Keyboard Builder
let keyboard = keyboard.builder()
|> keyboard.text("Yes")
|> keyboard.text("No")
|> keyboard.next_row()
|> keyboard.contact("๐ Contact")
|> keyboard.location("๐ Location")
|> keyboard.next_row()
|> keyboard.web_app("Open App", "https://example.com")
|> keyboard.build()
|> keyboard.one_time()
Inline Keyboard Builder
let callback_data = keyboard.string_callback_data("menu")
let result = keyboard.inline_builder()
|> keyboard.inline_text("Settings", keyboard.pack_callback(callback_data, "settings"))
|> result.try(keyboard.inline_text(_, "About", keyboard.pack_callback(callback_data, "about")))
|> result.try(fn(builder) {
let builder = keyboard.inline_next_row(builder)
let builder = keyboard.inline_url(builder, "Help", "https://help.example.com")
Ok(keyboard.inline_build(builder))
})
Best Practices
- Use builder pattern for complex keyboards (recommended over arrays)
- Use typed callback data for better type safety
- Validate callback data length before creating buttons
- Use grid layouts for better UX with many buttons
- Handle Result types properly when using validation functions
- Use one-time keyboards for single-use interactions
Advanced Usage Patterns
Dynamic Keyboards
// Create keyboards based on data
pub fn create_user_list_keyboard(users: List(User)) -> InlineKeyboard {
let user_callback = keyboard.int_callback_data("select_user")
let buttons = list.filter_map(users, fn(user) {
let callback = keyboard.pack_callback(user_callback, user.id)
case keyboard.inline_button(user.name, callback) {
Ok(button) -> Some(button)
Error(_) -> None
}
})
keyboard.inline_grid(buttons, 2) // 2 columns
}
Pagination Pattern
pub fn create_pagination_keyboard(
current_page: Int,
total_pages: Int,
) -> InlineKeyboard {
let page_callback = keyboard.int_callback_data("page")
let buttons = []
// Build buttons list
let buttons = case current_page > 1 {
True -> {
let prev_callback = keyboard.pack_callback(page_callback, current_page - 1)
case keyboard.inline_button("โ Previous", prev_callback) {
Ok(prev_btn) -> [prev_btn, ..buttons]
Error(_) -> buttons
}
}
False -> buttons
}
// Add page indicator
let page_info = int.to_string(current_page) <> "/" <> int.to_string(total_pages)
let info_btn = keyboard.inline_copy_text_button(page_info, page_info)
let buttons = [info_btn, ..buttons]
// Add next button
let buttons = case current_page < total_pages {
True -> {
let next_callback = keyboard.pack_callback(page_callback, current_page + 1)
case keyboard.inline_button("Next โ", next_callback) {
Ok(next_btn) -> [next_btn, ..buttons]
Error(_) -> buttons
}
}
False -> buttons
}
keyboard.new_inline([list.reverse(buttons)])
}
Error Handling Best Practices
// Always handle Result types properly
case keyboard.inline_button("Action", callback) {
Ok(button) -> {
let keyboard = keyboard.inline_single(button)
reply.with_markup(ctx, "Choose action:", keyboard.to_inline_markup(keyboard))
}
Error(msg) -> {
// Log the error and provide fallback
logging.log(logging.Error, "Keyboard error: " <> msg)
reply.with_text(ctx, "Sorry, something went wrong.")
}
}
Performance Tips
- Use
grid()andinline_grid()for better organization - Keep callback data under 64 bytes (validated automatically)
- Use typed callback data for better type safety
- Cache callback data configurations to avoid recreation
- Consider using
one_time()keyboards for single interactions
Types
pub opaque type InlineKeyboard
An inline keyboard builder for creating inline keyboards with method chaining.
pub opaque type InlineKeyboardBuilder
A keyboard builder for creating keyboards with method chaining. This provides a more ergonomic API similar to Grammy/GramIO.
pub opaque type KeyboardBuilder
pub type KeyboardCallback(data) {
KeyboardCallback(
data: data,
id: String,
payload: String,
callback_data: KeyboardCallbackData(data),
)
}
Constructors
-
KeyboardCallback( data: data, id: String, payload: String, callback_data: KeyboardCallbackData(data), )
pub opaque type KeyboardCallbackData(data)
Values
pub fn bool_callback_data(
id: String,
) -> KeyboardCallbackData(Bool)
Create a boolean callback data configuration. Useful for toggle buttons, settings, or yes/no choices.
Example
let toggle_callback = keyboard.bool_callback_data("notifications")
let enable_btn = keyboard.pack_callback(toggle_callback, True)
let disable_btn = keyboard.pack_callback(toggle_callback, False)
let assert Ok(enable) = keyboard.inline_button("๐ Enable", enable_btn)
let assert Ok(disable) = keyboard.inline_button("๐ Disable", disable_btn)
// In your callback handler:
let assert Ok(unpacked) = keyboard.unpack_callback(payload, toggle_callback)
case unpacked.data {
True -> // enable notifications
False -> // disable notifications
}
pub fn build(builder: KeyboardBuilder) -> Keyboard
Build the final keyboard from the builder.
pub fn builder() -> KeyboardBuilder
Create a new keyboard builder for method chaining. This provides a more ergonomic API for building complex keyboards.
Example
let keyboard = keyboard.builder()
|> keyboard.text("Option 1")
|> keyboard.text("Option 2")
|> keyboard.next_row()
|> keyboard.text("Cancel")
|> keyboard.build()
pub fn chat_button(
text: String,
request_id: Int,
chat_is_channel: Bool,
chat_is_forum: option.Option(Bool),
chat_has_username: option.Option(Bool),
chat_is_created: option.Option(Bool),
user_administrator_rights: option.Option(
types.ChatAdministratorRights,
),
bot_administrator_rights: option.Option(
types.ChatAdministratorRights,
),
bot_is_member: option.Option(Bool),
request_title: option.Option(Bool),
request_username: option.Option(Bool),
request_photo: option.Option(Bool),
) -> types.KeyboardButton
Create a button that requests a chat to be shared
pub fn contact(
builder: KeyboardBuilder,
text: String,
) -> KeyboardBuilder
Add a contact request button to the current row of the keyboard builder.
pub fn contact_button(text: String) -> types.KeyboardButton
Create a button that requests the userโs contact information. When pressed, the user will be prompted to share their phone number.
Example
let contact_btn = keyboard.contact_button("๐ Share Contact")
let keyboard = keyboard.single(contact_btn)
pub fn filter_inline_keyboard_query(
keyboard: InlineKeyboard,
) -> Result(bot.CallbackQueryFilter, String)
Create a filter for inline keyboard callback queries.
This filter can be used with telega.wait_callback_query() to only listen
for callbacks from buttons in this specific keyboard.
Example
let callback_data = keyboard.string_callback_data("action")
let assert Ok(button1) = keyboard.inline_button("Yes",
keyboard.pack_callback(callback_data, "yes"))
let assert Ok(button2) = keyboard.inline_button("No",
keyboard.pack_callback(callback_data, "no"))
let keyboard = keyboard.new_inline([[button1, button2]])
let assert Ok(filter) = keyboard.filter_inline_keyboard_query(keyboard)
// In your handler:
use ctx, payload, query_id <- telega.wait_callback_query(
ctx:, filter:, or: None, timeout: None
)
let assert Ok(callback) = keyboard.unpack_callback(payload, callback_data)
Errors
- Returns
Errorif the keyboard has no callback buttons - Returns
Errorif the callback data creates an invalid regex pattern
pub fn grid(
buttons: List(types.KeyboardButton),
columns: Int,
) -> Keyboard
Create a grid keyboard with buttons arranged in rows of specified width. This is useful for creating organized layouts with many buttons.
Example
let buttons = [
keyboard.button("1"), keyboard.button("2"), keyboard.button("3"),
keyboard.button("4"), keyboard.button("5"), keyboard.button("6"),
]
let grid_keyboard = keyboard.grid(buttons, 3) // 3 columns per row
// Results in: [[1, 2, 3], [4, 5, 6]]
pub fn hear(keyboard: Keyboard) -> bot.Hears
Extract button texts from a keyboard for use with telega.wait_hears().
This is useful for listening to button presses in conversation flows.
Example
let keyboard = keyboard.new([[keyboard.button("Yes"), keyboard.button("No")]])
let hears = keyboard.hear(keyboard)
use _, text <- telega.wait_hears(ctx:, hears:, or: None, timeout: None)
case text {
"Yes" -> // handle yes
"No" -> // handle no
_ -> // handle other
}
pub fn inline_build(
builder: InlineKeyboardBuilder,
) -> InlineKeyboard
Build the final inline keyboard from the builder.
pub fn inline_builder() -> InlineKeyboardBuilder
Create a new inline keyboard builder for method chaining. This provides a more ergonomic API for building complex inline keyboards.
Example
let callback_data = keyboard.string_callback_data("action")
let keyboard = keyboard.inline_builder()
|> keyboard.inline_text("Button 1", keyboard.pack_callback(callback_data, "btn1"))
|> keyboard.inline_text("Button 2", keyboard.pack_callback(callback_data, "btn2"))
|> keyboard.inline_next_row()
|> keyboard.inline_url("Visit", "https://example.com")
|> keyboard.inline_build()
pub fn inline_button(
text text: String,
callback_data callback_data: KeyboardCallback(data),
) -> Result(types.InlineKeyboardButton, String)
Create a new inline button with callback data
pub fn inline_copy_text(
builder: InlineKeyboardBuilder,
text: String,
copy_text: String,
) -> InlineKeyboardBuilder
Add an inline copy text button to the current row.
pub fn inline_copy_text_button(
text text: String,
copy_text copy_text: String,
) -> types.InlineKeyboardButton
Create an inline copy text button
pub fn inline_grid(
buttons: List(types.InlineKeyboardButton),
columns: Int,
) -> InlineKeyboard
Create a grid inline keyboard with buttons arranged in rows of specified width
pub fn inline_next_row(
builder: InlineKeyboardBuilder,
) -> InlineKeyboardBuilder
Start a new row in the inline keyboard builder.
pub fn inline_row(
buttons: List(types.InlineKeyboardButton),
) -> List(types.InlineKeyboardButton)
Create an inline keyboard row from a list of buttons
pub fn inline_single(
button: types.InlineKeyboardButton,
) -> InlineKeyboard
Create a single-button inline keyboard
pub fn inline_switch_query(
builder: InlineKeyboardBuilder,
text: String,
query: String,
) -> InlineKeyboardBuilder
Add an inline switch query button to the current row.
pub fn inline_switch_query_button(
text text: String,
query query: String,
) -> types.InlineKeyboardButton
Create an inline switch query button
pub fn inline_switch_query_current_chat_button(
text text: String,
query query: String,
) -> types.InlineKeyboardButton
Create an inline switch query current chat button
pub fn inline_text(
builder: InlineKeyboardBuilder,
text: String,
callback: KeyboardCallback(data),
) -> Result(InlineKeyboardBuilder, String)
Add an inline text button with callback data to the current row.
pub fn inline_to_markup(
keyboard: InlineKeyboard,
) -> types.SendMessageReplyMarkupParameters
Build a reply markup for Message from an inline keyboard
pub fn inline_url(
builder: InlineKeyboardBuilder,
text: String,
url: String,
) -> InlineKeyboardBuilder
Add an inline URL button to the current row.
pub fn inline_url_button(
text text: String,
url url: String,
) -> types.InlineKeyboardButton
Create an inline URL button
pub fn inline_web_app(
builder: InlineKeyboardBuilder,
text: String,
url: String,
) -> InlineKeyboardBuilder
Add an inline web app button to the current row.
pub fn inline_web_app_button(
text text: String,
url url: String,
) -> types.InlineKeyboardButton
Create an inline web app button
pub fn int_callback_data(id: String) -> KeyboardCallbackData(Int)
Create an integer callback data configuration. Useful for pagination, user IDs, or any numeric data.
Example
let page_callback = keyboard.int_callback_data("page")
let next_page = keyboard.pack_callback(page_callback, 2)
let assert Ok(button) = keyboard.inline_button("Next โ", next_page)
// In your callback handler:
let assert Ok(unpacked) = keyboard.unpack_callback(payload, page_callback)
let page_number = unpacked.data // Int
pub fn location(
builder: KeyboardBuilder,
text: String,
) -> KeyboardBuilder
Add a location request button to the current row of the keyboard builder.
pub fn location_button(text: String) -> types.KeyboardButton
Create a button that requests the userโs location. When pressed, the user will be prompted to share their current location.
Example
let location_btn = keyboard.location_button("๐ Share Location")
let keyboard = keyboard.single(location_btn)
pub fn new(buttons: List(List(types.KeyboardButton))) -> Keyboard
Create a new reply keyboard from a list of button rows.
Example
let keyboard = keyboard.new([
[keyboard.button("Yes"), keyboard.button("No")],
[keyboard.button("Cancel")],
])
pub fn new_callback_data(
id id: String,
serialize serialize: fn(data) -> String,
deserialize deserialize: fn(String) -> data,
) -> KeyboardCallbackData(data)
Create a new callback data configuration for inline keyboard buttons. This defines how data is serialized/deserialized when buttons are pressed.
Parameters
id: Unique identifier for this callback typeserialize: Function to convert your data to a stringdeserialize: Function to convert string back to your data
Example
// For custom enum types
pub type Action {
Save
Delete
Edit
}
let action_callback = keyboard.new_callback_data(
id: "action",
serialize: fn(action) {
case action {
Save -> "save"
Delete -> "delete"
Edit -> "edit"
}
},
deserialize: fn(str) {
case str {
"save" -> Save
"delete" -> Delete
_ -> Edit
}
}
)
pub fn new_inline(
buttons: List(List(types.InlineKeyboardButton)),
) -> InlineKeyboard
Create a new inline keyboard from a list of button rows. Inline keyboards appear directly below messages and donโt replace the userโs keyboard.
Example
let callback_data = keyboard.string_callback_data("action")
let assert Ok(callback_btn) = keyboard.inline_button("Click",
keyboard.pack_callback(callback_data, "click"))
let keyboard = keyboard.new_inline([
[callback_btn, keyboard.inline_url_button("Visit", "https://example.com")],
[keyboard.inline_copy_text_button("Copy", "Hello World!")],
])
pub fn next_row(builder: KeyboardBuilder) -> KeyboardBuilder
Start a new row in the keyboard builder.
pub fn pack_callback(
callback_data callback_data: KeyboardCallbackData(data),
data data: data,
) -> KeyboardCallback(data)
Pack callback data into a callback
pub fn placeholder(keyboard: Keyboard, text: String) -> Keyboard
Set the placeholder for the input field
pub fn poll_button(
text: String,
poll_type poll_type: option.Option(String),
) -> types.KeyboardButton
Create a button that requests poll creation
pub fn remove() -> types.SendMessageReplyMarkupParameters
Remove keyboard markup - useful for hiding keyboards
pub fn row(
buttons: List(types.KeyboardButton),
) -> List(types.KeyboardButton)
Create a keyboard row from a list of buttons
pub fn selected(keyboard: Keyboard) -> Keyboard
Make the keyboard selective. Use this parameter if you want to show the keyboard to specific users only.
pub fn set_callback_data_delimiter(
data: KeyboardCallbackData(data),
delimiter: String,
) -> KeyboardCallbackData(data)
Change the delimiter for the callback data, useful if you need to use : in the id
pub fn string_callback_data(
id: String,
) -> KeyboardCallbackData(String)
Create a simple string callback data configuration. This is the most common type for simple string-based callbacks.
Example
let callback_data = keyboard.string_callback_data("action")
let callback = keyboard.pack_callback(callback_data, "delete_user")
let assert Ok(button) = keyboard.inline_button("Delete", callback)
// In your callback handler:
let assert Ok(unpacked) = keyboard.unpack_callback(payload, callback_data)
case unpacked.data {
"delete_user" -> // handle deletion
_ -> // handle other actions
}
pub fn text(
builder: KeyboardBuilder,
text: String,
) -> KeyboardBuilder
Add a text button to the current row of the keyboard builder.
pub fn to_inline_markup(
keyboard: InlineKeyboard,
) -> types.SendMessageReplyMarkupParameters
Build a reply markup for Message from an inline keyboard
pub fn to_markup(
keyboard: Keyboard,
) -> types.SendMessageReplyMarkupParameters
Build a reply markup for Message from a keyboard
pub fn unpack_callback(
payload payload: String,
callback_data callback_data: KeyboardCallbackData(data),
) -> Result(KeyboardCallback(data), Nil)
Unpack payload into a callback
pub fn users_button(
text: String,
request_id: Int,
user_is_bot: option.Option(Bool),
user_is_premium: option.Option(Bool),
max_quantity: option.Option(Int),
request_name: option.Option(Bool),
request_username: option.Option(Bool),
request_photo: option.Option(Bool),
) -> types.KeyboardButton
Create a button that requests users to be shared
pub fn web_app(
builder: KeyboardBuilder,
text: String,
url: String,
) -> KeyboardBuilder
Add a web app button to the current row of the keyboard builder.
pub fn web_app_button(
text text: String,
url url: String,
) -> types.KeyboardButton
Create a new web app button