telega/keyboard

Keyboard Module

This module provides comprehensive keyboard functionality for Telegram bots, including:

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

Inline Button Types

Error Handling

The module uses Result types for operations that can fail:

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

  1. Use builder pattern for complex keyboards (recommended over arrays)
  2. Use typed callback data for better type safety
  3. Validate callback data length before creating buttons
  4. Use grid layouts for better UX with many buttons
  5. Handle Result types properly when using validation functions
  6. 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

Types

pub opaque type InlineKeyboard

An inline keyboard builder for creating inline keyboards with method chaining.

pub opaque type InlineKeyboardBuilder
pub opaque type Keyboard

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 button(text: String) -> types.KeyboardButton

Create a new keyboard button

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 Error if the keyboard has no callback buttons
  • Returns Error if 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 type
  • serialize: Function to convert your data to a string
  • deserialize: 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 one_time(keyboard: Keyboard) -> Keyboard

Make the keyboard one-time

pub fn pack_callback(
  callback_data callback_data: KeyboardCallbackData(data),
  data data: data,
) -> KeyboardCallback(data)

Pack callback data into a callback

pub fn persistent(keyboard: Keyboard) -> Keyboard

Make the keyboard persistent

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 resized(keyboard: Keyboard) -> Keyboard

Make the keyboard resizable

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 single(button: types.KeyboardButton) -> Keyboard

Create a single-button keyboard

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

โœจ Search Document