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();
}
}
}