"""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.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, ) -> 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 (truncated) text = tweet.text.replace("\n", " ").strip() if 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", " ")[:60] 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 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)