Coverage for src/countdown/display.py: 100%
47 statements
« prev ^ index » next coverage.py v7.11.1, created at 2026-03-27 20:23 -0700
« prev ^ index » next coverage.py v7.11.1, created at 2026-03-27 20:23 -0700
1"""Visual rendering and ANSI terminal control."""
3import sys
4from shutil import get_terminal_size
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 _format_time_string(seconds):
39 """Return the MM:SS string used for display based on seconds."""
40 seconds = max(0, int(seconds))
41 minutes, seconds = divmod(seconds, 60)
42 return f"{minutes:02d}:{seconds:02d}"
45def get_required_width(chars, time_string):
46 """Calculate the minimum width required to display the given time string."""
47 char_widths = {
48 char: max(len(line) for line in glyph.splitlines())
49 for char, glyph in chars.items()
50 }
51 # Each character in the timer output has a trailing space appended
52 return sum(char_widths[char] + 1 for char in time_string)
55def get_chars_for_terminal(seconds=0):
56 """Return the largest CHARS dictionary that fits in the current terminal.
58 Args:
59 seconds: Current countdown value, used to account for wide minute values.
60 """
61 width, height = get_terminal_size()
62 time_string = _format_time_string(seconds)
63 for size in DIGIT_SIZES:
64 chars = CHARS_BY_SIZE[size]
65 required_width = get_required_width(chars, time_string)
66 # For size 3 (smallest multi-line), allow it without padding
67 # For larger sizes, require 1 line of padding on top and bottom (2 total)
68 padding_needed = 0 if size == 3 else 2
69 if size + padding_needed <= height and required_width <= width:
70 return chars
71 # If terminal is too small, return the smallest available
72 return CHARS_BY_SIZE[min(DIGIT_SIZES)]
75def print_full_screen(lines, paused=False):
76 """Print the given lines centered in the middle of the terminal window."""
77 term_width, term_height = get_terminal_size()
79 # Calculate total content height
80 content_height = len(lines)
81 show_pause_text = False
82 if paused and content_height + 2 <= term_height:
83 # Only show PAUSED text if there's room
84 content_height += 2 # Blank line + PAUSED text
85 show_pause_text = True
87 # Calculate vertical padding (ensure it doesn't go negative)
88 vertical_padding = max(0, (term_height - content_height) // 2)
90 # Calculate horizontal padding for timer
91 max_line_width = max(len(line) for line in lines)
92 horizontal_padding = max(0, (term_width - max_line_width) // 2)
94 # Apply red color to timer if paused
95 if paused:
96 colored_lines = [INTENSE_MAGENTA + line + RESET for line in lines]
97 else:
98 colored_lines = lines
100 # Build the output
101 vertical_pad = "\n" * vertical_padding
102 padded_text = "\n".join(
103 " " * horizontal_padding + line for line in colored_lines
104 )
106 if show_pause_text:
107 pause_text = "PAUSED - Press any key to resume"
108 pause_padding = " " * max(0, (term_width - len(pause_text)) // 2)
109 padded_text += "\n\n" + pause_padding + pause_text
111 print(CLEAR + vertical_pad + padded_text, flush=True, end="")