File size: 6,507 Bytes
64ae41c beabfb7 64ae41c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
import os
import sys
import shutil
import click
from datetime import datetime
from typing import List
# --- Core Logic Imports ---
# These modules contain the application's functionality.
from src.config import AppConfig
from src.hn_mood_reader import HnMoodReader, FeedEntry
from src.vibe_logic import VIBE_THRESHOLDS
# --- Helper Functions ---
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))
# Define colors for different vibe levels
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")
# Fallback for the lowest score
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) # Exit with a non-zero code to indicate failure
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()
# Get terminal width, but default to 80 if it's too narrow
# to avoid breaking the layout.
try:
terminal_width = shutil.get_terminal_size()[0]
except OSError: # Handle cases where terminal size can't be determined (e.g., in a pipe)
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")
# Calculate the fixed width of the columns before the title
# Vibe: 5
# Score: | + ' ' + '0.0000' + ' ' = 9
# Published: | + ' ' + 'YYYY-MM-DD HH:MM' + ' ' + | + ' ' = 21
# Total fixed width = 5 + 9 + 21 = 35
fixed_width = 35
max_title_width = terminal_width - fixed_width
# --- MODIFICATION END ---
if not scored_entries:
click.echo("No entries found in the feed.")
else:
# Display the current "page" of entries based on the offset
for entry in scored_entries[offset:offset + top]:
status, color = get_status_text_and_color(entry.mood.raw_score)
# --- MODIFICATION: VIBE width changed from 12 to 5 ---
# Also ensure the status text itself is truncated if it's longer than 5
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} | "
# --- Title Truncation Logic ---
full_title = entry.title
if len(full_title) > max_title_width:
# Truncate and add ellipsis, reserving 3 chars for '...'
title_part = full_title[:max_title_width - 3] + "..."
else:
title_part = full_title
# --- End Title Truncation ---
# Combine parts and print
full_line = vibe_part + score_part + published_part + title_part
click.echo(full_line)
click.secho("-" * terminal_width, fg="blue")
# --- Main Application Logic (CLI Command) ---
@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.
"""
# --- State Management ---
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
# --- Initial Fetch ---
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)
# --- Main Loop ---
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 # Reset scroll on refresh
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
# Arrow key handling for scrolling (might produce escape sequences)
elif key == '\x1b[A': # Up Arrow
scroll_offset = max(0, scroll_offset - 1)
elif key == '\x1b[B': # Down Arrow
# Prevent scrolling past the last page
scroll_offset = min(scroll_offset + 1, max(0, len(scored_entries) - top))
if __name__ == "__main__":
main()
|