Skip to content

Paginators

attrs class Timeout

Attr attributes:

Name Type Description
paginator Paginator

The paginator that this timeout is associated with.

run bool

Whether or not this timeout is currently running.

ping Event

The event that is used to wait the paginator action.

Source code in naff/ext/paginators.py
@define(kw_only=False)
class Timeout:
    paginator: "Paginator" = field()
    """The paginator that this timeout is associated with."""
    run: bool = field(default=True)
    """Whether or not this timeout is currently running."""
    ping: asyncio.Event = asyncio.Event()
    """The event that is used to wait the paginator action."""

    async def __call__(self) -> None:
        while self.run:
            try:
                await asyncio.wait_for(self.ping.wait(), timeout=self.paginator.timeout_interval)
            except asyncio.TimeoutError:
                if self.paginator.message:
                    await self.paginator.message.edit(components=self.paginator.create_components(True))
                return
            else:
                self.ping.clear()

attrs class Page

Attr attributes:

Name Type Description
content str

The content of the page.

title Optional[str]

The title of the page.

prefix str

Content that is prepended to the page.

suffix str

Content that is appended to the page.

Source code in naff/ext/paginators.py
@define(kw_only=False)
class Page:
    content: str = field()
    """The content of the page."""
    title: Optional[str] = field(default=None)
    """The title of the page."""
    prefix: str = field(kw_only=True, default="")
    """Content that is prepended to the page."""
    suffix: str = field(kw_only=True, default="")
    """Content that is appended to the page."""

    @property
    def get_summary(self) -> str:
        """Get the short version of the page content."""
        return self.title or textwrap.shorten(self.content, 40, placeholder="...")

    def to_embed(self) -> Embed:
        """Process the page to an embed."""
        return Embed(description=f"{self.prefix}\n{self.content}\n{self.suffix}", title=self.title)

property readonly get_summary: str

Get the short version of the page content.

method to_embed(self)

Process the page to an embed.

Source code in naff/ext/paginators.py
def to_embed(self) -> Embed:
    """Process the page to an embed."""
    return Embed(description=f"{self.prefix}\n{self.content}\n{self.suffix}", title=self.title)

attrs class Paginator

Attr attributes:

Name Type Description
client Client

The NAFF client to hook listeners into

page_index int

The index of the current page being displayed

pages List[naff.ext.paginators.Page | naff.models.discord.embed.Embed]

The pages this paginator holds

timeout_interval int

How long until this paginator disables itself

callback Callable[..., Coroutine]

A coroutine to call should the select button be pressed

show_first_button bool

Should a First button be shown

show_back_button bool

Should a Back button be shown

show_next_button bool

Should a Next button be shown

show_last_button bool

Should a Last button be shown

show_callback_button bool

Show a button which will call the callback

show_select_menu bool

Should a select menu be shown for navigation

first_button_emoji Union[PartialEmoji, dict, str]

The emoji to use for the first button

back_button_emoji Union[PartialEmoji, dict, str]

The emoji to use for the back button

next_button_emoji Union[PartialEmoji, dict, str]

The emoji to use for the next button

last_button_emoji Union[PartialEmoji, dict, str]

The emoji to use for the last button

callback_button_emoji Union[PartialEmoji, dict, str]

The emoji to use for the callback button

wrong_user_message str

The message to be sent when the wrong user uses this paginator

default_title Optional[str]

The default title to show on the embeds

default_color Color

The default colour to show on the embeds

default_button_color Union[naff.models.discord.enums.ButtonStyles, int]

The color of the buttons

Source code in naff/ext/paginators.py
@define(kw_only=False)
class Paginator:
    client: "Client" = field()
    """The NAFF client to hook listeners into"""

    page_index: int = field(kw_only=True, default=0)
    """The index of the current page being displayed"""
    pages: List[Page | Embed] = field(factory=list, kw_only=True)
    """The pages this paginator holds"""
    timeout_interval: int = field(default=0, kw_only=True)
    """How long until this paginator disables itself"""
    callback: Callable[..., Coroutine] = field(default=None)
    """A coroutine to call should the select button be pressed"""

    show_first_button: bool = field(default=True)
    """Should a `First` button be shown"""
    show_back_button: bool = field(default=True)
    """Should a `Back` button be shown"""
    show_next_button: bool = field(default=True)
    """Should a `Next` button be shown"""
    show_last_button: bool = field(default=True)
    """Should a `Last` button be shown"""
    show_callback_button: bool = field(default=False)
    """Show a button which will call the `callback`"""
    show_select_menu: bool = field(default=False)
    """Should a select menu be shown for navigation"""

    first_button_emoji: Optional[Union["PartialEmoji", dict, str]] = field(
        default="⏮️", metadata=export_converter(process_emoji)
    )
    """The emoji to use for the first button"""
    back_button_emoji: Optional[Union["PartialEmoji", dict, str]] = field(
        default="⬅️", metadata=export_converter(process_emoji)
    )
    """The emoji to use for the back button"""
    next_button_emoji: Optional[Union["PartialEmoji", dict, str]] = field(
        default="➡️", metadata=export_converter(process_emoji)
    )
    """The emoji to use for the next button"""
    last_button_emoji: Optional[Union["PartialEmoji", dict, str]] = field(
        default="⏩", metadata=export_converter(process_emoji)
    )
    """The emoji to use for the last button"""
    callback_button_emoji: Optional[Union["PartialEmoji", dict, str]] = field(
        default="✅", metadata=export_converter(process_emoji)
    )
    """The emoji to use for the callback button"""

    wrong_user_message: str = field(default="This paginator is not for you")
    """The message to be sent when the wrong user uses this paginator"""

    default_title: Optional[str] = field(default=None)
    """The default title to show on the embeds"""
    default_color: Color = field(default=BrandColors.BLURPLE)
    """The default colour to show on the embeds"""
    default_button_color: Union[ButtonStyles, int] = field(default=ButtonStyles.BLURPLE)
    """The color of the buttons"""

    _uuid: str = field(factory=uuid.uuid4)
    _message: Message = field(default=MISSING)
    _timeout_task: Timeout = field(default=MISSING)
    _author_id: Snowflake_Type = field(default=MISSING)

    def __attrs_post_init__(self) -> None:
        self.client.add_component_callback(
            ComponentCommand(
                name=f"Paginator:{self._uuid}",
                callback=self._on_button,
                listeners=[
                    f"{self._uuid}|select",
                    f"{self._uuid}|first",
                    f"{self._uuid}|back",
                    f"{self._uuid}|callback",
                    f"{self._uuid}|next",
                    f"{self._uuid}|last",
                ],
            )
        )

    @property
    def message(self) -> Message:
        """The message this paginator is currently attached to"""
        return self._message

    @property
    def author_id(self) -> Snowflake_Type:
        """The ID of the author of the message this paginator is currently attached to"""
        return self._author_id

    @classmethod
    def create_from_embeds(cls, client: "Client", *embeds: Embed, timeout: int = 0) -> "Paginator":
        """Create a paginator system from a list of embeds.

        Args:
            client: A reference to the NAFF client
            embeds: The embeds to use for each page
            timeout: A timeout to wait before closing the paginator

        Returns:
            A paginator system
        """
        return cls(client, pages=list(embeds), timeout_interval=timeout)

    @classmethod
    def create_from_string(
        cls, client: "Client", content: str, prefix: str = "", suffix: str = "", page_size: int = 4000, timeout: int = 0
    ) -> "Paginator":
        """
        Create a paginator system from a string.

        Args:
            client: A reference to the NAFF client
            content: The content to paginate
            prefix: The prefix for each page to use
            suffix: The suffix for each page to use
            page_size: The maximum characters for each page
            timeout: A timeout to wait before closing the paginator

        Returns:
            A paginator system
        """
        content_pages = textwrap.wrap(
            content,
            width=page_size - (len(prefix) + len(suffix)),
            break_long_words=True,
            break_on_hyphens=False,
            replace_whitespace=False,
        )
        pages = [Page(c, prefix=prefix, suffix=suffix) for c in content_pages]
        return cls(client, pages=pages, timeout_interval=timeout)

    @classmethod
    def create_from_list(
        cls,
        client: "Client",
        content: list[str],
        prefix: str = "",
        suffix: str = "",
        page_size: int = 4000,
        timeout: int = 0,
    ) -> "Paginator":
        """
        Create a paginator from a list of strings. Useful to maintain formatting.

        Args:
            client: A reference to the NAFF client
            content: The content to paginate
            prefix: The prefix for each page to use
            suffix: The suffix for each page to use
            page_size: The maximum characters for each page
            timeout: A timeout to wait before closing the paginator

        Returns:
            A paginator system
        """
        pages = []
        page = ""
        for entry in content:
            if len(page) + len(f"\n{entry}") <= page_size:
                page += f"{entry}\n"
            else:
                pages.append(Page(page, prefix=prefix, suffix=suffix))
                page = ""
        if page != "":
            pages.append(Page(page, prefix=prefix, suffix=suffix))
        return cls(client, pages=pages, timeout_interval=timeout)

    def create_components(self, disable: bool = False) -> List[ActionRow]:
        """
        Create the components for the paginator message.

        Args:
            disable: Should all the components be disabled?

        Returns:
            A list of ActionRows

        """
        output = []

        if self.show_select_menu:
            current = self.pages[self.page_index]
            output.append(
                Select(
                    [
                        SelectOption(f"{i+1} {p.get_summary if isinstance(p, Page) else p.title}", str(i))
                        for i, p in enumerate(self.pages)
                    ],
                    custom_id=f"{self._uuid}|select",
                    placeholder=f"{self.page_index+1} {current.get_summary if isinstance(current, Page) else current.title}",
                    max_values=1,
                    disabled=disable,
                )
            )

        if self.show_first_button:
            output.append(
                Button(
                    self.default_button_color,
                    emoji=self.first_button_emoji,
                    custom_id=f"{self._uuid}|first",
                    disabled=disable or self.page_index == 0,
                )
            )
        if self.show_back_button:
            output.append(
                Button(
                    self.default_button_color,
                    emoji=self.back_button_emoji,
                    custom_id=f"{self._uuid}|back",
                    disabled=disable or self.page_index == 0,
                )
            )

        if self.show_callback_button:
            output.append(
                Button(
                    self.default_button_color,
                    emoji=self.callback_button_emoji,
                    custom_id=f"{self._uuid}|callback",
                    disabled=disable,
                )
            )

        if self.show_next_button:
            output.append(
                Button(
                    self.default_button_color,
                    emoji=self.next_button_emoji,
                    custom_id=f"{self._uuid}|next",
                    disabled=disable or self.page_index >= len(self.pages) - 1,
                )
            )
        if self.show_last_button:
            output.append(
                Button(
                    self.default_button_color,
                    emoji=self.last_button_emoji,
                    custom_id=f"{self._uuid}|last",
                    disabled=disable or self.page_index >= len(self.pages) - 1,
                )
            )

        return spread_to_rows(*output)

    def to_dict(self) -> dict:
        """Convert this paginator into a dictionary for sending."""
        page = self.pages[self.page_index]

        if isinstance(page, Page):
            page = page.to_embed()
            if not page.title and self.default_title:
                page.title = self.default_title
        if not page.footer:
            page.set_footer(f"Page {self.page_index+1}/{len(self.pages)}")
        if not page.color:
            page.color = self.default_color

        return {"embeds": [page.to_dict()], "components": [c.to_dict() for c in self.create_components()]}

    async def send(self, ctx: Context) -> Message:
        """
        Send this paginator.

        Args:
            ctx: The context to send this paginator with

        Returns:
            The resulting message

        """
        self._message = await ctx.send(**self.to_dict())
        self._author_id = ctx.author.id

        if self.timeout_interval > 1:
            self._timeout_task = Timeout(self)
            asyncio.create_task(self._timeout_task())

        return self._message

    async def reply(self, ctx: PrefixedContext) -> Message:
        """
        Reply this paginator to ctx.

        Args:
            ctx: The context to reply this paginator with
        Returns:
            The resulting message
        """
        self._message = await ctx.reply(**self.to_dict())
        self._author_id = ctx.author.id

        if self.timeout_interval > 1:
            self._timeout_task = Timeout(self)
            asyncio.create_task(self._timeout_task())

        return self._message

    async def stop(self) -> None:
        """Disable this paginator."""
        if self._timeout_task:
            self._timeout_task.run = False
            self._timeout_task.ping.set()
        await self._message.edit(components=self.create_components(True))

    async def update(self) -> None:
        """
        Update the paginator to the current state.

        Use this if you have programmatically changed the page_index

        """
        await self._message.edit(**self.to_dict())

    async def _on_button(self, ctx: ComponentContext, *args, **kwargs) -> Optional[Message]:
        if ctx.author.id == self.author_id:
            if self._timeout_task:
                self._timeout_task.ping.set()
            match ctx.custom_id.split("|")[1]:
                case "first":
                    self.page_index = 0
                case "last":
                    self.page_index = len(self.pages) - 1
                case "next":
                    if (self.page_index + 1) < len(self.pages):
                        self.page_index += 1
                case "back":
                    if (self.page_index - 1) >= 0:
                        self.page_index -= 1
                case "select":
                    self.page_index = int(ctx.values[0])
                case "callback":
                    if self.callback:
                        return await self.callback(ctx)

            await ctx.edit_origin(**self.to_dict())
        else:
            if self.wrong_user_message:
                return await ctx.send(self.wrong_user_message, ephemeral=True)
            else:
                # silently ignore
                return await ctx.defer(edit_origin=True)

property readonly message: Message

The message this paginator is currently attached to

property readonly author_id: Union[int, str, SnowflakeObject, SupportsInt]

The ID of the author of the message this paginator is currently attached to

classmethod method create_from_embeds(client, *embeds, *, timeout)

Create a paginator system from a list of embeds.

Parameters:

Name Type Description Default
client Client

A reference to the NAFF client

required
embeds Embed

The embeds to use for each page

()
timeout int

A timeout to wait before closing the paginator

0

Returns:

Type Description
Paginator

A paginator system

Source code in naff/ext/paginators.py
@classmethod
def create_from_embeds(cls, client: "Client", *embeds: Embed, timeout: int = 0) -> "Paginator":
    """Create a paginator system from a list of embeds.

    Args:
        client: A reference to the NAFF client
        embeds: The embeds to use for each page
        timeout: A timeout to wait before closing the paginator

    Returns:
        A paginator system
    """
    return cls(client, pages=list(embeds), timeout_interval=timeout)

classmethod method create_from_string(client, content, prefix, suffix, page_size, timeout)

Create a paginator system from a string.

Parameters:

Name Type Description Default
client Client

A reference to the NAFF client

required
content str

The content to paginate

required
prefix str

The prefix for each page to use

''
suffix str

The suffix for each page to use

''
page_size int

The maximum characters for each page

4000
timeout int

A timeout to wait before closing the paginator

0

Returns:

Type Description
Paginator

A paginator system

Source code in naff/ext/paginators.py
@classmethod
def create_from_string(
    cls, client: "Client", content: str, prefix: str = "", suffix: str = "", page_size: int = 4000, timeout: int = 0
) -> "Paginator":
    """
    Create a paginator system from a string.

    Args:
        client: A reference to the NAFF client
        content: The content to paginate
        prefix: The prefix for each page to use
        suffix: The suffix for each page to use
        page_size: The maximum characters for each page
        timeout: A timeout to wait before closing the paginator

    Returns:
        A paginator system
    """
    content_pages = textwrap.wrap(
        content,
        width=page_size - (len(prefix) + len(suffix)),
        break_long_words=True,
        break_on_hyphens=False,
        replace_whitespace=False,
    )
    pages = [Page(c, prefix=prefix, suffix=suffix) for c in content_pages]
    return cls(client, pages=pages, timeout_interval=timeout)

classmethod method create_from_list(client, content, prefix, suffix, page_size, timeout)

Create a paginator from a list of strings. Useful to maintain formatting.

Parameters:

Name Type Description Default
client Client

A reference to the NAFF client

required
content list

The content to paginate

required
prefix str

The prefix for each page to use

''
suffix str

The suffix for each page to use

''
page_size int

The maximum characters for each page

4000
timeout int

A timeout to wait before closing the paginator

0

Returns:

Type Description
Paginator

A paginator system

Source code in naff/ext/paginators.py
@classmethod
def create_from_list(
    cls,
    client: "Client",
    content: list[str],
    prefix: str = "",
    suffix: str = "",
    page_size: int = 4000,
    timeout: int = 0,
) -> "Paginator":
    """
    Create a paginator from a list of strings. Useful to maintain formatting.

    Args:
        client: A reference to the NAFF client
        content: The content to paginate
        prefix: The prefix for each page to use
        suffix: The suffix for each page to use
        page_size: The maximum characters for each page
        timeout: A timeout to wait before closing the paginator

    Returns:
        A paginator system
    """
    pages = []
    page = ""
    for entry in content:
        if len(page) + len(f"\n{entry}") <= page_size:
            page += f"{entry}\n"
        else:
            pages.append(Page(page, prefix=prefix, suffix=suffix))
            page = ""
    if page != "":
        pages.append(Page(page, prefix=prefix, suffix=suffix))
    return cls(client, pages=pages, timeout_interval=timeout)

method create_components(self, disable)

Create the components for the paginator message.

Parameters:

Name Type Description Default
disable bool

Should all the components be disabled?

False

Returns:

Type Description
List[naff.models.discord.components.ActionRow]

A list of ActionRows

Source code in naff/ext/paginators.py
def create_components(self, disable: bool = False) -> List[ActionRow]:
    """
    Create the components for the paginator message.

    Args:
        disable: Should all the components be disabled?

    Returns:
        A list of ActionRows

    """
    output = []

    if self.show_select_menu:
        current = self.pages[self.page_index]
        output.append(
            Select(
                [
                    SelectOption(f"{i+1} {p.get_summary if isinstance(p, Page) else p.title}", str(i))
                    for i, p in enumerate(self.pages)
                ],
                custom_id=f"{self._uuid}|select",
                placeholder=f"{self.page_index+1} {current.get_summary if isinstance(current, Page) else current.title}",
                max_values=1,
                disabled=disable,
            )
        )

    if self.show_first_button:
        output.append(
            Button(
                self.default_button_color,
                emoji=self.first_button_emoji,
                custom_id=f"{self._uuid}|first",
                disabled=disable or self.page_index == 0,
            )
        )
    if self.show_back_button:
        output.append(
            Button(
                self.default_button_color,
                emoji=self.back_button_emoji,
                custom_id=f"{self._uuid}|back",
                disabled=disable or self.page_index == 0,
            )
        )

    if self.show_callback_button:
        output.append(
            Button(
                self.default_button_color,
                emoji=self.callback_button_emoji,
                custom_id=f"{self._uuid}|callback",
                disabled=disable,
            )
        )

    if self.show_next_button:
        output.append(
            Button(
                self.default_button_color,
                emoji=self.next_button_emoji,
                custom_id=f"{self._uuid}|next",
                disabled=disable or self.page_index >= len(self.pages) - 1,
            )
        )
    if self.show_last_button:
        output.append(
            Button(
                self.default_button_color,
                emoji=self.last_button_emoji,
                custom_id=f"{self._uuid}|last",
                disabled=disable or self.page_index >= len(self.pages) - 1,
            )
        )

    return spread_to_rows(*output)

method to_dict(self)

Convert this paginator into a dictionary for sending.

Source code in naff/ext/paginators.py
def to_dict(self) -> dict:
    """Convert this paginator into a dictionary for sending."""
    page = self.pages[self.page_index]

    if isinstance(page, Page):
        page = page.to_embed()
        if not page.title and self.default_title:
            page.title = self.default_title
    if not page.footer:
        page.set_footer(f"Page {self.page_index+1}/{len(self.pages)}")
    if not page.color:
        page.color = self.default_color

    return {"embeds": [page.to_dict()], "components": [c.to_dict() for c in self.create_components()]}

async method send(self, ctx)

Send this paginator.

Parameters:

Name Type Description Default
ctx Context

The context to send this paginator with

required

Returns:

Type Description
Message

The resulting message

Source code in naff/ext/paginators.py
async def send(self, ctx: Context) -> Message:
    """
    Send this paginator.

    Args:
        ctx: The context to send this paginator with

    Returns:
        The resulting message

    """
    self._message = await ctx.send(**self.to_dict())
    self._author_id = ctx.author.id

    if self.timeout_interval > 1:
        self._timeout_task = Timeout(self)
        asyncio.create_task(self._timeout_task())

    return self._message

async method reply(self, ctx)

Reply this paginator to ctx.

Parameters:

Name Type Description Default
ctx PrefixedContext

The context to reply this paginator with

required

Returns:

Type Description
Message

The resulting message

Source code in naff/ext/paginators.py
async def reply(self, ctx: PrefixedContext) -> Message:
    """
    Reply this paginator to ctx.

    Args:
        ctx: The context to reply this paginator with
    Returns:
        The resulting message
    """
    self._message = await ctx.reply(**self.to_dict())
    self._author_id = ctx.author.id

    if self.timeout_interval > 1:
        self._timeout_task = Timeout(self)
        asyncio.create_task(self._timeout_task())

    return self._message

async method stop(self)

Disable this paginator.

Source code in naff/ext/paginators.py
async def stop(self) -> None:
    """Disable this paginator."""
    if self._timeout_task:
        self._timeout_task.run = False
        self._timeout_task.ping.set()
    await self._message.edit(components=self.create_components(True))

async method update(self)

Update the paginator to the current state.

Use this if you have programmatically changed the page_index

Source code in naff/ext/paginators.py
async def update(self) -> None:
    """
    Update the paginator to the current state.

    Use this if you have programmatically changed the page_index

    """
    await self._message.edit(**self.to_dict())