using YoutubeSummarizer.Models; namespace YoutubeSummarizer.Services; /// /// Handles all console output formatting. /// Keeping display logic separate from business logic makes it easy to /// later add output modes (JSON, Markdown file, HTML report) without /// touching the service layer. /// public static class ConsoleRenderer { // ANSI color codes. These render correctly in most Linux terminals. // If you pipe output to a file, the escape codes will appear as-is — // run with --no-color if that's a concern (not implemented here, left // as an exercise). private const string Reset = "\x1b[0m"; private const string Bold = "\x1b[1m"; private const string Cyan = "\x1b[36m"; private const string Yellow = "\x1b[33m"; private const string Green = "\x1b[32m"; private const string Red = "\x1b[31m"; private const string Dim = "\x1b[2m"; /// Prints the application banner on startup. public static void PrintBanner() { Console.WriteLine(); Console.WriteLine($"{Bold}{Cyan}╔════════════════════════════════════════╗{Reset}"); Console.WriteLine($"{Bold}{Cyan}║ YouTube Video Summarizer ║{Reset}"); Console.WriteLine($"{Bold}{Cyan}╚════════════════════════════════════════╝{Reset}"); Console.WriteLine(); } /// Prompts the user for a URL and reads input. public static string PromptForUrl() { Console.Write($"{Bold}Enter YouTube URL (or 'q' to quit):{Reset} "); return Console.ReadLine()?.Trim() ?? string.Empty; } /// /// Asks the user whether they want to save the transcript to a text file. /// Returns true if the user answers yes. /// public static bool PromptSaveTranscript() { Console.Write($"{Bold}Save transcript to file? (y/n):{Reset} "); var answer = Console.ReadLine()?.Trim() ?? string.Empty; return answer.Equals("y", StringComparison.OrdinalIgnoreCase) || answer.Equals("yes", StringComparison.OrdinalIgnoreCase); } /// Prints a success message with the saved file path. public static void PrintFileSaved(string filePath) { Console.WriteLine($" {Green}✓ Transcript saved to:{Reset} {filePath}"); Console.WriteLine(); } /// /// Prompts the user to choose a summary mode. /// Returns the selected . /// public static SummaryMode PromptSummaryMode() { Console.WriteLine($" {Dim}Summary modes:{Reset}"); Console.WriteLine($" {Bold}1{Reset} – Standard (detailed bullet-point summary)"); Console.WriteLine($" {Bold}2{Reset} – Personal Filter (relevance verdict: ACT / MONITOR / IGNORE)"); Console.Write($"{Bold}Choose summary mode [1]:{Reset} "); var choice = Console.ReadLine()?.Trim() ?? string.Empty; return choice == "2" ? SummaryMode.PersonalFilter : SummaryMode.Standard; } /// Displays a spinner-style "working" indicator while async work runs. public static void PrintWorking(string message) { Console.WriteLine($" {Dim}→ {message}...{Reset}"); } /// /// Renders the full summary result to the console in a structured, /// readable format. Includes metadata header, quality warning, and /// the summary body. /// public static void PrintSummary(VideoSummary summary, bool showTranscriptSource) { Console.WriteLine(); PrintDivider(); // ── Metadata header ────────────────────────────────────────────────── Console.WriteLine($"{Bold}{Green} {summary.Metadata.Title}{Reset}"); Console.WriteLine($" {Dim}Channel:{Reset} {summary.Metadata.ChannelTitle}"); Console.WriteLine($" {Dim}Published:{Reset} {summary.Metadata.PublishedAt:MMMM d, yyyy}"); Console.WriteLine($" {Dim}Duration:{Reset} {summary.Metadata.FormattedDuration}"); Console.WriteLine($" {Dim}URL:{Reset} https://youtu.be/{summary.Metadata.VideoId}"); // ── Transcript source badge ────────────────────────────────────────── if (showTranscriptSource) { var (badge, color) = summary.TranscriptSource switch { TranscriptSource.OwnerPublished => ("✓ Owner-published captions", Green), TranscriptSource.CommunityContributed=> ("✓ Community captions", Green), TranscriptSource.AutoGenerated => ("~ Auto-generated (ASR)", Yellow), TranscriptSource.MetadataOnly => ("✗ Metadata only", Red), _ => ("? Unknown", Dim) }; Console.WriteLine($" {Dim}Transcript:{Reset} {color}{badge}{Reset}"); } Console.WriteLine($" {Dim}Model:{Reset} {summary.ModelUsed}"); Console.WriteLine($" {Dim}Generated:{Reset} {summary.GeneratedAt:yyyy-MM-dd HH:mm} UTC"); PrintDivider(); // ── Quality warning ────────────────────────────────────────────────── if (summary.QualityWarning is not null) { Console.WriteLine(); Console.WriteLine($" {Yellow}{summary.QualityWarning}{Reset}"); } // ── Summary body ───────────────────────────────────────────────────── Console.WriteLine(); Console.WriteLine($"{Bold} SUMMARY{Reset}"); Console.WriteLine(); // Word-wrap the summary body at 80 characters so it's readable in // standard terminal widths without horizontal scrolling. foreach (var line in WordWrap(summary.SummaryText, maxWidth: 78)) { Console.WriteLine($" {line}"); } Console.WriteLine(); PrintDivider(); Console.WriteLine(); } /// Prints a styled error message. public static void PrintError(string message) { Console.WriteLine(); Console.WriteLine($" {Red}✗ Error: {message}{Reset}"); Console.WriteLine(); } /// Prints a styled warning (non-fatal). public static void PrintWarning(string message) { Console.WriteLine($" {Yellow}⚠ {message}{Reset}"); } // ───────────────────────────────────────────────────────────────────────── // Private helpers // ───────────────────────────────────────────────────────────────────────── private static void PrintDivider() { Console.WriteLine($" {Dim}{"─".PadRight(74, '─')}{Reset}"); } /// /// Splits text into lines no wider than characters, /// breaking only at word boundaries. Respects existing newlines in the input. /// private static IEnumerable WordWrap(string text, int maxWidth) { foreach (var paragraph in text.Split('\n')) { if (string.IsNullOrWhiteSpace(paragraph)) { yield return string.Empty; continue; } var words = paragraph.Split(' ', StringSplitOptions.RemoveEmptyEntries); var current = new System.Text.StringBuilder(); foreach (var word in words) { if (current.Length + word.Length + 1 > maxWidth) { yield return current.ToString(); current.Clear(); } if (current.Length > 0) current.Append(' '); current.Append(word); } if (current.Length > 0) yield return current.ToString(); } } }