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

1"""Visual rendering and ANSI terminal control.""" 

2 

3import sys 

4from shutil import get_terminal_size 

5 

6from .digits import CHARS_BY_SIZE, DIGIT_SIZES 

7 

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" 

14 

15# ANSI color codes 

16INTENSE_MAGENTA = "\x1b[95m" 

17RESET = "\033[0m" 

18 

19 

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 

24 

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 ) 

36 

37 

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}" 

43 

44 

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) 

53 

54 

55def get_chars_for_terminal(seconds=0): 

56 """Return the largest CHARS dictionary that fits in the current terminal. 

57 

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)] 

73 

74 

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() 

78 

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 

86 

87 # Calculate vertical padding (ensure it doesn't go negative) 

88 vertical_padding = max(0, (term_height - content_height) // 2) 

89 

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) 

93 

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 

99 

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 ) 

105 

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 

110 

111 print(CLEAR + vertical_pad + padded_text, flush=True, end="")