Coverage for src/countdown/display.py: 100%
43 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-07 20:45 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-07 20:45 -0800
1"""Visual rendering and ANSI terminal control."""
3import shutil
4import sys
6from .digits import CHARS_BY_SIZE, DIGIT_SIZES
8# ANSI escape codes for terminal control
9ENABLE_ALT_BUFFER = "\033[?1049h"
10DISABLE_ALT_BUFFER = "\033[?1049l"
11HIDE_CURSOR = "\033[?25l"
12SHOW_CURSOR = "\033[?25h"
13CLEAR = "\033[H\033[J"
15# ANSI color codes
16INTENSE_MAGENTA = "\x1b[95m"
17RESET = "\033[0m"
20def enable_ansi_escape_codes(): # pragma: no cover
21 """If running on Windows, enable ANSI escape codes."""
22 if sys.platform == "win32":
23 from ctypes import windll
25 k = windll.kernel32
26 stdout = -11
27 enable_processed_output = 0x0001
28 enable_wrap_at_eol_output = 0x0002
29 enable_virtual_terminal_processing = 0x0004
30 k.SetConsoleMode(
31 k.GetStdHandle(stdout),
32 enable_processed_output
33 | enable_wrap_at_eol_output
34 | enable_virtual_terminal_processing,
35 )
38def get_required_width(chars):
39 """Calculate the minimum width required to display MM:SS format."""
40 # MM:SS format has 4 digits, 1 colon, and 1 space after each char
41 digit_width = max(len(line) for line in chars["0"].splitlines())
42 colon_width = max(len(line) for line in chars[":"].splitlines())
43 # Total: 4 digits + 1 colon + 5 spaces (after each character)
44 return digit_width * 4 + colon_width + 5
47def get_chars_for_terminal():
48 """Return the largest CHARS dictionary that fits in the current terminal."""
49 width, height = shutil.get_terminal_size()
50 for size in DIGIT_SIZES:
51 chars = CHARS_BY_SIZE[size]
52 required_width = get_required_width(chars)
53 # For size 3 (smallest multi-line), allow it without padding
54 # For larger sizes, require 1 line of padding on top and bottom (2 total)
55 padding_needed = 0 if size == 3 else 2
56 if size + padding_needed <= height and required_width <= width:
57 return chars
58 # If terminal is too small, return the smallest available
59 return CHARS_BY_SIZE[min(DIGIT_SIZES)]
62def print_full_screen(lines, paused=False):
63 """Print the given lines centered in the middle of the terminal window."""
64 term_width, term_height = shutil.get_terminal_size()
66 # Calculate total content height
67 content_height = len(lines)
68 show_pause_text = False
69 if paused and content_height + 2 <= term_height:
70 # Only show PAUSED text if there's room
71 content_height += 2 # Blank line + PAUSED text
72 show_pause_text = True
74 # Calculate vertical padding (ensure it doesn't go negative)
75 vertical_padding = max(0, (term_height - content_height) // 2)
77 # Calculate horizontal padding for timer
78 max_line_width = max(len(line) for line in lines)
79 horizontal_padding = max(0, (term_width - max_line_width) // 2)
81 # Apply red color to timer if paused
82 if paused:
83 colored_lines = [INTENSE_MAGENTA + line + RESET for line in lines]
84 else:
85 colored_lines = lines
87 # Build the output
88 vertical_pad = "\n" * vertical_padding
89 padded_text = "\n".join(
90 " " * horizontal_padding + line for line in colored_lines
91 )
93 if show_pause_text:
94 pause_text = "PAUSED - Press any key to resume"
95 pause_padding = " " * max(0, (term_width - len(pause_text)) // 2)
96 padded_text += "\n\n" + pause_padding + pause_text
98 print(CLEAR + vertical_pad + padded_text, flush=True, end="")