feat: add full-text option for tweet tables

This commit is contained in:
jackwener
2026-03-11 20:58:12 +08:00
parent 88a9f4ce97
commit 1313eb0be1
5 changed files with 145 additions and 32 deletions

View File

@@ -311,8 +311,8 @@ def cli(ctx, verbose, compact):
ctx.obj["compact"] = compact
def _fetch_and_display(fetch_fn, label, emoji, max_count, as_json, as_yaml, output_file, do_filter, config=None, compact=False):
# type: (Any, str, str, Optional[int], bool, bool, Optional[str], bool, Optional[dict], bool) -> None
def _fetch_and_display(fetch_fn, label, emoji, max_count, as_json, as_yaml, output_file, do_filter, config=None, compact=False, full_text=False):
# type: (Any, str, str, Optional[int], bool, bool, Optional[str], bool, Optional[dict], bool, bool) -> None
"""Common fetch-filter-display logic for timeline-like commands."""
if config is None:
config = load_config()
@@ -343,12 +343,17 @@ def _fetch_and_display(fetch_fn, label, emoji, max_count, as_json, as_yaml, outp
if emit_structured(tweets_to_data(filtered), as_json=as_json, as_yaml=as_yaml):
return
print_tweet_table(filtered, console, title="%s %s%d tweets" % (emoji, label, len(filtered)))
print_tweet_table(
filtered,
console,
title="%s %s%d tweets" % (emoji, label, len(filtered)),
full_text=full_text,
)
console.print()
def _run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter, compact=False):
# type: (Optional[int], bool, bool, Optional[str], bool, bool) -> None
def _run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter, compact=False, full_text=False):
# type: (Optional[int], bool, bool, Optional[str], bool, bool, bool) -> None
config = load_config()
def _run():
@@ -364,6 +369,7 @@ def _run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter,
do_filter,
config,
compact=compact,
full_text=full_text,
)
_run_guarded(_run)
@@ -383,9 +389,10 @@ def _run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter,
@click.option("--input", "-i", "input_file", type=str, default=None, help="Load tweets from JSON file.")
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save filtered tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context
def feed(ctx, feed_type, max_count, as_json, as_yaml, input_file, output_file, do_filter):
# type: (Any, str, Optional[int], bool, bool, Optional[str], Optional[str], bool) -> None
def feed(ctx, feed_type, max_count, as_json, as_yaml, input_file, output_file, do_filter, full_text):
# type: (Any, str, Optional[int], bool, bool, Optional[str], Optional[str], bool, bool) -> None
"""Fetch home timeline with optional filtering."""
compact = ctx.obj.get("compact", False)
rich_output = use_rich_output(as_json=as_json, as_yaml=as_yaml, compact=compact)
@@ -430,7 +437,7 @@ def feed(ctx, feed_type, max_count, as_json, as_yaml, input_file, output_file, d
title = "👥 Following" if feed_type == "following" else "📱 Twitter"
title += "%d tweets" % len(filtered)
print_tweet_table(filtered, console, title=title)
print_tweet_table(filtered, console, title=title, full_text=full_text)
console.print()
@@ -439,11 +446,20 @@ def feed(ctx, feed_type, max_count, as_json, as_yaml, input_file, output_file, d
@structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context
def favorites(ctx, max_count, as_json, as_yaml, output_file, do_filter):
# type: (Any, Optional[int], bool, bool, Optional[str], bool) -> None
def favorites(ctx, max_count, as_json, as_yaml, output_file, do_filter, full_text):
# type: (Any, Optional[int], bool, bool, Optional[str], bool, bool) -> None
"""Fetch bookmarked (favorite) tweets."""
_run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter, compact=ctx.obj.get("compact", False))
_run_bookmarks_command(
max_count,
as_json,
as_yaml,
output_file,
do_filter,
compact=ctx.obj.get("compact", False),
full_text=full_text,
)
@cli.command(name="bookmarks")
@@ -451,11 +467,20 @@ def favorites(ctx, max_count, as_json, as_yaml, output_file, do_filter):
@structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context
def bookmarks(ctx, max_count, as_json, as_yaml, output_file, do_filter):
# type: (Any, Optional[int], bool, bool, Optional[str], bool) -> None
def bookmarks(ctx, max_count, as_json, as_yaml, output_file, do_filter, full_text):
# type: (Any, Optional[int], bool, bool, Optional[str], bool, bool) -> None
"""Fetch bookmarked tweets."""
_run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter, compact=ctx.obj.get("compact", False))
_run_bookmarks_command(
max_count,
as_json,
as_yaml,
output_file,
do_filter,
compact=ctx.obj.get("compact", False),
full_text=full_text,
)
@cli.command()
@@ -485,9 +510,10 @@ def user(screen_name, as_json, as_yaml):
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max number of tweets to fetch.")
@structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context
def user_posts(ctx, screen_name, max_count, as_json, as_yaml, output_file):
# type: (Any, str, int, bool, bool, Optional[str]) -> None
def user_posts(ctx, screen_name, max_count, as_json, as_yaml, output_file, full_text):
# type: (Any, str, int, bool, bool, Optional[str], bool) -> None
"""List a user's tweets. SCREEN_NAME is the @handle (without @)."""
screen_name = screen_name.lstrip("@")
compact = ctx.obj.get("compact", False)
@@ -501,7 +527,7 @@ def user_posts(ctx, screen_name, max_count, as_json, as_yaml, output_file):
_fetch_and_display(
lambda count: client.fetch_user_tweets(profile.id, count),
"@%s tweets" % screen_name, "📝", max_count, as_json, as_yaml, output_file, False, config,
compact=compact,
compact=compact, full_text=full_text,
)
_run_guarded(_run)
@@ -520,9 +546,10 @@ def user_posts(ctx, screen_name, max_count, as_json, as_yaml, output_file):
@structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context
def search(ctx, query, product, max_count, as_json, as_yaml, output_file, do_filter):
# type: (Any, str, str, int, bool, bool, Optional[str], bool) -> None
def search(ctx, query, product, max_count, as_json, as_yaml, output_file, do_filter, full_text):
# type: (Any, str, str, int, bool, bool, Optional[str], bool, bool) -> None
"""Search tweets by QUERY string."""
compact = ctx.obj.get("compact", False)
config = load_config()
@@ -532,7 +559,7 @@ def search(ctx, query, product, max_count, as_json, as_yaml, output_file, do_fil
_fetch_and_display(
lambda count: client.fetch_search(query, count, product),
"'%s' (%s)" % (query, product), "🔍", max_count, as_json, as_yaml, output_file, do_filter, config,
compact=compact,
compact=compact, full_text=full_text,
)
_run_guarded(_run)
@@ -543,9 +570,10 @@ def search(ctx, query, product, max_count, as_json, as_yaml, output_file, do_fil
@structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context
def likes(ctx, screen_name, max_count, as_json, as_yaml, output_file, do_filter):
# type: (Any, str, int, bool, bool, Optional[str], bool) -> None
def likes(ctx, screen_name, max_count, as_json, as_yaml, output_file, do_filter, full_text):
# type: (Any, str, int, bool, bool, Optional[str], bool, bool) -> None
"""Show tweets liked by a user. SCREEN_NAME is the @handle (without @).
NOTE: Twitter/X made all likes private since June 2024. You can only view
@@ -583,7 +611,7 @@ def likes(ctx, screen_name, max_count, as_json, as_yaml, output_file, do_filter)
_fetch_and_display(
lambda count: client.fetch_user_likes(profile.id, count),
"@%s likes" % screen_name, "❤️", max_count, as_json, as_yaml, output_file, do_filter, config,
compact=compact,
compact=compact, full_text=full_text,
)
_run_guarded(_run)
@@ -591,10 +619,11 @@ def likes(ctx, screen_name, max_count, as_json, as_yaml, output_file, do_filter)
@cli.command()
@click.argument("tweet_id")
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max replies to fetch.")
@click.option("--full-text", is_flag=True, help="Show full reply text in table output.")
@structured_output_options
@click.pass_context
def tweet(ctx, tweet_id, max_count, as_json, as_yaml):
# type: (Any, str, int, bool, bool) -> None
def tweet(ctx, tweet_id, max_count, full_text, as_json, as_yaml):
# type: (Any, str, int, bool, bool, bool) -> None
"""View a tweet and its replies. TWEET_ID is the numeric tweet ID or full URL."""
compact = ctx.obj.get("compact", False)
tweet_id = _normalize_tweet_id(tweet_id)
@@ -623,7 +652,7 @@ def tweet(ctx, tweet_id, max_count, as_json, as_yaml):
print_tweet_detail(tweets[0], console)
if len(tweets) > 1:
console.print("\n💬 Replies:")
print_tweet_table(tweets[1:], console, title="💬 Replies — %d" % (len(tweets) - 1))
print_tweet_table(tweets[1:], console, title="💬 Replies — %d" % (len(tweets) - 1), full_text=full_text)
console.print()
@@ -632,9 +661,10 @@ def tweet(ctx, tweet_id, max_count, as_json, as_yaml):
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max tweets to fetch.")
@structured_output_options
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context
def list_timeline(ctx, list_id, max_count, as_json, as_yaml, do_filter):
# type: (Any, str, int, bool, bool, bool) -> None
def list_timeline(ctx, list_id, max_count, as_json, as_yaml, do_filter, full_text):
# type: (Any, str, int, bool, bool, bool, bool) -> None
"""Fetch tweets from a Twitter List. LIST_ID is the numeric list ID."""
compact = ctx.obj.get("compact", False)
config = load_config()
@@ -643,7 +673,7 @@ def list_timeline(ctx, list_id, max_count, as_json, as_yaml, do_filter):
_fetch_and_display(
lambda count: client.fetch_list_timeline(list_id, count),
"list %s" % list_id, "📋", max_count, as_json, as_yaml, None, do_filter, config,
compact=compact,
compact=compact, full_text=full_text,
)
_run_guarded(_run)

View File

@@ -24,6 +24,7 @@ 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:
@@ -46,9 +47,9 @@ def print_tweet_table(
if tweet.is_retweet and tweet.retweeted_by:
author_text += "\n🔄 @%s" % tweet.retweeted_by
# Tweet text (truncated)
# Tweet text
text = tweet.text.replace("\n", " ").strip()
if len(text) > 120:
if not full_text and len(text) > 120:
text = text[:117] + "..."
# Media indicators
@@ -66,7 +67,9 @@ def print_tweet_table(
# Quoted tweet
if tweet.quoted_tweet:
qt = tweet.quoted_tweet
qt_text = qt.text.replace("\n", " ")[:60]
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