husteno-o

[email protected]
2026-09-07 03:24:14 +0600 committed NaN years ago
c8eca38
commit c8eca38
6 files changed
+342 -193
β–Όapps/bot/src/commands/help.ts
+44-18
4
4
import { createBaseEmbed } from "../ui/embeds.js";
5
5
import { NF } from "../ui/icons.js";
6
6
import {
7
+ ANSI,
7
8
renderTuiCard,
8
9
tuiBottomBar,
9
10
tuiDivider,
17
18
const map: Record<string, { title: string; synopsis: string; commands: [string, string][] }> = {
18
19
repo: {
19
20
title: "REPO INTELLIGENCE",
20
- synopsis: "Deep repository metrics, health score & dependencies",
21
+ synopsis: "Deep repository metrics, health scores, and dependencies",
21
22
commands: [
22
23
["/repo <repo>", "Master telemetry dashboard"],
23
24
["/repo <repo> view:health", "Health breakdown & audit"],
98
99
tuiTopBar(`MAN: ${info.title}`),
99
100
tuiPrompt(`gitbot man ${moduleName}`),
100
101
tuiDivider("SYNOPSIS"),
101
- tuiLine(info.synopsis),
102
+ tuiLine(`${ANSI.green}${info.synopsis}${ANSI.reset}`),
102
103
tuiDivider("COMMAND SUITE"),
103
104
];
104
105
105
106
for (const [cmd, desc] of info.commands) {
106
- tuiLines.push(tuiLine(cmd));
107
- tuiLines.push(tuiLine(` β–Έ ${desc}`));
107
+ tuiLines.push(tuiLine(`${ANSI.white}${cmd}${ANSI.reset}`));
108
+ tuiLines.push(tuiLine(` ${ANSI.cyan}β–Έ${ANSI.reset} ${ANSI.dim}${desc}${ANSI.reset}`));
108
109
}
109
110
110
111
tuiLines.push(tuiBottomBar());
111
112
const tui = renderTuiCard(tuiLines);
112
113
114
+ const header =
115
+ `### πŸ’» [**Manual: ${info.title}**](https://github.com/husteno-o/git-discord-bot)\n` +
116
+ `> *${info.synopsis}*\n\n`;
117
+
113
118
return createBaseEmbed(`${NF.terminal} Manual: ${info.title}`)
114
119
.setColor(BrandColors.primary)
115
- .setDescription(tui);
120
+ .setDescription(`${header}${tui}`);
116
121
}
117
122
118
123
export const helpCommand: Command = {
122
127
123
128
async execute(interaction) {
124
129
const tui = renderTuiCard([
125
- tuiTopBar("GITBOT TERMINAL v2.4.0"),
130
+ tuiTopBar("GITBOT CORE ENGINE v2.4"),
126
131
tuiPrompt("gitbot --help"),
127
132
tuiDivider("ACTIVE SUBSYSTEMS"),
128
- tuiLine("[1] REPO : /repo <target> [view]"),
129
- tuiLine("[2] PR : /pr <view|list|review>"),
130
- tuiLine("[3] SEARCH : /search <code|issues|prs>"),
131
- tuiLine("[4] CODE : /code view, /code blame"),
132
- tuiLine("[5] TRACE : /why, /investigate"),
133
- tuiLine("[6] METRICS : /activity, /team"),
134
- tuiLine("[7] RADAR : /trending, /watch"),
135
- tuiLine("[8] CI/CD : /actions status, /release"),
136
- tuiLine("[9] SECURE : /security audit, /connect"),
133
+ tuiLine(
134
+ `${ANSI.yellow}[1] REPO ${ANSI.reset}: ${ANSI.white}/repo <target> [view]${ANSI.reset}`,
135
+ ),
136
+ tuiLine(
137
+ `${ANSI.yellow}[2] PR ${ANSI.reset}: ${ANSI.white}/pr <view|list|review>${ANSI.reset}`,
138
+ ),
139
+ tuiLine(
140
+ `${ANSI.yellow}[3] SEARCH ${ANSI.reset}: ${ANSI.white}/search <code|issues|prs>${ANSI.reset}`,
141
+ ),
142
+ tuiLine(
143
+ `${ANSI.yellow}[4] CODE ${ANSI.reset}: ${ANSI.white}/code view, /code blame${ANSI.reset}`,
144
+ ),
145
+ tuiLine(
146
+ `${ANSI.yellow}[5] TRACE ${ANSI.reset}: ${ANSI.white}/why, /investigate${ANSI.reset}`,
147
+ ),
148
+ tuiLine(`${ANSI.yellow}[6] STATS ${ANSI.reset}: ${ANSI.white}/activity, /team${ANSI.reset}`),
149
+ tuiLine(
150
+ `${ANSI.yellow}[7] RADAR ${ANSI.reset}: ${ANSI.white}/trending, /watch${ANSI.reset}`,
151
+ ),
152
+ tuiLine(
153
+ `${ANSI.yellow}[8] CI/CD ${ANSI.reset}: ${ANSI.white}/actions status, /release${ANSI.reset}`,
154
+ ),
155
+ tuiLine(
156
+ `${ANSI.yellow}[9] SECURE ${ANSI.reset}: ${ANSI.white}/security audit, /connect${ANSI.reset}`,
157
+ ),
137
158
tuiDivider("COMMAND CENTER"),
138
- tuiLine("β–Έ Terminal CLI engineered for Discord"),
139
- tuiLine("β–Έ Select a manual below or run command"),
159
+ tuiLine(`${ANSI.green}β–Έ Interactive Developer Console for Discord${ANSI.reset}`),
160
+ tuiLine(`${ANSI.cyan}β–Έ Pick a manual below or type a command${ANSI.reset}`),
140
161
tuiBottomBar(),
141
162
]);
142
163
164
+ const header =
165
+ `### πŸ™ [**GITBOT β€” GitHub Developer Operating System**](https://github.com/husteno-o/git-discord-bot)\n` +
166
+ `> *The ultimate Discord command center engineered so developers never have to leave chat for GitHub workflows.*\n\n` +
167
+ `πŸš€ **Instant Telemetry** β€’ 🌿 **PR Power Tools** β€’ πŸ›‘οΈ **Zero-Config Security**\n\n`;
168
+
143
169
const embed = createBaseEmbed(`${NF.github} GITBOT Developer Center`)
144
170
.setColor(BrandColors.primary)
145
- .setDescription(tui);
171
+ .setDescription(`${header}${tui}`);
146
172
147
173
const components = [createHelpSelect()];
148
174
await interaction.reply({ embeds: [embed], components });
149
175
β–Όapps/bot/src/ui/components.ts
+12-12
15
15
const row1 = new ActionRowBuilder<ButtonBuilder>().addComponents(
16
16
new ButtonBuilder()
17
17
.setCustomId(`repo:overview:${owner}:${repo}`)
18
- .setLabel("Overview")
18
+ .setLabel("πŸ“¦ Overview")
19
19
.setStyle(activeTab === "overview" ? ButtonStyle.Primary : ButtonStyle.Secondary),
20
20
new ButtonBuilder()
21
21
.setCustomId(`repo:prs:${owner}:${repo}`)
22
- .setLabel("PRs")
22
+ .setLabel("πŸ”€ Pull Requests")
23
23
.setStyle(activeTab === "prs" ? ButtonStyle.Primary : ButtonStyle.Secondary),
24
24
new ButtonBuilder()
25
25
.setCustomId(`repo:issues:${owner}:${repo}`)
26
- .setLabel("Issues")
26
+ .setLabel("🐞 Issues")
27
27
.setStyle(activeTab === "issues" ? ButtonStyle.Primary : ButtonStyle.Secondary),
28
28
new ButtonBuilder()
29
29
.setCustomId(`repo:commits:${owner}:${repo}`)
30
- .setLabel("Commits")
30
+ .setLabel("πŸ“œ Commits")
31
31
.setStyle(activeTab === "commits" ? ButtonStyle.Primary : ButtonStyle.Secondary),
32
32
new ButtonBuilder()
33
33
.setCustomId(`repo:releases:${owner}:${repo}`)
34
- .setLabel("Releases")
34
+ .setLabel("🏷️ Releases")
35
35
.setStyle(activeTab === "releases" ? ButtonStyle.Primary : ButtonStyle.Secondary),
36
36
);
37
37
57
57
.setCustomId(`pr:merge:${owner}:${repo}:${prNumber}`)
58
58
.setLabel(`${NF.gitMerge} Merge`)
59
59
.setStyle(ButtonStyle.Primary),
60
- new ButtonBuilder().setLabel("Open GitHub β†—").setStyle(ButtonStyle.Link).setURL(prUrl),
60
+ new ButtonBuilder().setLabel("🌐 Open GitHub β†—").setStyle(ButtonStyle.Link).setURL(prUrl),
61
61
);
62
62
return [row];
63
63
}
66
66
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
67
67
new ButtonBuilder()
68
68
.setCustomId("home:nav:repos")
69
- .setLabel("Repositories")
69
+ .setLabel("πŸ“¦ Repositories")
70
70
.setStyle(ButtonStyle.Secondary),
71
71
new ButtonBuilder()
72
72
.setCustomId("home:nav:prs")
73
- .setLabel("Pull Requests")
73
+ .setLabel("πŸ”€ Pull Requests")
74
74
.setStyle(ButtonStyle.Secondary),
75
75
new ButtonBuilder()
76
76
.setCustomId("home:nav:issues")
77
- .setLabel("Issues")
77
+ .setLabel("🐞 Issues")
78
78
.setStyle(ButtonStyle.Secondary),
79
79
new ButtonBuilder()
80
80
.setCustomId("home:nav:activity")
81
- .setLabel("Activity")
81
+ .setLabel("πŸ“ˆ Activity")
82
82
.setStyle(ButtonStyle.Secondary),
83
83
);
84
84
return [row];
88
88
return new ActionRowBuilder<ButtonBuilder>().addComponents(
89
89
new ButtonBuilder()
90
90
.setCustomId(`secret:delete:${messageId}`)
91
- .setLabel(`${NF.cross} Delete Leaked Message`)
91
+ .setLabel("πŸ—‘οΈ Delete Leaked Secret")
92
92
.setStyle(ButtonStyle.Danger),
93
93
new ButtonBuilder()
94
- .setLabel("Security Guide β†—")
94
+ .setLabel("πŸ›‘οΈ Security Guide β†—")
95
95
.setStyle(ButtonStyle.Link)
96
96
.setURL(
97
97
"https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation",
98
98
β–Όapps/bot/src/ui/embeds.ts
+169-92
21
21
import { BrandColors } from "./colors.js";
22
22
import { NF } from "./icons.js";
23
23
import {
24
- padText,
24
+ ANSI,
25
+ clipAnsi,
26
+ padAnsi,
25
27
renderMeter,
26
28
renderTuiCard,
27
29
tuiBottomBar,
37
39
.setColor(BrandColors.primary)
38
40
.setTitle(title)
39
41
.setFooter({
40
- text: "GITBOT TUI β€’ Developer Operating System",
42
+ text: "GITBOT v2.4.0 β€’ Interactive Terminal Console",
41
43
iconURL: "https://github.githubassets.com/favicons/favicon.png",
42
44
})
43
45
.setTimestamp();
54
56
tuiTopBar("GITBOT ERROR"),
55
57
tuiPrompt("gitbot --last-error"),
56
58
tuiDivider("DIAGNOSTIC TRACE"),
57
- tuiLine(`Status : ${NF.cross} Execution Failed`),
58
- tuiLine(`Error : ${message}`),
59
+ tuiLine(`${ANSI.red}Status : βœ• Execution Failed${ANSI.reset}`),
60
+ tuiLine(`${ANSI.yellow}Error : ${clipAnsi(message, 30)}${ANSI.reset}`),
59
61
tuiDivider("RECOVERY"),
60
- tuiLine("Check parameters or repo access."),
62
+ tuiLine(`${ANSI.dim}Check parameters or repository access.${ANSI.reset}`),
61
63
tuiBottomBar(),
62
64
]);
63
65
75
77
metrics: RepoDashboardMetrics,
76
78
_activeTab = "overview",
77
79
): EmbedBuilder {
78
- const descQuote = repo.description ? `> ${repo.description.replace(/\n/g, " ")}\n\n` : "";
79
80
const healthScore = Math.round(
80
81
Math.min(
81
82
100,
87
88
),
88
89
),
89
90
);
91
+
90
92
const tui = renderTuiCard([
91
93
tuiTopBar("GITBOT TUI v2.4.0"),
92
94
tuiPrompt(`gitbot repo ${repo.fullName}`),
93
95
tuiDivider("METADATA"),
94
- tuiLine(`Target : ${repo.fullName}`),
95
96
tuiLine(
96
- `Stack : ${repo.language || "Plain Text"} β€’ ${repo.isPrivate ? "Private" : "Public"}`,
97
+ `${ANSI.dim}Target :${ANSI.reset} ${ANSI.bold}${clipAnsi(repo.fullName, 29)}${ANSI.reset}`,
98
+ ),
99
+ tuiLine(
100
+ `${ANSI.dim}Stack :${ANSI.reset} ${ANSI.magenta}${repo.language || "Plain Text"}${ANSI.reset} β€’ ${repo.isPrivate ? `${ANSI.red}Private` : `${ANSI.green}Public`}${ANSI.reset}`,
101
+ ),
102
+ tuiLine(
103
+ `${ANSI.dim}Branch :${ANSI.reset} ${ANSI.blue}${repo.defaultBranch}${ANSI.reset} β€’ ${ANSI.yellow}${repo.license?.spdxId || "No License"}${ANSI.reset}`,
97
104
),
98
- tuiLine(`Branch : ${repo.defaultBranch} β€’ ${repo.license?.spdxId || "No License"}`),
99
105
tuiDivider("TELEMETRY"),
100
106
tuiRow2(
101
107
"Stars",
102
- `β˜… ${repo.stars.toLocaleString()}`,
108
+ `${ANSI.yellow}${repo.stars.toLocaleString()}${ANSI.reset}`,
103
109
"Forks",
104
- `β‘‚ ${repo.forks.toLocaleString()}`,
110
+ `${ANSI.magenta}${repo.forks.toLocaleString()}${ANSI.reset}`,
105
111
),
106
112
tuiRow2(
107
113
"Issues",
108
- `β˜‰ ${repo.openIssuesCount.toLocaleString()} Open`,
114
+ `${ANSI.green}${repo.openIssuesCount.toLocaleString()} Open${ANSI.reset}`,
109
115
"PRs",
110
- `βŽ‡ ${metrics.activity.prsMerged} Merged`,
116
+ `${ANSI.cyan}${metrics.activity.prsMerged} Merged${ANSI.reset}`,
111
117
),
112
118
tuiDivider("14-DAY ACTIVITY"),
113
119
tuiRow2(
114
120
"Commits",
115
- `${metrics.activity.commitsCount} pushed`,
116
- "PR Cycle",
117
- formatDuration(metrics.cycleTime.avgPrCycleTimeMs),
121
+ `${ANSI.cyan}${metrics.activity.commitsCount} pushed${ANSI.reset}`,
122
+ "PR Time",
123
+ `${ANSI.white}${formatDuration(metrics.cycleTime.avgPrCycleTimeMs)}${ANSI.reset}`,
118
124
),
119
125
tuiRow2(
120
126
"Through",
121
- `${metrics.codingMetrics.prThroughputPerWeek.toFixed(1)} pr/wk`,
127
+ `${ANSI.white}${metrics.codingMetrics.prThroughputPerWeek.toFixed(1)} pr/wk${ANSI.reset}`,
122
128
"Resolved",
123
- `${metrics.codingMetrics.issueResolutionRatePercent.toFixed(0)}%`,
129
+ `${ANSI.green}${metrics.codingMetrics.issueResolutionRatePercent.toFixed(0)}%${ANSI.reset}`,
124
130
),
125
131
tuiDivider("HEALTH INDEX"),
126
- tuiLine(`Health : ${renderMeter(healthScore)} ${healthScore}/100`),
132
+ tuiLine(
133
+ `${ANSI.dim}Health :${ANSI.reset} ${renderMeter(healthScore)} ${ANSI.green}${healthScore}/100${ANSI.reset}`,
134
+ ),
127
135
tuiBottomBar(),
128
136
]);
129
137
138
+ const descQuote = repo.description ? `> *${repo.description.replace(/\n/g, " ")}*\n\n` : "";
139
+ const badges = `⭐ **${repo.stars.toLocaleString()}** Stars β€’ 🍴 **${repo.forks.toLocaleString()}** Forks β€’ ${repo.isPrivate ? "πŸ”΄ **Private**" : "🟒 **Public**"} β€’ 🏷️ **${repo.license?.spdxId || "No License"}**\n\n`;
140
+
130
141
return new EmbedBuilder()
131
- .setColor(BrandColors.github)
142
+ .setColor(healthScore >= 80 ? BrandColors.success : BrandColors.warning)
132
143
.setTitle(`${NF.github} ${repo.fullName}`)
133
144
.setAuthor({
134
- name: `GITBOT TUI β€’ ${repo.fullName}`,
145
+ name: `GITBOT Terminal β€’ ${repo.fullName}`,
135
146
iconURL: repo.owner.avatarUrl,
136
147
url: repo.htmlUrl,
137
148
})
138
149
.setURL(repo.htmlUrl)
139
- .setDescription(descQuote + tui)
150
+ .setDescription(`${descQuote}${badges}${tui}`)
140
151
.setFooter({
141
- text: "GITBOT TUI β€’ Use buttons below to switch tabs",
152
+ text: "GITBOT v2.4.0 β€’ Interactive Terminal Console β€’ Press buttons below to navigate",
142
153
iconURL: "https://github.githubassets.com/favicons/favicon.png",
143
154
})
144
155
.setTimestamp();
149
160
tuiTopBar(`HEALTH SCORE: ${health.overallScore}/100`),
150
161
tuiPrompt(`gitbot repo ${repo.fullName} --health`),
151
162
tuiDivider("METRIC GAUGES"),
152
- tuiLine(`Activity ${renderMeter(health.activityScore)} ${health.activityScore}/100`),
153
- tuiLine(`Maintenance ${renderMeter(health.maintenanceScore)} ${health.maintenanceScore}/100`),
154
- tuiLine(`CI / CD ${renderMeter(health.ciScore)} ${health.ciScore}/100`),
155
- tuiLine(`Community ${renderMeter(health.communityScore)} ${health.communityScore}/100`),
156
- tuiLine(`Releases ${renderMeter(health.releasesScore)} ${health.releasesScore}/100`),
163
+ tuiLine(
164
+ `${ANSI.dim}Activity ${ANSI.reset}${renderMeter(health.activityScore)} ${ANSI.green}${health.activityScore}/100${ANSI.reset}`,
165
+ ),
166
+ tuiLine(
167
+ `${ANSI.dim}Maintenance ${ANSI.reset}${renderMeter(health.maintenanceScore)} ${ANSI.green}${health.maintenanceScore}/100${ANSI.reset}`,
168
+ ),
169
+ tuiLine(
170
+ `${ANSI.dim}CI / CD ${ANSI.reset}${renderMeter(health.ciScore)} ${ANSI.green}${health.ciScore}/100${ANSI.reset}`,
171
+ ),
172
+ tuiLine(
173
+ `${ANSI.dim}Community ${ANSI.reset}${renderMeter(health.communityScore)} ${ANSI.green}${health.communityScore}/100${ANSI.reset}`,
174
+ ),
175
+ tuiLine(
176
+ `${ANSI.dim}Releases ${ANSI.reset}${renderMeter(health.releasesScore)} ${ANSI.green}${health.releasesScore}/100${ANSI.reset}`,
177
+ ),
157
178
tuiDivider("AUDIT METRICS"),
158
179
tuiRow2(
159
180
"Commits(30d)",
160
- `${health.details.commits30d}`,
181
+ `${ANSI.cyan}${health.details.commits30d}${ANSI.reset}`,
161
182
"Open Ratio",
162
- `${health.details.openIssuesRatio}`,
183
+ `${ANSI.yellow}${health.details.openIssuesRatio}${ANSI.reset}`,
163
184
),
164
185
tuiRow2(
165
186
"Releases",
166
- `${health.details.releasesCount} tags`,
187
+ `${ANSI.magenta}${health.details.releasesCount} tags${ANSI.reset}`,
167
188
"Status",
168
- health.overallScore >= 80 ? "HEALTHY" : "NEEDS WORK",
189
+ health.overallScore >= 80
190
+ ? `${ANSI.green}HEALTHY${ANSI.reset}`
191
+ : `${ANSI.yellow}NEEDS WORK${ANSI.reset}`,
169
192
),
170
193
tuiBottomBar(),
171
194
]);
172
195
196
+ const badges = `❀️ **Score:** \`${health.overallScore}/100\` β€’ ${health.overallScore >= 80 ? "🟒 **Healthy**" : "🟑 **Needs Maintenance**"} β€’ πŸ“œ **${health.details.commits30d}** Commits (30d)\n\n`;
197
+
173
198
return createBaseEmbed(`${NF.heart} Health Score: ${repo.fullName}`)
174
199
.setAuthor({
175
- name: `GITBOT TUI β€’ ${repo.fullName}`,
200
+ name: `GITBOT Terminal β€’ ${repo.fullName}`,
176
201
iconURL: repo.owner.avatarUrl,
177
202
url: repo.htmlUrl,
178
203
})
179
204
.setURL(repo.htmlUrl)
180
205
.setColor(health.overallScore > 80 ? BrandColors.success : BrandColors.warning)
181
- .setDescription(tui);
206
+ .setDescription(`${badges}${tui}`);
182
207
}
183
208
184
209
export function createRepoGrowthEmbed(repo: GitHubRepo, growth: RepoGrowthMetrics): EmbedBuilder {
187
212
tuiPrompt(`gitbot repo ${repo.fullName} --growth`),
188
213
tuiDivider("TELEMETRY ACCELERATION"),
189
214
tuiLine(
190
- `Stars Total : ${growth.starsTotal.toLocaleString()} (+${growth.starsDelta30d.toLocaleString()} in 30d)`,
215
+ `${ANSI.dim}Stars Total :${ANSI.reset} ${ANSI.yellow}${growth.starsTotal.toLocaleString()} (+${growth.starsDelta30d.toLocaleString()} in 30d)${ANSI.reset}`,
216
+ ),
217
+ tuiLine(
218
+ `${ANSI.dim}Forks Total :${ANSI.reset} ${ANSI.magenta}${growth.forksTotal.toLocaleString()} (+${growth.forksDelta30d.toLocaleString()} in 30d)${ANSI.reset}`,
191
219
),
192
220
tuiLine(
193
- `Forks Total : ${growth.forksTotal.toLocaleString()} (+${growth.forksDelta30d.toLocaleString()} in 30d)`,
221
+ `${ANSI.dim}Contributors :${ANSI.reset} ${ANSI.cyan}${growth.contributorsTotal} authors${ANSI.reset}`,
194
222
),
195
- tuiLine(`Contributors : ${growth.contributorsTotal} authors`),
196
223
tuiLine(
197
- `PRs Merged : ${growth.prsMerged30d} (${(growth.prsMerged30d / 4.2).toFixed(1)} / week)`,
224
+ `${ANSI.dim}PRs Merged :${ANSI.reset} ${ANSI.green}${growth.prsMerged30d} (${(growth.prsMerged30d / 4.2).toFixed(1)}/wk)${ANSI.reset}`,
198
225
),
199
226
tuiBottomBar(),
200
227
]);
201
228
229
+ const badges = `πŸ“ˆ **Velocity:** \`${(growth.prsMerged30d / 4.2).toFixed(1)} PRs/wk\` β€’ ⭐ **+${growth.starsDelta30d}** Stars β€’ πŸ‘₯ **${growth.contributorsTotal}** Authors\n\n`;
230
+
202
231
return createBaseEmbed(`${NF.speedometer} 30-Day Growth: ${repo.fullName}`)
203
232
.setAuthor({
204
- name: `GITBOT TUI β€’ ${repo.fullName}`,
233
+ name: `GITBOT Terminal β€’ ${repo.fullName}`,
205
234
iconURL: repo.owner.avatarUrl,
206
235
url: repo.htmlUrl,
207
236
})
208
237
.setColor(BrandColors.primary)
209
238
.setURL(repo.htmlUrl)
210
- .setDescription(tui);
239
+ .setDescription(`${badges}${tui}`);
211
240
}
212
241
213
242
export function createDependenciesEmbed(repo: GitHubRepo, deps: RepoDependencies): EmbedBuilder {
214
243
const topDepsLines = deps.dependencies.slice(0, 8).map((d) => {
215
- return tuiLine(`β–Έ ${padText(d.name, 22)} ${d.version}`);
244
+ return tuiLine(
245
+ `${ANSI.cyan}β–Έ${ANSI.reset} ${ANSI.bold}${padAnsi(clipAnsi(d.name, 22), 22)}${ANSI.reset} ${ANSI.green}${clipAnsi(d.version, 13)}${ANSI.reset}`,
246
+ );
216
247
});
217
248
218
249
const tui = renderTuiCard([
219
250
tuiTopBar("DEPENDENCY MANIFEST"),
220
251
tuiPrompt(`gitbot repo ${repo.fullName} --deps`),
221
252
tuiDivider("ECOSYSTEM & PACKAGES"),
222
- tuiLine(`Manifest : ${deps.manifestFile} (${deps.ecosystem})`),
223
- tuiRow2("Total", `${deps.totalCount}`, "Outdated", `${deps.outdatedCount}`),
253
+ tuiLine(
254
+ `${ANSI.dim}Manifest :${ANSI.reset} ${ANSI.yellow}${clipAnsi(deps.manifestFile, 18)}${ANSI.reset} (${deps.ecosystem})`,
255
+ ),
256
+ tuiRow2(
257
+ "Total",
258
+ `${ANSI.cyan}${deps.totalCount}${ANSI.reset}`,
259
+ "Outdated",
260
+ deps.outdatedCount > 0
261
+ ? `${ANSI.red}${deps.outdatedCount}${ANSI.reset}`
262
+ : `${ANSI.green}0${ANSI.reset}`,
263
+ ),
224
264
tuiDivider("TOP PACKAGES"),
225
265
...(topDepsLines.length > 0 ? topDepsLines : [tuiLine("No dependencies detected.")]),
226
266
tuiBottomBar(),
227
267
]);
228
268
269
+ const badges = `πŸ“¦ **Packages:** \`${deps.totalCount}\` β€’ ${deps.outdatedCount > 0 ? `⚠️ **${deps.outdatedCount}** Outdated` : "🟒 **All Up to Date**"} β€’ πŸ“„ **${deps.manifestFile}**\n\n`;
270
+
229
271
return createBaseEmbed(`${NF.network} Dependencies: ${repo.fullName}`)
230
272
.setAuthor({
231
- name: `GITBOT TUI β€’ ${repo.fullName}`,
273
+ name: `GITBOT Terminal β€’ ${repo.fullName}`,
232
274
iconURL: repo.owner.avatarUrl,
233
275
url: repo.htmlUrl,
234
276
})
235
277
.setURL(repo.htmlUrl)
236
- .setDescription(tui);
278
+ .setDescription(`${badges}${tui}`);
237
279
}
238
280
239
281
// 2. PR POWER TOOLS
252
294
: "In Review";
253
295
254
296
const approvedCount = pr.reviews.filter((r) => r.state === "APPROVED").length;
255
- const ciStatusText =
256
- pr.ciStatus === "success"
257
- ? "[βœ“] Passing"
258
- : pr.ciStatus === "failure"
259
- ? "[βœ•] Failed"
260
- : "[β ‹] Running";
261
- const conflictText = pr.mergeable === false ? "[βœ•] Conflicts" : "[βœ“] Clean";
297
+ const ciColor =
298
+ pr.ciStatus === "success" ? ANSI.green : pr.ciStatus === "failure" ? ANSI.red : ANSI.yellow;
299
+ const ciStatusText = `${ciColor}${pr.ciStatus === "success" ? "βœ“ Passing" : pr.ciStatus === "failure" ? "βœ• Failed" : "β ‹ Running"}${ANSI.reset}`;
300
+ const conflictText =
301
+ pr.mergeable === false
302
+ ? `${ANSI.red}βœ• Conflicts${ANSI.reset}`
303
+ : `${ANSI.green}βœ“ Clean${ANSI.reset}`;
262
304
263
305
const tui = renderTuiCard([
264
306
tuiTopBar(`PR #${pr.number} INSPECTION`),
265
307
tuiPrompt(`gh pr view ${pr.number}`),
266
308
tuiDivider("BRANCH & STATUS"),
267
- tuiLine(`Title : ${pr.title}`),
268
- tuiLine(`Branch : ${pr.headBranch} -> ${pr.baseBranch}`),
309
+ tuiLine(`${ANSI.dim}Title :${ANSI.reset} ${clipAnsi(pr.title, 28)}`),
310
+ tuiLine(
311
+ `${ANSI.dim}Branch :${ANSI.reset} ${ANSI.cyan}${clipAnsi(pr.headBranch, 12)}${ANSI.reset} βž” ${ANSI.blue}${clipAnsi(pr.baseBranch, 12)}${ANSI.reset}`,
312
+ ),
269
313
tuiRow2("Author", `@${pr.author.login}`, "Status", statusBadge),
270
314
tuiDivider("DIFF & CHECKS"),
271
- tuiLine(`Changes : +${pr.additions} -${pr.deletions} (${pr.changedFiles} files)`),
315
+ tuiLine(
316
+ `${ANSI.dim}Changes :${ANSI.reset} ${ANSI.green}+${pr.additions}${ANSI.reset} ${ANSI.red}-${pr.deletions}${ANSI.reset} (${pr.changedFiles} files)`,
317
+ ),
272
318
tuiRow2("CI Check", ciStatusText, "Conflicts", conflictText),
273
- tuiRow2("Reviews", `${approvedCount} approved`, "Cycle Time", formatDuration(pr.cycleTimeMs)),
319
+ tuiRow2(
320
+ "Reviews",
321
+ `${ANSI.green}${approvedCount} approved${ANSI.reset}`,
322
+ "Cycle Time",
323
+ formatDuration(pr.cycleTimeMs),
324
+ ),
274
325
tuiBottomBar(),
275
326
]);
276
327
328
+ const badges = `${isReady ? "🟒 **Ready to Merge**" : pr.draft ? "βšͺ **Draft**" : "🟑 **In Review**"} β€’ 🌿 \`${pr.headBranch}\` βž” \`${pr.baseBranch}\` β€’ πŸ‘€ **@${pr.author.login}**\n\n`;
329
+
277
330
const embed = createBaseEmbed(`${NF.gitPullRequest} PR #${pr.number}: ${pr.title}`)
278
331
.setURL(pr.htmlUrl)
279
332
.setColor(isReady ? BrandColors.success : BrandColors.warning)
280
- .setDescription(tui);
333
+ .setDescription(`${badges}${tui}`);
281
334
282
335
const warnings: string[] = [];
283
336
if (pr.changedFiles > 15 || pr.additions + pr.deletions > 500) {
284
- warnings.push(`${NF.warning} High change complexity (> 500 lines or > 15 files)`);
337
+ warnings.push(`${NF.warning} **High change complexity** (> 500 lines or > 15 files)`);
285
338
}
286
339
if (pr.waitingOn && pr.waitingOn.length > 0) {
287
340
warnings.push(
288
- `${NF.clock} Waiting on review: ${pr.waitingOn.map((u) => `@${u}`).join(", ")} (${pr.waitingHours}h)`,
341
+ `${NF.clock} **Waiting on review:** ${pr.waitingOn.map((u) => `@${u}`).join(", ")} (${pr.waitingHours}h)`,
289
342
);
290
343
}
291
344
if (pr.isStale) {
292
- warnings.push(`${NF.warning} Pull request has been inactive for over 14 days`);
345
+ warnings.push(`${NF.warning} **Stale PR:** Inactive for over 14 days`);
293
346
}
294
347
295
348
if (warnings.length > 0) {
358
411
const prefix = isLast ? "└──" : "β”œβ”€β”€";
359
412
const statusIcon =
360
413
r.conclusion === "success" ? NF.check : r.conclusion === "failure" ? NF.cross : NF.spinner;
361
- return `${prefix} ${padText(r.name, 16)} ${statusIcon} (${r.headBranch})`;
414
+ return `${prefix} ${padAnsi(clipAnsi(r.name, 16), 16)} ${statusIcon} (${r.headBranch})`;
362
415
});
363
416
364
417
return createBaseEmbed(`${NF.robot} GitHub Actions: ${repo}`).setDescription(
445
498
): EmbedBuilder {
446
499
const tui = renderTuiCard([
447
500
tuiTopBar("DEVELOPER CENTER"),
448
- tuiPrompt(`gitbot whoami --stats`),
501
+ tuiPrompt("gitbot whoami --stats"),
449
502
tuiDivider(`USER: @${username.toUpperCase()}`),
450
- tuiRow2("Commits", `${userStats.commits}`, "PRs", `${userStats.prs}`),
451
- tuiRow2("Reviews", `${userStats.reviews}`, "Issues", `${userStats.issues}`),
503
+ tuiRow2(
504
+ "Commits",
505
+ `${ANSI.cyan}${userStats.commits}${ANSI.reset}`,
506
+ "PRs",
507
+ `${ANSI.green}${userStats.prs}${ANSI.reset}`,
508
+ ),
509
+ tuiRow2(
510
+ "Reviews",
511
+ `${ANSI.yellow}${userStats.reviews}${ANSI.reset}`,
512
+ "Issues",
513
+ `${ANSI.magenta}${userStats.issues}${ANSI.reset}`,
514
+ ),
452
515
tuiBottomBar(),
453
516
]);
454
517
470
533
tuiTopBar("AUTH SUBSYSTEM"),
471
534
tuiPrompt("gitbot auth status"),
472
535
tuiDivider("SESSION METADATA"),
473
- tuiLine(`Identity : @${account}`),
474
- tuiLine(`Status : ${isConn ? "[βœ“] Connected via App" : "[βœ•] Disconnected"}`),
536
+ tuiLine(`${ANSI.dim}Identity :${ANSI.reset} ${ANSI.bold}@${account}${ANSI.reset}`),
537
+ tuiLine(
538
+ `${ANSI.dim}Status :${ANSI.reset} ${isConn ? `${ANSI.green}βœ“ Connected via App` : `${ANSI.red}βœ• Disconnected`}${ANSI.reset}`,
539
+ ),
475
540
tuiDivider("CAPABILITIES"),
476
- tuiLine(`Repos Read/Watch : [βœ“] Enabled`),
477
- tuiLine(`Issues & PRs : [βœ“] Enabled`),
478
- tuiLine(`Actions CI : [βœ“] Enabled`),
479
- tuiLine(`Write & Merge : ${scopes.includes("write") ? "[βœ“] Enabled" : "[βœ•] Read Mode"}`),
541
+ tuiLine(`${ANSI.dim}Repos Read/Watch :${ANSI.reset} ${ANSI.green}βœ“ Enabled${ANSI.reset}`),
542
+ tuiLine(`${ANSI.dim}Issues & PRs :${ANSI.reset} ${ANSI.green}βœ“ Enabled${ANSI.reset}`),
543
+ tuiLine(`${ANSI.dim}Actions CI :${ANSI.reset} ${ANSI.green}βœ“ Enabled${ANSI.reset}`),
544
+ tuiLine(
545
+ `${ANSI.dim}Write & Merge :${ANSI.reset} ${scopes.includes("write") ? `${ANSI.green}βœ“ Enabled` : `${ANSI.yellow}βœ• Read Mode`}${ANSI.reset}`,
546
+ ),
480
547
tuiBottomBar(),
481
548
]);
482
549
497
564
const commitLines = commits.slice(0, 7).map((c) => {
498
565
const sha = c.sha.slice(0, 7);
499
566
const msg = c.message.split("\n")[0];
500
- return tuiLine(`β–Έ ${sha} ${msg}`);
567
+ return tuiLine(
568
+ `${ANSI.cyan}β–Έ${ANSI.reset} ${ANSI.yellow}${sha}${ANSI.reset} ${clipAnsi(msg, 28)}`,
569
+ );
501
570
});
502
571
503
572
const tui = renderTuiCard([
504
573
tuiTopBar("GIT LOG --ONELINE"),
505
- tuiPrompt(`git log -n 7 --oneline`),
574
+ tuiPrompt("git log -n 7 --oneline"),
506
575
tuiDivider("RECENT COMMITS"),
507
576
...(commitLines.length > 0 ? commitLines : [tuiLine("No commits recorded.")]),
508
577
tuiBottomBar(),
509
578
]);
510
579
511
580
const markdownLinks = commits
512
- .slice(0, 6)
581
+ .slice(0, 5)
513
582
.map(
514
583
(c) =>
515
- `β€’ [\`${c.sha.slice(0, 7)}\`](${c.htmlUrl}) ${c.message.split("\n")[0]} β€” *@${c.author.name}*`,
584
+ `β€’ [\`${c.sha.slice(0, 7)}\`](${c.htmlUrl}) **${c.message.split("\n")[0].slice(0, 45)}** β€” *@${c.author.name}*`,
516
585
)
517
586
.join("\n");
518
587
519
588
return createBaseEmbed(`${NF.gitCommit} Commits: ${repo.fullName}`)
520
589
.setAuthor({
521
- name: `GITBOT TUI β€’ ${repo.fullName}`,
590
+ name: `GITBOT Terminal β€’ ${repo.fullName}`,
522
591
iconURL: repo.owner.avatarUrl,
523
592
url: repo.htmlUrl,
524
593
})
525
594
.setURL(repo.htmlUrl)
526
- .setDescription(`${tui}\n\n**Commit History:**\n${markdownLinks || "None"}`);
595
+ .setDescription(`${tui}\n\n**πŸ“œ Direct Commit Links:**\n${markdownLinks || "None"}`);
527
596
}
528
597
529
598
export function createRepoPrsEmbed(repo: GitHubRepo, prs: GitHubPullRequest[]): EmbedBuilder {
530
599
const prLines = prs.slice(0, 7).map((p) => {
531
600
const status = p.state.toUpperCase();
532
- return tuiLine(`[#${p.number}] ${padText(p.title, 24)} (${status})`);
601
+ const color = p.state === "open" ? ANSI.green : ANSI.magenta;
602
+ return tuiLine(
603
+ `${ANSI.cyan}[#${p.number}]${ANSI.reset} ${padAnsi(clipAnsi(p.title, 22), 22)} (${color}${status}${ANSI.reset})`,
604
+ );
533
605
});
534
606
535
607
const tui = renderTuiCard([
536
608
tuiTopBar("PULL REQUEST QUEUE"),
537
- tuiPrompt(`gh pr list --state all --limit 7`),
609
+ tuiPrompt("gh pr list --state all --limit 7"),
538
610
tuiDivider("RECENT PR RECORDS"),
539
611
...(prLines.length > 0 ? prLines : [tuiLine("No pull requests found.")]),
540
612
tuiBottomBar(),
541
613
]);
542
614
543
615
const markdownLinks = prs
544
- .slice(0, 6)
545
- .map((p) => `β€’ [#${p.number}](${p.htmlUrl}) **${p.title}** (\`${p.state}\`)`)
616
+ .slice(0, 5)
617
+ .map((p) => `β€’ [#${p.number}](${p.htmlUrl}) **${p.title.slice(0, 45)}** (\`${p.state}\`)`)
546
618
.join("\n");
547
619
548
620
return createBaseEmbed(`${NF.gitPullRequest} Pull Requests: ${repo.fullName}`)
549
621
.setAuthor({
550
- name: `GITBOT TUI β€’ ${repo.fullName}`,
622
+ name: `GITBOT Terminal β€’ ${repo.fullName}`,
551
623
iconURL: repo.owner.avatarUrl,
552
624
url: repo.htmlUrl,
553
625
})
554
626
.setURL(repo.htmlUrl)
555
- .setDescription(`${tui}\n\n**Quick PR Links:**\n${markdownLinks || "None"}`);
627
+ .setDescription(`${tui}\n\n**πŸ”€ Direct PR Links:**\n${markdownLinks || "None"}`);
556
628
}
557
629
558
630
export function createRepoIssuesEmbed(repo: GitHubRepo, issues: GitHubIssue[]): EmbedBuilder {
559
631
const issueLines = issues.slice(0, 7).map((i) => {
560
632
const status = i.state.toUpperCase();
561
- return tuiLine(`[#${i.number}] ${padText(i.title, 24)} (${status})`);
633
+ const color = i.state === "open" ? ANSI.green : ANSI.magenta;
634
+ return tuiLine(
635
+ `${ANSI.cyan}[#${i.number}]${ANSI.reset} ${padAnsi(clipAnsi(i.title, 22), 22)} (${color}${status}${ANSI.reset})`,
636
+ );
562
637
});
563
638
564
639
const tui = renderTuiCard([
565
640
tuiTopBar("ISSUE TRACKER"),
566
- tuiPrompt(`gh issue list --limit 7`),
641
+ tuiPrompt("gh issue list --limit 7"),
567
642
tuiDivider("ACTIVE ISSUES"),
568
643
...(issueLines.length > 0 ? issueLines : [tuiLine("No issues found.")]),
569
644
tuiBottomBar(),
570
645
]);
571
646
572
647
const markdownLinks = issues
573
- .slice(0, 6)
574
- .map((i) => `β€’ [#${i.number}](${i.htmlUrl}) **${i.title}** (\`${i.state}\`)`)
648
+ .slice(0, 5)
649
+ .map((i) => `β€’ [#${i.number}](${i.htmlUrl}) **${i.title.slice(0, 45)}** (\`${i.state}\`)`)
575
650
.join("\n");
576
651
577
652
return createBaseEmbed(`${NF.issue} Issues: ${repo.fullName}`)
578
653
.setAuthor({
579
- name: `GITBOT TUI β€’ ${repo.fullName}`,
654
+ name: `GITBOT Terminal β€’ ${repo.fullName}`,
580
655
iconURL: repo.owner.avatarUrl,
581
656
url: repo.htmlUrl,
582
657
})
583
658
.setURL(repo.htmlUrl)
584
- .setDescription(`${tui}\n\n**Quick Issue Links:**\n${markdownLinks || "None"}`);
659
+ .setDescription(`${tui}\n\n**🐞 Direct Issue Links:**\n${markdownLinks || "None"}`);
585
660
}
586
661
587
662
export function createRepoReleasesEmbed(repo: GitHubRepo, releases: GitHubRelease[]): EmbedBuilder {
588
663
const releaseLines = releases.slice(0, 5).map((r) => {
589
- return tuiLine(`β–Έ ${padText(r.tagName, 12)} ${r.name}`);
664
+ return tuiLine(
665
+ `${ANSI.cyan}β–Έ${ANSI.reset} ${ANSI.yellow}${padAnsi(clipAnsi(r.tagName, 10), 10)}${ANSI.reset} ${clipAnsi(r.name, 22)}`,
666
+ );
590
667
});
591
668
592
669
const tui = renderTuiCard([
593
670
tuiTopBar("RELEASE TAGS"),
594
- tuiPrompt(`gh release list --limit 5`),
671
+ tuiPrompt("gh release list --limit 5"),
595
672
tuiDivider("PUBLISHED RELEASES"),
596
673
...(releaseLines.length > 0 ? releaseLines : [tuiLine("No releases found.")]),
597
674
tuiBottomBar(),
607
684
608
685
return createBaseEmbed(`${NF.gitTag} Releases: ${repo.fullName}`)
609
686
.setAuthor({
610
- name: `GITBOT TUI β€’ ${repo.fullName}`,
687
+ name: `GITBOT Terminal β€’ ${repo.fullName}`,
611
688
iconURL: repo.owner.avatarUrl,
612
689
url: repo.htmlUrl,
613
690
})
614
691
.setURL(repo.htmlUrl)
615
- .setDescription(`${tui}\n\n**Release Details:**\n${markdownLinks || "None"}`);
692
+ .setDescription(`${tui}\n\n**🏷️ Direct Release Links:**\n${markdownLinks || "None"}`);
616
693
}
617
694
618
695
export function createSecurityAlertEmbed(secret: DetectedSecret): EmbedBuilder {
619
696
β–Όapps/bot/src/ui/icons.ts
+40-33
1
-// Universal Developer Glyph Suite for GITBOT
2
-// 100% supported across all Discord clients (Desktop, Web, iOS, Android)
3
-// Uses standard Unicode BMP glyphs (zero cartoon emojis, zero unrendered PUA codepoints)
1
+// Expressive Developer Emoji & Glyph Suite for GITBOT
2
+// Vibrant, colorful, and supported across Discord Desktop, Web, iOS, and Android.
4
3
5
4
export const NF = {
6
5
// Brand & Git
7
- github: "β—ˆ",
8
- gitBranch: "β‘‚",
9
- gitCommit: "β–ͺ",
10
- gitPullRequest: "βŽ‡",
11
- gitMerge: "β‘‚",
12
- gitTag: "βš‘",
13
- gitFork: "β‘‚",
6
+ github: "πŸ™",
7
+ gitBranch: "🌿",
8
+ gitCommit: "πŸ“œ",
9
+ gitPullRequest: "πŸ”€",
10
+ gitMerge: "πŸ”€",
11
+ gitTag: "🏷️",
12
+ gitFork: "🍴",
13
+ issue: "🐞",
14
+ bug: "πŸ›",
14
15
15
16
// Status & Telemetry
16
- check: "βœ“",
17
- cross: "βœ•",
18
- warning: "!",
19
- spinner: "β ‹",
20
- star: "β˜…",
21
- flame: "β–²",
22
- clock: "β—·",
23
- speedometer: "β—ˆ",
24
- package: "β–ͺ",
25
- terminal: "β–Έ",
26
- document: "≑",
27
- search: "βŒ•",
28
- shield: "β—†",
29
- network: "☍",
30
- heart: "β™₯",
31
- user: "@",
32
- users: "@@",
17
+ check: "βœ…",
18
+ cross: "❌",
19
+ warning: "⚠️",
20
+ spinner: "⏳",
21
+ star: "⭐",
22
+ flame: "πŸ”₯",
23
+ clock: "⏱️",
24
+ speedometer: "πŸ“ˆ",
25
+ package: "πŸ“¦",
26
+ terminal: "πŸ’»",
27
+ document: "πŸ“„",
28
+ search: "πŸ”",
29
+ shield: "πŸ›‘οΈ",
30
+ network: "🌐",
31
+ heart: "❀️",
32
+ user: "πŸ‘€",
33
+ users: "πŸ‘₯",
33
34
bullet: "β€’",
34
- arrowRight: "β†’",
35
- robot: "β—ˆ",
36
- sparkle: "✦",
37
- chat: "β–Έ",
38
- issue: "β˜‰",
39
- bug: "β–ͺ",
35
+ arrowRight: "βž”",
36
+ robot: "πŸ€–",
37
+ sparkle: "✨",
38
+ chat: "πŸ’¬",
39
+ rocket: "πŸš€",
40
+ tools: "πŸ› οΈ",
41
+ bell: "πŸ””",
42
+ lock: "πŸ”",
43
+ unlock: "πŸ”“",
44
+ greenDot: "🟒",
45
+ yellowDot: "🟑",
46
+ redDot: "πŸ”΄",
40
47
} as const;
41
48
β–Όapps/bot/src/ui/tui.test.ts
+20-17
3
3
TUI_WIDTH,
4
4
renderMeter,
5
5
renderTuiCard,
6
+ stripAnsi,
6
7
tuiBottomBar,
7
8
tuiDivider,
8
9
tuiLine,
9
10
tuiPrompt,
10
11
tuiRow2,
11
12
tuiTopBar,
13
+ visibleLength,
12
14
} from "./tui.js";
13
15
14
16
describe("TUI Rendering Engine", () => {
15
- it("enforces exact fixed line width of 43 characters across all components", () => {
17
+ it("enforces exact fixed visible width of 43 characters across all components", () => {
16
18
const top = tuiTopBar("GITBOT TUI");
17
19
const prompt = tuiPrompt("gitbot test");
18
20
const div = tuiDivider("SECTION");
19
21
const plainLine = tuiLine("Sample text here");
20
- const row = tuiRow2("Stars", "β˜… 10", "Forks", "β‘‚ 2");
22
+ const row = tuiRow2("Stars", "⭐ 10", "Forks", "🍴 2");
21
23
const singleRow = tuiRow2("License", "MIT License");
22
24
const bottom = tuiBottomBar();
23
25
24
- expect(top.length).toBe(TUI_WIDTH);
25
- expect(prompt.length).toBe(TUI_WIDTH);
26
- expect(div.length).toBe(TUI_WIDTH);
27
- expect(plainLine.length).toBe(TUI_WIDTH);
28
- expect(row.length).toBe(TUI_WIDTH);
29
- expect(singleRow.length).toBe(TUI_WIDTH);
30
- expect(bottom.length).toBe(TUI_WIDTH);
26
+ expect(visibleLength(top)).toBe(TUI_WIDTH);
27
+ expect(visibleLength(prompt)).toBe(TUI_WIDTH);
28
+ expect(visibleLength(div)).toBe(TUI_WIDTH);
29
+ expect(visibleLength(plainLine)).toBe(TUI_WIDTH);
30
+ expect(visibleLength(row)).toBe(TUI_WIDTH);
31
+ expect(visibleLength(singleRow)).toBe(TUI_WIDTH);
32
+ expect(visibleLength(bottom)).toBe(TUI_WIDTH);
31
33
});
32
34
33
35
it("handles long text by gracefully clipping to prevent Discord line breaks", () => {
34
36
const overflowText =
35
37
"This is a very long line that exceeds the standard terminal viewport and would otherwise wrap awkwardly on mobile Discord screens.";
36
38
const line = tuiLine(overflowText);
37
- expect(line.length).toBe(TUI_WIDTH);
38
- expect(line.startsWith("β”‚ ")).toBe(true);
39
- expect(line.endsWith(" β”‚")).toBe(true);
39
+ expect(visibleLength(line)).toBe(TUI_WIDTH);
40
+ const stripped = stripAnsi(line);
41
+ expect(stripped.startsWith("β”‚ ")).toBe(true);
42
+ expect(stripped.endsWith(" β”‚")).toBe(true);
40
43
});
41
44
42
45
it("renders meter gauges correctly across boundary percentages", () => {
43
- expect(renderMeter(0, 10)).toBe("[β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘]");
44
- expect(renderMeter(50, 10)).toBe("[β– β– β– β– β– β–‘β–‘β–‘β–‘β–‘]");
45
- expect(renderMeter(100, 10)).toBe("[β– β– β– β– β– β– β– β– β– β– ]");
46
+ expect(stripAnsi(renderMeter(0, 10))).toBe("[β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘]");
47
+ expect(stripAnsi(renderMeter(50, 10))).toBe("[β– β– β– β– β– β–‘β–‘β–‘β–‘β–‘]");
48
+ expect(stripAnsi(renderMeter(100, 10))).toBe("[β– β– β– β– β– β– β– β– β– β– ]");
46
49
});
47
50
48
- it("wraps TUI lines in a monospace code block", () => {
51
+ it("wraps TUI lines in an ANSI monospace code block", () => {
49
52
const card = renderTuiCard([tuiTopBar("TITLE"), tuiPrompt("cmd"), tuiBottomBar()]);
50
- expect(card.startsWith("```text\n")).toBe(true);
53
+ expect(card.startsWith("```ansi\n")).toBe(true);
51
54
expect(card.endsWith("\n```")).toBe(true);
52
55
});
53
56
});
54
57
β–Όapps/bot/src/ui/tui.ts
+57-21
1
-// Terminal User Interface (TUI) Rendering Engine for GITBOT
2
-// Strict fixed-width monospace containers guaranteed to render without line-wrapping across Discord Desktop, Web, iOS, and Android.
1
+// Terminal User Interface (TUI) Colorful ANSI Rendering Engine for GITBOT
2
+// Strict fixed-width (43-column) monospace containers with rich Discord ANSI colors.
3
3
4
4
export const TUI_WIDTH = 43;
5
5
6
-export function padText(str: string, len: number): string {
7
- if (str.length > len) {
8
- return str.slice(0, len);
6
+export const ANSI = {
7
+ reset: "\x1b[0m",
8
+ bold: "\x1b[1m",
9
+ dim: "\x1b[2;37m",
10
+ red: "\x1b[1;31m",
11
+ green: "\x1b[1;32m",
12
+ yellow: "\x1b[1;33m",
13
+ blue: "\x1b[1;34m",
14
+ magenta: "\x1b[1;35m",
15
+ cyan: "\x1b[1;36m",
16
+ white: "\x1b[1;37m",
17
+ gray: "\x1b[0;30m",
18
+} as const;
19
+
20
+const ANSI_REGEX = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
21
+
22
+export function stripAnsi(str: string): string {
23
+ return str.replace(ANSI_REGEX, "");
24
+}
25
+
26
+export function visibleLength(str: string): number {
27
+ return stripAnsi(str).length;
28
+}
29
+
30
+export function padAnsi(str: string, targetLen: number): string {
31
+ const visLen = visibleLength(str);
32
+ if (visLen >= targetLen) {
33
+ return str;
34
+ }
35
+ return `${str}${" ".repeat(targetLen - visLen)}`;
36
+}
37
+
38
+export function clipAnsi(str: string, maxLen: number): string {
39
+ if (visibleLength(str) <= maxLen) {
40
+ return str;
9
41
}
10
- return str + " ".repeat(len - str.length);
42
+ const stripped = stripAnsi(str);
43
+ return `${stripped.slice(0, maxLen - 1)}…`;
11
44
}
12
45
13
46
export function tuiTopBar(title: string, width = TUI_WIDTH): string {
14
- const prefix = `β”Œβ”€ [ ${title} ] `;
15
- const rem = Math.max(0, width - prefix.length - 1);
16
- return `${prefix}${"─".repeat(rem)}┐`;
47
+ const innerTitle = `${ANSI.cyan}β”Œβ”€ [ ${ANSI.green}${title}${ANSI.cyan} ] ${ANSI.reset}`;
48
+ const rem = width - visibleLength(innerTitle) - 1;
49
+ return `${innerTitle}${ANSI.cyan}${"─".repeat(Math.max(0, rem))}┐${ANSI.reset}`;
17
50
}
18
51
19
52
export function tuiBottomBar(width = TUI_WIDTH): string {
20
- return `β””${"─".repeat(width - 2)}β”˜`;
53
+ return `${ANSI.cyan}β””${"─".repeat(width - 2)}β”˜${ANSI.reset}`;
21
54
}
22
55
23
56
export function tuiDivider(title?: string, width = TUI_WIDTH): string {
24
57
if (!title) {
25
- return `β”œ${"─".repeat(width - 2)}─`;
58
+ return `${ANSI.cyan}β”œ${"─".repeat(width - 2)}─${ANSI.reset}`;
26
59
}
27
- const prefix = `β”œβ”€ ${title} `;
28
- const rem = Math.max(0, width - prefix.length - 1);
29
- return `${prefix}${"─".repeat(rem)}─`;
60
+ const prefix = `${ANSI.cyan}β”œβ”€ ${ANSI.yellow}${title} ${ANSI.cyan}`;
61
+ const rem = width - visibleLength(prefix) - 1;
62
+ return `${prefix}${"─".repeat(Math.max(0, rem))}─${ANSI.reset}`;
30
63
}
31
64
32
65
export function tuiLine(text = "", width = TUI_WIDTH): string {
33
66
const contentWidth = width - 4;
34
- return `β”‚ ${padText(text, contentWidth)} β”‚`;
67
+ const clipped = clipAnsi(text, contentWidth);
68
+ return `${ANSI.cyan}β”‚${ANSI.reset} ${padAnsi(clipped, contentWidth)} ${ANSI.cyan}β”‚${ANSI.reset}`;
35
69
}
36
70
37
71
export function tuiPrompt(cmd: string, width = TUI_WIDTH): string {
38
- return tuiLine(`$ ${cmd}`, width);
72
+ return tuiLine(`${ANSI.white}$ ${cmd}${ANSI.reset}`, width);
39
73
}
40
74
41
75
export function tuiRow2(
47
81
): string {
48
82
const contentWidth = width - 4;
49
83
if (!l2) {
50
- return tuiLine(`${l1.padEnd(10)}: ${v1}`, width);
84
+ const text = `${ANSI.dim}${l1.padEnd(9)}:${ANSI.reset} ${v1}`;
85
+ return tuiLine(text, width);
51
86
}
52
87
const half = Math.floor((contentWidth - 2) / 2);
53
- const col1 = padText(`${l1.padEnd(7)}: ${v1}`, half);
54
- const col2 = padText(`${l2.padEnd(7)}: ${v2}`, contentWidth - half - 2);
88
+ const col1 = padAnsi(`${ANSI.dim}${l1.padEnd(7)}:${ANSI.reset} ${v1}`, half);
89
+ const col2 = padAnsi(`${ANSI.dim}${l2.padEnd(7)}:${ANSI.reset} ${v2}`, contentWidth - half - 2);
55
90
return tuiLine(`${col1} ${col2}`, width);
56
91
}
57
92
58
93
export function renderMeter(pct: number, length = 10): string {
59
94
const filled = Math.min(length, Math.max(0, Math.round((pct / 100) * length)));
60
- return `[${"β– ".repeat(filled)}${"β–‘".repeat(length - filled)}]`;
95
+ const color = pct >= 80 ? ANSI.green : pct >= 50 ? ANSI.yellow : ANSI.red;
96
+ return `[${color}${"β– ".repeat(filled)}${ANSI.dim}${"β–‘".repeat(length - filled)}${ANSI.reset}]`;
61
97
}
62
98
63
99
export function renderTuiCard(lines: string[]): string {
64
- return `\`\`\`text\n${lines.join("\n")}\n\`\`\``;
100
+ return `\`\`\`ansi\n${lines.join("\n")}\n\`\`\``;
65
101
}
66
102