"""Tweet formatter for terminal output (rich) and JSON export.""" from __future__ import annotations from typing import List, Optional from rich.console import Console from rich.markdown import Markdown from rich.panel import Panel from rich.table import Table from .models import Tweet, UserProfile def format_number(n: int) -> str: """Format number with K/M suffixes.""" if n >= 1_000_000: return "%.1fM" % (n / 1_000_000) if n >= 1_000: return "%.1fK" % (n / 1_000) return str(n) def print_tweet_table( tweets: List[Tweet], console: Optional[Console] = None, title: Optional[str] = None, full_text: bool = False, ) -> None: """Print tweets as a rich table.""" if console is None: console = Console() if not title: title = "๐Ÿ“ฑ Twitter โ€” %d tweets" % len(tweets) table = Table(title=title, show_lines=True, expand=True) table.add_column("#", style="dim", width=3, justify="right") table.add_column("Author", style="cyan", width=18, no_wrap=True) table.add_column("Tweet", ratio=3) table.add_column("Stats", style="green", width=22, no_wrap=True) table.add_column("Score", style="yellow", width=6, justify="right") for i, tweet in enumerate(tweets): # Author verified = " โœ“" if tweet.author.verified else "" author_text = "@%s%s" % (tweet.author.screen_name, verified) if tweet.is_retweet and tweet.retweeted_by: author_text += "\n๐Ÿ”„ @%s" % tweet.retweeted_by # Tweet text text = tweet.text.replace("\n", " ").strip() if not full_text and len(text) > 120: text = text[:117] + "..." # Media indicators if tweet.media: media_icons = [] for m in tweet.media: if m.type == "photo": media_icons.append("๐Ÿ“ท") elif m.type == "video": media_icons.append("๐Ÿ“น") else: media_icons.append("๐ŸŽž๏ธ") text += " " + " ".join(media_icons) # Quoted tweet if tweet.quoted_tweet: qt = tweet.quoted_tweet qt_text = qt.text.replace("\n", " ") if not full_text and len(qt_text) > 60: qt_text = qt_text[:57] + "..." text += "\nโ”Œ @%s: %s" % (qt.author.screen_name, qt_text) # Tweet link text += "\n๐Ÿ”— x.com/%s/status/%s" % (tweet.author.screen_name, tweet.id) # Stats stats = ( "โค๏ธ %s ๐Ÿ”„ %s\n๐Ÿ’ฌ %s ๐Ÿ‘๏ธ %s" % ( format_number(tweet.metrics.likes), format_number(tweet.metrics.retweets), format_number(tweet.metrics.replies), format_number(tweet.metrics.views), ) ) # Score score_str = "%.1f" % tweet.score if tweet.score is not None else "-" table.add_row(str(i + 1), author_text, text, stats, score_str) console.print(table) def print_tweet_detail(tweet: Tweet, console: Optional[Console] = None) -> None: """Print a single tweet in detail using a rich panel.""" if console is None: console = Console() verified = " โœ“" if tweet.author.verified else "" header = "@%s%s (%s)" % (tweet.author.screen_name, verified, tweet.author.name) body_parts = [] if tweet.is_retweet and tweet.retweeted_by: body_parts.append("๐Ÿ”„ Retweeted by @%s\n" % tweet.retweeted_by) body_parts.append(tweet.text) if tweet.media: body_parts.append("") for m in tweet.media: icon = "๐Ÿ“ท" if m.type == "photo" else ("๐Ÿ“น" if m.type == "video" else "๐ŸŽž๏ธ") body_parts.append("%s %s: %s" % (icon, m.type, m.url)) if tweet.urls: body_parts.append("") for url in tweet.urls: body_parts.append("๐Ÿ”— %s" % url) if tweet.quoted_tweet: qt = tweet.quoted_tweet body_parts.append("") body_parts.append("โ”Œโ”€โ”€ Quoted @%s โ”€โ”€" % qt.author.screen_name) body_parts.append(qt.text[:200]) body_parts.append("") body_parts.append( "โค๏ธ %s ๐Ÿ”„ %s ๐Ÿ’ฌ %s ๐Ÿ”– %s ๐Ÿ‘๏ธ %s" % ( format_number(tweet.metrics.likes), format_number(tweet.metrics.retweets), format_number(tweet.metrics.replies), format_number(tweet.metrics.bookmarks), format_number(tweet.metrics.views), ) ) body_parts.append( "๐Ÿ• %s ยท https://x.com/%s/status/%s" % (tweet.created_at, tweet.author.screen_name, tweet.id) ) console.print(Panel( "\n".join(body_parts), title=header, border_style="blue", expand=True, )) def article_to_markdown(tweet: Tweet) -> str: """Convert a Twitter Article tweet into a Markdown document.""" title = tweet.article_title or "Twitter Article" lines = [ "# %s" % title, "", "- Author: @%s (%s)" % (tweet.author.screen_name, tweet.author.name), "- Published: %s" % (tweet.created_at or "unknown"), "- URL: https://x.com/%s/status/%s" % (tweet.author.screen_name, tweet.id), "- Likes: %s" % format_number(tweet.metrics.likes), "- Retweets: %s" % format_number(tweet.metrics.retweets), "- Replies: %s" % format_number(tweet.metrics.replies), "- Bookmarks: %s" % format_number(tweet.metrics.bookmarks), "- Views: %s" % format_number(tweet.metrics.views), ] if tweet.article_text: lines.extend(["", tweet.article_text.strip()]) return "\n".join(lines).strip() + "\n" def print_article(tweet: Tweet, console: Optional[Console] = None) -> None: """Print a Twitter Article with rich formatting.""" if console is None: console = Console() verified = " โœ“" if tweet.author.verified else "" title = tweet.article_title or "Twitter Article" meta_parts = [ "By @%s%s (%s)" % (tweet.author.screen_name, verified, tweet.author.name), "๐Ÿ• %s" % tweet.created_at, "๐Ÿ”— x.com/%s/status/%s" % (tweet.author.screen_name, tweet.id), "", "โค๏ธ %s ๐Ÿ”„ %s ๐Ÿ’ฌ %s ๐Ÿ”– %s ๐Ÿ‘๏ธ %s" % ( format_number(tweet.metrics.likes), format_number(tweet.metrics.retweets), format_number(tweet.metrics.replies), format_number(tweet.metrics.bookmarks), format_number(tweet.metrics.views), ), ] console.print(Panel( "\n".join(meta_parts), title="๐Ÿ“ฐ %s" % title, border_style="blue", expand=True, )) if tweet.article_text: console.print() console.print(Markdown(tweet.article_text)) def print_filter_stats( original_count: int, filtered: List[Tweet], console: Optional[Console] = None, ) -> None: """Print filter statistics.""" if console is None: console = Console() console.print( "๐Ÿ“Š Filter: %d โ†’ %d tweets" % (original_count, len(filtered)) ) if filtered: top_score = filtered[0].score or 0.0 bottom_score = filtered[-1].score or 0.0 console.print( " Score range: %.1f ~ %.1f" % (bottom_score, top_score) ) def print_user_profile(user: UserProfile, console: Optional[Console] = None) -> None: """Print user profile as a rich panel.""" if console is None: console = Console() verified = " โœ“" if user.verified else "" header = "@%s%s (%s)" % (user.screen_name, verified, user.name) lines = [] if user.bio: lines.append(user.bio) lines.append("") if user.location: lines.append("๐Ÿ“ %s" % user.location) if user.url: lines.append("๐Ÿ”— %s" % user.url) if user.location or user.url: lines.append("") lines.append( "๐Ÿ‘ฅ %s followers ยท %s following ยท %s tweets ยท %s likes" % ( format_number(user.followers_count), format_number(user.following_count), format_number(user.tweets_count), format_number(user.likes_count), ) ) if user.created_at: lines.append("๐Ÿ“… Joined %s" % user.created_at) lines.append("๐Ÿ”— x.com/%s" % user.screen_name) console.print(Panel( "\n".join(lines), title=header, border_style="cyan", expand=True, )) def print_user_table( users: List[UserProfile], console: Optional[Console] = None, title: Optional[str] = None, ) -> None: """Print a list of users as a rich table.""" if console is None: console = Console() if not title: title = "๐Ÿ‘ฅ Users โ€” %d" % len(users) table = Table(title=title, show_lines=True, expand=True) table.add_column("#", style="dim", width=3, justify="right") table.add_column("User", style="cyan", width=20, no_wrap=True) table.add_column("Bio", ratio=3) table.add_column("Stats", style="green", width=22, no_wrap=True) for i, user in enumerate(users): verified = " โœ“" if user.verified else "" user_text = "@%s%s\n%s" % (user.screen_name, verified, user.name) bio = (user.bio or "").replace("\n", " ").strip() if len(bio) > 100: bio = bio[:97] + "..." stats = ( "๐Ÿ‘ฅ %s followers\n๐Ÿ“ %s following" % ( format_number(user.followers_count), format_number(user.following_count), ) ) table.add_row(str(i + 1), user_text, bio, stats) console.print(table)