A library provides declarative UI for discord.py.
- Declarative UI, inspired by React.
- Component-oriented with State
- Fully typed
Python3.10 or higher is required
discord.py^2.2.0 is required; any compatibility under 2.1.x or 1.x is not guaranteed
Using the latest stable release of discord.py is recommended
pip install ductile-ui
You can define component as return value of View.render()
.
To store state, use State
.
# This example requires the 'message_content' privileged intent to function.
import random
import discord
from discord.ext import commands
from ductile import State, View, ViewObject
from ductile.controller import MessageableController
from ductile.ui import Button
class Bot(commands.Bot):
def __init__(self) -> None:
super().__init__(command_prefix="!", intents=discord.Intents.all())
async def on_ready(self) -> None:
print(f"Logged in as {self.user}")
print("Ready!")
class CounterView(View):
def __init__(self) -> None:
super().__init__()
self.count = State(0, self)
def render(self) -> ViewObject:
e = discord.Embed(title="Counter", description=f"Count: {self.count.get_state()}")
async def handle_increment(interaction: discord.Interaction) -> None:
await interaction.response.defer()
self.count.set_state(lambda x: x + 1)
async def handle_decrement(interaction: discord.Interaction) -> None:
await interaction.response.defer()
self.count.set_state(lambda x: x - 1)
async def stop(interaction: discord.Interaction) -> None:
await interaction.response.defer()
self.stop()
# Define UI using ViewObject
return ViewObject(
embeds=[e],
components=[
Button("+1", style={"color": "blurple", "row": 0}, on_click=handle_increment),
Button("-1", style={"color": "blurple", "row": 0}, on_click=handle_decrement),
Button(
"random",
style={"color": "green", "row": 1},
# if you passed synchronous function to Button.on_click,
# library automatically calls `await interaction.response.defer()`.
on_click=lambda _: self.count.set_state(random.randint(0, 100)),
),
Button("stop", style={"color": "red", "row": 1}, on_click=stop),
],
)
bot = Bot()
@bot.command(name="counter")
async def send_counter(ctx: commands.Context) -> None:
controller = MessageableController(CounterView(), messageable=ctx.channel)
await controller.send()
timed_out, states = await controller.wait()
await ctx.send(f"Timed out: {timed_out}\nCount: {states['count']}")
bot.run("MY_COOL_BOT_TOKEN")