Coverage for src/countdown/__main__.py: 100%

62 statements  

« prev     ^ index     » next       coverage.py v7.11.1, created at 2026-03-27 20:23 -0700

1"""Command-line interface.""" 

2 

3from time import sleep, time 

4 

5import click 

6 

7from . import timer 

8from .display import ( 

9 DISABLE_ALT_BUFFER, 

10 ENABLE_ALT_BUFFER, 

11 HIDE_CURSOR, 

12 SHOW_CURSOR, 

13 enable_ansi_escape_codes, 

14 get_chars_for_terminal, 

15 print_full_screen, 

16) 

17from .keys import get_time_adjustment, is_pause_key, is_time_adjust_key 

18from .terminal import ( 

19 check_for_keypress, 

20 drain_keypresses, 

21 read_key, 

22 restore_terminal, 

23 setup_terminal, 

24) 

25 

26 

27def get_number_lines(seconds): 

28 """Return list of lines which make large MM:SS glyphs for given seconds.""" 

29 return timer.get_number_lines(seconds, get_chars_for_terminal(seconds)) 

30 

31 

32def run_countdown(total_seconds): 

33 """Run the countdown timer for the specified duration. 

34 

35 Args: 

36 total_seconds: Duration in seconds to count down from 

37 """ 

38 enable_ansi_escape_codes() 

39 old_settings = setup_terminal() 

40 print(ENABLE_ALT_BUFFER + HIDE_CURSOR, end="") 

41 try: 

42 paused = False 

43 n = total_seconds 

44 sleep_until = time() + total_seconds 

45 pause_start = 0 

46 while n >= 0 or paused: 

47 lines = get_number_lines(n) 

48 print_full_screen(lines, paused=paused) 

49 

50 # Check for keypress to toggle pause or adjust time 

51 if check_for_keypress(): 

52 key = read_key() # Consume the keypress 

53 

54 if key == "q": 

55 # Quit the timer 

56 break 

57 elif is_pause_key(key): 

58 if paused: 

59 sleep_until += time() - pause_start 

60 pause_start = 0 

61 else: 

62 pause_start = time() 

63 paused = not paused 

64 drain_keypresses() # Ignore any additional rapid keypresses 

65 lines = get_number_lines(n) 

66 print_full_screen(lines, paused=paused) 

67 elif is_time_adjust_key(key): 

68 # Adjust the timer by +/- 30 seconds 

69 adjustment = get_time_adjustment(key) 

70 sleep_until += adjustment 

71 n = max(0, n + adjustment) # Don't go below 0 

72 drain_keypresses() # Ignore any additional rapid keypresses 

73 lines = get_number_lines(n) 

74 print_full_screen(lines, paused=paused) 

75 

76 # Only sleep and decrement if not paused 

77 if not paused: 

78 display_this_second_until = sleep_until - n + 1 

79 while time() < display_this_second_until: 

80 # Sleep in small chunks to check for keypresses more frequently 

81 sleep(0.05) 

82 if check_for_keypress(): 

83 break # Exit sleep early if key is pressed 

84 n -= 1 

85 else: 

86 # Short sleep when paused for responsive keypress checking 

87 sleep(0.05) 

88 except KeyboardInterrupt: 

89 pass 

90 finally: 

91 restore_terminal(old_settings) 

92 print(SHOW_CURSOR + DISABLE_ALT_BUFFER, end="") 

93 

94 

95@click.command() 

96@click.version_option(package_name="countdown-cli") 

97@click.argument("duration", type=timer.duration, required=False) 

98@click.pass_context 

99def main(ctx, duration): 

100 """Countdown from the given duration to 0. 

101 

102 DURATION should be a number followed by m or s for minutes or seconds. 

103 

104 Examples of DURATION: 

105 

106 \b 

107 - 5m (5 minutes) 

108 - 45s (45 seconds) 

109 - 2m30s (2 minutes and 30 seconds) 

110 

111 Press Space, p, k, or Enter to pause/resume the countdown. 

112 

113 Press +/= to add 30 seconds, - to subtract 30 seconds. 

114 

115 Press q to quit. 

116 """ # noqa: D301 

117 # Show help if no duration provided 

118 if duration is None: 

119 click.echo(ctx.get_help()) 

120 return 

121 

122 run_countdown(duration) 

123 

124 

125if __name__ == "__main__": 

126 main(prog_name="countdown") # pragma: no cover