@kyvrixon/utils 0.0.6 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/package.json +36 -29
- package/src/index.ts +5 -2
- package/src/lib/discord-utils/Command.ts +33 -0
- package/src/lib/discord-utils/Event.ts +61 -0
- package/src/lib/discord-utils/createPagination.ts +327 -0
- package/src/lib/formatSeconds.ts +58 -83
- package/src/lib/modules/LoggerModule.ts +96 -0
- package/src/lib/toOrdinal.ts +14 -16
- package/src/lib/cooldownManager.ts +0 -71
- package/src/lib/logger.ts +0 -125
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
General utility files I use for alot of projects!
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
bun install @kyvrixon/utils
|
|
7
|
-
|
|
1
|
+
# @kyvrixon/utils
|
|
2
|
+
|
|
3
|
+
General utility files I use for alot of projects! Designed for use with the [bun runtime](https://bun.sh/)!
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install @kyvrixon/utils
|
|
7
|
+
# or
|
|
8
|
+
bun install github:kyvrixon/utils#1.0.10
|
|
9
|
+
```
|
package/package.json
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
2
|
+
"name": "@kyvrixon/utils",
|
|
3
|
+
"main": "./src/index.ts",
|
|
4
|
+
"version": "1.0.10",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"files": [
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"engines": {
|
|
12
|
+
"bun": "1.3.10"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"url": "https://github.com/kyvrixon/utils"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"pub": "bun prebuild.ts && bun publish --access public",
|
|
19
|
+
"pretty": "bun biome format --write ."
|
|
20
|
+
},
|
|
21
|
+
"packageManager": "bun@1.3.10",
|
|
22
|
+
"types": "./src/index.ts",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@biomejs/biome": "2.4.6",
|
|
25
|
+
"@types/bun": "1.3.10",
|
|
26
|
+
"typescript": "5.9.3"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"chalk": "5.6.2",
|
|
30
|
+
"discord.js": "14.25.1"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./src/index.ts",
|
|
35
|
+
"import": "./src/index.ts"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
31
38
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export * from "./lib/formatSeconds";
|
|
2
|
-
export * from "./lib/
|
|
3
|
-
export * from "./lib/
|
|
2
|
+
export * from "./lib/toOrdinal";
|
|
3
|
+
export * from "./lib/discord-utils/Command";
|
|
4
|
+
export * from "./lib/discord-utils/createPagination";
|
|
5
|
+
export * from "./lib/discord-utils/Event";
|
|
6
|
+
export * from "./lib/modules/LoggerModule";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutocompleteInteraction,
|
|
3
|
+
ChatInputCommandInteraction,
|
|
4
|
+
Client,
|
|
5
|
+
SlashCommandBuilder,
|
|
6
|
+
SlashCommandOptionsOnlyBuilder,
|
|
7
|
+
SlashCommandSubcommandsOnlyBuilder,
|
|
8
|
+
} from "discord.js";
|
|
9
|
+
|
|
10
|
+
export class DiscordCommand<C extends Client<boolean>> {
|
|
11
|
+
public readonly data:
|
|
12
|
+
| SlashCommandBuilder
|
|
13
|
+
| SlashCommandOptionsOnlyBuilder
|
|
14
|
+
| SlashCommandSubcommandsOnlyBuilder;
|
|
15
|
+
public readonly execute: (
|
|
16
|
+
client: C,
|
|
17
|
+
interaction: ChatInputCommandInteraction,
|
|
18
|
+
) => Promise<void>;
|
|
19
|
+
public readonly autocomplete?: (
|
|
20
|
+
client: C,
|
|
21
|
+
interaction: AutocompleteInteraction,
|
|
22
|
+
) => Promise<void>;
|
|
23
|
+
|
|
24
|
+
constructor(ops: {
|
|
25
|
+
data: DiscordCommand<C>["data"];
|
|
26
|
+
execute: DiscordCommand<C>["execute"];
|
|
27
|
+
autocomplete?: DiscordCommand<C>["autocomplete"];
|
|
28
|
+
}) {
|
|
29
|
+
this.data = ops.data;
|
|
30
|
+
this.execute = ops.execute;
|
|
31
|
+
this.autocomplete = ops.autocomplete;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: Its fine */
|
|
2
|
+
import type { Client, ClientEvents, RestEvents } from "discord.js";
|
|
3
|
+
|
|
4
|
+
// biome-ignore lint/suspicious/noEmptyInterface: Its fine
|
|
5
|
+
export interface DiscordEventCustomType {}
|
|
6
|
+
/**
|
|
7
|
+
* To define custom types:
|
|
8
|
+
* @example
|
|
9
|
+
* declare module "@kyvrixon/utils" {
|
|
10
|
+
* interface DiscordEventCustomType {
|
|
11
|
+
* myEventName: [data: string];
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* // You can also add these to `ClientEvents` if you wish for better typing like so:
|
|
15
|
+
* declare module "discord.js" {
|
|
16
|
+
* interface ClientEvents extends DiscordEventCustomType {}
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
export class DiscordEvent<
|
|
20
|
+
V extends Client,
|
|
21
|
+
T extends "client" | "custom" | "rest" = "client",
|
|
22
|
+
K extends T extends "custom"
|
|
23
|
+
? keyof DiscordEventCustomType
|
|
24
|
+
: T extends "client"
|
|
25
|
+
? keyof ClientEvents
|
|
26
|
+
: keyof RestEvents = T extends "custom"
|
|
27
|
+
? keyof DiscordEventCustomType
|
|
28
|
+
: T extends "client"
|
|
29
|
+
? keyof ClientEvents
|
|
30
|
+
: keyof RestEvents,
|
|
31
|
+
> {
|
|
32
|
+
public readonly type: T;
|
|
33
|
+
public readonly name: K;
|
|
34
|
+
public readonly once: boolean;
|
|
35
|
+
public readonly method: (
|
|
36
|
+
client: V,
|
|
37
|
+
...args: T extends "client"
|
|
38
|
+
? K extends keyof ClientEvents
|
|
39
|
+
? ClientEvents[K]
|
|
40
|
+
: any[]
|
|
41
|
+
: T extends "rest"
|
|
42
|
+
? K extends keyof RestEvents
|
|
43
|
+
? RestEvents[K]
|
|
44
|
+
: any[]
|
|
45
|
+
: K extends keyof DiscordEventCustomType
|
|
46
|
+
? DiscordEventCustomType[K]
|
|
47
|
+
: any[]
|
|
48
|
+
) => Promise<void>;
|
|
49
|
+
|
|
50
|
+
constructor(opts: {
|
|
51
|
+
type: T;
|
|
52
|
+
name: K;
|
|
53
|
+
once?: boolean;
|
|
54
|
+
method: DiscordEvent<V, T, K>["method"];
|
|
55
|
+
}) {
|
|
56
|
+
this.type = opts.type;
|
|
57
|
+
this.name = opts.name;
|
|
58
|
+
this.once = opts.once ?? false;
|
|
59
|
+
this.method = opts.method;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { randomUUIDv7 } from "bun";
|
|
2
|
+
import {
|
|
3
|
+
ActionRowBuilder,
|
|
4
|
+
ButtonBuilder,
|
|
5
|
+
type ButtonInteraction,
|
|
6
|
+
ButtonStyle,
|
|
7
|
+
type ChannelSelectMenuBuilder,
|
|
8
|
+
type ChatInputCommandInteraction,
|
|
9
|
+
ComponentType,
|
|
10
|
+
ContainerBuilder,
|
|
11
|
+
type FileBuilder,
|
|
12
|
+
LabelBuilder,
|
|
13
|
+
type MediaGalleryBuilder,
|
|
14
|
+
type MentionableSelectMenuBuilder,
|
|
15
|
+
ModalBuilder,
|
|
16
|
+
type RoleSelectMenuBuilder,
|
|
17
|
+
type SectionBuilder,
|
|
18
|
+
type SeparatorBuilder,
|
|
19
|
+
type StringSelectMenuBuilder,
|
|
20
|
+
TextDisplayBuilder,
|
|
21
|
+
TextInputBuilder,
|
|
22
|
+
TextInputStyle,
|
|
23
|
+
type UserSelectMenuBuilder,
|
|
24
|
+
} from "discord.js";
|
|
25
|
+
|
|
26
|
+
export type MessageActionRow = ActionRowBuilder<
|
|
27
|
+
| ButtonBuilder
|
|
28
|
+
| StringSelectMenuBuilder
|
|
29
|
+
| UserSelectMenuBuilder
|
|
30
|
+
| RoleSelectMenuBuilder
|
|
31
|
+
| ChannelSelectMenuBuilder
|
|
32
|
+
| MentionableSelectMenuBuilder
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
export type LeaderboardComponentType =
|
|
36
|
+
| { type: "buttons" }
|
|
37
|
+
| { type: "display"; component: TextDisplayBuilder }
|
|
38
|
+
| { type: "section"; component: SectionBuilder }
|
|
39
|
+
| { type: "separator"; component: SeparatorBuilder }
|
|
40
|
+
| { type: "file"; component: FileBuilder }
|
|
41
|
+
| { type: "gallery"; component: MediaGalleryBuilder }
|
|
42
|
+
| { type: "actionrow"; component: MessageActionRow };
|
|
43
|
+
|
|
44
|
+
export interface LeaderboardOptions {
|
|
45
|
+
contentMarker?: string;
|
|
46
|
+
entriesPerPage?: number;
|
|
47
|
+
replacements?: Record<string, string>;
|
|
48
|
+
styling?: Array<{ accent_color?: number; spoiler?: boolean }>;
|
|
49
|
+
ephemeral?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function applyReplacements(
|
|
53
|
+
content: string,
|
|
54
|
+
replacements?: Record<string, string>,
|
|
55
|
+
): string {
|
|
56
|
+
if (!replacements) return content;
|
|
57
|
+
return Object.entries(replacements).reduce(
|
|
58
|
+
(acc, [key, value]) => acc.replaceAll(key, value),
|
|
59
|
+
content,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getPaginationRow(
|
|
64
|
+
prefix: string,
|
|
65
|
+
currentIndex: number,
|
|
66
|
+
entriesPerPage: number,
|
|
67
|
+
totalPages: number,
|
|
68
|
+
listLength: number,
|
|
69
|
+
ended: boolean,
|
|
70
|
+
): ActionRowBuilder<ButtonBuilder> {
|
|
71
|
+
return new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
72
|
+
new ButtonBuilder()
|
|
73
|
+
.setCustomId(`${prefix}back`)
|
|
74
|
+
.setLabel("Prev")
|
|
75
|
+
.setStyle(ButtonStyle.Secondary)
|
|
76
|
+
.setDisabled(ended || currentIndex === 0),
|
|
77
|
+
new ButtonBuilder()
|
|
78
|
+
.setCustomId(`${prefix}info`)
|
|
79
|
+
.setLabel(
|
|
80
|
+
`${Math.floor(currentIndex / entriesPerPage) + 1}/${totalPages}`,
|
|
81
|
+
)
|
|
82
|
+
.setStyle(ButtonStyle.Secondary)
|
|
83
|
+
.setDisabled(ended || totalPages === 1),
|
|
84
|
+
new ButtonBuilder()
|
|
85
|
+
.setCustomId(`${prefix}forward`)
|
|
86
|
+
.setLabel("Next")
|
|
87
|
+
.setStyle(ButtonStyle.Secondary)
|
|
88
|
+
.setDisabled(ended || currentIndex + entriesPerPage >= listLength),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function createPagination(
|
|
93
|
+
list: Array<string>,
|
|
94
|
+
structure: Array<Array<LeaderboardComponentType>>,
|
|
95
|
+
interaction: ButtonInteraction | ChatInputCommandInteraction,
|
|
96
|
+
options: LeaderboardOptions = {},
|
|
97
|
+
) {
|
|
98
|
+
const {
|
|
99
|
+
entriesPerPage = 5,
|
|
100
|
+
replacements,
|
|
101
|
+
styling,
|
|
102
|
+
ephemeral = false,
|
|
103
|
+
contentMarker = "--DATA_INPUT--",
|
|
104
|
+
} = options;
|
|
105
|
+
|
|
106
|
+
if (entriesPerPage <= 0)
|
|
107
|
+
throw new Error("entriesPerPage must be greater than 0");
|
|
108
|
+
|
|
109
|
+
const uID = randomUUIDv7();
|
|
110
|
+
const prefix = `~PAGINATION_${uID}_`;
|
|
111
|
+
let currentIndex = 0;
|
|
112
|
+
let ended = false;
|
|
113
|
+
|
|
114
|
+
if (!list.length) {
|
|
115
|
+
await interaction
|
|
116
|
+
.reply({
|
|
117
|
+
allowedMentions: {
|
|
118
|
+
parse: [],
|
|
119
|
+
repliedUser: false,
|
|
120
|
+
},
|
|
121
|
+
components: [
|
|
122
|
+
new ContainerBuilder().addTextDisplayComponents(
|
|
123
|
+
new TextDisplayBuilder().setContent("No data to show"),
|
|
124
|
+
),
|
|
125
|
+
],
|
|
126
|
+
flags: ephemeral ? ["Ephemeral", "IsComponentsV2"] : ["IsComponentsV2"],
|
|
127
|
+
})
|
|
128
|
+
.catch(() => {});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const totalPages = Math.ceil(list.length / entriesPerPage);
|
|
133
|
+
const lastPage = structure[structure.length - 1];
|
|
134
|
+
|
|
135
|
+
if (!lastPage) throw new Error("createLeaderboard is in a corrupted state");
|
|
136
|
+
|
|
137
|
+
while (structure.length < totalPages) {
|
|
138
|
+
structure.push(
|
|
139
|
+
lastPage.map((comp) => {
|
|
140
|
+
if (comp.type === "display") {
|
|
141
|
+
return {
|
|
142
|
+
type: "display",
|
|
143
|
+
component: new TextDisplayBuilder(comp.component.toJSON()),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return comp;
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function generateContainer(page: number): ContainerBuilder {
|
|
152
|
+
const pageStructure = structure[page];
|
|
153
|
+
if (!pageStructure) throw new Error(`Page ${page} structure not found`);
|
|
154
|
+
|
|
155
|
+
return new ContainerBuilder({
|
|
156
|
+
components: pageStructure.map((comp) => {
|
|
157
|
+
switch (comp.type) {
|
|
158
|
+
case "buttons":
|
|
159
|
+
return getPaginationRow(
|
|
160
|
+
prefix,
|
|
161
|
+
currentIndex,
|
|
162
|
+
entriesPerPage,
|
|
163
|
+
totalPages,
|
|
164
|
+
list.length,
|
|
165
|
+
ended,
|
|
166
|
+
).toJSON();
|
|
167
|
+
|
|
168
|
+
case "display": {
|
|
169
|
+
if (comp.component.data.content !== contentMarker) {
|
|
170
|
+
return comp.component.toJSON();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const content = list
|
|
174
|
+
.slice(
|
|
175
|
+
page * entriesPerPage,
|
|
176
|
+
page * entriesPerPage + entriesPerPage,
|
|
177
|
+
)
|
|
178
|
+
.join("\n");
|
|
179
|
+
comp.component.data.content = applyReplacements(
|
|
180
|
+
content,
|
|
181
|
+
replacements,
|
|
182
|
+
);
|
|
183
|
+
return comp.component.toJSON();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case "section":
|
|
187
|
+
case "separator":
|
|
188
|
+
case "file":
|
|
189
|
+
case "gallery":
|
|
190
|
+
case "actionrow":
|
|
191
|
+
return comp.component.toJSON();
|
|
192
|
+
|
|
193
|
+
default: {
|
|
194
|
+
return comp;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}),
|
|
198
|
+
accent_color: styling?.[page]?.accent_color,
|
|
199
|
+
spoiler: styling?.[page]?.spoiler,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
204
|
+
await interaction
|
|
205
|
+
.deferReply({
|
|
206
|
+
withResponse: true,
|
|
207
|
+
flags: ephemeral ? ["Ephemeral"] : [],
|
|
208
|
+
})
|
|
209
|
+
.catch(() => null);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const channel = interaction.channel;
|
|
213
|
+
if (!channel || !("createMessageComponentCollector" in channel)) {
|
|
214
|
+
throw new Error("Invalid channel type");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function render(): Promise<void> {
|
|
218
|
+
try {
|
|
219
|
+
await interaction.editReply({
|
|
220
|
+
components: [
|
|
221
|
+
generateContainer(Math.floor(currentIndex / entriesPerPage)),
|
|
222
|
+
],
|
|
223
|
+
flags: ["IsComponentsV2"],
|
|
224
|
+
allowedMentions: {
|
|
225
|
+
parse: [],
|
|
226
|
+
repliedUser: false,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const e = error as Error;
|
|
231
|
+
if (!e.message.includes("Unknown Message")) {
|
|
232
|
+
console.error("Failed to render leaderboard:", error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await render();
|
|
238
|
+
|
|
239
|
+
const collector = channel.createMessageComponentCollector({
|
|
240
|
+
componentType: ComponentType.Button,
|
|
241
|
+
time: 60000,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
collector.on("collect", async (btn) => {
|
|
245
|
+
if (btn.user.id !== interaction.user.id) {
|
|
246
|
+
return void btn.deferUpdate();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!btn.customId.startsWith(prefix)) return;
|
|
250
|
+
|
|
251
|
+
ended = false;
|
|
252
|
+
collector.resetTimer();
|
|
253
|
+
|
|
254
|
+
if (btn.customId === `${prefix}info`) {
|
|
255
|
+
const modal = new ModalBuilder()
|
|
256
|
+
.setCustomId(`${prefix}modal`)
|
|
257
|
+
.setTitle("Page Indexer")
|
|
258
|
+
.addLabelComponents(
|
|
259
|
+
new LabelBuilder()
|
|
260
|
+
// .setDescription("Input a page number")
|
|
261
|
+
.setLabel("Input a page number")
|
|
262
|
+
.setTextInputComponent(
|
|
263
|
+
new TextInputBuilder()
|
|
264
|
+
.setCustomId(`${prefix}number`)
|
|
265
|
+
.setRequired(true)
|
|
266
|
+
.setMinLength(1)
|
|
267
|
+
.setStyle(TextInputStyle.Short),
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
await btn.showModal(modal).catch((e) => console.error(e));
|
|
272
|
+
const modalSubmit = await btn
|
|
273
|
+
.awaitModalSubmit({ time: 60_000 })
|
|
274
|
+
.catch((e) => console.error(e));
|
|
275
|
+
|
|
276
|
+
if (!modalSubmit) {
|
|
277
|
+
await btn
|
|
278
|
+
.followUp({
|
|
279
|
+
content: "Modal timed out.",
|
|
280
|
+
flags: ["Ephemeral"],
|
|
281
|
+
})
|
|
282
|
+
.catch(() => null);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await modalSubmit.deferUpdate().catch(() => null);
|
|
287
|
+
const pageNumber = Number(
|
|
288
|
+
modalSubmit.fields.getTextInputValue(`${prefix}number`),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
Number.isNaN(pageNumber) ||
|
|
293
|
+
pageNumber < 1 ||
|
|
294
|
+
pageNumber > totalPages
|
|
295
|
+
) {
|
|
296
|
+
await modalSubmit
|
|
297
|
+
.reply({
|
|
298
|
+
content: `Invalid page! Choose a number between **1** and **${totalPages}**.`,
|
|
299
|
+
flags: ["Ephemeral"],
|
|
300
|
+
allowedMentions: {
|
|
301
|
+
parse: [],
|
|
302
|
+
repliedUser: false,
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
.catch(() => null);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
currentIndex = (pageNumber - 1) * entriesPerPage;
|
|
310
|
+
} else {
|
|
311
|
+
currentIndex +=
|
|
312
|
+
btn.customId === `${prefix}back` ? -entriesPerPage : entriesPerPage;
|
|
313
|
+
currentIndex = Math.max(
|
|
314
|
+
0,
|
|
315
|
+
Math.min(currentIndex, (totalPages - 1) * entriesPerPage),
|
|
316
|
+
);
|
|
317
|
+
await btn.deferUpdate().catch(() => {});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await render();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
collector.on("end", async () => {
|
|
324
|
+
ended = true;
|
|
325
|
+
await render();
|
|
326
|
+
});
|
|
327
|
+
}
|
package/src/lib/formatSeconds.ts
CHANGED
|
@@ -4,12 +4,12 @@ const UNITS: Record<
|
|
|
4
4
|
TimeUnitTypes,
|
|
5
5
|
{ label: string; short: string; ms: number }
|
|
6
6
|
> = {
|
|
7
|
-
y: { label: "year", short: "y", ms:
|
|
8
|
-
mo: { label: "month", short: "mo", ms:
|
|
9
|
-
w: { label: "week", short: "w", ms:
|
|
10
|
-
d: { label: "day", short: "d", ms:
|
|
11
|
-
h: { label: "hour", short: "h", ms:
|
|
12
|
-
m: { label: "minute", short: "m", ms:
|
|
7
|
+
y: { label: "year", short: "y", ms: 31536000000 },
|
|
8
|
+
mo: { label: "month", short: "mo", ms: 2628000000 },
|
|
9
|
+
w: { label: "week", short: "w", ms: 604800000 },
|
|
10
|
+
d: { label: "day", short: "d", ms: 86400000 },
|
|
11
|
+
h: { label: "hour", short: "h", ms: 3600000 },
|
|
12
|
+
m: { label: "minute", short: "m", ms: 60000 },
|
|
13
13
|
s: { label: "second", short: "s", ms: 1000 },
|
|
14
14
|
ms: { label: "millisecond", short: "ms", ms: 1 },
|
|
15
15
|
};
|
|
@@ -25,13 +25,6 @@ const ALL_UNITS_ORDER: Array<TimeUnitTypes> = [
|
|
|
25
25
|
"ms",
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* Format a number to a customisable and readable time string
|
|
30
|
-
*
|
|
31
|
-
* @param seconds Number in seconds to format
|
|
32
|
-
* @param options Options for formatting
|
|
33
|
-
* @returns string
|
|
34
|
-
*/
|
|
35
28
|
export function formatSeconds(
|
|
36
29
|
seconds: number,
|
|
37
30
|
options: {
|
|
@@ -45,98 +38,80 @@ export function formatSeconds(
|
|
|
45
38
|
) => string;
|
|
46
39
|
} = {},
|
|
47
40
|
): string {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
41
|
+
const {
|
|
42
|
+
includeZeroUnits = false,
|
|
43
|
+
onlyUnits = [],
|
|
44
|
+
format = "long",
|
|
45
|
+
customFormatter,
|
|
46
|
+
} = options;
|
|
47
|
+
let totalMs = Math.max(0, Math.round(seconds * 1000));
|
|
54
48
|
const unitsToDisplay = ALL_UNITS_ORDER.filter((u) =>
|
|
55
49
|
onlyUnits.length ? onlyUnits.includes(u) : true,
|
|
56
50
|
);
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
const zeroParts: Array<string> = [];
|
|
60
|
-
for (const u of unitsToDisplay) {
|
|
61
|
-
if (format === "short") {
|
|
62
|
-
zeroParts.push(`0${UNITS[u].short}`);
|
|
63
|
-
} else {
|
|
64
|
-
zeroParts.push(`0 ${UNITS[u].label}`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return zeroParts.join(format === "short" ? " " : ", ");
|
|
68
|
-
}
|
|
69
|
-
|
|
52
|
+
const diff: Partial<Record<TimeUnitTypes, number>> = {};
|
|
70
53
|
const now = new Date();
|
|
71
54
|
const end = new Date(now.getTime() + totalMs);
|
|
72
55
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
56
|
+
if (unitsToDisplay.includes("y")) {
|
|
57
|
+
let y = end.getFullYear() - now.getFullYear();
|
|
58
|
+
const tempDate = new Date(now);
|
|
59
|
+
tempDate.setFullYear(now.getFullYear() + y);
|
|
60
|
+
if (tempDate > end) y--;
|
|
61
|
+
diff.y = y;
|
|
62
|
+
totalMs -= new Date(now).setFullYear(now.getFullYear() + y) - now.getTime();
|
|
76
63
|
}
|
|
77
64
|
|
|
78
|
-
let months = 0;
|
|
79
65
|
if (unitsToDisplay.includes("mo")) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
const startTotalMonths = now.getFullYear() * 12 + now.getMonth();
|
|
67
|
+
const endTotalMonths = end.getFullYear() * 12 + end.getMonth();
|
|
68
|
+
let mo = endTotalMonths - startTotalMonths;
|
|
69
|
+
if (diff.y) mo -= diff.y * 12;
|
|
83
70
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
now.getMonth() + months,
|
|
89
|
-
now.getDate(),
|
|
90
|
-
now.getHours(),
|
|
91
|
-
now.getMinutes(),
|
|
92
|
-
now.getSeconds(),
|
|
93
|
-
now.getMilliseconds(),
|
|
94
|
-
).getTime();
|
|
71
|
+
const tempDate = new Date(now);
|
|
72
|
+
tempDate.setFullYear(now.getFullYear() + (diff.y || 0));
|
|
73
|
+
tempDate.setMonth(now.getMonth() + mo);
|
|
74
|
+
if (tempDate > end) mo--;
|
|
95
75
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
m: Math.floor((remainingMs % UNITS.h.ms) / UNITS.m.ms),
|
|
103
|
-
s: Math.floor((remainingMs % UNITS.m.ms) / UNITS.s.ms),
|
|
104
|
-
ms: remainingMs % 1000,
|
|
105
|
-
};
|
|
76
|
+
diff.mo = Math.max(0, mo);
|
|
77
|
+
const jumpDate = new Date(now);
|
|
78
|
+
jumpDate.setFullYear(now.getFullYear() + (diff.y || 0));
|
|
79
|
+
jumpDate.setMonth(now.getMonth() + (diff.mo || 0));
|
|
80
|
+
totalMs = end.getTime() - jumpDate.getTime();
|
|
81
|
+
}
|
|
106
82
|
|
|
107
|
-
const
|
|
83
|
+
for (const unit of ["w", "d", "h", "m", "s", "ms"] as const) {
|
|
84
|
+
if (unitsToDisplay.includes(unit)) {
|
|
85
|
+
diff[unit] = Math.floor(totalMs / UNITS[unit].ms);
|
|
86
|
+
totalMs %= UNITS[unit].ms;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
108
89
|
|
|
109
|
-
const parts:
|
|
90
|
+
const parts: string[] = [];
|
|
110
91
|
for (const unit of unitsToDisplay) {
|
|
111
92
|
const value = diff[unit] ?? 0;
|
|
112
|
-
if (value ||
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
} else {
|
|
120
|
-
label = `${UNITS[unit].label}s`;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
93
|
+
if (value > 0 || includeZeroUnits) {
|
|
94
|
+
const label =
|
|
95
|
+
format === "short"
|
|
96
|
+
? UNITS[unit].short
|
|
97
|
+
: value === 1
|
|
98
|
+
? UNITS[unit].label
|
|
99
|
+
: `${UNITS[unit].label}s`;
|
|
123
100
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
}
|
|
101
|
+
parts.push(
|
|
102
|
+
customFormatter
|
|
103
|
+
? customFormatter(unit, value, label)
|
|
104
|
+
: format === "short"
|
|
105
|
+
? `${value}${label}`
|
|
106
|
+
: `${value} ${label}`,
|
|
107
|
+
);
|
|
133
108
|
}
|
|
134
109
|
}
|
|
135
110
|
|
|
111
|
+
if (parts.length === 0) return format === "short" ? "0s" : "0 seconds";
|
|
136
112
|
if (format === "long" && parts.length > 1) {
|
|
137
113
|
const last = parts.pop();
|
|
138
114
|
return `${parts.join(", ")} and ${last}`;
|
|
139
115
|
}
|
|
140
|
-
|
|
141
|
-
return parts.join(format === "short" ? " " : ",");
|
|
116
|
+
return parts.join(format === "short" ? " " : ", ");
|
|
142
117
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
type LogLevel = "notif" | "alert" | "error" | "debug";
|
|
4
|
+
const { cyan, yellow, red, magenta, dim, gray, bold } = chalk;
|
|
5
|
+
const formatter = new Intl.DateTimeFormat("en-AU", {
|
|
6
|
+
weekday: "short",
|
|
7
|
+
hour: "2-digit",
|
|
8
|
+
minute: "2-digit",
|
|
9
|
+
second: "2-digit",
|
|
10
|
+
hour12: false,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export class LoggerModule {
|
|
14
|
+
private readonly colors: Record<LogLevel, typeof chalk> = {
|
|
15
|
+
notif: cyan,
|
|
16
|
+
alert: yellow,
|
|
17
|
+
error: red,
|
|
18
|
+
debug: magenta,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
private readonly logMethods: Record<
|
|
22
|
+
LogLevel,
|
|
23
|
+
"log" | "warn" | "error" | "debug"
|
|
24
|
+
> = {
|
|
25
|
+
notif: "log",
|
|
26
|
+
alert: "warn",
|
|
27
|
+
error: "error",
|
|
28
|
+
debug: "debug",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
private getTimestamp(): string {
|
|
32
|
+
const d = new Date();
|
|
33
|
+
return `${formatter.format(d)}.${d.getMilliseconds().toString().padStart(3, "0")}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private formatMessage(level: LogLevel, message: unknown): string {
|
|
37
|
+
const timestamp = gray(`[${this.getTimestamp()}]`);
|
|
38
|
+
const levelLabel = bold(this.colors[level](level.toUpperCase().padEnd(5)));
|
|
39
|
+
|
|
40
|
+
const content =
|
|
41
|
+
message instanceof Error
|
|
42
|
+
? `${red(message.message)}\n${this.sanitizeStack(message.stack || "")}`
|
|
43
|
+
: String(message);
|
|
44
|
+
|
|
45
|
+
return `${timestamp} ${levelLabel} ${dim("»")} ${content}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private sanitizeStack(stack: string): string {
|
|
49
|
+
return stack
|
|
50
|
+
.split("\n")
|
|
51
|
+
.slice(1)
|
|
52
|
+
.filter((line) => line.includes(":") && !line.includes("node_modules"))
|
|
53
|
+
.map((line) => {
|
|
54
|
+
return line
|
|
55
|
+
.trim()
|
|
56
|
+
.replace(/\\/g, "/")
|
|
57
|
+
.replace(
|
|
58
|
+
/at\s+(.+?)\s+\((.+):(\d+):(\d+)\)/,
|
|
59
|
+
(_, fn, f, l, c) => ` └─ ${fn} ${dim(f)} ${bold(`(L${l} C${c})`)}`,
|
|
60
|
+
)
|
|
61
|
+
.replace(
|
|
62
|
+
/at\s+(.+):(\d+):(\d+)/,
|
|
63
|
+
(_, f, l, c) => ` └─ ${dim(f)} ${bold(`(L${l} C${c})`)}`,
|
|
64
|
+
);
|
|
65
|
+
})
|
|
66
|
+
.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private log(
|
|
70
|
+
level: LogLevel,
|
|
71
|
+
message: unknown,
|
|
72
|
+
raw = false,
|
|
73
|
+
): string | undefined {
|
|
74
|
+
const msg = this.formatMessage(level, message);
|
|
75
|
+
if (raw) return msg;
|
|
76
|
+
console[this.logMethods[level]](msg);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public notif(m: unknown, raw = false) {
|
|
80
|
+
return this.log("notif", m, raw);
|
|
81
|
+
}
|
|
82
|
+
public alert(m: unknown, raw = false) {
|
|
83
|
+
return this.log("alert", m, raw);
|
|
84
|
+
}
|
|
85
|
+
public error(m: unknown, e?: Error, raw = false) {
|
|
86
|
+
return this.log("error", e ?? m, raw);
|
|
87
|
+
}
|
|
88
|
+
public debug(m: unknown, raw = false) {
|
|
89
|
+
return this.log("debug", m, raw);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public divider(text: string): void {
|
|
93
|
+
const line = dim("─".repeat(Math.max(0, (50 - text.length - 2) / 2)));
|
|
94
|
+
console.log(`\n${line} ${bold(text.trim())} ${line}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/lib/toOrdinal.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return `${n}${suffixes[new Intl.PluralRules("en-US", { type: "ordinal" }).select(n)]}`;
|
|
16
|
-
}
|
|
1
|
+
const pr = new Intl.PluralRules("en-US", { type: "ordinal" });
|
|
2
|
+
const suffixes: Record<Intl.LDMLPluralRule, string> = {
|
|
3
|
+
one: "st",
|
|
4
|
+
two: "nd",
|
|
5
|
+
few: "rd",
|
|
6
|
+
other: "th",
|
|
7
|
+
// Included for type safety
|
|
8
|
+
zero: "th",
|
|
9
|
+
many: "th",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function toOrdinal(n: number): string {
|
|
13
|
+
return `${n}${suffixes[pr.select(n)]}`;
|
|
14
|
+
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
export class CooldownManager {
|
|
2
|
-
public cds = new Map<string, Map<string, number>>();
|
|
3
|
-
|
|
4
|
-
set(uid: string, cmd: string, ttl: number): void {
|
|
5
|
-
if (ttl <= 0) return;
|
|
6
|
-
|
|
7
|
-
const expiry = Date.now() + ttl;
|
|
8
|
-
let userCmds = this.cds.get(uid);
|
|
9
|
-
|
|
10
|
-
if (!userCmds) {
|
|
11
|
-
userCmds = new Map();
|
|
12
|
-
this.cds.set(uid, userCmds);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
userCmds.set(cmd, expiry);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
has(uid: string, cmd: string): false | number {
|
|
19
|
-
const userCmds = this.cds.get(uid);
|
|
20
|
-
if (!userCmds) return false;
|
|
21
|
-
|
|
22
|
-
const expiry = userCmds.get(cmd);
|
|
23
|
-
if (!expiry) return false;
|
|
24
|
-
|
|
25
|
-
const remaining = expiry - Date.now();
|
|
26
|
-
if (remaining > 0) return remaining;
|
|
27
|
-
|
|
28
|
-
userCmds.delete(cmd);
|
|
29
|
-
|
|
30
|
-
if (userCmds.size === 0) this.cds.delete(uid);
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
del(uid: string, cmd: string): void {
|
|
35
|
-
const userCmds = this.cds.get(uid);
|
|
36
|
-
if (!userCmds) return;
|
|
37
|
-
|
|
38
|
-
userCmds.delete(cmd);
|
|
39
|
-
|
|
40
|
-
if (userCmds.size === 0) this.cds.delete(uid);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
list(): Record<string, Record<string, number>> {
|
|
44
|
-
const result: ReturnType<CooldownManager["list"]> = {};
|
|
45
|
-
|
|
46
|
-
for (const [uid, cmds] of this.cds) {
|
|
47
|
-
const cmdsObj: Record<string, number> = {};
|
|
48
|
-
for (const [cmd, expiry] of cmds) {
|
|
49
|
-
cmdsObj[cmd] = expiry;
|
|
50
|
-
}
|
|
51
|
-
result[uid] = cmdsObj;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
clean(): void {
|
|
58
|
-
const now = Date.now();
|
|
59
|
-
for (const [uid, cmds] of this.cds) {
|
|
60
|
-
for (const [cmd, expiry] of Array.from(cmds)) {
|
|
61
|
-
if (expiry < now) cmds.delete(cmd);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (cmds.size === 0) this.cds.delete(uid);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
clear(): void {
|
|
69
|
-
void this.cds.clear();
|
|
70
|
-
}
|
|
71
|
-
}
|
package/src/lib/logger.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { env } from "bun";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
|
|
4
|
-
type LogLevel = "notif" | "alert" | "error" | "debug";
|
|
5
|
-
const { cyan, yellow, red, magenta, dim, gray, bold } = chalk;
|
|
6
|
-
const formatter = new Intl.DateTimeFormat("en-AU", {
|
|
7
|
-
weekday: "short",
|
|
8
|
-
hour: "2-digit",
|
|
9
|
-
minute: "2-digit",
|
|
10
|
-
second: "2-digit",
|
|
11
|
-
hour12: false,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Console logger
|
|
16
|
-
*/
|
|
17
|
-
export class Logger {
|
|
18
|
-
private readonly colors: Record<LogLevel, typeof chalk> = {
|
|
19
|
-
notif: cyan,
|
|
20
|
-
alert: yellow,
|
|
21
|
-
error: red,
|
|
22
|
-
debug: magenta,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
private readonly logMethods: Record<
|
|
26
|
-
LogLevel,
|
|
27
|
-
"log" | "warn" | "error" | "debug"
|
|
28
|
-
> = {
|
|
29
|
-
notif: "log",
|
|
30
|
-
alert: "warn",
|
|
31
|
-
error: "error",
|
|
32
|
-
debug: "debug",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
private getTimestamp(): string {
|
|
36
|
-
const now = new Date();
|
|
37
|
-
const ms = now.getMilliseconds().toString().padStart(3, "0");
|
|
38
|
-
const base = formatter.format(now).replace(",", " @");
|
|
39
|
-
return `${base}.${ms}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
private formatMessage(level: LogLevel, message: string | Error): string {
|
|
43
|
-
const timestamp = gray(`[${this.getTimestamp()}]`);
|
|
44
|
-
const levelLabel = bold(this.colors[level](level.toUpperCase().padEnd(5)));
|
|
45
|
-
|
|
46
|
-
const content =
|
|
47
|
-
message instanceof Error
|
|
48
|
-
? `${red(message.message)}\n${dim(this.sanitizeStack(message.stack || ""))}`
|
|
49
|
-
: message;
|
|
50
|
-
|
|
51
|
-
return `${timestamp} ${levelLabel} ${dim("»")} ${content}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private sanitizeStack(stack: string): string {
|
|
55
|
-
return stack
|
|
56
|
-
.split("\n")
|
|
57
|
-
.filter((line) => !line.includes("(native") && line.includes(":"))
|
|
58
|
-
.map((line, index) => {
|
|
59
|
-
if (index === 0) return false;
|
|
60
|
-
return (
|
|
61
|
-
line
|
|
62
|
-
.trim()
|
|
63
|
-
// TODO Need to do something with this
|
|
64
|
-
// .replace(/\\/g, "/")
|
|
65
|
-
// .replace(
|
|
66
|
-
// /(.*):(\d+):(\d+)/,
|
|
67
|
-
// (_, f, l, c) => `${dim(f)} ${bold(`(L${l} C${c})`)}`,
|
|
68
|
-
// )
|
|
69
|
-
.replace(/at\s+/, " └─ ")
|
|
70
|
-
);
|
|
71
|
-
})
|
|
72
|
-
.filter(Boolean)
|
|
73
|
-
.join("\n");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private log(level: LogLevel, message: unknown, forceError?: boolean): void {
|
|
77
|
-
if (forceError) return void console.error(message);
|
|
78
|
-
void console[this.logMethods[level]](
|
|
79
|
-
this.formatMessage(level, message as string | Error),
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Pring a regular (info) message
|
|
85
|
-
*
|
|
86
|
-
* @param m Message to display
|
|
87
|
-
*/
|
|
88
|
-
public notif(m: unknown) {
|
|
89
|
-
this.log("notif", m);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Print an alert (warning)
|
|
94
|
-
*
|
|
95
|
-
* @param m Message to display
|
|
96
|
-
*/
|
|
97
|
-
public alert(m: unknown) {
|
|
98
|
-
this.log("alert", m);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Print an error
|
|
103
|
-
*
|
|
104
|
-
* @param m Message to display
|
|
105
|
-
* @param e Error (optional)
|
|
106
|
-
* @param f Force error - directly calls `console.error()`
|
|
107
|
-
*/
|
|
108
|
-
public error(m: unknown, e?: Error, f?: boolean) {
|
|
109
|
-
this.log("error", e ?? m, f);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Print a debug message
|
|
114
|
-
*
|
|
115
|
-
* @param m Message to display
|
|
116
|
-
*/
|
|
117
|
-
public debug(m: unknown) {
|
|
118
|
-
this.log("debug", m);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
public divider(text: string): void {
|
|
122
|
-
const line = dim("─".repeat(Math.max(0, (50 - text.length - 2) / 2)));
|
|
123
|
-
console.log(`\n${line} ${bold(text.trim())} ${line}`);
|
|
124
|
-
}
|
|
125
|
-
}
|