| import os |
| import sys |
| import shutil |
| import click |
| from datetime import datetime |
| from typing import List |
|
|
| |
| |
| from src.config import AppConfig |
| from src.hn_mood_reader import HnMoodReader, FeedEntry |
| from src.vibe_logic import VIBE_THRESHOLDS |
|
|
| |
|
|
| def get_status_text_and_color(score: float) -> (str, str): |
| """ |
| Determines the plain text status and a corresponding color for a given score. |
| """ |
| clamped_score = max(0.0, min(1.0, score)) |
| |
| |
| color_map = { |
| "VIBE:HIGH": "green", |
| "VIBE:GOOD": "cyan", |
| "VIBE:FLAT": "yellow", |
| "VIBE:LOW": "red" |
| } |
| |
| for threshold in VIBE_THRESHOLDS: |
| if clamped_score >= threshold.score: |
| status = threshold.status.split(" ")[-1].replace(' ', '') |
| return status, color_map.get(status, "white") |
| |
| |
| status = VIBE_THRESHOLDS[-1].status.split(" ")[-1].replace(' ', '') |
| return status, color_map.get(status, "white") |
|
|
| def initialize_reader(model_name: str) -> HnMoodReader: |
| """ |
| Initializes the HnMoodReader instance with the specified model. |
| Exits the script if the model fails to load. |
| """ |
| click.echo(f"Initializing mood reader with model: '{model_name}'...", err=True) |
| try: |
| reader = HnMoodReader(model_name=model_name) |
| click.secho("β
Model loaded successfully.", fg="green", err=True) |
| return reader |
| except Exception as e: |
| click.secho(f"β FATAL: Could not initialize model '{model_name}'.", fg="red", err=True) |
| click.secho(f" Error: {e}", fg="red", err=True) |
| sys.exit(1) |
|
|
| def display_feed(scored_entries: List[FeedEntry], top: int, offset: int, model_name: str): |
| """Clears the screen and displays the current slice of the feed.""" |
| click.clear() |
|
|
| |
| |
| try: |
| terminal_width = shutil.get_terminal_size()[0] |
| except OSError: |
| terminal_width = 80 |
|
|
| click.echo(f"π° Hacker News Mood Reader") |
| click.echo(f" Model: {model_name}") |
| click.echo(f" Showing {offset + 1}-{min(offset + top, len(scored_entries))} of {len(scored_entries)} stories") |
| click.secho("=" * terminal_width, fg="blue") |
|
|
| header = f"{'VIBE':<5} | {'SCORE':<7} | {'PUBLISHED':<16} | {'TITLE'}" |
| click.secho(header, bold=True) |
| click.secho("-" * terminal_width, fg="blue") |
|
|
| |
| |
| |
| |
| |
| fixed_width = 35 |
| max_title_width = terminal_width - fixed_width |
| |
|
|
| if not scored_entries: |
| click.echo("No entries found in the feed.") |
| else: |
| |
| for entry in scored_entries[offset:offset + top]: |
| status, color = get_status_text_and_color(entry.mood.raw_score) |
|
|
| |
| |
| truncated_status = status[5:] |
| vibe_part = click.style(f"{truncated_status:<5}", fg=color) |
|
|
| score_part = f"| {entry.mood.raw_score:>.4f} " |
| published_part = f"| {entry.published_time_str:<16} | " |
|
|
| |
| full_title = entry.title |
|
|
| if len(full_title) > max_title_width: |
| |
| title_part = full_title[:max_title_width - 3] + "..." |
| else: |
| title_part = full_title |
| |
|
|
| |
| full_line = vibe_part + score_part + published_part + title_part |
| click.echo(full_line) |
|
|
| click.secho("-" * terminal_width, fg="blue") |
|
|
|
|
| |
|
|
| @click.command() |
| @click.option( |
| "-m", "--model", |
| help="Name of the Sentence Transformer model from Hugging Face. Overrides MOOD_MODEL env var.", |
| default=None, |
| show_default=False |
| ) |
| @click.option( |
| "-n", "--top", |
| help="Number of stories to display on screen at once.", |
| default=15, |
| type=int, |
| show_default=True |
| ) |
| def main(model, top): |
| """ |
| Fetch and display Hacker News stories scored by a sentence-embedding model. |
| Runs continuously. Use arrow keys to scroll, [SPACE] to refresh, [q] to quit. |
| """ |
| |
| model_name = model or os.environ.get("MOOD_MODEL") or AppConfig.DEFAULT_MOOD_READER_MODEL |
| reader = initialize_reader(model_name) |
| scored_entries: List[FeedEntry] = [] |
| scroll_offset = 0 |
|
|
| |
| click.echo("Fetching initial feed...", err=True) |
| try: |
| scored_entries = reader.fetch_and_score_feed() |
| except Exception as e: |
| click.secho(f"β ERROR: Initial fetch failed: {e}", fg="red", err=True) |
|
|
| |
| while True: |
| display_feed(scored_entries, top, scroll_offset, reader.model_name) |
| |
| click.secho("Use [β|β] to scroll, [SPACE] to refresh, or [q] to quit.", bold=True, err=True) |
| key = click.getchar() |
|
|
| if key == ' ': |
| click.echo("Refreshing feed...", err=True) |
| try: |
| scored_entries = reader.fetch_and_score_feed() |
| scroll_offset = 0 |
| except Exception as e: |
| click.secho(f"β ERROR: Refresh failed: {e}", fg="red", err=True) |
| continue |
| |
| elif key in ('q', 'Q'): |
| click.echo("Exiting.") |
| break |
| |
| |
| elif key == '\x1b[A': |
| scroll_offset = max(0, scroll_offset - 1) |
| elif key == '\x1b[B': |
| |
| scroll_offset = min(scroll_offset + 1, max(0, len(scored_entries) - top)) |
|
|
| if __name__ == "__main__": |
| main() |
|
|
|
|
|
|