{"id":17673290,"url":"https://github.com/sushichan044/ductile-ui","last_synced_at":"2026-02-23T08:34:23.217Z","repository":{"id":197965112,"uuid":"699761555","full_name":"sushichan044/ductile-ui","owner":"sushichan044","description":"A typed, declarative UI library for discord.py","archived":false,"fork":false,"pushed_at":"2026-02-13T01:09:14.000Z","size":436,"stargazers_count":1,"open_issues_count":16,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-13T10:36:45.526Z","etag":null,"topics":["bot","discord","discord-bot","discord-py","python","python-3"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/ductile-ui/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sushichan044.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":"sushichan044"}},"created_at":"2023-10-03T09:35:42.000Z","updated_at":"2025-11-03T05:52:58.000Z","dependencies_parsed_at":null,"dependency_job_id":"3c6c821b-f415-4768-bb61-99f44be41409","html_url":"https://github.com/sushichan044/ductile-ui","commit_stats":{"total_commits":108,"total_committers":4,"mean_commits":27.0,"dds":0.25,"last_synced_commit":"093bd87c4ea9082f6229c5d694b671676f5410b9"},"previous_names":["sushi-chaaaan/ductile_ui","sushichan044/ductile-ui","sushi-chaaaan/ductile-ui"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/sushichan044/ductile-ui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sushichan044%2Fductile-ui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sushichan044%2Fductile-ui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sushichan044%2Fductile-ui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sushichan044%2Fductile-ui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sushichan044","download_url":"https://codeload.github.com/sushichan044/ductile-ui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sushichan044%2Fductile-ui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29740026,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-23T07:44:07.782Z","status":"ssl_error","status_checked_at":"2026-02-23T07:44:07.432Z","response_time":90,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bot","discord","discord-bot","discord-py","python","python-3"],"created_at":"2024-10-24T05:09:09.906Z","updated_at":"2026-02-23T08:34:23.191Z","avatar_url":"https://github.com/sushichan044.png","language":"Python","funding_links":["https://github.com/sponsors/sushichan044"],"categories":[],"sub_categories":[],"readme":"\r\n# ductile-ui\r\n\r\n![PyPI - Version](https://img.shields.io/pypi/v/ductile-ui)\r\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ductile-ui)\r\n\r\nA library provides declarative UI for [discord.py](https://github.com/Rapptz/discord.py).\r\n\r\n## Features\r\n\r\n- Declarative UI, inspired by [React](https://react.dev/).\r\n- Component-oriented with State\r\n- Fully typed\r\n\r\n## Installation\r\n\r\n**Python3.10 or higher is required**\r\n\r\n**discord.py^2.2.0 is required; any compatibility under 2.1.x or 1.x is not guaranteed**\r\n\r\nUsing the latest stable release of discord.py is recommended\r\n\r\n```bash\r\n  pip install ductile-ui\r\n```\r\n\r\n## Usage/Examples\r\n\r\nYou can define component as return value of `View.render()`.\r\nTo store state, use `State`.\r\n\r\n```python\r\n# This example requires the 'message_content' privileged intent to function.\r\n\r\n\r\nimport random\r\n\r\nimport discord\r\nfrom discord.ext import commands\r\nfrom ductile import State, View, ViewObject\r\nfrom ductile.controller import MessageableController\r\nfrom ductile.ui import Button\r\n\r\n\r\nclass Bot(commands.Bot):\r\n    def __init__(self) -\u003e None:\r\n        super().__init__(command_prefix=\"!\", intents=discord.Intents.all())\r\n\r\n    async def on_ready(self) -\u003e None:\r\n        print(f\"Logged in as {self.user}\")\r\n        print(\"Ready!\")\r\n\r\n\r\nclass CounterView(View):\r\n    def __init__(self) -\u003e None:\r\n        super().__init__()\r\n        self.count = State(0, self)\r\n\r\n    def render(self) -\u003e ViewObject:\r\n        e = discord.Embed(title=\"Counter\", description=f\"Count: {self.count.get_state()}\")\r\n\r\n        async def handle_increment(interaction: discord.Interaction) -\u003e None:\r\n            await interaction.response.defer()\r\n            self.count.set_state(lambda x: x + 1)\r\n\r\n        async def handle_decrement(interaction: discord.Interaction) -\u003e None:\r\n            await interaction.response.defer()\r\n            self.count.set_state(lambda x: x - 1)\r\n\r\n        async def stop(interaction: discord.Interaction) -\u003e None:\r\n            await interaction.response.defer()\r\n            self.stop()\r\n\r\n        # Define UI using ViewObject\r\n        return ViewObject(\r\n            embeds=[e],\r\n            components=[\r\n                Button(\"+1\", style={\"color\": \"blurple\", \"row\": 0}, on_click=handle_increment),\r\n                Button(\"-1\", style={\"color\": \"blurple\", \"row\": 0}, on_click=handle_decrement),\r\n                Button(\r\n                    \"random\",\r\n                    style={\"color\": \"green\", \"row\": 1},\r\n                    # if you passed synchronous function to Button.on_click,\r\n                    # library automatically calls `await interaction.response.defer()`.\r\n                    on_click=lambda _: self.count.set_state(random.randint(0, 100)),\r\n                ),\r\n                Button(\"stop\", style={\"color\": \"red\", \"row\": 1}, on_click=stop),\r\n            ],\r\n        )\r\n\r\n\r\nbot = Bot()\r\n\r\n\r\n@bot.command(name=\"counter\")\r\nasync def send_counter(ctx: commands.Context) -\u003e None:\r\n    controller = MessageableController(CounterView(), messageable=ctx.channel)\r\n    await controller.send()\r\n    timed_out, states = await controller.wait()\r\n    await ctx.send(f\"Timed out: {timed_out}\\nCount: {states['count']}\")\r\n\r\n\r\nbot.run(\"MY_COOL_BOT_TOKEN\")\r\n\r\n```\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsushichan044%2Fductile-ui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsushichan044%2Fductile-ui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsushichan044%2Fductile-ui/lists"}