diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2ec53..ef3f5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Automatic code block detection and reformatting feature - Full interaction handler system with file-based routing +- New `man` command for viewing Unix/Linux manual pages directly from Discord ### Changed diff --git a/src/commands/docs/man.ts b/src/commands/docs/man.ts new file mode 100644 index 0000000..90d7db6 --- /dev/null +++ b/src/commands/docs/man.ts @@ -0,0 +1,55 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, +} from "discord.js"; +import type { CommandCallbackOpts } from "../../types/command.ts"; +import { man } from "../../utils/man.ts"; + +export default { + name: "man", + description: "Search man pages", + aliases: ["manual"], + usage: "man ", + react: "📖", + async callback({ message, args }: CommandCallbackOpts) { + try { + // returns return { + // title, + // section, + // url, + // raw, + // sections, + // }; + const page = await man(args.join(" ")); + if (!page) { + return message.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("Man Page Not Found") + .setDescription( + `No man page found for the term "${args.join(" ")}".`, + ) + .setColor(0xff0000), + ], + }); + } + + const embed = new EmbedBuilder() + .setTitle(`${page.title}`) + .setDescription(page.description || "No description available.") + .setColor(0x7289da); + + const button = new ButtonBuilder() + .setLabel("Read Full Man Page") + .setStyle(ButtonStyle.Link) + .setURL(page.url); + const row = new ActionRowBuilder().addComponents(button); + return message.reply({ embeds: [embed], components: [row.toJSON()] }); + } catch (err) { + console.error(err); + return message.reply("Failed to fetch man page."); + } + }, +}; diff --git a/src/commands/meta/changelog.ts b/src/commands/meta/changelog.ts index 229e061..ccc2479 100644 --- a/src/commands/meta/changelog.ts +++ b/src/commands/meta/changelog.ts @@ -7,43 +7,10 @@ import path from "path"; import { fileURLToPath } from "url"; import type { CommandCallbackOpts } from "../../types/command.ts"; import { readFile } from "../../utils/fileOps.ts"; +import { parseChangelog } from "../../utils/parseChangelog.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -interface VersionSection { - title: string; - content: string; -} - -function parseChangelog(content: string): VersionSection[] { - if (!content?.trim()) return []; - - const parts = content.split(/^## /m); - const versions: VersionSection[] = []; - - for (let i = 1; i < parts.length; i++) { - const section = parts[i]?.trim(); - if (!section) continue; - - const firstNewline = section.indexOf("\n"); - const title = ( - firstNewline === -1 ? section : section.slice(0, firstNewline) - ).trim(); - const body = ( - firstNewline === -1 ? "" : section.slice(firstNewline) - ).trim(); - - if (title) { - versions.push({ - title, - content: `## ${title}\n\n${body}`, - }); - } - } - - return versions; -} - export default { name: "changelog", description: "See the latest changes made to the bot.", diff --git a/src/interactions/changelog.ts b/src/interactions/changelog.ts index 99fbe8e..e9b48ca 100644 --- a/src/interactions/changelog.ts +++ b/src/interactions/changelog.ts @@ -7,44 +7,10 @@ import { import path from "path"; import { fileURLToPath } from "url"; import { readFile } from "../utils/fileOps.ts"; +import { parseChangelog } from "../utils/parseChangelog.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -interface VersionSection { - title: string; - content: string; -} - -function parseChangelog(content: string): VersionSection[] { - if (!content?.trim()) return []; - - // Split on "## " headings (standard Keep a Changelog / conventional format) - const parts = content.split(/^## /m); - const versions: VersionSection[] = []; - - for (let i = 1; i < parts.length; i++) { - const section = parts[i]?.trim(); - if (!section) continue; - - const firstNewline = section.indexOf("\n"); - const title = ( - firstNewline === -1 ? section : section.slice(0, firstNewline) - ).trim(); - const body = ( - firstNewline === -1 ? "" : section.slice(firstNewline) - ).trim(); - - if (title) { - versions.push({ - title, - content: `## ${title}\n\n${body}`, - }); - } - } - - return versions; -} - export default { id: "changelog_select", async callback({ diff --git a/src/utils/man.ts b/src/utils/man.ts new file mode 100644 index 0000000..d768825 --- /dev/null +++ b/src/utils/man.ts @@ -0,0 +1,94 @@ +export interface ManPage { + title: string; + url: string; + raw: string; + description: string; +} + +export async function man(term: string): Promise { + term = term.trim(); + + if (!/^[a-zA-Z0-9._+-]+$/.test(term)) { + throw new Error("Invalid man page."); + } + + const url = `https://man.archlinux.org/man/${encodeURIComponent(term)}.txt`; + + const res = await fetch(url); + + if (!res.ok) return null; + + const raw = (await res.text()).replace(/\r\n/g, "\n").trim(); + + if (!raw) return null; + + const firstLine = raw.split("\n")[0]?.trim(); + + const match = firstLine?.match(/^([^(]+)\(([^)]+)\)$/); + + const sections: Record = {}; + + const headings = [ + "NAME", + "SYNOPSIS", + "DESCRIPTION", + "OPTIONS", + "COMMANDS", + "ARGUMENTS", + "OPERANDS", + "EXIT STATUS", + "RETURN VALUE", + "ERRORS", + "ENVIRONMENT", + "FILES", + "ATTRIBUTES", + "VERSIONS", + "STANDARDS", + "NOTES", + "BUGS", + "EXAMPLES", + "AUTHORS", + "AUTHOR", + "COPYRIGHT", + "SEE ALSO", + "HISTORY", + "DIAGNOSTICS", + "CAVEATS", + "SECURITY", + ]; + + const lines = raw.split("\n"); + + let current: string | null = null; + + for (const line of lines) { + const trimmed = line.trim(); + + if (headings.includes(trimmed)) { + current = trimmed; + sections[current] = ""; + continue; + } + + if (current) { + sections[current] += line + "\n"; + } + } + + for (const key of Object.keys(sections)) { + sections[key] = sections[key]?.trimEnd() || ""; + } + + const title = match?.[1]?.trim()?.toLowerCase() ?? term; + const section = match?.[2] ?? "?"; + const description = + sections["DESCRIPTION"]?.split("\n\n")[0]?.replace(/\s+/g, " ").trim() || + ""; + + return { + title, + description, + url, + raw, + }; +} diff --git a/src/utils/parseChangelog.ts b/src/utils/parseChangelog.ts new file mode 100644 index 0000000..87874a0 --- /dev/null +++ b/src/utils/parseChangelog.ts @@ -0,0 +1,33 @@ +interface VersionSection { + title: string; + content: string; +} + +export function parseChangelog(content: string): VersionSection[] { + if (!content?.trim()) return []; + + const parts = content.split(/^## /m); + const versions: VersionSection[] = []; + + for (let i = 1; i < parts.length; i++) { + const section = parts[i]?.trim(); + if (!section) continue; + + const firstNewline = section.indexOf("\n"); + const title = ( + firstNewline === -1 ? section : section.slice(0, firstNewline) + ).trim(); + const body = (firstNewline === -1 ? "" : section.slice(firstNewline)) + .trim() + .replace(/\r?\n\r?\n/g, "\n"); + + if (title) { + versions.push({ + title, + content: `## ${title}\n\n${body}`, + }); + } + } + + return versions; +}