using Spectre.Console; 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. /// /// Rendering is delegated to Spectre.Console, which handles colour, /// word-wrap to the current terminal width, and graceful degradation /// when output is redirected to a file or a non-ANSI terminal. /// public static class ConsoleRenderer { /// /// Sets the terminal window/tab title. Silently no-ops on terminals that /// don't support the escape sequence. /// public static void SetTitle(string title) { try { Console.Title = title; } catch { /* unsupported terminal — ignore */ } } /// Prints the application banner on startup. public static void PrintBanner() { SetTitle("Summarize"); AnsiConsole.WriteLine(); AnsiConsole.Write(new FigletText("YT Summary").Centered().Color(Color.Red)); AnsiConsole.Write( new Rule("[grey]YouTube Video Summarizer[/]") .RuleStyle("grey") .Centered()); AnsiConsole.WriteLine(); } /// /// True when the terminal can drive Spectre's interactive prompts. When output /// is redirected or piped we fall back to plain reads / default answers so the /// app still works in scripts and CI. /// private static bool Interactive => AnsiConsole.Profile.Capabilities.Interactive; /// Prompts the user for a URL and reads input. public static string PromptForUrl() { // Revert the title while waiting for the next video. SetTitle("Summarize"); const string label = "[bold]Enter a YouTube URL or a local file path[/] [grey](or 'q' to quit)[/]"; if (!Interactive) { AnsiConsole.Markup($"{label}: "); return Console.ReadLine()?.Trim() ?? string.Empty; } var input = AnsiConsole.Prompt(new TextPrompt($"{label}:").AllowEmpty()); return input.Trim(); } /// /// Asks the user whether they want to save the transcript to a text file. /// Defaults to true — pressing Enter accepts. /// public static bool PromptSaveTranscript() { if (!Interactive) return true; return AnsiConsole.Confirm("[bold]Save transcript to file?[/]", defaultValue: true); } /// Prints a success message with the saved file path. public static void PrintFileSaved(string filePath) { AnsiConsole.MarkupLine($"[green]✓[/] Transcript saved to: [blue]{Markup.Escape(filePath)}[/]"); AnsiConsole.WriteLine(); } /// /// Prompts the user to choose a summary mode via an arrow-key selection list. /// is listed first so it is the highlighted /// default — pressing Enter accepts it. /// public static SummaryMode PromptSummaryMode() { if (!Interactive) return SummaryMode.Standard; return AnsiConsole.Prompt( new SelectionPrompt() .Title("[bold]Choose summary mode:[/]") .AddChoices(SummaryMode.Standard, SummaryMode.PersonalFilter, SummaryMode.Custom) .UseConverter(m => m switch { SummaryMode.Standard => "Standard – detailed bullet-point summary", SummaryMode.PersonalFilter => "Personal Filter – relevance verdict (ACT / MONITOR / IGNORE)", SummaryMode.Custom => "Custom – write your own instructions", _ => m.ToString() })); } /// /// Reads multi-line custom summarization instructions from the user, /// terminated by a blank line. Spectre has no built-in multi-line text /// prompt, so this reads raw lines directly (still styled with a Spectre /// header) and joins them with newlines. /// public static string PromptCustomInstructions() { AnsiConsole.MarkupLine("[bold]Enter your custom instructions[/] [grey](finish with an empty line):[/]"); var lines = new List(); string? line; while (!string.IsNullOrEmpty(line = Console.ReadLine())) { lines.Add(line); } return string.Join('\n', lines).Trim(); } /// Prints a dim "working" indicator before an async step. public static void PrintWorking(string message) { AnsiConsole.MarkupLine($"[grey]→ {Markup.Escape(message)}...[/]"); } /// /// 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) { AnsiConsole.WriteLine(); var isDocument = summary.TranscriptSource == TranscriptSource.LocalDocument; // ── Metadata header ────────────────────────────────────────────────── var grid = new Grid(); grid.AddColumn(new GridColumn().PadRight(2)); grid.AddColumn(); grid.AddRow("[bold green]Title[/]", $"[bold]{Markup.Escape(summary.Metadata.Title)}[/]"); grid.AddRow(isDocument ? "[grey]Type[/]" : "[grey]Channel[/]", Markup.Escape(summary.Metadata.ChannelTitle)); grid.AddRow( isDocument ? "[grey]Modified[/]" : "[grey]Published[/]", Markup.Escape(summary.Metadata.PublishedAt.ToString("MMMM d, yyyy"))); if (summary.Metadata.Duration is not null) grid.AddRow("[grey]Duration[/]", Markup.Escape(summary.Metadata.FormattedDuration)); grid.AddRow( isDocument ? "[grey]Location[/]" : "[grey]URL[/]", isDocument ? Markup.Escape(summary.Metadata.SourcePath ?? summary.Metadata.Title) : $"https://youtu.be/{Markup.Escape(summary.Metadata.VideoId)}"); 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"), TranscriptSource.LocalDocument => ("✓ Local document", "green"), _ => ("? Unknown", "grey") }; grid.AddRow("[grey]Transcript[/]", $"[{color}]{Markup.Escape(badge)}[/]"); } grid.AddRow("[grey]Model[/]", Markup.Escape(summary.ModelUsed)); grid.AddRow("[grey]Generated[/]", Markup.Escape(summary.GeneratedAt.ToString("yyyy-MM-dd HH:mm")) + " UTC"); AnsiConsole.Write(grid); AnsiConsole.WriteLine(); // ── Quality warning ────────────────────────────────────────────────── if (summary.QualityWarning is not null) { AnsiConsole.Write( new Panel($"[yellow]{Markup.Escape(summary.QualityWarning)}[/]") { Header = new PanelHeader("Quality warning"), Border = BoxBorder.Rounded, BorderStyle = new Style(Color.Yellow) }); AnsiConsole.WriteLine(); } // ── Summary body ───────────────────────────────────────────────────── AnsiConsole.Write( new Panel(Markup.Escape(summary.SummaryText)) { Header = new PanelHeader("Summary"), Border = BoxBorder.Rounded, Padding = new Padding(2, 1, 2, 1) }.Expand()); AnsiConsole.WriteLine(); } /// Prints a styled error message. public static void PrintError(string message) { AnsiConsole.WriteLine(); AnsiConsole.MarkupLine($"[red]✗ Error:[/] {Markup.Escape(message)}"); AnsiConsole.WriteLine(); } /// Prints a styled warning (non-fatal). public static void PrintWarning(string message) { AnsiConsole.MarkupLine($"[yellow]⚠ {Markup.Escape(message)}[/]"); } }