Under development, possible breaking changes!
A Python library for building interactive Telegram bot interfaces with reusable UI components.
- ποΈ Modular UI components (Buttons, Checkboxes, Input fields)
- π₯οΈ Screen management with navigation support
- β»οΈ Stateful components with change callbacks
- π± Message and callback query handling built-in
- π Declarative layout (no manual
render()calls) - π‘οΈ Middleware pipeline for cross-cutting concerns
- πΎ Pluggable persistence (in-memory or JSON file)
- π Webhook or polling mode
pip install "tuican[ptb]"
# or for all backends
pip install "tuican[all]"- Create a
.envfile with your bot token:
echo "token=YOUR_BOT_TOKEN" > .env- Create a simple button screen:
import os
from dotenv import load_dotenv
from tuican import Application
from tuican.components import Button, Screen
class MyScreen(Screen):
description = 'main screen'
def __init__(self):
self.button = Button("Click me", on_change=self.handle_click)
super().__init__([self.button], message="click the button")
def handle_click(self, component):
self.message = "Hello world!"
def get_layout(self):
return [[self.button]] # declarative: no manual render() needed
load_dotenv()
token = os.getenv("token")
app = Application(token, {'start': MyScreen}, transport="ptb")
app.run()Basic interactive button with click handler:
Button(text="Click me", on_change=callback_function)Non-interactive text label (renders as a disabled button):
Label(text="Status: active")Horizontal separator line:
HLine() # renders as a divider rowToggleable checkbox with group support:
group = ExclusiveCheckBoxGroup()
CheckBox(text="Option 1", group=group)Note: Setting
checkbox.selected = Trueis a silent low-level override that does not fireon_changeand does not maintainExclusiveCheckBoxGroupinvariants. Usecheck()/uncheck()/toggle()for side-effectful state changes.
Validated input field with configurable prompt:
Input[int](
text="Age:",
validation_function=positive_int,
active_prompt="Enter: " # shown when input is active
)Screen: Base container for components (supportsadd_components()anddelete_components()for dynamic layouts)ScreenGroup: Handles navigation between screens
Screen.get_layout() can return components directly. The library automatically calls render() for you:
def get_layout(self):
return [
[self.btn1, self.btn2], # row 1
[self.checkbox], # row 2
[self.input_field], # row 3
]You can still mix pre-rendered KeyboardButton objects if needed.
Register middleware to handle cross-cutting concerns like auth or rate limiting:
@app.middleware
async def auth_middleware(update):
user_id = get_user_id(update)
if user_id not in ALLOWED_USERS:
await app.backend.send_plain_message(update, "Access denied")
return False
return TrueReturn False to stop processing the update.
User command state is persisted automatically. By default an in-memory store is used (lost on restart). Use JsonFileStateStore to survive restarts:
from tuican.stores import JsonFileStateStore
app = Application(
token,
{'start': MyScreen},
state_store=JsonFileStateStore("bot_state.json")
)Run the bot in webhook mode instead of polling (PTB transport only):
app.run_webhook(
webhook_url="https://your-domain.com/webhook",
listen="0.0.0.0",
port=8080
)Use the Telethon transport for user-bot or client-style interactions. Telethon requires api_id and api_hash from my.telegram.org and does not support webhook mode.
app = Application(
token,
{'start': MyScreen},
transport="telethon",
api_id=12345,
api_hash="your_api_hash",
)
app.run()The Telegram API is abstracted behind the MessageBackend protocol. You can provide a custom backend for testing or integrating with a different Telegram library:
from tuican.backend import MessageBackend
from tuican.update import TuicanUpdate
from tuican.keyboard_button import KeyboardButton
from collections.abc import Sequence
class MyBackend(MessageBackend):
async def send_keyboard_message(
self,
update: TuicanUpdate,
text: str,
keyboard_markup: Sequence[Sequence[KeyboardButton]],
parse_mode: str = "HTML",
) -> None:
...
async def send_plain_message(self, update: TuicanUpdate, text: str) -> None:
...
async def delete_message(self, update: TuicanUpdate, message_id: int) -> None:
...
async def set_bot_commands(self, commands: dict[str, str]) -> None:
...
app = Application(token, screens, backend=MyBackend())Main entry point:
# Signature:
# Application(token, screens: dict[str, StartScreenProtocol], *, transport="ptb", state_store=None, backend=None, api_id=None, api_hash=None)
app = Application(token, screens, transport="ptb", state_store=None, backend=None)Base class with:
handle_callback()- Process button clicksrender()- Create Telegram buttoncall_on_change()- Trigger callbacks
See the examples/ directory for:
hello_world.pyβ Simple counter + name input. Shows basic buttons and text input on a single screen.todo_list.pyβ Full todo app with dynamic layout updates and multi-screen navigation (list β add todo).
- Python 3.13+
- python-dotenv (core)
- python-telegram-bot (optional, for PTB transport)
- telethon (optional, for Telethon transport)
MIT
