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
« prev ^ index » next coverage.py v7.11.1, created at 2026-03-27 20:23 -0700
1"""Command-line interface."""
3from time import sleep, time
5import click
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)
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))
32def run_countdown(total_seconds):
33 """Run the countdown timer for the specified duration.
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)
50 # Check for keypress to toggle pause or adjust time
51 if check_for_keypress():
52 key = read_key() # Consume the keypress
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)
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="")
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.
102 DURATION should be a number followed by m or s for minutes or seconds.
104 Examples of DURATION:
106 \b
107 - 5m (5 minutes)
108 - 45s (45 seconds)
109 - 2m30s (2 minutes and 30 seconds)
111 Press Space, p, k, or Enter to pause/resume the countdown.
113 Press +/= to add 30 seconds, - to subtract 30 seconds.
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
122 run_countdown(duration)
125if __name__ == "__main__":
126 main(prog_name="countdown") # pragma: no cover