commit 0a1f4f609fbacb6301843b4d6ecb998e9b31d620 Author: disqualifier Date: Wed Jun 24 15:10:18 2026 -0400 init: button paginator for discord.py Signed-off-by: disqualifier diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae590bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# claude +CLAUDE.md + +# python +__pycache__/ +*.py[cod] +*.egg-info/ +build/ +dist/ +.eggs/ + +# env +.venv/ +venv/ +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..d628f09 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# dpy_paginator + +Button-navigated paginator for discord.py. A `discord.ui.View` that paginates mixed +content (strings, embeds, files, attachments, or dicts of mixed content with custom +buttons) behind previous / jump / next navigation, with an optional cache button. + +## Install + +`requirements.txt`: + +``` +dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.0 +``` + +Direct: + +```bash +pip install "dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.0" +``` + +Requires `discord.py` (pulled transitively). + +## Basic usage + +Plain pages — just navigation: + +```python +from dpy_paginator import ButtonPaginator + +pages = [discord.Embed(title=f"Page {i}") for i in range(5)] +await ButtonPaginator(pages, author_id=ctx.author.id).start(ctx) +``` + +`start()` accepts an `Interaction` or any `Messageable` (a `Context`, channel, etc.). + +## Emojis + +Navigation uses plain Unicode by default — no setup, no emoji upload required. Pass +`emojis=` to override with custom application/guild emojis the bot can use: + +```python +ButtonPaginator(pages, emojis={ + "previous": "<:icon_back:123...>", + "next": "<:icon_next:123...>", + "cache": "<:icon_cache:123...>", +}) +``` + +Unset keys fall back to the Unicode defaults. + +## Page types + +A page may be a `str`, `discord.Embed`, `discord.File`/`Attachment`, a sequence of +those, or a `dict`. A dict page can carry `content`, `embed`/`embeds`, +`file`/`files`, and a `buttons` list of custom button configs. + +## Custom per-page buttons + +Each page can declare its own buttons. They render alongside the navigation row and +are rebuilt from the page dict on every render — so mutating a page's button config +and re-rendering updates the button live. + +```python +async def callback_reorder(interaction, button, data, paginator): + await interaction.response.send_message(f"Reordering {data['email']}", ephemeral=True) + +pages = [] +for session in sessions: + pages.append({ + "content": session["_id"], + "embed": build_embed(session), + "buttons": [ + { + "label": "Reorder", + "style": discord.ButtonStyle.green, + "emoji": some_emoji, # optional, your own emoji + "disabled": False, + "data": {"email": session["_id"]}, + "callback": callback_reorder, + } + ], + }) + +paginator = ButtonPaginator( + pages, cache=None, timeout=900, delete_message_after=True, + mentions_allowed=discord.AllowedMentions.none(), ephemeral=True, + page_text="Session {} of {}", +) +await paginator.start(interaction) +``` + +Button config keys: `label`, `style`, `emoji`, `disabled`, `data` (arbitrary, passed +to the callback), `callback`. The callback signature is +`async def cb(interaction, button, data, paginator)`. With no callback, the button +echoes its `data`. + +## Cache button (mention priming) + +Discord clients render `<@id>` as a raw id until the user object is cached locally. +The cache button fixes that: pass `cache=[...]` with one entry per page, where each +entry is a string of the user mentions on that page (`"<@111> <@222> <@333>"`). + +When clicked, the button posts the page's mentions in a throwaway ephemeral message +(rendering the `<@id>` tags primes the viewer's client — `allowed_mentions` is +`none()` so no actual ping fires), waits `cache_sleep` seconds (default 1.0) for the +client to resolve them, then re-renders the current page — so the mentions now +display as names without the user having to flip pages manually. + +```python +pages, cache = [], [] +for group in groups: + pages.append(build_embed(group)) + cache.append(" ".join(f"<@{uid}>" for uid in group["user_ids"])) + +await ButtonPaginator(pages, cache=cache, cache_sleep=1.0).start(ctx) +``` + +Omit `cache` or pass `None`/`[]` and the button never appears. When set, `cache` +must have one entry per rendered page. + +## Constructor options + +- `pages` — sequence of page content +- `cache` — optional per-page cache strings (enables the cache button) +- `author_id` — restrict interaction to this user +- `timeout` — seconds before the view stops (default 180) +- `delete_message_after` — delete the message on timeout +- `per_page` — entries per page (default 1) +- `mentions_allowed` — `AllowedMentions` for sends (default `.all()`) +- `ephemeral` — send/edit ephemerally +- `page_text` — format string for the jump button label +- `emojis` — override navigation emojis + +## Subclassing + +Override `format_page(page)` to transform pages before rendering (e.g. wrap raw data +in an embed). It may be sync or async. + +## Versioning + +Tagged `vX.Y.Z`. Pin the tag in `requirements.txt`.