Skip to content

Commit 7e571e8

Browse files
authored
dev: display improvements (#705)
* [dev] Use 3.0 pagination, format exceptions for UX, and increase response length * Fix checks * [dev] Fix empty content messages
1 parent 4b69c90 commit 7e571e8

File tree

2 files changed

+140
-16
lines changed

2 files changed

+140
-16
lines changed

ballsdex/core/commands.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from discord.ext import commands
99
from django.db import connection
1010

11-
from ballsdex.core.dev import pagify, send_interactive
11+
from ballsdex.core.dev import send_interactive
1212
from ballsdex.core.discord import LayoutView, View
13+
from ballsdex.core.utils.formatting import pagify
1314
from ballsdex.core.utils.menus import Menu, TextFormatter, TextSource
1415
from bd_models.models import Ball
1516
from settings.models import load_settings, settings

ballsdex/core/dev.py

Lines changed: 138 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
from discord.ext import commands
2020
from django.db import connection
2121

22-
from ballsdex.core.utils.formatting import pagify
22+
from ballsdex.core.discord import LayoutView
23+
from ballsdex.core.utils.menus import Menu, TextFormatter, TextSource
2324
from bd_models import models
2425
from bd_models.enums import DonationPolicy, FriendPolicy, MentionPolicy, PrivacyPolicy
2526
from bd_models.models import (
@@ -52,6 +53,10 @@
5253
https://github.com/Rapptz/RoboDanny/blob/master/cogs/repl.py
5354
"""
5455

56+
ANSI_RED = "\x1b[31m"
57+
ANSI_BOLD = "\x1b[1m"
58+
ANSI_RESET = "\x1b[0m"
59+
5560

5661
def format_duration(time_taken: float) -> str:
5762
return f"{round(time_taken * 1000)}ms" if time_taken < 1 else f"{round(time_taken, 3)}s"
@@ -68,6 +73,28 @@ def text_to_file(
6873
return discord.File(file, filename, spoiler=spoiler)
6974

7075

76+
def format_exception(text: str) -> str:
77+
"""
78+
Formats the last line of an exception.
79+
80+
Parameters
81+
----------
82+
text: str
83+
The text you want to format.
84+
85+
Returns
86+
-------
87+
str
88+
The formatted text.
89+
"""
90+
lines = text.splitlines()
91+
92+
if lines:
93+
lines[-1] = f"{ANSI_RED}{ANSI_BOLD}{lines[-1]}{ANSI_RESET}"
94+
95+
return "\n".join(lines)
96+
97+
7198
async def send_interactive(
7299
ctx: commands.Context["BallsDexBot"],
73100
messages: Iterable[str],
@@ -141,7 +168,7 @@ def predicate(m: discord.Message):
141168
prompt_text.format(count=n_remaining, command_1="`more`", command_2="`file`")
142169
)
143170
try:
144-
resp = await ctx.bot.wait_for("message", check=predicate, timeout=15)
171+
resp = await ctx.bot.wait_for("message", check=predicate, timeout=timeout)
145172
except asyncio.TimeoutError:
146173
with contextlib.suppress(discord.HTTPException):
147174
await query.delete()
@@ -161,6 +188,72 @@ def predicate(m: discord.Message):
161188
return ret
162189

163190

191+
class FileRow(discord.ui.ActionRow):
192+
"""
193+
File row displayed in eval responses that uploads the specified content as a file.
194+
195+
Parameters
196+
----------
197+
content: str
198+
The content that will be exported as a file.
199+
"""
200+
201+
def __init__(self, content: str):
202+
super().__init__()
203+
self.content = content
204+
205+
@discord.ui.button(label="File")
206+
async def file_button(self, interaction: discord.Interaction["BallsDexBot"], _):
207+
await interaction.response.send_message(file=text_to_file(self.content))
208+
209+
210+
async def build_eval_response(
211+
ctx: commands.Context["BallsDexBot"], content: str, *, time_taken: float, error: bool = False
212+
) -> LayoutView | None:
213+
"""
214+
Creates a view with a paginator menu and a button to send the specified content as a file.
215+
216+
Parameters
217+
----------
218+
content: str
219+
The content that will be displayed.
220+
time_taken: float
221+
The amount of time taken in milliseconds for the code to execute.
222+
error: bool
223+
Whether ansi formatting will be enabled.
224+
225+
Returns
226+
-------
227+
LayoutView | None
228+
The created view.
229+
"""
230+
if content == "":
231+
return
232+
233+
code_prefix = "ansi" if error else "py"
234+
formatted_content = format_exception(content) if error else content
235+
236+
text_display = discord.ui.TextDisplay("")
237+
238+
view = LayoutView()
239+
view.add_item(text_display)
240+
view.add_item(discord.ui.TextDisplay(f"-# Took {format_duration(time_taken)}"))
241+
view.restrict_author(ctx.author.id)
242+
243+
menu = Menu(
244+
ctx.bot,
245+
view,
246+
TextSource(formatted_content, prefix=f"```{code_prefix}\n", suffix="\n```"),
247+
TextFormatter(text_display),
248+
)
249+
await menu.init()
250+
251+
if menu.source.get_max_pages() > 1:
252+
menu.view.add_item(FileRow(content))
253+
254+
return view
255+
256+
164257
START_CODE_BLOCK_RE = re.compile(r"^((```(py(thon)?|sql))(?=\s)|(```))")
165258

166259

@@ -197,19 +290,15 @@ def cleanup_code(content):
197290
return content.strip("` \n")
198291

199292
@classmethod
200-
def get_syntax_error(cls, e):
293+
def get_syntax_error(cls, e) -> str:
201294
"""Format a syntax error to send to the user.
202295
203296
Returns a string representation of the error formatted as a codeblock.
204297
"""
205298
if e.text is None:
206-
return cls.get_pages("{0.__class__.__name__}: {0}".format(e))
207-
return cls.get_pages("{0.text}\n{1:>{0.offset}}\n{2}: {0}".format(e, "^", type(e).__name__))
299+
return "{0.__class__.__name__}: {0}".format(e)
208300

209-
@staticmethod
210-
def get_pages(msg: str):
211-
"""Pagify the given message for output to the user."""
212-
return pagify(msg, delims=["\n", " "], priority=True, shorten_by=25)
301+
return "{0.text}\n{1:>{0.offset}}\n{2}: {0}".format(e, "^", type(e).__name__)
213302

214303
@staticmethod
215304
def sanitize_output(ctx: commands.Context, input_: str) -> str:
@@ -292,19 +381,35 @@ async def debug(self, ctx: commands.Context, *, code):
292381
result = await self.maybe_await(eval(compiled, env))
293382
except SyntaxError as e:
294383
t2 = time.time()
295-
await send_interactive(ctx, self.get_syntax_error(e), time_taken=t2 - t1)
384+
385+
view = await build_eval_response(ctx, self.get_syntax_error(e), time_taken=t2 - t1, error=True)
386+
387+
if view:
388+
await ctx.send(view=view)
389+
296390
return
297391
except Exception as e:
298392
t2 = time.time()
299-
await send_interactive(ctx, self.get_pages("{}: {!s}".format(type(e).__name__, e)), time_taken=t2 - t1)
393+
394+
view = await build_eval_response(
395+
ctx, "{}: {!s}".format(type(e).__name__, e), time_taken=t2 - t1, error=True
396+
)
397+
398+
if view:
399+
await ctx.send(view=view)
400+
300401
return
301402
t2 = time.time()
302403

303404
self._last_result = result
304405
result = self.sanitize_output(ctx, str(result))
305406

306407
await ctx.message.add_reaction("✅")
307-
await send_interactive(ctx, self.get_pages(result), time_taken=t2 - t1)
408+
409+
view = await build_eval_response(ctx, result, time_taken=t2 - t1)
410+
411+
if view:
412+
await ctx.send(view=view)
308413

309414
@commands.command(name="eval")
310415
@commands.is_owner()
@@ -340,20 +445,35 @@ async def _eval(self, ctx: commands.Context, *, body: str):
340445
exec(compiled, env)
341446
except SyntaxError as e:
342447
t2 = time.time()
343-
return await send_interactive(ctx, self.get_syntax_error(e), time_taken=t2 - t1)
448+
449+
view = await build_eval_response(ctx, self.get_syntax_error(e), time_taken=t2 - t1, error=True)
450+
451+
if view:
452+
await ctx.send(view=view)
453+
454+
return
344455
except Exception as e:
345456
t2 = time.time()
346-
await send_interactive(ctx, self.get_pages("{}: {!s}".format(type(e).__name__, e)), time_taken=t2 - t1)
457+
458+
view = await build_eval_response(
459+
ctx, "{}: {!s}".format(type(e).__name__, e), time_taken=t2 - t1, error=True
460+
)
461+
462+
if view:
463+
await ctx.send(view=view)
464+
347465
return
348466

349467
func = env["func"]
350468
result = None
469+
errored = False
351470
try:
352471
with redirect_stdout(stdout):
353472
result = await func()
354473
except Exception:
355474
t2 = time.time()
356475
printed = "{}{}".format(stdout.getvalue(), traceback.format_exc())
476+
errored = True
357477
else:
358478
t2 = time.time()
359479
printed = stdout.getvalue()
@@ -366,7 +486,10 @@ async def _eval(self, ctx: commands.Context, *, body: str):
366486
msg = printed
367487
msg = self.sanitize_output(ctx, msg)
368488

369-
await send_interactive(ctx, self.get_pages(msg), time_taken=t2 - t1)
489+
view = await build_eval_response(ctx, msg, time_taken=t2 - t1, error=errored)
490+
491+
if view:
492+
await ctx.send(view=view)
370493

371494
@commands.command()
372495
@commands.is_owner()

0 commit comments

Comments
 (0)