mirror of
https://github.com/curly60e/pyblock.git
synced 2026-08-14 12:43:15 +02:00
- Miner pool tag: shows who mined the last block (coinbase decode for local/remote, API for lite) - Block weight meter: colored fullness bar (green/yellow/red) - Block time histogram: sparkline of last 14 block intervals with color-coded speed + streak detection - Peer count: network connections indicator with health coloring - Moon phase: current lunar phase emoji + name All toggleable via Settings → D (Clock Display Settings). Also fixes negative countdown timer (clamp to 0) and countdown row tracking bug. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
235 lines
7.4 KiB
Python
235 lines
7.4 KiB
Python
"""Clock widget components: countdown, epoch bar, fees, UTC time, and visuals."""
|
|
|
|
import math
|
|
from datetime import datetime, timezone
|
|
|
|
from cfonts import render
|
|
|
|
SPARK_BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588"
|
|
|
|
|
|
def render_countdown(seconds_since_block, term_width):
|
|
"""Render time since last block with color coding.
|
|
|
|
Green: <600s (10min), Yellow: 600-1200s, Red: >1200s.
|
|
"""
|
|
mins = seconds_since_block // 60
|
|
secs = seconds_since_block % 60
|
|
|
|
if seconds_since_block < 600:
|
|
color = "\033[1;32;40m" # green
|
|
elif seconds_since_block < 1200:
|
|
color = "\033[1;33;40m" # yellow
|
|
else:
|
|
color = "\033[1;31;40m" # red
|
|
|
|
text = f"{color} \u23f1 {mins}m {secs:02d}s since last block\033[0;37;40m"
|
|
pad = max(0, (term_width - 35) // 2)
|
|
return ' ' * pad + text
|
|
|
|
|
|
def render_epoch_bar(block_height, epoch_block, blocks_to_halving,
|
|
next_halving_block, term_width):
|
|
"""Render difficulty epoch progress bar + halving info."""
|
|
# Difficulty adjustment progress
|
|
epoch_pct = (epoch_block / 2016) * 100
|
|
bar_width = min(30, term_width - 40)
|
|
filled = int(bar_width * epoch_block / 2016)
|
|
empty = bar_width - filled
|
|
blocks_left = 2016 - epoch_block
|
|
|
|
bar = f"\033[1;36;40m\u2593" * filled + f"\033[0;37;40m\u2591" * empty
|
|
epoch_line = (
|
|
f" \033[1;36;40mEpoch\033[0;37;40m [{bar}\033[0;37;40m] "
|
|
f"{epoch_block}/2016 ({epoch_pct:.1f}%) "
|
|
f"· {blocks_left} blocks to retarget"
|
|
)
|
|
|
|
# Halving progress
|
|
if next_halving_block > 0:
|
|
halving_num = next_halving_block // 210_000
|
|
halving_line = (
|
|
f" \033[1;35;40mHalving #{halving_num}\033[0;37;40m "
|
|
f"in {blocks_to_halving:,} blocks "
|
|
f"(block {next_halving_block:,})"
|
|
)
|
|
return epoch_line + "\n" + halving_line
|
|
|
|
return epoch_line
|
|
|
|
|
|
def render_fees(fastest, half_hour, hour, term_width):
|
|
"""Render compact fee rate display."""
|
|
if fastest == 0 and half_hour == 0 and hour == 0:
|
|
return ""
|
|
|
|
text = (
|
|
f" \033[1;31;40m\u26a1 {fastest}\033[0;37;40m | "
|
|
f"\033[1;33;40m\u23f3 {half_hour}\033[0;37;40m | "
|
|
f"\033[1;32;40m\u2623 {hour}\033[0;37;40m sat/vB"
|
|
)
|
|
pad = max(0, (term_width - 40) // 2)
|
|
return ' ' * pad + text
|
|
|
|
|
|
def render_utc_time(colors):
|
|
"""Render current UTC time in tiny cfonts font."""
|
|
now = datetime.now(timezone.utc).strftime("%H:%M")
|
|
output = render(now, colors=colors, align='center', font='tiny')
|
|
return output
|
|
|
|
|
|
def render_miner_pool(pool_name, term_width):
|
|
"""Render the mining pool that found the last block."""
|
|
if not pool_name:
|
|
return ""
|
|
text = f" \033[1;33;40m\u26cf\033[0;37;40m Mined by: \033[1;36;40m{pool_name}\033[0;37;40m"
|
|
pad = max(0, (term_width - len(pool_name) - 18) // 2)
|
|
return ' ' * pad + text
|
|
|
|
|
|
def render_block_weight(weight, max_weight, term_width):
|
|
"""Render block weight as a fullness meter."""
|
|
if weight <= 0:
|
|
return ""
|
|
pct = min(100.0, (weight / max_weight) * 100)
|
|
bar_width = min(20, term_width - 40)
|
|
filled = int(bar_width * pct / 100)
|
|
empty = bar_width - filled
|
|
|
|
if pct > 90:
|
|
color = "\033[1;31;40m" # red = nearly full
|
|
elif pct > 70:
|
|
color = "\033[1;33;40m" # yellow
|
|
else:
|
|
color = "\033[1;32;40m" # green
|
|
|
|
bar = f"{color}\u2588" * filled + f"\033[0;37;40m\u2591" * empty
|
|
text = f" \033[0;37;40mBlock weight [{bar}\033[0;37;40m] {pct:.0f}%"
|
|
pad = max(0, (term_width - bar_width - 22) // 2)
|
|
return ' ' * pad + text
|
|
|
|
|
|
def render_peer_count(peers, term_width):
|
|
"""Render connected peer count."""
|
|
if peers <= 0:
|
|
return ""
|
|
if peers >= 8:
|
|
color = "\033[1;32;40m" # green = healthy
|
|
elif peers >= 4:
|
|
color = "\033[1;33;40m" # yellow
|
|
else:
|
|
color = "\033[1;31;40m" # red = low
|
|
|
|
text = f" \033[0;37;40m\u2637 Peers: {color}{peers}\033[0;37;40m"
|
|
pad = max(0, (term_width - 16) // 2)
|
|
return ' ' * pad + text
|
|
|
|
|
|
def render_block_time_histogram(intervals, term_width):
|
|
"""Render mini histogram of recent block times.
|
|
|
|
Each bar represents one block interval. Height = time taken.
|
|
"""
|
|
if not intervals:
|
|
return ""
|
|
|
|
mn = min(intervals)
|
|
mx = max(intervals)
|
|
rng = mx - mn if mx != mn else 1
|
|
|
|
bars = ""
|
|
for iv in intervals:
|
|
idx = int((iv - mn) / rng * (len(SPARK_BLOCKS) - 1))
|
|
# Color: green for fast (<600s), yellow for normal, red for slow (>900s)
|
|
if iv < 300:
|
|
color = "\033[1;36;40m" # cyan = very fast
|
|
elif iv < 600:
|
|
color = "\033[1;32;40m" # green
|
|
elif iv < 900:
|
|
color = "\033[1;33;40m" # yellow
|
|
else:
|
|
color = "\033[1;31;40m" # red = slow
|
|
bars += f"{color}{SPARK_BLOCKS[idx]}"
|
|
|
|
avg_secs = sum(intervals) / len(intervals)
|
|
avg_min = avg_secs / 60
|
|
|
|
text = f" \033[0;37;40mBlock times {bars}\033[0;37;40m avg {avg_min:.1f}m"
|
|
pad = max(0, (term_width - len(intervals) - 26) // 2)
|
|
return ' ' * pad + text
|
|
|
|
|
|
def render_streak(streak_type, streak_count, term_width):
|
|
"""Render consecutive fast/slow block streak."""
|
|
if not streak_type or streak_count < 2:
|
|
return ""
|
|
|
|
if streak_type == "fast":
|
|
color = "\033[1;32;40m"
|
|
icon = "\u26a1"
|
|
label = "Fast streak"
|
|
else:
|
|
color = "\033[1;31;40m"
|
|
icon = "\u231b"
|
|
label = "Slow streak"
|
|
|
|
text = f" {color}{icon} {label}: {streak_count} blocks\033[0;37;40m"
|
|
pad = max(0, (term_width - 28) // 2)
|
|
return ' ' * pad + text
|
|
|
|
|
|
def render_moon_phase(term_width):
|
|
"""Render current lunar phase as ASCII art."""
|
|
# Calculate moon phase (0=new, 0.5=full)
|
|
now = datetime.now(timezone.utc)
|
|
# Known new moon: Jan 6, 2000 18:14 UTC
|
|
ref = datetime(2000, 1, 6, 18, 14, tzinfo=timezone.utc)
|
|
days = (now - ref).total_seconds() / 86400
|
|
lunation = 29.53058770576
|
|
phase = (days % lunation) / lunation # 0.0 to 1.0
|
|
|
|
# Moon ASCII art (8 phases)
|
|
moons = [
|
|
# New moon
|
|
[" _.--. ", "| |", "| |", " `--'\u00b4 "],
|
|
# Waxing crescent
|
|
[" _.--. ", "| )|", "| )|", " `--'\u00b4 "],
|
|
# First quarter
|
|
[" _.--. ", "| )|", "| )|", " `--'\u00b4 "],
|
|
# Waxing gibbous
|
|
[" _.--. ", "|( )|", "|( )|", " `--'\u00b4 "],
|
|
# Full moon
|
|
[" _.--. ", "|(())|", "|(())|", " `--'\u00b4 "],
|
|
# Waning gibbous
|
|
[" _.--. ", "|( )|", "|( )|", " `--'\u00b4 "],
|
|
# Last quarter
|
|
[" _.--. ", "|( |", "|( |", " `--'\u00b4 "],
|
|
# Waning crescent
|
|
[" _.--. ", "|( |", "|( |", " `--'\u00b4 "],
|
|
]
|
|
|
|
# Simple emoji-based moon (more reliable across terminals)
|
|
moon_chars = [
|
|
"\U0001f311", # new
|
|
"\U0001f312", # waxing crescent
|
|
"\U0001f313", # first quarter
|
|
"\U0001f314", # waxing gibbous
|
|
"\U0001f315", # full
|
|
"\U0001f316", # waning gibbous
|
|
"\U0001f317", # last quarter
|
|
"\U0001f318", # waning crescent
|
|
]
|
|
|
|
phase_names = [
|
|
"New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous",
|
|
"Full Moon", "Waning Gibbous", "Last Quarter", "Waning Crescent",
|
|
]
|
|
|
|
idx = int(phase * 8) % 8
|
|
moon = moon_chars[idx]
|
|
name = phase_names[idx]
|
|
|
|
text = f" \033[0;37;40m{moon} \033[1;37;40m{name}\033[0;37;40m"
|
|
pad = max(0, (term_width - len(name) - 8) // 2)
|
|
return ' ' * pad + text
|